Getting started
From a clean checkout to a window on screen
Today this path is macOS only. The renderer is compiled ahead of time into a shared library, and your Rust binary links against it, so the renderer has to be built before any Rust example will run.
Prerequisites
Before anything else, run ./scripts/setup-check.sh. It checks every tool the
build and the quality gate need, names what is missing, and prints the command that fixes
it. Everything in the table below is on its list.
| What | Why |
|---|---|
| macOS on Apple silicon or Intel | The native build script exits immediately on anything but Darwin. |
| Xcode command line tools | The build compiles a few C and Objective-C files and links the library. |
| A Rust toolchain (stable) | The Host crate. rustup is the usual way in. |
| Liberica NIK 25 Full | Builds the renderer with native-image. See the note below, an ordinary GraalVM will not do. |
Upstream GraalVM skips AWT support on Darwin (oracle/graal#13272, still open as of 2026-09). Compose Desktop draws through AWT, so an image built with upstream GraalVM has no window to show. Liberica NIK Full links AWT statically, which is why it is the required toolchain on macOS.
The build script looks for NIK in the standard install location and otherwise reads
GRAALVM_HOME:
# Auto-detected if installed here:
~/Library/Java/JavaVirtualMachines/bellsoft-liberica-vm-full-openjdk25*/Contents/Home
# Otherwise point at it explicitly:
export GRAALVM_HOME=/path/to/bellsoft-liberica-vm-full-openjdk25/Contents/Home
If you have no NIK at all, ./scripts/install-nik.sh installs it. The install is
addressed by version, so a warm cache makes a second run a no-op, the download happens once
per NIK version, not once per invocation. The build script itself refuses an installation
without lib/static/darwin-*/libawt_lwawt.a, which catches a stock GraalVM up
front instead of at the end of a long build.
Kotlin itself needs no separate install. The renderer project ships a self-bootstrapping
./kotlin wrapper that downloads the pinned Kotlin Toolchain on first use.
Build the renderer
One script does the whole thing: it runs the renderer briefly on the NIK JVM to capture the exact runtime classpath, compiles the C entry points, and then builds the shared library.
./dioxus-compose-renderer/desktop/scripts/build-native.sh
The result is a self-contained runtime directory:
libdioxus_compose_renderer.dylib # the renderer: AWT, Skiko JNI, Compose, our code
libskiko-macos-<arch>.dylib # Skia, which Skiko loads by path
libjawt.dylib # forwards JAWT_GetAWT into the renderer
libawt_lwawt.dylib # placeholder that libawt loads by path
The three small libraries alongside the renderer are not padding. Statically linked AWT on
macOS still looks for some things by file path at runtime, and these fill those slots.
Architecture explains the detail; the short version is
that the whole deployment layout is one lib/ directory whose parent the renderer
reports as java.home.
If you keep a renderer somewhere else, a downloaded artifact, a vendored copy, point
DIOXUS_COMPOSE_RENDERER_DIR at its lib directory and the Rust build
will use that instead of the workspace one (NFR-10).
The first build is slow, native-image compiles Compose, Skiko and AWT ahead of
time. You only repeat it when the Kotlin side changes; normal Rust work reuses the library.
Smoke-test the library
Before involving Rust, check that the library alone can open a window. This links a tiny C
host against it and calls dioxus_compose_renderer_run:
./dioxus-compose-renderer/desktop/scripts/smoke-test.sh
A window should appear. Closing it makes run return 0 and the
process exit normally. That is the acceptance criterion for the macOS runtime requirement
(SPEC PR-8), and it passed on 2026-09-20.
Run the demo
The desktop example is behind the native-renderer feature, because that feature
is what makes the Rust build link against the library you just produced:
cargo run -p dioxus-compose --features native-renderer --example desktop_demo
Type a message and press Enter to send it, or Shift+Enter for a newline. If you use a Korean, Japanese or Chinese IME, press Enter while a syllable is still composing: it should commit the composition and not send the message. That distinction is the point of the whole design and is covered in Writing UI.
Building without native-renderer gives you a Host with a no-op renderer:
launch returns immediately and no window appears. That build is what the unit
tests and benchmarks use, so it is useful, just not something you can look at.
Run the checks
The repository's quality gates are one script: formatting, Clippy with warnings denied, the test suite, the benchmarks in Criterion's quick mode, and the Kotlin build and tests.
./scripts/check.sh # quick benchmark sampling, for presubmit
./scripts/check.sh --full # full benchmark sampling
./scripts/check.sh --no-kotlin # Rust only; DXC_SKIP_KOTLIN=1 does the same
One of those tests compares the checked-in Kotlin protocol file against what the Rust schema generates. If you change the schema and forget to regenerate, the test fails, which is the intended behaviour, not an inconvenience.