Skip to main content

Command Palette

Search for a command to run...

Debian packaging Maven projects with JNI libraries

Published
6 min read
Debian packaging Maven projects with JNI libraries

Background

I recently had an opportunity to create a Debian package, from scratch, for a Maven-based Java project. The upstream project, as it is called in Debian parlance, is a Java security provider. It offers a Java API to the FIPS module of the system-installed OpenSSL library. Named openssl-fips-java, it is intended to be used with the 140-3 FIPS certified OpenSSL on Ubuntu Pro.

The openssl-fips-java project comprises Java classes and a native shared library that acts as a bridge between the Java code and the underlying OpenSSL FIPS module. The shared library is a part of the same Maven project and is loaded as a resource. Though Maven produces a single JAR as the binary build artifact, it is not anarchitecture-independent JAR - because it also bundles a shared library which is an arch-specific binary artifact.

Unlike a pure Java project, this provider needs to be built on every architecture that we intend to support. But when we consider the Debian packaging, it is not desirable to have an arch-dependent binary package containing a JAR file. Ideally, we must split this into two binary packages - an arch-independent one with the Java classes, and a second arch-dependent package with the native shared library, the latter being set up as a dependency of the former.

Maven Debian helper

The helper packages for Maven - maven-repo-helper and maven-debian-helper - prepare for and execute all the boilerplate related to building, testing and installing Maven-based projects. By policy, Debian package builds cannot access the internet, meaning remote Maven repositories are also not accessible. The maven-repo-helper sets up a local repository of dependencies (as declared in debian/control).

💡
The maven-repo-helper package provides tools for installing and maintaining POMs and jars in /usr/share/maven-repo, and will have no dependency on Maven so it can be used for any library. It provides the foundations for Maven Debian helper.

POM files are patched to point to dependency versions in the archive, available in the local /usr/share/maven-repo. The project is built, tested and the package is installed into the local maven-repo.

The mh_patchpoms tool is used to modify the POM files. We need to point to the POMs of the project through the <package-name>.poms file. The file format also lets us pass some options for POM patching.

# libopenssl-fips-java.poms
pom.xml --no-parent
💡
The --no-parent option removes reference to the parent POM.

If maven-repo-helper and maven-debian-helper are set up as dependencies in debian/control, the debian/rules file can be as concise as:

#!/usr/bin/make -f

export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-$(DEB_HOST_ARCH)

%:
        dh $@ --buildsystem=maven

Building for Ubuntu 22.04

Unlike most new packages that are first developed for the latest development release, openssl-fips-java is specifically being packaged for Ubuntu 22.04. This means POMs need to be patched to use dependency versions available on Ubuntu 22.04. For example, though the upstream project uses maven-compiler-plugin version 3.14.1, the version available on Ubuntu 22.04 is 3.8.1. Such information needs to be specifically supplied to the maven-repo-helper through the maven.rules file:

# debian/maven.rules
org.apache.maven.plugins maven-compiler-plugin * s/.*/3.8.1/ * *
org.apache.maven.plugins maven-jar-plugin * s/.*/3.1.2/ * *
org.codehaus.mojo exec-maven-plugin * s/.*/1.6.0/ * *
org.apache.maven.plugins maven-resources-plugin * s/.*/3.1.0/ * *

This file is one of the inputs to the mh_patchpoms command that modifies the POM file early in the Maven build process. It is a part of the maven-repo-helper package.

💡
The format of the maven.rules file is described in the maven-repo-helper documentation.

Disabling tests and test coverage reporting

Since we build on Launchpad, we do not want to report the code coverage done by the unit tests. The upstream project uses the jacoco-maven-plugin for this purpose; we want to ignore it. The maven.ignoreRules file comes in handy here. Matching dependencies are removed from the POM during the patching step.

# debian/maven.ignoreRules
org.jacoco jacoco-maven-plugin * * * *
org.apache.maven.plugins maven-surefire-plugin * * * *

