PEP 427 – 轮子二进制包格式 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 描述了一种名为“轮子”的 Python 构建包格式。
轮子是具有特殊格式的文件名和 .whl
扩展名的 ZIP 格式存档。它包含一个与 PEP 376 中定义的安装方式几乎相同的单个发行版,并采用特定的安装方案。虽然推荐使用专门的安装程序,但轮子文件可以通过使用标准的“unzip”工具将文件解压到 site-packages 来安装,同时保留足够的信息以便在以后随时将文件内容分散到其最终路径上。
PEP 接受
Alyssa Coghlan 于 2013 年 2 月 16 日接受了这个 PEP,并将定义的轮子版本更新为 1.0 [1]
理由
Python 需要一种比 sdist 更容易安装的包格式。Python 的 sdist 包由 distutils 和 setuptools 构建系统定义和要求,运行任意代码来构建和安装以及重新编译代码,以便将其安装到新的 virtualenv 中。这种将构建和安装混淆的系统速度慢,难以维护,并且阻碍了构建系统和安装程序两方面的创新。
轮子试图通过在构建系统和安装程序之间提供一个更简单的接口来解决这些问题。轮子二进制包格式使安装程序无需了解构建系统,通过将编译时间分摊到多个安装中来节省时间,并消除了在目标环境中安装构建系统的需要。
细节
安装轮子 “distribution-1.0-py32-none-any.whl”
轮子安装概念上包含两个阶段
- 解压。
- 解析
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。(卸载程序应该足够智能,即使 .pyc 没有在 RECORD 中提及,也要将其删除。)
- 解压后的存档包含
推荐的安装程序功能
- 重写
#!python
。 - 在轮子里,脚本被打包到
{distribution}-{version}.data/scripts/
中。如果scripts/
中文件的第一行以b'#!python'
精确开头,则重写以指向正确的解释器。如果存档是在 Windows 上创建的,则 Unix 安装程序可能需要为这些文件添加 +x 权限。允许使用
b'#!pythonw'
约定。b'#!pythonw'
表示 GUI 脚本而不是控制台脚本。 - 生成脚本包装器。
- 在轮子里,在 Unix 系统上打包的脚本肯定不会有相应的 .exe 包装器。Windows 安装程序可能希望在安装过程中添加它们。
推荐的归档器功能
- 将
.dist-info
放在存档的末尾。 - 鼓励归档器将
.dist-info
文件物理地放在存档的末尾。这使得一些潜在的有趣的 ZIP 技巧成为可能,包括无需重写整个存档即可修改元数据的能力。
文件格式
文件名约定
轮子文件名是 {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
。
- distribution
- 发行版名称,例如“django”,“pyramid”。
- version
- 发行版版本,例如 1.0。
- build tag
- 可选的构建编号。必须以数字开头。如果两个轮子文件名在所有其他方面(即名称、版本和其他标签)都相同,则充当断路器。如果未指定,则按空元组排序,否则按包含两个元素的元组排序,第一个元素是初始数字(作为
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
支持。
文件内容
轮子文件的内容,其中 {distribution} 被替换为包的名称,例如 beaglevote
,{version} 被替换为其版本,例如 1.0.0
,包含
/
,存档的根目录,包含所有要安装在purelib
或platlib
中的文件,如WHEEL
中指定。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
是 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
为 true 表示存档的顶级目录应该安装到 purelib 中;否则根目录应该安装到 platlib 中。Tag
是轮子的扩展兼容性标签;在示例中,文件名将包含py2.py3-none-any
。Build
是构建编号,如果不存在构建编号则省略。- 如果 Wheel-Version 大于安装程序支持的版本,则轮子安装程序应该发出警告,如果 Wheel-Version 的主版本号大于安装程序支持的版本,则必须失败。
- 轮子作为一种旨在跨多个 Python 版本工作的安装格式,通常不包含 .pyc 文件。
- 轮子不包含 setup.py 或 setup.cfg。
这个版本的轮子规范基于 distutils 安装方案,并未定义如何将文件安装到其他位置。该布局提供了现有 wininst 和 egg 二进制格式提供的功能的超集。
.dist-info 目录
- Wheel .dist-info 目录至少包含 METADATA、WHEEL 和 RECORD。
- METADATA 是软件包元数据,与 sdists 根目录中的 PKG-INFO 格式相同。
- WHEEL 是特定于软件包构建的轮子元数据。
- RECORD 是轮子中(几乎)所有文件的列表及其安全哈希。与 PEP 376 不同,除了不能包含自身哈希的 RECORD 之外,每个文件都必须包含其哈希。哈希算法必须是 sha256 或更强的算法;具体来说,不允许使用 md5 和 sha1,因为签名轮子文件依赖 RECORD 中的强哈希来验证归档文件的完整性。
- PEP 376 的 INSTALLER 和 REQUESTED 不包含在归档文件中。
- RECORD.jws 用于数字签名。它在 RECORD 中没有提及。
- RECORD.p7s 允许作为对任何希望使用 S/MIME 签名来保护其轮子文件的用户的礼貌。它在 RECORD 中没有提及。
- 在提取过程中,轮子安装程序会验证 RECORD 中的所有哈希值与文件内容是否一致。除了 RECORD 及其签名之外,如果归档文件中的任何文件没有在 RECORD 中被提及或哈希值不正确,安装将失败。
.data 目录
任何通常不会安装在 site-packages 内部的文件都会进入 .data 目录,命名为 .dist-info 目录,但扩展名为 .data/
distribution-1.0.dist-info/
distribution-1.0.data/
.data 目录包含子目录,其中包含来自发行版的脚本、头文件、文档等等。在安装过程中,这些子目录的内容将被移动到其目标路径。
签名轮子文件
轮子文件包含一个扩展的 RECORD,它支持数字签名。PEP 376 的 RECORD 被修改为包括一个安全哈希 digestname=urlsafe_b64encode_nopad(digest)
(没有尾随 = 字符的 url 安全的 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 格式签名。
轮子安装程序不需要理解数字签名,但 MUST 验证 RECORD 中的哈希值与提取的文件内容是否一致。当安装程序检查文件哈希值与 RECORD 是否一致时,一个单独的签名检查器只需要确定 RECORD 是否与签名匹配。
参见
与 .egg 的比较
- Wheel 是一种安装格式;egg 是可导入的。Wheel 归档文件不需要包含 .pyc,并且与特定 Python 版本或实现的绑定较少。Wheel 可以安装(纯 Python)使用先前版本的 Python 构建的软件包,因此您不必总是等到打包程序赶上。
- Wheel 使用 .dist-info 目录;egg 使用 .egg-info。Wheel 与 Python 打包的新世界及其带来的新概念兼容。
- Wheel 对于当今的多实现世界拥有更丰富的文件名约定。单个轮子归档文件可以指示其与多个 Python 语言版本和实现、ABI 和系统体系结构的兼容性。从历史上看,ABI 是特定于 CPython 版本的,wheel 已经为稳定 ABI 做好了准备。
- Wheel 是无损的。第一个 wheel 实现 bdist_wheel 总是生成 egg-info,然后将其转换为 .whl。还可以转换现有的 egg 和 bdist_wininst 发行版。
- Wheel 是带版本号的。每个轮子文件都包含 wheel 规范的版本和打包它的实现。希望下一次迁移可以简单地迁移到 Wheel 2.0。
- Wheel 是对另一个 Python 的引用。
常见问题解答
轮子定义了一个 .data 目录。我应该把所有数据都放在那里吗?
本规范对如何组织代码没有意见。.data 目录只是一个放置任何通常不会安装在site-packages
或 PYTHONPATH 中的文件的地方。换句话说,您仍然可以使用pkgutil.get_data(package, resource)
,即使这些文件通常不会在wheel 的.data
目录中分发。
为什么轮子包含附带签名?
附加签名比分离签名更方便,因为它们与归档文件一起传输。由于只有单个文件被签名,因此可以重新压缩归档文件而不使签名失效,或者可以验证单个文件而无需下载整个归档文件。
为什么轮子允许 JWS 签名?
JWS 所属的 JOSE 规范设计为易于实现,这是一个也是 wheel 的主要设计目标之一的功能。JWS 产生了有用的、简洁的纯 Python 实现。
为什么轮子也允许 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
中,等同于一个“Root-Is-Purelib: true”的轮子,其这些文件位于根目录中,并且允许在“purelib”和“platlib”类别中都有文件。在实践中,轮子应该只包含“purelib”或“platlib”之一,具体取决于它是纯 Python 还是非纯 Python,并且这些文件应该位于根目录中,并为“Root-is-purelib”设置适当的值。
是否可以从轮子文件直接导入 Python 代码?
从技术上讲,由于支持通过简单提取进行安装以及使用与zipimport
兼容的归档格式的组合,轮子文件的一个子集确实支持直接放置在sys.path
上。但是,虽然这种行为是格式设计的自然结果,但实际上依赖它通常是不鼓励的。首先,wheel是主要设计为一种分发格式,因此跳过安装步骤也意味着故意避免对任何依赖于完全安装的功能的依赖(例如,能够使用标准工具(如
pip
和virtualenv
)以一种可以适当地跟踪以供审计和安全更新目的的方式捕获和管理依赖项,或通过在适当的位置发布头文件来与 C 扩展的标准构建机制完全集成)。其次,虽然一些 Python 软件被编写为支持直接从 zip 归档文件运行,但代码通常仍然被编写为假设它已完全安装。当这种假设被尝试从 zip 归档文件运行软件而破坏时,故障往往是模糊的,难以诊断(尤其是在它们发生在第三方库中时)。与此相关的两个最常见的问题来源是:从 zip 归档文件导入 C 扩展不被CPython 支持(因为这样做不受任何平台上的动态加载机制的直接支持),以及当从 zip 归档文件运行时,
__file__
属性不再引用普通文件系统路径,而是引用包含文件系统上 zip 归档文件的位置和归档文件中模块的相对路径的组合路径。即使软件在内部正确地使用抽象资源 API,与外部组件的交互可能仍然需要实际的磁盘文件。与元类、猴子补丁和元路径导入器一样,如果您不确定是否需要利用此功能,您几乎肯定不需要它。如果您确实决定使用它,请注意,许多项目将需要在使用完全安装的软件包的情况下才能重现故障,然后才能将其接受为真正的错误。
参考资料
附录
示例 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
最后修改: 2023-10-11 12:05:51 GMT