# 05 — Per-app static binaries (`wo build apps/X`) **Context sources:** [`./00-overview.md`](./00-overview.md) §§ "Goal" (L21–23), "Design decisions" 3–4 (L32–33), [`./01-htmlx-format-spec.md`](./01-htmlx-format-spec.md), [`./02-ui-compiler.md`](./02-ui-compiler.md), [`./03-client-runtime.md`](./03-client-runtime.md), [`./04-workspace-layout.md`](./04-workspace-layout.md), [`./06-shared-db-daemon.md`](./06-shared-db-daemon.md) (the wire URL contract this phase consumes), [`docs/examples/ecommerce/apps/storefront/wo.toml`](../../../examples/ecommerce/apps/storefront/wo.toml). ## Goal `wo build apps/` produces a single static binary at `target/wo/` 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) 1. **One Cargo build per app, dynamic Cargo project templating.** `wo build apps/` materialises a Cargo project under `target/wo-build//`, fills `[bin] name = ""`, copies/generates `app_config.rs`, and invokes `cargo build --release`. The resulting binary is copied to `target/wo/`. 2. **No per-app Rust source generation beyond config.** The same `crates/app` is linked into every app binary. The only generated Rust file is `app_config.rs` containing the route table, embedded templates, and embedded runtime asset. Avoids exploding cargo metadata across N apps. 3. **`include_bytes!` bakes templates + runtime + CSS at compile time.** A Cargo `build.rs` writes `app_config.rs` enumerating every compiled `.htmlx`, every `.css` from `apps//ui//.css` and `shared/components/*.css`, plus the runtime JS via `RUNTIME_JS` from phase 03. 4. **Connection target precedence: `WO_DB` env > `[database].url` from manifest > error.** The app refuses to start if neither is set. Anchored in [`./00-overview.md`](./00-overview.md) "Goal" (L23) and [`docs/examples/ecommerce/apps/storefront/wo.toml`](../../../examples/ecommerce/apps/storefront/wo.toml) L21–26. 5. **HTTP listener address comes from `[server] listen`.** Different from the database URL — the database URL is what this binary connects *to*; `[server].listen` is what the binary itself exposes to browsers. `WO_LISTEN` env var overrides for ops. ## Scope ### New files | File | Responsibility | Port source | | --- | --- | --- | | `crates/app/src/build.rs` | `wo build apps/` 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 ` 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. ```toml [[bin]] name = "wo-app" path = "src/main.rs" ``` ### Generated `app_config.rs` shape ```rust // 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) ```rust // 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 1. `cargo build -p app` green; `crates/app` produces the `wo-app` library + the `wo build` driver. 2. **Storefront builds.** `cargo run --bin wo -- build docs/examples/ecommerce/apps/storefront` produces `target/wo/storefront`. `file target/wo/storefront` reports an ELF executable. 3. **Storefront boots.** `WO_DB=wo://127.0.0.1:5555 STOREFRONT_DB_KEY=test ./target/wo/storefront &` then `curl -fsS http://127.0.0.1:8080/healthz` returns `200`. (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 when `WO_DB` is unset.) 4. **Admin builds separately.** `wo build apps/admin` produces a *different* binary with a disjoint route table. Diffing the two `app_config.rs` files shows different route lists. 5. **Refuse-to-start without DB.** `./target/wo/storefront` with no `WO_DB` and no manifest URL exits non-zero with a clear error. 6. `cd reference/crates && cargo build && cargo test`. ## Non-scope - **No cross-compilation.** Linux x86_64 only this phase. `--target` flags 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 --strip` is a future flag. - **No incremental compile cache management.** `target/wo-build//` is reused across builds but not pruned. ## Verification ```bash 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 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.