PEP 516 – pip/conda 等的构建系统抽象
- 作者:
- Robert Collins <rbtcollins at hp.com>, Nathaniel J. Smith <njs at pobox.com>
- BDFL 委托:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 讨论至:
- Distutils-SIG 邮件列表
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2015年10月26日
- 决议:
- Distutils-SIG 消息
摘要
本 PEP 规定了一个程序接口,供 pip [1] 和其他分发或安装工具在处理 Python 源代码树(包括开发者树,例如 git 树,以及源代码分发)时使用。
该程序接口允许 pip 从其当前对 setuptools 的硬性依赖中解耦,[2] 这主要有以下两个原因:
- 它支持新的构建系统,这些系统可能更容易使用,而无需它们看起来像是 setuptools。
- 它有助于 setuptools 本身更改其用户界面而不破坏 pip,从而实现更松散的耦合。
允许 pip 安装构建系统的接口也使得 pip 能够安装包的构建时依赖项,这是 pip 实现与 easy-install 的安装组件完全功能对等的重要一步。
由于 PEP 426 尚处于草稿阶段,我们无法使用其定义的元数据格式。但是 PEP 427 wheel 已经被广泛使用并且相当规范,因此我们采纳了其 METADATA 格式来指定分发依赖项和通用项目元数据。PEP 508 提供了一种独立的语言来描述依赖项,我们将其封装在一个轻量级的 JSON 模式中,用于描述引导依赖项。
由于 PEP 314 中指定的 Python sdist 也是源代码树,因此本 PEP 更新了 sdist 的定义。
PEP 驳回
本 PEP 中提出的 CLI 方法已被否决,转而采用 PEP 517 中提出的 Python API 方法。用于与作为独立子进程运行的构建后端通信的特定 CLI 将被视为前端开发者工具实现的一个实现细节。
动机
Python 打包生态系统对当前构建系统和 pip 之间的锁定存在着巨大的不满。打破这种锁定对 pip、setuptools 以及 flit [3] 等其他构建系统都有好处。
规范
概述
构建工具将通过读取源代码树根目录下的 pypa.json 文件来定位。该文件描述了如何获取构建工具以及调用该工具的命令名称。
所有工具都应遵循一个模仿 pip 当前使用 setuptools setup.py 接口的单一命令行接口。
pypa.json
文件 pypa.json 作为 pip 和其他想要构建源代码树的工具的通用配置文件,用于咨询配置。Python 源代码树中缺少 pypa.json 文件意味着使用 setuptools 或兼容 setuptools 的构建系统。
JSON 具有以下模式。额外的键将被忽略,这允许使用 pypa.json 作为其他相关工具的配置文件。如果是这样做,则选择的键必须在 tools 下进行命名空间隔离。
{"tools": {"flit": ["Flits content here"]}}
- schema
- 模式的版本。本 PEP 定义版本“1”。如果不存在,则默认为“1”。所有读取文件的工具都必须在遇到无法识别的模式版本时报错。
- bootstrap_requires
- 可选的 PEP 508 依赖项规范列表,必须在运行构建工具之前安装。例如,如果使用 flit,那么依赖项可能是:
bootstrap_requires: ["flit"]
- build_command
- 一个强制性的键,这是一个描述要运行的命令的 Python 格式字符串 [8] 列表。例如,如果使用 flit,那么构建命令可能是:
build_command: ["flit"]
如果使用一个可运行模块 fred 作为命令
build_command: ["{PYTHON}", "-m", "fred"]
进程接口
要运行的命令由一个简单的 Python 格式字符串 [8] 定义。
这允许拥有专用脚本的构建系统以及通过“python -m somemodule”调用的构建系统。
进程将以源代码树根目录作为当前工作目录运行。
运行时,进程不应从 stdin 读取 - 虽然 pip 当前运行构建系统时 stdin 连接到其自身的 stdin、stdout 和 stderr,但 stdout 和 stderr 被重定向,无法与用户进行任何通信。
与进程一贯的做法一样,非零退出状态表示错误。
可用的格式变量
- PYTHON
- 正在使用的 Python 解释器。这对于调用仅仅是 Python 入口点的东西很重要。{PYTHON} -m foo
可用的环境变量
这些变量由构建系统的调用者设置,并且始终可用。
- PATH
- 标准的系统路径。
- PYTHON
- 与格式变量相同。
- PYTHONPATH
- 用于根据正常的 Python 机制控制 sys.path。
子命令
构建系统必须支持一些单独的子命令。下面的示例使用 flit 作为 build_command 以便说明。
- build_requires
- 查询构建依赖项。构建依赖项以 UTF-8 编码的 JSON 文档返回,其中有一个键
build_requires,包含一个 PEP 508 依赖项规范列表。其他键将被忽略。build_requires 命令是唯一在不设置构建环境的情况下运行的命令。示例命令
flit build_requires
- metadata
- 查询项目元数据。元数据(仅元数据)应以 UTF-8 编码输出到 stdout。pip 将仅运行一次 metadata 命令,以确定需要下载和安装哪些其他包。元数据按照 PEP 427 以 wheel METADATA 文件的形式输出。
请注意,metadata 命令生成的元数据以及生成的 wheel 中的元数据必须相同。
示例命令
flit metadata
- wheel -d OUTPUT_DIR
- 用于构建项目 wheel 的命令。OUTPUT_DIR 将指向一个现有的目录,wheel 将在此目录中输出。Stdout 和 stderr 没有语义意义。应该只输出一个文件 - 如果输出更多文件,pip 将选择一个任意文件进行消耗。
示例命令
flit wheel -d /tmp/pip-build_1234
- develop [–prefix PREFIX]
- 用于对项目进行就地“开发”安装的命令。Stdout 和 stderr 没有语义意义。
并非所有构建系统都能执行开发安装。如果构建系统无法执行开发安装,则在运行时应报错。请注意,这样做会导致像
pip install -e foo这样的操作失败。prefix 选项用于定义安装的替代前缀。虽然 setuptools 有
--root和--user选项,但它们可以通过--prefix等效地实现,而接受--root或--user选项的 pip 或其他工具应进行适当的转换。root 选项用于定义命令应在其操作的替代根目录。
例如
flit develop --root /tmp/ --prefix /usr/local
应该在
/tmp/usr/local/bin内安装脚本,即使正在使用的 Python 环境报告 sys.prefix 为/usr/,这将导致使用/tmp/usr/bin/。类似的逻辑也适用于包文件等。
构建环境
除了 build_requires 命令之外,所有命令都在构建环境中运行。不要求特定的实现,但构建环境必须满足以下要求。
- 项目 build_requires 指定的所有依赖项必须可以从
$PYTHON中导入。
- 由构建依赖包提供的所有命令行脚本必须存在于
$PATH中。
由此得出,构建系统不能假定可以访问任何未声明为 build_requires 或在 Python 标准库中的 Python 包。
隔离构建
本规范不规定构建是否应该是隔离的。现有的构建工具(如 setuptools)将使用已安装的构建时依赖项(例如 setuptools_scm),并且仅在版本冲突或依赖项缺失时才安装其他版本。然而,通过始终隔离构建并仅使用指定的依赖项,很可能会获得更好的一致性。
然而,这其中存在一些细微的问题——例如,用户如何强制避免一个满足某些包依赖项但存在问题的构建依赖项版本。未来的 PEP 可能会解决这个问题,但目前不在范围内——它不影响协调构建系统和需要进行构建的实体之间所需的元数据,因此不是 PEP 的范畴。
升级
‘pypa.json’ 进行了版本化,以允许未来的更改而无需兼容性。
在新 PEP 中升级任一模式的顺序将是:
- 发布新的 PEP,定义更新的模式。如果模式不是完全向后兼容的,则必须定义一个新的版本号。
- 消费者(例如 pip)实现对新模式版本的支持。
- 当包作者乐于引入对引入了新模式版本支持的“pip”(以及其他可能的消费者)版本的依赖时,他们可以选择采用新模式。
本 PEP 的初始部署将经历相同的过程:能够使用本 PEP 而无需 setuptools 包装器 的能力,将在很大程度上取决于第一个支持它的 pip 版本的采用率。
sdist 中的静态元数据
本 PEP 不处理当前无法信任 sdist 中静态元数据的问题。这是一个独立于识别和消费源代码树中使用的构建系统的问题,无论它来自 sdist 还是其他来源。
编译器选项的处理
不同编译器选项的处理超出了本规范的范围。
pip 当前通过将用户提供的字符串附加到其运行时命令上来处理编译器选项,当运行 setuptools 时。这种方法足以与本 PEP 中定义的构建系统接口配合使用,但有一个例外:随着不同构建系统的发展,全局指定的选项将不再全局生效。这个问题可以在 pip(或 conda 或其他安装程序)中解决,而不会影响互操作性。
从长远来看,wheel 应该能够表达用一个编译器或选项与另一个编译器或选项构建的 wheel 之间的区别,这属于 PEP 的范畴。
示例
一个使用 flit 的‘pypa.json’示例
{"bootstrap_requires": ["flit"],
"build_command": "flit"}
当 ‘pip’ 读取此文件时,它将准备一个包含 flit 的环境,然后再尝试使用 flit。
由于 flit 当前不支持 setup-requires,flit build_requires 只会输出一个常量字符串。
{"build_requires": []}
flit metadata 将查询 flit.ini,并将元数据打包成 wheel METADATA 文件,然后输出到 stdout。
flit wheel 需要接受一个 -d 参数,告知它 wheel 的输出位置(pip 需要这个)。
向后兼容性
旧版本的 pip 将无法处理替代构建系统。这与现状无异——并且各个构建系统项目可以决定是否包含一个 setup.py 包装器。
所有能够生成 wheel 和执行开发安装的现有构建系统都应该能够在该抽象下运行,并且只需要为它们构建并在 PyPI 上发布一个特定的适配器。
在没有 pypa.json 文件的情况下,pip 等工具应该假定使用 setuptools 构建系统,并直接使用 setuptools 命令。
网络效应
采用非 setuptools 兼容构建系统的项目——即没有 setup.py,或者 setup.py 不接受现有工具尝试使用的命令——将无法被这些现有工具安装。
当这些项目被其他项目使用时,这种影响将级联。
特别是,由于 pip 当前不支持 setup-requires,任何采用非 setuptools 兼容构建系统并被第二个项目(B)作为 setup-requirement 使用的项目(A),而项目 B 本身尚未迁移到拥有 pypa.json,那么 B 将无法被任何版本的 pip 安装。这是因为在 pip 运行 B 的 ‘setup.py egg_info’ 时会触发 easy-install,而 easy-install 会尝试安装 A 但会失败。
因此,我们建议目前被用作 setup-requires 的工具,要么确保它们保留一个 setuptools 包装器,要么找到它们的消费者并让它们在自身迁移到使用 pypa.json 之前全部升级。实际上这是不可能的,因此建议无限期地保留一个 setuptools 包装器——无论是对于 pbr、setuptools_scm 等项目,还是像 numpy 这样的项目。
setuptools 包装器
可以编写一个通用的 setuptools 包装器,它看起来像 setup.py,并在底层使用 pypa.json 来驱动构建。这对 pip 使用该系统不是必需的,但它将允许包作者使用新功能,同时仍然与旧版本的 pip 保持兼容。
基本原理
本 PEP 源于 distutils-sig 上的一个长篇邮件列表讨论 [6]。在此之后,在线举行了一个会议来调试所有参与者的立场。会议记录被发布到列表中 [7]。
本规范是将那里达成的共识转化为 PEP 形式,并对一些次要的遗留问题做出了一些任意的选择。
该设计的基本启发式方法是专注于引入一个抽象,而无需开发与该抽象严格无关的功能。当改进的差距很小时,或者使用现有接口的成本很高时,我们就将改进作为依赖项,否则就将其推迟到未来的迭代。
我们选择 wheel METADATA 文件而不是定义一个新规范,因为 pip 已经可以处理 wheel .dist-info 目录,这些目录以 METADATA 文件形式编码所有必需的数据。PEP 426 不能使用,因为它仍然是草稿,而定义一个新的元数据格式(尽管我们应该这样做)是一个单独的问题。在磁盘上使用目录不会为接口增加任何价值(由于 setuptools CLI 的限制,pip 今天必须这样做)。
使用‘develop’作为命令是因为没有 PEP 规范‘setuptools develop’所做的事情的互操作性——因此在 pip 承担‘develop’步骤的责任之前,我们需要定义它。一旦完成,我们就可以发布本 PEP 的后续 PEP。
使用命令行 API 而非 Python API 存在一些争议。从根本上说,任何东西都可以实现,而 pip 的维护者强烈主张保留基于进程的接口——这是 pip 当前成熟且稳健的东西。
选择 JSON 作为文件格式是权衡了几个约束的结果。首先,标准库中没有 YAML 解释器,也没有任何其他低摩擦的结构化文件格式的解释器。其次,INIParser 由于多种原因是一个糟糕的格式,主要是因为它结构非常有限——而且 pip 的维护者不喜欢它。JSON 在标准库中,具有足够的结构,允许我们将来嵌入任何我们想要的内容,而无需嵌入式 DSL。
Donald 建议使用 setup.cfg 和现有的 setuptools 命令行,而不是发明新的东西。虽然这可以实现更少可见的更改的互操作性,但它在 pip 端需要几乎同样多的工程工作——在 setup.cfg 中查找新键,在非安装环境中实现构建。而其他构建系统作者不希望通过提供看起来像 setuptools 但行为完全不同的东西来混淆他们的用户,这似乎是一个比 pip 学习如何调用自定义构建工具更大的问题。
metadata 和 wheel 命令需要具有一致的元数据,以避免可能发生的竞争条件,即 pip 读取元数据,然后根据元数据执行操作,最后生成的 wheel 具有不兼容的依赖项。今天,使用 PEP 426 环境标记的包会利用这种竞争条件,以便与不支持环境标记的旧版本 pip 一起工作。使用本 PEP 不需要这种利用,因为要么使用了 setuptools 包装器(对于旧版本 pip),要么使用了支持环境标记的 pip。setuptools 包装器可以处理旧版本 pip 所需的利用差异。
我们讨论了是否提供 sdist verb。这主要是为了确保构建系统能够生成 pip 可以构建的 sdist——但这有点循环:本 PEP 的全部意义在于让 pip 能够可靠地消费 sdist 或 VCS 源代码树,而无需实现 setuptools。从现有源代码树创建新的 sdist 是 pip 今天不做的事情,尽管有一个 PR 在从源代码构建的部分中这样做,但它存在争议且缺乏共识。我们不打算对所有构建系统施加要求,而是将其视为 YAGNI(你不需要它),并在将来需要时在此接口的后续版本中添加这样的 verb。sdist 的现有 PEP 314 要求仍然适用,distutils 或 setuptools 用户可以使用 setup.py sdist 来创建 sdist。其他工具应创建与 PEP 314 兼容的 sdist。请注意,pip 本身不需要 PEP 314 兼容性 - 它不使用 sdist 中的任何元数据 - 它们被视为来自磁盘或版本控制的源代码树。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0516.rst
最后修改: 2025-02-01 08:55:40 GMT