支持下游打包

页面状态

草稿

上次审核时间

2025-?

尽管 PyPI 和 Python 打包工具(例如 pip)是分发 Python 包的主要方式,但它们也经常作为其他打包生态系统的一部分提供。这些重新打包工作统称为下游打包(您自己的工作称为上游打包),包括 Linux 发行版、Conda、Homebrew 和 MacPorts 等项目。它们通常旨在为无法单独通过 Python 打包工具处理的用例提供更好的支持,例如与特定操作系统的原生集成,或确保与特定版本的非 Python 软件兼容。

本文旨在解释下游打包通常是如何完成的,以及下游打包者通常面临哪些额外挑战。它旨在提供一些可选指南,项目维护者可以选择遵循这些指南,从而使下游打包显著更容易(而不会给上游项目带来任何主要的维护麻烦)。请注意,这不是一个全有或全无的提议——上游维护者所做的任何事情都是有用的,即使只是一小部分。下游维护者也愿意准备补丁来解决这些问题。合并这些补丁非常有帮助,因为这消除了不同下游需要携带和不断重新应用相同补丁的需要,并避免了对相同问题应用不一致解决方案的风险。

在软件维护者和下游打包者之间建立良好的关系可以带来互惠互利。下游通常愿意分享他们的经验、时间和硬件来改进您的软件包。他们有时能够更好地了解您的软件包在实践中是如何使用的,并提供有关其与其他软件包关系的信息,否则这些信息将需要付出巨大的努力才能获得。打包者通常可以在用户在生产环境中遇到问题之前发现错误,提供高质量的错误报告,并在可能的情况下提供补丁。例如,他们会定期积极确保重新分发的软件包在发布新 Python 版本时更新任何兼容性问题。

请注意,下游构建不仅包括二进制重新分发,还包括在用户系统上完成的源构建(例如,在 Gentoo Linux 等源优先分发中)。

提供完整的源分发包

为什么?

绝大多数下游打包者更喜欢从源代码构建软件包,而不是使用上游提供的二进制软件包。在某些情况下,使用源代码实际上是软件包包含在发行版中的必要条件。对于提供通用 wheel 的纯 Python 软件包也是如此。使用源分发的原因可能包括

  • 能够审计所有软件包的源代码。

  • 能够运行测试套件和构建文档。

  • 能够轻松应用补丁,包括从项目仓库回溯提交并向项目发送补丁。

  • 能够在不受上游构建覆盖的特定平台上构建。

  • 能够针对特定版本的系统库进行构建。

  • 在所有 Python 软件包中保持一致的构建过程。

虽然通常可以从 Git 仓库构建软件包,但提供静态存档文件有几个重要原因:

  • 获取单个文件通常比使用 Git 克隆更高效、更可靠且支持更好。这可以帮助互联网连接较差的用户。

  • 下游通常使用哈希值来验证源文件在后续构建中的真实性,这要求它们随时间保持位级相同。例如,自动生成的 Git 存档不保证这一点,因为如果服务器上的 gzip 升级,压缩数据可能会改变。

  • 存档文件可以被镜像,从而减少上游和下游的带宽使用。实际构建随后可以在防火墙或离线环境中进行,这些环境只能访问本地镜像提供或先前重新分发的源文件。

  • 显式发布存档文件可以确保在创建源存档时解决对版本控制系统元数据的任何依赖。例如,自动生成的 Git 存档会省略所有提交标签信息,可能导致生成的构建中的版本详细信息不正确。

如何操作?

理想情况下,在 PyPI 上发布的源分发存档应包含软件包 Git 仓库中所有必要文件,这些文件用于构建软件包本身、运行其测试套件、构建和安装其文档,以及任何其他可能对最终用户有用的文件,例如 shell 补全、编辑器支持文件等。

这一点仅适用于软件包自身的文件。下游打包过程,与 Python 软件包管理器非常相似,将提供您的软件包及其构建脚本所需的 Python 依赖项、系统工具和外部库。但是,列出这些依赖项的文件(例如 requirements*.txt 文件)也应该包含在内,以帮助下游确定所需的依赖项,并检查它们的变化。

