Skip to content

Instantly share code, notes, and snippets.

@EasyG0ing1
Last active May 6, 2024 07:26
Show Gist options
  • Save EasyG0ing1/0a11f8b6b024f262d825a94cb7c8ee9d to your computer and use it in GitHub Desktop.
Save EasyG0ing1/0a11f8b6b024f262d825a94cb7c8ee9d to your computer and use it in GitHub Desktop.

Compiling Java and JavaFX GraalVM native-images

Build tools

You don't use the Java JDK when building native-images. Instead, you download GraalVM from their web site and it is packaged just like a JDK, complete with all of the usual JDK commands and modules etc. It needs to be set up as your JDK.

Maven of course is also necessary for this discussion but Graal can be compiled with Gradle as well.

Windows

You need some VisualStudio Build tools:

winget install Microsoft.VisualStudio.2022.BuildTools --exact

Next, launch the Visual Studio Installer. Then click on:

  • Workloads
  • Desktop development with C++

Then on the right, make sure these are all checked (if these versions are not there use the latest):

  • MSVC v143 - VS 2022 C++ x64/x86 build tools
  • Windows 11 SDK
  • C++ CMake tools for Windows
  • Testing tools core features - Build Tools
  • C++ AddressSanitizer
  • C++ ATL for latest vXXX build tools...
  • C++ MFC for latest vXXX build tools (x86...

Select whether to install while downloading (for fast internet connections) or not, then click on Modify and wait for everything to install.

Linux

sudo apt install curl -y
sudo apt install zip -y
sudo apt install zlib1g-dev -y
sudo apt install build-essential -y

MacOS

xcode-select --install

Simple Applications

When your project isn't modular and it doesn't have a GUI, the process is fairly straight forward. Generally, the steps are:

  1. Adding the Maven Assembly plugin to your build section
  2. Build the fat jar
  3. Use the GraalVM native-image-agent to build the reflection and various other config json files
  4. Compiling the native-image

Maven Assembly Plugin

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.7.1</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>${mainClass}</mainClass>
            </manifest>
        </archive>
        <descriptorRefs>
            <descriptorRef>fat-jar</descriptorRef>
        </descriptorRefs>
        <finalName>${artifactId}</finalName>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Build the fat jar

mvn clean package

Use the GraalVM native-image-agent

The way I like to do this is to make a bash script or a Windows bat file so that it handles things cleanly. It would look something like this:

#!/bin/bash

JP="$HOME/path/to/project/folder"
G="$JP/graalvm"
T="$JP/target"

mvn -f $JP/pom.xml clean package

java --enable-preview \
-agentlib:native-image-agent=config-merge-dir=$G \
-jar $T/programName-fat-jar.jar

programName would need to match whatever the artifactId value is in the POM file.

This will run the program and the developer needs to engage the program so that anywhere it uses reflection or other calls are all actualized because GraalVM will read those calls and create the json files into the graalvm folder in the project root.

Stop the program once the dev has run it through all of its code.

Compiling the native image

I also do this with a bash script or a bat file

JP="$HOME/JetBrainsProjects/IntelliJIdea/iGet"
G="$JP/graalvm"
T="$JP/target"

mvn -f $JP/pom.xml clean package

native-image \
--no-fallback \
--verbose \
--enable-preview \
-H:+UnlockExperimentalVMOptions \
-H:+ReportExceptionStackTraces \
-H:JNIConfigurationFiles=$G/jni-config.json \
-H:DynamicProxyConfigurationFiles=$G/proxy-config.json \
-H:ReflectionConfigurationFiles=$G/reflect-config.json \
-H:ResourceConfigurationFiles=$G/resource-config.json \
-H:SerializationConfigurationFiles=$G/serialization-config.json \
-H:Name=$T/programName \
-jar $T/programName-fat-jar.jar

Again, programName would need to match whatever the artifactId value is in the POM file.

This should compile everything into the native image which will end up in target/programName

Simply run the program

target/programName

JavaFX Applications

JavaFX applications need to be full modularized programs, which means there must be a module-info.java file in src/main/java and it must properly have all of the requires opens exports etc.

The steps are generally the same as above, only instead of using the fat jar, we're going to compile a modularized jar file using the Maven Dependency plugin in the build section

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.6.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}/modules</outputDirectory>
                <includeScope>runtime</includeScope>
            </configuration>
        </execution>
    </executions>
</plugin>

This will put all of the modules into a folder that we can then use for the native-image command by adding it as a module-path

It will also create a jar file named programName-version.jar which we will use as the jar to compile into the native-image and we will also use it as a module-path.

We first generate all of our json configuration files like we did before only using a slightly different method

#!/bin/bash

JP="$HOME/path/to/project/folder"
G="$JP/graalvm"
T="$JP/target"

mvn -f $JP/pom.xml clean install

java \
--enable-preview \
-agentlib:native-image-agent=config-merge-dir=$G \
--module-path $T/programName-version.jar:$T/modules \
-m SimpleOTP/com.xyz.Main

programName would need to match whatever the artifactId value is in the POM file

version needs to match whatever the version value is in the POM file.

com.xyz.Main needs to match whatever the mainClass value is in the POM file

Run the program through it's code then quit the program then build the native image like this:

#!/bin/bash

JP="$HOME/path/to/project/folder"
G="$JP/graalvm"
T="$JP/target"

mvn -f $JP/pom.xml clean install

native-image \
--no-fallback \
--verbose \
--enable-preview \
--module-path $T/programName-version.jar:$T/modules \
--module moduleName/com.xyz.Main \
-H:+UnlockExperimentalVMOptions \
-H:+ReportExceptionStackTraces \
-H:JNIConfigurationFiles=$G/jni-config.json \
-H:DynamicProxyConfigurationFiles=$G/proxy-config.json \
-H:ReflectionConfigurationFiles=$G/reflect-config.json \
-H:ResourceConfigurationFiles=$G/resource-config.json \
-H:SerializationConfigurationFiles=$G/serialization-config.json \
-H:Name=$T/programName

programName would need to match whatever the artifactId value is in the POM file

version needs to match whatever the version value is in the POM file.

com.xyz.Main needs to match whatever the mainClass value is in the POM file

moduleName needs to match whatever name is given in the first line of the module-info.java file. I like to use the programName in my module-info.java file to keep it simple.

##Comments

It is highly likely that you will encounter problems or errors during the native-image compile phase or even if the native-image compiles successfully but then the code does something that wasn't able to be caught during the native-image-agent inspection step.

What I find to be very helpful is to copy the complete error stack traces and give them to Chat GPT (openai.com) which seems to know a lot about the GraalVM native-image compile process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment