ADR 0002: Cargo builds the embedded dashboard
Status: accepted, 2026-10-01. Has a planned follow-up: see Next step: artifact dependencies.
Context
The dashboard is a Leptos client-side app compiled to WebAssembly and embedded
in the daemon by apps/piqueld/build.rs. Building it used to need four tools
outside Cargo:
- Trunk, to run the wasm build, wasm-bindgen, and wasm-opt, and to rewrite
index.html; - the wasm-bindgen CLI, which must exactly match the
wasm-bindgencrate version, so Nix and CI each pinned it separately; - binaryen’s
wasm-opt; - Tailwind.
Each tool needed its own pin in Nix, CI, and the docs, and each could break on
its own. In October 2026, Trunk 0.21 stopped compiling with GCC 16, both via
cargo install and in nixpkgs. It vendors a C library that uses a removed
compiler option.
Tailwind contributed almost nothing. None of the dashboard’s 97 classes is a Tailwind utility. Our stylesheets passed through it unchanged, so its only output was its preflight reset.
Decision
The daemon build script builds the bundle with Cargo alone (see
apps/piqueld/build/dashboard.rs):
- Compile. A nested Cargo compiles
piqueld-uiforwasm32-unknown-unknownwith thedashboardprofile (opt-level = "z", one codegen unit). It uses a sibling target directory, so it can’t deadlock on the outer build’s lock. - Bind.
wasm-bindgen-cli-support, the library behind the CLI, generates the bindings in-process. It’s pinned next towasm-bindgen, so Cargo enforces the version match. - Style. The stylesheets in
apps/piqueld-ui/styles/are concatenated in cascade order.reset.cssis the Tailwind preflight, vendored. - Shell. The script writes a one-line loader module, fills the
placeholders in
index.htmlwith content-hashed names, and embeds every file.
The shell has no inline scripts, so the Content-Security-Policy is a constant.
The only prerequisite is the wasm32 target, which rust-toolchain.toml
installs. The Nix packages build the module separately and pass it in through
PIQUELD_DASHBOARD_WASM, which skips step 1.
Measured on the dashboard at the time of the change:
| Build | wasm | gzipped |
|---|---|---|
Trunk, release profile + wasm-opt -Oz | 5,031 KB | 1,567 KB |
dashboard profile, no wasm-opt | 2,897 KB | 793 KB |
dashboard profile + wasm-opt -Oz | 2,497 KB | 903 KB |
wasm-opt made the gzipped module larger, so it was dropped rather than replaced. Screenshots of every dashboard page, at desktop and mobile widths, matched pixel for pixel before and after, apart from live timestamps.
Alternatives considered:
- Keep Trunk. This keeps the four external pins and their breakage.
- The
wasm-optcrate. It compiles binaryen’s C++, and it doesn’t help gzipped size here. - Server rendering without WebAssembly (Topcoat). Its runtime needs
'unsafe-eval', and the UI would run inside the daemon instead of using the public API.
Next step: artifact dependencies
The remaining workaround is the nested Cargo invocation. Cargo can’t yet
declare “build this crate for another target” as a dependency.
Artifact dependencies
(RFC 3028, -Z bindeps) will. Once they reach stable Cargo, replace the nested
build with a build dependency along these lines:
# apps/piqueld/Cargo.toml
[build-dependencies]
piqueld-ui = { path = "../piqueld-ui", artifact = "bin", target = "wasm32-unknown-unknown", optional = true }
The build script then reads the module path from Cargo’s
CARGO_BIN_FILE_PIQUELD_UI_piqueld-ui environment variable. That deletes:
- the nested build in
Dashboard::compile, andNestedCargo(apps/piqueld/build/nested.rs) with its environment scrubbing, once the health probe’s musl build, which shares it, moves too; - the
target-uiandtarget-probedirectories; - the manual
rerun-if-changedtracking, because Cargo tracks the dependency itself.
When doing this, check how the stabilised feature selects the profile for
artifact dependencies. The size settings in [profile.dashboard] may need to
become a [profile.*.package.piqueld-ui] override.
Revisit this when issue #9096 closes, or when bindeps is listed as stable in
the Cargo changelog.