跳至主要內容

二进制分发与源码分发

NixOS-CN大约 5 分钟

二进制分发与源码分发

二进制分发从字面意思上就很容易理解,软件包仓库开放预编译的二进制包,并供包管理器下载并部署到目标主机。

源码分发则是从各处拉取源码,下载编译,再部署到目标主机。看起来只是将编译步骤挪到了本地进行而已,但却给了软件包部署更多灵活性。

以 Debian 作为二进制分发的例子

Debian 是 GNU/Linux 世界中采用二进制分发的优秀发行版之一,APT 则是它最流行的包管理器之一。

APT包管理器
APT包管理器

为了从远程获取预编译的二进制包,APT 需要从三个地方读取源列表:

  • /etc/apt/sources.list
  • /etc/apt/sources.list.d/*.list
  • /etc/apt/sources.list.d/*.source

然后访问源列表里面的 URL ,获取软件包的元数据(名称,版本,依赖信息等)。这些元数据通常被存储在 /var/lib/apt/lists/

只要有元数据的协助,APT 很快就能推算出你要下载的包依赖哪些其他的包,并从软件包仓库开始下载需要的一揽子软件包。这些包被缓存到 /var/cache/apt/archives/

Deb 包里面只会包含二进制文件,以及控制这些二进制文件该释放至何处的描述文件。假如你想对这个软件包搞点个性化定制,自己再克隆源码,修改源码,编译和打包一条龙。你不能从官方包仓库的软件包直接衍生变体。

不同的打包方式带来了 Linux 软件包生态的分裂。你从 Debian 系发行版和 RHEL 系发行版就能观察到 “血脉兼容” 的现象。

提示

还存在一个 Linux 时间线项目open in new window,你可以看到宏观的 Linux 衍生历史。

以 Gentoo 作为源码分发的例子

Gentoo 似乎是另一个极端。它的包管理器叫做 Portage,它从官方的软件仓库获取的是 ebuild 文件,而这些文件会被缓存到 /var/db/repos/gentoo/ 下。

你可以将 ebuild 理解为一种构建软件的脚本。它包含了一些函数和变量,用来指定软件包的元数据、依赖关系、源码地址、编译选项、安装步骤等信息。Portage 会根据 ebuild 中的指令来下载、编译和安装软件包。

Portage 包管理器
Portage 包管理器

为了让这个解释更生动点,我在这里open in new window超链接了一个 ebuild 文件,你可以试图理解其中内容。

获取到源码以后,Portage 开始了繁忙的编译流程。它需要读取一些编译变量,而它们在 /etc/portage/make.conf 这个配置文件里。 其中包括全局的 USE 标志、编译选项、镜像源、许可证等。这些变量可以控制 Portage 的功能和性能,也可以根据用户的需求进行优化和定制。

每一台 Gentoo 主机都可能有不同的编译选项,因此编译出来的产物也不一致,它们已经是一种变体了

另外,许多人喜欢 Gentoo 的原因就是可以更极致地压榨运行性能。但是频繁的编译是相当耗费时间的。后面 Gentoo 又支持了分布式编译,可以让你的其他电脑分担工作量,不过依然是饮鸠止渴。

Nix 之道

Nix 是 NixOS 的包管理器,它是跨平台的,可以在 Linux 平台和 Darwin 平台使用。

二进制分发可以保持包的一致性,使本应正常依赖的包可以正常工作(不过需要包管理器控制版本);源码分发可以保持包的灵活性,可以自由拓展软件功能,衍生软件变体。有什么办法可以鱼与熊掌兼得呢?

Nix 则使用一种函数式语言来描述软件包及其依赖关系,每个软件包都被视为一个纯函数的输出,这个输出是 /nix/store/ 下一个带有哈希值的目录。

我明白上面的说法对许多人来说太过抽象,所以我又准备了例子。

/nix/store/ 下的哈希路径是根据软件包的表达式(描述打包的 Nix 文件)和构建过程(参与构建的参数)来计算出来的。每个软件包都有一个唯一的标识符,它由一个哈希值和一个包名组成,例如:

/nix/store/7wzgkjk6l9ng015wnx7dbzq73v4yr97g-nyancat-1.5.2

其中,7wzgkjk6l9ng015wnx7dbzq73v4yr97g 是一个 160 位的 SHA-256 哈希值,它是根据软件包的表达式和构建过程的所有输入参数计算出来的。这些输入参数包括:

  • 源码或二进制文件的 URL 和哈希值
  • 版本号和包名
  • 依赖关系和构建工具
  • 编译命令和参数
  • 补丁和修改
  • 元数据和测试

这些输入参数都会被转换成一个 Nix 语言的表达式,然后被序列化成一个字符串,再用 SHA-256 算法计算出一个哈希值。这个哈希值可以保证软件包的一致性和可复现性,因为只要输入参数不变,就会得到相同的哈希值和相同的软件包。

纯函数:唯一的输入有确定的唯一的输出,函数求值不依赖外部也不影响外部
纯函数:唯一的输入有确定的唯一的输出,函数求值不依赖外部也不影响外部

而哈希路径下的文件夹存放着软件包的所有文件,包括可执行文件、库文件、配置文件、文档文件等。这些文件都存放在 /nix/store/ 目录下,而不是在系统的其他目录下,例如 /usr/bin//bin/ 等。这样每个软件包之间都是隔离的,管理也是异常方便。

即使是这样,还是没有解决编译时间久的问题。于是社区提供了许多缓存构建主机,包管理器会优先从上面下载已有的构建结果。除非访问完了缓存主机列表也没找到需要的二进制缓存,才会自己拉取源码构建。