I need to include a directory containing a python script and binaries that need to be executed by the script based on the parsed arguments in the JavaFX application.
The project is modular and built using Maven (although the modular part is not such an important piece of information).
When built using the maven run configuration, application works properly but for the purpose of creating a runtime image I stumble upon the problem of not having the script executed when I run the generated launcher .bat script in the "bin" folder of the "target".
For the purpose of generating the runtime, I have put the script directory in the project "resources" folder. The script is executed from the Java code using the Java Runtime.
Let's say the code looks like this:
pyPath = Paths.get("src/main/resources/script/main.py").toAbsolutePath().toString();
command = "python"+pyPath+args;
runtime = Runtime.getRuntime();
process = runtime.exec(command);
And pom.xml file looks like this:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gui</artifactId>
<version>1.0-SNAPSHOT</version>
<name>gui</name>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>18</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>18</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>11.1.1</version>
</dependency>
<dependency>
<groupId>com.dlsc.formsfx</groupId>
<artifactId>formsfx-core</artifactId>
<version>11.3.2</version>
<exclusions>
<exclusion>
<groupId>org.openjfx</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>12.3.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jfoenix</groupId>
<artifactId>jfoenix</artifactId>
<version>9.0.10</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.5.2</version>
<configuration>
<name>gui</name>
<appVersion>1.0.0</appVersion>
<vendor>1234</vendor>
<destination>target/dist</destination>
<module>com.example.gui/com.example.gui.Application</module>
<runtimeImage>target/example-gui</runtimeImage>
<winDirChooser>true</winDirChooser>
<winPerUserInstall>true</winPerUserInstall>
<winShortcut>true</winShortcut>
<winMenuGroup>Applications</winMenuGroup>
<icon>${project.basedir}/main/resources/img/icon.ico</icon>
<javaOptions>
<option>-Dfile.encoding=UTF-8</option>
</javaOptions>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>18</source>
<target>18</target>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<id>default-cli</id>
<configuration>
<mainClass>com.example.gui/com.example.gui.Application</mainClass>
<launcher>gui-launcher</launcher>
<jlinkZipName>gui</jlinkZipName>
<jlinkImageName>gui</jlinkImageName>
<jlinkVerbose>true</jlinkVerbose>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
<options>
<option>--add-opens</option><option>javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED</option>
<option>--add-opens</option><option>javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED</option>
<option>--add-opens</option><option>javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED</option>
<option>--add-opens</option><option>javafx.base/com.sun.javafx.binding=ALL-UNNAMED</option>
<option>--add-opens</option><option>javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED</option>
<option>--add-opens</option><option>javafx.base/com.sun.javafx.event=ALL-UNNAMED</option>
<option>--add-exports</option><option>javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED</option>
<option>--add-exports</option><option>javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED</option>
<option>--add-exports</option><option>javafx.base/com.sun.javafx.binding=ALL-UNNAMED</option>
<option>--add-exports</option><option>javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED</option>
<option>--add-exports</option><option>javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED</option>
<option>--add-exports</option><option>javafx.base/com.sun.javafx.event=ALL-UNNAMED</option>
</options>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
*Note: additional options for the javafx-maven-plugin are added for the jfoenix package compatibility
Also module-info.java
module com.example.gui {
requires javafx.controls;
requires javafx.fxml;
requires org.controlsfx.controls;
requires com.dlsc.formsfx;
requires org.kordamp.ikonli.javafx;
requires com.jfoenix;
opens com.example.gui to javafx.fxml;
exports com.example.gui;
}
Now the question is how do I include the script in the application runtime image, have it executed when I call the generated .bat for the application and finally packed using the jpackage?
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.
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.
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!")
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!
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!
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.