PEP 427 – Wheel 二进制包格式 1.0
- 作者:
- Daniel Holth <dholth at gmail.com>
- BDFL 委托:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 讨论至:
- Distutils-SIG 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2012年9月20日
- 发布历史:
- 2012年10月18日,2013年2月15日
- 决议:
- Python-Dev 消息
摘要
本 PEP 描述了一种名为“wheel”的 Python 构建包格式。
一个 wheel 是一个 ZIP 格式的归档文件,具有特殊格式的文件名和 .whl
扩展名。它包含一个单独的分发包,几乎与根据 PEP 376 以特定安装方案安装时的情况相同。尽管建议使用专门的安装程序,但 wheel 文件可以通过简单的解压到 site-packages 中,并使用标准的“unzip”工具进行安装,同时保留足够的信息,以便在任何时候将其内容展开到最终路径。
PEP 接受
本 PEP 于 2013 年 2 月 16 日由 Alyssa Coghlan 接受,并将定义的 wheel 版本更新为 1.0 [1]
基本原理
Python 需要一种比 sdist 更容易安装的包格式。Python 的 sdist 包由 distutils 和 setuptools 构建系统定义并需要它们,运行任意代码来构建和安装,并重新编译代码,以便将其安装到新的 virtualenv 中。这种将构建和安装混为一谈的系统速度慢、难以维护,并阻碍了构建系统和安装程序两方面的创新。
Wheel 试图通过在构建系统和安装程序之间提供一个更简单的接口来解决这些问题。wheel 二进制包格式使安装程序无需了解构建系统,通过将编译时间分摊到多次安装中节省了时间,并消除了在目标环境中安装构建系统的需要。
详情
安装一个 wheel 包 ‘distribution-1.0-py32-none-any.whl’
Wheel 安装名义上包括两个阶段
- 解压。
- 解析
distribution-1.0.dist-info/WHEEL
。 - 检查安装程序是否与 Wheel-Version 兼容。如果次版本号较大则发出警告,如果主版本号较大则中止。
- 如果 Root-Is-Purelib == 'true',则将归档文件解压到 purelib (site-packages)。
- 否则将归档文件解压到 platlib (site-packages)。
- 解析
- 展开。
- 解压后的归档文件包括
distribution-1.0.dist-info/
和(如果存在数据)distribution-1.0.data/
。 - 将
distribution-1.0.data/
的每个子树移动到其目标路径。distribution-1.0.data/
的每个子目录都是目标目录字典的键,例如distribution-1.0.data/(purelib|platlib|headers|scripts|data)
。最初支持的路径取自distutils.command.install
。 - 如果适用,将以
#!python
开头的脚本更新为指向正确的解释器。 - 使用安装路径更新
distribution-1.0.dist-info/RECORD
。 - 删除空的
distribution-1.0.data
目录。 - 将所有安装的 .py 编译为 .pyc。(卸载程序应该足够智能,即使 RECORD 中未提及 .pyc 也能将其删除。)
- 解压后的归档文件包括
推荐的安装程序特性
- 重写
#!python
。 - 在 wheel 中,脚本打包在
{distribution}-{version}.data/scripts/
中。如果scripts/
中的文件第一行精确地以b'#!python'
开头,则重写以指向正确的解释器。如果归档文件是在 Windows 上创建的,Unix 安装程序可能需要为这些文件添加 +x 位。允许使用
b'#!pythonw'
约定。b'#!pythonw'
表示 GUI 脚本而非控制台脚本。 - 生成脚本封装器。
- 在 wheel 中,在 Unix 系统上打包的脚本肯定不会附带 .exe 封装器。Windows 安装程序可能需要在安装过程中添加它们。
推荐的归档器特性
- 将
.dist-info
放在归档文件的末尾。 - 鼓励归档器将
.dist-info
文件物理地放在归档文件的末尾。这可以实现一些潜在有趣的 ZIP 技巧,包括在不重写整个归档文件的情况下修改元数据的能力。
文件格式
文件名约定
wheel 文件名是 {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
。
- 分发名称
- 分发名称,例如 'django','pyramid'。
- version
- 分发版本,例如 1.0。
- 构建标签
- 可选的构建号。必须以数字开头。如果两个 wheel 文件名在所有其他方面(即名称、版本和其他标签)都相同,则作为决胜局。如果未指定,则排序为空元组,否则排序为两项元组,第一项是初始数字的
int
,第二项是标签的其余部分作为str
。 - 语言实现和版本标签
- 例如 'py27', 'py2', 'py3'。
- abi tag
- 例如 'cp33m', 'abi3', 'none'。
- platform tag
- 例如 'linux_x86_64','any'。
例如,distribution-1.0-1-py27-none-any.whl
是名为 'distribution' 的软件包的第一次构建,它与 Python 2.7(任何 Python 2.7 实现)兼容,没有 ABI(纯 Python),适用于任何 CPU 架构。
文件扩展名之前的最后三个组件被称为“兼容性标签”。兼容性标签表达了软件包的基本解释器要求,并在 PEP 425 中详细说明。
转义和 Unicode
文件名的每个组件通过用下划线 _
替换非字母数字字符串进行转义
re.sub("[^\w\d.]+", "_", distribution, re.UNICODE)
归档文件名是 Unicode。工具更新以支持非 ASCII 文件名还需要一段时间,但此规范支持它们。
归档文件内部的文件名编码为 UTF-8。尽管一些常用的 ZIP 客户端无法正确显示 UTF-8 文件名,但 ZIP 规范和 Python 的 zipfile
都支持该编码。
文件内容
wheel 文件的内容,其中 {distribution} 被包名替换,例如 beaglevote
,{version} 被其版本替换,例如 1.0.0
,包括
/
,归档文件的根目录,包含所有要安装到WHEEL
中指定的purelib
或platlib
的文件。purelib
和platlib
通常都是site-packages
。{distribution}-{version}.dist-info/
包含元数据。{distribution}-{version}.data/
包含每个未涵盖的非空安装方案键的子目录,其中子目录名称是安装路径字典的索引(例如data
、scripts
、headers
、purelib
、platlib
)。- Python 脚本必须出现在
scripts
中,并且必须以b'#!python'
精确开头,才能在安装时享受脚本包装器生成和#!python
重写。它们可以有任何或没有扩展名。 {distribution}-{version}.dist-info/METADATA
是元数据版本 1.1 或更高格式的元数据。{distribution}-{version}.dist-info/WHEEL
是关于归档文件本身的元数据,采用相同的基本键:值格式Wheel-Version: 1.0 Generator: bdist_wheel 1.0 Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any Build: 1
Wheel-Version
是 Wheel 规范的版本号。Generator
是生成归档文件的软件名称以及可选的版本。Root-Is-Purelib
如果归档的顶层目录应该安装到 purelib,则为 true;否则根目录应该安装到 platlib。Tag
是 wheel 的扩展兼容性标签;在示例中,文件名将包含py2.py3-none-any
。Build
是构建号,如果不存在构建号则省略。- 如果 Wheel-Version 大于其支持的版本,Wheel 安装程序应该发出警告,并且如果 Wheel-Version 的主版本大于其支持的版本,则必须失败。
- Wheel 是一种旨在跨多个 Python 版本工作的安装格式,通常不包含 .pyc 文件。
- Wheel 不包含 setup.py 或 setup.cfg。
此版本的 wheel 规范基于 distutils 安装方案,并未定义如何将文件安装到其他位置。该布局提供了现有 wininst 和 egg 二进制格式功能的超集。
`.dist-info` 目录
- Wheel .dist-info 目录至少包括 METADATA、WHEEL 和 RECORD。
- METADATA 是包元数据,格式与 sdist 根目录下的 PKG-INFO 相同。
- WHEEL 是特定于包构建的 wheel 元数据。
- RECORD 是 wheel 中(几乎)所有文件及其安全哈希的列表。与 PEP 376 不同,除了 RECORD 本身不能包含其哈希之外,每个文件都必须包含其哈希。哈希算法必须是 sha256 或更好;具体来说,不允许使用 md5 和 sha1,因为已签名的 wheel 文件依赖 RECORD 中的强哈希来验证归档文件的完整性。
- PEP 376 的 INSTALLER 和 REQUESTED 不包含在归档文件中。
- RECORD.jws 用于数字签名。它在 RECORD 中没有提及。
- 允许使用 RECORD.p7s,以方便那些希望使用 S/MIME 签名来保护其 wheel 文件的人。它在 RECORD 中没有提及。
- 在提取过程中,wheel 安装程序会根据文件内容验证 RECORD 中的所有哈希。除了 RECORD 及其签名之外,如果归档文件中的任何文件未在 RECORD 中提及或哈希不正确,则安装将失败。
`.data` 目录
任何通常不安装在 site-packages 内部的文件都放在 .data 目录中,命名方式与 .dist-info 目录相同,但带有 .data/ 扩展名
distribution-1.0.dist-info/
distribution-1.0.data/
`.data` 目录包含子目录,其中包含分发包中的脚本、头文件、文档等。在安装过程中,这些子目录的内容会移动到其目标路径。
已签名的 wheel 文件
Wheel 文件包含一个扩展的 RECORD,支持数字签名。PEP 376 的 RECORD 被修改,将安全哈希 digestname=urlsafe_b64encode_nopad(digest)
(urlsafe base64 编码,无末尾 = 字符)作为第二列,而不是 md5sum。所有可能的条目都会进行哈希,包括任何生成的文件,例如 .pyc 文件,但不包括 RECORD,因为它不能包含自身的哈希。例如
file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\_pNh2yI,3144
distribution-1.0.dist-info/RECORD,,
签名文件 RECORD.jws 和 RECORD.p7s 根本没有在 RECORD 中提及,因为它们只能在 RECORD 生成后添加。归档文件中的所有其他文件都必须在 RECORD 中有正确的哈希,否则安装将失败。
如果使用 JSON Web 签名,一个或多个 JSON Web 签名 JSON 序列化 (JWS-JS) 签名存储在 RECORD 旁边的 RECORD.jws 文件中。JWS 通过将 RECORD 的 SHA-256 哈希作为签名的 JSON 有效载荷来签署 RECORD
{ "hash": "sha256=ADD-r2urObZHcxBW3Cr-vDCu5RJwT4CaRTHiFmbcIYY" }
(哈希值与 RECORD 中使用的格式相同。)
如果使用 RECORD.p7s,它必须包含 RECORD 的分离式 S/MIME 格式签名。
Wheel 安装程序不要求理解数字签名,但必须根据提取的文件内容验证 RECORD 中的哈希。当安装程序检查文件哈希与 RECORD 时,一个独立的签名检查器只需要确认 RECORD 与签名匹配。
参见
与 `.egg` 的比较
- Wheel 是一种安装格式;egg 是可导入的。Wheel 归档文件不需要包含 .pyc,并且与特定的 Python 版本或实现关联性较小。Wheel 可以安装用以前版本的 Python 构建的(纯 Python)包,因此您不必总是等待打包者跟上。
- Wheel 使用 .dist-info 目录;egg 使用 .egg-info。Wheel 与 Python 打包的新世界及其带来的新概念兼容。
- Wheel 为当今多实现的世界提供了更丰富的文件命名约定。一个 wheel 归档文件可以表明其与多个 Python 语言版本和实现、ABI 和系统架构的兼容性。历史上 ABI 特定于 CPython 发布版,wheel 为稳定的 ABI 做好了准备。
- Wheel 是无损的。第一个 wheel 实现 bdist_wheel 总是生成 egg-info,然后将其转换为 .whl。也可以转换现有的 egg 和 bdist_wininst 分发。
- Wheel 是版本化的。每个 wheel 文件都包含 wheel 规范的版本以及打包它的实现。希望下一次迁移可以直接是 Wheel 2.0。
- Wheel 是对另一个 Python 的引用。
常见问题
Wheel 定义了一个 `.data` 目录。我应该把所有数据都放在那里吗?
此规范不就您应如何组织代码发表意见。.data 目录只是一个用于存放通常不安装在site-packages
或 PYTHONPATH 中的任何文件的地方。换句话说,您仍可继续使用pkgutil.get_data(package, resource)
,即使 这些 文件通常不会在 wheel 的.data
目录中分发。
为什么 wheel 包含附加签名?
附加签名比分离签名更方便,因为它们随归档文件一起传输。由于只对单个文件进行签名,因此可以重新压缩归档文件而不会使签名失效,或者可以在不下载整个归档文件的情况下验证单个文件。
为什么 wheel 允许 JWS 签名?
JWS 所属的 JOSE 规范旨在易于实现,这也是 wheel 的主要设计目标之一。JWS 产生了一个有用、简洁的纯 Python 实现。
为什么 wheel 也允许 S/MIME 签名?
S/MIME 签名允许那些需要或希望将现有公钥基础设施与 wheel 一起使用的用户。签名包只是安全包更新系统中的一个基本构建块。Wheel 只提供这个构建块。
“purelib” 与 “platlib” 有什么区别?
Wheel 保留了“purelib”与“platlib”的区别,这在某些平台上意义重大。例如,Fedora 将纯 Python 包安装到“/usr/lib/pythonX.Y/site-packages”,将平台相关包安装到“/usr/lib64/pythonX.Y/site-packages”。一个带有“Root-Is-Purelib: false”且所有文件都在
{name}-{version}.data/purelib
中的 wheel,等同于一个带有“Root-Is-Purelib: true”且这些相同文件在根目录中的 wheel,并且在“purelib”和“platlib”类别中同时拥有文件是合法的。实际上,一个 wheel 应该只包含“purelib”或“platlib”中的一个,具体取决于它是纯 Python 还是非纯 Python,并且这些文件应该在根目录下,并设置相应的“Root-is-purelib”。
是否可以直接从 wheel 文件导入 Python 代码?
从技术上讲,由于支持通过简单提取进行安装并使用与zipimport
兼容的归档格式,部分 wheel 文件 确实 支持直接放置在sys.path
上。然而,尽管这种行为是格式设计的自然结果,但通常不鼓励实际依赖它。首先,wheel 主要 设计为一种分发格式,因此跳过安装步骤也意味着故意避免依赖假定完全安装的功能(例如,能够使用像
pip
和virtualenv
这样的标准工具来捕获和管理依赖项,以便进行审计和安全更新,或通过在适当位置发布头文件来完全集成 C 扩展的标准构建机制)。其次,尽管一些 Python 软件被编写为支持直接从 zip 归档运行,但代码通常仍假定已完全安装。当尝试从 zip 归档运行软件而破坏此假设时,故障通常可能模糊且难以诊断(尤其是在第三方库中发生时)。造成此问题的两个最常见原因是:CPython 不 支持从 zip 归档导入 C 扩展(因为任何平台上的动态加载机制都不直接支持这样做),以及从 zip 归档运行时,
__file__
属性不再引用普通文件系统路径,而是引用包含 zip 归档在文件系统上的位置和模块在归档内相对路径的组合路径。即使软件在内部正确使用抽象资源 API,与外部组件的接口可能仍需要实际的磁盘文件。就像元类、猴子补丁和元路径导入器一样,如果您还不确定是否需要利用此功能,那么您几乎肯定不需要它。如果您 仍然 决定使用它,请注意许多项目将要求使用完全安装的包重现故障,然后才会将其视为真正的 bug。
参考资料
附录
urlsafe-base64-nopad 实现示例
# urlsafe-base64-nopad for Python 3
import base64
def urlsafe_b64encode_nopad(data):
return base64.urlsafe_b64encode(data).rstrip(b'=')
def urlsafe_b64decode_nopad(data):
pad = b'=' * (4 - (len(data) & 3))
return base64.urlsafe_b64decode(data + pad)
版权
本文档已进入公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0427.rst