Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 513 – 可移植 Linux 构建分发的平台标签

作者:
Robert T. McGibbon <rmcgibbo at gmail.com>, Nathaniel J. Smith <njs at pobox.com>
BDFL 委托
Alyssa Coghlan <ncoghlan at gmail.com>
讨论至:
Distutils-SIG 邮件列表
状态:
已取代
类型:
信息性
主题:
打包
创建日期:
2016年1月19日
发布历史:
2016年1月19日, 2016年1月25日, 2016年1月29日
取代者:
600
决议:
Distutils-SIG 消息

目录

摘要

本 PEP 提议为 Python 包的构建分发(例如 Wheels)创建一个新的平台标签 manylinux1_{x86_64,i686},其外部依赖项仅限于标准化的、受限的 Linux 内核和核心用户空间 ABI 子集。提议 PyPI 支持上传和分发带有此平台标签的 Wheels,并且 pip 支持在兼容的平台上下载和安装这些包。

基本原理

目前,Windows 和 OS X 的二进制 Python 扩展分发非常简单。开发者和打包者构建 Wheels(PEP 427, PEP 491),并为其分配 win32macosx_10_6_intel 等平台标签,然后上传到 PyPI。用户可以使用 pip 等工具下载和安装这些 Wheels。

对于 Linux,情况则要复杂得多。通常,在一个 Linux 发行版上构建的已编译 Python 扩展模块无法在其他 Linux 发行版上运行,甚至在安装了不同系统库的同一 Linux 发行版上的不同机器上也无法运行。

使用 PEP 425 平台标签的构建工具不会跟踪有关特定 Linux 发行版或已安装系统库的信息,而是为所有 Wheels 分配过于笼统的 linux_i686linux_x86_64 标签。由于这种歧义,无法保证在一个机器上编译的 linux 标记的已构建分发在另一台机器上能正常工作,因此 PyPI 不允许上传 Linux 的 Wheels。

理想情况下,我们可以构建能在 *任何* Linux 系统上运行的 Wheel 包。但是,由于 Linux 系统极大的多样性——从 PC 到 Android 再到具有自定义 libcs 的嵌入式系统——这通常无法保证。

相反,我们定义了一个内核+核心用户空间 ABI 的标准子集,该子集在实践中足够兼容,能够保证符合此标准的包在 *许多* Linux 系统上运行,包括当前普遍使用的几乎所有桌面和服务器发行版。我们知道这一点是因为有些公司一直在分发这种广泛可移植的预编译 Python 扩展模块到 Linux——例如 Enthought 的 Canopy [4] 和 Continuum Analytics 的 Anaconda [5]

基于这些公司积累的兼容性经验,我们因此为二进制 Python Wheels 定义了一个基线 manylinux1 平台标签,并引入了初步工具的实现,以帮助构建这些 manylinux1 Wheels。

Linux 二进制不兼容性的主要原因

为了正确定义一个标准,以保证符合此规范的 Wheel 包能在 *许多* Linux 平台上运行,我们有必要了解那些经常阻止 Linux 上预编译二进制文件可移植性的根本原因。这两个主要原因是对用户系统中不存在的共享库的依赖,以及对 glibc 等某些核心库特定版本的依赖。

外部共享库

大多数桌面和服务器 Linux 发行版都带有系统包管理器(例如 Debian 系列系统的 APTRPM 系列系统的 yum,以及 Arch Linux 的 pacman),它负责管理安装在 /usr/lib 等系统目录下的共享库。大多数非简单的 Python 扩展都依赖一个或多个这些共享库,因此只能在用户拥有正确库(及其正确版本)的系统上正常工作,无论是通过包管理器安装,还是通过设置 LD_LIBRARY_PATH 等环境变量来通知运行时链接器依赖共享库的位置来手动安装。

核心共享库的版本控制