The other plugin here is the maven-surefire-plugin which is used to run the Maven tests. Because the unit tests need the OpenSSL FIPS module installed and we can’t always build on an Ubuntu Pro instance with FIPS enabled, it makes sense to disable tests.

💡
The format of maven.ignoreRules is documented in the maven-repo-helper documentation.

The upstream project also runs native tests while building the native shared library. These tests also need to be disabled. The skipGenerateTestResources property lets us skip these:

                   <execution>
                        <id>generate-test-resources</id>
                        <phase>generate-test-resources</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>make</executable>
                            <arguments>
                                <argument>test-solib</argument>
                            </arguments>
                            <skip>
                                ${skipGenerateTestResources}
                            </skip>
                        </configuration>
                    </execution>

Maven properties can be configured using the maven.properties file.

# maven.properties
skipGenerateTestResources=true

Separation of Java and native binaries

As mentioned before, the shared JNI library needs to be separated into an arch-dependent package, while the Java classes may be packed into an arch-independent package as a JAR file.

$ grep "^Package:" debian/control
Package: libopenssl-fips-java
Package: libopenssl-fips-java-jni

The shared library needs to be excluded from the JAR file through a patch:

--- a/pom.xml
+++ b/pom.xml
@@ -66,7 +66,12 @@
             <!-- Jar Plugin -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-jar-plugin</artifactId>
+               <artifactId>maven-jar-plugin</artifactId>
+               <configuration>
+                   <excludes>
+                        <exclude>**/*.so</exclude>
+                    </excludes>
+                </configuration>
                 <version>3.5.0</version>
             </plugin>
             <plugin>

The loading of the shared library as a resource also needs to be changed to loading it through System.loadLibrary.

-    public static synchronized void load() {
+    public static synchronized void load(boolean installed) {
         if (loaded)
             return;

+       if (installed) {
+            System.loadLibrary("jssl");
+           loaded = true;
+           return;
+       }

To have the JNI library installed through the libopenssl-fips-java-jni package, we also need to define a libopenssl-fips-java-jni.install file with a single entry configuring the installation path to /usr/lib/jni.

build/bin/libjssl.so usr/lib/jni/

Even with this setup, I ran into an interesting issue. In the debian/control, I have this:

$ grep -B1 "^Architecture:" debian/control
Package: libopenssl-fips-java
Architecture: all 
--
Package: libopenssl-fips-java-jni
Architecture: amd64 arm64 s390x ppc64el

This looks clean. The libopenssl-fips-java package is pure-Java and hence Architecture: all. The libopenssl-fips-java-jni package is arch-dependent and hence the explicit set of supported archs. Now, libopenssl-fips-java being arch all, builds only on amd64! So, for non-amd64 builds, the Maven Debian helper searches for libopenssl-fips-java-jni.poms. This appears non-intuitive first, but you eventually get the point. We don’t build Java code on non-amd64 architectures!

A workaround here, and I hope there is a cleaner way out, is to copy libopenssl-fips-java.poms to libopenssl-fips-java-jni.poms. And the build succeeds!

But wait. Now, on non-amd64 architectures libopenssl-fips-java and libopenssl-fips-java-jni both try to install the JAR. This makes libopenssl-fips-java uninstallable!

The final hack here is to not let the JAR get packaged into the libopenssl-fips-java-jni package. And I could only think of overriding dh_auto_install to get this done. This is how the debian/rules looks:

#!/usr/bin/make -f

export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-$(DEB_HOST_ARCH)

%:
        dh $@ --buildsystem=maven

override_dh_auto_install:
ifeq ($(DEB_HOST_ARCH),amd64)
        dh_auto_install
endif

Conclusion

A non-native Debian package implemented as described above, is uploaded here: https://github.com/pushkarnk/openssl-fips-java-debian for reference.

It is indeed fascinating to learn how the Maven Debian helper tools help fit Maven packages into the Debian packaging framework. While I am not really happy with the hackery done here, I also hope to discover cleaner ways to address these issues. Or maybe the structure of the project itself isn’t good enough, or maybe Maven is the wrong build system for a project of this kind. I hope to find answers to these questions in the near future.