Nucleus
Performance & nativeGraalVM Native Image

GraalVM Native Image

GraalVM Native Image compiles a Nucleus app ahead of time into a self-contained native binary, with reflection, resource, and JNI metadata generated for every Nucleus module.

GraalVM Native Image ahead-of-time compiles a Compose Desktop app into a single self-contained binary that runs without a JVM. The trade-off is the closed-world assumption: no dynamic class loading, no agents, and every reflective call declared at build time. Nucleus generates that metadata for you, so the same source that builds a JVM app also builds a native image.

When to use GraalVM Native Image

  • Background services, menu-bar apps, and system-tray tools that stay idle but must remain responsive.
  • Utilities and launchers where startup time matters.
  • Store distribution (App Store, MSIX, Snap) and small self-contained installers that ship no separate JRE.
  • Memory-constrained environments such as sandboxes and low-spec hardware.

Trade-offs

  • No JIT. A native image runs ahead-of-time compiled code with no just-in-time optimization, so sustained CPU-heavy workloads run slower than on the JVM. For those, use the AOT cache instead.
  • Closed world. No runtime Class.forName driven by dynamic values, no custom classloaders, no scripting engines, no runtime bytecode generation. Libraries that rely on these — Spring, Groovy, ByteBuddy-based mocking, heavy JNA use — are not compatible; ship them with the AOT cache.
  • Per-platform builds. A native image must be compiled on each target OS, so producing binaries for macOS, Windows, and Linux requires a CI matrix.

Most idiomatic Kotlin libraries — Ktor, kotlinx.serialization, Coil, SQLite, Jewel, Compose, SLF4J — work without extra configuration.

Requirements

GraalVM toolchain

Nucleus downloads and caches GraalVM Community Edition automatically on the first native build — you no longer install a toolchain yourself. A GRAALVM_HOME environment variable takes precedence if you want to point at your own. See Provision the toolchain.

Community Edition by default, Oracle GraalVM on request

GraalVM CE is the default because it is licensed under GPLv2 with the Classpath Exception, which places no restriction on shipping it inside a paid application. Switch to Oracle GraalVM with toolchain { distribution = GraalvmDistribution.ORACLE } when you need its exclusive optimizations — Profile-Guided Optimization, -O3, ML-inferred profiles and advanced obfuscation. Oracle GraalVM is free for production use under the GraalVM Free Terms and Conditions, but the GFTC forbids charging any fee associated with redistributing it, so review the terms before shipping a paid app. The deprecated AWT backend still requires a local BellSoft Liberica NIK install for its AWT/Swing native-image support.

Platform toolchains

PlatformRequired
macOSXcode Command Line Tools
WindowsMSVC (Visual Studio Build Tools)
LinuxGCC, patchelf, and xvfb for headless compilation

How Nucleus builds the native image

Three modules cooperate to produce a native image from the same source as a JVM build:

  • nucleus.graalvm-runtime — the runtime support library. It provides SVM @TargetClass substitutions for the AWT internals that don't work as-is under native image (the Fontconfig and Win32 font managers, font creation, the splash screen, and the X11 toolkit WMClass), registers app resources, and exposes GraalVmInitializer.
  • The Nucleus Gradle plugin — generates the reachability metadata (reflection, resources, JNI) for your dependencies and wires the packageGraalvmNative task graph.
  • nucleus.nucleus-application — calls GraalVmInitializer.initialize() for you and exposes the same DecoratedWindow / NucleusWindow API as the JVM path, so one source tree produces both artifacts.
build.gradle.kts
nucleus.application {
    mainClass = "com.example.MainKt"
    graalvm {
        isEnabled = true
        imageName = "my-app"
    }
}

nucleusApplication calls GraalVmInitializer.initialize() at startup. If you write your own main() without it, call GraalVmInitializer.initialize() yourself before any AWT or Compose code runs.

What's next

  • Configuration — the full graalvm { } DSL and recommended build arguments.
  • Automatic metadata — how reflection, resources, and JNI are resolved for you.
  • Tasks and CI — Gradle tasks, output locations, and the GitHub Actions matrix.