Build and deploy JavaFX Applicationn with jlink - java

I have a script build.gradle, which created the IDEA development environment when creating a JavaFX project with Gradle support:
plugins {
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'org.beryx.jlink' version '2.24.4'
id 'org.javamodularity.moduleplugin' version '1.8.10' apply false
}
group 'com.prototype'
version '1.0'
repositories {
mavenCentral()
}
ext {
junitVersion = '5.8.2'
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
sourceCompatibility = '17'
targetCompatibility = '17'
}
application {
mainModule = 'com.prototype.simulationcrystalgrowth'
mainClass = 'com.prototype.simulationcrystalgrowth.SimulationApplication'
}
javafx {
version = '17.0.1'
modules = ['javafx.controls', 'javafx.fxml', 'javafx.web']
}
dependencies {
implementation('org.controlsfx:controlsfx:11.1.1')
implementation('com.dlsc.formsfx:formsfx-core:11.4.2') {
exclude(group: 'org.openjfx')
}
implementation('net.synedra:validatorfx:0.2.1') {
exclude(group: 'org.openjfx')
}
implementation('org.kordamp.ikonli:ikonli-javafx:12.2.0')
implementation('org.kordamp.bootstrapfx:bootstrapfx-core:0.4.0')
implementation('eu.hansolo:tilesfx:17.0.11') {
exclude(group: 'org.openjfx')
}
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 = 'app'
}
}
jlinkZip {
group = 'distribution'
}
After the "build" task is completed, the "distributions" folder appears in the build folder. It contains a zip archive with the following contents:
The bin folder contains two scripts, sh and bat.
The lib folder contains, as I understand it, all the required jar modules.
If JAVA_HOME is installed on Java 17 in my environment, then when executing the bat script, my program starts.
I expected that jlink is a kind of analogue of a more user-friendly assembly and packaging of the application, which will help to create something like an exe application launcher.
I also noticed that there are no tasks related to jlink in build.gradle is not called during the build process using the "build" task.
I tried to run them myself, and I got the same error:
I am confused by the mention of the "distributions/app" path in build.gradle, I expect there should be something else after the build.
What am I doing wrong?
What should I get at the output using jlink ?

The problem is solved.
The exclude of the org.openjfx module was removed from all dependencies.
Useful links:
https://openjfx.io/openjfx-docs/#gradle
https://github.com/openjfx/samples
https://developer.tizen.org/development/articles/openjdk-and-openjfx-installation-guide

Related

Gradle Kotlin DSL: could not find or load main class

