PEP 751 – 用于列出 Python 依赖项以实现安装可重复性的文件格式
- 作者:
- Brett Cannon <brett at python.org>
- 讨论邮件列表:
- Discourse 帖子
- 状态:
- 草稿
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2024年7月24日
- 修订历史:
- 2024年7月25日
- 替代:
- 665
目录
- 摘要
- 动机
- 基本原理
- 规范
- 文件名
- 文件格式
version
hash-algorithm
dependencies
[[file-locks]]
[package-lock]
[[packages]]
packages.name
packages.version
packages.multiple-entries
packages.description
packages.simple-repo-package-url
packages.marker
packages.requires-python
packages.dependents
packages.dependencies
packages.direct
[[packages.files]]
[packages.vcs]
[packages.directory]
[[packages.build-requires]]
[packages.tool]
[tool]
- 示例
- 对锁定工具的期望
- 对安装工具的期望
- 向后兼容性
- 安全隐患
- 如何教授
- 参考实现
- 被拒绝的想法
- 未解决的问题
- 致谢
- 版权
摘要
本 PEP 提出了一种用于依赖项规范的新文件格式,以在 Python 环境中实现可重复的安装。该格式旨在易于人类阅读和机器生成。使用该文件的安装程序应该能够独立地评估每个相关的包,在安装时无需进行依赖项解析。
动机
目前,尚不存在标准来
- 指定应将哪些顶级依赖项安装到 Python 环境中。
- 创建一个不可变的记录(例如锁定文件),记录安装了哪些依赖项。
考虑到社区中至少有五种众所周知的解决方案来解决此问题(pip freeze
、pip-tools、uv、Poetry 和 PDM),似乎人们普遍对锁定文件有需求。
这些工具在它们支持的锁定场景方面也各不相同。例如,pip freeze
和 pip-tools 仅为当前环境生成锁定文件,而 PDM 和 Poetry 则试图在某种程度上为任何环境锁定。而且,它们都没有直接支持锁定到特定文件以进行安装,这对于某些工作流程来说可能很重要。还存在一些关于在供应链攻击面前缺乏安全默认值的担忧(例如,始终包含文件的哈希值)。最后,并非所有格式都易于审核,以提前确定将在环境中安装什么。
缺乏标准也有一些缺点。例如,任何想要使用锁定文件的工具都必须选择支持哪种格式,这可能会导致用户得不到支持(例如,Dependabot 仅支持选定的工具,云提供商也是如此,它们可以代表您执行依赖项安装等)。它还会影响工具之间的可移植性,从而导致供应商锁定。由于缺乏兼容性和互操作性,它导致围绕锁定文件的工具出现碎片化,用户和工具都必须预先选择使用哪种锁定文件格式,并且切换到其他格式的成本很高。围绕单一格式团结起来消除了这种成本/障碍。
注意
PEP 665 中的许多动机也适用于本 PEP。
基本原理
该格式的设计使得生成锁定文件的锁定工具和使用锁定文件的安装工具可以是独立的工具。这允许云托管提供商使用针对其系统优化的自己的安装程序,而这与用户用于创建锁定文件的锁定工具无关。
该文件格式旨在易于人类阅读。这样,人类就可以审核文件内容,以确保没有不希望的依赖项最终被包含在锁定文件中。它还旨在促进对将从锁定文件中安装什么的简单理解,而无需运行工具,再次有助于审核。最后,该格式的设计使得通过集中相关细节来轻松查看文件的差异。
该文件格式还旨在在安装时不需要解析器。能够在锁定文件中列出时彼此隔离地分析依赖项可以带来一些好处。首先,它通过使确定特定环境是否会安装特定依赖项变得容易(而无需上下文引用文件的其他部分)来支持审核。它还应该能够加快安装速度,安装速度比创建锁定文件要频繁得多。最后,在动机部分中提到的四种工具要么已经实现了这种隔离评估依赖项的方法,要么已经表示它们可以实现(在Poetry 的案例中)。
锁定场景
锁定文件格式旨在支持两种锁定场景。该格式还应该足够灵活,以便可以通过单独的 PEP 添加对其他锁定场景的支持。
每个文件锁定
每个文件锁定基于以下前提:希望在任何匹配的环境中安装完全相同的文件。因此,锁定文件指定要安装的文件。单个文件中可以指定多个环境,每个环境都有自己的一组要安装的文件。通过指定要安装的确切文件,安装程序避免执行任何解析以决定要安装什么。
这种锁定方法的动机是针对那些使用受控环境的人。例如,如果您有特定的受控开发和生产环境,那么您可以使用每个文件锁定来确保每个人都在两个环境中安装相同的文件。这类似于pip freeze
和 pip-tools 支持的功能,但更加严格地要求使用确切的文件,以及合并支持以在同一文件中为多个环境指定锁定文件的功能。
如果目标平台没有明确预先批准的安装工件集,则应使用每个文件锁定,此时安装尝试应彻底失败。例如:锁定托管 Web 服务的部署依赖项。
包锁定
包锁定列出了可能适用于任何要安装的环境的包及其版本。包及其版本的列表是单独评估的,并且独立于文件中列出的任何其他包和版本。这允许安装成为线性的——读取每个包和版本,并独立做出是否应安装的决定。这避免了要求安装程序执行解析(即,根据要安装的其他内容确定要安装的内容)。
这种方法的动机来自PDM 锁定文件。通过列出可能安装的潜在包和版本,以一种易于理解的方式控制了安装的内容。这也允许不指定锁定文件将支持的确切环境,因此与锁定文件兼容的环境具有更大的灵活性。此方法支持以下场景:开源项目希望锁定人们应该用来构建文档的内容,而无需预先知道其贡献者使用的环境。
如前所述,此方法受PDM 支持。Poetry 也表示出了一些兴趣。
当生成锁定文件时不知道潜在目标平台的确切集合时,应使用每个包锁定,因为它允许安装工具从预先批准的集合中为每个平台选择最合适的工件。例如:锁定开源项目的开发依赖项。
规范
文件名
锁定文件必须命名为pylock.toml
,或者如果需要锁定文件的名称或存在多个锁定文件,则必须与正则表达式r"pylock\.(.+)\.toml"
匹配。.toml
文件扩展名的使用是为了使编辑器中的语法高亮显示更容易,并强调该文件格式旨在易于人类阅读。命名文件的开头和结尾必须是小写字母,以便于检测和剥离以查找名称,例如
if filename.startswith("pylock.") and filename.endswith(".toml"):
name = filename.removeprefix("pylock.").removesuffix(".toml")
本 PEP 对锁定文件的位置(即在项目的根目录或子目录中)没有意见。
文件格式
文件格式为TOML。
下面列出的所有键都是必需的,除非另有说明。如果两个键彼此互斥,则其中一个键是必需的,而另一个键是不允许的。
表中的键——包括顶级表——在适用时,应在锁定工具发出时按照本 PEP 中列出的顺序发出,除非指定了其他排序顺序以最大程度地减少差异中的噪音。如果本 PEP 中没有明确指定这些键,则应按字典顺序对这些键进行排序。
同样地,出于同样的原因,锁文件**应该**按字典序对数组进行排序,除非另有说明。
version
- 字符串
- 锁文件格式的版本。
- 本 PEP 指定了初始版本——在标准的未来更新更改它之前,唯一有效的取值是
"1.0"
。 - 如果安装程序支持主版本但不支持次版本,则在遇到未知键时,工具**应该**发出警告。
- 如果安装程序不支持主版本,则**必须**引发错误。
hash-algorithm
- 字符串
- 用于计算所有哈希值的哈希算法的名称。
- 整个文件只使用单个哈希算法,以便
[[packages.files]]
表格能够以内联方式写入,以提高可读性和紧凑性,方法是只列出一个哈希值,而不是基于多个哈希算法列出多个值。 - 指定单个哈希算法可以保证始终如一地使用用户首选的算法,而无需分别审核每个文件的哈希值。
- 允许将整个文件更新到新的哈希算法,而不会冒在文件中意外保留旧哈希值的风险。
- 基于 JSON 的 Python 包索引的简单 API 和项目详细信息字典的
files
字典的hashes
字典指定了哪些值有效以及有关使用哪些哈希算法的指南。 - 未能验证任何要安装的文件的任何哈希值**必须**引发错误。
dependencies
- 字符串数组
- 作为锁文件输入的 依赖项规范 列表,表示要安装的直接的、顶层的依赖项。
[[file-locks]]
- 表格数组
- 与
[package-lock]
互斥。 - 数组的存在意味着使用每个文件的锁定方法。
- 满足表格中所有指定条件的环境将被视为与已锁定环境兼容。
- 锁文件**不得**生成多个
[file-locks]
表格,这些表格将被视为对同一环境兼容。 - 在存在冲突但仍需要锁定的情况下,可以编写单独的锁文件或使用每个包的锁定。
- 数组中的条目**应该**按
file-locks.name
字典序排序。
file-locks.name
- 字符串
- 此表格表示的环境在数组中的唯一名称。
[file-locks.marker-values]
- 可选
- 字符串表格
- 键表示 环境标记 的名称,值是这些标记的值。
- 兼容性由环境的值与表格中的值匹配来定义。
[package-lock]
- 表格
- 与
[[file-locks]]
互斥。 - 表示使用包锁定方法。
package-lock.requires-python
- 字符串
- 包含用于整体包锁定的 Python 版本兼容性的 版本规范。
- 提供一目了然的信息,以了解锁文件**可能**是否适用于 Python 的某个版本,而不必扫描整个文件来编译相同的信息。
[[packages]]
- 表格数组
- 该数组包含所有关于锁定包版本的数据。
- 锁文件**应该**按
packages.name
字典序、packages.version
按 版本规范 的排序顺序以及packages.markers
字典序记录包。 - 锁文件**应该**按本 PEP 中写入的顺序记录键,以最大程度地减少更新时的更改。
- 条目的设计使得包含包的相关详细信息都位于一个位置,以便更容易阅读差异。
packages.name
- 字符串
- 包的 规范化名称。
- 唯一标识此条目的必要部分之一。
packages.version
- 字符串
- 包的版本。
- 唯一标识此条目的必要部分之一。
packages.multiple-entries
- 可选(默认为
false
) - 布尔值
- 如果通过
[package-lock]
进行包锁定,则同一包的多个条目**必须**通过packages.marker
互斥(对于每个文件的锁定不需要这样做,因为packages.*.lock
条目暗示了互斥性)。 - 通过知道存在可能需要考虑的同一包的多个条目来帮助进行审核。
packages.description
- 可选
- 字符串
- 包的来自其 核心元数据 的
Summary
。 - 有助于根据其用途了解为什么将包包含在文件中。
packages.simple-repo-package-url
packages.marker
- 可选
- 字符串
- 指定此包和版本是否适用于环境的 环境标记 表达式。
- 仅适用于
[package-lock]
和包锁定场景。 - 缺少此键意味着需要安装此包和版本。
packages.requires-python
- 可选
- 字符串
- 包含用于包和版本的 Python 版本兼容性的 版本规范。
- 有助于记录为什么将此包和版本包含在文件中。
- 还有助于记录为什么选择
package-lock.requires-python
中的版本限制。 - 它不应为安装程序提供有用的信息,因为它将被
package-lock.requires-python
捕获,并且在使用[[file-locks]]
时不相关。
packages.dependents
- 可选
- 字符串数组
- 依赖于此包和版本的包的记录。
- 用于分析为什么某个包恰好列在文件中,以供审核目的。
- 这不会提供影响安装程序的信息。
packages.dependencies
- 可选
- 字符串数组
- 包和版本的依赖项的记录。
- 用于分析为什么某个包恰好列在文件中,以供审核目的。
- 这不会提供影响安装程序的信息,因为
[[file-locks]]
指定了要使用的确切文件,并且[package-lock]
的适用性由packages.marker
确定。
packages.direct
- 可选(默认为
false
) - 布尔值
- 表示安装是否通过 直接 URL 引用 进行。
[[packages.files]]
- 如果未指定
[packages.vcs]
和[packages.directory]
,则**必须**指定此项(尽管可以与其他选项同时指定)。 - 表格数组
- 表格可以内联写入。
- 表示要为包和版本潜在安装的文件。
[[packages.files]]
中的条目**应该**按packages.files.name
键字典序排序,以最大程度地减少差异中的更改。
packages.files.name
- 字符串
- 文件名。
- 在使用包锁定时,安装程序必须决定安装什么。
packages.files.lock
- 在使用
[[file-locks]]
时是必需的(不适用于每个包的锁定)。 - 字符串数组
file-locks.name
值数组,表示当相应的[[file-locks]]
表格适用于环境时,要安装此文件。- 每个包中,任何一个
file-locks.name
条目**必须**只有一个文件,无论版本如何。
packages.files.simple-repo-package-url
packages.files.origin
- 可选
- 字符串
- 生成锁文件时找到文件的 URI。
- 如果 URI 是相对文件路径,则它被视为相对于锁文件。
- 有助于记录文件最初在哪里找到,以及如果文件尚未下载/可用,则可能在哪里查找文件。
- 安装程序**不得**假设 URI 将始终有效,但安装程序**可以**在 URI 恰好有效时使用它。
packages.files.hash
- 字符串
- 使用
hash-algorithm
指定的哈希算法计算的文件内容的哈希值。 - 安装程序用它来验证文件内容是否与锁文件使用的内容匹配。
[packages.vcs]
- 如果未指定
[[packages.files]]
和[packages.directory]
,则**必须**指定此项(尽管可以与其他选项同时指定)。 - 表示包含包和版本的版本控制系统的表格。
packages.vcs.type
- 字符串
- 使用的版本控制系统的类型。
- 有效值由直接 URL 数据结构的注册的 VCS 指定。
packages.vcs.origin
- 字符串
- 生成锁定文件时存储库所在的 URI。
packages.vcs.commit
- 字符串
- 表示包和版本的存储库的提交 ID。
- 出于安全目的,该值对于 VCS 必须是不可变的(例如,没有 Git 标签)。
packages.vcs.lock
- 当使用
[[file-locks]]
时需要。 - 字符串数组。
- 一个
file-locks.name
值数组,表示当相应的[[file-locks]]
表应用于环境时,应安装指定提交处的存储库。 - 仅当
packages.files.lock
中列出的任何文件都不包含相同包的名称(无论版本如何)时,数组中的名称才能出现。
[packages.directory]
- 如果未使用
[[packages.files]]
和[packages.vcs]
并且正在执行每个包锁定,则必须指定。 - 表示在本地文件系统上找到的源树的表。
packages.directory.path
- 字符串
- 包和版本存在源树的本地目录。
- 路径必须使用正斜杠作为路径分隔符。
- 如果路径是相对路径,则相对于锁定文件的位置。
packages.directory.editable
- 布尔值
- 可选(默认为
false
) - 表示源树是否应作为可编辑安装安装的标志。
[[packages.build-requires]]
- 可选
- 一个表的数组,其结构与
[[packages]]
的结构匹配。 - 每个条目表示在构建封闭包和版本时要使用的包和版本。
- 该数组与
[[packages]]
本身一样完整/锁定(即,安装程序对[[packages.build-requires]]
遵循与[[packages]]
相同的安装过程)。 - 选择要用于环境的条目,与
[[packages]]
本身相同,只不过仅在安装构建后端及其依赖项时应用。 - 这有助于通过记录如果锁需要构建包将使用什么或将会使用什么来提高包构建的可重复性。
- 如果安装程序和用户选择从源代码安装并且此数组丢失,则安装程序可以选择在安装时解决要安装的内容以进行构建,否则安装程序必须引发错误。
[packages.tool]
- 可选
- 表格
- 与pyproject.toml 规范 中
[tool]
表的用法类似,但在包版本级别而不是在锁定文件级别(也可以通过[tool]
获取)。 - 对于限定包版本/发行版详细信息很有用(例如,记录签名标识以然后用于验证包完整性,独立于包托管位置,为该文件格式的未来扩展原型设计等)。
[tool]
- 可选
- 表格
- 与pyproject.toml 规范 中等效的
[tool]
表的用法相同。
示例
每个文件锁定
version = '1.0'
hash-algorithm = 'sha256'
dependencies = ['cattrs', 'numpy']
[[file-locks]]
name = 'CPython 3.12 on manylinux 2.17 x86-64'
marker-values = {}
wheel-tags = ['cp312-cp312-manylinux_2_17_x86_64', 'py3-none-any']
[[file-locks]]
name = 'CPython 3.12 on Windows x64'
marker-values = {}
wheel-tags = ['cp312-cp312-win_amd64', 'py3-none-any']
[[packages]]
name = 'attrs'
version = '23.2.0'
multiple-entries = false
description = 'Classes Without Boilerplate'
requires-python = '>=3.7'
dependents = ['cattrs']
dependencies = []
direct = false
files = [
{name = 'attrs-23.2.0-py3-none-any.whl', lock = ['CPython 3.12 on manylinux 2.17 x86-64', 'CPython 3.12 on Windows x64'], origin = 'https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl', hash = '99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1'}
]
[[packages]]
name = 'cattrs'
version = '23.2.3'
multiple-entries = false
description = 'Composable complex class support for attrs and dataclasses.'
requires-python = '>=3.8'
dependents = []
dependencies = ['attrs']
direct = false
files = [
{name = 'cattrs-23.2.3-py3-none-any.whl', lock = ['CPython 3.12 on manylinux 2.17 x86-64', 'CPython 3.12 on Windows x64'], origin = 'https://files.pythonhosted.org/packages/b3/0d/cd4a4071c7f38385dc5ba91286723b4d1090b87815db48216212c6c6c30e/cattrs-23.2.3-py3-none-any.whl', hash = '0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108'}
]
[[packages]]
name = 'numpy'
version = '2.0.1'
multiple-entries = false
description = 'Fundamental package for array computing in Python'
requires-python = '>=3.9'
dependents = []
dependencies = []
direct = false
files = [
{name = 'numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl', lock = ['cp312-manylinux_2_17_x86_64'], origin = 'https://files.pythonhosted.org/packages/2c/f3/61eeef119beb37decb58e7cb29940f19a1464b8608f2cab8a8616aba75fd/numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl', hash = '6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a'},
{name = 'numpy-2.0.1-cp312-cp312-win_amd64.whl', lock = ['cp312-win_amd64'], origin = 'https://files.pythonhosted.org/packages/b5/59/f6ad30785a6578ad85ed9c2785f271b39c3e5b6412c66e810d2c60934c9f/numpy-2.0.1-cp312-cp312-win_amd64.whl', hash = 'bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171'}
]
每个包锁定
省略了一些packages.files.origin
的值,以便更容易地手动创建此示例。
version = '1.0'
hash-algorithm = 'sha256'
dependencies = ['cattrs', 'numpy']
[package-lock]
requires-python = ">=3.9"
[[packages]]
name = 'attrs'
version = '23.2.0'
multiple-entries = false
description = 'Classes Without Boilerplate'
requires-python = '>=3.7'
dependents = ['cattrs']
dependencies = []
direct = false
files = [
{name = 'attrs-23.2.0-py3-none-any.whl', lock = ['cp312-manylinux_2_17_x86_64', 'cp312-win_amd64'], origin = 'https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl', hash = '99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1'}
]
[[packages]]
name = 'cattrs'
version = '23.2.3'
multiple-entries = false
description = 'Composable complex class support for attrs and dataclasses.'
requires-python = '>=3.8'
dependents = []
dependencies = ['attrs']
direct = false
files = [
{name = 'cattrs-23.2.3-py3-none-any.whl', lock = ['cp312-manylinux_2_17_x86_64', 'cp312-win_amd64'], origin = 'https://files.pythonhosted.org/packages/b3/0d/cd4a4071c7f38385dc5ba91286723b4d1090b87815db48216212c6c6c30e/cattrs-23.2.3-py3-none-any.whl', hash = '0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108'}
]
[[packages]]
name = 'numpy'
version = '2.0.1'
multiple-entries = false
description = 'Fundamental package for array computing in Python'
requires-python = '>=3.9'
dependents = []
dependencies = []
direct = false
files = [
{name = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"},
{name = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"},
{name = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"},
{name = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"},
{name = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"},
{name = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"},
{name = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"},
{name = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"},
{name = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"},
{name = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"},
]
对锁定工具的期望
- 为
[package-lock]
创建锁定文件时,锁应该读取最终列在[[packages.files]]
中的**所有**文件的元数据,以确保涵盖所有潜在的元数据情况。 - 如果锁选择不检查每个文件的元数据,则该工具必须要么为用户提供检查所有文件的选项(无论这是选择加入还是选择退出都由工具决定),要么以某种方式通知用户正在采取这种违反标准的快捷方式(无论这是通过文档还是在运行时都由工具决定)。
- 锁可能希望提供一种方法,让用户在执行每个文件锁定时一次为多个环境提供安装所需的信息,例如,支持 JSON 文件格式,该格式指定轮标签和标记值,类似于
[[file-locks]]
中的格式,其中可以指定多个文件,然后可以直接记录在相应的[[file-locks]]
表中(如果它允许明确的每个文件锁定环境选择)。
{
"marker-values": {"<marker>": "<value>"},
"wheel-tags": ["<tag>"]
}
对安装工具的期望
- 安装程序可以支持非二进制文件的安装(即源分发、源树和 VCS),但不是必须的。
- 安装程序必须提供一种方法来避免非二进制文件安装,以确保可重复性和安全性。
- 安装程序应该使使用非二进制文件安装成为选择加入,以促进默认的安全方法。
- 在每个文件锁定下,如果要安装的内容不明确,则安装程序必须引发错误。
安装用于每个文件锁定
- 如果找不到兼容的环境,则必须引发错误。
- 如果发现多个环境兼容,则必须引发错误。
- 如果
[[packages.files]]
包含多个匹配的条目,则必须引发错误,因为要安装的内容不明确。 - 如果相同包的多个
[[packages]]
条目具有匹配的文件,则必须引发错误,因为要安装的内容不明确。
示例工作流程
- 遍历每个
[[file-locks]]
表以找到适用于要安装的环境的表。 - 如果找不到兼容的环境,则必须引发错误。
- 如果发现多个环境兼容,则必须引发错误。
- 对于兼容的环境,遍历
[[packages]]
中的每个条目。 - 对于每个
[[packages]]
条目,遍历[[packages.files]]
以查找任何在packages.files.lock
中列出file-locks.name
的文件。 - 如果找到具有匹配锁定名称的文件,则将其添加到要安装的候选文件列表中,并继续处理下一个
[[packages]]
条目。 - 如果未找到文件,则检查
packages.vcs.lock
是否包含匹配项(不匹配也可以接受)。 - 如果
[[packages.files]]
包含多个匹配的条目,则必须引发错误,因为要安装的内容不明确。 - 如果相同包的多个
[[packages]]
条目具有匹配的文件,则必须引发错误,因为要安装的内容不明确。 - 根据其哈希值或提交 ID(视情况而定)查找并验证候选文件和/或 VCS 条目。
- 如果选择了源分发或 VCS 并且存在
[[packages.build-requires]]
,则根据需要重复上述过程以安装构建包所需的构建依赖项。 - 安装候选文件。
安装用于包锁定
- 验证环境是否与
package-lock.requires-python
兼容;如果不兼容,则必须引发错误。 - 如果找不到安装所需包的方法,则必须引发错误。
示例工作流程
- 验证环境是否与
package-lock.requires-python
兼容;如果不兼容,则必须引发错误。 - 遍历
[packages]]
中的每个条目。 - 对于每个条目,如果存在
packages.marker
键,则评估表达式。- 如果表达式为假,则继续。
- 否则必须以某种方式安装包条目。
- 遍历
[[packages.files]]
中列出的文件,查找要安装的“最佳”文件。 - 如果未找到文件,则检查
[packages.vcs]
。 - 如果未找到 VCS,则检查
packages.directory
。 - 如果未找到匹配项,则必须引发错误。
- 根据其哈希值或提交 ID(视情况而定)查找并验证选定的文件和/或 VCS 条目。
- 如果匹配项是源分发或 VCS 并且提供了
[[packages.build-requires]]
,则根据需要重复上述操作以构建包。 - 安装选定的文件。
向后兼容性
由于没有预先存在的锁定文件格式,因此在 Python 打包标准方面没有明确的向后兼容性问题。
至于打包工具本身,这将是每个工具的决定。对于没有记录其锁定文件格式的工具,它们可以选择简单地在内部开始使用该格式,然后过渡到使用此 PEP 支持的名称保存其锁定文件。对于具有预先存在的已记录格式的工具,它们可以提供一个选项来选择要输出的格式。
安全隐患
希望通过标准化一种从安全优先的立场开始的锁定文件格式,这将有助于使整体打包安装更安全。但是,此 PEP 并没有解决所有潜在的安全问题。
一个潜在的担忧是篡改锁定文件。如果锁定文件未保存在源代码控制中并且未正确审计,则恶意行为者可能会以恶意方式更改文件(例如,指向软件包的恶意软件版本)。篡改也可能在传输到例如将代表用户执行安装的云提供商的过程中发生。两者都可以通过在[tool]
条目中的文件中或通过锁定文件本身外部的侧信道对锁定文件进行签名来缓解。
此 PEP 不会做任何事情来阻止用户安装不正确的软件包。虽然包含许多有助于审计软件包包含的详细信息,但没有任何机制可以阻止例如通过错别字攻击进行的名称混淆。锁可以提供一些用户体验来帮助解决此问题(例如,通过为软件包提供下载次数)。
如何教授
应告知用户,当他们请求安装某个软件包时,该软件包可能具有其自身的依赖项,这些依赖项可能具有依赖项,依此类推。如果没有写下作为安装他们请求的软件包的一部分而安装的内容,那么事情可能会从他们下面发生变化(例如,软件包版本)。对底层依赖项的更改可能导致其代码意外中断。锁定文件通过提供一种记录安装内容的方法来帮助解决此问题。
将要安装的内容写下来也有助于与他人协作。通过同意锁定文件的内容,每个人最终都会安装相同的软件包。这有助于确保没有人依赖例如仅在特定版本中可用的 API,而该版本并非项目中的每个人都已安装。
锁定文件还有助于安全性,确保您始终安装相同的文件,而不是某人可能插入的恶意文件。它还可以让人们更谨慎地升级其依赖项,从而确保更改是有意的,而不是由恶意行为者插入的更改。
参考实现
可以在https://github.com/brettcannon/mousebender/tree/pep 中找到每个文件锁定的粗略概念证明。可以在https://github.com/brettcannon/mousebender/blob/pep/pylock.example.toml 中看到一个示例锁定文件。
对于每个包锁定,PDM 间接证明了该方法有效,因为此 PEP 保留与 PDM 对其锁定文件(其格式受Poetry 启发)所做的数据相同。PDM 方法的一些详细信息在https://frostming.com/en/2024/pdm-lockfile/ 和https://frostming.com/en/2024/pdm-lock-strategy/ 中进行了介绍。
被拒绝的想法
仅支持包锁定
在某个阶段,有人建议跳过每个文件的锁定,只支持包锁定,因为前者在更大的 Python 生态系统中没有明确的支持,而后者则有。但由于本 PEP 认为安全很重要,并且每个文件的锁定是两种选项中更安全的,因此从未考虑过省略每个文件的锁定。
指定一个新的核心元数据版本,该版本要求文件之间具有一致的元数据
在某个阶段,为了处理文件之间元数据不同的问题,因此需要检查每个发布的文件以获取包和版本以获得准确的锁定结果,有人提出了引入一个新的核心元数据版本的思路,该版本要求单个版本的包的所有 wheel 文件的所有元数据都相同。然而,最终认定这是不必要的,因为本 PEP 将会促使人们出于性能原因或让索引提供与 wheel 文件本身分开的元数据,使文件保持一致。此外,没有简单的执行机制,因此社区期望也能与新的元数据版本一样有效。
让安装程序执行依赖项解析
为了支持更类似于 PEP 起草时 Poetry 工作方式的格式,有人建议锁定器有效地记录可能需要让安装在任何可能的情况下都能工作的包及其版本,然后安装程序解析要安装的内容。但这会使审计锁定文件变得复杂,因为需要付出更多脑力才能知道在任何给定情况下可能安装哪些包。此外,一位 Poetry 开发者建议,本 PEP 的包锁定方法中表示的标记可能足以满足 Poetry 的需求。不让安装程序进行解析还可以简化它们的实现,将复杂性集中在锁定器中。
要求特定的哈希算法支持
有人提议要求为文件设置基线哈希算法。这被拒绝了,因为没有其他 Python 打包规范需要特定的哈希算法支持。此外,建议的最小哈希算法最终可能会变得过时/不安全,需要进一步更新。为了促进始终使用最佳算法,没有提供基线,以避免工具在不考虑该哈希算法的安全影响的情况下简单地默认为基线。
文件名
使用 *.pylock.toml
作为文件名
有人提议将pylock
文件名的常量部分放在标识符之后,用于锁定文件。决定不这样做,以便在查看目录内容时锁定文件可以一起排序,而不是仅仅根据其用途排序,这可能会在目录中分散它们。
使用 *.pylock
作为文件名
有人提议不使用.toml
作为文件扩展名,而是将其本身设为.pylock
。最终决定不这样做,以便代码编辑器能够知道如何为锁定文件提供语法高亮显示,而无需了解文件扩展名的特殊知识。
没有文件名的命名约定
考虑过不为锁定文件名称设置任何要求或指南,但最终被否决了。通过使用标准化的命名约定,可以让人和代码编辑器都轻松识别锁定文件。这有助于在例如工具想要知道所有可用锁定文件时促进发现。
文件格式
使用 JSON 而不是 TOML
由于本 PEP 的目标之一是创建机器可写的格式,有人建议使用 JSON。但它被认为不如 TOML 易于人类阅读,同时在机器可写方面也没有足够的改进以证明更改的合理性。
使用 YAML 而不是 TOML
有些人认为 YAML 比 TOML 更好地满足了机器可写/人类可读的要求。但由于这主观性很强,并且pyproject.toml
已经作为 Python 打包标准使用的人类可写文件存在,因此认为继续使用 TOML 更重要。
其他键
每个文件多个哈希值
本 PEP 的初始版本提议支持每个文件多个哈希值。其想法是允许用户在安装时选择他们想要的哈希算法。但经过反思,这似乎是一个不必要的复杂化,因为无法保证提供的哈希值能满足用户的需求。此外,如果锁定文件中使用的单个哈希算法不足,重新哈希相关文件作为迁移到不同算法的方法似乎并非不可克服。
对锁定文件本身的内容进行哈希运算
有人在某个阶段提议对文件字节的内容进行哈希,并将哈希值存储在文件本身中。这被删除了,以便在合并对锁定文件的更改时更容易,因为每次合并都必须重新计算哈希值以避免合并冲突。
也有人提议对文件的语义内容进行哈希,但这会导致相同的合并冲突问题。
无论对哪些内容进行哈希,如果需要这种哈希,这两种方法都可以将哈希值存储在文件外部。
记录锁定文件的创建日期
为了了解锁定文件可能有多陈旧,早期的提案建议记录锁定文件的创建时间。但由于与存储文件内容的哈希值相同的合并冲突原因,这个想法被放弃了。
记录使用的包索引
考虑过记录锁定器用来决定锁定什么的包索引。但最终被拒绝了,因为它被认为是不必要的簿记。
未解决的问题
N/A
致谢
感谢所有参与https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593/讨论的人,特别是 Alyssa Coghlan,她可能是导致初始提案发生最大结构变化的人。
还要感谢 Randy Döring、Seth Michael Larson、Paul Moore 和 Ofek Lev 对本 PEP 的草稿版本提供反馈。
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可证,以两者中更宽松的为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0751.rst
上次修改时间:2024-08-20 10:29:32 GMT