引言
「同样的输入永远产生同样的输出」是 Nix 的核心承诺,但默认并不保证——构建脚本仍可能读网络、依赖系统环境、嵌入时间戳。本文讲清 Nix 如何逼近「可复现构建」:固定哈希的构建结果校验、沙箱与网络隔离、确定性的敌人(时间戳/路径/环境变量)、以及如何用 nix build --rebuild 和 diffoscope 验证两个构建产物完全一致。
前置:/nix-language-basics/(derivation 模型)、/nix-package-management/(构建与缓存)、/nix-ci-cachix/(二进制缓存复用)。
目录
- 1. 可复现构建是什么
- 2. 固定哈希:结果被加密校验的 derivation
- 3. 沙箱与网络隔离
- 4. 确定性的敌人:时间戳路径与环境
- 5. 时间戳归一化与 SOURCE_DATE_EPOCH
- 6. 用 nix build –rebuild 验证可复现
- 7. diffoscope:逐字节对比两个产物
- 8. Reproducible Builds 标准与实践
- 9. 让构建可复现的工程手段
- 10. 速查表与一句话记忆
- 延伸阅读
1. 可复现构建是什么
1.1 定义
可复现构建(Reproducible Builds):用同一份源码 + 同一套构建环境,无论在谁的机器、任何时候构建,产物逐字节一致。这是供应链安全(谁能保证你装的和你写的完全一致?)与调试可追溯(这个二进制是从哪份源码来的?)的基石。
1.2 Nix 的承诺与边界
Nix 保证:输入(src + 依赖的哈希)确定 → 构建脚本在受控环境跑
边界:若构建脚本自身不确定(读网络、读系统状态、嵌时间戳),产物仍可能不同
所以:"可复现"不是 Nix 免费送的,是要工程手段保证的
记忆:可复现构建=同源码同环境同产物(逐字节一致);Nix 管住输入确定,但构建脚本的不确定性(网络/时间戳/环境)要自己排。
2. 固定哈希:结果被加密校验的 derivation
2.1 固定哈希是什么
Nix 里下载源码用的是 fixed-output derivation:声明「我会产出内容为某哈希的东西」,构建后按哈希校验,不一致直接失败。这是源码完整性与供应链信任的基础:
src = pkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nix";
rev = "2.20.0";
hash = "sha256-AAAAAAAA...="; # 声明的期望哈希
};
2.2 固定哈希的意义
# 1) 防篡改:下载内容变了 → 哈希不符 → 构建失败
# 2) 可缓存:哈希确定 → 结果可复用(不重复下载)
# 3) 供应链信任:锁住的哈希就是"我信任这个内容"
2.3 更新哈希
# 改了 rev 忘了 hash:构建报错并提示实际 hash,复制替换即可
nix build .#mypkg # 报 hash mismatch,输出 actual hash
记忆:固定哈希声明「我要内容=某哈希的东西」,构建后校验——防篡改、可缓存、供应链信任;改版本报 hash mismatch 时复制实际 hash 替换。
3. 沙箱与网络隔离
3.1 Nix 沙箱
Nix 默认用沙箱隔离构建:空文件系统 + 只读依赖 + 无网络(fixed-output 例外)。这保证构建不受宿主环境干扰:
默认沙箱(build sandbox):
✓ 干净文件系统(只有声明的依赖)
✓ 网络隔离(大部分 derivation 不能联网)
✓ 环境变量受限
✗ 若脚本偷偷读 /etc、家目录 → 在不同机器会不一致
3.2 检查一个构建是否 impure
# 强制不沙箱构建 vs 沙箱构建对比,可暴露 impure 依赖
nix build --option sandbox false .#mypkg
如果去掉沙箱结果就变,说明脚本依赖了未声明的东西——这是不可复现的根源。
记忆:沙箱给构建干净文件系统+网络隔离;怀疑构建 impure(偷读系统文件/环境)就用
--option sandbox false对比,产物变即有问题。
4. 确定性的敌人:时间戳路径与环境
4.1 三大不确定性来源
| 来源 | 例子 | 影响 |
|---|---|---|
| 时间戳 | 构建时间写进二进制/归档 | 两次构建产物不同 |
| 绝对路径 | 编译缓存了 /nix/store/xxx 外的宿主路径 | 换机器产物不同 |
| 环境变量 | 读了未声明的 $HOME/$PATH | 环境不同结果不同 |
4.2 为什么「看起来一样」会不同
# 编译器的 build id、ar 归档时间戳、zip 条目时间、__DATE__/__TIME__ 宏
# 都会把"当时的环境"烙进产物
# 排查思路:固定它们(见下节),而不是祈祷两次恰好一致
记忆:不确定性的三敌——时间戳(构建时间烙进产物)、绝对路径(宿主路径泄漏)、环境变量(未声明的读);「恰好一致」是侥幸,要主动固定。
5. 时间戳归一化与 SOURCE_DATE_EPOCH
5.1 SOURCE_DATE_EPOCH
业界标准 SOURCE_DATE_EPOCH:固定一个时间戳给构建工具,让所有嵌入时间一致:
# nixpkgs 已默认设置 SOURCE_DATE_EPOCH=1(1970-01-01)
# 构建工具读取它而非系统当前时间
5.2 让补丁/脚本尊重它
# 打包脚本里用固定时间戳,别用 `date`
tar --mtime=@$SOURCE_DATE_EPOCH ...
gzip -n # 不写时间戳
# gcc 用 -frandom-seed= 固定随机
记忆:SOURCE_DATE_EPOCH 是时间戳归一化的标准——构建工具读它而非当前时间;打包脚本用
--mtime=@$SOURCE_DATE_EPOCH/gzip -n,编译器加-frandom-seed。
6. 用 nix build –rebuild 验证可复现
6.1 –rebuild 的原理
--rebuild 强制重新构建两次,比较两次产物哈希是否一致:
nix build .#mypkg --rebuild
# 两次构建产物哈希相同 → 可复现 ✓
# 报 "mismatch" → 存在不确定性 ✗
6.2 定位不可复现的依赖
# --rebuild 会告诉你哪个 derivation 两次结果不同
# 沿着依赖链一层层 rebuild,缩小到具体的不确定包
# 常见嫌疑:Cargo/Gradle/npm 这类联网+缓存的构建器
记忆:
nix build --rebuild强制建两次比哈希,mismatch 即不可复现;沿依赖链逐层 rebuild 缩小到具体包。
7. diffoscope:逐字节对比两个产物
7.1 diffoscope 是什么
diffoscope 能「看进」二进制、归档、镜像内部,报告哪里不一样:
nix-shell -p diffoscope
# 提取两次构建的产物(或用 --rebuild 保存两次结果)
diffoscope old-build.tar new-build.tar
# 输出:哪个文件、偏移量、值不同(如 zip 条目时间戳)
7.2 读 diffoscope 报告
# 报告会递归展开:tar → 文件 → 若还是归档继续展开
# 定位到"时间戳字段差 1 秒" vs "内容字节真的不同"是两回事
# 时间戳类:加 SOURCE_DATE_EPOCH/去随机
# 内容类:查源码、编译器参数、依赖
记忆:diffoscope 深入二进制/归档内部逐字节对比,把「差在哪里」展开到字段级;时间戳差加归一化、内容差查源码与依赖。
8. Reproducible Builds 标准与实践
8.1 国际标准运动
Reproducible Builds(reproducible-builds.org)是跨发行版的可复现构建运动——Debian 逐包统计可复现率、Arch/OpenWrt 等跟进。Nix 与它理念一致,且因沙箱与哈希更接近「开箱可复现」。
8.2 NixOS 的实践
# nixpkgs 的每个包构建时记录"可复现与否"的状态
# 贡献可复现补丁:修掉时间戳/路径/环境依赖
# 供应链场景:把 .drv 哈希 + 产物哈希一起归档,可追溯
记忆:可复现构建是全球发行版运动(reproducible-builds.org),Nix 因沙箱+哈希天然接近;在 nixpkgs 修可复现问题就是给整个生态供确定性。
9. 让构建可复现的工程手段
9.1 清单
# ☐ 源码一律 fixed-output 且锁哈希(fetchpatch/fetchFromGitHub)
# ☐ 构建依赖全部声明(buildInputs/nativeBuildInputs),不读系统
# ☐ 不联网:构建脚本不 curl、不自动下载
# ☐ 时间戳归一化:SOURCE_DATE_EPOCH + 打包参数
# ☐ 固定随机:gcc -frandom-seed、工具链固定版本
# ☐ 验证:nix build --rebuild 通过 + diffoscope 无差异
9.2 常见不可复现构建器
# Cargo(网络+指纹):锁定 Cargo.lock、offline 构建
# Gradle/npm:锁 lockfile、offline cache
# 汇编器/打包器:显式 --mtime、--sort
# 处理方式:把"网络获取"变成 fixed-output 预下载,再离线构建
记忆:可复现工程手段 = 锁哈希 + 全声明依赖 + 不联网 + 时间戳归一化 + 固定随机 + –rebuild 验证;Cargo/npm 类联网构建器把网络获取挪进 fixed-output 预下载再离线构建。
10. 速查表与一句话记忆
| 手段 | 作用 | 一句话 |
|---|---|---|
| 固定哈希 | 结果加密校验 | 防篡改可缓存 |
| 沙箱 | 干净隔离构建 | 排除宿主干扰 |
| SOURCE_DATE_EPOCH | 时间戳归一化 | 烙入时间一致 |
| –rebuild | 建两次比哈希 | 快速判定可复现 |
| diffoscope | 逐字节定位差异 | 展开到字段级 |
| 锁 lockfile | 依赖确定性 | 联网构建器必备 |
一句话记忆:可复现构建=同源码同环境同产物——Nix 用固定哈希(fixed-output)校验下载内容、沙箱隔离构建排除宿主干扰,剩下的不确定性集中在三处:时间戳(用 SOURCE_DATE_EPOCH 归一化 + 打包参数固定)、绝对路径(全声明依赖别读系统)、环境变量(沙箱已隔离未声明的读);验证用 nix build --rebuild 建两次比哈希、diffoscope 逐字节定位差异(时间戳差→归一化、内容差→查源码/依赖);Cargo/npm 类联网构建器把网络获取挪进 fixed-output 预下载再离线构建——「输入锁哈希、环境进沙箱、时间戳归一化、产物可复验」四件事做到,供应链才有底气。
延伸阅读
- /nix-package-management/ — derivation 与构建模型
- /nix-ci-cachix/ — 二进制缓存与可复现产物复用
- /nix-package-patching/ — 定制包与 fetchpatch
- /nix-language-basics/ — fixed-output 与属性集
- [[devops]] — 供应链与 CI 可靠性
- Reproducible Builds 官网
- Nix 沙箱文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。