Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 1.0 最擅长将文件安装到 site-packages 和 distutils 指定的其他几个位置,但用户希望将单个发行版的文件安装到许多目录中——也许文档、数据和代码有单独的位置。不幸的是,并不是每个人都同意这些安装位置应该相对于根目录在哪里。此版本的格式增加了更多类别,每个类别都可以根据策略安装到不同的目标。由于在运行时定位已安装文件也很重要,因此此版本的格式还添加了一种记录已安装路径的方法,以便已安装软件可以读取。

详情

安装 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)
    3. 更新以 #!python 开头的脚本以指向正确的解释器。(注意:Python 脚本通常由包元数据处理,而不按字面包含在 wheel 中。)
    4. 使用已安装的路径更新 distribution-1.0.dist.info/RECORD
    5. 如果为空,则删除 distribution-1.0.data 目录。
    6. 将所有已安装的 .py 编译为 .pyc。(即使 RECORD 中未提及 .pyc,卸载程序也应该足够智能地删除 .pyc。)

实际上,安装程序通常会将文件直接从存档解压到其目标位置,而无需写入临时 distribution-1.0.data/ 目录。

文件格式

文件命名约定

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,包括

  1. /,存档的根目录,包含所有要安装到 purelibplatlib 的文件,如 WHEEL 中指定。purelibplatlib 通常都是 site-packages
  2. {distribution}-{version}.dist-info/ 包含元数据。
  3. {distribution}-{version}.data/ 包含每个未覆盖的非空安装方案键的子目录,其中子目录名称是安装路径字典的索引(例如 datascriptsincludepurelibplatlib)。
  4. Python 脚本必须出现在 scripts 中,并且必须以 b'#!python' 精确开头,才能在安装时享受脚本包装器生成和 #!python 重写。它们可以有任何扩展名或没有扩展名。
  5. {distribution}-{version}.dist-info/METADATA 是元数据版本 1.1 或更高格式的元数据。
  6. {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
    
  7. Wheel-Version 是 Wheel 规范的版本号。
  8. Generator 是生成存档的软件名称和可选的版本。
  9. Root-Is-Purelib 如果存档的顶层目录应该安装到 purelib,则为 true;否则根目录应该安装到 platlib。
  10. Tag 是 wheel 的扩展兼容性标签;在示例中,文件名将包含 py2.py3-none-any
  11. Build 是构建号,如果没有构建号则省略。
  12. Install-Paths-To 是一个*相对于存档*的位置,它将被安装方案中每个类别的安装时路径覆盖。请参阅安装路径部分。可以出现 0 次或更多次。
  13. 如果 Wheel-Version 大于其支持的版本,wheel 安装程序应发出警告,并且如果 Wheel-Version 的主版本大于其支持的版本,则必须失败。
  14. Wheel 作为一种旨在跨多个 Python 版本工作的安装格式,通常不包含 .pyc 文件。
  15. Wheel 不包含 setup.py 或 setup.cfg。
.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 中提及也未正确哈希,则安装将失败。
.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 的比较

  1. Wheel 是一种安装格式;egg 是可导入的。Wheel 存档不需要包含 .pyc,并且与特定 Python 版本或实现的相关性较小。Wheel 可以安装使用以前版本的 Python 构建的(纯 Python)包,因此您不必总是等待打包程序跟上。
  2. Wheel 使用 .dist-info 目录;egg 使用 .egg-info。Wheel 与 Python 打包的新世界及其带来的新概念兼容。
  3. Wheel 为当今多实现的世界提供了更丰富的文件命名约定。单个 wheel 存档可以指示其与多个 Python 语言版本和实现、ABI 和系统架构的兼容性。历史上,ABI 是 CPython 版本特有的,wheel 已为稳定的 ABI 做好准备。
  4. Wheel 是无损的。第一个 wheel 实现 bdist_wheel 总是生成 egg-info,然后将其转换为 .whl。也可以转换现有的 egg 和 bdist_wininst 发行版。
  5. Wheel 是有版本的。每个 wheel 文件都包含 wheel 规范的版本和打包它的实现。希望下一次迁移可以直接到 Wheel 2.0。
  6. 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 *主要*设计为一种分发格式,因此跳过安装步骤也意味着故意避免依赖假定完全安装的功能(例如,能够使用 pipvirtualenv 等标准工具来捕获和管理依赖项,以便进行审计和安全更新,或通过在适当位置发布头文件来与 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

最后修改:2025-02-01 08:55:40 GMT