让构建可复现:固定输入、消除时间污染与二进制比对

相同源码经过确定性流水线产出一致制品

“能在我电脑上打出来”只说明构建曾经成功,不代表它可验证、可迁移。更可靠的目标是:在相同声明的输入下,两台独立环境产出字节完全相同的制品。这样,无论是排查发布差异、复核第三方构建,还是追踪供应链异常,都有一个明确的比较基线。

可复现构建究竟验证什么

可复现不等于“每次都能编译通过”。前者关注最终字节,要求源码、依赖、构建工具、环境变量与构建规则相同时,输出的哈希也相同。它能把模糊的“这包大概来自这次提交”变成可自动检查的事实。

变化来源 常见表现 应对方向
浮动依赖或基础镜像 同一提交隔天构建不同 锁定版本和镜像摘要
时间、时区、语言环境 包内日期或格式不同 固定时钟与 locale
文件顺序、权限、路径 压缩包哈希变化 规范化元数据和排序
并发、随机数、网络 偶发差异且难复现 让构建离线且显式注入值

不要把“哈希相同”误解为安全签名:它只证明两次输出相同,不能单独证明构建者可信。签名、访问控制和制品校验仍然必要;可复现构建提供的是独立复核制品来源的基础。

先把输入收进边界

构建入口应接受明确的 Git 提交,而不是工作区中未提交的文件。语言依赖使用锁文件安装,例如 npm cipip 的哈希约束或其他生态的等价机制;容器基础镜像则引用不可变摘要,而非会移动的标签。构建脚本还应把时区和语言环境写明,避免不同 Runner 以本地默认值参与输出。

1
2
3
4
5
6
7
8
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
export TZ=UTC
export LANG=C.UTF-8
export LC_ALL=C.UTF-8

# 只从锁文件恢复依赖,不更新版本范围
npm ci
npm run build

SOURCE_DATE_EPOCH 可取目标提交的提交时间,作为制品内允许出现的统一时间。它不是让所有业务逻辑都读取假时钟,而是只供打包器、文档生成器等需要写入时间戳的步骤使用。若某个工具不支持它,应在该工具的参数中显式传入时间,不能依赖构建机当前时钟。

锁定源码、依赖、编译器和时钟的受控构建环境

网络也是输入的一部分。构建阶段最好只读取预先缓存或已审核的依赖仓库;若必须下载,至少记录 URL、摘要和解析后的版本。把“下载最新兼容版本”的动作留给更新依赖的专门流水线,而不是悄悄发生在发布构建中。

让输出不携带偶然信息

许多差异藏在压缩包而不在编译结果中:文件遍历顺序、UID/GID、权限、归档时间和绝对路径都会改变字节。归档时应选择支持确定性选项的工具,并把元数据收敛到固定值。以下示例适用于 GNU tar:

1
2
3
4
tar --sort=name \
--mtime="@${SOURCE_DATE_EPOCH}" \
--owner=0 --group=0 --numeric-owner \
-czf app.tar.gz dist/

编译器和代码生成器也常把构建路径、主机名或随机标识写进产物。优先使用其“路径映射”“确定性输出”或“关闭构建信息”选项;确实需要构建编号时,将编号作为显式参数写入发布元数据,而不是自动读取当前日期。并行构建本身没有问题,但不要让竞争顺序决定文件列表、代码生成编号或最终拼接顺序。

用两次独立构建作为 CI 闸口

验收不应只在同一个工作目录连续执行两遍,因为残留缓存可能掩盖问题。让两个干净的容器或两个独立 Runner 使用同一提交、同一锁文件和同一工具镜像分别构建,再比较最终交付文件:

1
2
3
sha256sum artifacts-a/app.tar.gz artifacts-b/app.tar.gz
cmp --silent artifacts-a/app.tar.gz artifacts-b/app.tar.gz \
|| diffoscope artifacts-a/app.tar.gz artifacts-b/app.tar.gz

哈希不同不是“再跑一次试试”的理由。保留两个制品、构建日志、工具镜像摘要和输入清单,再用 diffoscope 或格式对应的分析工具定位首个差异:是归档头、依赖字节码、还是嵌入路径。差异被解释并修复后,才将比较检查设为发布前的必经步骤。

两个独立 CI 环境产出制品并进行字节级比较

渐进落地清单

阶段 先做什么 完成信号
输入 锁定依赖、镜像和提交 构建日志可列出全部版本与摘要
环境 固定时区、locale 和时间来源 换 Runner 后结果稳定
输出 规范化归档顺序和元数据 同环境连续构建哈希一致
验收 双环境比对并保留差异证据 CI 能阻止未解释的差异进入发布

不必一次改完所有工具链。先挑一个最常发布、制品格式最简单的服务,记录基线并消除最显眼的时间戳;随后把固定输入和双构建比较推广到其他项目。构建一旦可复现,发布产物就不再是某台机器偶然留下的结果,而是任何受控环境都能重新推导、验证和信任的工程交付物。