Skip to content

Commit d1794f2

Browse files
committed
docs(graalvm): split into multi-page section with diagrams and compatibility guide
- Split monolithic graalvm-native-image.md into 5 pages under graalvm/ - Add Mermaid diagrams for metadata pipeline and static analyzer - Add "When to avoid" section (JNA-heavy libs, Lucene, scripting engines, etc.) - Document all 5 metadata levels (L1-L5) including static bytecode analysis - Enable Mermaid support in mkdocs.yml - Remove obsolete migration and sponsorship sections
1 parent 850912f commit d1794f2

7 files changed

Lines changed: 541 additions & 509 deletions

File tree

docs/graalvm-native-image.md

Lines changed: 0 additions & 507 deletions
This file was deleted.

docs/graalvm/automatic-metadata.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Automatic Metadata Resolution
2+
3+
The goal of Nucleus is to make GraalVM native-image compilation **as transparent as possible**. In most cases, you should be able to run `packageGraalvmNative` and get a working binary without writing a single line of reflection configuration. To achieve this, Nucleus combines five complementary metadata sources that are resolved and merged automatically at build time.
4+
5+
## Level 1 — Per-library conditional metadata
6+
7+
The Nucleus Gradle plugin ships **28 per-library metadata files** covering Compose UI, Skiko, ktor, kotlinx.serialization, SQLite, Coil, JNA, FileKit, and many others. Each file declares a `matchPackages` condition — the metadata is only included if the corresponding library is actually present on your runtime classpath.
8+
9+
This means libraries like ktor or SQLite JDBC **just work** in native image without any manual configuration.
10+
11+
## Level 2 — Oracle Reachability Metadata Repository
12+
13+
Nucleus automatically downloads the [Oracle GraalVM Reachability Metadata Repository](https://github.com/oracle/graalvm-reachability-metadata) and resolves metadata for all dependencies on your runtime classpath. This covers libraries that are not yet covered by Nucleus's own L1 metadata — SLF4J, Logback, and many others. The resolved metadata directories are passed to `native-image` via `-H:ConfigurationFileDirectories=`.
14+
15+
This is enabled by default. To customize:
16+
17+
```kotlin
18+
graalvm {
19+
metadataRepository {
20+
enabled = true // disable with false
21+
version = "0.10.6" // override repository version
22+
excludedModules.add("group:artifact") // skip specific dependencies
23+
moduleToConfigVersion.put( // pin a specific metadata version
24+
"io.ktor:ktor-client-core",
25+
"3.0.0",
26+
)
27+
}
28+
}
29+
```
30+
31+
## Level 3 — Platform-specific metadata
32+
33+
The Nucleus Gradle plugin ships pre-built platform-specific metadata for macOS, Windows, and Linux. These cover platform-specific AWT implementations (`sun.awt.windows.*`, `sun.lwawt.macosx.*`, `sun.awt.X11.*`), Java2D pipelines, font managers, and security providers. The plugin writes the correct platform metadata to the build directory at compile time — **no per-platform configuration needed in your build script**.
34+
35+
## Level 4 — Static bytecode analysis
36+
37+
Nucleus includes a **static bytecode analyzer** that scans all compiled classes on your runtime classpath at build time and automatically detects reflection, JNI, and resource requirements — without running the application. The analyzer detects:
38+
39+
- **Native methods** and their parameter/return types (JNI metadata)
40+
- **`Class.forName()`** and **`MethodHandles.Lookup.findClass()`** calls (reflection metadata)
41+
- **`getResource()` / `getResourceAsStream()`** calls (resource metadata)
42+
- **JNI callback parameters** — classes passed to native code that call back into Java
43+
- **JNI superclass chains** — parent classes needed for field access from native code
44+
- **`@Serializable` classes** — automatically emits `Companion.serializer()` reflection entries
45+
46+
This analysis runs transparently as part of the build (the `analyzeGraalvmStaticMetadata` task) and its output is passed to `native-image` alongside the other metadata levels.
47+
48+
```mermaid
49+
flowchart LR
50+
jars["Runtime classpath JARs"] --> scan["Bytecode scanner"]
51+
52+
scan --> jni["JNI\nnative methods\n+ parameter types"]
53+
scan --> reflect["Reflection\nClass.forName()\nfindClass()"]
54+
scan --> res["Resources\ngetResource()\ngetResourceAsStream()"]
55+
scan --> serial["Serialization\n@Serializable\nCompanion.serializer()"]
56+
scan --> callback["JNI callbacks\n+ superclass chains"]
57+
58+
jni --> out["reachability-metadata.json"]
59+
reflect --> out
60+
res --> out
61+
serial --> out
62+
callback --> out
63+
64+
style scan fill:#0f3460,stroke:#16213e,color:#e0e0e0
65+
style out fill:#533483,stroke:#16213e,color:#e0e0e0
66+
```
67+
68+
## Level 5 — Generic cross-platform metadata
69+
70+
The `graalvm-runtime` module ships a `reachability-metadata.json` inside its JAR that covers all cross-platform reflection entries: Compose Desktop, AWT/Swing, Skiko, security providers, font managers, and more (~300+ types). This metadata is **automatically picked up** by native-image from the classpath — no configuration needed.
71+
72+
## How it all fits together
73+
74+
When you run `packageGraalvmNative`, Nucleus automatically resolves all five metadata levels and passes them to `native-image`:
75+
76+
```mermaid
77+
flowchart TB
78+
subgraph build ["packageGraalvmNative"]
79+
direction TB
80+
L1["L1 — Per-library metadata\n(28 conditional files in plugin)"]
81+
L2["L2 — Oracle Repository\n(auto-resolved for classpath deps)"]
82+
L3["L3 — Platform metadata\n(macOS / Windows / Linux)"]
83+
L4["L4 — Static bytecode analysis\n(scans compiled classes)"]
84+
L5["L5 — Generic metadata\n(graalvm-runtime JAR)"]
85+
end
86+
87+
L1 --> merge["Merge all metadata"]
88+
L2 --> merge
89+
L3 --> merge
90+
L4 --> merge
91+
L5 --> merge
92+
93+
merge --> NI["`**native-image**
94+
-H:ConfigurationFileDirectories=...`"]
95+
NI --> bin["Native binary"]
96+
97+
style build fill:#1a1a2e,stroke:#16213e,color:#e0e0e0
98+
style merge fill:#0f3460,stroke:#16213e,color:#e0e0e0
99+
style NI fill:#533483,stroke:#16213e,color:#e0e0e0
100+
style bin fill:#e94560,stroke:#16213e,color:#e0e0e0
101+
```
102+
103+
All of this happens transparently — no manual steps required. The result is that **most applications compile and run as native images without any manual reflection configuration**.
104+
105+
## The tracing agent — a final safety net
106+
107+
Even with five levels of automatic metadata, there can be edge cases that static analysis cannot catch: reflection driven by runtime values, dynamically loaded classes, or unusual library patterns. The tracing agent (`runWithNativeAgent`) remains available as a **final verification step**:
108+
109+
```bash
110+
./gradlew runWithNativeAgent
111+
```
112+
113+
During the tracing run, navigate through every screen and feature of your application. The agent records all reflection, JNI, resource, and proxy accesses and **merges** the results into your existing configuration. Entries already covered by the five metadata levels are **automatically deduplicated** — the agent output stays minimal.
114+
115+
In many cases, the agent will find nothing new — the automatic metadata already covers everything. But running it once before release is a good safety net, especially for applications with complex library dependencies.
116+
117+
## Cleaning up manual metadata
118+
119+
If you accumulated manual entries in your `reachability-metadata.json` that are now covered by the automatic metadata levels, you can clean them up:
120+
121+
```bash
122+
./gradlew cleanupGraalvmMetadata
123+
```
124+
125+
This task compares your manual entries against the combined baseline (L1 + L2 + L3 + L4) and removes any that are already covered. It reports what was removed and what remains, so you can verify the cleanup is safe.

docs/graalvm/configuration.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Configuration
2+
3+
## Gradle DSL
4+
5+
```kotlin
6+
nucleus.application {
7+
mainClass = "com.example.MainKt"
8+
9+
graalvm {
10+
isEnabled = true
11+
imageName = "my-app"
12+
13+
// Gradle Java Toolchain: auto-downloads Liberica NIK 25
14+
// if it's not already installed on the machine.
15+
// In CI, the JDK is set up by graalvm/setup-graalvm@v1 instead.
16+
javaLanguageVersion = 25
17+
jvmVendor = JvmVendorSpec.BELLSOFT
18+
19+
buildArgs.addAll(
20+
"-H:+AddAllCharsets",
21+
"-Djava.awt.headless=false",
22+
"-Os",
23+
"-H:-IncludeMethodData",
24+
)
25+
26+
// Optional: customize Oracle Reachability Metadata Repository
27+
metadataRepository {
28+
enabled = true // default
29+
version = "0.10.6" // default
30+
excludedModules.add("com.example:my-lib")
31+
}
32+
33+
// Optional: point to your own app-specific metadata
34+
nativeImageConfigBaseDir.set(
35+
layout.projectDirectory.dir("src/main/graalvm-config"),
36+
)
37+
}
38+
}
39+
```
40+
41+
!!! info "About `nativeImageConfigBaseDir`"
42+
Nucleus ships all generic and platform-specific reflection metadata automatically. The `nativeImageConfigBaseDir` is only needed if you have app-specific entries that the automatic metadata doesn't cover — which is rare.
43+
44+
## DSL Reference
45+
46+
| Property | Type | Default | Description |
47+
|----------|------|---------|-------------|
48+
| `isEnabled` | `Boolean` | `false` | Enable GraalVM native compilation |
49+
| `javaLanguageVersion` | `Int` | `25` | Gradle toolchain language version — triggers auto-download of the matching JDK if not installed locally |
50+
| `jvmVendor` | `JvmVendorSpec` || Gradle toolchain vendor filter — set to `BELLSOFT` to auto-provision Liberica NIK |
51+
| `imageName` | `String` | project name | Output executable name |
52+
| `march` | `String` | `"native"` | CPU architecture target (`native` for current CPU, `compatibility` for broad compatibility) |
53+
| `buildArgs` | `ListProperty<String>` | empty | Extra arguments passed to `native-image` |
54+
| `nativeImageConfigBaseDir` | `DirectoryProperty` || Directory containing app-specific `reachability-metadata.json` (optional — generic/platform metadata is built-in) |
55+
| `metadataRepository` | `MetadataRepositorySettings` | enabled | Oracle GraalVM Reachability Metadata Repository settings (see below) |
56+
57+
### `metadataRepository` DSL Reference
58+
59+
| Property | Type | Default | Description |
60+
|----------|------|---------|-------------|
61+
| `enabled` | `Boolean` | `true` | Whether to auto-resolve metadata from the Oracle repository for classpath dependencies |
62+
| `version` | `String` | `"0.10.6"` | Version of the metadata repository artifact |
63+
| `excludedModules` | `SetProperty<String>` | empty | Module coordinates (`group:artifact`) to exclude from repository resolution |
64+
| `moduleToConfigVersion` | `MapProperty<String, String>` | empty | Override the metadata version for specific modules (key: `group:artifact`, value: version directory) |
65+
66+
## Recommended build arguments
67+
68+
| Argument | Purpose |
69+
|----------|---------|
70+
| `-H:+AddAllCharsets` | Include all character sets (required for text I/O) |
71+
| `-Djava.awt.headless=false` | Enable GUI support (mandatory for desktop apps) |
72+
| `-Os` | Optimize for binary size |
73+
| `-H:-IncludeMethodData` | Reduce binary size by excluding method metadata |
74+
75+
## No Release Build Type
76+
77+
Unlike standard JVM builds, GraalVM native-image builds **do not have a release variant**. There is no `packageReleaseGraalvmNative` or `runReleaseGraalvmNative` task. This is intentional:
78+
79+
- **ProGuard is redundant** — GraalVM native-image already performs closed-world dead code elimination at compile time. Running ProGuard beforehand provides no additional size benefit.
80+
- **ProGuard is harmful** — ProGuard can rename or remove classes that are referenced in `reachability-metadata.json`, causing runtime crashes. Maintaining both ProGuard keep rules and reflection metadata is error-prone.
81+
82+
All GraalVM tasks use the default (non-ProGuard) build type. Use `-Os` in `buildArgs` for size optimization.

docs/graalvm/index.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# GraalVM Native Image
2+
3+
!!! danger "Experimental — for advanced developers only"
4+
GraalVM Native Image compilation for Compose Desktop is **highly experimental**. If reflection is not fully resolved at build time, the application **will crash at runtime**. This mode requires significant effort to configure and debug. Proceed only if you are comfortable with native-image internals.
5+
6+
## Why Native Image?
7+
8+
For most Compose Desktop applications, [AOT Cache (Leyden)](../runtime/aot-cache.md) is the recommended way to improve startup. It's simple to set up and provides a major boost. But there are cases where even Leyden isn't enough:
9+
10+
- **Background services / system tray apps** — a lightweight app that mostly sits idle in the background will consume **300–400 MB of RAM** on a JVM, versus **100–150 MB** as a native image. For an app that's always running, this matters.
11+
- **Instant-launch expectations** — Leyden brings cold boot down to ~1.5 s, but a native image starts in ~0.5 s. For utilities, launchers, or CLI-like tools where every millisecond counts, native image is the way to go.
12+
- **Bundle size** — no bundled JRE means a much smaller distributable.
13+
14+
GraalVM Native Image compiles your entire application **ahead of time** into a standalone native binary that feels truly native to the OS.
15+
16+
## Trade-offs
17+
18+
Native image is not a free lunch. In addition to significantly more complex configuration (reflection, see below), there is a real **CPU throughput penalty**: the JVM's JIT compiler optimizes hot loops and polymorphic calls at runtime far better than AOT compilation can. For CPU-intensive workloads (heavy computation, real-time rendering, large data processing), a JVM with Leyden AOT cache will outperform a native image in sustained throughput.
19+
20+
| | JVM + Leyden | Native Image |
21+
|---|---|---|
22+
| Cold boot | ~1.5 s | ~0.5 s |
23+
| RAM (idle) | 300–400 MB | 100–150 MB |
24+
| CPU throughput | Excellent (JIT) | Lower (no JIT) |
25+
| Bundle size | Larger (includes JRE) | Smaller |
26+
| Configuration | Simple (`enableAotCache = true`) | Simplified (centralized metadata) |
27+
| Stability | Stable | Experimental |
28+
29+
**Choose native image when** startup speed and memory footprint are critical and CPU throughput is secondary. **Choose Leyden when** you want the best balance of performance, simplicity, and stability.
30+
31+
## Requirements
32+
33+
### BellSoft Liberica NIK 25 (Full)
34+
35+
GraalVM Native Image compilation **requires [BellSoft Liberica NIK 25](https://bell-sw.com/liberica-native-image-kit/)** (full distribution, not lite). This is the only supported distribution — standard GraalVM CE does not include the AWT/Swing support needed for desktop GUI applications.
36+
37+
!!! failure "Will not work with other distributions"
38+
Using Oracle GraalVM, GraalVM CE, or Liberica NIK Lite will fail. Desktop GUI applications require the **full** Liberica NIK distribution which includes AWT and Swing native-image support.
39+
40+
### Platform toolchains
41+
42+
| Platform | Required |
43+
|----------|----------|
44+
| **macOS** | Xcode Command Line Tools (Xcode 26 for macOS 26 appearance) |
45+
| **Windows** | MSVC (Visual Studio Build Tools) — `ilammy/msvc-dev-cmd` in CI |
46+
| **Linux** | GCC, `patchelf`, `xvfb` (for headless compilation) |
47+
48+
## When to avoid native image
49+
50+
Some libraries and use cases make native image compilation **extremely difficult or impractical**. Nucleus can handle most standard Compose Desktop dependencies automatically, but the following categories will likely require extensive manual configuration — or may not work at all:
51+
52+
!!! danger "Libraries that are very hard to support"
53+
54+
- **Heavy JNA users** — Libraries that rely extensively on JNA (Java Native Access) for dynamic function calls. JNA's runtime proxy generation is fundamentally at odds with native-image's closed-world assumption. Examples: some system tray libraries, platform bridge libraries.
55+
- **Full-text search engines** — Apache Lucene, Elasticsearch client, and similar libraries use heavy reflection, dynamic class loading, custom classloaders, and `MethodHandle`-based access patterns that are nearly impossible to capture statically.
56+
- **Dynamic scripting engines** — Embedding Groovy, JRuby, Nashorn, or other scripting runtimes that rely on runtime code generation.
57+
- **Annotation-processing frameworks at runtime** — Libraries like Spring that scan classpath annotations and create proxies at runtime. (Compile-time DI frameworks like Koin or manual DI are fine.)
58+
- **OSGi or custom classloaders** — Any library that loads classes through non-standard classloaders will bypass native-image's static analysis entirely.
59+
- **Byte-code generation at runtime** — Libraries using ByteBuddy, cglib, or ASM to generate classes at runtime (e.g., mocking frameworks, some ORM lazy-loading proxies).
60+
61+
If your application depends on libraries in these categories, **prefer AOT Cache (Leyden)** instead — it provides significant startup improvement with zero configuration overhead and full compatibility.
62+
63+
For everything else — ktor, kotlinx.serialization, Coil, SQLite, Jewel, Compose Multiplatform resources, SLF4J, and most idiomatic Kotlin libraries — Nucleus handles native image transparently.
64+
65+
## Next steps
66+
67+
- [Configuration](configuration.md) — Gradle DSL and build arguments
68+
- [Automatic Metadata](automatic-metadata.md) — How Nucleus resolves reflection metadata transparently
69+
- [Runtime Bootstrap](runtime-bootstrap.md)`graalvm-runtime` module, initializer, font fixes, resource inclusion
70+
- [Tasks & CI/CD](tasks-ci.md) — Gradle tasks, output locations, CI workflows, debugging

0 commit comments

Comments
 (0)