PEP 668 – 将 Python 基础环境标记为“外部管理”
- 作者:
- Geoffrey Thomas <geofft at ldpreload.com>,Matthias Klose <doko at ubuntu.com>,Filipe Laíns <lains at riseup.net>,Donald Stufft <donald at stufft.io>,Tzu-ping Chung <uranusjr at gmail.com>,Stefano Rivera <stefanor at debian.org>,Elana Hashman <ehashman at debian.org>,Pradyun Gedam <pradyunsg at gmail.com>
- PEP 代理:
- Paul Moore <p.f.moore at gmail.com>
- 讨论邮件列表:
- Discourse 帖子
- 状态:
- 已接受
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建:
- 2021年5月18日
- 历史记录:
- 2021年5月28日
- 决议:
- Discourse 消息
摘要
长期以来,Python 用户面临的一个实际问题是操作系统包管理器与 Python 特定包管理工具(如 pip)之间的冲突。这些冲突包括 Python 级别的 API 不兼容性和文件所有权冲突。
历史上,Python 特定包管理工具默认将包安装到隐式的全局上下文中。随着虚拟环境的标准化和普及,对于大多数(但不是全部)用例来说,更好的解决方案是在虚拟环境中仅使用 Python 特定包管理工具。
本 PEP 提出了一种机制,允许 Python 安装向 pip 等工具传达其全局包安装上下文由某些外部手段(例如操作系统包管理器)管理。它规定,默认情况下,Python 特定包管理工具不应在解释器的全局上下文中安装或删除包,而应引导最终用户使用虚拟环境。
它还标准化了 sysconfig
方案的解释,以便如果 Python 特定包管理器即将在解释器范围的上下文中安装包,它可以以避免与外部包管理器冲突的方式进行,并降低破坏外部包管理器提供的软件的风险。
术语
本 PEP 中使用的几个术语在其所涵盖的上下文中具有多种含义。为了清楚起见,本 PEP 以特定的方式使用以下术语
- 发行版
- 简称“distribution”,是一组各种类型的软件,理想情况下旨在协同工作,包括(在本文档相关的上下文中)Python 解释器本身、用 Python 编写的软件以及用其他语言编写的软件。也就是说,这是在诸如“Linux 发行版”或“Berkeley 软件发行版”之类的短语中使用的含义。
发行版可以是其自身的操作系统(OS),例如 Debian、Fedora 或 FreeBSD。它也可以是在现有操作系统上安装的覆盖发行版,例如 Homebrew 或 MacPorts。
本文档使用简称“发行版”,因为术语“distribution”在 Python 打包上下文中还有另一种含义:单个 Python 语言软件的源代码或二进制发行版包,即
setuptools.dist.Distribution
或“sdist”的含义。为避免混淆,本文档根本不使用简单的术语“distribution”。在 Python 打包意义上,它使用完整的短语“distribution package”或仅“package”(见下文)。发行版提供者 - 收集和发布软件并进行任何必要修改的团队或公司 - 是其**分发者**。
- 包
- 可以在 Python 中安装和使用的软件单元。也就是说,这指的是 Python 特定打包工具倾向于称之为“distribution package”或简称“distribution”;口语化的缩写“package”是在 Python 包索引的意义上使用的。
本文档不使用“package”来指代包含 Python 模块的可导入名称,尽管在许多情况下,distribution package 由具有相同名称的单个可导入包组成。
本文档通常不使用术语“package”来指代发行版包管理器(例如
.deb
或.rpm
文件)的安装单元。在需要时,它使用诸如“发行版的包”之类的短语。(同样,在许多情况下,Python 包被打包在名为类似python-
加上 Python 包名称的发行版的包中。) - Python 特定包管理器
- 一种以符合 Python 打包标准(例如 PEP 376 和 PEP 427)的方式安装、升级和/或删除 Python 包的工具。最流行的 Python 特定包管理器是 pip [1];其他示例包括旧的 Easy Install 命令 [2] 以及直接使用
setup.py
命令。(Conda [3] 有点特殊,因为
conda
命令可以安装的不仅仅是 Python 包,在某些方面更像是一个发行版包管理器。由于conda
命令通常只操作 Conda 创建的环境,因此本文档中的大多数问题在conda
作为 Python 特定包管理器时并不适用。) - 发行版包管理器
- 一种在已安装的发行版实例中安装、升级和/或删除发行版包的工具,它能够安装 Python 包以及非 Python 包,因此通常具有与 PEP 376 无关的自己的已安装软件数据库。例如
apt
、dpkg
、dnf
、rpm
、pacman
和brew
。显著特征是,如果一个包是由发行版包管理器安装的,那么以满足 Python 特定包管理器的需求的方式删除或升级它通常会导致发行版包管理器处于不一致状态。本文档还使用诸如“外部包管理器”或“系统的包管理器”之类的短语在某些上下文中指代发行版包管理器。
- 覆盖
- 覆盖已安装的 Python 包是指导致某些其他包在导入时优先于被覆盖的包,而不会从被覆盖的包中删除任何文件。这需要在
sys.path
上有多个条目:如果包 A 2.0 在一个sys.path
条目中安装了模块a.py
,而包 A 1.0 在以后的sys.path
条目中安装了模块a.py
,那么import a
返回来自前者的模块,我们说 A 2.0 覆盖了 A 1.0。
动机
由于 Python 的巨大普及,软件发行版(我们指的是 Linux 和其他操作系统发行版以及 Homebrew 和 MacPorts 等覆盖发行版)通常出于两个目的提供 Python:作为软件包供最终用户使用,以及作为发行版中其他软件的语言依赖项。
例如,Fedora 和 Debian(及其下游发行版,以及许多其他发行版)提供一个 /usr/bin/python3
二进制文件,该文件提供可供最终用户使用的 python3
命令以及发行版中包含的 Python 语言软件的 #!/usr/bin/python3
释伴行。
可供发行版用户使用的 python3
可执行文件和作为发行版中其他软件的依赖项提供的 python3
可执行文件通常是同一个二进制文件。这意味着,如果最终用户在虚拟环境之外使用类似 pip
的工具安装了 Python 包,那么该包对发行版提供的 Python 语言软件可见。如果新安装的包(或其依赖项之一)是通过发行版安装的包的更新的、向后不兼容的版本,则可能会破坏发行版提供的软件。
这可能对发行版的完整性构成严重问题,因为发行版通常具有本身是用 Python 编写的包管理工具。例如,可以使用 pip install
命令意外地破坏 Fedora 的 dnf
命令,从而难以恢复。
这既适用于系统范围的安装(sudo pip install
),也适用于用户主目录安装(pip install --user
),因为这两个位置的包都显示在 /usr/bin/python3
的 sys.path
上。
系统范围的安装存在一个更严重的问题:如果您尝试使用 sudo pip uninstall
从这种情况中恢复,则可能会最终删除由系统包管理器提供的包。事实上,即使您只是升级了一个包,也会发生这种情况 - pip 会尝试删除旧版本的包,即操作系统提供的包。此时,可能无法仅使用系统上剩余的软件将系统恢复到一致状态。
过去多年来,人们达成共识,认为安装 Python 库或应用程序的最佳方法(不使用发行版的软件包时)是使用虚拟环境。这种方法由 PyPA 的 virtualenv 项目推广,现在 Python 标准库中也提供了该方法的简化版本,即 venv
。将 Python 包安装到虚拟环境中可以防止它被未限定的 /usr/bin/python3
解释器访问,并防止破坏系统软件。
然而,在某些情况下,从发行版外部安装 Python 包以影响发行版提供的命令的行为是很有用且有意为之的。这在 Sphinx 或 Ansible 等软件中很常见,这些软件具有编写 Python 语言扩展的机制。用户可能希望使用其发行版的基本软件版本(出于付费支持或安全更新的原因),但从 PyPI 安装一个小扩展,并且希望基本系统中的软件能够导入该扩展。
虽然此操作仍然存在安装比操作系统期望的更新版本的依赖项或以其他方式负面影响应用程序行为的风险,但它不需要承担删除操作系统文件的风险。如果特别请求,类似 pip 这样的工具应该能够将包安装到默认 sys.path
上的某个目录中,而不会删除系统包管理器拥有的文件。
因此,本 PEP 提出了两件事。
首先,它提出了一种**方法供 Python 解释器分发者标记该解释器其包由 Python 外部的方式管理**,以便 pip 等 Python 特定的工具不会以任何方式更改解释器全局 sys.path
中的已安装包(添加、升级/降级或删除),除非特别覆盖。它还提供了一种方法供分发者指示如何使用虚拟环境作为替代方案。
这是一种选择加入机制:默认情况下,从上游源代码编译的 Python 解释器不会被如此标记,因此使用自编译解释器或未明确标记解释器版本的发行版运行 pip install
的方式将与以往相同。
其次,它设定了一条规则,即当将包安装到解释器的全局上下文中时(安装到未标记的解释器,或覆盖标记),**Python 特定的包管理器应仅修改或删除其创建文件的 sysconfig 方案目录中的文件**。这允许 Python 解释器的分发者设置两个目录,一个用于其自身管理的包,另一个用于最终用户安装的非管理包,并确保安装非管理包不会删除(或覆盖)外部包管理器拥有的文件。
基本原理
如下一节中详细描述,第一个行为更改涉及创建名为 EXTERNALLY-MANAGED
的标记文件,其存在表示非虚拟环境包安装由 Python 外部的一些方式管理,例如发行版的包管理器。此文件指定位于默认 sysconfig
方案中的 stdlib
目录中,它标记整个解释器/安装,而不是 sys.path
上的特定位置。这样做的原因是,如上所述,有两个相关的风险导致破坏外部管理的 Python:可以系统范围地安装不兼容的新版本包(例如,使用 sudo pip install
),并且可以在用户帐户中单独安装,但在标准 Python 命令的 sys.path
上的位置安装(例如,使用 pip install --user
)。如果标记文件位于系统范围的 site-packages
目录中,则它不会清楚地应用于第二种情况。替代方案 部分进一步讨论了可能的位置。
第二个行为更改利用了已遇到此类问题的发行版中现有的 sysconfig
设置,并专门解决了 Python 特定的包管理器删除或覆盖外部包管理器拥有的文件的问题。
用例
本 PEP 中的行为更改旨在尽可能地“做正确的事情”。在本节中,我们考虑本 PEP 为几个代表性用例/上下文指定的更改。具体来说,我们询问了本 PEP 可能更改的两种行为
- 在实现本 PEP 之后,类似
pip install
的 Python 特定安装程序工具是否默认允许安装? - 如果确实运行了此类工具,它是否应该愿意删除该上下文中外部(非 Python 特定)包管理器提供的包,例如发行版包管理器?
(为简单起见,本节将 pip 作为 Python 特定的安装程序工具进行讨论,尽管分析应同样适用于任何其他 Python 特定的包管理工具。)
此表总结了下面详细讨论的用例
案例 | 描述 | pip install 允许 |
允许删除外部安装的包 |
---|---|---|---|
1 | 未修补的 CPython | 目前是;仍然是 | 目前是;仍然是 |
2 | 发行版 /usr/bin/python3 |
目前是;变为否(假设发行版添加了标记文件) | 目前是(Debian 除外);变为否 |
3 | 虚拟环境中的发行版 Python | 目前是;仍然是 | 没有外部安装的包 |
4 | 使用 --system-site-packages 的虚拟环境中的发行版 Python |
目前是;仍然是 | 目前否;仍然否 |
5 | Docker 中的发行版 Python | 目前是;变为否(假设发行版添加了标记文件) | 目前是;变为否 |
6 | Conda 环境 | 目前是;仍然是 | 目前是;仍然是 |
7 | 面向开发人员的发行版 | 目前是;变为否(假设他们添加了标记文件) | 目前通常是;变为否(假设他们根据需要配置了 sysconfig ) |
8 | 发行版构建包 | 目前是;可以仍然是 | 目前是;变为否 |
9 | 从发行版 Python stdlib 复制的 PYTHONHOME |
目前是;变为否 | 目前是;变为否 |
10 | 从上游 Python stdlib 复制的 PYTHONHOME |
目前是;仍然是 | 目前是;仍然是 |
更详细地说,上述用例是
- 标准的未修补 CPython,没有任何特殊的配置或对
sysconfig
的补丁,也没有标记文件。本 PEP 不会更改其行为。此类 CPython 应该(无论本 PEP 如何)不要以与同一系统上任何发行版安装的 Python 重叠的方式安装。例如,在将 Python 安装在
/usr/bin
中的操作系统上,不应该使用./configure --prefix=/usr
构建自定义 CPython 并安装,否则它将覆盖发行版的一些文件,并且发行版最终将覆盖安装中的一些文件。相反,安装应该在单独的目录中(可能是/usr/local
、/opt
或主目录)。因此,我们可以假设此类 CPython 拥有自己的
stdlib
目录和自己的sysconfig
方案,这些方案不会与任何发行版安装的 Python 重叠。因此,任何操作系统安装的包在这里都不可见或不相关。如果在这种情况下存在“外部安装”包的概念,则它位于操作系统外部,通常由构建和安装此 CPython 的人管理。因为安装程序选择不添加标记文件或修改
sysconfig
方案,所以他们选择了当前的行为,并且pip install
可以删除此 CPython 中可用的任何包。 - 发行版的
/usr/bin/python3
,无论以 root 身份运行pip install
还是运行pip install --user
,都遵循我们的 发行版建议。这些建议包括在
stdlib
目录中提供标记文件,以默认防止pip install
,并将发行版提供的包放在默认sysconfig
方案以外的位置,以便以 root 身份运行的pip
不会写入该位置。许多发行版(包括 Debian、Fedora 及其衍生版本)已经在执行后者。
在 Debian 及其衍生版本上,
pip install
目前不会删除发行版安装的包,因为 Debian 附带了一个 阻止此操作的 pip 补丁。因此,对于这些发行版,本 PEP 不是行为更改;它只是以不再特定于 Debian 的方式标准化该行为,并且可以包含到上游 pip 中。(我们已经看到用户报告了 Debian 或其衍生版本中外部安装的包被删除。我们怀疑这是因为用户之前运行过
sudo pip install --upgrade pip
,因此现在拥有了一个没有 Debian 补丁的/usr/bin/pip
版本;在上游包安装程序中标准化此行为将解决此问题。) - 在虚拟环境(来自
venv
或virtualenv
)中使用时,发行版 Python。在虚拟环境中,所有包都由该环境拥有。即使将
pip
、setuptools
等安装到环境中,它们也应该由特定于该环境的工具管理;它们不是系统管理的。 - 在使用
--system-site-packages
的虚拟环境中使用时,发行版 Python。这与上一个案例类似,但值得明确指出,因为全局sys.path
上的任何内容都是可见的。目前,“pip 是否会删除外部安装的包”的答案是否定的,因为 pip 针对在虚拟环境中运行并尝试删除外部包有一个特殊情况。在本 PEP 之后,答案仍然是否定的,但推理变得更通用:系统站点包将位于环境中用于包管理的任何
sysconfig
方案之外。 - 当在单个应用程序容器镜像(例如 Docker 容器)中使用发行版 Python 时。在这种用例中,破坏系统软件的风险较低,因为通常容器中只运行单个应用程序,并且影响较小,因为您可以重建容器,并且不必费力地恢复正在运行的机器。此外,还存在大量带有未限定的
RUN pip install ...
语句等的现有 Dockerfile,最好不要破坏它们。因此,基础容器镜像的构建者可能希望确保标记文件不存在,即使底层操作系统默认情况下提供了一个。存在一个小的行为变化:目前,以 root 身份运行的
pip
会删除外部安装的包,但在此 PEP 之后将不会删除。我们不建议提供覆盖此行为的方法。但是,由于基础镜像通常是最小的,因此没有太多用例仅仅是为了卸载包(尤其是在不使用发行版自己的工具的情况下)。常见的情况是,当 pip 想要升级包时,它之前会删除旧版本(Debian 除外)。在此更改之后,旧版本仍将保留在磁盘上,但 pip 仍将覆盖外部安装的包,并且我们认为这足以避免在实践中成为破坏性更改 - Pythonimport
语句仍然可以获取新安装的包。如果需要一种方法来执行此操作,我们建议发行版应记录安装工具访问发行版本身使用的
sysconfig
方案的方法。有关更多讨论,请参阅发行版建议部分。此 PEP 的作者认为,即使在单个应用程序容器镜像中,使用发行版安装的 Python 解释器的虚拟环境仍然是一个好主意。即使它们运行单个应用程序,该应用程序也可能运行由 Python 实现的操作系统命令,如果您使用 Python 特定的工具安装或升级了发行版提供的 Python 包,则这些命令可能会中断。
- Conda 特别支持使用非
conda
工具(如 pip)来安装 Conda 存储库中不可用的软件。在这种情况下,Conda 充当外部包管理器/发行版,而 pip 充当 Python 特定的包管理器。从某种意义上说,这类似于第一种情况,因为 Conda 提供了它自己的 Python 解释器安装。
我们认为此 PEP 不需要对 Conda 进行任何更改,并且已实现此 PEP 中更改的 pip 版本将在 Conda 环境中继续像现在一样运行。(也就是说,可能值得考虑是否为 pip 安装和 Conda 安装的软件使用单独的
sysconfig
方案,因为对于其他发行版来说,这是一个好主意。) - 对于“面向开发人员的发行版”,我们指的是一种特定类型的发行版,其中预期或鼓励发行版中 Python 或其他语言的直接用户在希望添加库时对发行版本身进行更改。常见示例包括软件开发公司中的私有“monorepos”,其中单个存储库构建第三方和内部软件,并且发行版 Python 解释器的直接用户通常是编写上述内部软件的软件开发人员。像Nixpkgs这样的用户级包管理器也可能算在内,因为它们鼓励作为 Python 开发人员的 Nix 用户为 Nix 打包他们的软件。
在这些情况下,发行版可能希望通过提供指导来响应尝试进行的
pip install
,鼓励使用发行版自己的工具添加新包,并提供文档链接。如果发行版支持/鼓励从发行版的 Python 解释器创建虚拟环境,则可能还存在有关如何正确设置虚拟环境的自定义说明(例如 Nixpkgs 所做的那样)。
- 在为发行版 Python 构建发行版 Python 包时(情况 2),将
pip install
用作发行版包构建过程的一部分可能很有用。(例如,考虑使用pip install .
在xyz
的 sdist/源代码包中构建python-xyz
RPM。)发行版可能还希望使用更具针对性但仍然是 Python 特定的安装工具,例如installer。对于这种情况,构建过程需要找到某种方法来抑制标记文件以允许
pip install
工作,并且可能需要将 Python 特定的工具指向发行版的sysconfig
方案而不是提供的默认方案。有关如何实现此操作的更多讨论,请参阅发行版建议部分。由于此 PEP 的结果,pip 将不再能够删除系统上已有的包。但是,此行为更改是可以的,因为包构建过程不应(并且通常无法)包含删除系统上其他一些文件的指令;它只能打包它自己的文件。
- 使用
PYTHONHOME
设置备用 Python 环境(而不是虚拟环境)的发行版 Python(与虚拟环境相反),其中PYTHONHOME
设置为直接从发行版 Python 复制的某个目录(例如,cp -a /usr/lib/python3.x pyhome/lib
)。假设没有修改,则行为就像底层发行版 Python(情况 2)一样。因此存在行为更改 - 您不再能够默认使用
pip install
,并且如果您覆盖它,它将不再删除外部安装的包(即从操作系统复制并位于操作系统管理的sys.path
条目中的 Python 包)。这种行为更改似乎是合理的,因为如果您的
PYTHONHOME
是发行版 Python 的直接副本,则它应该像发行版的 Python 一样运行。 - 使用从兼容的未修改的上游 Python 获取的
PYTHONHOME
的发行版 Python(或任何 Python 解释器)。由于此 PEP 中的行为更改以标准库中的文件(
stdlib
中的标记文件和sysconfig
模块的行为)为关键,因此行为就像未修改的上游 CPython(情况 1)一样。
规范
将解释器标记为使用外部包管理器
在 Python 特定的包安装程序(即 pip 等工具 - 而不是 apt 等外部工具)将包安装到某个 Python 上下文之前,它应该默认执行以下检查
- 它是否在虚拟环境之外运行?它可以通过
sys.prefix == sys.base_prefix
来确定这一点(但请参阅向后兼容性)。 - 在由
sysconfig.get_path("stdlib", sysconfig.get_default_scheme())
标识的目录中是否存在EXTERNALLY-MANAGED
文件?
如果这两个条件都为真,则安装程序应退出并显示错误消息,指示在虚拟环境之外禁用对此 Python 解释器的目录中的包安装。
安装程序应该有一种方法允许用户覆盖这些规则,例如命令行标志 --break-system-packages
。此选项不应默认启用,并且应带有其使用存在风险的含义。
EXTERNALLY-MANAGED
文件是一个 INI 样式的元数据文件,旨在由标准库configparser 模块解析。如果该文件可以使用 UTF-8 编码由 configparser.ConfigParser(interpolation=None)
解析,并且它包含一个节 [externally-managed]
,则安装程序应查找文件中指定的错误消息并将其作为错误的一部分输出。如果由 locale.getlocale(locale.LC_MESSAGES)
返回的元组的第一个元素(即语言代码)不是 None
,则它应查找名为 Error-
后跟语言代码的键的值。如果该键不存在,并且如果语言代码包含下划线或连字符,则它应查找名为 Error-
后跟语言代码下划线或连字符之前部分的键。如果它找不到这两个键中的任何一个,或者如果语言代码是 None
,则它应查找名为 Error
的键。
如果安装程序在文件中找不到错误消息(因为文件无法解析或因为不存在合适的错误键),则安装程序应仅使用其自己的预定义错误消息,该消息应建议用户创建一个虚拟环境来安装包。
软件发行版如果有一个非 Python 特定的包管理器来管理其 Python 包的 sys.path
中的库,则通常应在其标准库目录中提供一个 EXTERNALLY-MANAGED
文件。例如,Debian 可能会在 /usr/lib/python3.9/EXTERNALLY-MANAGED
中提供一个文件,其内容类似于
[externally-managed]
Error=To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.
If you wish to install a non-Debian packaged Python application,
it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.
See /usr/share/doc/python3.9/README.venv for more information.
这为尝试安装包的用户提供了有用且与发行版相关的信息。可选地,可以在同一文件中提供翻译
Error-de_DE=Wenn ist das Nunstück git und Slotermeyer?
Ja! Beiherhund das Oder die Virtualenvironment gersput!
在某些上下文中,例如创建后不再更新的单个应用程序容器镜像,发行版可以选择不提供 EXTERNALLY-MANAGED
文件,以便用户可以安装他们想要的任何内容(就像他们今天可以做的那样)而不必手动覆盖此规则。
仅写入目标 sysconfig
方案
通常,Python 包安装程序安装到由 sysconfig
标准库包返回的方案中的目录。通常,这是由 sysconfig.get_default_scheme()
返回的方案,但基于配置(例如 pip install --user
),它可能会使用不同的方案。
无论何时安装程序安装到 sysconfig
方案中,此 PEP 都规定安装程序绝不应修改或删除该方案之外的文件。例如,如果它正在升级包,并且该包已安装在该方案之外的目录中(可能在来自另一个方案的目录中),则它应保留现有文件。
如果安装程序最终在升级期间覆盖了现有安装,我们建议它在运行结束时发出警告。
如果安装程序安装到 sysconfig
方案之外的位置(例如,pip install --target
),则本小节不适用。
针对发行版的建议
本节是非规范性的。它提供了我们认为发行版应该遵循的最佳实践,除非它们有其他具体原因。
将安装标记为外部管理
发行版应在其 stdlib
目录中创建一个 EXTERNALLY-MANAGED
文件。
引导用户使用虚拟环境
该文件应包含一条有用且与发行版相关的错误消息,指示如何通过发行版的包管理器安装系统范围的包以及如何设置虚拟环境。如果您的发行版经常被用户在 python3
命令可用(尤其是在 pip
或 get-pip
可用)但 python3 -m venv
不起作用的状态下使用,则消息应清楚地指示如何使 python3 -m venv
正确工作。
考虑打包 pipx(一种用于安装 Python 语言应用程序的工具),并在错误中建议使用它。pipx 自动为该应用程序单独创建一个虚拟环境,这对于想要安装一些 Python 语言软件(发行版中没有提供)但本身不是 Python 用户的最终用户来说是一个更好的默认选项。在发行版中打包 pipx 可以避免这样一种讽刺:指示用户使用 pip install --user --break-system-packages pipx
来避免破坏系统包。考虑安排以下事项:您的发行版为最终用户提供的 Python 包/环境(例如,Fedora 上的 python3
或 Debian 上的 python3-full
)依赖于 pipx。
在容器镜像中保留标记文件
为单应用程序容器生成官方镜像的发行版(例如,Docker 容器镜像)应保留 EXTERNALLY-MANAGED
文件,最好以一种即使镜像用户在其镜像中安装了包更新也不会消失的方式(想想 RUN apt-get dist-upgrade
)。
创建单独的发行版和本地目录
发行版应在系统解释器的 sys.path
上放置两条独立的路径,一条用于发行版安装的包,另一条用于本地系统管理员安装的包,并配置 sysconfig.get_default_scheme()
指向后一条路径。这确保了像 pip 这样的工具不会修改发行版安装的包。本地系统管理员的路径应位于 sys.path
上的发行版路径之前,以便本地安装优先于发行版包。
例如,Fedora 和 Debian(及其衍生产品)都通过使用 /usr/local
用于本地安装的包和 /usr
用于发行版安装的包来实现这种拆分。Fedora 使用 /usr/local/lib/python3.x/site-packages
与 /usr/lib/python3.x/site-packages
。(Debian 使用 /usr/local/lib/python3/dist-packages
与 /usr/lib/python3/dist-packages
作为与本地编译的 Python 解释器进一步分离的层:如果您在 /usr/local/bin
中构建和安装上游 CPython,它将查看 /usr/local/lib/python3/site-packages
,并且 Debian 希望确保通过本地构建的解释器安装的包不会出现在发行版解释器的 sys.path
上。)
请注意,/usr/local
与 /usr
的拆分类似于 PATH
环境变量通常包含 /usr/local/bin:/usr/bin
并且非发行版软件默认安装到 /usr/local
的方式。这种拆分 由文件系统层次结构标准推荐。
您可以通过两种方式做到这一点。一种是,如果您正在直接构建和打包 Python 库(例如,您的打包助手解压缩一个 PEP 517 构建的 wheel 或调用 setup.py install
),则安排这些工具使用不在 sysconfig
方案中但仍在 sys.path
上的目录。
另一种是在包构建中运行时与在已安装的系统上运行时安排默认 sysconfig
方案发生变化。bpo-43976 中的 sysconfig
自定义挂钩应该使这变得容易(一旦被接受并实现):让您的打包工具设置环境变量或其他可检测的配置,并定义一个 get_preferred_schemes
函数,以便在从包构建内部调用时返回不同的方案。然后,您可以在发行版打包过程中使用 pip install
。
我们建议添加一个 --scheme=...
选项来指示 pip 对特定方案运行。(有关 pip 当前如何确定方案,请参见下面的 实现说明。)一旦该选项可用,为了进行本地测试以及可能进行实际打包,您将能够运行类似 pip install --scheme=posix_distro
的命令来明确地将包安装到您的发行版位置(绕过 get_preferred_schemes
)。如果绝对需要,还可以使用 pip uninstall --scheme=posix_distro
使用 pip 从系统管理目录中删除包,这解决了 基本原理 中用例 5 中(希望是理论上的)回归。
要使用 pip 安装包,您还需要要么抑制 EXTERNALLY-MANAGED
标记文件以允许 pip 运行,要么在命令行上覆盖它。您可能希望在构建 chroot 中使用与在容器镜像中相同的方法来抑制标记文件。
将这些设置成自动化的优点(在您的构建环境中抑制标记文件并让 get_preferred_schemes
自动返回您的发行版的方案)在于,未修饰的 pip install
将在包构建内部工作,这通常意味着碰巧在内部调用 pip install
的未修改的上游构建脚本将执行正确操作。当然,您也可以确保您的打包过程始终调用 pip install --scheme=posix_distro --break-system-packages
,这也可以工作。
这里最好的方法在很大程度上取决于您发行版的约定和打包机制。
类似地,不是用于可导入 Python 代码的 sysconfig
路径 - 即 include
、platinclude
、scripts
和 data
- 也应该有两个变体,一个用于发行版打包的软件,另一个用于本地安装的软件,并且发行版应设置为两者都可用。例如,一个典型的符合 FHS 的发行版将使用 /usr/local/include
作为默认方案的 include
和 /usr/include
作为发行版打包的头文件并将两者都放在编译器的搜索路径上,它将使用 /usr/local/bin
作为默认方案的 scripts
和 /usr/bin
作为发行版打包的入口点并将两者都放在 $PATH
上。
向后兼容性
所有这些机制仅针对新的发行版版本和 pip 等工具的新版本提出。
特别是,我们强烈建议具有主要版本概念的发行版仅在新主要版本中添加标记文件或更改 sysconfig
方案;否则,在现有系统上,通过 Python 特定包管理器安装的软件存在变得无法管理的风险(没有覆盖选项)。对于滚动发布的发行版,如果可能,仅在新 Python 次要版本中添加标记文件或更改 sysconfig
方案。
对于包安装工具来说,一个特殊的向后兼容性难题可能是管理由旧版本的 virtualenv
创建的环境,这些环境安装了最新版本的工具。“虚拟环境”现在有一个相当精确的定义:它使用 pyvenv.cfg
机制,这会导致 sys.base_prefix != sys.prefix
。但是,用户可能有一个由旧版本的 virtualenv
创建的旧虚拟环境;在撰写本文时,pip 支持 Python 3.6 及更高版本,而 Python 3.6 又受 virtualenv
15.1.0 及更高版本支持,因此这种情况是可能的。在旧版本的 virtualenv
中,该机制改为设置一个新属性 sys.real_prefix
,并且它不使用虚拟环境的标准库支持,因此 sys.base_prefix
与 sys.prefix
相同。因此,稳健地检测虚拟环境的逻辑类似于
def is_virtual_environment():
return sys.base_prefix != sys.prefix or hasattr(sys, "real_prefix")
安全影响
此功能的目的是为了防止无意的更改意外破坏用户的环境,而不是为了实现安全边界。也就是说,此 PEP 限制在虚拟环境外部使用pip install
的原因,并不是因为这样做存在安全风险;而是因为“应该有一种——最好只有一种——显而易见的方法来做到这一点”,而这种方法应该是使用虚拟环境。pip install
在虚拟环境外部使用过于明显,而这几乎总是错误的做法。
如果存在用户不应该能够使用sudo pip install
或pip install --user
并将文件添加到sys.path
出于安全原因的情况,则需要通过访问控制规则(控制用户可以写入的文件)或为相关程序提供明确的安全sys.path
来实现。此 PEP 中的任何机制都不应被解释为解决此类场景的方法。
出于这些原因,尝试在存在标记文件的情况下进行安装并非安全事件,无需为此引发审计事件。如果调用用户合法地拥有sudo pip install
或pip install --user
的访问权限,则他们可以在完全脱离 Python 的情况下完成相同的安装;如果他们没有合法地拥有此类访问权限,则这是一个超出此 PEP 范围的问题。
标记文件本身位于标准库目录中,这是一个可信位置(即,任何可以写入特定安装程序使用的标记文件的人,都可以运行安装程序内部的任意代码)。因此,通常无需过滤掉错误消息中的终端转义序列或其他潜在的恶意内容。
备选方案
我们考虑了许多类似的提案,但此 PEP 拒绝或推迟了这些提案,主要是为了在基本原理中的逐案分析中保留行为。
标记文件
标记文件是否应该位于sys.path
中,将特定目录标记为 Python 特定包管理器不允许写入?这将有助于解决此 PEP 解决的第二个问题(不覆盖删除发行版拥有的文件),但不能解决第一个问题(不兼容的安装)。/usr/lib/python3.x/site-packages
中的目录特定标记不会阻止安装到/usr/local/lib/python3.x/site-packages
或~/.local/lib/python3.x/site-packages
中,这两者都位于sys.path
中,用于/usr/bin/python3
。换句话说,标记文件不应被解释为标记单个目录为外部管理(即使它碰巧位于sys.path
上的目录中);它将整个Python 安装标记为外部管理。
上述内容的另一种变体:标记文件是否应该位于sys.path
中,如果它可以在sys.path
中的任何目录中找到,则它将安装标记为外部管理?这种方法的一个明显的优势是它会自动在虚拟环境中禁用自身。不幸的是,这在具有--system-site-packages
的虚拟环境中具有错误的行为,其中系统范围内的sys.path
是可见的,但允许进行包安装。(如果保留免除虚拟环境的规则,它可以工作,但这似乎与当前方案相比没有任何优势。)
标记是否应该只是sysconfig
方案的新属性?这在概念上有一些简洁性,除了很难覆盖之外。我们希望使容器镜像、包构建环境等能够轻松抑制标记文件。可以删除的文件很容易;sysconfig
中的代码更难修改。
文件是否应该位于/etc
中?不,因为同样,它指的是特定的 Python 安装。安装自己 Python 的用户可能希望在该解释器的全局上下文中安装包。
配置设置是否应该位于pip.conf
或distutils.cfg
中?除了上面关于标记安装的反对意见之外,此机制对这两个工具都不是特有的。(pip 也实现一个配置标志以防止用户在任何 Python 安装中执行意外的非虚拟环境安装似乎是合理的,但这超出了此 PEP 的范围。)
文件是否应该为 TOML 格式?TOML 正在越来越受欢迎用于打包(例如,参见PEP 517),但尚未在标准库中实现。严格来说,这不是一个障碍——发行版只需要写入文件,而无需读取它,因此它们不需要 TOML 库(无论格式如何,文件都可能手动编写),并且打包工具可能已经有一个 TOML 阅读器。但是,INI 格式目前用于各种其他形式的打包元数据(例如,pydistutils.cfg
和setup.cfg
),满足我们的需求,并且可以由标准库解析,并且 pip 维护者表示希望避免目前使用 TOML。
文件是否应该为email.message
样式?虽然此格式也用于打包元数据(例如 sdist 和 wheel 元数据)并且也可以由标准库解析,但它处理多行条目不太清楚,而这是我们的主要用例。
标记文件是否应该为可执行的 Python 代码,用于评估是否允许安装?除了上面关于将文件放在sys.path
中的担忧之外,我们还担心使其可执行会使用过强的 API 并可能使行为更难以理解。(请注意,bpo-43976的get_default_scheme
钩子实际上是可执行的,但该代码需要在解释器构建时提供;它并非旨在在构建后提供。)
在覆盖标记时,是否应禁止 Python 特定包管理器覆盖由外部包管理器安装的包(即,安装相同名称的模块)?这将最大程度地降低破坏系统软件的风险,但目前尚不清楚这是否值得付出额外的用户体验复杂性。有合理的用例用于覆盖系统包,并且额外的命令行选项以允许它会更令人困惑。同时,不传递该选项不会消除破坏系统软件的风险,系统软件可能依赖于try: import xyz
失败、找到有限的入口点等。传达这种区别似乎很困难。我们认为 Python 特定包管理器在覆盖包时打印警告是一个好主意,但我们认为默认情况下禁用它不值得。
为什么不使用PEP 376中的INSTALLER
文件来确定谁安装了包以及是否可以将其删除?首先,它特定于特定包(位于包的dist-info
目录中),因此,与上面的一些替代方案一样,它不提供有关整个环境以及是否允许包安装的信息。PEP 627还更新了PEP 376以防止以编程方式使用INSTALLER
,指定该文件“仅用于信息目的。 […]我们的目标是支持互操作工具,并且根据哪个工具恰好安装了包来采取任何行动与该目标背道而驰。”最后,正如PEP 627设想的那样,有一个工具知道如何处理另一个工具安装的包的合理用例;例如,conda
可以安全地删除由pip
安装到 Conda 环境中的包。
为什么规范没有提供在虚拟环境中禁用包安装的方法?我们看不到特别强的用例(至少与此 PEP 的目的无关)。如果您需要它,在该环境中pip uninstall pip
就足够简单了,这至少会阻止对环境的无意更改(并且此规范没有规定禁用有意更改,因为毕竟标记文件很容易删除)。
系统 Python
发行版软件是否应该只使用发行版site-packages
目录在sys.path
上,并忽略本地系统管理员的site-packages
以及用户特定的site-packages
?这是一个有价值的想法,并且它的各种版本在一段时间内以“系统 Python”或“平台 Python”(以及用于最终用户编写 Python 或安装与系统分开的 Python 软件的单独“用户 Python”)的名义流传。但是,这是一个更复杂的变化。首先,这将是一个向后不兼容的更改。如动机部分所述,有有效的用例用于运行使用其sys.path
上本地安装的 Python 库的发行版安装的 Python 应用程序,例如 Sphinx 或 Ansible。完全切换到忽略本地包将破坏这些用例,并且发行版必须逐案分析应用程序是否应该查看本地安装的库。
此外,Fedora 尝试了此更改并将其恢复,具有讽刺意味的是,他们发现其更改的实现破坏了其包管理器。鉴于这种经验,在发行版能够可靠地实现这种方法之前,显然需要解决一些细节,并且建议使用 PEP 还为时尚早。
本 PEP 旨在成为一个完整且独立的变更,与发行版是否选择“系统 Python”或类似提案无关。它与发行版将来实现“系统 Python”并不冲突,即使这两个提案都解决了同一类问题,在实现本 PEP 之后,仍然有理由支持实现类似“系统 Python”的东西。 同时,本 PEP 特别尝试进行更具针对性和最小的更改,以便那些不期望采用“系统 Python”(或不期望立即实现它)的发行版也能实现它。本 PEP 中的更改具有自身优点,而不是某个未来提案的中间步骤。本 PEP 降低了(但没有消除)破坏系统软件的风险,同时最大程度地减少了(但没有完全避免)破坏性更改,因此,与完整的“系统 Python”方案相比,它应该更容易实现,而“系统 Python”方案存在上述缺点。
我们预计本 PEP 中的指导——用户应尽可能使用虚拟环境,并且发行版应为发行版管理的模块和本地管理的模块提供单独的 sys.path
目录——将使未来的进一步实验更容易。这些实验可能包括:分发完全独立的“系统”和“用户”Python 解释器;在发行版拥有的虚拟环境或 PYTHONHOME
中运行系统软件(但提供单个解释器);或修改某些软件(例如发行版的包管理器)的入口点以使用仅看到发行版管理目录的 sys.path
。但是,这些想法本身不在本 PEP 的范围之内。
实现说明
本节是非规范性的,包含与规范和潜在实现相关的注释。
目前,pip 并没有直接提供选择目标 sysconfig
方案的方法,但在安装时有三种查找方案的方式
pip install
- 调用
sysconfig.get_default_scheme()
,这通常(在上游 CPython 和大多数当前发行版中)与get_preferred_scheme('prefix')
相同。 pip install --prefix=/some/path
- 调用
sysconfig.get_preferred_scheme('prefix')
。 pip install --user
- 调用
sysconfig.get_preferred_scheme('user')
。
最后,pip install --target=/some/path
直接写入 /some/path
,而不查找任何方案。
Debian 目前包含一个 更改虚拟环境内默认安装位置的补丁,使用一些启发式方法(包括检查 VIRTUAL_ENV
环境变量),主要目的是使虚拟环境中使用的目录保持为 site-packages
而不是 dist-packages
。这不会特别影响本提案,因为该补丁的实现实际上并没有更改默认的 sysconfig
方案,并且值得注意的是,它没有更改 sysconfig.get_path("stdlib")
的结果。
Fedora 目前包含一个 在未在 rpmbuild 内运行时更改默认安装位置的补丁,他们用它来实现双系统级目录方法。从概念上讲,这是 bpo-43976 中设想的钩子类型,只是作为对 distutils
的代码补丁而不是更改的 sysconfig
方案来实现的。
上面 is_virtual_environment
的实现,以及加载 EXTERNALLY-MANAGED
文件并从中查找错误消息的逻辑,也可以添加到标准库(分别为 sys
和 sysconfig
)中,以集中它们的实现,但目前不需要添加。
参考文献
有关这些问题以及之前解决尝试的更多背景信息,请参阅 2014 年的 Debian 错误 771794“pip 静默删除/更新系统提供的 python 包”、Fedora 2018 年的文章 使 sudo pip 安全(关于将 sudo pip
指向 /usr/local)(承认这些更改仍然不能使 sudo pip
完全安全)、2018 年的 pip 问题 5605(“禁用对非 pip 安装的现有 python 模块的升级”)和 5722(“pip 应尊重 /usr/local”),以及 PyCon US 2019 后的讨论主题 与外部包管理器友好相处。
版权
本文件置于公有领域或根据 CC0-1.0-Universal 许可证,以两者中更具许可性的为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0668.rst
上次修改时间:2024-05-17 01:32:43 GMT