一些项目对 Python 包管理器使用来自 PyPI 的源分发包存在担忧。他们不希望通过这些工具未使用的文件增加其大小,或者他们根本不希望发布源分发包,因为它们会导致从源代码构建特定项目时出现问题或完全无法工作的回退。在这些情况下,一个好的折衷方案可能是为下游使用在其他地方发布一个单独的源存档,例如将其附加到 GitHub 发布中。或者,大文件,例如测试数据,可以分成单独的存档。

另一方面,一些项目(例如 NumPy)决定在已安装的软件包中包含测试。这有一个额外的好处,即允许用户在安装后运行测试,例如在升级依赖项后检查回归。另一种方法是将测试或测试数据拆分为单独的 Python 软件包。 cryptography 项目采用了这种方法,将大型测试向量拆分到 cryptography-vectors 软件包中。

一个好主意是在发布工作流中使用您的源分发包。例如,build 工具正是这样做的——它首先构建一个源分发包,然后使用它来构建一个 wheel。这确保了源分发包实际上是可用的,并且它不会意外地安装比官方 wheel 少的文件。

理想情况下,也使用源分发包来运行测试、构建文档等,或者添加特定的测试以确保所有必要的文件都已实际包含在内。可以理解,这需要更多的努力,所以不这样做也可以——下游打包者会及时报告任何缺失的文件。

构建过程中不要使用互联网

为什么?

下游构建通常在无法访问互联网的沙盒环境中进行。软件包源被解压到此环境中,并安装所有必要的依赖项。

即使情况并非如此,并且假设您已充分注意正确验证下载,出于多种原因也不鼓励使用互联网:

  • 互联网连接可能不稳定(例如,由于信号不佳)或遭受临时问题,可能导致进程失败或挂起。

  • 远程资源可能暂时甚至永久不可用,导致构建不再可能。当有人需要构建旧软件包版本时,这尤其成问题。

  • 远程资源可能发生变化,导致构建不可复现。

  • 访问远程服务器会带来隐私问题和潜在的安全问题,因为它会暴露有关构建软件包的系统的信息。

  • 用户可能正在使用数据流量有限的服务,不受控制的互联网访问可能会导致额外费用或其他不便。

如何操作?

如果软件包正在实现任何使用互联网的自定义构建后端操作,例如自动下载供应商依赖项或获取 Git 子模块,则其源分发应包含所有这些文件或允许外部供应它们,并且如果文件已存在,则不得使用互联网。

请注意,这一点不适用于软件包元数据中指定的 Python 依赖项,这些依赖项在构建和安装过程中由前端(如 buildpip)获取。下游使用利用本地供应 Python 依赖项的前端。

理想情况下,自定义构建脚本甚至不应尝试访问互联网,除非明确请求。如果缺少任何资源需要获取,它们应首先征求用户的许可。如果不可行,次优选择是提供一个选择退出开关来禁用所有互联网访问。这可以通过例如检查 NO_NETWORK 环境变量是否设置为非空值来实现。

由于下游也经常运行测试和构建文档,上述内容理想情况下也应扩展到这些过程。

另请记住,如果您正在获取远程资源,您绝对必须验证其真实性(通常是根据哈希值),以防止文件被恶意方替换。

支持针对系统依赖项进行构建

为什么?

一些 Python 项目有非 Python 依赖项,例如用 C 或 C++ 编写的库。在上游打包中尝试使用这些依赖项的系统版本可能会给最终用户带来许多问题:

  • 发布的 wheel 包要求用户系统上存在所用库的二进制兼容版本。如果库缺失或安装了不兼容的版本,Python 包可能会因对经验不足的用户不清楚的错误而失败,甚至在运行时出现异常行为。

  • 从源分发包构建需要存在依赖项的源兼容版本,以及其开发头文件和其他一些系统单独打包的辅助文件。

  • 即使对于有经验的用户,安装兼容的依赖项版本也可能非常困难。例如,所使用的 Linux 发行版可能不提供所需的版本,或者其他软件包可能需要不兼容的版本。

  • Python 包与其系统依赖项之间的链接未被打包系统记录。下一次系统更新可能会将库升级到与 Python 包不兼容的较新版本,需要用户干预才能修复。