即使 Python 扩展模块的开发者希望不使用任何外部共享库,这些模块通常也会对 GNU C 库 glibc 产生动态运行时依赖。虽然有可能,但静态链接 glibc 通常是个坏主意,因为某些重要的 C 函数(如 dlopen())无法从静态链接 glibc 的代码中调用。实际上,对系统提供的 glibc 的运行时共享库依赖是不可避免的。

GNU C 库的维护者遵循严格的符号版本控制方案以实现向后兼容。这确保了针对旧版本 glibc 编译的二进制文件可以在拥有较新 glibc 的系统上运行。反之通常不成立——在较新 Linux 发行版上编译的二进制文件往往依赖于 glibc 中未在旧系统上提供的版本化函数。

这通常会阻止在最新 Linux 发行版上编译的 Wheels 具有可移植性。

manylinux1 策略

因此,为了实现广泛的可移植性,Python Wheels

  • 应仅依赖于极少数外部共享库;并且
  • 应仅依赖于这些外部共享库中的“旧”符号版本;并且
  • 应仅依赖于广泛兼容的内核 ABI。

为了有资格获得 manylinux1 平台标签,Python Wheel 必须同时满足 (a) 包含的二进制可执行文件和编译代码 *仅* 链接到以下列表中包含的 SONAME 的库

libpanelw.so.5
libncursesw.so.5
libgcc_s.so.1
libstdc++.so.6
libm.so.6
libdl.so.2
librt.so.1
libc.so.6
libnsl.so.1
libutil.so.1
libpthread.so.0
libresolv.so.2
libX11.so.6
libXext.so.6
libXrender.so.1
libICE.so.6
libSM.so.6
libGL.so.1
libgobject-2.0.so.0
libgthread-2.0.so.0
libglib-2.0.so.0

以及 (b) 可以在包含这些库的系统包管理器所提供版本的标准 CentOS 5.11 [6] 系统上运行。

在 Fedora 30 发布时使用 libcrypt.so.2 而不是 libcrypt.so.1 之后,libcrypt.so.1 被追溯性地从白名单中移除。

由于 CentOS 5 仅适用于 x86_64 和 i686 架构,这是当前 manylinux1 策略支持的唯一架构。

在基于 Debian 的系统上,这些库由以下软件包提供

libncurses5 libgcc1 libstdc++6 libc6 libx11-6 libxext6
libxrender1 libice6 libsm6 libgl1-mesa-glx libglib2.0-0

在基于 RPM 的系统上,这些库由以下软件包提供

ncurses libgcc libstdc++ glibc libXext libXrender
libICE libSM mesa-libGL glib2

此列表是通过检查 Canopy [4] 和 Anaconda [5] 发行版的外部共享库依赖项编译而成的,这两个发行版都包含大量最受欢迎的 Python 模块,并且已在实践中被证实能在广泛的真实 Linux 系统中运行。

上述许多允许的系统库使用符号版本控制方案来实现向后兼容。CentOS 5.11 版本中的这些库提供的最新符号版本是

GLIBC_2.5
CXXABI_3.4.8
GLIBCXX_3.4.9
GCC_4.2.0

因此,作为要求 (b) 的结果,任何依赖上述共享库中版本化符号的 Wheel,其依赖的符号版本只能是以下版本

GLIBC <= 2.5
CXXABI <= 3.4.8
GLIBCXX <= 3.4.9
GCC <= 4.2.0

以下建议是 2016 年 1 月相关讨论的结果 [7], [8]

请注意,在我们下面的建议中,我们不建议 pip 或 PyPI 尝试检查和强制执行此策略的细节(正如它们不检查和强制执行现有平台标签 win32 的细节一样)。上述文本(a)作为对包构建者的建议,以及(b)作为当给定 Wheel 在某些系统上不起作用时分配责任的方法:如果它满足上述策略,那么这是规范或安装工具中的一个错误;如果它不满足上述策略,那么是 Wheel 中的一个错误。这种方法的一个有用结果是,它为我们提供了更多经验时进行进一步更新和调整的可能性,例如,我们可以有一个“manylinux 1.1”策略,它针对相同的系统并使用相同的 manylinux1 平台标签(因此不需要对 pip 或 PyPI 进行任何进一步的更改),但调整上面的列表以删除已证明有问题的库或添加已证明安全的库。

