Repository: mockito/mockito
Issue: Build is not reproducible

Repeated subsequent builds of the project create outputs which are not byte-for-byte identical, and so cannot be reproduced locally (see: [Reproducible Builds](https://reproducible-builds.org/))

This is due to:
 * File modification timestamps in JARs.
 * Non-deterministic file ordering in JARs.
 * OSGi bundle embedding timestamps in JAR manifests.

Given the build environment under which a version was created, I would like to be able to compare the checksums published on Bintray and/or Maven Central with a locally built version against the same environment, to assure myself that the artifact being pulled has actually been generated from this source code.

(I have a PR to address this which I am happy to raise)



Issue comments:
Feel free to open a PR and we can take a look 😄 

Pull request review comments:
Nit: Use full Java block comments (e.g. lines starting with `*`)
Nit: change this link to https://github.com/gradle/gradle/blob/58538513a3bff3b7015718976fe1304e23f40694/subprojects/core/src/main/java/org/gradle/api/internal/file/archive/ZipCopyAction.java#L42-L57
This runs the tests as well, which is not what we want. We only want to package the jar, to not incur a high cost on our CI. Can we only package the jar and verify the checksums on that?
Oh, ofc, we can run `assemble` instead of `build` which creates the artifacts but doesn't run the tests. My mistake!

Patch that resolved the issue:
diff --git a/.gitignore b/.gitignore
index 9d0c115582..09ab494393 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,6 @@ out
 classes
 .gradletasknamecache
 *.log
+
+# Used to check reproducibility of Mockito jars
+checksums/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 26c444f3ef..43f616aa7b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -50,7 +50,7 @@ script:
   # We are using && below on purpose
   # ciPerformRelease must run only when the entire build has completed
   # This guarantees that no release steps are executed when the build or tests fail
-  - ./gradlew spotlessCheck && ./gradlew build idea -s && ./gradlew ciPerformRelease
+  - ./gradlew spotlessCheck && ./gradlew build idea -s && ./gradlew checkReproducibility && ./gradlew ciPerformRelease
 
 after_success:
   #Generates coverage report:
diff --git a/build.gradle b/build.gradle
index a44878c0a9..7e11a05e1f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -42,6 +42,7 @@ apply from: 'gradle/root/coverage.gradle'
 apply from: 'gradle/mockito-core/inline-mock.gradle'
 apply from: 'gradle/mockito-core/osgi.gradle'
 apply from: 'gradle/mockito-core/javadoc.gradle'
+apply from: 'gradle/mockito-core/reproducible-build.gradle'
 apply from: 'gradle/mockito-core/testing.gradle'
 
 apply from: 'gradle/dependencies.gradle'
@@ -64,6 +65,12 @@ allprojects { proj ->
         options.addStringOption('charSet', 'UTF-8')
         options.setSource('8')
     }
+
+    tasks.withType(AbstractArchiveTask) {
+        preserveFileTimestamps = false
+        reproducibleFileOrder = true
+    }
+
     apply plugin: 'checkstyle'
     checkstyle {
        configFile = rootProject.file('config/checkstyle/checkstyle.xml')
diff --git a/gradle/mockito-core/osgi.gradle b/gradle/mockito-core/osgi.gradle
index 1e942f6569..9ed59f8363 100644
--- a/gradle/mockito-core/osgi.gradle
+++ b/gradle/mockito-core/osgi.gradle
@@ -1,3 +1,12 @@
+import java.nio.file.attribute.FileTime
+import java.util.jar.Attributes
+import java.util.jar.JarEntry
+import java.util.jar.JarFile
+import java.util.jar.JarOutputStream
+import java.util.jar.Manifest
+
+import static java.util.Calendar.FEBRUARY
+
 apply plugin: 'osgi'
 
 afterEvaluate {
@@ -31,5 +40,88 @@ afterEvaluate {
 
             attributes 'Automatic-Module-Name': 'org.mockito'
         }
+
+        jar.doLast {
+            JarFile originalJar = new JarFile(jar.archivePath)
+
+            new RemoveOsgiLastModifiedHeader(originalJar)
+                .transform()
+                .renameTo jar.archivePath
+        }
+
     }
 }
+
+
+/*
+ * The OSGi plugin in Gradle 5.3 has a known issue (https://github.com/gradle/gradle/issues/6187) where it embeds
+ * a Manifest entry "Bnd-LastModified" (set to the last modified time of the build directory) even if you ask it not to
+ * using `-noextraheaders` or `-removeheaders`.
+ *
+ * It cannot be pinned or removed, and is a source of non-determinism in the build.
+ *
+ * This class addresses the problem by rewriting the JAR and removing that entry from the Manifest. A side-effect of this
+ * is having to set `lastModifiedTime` for each entry to a fixed value, else this would also introduce non-determinism.
+ *
+ * The fixed value is the same as the default value for Gradle's fixed timestamps, and the rationale behind it is detailed
+ * in https://github.com/gradle/gradle/blob/58538513a3bff3b7015718976fe1304e23f40694/subprojects/core/src/main/java/org/gradle/api/internal/file/archive/ZipCopyAction.java#L42-L57
+ */
+
+class RemoveOsgiLastModifiedHeader {
+    private final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, FEBRUARY, 1, 0, 0, 0).timeInMillis
+
+    private final JarFile artifact
+
+    RemoveOsgiLastModifiedHeader(JarFile artifact) {
+        this.artifact = artifact
+    }
+
+    File transform() {
+        File temp = File.createTempFile("temp",".jar")
+
+        temp.withOutputStream { os ->
+            JarOutputStream stream = new JarOutputStream(os)
+
+            this.artifact
+                .entries()
+                .sort { it.name }
+                .each { JarEntry entry ->
+
+                    stream.putNextEntry(newJarEntry(entry))
+
+                    if (entry.name == "META-INF/MANIFEST.MF") {
+                        newManifest(entry).write(stream)
+                    } else {
+                        stream << this.artifact.getInputStream(entry)
+                    }
+
+                    stream.closeEntry()
+                }
+
+            stream.finish()
+        }
+
+        temp
+    }
+
+    Manifest newManifest(JarEntry entry) {
+        Manifest manifest = new Manifest()
+
+        manifest.read(this.artifact.getInputStream(entry))
+
+        manifest
+            .getMainAttributes()
+            .remove(new Attributes.Name("Bnd-LastModified"))
+
+        manifest
+    }
+
+    JarEntry newJarEntry(JarEntry original) {
+        JarEntry newEntry = new JarEntry(original.name)
+
+        newEntry.setLastModifiedTime(FileTime.fromMillis(CONSTANT_TIME_FOR_ZIP_ENTRIES))
+
+        newEntry
+    }
+
+}
diff --git a/gradle/mockito-core/reproducible-build.gradle b/gradle/mockito-core/reproducible-build.gradle
new file mode 100644
index 0000000000..bb513706ab
--- /dev/null
+++ b/gradle/mockito-core/reproducible-build.gradle
@@ -0,0 +1,69 @@
+import java.security.DigestInputStream
+import java.security.MessageDigest
+
+/*
+ * The `checkReproducibility` task compares two sets of checksums, generated
+ * in two independent builds, from non-javadoc output archives.
+ *
+ * This task ensures that a change has not regressed the reproducibility of
+ * the Mockito project and its subprojects.
+ */
+
+task checkReproducibility {
+    doLast {
+        def first = new File("checksums", "checksums-1.txt").text.split('\n')
+        def second = new File("checksums", "checksums-2.txt").text.split('\n')
+
+        assert first.sort() == second.sort()
+    }
+}
+
+task getChecksums1(type: GradleBuild) {
+    tasks = ['clean', 'assemble']
+    startParameter.projectProperties = ['build-id': "1"]
+}
+
+task getChecksums2(type: GradleBuild) {
+    tasks = ['clean', 'assemble']
+    startParameter.projectProperties = ['build-id': "2"]
+}
+
+getChecksums2.dependsOn(getChecksums1)
+checkReproducibility.dependsOn(getChecksums2)
+
+if (project.hasProperty('build-id')) {
+
+    String id = project.getProperty('build-id')
+
+    mkdir "checksums"
+
+    File checksums = new File("checksums", "checksums-${id}.txt")
+    checksums.delete()
+
+    afterEvaluate {
+
+        ([project] + subprojects).each { p ->
+
+            p.tasks.withType(AbstractArchiveTask) { task ->
+
+                if (!task.name.contains('javadoc')) {
+                    task.doLast {
+                        File output = task.archiveFile.get().getAsFile()
+
+                        checksums << "${output.name},${calculateHash(output)}\n"
+                    }
+                }
+            }
+        }
+
+    }
+}
+
+static def calculateHash(File file) {
+    file.withInputStream {
+        new DigestInputStream(it, MessageDigest.getInstance("SHA-256")).withStream {
+            it.eachByte {}
+            it.messageDigest.digest().encodeHex() as String
+        }
+    }
+}


Test patch: