二进制分发格式

本页面规定了 Python 包的二进制分发格式,也称为 wheel 格式。

wheel 是一个 ZIP 格式的存档文件,具有特殊格式的文件名和 .whl 扩展名。它包含一个单独的分发包,几乎按照 PEP 376 的规定和特定的安装方案安装。虽然建议使用专门的安装程序,但 wheel 文件可以通过标准 'unzip' 工具简单地解压到 site-packages 中进行安装,同时保留足够的信息,以便在任何时候将其内容散布到最终路径。

详情

安装 wheel 'distribution-1.0-py32-none-any.whl'

Wheel 安装名义上包括两个阶段

  • 解压。

    1. 解析 distribution-1.0.dist-info/WHEEL

    2. 检查安装程序是否与 Wheel-Version 兼容。如果次版本号较大则发出警告,如果主版本号较大则中止。

    3. 如果 Root-Is-Purelib == 'true',则将存档解压到 purelib (site-packages)。

    4. 否则将存档解压到 platlib (site-packages)。

  • 散布。

    1. 解压后的存档包括 distribution-1.0.dist-info/ 和(如果存在数据)distribution-1.0.data/

    2. distribution-1.0.data/ 的每个子树移动到其目标路径。 distribution-1.0.data/ 的每个子目录都是目标目录字典的键,例如 distribution-1.0.data/(purelib|platlib|headers|scripts|data)。这些子目录是 sysconfig 定义的安装路径

    3. 如果适用,更新以 #!python 开头的脚本,使其指向正确的解释器。

    4. 使用已安装的路径更新 distribution-1.0.dist-info/RECORD

    5. 删除空的 distribution-1.0.data 目录。

    6. 将任何已安装的 .py 编译为 .pyc。(卸载程序应该足够智能,即使 RECORD 中没有提到 .pyc 也要将其删除。)

文件格式

文件名约定

