build: exact per-binary imports — the pre-wired default set is gone

Every binary's build.zig now names precisely the modules its source
imports (derived by scanning each artifact's sources, transitively
through same-directory files), and its zon carries only the domains
those come from — kernel stays implicit (the root shim + link script
live there). build-support's userBinary resolves each name through one
module-to-domain table (module_homes); Domains/domains()/defaultImports
and the raw recipe entry point are deleted. An undeclared @import is a
compile error (verified: injecting @import("xkeyboard-config") into
logger fails with 'no module named ... available within module
program'), and e.g. xkeyboard-config now appears in exactly two
manifests — the two keyboard drivers. Availability never bloated the
emitted binaries (Zig compiles only what a program imports); this makes
the declared interfaces honest. Production and -Dtest-case manifests
byte-identical; all build variants and standalone package builds
green.
This commit is contained in:
Daniel Samson
2026-07-30 06:42:31 +01:00
parent c621b649f6
commit 4476208361
60 changed files with 401 additions and 450 deletions
+23 -15
View File
@@ -11,7 +11,9 @@ root split into `build/images.zig` + `build/qemu.zig`; the root `build.zig` is
tests, the QEMU suite at parity with main, boot-image file list unchanged.
The `lazyDependency` payoff (What-this-buys #4) is in too: the /test fixtures
and the unselected discovery package are lazy — a build loads and compiles
only what it ships.
only what it ships. And imports are exact: the pre-wired default set is gone;
every binary names precisely the modules its source imports and carries only
those domains in its manifest (rule 1 below).
## Why
@@ -55,10 +57,15 @@ 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.
- **Imports are exact and per binary.** A binary's build.zig names precisely
the modules its source `@import`s — the moral equivalent of a C file's
include list — and its zon names only the domains those modules come from
(plus `build-support` and `kernel`, which is implicit in every binary: the
root shim and user link script live there). Nothing is pre-wired: an
undeclared `@import` is a compile error, and build-support's one
module-to-domain table (`module_homes`) resolves each name. Availability
never meant bloat — Zig only compiles what a program actually imports — but
exactness makes the declared interface honest and machine-checked.
- **Modules export source, not artifacts** — each consumer compiles libraries
with its own flags, so per-binary optimization choices keep working; Zig's
cache deduplicates.
@@ -71,9 +78,10 @@ Rules:
## 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).
1. Library interfaces become machine-checked: a consumer can only import what
it declared — per binary, down to the single module — and each domain's zon
declares what it needs (claim-before-touch, applied to source). A keyboard
driver carries `xkeyboard-config` in its manifest; nothing else does.
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
@@ -124,15 +132,15 @@ rewritten against the package template.
## Execution notes (the finished shape)
- The shared recipe lives in `build-support/build.zig`: `userBinary` (what
every binary package calls), `userBinaryFromImports` (the underlying
recipe), and `defaultImports` — the ONE list of default modules. The `start`
root shim and `user.ld` are named through the kernel package
(Dependency.path).
every binary package calls, resolving each named import through the
`module_homes` table) and `programModule` (for per-binary addOptions
modules). The `start` root shim and `user.ld` are named through the kernel
package (Dependency.path).
- Adding a binary = adding a directory with source + a ~15-line build.zig +
zon (copy any existing binary package, e.g.
`system/drivers/pci-bus/build.zig`), then one dependency + one bundled
entry in the root build.zig and one zon line. Per-binary extras go through
`build_support.programModule(exe).addImport(...)` inside the package.
`system/drivers/pci-bus/build.zig`) listing exactly the modules the source
imports and the domains they come from, then one dependency + one bundled
entry in the root build.zig and one zon line.
- The boot-tree array in the root (search `"etc/init.csv"` or
`.getEmittedBin()`) is the image file list — the authoritative comparison
target for any future build change.
+10 -9
View File
@@ -138,15 +138,16 @@ a higher-level service (block ↔ filesystem, a scanout driver ↔ the composito
private wire to its *hardware* — virtio-gpu's command set — is not that; it stays a
driver-private file, like the virtio-pci transport beside it.
The build side of this has since landed: the shared recipe in
[`build-support/build.zig`](../../build-support/build.zig) (`defaultImports` +
`userBinary`) injects the default modules — the library/kernel concern modules (`ipc`,
`memory`, `process`, `time`, `logging`, `file-system`, `thread`, `service`), the
device/service clients (`driver`, `block`, `display`, `input`), plus `mmio`,
`xkeyboard-config`, `acpi-ids` — into every user binary, and per-binary extras —
protocol modules, bus logic — are added with `programModule(exe).addImport(...)`.
Every binary owns a package with its own ~15-line `build.zig` calling that recipe
(see [build-packages-plan.md](../build-packages-plan.md)). That's the *entire*
The build side of this has since landed: every binary owns a package whose
~15-line `build.zig` names EXACTLY the modules its source imports — the moral
equivalent of a C file's include list — and the shared recipe in
[`build-support/build.zig`](../../build-support/build.zig) (`userBinary`)
resolves each name from the library domain that exports it (kernel's concern
modules, the device driver libraries, the service clients, the protocols). An
undeclared `@import` is a compile error, and a domain none of the imports come
from never appears in the binary's manifest — a keyboard driver declares
`xkeyboard-config`; nothing else does (see
[build-packages-plan.md](../build-packages-plan.md)). That's the *entire*
mechanism — Zig modules already give you everything else.
The discipline that makes this work: **a class driver must not import a bus's *hardware*
@@ -117,24 +117,28 @@ The driver directory is its own build package
([build-packages-plan.md](../build-packages-plan.md)): a ~15-line `build.zig`
plus a `build.zig.zon` beside the source. Copy both from an existing driver —
`system/drivers/pci-bus/` is the template — and adjust the name, root source
file, and per-driver extras:
file, and the import list. The list names EXACTLY the modules the driver's
source `@import`s (the moral equivalent of its include list; an undeclared
import is a compile error):
```zig
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "intel-uhd-graphics-750",
.root_source_file = b.path("intel-uhd-graphics-750.zig"),
.domains = build_support.domains(b),
.imports = &.{ "driver", "ipc", "memory", "process", "service" },
});
build_support.programModule(exe).addImport("pci", b.dependency("device", .{}).module("pci")); // if the pci library is used
b.installArtifact(exe);
}
```
The zon declares `build-support`, the four domain packages, and any extras'
homes by relative path (again, copy pci-bus's and adjust); for the
`.fingerprint` field, leave the copied value in place and `zig build` will
reject it and suggest the fresh one to paste.
The zon declares `build-support`, `kernel` (implicit in every binary: the root
shim lives there), and the homes of the listed imports — for the minimal
driver above that is kernel alone plus `device` (for `driver`); add
`protocol`, `client`, ... only when an import comes from them (again, copy
pci-bus's zon and adjust). For the `.fingerprint` field, leave the copied
value in place and `zig build` will reject it and suggest the fresh one to
paste.
Then three one-liners in the root build register the package: the dependency
and a row in the boot-tree array in `build.zig` (search for