由于这些原因,您可能合理地决定静态链接您的依赖项,或在已安装的软件包中提供本地副本。您也可以在您的源分发包中提供依赖项。有时这些依赖项也会在 PyPI 上重新打包,并可以像其他 Python 包一样声明为项目依赖项。

然而,这些问题都不适用于下游打包,并且下游有充分的理由更喜欢动态链接到系统依赖项。特别是

  • 在许多情况下,在组件之间可靠地共享动态依赖项是下游打包生态系统目的的重要组成部分。支持这一点使用户更容易以他们喜欢的格式访问上游项目。

  • 静态链接和嵌入模糊了外部依赖项的使用,使得源代码审计更加困难。

  • 动态链接使得在整个下游打包生态系统中快速系统地替换使用的库成为可能,当它们被发现包含安全漏洞或严重错误时,这一点尤其重要。

  • 使用系统依赖项使得软件包受益于下游定制,这可以改善特定平台上的用户体验,而无需下游维护者不断修补不同软件包中嵌入的依赖项。这可以包括兼容性改进和安全强化。

  • 静态链接和嵌入可能导致同一进程中加载同一库的多个不同版本(例如,尝试导入链接到同一库不同版本的两个 Python 包)。这有时可以顺利运行,但也可能导致从库加载错误到细微的运行时错误,再到灾难性故障(例如突然崩溃并丢失数据)。

  • 最后但并非最不重要的一点是,静态链接和嵌入会导致重复,并可能增加磁盘空间和内存的使用。

如何操作?

在双方需求之间的一个良好折衷是提供一个在捆绑和系统依赖项之间切换的选项。理想情况下,如果软件包有多个捆绑依赖项,它应该为每个依赖项提供单独的开关,以及一个控制默认值的通用开关,例如通过 USE_SYSTEM_DEPS 环境变量。

如果用户请求使用系统依赖项,并且某个特定依赖项缺失或不兼容,构建应该失败并显示解释性消息,而不是回退到捆绑版本。这给了打包者发现错误并有机会有意识地决定如何解决它的机会。

上游项目将其使用系统依赖项进行测试的工作留给下游重新打包者是合理的。这些指南的目标是促进上游项目和下游重新打包者之间更有效的协作,而不是建议上游项目承担下游重新打包者更擅长处理的任务。

支持下游测试

为什么?

各种下游项目都会对打包的 Python 项目进行一定程度的测试。根据具体情况,这可能包括最小的冒烟测试到完整测试套件的全面运行。这样做可能有各种原因,例如

  • 验证下游打包没有引入任何错误。

  • 在未被上游测试覆盖的附加平台上进行测试。

  • 发现只能在特定硬件、系统包版本等环境下重现的细微错误。

  • 针对比上游发布测试期间存在更新(或更旧)的依赖项版本测试已发布的包。

  • 在与生产设置密切相关的环境中测试软件包。这可以检测由不同已安装软件包之间非平凡交互引起的问题,包括不是您软件包依赖项但仍然可能引起问题的软件包。

  • 针对更新的 Python 版本(包括更新的点发布版本)或测试较少的 Python 实现(例如 PyPy)测试已发布的软件包。

诚然,有时下游测试可能会产生误报或关于上游项目不感兴趣支持的场景的错误报告。然而,或许更常见的是,它确实提供了问题的早期通知,或者发现了非平凡的错误,否则这些错误会给上游项目的用户造成问题。虽然错误确实会发生,但大多数下游打包者都在尽力仔细检查他们的结果,并帮助上游维护者分类和修复他们报告的错误。

如何操作?

