/JPackageScriptFX-

JPackageScriptFX

Primary LanguageShellApache License 2.0Apache-2.0

JPackageScriptFX

This project demonstrates how projects can use scripts to build self-contained, platform-specific executables and installers of their JavaFX applications via the jdeps, jlink, and jpackage tools. Two scripts are included for running builds on Mac/Linux and Windows. The jpackage tool is bundled with the JDK since version 14.

Important: the scripts do not try to create a fully modularized solution but instead try to enable existing projects / applications, which often use non-modularized 3rd party dependencies, to be packaged again after the previous packaging tool stopped working since Java 11.

Prerequisites

Environment

Both platform-specific build scripts need to know where they can find the java installation with the jpackage tool. Therefore you have to set the environment variable JAVA_HOME. How you set it depends on your operating system. On Mac/Linux you can set it inside the .bash_profiles file in your user home directory. On Windows you would set it in the "environment variables" dialog. In your IDE you can normally also set it as part of a Maven run configuration. If you are the only one working on the project then you can even add it to the pom.xml file of the main module.

Project Structure

The project in this repository uses a multi-module Maven setup with a parent module containing three child modules. One of these child modules is the "main" module as it contains the main class. This module also contains the build scripts and its target directory will contain the results of the build. The JavaFX application consists of a single window displaying three labels. The first one shows the currently configured locale and the other two labels get imported from module 1 and module 2 respectively.

alt text

Launcher Class

Upon closer inspection you will notice that the scripts are not creating packages and executables for App (which extends the standard JavaFX Application class) but for AppLauncher. When an Application class gets launched then JavaFX will check whether the JavaFX modules are present on the module path. But since we are placing them on the classpath the application can not launch. As a work-around we are starting a standard Java class with a main method in it. This prevents the module path check and the application will launch just fine without us having to provide any of the module system specific options.

Icons

Executables require an application icon. Default icons are part of the project. Feel free to use it for your development efforts but make sure to replace it with your own before shipping your product. The platform-specific icons can be found inside jpackagefx-main/src/main/logo.

Building the Project

Once your environment is set up you can simply call the mvn clean install on the root / parent module. It will do a standard build of the application and in addition it will analyze all the dependencies and copy the resulting set of JAR files into the folder target/libs. This work is done via the Maven dependency plugin. Once the standard build is completed Maven will invoke the shell script (on Mac/Linux) or the batch script (on Windows). The Maven main-ui/pom.xml uses two different profiles, both of them being activated via the OS that they are running on.

The scripts both have the same structure:

  • Environment
  • Dependency Analysis
  • Runtime Image Generation
  • Packaging

Environment

The required environment for the scripts consists of environment variables and a directory structure. The following variables are used:

  • JAVA_HOME - the location of the JDK matching the Java version
  • JAVA_VERSION - defines the runtime environment that the final application is targeting
  • PROJECT_VERSION - the version of the project as defined in the pom.xml file (e.g. 1.0-SNAPSHOT)
  • APP_VERSION - the version for the executable (careful: do not re-use project version. The supported Windows version number format has to be major.minor.build, e.g. 1.0.2412. Mac seems to be more flexible.)
  • MAIN_JAR - the name of the jar file that contains the main class

The directory structure required by the build:

  • target/java-runtime - contains the runtime environment generated by jlink as part of the build
  • target/installer - contains the executables generated by jpackage as part of the build
  • target/installer/input/libs - contains the jars required by the application to run

Dependency Analysis

The scripts use the jdeps tool to analyze the dependencies of the application to determine which modules need to be included in the final package. These modules are stored in the list detected_modules.

detected_modules=$JAVA_HOME/bin/jdeps
  --multi-release ${JAVA_VERSION}
  --ignore-missing-deps
  --print-module-deps
  --class-path "target/installer/input/libs/*"
    target/classes/com/dlsc/jpackagefx/App.class

However, the tool can not always find all modules via this static analysis. E.g., when they are only needed to provide a specific service implementation, some manual intervention is required. For this you can add modules to the comma-separated list called manual_modules.

If your application is localized you should also always add jdk.localedata to this list. This can be reduced to the actually needed locales via a jlink paramter later, e.g., --include-locales=en,de.

