Package a non-modular JavaFX application

12,151

These are a few options for packaging/distributing a (non-modular) JavaFX 11 end application. Most of them are explained in the official OpenJFX docs.

I'll use this sample as reference. I'll also use Gradle. Similar can be done with Maven (different plugins), and even without build tools (but this is not recommended...). Build tools are a must nowadays.

Fat Jar

This is still a valid option, but not the preferred one, as it breaks the modular design and bundles everything all together, and it is not cross-platform unless you take care of that.

For the given sample, you have a build.gradle file like this:

plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.5'
}

repositories {
    mavenCentral()
}

dependencies {
}

javafx {
    modules = [ 'javafx.controls' ]
}

mainClassName = 'hellofx.HelloFX'

jar {
    manifest {
        attributes 'Main-Class': 'hellofx.Launcher'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Note the use of a Launcher class. As mentioned by the OP or explained here, a launcher class that not extends from Application is required now to create a fat jar.

Running ./gradlew jar produces a fat jar (~8 MB) that includes the JavaFX classes, and the native libraries of your current platform.

You can run java -jar build/libs/hellofx.jar as usual, but only in the same platform.

As explained in the OpenJFX docs or here, you can still create a cross-platform jar.

In this case, we can include the three graphics jars, as those are the ones that have platform-dependent code and libraries. Base, controls and fxml modules are the platform-independent.

dependencies {
    compile "org.openjfx:javafx-graphics:11.0.1:win"
    compile "org.openjfx:javafx-graphics:11.0.1:linux"
    compile "org.openjfx:javafx-graphics:11.0.1:mac"
}

./gradlew jar will produce a fat jar (19 MB) now that can be distributed to these three platforms.

(Note Media and Web have also platform-dependent code/native libraries).

So this works as it used to on Java 8. But as I said before, it breaks how modules work, and it doesn't align on how libraries and apps are distributed nowadays.

And don't forget that the users of these jars will still have to install a JRE.

jlink

So what about distributing a custom image with your project, that already includes a native JRE and a launcher?

You will say that if you have a non-modular project, that won't work. True. But let's examine two options here, before talking about jpackage.

runtime-plugin

The badass-runtime-plugin is a Gradle plugin that creates runtime images from non-modular projects.

With this build.gradle:

plugins {
    id 'org.openjfx.javafxplugin' version '0.0.5'
    id 'org.beryx.runtime' version '1.0.0'
    id "com.github.johnrengelman.shadow" version "4.0.3"
}

repositories {
    mavenCentral()
}

dependencies {
}

javafx {
    modules = [ 'javafx.controls' ]
}

mainClassName = 'hellofx.Launcher'

runtime {
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
}

When you run ./gradlew runtime it will create a runtime, with its launcher, so you can run:

cd build/image/hellofx/bin
./hellofx

Note it relies on the shadow plugin, and it requires a Launcher class as well.

If you run ./gradlew runtimeZip, you can get a zip for this custom image of about 32.5 MB.

Again, you can distribute this zip to any user with the same platform, but now there is no need of an installed JRE.

See targetPlatform for building images for other platforms.

Going Modular

We keep thinking that we have non-modular project, and that can't be changed... but what if we do change it?

Going modular is not that big of a change: you add a module-info.java descriptor, and you include the required modules on it, even if those are non-modular jars (based on automatic names).

Based on the same sample, I'll add a descriptor:

module hellofx {
    requires javafx.controls;

    exports hellofx;
}

And now I can use jlink on command line, or use a plugin for it. The badass-gradle-plugin is a gradle plugin, from the same author as the one mentioned before, that allows creating a custom runtime.

With this build file:

plugins {
    id 'org.openjfx.javafxplugin' version '0.0.5'
    id 'org.beryx.jlink' version '2.3.0'
}

repositories {
    mavenCentral()
}

dependencies {
}

javafx {
    modules = [ 'javafx.controls' ]
}

mainClassName = 'hellofx/hellofx.HelloFX'

you can run now:

./gradlew jlink
cd build/image/bin/hellofx
./hellofx

or ./gradlew jlinkZip for a zipped version (31 MB) that can be distributed and run in machines, of the same platform, even if there is no JRE installed.

As you can see, no need for shadow plugin or Launcher class. You can also target other platforms, or include non-modular dependencies, like in this question.

jpackage

Finally, there is a new tool to create executable installers that you can use to distribute your application.

So far there is no GA version yet (probably we'll have to wait for Java 13), but there are two options to use it now with Java 11 or 12:

With Java/JavaFX 11 there is a back port from the initial work on the JPackager on Java 12 that you can find here. There is a nice article about using it here, and a gradle project to use it here.

With Java/JavaFX 12 there is already a build 0 version of the jpackage tool that will be available with Java 13.

This is a very preliminary use of the tool:

plugins {
    id 'org.openjfx.javafxplugin' version '0.0.5'
}

repositories {
    mavenCentral()
}

dependencies {
}

javafx {
    version = "12-ea+5"
    modules = [ 'javafx.controls' ]
}

mainClassName = 'hellofx/hellofx.HelloFX'

def java_home = '/Users/<user>/Downloads/jdk-12.jdk/Contents/Home'
def installer = 'build/installer'
def appName = 'HelloFXApp'

task copyDependencies(type: Copy) {
    dependsOn 'build'
    from configurations.runtime
    into "${buildDir}/libs"
}

task jpackage(type: Exec) {
    dependsOn 'clean'
    dependsOn 'copyDependencies'

    commandLine "${java_home}/bin/jpackage", 'create-installer', "dmg",
            '--output', "${installer}", "--name", "${appName}",
            '--verbose', '--echo-mode', '--module-path', 'build/libs',
            '--add-modules', "${moduleName}", '--input', 'builds/libraries',
            '--class', "${mainClassName}", '--module', "${mainClassName}"
}

Now running ./gradlew jpackage generates a dmg (65 MB), that I can distribute to be installed:

installer

Conclusion

While you can stick to classic fat jars, when moving to Java 11 and beyond, everything is supposed to be modular. The new (soon to be) available tools and plugins, including the IDE support, are helping during this transition.

I know I've presented here the simplest use case, and that when trying more complex real cases, there will be several issues... But we should better work on solving those issues rather than keep using outdated solutions.

Share:
12,151
taranion
Author by

taranion

Updated on June 06, 2022

Comments

  • taranion
    taranion almost 2 years

    I have a Java 8 application, that uses JavaFX and where the main class extends javafx.application.Application . Currently, I deliver it as a fat jar and it runs fine on Oracle Java 8.

    Now I want it to be able to run on OpenJDK 11. To add JavaFX, I already added the artifacts from org.openjfx to the classpath and am including them in the fat jar. If I start my jar from the command line, I get

    Error: JavaFX runtime components are missing, and are required to run this
    application
    

    I found two possible ways around this problem:

    1. The dirty one: Write a special launcher that does not extend Application and circumvent the module check. See http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-June/021977.html
    2. The clean one: Add --module-path and --add-modules to my command line. The problem with this solution is, that I want my end users to be able to just launch the application by double clicking it.

    While I could go with 1. as a workaround, I wonder what is currently (OpenJDK 11) the intended way to build/deliver executable fat jars of non-modular JavaFX applications. Can anyone help?

  • taranion
    taranion over 5 years
    Thanks José, that was a very extensive answer. I will investigate the options. Some of them look similiar to my attempts with Maven and the moditect plugin, but I was struggling with automatic modules and jlink, so perhaps your suggestions are easier to work with.
  • elect
    elect almost 5 years
    Hallo José, one question: does it make sense or is it worth for end applications (that is they are not supposed to be used/imported as library) to be moved to modular structure?
  • José Pereda
    José Pereda almost 5 years
    @elect That is a good question. At this point it is up to you. One of the main benefits is the use of jlink for distribution, but even the future jpackage tool deals with non-modular projects.
  • elect
    elect almost 5 years
    Do you happen to have a good link showing the jlink benefits? this medium.com/codefx-weekly/is-jlink-the-future-1d8cb45f6306 for example?
  • José Pereda
    José Pereda almost 5 years
    @elect That's a good post, indeed. He mentions that as an alternative you can always build a jar with your app, and distribute it, without a runtime. But now there is no JRE anymore (the user will need to install a JDK). jlink precisely adds that, the JRE specific for your app. Two use cases: Scene Builder is not modular, but it is distributed with jpackage, ScenicView is modular and distributed with jlink. Anyway this is not a conversation to have through comments.
  • KenobiBastila
    KenobiBastila over 4 years
    @JoséPereda would you take a look at this? please. I even set a bounty up but I couldnt get the help I need. Theres also an issue opened at: github.com/beryx/badass-runtime-plugin/issues/40 stackoverflow.com/questions/58344590/…
  • José Pereda
    José Pereda over 4 years
    @KenobiShan Have you checked this stackoverflow.com/questions/54892406/… ?
  • KenobiBastila
    KenobiBastila over 4 years
    Oh, I didnt find that. Going to look through it and report back ASAP. Im just confused about mixing that with jPackage call.
  • KenobiBastila
    KenobiBastila over 4 years
    @JoséPereda I can run the proguard tasks, but when I Run jPackage, the code doesnt get obfuscated! Ive updated the question! What should I do?
  • KenobiBastila
    KenobiBastila over 4 years
  • Jawad El Fou
    Jawad El Fou over 2 years
    What about maven?