Automatic metadata resolution
How Nucleus resolves and merges GraalVM reachability metadata at build time so native images compile without hand-written configuration.
Nucleus resolves and merges GraalVM reachability metadata at build time, so
packageGraalvmNative produces a working native binary without you writing reflection,
resource, or JNI configuration by hand. The metadata comes from five sources. Each is
generated into its own directory and passed to native-image, and the module's own metadata
is discovered from the classpath.
Level 1 — per-library metadata
The plugin ships 29 metadata files curated for the Kotlin desktop ecosystem, covering Compose,
Skiko, ktor (including client plugins, server, and websockets), kotlinx.serialization,
SQLite JDBC, androidx.sqlite bundled, Coil, JNA, FileKit, SLF4J, Sentry, and others.
Each file may declare a matchPackages condition. The filterGraalvmLibraryMetadata task reads
these files from the plugin JAR and includes a file only when the corresponding packages are on
the runtime classpath; files without a condition are always included. Libraries in the curated
set are covered without extra configuration.
Level 2 — GraalVM Reachability Metadata Repository
The resolveGraalvmReachabilityMetadata task resolves entries from the
Oracle GraalVM Reachability Metadata Repository
for each dependency on your runtime classpath. Resolution is enabled by default and uses
repository version 0.10.6.
Configure this source with the metadataRepository { } block: enabled, version,
excludedModules, and moduleToConfigVersion. See
Configuration.
Level 3 — platform-specific metadata
The generateGraalvmPlatformMetadata task writes reflection metadata for the current operating
system: sun.awt.windows.* on Windows, sun.lwawt.macosx.* on macOS, and sun.awt.X11.* on
Linux, along with Java2D pipelines, font managers, and security providers. It also injects a
reflection entry for your main class. No per-platform configuration is required in your build
script.
Level 4 — static bytecode analysis
The analyzeGraalvmStaticMetadata task scans every compiled class on the runtime classpath,
including your own module's classes, and detects:
- Native methods and their parameter and return types (JNI metadata).
Class.forName()andMethodHandles.Lookup.findClass()calls with literal class names (reflection metadata).getResource()andgetResourceAsStream()calls with literal paths (resource metadata).- JNI callback parameters — classes passed to native code that call back into Java.
- JNI superclass chains — parent classes needed for field access from native code.
@Serializableclasses — emits theCompanion.serializer()reflection entry.
Detect orphan project classes
Annotation processors and naming-convention loaders (Room *_Impl, Dagger/Hilt *_Factory,
Moshi adapters, …) produce classes that no classpath bytecode references. Literal
Class.forName analysis and L1–L3 metadata never see them. From 2.3, a single pass inside the
bytecode analyzer registers a public no-arg <init> for those orphans when:
- the class is concrete and public with a public no-arg constructor, and
- a supertype other than
Object(or an interface) is referenced by app code.
graalvm {
detectOrphanProjectClasses = true // default
// reflectionForProjectClasses = true // opt-in: every project class with a public no-arg ctor
}Build logs emit [orphan] fqcn for each registration. Prefer the default detector; only enable
reflectionForProjectClasses when you need a temporary sledgehammer (image growth is measurable).
From 2.3.1 the detector resolves project class directories from the application target's
compilation outputs (for example jvm("desktop") → desktopMainClasses) instead of hardcoded
jvm/main paths, so KMP named JVM targets are covered. Nest-internal reference edges no longer
hide real Room *_Impl classes that contain nested types.
Level 5 — bundled runtime metadata
The nucleus.graalvm-runtime module bundles GraalVM metadata inside its JAR:
- A
reachability-metadata.jsonwith serialization entries. - A
native-image.propertiesthat registers resource patterns (.svg,.ttf,.otf,nucleus/.*,META-INF/services/.*, andcomposeResources/.*). - SVM substitutions for the AWT font managers, the splash screen, and the X11 window class.
native-image discovers all of it from the classpath, so no configuration is needed.
How the sources merge
packageGraalvmNative runs the generation tasks and then passes each resulting directory to
native-image through repeated -H:ConfigurationFileDirectories= arguments — the per-library
output (Level 1), the resolved repository directories (Level 2), the platform metadata
(Level 3), and the static-analysis output (Level 4). Any manual entries in your project's own
config directory are included as well. Level 5 is read from the classpath by native-image
itself. Most apps compile to a native binary without any hand-written metadata.
Fill gaps with the tracing agent
The static sources can miss reflection driven by runtime values, dynamically loaded classes, or unusual library patterns. Run the agent once before a release to catch them:
./gradlew runWithNativeAgentExercise every screen and feature. The agent records reflection, JNI, resource, and proxy accesses into a temporary directory and merges them into your config without overwriting existing entries. In many cases it finds nothing new.
Clean up manual entries
If you accumulated manual entries that the automatic sources now cover, remove the redundant ones:
./gradlew cleanupGraalvmMetadataThe task compares your manual reachability-metadata.json against the combined baseline of
Levels 1–4 and removes anything already managed, reporting which entries were removed and which
remain.
From 2.3 it also reports entries whose types resolve nowhere on the runtime classpath
(typical agent noise: kotlin.Any, kotlin.Int, …). Default is report-only. To prune them:
./gradlew cleanupGraalvmMetadata -Pnucleus.graalvm.cleanup.removeUnresolvable=true
# Analyze without rewriting:
./gradlew cleanupGraalvmMetadata -Pnucleus.graalvm.cleanup.dryRun=trueFrom 2.3.1, well-known Kotlin tracing-agent phantoms (kotlin.Any / Int / FunctionN /
collections mapped types) are always removed during cleanup, without requiring
removeUnresolvable.
Types under the configured exactReachabilityMetadata packages are never removed — a missing
registration under exact mode is what restores a clear error. Remaining manual entries are listed
as kept — not covered by any managed source.
What's next
- Native access — resource patterns and font substitutions on top of this metadata system.
- Tasks & CI — the full GraalVM task graph.
Configuration
Reference for the graalvm { } DSL block that configures GraalVM native-image builds, including toolchain, image name, build arguments, metadata repository, and per-OS settings.
Tasks & CI
The Gradle tasks the Nucleus plugin adds for GraalVM native image, where their output lands, and how to run them on each OS in CI.