为什么同一个 JAR 文件在我每次构建时都有不同的哈希值?

Why does the same JAR file have different hash every time I build it?

我一直在考虑检查 jar 文件的哈希值以确定它是否已更改,但事实证明每次构建时同一个 jar 文件都有不同的哈希值(从 eclipse 导出为 jar 文件,或使用 maven 构建它)。我已经删除了清单文件的日期值和内容,但它仍然不同。字节码生成中是否包含时间戳或其他内容?

JAR 文件是 ZIP 文件,它在 local file headers 和中央目录文件头中包含最后修改日期。这将导致您的构建具有不同的哈希值。

如果您 运行 在完全相同的一组文件(具有相同的文件日期)上执行 JAR 命令并跳过清单文件创建,它应该会为您提供完全相同的 JAR 文件(如果ZIP 不变)。

我在 Gradle 构建时遇到了同样的问题。就我而言,我的 .war 文件包含许多内置的 .jar 文件。

在 Gradle 中,Jar 和 War 任务本质上都是 Zip 任务的变体,后者有一个名为 "preserveFileTimestamps" 的 属性(https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Zip.html#org.gradle.api.tasks.bundling.Zip:preserveFileTimestamps ) 要使 SHA 相同,请将此 属性 用于 jar 和 war 任务,例如,在 build.gradle:

中的某处
plugins.withType(WarPlugin).whenPluginAdded {
    war {
        preserveFileTimestamps = false
    }
}
jar {
    preserveFileTimestamps = false
}

还有一个有趣的注意事项,如果您在 MacOS 上构建,请确保 .DS_Store 文件不会进入构建的存档,因为它也会导致不同的 SHA。

要在 MacOS 上禁用,运行 在终端中这样做:

defaults write com.apple.desktopservices DSDontWriteNetworkStores true

然后重新启动它。您仍然需要删除现有的 .DS_Store 文件,因此从您的项目文件夹中,运行:

find . -name '.DS_Store' -exec rm {} \;

如果你想在不同的操作系统上构建相同的 SHA,请为 war 和 jar 任务设置 reproducibleFileOrder 属性 为真,并确保umask 在您构建的两个系统上是相同的(显然 gradle 包括 war/jar 文件中的文件属性,当这些属性不同时我有不同的 SHA)。

最后,无论我在哪里构建,我都能获得相同的工件 SHA。

干杯

使用 Java 获得可重现的构建,即。始终产生相同二进制输出的构建需要一些调整,因为 Java 从一开始就不是可重现友好的:带有文件顺序和时间戳的 jar 文件是变化的第一个自然来源。除了由 Java 引起的问题外,一些 Maven 插件还会引起其他变化:请参阅 Maven Reproducible/Verifiable Builds Wiki 页面 https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=74682318

您可以使用 reproducible-build-maven-plugin: https://zlika.github.io/reproducible-build-maven-plugin 用于 Apache Maven 构建工具,在 Java 个项目中很受欢迎,或者 sbt-reproducible-builds 插件 https://github.com/raboof/sbt-reproducible-builds 用于 sbt 构建工具,在 Scala 项目中很受欢迎。 对于 Gradle 工具:https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives

有关 'Reproducible Builds' 的一般信息,请参阅 https://reproducible-builds.org

我的 gradle 文件中最适合我的解决方案如下(请注意,我还删除了可以通过某些任务更改的清单日期):

// Prevent manifest from changing every build
project.tasks.withType(Jar) {
    manifest.attributes Date: ''
}

// Prevent timestamps from appearing in JAR and use reproducible file order
tasks.withType(AbstractArchiveTask) {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

灵感来自:https://dzone.com/articles/reproducible-builds-in-java