使用Nix构建系统软件
摘要
一篇博客文章,讨论Nix如何帮助解决构建系统软件时的依赖和可重现性问题,特别是针对像BPF和io_uring这样快速演进的子系统。
<p><a href="https://lobste.rs/s/wdopf8/building_systems_software_with_nix">评论</a></p>
查看缓存全文
缓存时间: 2026/07/28 14:26
# 使用 Nix 构建(系统)软件
来源: https://hondu.co/blog/building-systems-software
刚开始编程时,有一件事让我非常沮丧:我无法花更多时间构建项目,反而要不断与那些看起来像是干扰的错误作斗争。这些巨大的时间消耗似乎仅仅是不便,阻碍我取得进展并专注于"有趣的部分"。
更糟糕的是,曾经能正常工作的东西经常会突然失效,而且原因不明。当时我甚至没有足够的知识去猜测可能发生了什么。
多年后,尽管我已经成长为一名工程师,但我发现自己还是会有类似的感受,尤其是在处理系统级软件时。昨天还能构建或运行的项目,今天就不一样了。现在我能想出一些可能的原因,测试一些理论等等,这固然不错,但这往往需要投入大量时间,更重要的是,这种工作我并不觉得有趣,而且占用了我想做的事情的时间:真正去构建软件。"有趣的部分"。
## 没错,又一个移动靶
现代大多数编程语言都自带包管理器(bundler, cargo, npm 等),它们会获取依赖,并在其他功能中尽力确保这些依赖不会在用户不知情的情况下被更改。这对安全至关重要,也*有助于*保持跨机器的一致性。相比不久之前大多数人的构建方式,这已经是一个巨大的进步。
然而,这并非万无一失,因为还有其他可变的来源:编译器或解释器版本、系统提供的库、环境变量等。
如果这还不够,当我们处理系统级软件时,我们可能会使用原生库,并以多数其他软件从未有过的方式调用操作系统!表面面积越大,我们依赖的软件随行为变化的可能性就越高。
那我们的操作系统内核呢?从抽象的角度看,它是代码与之交互的另一个"库"。这就引出了 Linux 内核中一些发展最快的部分:BPF (https://ebpf.io/) 子系统、花哨的新 sched_ext (https://sched-ext.com/) 调度器、io_uring (https://unixism.net/loti/) 等等。
就整个操作系统而言,一次看似常规的系统更新可能会更新编译器或链接器等工具链的一部分。你的代码所使用的系统库可能会改变,甚至内核本身也会改变!这可能导致你的代码无法构建,更糟糕的是,改变其行为。
## 每样来一份
我没有查过数据,但如果说 BPF 生态不是 Linux 中发展最快的生态系统之一,我会觉得很惊讶。这很棒,因为这意味着每个内核版本都带来了令人兴奋的新特性和修复,但这也会给我们的开发(和生产!)环境带来一些"辣味"。此外,对于一个基于 libbpf (https://github.com/libbpf/libbpf) 并用 Rust (https://github.com/libbpf/libbpf-rs) 编写的基本应用,我们依赖于:
- Rust 编译器(`rustc`)和相关工具(`cargo`);
- C 编译器(`clang`)。通常 BPF 代码用 clang 编译,但大多数 Linux 系统对其他原生代码使用 `gcc`;
- 原生依赖(`libelf`、`zlib`、`libbpf`、`libc`);
- Rust 依赖;
- Linux 内核头文件(`vmlinux.h`),如果在构建时生成,还需要系统的 BTF 信息和 `bpftool` 来生成头文件;
- BPF 工具的内核侧(系统调用层、验证器等);
- 静态链接器;
- 动态加载器(`ld.so`);
- 环境(`env`);
如果我要赌这个列表是否完整,我大概会输。但希望它足以说明我们面临的问题。
**我们离软件依赖大部分可能发生变化,只有一个 `apt`/`dnf`/`你的包管理器` 的差距**。默认情况下,方程式中只有一部分是完全可以理解并受我们控制的:Rust 依赖(假设 SCM 中有 Cargo.lock)。也许还有 `libbpf`,因为默认情况下它是被 vendored 的。
如果你运气不好,查找差异的重担就落在你身上了。通常,弄清楚系统的先前状态是一项艰巨的任务。
## 驯服依赖混沌
现代依赖管理器背后的核心思想之一是包含锁文件。它们代表了某个时间点被"锁定"的依赖的序列化视图。除其他数据外,这包括包的位置、精确版本以及确保未被篡改或损坏的校验和。
如果我们能将其应用于上面列表中的所有项目,那不是很棒吗?事实证明,这个想法有不止一种实现,其中之一就是 Nix (https://en.wikipedia.org/wiki/Nix_(package_manager))。Nix 是:
- 一个包管理器(提供库、二进制文件等);
- 一个构建系统(关于如何构建代码的说明);
- 用于上述内容的编程语言;
- 一个 Linux 发行版 NixOS,基于同名的包管理器和构建系统;
- 支持上述内容的基础设施,如构建农场、缓存等;
这里我们指的是包管理器和构建系统部分,它们适用于大多数 Linux 发行版和其他操作系统,如 macOS。事实上,虽然我已经使用这些将近两年了,但我还是 NixOS 的新手。网上有很多比我更了解 Nix 的人提供的关于 Nix 语言、开发环境、底层工作原理等内容。
对于简化的心智模型,我们需要知道的有用部分包括:"官方" Nix 包位于一个叫做 nixpkgs (https://github.com/NixOS/nixpkgs) 的仓库中,如果我们想要像 Cargo.lock 那样的东西用于 Nix,我们可能想使用 flake (https://nixos.wiki/wiki/Flakes)。通过使用 flake,我们可以选择 nixpkgs 仓库的特定修订版,我们使用的每个依赖都将匹配其中定义和构建的内容。
## 使用 Nix 的 Rust + BPF 环境
查看 `runqslower`,它的 Nix flake 类似于我使用的一个 (https://github.com/javierhonduco/blog/tree/b69d44d6be6c953b7868f715bacc35bfdaab1760/code-samples/building-systems-software)。我添加了足够的注释,让它对新手更易理解,但也尽量不显得啰嗦。
一旦安装了 Nix,只需一个简单的 `nix develop` 就能创建一个包含所有所需软件的环境。最棒的是,它当然是可重现的。如果它现在能构建并运行,那么明天也可以。如果不是这样,我们就知道是由于 Nix 和 Cargo 控制之外的其他环境变化造成的。
## 实际例子
这些都很好,但没有什么比具体例子更能说明这个设置对我的帮助了:
### 不再有"在我的机器上能运行"
使用 Nix 的项目的贡献者,尽管运行着不同的发行版或操作系统,但拥有一个非常少出问题的一致开发环境。
### 二分查找变更变得更容易
由于包管理器和构建系统紧密相关,二分查找变更可能比你习惯的更容易一些。一个实际的例子是,我的某个项目的 BPF 代码在使用 clang 19 编译时能正常加载到我的内核中,但如果使用 clang 21 则不被接受。
选择不同 clang/LLVM 版本的代码不超过 10 行,很快就能发现是 BPF 的 ISA v3 成为默认 (https://github.com/llvm/llvm-project/commit/7852ebc088b925ef1c1940cbd56a93d9f8e3e330) 导致了某些旧内核上的问题。
### 更好的默认设置
我使用的一个用 C++ 编写的项目在生产环境中崩溃了,但仅在 Nix 下出现。这是因为 Nix 的安全策略比其他软件打包者更严格,并且默认使用 `-D_FORTIFY_SOURCE=2`。本应极其难以发现、甚至可能导致数据损坏的未定义行为,在这个标志下以清晰的崩溃呈现。这个问题已向上游分享,修复已在几个月前推出。
### 无需痛苦的依赖定制
无需修补我不熟悉的构建系统。此外,Nix derivations(类似于 buck2/bazel 中的规则)组合得很好。
### 更快的构建
由于它是一个真正的构建系统,只重建必要的内容。如果你对这个概念不熟悉,可以想象你最喜欢的电子表格软件,单元格引用其他单元格。只有需要重新计算的数据才会被刷新。
另一个速度来源是 Nix 和其他构建系统由于可重现性而实现的智能缓存。clang/LLVM 的二分查找没有重建数百万行代码,因为有很多缓存命中。
## 内核本身呢?
这篇文稿有点长了,如果有兴趣的话,我或许可以在未来的文章中谈谈这个。
## 结论
这个设置帮助我专注于最能给我带来快乐的部分,并且基本没有给我添麻烦,这正是我希望在构建系统和包管理器中得到的。
Nix 生态和工具非常庞大,我只知道其中一小部分,但我期待继续使用它并学习 NixOS。
*注*:本文未使用 AI 写作。
相似文章
后现代构建系统
一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。
Guix Nix 的怪异融合:在 Nix 中利用 Guix 派生
一项技术探索,展示了 Nix 如何构建 Guix 派生项,强调了共享底层“输入输出机”架构以及跨生态系统互操作的可能性。
不到100行代码实现nix-build
本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。
从 Proxmox 迁移到 NixOS 和 Incus
作者描述了将其家庭实验室从 Proxmox 迁移到使用 Incus 的 NixOS 的过程,强调了声明式配置和可重现性相对于命令式系统的优势。
我喜欢的 NixOS 声明式安装方式
一份关于使用 nixos-anywhere 等工具通过网络声明式安装 NixOS 的指南,重点强调在版本控制下管理配置文件。