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

Python 增强提案

PEP 600 – 未来‘manylinux’平台标签,用于可移植的 Linux 构建发行版

作者:
Nathaniel J. Smith <njs at pobox.com>, Thomas Kluyver <thomas at kluyver.me.uk>
发起人:
Paul Moore <p.f.moore at gmail.com>
BDFL 委托
Paul Moore <p.f.moore at gmail.com>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
主题:
打包
创建日期:
2019年5月3日
发布历史:
2019年5月3日
取代:
513, 571, 599
决议:
Discourse 消息

目录

摘要

本 PEP 提出了一个方案,用于定义新的“manylinux” wheel 标签,而无需为每个特定标签都提交一个 PEP,类似于 Windows 和 macOS 标签的运作方式。这将允许包维护者更快地利用新标签,同时更好地利用有限的志愿者时间。

非目标包括:处理非 glibc 平台;与外部包管理器集成或处理外部依赖项(如 CUDA);使 manylinux 标签比其 Windows/macOS 等效标签更复杂;除了采用我们现有经过验证的方法并对其进行简化之外的任何其他操作。这些都是重要问题,其他 PEP 未来可能会解决它们,但对于本 PEP 而言,它们不在范围之内。

基本原理

Python 用户喜欢 PyPI 上为其平台提供预编译包,因为这使得安装快速而简单。但在 Linux 上分发预编译二进制文件具有挑战性,因为基于 Linux 的平台种类繁多。例如,Debian、Android 和 Alpine 都使用 Linux 内核,但用户空间库截然不同,这使得创建在所有三个平台上都能工作的单个 wheel 变得困难或不可能。这种复杂性导致了许多以前关于 Linux wheel 的讨论停滞不前。

“manylinux”项目通过采取一种无情实用主义的策略取得了成功。我们选择了一组庞大但易于处理的 Linux 平台——具体来说,是主流的基于 glibc 的发行版,如 Debian、OpenSuSE、Ubuntu、RHEL 等——然后我们不惜一切代价制作能在所有这些平台上工作的 wheel。

这种方法需要许多折衷。Manylinux wheel 只能依赖于维护一致 ABI 并在所有这些发行版上普遍可用的外部库,这实际上将它们限制在一小组核心库(如 glibc 和少数其他库)中。Wheel 必须在精心挑选的最古老的平台上构建,使用本身在精心挑选的配置中构建的 Python。其他共享库依赖项必须捆绑到 wheel 中,这需要一个复杂的过程来避免不相关 wheel 之间的冲突。最后,这些要求的细节会随着时间的推移而变化,因为新的发行版会发布,旧的发行版会停止使用。

事实证明,这些要求并不太繁重:它们本质上与分发 Windows 或 macOS wheel 所必须做的事情相同,而且 manylinux 方法在包维护者和最终用户中都取得了显著的普及。但任何 manylinux PEP 都需要某种方式来解决这些复杂性。

在以前的 manylinux PEP 中(PEP 513PEP 571PEP 599),我们通过尝试在 PEP 中写下我们认为会使 wheel 在所有主流基于 glibc 的 Linux 系统上工作的确切库集、符号版本、Python 配置等来做到这一点。但这造成了几个问题。

首先,PEP 通常被认为是规范性参考:如果软件不符合 PEP,那么我们就修复软件。但在这种情况下,PEP 试图描述 Linux 发行版,它们是一个不断变化的目标,并且不认为我们的 PEP 限制了它们的行为。这意味着我们承担了一项无限期的承诺,即每当 Linux 发行版格局发生变化时,都要不断更新每个 manylinux PEP。对于无资金资助的志愿者来说,这是一项巨大的承诺,而且尚不清楚这项工作是否为我们的用户创造了价值。

其次,每次我们将 manylinux 推向更新的支持平台范围,或者添加对新架构的支持时,我们都必须经历一个相当复杂的过程:编写新的 PEP,更新 PyPI 和 pip 代码库以识别新标签,等待新的 pip 传播给用户等。这些在 Windows/macOS 上都不会发生;这只是 Linux 维护者的负担。这减缓了新 manylinux 版本的部署,并消耗了我们社区有限的 PEP 审查带宽,从而减缓了整个 Python 打包生态系统的进展。这对于不太流行的架构尤其成问题,它们拥有更少的志愿者资源来克服这些障碍。

我们该如何解决这个问题?

