一个随机构建图

Hacker News Top 工具

摘要

本文探讨了Nix中的动态推导如何将其构建系统从应用式转向单子式,从而允许构建图在构建过程中定义,而不是预先定义。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/22 12:51

# 一个掷骰子的构建图 来源:https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice 我们即将迎来 NixCon 2026 (https://2026.nixcon.org/)。又是一年,很遗憾我将缺席。我看了看演讲列表,发现有不少关于动态推导的报告,再加上最近关于该主题的其他帖子,例如 cargo-dyndrv (https://blog.obsidian.systems/cargo-dyndrv-a-beginning/),这让我开始思考重新审视这个话题,因为我的早期帖子 (https://fzakaria.com/2025/03/10/an-early-look-at-nix-dynamic-derivations) (https://fzakaria.com/2025/03/11/nix-dynamic-derivations-a-practical-application) 涉及过该主题。我想更好地理解动态推导的含义,以及它们如何改变构建图。最初,我关注的是它如何能取代 *lang2nix* 风格的构建图生成,但我意识到其影响远不止于此。构建图无需预先知晓。它可以在构建过程中定义。 🧐 我是什么意思? 传统上,Nix 要求你请求它构建任何东西,它都会提前告诉你它将采取的确切步骤,这些步骤构成了构建图的“推导”。 ``` $ nix-store -q --requisites \ $(nix-instantiate '' -A hello) | grep -c '\.drv$' 196 ``` 这是 Nix 的一个重要特性。它使得无需运行构建就能理解将*会*构建什么,从而对构建进行推理成为可能。许多 Nix 工具通过 `nix build --dry-run` 依赖这一特性。例如,nix-diff (https://github.com/Gabriella439/nix-diff) 可以告诉你两个闭包为何不同**而无需构建其中任何一个**。 为什么动态推导会改变这一点? ## § (https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice#applicative-monadic-bind) 应用函子、单子、绑定 在函数式语言中,很容易变得抽象,使用“花哨”的词汇如“应用函子”和“单子”来描述不同计算类型之间的区别。这种区别很微妙,但意义深远。 Nix 传统上是一个“应用函子”构建系统。动态推导使其成为了一个“单子”构建系统。 区别在于:在应用函子构建系统中,整个构建图是预先知道的;而在单子构建系统中,构建图可以在构建过程中定义。¹ ¹ 论文《Build Systems à la Carte》(https://www.microsoft.com/en-us/research/uploads/prod/2018/03/build-systems.pdf) 是该主题的事实标准读物。 思考这种差异的一种方式是看定义它们的两个操作的类型签名: ``` <*> apply :: f (a -> b) -> f a -> f b >>= bind :: m a -> (a -> m b) -> m b ``` `apply`(应用函子)接受 `f a`,一个预先知道的值,并返回结果 `f b`。你可以在运行前看到整个图。 `bind`(单子)接受 `(a -> m b)`,一个*函数*,来返回 `m b`。你无法在运行前看到整个图。 **应用函子**可以理解为在离开家之前写下购物清单。你阅读食谱,写下每一种配料,开车去商店一次。清单取决于食谱,仅此而已。 **单子**则像是一个食谱,其中一个步骤写着*尝一尝,如果太咸就去买个土豆*。你无法预先写出那份购物清单。是否要买土豆取决于咸淡程度,而在你开始烹饪之前,咸淡程度是不存在的。 Nix 曾经仅是前者,而动态推导加入了后者。 ## § (https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice#actually-nix-always-had-bind) 实际上,Nix 一直都有绑定 (https://fzakaria.com/assets/images/always_has_been_bind_meme.png) 好吧,我想我应该说“Nix 现在在调度器中是单子的”。Nix 在求值器中一直都有绑定。我们多年来一直在进行单子构建。我们称之为从推导导入 (import from derivation) (https://fzakaria.com/2020/10/20/nix-parallelism-import-from-derivation)。 ```nix let inner = pkgs.runCommand "inner" {} "sleep 10; echo hi > $out"; in pkgs.runCommand "outer" {} "echo ${builtins.readFile inner} > $out" ``` 对推导输出使用 `builtins.readFile` 是一个**绑定**,其含义完全如上所述:下一步构建什么取决于一个尚不存在的值。Nix 求值器在没有该值的情况下无法生成构建图,而获取它的唯一方法就是停止求值并运行一个构建器。不幸的是,这有很多陷阱,例如导致 `nix-instantiate` 耗时十秒 (https://fzakaria.com/2025/03/10/an-early-look-at-nix-dynamic-derivations),也是为什么 nixpkgs *完全禁止*这种技术。 如果说*从推导导入*是求值器中的绑定,那么动态推导现在在调度器中增加了绑定。区别在于调度器可以并行运行构建器、将它们发送到远程机器,并且可以从缓存中替换它们的结果。求值器无法做到这些。 动态推导并没有增加绑定。绑定一直都在。改变的是哪个层执行它。 🤓 ## § (https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice#lets-roll-some-dice) 让我们掷一些骰子 许多动态推导的例子都集中在通过 *lang2nix* 工具生成构建图,但我想更一般地探讨动态推导的含义。在真正的*单子*意义上,构建图可以在构建过程中定义。构建图的下一步可以依赖于前一步的结果。 这里是一个非常简单的例子,`chain.nix` 和 `step.sh`,它掷一个骰子,然后要么停止,要么通过向构建图添加一个新的推导步骤来继续链条。链条的深度是随机的,构建结果是我们达到的深度。 当你运行 `nix-instantiate` 时,只有最左边的方框存在。它右边的所有内容都是由构建器在构建进行中时写入的。 (https://fzakaria.com/assets/graphviz/219c1cc35d20e5b6.svg) **chain.nix** ```nix { depth ? 1 , pkgs ? import <nixpkgs> { } , bash ? pkgs.bash , coreutils ? pkgs.coreutils , nix ? pkgs.nixVersions.latest , chain ? ./chain.nix , step ? ./step.sh }: let roller = derivation { name = "step-${toString depth}.drv"; system = builtins.currentSystem; builder = "${bash}/bin/bash"; args = [ "-e" "${step}" ]; DEPTH = toString depth; BASHPKG = "${bash}"; COREUTILS = "${coreutils}"; NIXPKG = "${nix}"; CHAIN = "${chain}"; STEP = "${step}"; PATH = "${coreutils}/bin:${nix}/bin"; # 构建器实例化推导,因此它需要一个存储来通信。 requiredSystemFeatures = [ "recursive-nix" ]; # 此推导的输出是一个 .drv 文件。这正是它成为动态的原因。 __contentAddressed = true; outputHashMode = "text"; outputHashAlgo = "sha256"; }; in builtins.outputOf roller.outPath "out" ``` **step.sh** ```bash set -eu export NIX_CONFIG='experimental-features = nix-command ca-derivations dynamic-derivations' roll=$(( $(od -An -N1 -tu1 < /dev/urandom) % 6 + 1 )) echo "depth $DEPTH rolled a $roll" if [ "$roll" -eq 6 ]; then # 六点。停止。答案是我们达到的深度。 cat > answer.nix <<EOF { pkgs ? import <nixpkgs> { } }: let roller = derivation { name = "answer.drv"; system = builtins.currentSystem; builder = "\${pkgs.bash}/bin/bash"; args = [ "-c" "echo $DEPTH > \$out" ]; __contentAddressed = true; outputHashMode = "recursive"; outputHashAlgo = "sha256"; }; in builtins.outputOf roller.outPath "out" EOF nix-build answer.nix else # 不是六点。我的答案就是下一级返回的答案。 cat > answer.nix <<EOF { pkgs ? import <nixpkgs> { } }: let roller = derivation { name = "answer.drv"; system = builtins.currentSystem; builder = "\${pkgs.bash}/bin/bash"; args = [ "-c" "nix-build ${chain} --arg depth ${toString ((depth + 1))} > \$out" ]; __contentAddressed = true; outputHashMode = "recursive"; outputHashAlgo = "sha256"; }; in builtins.outputOf roller.outPath "out" EOF nix-build answer.nix fi ```

相似文章

后现代构建系统

Lobsters Hottest

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

使用Nix构建系统软件

Lobsters Hottest

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

不到100行代码实现nix-build

Lobsters Hottest

本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。

Nix for Haskell: 静态构建

Lobsters Hottest

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