PEP 600 – 用于可移植 Linux 构建发行版的未来“manylinux”平台标签
- 作者:
- 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”轮子标签的方案,无需为每个特定标签创建 PEP,类似于 Windows 和 macOS 标签的工作方式。这将允许软件包维护人员更快地利用新标签,同时更好地利用有限的志愿者时间。
非目标包括:处理非 glibc 平台;与外部包管理器集成或处理 CUDA 等外部依赖项;使 manylinux 标签比其 Windows/macOS 等效项更复杂;除了采用我们现有的经过验证的方法并对其进行简化之外,不做任何其他事情。这些都是重要的问题,其他 PEP 可能会在将来解决这些问题,但对于本 PEP 来说,它们超出了范围。
基本原理
Python 用户希望 PyPI 为其平台提供预编译的软件包,因为这使安装变得快速而简单。但在 Linux 上分发预编译的二进制文件具有挑战性,因为 Linux 平台的多样性。例如,Debian、Android 和 Alpine 都使用 Linux 内核,但具有截然不同的用户空间库,这使得创建可在所有三个平台上运行的单个轮子变得困难或不可能。这种复杂性导致了之前许多关于 Linux 轮子的讨论陷入停滞。
“manylinux”项目通过采用务实策略取得了成功。我们选择了一组庞大但易于处理的 Linux 平台——具体来说,主流基于 glibc 的发行版,如 Debian、OpenSuSE、Ubuntu、RHEL 等——然后我们尽一切努力制作可在所有这些平台上运行的轮子。
这种方法需要许多妥协。Manylinux 轮子只能依赖于维护一致 ABI 并在所有这些发行版中普遍可用的外部库,这在实践中将它们限制在一组小的核心库,如 glibc 和其他几个库。轮子必须在精心挑选的最旧版本的平台上构建,使用本身以精心挑选的配置构建的 Python。其他共享库依赖项必须捆绑到轮子中,这需要一个复杂的过程来避免不相关轮子之间的冲突。最后,随着新发行版版本的发布和旧发行版的使用减少,这些要求的细节会随着时间的推移而发生变化。
事实证明,这些要求并不算太繁重:它们基本上等同于您必须为分发 Windows 或 macOS 轮子所做的工作,并且 manylinux 方法已在软件包维护人员和最终用户之间获得了广泛的采用。但是任何 manylinux PEP 都需要某种方式来解决这些复杂性。
在以前的 manylinux PEP 中(PEP 513、PEP 571、PEP 599),我们通过尝试在 PEP 中写下我们认为会导致轮子在所有主流基于 glibc 的 Linux 系统上运行的确切库集、符号版本、Python 配置等来做到这一点。但这产生了几个问题
首先,PEP 通常应该作为规范性参考:如果软件不符合 PEP,那么我们就修复软件。但在这种情况下,PEP 试图描述 Linux 发行版,而 Linux 发行版是一个不断变化的目标,并且不认为我们的 PEP 限制了它们的行为。这意味着我们一直在承担无限的承诺,即在 Linux 发行版环境发生变化时始终更新每个 manylinux PEP。这对无偿志愿者来说是一个巨大的承诺,而且不清楚这项工作是否为我们的用户创造了价值。
其次,每次我们将 manylinux 向前推进到更新的支持平台范围或添加对新架构的支持时,我们都必须经历一个相当复杂的过程:编写新的 PEP、更新 PyPI 和 pip 代码库以识别新标签、等待新的 pip 传播到用户等。在 Windows/macOS 上都不会发生这种情况;这只是对 Linux 维护人员的负担。这会减慢新 manylinux 版本的部署速度,并消耗我们社区有限的 PEP 审查带宽,从而减缓 Python 打包生态系统整体的进步。这对于不太流行的架构来说尤其成问题,因为它们拥有更少的志愿者资源来克服这些障碍。
我们该如何解决它?
Manylinux PEP 必须针对三个主要受众
- **软件包安装程序**,如 pip,需要能够确定哪些轮子标签与其运行的系统兼容。这需要某种自动化流程来内省系统并将其与轮子标签匹配。
- **软件包索引**,如 PyPI,需要能够验证哪些轮子标签是有效的。通常,这只需要一个有效标签列表或它们匹配的正则表达式,而无需了解单个标签的实际语义。(但请参阅下面关于上传验证的讨论。)
- **软件包维护人员**需要能够构建满足给定轮子标签要求的轮子。
这是此新 PEP 背后的关键见解:对于不同的**软件包安装程序**和**软件包索引**都同意哪些 manylinux 标签有效以及它们安装在哪些系统上至关重要,因此我们需要一个 PEP 来指定这些——但是,这些非常简单,并且在 manylinux 版本之间并没有真正改变。不断变化的复杂部分是实际**构建轮子**的过程——但是,如果有多个竞争的构建环境,那么它们是否使用完全相同的规则并不重要,只要它们都生成在最终用户系统上运行的轮子即可。因此,我们不需要构建轮子的互操作性标准,因此我们不需要将详细信息写入 PEP 中。
为了进一步确信这种方法将奏效,让我们再次看看我们如何在 Windows 和 macOS 上处理轮子:PEP 描述了哪些标签有效以及它们应该在哪些系统上运行,但没有描述如何实际构建这些平台的轮子。在实践中,如果您想分发 Windows 或 macOS 轮子,您可能必须跳过一些复杂且记录不佳的障碍才能捆绑依赖项、定位正确的 OS 版本范围等。但系统有效,改进它的方法是编写更好的文档和构建更好的工具;没有人认为使 Windows 轮子更好地工作的方法是发布一个 PEP,描述我们认为 Microsoft 应该在其库中包含哪些符号以及它们的链接器应该如何工作。本 PEP 也将此理念扩展到 manylinux。
规范
核心定义
使用新方案的标签将如下所示
manylinux_2_17_x86_64
或者更一般地
manylinux_${GLIBCMAJOR}_${GLIBCMINOR}_${ARCH}
此标签是一个承诺:轮子的创建者承诺轮子将在任何使用 glibc 版本${GLIBCMAJOR}.${GLIBCMINOR}
或更高版本的主流 Linux 发行版上运行,并且其中${ARCH}
与distutils.util.get_platform()
的返回值匹配。(有关架构标签的更多详细信息,请参阅PEP 425。)
如果用户将此轮子安装到满足这些要求的环境中,但它无法工作,则该轮子不符合此规范。这应该被视为轮子中的错误,并且轮子创建者有责任寻找解决方案(可能在更广泛的社区的帮助下)。
“主流”一词有意地有些模糊,应该进行广泛的解释。目标是排除奇怪的自酿 Linux 系统;通常,您实际听说过的任何发行版都应该被视为“主流”。我们还提供了一种方法,允许“奇怪”发行版的维护人员手动覆盖此检查,但根据之前 manylinux PEP 的经验,我们预计此功能的使用不会很多。
最后,符合条件的轮子必须“与其他轮子良好协作”,即安装 manylinux 轮子不得导致其他不相关的软件包崩溃。
任何满足这些条件的生成轮子的方法都是可以接受的。但是,在实践中,我们预计 auditwheel 项目将维护一套最新的工具和构建镜像以生成 manylinux 轮子,以及有关它们的工作原理和使用方法的文档,并且大多数维护人员希望使用这些工具。有关构建 manylinux 轮子的最新信息,包括有关使用哪些构建镜像的建议,请参阅https://packaging.pythonlang.cn。
由于这些要求相当高级,以下是一些它们在特定情况下如何发挥作用的示例
示例:如果轮子被标记为manylinux_2_17_x86_64
,但它使用仅在 glibc 2.18 中添加的符号,则该轮子在 glibc 2.17 的系统上将无法工作。因此,我们可以得出结论,此轮子违反了此规范。
示例:直到大约 2017 年,所有主要 Linux 发行版都将libncursesw.so.5
作为其默认安装的一部分。在此日期之前,链接到libncursesw.so.5
的轮子符合此规范。然后,发行版开始切换到 ncurses 6,后者具有不同的名称和不兼容的 ABI,并且默认情况下停止安装libncursesw.so.5
。因此,在此日期之后,链接到libncursesw.so.5
的轮子不再符合此规范。
示例:Linux ELF 链接器将所有共享库 SONAME 放入单个进程全局命名空间中。如果独立的轮子对捆绑的库使用相同的 SONAME,它们可能会最终发生冲突并使用错误的库版本,这将违反“与其他轮子良好协作”规则。因此,此规范要求轮子对所有捆绑的库使用全局唯一的名称。(Auditwheel 目前通过将所有捆绑的库重命名为包含全局唯一哈希来实现此目的。)
**示例:**我们观察到某些使用 C++ 构建的 wheel 以某种不清晰的机制干扰了其他软件包。这也违反了“与其他软件包良好共存”的规则,因此这些 wheel 不符合本规范。
**示例:**假设一种名为 LEG v7 的架构既有大端字节序也有小端字节序的变体。大端字节序的二进制文件需要大端字节序系统,小端字节序的二进制文件需要小端字节序系统。但不幸的是,由于PEP 425中的一个错误,这两个变体使用了相同的架构标签legv7
。这使得无法创建符合规范的manylinux_2_17_legv7
wheel:无论我们做什么,它都会在某些用户的系统上崩溃。因此,我们编写了一个新的 PEP,定义了架构标签legv7le
和legv7be
;现在我们可以发布 manylinux LEG v7 wheel 了。
**示例:**还有一个 LEG v8。它也具有大端字节序和小端字节序的变体。但幸运的是,事实证明PEP 425已经对 LEG v8 做了正确的事情,因此 LEG v8 爱好者一旦此 PEP 实施,就可以立即开始发布manylinux_2_17_legv8le
和manylinux_2_17_legv8be
wheel,即使此 PEP 的作者对 LEG v8 一无所知。
软件包安装程序
通常,软件包安装程序应在具有适当 glibc 和架构的系统上安装 manylinux wheel,否则不应安装。如果有多个兼容的 manylinux wheel 可用,则应优先选择 glibc 版本最高的 wheel,以利用更新的编译器和 glibc 功能。
此外,我们遵循以前的规范,并允许 Python 发行版通过将其标准库中添加_manylinux
模块来手动覆盖此检查。如果此软件包可导入,并且如果它定义了一个名为manylinux_compatible
的函数,则软件包安装程序应调用此函数,传入来自 manylinux 标签的主版本、次版本和架构,它将返回一个布尔值,表示是否应将具有给定标签的 wheel 视为与当前系统兼容,或者返回None
以指示应使用默认逻辑。
为了与以前的规范兼容,如果标签正好是manylinux1
或manylinux_2_5
,那么我们还会检查模块中是否存在一个布尔属性manylinux1_compatible
;如果标签版本正好是manylinux2010
或manylinux_2_12
,那么我们还会检查模块中是否存在一个布尔属性manylinux2010_compatible
;如果标签版本正好是manylinux2014
或manylinux_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 维护者可能会选择将公共部分分解成一个共享文档。
另一个相关问题是,使用“永久”方法,软件包维护者可能难以决定要定位哪个构建配置文件:他们不必在manylinux1
、manylinux2010
、manylinux2014
等之间进行选择,他们现在有更广泛的选择,例如manylinux_2_5
、manylinux_2_6
、…、manylinux_2_20
等。但同样,我们认为这在实践中不会成为问题。在这两种系统中,大多数软件包维护者都不会从阅读 PEP 并尝试从头开始实现它们开始。如果您是一位特别专业且雄心勃勃的软件包维护者,需要定位新版本或新架构,则“永久”方法为您提供了额外的灵活性。但对于日常维护者而言,我们预计他们将从 packaging.python.org 等教程开始,并从现有的构建镜像中进行选择。教程可以像推荐manylinux_2_17
一样轻松地推荐manylinux2014
,并且我们预计实际提供的构建镜像集在这两种情况下都是相同的。同样,通过将此文档保存在正确的位置,而不是尝试在 PEP 存储库中执行此操作,我们预计最终将获得更高质量且更符合目的的文档。
最后,一些参与者指出,能够查看一个 wheel 并明确地判断它是否满足规范的要求是非常好的。使用新的“永久”方法,我们永远无法百分之百确定一个 wheel 是否满足规范,因为这取决于 Linux 发行版。作为工程师,我们对这种不确定性有合理的厌恶。
然而:如上例所示,我们仍然可以明确地判断车轮何时不符合规范,这在实践中被证明是重要的。而且,在实践中,使用 manylinux20XX 方法,每当发行版发生变化时,我们实际上都会更改规范;这需要更长的时间。因此,即使某个车轮今天符合规范,明天也可能变得不符合规范。这令人沮丧,但不幸的是,如果您关心的是向用户分发可用的车轮,那么这种不确定性是不可避免的。
因此,即使在这些旧方法最初似乎具有优势的方面,我们也预计新方法实际上也能达到或超过旧方法的水平。
切换到永久标签,但继续为每个版本编写 PEP:这被提议作为一种混合方法,尝试获得永久标记系统的某些优势——例如更容易推出新版本——同时保留 manylinux20XX 方案的优势,例如迫使我们编写有关 Linux 发行版的文档,简化软件包维护人员的选择,并能够明确地判断车轮何时满足规范。但如上所述,仔细观察后发现,这些优势在很大程度上是虚幻的。而且,这也继承了 manylinux20XX 方案的显著缺点,例如对更新不断增长的复制粘贴 PEP 列表产生无限的义务。
使 auditwheel 成为规范:另一个考虑的可能性是使 auditwheel 成为 manylinux 定义的规范参考,即,当且仅当auditwheel check
完成且没有错误时,车轮才符合规范。这被拒绝了,因为编写 PEP 的目的是定义工具之间的互操作性,而不是认可特定工具。
在标签字符串中添加额外的词语:我们考虑的另一个建议是在车轮标签中添加额外的词语,例如manylinux_glibc_2_17
而不是manylinux_2_17
。其动机是为将来其他类型的版本控制启发式方法留出空间——例如,我们可以有manylinux_glibc_$VERSION
和manylinux_alpine_$VERSION
。
但“manylinux”一直是“与主流基于 glibc 的发行版广泛兼容”的同义词;将其重新用于与 alpine 等无关的构建配置文件会造成更多困惑而不是帮助。此外,一些没有深入了解打包细节的早期审阅者发现单词glibc
具有误导性,误以为这意味着他们需要一个具有完全相同的 glibc 版本的系统。而像manylinux_$VERSION
和alpine_$VERSION
这样的标签也具有简洁性和直接性的优势。所以我们将采用这种方法。
来源:https://github.com/python/peps/blob/main/peps/pep-0600.rst
上次修改时间:2023-09-09 17:39:29 GMT