```Nix 衍生混乱```
Nix Derivation Madness

原始链接: https://fzakaria.com/2025/10/29/nix-derivation-madness

## Nix 疑难:理解固定输出派生 最近对 Nix 包管理的一次探索揭示了关于固定输出派生 (FOD) 的一种令人惊讶的行为。作者遇到一个问题,尽管 Ruby 二进制文件存在,Nix 却无法找到 Ruby 安装的派生文件 (`.drv`)。这导致深入研究 Nix 如何处理派生和二进制缓存。 问题的核心在于 FOD——其输出仅由 `$out` 的内容决定,而非派生本身。对 FOD 定义的更改(超出 `$out` 之外)会创建 *新的* `.drv` 文件,但可能导致 *相同的* 输出路径。这意味着 Nix 缓存可能会将新的派生链接到现有的输出,从而造成混淆。 进一步的实验表明,多个派生甚至可以映射到相同的输出,并且从派生中删除输入并不一定会改变最终的输出路径。派生与输出之间这种“1:N”的关系凸显了 Nix 内部复杂的交互,并强调了完全理解其底层机制的挑战。作者得出结论,掌握 Nix 需要应对这些意外行为,并愿意不断完善自己的心理模型。

## Nix 派生 & 存储路径讨论总结 这次黑客新闻讨论围绕着 Nix(一个强大的包管理器)的持续改进,具体解决“派生器”(构建派生的标识符)和存储路径的问题。核心问题是派生器并非唯一,导致缓存构建与本地评估不匹配时产生混淆,尤其是在固定输出派生时。 提出的解决方案涉及一个“构建追踪”功能,以更好地跟踪溯源,可能取代当前的派生器系统。这将允许精确识别存储路径的来源并改进 SBOM 生成。 争论的焦点在于*在哪里*存储 flake 特定的元数据——本地以保证用户一致性,还是存储在构建追踪本身中。 一个关键点是希望摆脱完全依赖存储路径*名称*,提倡将它们视为不透明的能力,而不是具有语义意义的标识符。一些人建议限制对 `/nix/store` 目录的访问以强制执行此操作,但提出了关于调试的担忧。最终目标是建立一个更强大、更可靠的系统来管理和理解 Nix 构建。
相关文章

原文

I’ve written a bit about Nix and I still face moments where foundational aspects of the package system confounds and surprises me.

Recently I hit an issue that stumped me as it break some basic comprehension I had on how Nix works. I wanted to produce the build and runtime graph for the Ruby interpreter.

> nix-shell -p ruby

> which ruby
/nix/store/mp4rpz283gw3abvxyb4lbh4vp9pmayp2-ruby-3.3.9/bin/ruby

> nix-store --query --include-outputs --graph \
  $(nix-store --query --deriver $(which ruby))
error: path '/nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv' is not valid

> ls /nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv
ls: cannot access '/nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv':
No such file or directory

Huh. 🤔

I have Ruby but I don’t seem to have the derivation, 24v9wpp393ib1gllip7ic13aycbi704g, file present on my machine.

No worries, I think I can --realize it and download it from the NixOS cache.

> nix-store --realize /nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv
don't know how to build these paths:
  /nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv
error: cannot build missing derivation '/nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv'

I guess the NixOS cache doesn’t seem to have it. 🤷

This was actually perplexing me at this moment. In fact there are multiple discourse posts about it.

My mental model however of Nix though is that I must have first evaluated the derivation (drv) in order to determine the output path to even substitute. How could the NixOS cache not have it present?

Is this derivation wrong somehow? Nope. This is the derivation Nix believes that produced this Ruby binary from the sqlite database. 🤨

> sqlite3 "/nix/var/nix/db/db.sqlite" 
    "select deriver from ValidPaths where path = 
    '/nix/store/mp4rpz283gw3abvxyb4lbh4vp9pmayp2-ruby-3.3.9'"
/nix/store/24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv

What does the binary cache itself say? Even the cache itself thinks this particular derivation, 24v9wpp393ib1gllip7ic13aycbi704g, produced this particular Ruby output.

> curl -s https://cache.nixos.org/mp4rpz283gw3abvxyb4lbh4vp9pmayp2.narinfo |\
  grep Deriver
Deriver: 24v9wpp393ib1gllip7ic13aycbi704g-ruby-3.3.9.drv

What if I try a different command?

> nix derivation show $(which ruby) | jq -r "keys[0]"
/nix/store/kmx8kkggm5i2r17s6l67v022jz9gc4c5-ruby-3.3.9.drv

> ls /nix/store/kmx8kkggm5i2r17s6l67v022jz9gc4c5-ruby-3.3.9.drv
/nix/store/kmx8kkggm5i2r17s6l67v022jz9gc4c5-ruby-3.3.9.drv

So I seem to have a completely different derivation, kmx8kkggm5i2r17s6l67v022jz9gc4c5, that resulted in the same output which is not what the binary cache announces. WTF? 🫠

Thinking back to a previous post, I remember touching on modulo fixed-output derivations. Is that what’s going on? Let’s investigate from first principles. 🤓

Let’s first create fod.nix which is our fixed-output derivation.

let
  system = builtins.currentSystem;
in derivation {
  name = "hello-world-fixed";
  builder = "/bin/sh";
  system = system;
  args = [ "-c" ''
    echo -n "hello world" > "$out"
  '' ];
  outputHashMode = "flat";
  outputHashAlgo = "sha256";
  outputHash = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
}

☝️ Since this is a fixed-output derivation (FOD) the produced /nix/store path will not be affected to changes to the derivation beyond the contents of $out.

> nix-instantiate fod.nix
/nix/store/k2wjpwq43685j6vlvaarrfml4gl4196n-hello-world-fixed.drv

> nix-build fod.nix
/nix/store/ajk19jb8h5h3lmz20yz6wj9vif18lhp1-hello-world-fixed

Now we will create a derivation that uses this FOD.

{ fodDrv ? import ./fod.nix }:

let
  system = builtins.currentSystem;
in
builtins.derivation {
  name = "uses-fod";
  inherit system;
  builder = "/bin/sh";
  args = [ "-c" ''
    echo ${fodDrv} > $out
    echo "Good bye world" >> $out
  '' ];
}

The /nix/store for the output for this derivation will change on changes to the derivation except if the derivation path for the FOD changes. This is in fact what makes it “modulo” the fixed-output derivations.

> nix-instantiate uses-fod.nix
/nix/store/85d15y7irq7x4fxv4nc7k1cw2rlfp3ag-uses-fod.drv

> nix-build uses-fod.nix
/nix/store/sd12qjak7rlxhdprj10187f9an787lk3-uses-fod

Let’s test this all out by changing our fod.nix derivation. Let’s do this by just adding some garbage attribute to the derivation.

@@ -4,6 +4,7 @@
   name = "hello-world-fixed";
   builder = "/bin/sh";
   system = system;
+  garbage = 123;
   args = [ "-c" ''
     echo -n "hello world" > "$out"
   '' ];

What happens now?

> nix-instantiate fod.nix
/nix/store/yimff0d4zr4krwx6cvdiqlin0y6vkis0-hello-world-fixed.drv

> nix-build fod.nix
/nix/store/ajk19jb8h5h3lmz20yz6wj9vif18lhp1-hello-world-fixed

The path of the derivation itself, .drv, has changed but the output path ajk19jb8h5h3lmz20yz6wj9vif18lhp1 remains consistent.

What about the derivation that leverages it?

> nix-instantiate uses-fod.nix
/nix/store/85wkdaaq6q08f71xn420v4irll4a8g8v-uses-fod.drv

> nix-build uses-fod.nix
/nix/store/sd12qjak7rlxhdprj10187f9an787lk3-uses-fod

It also got a new derivation path but the output path remained unchanged. 😮

That means changes to fixed-output-derivations didn’t cause new outputs in either derivation but it did create a complete new tree of .drv files. 🤯

That means in nixpkgs changes to fixed-output derivations can cause them to have new store paths for their .drv but result in dependent derivations to have the same output path. If the output path had already been stored in the NixOS cache, then we lose the link between the new .drv and this output path. 💥

Derivation graphic

The amount of churn that we are creating in derivations was unbeknownst to me.

It can get even weirder! This example came from @ericson2314.

We will duplicate the fod.nix to another file fod2.nix whose only difference is the value of the garbage.

@@ -4,7 +4,7 @@
   name = "hello-world-fixed";
   builder = "/bin/sh";
   system = system;
-  garbage = 123;
+  garbage = 124;
   args = [ "-c" ''
     echo -n "hello world" > "$out"
   '' ];

Let’s now use both of these in our derivation.

{ fodDrv ? import ./fod.nix,
  fod2Drv ? import ./fod2.nix
}:
let
  system = builtins.currentSystem;
in
builtins.derivation {
  name = "uses-fod";
  inherit system;
  builder = "/bin/sh";
  args = [ "-c" ''
    echo ${fodDrv} > $out
    echo ${fod2Drv} >> $out
    echo "Good bye world" >> $out
  '' ];
}

We can now instantiate and build this as normal.

> nix-instantiate uses-fod.nix
/nix/store/z6nr2k2hy982fiynyjkvq8dliwbxklwf-uses-fod.drv

> nix-build uses-fod.nix
/nix/store/211nlyx2ga7mh5fdk76aggb04y1wsgkj-uses-fod

What is weird about that?

Well, let’s take the JSON representation of the derivation and remove one of the inputs.

> nix derivation show \
    /nix/store/z6nr2k2hy982fiynyjkvq8dliwbxklwf-uses-fod.drv \
    jq 'values[].inputDrvs | keys[]'
"/nix/store/6p93r6x0bwyd8gngf5n4r432n6l380ry-hello-world-fixed.drv"
"/nix/store/yimff0d4zr4krwx6cvdiqlin0y6vkis0-hello-world-fixed.drv"

We can do this because although there are two input derivations, we know they both produce the same output!

@@ -12,12 +12,6 @@
       "system": "x86_64-linux"
     },
     "inputDrvs": {
-      "/nix/store/6p93r6x0bwyd8gngf5n4r432n6l380ry-hello-world-fixed.drv": {
-        "dynamicOutputs": {},
-        "outputs": [
-          "out"
-        ]
-      },
       "/nix/store/yimff0d4zr4krwx6cvdiqlin0y6vkis0-hello-world-fixed.drv": {
         "dynamicOutputs": {},
         "outputs": [

Let’s load this modified derivation back into our /nix/store and build it again!

> nix derivation add < derivation.json
/nix/store/s4qrdkq3a85gxmlpiay334vd1ndg8hm1-uses-fod.drv

> nix-build /nix/store/s4qrdkq3a85gxmlpiay334vd1ndg8hm1-uses-fod.drv
/nix/store/211nlyx2ga7mh5fdk76aggb04y1wsgkj-uses-fod

We got the same output 211nlyx2ga7mh5fdk76aggb04y1wsgkj. Not only do we have a 1:N trait for our output paths to derivations but we can also take certain derivations and completely change them by removing inputs and still get the same output! 😹

The road to Nix enlightenment is no joke and full of dragons.

联系我们 contact @ memedata.com