Skip to main content

Native images

njMule's engine is distributed as a native executable made by GraalVM Native Image: users need no Java runtime, and njMule starts at once. There is one native build, the command line and headless engine (njmule-app), made by the GraalVM Native Build Tools plugin: njmule. Every package carries it; the desktop packages add the desktop window, a Tauri shell that starts it (Building), and the Windows packages the service host.

GraalVM Community only​

njMule is built with GraalVM Community 25.0.4.1.1 (Java 25, Innovation 4), the same release the CI uses, from the GraalVM Community builds (release graal-25.4.4.1.1, archives named graalvm-community-jdk-25i4-25.0.4.1.1_<platform>). Oracle GraalVM, which adds profile-guided optimization, is licensed under the GraalVM Free Terms and Conditions, which are not compatible with distributing GPL binaries: do not use it for njMule.

Set up a local GraalVM​

  1. Unpack GraalVM Community, for example into %LOCALAPPDATA%\Programs\GraalVM, and set the environment variable GRAALVM_HOME to that folder.

  2. Install the Visual Studio 2022 Build Tools with the C++ workload; native-image needs the Visual C++ compiler and linker, and the build needs the Windows SDK's resource compiler for the executables' icon and version information:

    winget install --id Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"

The Gradle build keeps using your JDK 25; only the native tasks call GraalVM, through GRAALVM_HOME.

The command-line executable​

./gradlew :njmule-app:nativeCompile

The executable is njmule-app/build/native/nativeCompile/njmule (njmule.exe on Windows, with its icon and version information linked in). Try it with njmule --version.

Optimization and CPU level​

Property or flagDefaultWhat it does
native.optimization-O2native-image's optimization level. -Pnative.optimization=-Ob builds faster, as the CI's checks do; releases always use -O2, never -Ob
-march=x86-64-v2always on x86-64native-image's default, x86-64-v3, needs AVX2: v2 also runs on CPUs without it, such as the Celeron of many Synology NAS. ARM builds keep their default
native.staticunset-Pnative.static=musl links the executable fully static with musl (--static --libc=musl); needs the musl toolchain on the PATH

Linux builds in Docker​

Without a local GraalVM, Docker builds the Linux x64 executable in the same pinned image as the CI. Run from the root of your clone:

docker run --rm -v "$PWD:/work" -v njmule-gradle:/root/.gradle --entrypoint bash \
ghcr.io/graalvm/native-image-community:25i4-25.0.4.1.1 /work/tools/native-docker.sh

In Git Bash on Windows, prefix the command with MSYS_NO_PATHCONV=1.

The result is njmule-app/build/native-linux/njmule. The script copies the sources into the container's own file system and runs Gradle there: on Docker Desktop (Windows, macOS) a build on the bind-mounted checkout is several times slower. Gradle's user home stays in the Docker volume njmule-gradle, so the next build reuses its downloads; sharing your own ~/.gradle across operating systems would mix their file locks. Extra arguments after the script's path go to Gradle, for example -Pnative.optimization=-Ob.

The static Linux executable​

The Synology package and the linux-x64-static archive use a fully static x64 executable linked with musl: it needs no C library of the system. Synology DSM has glibc 2.26, while the glibc build needs 2.34. tools/native-static-docker.sh builds it in the same image, with one more volume for the musl toolchain it downloads and checks against its SHA-256:

docker run --rm -v "$PWD:/work" -v njmule-gradle:/root/.gradle -v njmule-musl:/musl \
--entrypoint bash ghcr.io/graalvm/native-image-community:25i4-25.0.4.1.1 \
/work/tools/native-static-docker.sh

The result is njmule-app/build/native-static/njmule; the script checks that ldd finds no library in it and runs njmule --version.

The Windows service host​

On Windows a small C program, compiled with the Visual C++ tools, goes with the native executable:

./gradlew :njmule-app:nativeService

It lands in njmule-app/build/launcher/njmule-service.exe (from njmule-app/native/windows/service.c): the Windows service host that runs njmule.exe headless once njmule --service=install set the service up. Copy it next to njmule.exe to use it. GraalVM has no Windows on Arm build yet: the x64 programs run there under the system's x64 emulation.

Reachability metadata​

Native Image must know at build time what the code reaches by reflection, JNI, resources and ServiceLoader. That knowledge is the reachability metadata under src/main/resources/META-INF/native-image/ of each module, written by hand:

  • njmule-core declares its own, and initializes the class that holds the Windows-1252 charset at build time, so that the charset is in the image (about 12 KB instead of the 6 MB of all charsets).
  • njmule-http declares the resources it serves: the interface's files and the browser icons.
  • The libraries' own metadata comes from the GraalVM reachability metadata repository, which the Native Build Tools plugin reads.

Keep the code native-friendly​

  • No undeclared reflection and no dynamic proxies.
  • Register resources and ServiceLoader providers in the reachability metadata or in META-INF/services (as njmule-app does for its Logback configurator).
  • Prefer libraries that work in native images; the benchmarks can measure a choice inside a native image too.
  • Packaging: installers and images around these executables.
  • Releases: the release workflow builds them on every target system.