强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

GNU Guix 函数式包管理教程 / 第九章 可重现构建

第九章:可重现构建

9.1 什么是可重现构建?

可重现构建(Reproducible Build)是指:给定相同的源码和构建环境,任何人在任何时间、任何机器上执行构建,都能得到完全相同的二进制输出

相同输入 ──► 相同构建过程 ──► 相同输出
   │              │              │
   │  源码        │  沙箱化      │  二进制完全一致
   │  依赖        │  确定性      │  SHA256 哈希相同
   └──────────────┴──────────────┘

9.1.1 为什么可重现构建很重要?

重要性说明
安全性验证二进制是否确实来自源码,防止供应链攻击
可审计性任何人都可以验证发布的二进制
可调试性相同环境可重现 bug
可回滚精确回退到任意历史版本
协作团队成员使用完全相同的环境

9.1.2 Guix 的可重现构建策略

Guix 在以下层面保证可重现性:

层面措施
构建沙箱每次构建在隔离的沙箱中执行
时间固定构建时间被固定为 Unix epoch (1970-01-01)
环境变量清理只暴露必要的环境变量
路径固定Store 路径包含输入哈希
确定性排序文件列表排序固定
去除不确定性移除时间戳、UID 等非确定性信息

9.2 时间戳问题

9.2.1 时间戳如何影响可重现性

许多构建工具会在输出中嵌入时间戳:

// 源码中的 __DATE__ 宏
printf("Built on %s at %s\n", __DATE__, __TIME__);
// 每次编译输出不同 → 不可重现

9.2.2 Guix 的解决方案

Guix 构建沙箱自动将时间固定为 Unix epoch:

# 验证构建沙箱中的时间
guix build vim --no-substitutes 2>&1 | grep "epoch"

# 构建日志中可以看到:
# date: Thu Jan  1 00:00:00 UTC 1970

9.2.3 处理源码中的时间戳