libpythonX.Y.so.1

libpythonX.Y.so.1 *不在* manylinux1 扩展允许链接的库列表中。在几乎所有情况下,显式链接到 libpythonX.Y.so.1 都是不必要的:根据 ELF 链接的工作方式,加载到解释器中的扩展模块会自动获得对解释器所有符号的访问权限,无论扩展本身是否显式链接到 libpython。此外,在 Python 未使用 --enable-shared 构建的常见配置中,显式链接到 libpython 会导致问题。特别地,在 Debian 和 Ubuntu 系统上,apt install pythonX.Y 甚至不安装 libpythonX.Y.so.1,这意味着任何 *依赖* libpythonX.Y.so.1 的 Wheel 都可能无法导入。

在这种方式下链接的扩展可能无法工作的唯一情况是:如果一个宿主程序(例如 apache2)使用 dlopen() 加载一个模块(例如 mod_wsgi),该模块嵌入 CPython 解释器,并且宿主程序 *未* 将 RTLD_GLOBAL 标志传递给 dlopen(),那么嵌入的 CPython 将无法加载任何未显式链接到 libpythonX.Y.so.1 的扩展模块。幸运的是,apache2 *会* 设置 RTLD_GLOBAL 标志,我们能找到的所有其他通过 dlopened 插件嵌入 CPython 的程序也是如此,所以这似乎不是一个严重的问题。与 Debian/Ubuntu 的不兼容性比理论上与一个相当晦涩的角落情况的不兼容性更令人关注。

这是一个相当复杂和微妙的问题,超出了 manylinux1 的范围;更多讨论请参见:[9], [10], [11]

UCS-2 vs UCS-4 构建

所有 CPython 2.x 版本,以及 CPython 3.0-3.2(包含),都可以用两种 ABI 不兼容的模式构建:使用 --enable-unicode=ucs2 配置标志构建的版本将 Unicode 数据存储在 UCS-2(或实际上是 UTF-16)格式中,而使用 --enable-unicode=ucs4 配置标志构建的版本将 Unicode 数据存储在 UCS-4 中。(CPython 3.3 及更高版本使用不同的存储方法,该方法始终支持 UCS-4。)如果我们想确保 ucs2 Wheels 不会被安装到 ucs4 CPythons 中,反之亦然,那么就必须做些什么。

此 PEP 的早期版本包含一项要求,即针对这些旧 CPython 版本的 manylinux1 Wheels 应始终使用 ucs4 ABI。但是,在 PEP 最初被接受和实施之间,pipwheel 获得了对跟踪和检查相关 CPython 版本 ABI 兼容性这一方面的一流支持,这是一个更好的解决方案。因此,我们现在允许 manylinux1 平台标签与任何 ABI 标签结合使用。然而,为了保持兼容性,确保所有 manylinux1 Wheels 都包含一个非平凡的 abi 标签至关重要。例如,针对 ucs4 CPython 构建的 Wheel 可能的名称是

PKG-VERSION-cp27-cp27mu-manylinux1_x86_64.whl
                 ^^^^^^ Good!

而针对 ucs2 ABI 构建的 Wheel 可能的名称是

PKG-VERSION-cp27-cp27m-manylinux1_x86_64.whl
                 ^^^^^ Okay!

但您永远不应该有一个类似以下名称的 Wheel

PKG-VERSION-cp27-none-manylinux1_x86_64.whl
                 ^^^^ BAD! Don't do this!

这个 Wheel 声称同时兼容 *ucs2* 和 *ucs4* 构建,这是不好的。

