Issue
I am using IntelliJ on a Windows machine. I am able to follow various instructions found on the web to create a HelloWorld JavaFX application. I am building and running using Gradle. I am able to run the application within IntelliJ without any issue. I am using the jpackage plugin to package the app into an exe. This is also successful as I'm able to then click on the generated .exe file and the application launches as expected.
However, my application ultimately will use HSQLDB. I am having trouble figuring out how to include this into a modular application.
In build.gradle I have included the dependency:
implementation("org.hsqldb:hsqldb:2.6.1")
I have then added it to my module-info.java file:
module my.openjfx.hellofx {
requires javafx.controls;
requires javafx.fxml;
requires org.hsqldb;
opens my.openjfx.hellofx to javafx.fxml;
exports my.openjfx.hellofx;
}
When doing this I can still run the application successfully within IntelliJ using the gradle 'run' command. However, after I package it up using jpackage, the exe fails with the following error:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.InternalError: Module my.openjfx.hellofx not in boot Layer
at java.base/sun.launcher.LauncherHelper.loadModuleMainClass(Unknown Source)
at java.base/sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
Failed to launch JVM
Child process exited with code 1
I know for certain it is the line requires org.hsqldb;
that is causing the problem because if I comment out that line, I am able to run the packaged program.
Does anyone have experience packaging HSQLDB into a JavaFX modular program packaged using jpackage?
Additional information: I'm using the badass plugin to package the application. Here are all the files: build.gradle:
plugins {
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'org.beryx.jlink' version '2.24.1'
}
group 'my.openjfx'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
ext {
junitVersion = '5.8.2'
}
sourceCompatibility = '17'
targetCompatibility = '17'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
application {
mainModule = 'my.openjfx.hellofx'
mainClass = 'my.openjfx.hellofx.HelloApplication'
}
javafx {
version = '17.0.1'
modules = ['javafx.controls', 'javafx.fxml']
}
dependencies {
// https://mvnrepository.com/artifact/org.hsqldb/hsqldb
implementation("org.hsqldb:hsqldb:2.6.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
}
test {
useJUnitPlatform()
}
jlink {
imageZip = project.file("${buildDir}/distributions/app-${javafx.platform.classifier}.zip")
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
launcher {
name = 'HelloFX'
}
jpackage {
appVersion = '0.0.7'
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut']
imageOptions += ['--win-console']
}
}
}
jlinkZip {
group = 'distribution'
}
module-info.java
module my.openjfx.hellofx {
requires javafx.controls;
requires javafx.fxml;
requires org.hsqldb;
opens my.openjfx.hellofx to javafx.fxml;
exports my.openjfx.hellofx;
}
HelloApplication.java
package my.openjfx.hellofx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
HelloController.java
package my.openjfx.hellofx;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class HelloController {
@FXML
private Label welcomeText;
@FXML
protected void onHelloButtonClick() {
welcomeText.setText("Welcome to JavaFX Application!");
}
}
fxml file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
fx:controller="my.openjfx.hellofx.HelloController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
<Label fx:id="welcomeText"/>
<Button text="Hello!" onAction="#onHelloButtonClick"/>
</VBox>
Picture of file structure within IntelliJ:
Solution
Work-around Solution: Set a Module Path
This can be fixed by providing a module path in the jvmArgs to be used when executing the packaged application:
jvmArgs = ['-p', '.']
For example, the jpackage block in build.gradle becomes:
jpackage {
appVersion = '1.0.0'
installerType = 'pkg'
jvmArgs = ['-p', '.']
}
I don't know why the work-around works
I actually do not why this works. What it does is use the current directory for the module path the JVM uses when the packaged application is run. The modules need aren't actually in the current directory, they have already been packaged into the application image by the packaging process. So setting a module path should not be necessary (and isn't necessary if you don't require hsqldb.org
). However, if you require hsqldb.org
, just the act of setting the module path to anything rather than leaving it empty will allow the app to function correctly. Setting the path manually must trigger some different startup logic in the JVM, but I don't know what it is.
I speculate that it could be because the hsql module is binding as an implementation for the standard JDK java.sql
module as a service and (for whatever reason it doesn't work unless a module path is set, no matter what it is). Though that is just speculation, I don't really know what is going on.
Alternate Solution: Non-Modular Application
An alternate solution is to make your application non-modular and use the badass runtime plugin rather than the badass jlink plugin, as noted in the comments on the question:
I switched over to the badass runtime instead of jlink and it does what I want, so all of this may be a moot point but I'm still curious.
However, this, should not be necessary. It should actually work in the original form of building a modular application and packaging it using the badass jlink plugin, but unfortunately, it does not.
The runtime plugin works because then your application (and hsql) is run in a non-modular way, which, for whatever reason, does not experience this issue. The issue is caused by packaging and requiring the hsql module in the application created by jpackage (though I don't know why).
Background Investigation
The rest of this answer is just findings from my investigation of the issue, not a solution, so ignore it unless interested.
I was able to replicate exactly the setup you describe. With the requires clause for hsqldb in the module info, executing the packaged application fails with the same error message, comment out the requires clause and the packaged app runs fine.
The actual image created by jlink and included in the package is fine and will run from the command line. But the package adds an additional executable that the launcher uses and it is the execution of that which fails.
This issue is unrelated to gradle, javafx, the javafx plugin, the badass jlink plugin, or jlink itself. As you can remove all of those, and just try to package a simple modular application that only requires the hsql.db
module (version 2.6.1) and outputs hello, world (doesn't even try any SQL operations), and the packaged application will fail with the same error. The application will work fine when you process it through the jlink tool but will fail when processing it through the jpackage tool. The issue is platform-independent as I tested on OS X and the original poster was using Windows. It is the combination of jpackage 18.0.1.1 + hsql.db module 2.6.1 which causes an error.
As an example, if the build output for your project is:
<projectdir>/build/
Then the badass jlink plugin will create the following directories for the output of jpackage for a OS X app image named hsqlfx:
<projectdir>/build/jpackage/hsqlfx.app
And with a launcher script (also named hsqlfx), this is what the launcher will try to run:
<projectdir>/build/jpackage/hsqlfx.app/Contents/MacOS/hsqlfx
This will fail with:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.InternalError: Module com.example.hsqlfx not in boot Layer
at java.base/sun.launcher.LauncherHelper.loadModuleMainClass(Unknown Source)
at java.base/sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
However, the installed app includes the generated jlink image and if you run from the execution point (generated launcher script file) for that image, it all works fine:
<projectdir>/build/jpackage/hsqlfx.app/Contents/runtime/Contents/Home/bin/hsqlfx
You get the expected output:
hello, world
However, the installed application (e.g. when you click the icon) tries to run the failing command and not the working script. The issue is that the failing command should work.
Reproducible example
com/example/hsqlfx/HelloApplication.java
package com.example.hsqlfx;
public class HelloApplication {
public static void main(String[] args) {
System.out.println("hello, world");
}
}
module-info.java
module com.example.hsqlfx {
requires org.hsqldb;
exports com.example.hsqlfx;
}
build.gradle
plugins {
id 'java'
id 'application'
id 'org.beryx.jlink' version '2.25.0'
}
group 'com.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
ext {
junitVersion = '5.8.2'
}
sourceCompatibility = '18'
targetCompatibility = '18'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
application {
mainModule = 'com.example.hsqlfx'
mainClass = 'com.example.hsqlfx.HelloApplication'
}
dependencies {
implementation "org.hsqldb:hsqldb:2.6.1"
}
test {
useJUnitPlatform()
}
jlink {
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
launcher {
name = 'hsqlfx'
}
jpackage {
appVersion = '1.0.0'
installerType = 'pkg'
// ### uncomment the next line to allow the packaged app to run correctly ###
// jvmArgs = ['-p', '.']
}
}
Alternate jpackage command to using the badass jlink plugin:
/Users/<username>/Library/Java/JavaVirtualMachines/openjdk-18.0.1.1/Contents/Home/bin/jpackage --dest /Users/<username>/dev/hsqlfx/build/jpackage --type pkg --verbose --name hsqlfx -p /Users/<username>/Library/Java/JavaVirtualMachines/openjdk-18.0.1.1/Contents/Home/jmods/:/Users/<username>/dev/hsqlfx/build/jlinkbase/jlinkjars --module com.example.hsqlfx/com.example.hsqlfx.HelloApplication --dest /Users/<username>/dev/hsqlfx/build/apps/hsqlfx.app --app-version 1.0.0 --jlink-options "--strip-debug --compress 2 --no-header-files --no-man-pages" --java-options "-p ."
Where the jlinkJars directory contains the hsqldb jar and the application jar.
It is the addition of the switch:
--java-options "-p ."
At the end of the command which allows the execution of the packaged application to work.
Answered By - jewelsea
Answer Checked By - Candace Johnson (JavaFixing Volunteer)