20 KiB
Nucleus
A multi-module Gradle plugin and runtime library toolkit for shipping production-ready JVM desktop applications on macOS, Windows, and Linux.
Published releases are 2.4.x (latest tag v2.4.4). Do not treat IDEAL_API.md as current — that file is gone; the real entry point is nucleusApplication(args) { } in nucleus-application. Plugin-injected strings are NucleusApp, not a generated NucleusGenerated object.
Project Structure
nucleus-application-nucleusApplication, backend-agnosticDecoratedWindow/HostedWindow,onDeepLink,aotTrainingcore-runtime- Executable type detection, single instance, deep links, platform detection, app metadata (NucleusApp)aot-runtime- AOT cache mode detection for JDK 25+ (Project Leyden)updater-runtime- Auto-update engine (GitHub/S3), SHA-512, delta/blockmap, progress, update level, post-update eventsfreedesktop-icons- Type-safe freedesktop Icon Naming Specification constants (shared by notification-linux and launcher-linux)sf-symbols- Type-safe SF Symbols catalognotification-common- Cross-platform notification DSL with per-platform option blocksnotification-macos- macOS User Notificationsnotification-linux- Freedesktop Desktop Notifications API via JNI (D-Bus org.freedesktop.Notifications)notification-windows- Windows Toast Notifications API via JNI (WinRT)launcher-macos- macOS Dock API — badge, menuslauncher-windows- Windows Launcher API via JNI (WinRT/COM) — badge notifications, jump lists (ICustomDestinationList), overlay icons, and thumbnail toolbar buttons (ITaskbarList3) on taskbarlauncher-linux- Unity Launcher API via JNI (badge, progress, urgency, quicklist via com.canonical.Unity.LauncherEntry + com.canonical.dbusmenu)menu-macos- Native macOS menu barmedia-control- OS media controls — MPRIS (Linux), Now Playing (macOS), SMTC (Windows)global-hotkey- System-wide keyboard shortcutstaskbar-progress- Native taskbar/dock progress bar and attention requests (Windows ITaskbarList3, macOS NSDockTile, Linux delegates to launcher-linux)taskbar-progress-tao- Taskbar progress on the Tao backenddarkmode-detector- Reactive OS dark mode detection via JNIsystem-color- Reactive system accent color and high contrast detection via JNIsystem-info- CPU, memory, GPU, temperature, network, processesenergy-manager- Energy efficiency & screen-awake APIsautolaunch- Start at login (Win32/MSIX/SMAppService/systemd/Flatpak portal)scheduler/scheduler-testing- OS-scheduled background tasks (Task Scheduler / launchd / systemd) + test doublesfs-watcher- Native filesystem watcherservice-management-macos- macOSSMAppService— login items, launch agents, daemonsnative-ssl/native-http/native-http-okhttp/native-http-ktor- OS trust store integrationlinux-hidpi- Native HiDPI scale detection on Linuxgraalvm-runtime- GraalVM native-image bootstrapdecorated-window-core- Shared types, layout, styling (design-system agnostic)decorated-window-tao- Default/recommended backend — no-AWT window shell over the Rusttaocrate via JNI (Metal on macOS, EGL on Linux, ANGLE/GLES on Windows), single native event-loop thread asDispatchers.Maindecorated-window-awt- AWT chrome shared by the JBR/JNI backendsdecorated-window-jbr- JBR-based implementation (requires JetBrains Runtime) — legacy/maintenance-onlydecorated-window-jni- JNI-based implementation (any JVM, GraalVM compatible) — legacy/maintenance-onlydecorated-window-jewel- Jewel (IntelliJ theme) integrationdecorated-window-material2- Material 2 color mappingdecorated-window-material3- Material 3 color mappingplugin-build/plugin- Gradle plugin for packaging & distributionbuildSrc- Build-only convention plugins (nucleus.native-module: the sharedbuildNative*wiring for every JNI module)examples/- Demo & sample applications:nucleus-demo(flagship),compose-demo,tao-demo,swing-tao-demo,jni-demo,jewel-demo,cmp-demo(KMP),window-scaffold-demo,zstd-demo,scheduler-demo,service-management-demo,system-info-demo,fs-watcher-smoke,orphan-reflect-smoke,extra-launcher-demo,tao-native-test(GraalVM + SLF4J fixture),benchmark-demo(JIT-vs-GraalVM-O3, ports underports/),gstreamer-demo/mediafoundation-demo/avfoundation-demo(platform video into aTextureView), plusshared(Compose helper used by tao/jni demos).native-proxyandspellcheckdirectories on disk are not onmain— ignore them unless the matching feature branch is checked out.
Build & Run
./gradlew :examples:nucleus-demo:run # Run the flagship demo app
./gradlew packageDistributionForCurrentOS # Package for current OS
./gradlew packageReleaseDistributionForCurrentOS # Release build with ProGuard
./gradlew preMerge # Full CI verification
./gradlew reformatAll # Format all code
Key Technologies
- Kotlin 2.4 with Compose Desktop 1.11
- JNI for all native interop (no JNA in runtime modules)
- JBR (JetBrains Runtime) API for decorated-window-jbr
- Gradle 9.4 with version catalog (
gradle/libs.versions.toml) - Detekt + KtLint for code quality
Development Notes
- Target: JDK 17+ runtime, JDK 25+ recommended for AOT
- JNI code: be careful with macOS ARC/retain and weak references
- Native modules use platform-specific JNI implementations — test on each OS
- Plugin is published via included build in
plugin-build/ - Version catalog is the source of truth for all dependency versions
- Public API freeze: root
build.gradle.ktsapplies kotlinx binary-compatibility-validator +explicitApi()to every non-example module. Baselines live in<module>/api/<module>.api. After intentional public API changes run./gradlew apiDumpand commit the dump;apiCheck(wired intocheck/preMerge) fails on accidental ABI drift. Exception:decorated-window-jewel(JVM 25) is ignored by BCV until ASM supports class-file 69 — still usesexplicitApi(). Helper:scripts/fix-explicit-api.pyfor mechanical visibility/return-type fixes from kotlinc diagnostics. - KDoc on public API:
UndocumentedPublicClass/UndocumentedPublicFunctionare enforced by detekt (detektis wired intocheck/preMerge). Pre-existing gaps are grandfathered in per-module<module>/detekt-baseline.xmlfiles — any new undocumented public class or function fails the build. Do not regenerate a baseline to silence a new finding; write the KDoc.UndocumentedPublicPropertystays off because the generated icon/symbol catalogs (sf-symbols,freedesktop-icons) would swamp it - Logging:
java.util.loggingis the single facade for every runtime module — no SLF4J dependency forced on consumers, no rawprintln/System.errinsrc/main. Logger names must be the fully-qualified class name (or an explicitdev.nucleusframework.*string) so the whole framework sits under one JUL namespace.allowNucleusRuntimeLogging = trueis an opt-in convenience that raises thedev.nucleusframeworklogger tonucleusLoggingLeveland attaches a colored console handler; apps that configure JUL themselves (logging.properties,jul-to-slf4j) leave itfalseand Nucleus never touches the JUL configuration decorated-window-taois the recommended backend for new projects (no AWT, native event-loop-driven, true Windows fullscreen, GraalVM native-image first-class).decorated-window-jnianddecorated-window-jbr(the AWT-based backends) are legacy/maintenance-only- macOS trackpad on Tao (#652–#654): scroll deltas are AWT-shaped (
preciseWheelRotation, no display scale). Trackpad gestures reach Compose asPanStart/PanMove/PanEnd(panOffset= AWT delta × 10 dp), wheel notches asScroll; foundation'sModifier.scrollablehandles both. Custom handlers that only listen forPointerEventType.Scrollmust also handle Pan, or the app can set-Dnucleus.tao.trackpadPanEvents=falseto get AWT-styleScrollfor everything. Everything scroll-related enters the scene throughTaoSceneScrollRouter(window + NSPanel popups); the phase wire (RustSCROLL_GESTURE_*,popup_panel.m,TaoScrollGesturePhase) is guarded byTaoScrollWireDriftTest - macOS Liquid Glass enabled by default via
macOsSdkVersion = "26.0"(vtool SDK patching) - The HotSpot GC is selected type-safely with
application { garbageCollector = GarbageCollector.Z }(unset = JVM ergonomics). The flags are prepended to the launcher.cfgjava-options and to theruntask — beforejvmArgs, so an explicit-XX:+Use…GCthere still wins — and the AOT training run inherits them from the.cfg
Adding a Native JNI Module
When creating a new module with platform-specific JNI libraries, all steps below are required:
- Native source —
<module>/src/main/native/{linux,macos,windows}/withbuild.sh/build.bat+ C/ObjC source. Library name:nucleus_<feature>. Linux: preferdlopenover hard compile-time deps. Rust modules keep the crate atsrc/main/native/(Cargo.toml+src/) and only the launcher script under the per-OS directory. - Build output — scripts must place binaries in
<module>/src/main/resources/nucleus/native/{linux-x64,linux-aarch64,darwin-x64,darwin-aarch64,win32-x64,win32-aarch64}/. - Gradle wiring — apply
id("nucleus.native-module")and declare one line per platform. That is the whole build-script surface: the convention plugin (buildSrc/src/main/kotlin/dev/nucleusframework/gradle/NativeModulePlugin.kt) registersbuildNative{Windows,MacOs,Linux}with the source inputs, the resources output, the host-OS and prebuilt-on-CI guards, the absolute build-script path, theNativeLibraryLoadercache eviction, and theprocessResources/sourcesJardependencies.nucleusNative { macos("nucleus_feature") // → libnucleus_feature.dylib linux("nucleus_feature") // → libnucleus_feature.so windows("nucleus_feature") // → nucleus_feature.dll } - Kotlin JNI bridge —
internal objectusingNativeLibraryLoader.load()with@JvmStatic externalmethods. Always provide a Kotlin fallback when native lib is unavailable. - GraalVM reachability metadata — create
<module>/src/main/resources/META-INF/native-image/dev.nucleusframework/nucleus.<module>/reachability-metadata.jsondeclaring all JNI-accessible classes/methods. Without this, native-image silently eliminates the bridge. - CI build (
build-natives.yaml) — add one build step per platform job, gated withif: steps.natives-cache.outputs.cache-hit != 'true', plus the library entries in that platform'sVerify ... nativesFILES list. Native outputs are cached keyed onhashFiles('**/src/main/native/**', ...), so the new sources invalidate the cache automatically. Each platform job publishes a single merged artifact (natives-windows,natives-macos,natives-linux-{x64,aarch64}); consumer workflows fetch them all with onepattern: 'natives-*'download step and need no changes for a new module. - CI verify lists — add the 6 arch paths to the EXPECTED arrays of the "Verify all natives present" steps in
pre-merge.yamlandpublish-maven.yaml.
Common pitfalls: forgetting Linux .so in verify lists, missing reachability-metadata.json, forgetting the cache-hit guard on new build steps in build-natives.yaml.
Existing build.sh/build.bat scripts also clear the NativeLibraryLoader cache themselves so a bare ./build.sh (outside Gradle) is safe; new scripts don't have to, since nucleus.native-module does it after every run.
Publishing to Maven Local
Version is resolved from GITHUB_REF in every build.gradle.kts (refs/tags/v2.4.4 → 2.4.4). Without it, defaults to 1.0.0.
Prerequisites:
- Use JDK 21 (
JAVA_HOME=/usr/lib/jvm/java-1.21.0-openjdk-amd64) — Kotlin DSL script compiler crashes on JDK 25 - Use
--no-configuration-cache— configuration cache can serve a stale cached version - Use absolute
-ppaths to avoid working-directory confusion - No signing needed for local publish (signing is conditional on
signingInMemoryKeyproperty)
Runtime libraries (main project):
GITHUB_REF=refs/tags/v2.4.4 JAVA_HOME=/usr/lib/jvm/java-1.21.0-openjdk-amd64 \
./gradlew -p /absolute/path/to/Nucleus publishToMavenLocal --no-configuration-cache
Plugin (plugin-build):
GITHUB_REF=refs/tags/v2.4.4 JAVA_HOME=/usr/lib/jvm/java-1.21.0-openjdk-amd64 \
./gradlew -p /absolute/path/to/Nucleus/plugin-build :plugin:publishToMavenLocal --no-configuration-cache
Published tags are v2.4.x. The v prefix is stripped for the Maven version.
GraalVM Native Image
- Reflection metadata is centralized in 3 levels — users no longer copy hundreds of entries:
- L1: Generic cross-platform metadata shipped in
graalvm-runtimeJAR (reachability-metadata.jsonwith ~300+ types) - L2: Oracle GraalVM Reachability Metadata Repository — auto-resolved for classpath deps (enabled by default,
metadataRepository {}DSL) - L3: Platform-specific metadata (macOS/Windows/Linux) shipped inside the plugin JAR under
nucleus/graalvm/platform-metadata/
- L1: Generic cross-platform metadata shipped in
- GraalVM Deb/Rpm/Pacman packages honor
linux { afterInstall / afterRemove / beforeInstall / beforeRemove }the same as JVM jpackage/electron-builder. User scripts are concatenated after Nucleus templates. electron-builder substitutes${sanitizedProductName}and${executable}only when those tokens are single-quoted ('${sanitizedProductName}-daemon.service'); double-quoted"${sanitizedProductName}"is left unsubstituted and systemd hooks silently no-op. Pacman.INSTALLpre_removedoes not get deb-style$1=upgrade, so stop/disable the unit unconditionally and let after-install re-enable on upgrade. graalvm { headless = true }is for daemons/CLIs: skips L3 AWT/Java2D platform metadata, skips always-on L1 packs (jdk-awt,jdk-fonts,jdk-graphics2d, Skiko/Compose/tray), skips copying companion GUI native libs (libawt,libfontmanager, Skiko, …), and bakes-Djava.awt.headless=true. Defaultfalse(GUI). Without this, JNI registration of AWT types makesnative-imagepulllibawt/libawt_xawteven when app code never referencesjava.awt.graalvm-runtimeauto-includes.svg,.ttf,.otf,composeResources/*,nucleus/native/*, andMETA-INF/services/*viareachability-metadata.jsonresource globs (the deprecated-H:IncludeResourcesoption was dropped). The blanket**/*.{svg,ttf,otf}globs are a required catch-all for fonts/icons bundled inside library JARs (e.g. Jewel SVG icons) — those are not the app's own resources soautoIncludeResourcesdoesn't cover them. They knowingly trigger native-image's advisory "pattern too generic" warning; do not remove them (it breaks Jewel icons in native image)- The tracing agent (
runWithNativeAgent) is only needed for app-specific reflection, uncommon libraries, and resource bundles - PGO (Oracle GraalVM):
runWithPgoInstrumentbuilds + runs an instrumented image and recordsgraalvm/pgo/default.iprofon exit; later native-image builds apply the profile automatically. Opt out with-Pnucleus.graalvm.pgo=off; customize viagraalvm { pgo { enabled / profile } } - Agent output is automatically deduplicated against library metadata on the classpath
- Sample apps have near-empty
reachability-metadata.json— only app-specific entries remain nucleusApplicationcallsGraalVmInitializer.initialize()first. If you write amainthat does not go throughnucleusApplication, callGraalVmInitializer.initialize()yourself before anything else (required for native-image). Do not tell users to sequence GraalVM / single-instance / autolaunch / AUMID by hand — that bootstrap is already insidenucleusApplication.Dispatchers.Mainon Tao is installed viaTaoMainDispatcherFactory(ServiceLoader), not a manualsetMain.- Font substitutions (
@TargetClass) ingraalvm-runtimefixInternalError: platform encoding not initializedon Windows/Linux - SLF4J is not initialized at build time — the API and the app-selected backend both initialize at run time, so the app keeps control of its provider, levels and environment-dependent config. Forcing
--initialize-at-build-time=org.slf4jfrom a shared module breaks any run-time-initialized backend (SLF4J 2.x provider discovery parks Logback'sLogbackMDCAdapter/LoggerContextin the image heap → build failure; adding backend classes one by one only exposes the next object). Apps with a fixed backend can opt in viagraalvm { buildArgs.add("--initialize-at-build-time=org.slf4j") }— it trades a frozen provider and build-machine-captured config for a cheaper first log call.examples/tao-native-testbundles Logback + anMDCround-trip as the regression fixture - GraalVM task surface mirrors the JVM one:
runGraalvmNativeis the fast dev loop (forces quick-build-Ob, ignoring the configuredoptimization), whilecreateGraalvmNativeDistributable/runGraalvmNativeDistributable/packageGraalvmNativeDistributionForCurrentOSbuild & run the full app folder with the configured optimization (mirrorcreateDistributable/runDistributable/packageDistributionForCurrentOS). Quick vs distributable is detected from the invoked task name and tracked as a compile input, so switching re-compiles - Native images bake a default max heap of 25% of RAM (
-R:MaximumHeapSizePercent=25, JVM/HotSpot parity) instead of native-image's Serial-GC default of 80%; configurable viagraalvm { maxHeapSizePercent = N }or an absolutegraalvm { maxHeapSize = "2g" }, and always overridable at runtime with-Xmx - The image's GC is baked at build time via
graalvm { garbageCollector = NativeImageGarbageCollector.G1 }(--gc=, unset = native-image's Serial GC).G1is Oracle GraalVM + Linux only and degrades to a warning plus the Serial GC anywhere else; the baked heap percentage follows the collector (-R:MaximumHeapSizePercentfor Serial/Epsilon,-R:MaxRAMPercentagefor G1, which does not know the former) - The GraalVM toolchain is auto-downloaded by default (
graalvm { toolchain { } }DSL), but only whengraalvm { isEnabled = true }and only when a native-image task actually runs — every provider is resolved indoFirst, so an IDE sync orgradlew tasksnever pulls a JDK. Cached under~/.gradle/nucleus/graalvm/. - Distribution defaults to GraalVM Community Edition (
toolchain { distribution }, GPLv2+CE, resolved from thegraalvm/graalvm-ce-buildsGitHub releases).GraalvmDistribution.ORACLEopts into Oracle GraalVM and logs a GFTC licensing warning — the GFTC forbids charging any fee associated with redistributing the Program, and the plugin ships GraalVM runtime libs (libjvm,libawt, …) next to the executable. In community mode the Oracle-onlyrunWithPgoInstrumenttask is not registered at all;-O3,--pgoand-H:AdvancedObfuscationdegrade to a warning.examples/benchmark-demoopts into ORACLE because-O3/PGO are its whole point. - Install dirs embed the distribution (
graalvm-community-jdk-*vsgraalvm-jdk-*), so a pre-existing Oracle download is never silently reused after the default flipped; aGRAALVM_HOMEwhose distribution disagrees with the DSL is ignored with a warning. The CI cache key includes the distribution too. - Channel/version: innovation by default (
25i3/ GraalVM 25.3.4.1),channel = GraalvmChannel.LTSor an explicitversion("25", "25.0.1"). On Intel macs (dropped by both distributions after 25.0.1) it falls back to Liberica NIK via the BellSoft API — only the JDK feature version carries over there (BellSoft ships the LTS line only, so Intel macs get NIK 25.0.x even on the innovation channel).toolchain { autoDownload = false }restores Gradle toolchain resolution viajavaLanguageVersion/jvmVendor.