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
- Windows
- macOS
- Linux
-
Unpack GraalVM Community, for example into
%LOCALAPPDATA%\Programs\GraalVM, and set the environment variableGRAALVM_HOMEto that folder. -
Install the Visual Studio 2022 Build Tools with the C++ workload;
native-imageneeds 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"
- Unpack GraalVM Community and set
GRAALVM_HOMEto itsContents/Homefolder. - Install the Xcode command line tools (
xcode-select --install) for the C toolchain.
- Unpack GraalVM Community and set
GRAALVM_HOMEto its folder. - Install a C toolchain and the zlib headers, for example
build-essentialandzlib1g-devon Debian and Ubuntu.
Without a local GraalVM, build in Docker instead: see Linux builds in Docker.
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 flag | Default | What it does |
|---|---|---|
native.optimization | -O2 | native-image's optimization level. -Pnative.optimization=-Ob builds faster, as the CI's checks do; releases always use -O2, never -Ob |
-march=x86-64-v2 | always on x86-64 | native-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.static | unset | -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:
- Bash
- PowerShell
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.
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
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-coredeclares 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-httpdeclares 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
ServiceLoaderproviders in the reachability metadata or inMETA-INF/services(asnjmule-appdoes for its Logback configurator). - Prefer libraries that work in native images; the benchmarks can measure a choice inside a native image too.