Case study: AtlantaFX

AtlantaFX is a new CSS theme for JavaFX that implements a modern design language using the GitHub Primer color system. JavaFX is an advanced and mature desktop UI toolkit that can be used from any language with a JVM implementation, not just Java. You can of course use JVM-first languages like Kotlin, Scala and for Lispers Clojure (via the cljfx functional/reactive UI library). But in addition, thanks to GraalVM, you can also use languages like Python 3, JavaScript, Ruby, R, and even binary languages like WebAssembly or LLVM bitcode (i.e. any language that can compile to these targets can also access JVM libraries).

You can now download the AtlantaFX sampler app packaged with Conveyor and it’ll keep itself up to date as the project evolves from the current early development stage to full production maturity.

In this article we’ll show how the packages were made and along the way, see how to package JavaFX apps that don’t use Gradle. The packaging was contributed upstream and is now a part of the AtlantaFX project itself.

AtlantaFX in action

AtlantaFX is primarily a set of CSS stylesheets built using SASS, but it also provides a few commonly used controls that core JavaFX lacks such as a toggle switch.

Finding the app JARs

The first step to package the sampler app is to figure out how to get a set of JARs for it, without including platform specific JARs like JavaFX itself. The reason those need to be excluded is that the build system of a typical project will only download OS-specific libraries for the platform you’re compiling on. To build packages for every OS we will tell Conveyor where to find the JMOD files for each OS and let them be integrated into the bundled JVM. This integration process (“jlinking”) has a number of advantages, primarily related to performance and download size.

AtlantaFX uses a Maven build system with some customizations. By reading the README.md file we can figure out the following set of commands to build the project and obtain a directory with the platform neutral JARs:

 mvn install -pl styles
 mvn install -pl base
 mvn prepare-package jar:jar -pl sampler

These commands will use SASS to build a unified stylesheet, then compile the base library including those stylesheets, then finally prepare a set of JARs for the sampler app and all its platform neutral dependencies. The results can be found in the sampler/target/dependencies directory which, despite the name, will also include the JAR for the sampler app itself.

This is the general patten for packaging any app that uses the JVM regardless of language or build system - start by splitting the platform neutral JARs from any platform specific JARs (or JMODs, for JavaFX).

With this done we can now begin to write a conveyor.conf file.

Import useful configs

// Use a vanilla Java 17 build, latest as of packaging.
include required("/stdlib/jdk/17/openjdk.conf")

// Import JavaFX JMODs.
include required("/stdlib/jvm/javafx/from-jmods.conf")
javafx.version = 18.0.2

// Small tweaks e.g. enabling proxy detection (https://conveyor.hydraulic.dev/2/stdlib/jvm-clients/)
include required("/stdlib/jvm/enhancements/client/v1.conf")

This prologue imports various snippets of configuration from the Conveyor standard library. The first adds platform specific inputs for the latest OpenJDK 17 JDK (latest as of the release of your Conveyor version, that is). The second adds in platform specific URLs of the JavaFX JMOD downloads, which is what we need to make a customized, minimal bundled JVM. The third sets some system properties and other small tweaks that make JVM apps work better on the desktop. What these imports do is documented more thoroughly in the guidebook.

Set app metadata

Now we add:

app {
  display-name = AtlantaFX Sampler
  fsname = atlantafx-sampler
  rdns-name = io.github.mkpaz.atlantafx
  
  // Where the apps will look for updates.
  site.base-url = downloads.hydraulic.dev/atlantafx/sampler

  // Not allowed to have versions ending in -SNAPSHOT
  version = 0.1

  // Open source projects use Conveyor for free.
  vcs-url = github.com/mkpaz/atlantafx
}

We start by supplying how the app should appear to end users (the “display name”) and how it should appear on disk (the “fsname”). In fact, Conveyor can derive some keys from other keys and in this case the display name wouldn’t need to be specified at all if it was written “Atlantafx Sampler”, but the capitalization is an important part of the brand name so we override the default. We also specify a globally unique reverse DNS name. This isn’t a JVM specific thing: macOS and Linux like to have such names to disambiguate packages from each other.