Manylinux PEP 必须面向三个主要受众

  • 像 pip 这样的包安装器需要能够确定哪些 wheel 标签与它们运行的系统兼容。这需要一些自动化过程来内省系统并将其与 wheel 标签匹配。
  • 像 PyPI 这样的包索引需要能够验证哪些 wheel 标签是有效的。通常,这只需要一个有效标签列表或它们匹配的正则表达式,而无需了解单个标签的实际语义。(但请参阅下面的上传验证讨论。)
  • 包维护者需要能够构建符合给定 wheel 标签要求的 wheel。

这是本新 PEP 背后的关键见解:重要的是,不同的包安装器包索引都同意哪些 manylinux 标签有效以及它们安装在哪些系统上,因此我们需要一个 PEP 来指定这些——但是,这些是简单的,并且在 manylinux 版本之间没有真正变化。不断变化的复杂部分是实际构建 wheel 的过程——但是,如果存在多个相互竞争的构建环境,只要它们都生成在最终用户系统上工作的 wheel,那么它们是否使用完全相同的规则就不重要。因此,我们不需要一个用于构建 wheel 的互操作性标准,所以我们不需要将细节写入 PEP。

为了进一步确信这种方法会奏效,让我们再次看看我们如何处理 Windows 和 macOS 上的 wheel:PEP 描述了哪些标签有效,以及它们应该在哪些系统上工作,但没有描述如何实际为这些平台构建 wheel。实际上,如果您想分发 Windows 或 macOS wheel,您可能需要经历一些复杂且文档不全的繁琐过程才能捆绑依赖项、定位正确的操作系统版本范围等。但该系统有效,改进它的方法是编写更好的文档和构建更好的工具;没有人认为让 Windows wheel 更好地工作的方法是发布一个 PEP,描述我们认为 Microsoft 应该在其库中包含哪些符号以及它们的链接器应该如何工作。本 PEP 将这种理念也扩展到 manylinux。

规范

核心定义

使用新方案的标签将如下所示

manylinux_2_17_x86_64

或者更一般地

manylinux_${GLIBCMAJOR}_${GLIBCMINOR}_${ARCH}

这个标签是一个承诺:wheel 的创建者承诺 wheel 将在任何使用 glibc 版本 ${GLIBCMAJOR}.${GLIBCMINOR} 或更高版本,并且 ${ARCH}distutils.util.get_platform() 的返回值匹配的主流 Linux 发行版上工作。(有关架构标签的更多详细信息,请参阅 PEP 425。)

如果用户将此 wheel 安装到符合这些要求的环境中,但它不起作用,则该 wheel 不符合此规范。这应被视为 wheel 中的一个错误,并且轮子创建者有责任寻找修复方案(可能在更广泛社区的帮助下)。

“主流”一词有意地有些模糊,应作广义解释。目标是排除奇怪的自制 Linux 系统;通常,您听说过的任何发行版都应被视为“主流”。我们还为“奇怪”发行版的维护者提供了一种手动覆盖此检查的方法,尽管根据以前 manylinux PEP 的经验,我们预计此功能不会被大量使用。

最后,符合要求的 wheel 需要“与他人和谐共处”,即安装 manylinux wheel 不得导致其他不相关的包损坏。

任何符合这些标准的生成 wheel 的方法都是可接受的。然而,实际上我们期望 auditwheel 项目将维护一套最新的工具和构建镜像来生成 manylinux wheel,以及关于它们如何工作和如何使用的文档,并且大多数维护者会希望使用这些工具。有关构建 manylinux wheel 的最新信息,包括关于使用哪些构建镜像的建议,请参阅 https://packaging.pythonlang.cn

由于这些要求相当高层次,以下是一些它们在特定情况下的示例

示例:如果一个 wheel 被标记为 manylinux_2_17_x86_64,但它使用了只在 glibc 2.18 中添加的符号,那么该 wheel 将无法在 glibc 2.17 的系统上工作。因此,我们可以得出结论,此 wheel 违反了本规范。

示例:直到大约 2017 年,所有主要的 Linux 发行版都将 libncursesw.so.5 作为默认安装的一部分。在此之前,链接到 libncursesw.so.5 的 wheel 符合此规范。然后,发行版开始转向 ncurses 6,它具有不同的名称和不兼容的 ABI,并停止默认安装 libncursesw.so.5。因此,在此之后,链接到 libncursesw.so.5 的 wheel 不再符合此规范。

