抽象
“就像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 中定义的轮子文件格式匹配,除了删除{python tag}和{abi tag}并将扩展名从 .whl→ 更改为 .pybi。
例如:
- cpython-3.9.3-manylinux_2014.pybi
- cpython-3.10b2-win_amd64.pybi
就像轮子一样,如果 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 lines中嵌入绝对路径。
与 wheels 的 <package>-<version>.dist-info 目录类似,pybi 归档文件必须包含一个名为 pybi-info/ 的顶级目录。(理由是:将其称为 pybi-info 而不是 dist-info 可以确保工具不会混淆他们正在查看的元数据的种类;省去 {name}-{version} 部分是好的,因为一个给定的目录中只能安装一个 pybi。)pybi-info/目录至少包含以下文件:
- .../PYBI:关于档案本身的元数据,采用与METADATA和WHEEL文件相同的RFC822-ish格式:
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:所有PEP 508环境标记变量的值,这些变量在这个Pybi的不同安装中是静态的,是一个JSON dict。因此,例如: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 通用除外2 Pybis:这些可以在 x86-64 或 ARM64 模式下运行,我们 在实际调用解释器之前不知道哪个,所以我们不能 将其记录在静态元数据中。
理由:在许多情况下,这应该允许运行在 Linux 上的解析器为 Windows 上的 Python 环境计算软件包引脚,反之亦然,只要解析器能够访问目标平台的 .pybi 文件。(注意 Requires-Python 限制可以通过使用 python_full_version 值来检查。) 虽然我们有时不得不漏掉一些键,但它们要么是相当无用的(platform_version, platform_release),要么是可以被解析器重构的(platform_machine)。
这些标记也只是一般有用的信息,可以访问。例如,如果你有一个 pypy3-7.3.2 pybi,你想知道它所支持的 Python 语言的版本,那么这将记录在 python_version 标记中。
(注意:我们可能想废除/删除platform_version和platform_release?它们是有问题的,我想不出它们在什么情况下是有用的。但这已经超出了这个特定PEP的范围)。
- Pybi-Paths: 安装轮子所需的安装路径(与sysconfig.get_paths()的键值相同),作为相对路径从zip文件的根部开始,以JSON dict的形式。 这些路径必须以 Unix 格式书写,使用正斜线作为分隔符,而不是反斜线。 必须能够通过运行 {paths["scripts"]}/python 调用 Python 解释器。如果有其他的解释器入口点 (例如用于 Windows GUI 应用程序的 pythonw),那么它们也应该在该目录中以常规的名字出现,并且不附加版本号。(如果你想的话,你也可以有一个python3.11的符号链接;没有规定禁止这样做。这只是因为 python 必须存在并且能够工作)。 理由:Pybi-Paths 和 Pybi-Wheel-Tags (见下文) 在一起,足以让安装程序选择轮子并将其安装到已解压的 pybi 环境中,而无需调用 Python。此外,我们需要在某个地方写下解释器的位置,所以这是一石二鸟。
- Pybi-Wheel-Tag:这个解释器支持的wheel tags,按优先顺序排列(最喜欢的在前,最不喜欢的在后),但特殊的平台标签PLATFORM应该取代任何取决于最终安装系统的平台标签。 讨论:如果安装人员能够提前计算出 pybi 相应的轮子标签就好了,这样他们就可以将轮子安装到解压后的 pybi 中,而不需要实际调用 python 解释器来查询其标签--这既是为了提高效率,也是为了允许更多奇特的使用情况,比如从 Linux 主机上设置 Windows 环境。 但不幸的是,我们不可能提前计算出Python安装所支持的全部平台标签,因为它们可能取决于最终的系统:一个标记为 manylinux_2_12_x86_64 的 pybi 总是可以使用标记为 manylinux_2_12_x86_64 的 wheels。它也可以使用标记为 manylinux_2_17_x86_64 的轮子,但前提是最终安装的系统有 glibc 2.17+。一个标记为macosx_11_0_universal2(=同一二进制中支持x86-64和arm64)的pybi可能能够使用标记为macosx_11_0_arm64的轮子,但只有当它安装在 "Apple Silicon "机器上并以arm64模式运行时才能使用。 在这两种情况下,安装工具仍然可以通过计算本地平台标签,从Pybi-Wheel-Tag中获取轮子标签模板,并将实际支持的平台换成神奇的PLATFORM字符串,从而计算出适当的轮子标签集。 然而,还有一些情况甚至更加复杂:你可以(通常)在64位Windows上同时运行32位和64位的应用程序。因此,一个Pybi 安装程序可能会计算出当前平台上允许的 pybi 标签集为 [win32, win_amd64]。但你不能把这组标签换到 pybi 的轮子标签模板中,否则你会得到胡言乱语:[ "cp39-cp39-win32", "cp39-cp39-win_amd64", "cp39-abi3-win32", "cp39-abi3-win_amd64", ... ] 为了处理这个问题,安装程序需要理解manylinux_2_12_x86_64 pybi可以使用manylinux_2_17_x86_64轮,只要这两个标签在当前机器上都是有效的,但win32 pybi不能使用win_amd64轮,即使这两个标签在当前机器上都是有效的。一个标记为macosx_11_0_universal2的pybi可能能够使用标记为macosx_11_0_x86_64的轮子,但是只有当它被安装在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 dict,为每一组pybi特定的标签提供单独的条目。
符号链接
目前,在所有的Unix Python安装中,默认使用符号链接(例如,bin/python3 -> bin/python3.9)。此外,在.pybi文件中存储macOS框架构建时也需要符号链接。因此,与轮子文件不同,我们必须在.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,然后将实际的symlink目标作为 "哈希值 "存储。而长度则留空。
理由:我们已经承诺在RECORD文件中对主存档中的所有内容进行多余的检查,所以对于符号链接,我们至少需要存储某种哈希值,再加上某种标志来表明这是一个符号链接。鉴于符号链接的目标字符串与哈希值的大小大致相同,我们不妨直接存储它们。这也使得符号链接信息更容易被那些不理解Info-Zip符号链接扩展的工具所访问,并且使得在Windows系统上无损地解压和重新打包Unix pybi成为可能,这在某些时候可能会被人发现很方便。
在pybi文件中储存符号链接
当 pybi 创建者存储一个符号链接时,他们必须使用上面定义的两种机制:使用 Info-Zip 表示法直接将其存储在 zip 档案中,同时将其记录在 RECORD 文件中。
Pybi消费者应该验证归档文件和RECORD文件中的符号链接是否相互一致。
我们也考虑过只用 RECORD 文件来存储符号链接,但那样的话,vanilla 解压工具就不能解压了,这将使我们很难从 shell 脚本中安装 pybi。
局限性
Symlinks使很多潜在的混乱成为可能。为了控制事情,我们施加了以下限制:
- Symlinks 不能在针对 Windows 的 .pybis 中使用,也不能在其他缺少一流 symlink 支持的平台上使用。
- 符号链接不能在 pybi-info 目录中使用。(理由是:没有必要,而且对于需要从 pybi-info 中提取信息而不需要解压整个归档文件的解析器来说,这样做会更简单。)
- Symlink 目标必须是相对路径,而且必须在 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 的用户!但是,有很多不是 conda 用户的 python 用户,他们也应该得到好东西。这个PEP只是给了他们另一个选择。
更深层次的答案是:向PyPI上传软件包的维护者是Python生态系统的中坚力量。他们是Python打包工具的第一受众。他们想做的一件事是上传一次软件包,并让它在所有不同的Python部署方式中都能被访问:在Debian、Fedora、Homebrew和FreeBSD中,在Conda环境中,在大公司的Monorepos中,在Nix中,在Blender插件中,在RenPy游戏中,.....,你明白的。
所有这些环境都有自己的工具和策略来管理软件包和依赖关系。因此,PyPI和wheel的特别之处在于,它们被设计为以一种标准的、抽象的方式来描述依赖关系,所有这些下游系统都可以消费并转换为他们的本地惯例。这就是为什么软件包维护者使用Python特定的元数据并上传到PyPI的原因:因为这让他们可以同时处理所有这些系统。每次你为conda构建一个Python包,都会有一个中间轮子产生,因为轮子是Python包构建系统和conda可以用来相互交流的通用语言。
但是,如果你是一个发布sdist+wheels的维护者,那么你自然希望测试你所发布的东西,这可能依赖于任意的PyPI包和版本。所以你需要直接从PyPI构建Python环境的工具,而conda从根本上说不是用来做这个的。所以conda和pip在不同的情况下都是必要的,而这个提议恰好是针对这个等式中的pip一方。
Sdists(或不)
为 pybis 提供一个 "sdist "等价物可能会很酷,也就是说,某种 Python 源代码的格式,其结构足以让工具自动获取并构建成 pybi,适用于没有预置 pybis 的平台。但是,这对 MVP 来说并不是必要的,而且会带来很多麻烦,所以我们以后再担心这个问题吧。
pybi 中应该捆绑哪些包?
Pybi 构建者有能力挑选里面的具体内容。例如,你可以在 pybi 的 site-packages 目录中包含一些预装包,或者剪掉你不想要的 stdlib 的部分。我们不能阻止你! 不过如果你真的预装了软件包,那么强烈建议你也包括正确的元数据(.dist-info等),这样Pip或其他工具就有可能了解正在发生什么。
对于我的原型 "通用 "Pybi's,我所选择的是:
- 确保site-packages为空。 理由:对于针对终端用户的传统独立的 python 安装程序,你可能希望至少包括 pip,以避免引导问题(PEP 453)。但 pybis 是不同的:它们被设计成由 "智能 "工具来安装,这些工具将 pybi 作为某种更大的自动部署过程的一部分来使用。对于这些安装程序来说,从一张白纸开始,然后添加任何他们需要的东西,要比从一些他们可能想要或不想要的预装包开始更容易。(此外,你仍然可以运行 python -m ensurepip)。
- 包括整个stdlib,除了test。 理由:顶层的测试模块包含了CPython自己的测试套件。它非常庞大(CPython不包含测试是~37 MB,然后测试在此基础上又增加了~25 MB!),而且基本上不会被普通用户代码使用。另外,如前所述,官方的nuget包、官方的manylinux镜像和多个Linux发行版都没有使用它,这也没有引起任何大问题。 因此,这似乎是平衡广泛的兼容性和合理的下载/安装大小的最好方法。
- 我不发送任何.pyc文件。它们会占用下载的空间,可以在最终的系统上以最小的成本生成,而且放弃它们可以消除对位置的依赖。(.pyc文件存储了相应的.py文件的绝对路径,并在回溯中包含它;但是,pybis是可重定位的,所以正确的路径在安装后才知道。)
向后兼容性
没有向后兼容的考虑。
安全隐患
没有安全方面的影响,除了那些自己发布二进制文件的人必须想出一个计划来管理他们的安全(例如,他们是否在OpenSSL CVE下降后推出新的构建)。但总的来说,我们这些Python核心人员已经在为所有主要平台维护二进制版本了(通过python.org维护macOS + Windows,通过官方manylinux镜像维护Linux版本),所以即使我们开始在PyPI上发布官方CPython版本,也不会真正引发任何新的安全问题。
如何进行教学
这不是针对终端用户的;他们的体验只是,例如,他们的pyenv或tox调用神奇地变得更快、更可靠(如果这些项目的维护者决定利用这个PEP)。
版权声明
本文件被置于公共领域或CC0-1.0-Universal许可之下,以更许可的方式为准。