Customize JAR manifest entries with Maven/Gradle

How many times have you found yourself in a situation where you can't tell if the artifacts used by the application are the correct ones or not? How many times have you looked at a bug report stating that the problem is caused by version X of a particular artifact but you are certain the bug was fixed in said release? On closer inspection (after unpacking, debugging, even decompiling!) you find that the artifact in production is not the right one even though the version appears to be the correct one.

One option we have to mitigate this problem is to provide additional metadata as part of the artifact's archive. We could write down said metadata to a properties file and add it to the package archive, or we could reuse an existing facility: the archive's manifest.

Personally I'd like to find out the following information when inspecting the metadata for a particular artifact:

  • When was the artifact built?
  • Who built it?
  • Which JDK version was used to compile the classes?
  • What's the SCM reference? (the Git commit hash for example).
  • Which operating system was used to build it?

Let's start with Gradle as it's the one that requires less setup. Gradle let's you customize archive manifest from the get go, you just have to add entries you desire to the manifest's attributes. There's a catch though, Gradle does not know a thing about SCM properties, you'll have to add a plugin that exposes this information, such as the net.nemerosa.versioning plugin. The following snippet shows the minimum configuration to setup the metadata that answers my previous questions:

build.gradle

plugins {
    id 'java'
    id 'net.nemerosa.versioning' version '2.6.1'
}

jar {
    manifest {
        attributes(
            'Built-By'       : System.properties['user.name'],
            'Build-Timestamp': new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
            'Build-Revision' : versioning.info.commit,
            'Created-By'     : "Gradle ${gradle.gradleVersion}",
            'Build-Jdk'      : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
            'Build-OS'       : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
        )
    }
}

Packaging the artifact and inspecting the generated manifest (found already unpacked at build/tmp/jar/MANIFEST.MF) we may see something like this

MManifest-Version: 1.0
Build-Timestamp: 2018-04-11T13:31:42.880+0200
Built-By: aalmiray
Build-Revision: dd621a6912989ea44e86d35d393ec5c0d49b48e4
Build-OS: Mac OS X x86_64 10.12.5
Build-Jdk: 1.8.0_162 (Oracle Corporation 25.162-b12)
Created-By: Gradle 4.6

As you can see we get the expected result. An advantage of using Gradle is that attribute values may be computed on the spot, as you have access to a full programming language (either Groovy or Kotlin), useful when you may need to trim, modify, and/or format certain values. Next is Maven. In this case we must also configure an external plugin in order to grab hold of SCM properties, we'll use the maven-git-commit-id-plugin from Konrad (@ktosopl); we'll also need to update the configuration of the core maven-jar-plugin as explained here.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.acme</groupId>
    <artifactId>sample</artifactId>
    <packaging>jar</packaging>
    <version>0.1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>pl.project13.maven</groupId>
                <artifactId>git-commit-id-plugin</artifactId>
                <version>2.2.4</version>
                <executions>
                    <execution>
                        <id>get-the-git-infos</id>
                        <goals>
                            <goal>revision</goal>
                        </goals>
                        <phase>validate</phase>
                    </execution>
                </executions>
                <configuration>
                    <dateFormat>yyyy-MM-dd'T'HH:mm:ss.SSSZ</dateFormat>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Build-Jdk>${java.version} (${java.vendor} ${java.vm.version})</Build-Jdk>
                            <Build-Timestamp>${git.build.time}</Build-Timestamp>
                            <Build-Revision>${git.commit.id}</Build-Revision>
                            <Build-OS>${os.name} ${os.arch} ${os.version}</Build-OS>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Just like in Gradle, the build file has access to all System properties we need (more information about project properties can be found at this link). When packaging the project we get a manifest that may look like this

Manifest-Version: 1.0
Build-Timestamp: 2018-04-11T13:32:31.854+0200
Built-By: aalmiray
Build-Revision: dd621a6912989ea44e86d35d393ec5c0d49b48e4
Build-OS: Mac OS X x86_64 10.12.5
Build-Jdk: 1.8.0_162 (Oracle Corporation 25.162-b12)
Created-By: Apache Maven 3.5.3

It looks quite similar to the one generated by Gradle with just the timestamp and the creator having different values. The only drawback I see with using Maven in this way is that it's not possible to format values or create new ones on the spot. Values must exist as standard project properties, System properties, or custom project properties, such as the ones exposed by the git-commit-id plugin or any other plugin. Perhaps you may be thinking in using the antrun plugin with a short embedded script to be able to get around this problem, well, if you need to bring a second build tool in order to fix the shortcomings of the first, isn't the problem somewhere else?

In any case, both build tools provide you the means to add essential artifact metadata to the archive's manifest.

Happy Hacking!

Liked it? Take a second to support aalmiray on Patreon!
Become a patron at Patreon!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ˆ Back To Top