nix:channel 快照滚动指针大坑

结论

把 <nixpkgs> 指向「镜像站的频道快照 URL」,等于把它指向一个滚动指针:镜像站一推进,同一条 URL 的内容就换成了另一个 nixpkgs 修订,下一次重建便要把新修订的整个闭包重新取回来。症状是「同一份配置,一小时前还是 0 构建,一小时后突然要处理几百个包」,且在网络受限时直接失败。

正确形态:来源钉死到具体修订(完整 40 位提交号 + sha256 内容校验),并让钉住的源码树留在系统闭包里(否则会被垃圾回收收走,下次重建又要联网取)。

症状

时间操作结果
16:15 / 18:38同一条重建命令(带描述)秒级完成,0 构建
19:05同样命令,仅多了一行注释these 944 derivations will be built → 抓源码失败 → 退出码 102
19:07对照实验:把那一行注释还原后重跑these 903 derivations will be built ⇒ 与改动无关

这两行数字最容易把人带偏:它看起来像「要从源码重建整个系统」,其实真正要本地构建的只有几十个系统自身的派生,其余是可下载的。判据见下节。

根因链

nixos-rebuild
  → 由搜索路径解析 <nixpkgs>
    → 该条目 = 镜像站的频道快照 URL(滚动指针)
      → 镜像推进 ⇒ 落到另一个修订 ⇒ 闭包几乎全部重取
  → 需要联网抓取时,若 nix-daemon 没有代理、而境外缓存又直连不通
      ⇒ 报 TLS/SSL 连接错误 或 固定输出派生的哈希不匹配 ⇒ 重建失败

实测证据

判据实测值
同配置在不同时刻的构建数0 → 903/944(间隔约一小时)⇒ 来源漂了
对照实验(改动还原)903 = 903 ⇒ 与那行改动无关
钉死来源 + 开着国内镜像,重建代价47 个派生本地构建 + 244 KiB 下载(5 个路径)
钉住的源码树是否已在本地 store命中现成树 ⇒ 0 下载(用死代理验证:无网络也解析成功)
旧的同类记录同一机制的推进曾造成一次约 75 分钟的持续下载

为什么「钉版本」曾被判定做不到

镜像站不提供按修订取快照的路径(/nixpkgs/archive/<修订>.tar.gz 返回 HTTP 404),于是当时记下了「无法钉版本」。这个结论只对镜像站成立:上游代码托管站的归档接口支持按完整 40 位提交号取快照,所以来源可以钉死。教训是——「某个镜像做不到」不等于「这件事做不到」。

处理办法(三处配合)

  1. 源码来源:把搜索路径里的 nixpkgs 条目改成带修订号 + 哈希的归档地址;取一次即进本地 store,之后重建不再需要网络。
  2. 留在闭包里:搜索路径条目只是字符串,不会让源码树成为 GC 根 ⇒ 显式把它加进系统闭包,否则回收后再重建又要联网。
  3. 登记修订号:源码走归档地址时,工具自己推不出修订号(世代里会显示 Unknown,事后无从查证)⇒ 显式写入修订号。
rev    = "<完整 40 位提交号>";
sha256 = "<nix-prefetch-url --unpack <归档地址> 的值>";
tarball = "https://<代码托管站>/<仓库>/archive/${rev}.tar.gz";
nixpkgsSrc = builtins.fetchTarball { url = tarball; inherit sha256; };

nix.nixPath = [ "nixpkgs=${nixpkgsSrc}" ] ++ 原有条目;
system.extraDependencies = [ nixpkgsSrc ];  # 防 GC 收走
system.nixos.revision = rev;            # 让版本查询显示修订号

另外:nix-daemon 要有自己的出网代理。境外二进制缓存直连不通,不给它代理,兜底那一档永远取不到东西,表现为 TLS/SSL 错误或固定输出哈希不匹配。国内镜像按分流规则直连即可。

判读纪律:别把「要下载」看成「要重建」

  • 判代价只看 dry-build 的两类数字:these N derivations will be built(真要本地构建)与 these M paths will be fetched (… KiB …)(要下载)。只看前者会得到错误结论。
  • 不要在重建命令上加 --option substituters ""(除非确知不需要任何外部输入):它把镜像一起关掉,于是几百个本可下载的路径全被算成「要自己构建」——正是本次误判的成因。
  • 怀疑「是不是来源漂了」用 A/B 对照自证:把可疑改动还原再跑一次,数字相同 ⇒ 与改动无关,转去查来源与 store 侧。
  • 世代里 Configuration Revision 显示 Unknown 的含意:该世代不是从带版本后缀的频道快照求值的(归档地址/工作树都是这样)⇒ 事后无法从世代反查修订号。修订号以版本查询命令的输出为准。

换修订的操作

# 1) 取目标修订的哈希(--unpack 才是归档抓取所需的那个值)
nix-prefetch-url --unpack https://<代码托管站>/<仓库>/archive/<40 位提交号>.tar.gz

# 2) 改配置里的 rev / sha256 两行,然后重建
sudo nixos-rebuild switch <描述>

# 3) 验证
nixos-version                # 应显示钉住的修订号
sudo nixos-rebuild dry-build  # 先看要下载/构建多少

踩坑记录

  • 现象:重建要求 900+ 个派生 → 成因:命令上带了「关闭替代源」的选项,把国内镜像也关了,几百个可下载路径被算成要构建 → 改法:去掉该选项,用两类数字判代价。
  • 现象:世代里修订号显示 Unknown,查不出跑的是哪一版 → 成因:源码走归档地址,没有版本后缀文件;且未显式登记修订号 → 改法:显式写入修订号;判据以版本查询命令为准(列表命令那一列读的不是同一字段)。
  • 现象:钉好后过一阵子重建又开始联网取源码 → 成因:搜索路径条目只是字符串,不是 GC 根,源码树被回收收走 → 改法:把它加进系统闭包。
  • 现象:需要抓取时报 TLS/SSL 错误或固定输出哈希不匹配 → 成因:nix-daemon 没有出网代理,境外缓存直连不通 → 改法:给 daemon 配代理(国内镜像按分流规则直连)。

边界

本档只讲「nixpkgs 来源为什么会漂 + 怎么钉 + 重建代价怎么判读」,不含具体机器的镜像/代理配置、也不含「钉住之后要不要跟着上游升级、何时升、由谁定」这类策略问题。