我们附带说明,ucs4 ABI 在 Linux CPython 分发版中似乎更为普遍。

fpectl 构建 vs. 无 fpectl 构建

所有现存的 CPython 版本都可以使用 `--with-fpectl` 标志或不使用该标志进行编译。事实证明,这会改变 CPython ABI:针对无 fpectl 的 CPython 构建的扩展总是兼容有 fpectl 的 CPython,但反之则不一定成立。(症状:导入时出现关于 undefined symbol: PyFPE_jbuf 的错误。)请参见:[16]

因此,为了最大的兼容性,用于构建 manylinux1 Wheels 的 CPython 必须 *不带* --with-fpectl 标志编译,并且 manylinux1 扩展不得引用 PyFPE_jbuf 符号。

兼容 Wheel 的编译

glibc、libgcc 和 libstdc++ 管理其符号版本的方式意味着,在实践中,大多数开发者用于日常工作的编译器工具链无法构建 manylinux1 兼容的 Wheels。因此,我们不尝试改变 pip wheel / bdist_wheel 的默认行为:它们将继续生成常规的 linux_* 平台标签,而希望使用它们生成 manylinux1 标记的 Wheels 的开发者将不得不在第二步后处理中更改标签。

为了支持构建符合 manylinux1 标准的 Wheels,我们提供了两个工具的初始草案。

Docker 镜像

第一个工具是一个基于 CentOS 5.11 的 Docker 镜像,它被推荐作为一个易于使用的、自包含的构建环境,用于编译 manylinux1 Wheels [12]。在一个较新发布的 Linux 发行版上编译通常会引入对过新版本化符号的依赖。该镜像自带完整的编译器套件(gcc, g++, 和 gfortran 4.8.2)以及最新版本的 Python 和 pip

Auditwheel

第二个工具是一个名为 auditwheel [13] 的命令行可执行文件,它可以帮助包维护者处理第三方外部依赖项。

有至少三种方法可以构建使用第三方外部库且符合上述策略的 Wheels。

  1. 第三方库可以被静态链接。
  2. 第三方共享库可以以单独的 PyPI 包形式分发,并由 Wheel 依赖。
  3. 第三方共享库可以被捆绑在 Wheel 库内部,并使用相对路径链接。

所有这些都是有效的选项,不同的包和社区可以有效地使用它们。静态链接通常需要针对特定包的构建系统修改,而在 PyPI 上分发第三方依赖项可能需要对包的用户社区进行一些协调。

作为这些选项的一个经常自动化的替代方案,我们引入了 auditwheel。该工具会检查 Wheel 内的所有 ELF 文件,以查找对版本化符号或外部共享库的依赖,并验证是否符合 manylinux1 策略。这包括为符合要求的 Wheels 添加新平台标签的能力。更重要的是,auditwheel 能够通过将系统中的共享库复制到 Wheel 本身,并修改适当的 RPATH 条目,从而自动修改依赖外部共享库的 Wheels,以便这些库在运行时被加载。这实现了与静态链接类似的结果,而无需修改构建系统。建议打包者注意,捆绑(与静态链接类似)可能涉及版权问题。

Linux 上的捆绑 Wheel

虽然我们承认 manylinux1 Wheels 中处理第三方库依赖的多种方法,但我们认识到 manylinux1 策略鼓励捆绑外部依赖项,这种做法与许多 Linux 发行版系统包管理器的包管理策略背道而驰 [14], [15]。这样做的主要目的是跨发行版兼容性。此外,PyPI 上的 manylinux1 Wheels 占用了与系统包管理器提供的 Python 包不同的细分市场。