示例:Linux ELF 链接器将所有共享库 SONAME 放置在单个进程全局命名空间中。如果独立的 wheel 为其捆绑库使用相同的 SONAME,它们最终可能会发生冲突并使用错误的库版本,这将违反“与他人和谐共处”的规则。因此,此规范要求 wheel 为所有捆绑库使用全局唯一的名称。(Auditwheel 目前通过重命名所有捆绑库以包含全局唯一的哈希来实现这一点。)

示例:我们观察到某些 wheel 以某种方式使用 C++,通过一种不明确的机制干扰其他包。这也违反了“与他人和谐共处”的规则,因此这些 wheel 不符合本规范。

示例:假想的 LEG v7 架构有大端和小端两种变体。大端二进制文件需要大端系统,小端二进制文件需要小端系统。但不幸的是,发现由于 PEP 425 中的一个 bug,两种变体使用相同的架构标签 legv7。这使得创建符合要求的 manylinux_2_17_legv7 wheel 成为不可能:无论我们做什么,它都会在某些用户的系统上崩溃。所以,我们编写了一个新的 PEP,定义了架构标签 legv7lelegv7be;现在我们可以分发 manylinux LEG v7 wheel 了。

示例:还有 LEG v8。它也有大端和小端变体。但幸运的是,事实证明 PEP 425 已经为 LEG v8 做对了,所以 LEG v8 爱好者一旦本 PEP 实施,就可以立即开始分发 manylinux_2_17_legv8lemanylinux_2_17_legv8be wheel,即使本 PEP 的作者对 LEG v8 一无所知。

遗留 manylinux 标签

现有的 manylinux 标签被重新定义为新式标签的别名

  • manylinux1_x86_64 现在是 manylinux_2_5_x86_64 的别名
  • manylinux1_i686 现在是 manylinux_2_5_i686 的别名
  • manylinux2010_x86_64 现在是 manylinux_2_12_x86_64 的别名
  • manylinux2010_i686 现在是 manylinux_2_12_i686 的别名
  • manylinux2014_x86_64 现在是 manylinux_2_17_x86_64 的别名
  • manylinux2014_i686 现在是 manylinux_2_17_i686 的别名
  • manylinux2014_aarch64 现在是 manylinux_2_17_aarch64 的别名
  • manylinux2014_armv7l 现在是 manylinux_2_17_armv7l 的别名
  • manylinux2014_ppc64 现在是 manylinux_2_17_ppc64 的别名
  • manylinux2014_ppc64le 现在是 manylinux_2_17_ppc64le 的别名
  • manylinux2014_s390x 现在是 manylinux_2_17_s390x 的别名

这个重定义基本是一个无操作,但确实影响了一些事情

  • 以前,我们曾有一个开放且不断增长的承诺,即每当新的 Linux 发行版发布时,都要永远更新每个 manylinux PEP。通过使本 PEP 成为旧标签的规范,这一义务便消失了。当本 PEP 被接受后,以前的 manylinux PEP 将收到最终更新,说明它们不再维护,并指向本 PEP。
  • “与他人和谐共处”的规则一直是意图如此,但之前的 PEP 没有明确说明;现在它变得明确。
  • 以前的 PEP 假设 glibc 3.x 可能与 glibc 2.x 不兼容,因此我们使用以下逻辑检查系统和标签之间的兼容性
    sys_major == tag_major and sys_minor >= tag_minor
    

    最近,glibc 维护者建议我们,即使他们提升主版本号,我们也应该假设 glibc 将无限期地保持向后兼容性。因此,新的兼容性检查是

    (sys_major, sys_minor) >= (tag_major, tag_minor)
    

包安装器

通常,包安装程序应在具有适当 glibc 和架构的系统上安装 manylinux wheel,否则不安装。如果有多个兼容的 manylinux wheel 可用,则应首选具有最高 glibc 版本的 wheel,以便利用较新的编译器和 glibc 功能。

此外,我们遵循之前的规范,并允许 Python 分发商通过在其标准库中添加一个 _manylinux 模块来手动覆盖此检查。如果此包可导入,并且它定义了一个名为 manylinux_compatible 的函数,则包安装程序应调用此函数,传入 manylinux 标签中的主版本、次版本和架构,它将返回一个布尔值,表示具有给定标签的 wheel 是否应被视为与当前系统兼容,或者返回 None 以指示应使用默认逻辑。

