Build profile
This documents how to build the self-contained release binary and how that "one file, no external runtime assets" property is validated. Story 0.4 lifts the two commands below directly into the CI release gate.
TL;DR
scripts/build-release.sh # produce target/release/server (assets embedded)
scripts/verify-self-contained.sh # prove it runs from an empty dir
Deploying to a Raspberry Pi? This page builds the native host binary. For the
aarch64(Pi 4/5) cross build — same two-step embed, run inside acrosscontainer — and the on-Pi run/verify procedure, seecross-compile.md.
Release build — and why it is two steps
The product ships as a single binary: the cargo-leptos site/ bundle (/pkg/hamradio.{wasm,js,css}, /favicon.ico) is folded into the server binary at compile time via rust-embed (see crates/server/src/assets.rs). This eliminates binary↔WASM skew (AR-2) and underpins the single-artifact signed-update story (4.5/4.6).
You must build with cargo leptos (not plain cargo build): cargo-leptos compiles the WASM frontend and writes the site/ bundle, and sets the LEPTOS_* environment the server reads. A plain cargo build produces neither.
But one cargo leptos build --release is not enough on its own. cargo-leptos 0.3.6 builds in this order within a single run:
erase target/site → sync favicon + compile CSS → compile `server` (rust-embed
embeds target/site HERE) → compile WASM → wasm-bindgen writes pkg/hamradio.{js,wasm}
Because rust-embed embeds at compile time and the WASM/JS are written after the server is compiled, a single run embeds a bundle missing the WASM/JS (CSS and favicon make it; the WASM does not). So we re-embed in a second step, once site/ is complete:
# 1. produce a complete target/site (the binary from this step has an incomplete embed)
cargo leptos build --release
# 2. force-rebuild the server so rust-embed re-reads the now-complete site/.
# The force-rebuild (cargo clean -p server) is REQUIRED: rust-embed is a
# proc-macro and cannot emit `cargo:rerun-if-changed`, so cargo would otherwise
# treat the unchanged server source as up-to-date and skip the re-embed.
# LEPTOS_OUTPUT_NAME must be set: it is baked at compile time (option_env!) and
# drives the /pkg/<name>.{js,wasm} hydration paths — without it the SSR shell
# references /pkg/.js and the app never hydrates.
cargo clean -p server --release
LEPTOS_OUTPUT_NAME=hamradio LEPTOS_SITE_PKG_DIR=pkg cargo build -p server --release
Self-contained validation (the empty-directory check)
scripts/verify-self-contained.sh copies only target/release/server into a fresh empty temp dir (no sibling site/, no target/), boots it, and asserts:
-
/pkg/hamradio.wasm→200 application/wasm -
/pkg/hamradio.js→200 text/javascript -
/pkg/hamradio.css→200 text/css -
/favicon.ico→200 image/x-icon -
/→200SSR HTML containing the hydration<script type="module">
A green run proves the binary carries all static assets internally. The unit test crates/server/src/assets.rs::tests::embed_is_non_empty is a cheaper compile-time guard against a silently-empty embed, but the empty-dir proof is the real acceptance check (it alone exercises the release embed; the unit test passes in debug builds, where rust-embed reads from disk).
Runtime config (site address/port via
LEPTOS_SITE_ADDRor an optionalCargo.toml) is not an asset — seecross-compile.mdfor how the binary is told its listen address on a Pi. The empty-dir proof passes with neither present; the binary falls back to leptos defaults.
Development
Use cargo leptos watch for the dev loop (serves http://127.0.0.1:3000 with live CSS/asset reload). In debug builds rust-embed reads assets from disk at runtime — we deliberately do not enable its debug-embed feature — so hot-reload keeps working. The single-binary embedding only takes effect in --release builds.
Continuous integration (CI gates)
CI lives in .gitlab-ci.yml at the repo root and runs on every merge request (origin is GitLab: git@gitlab.com:hamspirit/spacer.git). It is the per-MR quality pipeline — the release/cross-build/signing pipeline is Epic 4.
Every gate is a release blocker (no allow_failure); a red gate is a red pipeline:
| Gate | Command | Enforces |
|---|---|---|
|
|
house formatting |
|
|
lints + |
|
|
the |
|
|
unit/integration tests (incl. the embed guard) |
|
|
the empty-dir single-binary embed proof |
|
|
WCAG 2 A/AA on every route — paired with the manual |
Two ordering/caching constraints the pipeline encodes (and why):
-
cargo leptos buildruns before any plain-cargogate.crates/server/src/assets.rsembedstarget/sitevia a rust-embed derive that needs the folder to exist at compile time, so a clean-checkoutcargo clippy/test/buildofserverfails untilcargo leptos buildhas produced it. Thebuild:sitejob runs first and passestarget/siteto the cargo gates as an artifact. -
target/is never cached.build-release.sh'scargo clean -p server --releaseis load-bearing (rust-embed can’t emitcargo:rerun-if-changed); a cache restoringserver's release artifacts across that clean would ship a stale/WASM-less embed. Only the cargo registry and thecargo-leptosbinary are cached.
Run the same gates locally before pushing:
scripts/build-release.sh # produces target/site + the binary
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
scripts/check-feature-unification.sh
scripts/verify-self-contained.sh
( cd end2end && npm ci && npx playwright test ) # a11y (needs the release binary built)