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 描述了名为“wheel”的 Python 构建包格式的第二个版本。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。(卸载程序应该足够智能,即使 .pyc 未在 RECORD 中提及,也要将其删除。)

在实践中,安装程序通常会直接从归档文件中提取文件到其目标位置,而不会写入临时 distribution-1.0.data/ 目录。

文件格式

文件名约定

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

发行版
发行版名称,例如 ‘django’,‘pyramid’。
版本
发行版版本,例如 1.0。
构建标记
可选构建编号。必须以数字开头。如果两个 wheel 具有相同的版本,则为一个系。
语言实现和版本标记
例如 ‘py27’,‘py2’,‘py3’。
ABI 标记
例如 ‘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

文件名的每个组件都通过用下划线 _ 替换非字母数字字符的运行来转义

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 中;否则根目录应安装到 platlib 中。
  10. Tag 是 wheel 的扩展兼容性标签;在示例中,文件名将包含 py2.py3-none-any
  11. Build 是构建编号,如果不存在构建编号则省略。
  12. Install-Paths-To 是一个相对于归档文件的路径,它将被安装时每个类别在安装方案中的路径覆盖。请参阅安装路径部分。可以出现 0 次或多次。
  13. 如果 Wheel-Version 大于 wheel 安装程序支持的版本,则 wheel 安装程序应发出警告,如果 Wheel-Version 的主版本号大于 wheel 安装程序支持的版本,则安装必须失败。
  14. Wheel 作为一种旨在跨多个 Python 版本工作的安装格式,通常不包含 .pyc 文件。
  15. Wheel 不包含 setup.py 或 setup.cfg。
.dist-info 目录
  1. Wheel 的 .dist-info 目录至少包含 METADATA、WHEEL 和 RECORD。
  2. METADATA 是包元数据,与 sdist 根目录中找到的 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 本身(因为它不能包含自身的哈希值)。例如

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 上的文件的位置。换句话说,即使*这些*文件通常不会分发在 wheel 的 .data 目录中,您也可以继续使用 pkgutil.get_data(package, resource)

为什么 wheel 包含附加签名?

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

为什么 wheel 允许 JWS 签名?

JWS 所属的 JOSE 规范旨在易于实现,这也是 wheel 的主要设计目标之一。JWS 生成了一个有用的、简洁的纯 Python 实现。

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

对于需要或希望使用现有公钥基础设施的用户,允许使用 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,与外部组件的交互可能仍然需要实际磁盘文件的存在。

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

附录

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

上次修改:2023-09-09 17:39:29 GMT