为了与以前的规范兼容,如果标签精确地是 manylinux1manylinux_2_5,那么我们还会检查模块中是否存在布尔属性 manylinux1_compatible;如果标签版本精确地是 manylinux2010manylinux_2_12,那么我们还会检查模块中是否存在布尔属性 manylinux2010_compatible;如果标签版本精确地是 manylinux2014manylinux_2_17,那么我们还会检查模块中是否存在布尔属性 manylinux2014_compatible。如果新旧属性都已定义,则 manylinux_compatible 优先。

这里有一些示例代码。您不必实际使用此代码,但如果您对确切的语义有疑问,可以将其用作参考

LEGACY_ALIASES = {
    "manylinux1_x86_64": "manylinux_2_5_x86_64",
    "manylinux1_i686": "manylinux_2_5_i686",
    "manylinux2010_x86_64": "manylinux_2_12_x86_64",
    "manylinux2010_i686": "manylinux_2_12_i686",
    "manylinux2014_x86_64": "manylinux_2_17_x86_64",
    "manylinux2014_i686": "manylinux_2_17_i686",
    "manylinux2014_aarch64": "manylinux_2_17_aarch64",
    "manylinux2014_armv7l": "manylinux_2_17_armv7l",
    "manylinux2014_ppc64": "manylinux_2_17_ppc64",
    "manylinux2014_ppc64le": "manylinux_2_17_ppc64le",
    "manylinux2014_s390x": "manylinux_2_17_s390x",
}

def manylinux_tag_is_compatible_with_this_system(tag):
    # Normalize and parse the tag
    tag = LEGACY_ALIASES.get(tag, tag)
    m = re.match("manylinux_([0-9]+)_([0-9]+)_(.*)", tag)
    if not m:
        return False
    tag_major_str, tag_minor_str, tag_arch = m.groups()
    tag_major = int(tag_major_str)
    tag_minor = int(tag_minor_str)

    if not system_uses_glibc():
        return False
    sys_major, sys_minor = get_system_glibc_version()
    if (sys_major, sys_minor) < (tag_major, tag_minor):
        return False
    sys_arch = get_system_arch()
    if sys_arch != tag_arch:
        return False

    # Check for manual override
    try:
        import _manylinux
    except ImportError:
        pass
    else:
        if hasattr(_manylinux, "manylinux_compatible"):
            result = _manylinux.manylinux_compatible(
                tag_major, tag_minor, tag_arch,
            )
            if result is not None:
                return bool(result)
        else:
            if (tag_major, tag_minor) == (2, 5):
                if hasattr(_manylinux, "manylinux1_compatible"):
                    return bool(_manylinux.manylinux1_compatible)
            if (tag_major, tag_minor) == (2, 12):
                if hasattr(_manylinux, "manylinux2010_compatible"):
                    return bool(_manylinux.manylinux2010_compatible)

    return True

包索引

PyPI 或任何包索引接受的 wheel 标签的确切集合是一个政策问题,取决于该索引的维护者。但是,我们建议包索引接受平台标签符合以下正则表达式的任何 wheel

  • manylinux1_(x86_64|i686)
  • manylinux2010_(x86_64|i686)
  • manylinux2014_(x86_64|i686|aarch64|armv7l|ppc64|ppc64le|s390x)
  • manylinux_[0-9]+_[0-9]+_(.*)

包索引可能会施加额外的要求;例如,它们可能会审计上传的 wheel 并拒绝那些包含已知问题的 wheel,例如引用了更高 glibc 版本的符号的 manylinux_2_17 wheel,或者依赖于已知不存在于所有系统上的外部库。或者包索引可能会决定保守地拒绝标记为 manylinux_2_999 的 wheel,理由是当 glibc 2.999 发布时,没有人知道 Linux 发行版的情况会是怎样的。我们将任何此类检查的细节留给包索引维护者的自由裁量权。

被拒绝的替代方案

继续 manylinux20XX 系列:如上所述,这导致新版本的推出需要更多的精力、更慢且更复杂。虽然最初看起来在两个地方有一些补偿性的好处,但如果仔细观察,事实并非如此。

首先,这迫使我们在 PEP 的文本中提供关于 Linux 发行版如何工作的人类可读描述。但这并没有最初看起来那么有价值,而且通过新的“常青”方法也可以更好地处理。

如果您正在尝试构建 wheel,您最需要的是关于如何使用构建镜像及其工具的教程。如果您正在尝试添加对新构建配置文件的支持或创建 auditwheel 的竞争对手,那么您最好的资源将是 auditwheel 源代码和问题跟踪器,它们总是比用英文编写且没有测试的摘要规范更详细、更精确、更可靠。像旧的 manylinux20XX PEP 这样的文档确实增加了价值!但在两种情况下,它主要作为提供概述和上下文的次要参考。

