7.2 KiB
05 — Per-app static binaries (wo build apps/X)
Context sources: ./00-overview.md §§ "Goal" (L21–23), "Design decisions" 3–4 (L32–33), ./01-htmlx-format-spec.md, ./02-ui-compiler.md, ./03-client-runtime.md, ./04-workspace-layout.md, ./06-shared-db-daemon.md (the wire URL contract this phase consumes), docs/examples/ecommerce/apps/storefront/wo.toml.
Goal
wo build apps/<X> produces a single static binary at target/wo/<X> that contains the app's ##ui / ##app blocks compiled to .htmlx, the imported shared/ dirs the app's wo.toml names, the vanilla-JS client runtime from phase 03, and a thin main() that reads WO_DB, opens a wire connection to the shared daemon, and serves the public HTTP listener declared in [server] listen.
Design decisions (locked)
- One Cargo build per app, dynamic Cargo project templating.
wo build apps/<X>materialises a Cargo project undertarget/wo-build/<X>/, fills[bin] name = "<X>", copies/generatesapp_config.rs, and invokescargo build --release. The resulting binary is copied totarget/wo/<X>. - No per-app Rust source generation beyond config. The same
crates/appis linked into every app binary. The only generated Rust file isapp_config.rscontaining the route table, embedded templates, and embedded runtime asset. Avoids exploding cargo metadata across N apps. include_bytes!bakes templates + runtime + CSS at compile time. A Cargobuild.rswritesapp_config.rsenumerating every compiled.htmlx, every.cssfromapps/<X>/ui/<screen>/<screen>.cssandshared/components/*.css, plus the runtime JS viaRUNTIME_JSfrom phase 03.- Connection target precedence:
WO_DBenv >[database].urlfrom manifest > error. The app refuses to start if neither is set. Anchored in./00-overview.md"Goal" (L23) anddocs/examples/ecommerce/apps/storefront/wo.tomlL21–26. - HTTP listener address comes from
[server] listen. Different from the database URL — the database URL is what this binary connects to;[server].listenis what the binary itself exposes to browsers.WO_LISTENenv var overrides for ops.
Scope
New files
| File | Responsibility | Port source |
|---|---|---|
crates/app/src/build.rs |
wo build apps/<X> driver: template Cargo project, run cargo, copy binary |
new (~250 LOC) |
crates/app/build.rs (Cargo build script) |
Generates app_config.rs with embedded templates + runtime + routes |
new (~100 LOC) |
crates/app/src/main.rs |
App-binary entrypoint: reads WO_DB, opens wire, starts HTTP |
new (~150 LOC) |
crates/app/src/route_table.rs |
Compile-time route table from app.wo routes block |
new (~120 LOC) |
crates/rt/src/bin/wo.rs |
Wire wo build <path> subcommand |
modify (+40 LOC) |
Total: ~660 LOC new + ~40 LOC modified.
Cargo.toml change
crates/app adds itself as a workspace member that produces a binary. No new external deps beyond what phases 01–04 already brought in.
[[bin]]
name = "wo-app"
path = "src/main.rs"
Generated app_config.rs shape
// Generated by crates/app/build.rs — do not edit.
pub const APP_NAME: &str = "storefront";
pub const APP_LISTEN: &str = ":8080";
pub const APP_DB_URL: Option<&str> = Some("wo://127.0.0.1:5555");
pub const APP_API_KEY_ENV: &str = "STOREFRONT_DB_KEY";
pub const TEMPLATES: &[(&str, &[u8])] = &[
("home", include_bytes!("../target/wo/storefront/ui/home.htmlx")),
("orders", include_bytes!("../target/wo/storefront/ui/orders.htmlx")),
];
pub const ROUTES: &[(&str, &str, &str)] = &[
("GET", "/", "ui.home"),
("GET", "/orders", "ui.orders"),
];
API shape (target)
// build-time API used by the wo CLI:
use app::build;
let binary_path: PathBuf = build::build(Path::new("docs/examples/ecommerce/apps/storefront"),
Path::new("target/wo"))?;
// runtime: every app binary's main() looks like this
fn main() -> Result<()> {
let db_url = env::var("WO_DB").ok()
.or(app_config::APP_DB_URL.map(str::to_owned))
.ok_or(BootError::NoDatabase)?;
let api_key = env::var(app_config::APP_API_KEY_ENV)?;
let db = wire::connect(&db_url, &api_key)?;
let r = build_router(&app_config::ROUTES, &app_config::TEMPLATES, db);
rt::http::serve(app_config::APP_LISTEN, r)
}
Exit criteria
cargo build -p appgreen;crates/appproduces thewo-applibrary + thewo builddriver.- Storefront builds.
cargo run --bin wo -- build docs/examples/ecommerce/apps/storefrontproducestarget/wo/storefront.file target/wo/storefrontreports an ELF executable. - Storefront boots.
WO_DB=wo://127.0.0.1:5555 STOREFRONT_DB_KEY=test ./target/wo/storefront &thencurl -fsS http://127.0.0.1:8080/healthzreturns200. (The DB daemon from phase 06 is mocked or stubbed for this test if 06 hasn't landed yet — refuse-to-start without DB is the contract; the test verifies refuse-to-start whenWO_DBis unset.) - Admin builds separately.
wo build apps/adminproduces a different binary with a disjoint route table. Diffing the twoapp_config.rsfiles shows different route lists. - Refuse-to-start without DB.
./target/wo/storefrontwith noWO_DBand no manifest URL exits non-zero with a clear error. cd .dev/reference/crates && cargo build && cargo test.
Non-scope
- No cross-compilation. Linux x86_64 only this phase.
--targetflags are passed through but untested. - No musl static linking. glibc-linked binaries are fine for prototype.
- No container packaging, no systemd unit generation.
- No binary-size optimisation beyond
--release.wo build --stripis a future flag. - No incremental compile cache management.
target/wo-build/<X>/is reused across builds but not pruned.
Verification
cargo build -p app
# storefront build + boot
cargo run --bin wo -- build docs/examples/ecommerce/apps/storefront
file target/wo/storefront # ELF 64-bit
ls -la target/wo/storefront/ui/ # home.htmlx, orders.htmlx (compiled in phase 02)
# refuse-to-start without WO_DB
./target/wo/storefront 2>&1 | grep -q "WO_DB"
# admin builds independently
cargo run --bin wo -- build docs/examples/ecommerce/apps/admin
test -x target/wo/admin
# v1 regression
cargo run --bin wo -- run docs/examples/blog &
PID=$!; sleep 1; curl -fsS http://127.0.0.1:8080/ >/dev/null; kill $PID
cd .dev/reference/crates && cargo build && cargo test
After this phase
wo build produces shippable per-app binaries; what they connect to is owned by phase 06 (06-shared-db-daemon.md), and the policy gate they enforce on every query is owned by phase 07 (07-per-app-policies.md). After 06 + 05 + 07 land together, the prototype demo path closes: wo db serve + wo build apps/storefront + wo build apps/admin running side by side, sharing one data dir, with admin live updates triggered by storefront commits.