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 日
取代的 PEP:
600
解决方案:
Distutils-SIG 邮件

目录

摘要

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

理由

目前,Windows 和 OS X 的 Python 二进制扩展的发布很简单。开发人员和打包人员构建 Wheel(PEP 427PEP 491),这些 Wheel 被分配了平台标签,如 win32macosx_10_6_intel,然后将其上传到 PyPI。用户可以使用 pip 等工具下载和安装这些 Wheel。

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

使用 PEP 425 平台标签的构建工具不会跟踪有关特定 Linux 发行版或已安装系统库的信息,而是将所有 Wheel 分配过于模糊的 linux_i686linux_x86_64 标签。由于这种歧义,没有理由期望在某台机器上编译的 linux 标签的构建发行版在另一台机器上能正常运行,出于这个原因,PyPI 不允许上传 Linux 的 Wheel。

理想情况下,Wheel 包的编译应该能够在任何 Linux 系统上运行。但是,由于 Linux 系统的巨大差异性——从 PC 到 Android 到具有自定义 libcs 的嵌入式系统,这在一般情况下无法保证。

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

基于从这些公司汲取的兼容性教训,我们定义了一个基本 manylinux1 平台标签,供二进制 Python Wheel 使用,并引入了初步工具的实现,以帮助构建这些 manylinux1 Wheel。

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

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

外部共享库

大多数桌面和服务器 Linux 发行版都带有一个系统包管理器(例如,基于 Debian 的系统上的 APT,基于 RPM 的系统上的 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 发行版上编译的 Wheel 可移植。

manylinux1 策略

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

  • 应该仅依赖于一组极其有限的外部共享库;以及
  • 应该仅依赖于这些外部共享库中的“旧”符号版本;以及
  • 应该仅依赖于广泛兼容的内核 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] 系统上运行。

libcrypt.so.1 在 Fedora 30 发布后被从白名单中删除,因为 Fedora 30 使用的是 libcrypt.so.2

由于 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。此外,显式链接到 libpython 会在 Python 没有使用 --enable-shared 构建的常见配置中造成问题。特别是在 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 与 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 wheel 不会安装到 ucs4 CPython 中,反之亦然,那么必须做些什么。

PEP 的早期版本包含一个要求,即针对这些较旧的 CPython 版本的 manylinux1 wheel 应始终使用 ucs4 ABI。但是,在 PEP 最初被接受和实施之间,pipwheel 获得了一流的支持,用于跟踪和检查相关 CPython 版本的 ABI 兼容性,这是一个更好的解决方案。因此,我们现在允许 manylinux1 平台标签与任何 ABI 标签一起使用。但是,为了保持兼容性,务必确保所有 manylinux1 wheel 包含一个非平凡的 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 声称同时兼容 ucs2ucs4 构建,这很糟糕。

我们注意到,ucs4 ABI 在 Linux CPython 发行版中似乎更为普遍。

fpectl 构建与非 fpectl 构建

所有现有的 CPython 版本都可以使用或不使用 --with-fpectl 标志构建到 configure 中。事实证明,这会改变 CPython ABI:针对无 fpectl 的 CPython 构建的扩展始终与有 fpectl 的 CPython 兼容,但反之则不一定。(症状:在导入时出现有关 undefined symbol: PyFPE_jbuf 的错误。)请参见:[16]

因此,为了最大限度地提高兼容性,用于构建 manylinux1 wheel 的 CPython 必须不使用 --with-fpectl 标志进行编译,并且 manylinux1 扩展不能引用 PyFPE_jbuf 符号。

兼容 Wheel 的编译

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

为了支持符合 manylinux1 标准的 wheel 的编译,我们提供了两个工具的初始草稿。

Docker 镜像

第一个工具是基于 CentOS 5.11 的 Docker 镜像,建议将其用作编译 manylinux1 wheel 的易于使用的自包含构建盒子 [12]。在较新的 Linux 发行版上进行编译通常会导致对版本过新的符号产生依赖关系。该镜像安装了完整的编译器套件(gccg++gfortran 4.8.2),以及最新版本的 Python 和 pip