上游项目可以做许多事情来帮助下游重新打包者高效且有效地测试其软件包,其中包括上面已经提到的一些建议。这些通常是使测试套件对每个人(而不仅仅是下游打包者)更可靠和更易于使用的改进。一些具体建议是:

  • 将测试文件和 fixture 包含在源分发中,或者使其能够轻松单独下载。

  • 在测试期间不要写入包目录。下游测试设置有时会在已安装的包之上运行测试,测试期间执行的修改和临时测试文件最终可能会成为已安装包的一部分!

  • 使测试套件脱机工作。使用 responsesvcrpy 等包模拟网络交互。如果不可能,则应使其能够轻松禁用使用互联网访问的测试,例如通过 pytest 标记。使用 pytest-socket 来验证您的测试是否脱机工作。这通常也会使您自己的测试工作流更快、更可靠。

  • 使您的测试在没有专门设置的情况下也能工作,或者作为测试夹具的一部分执行必要的设置。切勿假设您可以连接到数据库等系统服务——在极端情况下,您可能会使生产服务崩溃!

  • 如果您的软件包有可选依赖项,也应使它们的测试可选。如果未安装所需的软件包,则跳过它们,或添加标记以使其易于取消选择。

  • 更一般地说,为具有特殊要求的测试添加标记。这可能包括例如显著的空间使用、显著的内存使用、长时间运行、与并行测试不兼容。

  • 不要假设测试套件将使用 -Werror 运行。下游通常需要禁用它,因为它会导致误报,例如由于较新的依赖项版本。使用 pytest.warns() 而不是 pytest.raises() 来断言警告!

  • 力求使您的测试套件可靠且可复现。避免不稳定的测试。避免依赖特定的平台细节,不要依赖浮点计算的精确结果,或者操作的时序等等。模糊测试有其优点,但您也希望有静态测试用例以确保完整性。

  • 按测试目的划分测试,并使其易于跳过不相关或有问题的类别。由于下游测试的主要目的是确保包本身正常工作,因此下游通常对检查代码覆盖率、代码格式、类型检查或运行基准测试等任务不感兴趣。这些测试可能会随着依赖项升级或系统负载而失败,而实际上不会影响包本身。

  • 如果您的测试套件运行时间较长,请支持并行测试。下游通常维护大量软件包,测试所有这些软件包需要大量时间。使用 pytest-xdist 可以帮助他们避免瓶颈。

  • 理想情况下,支持通过 pytest 运行您的测试套件。pytest 有许多命令行参数对下游非常有用,例如方便地取消选择测试、重新运行不稳定的测试(通过 pytest-rerunfailures)、添加超时以防止测试挂起(通过 pytest-timeout)或并行运行测试(通过 pytest-xdist)。请注意,测试套件无需用 pytest 编写即可用 pytest 执行pytest 能够找到并执行几乎所有与标准库的 unittest 测试发现兼容的测试用例。

力求稳定的发布

为什么?

许多下游除了主软件包流之外,还提供稳定的发布渠道。这些渠道的目标是为具有更高稳定性需求的用户提供更保守的升级。这些用户通常更愿意牺牲最新功能以换取较低的问题风险。

虽然具体策略有所不同,但将新软件包版本纳入稳定发布渠道的一个重要标准是它已经在测试中存在一段时间,并且没有已知的重大回归。例如,在 Gentoo Linux 中,一个软件包通常会在测试中存在一个月后被标记为稳定,并针对当时标记为稳定的依赖项版本进行测试。

然而,有些情况需要更迅速的行动。例如,如果当前稳定渠道中可用的版本中发现安全漏洞或重大错误,下游就需要解决它。在这种情况下,他们需要考虑各种选项,例如:

  • 提前将新版本放入稳定渠道,

  • 向当前发布的版本添加补丁,

  • 甚至将稳定渠道降级到更早的版本。

这些选项中的每一个都涉及一定的风险和一定量的工作,打包者需要权衡它们以确定行动方案。

如何操作?

上游可以做一些事情来调整其工作流以适应稳定发布渠道。这些行动通常也对软件包的用户有益。一些具体建议是:

  • 根据代码更改率调整发布频率。很少发布的软件包通常每次发布都会带来重大更改,并且意外回归的风险更高。

  • 如果可能,避免混合错误修复和新功能。特别是,如果已知已合并错误修复,请考虑在合并功能分支之前进行新发布。

  • 考虑在重大更改后进行预发布,以便为愿意选择加入的用户和下游提供更多测试机会。

  • 如果您的项目正在进行非常密集的开发,请考虑拆分一个或多个包含更保守的提交子集的分支,并单独发布。例如,Django 目前除了主分支之外还维护着三个发布分支。

  • 即使您不想永久维护额外的分支,也请考虑发布带有对先前版本进行最小更改的额外补丁版本,尤其是在发现安全漏洞时。

  • 将您的更改分解为一次解决一个问题的重点提交,以便在必要时更容易地将更改挑选到早期版本。