本 PEP 中鼓励偏离 Linux 发行版普遍的非捆绑策略的决定是基于以下顾虑

  1. 在当今自动化的持续集成和部署流水线时代,发布新版本和更新依赖项比定义这些策略时更容易。
  2. pip 用户仍然可以自由使用 "--no-binary" 选项,如果他们希望强制本地构建而不是使用预构建的 Wheel 文件。
  3. 现代容器化部署和“不可变基础设施”模型的流行,无论如何在应用程序层已经进行了大量的捆绑。
  4. 通过 PyPI 分发捆绑的 Wheels 目前是 Windows 和 OS X 的标准做法。
  5. 本 PEP 并没有排除将来为特定 Linux 发行版提供更具针对性的二进制文件的可能性。

本 PEP 中描述的模型最适合跨平台 Python 包,因为它意味着它们可以重用大量工作,以创建静态的 Windows 和 OS X Wheels。我们认识到,对于更偏向于与 Linux 独特的包管理功能进行更紧密交互且只关心针对少数特定发行版的 Linux 特定包而言,该模型不够理想。

安全隐患

Linux 中依赖集中式库的一个优势是,bug 修复和安全更新可以跨系统部署,并且依赖这些库的应用程序将在底层库更新时自动感受到这些补丁的影响。这对于涉及网络通信或加密的包中的安全更新尤其重要。

通过 PyPI 分发的 manylinux1 Wheels,如果捆绑了 OpenSSL 等安全关键库,将承担及时响应已披露漏洞和补丁的责任。这与 Windows 上二进制 Wheels 的分发在安全方面的影响非常相似,因为 Windows 缺乏系统包管理器,通常会捆绑其依赖项。特别是,由于缺乏稳定的 ABI,OpenSSL 不能包含在 manylinux1 配置文件中。

安装程序的平台检测

上面,我们定义了一个 *Wheel* 如何成为 manylinux1 兼容的。在这里,我们讨论一个 *Python 安装* 如何成为 manylinux1 兼容的。特别是,这对于 pip 等工具在决定是否应考虑安装 manylinux1 标记的 Wheels 时很重要。

由于 manylinux1 配置文件已被证明对数以万计的流行商业 Python 发行版用户有效,我们建议安装工具应倾向于假设一个系统 *是* 兼容的,除非有特定理由认为并非如此。

我们知道四种可能在实践中出现的潜在不兼容的主要来源

  • 最终,在未来,可能会出现破坏与此配置文件兼容性的发行版(例如,如果配置文件中的某个库更改其 ABI 而不向后兼容)
  • 过旧的 Linux 发行版(例如 RHEL 4)
  • 不使用 glibc 的 Linux 发行版(例如基于 musl libc 的 Alpine Linux,或 Android)

为了解决这些问题,我们提出一个两管齐下的方法。为了处理未来潜在的不兼容性,我们标准化了一个机制,供 Python 发行版明确表示某个 Python 安装是否肯定兼容或不兼容 manylinux1:这是通过安装一个名为 _manylinux 的模块,并设置其 manylinux1_compatible 属性来完成的。我们不建议将任何此类模块添加到标准库——这只是一个知名的名称,发行版和安装工具可以通过它来汇合。但是,如果发行版确实添加了这个模块,*它们应该将其添加到标准库*,而不是添加到 site-packages/ 目录,因为标准库会被虚拟环境继承(这是我们想要的),而 site-packages/ 通常不会。

然后,为了处理现有 Python 发行版最后两种情况,我们建议一种简单可靠的方法来检查 glibc 的存在和版本(基本上将其用作发行版整体年龄的“时钟”)。

具体来说,我们提议的算法是

def is_manylinux1_compatible():
    # Only Linux, and only x86-64 / i686
    from distutils.util import get_platform
    if get_platform() not in ["linux-x86_64", "linux-i686"]:
        return False

    # Check for presence of _manylinux module
    try:
        import _manylinux
        return bool(_manylinux.manylinux1_compatible)
    except (ImportError, AttributeError):
        # Fall through to heuristic check below
        pass

    # Check glibc version. CentOS 5 uses glibc 2.5.
    return have_compatible_glibc(2, 5)

def have_compatible_glibc(major, minimum_minor):
    import ctypes

    process_namespace = ctypes.CDLL(None)
    try:
        gnu_get_libc_version = process_namespace.gnu_get_libc_version
    except AttributeError:
        # Symbol doesn't exist -> therefore, we are not linked to
        # glibc.
        return False

    # Call gnu_get_libc_version, which returns a string like "2.5".
    gnu_get_libc_version.restype = ctypes.c_char_p
    version_str = gnu_get_libc_version()
    # py2 / py3 compatibility:
    if not isinstance(version_str, str):
        version_str = version_str.decode("ascii")

    # Parse string and check against requested version.
    version = [int(piece) for piece in version_str.split(".")]
    assert len(version) == 2
    if major != version[0]:
        return False
    if minimum_minor > version[1]:
        return False
    return True

已拒绝的替代方案:我们还考虑使用配置文件,例如 /etc/python/compatibility.cfg。问题在于,单个文件系统可能包含许多不同的解释器环境,每个环境都有自己的 ABI 配置文件——系统安装的 x86_64 CPython 的 manylinux1 兼容性可能无法告诉我们用户安装的 i686 PyPy 的 manylinux1 兼容性。将此配置信息放在 Python 环境本身内,可以确保它附属于正确的二进制文件,并大大简化查找代码。

我们还考虑使用更复杂的结构,例如一个应被视为兼容的所有平台标签列表,以及它们的偏好顺序,例如:_binary_compat.compatible = ["manylinux1_x86_64", "centos5_x86_64", "linux_x86_64"]。然而,这带来了一些复杂性。例如,我们希望能够区分“不支持 manylinux1”(或最终 manylinux2 等)的状态与“不明确是支持 manylinux1”的状态,这在上述表示中并不完全清楚;而且,考虑到目前唯一可能的平台标签是 manylinux1linux,那么到底需要哪些功能来确定偏好顺序,这一点并不十分清楚。因此,我们推迟了更完整的解决方案,留待单独的 PEP,在 Linux 拥有更多平台标签时/如果需要的话。

对于库兼容性检查,我们也考虑了更复杂的检查(例如,检查内核版本,搜索并检查 manylinux1 配置文件中列出的所有单个库的版本等),但最终认为这比实际帮助用户更容易引入令人困惑的错误。(例如:不同的发行版在实际放置这些库的位置上有所不同,如果我们的检查代码未能使用正确的路径搜索,则很容易返回不正确的结果。)

PyPI 支持

PyPI 应允许上传包含 manylinux1 平台标签的 Wheels。PyPI 不应尝试正式验证包含 manylinux1 平台标签的 Wheels 是否符合本文档所述的 manylinux1 策略。此验证任务应留给其他工具,如 auditwheel,这些工具是单独开发的。

被拒绝的替代方案

一种替代方法是为每个 Linux 发行版(及其每个版本)提供单独的平台标签,例如 RHEL6ubuntu14_10debian_jessie 等。本提案中没有任何内容排除将来添加此类平台标签的可能性,或扩展 Wheel 元数据以允许 Wheels 声明对外部系统安装的包的依赖。然而,这些扩展需要比本提案多得多的工作量,并且包开发者仍可能不赞成,因为他们不愿意维护多个构建环境并构建多个 Wheels 以覆盖所有常见的 Linux 发行版。因此,我们认为此类提案超出了本 PEP 的范围。

未来更新

我们预计在未来的某个时候,会有一个 manylinux2,指定一个更现代化的基线环境(可能基于 CentOS 6),以及将来某个时候的 manylinux3 等等,但我们将其指定推迟到我们对初始 manylinux1 提案有更多经验之后。

参考资料


Source: https://github.com/python/peps/blob/main/peps/pep-0513.rst

Last modified: 2025-02-01 08:59:27 GMT