init: data-driven boot service list via /etc/init.csv

Replace init's hardcoded boot_services array with an authoritative,
human-readable service list read at boot, mirroring /etc/devices.csv. Each row
is a service binary path followed by its argv; startup order is file order,
shutdown the reverse. There is no hardcoded fallback — a missing file starts
nothing (the no-ramdisk isolation behavior).

- library/csv: shared CSV helpers (comment strip, field iteration) with unit
  tests; device-registry is refactored onto them so both /etc/*.csv files parse
  through one place.
- init reads /etc/init.csv into fixed-max static tables (the same pattern as the
  device registry) and passes each row's argv straight to spawnSupervised. This
  also makes boot-time modes (e.g. device-manager test-usb-restart) expressible
  as data rather than hardcoded.
- Diagnose mode (-Ddiagnose omits the display stack) becomes build-time file
  selection between etc/init.csv and etc/init-diagnose.csv, so init carries no
  comptime service logic; the diagnose build option is dropped from init.
- build.zig: csv module wired; /etc/init.csv bundled into the initrd; the
  device-registry tests move to a dedicated block since they now import csv.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd
This commit is contained in:
2026-07-26 17:13:51 +01:00
co-authored by Claude Opus 4.8
parent 203528c8a7
commit 081ba1d74e
6 changed files with 235 additions and 77 deletions
+31 -3
View File
@@ -284,12 +284,18 @@ pub fn build(b: *std.Build) void {
const pci_class_module = b.addModule("pci-class", .{
.root_source_file = b.path("library/device/pci/pci-class.zig"),
});
// Shared CSV helpers (comment stripping, field iteration) for the /etc/*.csv
// config files — the device registry and the init service list both parse them.
const csv_module = b.addModule("csv", .{
.root_source_file = b.path("library/csv/csv.zig"),
});
// The device registry: parse /etc/devices.csv into match rules and bind a
// reported device to a driver — the data-driven, authoritative replacement for
// the manager's hand-written switch tables. Pure logic (no hardware, no
// syscalls), so it unit-tests with plain `zig test`; the manager imports it.
// syscalls), so it unit-tests on the host; the manager imports it.
const device_registry_module = b.addModule("device-registry", .{
.root_source_file = b.path("library/device/registry/device-registry.zig"),
.imports = &.{.{ .name = "csv", .module = csv_module }},
});
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
// nodes. Also shared reference data.
@@ -646,6 +652,8 @@ pub fn build(b: *std.Build) void {
// 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
@@ -653,7 +661,8 @@ pub fn build(b: *std.Build) void {
// the heartbeat stays present under test.
const init_options = b.addOptions();
init_options.addOption(bool, "serial", serial);
init_options.addOption(bool, "diagnose", diagnose);
// 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());
// --- the rest of the boot tree: /system services and drivers, /test fixtures ---
@@ -760,6 +769,10 @@ pub fn build(b: *std.Build) void {
// structure is the single source of truth. Entry names (and hence argv[0] and
// task names) are these paths with a leading slash. Test fixtures mirror their
// repo home: test/system/services/<name> in the source tree IS the boot path.
// init's boot service list is data (/etc/init.csv). -Ddiagnose selects the
// variant that omits the display stack (so the kernel's boot transcript stays
// on screen); both are bundled at the same /etc/init.csv path.
const init_csv_source = if (diagnose) "etc/init-diagnose.csv" else "etc/init.csv";
const bundled = [_]BundledBinary{
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
@@ -774,6 +787,8 @@ pub fn build(b: *std.Build) void {
// initrd tree (system/kernel/vfs.zig setInitialRamdisk), so the manager can
// fs.open("/etc/devices.csv") with no filesystem service running.
.{ .path = "etc/devices.csv", .binary = b.path("etc/devices.csv") },
// init's service list, likewise read from the kernel-served initrd /etc.
.{ .path = "etc/init.csv", .binary = b.path(init_csv_source) },
.{ .path = "system/drivers/ps2-bus", .binary = ps2_bus_exe.getEmittedBin() },
.{ .path = "system/drivers/ps2-keyboard", .binary = ps2_keyboard_exe.getEmittedBin() },
.{ .path = "system/drivers/ps2-mouse", .binary = ps2_mouse_exe.getEmittedBin() },
@@ -1088,7 +1103,7 @@ pub fn build(b: *std.Build) void {
"library/device/acpi/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21)
"library/device/usb/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
"library/device/usb/usb-ids.zig", // class/subclass/protocol code assignments
"library/device/registry/device-registry.zig", // /etc/devices.csv parse + most-specific driver match
"library/csv/csv.zig", // shared /etc/*.csv comment-strip + field-split helpers
"library/device/mmio/mmio.zig", // barriers assemble + registers round-trip
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
@@ -1113,6 +1128,19 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
}
// The device registry imports the shared `csv` module, so its tests need that
// import wired and don't fit the plain loop above. These prove the /etc/devices.csv
// parse + most-specific driver match (incl. virtio 1AF4:1050 beating a class rule).
const device_registry_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("library/device/registry/device-registry.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "csv", .module = csv_module }},
}),
});
test_step.dependOn(&b.addRunArtifact(device_registry_tests).step);
// The xkeyboard-config keymap tests need its generated `layouts` import wired, so they
// don't fit the plain loop above. Its keycode->character assertions are the end-to-end
// proof that the xkb-data -> generator -> Zig-lookup pipeline is correct.