PEP 711 – PyBI:Python 二进制文件分发标准格式
- 作者:
- Nathaniel J. Smith <njs at pobox.com>
- PEP 委托人:
- 待定
- 讨论邮件列表:
- Discourse 帖子
- 状态:
- 草稿
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2023年4月6日
- 修订历史:
- 2023年4月6日
摘要
“类似于 wheel,但它不是预编译的 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.pybi
cpython-3.10b2-win_amd64.pybi
与 wheel 一样,如果 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 行中嵌入绝对路径。
类似于 wheel 的 <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
:与 wheel 中的相同,但请参见下面关于符号链接的说明。.../METADATA
:与当前核心元数据规范中描述的格式相同,但以下键被禁止,因为它们没有意义Requires-Dist
Provides-Extra
Requires-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
:所有在安装此 Pybi 时保持静态的 PEP 508 环境标记变量的值,以 JSON 字典形式表示。例如python_version
将始终存在,因为 Python 3.10 包始终具有python_version == "3.10"
。platform_version
通常不会存在,因为它提供了有关 Python 运行所在操作系统的详细信息,例如#60-Ubuntu SMP Thu May 6 07:46:32 UTC 2021
platform_release
存在类似问题。platform_machine
通常会存在,但 macOS universal2 pybi 除外:这些 pybi 可以在 x86-64 或 arm64 模式下运行,我们只有在实际调用解释器时才知道哪个模式,因此我们无法在静态元数据中记录它。
**理由:**在许多情况下,这应该允许在 Linux 上运行的解析器计算 Windows 上 Python 环境的包固定,反之亦然,只要解析器可以访问目标平台的 .pybi 文件。(请注意,可以使用
python_full_version
值检查Requires-Python
约束。)虽然我们有时必须省略一些键,但它们要么相当无用(platform_version
、platform_release
),要么可以由解析器重建(platform_machine
)。标记通常也是有用的信息。例如,如果您有一个
pypy3-7.3.2
pybi,并且您想知道它支持哪个版本的 Python 语言,那么该信息记录在python_version
标记中。(注意:我们可能希望弃用/删除
platform_version
和platform_release
?它们存在问题,我无法找到任何它们有用的情况。但这不在本 PEP 的范围内。)Pybi-Paths
:安装 wheel 所需的安装路径(与sysconfig.get_paths()
中的键相同),以从 zip 文件根目录开始的相对路径形式表示,以 JSON 字典形式表示。这些路径**必须**以 Unix 格式编写,使用正斜杠作为分隔符,而不是反斜杠。
必须能够通过运行
{paths["scripts"]}/python
来调用 Python 解释器。如果有其他解释器入口点(例如 Windows GUI 应用程序的pythonw
),那么它们也应该位于该目录下,使用其常规名称,并且不附加版本号。(如果需要,您也可以拥有一个python3.11
符号链接;没有规则禁止这样做。只是python
必须存在并工作。)**理由:**
Pybi-Paths
和Pybi-Wheel-Tag
(见下文)一起足以让安装程序选择 wheel 并将其安装到解压的 pybi 环境中,而无需调用 Python。此外,我们需要在某处写下解释器的位置,所以一举两得。Pybi-Wheel-Tag
:此解释器支持的 wheel 标签,按优先级排序(最优先的排在最前面,最不优先的排在最后),但特殊平台标签PLATFORM
应该替换任何依赖于最终安装系统的平台标签。**讨论:**如果安装程序能够提前计算 pybi 的对应 wheel 标签,那将非常棒™,这样它们就可以将 wheel 安装到解压的 pybi 中,而无需实际调用 Python 解释器来查询其标签——这既是为了效率,也为了允许更奇特的用例,例如从 Linux 主机设置 Windows 环境。
但不幸的是,无法提前计算 Python 安装支持的完整平台标签集,因为它们可能依赖于最终系统
- 标记为
manylinux_2_12_x86_64
的 pybi 始终可以使用标记为manylinux_2_12_x86_64
的 wheel。它也**可能**能够使用标记为manylinux_2_17_x86_64
的 wheel,但前提是最终安装系统具有 glibc 2.17+。 - 标记为
macosx_11_0_universal2
(= 在同一个二进制文件中支持 x86-64 和 arm64)的 pybi 可能能够使用标记为macosx_11_0_arm64
的 wheel,但前提是它安装在“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_64
和manylinux_2_17_x86_64
在当前机器上都是有效的标签,manylinux_2_12_x86_64
pybi 就可以使用manylinux_2_17_x86_64
wheel,但是win32
pybi**不能**使用win_amd64
wheel,即使它们在当前机器上都是有效的标签。
- 一个标记为
macosx_11_0_universal2
的 pybi 或许能够使用标记为macosx_11_0_x86_64
的 wheel,但前提是它安装在 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)
这会在标准输出上输出一个 JSON 字典,其中包含每个 pybi 特定标签集的单独条目。
符号链接
目前,符号链接默认在所有 Unix Python 安装中使用(例如,bin/python3 -> bin/python3.9
)。此外,符号链接是 *必须* 用于将 macOS 框架构建存储在 .pybi
文件中。因此,与 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 很好!但是,还有很多不是 conda 用户的 Python 用户,他们也应该享受美好的事物。本 PEP 只是给了他们另一种选择。
更深入的答案是:将软件包上传到 PyPI 的维护者是 Python 生态系统的支柱。他们是 Python 打包工具的首批受众。他们想要做的一件事就是上传一个软件包,并使其能够通过 Python 部署的所有不同方式访问:在 Debian 和 Fedora 和 Homebrew 和 FreeBSD 中,在 Conda 环境中,在大公司的单体存储库中,在 Nix 中,在 Blender 插件中,在 RenPy 游戏中……您明白了。
所有这些环境都有自己的工具和策略来管理软件包和依赖项。因此,PyPI 和 wheel 的特别之处在于,它们旨在以 *标准的、抽象的方式* 描述依赖项,所有这些下游系统都可以使用这些依赖项并将其转换为其本地约定。这就是软件包维护者使用 Python 特定的元数据并上传到 PyPI 的原因:因为它允许他们同时解决所有这些系统。每次为 conda 构建 Python 软件包时,都会生成一个中间 wheel,因为 wheel 是 Python 软件包构建系统和 conda 可以用来相互通信的通用语言。
但是,如果您是发布 sdist+wheel 的维护者,那么您自然希望测试您发布的内容,这可能依赖于任意 PyPI 软件包和版本。因此,您需要可以直接从 PyPI 构建 Python 环境的工具,而 conda 从根本上来说并不是为了这个目的而设计的。因此,conda 和 pip 在不同的情况下都是必要的,而本提案恰好针对的是 pip 方面。
源代码分发包(或不使用)
拥有 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