可复现与封闭构建:固定哈希、网络隔离与确定性

可复现与封闭构建实战:什么是可复现构建、固定哈希(fixed-output derivation)、构建网络隔离与 impure 检测、确定性问题(时间戳/路径/环境)、diffoscope 对比、Reproducible Builds 标准、nix build --rebuild 验证、让构建可复现的工程手段、常见坑。

引言

「同样的输入永远产生同样的输出」是 Nix 的核心承诺,但默认并不保证——构建脚本仍可能读网络、依赖系统环境、嵌入时间戳。本文讲清 Nix 如何逼近「可复现构建」:固定哈希的构建结果校验、沙箱与网络隔离、确定性的敌人(时间戳/路径/环境变量)、以及如何用 nix build --rebuild 和 diffoscope 验证两个构建产物完全一致。

前置:/nix-language-basics/(derivation 模型)、/nix-package-management/(构建与缓存)、/nix-ci-cachix/(二进制缓存复用)。


目录


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 沙箱文档

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nix」更多文章

  1. Nix 源码获取与 fetchers:fetchFromGitHub、哈希与私有源
  2. Nix 构建调试与错误排查:常见错误、trace 与诊断手段
  3. NixOS 虚拟机与集成测试:nixosTest 框架与系统级验证