PEP 668 – 将 Python 基础环境标记为“外部管理”
- 作者:
- Geoffrey Thomas <geofft at ldpreload.com>, Matthias Klose <doko at ubuntu.com>, Filipe Laíns <lains at python.org>, 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 用户面临的一个实际问题是操作系统包管理器和像 pip 这样的 Python 专用包管理工具之间的冲突。这些冲突包括 Python 级别的 API 不兼容以及文件所有权冲突。
历史上,Python 专用包管理工具默认将包安装到隐式全局上下文中。随着虚拟环境的标准化和普及,对于大多数(但不是所有)用例来说,更好的解决方案是仅在虚拟环境中使用 Python 专用包管理工具。
本 PEP 提出了一种机制,供 Python 安装向 pip 等工具传达其全局包安装上下文由 Python 外部的某种方式(例如操作系统包管理器)管理。它规定 Python 专用包管理工具默认情况下不应在解释器的全局上下文中安装或删除包,而应引导最终用户使用虚拟环境。
它还标准化了 sysconfig 方案的解释,以便如果 Python 专用包管理器要在解释器范围的上下文中安装包,它可以通过一种方式进行,以避免与外部包管理器冲突,并降低破坏外部包管理器附带软件的风险。
术语
本 PEP 中使用的一些术语在其所涵盖的上下文中具有多种含义。为清楚起见,本 PEP 以特定方式使用以下术语:
- 分发版
- “Distribution”的简称,是各种软件的集合,理想情况下设计为可以正常协同工作,包括(与本文相关的上下文中)Python 解释器本身、用 Python 编写的软件以及用其他语言编写的软件。也就是说,这是在“Linux 分发版”或“Berkeley Software Distribution”等短语中使用的含义。
分发版可以是它自己的操作系统(OS),例如 Debian、Fedora 或 FreeBSD。它也可以是安装在现有操作系统之上的叠加分发版,例如 Homebrew 或 MacPorts。
本文使用短语“distro”,因为术语“distribution”在 Python 打包上下文中还有另一个含义:单个 Python 语言软件的源或二进制分发包,即
setuptools.dist.Distribution或“sdist”的含义。为了避免混淆,本文根本不使用简单的术语“distribution”。在 Python 打包的意义上,它使用完整的短语“distribution package”或简称“package”(见下文)。分发版的提供者——收集和发布软件并进行任何必要修改的团队或公司——是它的分发者。
- 包
- 可以在 Python 中安装和使用的软件单元。也就是说,这指的是 Python 专用打包工具倾向于称之为“distribution package”或简称为“distribution”;口语缩写“package”用于 Python Package Index 的含义。
本文不使用“package”来指代包含 Python 模块的可导入名称,尽管在许多情况下,一个分发包由一个同名的可导入包组成。
本文通常不使用“package”来指代分发版包管理器(例如
.deb或.rpm文件)的安装单元。当需要时,它使用诸如“a distro’s package”之类的措辞。(同样,在许多情况下,一个 Python 包被打包在一个名为python-加上 Python 包名的分发版包中。) - Python 专用包管理器
- 一个按照 Python 打包标准(例如 PEP 376 和 PEP 427)安装、升级和/或删除 Python 包的工具。最流行的 Python 专用包管理器是 pip [1];其他示例包括旧的 Easy Install 命令 [2] 以及直接使用
setup.py命令。(Conda [3] 是一个特例,因为
conda命令可以安装远不止 Python 包,这使得它在某些方面更像一个分发版包管理器。由于conda命令通常只操作 Conda 创建的环境,因此本文中的大多数问题不适用于作为 Python 专用包管理器时的conda。) - 分发版包管理器
- 一个在已安装的分发版实例中安装、升级和/或删除分发版包的工具,它能够安装 Python 包以及非 Python 包,因此通常有自己的已安装软件数据库,与 PEP 376 无关。示例包括
apt、dpkg、dnf、rpm、pacman和brew。显著的特点是,如果一个包是由分发版包管理器安装的,以满足 Python 专用包管理器的方式删除或升级它通常会导致分发版包管理器处于不一致的状态。本文还使用“外部包管理器”或“系统包管理器”等短语来指代某些上下文中的分发版包管理器。
- 遮蔽 (shadow)
- 遮蔽一个已安装的 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 shebang。由于没有适用于 Linux/UNIX 的官方 Python 二进制版本,这些操作系统上的几乎所有 Python 最终用户都使用其分发版构建和提供的 Python 解释器。
可供分发版用户使用的 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 包安装到 virtualenv 中可以防止它对未限定的 /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 | venv 中的分发版 Python | 目前是;保持是 | 没有外部安装的包 |
| 4 | 带有 --system-site-packages 的 venv 中的分发版 Python |
目前是;保持是 | 目前否;保持否 |
| 5 | Docker 中的分发版 Python | 目前是;变为否(假设分发版添加了标记文件) | 目前是;变为否 |
| 6 | Conda 环境 | 目前是;保持是 | 目前是;保持是 |
| 7 | 面向开发者的分发版 | 目前是;变为否(假设它们添加了标记文件) | 目前通常是;变为否(假设它们根据需要配置 sysconfig) |
| 8 | 分发版构建包 | 目前是;可以保持是 | 目前是;变为否 |
| 9 | 从分发版 Python stdlib 复制的 PYTHONHOME |
目前是;变为否 | 目前是;变为否 |
| 10 | 从上游 Python stdlib 复制的 PYTHONHOME |
目前是;保持是 | 目前是;保持是 |
更详细地说,上述用例包括:
- 一个标准未打补丁的 CPython,没有任何特殊配置或
sysconfig补丁,并且没有标记文件。本 PEP 不改变其行为。这样的 CPython(无论本 PEP 如何)都不应以与同一系统上任何分发版安装的 Python 重叠的方式安装。例如,在
/usr/bin中提供 Python 的操作系统上,您不应安装使用./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 之后,答案仍然是否定的,但理由变得更通用:系统 site 包将位于环境中用于包管理的任何sysconfig方案之外。 - 在单应用容器镜像(例如 Docker 容器)中使用的分发版 Python。在此用例中,破坏系统软件的风险较低,因为容器中通常只运行一个应用,而且影响也较低,因为您可以重建容器,并且不必费力恢复正在运行的机器。此外,还有大量现有的 Dockerfile 包含不加限定的
RUN pip install ...语句等,最好不要破坏这些。因此,基础容器镜像的构建者可能希望确保标记文件不存在,即使底层操作系统默认提供了标记文件。有一个小的行为变化:目前,作为 root 运行的
pip会删除外部安装的包,但在本 PEP 之后则不会。我们不建议提供覆盖此行为的方法。然而,由于基础镜像通常是最小的,因此简单地卸载包(尤其是不使用分发版自己的工具)的使用场景不应太多。常见情况是 pip 想要升级一个包,这以前会删除旧版本(Debian 除外)。在此更改之后,旧版本仍将保留在磁盘上,但 pip 仍将遮蔽外部安装的包,我们认为这足以使其在实践中不构成破坏性更改——Pythonimport语句仍将为您提供新安装的包。如果需要这样做,我们建议分发版应记录一种方法,供安装工具访问分发版自身使用的
sysconfig方案。有关更多讨论,请参阅分发版建议部分。本 PEP 作者认为,即使在单应用容器镜像中,将虚拟环境与分发版安装的 Python 解释器一起使用仍然是一个好主意。即使它们运行单个应用程序,该应用程序也可能运行由操作系统实现的命令,如果您使用 Python 专用工具安装或升级了分发版提供的 Python 包,这些命令可能会损坏。
- Conda 特别支持使用非
conda工具(如 pip)来安装 Conda 存储库中不可用的软件。在这种情况下,Conda 充当外部包管理器/分发版,pip 充当 Python 专用包管理器。在某种意义上,这类似于第一种情况,因为 Conda 提供其自己的 Python 解释器安装。
我们不认为本 PEP 需要对 Conda 进行任何更改,并且已实施本 PEP 中更改的 pip 版本将继续在 Conda 环境中表现出与当前相同的行为。(尽管如此,值得考虑是否为 pip 安装和 Conda 安装的软件使用单独的
sysconfig方案,原因与对其他分发版来说是个好主意的原因相同。) - 通过“面向开发者的分发版”,我们指的是一种特定的分发版类型,其中分发版中 Python 或其他语言的直接用户被期望或鼓励在他们希望添加库时对分发版本身进行更改。常见示例包括软件开发公司的私有“monorepos”,其中单个存储库构建第三方和内部软件,并且分发版 Python 解释器的直接用户通常是编写上述内部软件的软件开发人员。像 Nixpkgs 这样的用户级包管理器也可能算作在内,因为它们鼓励使用 Nix 的 Python 开发者为 Nix 打包他们的软件。
在这些情况下,分发版可能希望通过提供引导信息来响应尝试的
pip install,鼓励使用分发版自己的设施来添加新包,并附带文档链接。如果分发版支持/鼓励从分发版的 Python 解释器创建虚拟环境,则可能还有关于如何正确设置虚拟环境的自定义说明(例如 Nixpkgs 所做的那样)。
- 当为分发版 Python (情况 2) 构建分发版 Python 包时,在分发版包构建过程中使用
pip install可能很有用。(例如,考虑通过在xyz的 sdist / 源 tarball 中使用pip install .来构建python-xyzRPM。)分发版可能还希望使用更具针对性但仍然是 Python 专用的安装工具,例如 installer。对于这种情况,构建过程需要找到某种方法来抑制标记文件以允许
pip install工作,并且可能需要将 Python 专用工具指向分发版的sysconfig方案,而不是提供的默认方案。有关如何实现此目的的更多讨论,请参阅分发版建议部分。作为本 PEP 的结果,pip 将不再能够删除系统上已有的包。然而,这种行为改变是合理的,因为包构建过程不应该(通常也不能)包含删除系统上其他文件的指令;它只能打包自己的文件。
- 一个分发版 Python 与
PYTHONHOME一起使用以设置替代的 Python 环境(而不是虚拟环境),其中PYTHONHOME设置为直接从分发版 Python 复制的某个目录(例如,cp -a /usr/lib/python3.x pyhome/lib)。假设没有修改,那么行为就像底层分发版 Python(情况 2)。因此存在行为更改——默认情况下不能再
pip install,如果覆盖它,它将不再删除外部安装的包(即从操作系统复制并存在于操作系统管理的sys.path条目中的 Python 包)。这种行为改变似乎是站得住脚的,因为如果你的
PYTHONHOME是分发版 Python 的直接副本,它应该表现得像分发版 Python。 - 一个分发版 Python(或任何 Python 解释器)与从兼容的未修改上游 Python 获取的
PYTHONHOME一起使用。由于本 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 模块解析。如果文件可以通过 configparser.ConfigParser(interpolation=None) 使用 UTF-8 编码解析,并且包含一个 [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 以避免破坏系统包的讽刺。考虑安排您的分发版的包/环境(例如,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 及更高版本,而 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,这两个都在 /usr/bin/python3 的 sys.path 上。换句话说,标记文件不应被解释为标记单个目录为外部管理(即使它恰好位于 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”分开)的名义流通了一段时间。然而,这涉及到更复杂的更改。首先,这将是一个向后不兼容的更改。如动机部分所述,将发行版安装的 Python 应用程序(如 Sphinx 或 Ansible)与本地安装的 Python 库一起在其 sys.path 上运行存在有效的用例。全面切换到忽略本地软件包将破坏这些用例,并且发行版必须对应用程序是否应该看到本地安装的库进行逐案分析。
此外,Fedora 尝试了这项更改并撤销了它,讽刺地发现他们的实现破坏了他们的软件包管理器。鉴于这一经验,在发行版能够可靠地实施该方法之前,显然还有细节需要解决,建议采用这种方法的 PEP 将为时过早。
本 PEP 旨在成为一项完整且独立的更改,它独立于分发商支持或反对“系统 Python”或类似提案的决定。它不与发行版将来实施“系统 Python”相冲突,尽管两个提案都解决了同一类问题,即使在实施本 PEP 之后,仍有支持实施类似“系统 Python”的论据。然而,与此同时,本 PEP 明确尝试进行更具针对性和最小的更改,以便那些不希望采用“系统 Python”(或不希望立即实施)的分发商可以实施。本 PEP 中的更改本身具有价值,而不是未来某个提案的中间步骤。本 PEP 降低(但未消除)破坏系统软件的风险,同时最大限度地减少(但未完全避免)破坏性更改,因此应该比完整的“系统 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 bug 771794“pip 静默删除/更新系统提供的 python 软件包”,Fedora 2018 年的文章 Making sudo pip safe,关于将 sudo pip 指向 /usr/local(该文章承认这些更改仍然无法使 sudo pip 完全安全),pip 问题 5605(“禁用升级现有非 pip 安装的 python 模块”)和 5722(“pip 应该尊重 /usr/local”)来自 2018 年,以及 2019 年 PyCon US 之后的讨论主题 与外部包管理器友好相处。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0668.rst