Then we specify where the app will be downloaded from. This is needed so the online update systems for each OS know where to check for online updates. If you don’t specify an rdns-name for a project then one will be derived from the URL (in this case, it’d be dev.hydraulic.downloads.atlantafx-sampler), which would work fine but isn’t pedantically correct.

Next we specify the version. Maven projects will typically have a -SNAPSHOT extension which isn’t compatible with update systems, so we override it here. This could also be taken from a git tag, and future versions of Conveyor will be able to read pom.xml files directly. But for now, we must be explicit.

Finally we define where the project’s source code can be located. Conveyor is free for everyone during the introductory period, but will eventually require a paid license per project for proprietary projects. By specifying the GitHub URL we tell Conveyor that this project qualifies for a free license. All we have to do is have a link to Conveyor on the download page, which the default generated download.html file does already.

Import the needed files

We can continue adding keys inside the app {} section to tell Conveyor where to find the files it needs. Some of the include statements we started with contain config like this to add extra files.

// Import the JARs.
inputs += sampler/target/dependencies

// Linux/macOS want rounded icons, Windows wants square.
icons = "sampler/icons/icon-rounded-*.png"
windows.icons = "sampler/icons/icon-square-*.png"

jvm {
  gui.main-class = atlantafx.sampler.Launcher
  modules = [ java.logging, jdk.localedata, java.desktop, javafx.controls, javafx.swing, javafx.web ]
}

The app.inputs key is used for generic cross platform inputs, like JAR files. Input files are always placed together in the root of the app’s install directory (or for JVM apps, a directory called app), so we don’t need to worry about the build system path appearing in our packages. Adding a directory as an input is equivalent to importing every file found inside it.

Converting icons into each platform specific format is a tedious and time consuming step previously required for distributing desktop apps. Conveyor’s mission is to eliminate tedium so it does this task for you. All you need to do is supply PNG files in various power-of-two sizes. Like quite a few settings, the icons used can be overridden per-platform and we use this here to ensure the Windows icons are square rather than using rounded corners.

Finally we specify some JVM specific configuration. We specify the main class and then enumerate the modules the app needs - this list is taken from the AtlantaFX Maven POM.

Build the download site

Conveyor doesn’t just generate standalone packages for you as some other tools do, it generates full blown signed and (for macOS) notarized download and update repositories. All you have to do is upload them to the site URL specified in the config.

Before we do that though we can try out the app we’ll be distributing. This step assumes you already downloaded and set up Conveyor:

❯ conveyor -Kapp.machines=mac.amd64 make app

You can replace the string mac.amd64 with windows, mac.aarch64 (for ARM/Apple Silicon Macs), or linux.amd64 for Intel Linux. In the output directory you should now find a self-contained app directory that’s ready to run.

For testing you’ll probably want an actual package you can try:

❯ conveyor -Kapp.machines=mac.amd64 make package

Finally, doing this once per machine is tedious. Normally you will just use:

❯ conveyor make site

The output directory will now contain a set of packages, update repository metadata files and a generated download.html page ready for upload to your web server (or GitHub Releases). The download page will give the user a big green button by detecting the operating system and CPU.

If you don’t have code signing keys then the app will be self signed and the download site will provide command line scripts for installing the app in ways that bypass the certificate requirements. The AtlantaFX Sampler downloads Hydraulic supplies are however fully signed so the download and install experience can be as smooth as possible:

  • On Windows, all users have to do is open the tiny stub EXE which will then download any parts of the app they don’t already have, install the app and launch it. It takes two clicks and they’re done (one to click the download button, one to run the downloaded installer). Because apps packaged using MSIX are immutable, any other apps from different vendors will be used by Windows to avoid redundant downloads. Apps will update silently in the background every 8 hours or so and there are also plain non-updating ZIPs available.
  • On macOS, the user will get a zip containing a standard signed and notarized .app bundle, with the popular Sparkle Framework already integrated.
  • On Debian/Ubuntu the user will get an apt repository along with shell commands they can copy/paste into their terminal to install it.
  • On other Linux distributions, the user gets a tarball and will have to update it themselves.

Have fun!

.