https://reproducible-builds.org/ Reproducible builds are a set of software development practices that create a verifiable path from human readable source code to the binary code used by computers
How?
First, the build system needs to be made entirely deterministic: transforming a given source must always create the same result. Typically, the current date and time must not be recorded and output always has to be written in the same order.
Second, the set of tools used to perform the build and more generally the build environment should either be recorded or pre-defined.
Third, users should be given a way to recreate a close enough build environment, perform the build process, and verify that the output matches the original build.
Tooling like diffoscope have been created to measure differences between archives content.
Java builds are naturally not immediately reproducible: timestamps in jar files are the first source of non-idempotence (if you do a build twice, the result won't be the same bit for bit).
But Maven sometimes adds some variable parts that adds to the problem: timestamp text or username in MANIFEST.MF, ... reproducible-build-maven-plugin has been created to try to fix issues after packaging.
The goal of this proposal is to prepare a set of configuration and practices to have reproducible/verifiable builds at packaging time, both by enhancing java natural build behaviour and by removing some variability introduced by some Maven plugins (core plugins at first, but also in the Maven eco-system)
Use cases
- As a user of artifacts published on repositories like Maven Central, I want to be able to check that the binary version of the artifact matches its source version. On a software QA point of view, this would allow to detect quality problems in the build/publish process. On a computer security point of view, this would allow to detect the introduction of a backdoor during the build/publish process.
- As a developer voting on an Apache source release against a staging repository, I want to verify that the binary from my local build from sources is the same as the binary that is staged and signed by the release manager
Sources of unreproducible bits
- Timestamps:
- Timestamp in pom.properties generated by maven-archiver (MSHARED-494)
- Timestamp in plugin.xml and plugin-help.xml descriptors generated by maven-plugin-tools-generator (MPLUGIN-326)
- Timestamp in MANIFEST.MF (Bnd-LastModified) generated by maven-bundle-plugin
- Timestamps in ZIP/JAR files: file last modification time/date in central directory and file entry headers + possible optional fields "X5455_ExtendedTimestamp" (PLEXUS-ARCHIVER-48)
- Timestamps in generated javadoc HTML files (can be disabled with javadoc options "notimestamp" and "bottom")
- Timestamps in bytecode generated from Groovy code (added by GroovyClassLoader.addTimeStamp())
- Username:
- Username in MANIFEST.MF (Built-By) generated by maven-archiver (MSHARED-661)
- UID/GID in tar file entries
- Ordering:
- Order of the file entries in a ZIP/JAR file (depends on file system order)
- Order of goals in plugin.xml generated by maven-plugin-tools (MPLUGIN-261)
- Order of the methods of the ObjectFactory.java file generated by JAXB/xjc (JAXB-598)
- Order of components in META-INF/plexus/components.xml generated by plexus metadata
Version numbers of some of the tools used to build the artifact are also added to the artifact itself (e.g. "Built-Jdk" and "Created-By" MANIFEST entries added by maven-archiver to record the exact JDK and Maven versions used to build). This does not make the build unreproducible (because you just have to use the same tool versions to generate the same artifact) but it makes it more difficult to reproduce because of that. Line endings is also a problem, and even if we could force given line endings for build-generated text files (MANIFEST, pom.properties...), it would be hazardous to try to change the line endings of the resource files.
The underlying problem is that the pom file does not capture all the configuration of the build environment: it includes the plugins used during the build with their version number, but it does not include the version of Maven and the JDK, the Operating System and the architecture used to produce the artifact, etc...
What can we do? (non-exhaustive list):
- Drop the Maven/JDK version numbers in MANIFEST: most of the time you should get exactly the same result with two different versions of Maven and/or JDK (if you keep the same major version number). But you have to restrain to a given OS family because of line endings.
- Keep the version numbers in the MANIFEST: there are not easily accessible here. Moreover the semantic is poor (e.g. there is no JDK vendor so you can have different results if you use OpenJDK/Oracle JDK/Eclipse compiler, no reference of the Operating System used for line endings).
- Add a "reproducible build bill of materials" in an external file. This is the way Debian took to manage reproducible builds: they record the "build environment" in an external ".buildinfo" file that has all the information required to reproduce the build environment. If the frame of Maven we can think of several ways to achieve that (non-exhaustive list):
- Create a secondary artifact (e.g. *-buildinfo.xml) with the required information
- "Patch" the published pom file to add properties with the required information, something like:
<properties> <maven.reproducible.build.maven.version>3.5.0</maven.reproducible.build.maven.version> <maven.reproducible.build.jdk.version>8u123</maven.reproducible.build.jdk.version> <maven.reproducible.build.jdk.vendor>openjdk</maven.reproducible.build.jdk.vendor> <maven.reproducible.build.arch>amd64</maven.reproducible.build.arch> <maven.reproducible.build.os>linux</maven.reproducible.build.os> </properties>
What are the issues to solve?
issue tracking | description |
---|---|
MSHARED-661 | maven-archiver adds "Built-By" and "Built-Jdk" Manifest entries |
MSHARED-494 | Timestamp in pom.properties |
support SOURCE_DATE_EPOCH environment variable or equivalent: see https://reproducible-builds.org/docs/timestamps/ | |
generated plugin.xml is non-deterministic | |
MPLUGIN-326 | Timestamp in plugin.xml and plugin-help.xml descriptors generated by maven-plugin-tools-generator |
codehaus-plexus/plexus-archiver issue #48 | avoid timestamp issues in archives created by plexus-archiver (widely used in Maven plugins creating jar, zip, war, tar... archives) |
codehaus-plexus/plexus-containers | sort components when generating META-INF/plexus/components.xml |
Debian approach
Debian has a strong reproducible builds structure working on the topic for a few years.
On java and Maven issues, Debian maintains a serie of patches that perhaps could be integrated (thank you Emmanuel Bourg for the summary):
- Maven core management of SOURCE_DATE_EPOCH: https://github.com/Debian/maven/blob/master/debian/patches/reproducible-build-timestamp.patch
- fix for MPLUGIN-326: https://sources.debian.net/src/maven-plugin-tools/3.5-4/debian/patches/04-reproducible-plugin-descriptor.patch/
- dates in javadoc footer: https://sources.debian.net/src/maven-javadoc-plugin/2.10.4-1/debian/patches/reproducible-footer.patch/
- plexus containers issue #8: https://sources.debian.net/src/plexus-containers1.5/1.7.1-4/debian/patches/03-reproducible-metadata.patch/ and old https://sources.debian.net/src/plexus-maven-plugin/1.3.8-10/debian/patches/0005-reproducible-metadata.patch/
- sisu-inject: https://sources.debian.net/src/sisu-ioc/2.3.0-9/debian/patches/reproducible-index.patch/
REX on Clojure: source .clj must have one second difference with .class, or Clojure will recompile