Files
danos/docs/build-packages-plan.md
T
Daniel Samson 902e4a0a9e docs: catch the build docs up with the package split
Review findings: the plan doc claimed no implementation existed, had the
domain dependency order wrong (kernel depends on protocol; device on
kernel + protocol + csv), never placed the three shared contracts, and
named a nonexistent new-driver-checklist.md. Its status now records the
implemented phases (and the deliberate pci-bus-first pilot), the target
shape carries the contract placements and the path-dependency-only
constraint on the kernel package's out-of-root abi export, and the
execution notes describe the post-pilot build for whichever session
runs the remaining waves. README's repo layout gains build-support/
and the packages-note; driver-model, threading, system-requirements,
and the two display plan docs stop citing root build.zig for recipe
facts that now live in build-support.
2026-07-26 23:12:14 +01:00

157 lines
8.4 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.
# Plan: packages — hierarchical builds for libraries and binaries
**Status: in progress.** Implemented on branch `claude/build-packages-plan-174144`:
phase 0 (`build-support`), phase 1 (all six library domains as packages, root as
the pilot consumer), and the first phase-2 binary package — `pci-bus`, a
deliberate deviation from wave A's two-small-services opener, because a driver
with per-binary extras (device-manager-protocol, pci-class) exercises the
template harder than a plain service. Every phase landed green (unit tests, the
QEMU suite at parity with main, boot-image file list unchanged). Remaining:
waves B–D of phase 2, then phase 3.
## Why
`build.zig` was ~1,250 lines, growing by three hand-written stanzas per binary;
at a driver per device family that does not scale. More fundamentally: in one
monolithic build every binary compiles against library *source*, so a library
interface break is silently absorbed by whoever edits everything in one commit —
the interface never has to be honest. danos is about isolation; the build should
mirror it.
A **package** here is a build-time unit only — a directory owning a `build.zig`
(recipe: what it exports, how to test it) and a `build.zig.zon` (manifest: name
+ dependencies). Binaries remain fully static freestanding ELFs; packages change
who declares what, not what links to what. Source code is untouched: `@import`
uses module names (`"pci"`, `"service"`) exactly as today — only build files
know where anything lives.
## Target shape
```
build-support/ package: the danos build API (userBinary(), defaultImports(), targets)
library/kernel/ package "kernel": modules abi, ipc, service, memory, process, logging, time, ... (depends on protocol)
library/device/ package "device": modules driver, pci, usb-abi, model, ... (depends on kernel, protocol, csv)
library/protocol/ package "protocol": the wire protocols
library/client/ package "client" (depends on kernel, protocol)
library/csv/ package "csv"
library/xkeyboard-config/ package "xkeyboard-config"
system/services/<name>/ one package per binary: ~15-line build.zig + zon
system/drivers/<name>/ one package per binary
build.zig (root) orchestrator: dependency() per binary, image assembly, QEMU, test steps
```
The three shared contracts: `boot-handoff` stays a root module (only the
loader↔kernel pair speaks it); `abi` is exported by the kernel package from
`../../system/abi.zig` (the source stays with the kernel; userspace's one view
of it lives in the package, so every consumer names the same module instance);
`device-abi` is exported by device. Reaching outside the package root means the
kernel package is valid only as an in-repo path dependency — it could never be
fetched by hash — which is fine: path dependencies are the only way any of
these packages is consumed.
Rules:
- **Dependencies are declared at domain level** (a binary's zon names `kernel`,
`device`), **imports stay module-level** (`@import("pci")`). `build-support`
pre-wires the core set every binary uses (service, ipc, memory, process,
logging); per-binary build files name only extras.
- **Modules export source, not artifacts** — each consumer compiles libraries
with its own flags, so per-binary optimization choices keep working; Zig's
cache deduplicates.
- **Zon paths are relative and that is accepted.** Binaries sit exactly three
levels deep, so the `../../../` prefix is a constant idiom; a library-domain
move is a rare, already-breaking event fixed by one sed across manifests, and
a stale path fails loudly before anything compiles.
- **Cross-cutting build changes live in `build-support` only** — that is the
contract that keeps per-binary build files declarative.
## What this buys
1. Library interfaces become machine-checked: a consumer can only import what a
domain exports, and each domain's zon declares what it needs (claim-before-
touch, applied to source).
2. Each library domain gets a standalone `zig build test` — runtime-library
stability testing in isolation.
3. Adding a binary = adding a directory (source + two small files), not editing
three places in a 1,250-line file.
4. Later: `lazyDependency` lets an image target build only what it ships.
## Phases
Each phase ends green: `zig build test` passes (88/88 QEMU) and the boot
image's file list is unchanged. Byte-identical binaries are expected but not
required (module reorganization can perturb symbol order); file list is the
hard gate.
**Phase 0 — `build-support`.** Extract `addUserBinary`/`addThreadedUserBinary`,
the freestanding target setup, and the default-import wiring into the
`build-support` package. Root build consumes it; nothing else moves. This is
the cross-cutting-change home, so it lands first.
**Phase 1 — library domains become packages.** In dependency order: `protocol`
and `csv` (the roots) → `kernel` (depends on protocol: file-system speaks
vfs-protocol) → `device`, `client`; `xkeyboard-config` stands alone. Each gets
build.zig + zon + a standalone test step (client's is empty until its modules
grow host tests — kept for uniformity, since the root aggregate depends on
every domain's test step). The root build swaps its `createModule` calls for
`b.dependency("<domain>").module("<name>")`. **No binary moves in this phase**
— the root build is the pilot consumer, which proves the packages without
touching 30 binaries.
**Phase 2 — binaries become packages, in waves.** The template was shaken out
by the pci-bus pilot (see Status). Wave A: services. Wave B: the remaining
drivers. Wave C: test fixtures. Root build shrinks to orchestration per wave.
**Phase 3 — root cleanup.** Split what remains of the root build into
`build/images.zig`, `build/qemu.zig`, imported by a short root `build.zig`.
**Afterwards** (outside this plan): the intel-uhd-graphics-750 driver is
(re)created as a greenfield package — the "Adding a driver" checklist's build
step (docs/device-driver-development/devices-csv.md, step 1) gets rewritten
against the package template at that point.
## Execution notes (for whichever session runs the remaining waves)
Anchors in the root `build.zig` as it stands after the pilot:
- The shared recipe lives in `build-support/build.zig`: `userBinary` (domains
form, what binary packages call), `userBinaryFromImports` (the underlying
recipe root's stanzas still use), and `defaultImports` — the ONE list of
default modules; root and the packages both draw from it. The `start` root
shim and `user.ld` are named through the kernel package (Dependency.path).
- `programModule(<exe>).addImport(...)` calls in root (search `programModule`)
are the per-binary extra imports — the data for each binary's future
build.zig. `system/drivers/pci-bus/build.zig` is the template to copy.
- The boot-tree array (search `"etc/init.csv"` or `.getEmittedBin()`) is the
image file list — the authoritative before/after comparison target. A
converted binary's stanza becomes
`b.dependency("<name>", .{}).artifact("<name>")` plus a zon entry.
- The QEMU size-check tests hardcode source paths (search
`virtio-gpu-protocol.zig` in the root test list) — they move with their
binaries' waves.
Verification per phase:
- Unit tests: `zig build test`.
- QEMU integration suite: `python3 test/qemu_test.py` (docs/testing.md; the
full suite, all cases must pass).
- Image file list: the boot-tree array is the source of truth — snapshot it
(paths only) before phase 0 and diff after each phase; `zig build
check-fat-image` must also stay green.
Context a fresh session should read first: this doc, docs/testing.md,
docs/coding-standards.md (kebab-case names, no abbreviations), and the
`addUserBinaryImpl` body. Commit style: no Co-Authored-By trailers.
## Risks / notes
- Zig version churn: the package API (`b.dependency`, zon schema) has moved
between releases; the work pins against the repo's current Zig and any
upgrade lands separately, never mid-phase.
- The QEMU size-check tests hardcode source paths (e.g. virtio-gpu protocol
struct sizes in the root build) — phase 2 wave C must carry those along.
- Doc updates ride each phase: docs/README.md (repo layout + source map),
docs/device-driver-development/devices-csv.md ("Adding a driver", step 1),
and the docs that cite the build recipe (driver-model.md, threading.md,
system-requirements.md) reference build shapes that keep changing.