作者 | PolyWolf 译者 | 核子可乐 策划 | Tina
的设计要点,这让我对软件打包有了新的认识。根据我的推测,亚马逊的构建系统“Brazil”在原理上与Nix/NixPkgs有些相似,它基于声明式的方式构建几乎所有现有的软件包,并具备完全可重现性的能力。但是,与Nix类似,Brazil不仅允许为软件包的不同版本创建独立的快照,还可以通过单元测试来确保这些版本在创建新的不可变构建时彼此兼容,从而提供了一个可靠的最终更新版本。亚马逊真是太厉害了!而且,Brazil还具备以下特性:
这些特性真是太令人惊叹了!我的朋友在亚马逊工作,对此给予了高度评价,认为软件构建从未如此简单。实际上,这真的很难相信:
主构建驱动会使用Perl脚本生成大量的Makefile。整个构建系统只由最小的Perl脚本引导,这个脚本假设环境中只包含最基本的Perl依赖和GCC,并下载所有其他依赖项。但是,既然他们说可以实现,那肯定是可以实现的!
然而,大多数软件包并不是这样的。在开始讨论之前,让我们先明确一些术语的定义:
根据我所了解,目前有两种常见的方法来分发软件包并创建运行环境。当然,还有其他方法,但是我们先讨论最典型的情况。
一种方法是共享一切。这是软件环境的典型模型,例如Arch Linux、RHEL、pip、npm、Homebrew、Forge等等。虽然它们在更新频率、semver固定原理和所负责的工作方面各有差异,但我列出的所有示例都具有共同特征。
老实说,这种模型相当差劲。能够正式安装的软件包只能有一个版本,这确实太少了。如果想在中央版本集之外保留一个包含某个依赖项的构建版本,只有以下三种办法:
- 重新命名这个依赖项,然后进行全局安装。
- 在包管理器的控制范围之外“安装”这个依赖项。
- 直接放弃。
第一种选项太蠢了,因为这意味着我们必须自己将接口/build版本指定为包名称,而这类版本区分的工作本来应该由包管理器负责。第二种选项也很蠢,因为虽然我们有了好用的包管理器,但仍然需要使用CMakeLists.txt和shell脚本来进行滚动更新。第三种选项更不可取,因为作为开发者,我们不能轻易放弃。
有时候,我们可以允许软件包拥有自己的依赖项范围,毕竟并不是所有东西都需要全局化。但是,目前这种糟糕的本地安装支持实在让人无法接受。因此,让我们再来看看另一个极端情况:完全不共享。
如果一个包有依赖项,可以将这些依赖项以自包含的方式放入环境中。目前有多种方法可以让单独安装的软件包融入同一环境。但是,如果没有包管理器的支持,这些方法要么缺乏可扩展性(这是最好的情况),要么会引发令人恼火的错误。令人奇怪的是,消费级操作系统如Windows和MacOS居然将这种方式作为默认方法。更奇怪的是,最近容器化技术如Docker、Snap、Flatpak的普及也使得Linux软件开始以这种模式进行分发。这是为什么呢?
我猜测这种模式之所以流行起来,是因为它更有利于产生一致的软件。Linux发行版长期以来一直面临的一个主要问题是“在我的机器上可以运行,在我的发行版上却无法运行”的不一致性。如果一切都共享,那么只要在全局版本集之外进行尝试,甚至是在同一发行版的不同时间点,软件包的构建就可能出现令人沮丧的意外。正因为如此,具有虚拟环境的特定语言包管理器选择了完全不共享的方式,而Docker之所以如此受欢迎,也是因为这个原因。全局环境不可避免地存在“幽灵”,这些无形的依赖项会随时侵扰构建过程,因此隔离一切并消除“幽灵”是实现可复现性的前提条件。
当然,完全不共享的方法也有自己的缺点。要求软件包将所有依赖项都捆绑在一起,建立内部的“共享一切”小环境会导致体积迅速膨胀。我个人不太愿意在我的机器上重复安装5个Tensorflow或PyTorch的副本,但我也不想将所有一次性的AI项目都塞进同一个全局Python环境,所以情况就很尴尬了。
那么,有没有更好的方法呢?
让我们总结一下理想构建系统的基本要求:
很明显,前面介绍的两种常见方法都无法满足要求,甚至可以说差距很大!也就是说,目前的软件包分发机制存在根本性的缺陷,导致我们陷入困境。
但是,亚马逊Brazil的出现让我们看到了希望。它不仅允许隔离各个软件包并分别指定其依赖项,而且能够稳定地复现构建过程,甚至可以让各个包共享具有相同接口版本的依赖项!这真是太棒了!那么,亚马逊是如何做到的呢?
从技术挑战的角度来看,其实并没有现成的解决方案并不是因为做不到。各种主流操作系统已经能够很好地隔离不同层级的环境,为什么软件包就做不到呢?
而从社会挑战的角度来看,最大的问题可能并不是技术,而更多地来自于人们的漠不关心。开发者和发行版贡献者大多认为“为什么要改变我构建软件的方式?目前的方案对我的用例已经足够了!”
就我个人而言,我也曾在与预期环境略有不同的环境中构建过许多软件,而且深受其害。每个包都有自己的脚本、命令行标志、环境变量和构建目录,这一切都让工作充满了不确定性。正如Brazil项目下的一位评论者所说:
根据我的经验,Brazil的打包概念之所以没有普及,是因为之前的问题还没有严重到需要改变的临界点。亚马逊有Brazil,可以轻松处理Gem、NPM包、*.so或JAR等依赖项。所以,即使要经历一些痛苦(特别是在引入新的构建系统时),问题总是可以解决的。而且,在打包完成后,这个问题就过去了。
只有那些闲着没事干的书呆子才愿意为此构建一个完整的生态系统。Gentoo、NixPkgs、Guix、AUR的软件包维护者们各自拿出自己的神器,试图让整个软件世界臣服于他们的脚下。因此,在同一个系统内“一切正常运行”,但对于我们这些不幸需要在不同系统之间进行开发的人来说,这只会带来无休止的噩梦。什么都可能出问题,什么都无法顺利实现,而且没有人愿意真正投入时间和精力来构建一个整体解决方案。虽然问题是可以解决的,但我们只能忍受下去……
亚马逊是如何做到的呢?
简而言之,他们选择了花钱解决问题。这些钱用于处理每个依赖项传递中的计算成本,用于确保接口版本符合semver标准,用于存储完整的软件历史记录(包括源代码和二进制文件),以防止旧的构建版本丢失。最重要的是,亚马逊愿意支持开发人员将他们想要使用的所有软件移植到这个构建系统中。
因此,这种方法只适用于像亚马逊这样的科技巨头,因为对他们来说,这种投入绝对物有所值。但是,对于我们其他人呢?
我们能否学习一些经验呢?
老实说,我也不知道。也许NixPkgs和Guix在某种程度上接近我所期望的效果,能够满足我对理想构建系统的要求(当然,除了semver固定这种需要大量资金的要求)。我没有太多使用它们的经验,所以无法评价它们的使用体验。但是,一方面我听说过关于NixPkgs的抱怨,另一方面我几乎没有听说过Guix,这两种情况似乎都不太理想。
作为个人,我也没有能力去迎难而上。我已经习惯了生活在噩梦的阴影下,通过修修补补的方式维持我的Windows开发环境,短时间内这种情况很难改变。但是,我认为整个技术社区应该共同努力去迎难而上,这样即使我手头的Arch安装仍然存在许多问题,下一次安装Linux时就能拥有稳定的可复现性。我希望更多的人能和我有着相同的期望。