Site icon Craig Andrews

Reproducible Builds in Java

Reproducible builds are a set of software development practices that create an independently-verifiable path from source to binary code.

https://reproducible-builds.org/

Reproducible builds are important and provide benefits in many areas, including:

Given how important and valuable reproducible builds are, how can this goal be achieved?

Reasons Why Builds are Already Reproducible

There are a number of common reasons why builds aren’t already reproducible, including:

Reproducible Foundation: Java and the Operating System

Reproducible builds require a reproducible foundation: if the tools that execute the build aren’t reproducible, then the output of the build (probably) won’t be either. Docker containers are a great way to make the build environment reproducible. For example, if you execute builds using Docker with the openjdk:11.0.7-jdk tag, then you know exactly what version of Java and all of its dependencies are being used, and that they’ll never change.

However, with Java, this concern isn’t as great as it is with some other platforms because Java (within a major release) is very stable. If your project is built with Java 11.0.0, then again with Java 11.0.7, the output artifacts will (almost certainly) be identical.

Reproducible Build System: Using Wrappers

The next step is making the build system reproducible. Having a reproducible build system means that a commit expresses the exact version of the build system to be used. For example, the commit would express that Maven 3.6.3 is to be used. Both Gradle and Maven offer wrappers which make them reproducible.

As an added bonus, developers / CI systems / etc will no longer need to install the build system (Gradle or Maven) at all. When they use the wrapper command (mvnw/gradlew), it will download the correct version of the build system and take care of everything else.

Using the Maven Wrapper

Use Maven Wrapper to ensure that the same version of Maven is used and that that version is source controlled. To enable it, run:

mvn -N io.takari:maven:0.7.7:wrapper

and commit the added files to source control. From then on, instead of running mvn use mvnw.

Using the Gradle Wrapper

Gradle Wrapper allows the version of Gradle used for a build to be source controlled. Unlike Maven, use of the Gradle wrapper is extremely common in Gradle so your project likely uses it already. To enable it, run:

gradle wrapper

then commit the resulting files to source control. From then on, use the gradlew command instead of gradle.

Reproducible Build Itself

Finally, the build itself needs to be made reproducible. When using the same foundation and the same version of the same build system, the build should produce exactly the same output to the byte.

Reproducible Maven Builds

Maven provides documentation on reproducible builds which includes steps on how to modify pom.xml and then how to determine what parts of the output artifact(s) are not reproducible. That approach will (eventually) work, but I’ve found that there is an easier way.

The Reproducible Build Maven Plugin is a quick and easy way to make builds reproducible. Add it to the project’s pom.xml:

<build>
                <plugin>
                        <groupId>io.github.zlika</groupId>
                        <artifactId>reproducible-build-maven-plugin</artifactId>
                        <version>0.12</version>
                        <executions>
                                <execution>
                                        <goals>
                                                <goal>strip-jar</goal>
                                        </goals>
                                        <configuration>
                                                <zipDateTimeFormatPattern>yyyy-MM-dd'T'HH:mm:ssZ</zipDateTimeFormatPattern>
                                                <zipDateTime>${git.commit.time}</zipDateTime>
                                        </configuration>
                                </execution>
                        </executions>
                </plugin>
	</plugins>
</build>

And that’s it – no more configuration necessary. This plugin includes work around for common plugins that don’t (yet) produce reproducible artifacts, such as springboot-maven-plugin and maven-war-plugin.

Making the Gradle Build Reproducible

Gradle provides documentation on how to make the Gradle build reproducible. In summary, add the following to build.gradle:

tasks.withType(AbstractArchiveTask) {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

And that’s it.

Confirm Reproducibility

A quick and dirty way to check for reproducibility is to run the build twice and see if the checksum of the artifacts match for both builds.

Here’s a one liner that does that gradle:

( ./gradlew clean && ./gradlew build ) > /dev/null && md5sum ./build/libs/*.jar && ( ./gradlew clean && ./gradlew build ) > /dev/null && md5sum ./build/libs/*.jar

And here’s a one liner that does that for Maven:

( ./mvnw clean && ./mvnw package ) > /dev/null && md5sum ./target/*.jar && ( ./mvnw clean && ./mvnw package ) > /dev/null && md5sum ./target/*.jar

If the before and after checksums match, then enjoy all of the security, maintainability, and other advantages of your reproducible build.

Reproducible Builds in Java by Craig Andrews is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Exit mobile version