Nix那个看着像乱码的Hash,其实挺有讲究
那些 Nix 路径里的神秘字符,到底是怎么来的?
你肯定见过。在 /nix/store 目录下,那些让人头皮发麻的目录名,像是把包名丢进了搅拌机里:
/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9
右边那部分挺好认——bash-5.3p9 说的是什么东西。但横杠前面那 32 个字符是什么鬼?这就是谜团所在。
问题是:网上大多数解释,都是说一半留一半。
常见的解释,错了?
你听到的答案八成是:"哈希值来自输入"。这话不算错,但关键的地方没讲清楚。真正的问题是:什么 输入?是你写的源码?Nix 生成的构建配方?还是构建出来的二进制本身?又或者是依赖?
说实话:得看情况。
两套命名规则,住在同一个目录
Nix 其实用了两套不同的命名机制,它们都在 /nix/store 里和平共处。搞混这两套规则,才是让人困惑的根源。
输入寻址:构建之前就起好名
大多数包在构建时,Nix 像个审图的建筑师。你写了一个 .nix 表达式,Nix 会把它算成一个** derivation(构建配方)**——一份详细的说明书,里面写着:
- 构建脚本
- 构建参数
- 环境变量
- 声明的输出
- 依赖项
拿到这份说明书,Nix 就能在构建之前算出一个唯一的标识符。二进制文件还没影儿呢,Nix 已经开始给它想名字了。
这就是为什么改一下 Nix 表达式,不一定就会改变 store path。只要你的改动没影响到构建配方本身,Nix 就会给出一模一样的标识符。两段不同的表达式,可能算出完全相同的配方,所以它们共享同一个路径。Nix 命名的是它要构建的东西,不是你代码的字面样子。
内容寻址:直接看内容说话
Nix 也能直接用内容来命名。你运行 nix store add 时,给 Nix 的是已经存在的东西,它会立刻对字节进行哈希。没有求值,没有配方——只看内容。
这种情况下,路径取决于文件里到底装了什么。改一个字节,哈希值就变了。
为什么这个区别重要
搞懂这两套命名规则,好多 Nix 行为一下就解释通了:
预测路径变化。 如果你在想"改了这个会不会导致新的 store path?",先问问自己:它改变的是构建配方,还是实际内容?改个 Nix 表达式里的注释?路径大概率不变。换个依赖版本?那新路径是跑不掉的。
理解新旧构建共存。 为什么旧的构建产物能和新的在 store 里做邻居?因为它们的标识符不一样。Nix 不覆盖——它根本不需要。
缓存复用就说得通了。 二进制缓存能放心地提供预构建的路径,因为名字能唯一确定构建输入。名字一样,内容就一样,这是保证的。
不只是个名字
store path 从来不是孤零零的。构建好的包会引用其他 store path(动态库、解释器、共享资源),Nix 把这些关系追踪成一个图。
这不是单纯的元数据——这是一张完整的依赖地图。从任何一个路径出发,你都能追溯它的闭包:要让它在另一台机器上跑起来,必须跟着一起走的全部东西。这就是 nix-copy-closure 的原理,也是 Nix 部署可靠的原因。
同一套数据库让 Nix 能验证完整性。它可以检查磁盘上的字节,和当初创建路径时记录的是否一致。store path 不是什么看不透的目录名——它们是可查询的身份,带着完整的审计记录。
说白了
那些看着很玄乎的哈希值,并不是随便生成的。这是刻意的设计选择:Nix 用输入命名 derivation,用字节命名内容。这种双轨制,才是 Nix 可复现、可验证、并且了解原理后其实挺好预测的真正原因。
下次再看到 store path,别慌。从右往左读——那是给人看的地方。左边那串哈希,是 Nix 在许诺:这个名字代表这次构建,不多不少。
想深入研究? 无论你在部署容器、管理基础设施,还是单纯对可复现构建感兴趣,了解系统的内部原理永远不亏。在 NameOcean,我们相信最好的开发者,是那些真正懂底层发生了什么的人——而不只是会敲命令。
构建愉快。