compose-rust guide

Getting started

Adding the crate, and building this repository

Using the crate is one line in a Cargo.toml, because the build downloads the renderer it needs. Working on compose-rust itself is the longer path further down.

What 0.0.1 gives you

0.0.1 contains the boundary, the protocol, the schema, the Runtime trait and the renderer download. It does not contain the authoring API yet, so there is no launch(app) to call and no screen to write. That arrives with 1.0.0 on 2026-10-20 (#23, #64). Until then the crate serves a layer that builds the tree by implementing Runtime.

Adding it to a project

The renderer is far too large for crates.io, so each release carries it as a checksummed artifact. Fetching it is the build script's job, not yours.

shell
cargo add compose-rust@0.0.1
Cargo.toml, the line that command writes
[dependencies]
compose-rust = "0.0.1"

cargo build works out which renderer the target needs. It downloads the release artifact for the crate's exact version, checks it against the published .sha256, unpacks it into a cache outside target/, and links it. There is no environment variable to set and no script to run. The cache is keyed by version and target, so it survives cargo clean and is shared between projects.

VariableEffect
COMPOSE_RUST_RENDERER_DIRUse the renderer in this directory. It is checked first, and nothing is downloaded when it is set. A renderer you built yourself, a vendored copy and an offline build all work through it.
COMPOSE_RUST_CACHE_DIRMoves the cache off $HOME/.cache/compose-rust (%LOCALAPPDATA%\compose-rust on Windows).

A build with no network says which files to put where. Putting them there is all it takes.

Building without a renderer

default-features = false builds with no renderer at all, for a headless or documentation build. A binary built that way says what is missing and exits non-zero. It does not open no window and return 0, because a program that drew nothing has to be distinguishable from one that finished its work. The mock-renderer feature drives the boundary with no renderer on purpose, for tests and for embedding the Host in something that draws for itself.

What you can build on today

The crate's public surface in 0.0.1 is the part every authoring layer shares. A layer implements Runtime: it writes the tree's records into a Batch on rebuild, writes what changed on render, and runs a handler in handle_event, returning the value the boundary hands back. It then registers itself with launch_runtime or LaunchBuilder. Everything else, the entry points, the batch, the theme, assets, messages and frame requests, belongs to the Host and behaves the same whatever built the tree.

The Theme, DesignSystem and Palette values on Design systems compile today. The full API reference is on docs.rs.

Working on this repository

Everything below is for working on compose-rust itself, which needs the renderer toolchain as well. Start with the setup check. It checks every tool the build needs, names what is missing, and prints the command that fixes it.

shell
./scripts/setup-check.sh   # every tool the build needs, with the fix for anything missing
./scripts/check.sh         # fmt, clippy, tests, quick benchmarks, Kotlin build and tests

The workspace targets Rust 1.85+ (edition 2024), and rustfmt and clippy are required. Kotlin needs nothing installed: renderer/kotlin (kotlin.bat on Windows) downloads the pinned toolchain on first use. ./scripts/check.sh --no-kotlin skips the Kotlin gate, and --full runs the full benchmark sample.

Build the renderer

The renderer uses a pinned commit of thisisthepy/compose-multiplatform-core-extended. It is a Compose fork that adds what the published build lacks: the Linux targets, the text selection menu and the copy keys. Build the fork once, then the renderer.

shell
# macOS
./renderer/scripts/build-compose.sh
cd renderer && ./desktop/scripts/build-macos.sh --release      # build/macos/<target>/

# Linux
./renderer/scripts/build-compose.sh --target linuxX64
cd renderer && ./desktop/scripts/build-linux.sh --release      # build/linux/<target>/

Each produces one static library, libcompose_rust_renderer.a, with Compose, Skia and the interpreter inside it. The Rust build links it when DXC_MACOS_NATIVE_LIB or DXC_LINUX_NATIVE_LIB names its directory. Without one of those, a checkout links the GraalVM native image from renderer/build/native-image/dist/lib, which is what 0.0.1 ships.

Windows still builds the GraalVM native image, with renderer/desktop/scripts/build-native-windows.ps1. The native image on macOS needs Liberica NIK 25 Full, because upstream GraalVM skips AWT on Darwin (oracle/graal#13272). ./scripts/install-nik.sh installs it. See Troubleshooting for how a wrong JDK shows up.

The first build is slow, because Compose and Skia are compiled ahead of time. You repeat it only when the Kotlin side changes; ordinary Rust work reuses the library.

Smoke-test the library

Before involving Rust, check that the native-image library alone can open a window. The smoke test links a small C host against it and calls compose_rust_renderer_run.

shell
./renderer/desktop/scripts/build-native.sh
./renderer/desktop/scripts/smoke-test.sh

A window should appear. Closing it makes run return 0, and the process exits normally.

The JVM development shell

The development shell is the fastest loop when you work on the renderer itself, with hot reload and @Preview. The JVM is allowed only here. Shipped artifacts never contain one.

shell
cd renderer && ./kotlin run -m desktop

Samples

The samples/ directory holds 12 samples, clones of real applications such as a calculator, a notepad, a task list and a chat client. They do not build today, because they are being rewritten on the authoring API Planned (#85). The rewritten set becomes the public samples (#2).

The generated code check

One test compares the checked-in Kotlin protocol file against what the Rust schema generates. If you change the schema and forget to regenerate, that test fails. That is the intended alarm: the schema is the single source, and a Kotlin side that was not updated should fail the build rather than misbehave at runtime. Regenerate with the codegen binary.

shell
cargo run -p compose-rust --bin codegen