PEP 491 – Wheel 二进制包格式 1.9
- 作者:
- Daniel Holth <dholth at gmail.com>
- 讨论至:
- Distutils-SIG 邮件列表
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2015年4月16日
摘要
本 PEP 描述了 Python 内置包格式“wheel”的第二个版本。Wheel 提供了一种 Python 特定的、可重定位的包格式,它允许人们比每次从源代码重新构建更快、更可预测地安装软件。
一个 wheel 是一个 ZIP 格式的存档文件,具有特殊格式的文件名和 .whl 扩展名。它包含一个根据 PEP 376 以及特定安装方案安装的单一发行版。简单的 wheel 可以解压到 sys.path 并直接使用,但 wheel 通常使用专门的安装程序进行安装。
此版本的 wheel 规范增加了对将发行版安装到许多不同目录的支持,并增加了一种在安装后查找这些文件的方法。
PEP 延期
本 PEP 目前并未积极推进,Python 打包改进目前专注于包构建过程,而不是扩展二进制存档格式以涵盖其他用例。
未来恢复本 PEP 工作时需要解决的一些具体要素
- 将官方 wheel 格式定义迁移到 https://packaging.pythonlang.cn/specifications/(类似于 PEP 566 为 https://packaging.pythonlang.cn/specifications/core-metadata/ 所做的)
- 更新 PEP 本身,重点关注两种格式版本之间所做的*更改*以及这些更改的理由,而不是重复 PEP 427 中未更改的所有信息
- 澄清 PEP 是有意编写的,以允许现有安装程序在使用现有安装方案定义时符合规范,同时还允许创建新的安装方案定义,利用二进制存档内容的更丰富的分类方案
基本原理
Wheel 1.0 最擅长将文件安装到 site-packages 和 distutils 指定的其他几个位置,但用户希望将单个发行版的文件安装到许多目录中——也许文档、数据和代码有单独的位置。不幸的是,并不是每个人都同意这些安装位置应该相对于根目录在哪里。此版本的格式增加了更多类别,每个类别都可以根据策略安装到不同的目标。由于在运行时定位已安装文件也很重要,因此此版本的格式还添加了一种记录已安装路径的方法,以便已安装软件可以读取。
详情
安装 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)。 - 更新以
#!python开头的脚本以指向正确的解释器。(注意:Python 脚本通常由包元数据处理,而不按字面包含在 wheel 中。) - 使用已安装的路径更新
distribution-1.0.dist.info/RECORD。 - 如果为空,则删除
distribution-1.0.data目录。 - 将所有已安装的 .py 编译为 .pyc。(即使 RECORD 中未提及 .pyc,卸载程序也应该足够智能地删除 .pyc。)
- 解包的存档包括
实际上,安装程序通常会将文件直接从存档解压到其目标位置,而无需写入临时 distribution-1.0.data/ 目录。
推荐的安装程序功能
- 重写
#!python。 - 在 wheel 中,字面脚本打包在
{distribution}-{version}.data/scripts/中。如果scripts/中文件的第一行以b'#!python'精确开头,则重写以指向正确的解释器。如果存档是在 Windows 上创建的,Unix 安装程序可能需要向这些文件添加 +x 位。允许使用
b'#!pythonw'约定。b'#!pythonw'表示 GUI 脚本而不是控制台脚本。 - 生成脚本包装器。
- Python 脚本更常见地在包元数据中表示为
module:callable字符串,而不按字面包含在 wheel 存档的scripts目录中。这种脚本为安装程序提供了生成平台特定包装器的机会。
推荐的打包程序功能
- 将
.dist-info放在存档的末尾。 - 鼓励打包程序将
.dist-info文件物理地放在存档的末尾。这允许一些潜在有趣的 ZIP 技巧,包括在不重写整个存档的情况下修改元数据。
文件格式
文件命名约定
wheel 文件名为 {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl。
- distribution
- 发行版名称,例如“django”、“pyramid”。
- version
- 发行版版本,例如 1.0。
- build tag
- 可选的构建号。必须以数字开头。如果两个 wheel 具有相同的版本,则作为决胜局。如果未指定则按空字符串排序,否则将初始数字按数字排序,其余按字典顺序排序。
- 语言实现和版本标签
- 例如“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 包名,但本规范支持 Unicode 文件名。
存档*内部*的文件名以 UTF-8 编码。尽管一些常用 ZIP 客户端不能正确显示 UTF-8 文件名,但 ZIP 规范和 Python 的 zipfile 都支持该编码。
文件内容
wheel 文件的内容,其中 {distribution} 替换为包名,例如 beaglevote,{version} 替换为版本号,例如 1.0.0,包括
/,存档的根目录,包含所有要安装到purelib或platlib的文件,如WHEEL中指定。purelib和platlib通常都是site-packages。{distribution}-{version}.dist-info/包含元数据。{distribution}-{version}.data/包含每个未覆盖的非空安装方案键的子目录,其中子目录名称是安装路径字典的索引(例如data、scripts、include、purelib、platlib)。- Python 脚本必须出现在
scripts中,并且必须以b'#!python'精确开头,才能在安装时享受脚本包装器生成和#!python重写。它们可以有任何扩展名或没有扩展名。 {distribution}-{version}.dist-info/METADATA是元数据版本 1.1 或更高格式的元数据。{distribution}-{version}.dist-info/WHEEL是关于存档本身的元数据,采用相同的基本键:值格式Wheel-Version: 1.9 Generator: bdist_wheel 1.9 Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any Build: 1 Install-Paths-To: wheel/_paths.py Install-Paths-To: wheel/_paths.json
Wheel-Version是 Wheel 规范的版本号。Generator是生成存档的软件名称和可选的版本。Root-Is-Purelib如果存档的顶层目录应该安装到 purelib,则为 true;否则根目录应该安装到 platlib。Tag是 wheel 的扩展兼容性标签;在示例中,文件名将包含py2.py3-none-any。Build是构建号,如果没有构建号则省略。Install-Paths-To是一个*相对于存档*的位置,它将被安装方案中每个类别的安装时路径覆盖。请参阅安装路径部分。可以出现 0 次或更多次。- 如果 Wheel-Version 大于其支持的版本,wheel 安装程序应发出警告,并且如果 Wheel-Version 的主版本大于其支持的版本,则必须失败。
- Wheel 作为一种旨在跨多个 Python 版本工作的安装格式,通常不包含 .pyc 文件。
- Wheel 不包含 setup.py 或 setup.cfg。
.dist-info 目录
- Wheel .dist-info 目录至少包含 METADATA、WHEEL 和 RECORD。
- METADATA 是包元数据,与 sdists 根目录中的 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 目录包含子目录,其中包含发行版的脚本、头文件、文档等。在安装期间,这些子目录的内容会移动到其目标路径。
如果在安装方案中找不到子目录,安装程序应发出警告,并且应将其安装在 distribution-1.0.data/...,就像包由标准解压工具解压一样。
安装路径
除了 distutils 安装路径之外,wheel 现在还包含基于 GNU autotools 列出的类别。此扩展方案应有助于安装程序实施系统策略,但安装程序可以在任何位置设置每个类别的根目录。
UNIX 安装方案可能将类别映射到其安装路径,如下所示
{
'bindir': '$eprefix/bin',
'sbindir': '$eprefix/sbin',
'libexecdir': '$eprefix/libexec',
'sysconfdir': '$prefix/etc',
'sharedstatedir': '$prefix/com',
'localstatedir': '$prefix/var',
'libdir': '$eprefix/lib',
'static_libdir': r'$prefix/lib',
'includedir': '$prefix/include',
'datarootdir': '$prefix/share',
'datadir': '$datarootdir',
'mandir': '$datarootdir/man',
'infodir': '$datarootdir/info',
'localedir': '$datarootdir/locale',
'docdir': '$datarootdir/doc/$dist_name',
'htmldir': '$docdir',
'dvidir': '$docdir',
'psdir': '$docdir',
'pdfdir': '$docdir',
'pkgdatadir': '$datadir/$dist_name'
}
如果一个包需要在运行时查找其文件,它可以要求安装程序将其写入指定文件或文件,*并*将其包含在存档本身内的相同文件中,相对于它们在存档中的位置(因此,如果用标准解压工具解压,甚至根本不解压,wheel 仍能正确安装)。
如果 WHEEL 元数据包含这些字段
Install-Paths-To: wheel/_paths.py
Install-Paths-To: wheel/_paths.json
那么 wheel 安装程序在即将从存档解压 wheel/_paths.py 时,会将其替换为安装时使用的实际路径。路径可以是绝对路径,也可以是相对于生成文件的路径。
如果文件名以 .py 结尾,则会写入一个 Python 脚本。该脚本必须执行才能获取路径,但它可能看起来像这样
data='../wheel-0.26.0.dev1.data/data'
headers='../wheel-0.26.0.dev1.data/headers'
platlib='../wheel-0.26.0.dev1.data/platlib'
purelib='../wheel-0.26.0.dev1.data/purelib'
scripts='../wheel-0.26.0.dev1.data/scripts'
# ...
如果文件名以 .json 结尾,则会写入一个 JSON 文档
{ "data": "../wheel-0.26.0.dev1.data/data", ... }
只有特定 wheel 实际使用的类别才必须写入此文件。
这些文件旨在写入一个可以由已安装包找到的位置,而无需引入对打包库的任何依赖。
签名的 wheel 文件
Wheel 文件包含一个扩展的 RECORD,它支持数字签名。PEP 376 的 RECORD 经过修改,将安全哈希 digestname=urlsafe_b64encode_nopad(digest)(不带尾随 = 字符的 urlsafe base64 编码)作为第二列,而不是 md5sum。所有可能的条目都经过哈希处理,包括任何生成的文件(如 .pyc 文件),但不包括 RECORD,因为 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,并且这些文件应该在根目录下,并为“Root-is-purelib”设置适当的值。
是否可以直接从 wheel 文件导入 Python 代码?
技术上,由于支持通过简单提取进行安装并使用与zipimport兼容的存档格式,一部分 wheel 文件*确实*支持直接放置在sys.path上。然而,虽然这种行为是格式设计的自然结果,但通常不鼓励实际依赖它。首先,wheel *主要*设计为一种分发格式,因此跳过安装步骤也意味着故意避免依赖假定完全安装的功能(例如,能够使用
pip和virtualenv等标准工具来捕获和管理依赖项,以便进行审计和安全更新,或通过在适当位置发布头文件来与 C 扩展的标准构建机制完全集成)。其次,虽然有些 Python 软件支持直接从 zip 存档运行,但代码通常仍假设已完全安装。当尝试从 zip 存档运行软件而打破这种假设时,故障通常可能晦涩难懂且难以诊断(尤其是在第三方库中发生时)。造成此问题的两个最常见原因是从 zip 存档导入 C 扩展*不*受 CPython 支持(因为任何平台上的动态加载机制都不直接支持这样做),以及从 zip 存档运行时
__file__属性不再引用普通的filesystem 路径,而是引用包含 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-0491.rst