wheel 文件名是 {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl

distribution

分发名称,例如 'django'、'pyramid'。

版本

分发版本,例如 1.0。

build tag

可选的构建号。必须以数字开头。如果两个 wheel 文件名在所有其他方面(即名称、版本和其他标签)都相同,则作为决胜因素。如果未指定,则排序为空元组,否则排序为两项元组,第一项是初始数字作为 int,第二项是标签的其余部分作为 str

构建号的一个常见用例是由于构建环境的变化而重新构建二进制分发包,例如在使用 manylinux 镜像构建使用预发布 CPython 版本的发行版时。

警告

构建号不是发行版版本的一部分,因此难以在外部引用,尤其是在 Python 工具和标准生态系统之外。发行版需要外部引用的一个常见情况是解决安全漏洞时。

由于此限制,需要外部引用的新发行版在构建新发行版时**不应**使用构建号。相反,对于此类情况,应创建**新的发行版版本**。

语言实现和版本标签

例如 'py27', 'py2', 'py3'。

abi tag

例如 'cp33m', 'abi3', 'none'。

平台标签

例如 '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

由于文件名的组件由破折号(-,连字符-减号)分隔,因此此字符不能出现在任何组件中。处理方式如下

  • 在分发名称中,任何连续的 -_ 字符(连字符-减号、下划线和句号)都应替换为 _(下划线),并且大写字符应替换为相应的小写字符。这等同于常规的名称规范化,然后将 - 替换为 _。但是,使用 wheel 的工具必须准备好接受 .(句号)和大写字母,因为这些在规范的早期版本中是允许的。

  • 版本号应根据版本说明符规范进行规范化。规范化的版本号不能包含 -

  • 其余组件不得包含 - 字符,因此无需转义。

生成 wheel 的工具应验证文件名组件是否不包含 -,因为如果包含,生成的文件可能无法正确处理。

存档文件名是 Unicode。工具更新以支持非 ASCII 文件名还需要一段时间,但此规范支持它们。

存档内部的文件名以 UTF-8 编码。尽管一些常用 ZIP 客户端无法正确显示 UTF-8 文件名,但 ZIP 规范和 Python 的 zipfile 都支持该编码。

文件内容

wheel 文件的内容,其中 {distribution} 被替换为包的规范化名称,例如 beaglevote,{version} 被替换为其规范化版本,例如 1.0.0,(两个字段中的破折号/- 字符都被下划线/_ 字符替换)包括

  1. /,存档的根目录,包含所有要安装到 purelibplatlib 的文件,如 WHEEL 中指定。purelibplatlib 通常都是 site-packages

  2. {distribution}-{version}.dist-info/ 包含元数据。

  3. distribution-version.dist-info/licenses/ 包含许可证文件。

  4. {distribution}-{version}.data/ 为每个尚未覆盖的非空安装方案键包含一个子目录,其中子目录名称是安装路径字典的索引(例如 datascriptsheaderspurelibplatlib)。

  5. Python 脚本必须出现在 scripts 中,并且必须以 b'#!python' 开头,才能在安装时生成脚本包装器并重写 #!python。它们可以有任何扩展名或没有扩展名。scripts 目录只能包含常规文件。

  6. {distribution}-{version}.dist-info/METADATA 是元数据版本 1.1 或更高格式的元数据。

  7. {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
    
  8. Wheel-Version 是 Wheel 规范的版本号。

  9. Generator 是生成存档的软件的名称,可选包含版本。

  10. Root-Is-Purelib 如果存档的顶级目录应安装到 purelib 中,则为 true;否则,根目录应安装到 platlib 中。

  11. Tag 是 wheel 扩展的兼容性标签;在示例中,文件名将包含 py2.py3-none-any

  12. Build 是构建号,如果不存在构建号则省略。

  13. 如果 Wheel-Version 大于其支持的版本,wheel 安装程序应发出警告,如果 Wheel-Version 的主版本号大于其支持的版本,则必须失败。

  14. Wheel 是一种旨在跨多个 Python 版本工作的安装格式,通常不包含 .pyc 文件。

  15. Wheel 不包含 setup.py 或 setup.cfg。

此版本的 wheel 规范基于 distutils 安装方案,不定义如何将文件安装到其他位置。该布局提供了现有 wininst 和 egg 二进制格式提供的功能的超集。

.dist-info 目录
  1. Wheel .dist-info 目录至少包含 METADATA、WHEEL 和 RECORD。

  2. METADATA 是包元数据,格式与 sdists 根目录中的 PKG-INFO 相同。

  3. WHEEL 是特定于包构建的 wheel 元数据。

  4. RECORD 是 wheel 中(几乎)所有文件及其安全哈希的列表。与 PEP 376 不同,除了 RECORD(不能包含其自身的哈希)之外,每个文件都必须包含其哈希。哈希算法必须是 sha256 或更好;具体来说,md5 和 sha1 不允许,因为签名的 wheel 文件依赖于 RECORD 中的强哈希来验证存档的完整性。

  5. PEP 376 的 INSTALLER 和 REQUESTED 不包含在存档中。

  6. RECORD.jws 用于数字签名。它未在 RECORD 中提及。

  7. RECORD.p7s 允许作为对任何希望使用 S/MIME 签名来保护其 wheel 文件的用户的礼遇。它未在 RECORD 中提及。

  8. 在提取过程中,wheel 安装程序会根据文件内容验证 RECORD 中的所有哈希。除了 RECORD 及其签名之外,如果存档中的任何文件既未在 RECORD 中提及也未正确哈希,则安装将失败。

.dist-info/ 中的子目录

.dist-info/ 下的子目录保留供将来使用。.dist-info/ 下的以下子目录名称保留用于特定用途

子目录名称

PEP / 标准

licenses

PEP 639

license_files

PEP 639

LICENSES

REUSE 许可框架

sboms

PEP 770

.dist-info/licenses/ 目录

如果元数据版本为 2.4 或更高版本,并且指定了一个或多个 License-File 字段,则 .dist-info/ 目录**必须**包含一个 licenses/ 子目录,该子目录**必须**包含 METADATA 文件中 License-File 字段中列出的文件,其路径相对于 licenses/ 目录。

.dist-info/sboms/ 目录

.dist-info/sboms/ 目录中包含的所有文件**必须**是软件物料清单 (SBOM) 文件,用于描述分发存档中包含的软件。

.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 与签名匹配。

请参阅

常见问题

Wheel 定义了一个 .data 目录。我应该把所有数据都放在那里吗?

本规范不涉及您应该如何组织代码。 .data 目录只是一个用于存放通常不安_装在 site-packages 或 PYTHONPATH 中的文件的位置。换句话说,您可能仍然可以使用 pkgutil.get_data(package, resource),尽管那些文件通常不会在wheel 的 .data 目录中分发。

为什么 wheel 包含附加签名?

附加签名比分离签名更方便,因为它们随存档一起传输。由于只对单个文件进行签名,因此可以重新压缩存档而不会使签名失效,或者可以验证单个文件而无需下载整个存档。

为什么 wheel 允许 JWS 签名?

JOSE 规范(JWS 是其一部分)旨在易于实现,这也是 wheel 主要设计目标之一。JWS 产生了有用、简洁的纯 Python 实现。

为什么 wheel 也允许 S/MIME 签名?

对于需要或希望将现有公钥基础设施与 wheel 一起使用的用户,允许使用 S/MIME 签名。

签名包只是安全包更新系统中的一个基本构建块。Wheel 只提供构建块。

“purelib”与“platlib”有什么关系?

Wheel 保留了“purelib”与“platlib”的区别,这在某些平台上很重要。例如,Fedora 将纯 Python 包安装到 '/usr/lib/pythonX.Y/site-packages',将平台相关包安装到 '/usr/lib64/pythonX.Y/site-packages'。

一个“Root-Is-Purelib: false”的 wheel,所有文件都在 {name}-{version}.data/purelib 中,等同于一个“Root-Is-Purelib: true”的 wheel,其中这些相同的文件在根目录中,并且在“purelib”和“platlib”类别中同时拥有文件是合法的。

实际上,一个 wheel 应该只包含“purelib”或“platlib”之一,具体取决于它是否是纯 Python 包,并且这些文件应该位于根目录,并设置适当的“Root-is-purelib”设置。

是否可以直接从 wheel 文件导入 Python 代码?

从技术上讲,由于支持通过简单提取安装以及使用与 zipimport 兼容的存档格式,一部分 wheel 文件确实支持直接放置在 sys.path 上。然而,虽然这种行为是格式设计的自然结果,但通常不鼓励实际依赖它。

首先,wheel 主要被设计为一种分发格式,因此跳过安装步骤也意味着刻意避免依赖假定完全安装的功能(例如能够使用 pipvirtualenv 等标准工具以可正确跟踪审计和安全更新目的的方式捕获和管理依赖项,或通过在适当位置发布头文件来与 C 扩展的标准构建机制完全集成)。

其次,虽然有些 Python 软件支持直接从 zip 存档运行,但代码通常仍然假定已完全安装。当通过尝试从 zip 存档运行软件而打破此假设时,失败通常会模糊不清且难以诊断(尤其是在第三方库中发生时)。最常见的两个问题来源是 CPython 不支持从 zip 存档导入 C 扩展(因为在任何平台上,动态加载机制都不直接支持这样做),以及当从 zip 存档运行时,__file__ 属性不再指向普通文件系统路径,而是指向包含文件系统上 zip 存档位置和存档内模块相对路径的组合路径。即使软件内部正确使用抽象资源 API,与外部组件的接口可能仍然需要实际的磁盘文件。

就像元类、猴子补丁和元路径导入器一样,如果您不确定是否需要利用此功能,那么您几乎肯定不需要它。如果您确实决定使用它,请注意许多项目会要求在接受其为真实错误之前,必须使用完全安装的包重现故障。

历史

  • 2013 年 2 月:此规范通过PEP 427 获得批准。

  • 2021 年 2 月:修订了 wheel 文件名中的转义规则,使其与常用工具的实际做法保持一致。

  • 2024 年 12 月:澄清了 scripts 文件夹应仅包含常规文件(当使用工具遇到此文件夹中的符号链接或子目录时,预期行为没有正式定义,因此可能因工具而异)。

  • 2024 年 12 月:.dist-info/licenses/ 目录已通过PEP 639 指定。

  • 2025 年 1 月:澄清了 .dist-info.data 目录的名称和版本需要规范化。

附录

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)