Skip to content

可重现构建实战

可重现构建(Reproducible Builds)是指在不同的环境(开发机、CI 服务器、甚至 5 年后的新电脑)中,针对同一份源码进行构建,生成的产物(APK/AAB)在字节码层面必须是完全一致的。

为什么需要可重现构建?

  1. 安全性: 确保下载到的 APK 就是源码生成的,没有被编译器或 CI 注入后门。
  2. 调试一致性: 避免“为什么 CI 编出来的包会崩,我本地编的就没事”。
  3. 缓存命中率: 输入一致,输出才一致,才能最大化利用远程构建缓存。

破坏重现性的元凶

  • 绝对路径: Task 输入中包含了 /Users/virogu/... 这种用户特定的路径。
  • 非确定性 Task: 任务代码里使用了 new Date()Random()
  • 文件排序: 在遍历目录时,不同的操作系统(Windows vs Linux)返回的文件顺序不同。

工业级规避方案

1. 路径敏感度处理

在自定义 Task 中,务必对文件路径标注敏感度:

kotlin
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // 仅关注相对路径
abstract val inputFiles: ConfigurableFileCollection

2. 移除动态时间戳

不要在 BuildConfig 中直接写入编译时间。如果非要写,建议从 Git 提交时间获取。

kotlin
// 错误写法
buildConfigField("String", "BUILD_TIME", "\${Date()}")

// 正确写法:从环境变量或 Git 读一个固定值
val buildTime = System.getenv("BUILD_TIME") ?: "0"

3. 固定排序规则

在代码生成插件中,对源文件列表进行显式排序。

kotlin
val sortedFiles = sourceFiles.files.sortedBy { it.name }

环境 Parity (环境对等)

为了确保重现性,团队应强制要求:

  1. Gradle Wrapper: 版本必须完全锁定。
  2. JDK 版本: 建议使用 toolchains 强制指定构建版本。
    kotlin
    kotlin {
        jvmToolchain(17)
    }
  3. OS 差异: 对于处理文件路径的逻辑,始终使用 / 作为分隔符,避免使用 File.separator

验证重现性

你可以使用 diffoscope 工具对比两次构建生成的 APK:

bash
diffoscope build1.apk build2.apk

如果输出为空,说明你的构建已达成完美的“可重现性”。