;; 在包定义中添加补丁来移除时间戳
(package
  ;; ...
  (source (origin
            ;; ...
            (patches (search-patches "myapp-remove-timestamp.patch"))))
  (arguments
    (list
      #:phases
      #~(modify-phases %standard-phases
          ;; 方法一:设置 SOURCE_DATE_EPOCH 环境变量
          (add-before 'configure 'set-source-date-epoch
            (lambda _
              (setenv "SOURCE_DATE_EPOCH" "0")))

          ;; 方法二:替换硬编码的时间戳
          (add-after 'unpack 'remove-build-timestamp
            (lambda _
              (substitute* "src/version.c"
                (("__DATE__")
                 "\"1970-01-01\"")
                (("__TIME__")
                 "\"00:00:00\""))
              #t))))))

9.2.4 SOURCE_DATE_EPOCH

SOURCE_DATE_EPOCH 是一个标准环境变量,许多工具支持它来控制时间戳输出:

# 手动设置
export SOURCE_DATE_EPOCH=0

# 在构建系统中
make SOURCE_DATE_EPOCH=0

# Guix 沙箱中自动设置为 0

支持 SOURCE_DATE_EPOCH 的工具:

工具支持情况
GCC✅ 使用 epoch 替代 __DATE__
Python✅ .pyc 文件使用 epoch
gzip✅ 头部时间戳
zip✅ 时间戳
tar✅ 使用 --mtime
TeX✅ PDF 元数据
Rust✅ 通过 env!

9.3 环境变量控制

9.3.1 沙箱中的环境变量

Guix 构建沙箱严格控制环境变量:

沙箱中的环境变量:
├── HOME=/homeless-shelter    (固定路径)
├── PATH=/gnu/store/.../bin   (仅构建输入的 PATH)
├── SOURCE_DATE_EPOCH=0       (时间戳固定)
├── LANG=C.UTF-8              (locale 固定)
└── 其他通过 #:make-flags 传递的变量

9.3.2 自定义构建环境变量

(arguments
  (list
    #:make-flags
    #~(list
        ;; 设置编译器
        (string-append "CC=" #$(cc-for-target))
        ;; 设置安装前缀
        (string-append "PREFIX=" #$output)
        ;; 自定义变量
        "VERBOSE=1")
    #:phases
    #~(modify-phases %standard-phases
        (add-before 'build 'set-env
          (lambda _
            ;; 设置环境变量
            (setenv "CFLAGS" "-O2 -g")
            (setenv "LDFLAGS" "-Wl,-z,relro")
            ;; 禁止记录构建路径
            (setenv "LC_ALL" "C.UTF-8")
            #t)))))

9.3.3 禁止非确定性输出

(arguments
  (list
    #:phases
    #~(modify-phases %standard-phases
        ;; strip 阶段移除调试信息中的构建路径
        ;; (这通常由标准阶段自动处理)

        ;; 处理 Go 二进制中的构建路径
        (add-before 'build 'trim-build-path
          (lambda _
            (setenv "GOPATH" "/tmp/gopath")
            (setenv "GOCACHE" "/tmp/gocache")
            #t)))))

9.4 固定构建输入

9.4.1 Store 路径的哈希计算

Guix Store 中每个对象的路径都包含其所有输入的哈希:

/gnu/store/abc123...-vim-9.0
                │
                └── 哈希 = hash(源码 + 所有依赖 + 构建脚本)

哈希计算递归包含所有依赖:

vim 的 store 路径
= hash(
    vim-source,
    hash(ncurses,
         hash(glibc,
              hash(gcc, ...))),
    build-script
  )

9.4.2 内容寻址存储

Guix 正在向**内容寻址存储(Content-Addressed Storage)**迁移:

# 当前:输入寻址
/gnu/store/<hash-of-inputs>-vim-9.0

# 未来:内容寻址
/gnu/store/<hash-of-content>-vim-9.0

内容寻址的优势:

  • 相同内容的包只存储一份
  • 减少二进制替代的下载量
  • 增强去重

9.4.3 固定源码版本

;; 使用精确的 commit hash,而非 tag
(origin
  (method git-fetch)
  (uri (git-reference
        (url "https://github.com/user/project")
        (commit "abc1234def56789012345678901234567890abcd")))
  (sha256 (base32 "0xyz...")))

9.5 补丁管理

9.5.1 添加补丁

(source (origin
          (method url-fetch)
          (uri (string-append "https://example.com/foo-"
                              version ".tar.gz"))
          (sha256 (base32 "0abc..."))
          ;; 补丁列表
          (patches (search-patches
                     "foo-fix-build.patch"
                     "foo-security-cve-2024-1234.patch"
                     "foo-reproducible-build.patch"))))

9.5.2 创建可重现构建补丁

# 创建补丁目录
mkdir -p /path/to/guix-patches

# 常见的可重现构建补丁类型:

# 1. 移除 __DATE__ 和 __TIME__
cat > foo-fix-date.patch << 'EOF'
--- a/src/version.c
+++ b/src/version.c
@@ -1,3 +1,3 @@
-char *build_date = __DATE__;
-char *build_time = __TIME__;
+char *build_date = "1970-01-01";
+char *build_time = "00:00:00";
EOF

# 2. 固定 sort 排序
cat > foo-fix-sort.patch << 'EOF'
--- a/Makefile
+++ b/Makefile
@@ -10,1 +10,1 @@
-LIST = $(shell ls src/)
+LIST = $(shell ls src/ | LC_ALL=C sort)
EOF

# 3. 移除用户名和主机名
cat > foo-fix-user.patch << 'EOF'
--- a/configure.ac
+++ b/configure.ac
@@ -5,2 +5,2 @@
-AC_DEFINE_UNQUOTED([BUILD_USER], ["$USER"])
-AC_DEFINE_UNQUOTED([BUILD_HOST], ["$HOSTNAME"])
+AC_DEFINE_UNQUOTED([BUILD_USER], ["guix"])
+AC_DEFINE_UNQUOTED([BUILD_HOST], ["guix"])
EOF

9.5.3 搜索和管理补丁

# Guix 补丁通常存储在 gnu/packages/patches/ 目录
ls ~/.config/guix/current/share/guix/patches/

# 使用 search-patches 函数搜索
# (search-patches "foo-fix.patch") 会在标准路径中搜索

9.6 常见不可重现因素

9.6.1 不确定性来源表

来源示例解决方案
时间戳__DATE__, __TIME__SOURCE_DATE_EPOCH、补丁
用户信息$USER, $HOME沙箱中固定为 “guix” 和 “/homeless-shelter”
主机名$HOSTNAME沙箱中无主机名
随机数rand(), /dev/urandom固定种子或使用确定性随机
文件排序ls, glob使用 LC_ALL=C sort
并行构建线程竞争禁用并行或修复并发 bug
网络访问下载动态内容沙箱中禁止网络
绝对路径硬编码 /usr/lib使用相对路径或 store 路径
构建路径调试信息中的路径strip 或 --fdebug-prefix-map
Locale不同排序规则固定为 C.UTF-8

9.6.2 诊断不可重现问题

# 方法一:两次构建对比
guix build vim --no-substitutes -o /tmp/build1
guix build vim --no-substitutes -o /tmp/build2
diffoscope /tmp/build1 /tmp/build2

# 方法二:使用 diffoscope 工具
guix shell diffoscope -- diffoscope /gnu/store/abc... /gnu/store/def...
;; 在 manifest 中添加诊断工具
(specifications->manifest
  '("diffoscope"      ; 二进制比较工具
    "strip-nondeterminism"  ; 移除非确定性信息
    "diffutils"       ; diff 工具
    "binutils"))      ; objdump 等

9.7 验证可重现性

9.7.1 Rebasing 验证

# 从源码重新构建并与官方二进制对比
guix build --no-substitutes --check vim

# --check 选项会:
# 1. 从源码构建
# 2. 与已有的 store 对象对比
# 3. 如果不一致则报错

9.7.2 批量验证

# 验证多个包
for pkg in vim git python gcc; do
  echo "Checking $pkg..."
  guix build --no-substitutes --check $pkg || \
    echo "FAILED: $pkg"
done

9.7.3 社区验证基础设施

https://reproducible-builds.org/
https://tests.reproducible-builds.org/

Guix 参与可重现构建社区:
- 定期提交构建报告
- 修复不可重现的包
- 参与标准制定

9.8 可重现的开发环境

9.8.1 Manifest 驱动的可重现环境

;; project-env.scm — 完全确定性的开发环境
(specifications->manifest
  '("gcc-toolchain"     ; 编译器工具链
    "cmake"             ; 构建系统
    "pkg-config"        ; 库发现
    "gdb"               ; 调试器
    "valgrind"          ; 内存检查
    "strace"            ; 系统调用跟踪
    "python"            ; 脚本
    "python-pytest"))   ; 测试框架
# 从 manifest 创建可重现环境
guix time-machine --channels=channels.scm -- \
  shell --manifest=project-env.scm

9.8.2 跨团队的环境一致性

# 步骤 1:团队负责人锁定环境
guix describe --format=channels > channels.scm
# channels.scm 和 manifest.scm 提交到 Git 仓库

# 步骤 2:团队成员使用
git pull
guix time-machine --channels=channels.scm -- \
  shell --manifest=manifest.scm

# 现在所有成员拥有完全相同的开发环境

9.9 CI/CD 中的可重现构建

9.9.1 GitLab CI 配置

# .gitlab-ci.yml
stages:
  - build
  - test

build:
  stage: build
  image: guix
  script:
    - guix time-machine --channels=channels.scm -- \
        build --no-substitutes my-project

test:
  stage: test
  image: guix
  script:
    - guix time-machine --channels=channels.scm -- \
        shell --manifest=manifest.scm -- make test

9.9.2 GitHub Actions

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Guix
        run: |
          wget https://git.savannah.gnu.org/cgit/guix.git/plain/etc/guix-install.sh
          chmod +x guix-install.sh
          sudo ./guix-install.sh
      - name: Build
        run: |
          guix time-machine --channels=channels.scm -- \
            build --no-substitutes my-project

9.10 可重现构建检查清单

检查项命令/方法
时间戳是否固定strings binary | grep -i date
用户名是否固定strings binary | grep -i user
主机名是否固定strings binary | grep -i host
构建路径是否固定strings binary | grep /gnu/store
文件排序是否确定检查 Makefile 中的 sort 命令
并行构建是否安全--parallel-build? #f 测试
补丁是否应用guix build -v 3 查看日志
两次构建一致guix build --no-substitutes --check

9.11 总结

本章深入讲解了可重现构建的原理与实践:

  1. 核心概念——相同输入产出相同输出
  2. 时间戳处理——SOURCE_DATE_EPOCH 和补丁
  3. 环境变量控制——沙箱中的确定性环境
  4. 输入固定——哈希计算和版本锁定
  5. 补丁管理——修复不可重现问题
  6. 诊断工具——diffoscope 和验证命令
  7. CI 集成——在持续集成中实现可重现构建

下一章我们将学习 Guix Home 的用户环境管理。


扩展阅读