build: phase 2 wave A — services build as packages

init, fat, display, display-demo, device-manager, input, logger, and
the two discovery fillers (acpi, fdt — each exporting an artifact named
"discovery"; the root -Ddiscovery picks which ships) convert to binary
packages on the pci-bus template. init's serial heartbeat flag rides
the dependency options (the root forwards its -Dserial). fat's and
display's unit tests move into their packages and the root aggregate
delegates to them. Boot-image file list unchanged.
This commit is contained in:
Daniel Samson
2026-07-30 04:02:49 +01:00
parent 902e4a0a9e
commit 3b23b11b0e
56 changed files with 1064 additions and 60 deletions
+36 -60
View File
@@ -251,9 +251,6 @@ pub fn build(b: *std.Build) void {
const abi_module = kernel_library.module("abi");
const device_abi_module = device_library.module("device-abi");
const pci_class_module = device_library.module("pci-class");
const csv_module = csv_library.module("csv");
const device_registry_module = device_library.module("device-registry");
const aml_module = device_library.module("aml");
const usb_abi_module = device_library.module("usb-abi");
const usb_ids_module = device_library.module("usb-ids");
const usb_transfer_protocol_module = protocol_library.module("usb-transfer-protocol");
@@ -299,13 +296,12 @@ pub fn build(b: *std.Build) void {
},
});
// The wire protocols, all exported by the protocol package.
const vfs_protocol_module = protocol_library.module("vfs-protocol");
// The wire protocols some binaries name as per-binary extras, exported by
// the protocol package.
const input_protocol_module = protocol_library.module("input-protocol");
const device_manager_protocol_module = protocol_library.module("device-manager-protocol");
const display_protocol_module = protocol_library.module("display-protocol");
const scanout_protocol_module = protocol_library.module("scanout-protocol");
const power_protocol_module = protocol_library.module("power-protocol");
// The driver-side libraries some binaries name as per-binary extras. (The
// rest of the domain modules reach the root only through build-support's
@@ -370,23 +366,14 @@ pub fn build(b: *std.Build) void {
.xkeyboard_config = xkeyboard_config_library,
});
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the library/kernel modules, and
// started in ring 3 by the kernel's user-ELF loader.
const init_exe = addUserBinary(b, kernel_target, &default_imports, "init", "system/services/init/init.zig");
programModule(init_exe).addImport("power-protocol", power_protocol_module);
// init parses its boot service list from /etc/init.csv with the shared csv helpers.
programModule(init_exe).addImport("csv", csv_module);
// init reads the same `serial` flag the kernel does: its liveness heartbeat is a
// serial/test-build diagnostic (the QEMU harness's init tests assert on it, and
// -Dserial images emit it), so a flashable image runs a purely event-driven PID 1
// that wakes only for real work. The test harness builds with -Dserial=true, so
// the heartbeat stays present under test.
const init_options = b.addOptions();
init_options.addOption(bool, "serial", serial);
// Which services init starts is no longer a comptime option: it reads /etc/init.csv,
// and -Ddiagnose selects which init.csv is bundled (see the `bundled` list below).
programModule(init_exe).addImport("build_options", init_options.createModule());
// Binary packages (docs/build-packages-plan.md, phase 2): each binary
// builds itself against the domain packages via build-support's shared
// recipe, started in ring 3 by the kernel's user-ELF loader like always;
// the root build just takes artifacts for the boot image. init receives
// the root's -Dserial as a dependency option (its liveness heartbeat is a
// serial/test-build diagnostic the QEMU harness asserts on; a flashable
// image leaves it out).
const init_exe = b.dependency("init", .{ .serial = serial }).artifact("init");
// --- the rest of the boot tree: /system services and drivers, /test fixtures ---
// Each is built by the same user-binary recipe and laid out at its FHS path on
@@ -424,16 +411,13 @@ pub fn build(b: *std.Build) void {
const usb_storage_exe = addUserBinary(b, kernel_target, &default_imports, "usb-storage", "system/drivers/usb-storage/usb-storage.zig");
programModule(usb_storage_exe).addImport("usb", usb_module);
programModule(usb_storage_exe).addImport("block-protocol", block_protocol_module);
// The FAT filesystem server: mounts the block device and serves it into the VFS
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
const fat_exe = addUserBinary(b, kernel_target, &default_imports, "fat", "system/services/fat/fat.zig");
programModule(fat_exe).addImport("vfs-protocol", vfs_protocol_module);
// Threaded: the display runs a mouse-listener thread alongside its compositor loop
// (docs/threading.md, docs/display.md), so it opts into real atomics/TLS.
const display_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "display", "system/services/display/display.zig");
programModule(display_exe).addImport("display-protocol", display_protocol_module);
programModule(display_exe).addImport("scanout-protocol", scanout_protocol_module);
const display_demo_exe = addUserBinary(b, kernel_target, &default_imports, "display-demo", "system/services/display-demo/display-demo.zig");
// The FAT filesystem server and the display stack, each its own package
// (fat's and display's unit tests ride along in their packages).
const fat_package = b.dependency("fat", .{});
const fat_exe = fat_package.artifact("fat");
const display_package = b.dependency("display", .{});
const display_exe = display_package.artifact("display");
const display_demo_exe = b.dependency("display-demo", .{}).artifact("display-demo");
const virtio_gpu_exe = addUserBinary(b, kernel_target, &default_imports, "virtio-gpu", "system/drivers/virtio-gpu/virtio-gpu.zig");
programModule(virtio_gpu_exe).addImport("pci", pci_module); // library/device/pci — the claimed-function view
programModule(virtio_gpu_exe).addImport("display-protocol", display_protocol_module);
@@ -466,32 +450,24 @@ pub fn build(b: *std.Build) void {
// "discovery" so the device manager never learns which firmware it is on.
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
// flattened device tree — the aarch64 target flips the default when it
// lands (docs/arm.md). Both are placeholders until M20.1 (acpi) and the
// ARM bring-up (fdt).
// lands (docs/arm.md). Each firmware's service is its own package; both
// export an artifact named "discovery", and this option picks which one
// ships (only the chosen one is compiled).
const Discovery = enum { acpi, fdt };
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
const discovery_source: []const u8 = switch (discovery) {
.acpi => "system/services/acpi/acpi.zig",
.fdt => "system/services/fdt/fdt.zig",
const discovery_exe = switch (discovery) {
.acpi => b.dependency("acpi", .{}).artifact("discovery"),
.fdt => b.dependency("fdt", .{}).artifact("discovery"),
};
const discovery_exe = addUserBinary(b, kernel_target, &default_imports, "discovery", discovery_source);
if (discovery == .acpi) programModule(discovery_exe).addImport("aml", aml_module);
if (discovery == .acpi) programModule(discovery_exe).addImport("device-manager-protocol", device_manager_protocol_module);
if (discovery == .acpi) programModule(discovery_exe).addImport("power-protocol", power_protocol_module);
const device_manager_exe = addUserBinary(b, kernel_target, &default_imports, "device-manager", "system/services/device-manager/device-manager.zig");
programModule(device_manager_exe).addImport("device-manager-protocol", device_manager_protocol_module);
// Driver matching is data-driven: the manager parses /etc/devices.csv into this
// module's rules and binds each reported device by most-specific match.
programModule(device_manager_exe).addImport("device-registry", device_registry_module);
const device_manager_exe = b.dependency("device-manager", .{}).artifact("device-manager");
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
const input_exe = addUserBinary(b, kernel_target, &default_imports, "input", "system/services/input/input.zig");
programModule(input_exe).addImport("input-protocol", input_protocol_module);
const input_exe = b.dependency("input", .{}).artifact("input");
const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "test/system/services/input-source/input-source.zig");
const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "test/system/services/input-test/input-test.zig");
const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "test/system/services/args-echo/args-echo.zig");
const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "test/system/services/process-test/process-test.zig");
const logger_exe = addUserBinary(b, kernel_target, &default_imports, "logger", "system/services/logger/logger.zig");
const logger_exe = b.dependency("logger", .{}).artifact("logger");
// The first multi-threaded binary: exercises runtime.Thread over the thread ABI
// (docs/threading.md). Built threaded so its shared-memory poll is real.
const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "test/system/services/thread-test/thread-test.zig");
@@ -852,9 +828,6 @@ pub fn build(b: *std.Build) void {
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
"system/drivers/virtio-gpu/virtio-gpu-protocol.zig", // virtio-gpu command struct sizes
"system/drivers/virtio-gpu/virtio-pci.zig", // virtio 1.0 PCI transport struct sizes
}) |root| {
@@ -868,11 +841,12 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
}
// The library domains own their unit tests (each package's standalone
// `zig build test` step); the root aggregate delegates to those steps so
// one command still runs everything and a test added inside a domain
// package can never be silently skipped here. Domain tests are host-only,
// so root's -Dtarget/-Doptimize deliberately do not reach them.
// The library domains and the binary packages own their unit tests (each
// package's standalone `zig build test` step); the root aggregate
// delegates to those steps so one command still runs everything and a
// test added inside a package can never be silently skipped here. Package
// tests are host-only, so root's -Dtarget/-Doptimize deliberately do not
// reach them.
for ([_]*std.Build.Dependency{
kernel_library,
device_library,
@@ -880,8 +854,10 @@ pub fn build(b: *std.Build) void {
protocol_library,
csv_library,
xkeyboard_config_library,
}) |library| {
test_step.dependOn(&library.builder.top_level_steps.get("test").?.step);
fat_package,
display_package,
}) |package| {
test_step.dependOn(&package.builder.top_level_steps.get("test").?.step);
}
// The tagged kernel log ring: append/wrap/reclaim/sequence-gap behavior over