I have a Java project with multiple dependencies that I would like to package into a fat jar with Gradle using Kotlin DSL.
When I run ./gradlew jar, the build succeeds, but it cannot find the main class when I try to run it:
$ ./gradlew jar
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
$ java -jar build/libs/myapp-0.0.1-SNAPSHOT.jar
Error: Could not find or load main class mypackage.Hello
Caused by: java.lang.ClassNotFoundException: mypackage.Hello
$ jar xf build/libs/myapp-0.0.1-SNAPSHOT.jar META-INF/MANIFEST.MF
$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: mypackage.Hello
$ jar -tf build/libs/myapp-0.0.1-SNAPSHOT.jar | grep mypackage 1
mypackage/
mypackage/Hello.class
Here is the full build.gradle.kts file:
group = "myapp"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withSourcesJar()
}
plugins {
java
}
repositories {
mavenCentral()
maven {
url = uri("https://mvnrepository.com/artifact/com.microsoft.azure/azure-storage")
}
}
dependencies {
implementation("com.azure:azure-storage-blob:12.12.0")
}
val mainClassName = "mypackage.Hello"
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
manifest.attributes.apply {
put("Class-Path", configurations.runtimeClasspath.get().asPath)
put("Main-Class", mainClassName)
}
from(configurations.compileClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
tasks.test {
useJUnitPlatform()
}
And a link to my repo: https://github.com/darkasphalt/myapp
Check the content of jar. The problem is, that you include all the contents of the the libs. This also also adds files from the java module system like the module-info.class. This may hide your Main class.
Simple Fix
Your main works, if you:
a) Remove the import
implementation("com.azure:azure-storage-blob:12.12.0")
or
b) Remove the classpath config:
from(configurations.compileClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
Than a simple java -jar myapp-0.0.1-SNAPSHOT.jar will print out the:
Hello
Setup with ShadowJar
To solve the problem of packaging a fat jar the right way, you might use the ShadowJar plugin. With this setup, the bundled jar works:
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
group = "myapp"
version = "0.0.1-SNAPSHOT"
plugins {
java
application
id("com.github.johnrengelman.shadow").version("7.0.0")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withSourcesJar()
}
repositories {
mavenCentral()
maven {
url = uri("https://mvnrepository.com/artifact/com.microsoft.azure/azure-storage")
}
}
application() {
mainClass.set("mypackage.Hello")
}
dependencies {
implementation("com.azure:azure-storage-blob:12.12.0")
}
// Configure Shadow to output with normal jar file name:
tasks.named<ShadowJar>("shadowJar").configure {
minimize()
}
tasks {
build {
dependsOn(shadowJar)
}
}
tasks.test {
useJUnitPlatform()
}

Error modularizing a project with gluon Charm: The package com.gluonhq.charm.down.plugins is accessible from more than one module

I'm going through modularizing my own projects. One of my classes uses the following imports:
import com.gluonhq.charm.down.Services;
import com.gluonhq.charm.down.plugins.StorageService; // error here on "com.gluonhq.charm.down.plugins"
import com.gluonhq.charm.glisten.application.MobileApplication;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.control.Dialog;
import com.gluonhq.charm.glisten.control.SettingsPane;
import com.gluonhq.charm.glisten.control.settings.DefaultOption;
import com.gluonhq.charm.glisten.control.settings.Option;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
In my module-info.java I have declared:
requires charm.glisten;
requires charm.down.core;
requires charm.down.plugin.storage;
as per auto-fix suggestions of Eclipse. However, I get the error specified for the above line:
The package com.gluonhq.charm.down.plugins is accessible from more than one module:
charm.down.plugin.device,
charm.down.plugin.display,
charm.down.plugin.in.app.billing,
charm.down.plugin.lifecycle,
charm.down.plugin.push.notifications,
charm.down.plugin.runtime.args,
charm.down.plugin.statusbar,
charm.down.plugin.storage
The charm modules are automatically named since they are not Java modules apparently. This could be related to the issue. Before modularizing my projects, there were no such issues. How do I solve this?
build.gradle:
buildscript {
repositories {
jcenter()
google()
mavenCentral()
maven {
url 'http://nexus.gluonhq.com/nexus/content/repositories/releases'
}
maven {
url 'https://plugins.gradle.org/m2/'
}
}
dependencies {
classpath 'com.gluonhq:client-gradle-plugin:0.1.30'
}
}
plugins {
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.beryx.jlink' version '2.21.2'
id 'com.google.osdetector' version '1.6.2'
id 'eclipse'
id 'org.kordamp.gradle.jdeps' version '0.11.0'
}
apply plugin: 'com.gluonhq.client-gradle-plugin'
repositories {
jcenter()
maven {
url 'http://nexus.gluonhq.com/nexus/content/repositories/releases'
}
}
sourceCompatibility = 14
targetCompatibility = 14
ext.platform = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
dependencies {
compile "org.openjfx:javafx-base:14:$platform"
compile "org.openjfx:javafx-graphics:14:$platform"
compile "org.openjfx:javafx-controls:14:$platform"
compile "org.openjfx:javafx-fxml:14:$platform"
runtimeOnly "org.openjfx:javafx-graphics:14:win"
runtimeOnly "org.openjfx:javafx-graphics:14:mac"
runtimeOnly "org.openjfx:javafx-graphics:14:linux"
compile 'com.gluonhq:charm:5.0.0-jdk9'
compile 'org.reactfx:reactfx:2.0-M5'
compileOnly "org.projectlombok:lombok:1.18.12"
annotationProcessor 'org.projectlombok:lombok:1.18.12'
}
javafx {
version = "14"
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
mainClassName = 'com.my.app.MainClass'
jar {
manifest {
attributes 'Main-Class': 'com.my.app.Launcher'
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
}
eclipse {
classpath {
downloadJavadoc = true
downloadSources = true
}
}
wrapper {
gradleVersion = '6.5.1'
}
JDK 14
Eclipse 4.16
buid.gradle declares the dependency 'com.gluonhq:charm:5.0.0-jdk9' (I saw that version 6 exists, should I upgrade?)
The old Gluon jfxmobile plugin, that was used to create mobile applications with Java 1.8 or Java 9, is EOL.
To be able to run those mobile applications with Java/JavaFX 11+, you have to replace that plugin new Gluon Client plugin.
More details:
Client Maven plugin,
Client basic samples.
Documentation.
One main difference, the new plugin uses Maven instead of Gradle. However, thanks to the community, there is also a version of the Client plugin for Gradle, that might be a little bit behind the maven counterpart).
In order to migrate your project to Java 11+ and replace one plugin with the other, you have to modify your build file.
plugins {
// new client plugin
id 'com.gluonhq.client-gradle-plugin' version '0.1.30'
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.beryx.jlink' version '2.21.2'
id 'com.google.osdetector' version '1.6.2'
id 'eclipse'
id 'org.kordamp.gradle.jdeps' version '0.11.0'
}
repositories {
jcenter()
maven {
url 'http://nexus.gluonhq.com/nexus/content/repositories/releases'
}
}
sourceCompatibility = 14
targetCompatibility = 14
dependencies {
compile 'com.gluonhq:charm:6.0.5'
compile 'org.reactfx:reactfx:2.0-M5'
compileOnly "org.projectlombok:lombok:1.18.12"
annotationProcessor 'org.projectlombok:lombok:1.18.12'
}
javafx {
version = "14"
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
gluonClient {
// target = "ios" // uncomment to deploy on iOS
// target = "android" // uncomment to deploy on Android
attachConfig {
version = "4.0.8"
services 'display', 'lifecycle', 'statusbar', 'storage'
}
}
You will notice the main changes:
Charm (Gluon Mobile) is now 6.0+
Charm Down has been renamed to Attach, current version is 4.0.8 (you will have to refactor the package names, like com.gluonhq.charm.down.plugins.StorageService to com.gluonhq.attach.storage.StorageService).
You can still run the project with your JDK, with ./gradlew run (using the JavaFX plugin).
With the new Client plugin you will also be able to create a native image, that will run on desktop (Windows, Linux, MacOS) and mobile (Android, iOS), leveraging GraalVM.
Following the Client requirements, download GraalVM for your host machine from here, set GRAALVM_HOME, and you will be able to run:
// build the native image, it takes some time:
./gradlew clean nativeBuild
// run the native image
./gradlew nativeRun
And if you have a mobile device at hand, only by enabling the target to iOS or Android in your build will build and deploy the native image to that mobile platform.

Gradle build jar file with JavaFX components can't find main class [duplicate]

I am trying to upgrade a JavaFX project from the 8 Java version to the 11 version. It works when I use the "run" Gradle task (I followed the Openjfx tutorial), but when I build (with the "jar" Gradle task) and execute (with "java -jar") a jar file, the message "Error: JavaFX runtime components are missing, and are required to run this application" appears.
Here is my build.gradle file :
group 'Project'
version '1.0'
apply plugin: 'java'
sourceCompatibility = 1.11
repositories {
mavenCentral()
}
def currentOS = org.gradle.internal.os.OperatingSystem.current()
def platform
if (currentOS.isWindows()) {
platform = 'win'
} else if (currentOS.isLinux()) {
platform = 'linux'
} else if (currentOS.isMacOsX()) {
platform = 'mac'
}
dependencies {
compile "org.openjfx:javafx-base:11:${platform}"
compile "org.openjfx:javafx-graphics:11:${platform}"
compile "org.openjfx:javafx-controls:11:${platform}"
compile "org.openjfx:javafx-fxml:11:${platform}"
}
task run(type: JavaExec) {
classpath sourceSets.main.runtimeClasspath
main = "project.Main"
}
jar {
manifest {
attributes 'Main-Class': 'project.Main'
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}
compileJava {
doFirst {
options.compilerArgs = [
'--module-path', classpath.asPath,
'--add-modules', 'javafx.controls,javafx.fxml'
]
}
}
run {
doFirst {
jvmArgs = [
'--module-path', classpath.asPath,
'--add-modules', 'javafx.controls,javafx.fxml'
]
}
}
Do you know what I should do ?
With Java/JavaFX 11, the shadow/fat jar won't work.
As you can read here:
This error comes from sun.launcher.LauncherHelper in the java.base module. The reason for this is that the Main app extends Application and has a main method. If that is the case, the LauncherHelper will check for the javafx.graphics module to be present as a named module:
Optional<Module> om = ModuleLayer.boot().findModule(JAVAFX_GRAPHICS_MODULE_NAME);
If that module is not present, the launch is aborted.
Hence, having the JavaFX libraries as jars on the classpath is not allowed
in this case.
What's more, every JavaFX 11 jar has a module-info.class file, at the root level.
When you bundle all the jars content into one single fat jar, what happens to those files with same name and same location? Even if the fat jar keeps all of them, how does that identify as a single module?
There is a request to support this, but it hasn't been addressed yet: http://openjdk.java.net/projects/jigsaw/spec/issues/#MultiModuleExecutableJARs
Provide a means to create an executable modular “uber-JAR” that contains more than one module, preserving module identities and boundaries, so that an entire application can be shipped as a single artifact.
The shadow plugin still does make sense to bundle all your other dependencies into one jar, but after all you will have to run something like:
java --module-path <path-to>/javafx-sdk-11/lib \
--add modules=javafx.controls -jar my-project-ALL-1.0-SNAPSHOT.jar
This means that, after all, you will have to install the JavaFX SDK (per platform) to run that jar which was using JavaFX dependencies from maven central.
As an alternative you can try to use jlink to create a lightweight JRE, but your app needs to be modular.
Also you could use the Javapackager to generate an installer for each platform. See http://openjdk.java.net/jeps/343 that will produce a packager for Java 12.
Finally, there is an experimental version of the Javapackager that works with Java 11/JavaFX 11: http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-September/022500.html
EDIT
Since the Java launcher checks if the main class extends javafx.application.Application, and in that case it requires the JavaFX runtime available as modules (not as jars), a possible workaround to make it work, should be adding a new Main class that will be the main class of your project, and that class will be the one that calls your JavaFX Application class.
If you have a javafx11 package with the Application class:
public class HelloFX extends Application {
#Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
Scene scene = new Scene(new StackPane(l), 400, 300);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Then you have to add this class to that package:
public class Main {
public static void main(String[] args) {
HelloFX.main(args);
}
}
And in your build file:
mainClassName='javafx11.Main'
jar {
manifest {
attributes 'Main-Class': 'javafx11.Main'
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}
Now you can run:
./gradlew run
or
./gradlew jar
java -jar build/libs/javafx11-1.0-SNAPSHOT.jar
The final goal is to have the JavaFX modules as named modules on the module path, and this looks like a quick/ugly workaround to test your application. For distribution I'd still suggest the above mentioned solutions.
With the latest versions of JavaFX, you can use two Gradle plugins to easily distribute your project (javafxplugin and jlink).
With these plugins, you can:
Create a distributable zip file that contains all the needed jar files: it requires a JRE to be executed (with the bash or batch script)
Create a native application with Jlink for a given OS: a JRE is not needed to execute it, as Jlink includes a "light" JRE (including only the needed java modules and dependencies) in the distribution folder
I made an example on bitbucket, if you want an example.
[Edit: For the latest versions of JavaFX, please check my second answer]
If someone is interested, I found a way to create jar files for a JavaFX11 project (with Java 9 modules). I tested it on Windows only (if the application is also for Linux, I think we have to do the same steps but on Linux to get JavaFX jars for Linux).
I have a "Project.main" module (created by IDEA, when I created a Gradle project) :
src
+-- main
| +-- java
| +-- main
| +-- Main.java (from the "main" package, extends Application)
| +-- module-info.java
build.gradle
settings.gradle
...
The module-info.java file :
module Project.main {
requires javafx.controls;
exports main;
}
The build.gradle file :
plugins {
id 'java'
}
group 'Project'
version '1.0'
ext.moduleName = 'Project.main'
sourceCompatibility = 1.11
repositories {
mavenCentral()
}
def currentOS = org.gradle.internal.os.OperatingSystem.current()
def platform
if (currentOS.isWindows()) {
platform = 'win'
} else if (currentOS.isLinux()) {
platform = 'linux'
} else if (currentOS.isMacOsX()) {
platform = 'mac'
}
dependencies {
compile "org.openjfx:javafx-base:11:${platform}"
compile "org.openjfx:javafx-graphics:11:${platform}"
compile "org.openjfx:javafx-controls:11:${platform}"
}
task run(type: JavaExec) {
classpath sourceSets.main.runtimeClasspath
main = "main.Main"
}
jar {
inputs.property("moduleName", moduleName)
manifest {
attributes('Automatic-Module-Name': moduleName)
}
}
compileJava {
inputs.property("moduleName", moduleName)
doFirst {
options.compilerArgs = [
'--module-path', classpath.asPath,
'--add-modules', 'javafx.controls'
]
classpath = files()
}
}
task createJar(type: Copy) {
dependsOn 'jar'
into "$buildDir/libs"
from configurations.runtime
}
The settings.gradle file :
rootProject.name = 'Project'
And the Gradle commands :
#Run the main class
gradle run
#Create the jars files (including the JavaFX jars) in the "build/libs" folder
gradle createJar
#Run the jar file
cd build/libs
java --module-path "." --module "Project.main/main.Main"

How to deploy a Jar from a library project

I have a library project in Java which is several folders, each one doing specific parts and having its own dependencies.
Since I am working locally I would like to deploy this library locally and get the Jar to import to another project.
For this reason I am using gradle and what I did was going to the directory where I have all the folders of the library and gradle init and then gradle build.
Since I want the files locally, I saw that I can use gradle publishToMavenLocal, which I did and it created a jar file under ~/.m2/..... Now the issue is that this jar file appear to only contain a META-INF folder and inside of it a manifest.mf file.
This is the build.gradle file used.
What am I doing wrong? Should I do something different?
check gradle docs
there is also a complete example.
be sure to add your sourceSets that you want to compile and build in the jar.
build.gradle
plugins {
id 'java'
id 'maven-publish'
}
repositories {
mavenLocal()
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
sourceSets {
main {
java { srcDir 'src/main/java' }
resources {
srcDirs 'src/main/resources'
}
}
test {
java { srcDir 'src/test/java' }
resources {
srcDirs 'src/test/resources'
}
}
}
publishing {
publications {
maven(MavenPublication) {
groupId = 'org.gradle.sample'
artifactId = 'project1-sample'
version = '1.1'
from components.java
}
}
}
You could also add your library project to your main project like this :
build.gradle
dependencies {
compile project(':library_project')
}
settings.gradle
rootProject.name = 'Project'
include ":library_project"
project(':library_project').projectDir = new File(settingsDir, '../library_project')

Trying to use tools.jar - gradle

I am using gradle to package some java code into a jar. I am using some classes from tools.jar. I have had success in gradle building it and making a jar, but when I run that jar using java -jar <package>.jar I get the folowing
java.lang.NoClassDefFoundError: com/sun/tools/attach/VirtualMachine.
Since tools.jar is something you get with a jdk, not a jre. Is there a way I can bundle tools.jar with my package.jar and have my jar work anywhere?
Here is my build.gradle so far.
buildscript {
repositories {
maven {
url 'https://plugins.gradle.org/m2/'
}
}
}
description = "A java program"
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
mavenCentral()
flatDir {
dirs System.properties['java.home'] + '/../lib'
}
}
jar {
archiveName = "jProg.jar"
manifest {
attributes(
'Dependencies': 'com.sun.tools'
)
}
}
dependencies {
compile group: 'com.sun', name: 'tools'
}
Probably what you need is called 'fat jar' (Gradle packs all dependencies to single jar)

Categories

Resources