Skip to content

Commit

Permalink
Make JAR builds reproducible (junit-team#2217)
Browse files Browse the repository at this point in the history
This change uses Gradle's reproducible archives feature to consistently
build the output JARs.

Additionally, the build now supports `SOURCE_DATE_EPOCH` to allow
overriding of `buildTimeAndDate` as this introduces non-determinism
into the build.

Timestamps in special-case JARs are now rewritten to make them
reproducible using the same fixed-time constant as is being used when
`isPreserveFileTimestamps` is set to false.

Moreover, a GitHub Actions workflow is added that checks whether build
is still reproducible on CI.
  • Loading branch information
mrwilson authored Apr 3, 2020
1 parent 0e06ccc commit 5631ebc
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 1 deletion.
25 changes: 25 additions & 0 deletions .github/workflows/reproducible-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Reproducible build

on:
push:
branches:
- master
- 'releases/*'
pull_request:
branches:
- '*'

jobs:
check_build_reproducibility:
name: 'Check build reproducibility'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Set up JDK 11'
uses: actions/setup-java@v1
with:
java-version: 11
- name: Build and compare checksums
shell: bash
run: |
./src/checkBuildReproducibility.sh
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ gradle-app.setting
*.graphml
coverage.db*
.metadata

checksums*
26 changes: 25 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import java.time.OffsetDateTime
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

plugins {
Expand All @@ -16,7 +18,24 @@ buildScan {
}
}

val buildTimeAndDate = OffsetDateTime.now()
val buildTimeAndDate by extra {

// SOURCE_DATE_EPOCH is a UNIX timestamp for pinning build metadata against
// in order to achieve reproducible builds
//
// More details - https://reproducible-builds.org/docs/source-date-epoch/

if (System.getenv().containsKey("SOURCE_DATE_EPOCH")) {

val sourceDateEpoch = System.getenv("SOURCE_DATE_EPOCH").toLong()

Instant.ofEpochSecond(sourceDateEpoch).atOffset(ZoneOffset.UTC)

} else {
OffsetDateTime.now()
}
}

val buildDate by extra { DateTimeFormatter.ISO_LOCAL_DATE.format(buildTimeAndDate) }
val buildTime by extra { DateTimeFormatter.ofPattern("HH:mm:ss.SSSZ").format(buildTimeAndDate) }
val buildRevision by extra { versioning.info.commit }
Expand Down Expand Up @@ -108,6 +127,11 @@ subprojects {
version = property("vintageVersion")!!
}

tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}

pluginManager.withPlugin("java") {

spotless {
Expand Down
42 changes: 42 additions & 0 deletions buildSrc/src/main/kotlin/java-repackage-jars.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import org.gradle.api.internal.file.archive.ZipCopyAction

// This registers a `doLast` action to rewrite the timestamps of the project's output JAR
afterEvaluate {
val jarTask = (tasks.findByName("shadowJar") ?: tasks["jar"]) as Jar

jarTask.doLast {

val newFile = createTempFile("rewrite-timestamp")
val originalOutput = jarTask.archiveFile.get().getAsFile()

newFile.outputStream().use { os ->

val newJarStream = JarOutputStream(os)
val oldJar = JarFile(originalOutput)

oldJar.entries()
.toList()
.distinctBy { it.name }
.sortedBy { it.name }
.forEach { entry ->
val jarEntry = JarEntry(entry.name)

// Use the same constant as the fixed timestamps in normal copy actions
jarEntry.time = ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES

newJarStream.putNextEntry(jarEntry)

oldJar.getInputStream(entry).copyTo(newJarStream)
}

newJarStream.finish()
}

newFile.renameTo(originalOutput)
}
}
12 changes: 12 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/appendix.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
[[appendix]]
== Appendix

[[reproducible-builds]]
=== Reproducible Builds

Starting with version 5.7, JUnit 5 aims for its non-javadoc JARs to be https://reproducible-builds.org/[reproducible]

Under identical build conditions, such as Java version, repeated builds should provide the
same output byte-for-byte.

This means that anyone can reproduce the build conditions of the artifacts on Maven
Central/Sonatype and produce the same output artifact locally, confirming that the
artifacts in the repositories were actually generated from this source code.

[[dependency-metadata]]
=== Dependency Metadata

Expand Down
1 change: 1 addition & 0 deletions junit-platform-commons/junit-platform-commons.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import java.util.spi.ToolProvider
plugins {
`java-library-conventions`
`java-multi-release-sources`
`java-repackage-jars`
}

description = "JUnit Platform Commons"
Expand Down
1 change: 1 addition & 0 deletions junit-platform-console/junit-platform-console.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
`java-library-conventions`
`shadow-conventions`
`java-multi-release-sources`
`java-repackage-jars`
}

description = "JUnit Platform Console"
Expand Down
23 changes: 23 additions & 0 deletions src/checkBuildReproducibility.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash -e

rm -rf checksums*

export SOURCE_DATE_EPOCH=$(date +%s)

function calculate_checksums() {
OUTPUT=$1

./gradlew --no-build-cache clean assemble --parallel

find . -name '*.jar' \
| grep '/build/libs/' \
| grep --invert-match 'javadoc' \
| sort \
| xargs sha256sum > ${OUTPUT}
}


calculate_checksums checksums-1.txt
calculate_checksums checksums-2.txt

diff checksums-1.txt checksums-2.txt

0 comments on commit 5631ebc

Please sign in to comment.