Auditwheel

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

至少有三种方法可以构建以满足上述策略的方式使用第三方外部库的 wheel。

  1. 第三方库可以静态链接。
  2. 第三方共享库可以作为单独的软件包分发到 PyPI 上,这些软件包由 wheel 依赖。
  3. 第三方共享库可以捆绑在 wheel 库内部,并使用相对路径进行链接。

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

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

Linux 上打包的 Wheel

虽然我们承认在 manylinux1 wheel 中处理第三方库依赖关系的许多方法,但我们认识到,manylinux1 策略鼓励捆绑外部依赖关系,这与许多 Linux 发行版的系统包管理器的一般包管理策略背道而驰 [14][15]。这样做最主要的目的是跨发行版兼容性。此外,PyPI 上的 manylinux1 wheel 与通过系统包管理器提供的 Python 软件包占据不同的位置。

本 PEP 中鼓励偏离一般 Linux 发行版解绑策略的决定,是基于以下考虑

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

本 PEP 中描述的模型最适合跨平台 Python 软件包,因为它意味着它们可以重复使用它们已经在为 Windows 和 OS X 创建静态 wheel 所做的很多工作。我们认识到,它对于可能更喜欢与 Linux 独有的包管理功能进行更紧密交互的 Linux 特定软件包来说并不理想,并且只关心针对一小组特定发行版。

安全影响

在 Linux 中依赖于集中式库的一个优势是,可以系统范围地部署错误修复和安全更新,依赖于这些库的应用程序在底层库更新后将自动感受到这些补丁的影响。这对于网络通信或加密相关的软件包的安全更新尤其重要。

通过 PyPI 分发的包含 OpenSSL 等安全关键库的 manylinux1 轮子将承担对披露的漏洞和补丁进行快速更新的责任。这与 Windows 上二进制轮子的分发安全影响非常相似,因为该平台缺乏系统包管理器,通常会捆绑它们的依赖项。特别是,由于缺乏稳定的 ABI,OpenSSL 不能包含在 manylinux1 配置文件中。

安装程序的平台检测

上面,我们定义了轮子manylinux1 兼容意味着什么。这里我们讨论Python 安装manylinux1 兼容意味着什么。特别是,这对于像 pip 这样的工具在决定是否应该考虑安装带有 manylinux1 标签的轮子时很重要。

由于 manylinux1 配置文件已经被证明适用于数以千计的流行商业 Python 发行版用户,我们建议安装工具应该在假设系统兼容的情况下出错,除非有明确的理由认为并非如此。

我们知道四种主要的潜在不兼容来源,这些来源很可能在实际中出现。

  • 最终,在将来,可能存在与该配置文件不兼容的发行版(例如,如果配置文件中的某个库以向后不兼容的方式更改了其 ABI)。
  • 过时的 Linux 发行版(例如 RHEL 4)。
  • 不使用 glibc 的 Linux 发行版(例如 Alpine Linux,它基于 musl libc,或 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,因此尚不清楚真正需要哪些功能以及优先级排序。因此,我们将在这里延迟更完整的解决方案,以便在 Linux 获得更多平台标签时,再通过单独的 PEP 进行处理。

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

PyPI 支持

PyPI 应该允许上传包含 manylinux1 平台标签的轮子。PyPI 不应该试图正式验证包含 manylinux1 平台标签的轮子是否符合本文档中描述的 manylinux1 策略。此验证任务应留给其他工具(例如 auditwheel)单独开发。

被拒绝的方案

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

未来更新

我们预计,在将来某个时刻,将出现一个 manylinux2,它指定一个更现代的基线环境(可能基于 CentOS 6),并且有一天会出现 manylinux3 等等,但我们将延迟指定这些内容,直到我们对最初的 manylinux1 提案有更多经验。

参考文献


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

最后修改时间:2023-10-11 12:05:51 GMT