此外,PEP 流程不适合维护这种参考文档——我们不将 pip 用户手册保存在 PEP 存储库中是有原因的!auditwheel 维护者最能理解哪种文档对他们的用户有用,并能够长期维护这些文档。例如,不同的 manylinux 版本之间存在大量重叠,而 PEP 流程目前迫使我们通过在不断增长的文档列表中复制粘贴所有内容来处理这个问题;相反,auditwheel 维护者可以选择将共同部分分解到一份共享文档中。

一个相关的问题是,使用常青方法,包维护者可能更难决定要针对哪个构建配置文件:他们不再需要在 manylinux1manylinux2010manylinux2014 等之间进行选择,而是有更多的选项,如 manylinux_2_5manylinux_2_6、……、manylinux_2_20 等。但我们仍然不认为这在实践中会成为问题。在这两种系统中,大多数包维护者都不会从阅读 PEP 并尝试从头开始实现它们。如果您是一位特别专业且雄心勃勃的包维护者,需要针对新版本或新架构,常青方法为您提供了额外的灵活性。但对于普通的日常维护者,我们预计他们会从像 packaging.python.org 这样的教程开始,并从现有构建镜像中进行选择。教程可以像推荐 manylinux2014 一样轻松地推荐 manylinux_2_17,我们预计实际预提供的构建镜像在这两种情况下都是相同的。再次强调,通过在正确的位置维护这份文档,而不是试图在 PEP 存储库中进行,我们预计最终会得到更高质量、更符合目的的文档。

最后,一些参与者指出,能够明确判断一个 wheel 是否符合规范要求是非常好的。使用新的“常青”方法,我们永远无法 100% 确定一个 wheel 确实符合规范,因为这取决于 Linux 发行版。作为工程师,我们对这种不确定性有一种充分理由的不喜欢。

然而:如上面的例子所示,我们仍然可以明确地判断一个 wheel 符合规范,这在实践中被证明是重要的。而且,在实践中,使用 manylinux20XX 方法,每当发行版更改时,我们实际上都会更改规范;这需要更长的时间。所以,即使一个 wheel 今天符合规范,明天也可能变得不符合规范。这令人沮丧,但不幸的是,如果您关心的是向用户分发可工作的 wheel,这种不确定性是不可避免的。

因此,即使在旧方法最初看起来有优势的这些方面,我们预计新方法实际上也会做得同样好甚至更好。

切换到常青标签,但继续为每个版本编写 PEP:这被提出作为一种混合方案,试图获得常青标签系统的一些优势——例如更容易推出新版本——同时保留 manylinux20XX 方案的优势,例如强制我们编写有关 Linux 发行版的文档,简化包维护者的选项,以及能够明确判断一个 wheel 是否符合规范。但如上所述,仔细观察后,事实证明这些优势在很大程度上是虚幻的。而且,这也继承了 manylinux20XX 方案的显著缺点,例如创建了无限期地更新不断增长的复制粘贴 PEP 列表的义务。

使 auditwheel 成为规范:另一个被考虑的可能性是使 auditwheel 成为 manylinux 定义的规范参考,即,一个 wheel 仅当 auditwheel check 完成而没有错误时才符合要求。这被拒绝了,因为打包 PEP 的目的是定义工具之间的互操作性,而不是认可特定工具。

在标签字符串中添加额外词语:我们考虑的另一个提议是在 wheel 标签中添加额外词语,例如 manylinux_glibc_2_17 而不是 manylinux_2_17。这样做的动机是为将来其他类型的版本控制启发式方法敞开大门——例如,我们可以有 manylinux_glibc_$VERSIONmanylinux_alpine_$VERSION

但是“manylinux”一直都是“与主流基于 glibc 的发行版广泛兼容”的同义词;将其用于与 alpine 等不相关的构建配置文件会造成更多的混淆而不是帮助。此外,一些不熟悉打包细节的早期审阅者发现 glibc 这个词具有误导性,他们很快得出结论,认为它意味着他们需要一个精确该 glibc 版本的系统。而像 manylinux_$VERSIONalpine_$VERSION 这样的标签也具有简洁和直接的优点。所以我们将采用这种方式。


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

最后修改:2025-02-01 08:59:27 GMT