不到100行代码实现nix-build

Lobsters Hottest 工具

摘要

本文通过用不到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/) 许可。

相似文章

使用Nix构建系统软件

Lobsters Hottest

一篇博客文章,讨论Nix如何帮助解决构建系统软件时的依赖和可重现性问题,特别是针对像BPF和io_uring这样快速演进的子系统。

Nix for Haskell: 静态构建

Lobsters Hottest

本教程介绍如何使用 Nix 为 Haskell 项目创建静态链接的可执行文件,涵盖 GHC 的静态构建配置以及与 Docker 的集成。

后现代构建系统

Lobsters Hottest

一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。