How to Add Maven Dependencies While Using the Maven-Jlink-Plugin

Is there a way to add maven dependencies while using the maven-jlink-plugin?

This has not much to do with the plugin I believe. Module joda.time in your case seems to be an automatic module.

The jlink tool does not support linking of automatic modules because they can rely on the arbitrary content of the classpath, which goes against the idea of a self-contained Java runtime.

So there are two ways to fix this probably :-

  • (you don't own the jar) Temporarily go ahead create a module-info.java[you could use jdeps tool for it] and update the jar[using jar tool] with the corresponding compiled class as in projects under Java 9.

  • (you own the dependency) Permanently migrate the jar to Java 9 itself, where it would consist of the module-info.class by itself after being compiled and packaged.

Maven javafx:jlink. How to specifiy the module-path used?

Specifically to answer your question:

How to specify the module-path used?

You could run jlink from the command-line instead of via Maven.

That way you can specify any module path you wish.

From the man page for jlink:

jlink [options] --module-path modulepath --add-modules module [, module...]

modulepath

The path where the jlink tool discovers observable modules. These modules can be modular JAR files, JMOD files, or exploded modules.

module

The names of the modules to add to the runtime image. The jlink tool adds these modules and their transitive dependencies.

If you wish to continue using the openjfx maven plugin jlink command, you can do that.

The code which configures the module path passed to jlink by the maven plugin is in git.

if (modulepathElements != null && !modulepathElements.isEmpty()) {
commandArguments.add(" --module-path");
String modulePath = StringUtils.join(modulepathElements.iterator(), File.pathSeparator);
if (jmodsPath != null && ! jmodsPath.isEmpty()) {
getLog().debug("Including jmods from local path: " + jmodsPath);
modulePath = jmodsPath + File.pathSeparator + modulePath;
}
commandArguments.add(modulePath);

commandArguments.add(" --add-modules");
if (moduleDescriptor != null) {
commandArguments.add(" " + moduleDescriptor.name());
} else {
throw new MojoExecutionException("jlink requires a module descriptor");
}
}

This is based on the javafx:jlink options:

jmodsPath: When using a local JavaFX SDK, sets the path to the local JavaFX jmods

So if you put your module on the path specified by that option, it should be found.


Here are some other options.

Package your app as a non-modular app using jpackage

I think mipa's suggestion of using JPackageScriptFX is probably the best approach:

  • github.com/dlemmermann/JPackageScriptFX

Create a new jar from binaries

  1. Unjar the JBibTex.jar.
  2. Add in the compiled module-info to unjared directory.
  3. Create a new jar, e.g. JBibTexModule.jar.
  4. Install that to your maven repository via a mvn install command.
  5. Add a maven dependency on JBibTexModule instead of JBibTex.
  6. Add any needed statements to your main app module-info file to use the library.

Then, I believe you are done, and it would build, link, and run ok.

Create a new jar from source

The jbibtex library is also in github, so you could create an issue or pull-request on the project to ask it to be modularized.

Also, you could:

  1. Clone the repository.
  2. Add the module-info.java.
  3. Build the jar file.
  4. Install it locally in your local maven repository.
  5. Then depend on the local version you have created.

Jlink - including a directory that contains a custom python script in a JavaFX application

Problems

The src/main/resources directory only exists in your project sources. It does not exist in the build output, and it definitely does not exist in your deployment location. In other words, using:

var pyPath = Paths.get("src/main/resources/script/main.py").toAbsolutePath().toString();

Will only work when your working directory is your project directory. It's also reading the "wrong" main.py resource, as the "correct" one will be in your target directory. Additionally, resources are not files. You must access resources using the resource-lookup API. For example:

var pyPath = getClass().getResource("/script/main.py").toString();

And note src/main/resources is not included in the resource name.

Executing the Script

But even after you correctly access the resource you still have a problem. Your script is a resource, which means it will be embedded in a JAR file or custom run-time image when deployed. I strongly doubt Python will know how to read and execute such a resource. This means you need to find a way to make the Python script a regular file on the host computer.



Potential Solutions

I can think of at least three approaches that may solve the problems described above. As I only have Windows, I can't promise the below examples will work on other platforms, or otherwise be easily translated for other platforms.

My examples don't include JavaFX, as I don't believe that's necessary to demonstrate how to include a Python script that is executed at runtime.

Here are some common aspects between all solutions.

module-info.java:

module com.example {}

main.py:

print("Hello from Python!")

1. Extract the Script at Runtime

One approach is to extract the Python script to a known location on the host computer at runtime. This is likely the most versatile solution, as it doesn't depend much on the way you deploy your application (jar, jlink, jpackage, etc.).

This example extracts the script to a temporary file, but you can use another location, such as an application-specific directory under the user's home directory. You can also code it to extract only if not already extracted, or only once per application instance.

I think this is the solution I would use, at least to start with.

Project structure:

|   pom.xml
|
\---src
\---main
+---java
| | module-info.java
| |
| \---com
| \---example
| \---app
| Main.java
|
\---resources
\---scripts
main.py

Main.java:

package com.example.app;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {

public static void main(String[] args) throws Exception {
Path target = Files.createTempDirectory("sample-1.0").resolve("main.py");
try {
// extract resource to temp file
try (InputStream in = Main.class.getResourceAsStream("/scripts/main.py")) {
Files.copy(in, target);
}

String executable = "python";
String script = target.toString();

System.out.printf("COMMAND: %s %s%n", executable, script); // log command
new ProcessBuilder(executable, script).inheritIO().start().waitFor();
} finally {
// cleanup for demo
Files.deleteIfExists(target);
Files.deleteIfExists(target.getParent());
}
}
}

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>sample</artifactId>
<version>1.0</version>

<name>sample</name>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>18</maven.compiler.release>
</properties>

<build>
<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.app.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jlink-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>default-cli</id>
<phase>package</phase>
<goals>
<goal>jlink</goal>
</goals>
</execution>
</executions>
<configuration>
<classifier>win</classifier>
</configuration>
</plugin>

<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.5.2</version>
<executions>
<execution>
<id>default-cli</id>
<phase>package</phase>
<goals>
<goal>jpackage</goal>
</goals>
</execution>
</executions>
<configuration>
<type>APP_IMAGE</type>
<runtimeImage>${project.build.directory}/maven-jlink/classifiers/win</runtimeImage>
<module>com.example/com.example.app.Main</module>
<destination>${project.build.directory}/image</destination>
<winConsole>true</winConsole>
</configuration>
</plugin>

</plugins>
</build>

</project>

Build the project with:

mvn package

And then execute the built application image:

> .\target\image\sample\sample.exe

COMMAND: python C:\Users\***\AppData\Local\Temp\sample-1.015076039373849618085\main.py
Hello from Python!

2. Place the Script in the "Application Directory"

Disclaimer: I don't know if doing this is a smart or even supported approach.

This solution makes use of --input to place the script in the "application directory" of the application image. You can then get a reference to this directory by setting a system property via --java-options and $APPDIR. Note I tried to get this to work with the class-path, so as to not need the $APPDIR system property, but everything I tried resulted in Class#getResource(String) returning null.

The application directory is the app directory shown in this documentation.

As this solution places the Python script with the rest of the application image, which means it's placed in the installation location, you may be more likely to run into file permission issues.

Given the way I coded Main.java, this demo must be executed only after packaging with jpackage. I suspect there's a more robust way to implement this solution.

Project structure:

|   pom.xml
|
+---lib
| \---scripts
| main.py
|
\---src
\---main
\---java
| module-info.java
|
\---com
\---example
\---app
Main.java

Main.java:

package com.example.app;

import java.nio.file.Path;

public class Main {

public static void main(String[] args) throws Exception {
String executable = "python";
String script = Path.of(System.getProperty("app.dir"), "scripts", "main.py").toString();

System.out.printf("COMMAND: %s %s%n", executable, script); // log command
new ProcessBuilder(executable, script).inheritIO().start().waitFor();
}
}

pom.xml:

(this is only the <configuration> of the org.panteleyev:jpackage-maven-plugin plugin, as everything else in the POM is unchanged from the first solution)

<configuration>
<type>APP_IMAGE</type>
<runtimeImage>${project.build.directory}/maven-jlink/classifiers/win</runtimeImage>
<input>lib</input>
<javaOptions>
<javaOption>-Dapp.dir=$APPDIR</javaOption>
</javaOptions>
<module>com.example/com.example.app.Main</module>
<destination>${project.build.directory}/image</destination>
<winConsole>true</winConsole>
</configuration>

Build the project with:

mvn package

And then execute the built application image:

> .\target\image\sample\sample.exe

COMMAND: python C:\Users\***\Desktop\sample\target\image\sample\app\scripts\main.py
Hello from Python!

3. Add the Script as Additional "App Content"

Disclaimer: Same as the disclaimer for the second solution.

This would make use of the --app-content argument when invoking jpackage. Unfortunately, I could not figure out how to configure this using Maven, at least not with the org.panteleyev:jpackage-maven-plugin plugin. But essentially this solution would have been the same as the second solution above, but with --input removed and --app-content lib/scripts added. And a slight change to how the script Path is resolved in code.

The --app-content argument seems to put whatever directories/files are specified in the root of the generated application image. I'm not sure of a nice convenient way to get this directory, as the application image structure is slightly different depending on the platform. And as far as I can tell, there's no equivalent $APPDIR for the image's root directory.

JavaFX Maven with moditect and Java 11

I have added a complete example for OpenJDK11 with OpenJFX 11 and auto generated modul-info.class with moditect plugin (Click here!)

Try this:

Note: The moduleName property must be the same as in the module info file

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>md</groupId>
<artifactId>test33</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>test33</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>md.test33.MainApp</mainClass>
<moduleName>md</moduleName>
</properties>

<organization>
<!-- Used as the 'Vendor' for JNLP generation -->
<name>Your Organisation</name>
</organization>

<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>11-ea+25</version>
</dependency>
</dependencies>

<build>
<plugins>
<!-- sets up the version of Java you are running and complines the Code -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release> <!-- or <release>10</release>-->
</configuration>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.2.1</version> <!-- Use newer version of ASM -->
</dependency>
</dependencies>
</plugin>
<!-- used to make the program run -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>--module-path</argument>
<argument>
${project.build.directory}/modules
</argument>
<argument>--module</argument>
<argument>${moduleName}/${mainClass}</argument>
</arguments>
</configuration>
</execution>
</executions>

</plugin>

<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
</configuration>
</plugin>

<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Beta1</version>
<executions>
<execution>
<id>create-runtime-image</id>
<phase>package</phase>
<goals>
<goal>create-runtime-image</goal>
</goals>
<configuration>
<modulePath>
<path>${project.build.directory}/modules</path>
</modulePath>
<modules>
<module>${moduleName}</module>
<!-- <module>javafx.controls</module>
<module>javafx.graphics</module> -->
</modules>
<launcher>
<name>test33</name>
<module>${moduleName}/${mainClass}</module>
</launcher>
<compression>2</compression>
<stripDebug>true</stripDebug>
<outputDirectory>${project.build.directory}/jlink-image</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

modul-info.java

module md { 
requires javafx.fxml;
requires javafx.controls;
requires javafx.graphics;
requires javafx.base;
opens md.test33;
}

Error: automatic module cannot be used with jlink: - Maven with JavaFX

The jlink requires all dependencies to be modular. After generation, it generates a custom JRE image including the required modules. The ini4j seems non-modular.
For non-modular dependencies, you can go with the old Classpath approach after getting the custom JRE which has been generated without non-modular ones.

Briefly, run jlink excluding the non-modulars than add the jar files of non-modulars to the generated JRE image. The modules method and Classpath method can be combined this way.

A bit of fiddling with maven plugins should do this automatically.

Example for ini4j

  • Define some properties for convenience.

pom.xml

<properties>
<jlink-image-name>JUSBPlotter</jlink-image-name>
<ini4j-jar-name>ini4j.jar</ini4j-jar-name>
</properties>
  1. Disable ini4j from module-info.java (It should be enable during development, only do this when you want to package the project)
module org.openjfx.JUSBPlotter {
requires javafx.controls;
requires javafx.fxml;
requires com.fazecast.jSerialComm;
//requires ini4j;
requires org.apache.commons.io;

opens org.openjfx.JUSBPlotter to javafx.fxml;
exports org.openjfx.JUSBPlotter;
}

  1. Configure maven-dependency-plugin to copy the jar file of ini4j into the lib/ folder in jlink image.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
</execution>
</executions>
<configuration>
<artifactItems>
<!-- Copy ini4j jar into the jlink image -->
<artifactItem>
<groupId>org.ini4j</groupId>
<artifactId>ini4j</artifactId>
<version>0.5.4</version>
<type>jar</type>
<destFileName>${ini4j-jar-name}</destFileName>
</artifactItem>
</artifactItems>
<!-- Set output directory to lib folder in jlink image -->
<outputDirectory>${project.build.directory}/${jlink-image-name}/lib</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</plugin>

  1. Configure jlink launcher option in the javafx-maven-plugin in order to add the jar file of non-modular ini4j to the Classpath.
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<stripDebug>true</stripDebug>
<compress>2</compress>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
<launcher>JUSBPlotter</launcher>
<jlinkImageName>JUSBPlotter</jlinkImageName>
<mainClass>org.openjfx.JUSBPlotter.Main</mainClass>
<!-- ini4j jar file will be copied to the {image-folder}/lib/ folder. The launcher script should have this option to add it to the classpath -->
<options>-cp ../lib/${init4j-jar-name}</options>
</configuration>
</plugin>

Run:

  • mvn clean javafx:jlink
  • mvn package
  • cd target/JUSBPlotter/bin
  • ./JUSBPlotter

maven-dependeny-plugin will copy the jar file when you run mvn package. But the jlink image must be already generated. So run the mvn javafx:jlink first. Then run mvn package.

Refer here to see how I applied for sqlite-jdbc in my project.



Related Topics



Leave a reply



Submit