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 a cross container — and the on-Pi run/verify procedure, see cross-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

scripts/build-release.sh wraps exactly these two steps. LEPTOS_OUTPUT_NAME mirrors name under in the root Cargo.toml.

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.wasm200 application/wasm

  • /pkg/hamradio.js200 text/javascript

  • /pkg/hamradio.css200 text/css

  • /favicon.ico200 image/x-icon

  • /200 SSR 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_ADDR or an optional Cargo.toml) is not an asset — see cross-compile.md for 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

fmt

cargo fmt --all --check

house formatting

clippy

cargo clippy --workspace --all-targets -- -D warnings

lints + unwrap/expect/panic denied on engine+server (non-test)

feature-unification

scripts/check-feature-unification.sh

the testing feature never unifies into a release build (fakes can’t ship)

test

cargo test --workspace

unit/integration tests (incl. the embed guard)

self-contained

scripts/verify-self-contained.sh

the empty-dir single-binary embed proof

a11y

npx playwright test (axe-core per route)

WCAG 2 A/AA on every route — paired with the manual docs/a11y-manual-checklist.md

Two ordering/caching constraints the pipeline encodes (and why):

  • cargo leptos build runs before any plain-cargo gate. crates/server/src/assets.rs embeds target/site via a rust-embed derive that needs the folder to exist at compile time, so a clean-checkout cargo clippy/test/build of server fails until cargo leptos build has produced it. The build:site job runs first and passes target/site to the cargo gates as an artifact.

  • target/ is never cached. build-release.sh's cargo clean -p server --release is load-bearing (rust-embed can’t emit cargo:rerun-if-changed); a cache restoring server's release artifacts across that clean would ship a stale/WASM-less embed. Only the cargo registry and the cargo-leptos binary 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)