可重现构建实战
可重现构建(Reproducible Builds)是指在不同的环境(开发机、CI 服务器、甚至 5 年后的新电脑)中,针对同一份源码进行构建,生成的产物(APK/AAB)在字节码层面必须是完全一致的。
为什么需要可重现构建?
- 安全性: 确保下载到的 APK 就是源码生成的,没有被编译器或 CI 注入后门。
- 调试一致性: 避免“为什么 CI 编出来的包会崩,我本地编的就没事”。
- 缓存命中率: 输入一致,输出才一致,才能最大化利用远程构建缓存。
破坏重现性的元凶
- 绝对路径: Task 输入中包含了
/Users/virogu/...这种用户特定的路径。 - 非确定性 Task: 任务代码里使用了
new Date()或Random()。 - 文件排序: 在遍历目录时,不同的操作系统(Windows vs Linux)返回的文件顺序不同。
工业级规避方案
1. 路径敏感度处理
在自定义 Task 中,务必对文件路径标注敏感度:
kotlin
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // 仅关注相对路径
abstract val inputFiles: ConfigurableFileCollection2. 移除动态时间戳
不要在 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 (环境对等)
为了确保重现性,团队应强制要求:
- Gradle Wrapper: 版本必须完全锁定。
- JDK 版本: 建议使用
toolchains强制指定构建版本。kotlinkotlin { jvmToolchain(17) } - OS 差异: 对于处理文件路径的逻辑,始终使用
/作为分隔符,避免使用File.separator。
验证重现性
你可以使用 diffoscope 工具对比两次构建生成的 APK:
bash
diffoscope build1.apk build2.apk如果输出为空,说明你的构建已达成完美的“可重现性”。