crush depth

Batch Files Are Not Your Only Option

A while ago I got into a fight with jpackage. Long story short, I concluded that it wasn't possible to use jpackage to produce an application that runs in "module path" mode but that also has one or more automatic modules.

It turns out that it is possible, but it's not obvious from the documentation at all and requires some extra steps. The example project demonstrates this.

Assume I've got an application containing modules com.io7m.demo.m1, com.io7m.demo.m2, and com.io7m.demo.m3. The com.io7m.demo.m3 module is the module that contains the main class (at com.io7m.demo.m3/com.io7m.demo.m3.M3). In this example, assume that com.io7m.demo.m2 is actually an automatic module and therefore would cause jlink to fail if it tried to process it.

I first grab a JDK from Foojay and unpack it:

$ wget -O jdk.tar.gz -c 'https://api.foojay.io/disco/v3.0/ids/9604be3e0c32fe96e73a67a132a64890/redirect'
$ mkdir -p jdk
$ tar -x -v --strip-components=1 -f jdk.tar.gz --directory jdk

Then I grab a JRE from Foojay and unpack it:

$ wget -O jre.tar.gz -c 'https://api.foojay.io/disco/v3.0/ids/3981936b6f6b297afee4f3950c85c559/redirect'
$ mkdir -p jre
$ tar -x -v --strip-components=1 -f jre.tar.gz --directory jre

I could reuse the same JDK from the first step, but the JRE is smaller and thus it makes the application distribution smaller as we won't be using jlink to strip out any unused modules.

I then build the application, and this produces a set of platform-independent modular jar files:

$ mvn clean package

I copy the jars into a jars directory:

$ cp ./m1/target/m1-20231111.jar jars
$ cp ./m2/target/m2-20231111.jar jars
$ cp ./m3/target/m3-20231111.jar jars

Then I call jpackage:

$ jpackage \
  --runtime-image jre \
  -t app-image \
  --module com.io7m.demo.m3 \
  --module-path jars 
  --name jpackagetest

The key argument that makes this work is the --runtime-image option. It effectively means "don't try to produce a reduced jlink runtime".

This produces an application that works correctly:

$ file jpackagetest/bin/jpackagetest
jpackagetest/bin/jpackagetest: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, not stripped

$ ./jpackagetest/bin/jpackagetest 
M1: Module module com.io7m.demo.m1
M2: Module module m2
JRT: java.base
JRT: java.compiler
JRT: java.datatransfer
JRT: java.desktop
...

We can see from the first two lines of output that both com.io7m.demo.m1 and (the badly-named) m2 are on the module path and have not been placed on the class path. This means that any services declared in the module descriptors will actually work properly.

We can take a look at the internal configuration:

$ cat jpackagetest/lib/app/jpackagetest.cfg 
[Application]
app.mainmodule=com.io7m.demo.m3/com.io7m.demo.m3.M3

[JavaOptions]
java-options=-Djpackage.app-version=20231111
java-options=--module-path
java-options=$APPDIR/mods

We can see that the internal configuration uses an (undocumented) $APPDIR variable that expands to the full path to a mods directory inside the application distribution. The mods directory contains the unmodified application jars:

$ ls jpackagetest/lib/app/mods/
m1-20231111.jar  m2-20231111.jar  m3-20231111.jar

$ sha256sum jars/*
f8de3acf245428576dcf2ea47f5eb46cf64bb1a5daf43281e9fc39179cb3154f  jars/m1-20231111.jar
6ad0f7357cf03dcc654a3f9b8fa8ce658826fc996436dc848165f6f92973bb90  jars/m2-20231111.jar
b5c4d7d858dad6f819d224dd056b9b54009896a02b0cd5c357cf463de0d9fdd2  jars/m3-20231111.jar

$ sha256sum jpackagetest/lib/app/mods/*
f8de3acf245428576dcf2ea47f5eb46cf64bb1a5daf43281e9fc39179cb3154f  jpackagetest/lib/app/mods/m1-20231111.jar
6ad0f7357cf03dcc654a3f9b8fa8ce658826fc996436dc848165f6f92973bb90  jpackagetest/lib/app/mods/m2-20231111.jar
b5c4d7d858dad6f819d224dd056b9b54009896a02b0cd5c357cf463de0d9fdd2  jpackagetest/lib/app/mods/m3-20231111.jar

Now to try to get this working on Windows with the elderly wix tools...