PEP 711 – PyBI:一种用于分发 Python 二进制文件的标准格式
- 作者:
- Nathaniel J. Smith <njs at pobox.com>
- PEP 代理人:
- 待办事项
- 讨论至:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2023年4月6日
- 发布历史:
- 2023年4月6日
摘要
“像 wheels 一样,但它不是一个预构建的 python 包,而是一个预构建的 python 解释器”
动机
最终目标:Pypi.org 上有所有流行平台上所有 Python 版本的预构建包,因此自动化工具可以轻松获取任何一个并进行设置。这样可以快速轻松地尝试 Python 预发布版、在 CI 中固定 Python 版本、创建临时环境以重现仅在特定 Python 点发布版中发生的错误报告等。
第一步(本 PEP):定义一个标准的打包文件格式来存储预构建的 Python 解释器,并尽可能重用现有的 Python 打包标准。
示例
示例 pybi 构建可在 pybi.vorpus.org 获得。它们是 zip 文件,因此您可以解压并查看内部,以便了解它们的布局。
您还可以查看我用于创建它们的工具。
规范
文件名
文件名:{distribution}-{version}[-{build tag}]-{platform tag}.pybi
这与 PEP 427 中定义的 wheel 文件格式匹配,只是删除了 {python tag} 和 {abi tag},并将扩展名从 .whl 更改为 .pybi。
例如
cpython-3.9.3-manylinux_2014.pybicpython-3.10b2-win_amd64.pybi
就像 wheels 一样,如果 pybi 支持多个平台,您可以使用点分隔它们,以创建“压缩标签集”
cpython-3.9.5-macosx_11_0_x86_64.macosx_11_0_arm64.pybi
(尽管在实践中这可能不会被广泛使用,例如上述文件名更惯用地写作 cpython-3.9.5-macosx_11_0_universal2.pybi。)
文件内容
一个 .pybi 文件是一个 zip 文件,可以直接解压到任意位置,然后作为一个独立的 Python 环境使用。没有 .data 目录或安装方案键,因为 Python 环境知道它正在使用哪个安装方案,因此它可以直接将文件放置到正确的位置。
“任意位置”部分很重要:pybi 不能包含任何硬编码的绝对路径。特别是,任何预安装的脚本都 **不得** 在其 shebang 行中嵌入绝对路径。
类似于 wheels 的 <package>-<version>.dist-info 目录,pybi 档案必须包含一个名为 pybi-info/ 的顶级目录。(理由:将其命名为 pybi-info 而不是 dist-info 可确保工具不会混淆它们正在查看哪种元数据;省略 {name}-{version} 部分没问题,因为一个给定目录中只能安装一个 pybi。)pybi-info/ 目录至少包含以下文件:
.../PYBI:关于档案本身的元数据,采用与METADATA和WHEEL文件相同的 RFC822 格式。Pybi-Version: 1.0 Generator: {name} {version} Tag: {platform tag} Tag: {another platform tag} Tag: {...and so on...} Build: 1 # optional
.../RECORD:与 wheels 中的相同,但请参见下面的符号链接说明。.../METADATA:与当前核心元数据规范中描述的格式相同,但以下键被禁止,因为它们没有意义:Requires-DistProvides-ExtraRequires-Python
此外,还有一些下面描述的新的必需键。
Pybi 特有的核心元数据
在我们给出完整详细信息之前,这里有一个新 METADATA 字段的示例
Pybi-Environment-Marker-Variables: {"implementation_name": "cpython", "implementation_version": "3.10.8", "os_name": "posix", "platform_machine": "x86_64", "platform_system": "Linux", "python_full_version": "3.10.8", "platform_python_implementation": "CPython", "python_version": "3.10", "sys_platform": "linux"}
Pybi-Paths: {"stdlib": "lib/python3.10", "platstdlib": "lib/python3.10", "purelib": "lib/python3.10/site-packages", "platlib": "lib/python3.10/site-packages", "include": "include/python3.10", "platinclude": "include/python3.10", "scripts": "bin", "data": "."}
Pybi-Wheel-Tag: cp310-cp310-PLATFORM
Pybi-Wheel-Tag: cp310-abi3-PLATFORM
Pybi-Wheel-Tag: cp310-none-PLATFORM
Pybi-Wheel-Tag: cp39-abi3-PLATFORM
Pybi-Wheel-Tag: cp38-abi3-PLATFORM
Pybi-Wheel-Tag: cp37-abi3-PLATFORM
Pybi-Wheel-Tag: cp36-abi3-PLATFORM
Pybi-Wheel-Tag: cp35-abi3-PLATFORM
Pybi-Wheel-Tag: cp34-abi3-PLATFORM
Pybi-Wheel-Tag: cp33-abi3-PLATFORM
Pybi-Wheel-Tag: cp32-abi3-PLATFORM
Pybi-Wheel-Tag: py310-none-PLATFORM
Pybi-Wheel-Tag: py3-none-PLATFORM
Pybi-Wheel-Tag: py39-none-PLATFORM
Pybi-Wheel-Tag: py38-none-PLATFORM
Pybi-Wheel-Tag: py37-none-PLATFORM
Pybi-Wheel-Tag: py36-none-PLATFORM
Pybi-Wheel-Tag: py35-none-PLATFORM
Pybi-Wheel-Tag: py34-none-PLATFORM
Pybi-Wheel-Tag: py33-none-PLATFORM
Pybi-Wheel-Tag: py32-none-PLATFORM
Pybi-Wheel-Tag: py31-none-PLATFORM
Pybi-Wheel-Tag: py30-none-PLATFORM
Pybi-Wheel-Tag: py310-none-any
Pybi-Wheel-Tag: py3-none-any
Pybi-Wheel-Tag: py39-none-any
Pybi-Wheel-Tag: py38-none-any
Pybi-Wheel-Tag: py37-none-any
Pybi-Wheel-Tag: py36-none-any
Pybi-Wheel-Tag: py35-none-any
Pybi-Wheel-Tag: py34-none-any
Pybi-Wheel-Tag: py33-none-any
Pybi-Wheel-Tag: py32-none-any
Pybi-Wheel-Tag: py31-none-any
Pybi-Wheel-Tag: py30-none-any
规范
Pybi-Environment-Marker-Variables:作为 JSON 字典,包含此 Pybi 在所有安装中保持静态的所有 PEP 508 环境标记变量的值。例如:python_version将始终存在,因为 Python 3.10 包始终具有python_version == "3.10"。platform_version通常不会出现,因为它提供了有关 Python 运行所在操作系统的详细信息,例如#60-Ubuntu SMP Thu May 6 07:46:32 UTC 2021platform_release也有类似的问题。platform_machine通常会存在,但 macOS universal2 pybi 除外:它们可能在 x86-64 或 arm64 模式下运行,我们直到实际调用解释器才知道是哪种模式,因此我们无法将其记录在静态元数据中。
原理:在许多情况下,只要解析器能够访问目标平台的 .pybi 文件,它就应该能够让在 Linux 上运行的解析器计算 Windows 上的 Python 环境的包依赖项,反之亦然。(请注意,可以通过使用
python_full_version值来检查Requires-Python约束。)虽然我们有时不得不省略一些键,但它们要么相当无用(platform_version、platform_release),要么可以由解析器重建(platform_machine)。标记通常也是非常有用的信息。例如,如果您有一个
pypy3-7.3.2pybi,并且您想知道它支持哪个版本的 Python 语言,那么该信息会记录在python_version标记中。(注意:我们可能需要弃用/删除
platform_version和platform_release?它们有问题,我找不到任何它们有用的情况。但这超出了本 PEP 的范围。)Pybi-Paths:安装 wheels 所需的安装路径(与sysconfig.get_paths()相同的键),作为从 zip 文件根目录开始的相对路径,以 JSON 字典形式。这些路径必须以 Unix 格式编写,使用正斜杠作为分隔符,而不是反斜杠。
必须可以通过运行
{paths["scripts"]}/python来调用 Python 解释器。如果存在其他解释器入口点(例如用于 Windows GUI 应用程序的pythonw),那么它们也应该在该目录中,使用其传统名称,不带版本号。(如果需要,您也可以有一个python3.11符号链接;对此没有限制。只是python必须存在并能工作。)原理:
Pybi-Paths和Pybi-Wheel-Tags(见下文)共同足以让安装程序选择 wheels 并将其安装到解压后的 pybi 环境中,而无需调用 Python。此外,我们需要在某个地方记录解释器位置,这样一石二鸟。Pybi-Wheel-Tag:此解释器支持的 wheel 标签,按优先顺序排列(最优先的在前,最不优先的在后),但特殊平台标签PLATFORM应替换任何依赖于最终安装系统的平台标签。讨论:如果安装程序能够预先计算 pybi 对应的 wheel 标签,以便它们可以将 wheels 安装到未打包的 pybi 中,而无需实际调用 python 解释器来查询其标签,那将是极好的™——既提高了效率,又允许更奇特的用例,例如从 Linux 主机设置 Windows 环境。
但遗憾的是,无法提前计算 Python 安装支持的完整平台标签集,因为它们可能取决于最终系统
- 标记为
manylinux_2_12_x86_64的 pybi 始终可以使用标记为manylinux_2_12_x86_64的 wheels。它也可能能够使用标记为manylinux_2_17_x86_64的 wheels,但前提是最终安装系统具有 glibc 2.17+。 - 标记为
macosx_11_0_universal2(= x86-64 + arm64 支持在同一二进制文件中) 的 pybi 可能能够使用标记为macosx_11_0_arm64的 wheels,但前提是它安装在“Apple Silicon”机器上并以 arm64 模式运行。
在这两种情况下,安装工具仍然可以通过计算本地平台标签,从
Pybi-Wheel-Tag获取 wheel 标签模板,并用实际支持的平台替换神奇的PLATFORM字符串来确定合适的 wheel 标签集。但是,还有其他更复杂的情况
- 您(通常)可以在 64 位 Windows 上运行 32 位和 64 位应用程序。因此,pybi
- 安装程序可能会计算当前平台上允许的 pybi 标签集为 [
win32,win_amd64]。但您不能直接将该集合替换到 pybi 的 wheel 标签模板中,否则会得到乱码。[ "cp39-cp39-win32", "cp39-cp39-win_amd64", "cp39-abi3-win32", "cp39-abi3-win_amd64", ... ]
为了处理这个问题,安装程序需要以某种方式理解
manylinux_2_12_x86_64pybi 可以使用manylinux_2_17_x86_64wheel,只要这些都是当前机器上有效的标签,但是win32pybi **不能** 使用win_amd64wheel,即使这些都是当前机器上有效的标签。
- 标记为
macosx_11_0_universal2的 pybi 可能能够使用标记为macosx_11_0_x86_64的 wheels,但前提是它安装在 x86-64 机器上 **或者** 它安装在 ARM 机器上 **并且** 解释器被调用时带有指示 macOS 以 x86-64 模式运行二进制文件的神奇咒语。因此,安装程序计划如何调用 pybi 也很重要!
因此,实际使用
Pybi-Wheel-Tag值比看起来要复杂,并且它们可能只对相当复杂的工具链有用。但是,智能 pybi 安装程序已经必须理解许多这些平台兼容性问题才能选择一个可用的 pybi,并且对于跨平台固定/环境构建的情况,用户可以潜在地提供任何所需的信息来消除他们所针对的平台的歧义。因此,将其包含在 PyBI 元数据中仍然足够有用——那些认为它没用的工具可以简单地忽略它。- 标记为
您可能可以通过在构建的解释器上运行此脚本来生成这些元数据值
import packaging.markers
import packaging.tags
import sysconfig
import os.path
import json
import sys
marker_vars = packaging.markers.default_environment()
# Delete any keys that depend on the final installation
del marker_vars["platform_release"]
del marker_vars["platform_version"]
# Darwin binaries are often multi-arch, so play it safe and
# delete the architecture marker. (Better would be to only
# do this if the pybi actually is multi-arch.)
if marker_vars["sys_platform"] == "darwin":
del marker_vars["platform_machine"]
# Copied and tweaked version of packaging.tags.sys_tags
tags = []
interp_name = packaging.tags.interpreter_name()
if interp_name == "cp":
tags += list(packaging.tags.cpython_tags(platforms=["xyzzy"]))
else:
tags += list(packaging.tags.generic_tags(platforms=["xyzzy"]))
tags += list(packaging.tags.compatible_tags(platforms=["xyzzy"]))
# Gross hack: packaging.tags normalizes platforms by lowercasing them,
# so we generate the tags with a unique string and then replace it
# with our special uppercase placeholder.
str_tags = [str(t).replace("xyzzy", "PLATFORM") for t in tags]
(base_path,) = sysconfig.get_config_vars("installed_base")
# For some reason, macOS framework builds report their
# installed_base as a directory deep inside the framework.
while "Python.framework" in base_path:
base_path = os.path.dirname(base_path)
paths = {key: os.path.relpath(path, base_path).replace("\\", "/") for (key, path) in sysconfig.get_paths().items()}
json.dump({"marker_vars": marker_vars, "tags": str_tags, "paths": paths}, sys.stdout)
这会在 stdout 上输出一个 JSON 字典,其中包含每个 pybi 特定标签集的单独条目。
符号链接
目前,所有 Unix Python 安装默认都使用符号链接(例如,bin/python3 -> bin/python3.9)。此外,在 .pybi 文件中存储 macOS 框架构建 **需要** 符号链接。因此,与 wheel 文件不同,我们绝对必须支持 .pybi 文件中的符号链接,它们才能发挥作用。
在 zip 文件中表示符号链接
在 zip 文件中表示符号链接的事实标准是 Info-Zip 符号链接扩展,其工作方式如下:
- 符号链接的目标路径被存储,就像它是文件内容一样
- Unix 权限字段的最高 4 位设置为
0xa,即:permissions & 0xf000 == 0xa000 - Unix 权限字段反过来又存储在“外部属性”字段的最高 16 位中。
因此,如果使用 Python 的 zipfile 模块,您可以通过以下方式检查 ZipInfo 是否表示符号链接:
(zip_info.external_attr >> 16) & 0xf000 == 0xa000
或者,如果使用 Rust 的 zip crate,等效的检查是
fn is_symlink(zip_file: &zip::ZipFile) -> bool {
match zip_file.unix_mode() {
Some(mode) => mode & 0xf000 == 0xa000,
None => false,
}
}
如果您在 Unix 上,您的 zip 和 unzip 命令可能已经理解这种格式了。
在 RECORD 文件中表示符号链接
通常,RECORD 文件会列出每个文件 + 其哈希值 + 其长度
my/favorite/file,sha256=...,12345
对于符号链接,我们改为写入
name/of/symlink,symlink=path/to/symlink/target,
也就是说:我们使用一个特殊的“哈希函数”叫做 symlink,然后将实际的符号链接目标作为“哈希值”存储。长度留空。
原理:我们已经承诺 RECORD 文件包含对主存档中所有内容的冗余检查,因此对于符号链接,我们至少需要存储某种哈希,以及某种标志来指示这是一个符号链接。鉴于符号链接目标字符串的大小与哈希大致相同,我们不妨直接存储它们。这也使得符号链接信息对于不理解 Info-Zip 符号链接扩展的工具更容易访问,并且可以在 Windows 系统上无损地解压和重新打包 Unix pybi,这在某些时候可能会派上用场。
在 pybi 文件中存储符号链接
当 pybi 创建者存储符号链接时,他们 **必须** 使用上述两种机制:直接使用 Info-Zip 表示法将其存储在 zip 存档中,并将其记录在 RECORD 文件中。
Pybi 消费者 **应该** 验证存档中的符号链接和 RECORD 文件彼此一致。
我们还考虑过 **只** 使用 RECORD 文件来存储符号链接,但那样的话,普通的 unzip 工具将无法解压它们,这将使从 shell 脚本安装 pybi 变得困难。
限制
符号链接可能会带来很多潜在的混乱。为了控制局面,我们施加了以下限制:
- 符号链接 **不得** 用于面向 Windows 或其他缺少一流符号链接支持的平台上的
.pybi。 - 符号链接 **不得** 在
pybi-info目录内部使用。(理由:没有必要,并且这简化了需要从pybi-info中提取信息而不解压整个存档的解析器的工作。) - 符号链接目标 **必须** 是相对路径,并且 **必须** 位于 pybi 目录内。
- 如果
A/B/...在存档中记录为符号链接,则存档中 **不得** 有任何其他名为A/B/.../C的条目。例如,如果一个存档有一个符号链接
foo -> bar,然后存档中有一个名为foo/blah.py的常规文件,那么一个天真的解压程序可能会最终写入一个名为bar/blah.py的文件。不要天真。
解包器 **必须** 验证是否遵循这些规则,因为没有它们,攻击者可能会创建恶意的符号链接,如 foo -> /etc/passwd 或 foo -> ../../../../../etc + foo/passwd -> ...,造成混乱。
非规范性注释
为什么不直接使用 conda?
这并不完全在本 PEP 的范围之内,但由于 conda 是一种流行的分发二进制 Python 解释器的方式,这是一个自然而然的问题。
简单的答案是:conda 很棒!但是,有很多 Python 用户不是 conda 用户,他们也应该拥有好东西。这个 PEP 只是给了他们另一个选择。
更深层次的答案是:将包上传到 PyPI 的维护者是 Python 生态系统的支柱。他们是 Python 打包工具的首要受众。他们想要的一件事是:上传一个包一次,然后它就可以通过所有不同的 Python 部署方式访问:在 Debian、Fedora、Homebrew 和 FreeBSD 中,在 Conda 环境中,在大公司的 monorepos 中,在 Nix 中,在 Blender 插件中,在 RenPy 游戏中……你懂的。
所有这些环境都有自己的工具和策略来管理包和依赖项。因此,PyPI 和 wheels 的特别之处在于,它们旨在以 **标准、抽象的方式** 描述依赖项,所有这些下游系统都可以使用并转换为其本地约定。这就是为什么包维护者使用 Python 特定的元数据并上传到 PyPI:因为它允许他们同时处理所有这些系统。每次为 conda 构建 Python 包时,都会生成一个中间 wheel,因为 wheels 是 Python 包构建系统和 conda 可以相互交流的通用语言。
但是,如果您是发布 sdist+wheels 的维护者,那么您自然会希望测试您发布的内容,这可能取决于任意的 PyPI 包和版本。因此,您需要直接从 PyPI 构建 Python 环境的工具,而 conda 从根本上就不是为此目的设计的。因此,conda 和 pip 在不同情况下都是必需的,而本提案恰好针对的是等式的 pip 端。
Sdists(或不使用)
为 pybi 提供一个“sdist”等效物可能很酷,即某种 Python 源代码发布格式,其结构足够让工具自动获取并将其构建为 pybi,适用于没有预构建 pybi 的平台。但是,这对于 MVP 来说并非必需,并且会引发一系列问题,所以我们稍后再考虑。
Pybi 中应捆绑哪些包?
Pybi 的构建者有权选择具体包含哪些内容。例如,您可以在 pybi 的 site-packages 目录中包含一些预安装的包,或者删除您不需要的标准库部分。我们无法阻止您!不过,如果您预安装了包,则强烈建议您也包含正确的元数据(.dist-info 等),以便 Pip 或其他工具能够理解发生了什么。
对于我的原型“通用”pybi,我选择的是
- 确保
site-packages为 **空**。原理:对于面向最终用户的传统独立 Python 安装程序,您可能至少需要包含
pip,以避免引导问题(PEP 453)。但 pybi 不同:它们旨在由“智能”工具安装,这些工具将 pybi 作为某种更大的自动化部署过程的一部分。对于这些安装程序来说,从一个空白环境开始,然后添加他们所需的任何内容,比从一些他们可能不需要的预安装包开始更容易。(此外,您仍然可以运行python -m ensurepip。) - 包含完整的标准库,**除了**
test模块。原理:顶级
test模块包含 CPython 自己的测试套件。它非常大(没有test的 CPython 大约 37 MB,然后test又增加了大约 25 MB!),而且普通用户代码几乎从不使用。此外,作为先例,官方 nuget 包、官方 manylinux 镜像以及多个 Linux 发行版都省略了它,这并未引起任何重大问题。因此,这似乎是在广泛兼容性和合理的下载/安装大小之间取得平衡的最佳方式。
- 我不分发任何
.pyc文件。它们在下载中占用空间,可以在最终系统上以最小的成本生成,并且删除它们消除了位置依赖的来源。(.pyc文件存储相应.py文件的绝对路径并将其包含在回溯中;但是,pybi 是可重定位的,因此在安装后才能知道正确的路径。)
向后兼容性
不考虑向后兼容性。
安全隐患
除了任何负责分发二进制文件的人都必须制定一个管理其安全性的计划(例如,在 OpenSSL CVE 出现后是否发布新版本)之外,没有其他安全影响。但总的来说,我们 Python 核心开发人员已经为所有主要平台(通过 python.org 提供 macOS + Windows,通过官方 manylinux 镜像提供 Linux 构建)维护二进制构建,因此即使我们开始在 PyPI 上发布官方 CPython 构建,也不会真正引发任何新的安全问题。
如何教授此内容
这并非针对最终用户;他们的体验将是,例如,他们的 pyenv 或 tox 调用神奇地变得更快更可靠(如果这些项目的维护者决定利用本 PEP 的话)。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0711.rst