manual_modules=,jdk.crypto.ec,jdk.localedata

Runtime Image Generation

The next tool to call is called jlink. It generates a java runtime environment for our application and places its content inside the folder target/java-runtime. We could have relied on jpackage to perform the linking for us but unfortunately it does not behave very well with automatic modules, yet. So in order to have full control over the image generation we are letting the script do it via jlink.

$JAVA_HOME/bin/jlink
  --no-header-files
  --no-man-pages 
  --compress=2 
  --strip-debug
  --add-modules "${detected_modules}${manual_modules}"
  --include-locales=en,de
  --output target/java-runtime

In our example code whe have added the module jdk.crypto.ec, which would be needed to make https-requests. But this is just an example here because the code does not actually make use of it.

If you want to stay on the safe side or if you experience strange errors, which might be due to some missing service providers, you can add the option --bind-services to the jlink command. This will, however, make the resulting image much bigger because it will include all service implementations and not just the ones that you actually need.

The jlink command has an option --suggest-providers which, without any further parameters, just outputs a list of all service providers on the module path (by default the whole JDK), but that list is far too long and unspecific to be really useful. In a variant of this option you can explicitly specify a service interface and you will get a list of all implementors of this interface but that implies that you have to know all relevant service interfaces which are possibly used deep down in your dependencies or the JDK. So, at the moment this remains a try-and-error game.

Packaging

Basics

Finally we are invoking the jpackage tool in a loop so that it generates all available package types for the platform that the build is running on. Please be aware that jpackage can not build cross-platform installers. The build has to run separately on all platforms that you want to support. When the build is done you will find the installers inside the directory target/installer. On Mac you will find a DMG, PKG, or an APP. On Linux a DEB, RPM or an application directory. On Windows you will find an application directory, an EXE, and an MSI. Please be aware that the EXE is not the application itself but an installer.

for type in "app-image" "dmg" "pkg"
do
  $JPACKAGE_HOME/bin/jpackage
  --package-type $type
  --dest target/installer
  --input target/installer/input/libs
  --name JPackageScriptFX
  --main-class com.dlsc.jpackagefx.AppLauncher
  --main-jar ${MAIN_JAR}
  --java-options -Xmx2048m
  --runtime-image target/java-runtime
  --icon src/main/logo/macosx/duke.icns
  --app-version ${APP_VERSION}
  --vendor "ACME Inc."
  --copyright "Copyright © 2019-21 ACME Inc."
  --mac-package-identifier com.acme.app
  --mac-package-name ACME
done

Customization

Once you have come this far, you can now consider to customize your packaging. Under the hood the jpackage tool uses platform-specific tooling to create the various package types. The customization of the packaging is therefore also very platform-specific and has to be individually for each supported platform and package type. However, there are two common features of jpackage that you can use to make this task easier.

The first one is the option --temp some_temp_dir which asks jpackage to copy all scripts and resources needed to create the selected package type into a directory some_temp_dir. These are the scripts and resources that jpackage would use by default.

The second one is the option --resource-dir some_resource_dir which asks jpackage to first look for resources in the directory some_resource_dir and then use its defaults only for the ones it does not find there.

With these two options you can first generate a set of default resources from which you can pick the ones you want to modify and copy them over to the directory some_resource_dir. In a second run of jpackage you can then apply these changes. The most likely changes you can make this way is to exchange the default icons, used by the various package types, with your own ones.

We do not yet provide an example for these customizations because this is still work in progress and there are still some problems.

Testing

In case building and packaging works but the application does not do what it is supposed to do you can try the following.

You can run the app as a regualar Java application with the collected libs in order to see what is going wrong. Very often applications behave differently when packaged compared to running them from an IDE for example. The reason very often is a wrong handling of resource loading but there are other reasons too.

From the folder jpackagefx-main call

$JAVA_HOME/bin/java -cp "target/installer/input/libs/*" com.dlsc.jpackagefx.AppLauncher

You may have to adapt that to the Windows calling conventions. Ignore the warning

Unsupported JavaFX configuration: classes were loaded from 'unnamed module @6f1eab88'