writeonce/docs/plan/exploration/ui/05-per-app-binaries.md
shoney.arickathil a55971d857 docs: status board at docs/00-status.md; gap-closure spec applied; recover lost doc
- Board renamed docs/plan/00-kanban.md -> docs/00-status.md and rebuilt: ▶ NEXT
  PLAN pointer (iteration 4 — emitter, corpus, `woc build`) then six buckets —
  stories, in progress, done, pending, discarded, learnings. It covered only the
  Rust runtime before, so the whole OOP track was invisible. All 16 inbound refs
  repointed; `Kanban:` banners renamed to `Status:`.
- New discarded.md (settled rejections with reasons: inheritance, `abstract`,
  Money/SKU/Float, Dynamic/cast/macro/extern, AOT-to-C, Menhir, shared engine
  state) and learnings.md (plumbed≠enforced, vacuous goldens, exit-0-wrong-
  output, malloc-path ASan trick, deferred checks that never reach the VM).
- RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk,
  never committed (gitignored path), cited by five docs incl. principle 12.
  Root cause was broader: all seven forward-roadmap plans in
  docs/superpowers/plans/ were untracked and ignored, on one disk only. Dropped
  the docs ignore rules with a do-not-re-add note; added __pycache__/*.pyc.
- Repaired broken links across docs/, 270 -> 36: fixes a regression from the
  earlier reference/ -> .dev/reference/ move (relative paths at ../../ and
  deeper were skipped), plus depth and reorg drift. The 36 residual point at
  content that does not exist and need decisions, not paths.
- New spec docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md,
  applied: `and`/`or` verdict row; Part 3 gains `env` (six modules), swaps
  time.mono for iso/local, adds 22 bare core builtins; throw/time.mono/is cut
  (0 uses in the sample). Plan 8: Task 2 gains and/or, Task 5 drops throw,
  abstract+`is` task deleted, 8/9 renumber to 7/8. Plan 9 gains core builtins.
  Plan 10 gains the 307 -> 0 diagnostic gate. WO-E205 re-filed unreachable-by-
  design. types.ml header drops its false satisfaction-set claim. 00-code-
  review.md reduced to a stub — its rival Phase 1-4 roadmap retired.
2026-08-10 23:42:26 +02:00

124 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<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)
1. **One Cargo build per app, dynamic Cargo project templating.** `wo build apps/<X>` materialises a Cargo project under `target/wo-build/<X>/`, fills `[bin] name = "<X>"`, copies/generates `app_config.rs`, and invokes `cargo build --release`. The resulting binary is copied to `target/wo/<X>`.
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/<X>/ui/<screen>/<screen>.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/<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.
```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/<X>/` 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.