Publishing Scala applications with jpackage

Publication date: 2024-06-05

Recently I've been working on Checktimer, my time tracking application written using Scala and ScalaFX. I wanted to make it easier to install and run, so I decided to package it to be runnable natively, without a requirement to install any external dependencies such as Java virtual machine.

There are several options how to produce native artifacts from a Scala applications, and some of them are outdated or quite complex. I was looking for a simplest solution, and found disturbing lack of information, even though the easiest solution is readily available in the JDK.

After evaluating the available options (such as Graal, jlink, sbt-native-packager, etc.), I decided to use jpackage tool, which is a part of any modern JDK. It is the simplest way to package a Java application into a native executable, and it is quite easy to use — if you know how to do it.

First things first, make sure your JDK includes jpackage:

$ jpackage --version

If it prints a version (same as your JDK), then you are okay and ready for the next step.

So, jpackage is very well suited for use in modular projects or in projects where you have an "uberjar" of "fat jar" — a package with all of your classes in one .jar file that you may set up as --main-jar and that's it.

Sadly, there are problems with either of approaches if you prefer to use Scala for your solution.

  1. Scala is still unable to produce Java modules as far as I understand.
  2. I tried packing the whole ScalaFX into an uberjar and it didn't work. There are a lot of resource conflicts, and if you just resolve taking the first resource from each conflicting group, the result won't start.

So, something else is needed.

I was hoping that jpackage would be able to just pack all my .jar files, and then run them in the same manner as

$ java -cp <my-classpath-jars> my.main.class

but it is unable to do so. jpackage requires you to use either --main-jar (and thus makes it analogous to java -jar) or --main-class (and requires a modular project).

After some digging, I found a solution. It is possible to make any project to work with java -jar, you just need the right manifest in the .jar's META-INF. In particular, if you want your application to use other JAR files, you need to use Class-Path attribute in the manifest.

So, here is my solution for Scala.

  1. Make sure you have sbt-native-packager plugin enabled in your project.

    project/plugins.sbt:

    addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.0")
    
  2. Activate the JavaAppPackaging and LauncherJarPlugin plugins in your build.sbt:

    enablePlugins(JavaAppPackaging)
    enablePlugins(LauncherJarPlugin)
    

    The former allows to pack all the dependency JARs into the output directory, and the latter will fill the Class-Path attribute in the manifest (see the `MA).

  3. Build the project:

    $ sbt universal:stage
    
  4. Now, run jpackage:

    $ jpackage --type app-image --input target/universal/stage/lib --main-jar your.jar-<version>-launcher.jar
    

This will produce a package containing a working JVM for the current platform, all your JAR files, and an executable launcher binary to wire everything together. In my case, a simple ScalaFX app weighs for about 100 MiB in compressed form: quite a lot, but not that much comparing with other rich UI technologies — and I hope it's possible to reduce the size by excluding some unnecessary parts of the JVM (didn't investigate that yet).

Enjoy your self-contained application!