不到100行代码实现nix-build
摘要
本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。
<p><a href="https://lobste.rs/s/gig3cr/nix_build_under_100_lines">评论</a></p>
查看缓存全文
缓存时间: 2026/06/22 01:29
# 不到100行代码实现 nix-build 来源:https://fzakaria.com/2026/06/21/nix-build-in-under-100-lines 发布于 2026-06-21,Farid Zakaria 的博客 (https://fzakaria.com/)
我之前说过,[Nix 是一个谎言](https://fzakaria.com/2026/03/07/nix-is-a-lie-and-that-s-ok),在那些繁文缛节之下,Nix 实际上只是一个[输入输出机器](https://fzakaria.com/2026/06/05/the-guix-nix-abomination-leveraging-guix-derivations-in-nix)。`nix` 守护进程*感觉*像一个黑盒子。你输入 `nix build`,然后在某个 Unix 套接字背后,一个特权进程执行着难以捉摸的操作,另一端就弹出了 `/nix/store` 中的一个路径。🪄
如果我告诉你,每个人认为是魔法的那部分——把 derivation 变成 store 路径——几乎就是一个 `exec`,你会怎么想?让我们用不到 100 行 Go 代码重新实现 `nix-build`。
首先,derivation 到底是什么?一个 derivation(`.drv`)只是一个构建计划。让我们实例化一个你能想象到的最无聊的 derivation。
```nix
# hello.nix
derivation {
name = "hello";
system = builtins.currentSystem;
builder = "/bin/sh";
args = [ "-c" "echo 'Hello World' > $out" ];
}
```
```
$ nix derivation show $(nix-instantiate hello.nix)
```
```json
{
"derivations": {
"gifgxsqfsjg8pxna1kv0nbzz1zvivs0b-hello.drv": {
"args": [
"-c",
"echo 'Hello World' > $out"
],
"builder": "/bin/sh",
"env": {
"builder": "/bin/sh",
"name": "hello",
"out": "/nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello",
"system": "x86_64-linux"
},
"inputs": {
"drvs": {},
"srcs": []
},
"name": "hello",
"outputs": {
"out": {
"path": "ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello"
}
},
"system": "x86_64-linux",
"version": 4
}
},
"version": 4
}
```
这就是*全部*内容。一个要运行的程序(`builder` + `args`),一个环境(`env`),它必须产生的输出,以及它依赖的其他 derivation(`inputDrvs`),这里为空。没有魔法 🪄。
所以,“实现”(realising)一个 derivation 只需要四步:
1. 首先递归实现它的 `inputDrvs`。这*就是*构建图。
2. 将环境清理到一组已知变量。
3. 设置 `$out` 为构建必须创建的 store 路径。
4. `exec` builder 并检查它产生了 `$out`。
下面是整个 Go 程序,不到 100 行(不包括注释 😉)。你可以在这里 (https://gist.github.com/fzakaria/1be0657cc5f10df5e45d4ff1574b0273) 找到源码。
> **注意**我*稍微*偷了点懒,没有为 Nix 的 ATerm 格式 (https://nix.dev/manual/nix/2.25/protocols/derivation-aterm) 编写解析器,而是利用 `nix show derivation` 获取 JSON 等效内容。
build.go
```go
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
const store = "/nix/store"
type drv struct {
Args []string `json:"args"`
Builder string `json:"builder"`
Env map[string]string `json:"env"`
Inputs struct {
Drvs map[string]any `json:"drvs"`
} `json:"inputs"`
Outputs map[string]struct {
Path string `json:"path"`
} `json:"outputs"`
}
func exists(path string) bool {
_, err := os.Stat(path); return err == nil
}
// storePath 将 store 路径设为绝对路径;Nix 的 JSON 使用裸的基本名称。
func storePath(p string) string {
if strings.HasPrefix(p, "/") {
return p
}
return store + "/" + p
}
// loadDrv 通过 shell 调用 Nix 将 .drv 转换为 JSON,然后解码。
func loadDrv(path string) (error, drv) {
data, err := exec.Command("nix", "--extra-experimental-features", "nix-command", "derivation", "show", path).Output()
if err != nil {
return err, drv{}
}
var doc struct {
Derivations map[string]drv `json:"derivations"`
}
if err := json.Unmarshal(data, &doc); err != nil {
return err, drv{}
}
for _, d := range doc.Derivations {
return nil, d // 恰好一个条目:我们请求的 derivation
}
panic("no derivation found for " + path)
}
// realise 确保 derivation 的输出存在,先构建其输入,
// 并返回默认输出的 store 路径。
func realise(path string) (error, string) {
err, d := loadDrv(path)
if err != nil {
return err, ""
}
out := storePath(d.Outputs["out"].Path)
if exists(out) {
return nil, out // 已构建(这也记忆化了共享依赖)
}
for dep := range d.Inputs.Drvs {
realise(storePath(dep)) // 递归:先依赖后依属
}
fmt.Fprintln(os.Stderr, "building", out)
tmp, err := os.MkdirTemp("", "simple-nix-")
if (err != nil) {
return err, ""
}
defer os.RemoveAll(tmp)
// 构建的整个环境:几个固定变量,derivation 自身的属性,
// 以及每个输出一个变量(这就是 $out 的来源)。
// 这些固定变量及其值由 Nix 手册指定:
// https://github.com/NixOS/nix/blob/f8bb823a23bf6d62f4c8feb792a77702d7a49fe1/doc/manual/source/store/building.md?plain=1#L154
env := map[string]string{
"PATH": "/path-not-set",
"HOME": "/homeless-shelter",
"NIX_STORE": store,
"NIX_BUILD_TOP": tmp,
"TMPDIR": tmp,
"TEMPDIR": tmp,
"TMP": tmp,
"TEMP": tmp,
}
for k, v := range d.Env {
env[k] = v
}
for name, o := range d.Outputs {
env[name] = storePath(o.Path)
}
cmd := exec.Command(d.Builder, d.Args...)
cmd.Dir, cmd.Stdout, cmd.Stderr = tmp, os.Stderr, os.Stderr
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
if err := cmd.Run(); err != nil {
return err, ""
}
if !exists(out) {
panic(fmt.Sprintf("builder did not produce %s", out))
}
return nil, out
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: simple-nix ...")
os.Exit(2)
}
for _, arg := range os.Args[1:] {
fmt.Println(realise(arg))
}
}
```
就这些。它能用吗?
```
$ go build -o simple-nix .
$ ./simple-nix $(nix-instantiate hello.nix)
building /nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello
/nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello
$ cat /nix/store/ddmbmrgzcqqp0b8i9gmzav8zs8ch3176-hello
Hello World
```
我们甚至可以构建一个真实世界的 derivation。
```
$ ./simple-nix $(nix eval nixpkgs#hello --raw)
Using versionCheckHook
Running phase: unpackPhase
unpacking source archive /nix/store/wj7phsmi7ncidl8k00p489krqss7n9sd-hello-2.12.3.tar.gz
source root is hello-2.12.3
setting SOURCE_DATE_EPOCH to timestamp 1773804383 of file "hello-2.12.3/ChangeLog"
Running phase: patchPhase
Running phase: updateAutotoolsGnuConfigScriptsPhase
Updating Autotools / GNU config script to a newer upstream version: ./build-aux/config.sub
Updating Autotools / GNU config script to a newer upstream version: ./build-aux/config.guess
...
```
那么,*缺少*了什么?说实话,很多,**但是**没有一样是那个把 derivation 变成路径的部分。
- **沙盒**:Nix 在挂载/网络/PID 命名空间中运行 builder,以保证安全性和隔离性。
- **数据库**:Nix 在 SQLite 数据库中记录每个有效路径及其引用。我们只检查文件是否存在。
- **替换**:Nix 向二进制缓存询问 derivation 是否已构建。
- **其他所有**:多输出路径、固定输出 derivation(`fetchurl`)支持、垃圾回收等。
Nix 的美妙之处在于 derivation 是一个纯函数。获取 store 路径不是魔法。它就是带有干净环境的 `exec`。其他东西,*基本上*都是记账和安全措施。
---
改进此页面 @ 212fbd5 (https://github.com/fzakaria/fzakaria.com/tree/212fbd51833fe59017e627293f89671335b0e76e/_posts/2026-06-21-nix-build-in-under-100-lines.md)
本网站的内容采用 CC-BY-SA (https://creativecommons.org/licenses/by-sa/2.0/) 许可。
相似文章
Guix Nix 的怪异融合:在 Nix 中利用 Guix 派生
一项技术探索,展示了 Nix 如何构建 Guix 派生项,强调了共享底层“输入输出机”架构以及跨生态系统互操作的可能性。
使用Nix构建系统软件
一篇博客文章,讨论Nix如何帮助解决构建系统软件时的依赖和可重现性问题,特别是针对像BPF和io_uring这样快速演进的子系统。
Nix for Haskell: 静态构建
本教程介绍如何使用 Nix 为 Haskell 项目创建静态链接的可执行文件,涵盖 GHC 的静态构建配置以及与 Docker 的集成。
后现代构建系统
一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。
Derivations to Deployments: Practical Nix in Production
这篇演讲介绍了如何用 Nix 实现从开发环境、构建到云部署的全面可复现性,并展示了 Antithesis 基于 Nix 的“命令集”框架来替代杂乱脚本。