18 Commits
Author SHA1 Message Date
Daniel Samson fa8203cdba kernel: the IOMMU backends move behind the architecture boundary
VT-d and AMD-Vi are x86 hardware, but lived in the architecture-neutral
kernel tree and leaked further: the core's public Kind enum named both
vendors, and the ACPI parser read the VT-d version/capability registers
(raw volatile MMIO inside table discovery). Now the vendor backends
live in architecture/x86_64/ behind architecture.iommu — the core hands
over the discovery facts plus an injected environment (frame allocation
+ the log sink, the same pattern enablePaging uses) and receives the
hardware vtable back, so the backends never import kernel internals and
an ARM port supplies its SMMU with no core change. Discovery keeps
table facts only; the live-unit register check moved into VT-d detect
(version reading zero now stays fail-open). The unused kindOf() is
gone. Log shapes the harness pins (iommu online, DANOS-IOMMU-FAULT)
are unchanged; all five IOMMU QEMU cases pass.
2026-07-30 07:16:08 +01:00
Daniel Samson e53d6ebafb library: client modules end in -client, like protocols end in -protocol
display-client and input-client (files and module names), so a service,
its wire contract, and its client never share a name: `display` the
service, `display-protocol` the contract, `display-client` a program's
view of it. The nine consumers' imports and their packages' declared
lists follow (regenerated from the source scan); build-support's
module_homes table carries the new names.
2026-07-30 06:56:12 +01:00
Daniel Samson 4476208361 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.
2026-07-30 06:42:31 +01:00
Daniel Samson c621b649f6 build: lazy dependencies — a build loads only what it ships
The 13 /test fixtures and the acpi/fdt discovery pair are .lazy in the
root zon, resolved with b.lazyDependency only when a build actually
bundles them: a plain `zig build` neither compiles the fixtures nor
loads their build files, and only the -Ddiscovery-selected package ever
loads. Fixture packages are uniform (dependency name = artifact name =
boot-path leaf), so the bundled list shrinks to a name loop. Production
manifest byte-identical; the -Dtest-case manifest carries the same 13
entries in the same order; -Ddiscovery=fdt exercises the lazy fdt path.
Discharges the plan's deferred what-this-buys #4.
2026-07-30 06:26:22 +01:00
Daniel Samson d27670ec39 tests: fix the two stale kernel self-tests the FHS boot tree broke
initial_ramdisk's spawn-everything sweep counted the /etc data files
(devices.csv, init.csv) as spawnable programs — BadElf ever since the
boot tree started ferrying them — so it now counts only the /system and
/test trees. The init test waited for two raw user writes before
checking the last write for the heartbeat text; init's boot chatter
(heap ok, the /etc/init.csv lookup) satisfies the count long before the
first beat, so it now waits for the heartbeat itself. Both cases pass
again; these failures predate the build-packages work.
2026-07-30 06:23:31 +01:00
Daniel Samson ab7594df6e docs: commit the new-driver checklist, step 2 rewritten for packages
The checklist (with the Intel UHD 750 worked example) existed only as
an untracked file in the main checkout — the plan referenced it but no
branch could see it. It lands here with its build step rewritten
against the finished package template: create the driver's own
build.zig/zon from the pci-bus template, then one dependency + one
boot-tree row + one zon line in the root build. devices-csv.md's
adding-a-driver section and the plan's checklist references point at
it again.
2026-07-30 04:41:41 +01:00
Daniel Samson d03942b543 docs: catch the docs up with the finished package split
The plan doc's status records completion (all waves + phase 3) and its
execution notes describe the finished shape; the fresh-session pointer
names build-support's userBinary instead of the deleted
addUserBinaryImpl, and the size-check carry-along note is discharged.
README gains the build/ directory in the layout tree and splits the
source-map row across root build.zig / build/images.zig /
build/qemu.zig. testing.md points at the distributed per-package test
steps; threading.md, threading-plan.md, driver-model.md, efi.md,
display.md, system-requirements.md, and devices-csv.md's adding-a-
driver checklist stop describing the pre-package build.
2026-07-30 04:24:34 +01:00
Daniel Samson 3f9b6813f7 build: thread-test package opts back into threading
Wave C dropped the .threaded flag the old addThreadedUserBinary call
carried, so thread-test compiled single_threaded: the heap mutex was
compiled out and atomics lowered to plain ops while the fixture spawns
real kernel threads — the QEMU thread-alloc case caught it (corruption
under concurrent allocation), and the completion review confirmed the
same finding independently. All 12 QEMU thread cases pass again.
2026-07-30 04:24:34 +01:00
Daniel Samson bc2eb67581 build: phase 3 — split image assembly and QEMU runners out of the root
build/images.zig owns everything between built binaries and a bootable
volume: the FHS zig-out install tree, boot manifest + capsule, both
FAT32 images, the release ISO, and their check steps. build/qemu.zig
owns run-x86-64 / run-x86-64-gpu and the OVMF probing. The root
build.zig (461 lines, from 1,242 pre-split) now only decides what
ships: kernel + loader, the package list, the bundled boot tree, and
the aggregate test step. The stale commented-out run scaffold is gone.
Boot-image file list unchanged; check-fat-image and check-iso-image
both green.
2026-07-30 04:11:22 +01:00
Daniel Samson cb98a9844e build: phase 2 wave C — test fixtures build as packages
All thirteen /test/system/services fixtures convert on the pci-bus
template (thread-test threaded; crash-test/device-list/pci-cap-test/
iommu-fault-test carrying their protocol and PCI extras). With the last
addUserBinary caller gone, the root build's wrapper functions and its
default-imports plumbing are deleted — every user binary now reaches
the shared recipe only through its own package. Production boot-image
file list unchanged; a -Dtest-case build bundles the fixtures exactly
as before.
2026-07-30 04:07:19 +01:00
Daniel Samson 4701fbd123 build: phase 2 wave B — drivers build as packages
ps2-bus (bus + keyboard + mouse artifacts from one package),
usb-xhci-bus, usb-hid (keyboard + mouse), usb-storage, and virtio-gpu
convert on the pci-bus template. Their unit tests — PS/2 decode, HID
boot reports, Bulk-Only Transport/SCSI encodings, and the virtio-gpu
size checks the plan flagged as a wave carry-along — move into their
packages; the root aggregate delegates. Boot-image file list
unchanged.
2026-07-30 04:04:50 +01:00
Daniel Samson 3b23b11b0e 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.
2026-07-30 04:02:49 +01:00
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
Daniel Samson 15575960bd build: single-source the default import set; delegate library tests
Review findings from the pilot: the 17-module default import set was
maintained twice (root's array and build-support's userBinary) — drift
would silently make packaged binaries differ from root-built ones. It
now lives once, as build-support.defaultImports; root and userBinary
both draw from it, and root's user-binary wrapper names the shim +
link script through the kernel package like build-support does.
The root aggregate test step likewise duplicated the library domains'
14 test definitions; it now depends on each domain's own standalone
test step (host-only, so root's -Dtarget/-Doptimize deliberately do
not reach them), and the client domain gains an empty test step for
uniformity. Boot-image file list unchanged.
2026-07-26 23:10:10 +01:00
Daniel Samson 6f4fdc2789 build: phase 2 pilot — pci-bus builds as a package
build-support gains the domains-based userBinary: the default import set
(the library/kernel concern modules, driver/service clients, mmio,
acpi-ids, xkeyboard-config) is assembled from the domain packages, and
the root shim + user link script come from the kernel package directory.
system/drivers/pci-bus is the first binary package: a ~15-line
declarative build.zig naming only its extras (device-manager-protocol,
pci-class); the root build consumes the artifact for the boot image and
the driver also builds standalone from its own directory. Boot-image
file list unchanged.
2026-07-26 22:55:47 +01:00
Daniel Samson 721288c516 build: phase 1 — library domains become packages
Each library domain (kernel, device, client, protocol, csv,
xkeyboard-config) now owns a build.zig + build.zig.zon that wires and
exports its modules, with a standalone `zig build test` per domain.
The kernel package also exports abi (source stays in system/abi.zig) so
every consumer names one module instance. The root build swaps its
createModule calls for b.dependency(...).module(...) — no binary moves;
the root is the pilot consumer (docs/build-packages-plan.md). Path
dependencies deduplicate by resolved location, so the diamond
(root -> device -> kernel, root -> kernel) yields a single instance of
each module. Boot-image file list unchanged.
2026-07-26 22:53:53 +01:00
Daniel Samson 4194bb6e32 build: phase 0 — extract the build-support package
The shared user-binary recipe (freestanding target, root-shim wiring,
link-script and image-base settings) moves out of the root build into
build-support/, the package that is the single home for cross-cutting
build changes (docs/build-packages-plan.md). The root build's
addUserBinary/addThreadedUserBinary keep their signatures and delegate;
nothing else moves. Boot-image file list unchanged.
2026-07-26 22:49:05 +01:00
Daniel Samson fc0b934b7f docs: add the build-packages plan 2026-07-26 22:43:22 +01:00
106 changed files with 2857 additions and 1142 deletions
+173
View File
@@ -0,0 +1,173 @@
//! The danos build API (docs/build-packages-plan.md): the one shared recipe
//! for building a user-space binary. A binary package's build.zig names its
//! binary and EXACTLY the modules its source imports — the moral equivalent
//! of a C file's include list — and `userBinary` resolves each name from the
//! library domain package that exports it. Nothing is pre-wired: an @import
//! the package did not declare is a compile error, and a domain none of the
//! imports come from never appears in the package's manifest. The only
//! implicit dependency is the kernel package, because the shared root shim
//! (root.zig, user.ld) lives there and itself reaches start + logging.
//!
//! Consumers declare this package in their build.zig.zon (as "build-support")
//! and @import its build.zig from their own build.zig; nothing is compiled
//! from this package itself — it exports build-time functions only.
const std = @import("std");
pub fn build(b: *std.Build) void {
_ = b; // nothing to build: this package exports build-time functions only
}
/// The freestanding x86-64 target every danos binary (kernel and user) is
/// built for. SSE2 is part of the x86_64 baseline and UEFI leaves it enabled
/// at handoff, so we keep it: disabling it forces soft-float and makes the
/// compiler unable to encode the vector ops that std's formatting/runtime
/// still emit.
pub fn freestandingTarget(b: *std.Build) std.Build.ResolvedTarget {
return b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .freestanding,
.abi = .none,
});
}
/// Which library domain package exports each importable module — the one
/// name -> home table. When a domain grows a module, it gets a row here; a
/// binary naming a module whose home is missing from its own build.zig.zon
/// fails loudly at dependency resolution.
const ModuleHome = struct { name: []const u8, home: []const u8 };
const module_homes = [_]ModuleHome{
// library/kernel — the userspace private-ABI library, split by concern.
.{ .name = "abi", .home = "kernel" },
.{ .name = "system-call", .home = "kernel" },
.{ .name = "ipc", .home = "kernel" },
.{ .name = "time", .home = "kernel" },
.{ .name = "thread", .home = "kernel" },
.{ .name = "logging", .home = "kernel" },
.{ .name = "process", .home = "kernel" },
.{ .name = "file-system", .home = "kernel" },
.{ .name = "memory", .home = "kernel" },
.{ .name = "service", .home = "kernel" },
.{ .name = "start", .home = "kernel" },
// library/device — driver-side libraries + the flat reference data.
.{ .name = "mmio", .home = "device" },
.{ .name = "acpi-ids", .home = "device" },
.{ .name = "device-abi", .home = "device" },
.{ .name = "aml", .home = "device" },
.{ .name = "usb-abi", .home = "device" },
.{ .name = "usb-ids", .home = "device" },
.{ .name = "usb", .home = "device" },
.{ .name = "driver", .home = "device" },
.{ .name = "block", .home = "device" },
.{ .name = "pci", .home = "device" },
.{ .name = "pci-class", .home = "device" },
.{ .name = "device-registry", .home = "device" },
// library/client — userspace service clients.
.{ .name = "display-client", .home = "client" },
.{ .name = "input-client", .home = "client" },
// library/protocol — the wire protocols.
.{ .name = "vfs-protocol", .home = "protocol" },
.{ .name = "input-protocol", .home = "protocol" },
.{ .name = "block-protocol", .home = "protocol" },
.{ .name = "usb-transfer-protocol", .home = "protocol" },
.{ .name = "device-manager-protocol", .home = "protocol" },
.{ .name = "display-protocol", .home = "protocol" },
.{ .name = "scanout-protocol", .home = "protocol" },
.{ .name = "power-protocol", .home = "protocol" },
// library/csv — the /etc/*.csv helpers.
.{ .name = "csv", .home = "csv" },
// library/xkeyboard-config — keycode -> keysym/character tables.
.{ .name = "xkeyboard-config", .home = "xkeyboard-config" },
};
fn moduleHome(name: []const u8) ?[]const u8 {
for (module_homes) |entry| {
if (std.mem.eql(u8, entry.name, name)) return entry.home;
}
return null;
}
/// What `userBinary` needs to know about one user binary.
pub const UserBinaryOptions = struct {
name: []const u8,
/// The program's own source file — it becomes the `program` module the
/// root shim imports; a program only defines `pub fn main`.
root_source_file: std.Build.LazyPath,
/// Exactly the modules the program's source @imports (directly or through
/// its same-directory files) — no more, no less. Order is free; sorted
/// reads best. An undeclared @import fails the compile; a declared name no
/// domain exports fails the build graph with a pointer to module_homes.
imports: []const []const u8,
/// Built multi-threaded (`single_threaded = false`) so real atomics/TLS
/// work — required before a binary may call `Thread.spawn`
/// (docs/threading.md). Threads are a deliberate per-binary opt-in.
threaded: bool = false,
};
/// Build one user-space binary the same way for every program (init, the
/// services, the drivers): freestanding, ReleaseSmall, `.large` code model
/// (the image base is above 4 GiB — smaller models emit 32-bit relocations
/// that can't reach), linked with the shared user link script. Pinned to
/// LLVM + LLD so the script's PHDRS (segment permissions) are authoritative —
/// the kernel's W^X user-ELF loader requires exact perms.
///
/// The compilation root is not the program's own file but the shared shim
/// (the kernel package's root.zig), which supplies the root declarations
/// (`main` re-export, panic handler, `_start` pull) so a program only defines
/// `pub fn main`. The program's file becomes the `program` module the shim
/// imports; reach it through `programModule` to add per-binary non-library
/// modules (compile-time options).
pub fn userBinary(b: *std.Build, options: UserBinaryOptions) *std.Build.Step.Compile {
const kernel = b.dependency("kernel", .{});
var imports: std.ArrayListUnmanaged(std.Build.Module.Import) = .empty;
for (options.imports) |name| {
const home = moduleHome(name) orelse @panic(b.fmt(
"no library domain exports a module named '{s}' — if a domain grew it, add its row to module_homes in build-support/build.zig",
.{name},
));
const dependency = if (std.mem.eql(u8, home, "kernel")) kernel else b.dependency(home, .{});
imports.append(b.allocator, .{ .name = name, .module = dependency.module(name) }) catch @panic("OOM");
}
// Settings (target, optimize, code model, ...) live on the root module
// only; the program module inherits them.
const program_module = b.createModule(.{
.root_source_file = options.root_source_file,
.imports = imports.items,
});
const exe = b.addExecutable(.{
.name = options.name,
.root_module = b.createModule(.{
.root_source_file = kernel.path("root.zig"),
.target = freestandingTarget(b),
.optimize = .ReleaseSmall,
.code_model = .large,
.single_threaded = !options.threaded, // a threaded binary needs real atomics/TLS
.sanitize_c = .off,
.stack_check = false,
.stack_protector = false,
// The root shim itself imports only start (_start + panic) and
// logging (std_options) — straight from the kernel package, so a
// program's own import list stays exactly its own.
.imports = &.{
.{ .name = "start", .module = kernel.module("start") },
.{ .name = "logging", .module = kernel.module("logging") },
.{ .name = "program", .module = program_module },
},
}),
});
exe.setLinkerScript(kernel.path("user.ld"));
exe.entry = .{ .symbol_name = "_start" };
exe.image_base = 0x7000_0000_0000;
exe.use_llvm = true;
exe.use_lld = true;
return exe;
}
/// The `program` module of a binary built by `userBinary` — the module rooted
/// at the program's own source file. Per-binary non-library modules (an
/// addOptions build_options) go here, not on the root shim: module imports
/// are not transitive, so an import added to the root would be invisible to
/// the program's code.
pub fn programModule(exe: *std.Build.Step.Compile) *std.Build.Module {
return exe.root_module.import_table.get("program").?;
}
+8
View File
@@ -0,0 +1,8 @@
.{
.name = .build_support,
.version = "0.0.0",
.fingerprint = 0xad91962994f4be41, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{""},
}
+140 -929
View File
File diff suppressed because it is too large Load Diff
+43 -8
View File
@@ -32,6 +32,48 @@
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// The danos build API — the shared user-binary recipe every build file
// (root and per-binary packages) consumes (docs/build-packages-plan.md).
.@"build-support" = .{ .path = "build-support" },
// The library domains, each a package exporting its modules.
.kernel = .{ .path = "library/kernel" },
.device = .{ .path = "library/device" },
.client = .{ .path = "library/client" },
.protocol = .{ .path = "library/protocol" },
.csv = .{ .path = "library/csv" },
.@"xkeyboard-config" = .{ .path = "library/xkeyboard-config" },
// Binary packages (phase 2), consumed as artifacts for the boot image.
.@"pci-bus" = .{ .path = "system/drivers/pci-bus" },
.init = .{ .path = "system/services/init" },
.fat = .{ .path = "system/services/fat" },
.display = .{ .path = "system/services/display" },
.@"display-demo" = .{ .path = "system/services/display-demo" },
.@"device-manager" = .{ .path = "system/services/device-manager" },
.input = .{ .path = "system/services/input" },
.logger = .{ .path = "system/services/logger" },
// The discovery pair and the /test fixtures are lazy: only what a
// given build actually ships gets its build file loaded and compiled
// (-Ddiscovery picks one of the pair; -Dtest-case pulls the fixtures).
.acpi = .{ .path = "system/services/acpi", .lazy = true },
.fdt = .{ .path = "system/services/fdt", .lazy = true },
.@"ps2-bus" = .{ .path = "system/drivers/ps2-bus" },
.@"usb-xhci-bus" = .{ .path = "system/drivers/usb-xhci-bus" },
.@"usb-hid" = .{ .path = "system/drivers/usb-hid" },
.@"usb-storage" = .{ .path = "system/drivers/usb-storage" },
.@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" },
.@"vfs-test" = .{ .path = "test/system/services/vfs-test", .lazy = true },
.@"fat-test" = .{ .path = "test/system/services/fat-test", .lazy = true },
.@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server", .lazy = true },
.@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client", .lazy = true },
.@"crash-test" = .{ .path = "test/system/services/crash-test", .lazy = true },
.@"device-list" = .{ .path = "test/system/services/device-list", .lazy = true },
.@"pci-cap-test" = .{ .path = "test/system/services/pci-cap-test", .lazy = true },
.@"iommu-fault-test" = .{ .path = "test/system/services/iommu-fault-test", .lazy = true },
.@"input-source" = .{ .path = "test/system/services/input-source", .lazy = true },
.@"input-test" = .{ .path = "test/system/services/input-test", .lazy = true },
.@"args-echo" = .{ .path = "test/system/services/args-echo", .lazy = true },
.@"process-test" = .{ .path = "test/system/services/process-test", .lazy = true },
.@"thread-test" = .{ .path = "test/system/services/thread-test", .lazy = true },
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
@@ -70,12 +112,5 @@
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
.paths = .{""},
}
+166
View File
@@ -0,0 +1,166 @@
//! Boot-image assembly (docs/build-packages-plan.md, phase 3): everything
//! between "here are the built binaries" and "here is a bootable volume".
//! The FHS-shaped zig-out install tree, the boot manifest, the boot capsule,
//! the FAT32 USB image (+ its serial-enabled twin for the QEMU run steps),
//! and the release ISO — with their check steps. The root build.zig decides
//! WHAT ships (the bundled list); this file owns HOW it becomes an image.
const std = @import("std");
/// One user binary and its FHS home on the boot volume (and in zig-out).
pub const BundledBinary = struct { path: []const u8, binary: std.Build.LazyPath };
pub const Options = struct {
/// The installed/flashable kernel (serial follows the root -Dserial).
kernel: *std.Build.Step.Compile,
/// The serial-enabled kernel variant the `run-x86-64` image boots.
kernel_serial: *std.Build.Step.Compile,
/// The UEFI loader (BOOTX64).
efi: *std.Build.Step.Compile,
/// Every user binary and data file at its FHS path.
bundled: []const BundledBinary,
};
/// Wire up the install tree, both FAT32 boot images, the release ISO, and the
/// check steps. Returns the serial-enabled FAT image for the QEMU run steps.
pub fn addImageSteps(b: *std.Build, options: Options) std.Build.LazyPath {
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
// kernel at zig-out/system/kernel (from system/kernel/kernel.zig), init at
// zig-out/system/services/init, and so on (see docs/README.md). The bootloader
// then loads these FHS paths off the volume.
const kernel_install = b.addInstallArtifact(options.kernel, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
b.getInstallStep().dependOn(&kernel_install.step);
// UEFI firmware requires the removable-media loader at exactly \EFI\BOOT\BOOTX64.efi,
// so that path is fixed by the firmware (it is /boot's EFI stub, conceptually).
const efi_install = b.addInstallArtifact(options.efi, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
b.getInstallStep().dependOn(&efi_install.step);
// The boot manifest: the FHS path of every bundled binary, one per line. The
// EFI loader reads THIS by name and opens each listed path by name — FAT
// name lookup is case-insensitive and firmware-portable, unlike directory
// ENUMERATION, whose returned names vary by firmware (bare 8.3 entries come
// back uppercase on some FAT drivers). The tree walk remains only as the
// loader's fallback for hand-assembled sticks without a manifest.
var manifest_text: std.ArrayListUnmanaged(u8) = .empty;
for (options.bundled) |item| {
manifest_text.append(b.allocator, '/') catch @panic("OOM");
manifest_text.appendSlice(b.allocator, item.path) catch @panic("OOM");
manifest_text.append(b.allocator, '\n') catch @panic("OOM");
}
const manifest_files = b.addWriteFiles();
const manifest_file = manifest_files.add("manifest", manifest_text.items);
const manifest_install = b.addInstallFileWithDir(manifest_file, .prefix, "system/manifest");
b.getInstallStep().dependOn(&manifest_install.step);
// The boot capsule: the same bundled list packed into ONE file (v2
// initial_ramdisk format), because a single open + sequential read is the
// only firmware file I/O shape that is fast everywhere — a per-file tree
// walk measured MINUTES on real firmware. The loader tries this first,
// then the manifest, then the walk; the running system cannot tell the
// difference (it always receives the same in-RAM table). Derived from the
// tree in the same build graph, so the two cannot drift.
const mk_capsule = b.addSystemCommand(&.{"python3"});
mk_capsule.addFileArg(b.path("tools/pack-system-image.py"));
const capsule_img = mk_capsule.addOutputFileArg("system.img");
for (options.bundled) |item| {
mk_capsule.addArg(item.path);
mk_capsule.addFileArg(item.binary);
}
const capsule_install = b.addInstallFile(capsule_img, "boot/system.img");
b.getInstallStep().dependOn(&capsule_install.step);
// Install every bundled binary to its FHS home, so zig-out is a true image of
// the filesystem — the same tree make-fat-image.py lays out on the boot volume.
for (options.bundled) |item| {
const install = b.addInstallFileWithDir(item.binary, .prefix, item.path);
b.getInstallStep().dependOn(&install.step);
}
// --- danos-usb.img: the bootable FAT32 USB image ---
// Format a real FAT32 image (the in-repo Python builder, no external tools)
// holding the EFI stub, the kernel, and the whole /system tree of user
// binaries at their FHS paths. QEMU presents this image as a USB mass-storage
// device the guest boots from (see run-x86-64 and the test harness), and the
// danos fat driver mounts the same image at /mnt/usb.
const fat_image = addBootImage(b, options.kernel.getEmittedBin(), options.efi.getEmittedBin(), manifest_file, capsule_img, options.bundled);
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
b.getInstallStep().dependOn(&fat_image_install.step);
// The image `run-x86-64` boots: identical to the flashable one but with the
// serial log sink compiled in, so a developer always gets the machine-readable
// log captured to serial0 — without baking serial into the image users flash.
// Built lazily (only when `run-x86-64` is requested), and never installed.
const fat_image_serial = addBootImage(b, options.kernel_serial.getEmittedBin(), options.efi.getEmittedBin(), manifest_file, capsule_img, options.bundled);
// `zig build check-fat-image` — validate the produced image is a real FAT32
// with the EFI stub present (the builder's own --verify, no external tools).
const check_fat = b.addSystemCommand(&.{"python3"});
check_fat.addFileArg(b.path("tools/make-fat-image.py"));
check_fat.addArg("--verify");
check_fat.addFileArg(fat_image);
const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
check_fat_step.dependOn(&check_fat.step);
// --- release-x86-64: danos-x86-64.iso, the flashable release image ---
// Wrap the FAT32 boot volume in a hybrid ISO (the in-repo Python builder
// again, no xorriso/isohybrid): an ISO9660 whose El Torito EFI boot entry
// and MBR ESP partition entry both point at the embedded FAT image. One
// file then boots every way release media is consumed — flashed raw to a
// USB stick with Etcher or dd, or burned to optical media — while
// danos-usb.img stays the raw superfloppy QEMU and the test harness boot.
const mk_iso = b.addSystemCommand(&.{"python3"});
mk_iso.addFileArg(b.path("tools/make-iso-image.py"));
const iso_image = mk_iso.addOutputFileArg("danos-x86-64.iso");
mk_iso.addFileArg(fat_image);
const iso_install = b.addInstallFile(iso_image, "danos-x86-64.iso");
const release_step = b.step("release-x86-64", "Build the flashable x86-64 release ISO (zig-out/danos-x86-64.iso; flash with Etcher or dd)");
release_step.dependOn(&iso_install.step);
// `zig build check-iso-image` — the ISO builder's own --verify (mirroring
// check-fat-image): the MBR partition, the El Torito catalog, and the
// embedded FAT32 image must all agree.
const check_iso = b.addSystemCommand(&.{"python3"});
check_iso.addFileArg(b.path("tools/make-iso-image.py"));
check_iso.addArg("--verify");
check_iso.addFileArg(iso_image);
const check_iso_step = b.step("check-iso-image", "Verify the release ISO is a valid hybrid (MBR ESP partition + El Torito EFI entry)");
check_iso_step.dependOn(&check_iso.step);
return fat_image_serial;
}
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding the
/// EFI stub, the kernel, and every user binary at its FHS path — the volume's
/// /system tree IS the system image; the EFI loader walks it at boot and builds
/// the in-RAM initial_ramdisk from it. Factored so the serial-enabled
/// `run-x86-64` variant can bundle its own serial kernel while sharing the
/// loader and user tree (the loader's boot breadcrumbs and init's heartbeat both
/// follow the top-level -Dserial). Returns the image's LazyPath.
fn addBootImage(
b: *std.Build,
kernel_bin: std.Build.LazyPath,
efi_bin: std.Build.LazyPath,
manifest: std.Build.LazyPath,
capsule: std.Build.LazyPath,
bundled: []const BundledBinary,
) std.Build.LazyPath {
const mk_fat = b.addSystemCommand(&.{"python3"});
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
mk_fat.addArg("64"); // MiB
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
mk_fat.addFileArg(efi_bin);
mk_fat.addArg("system/kernel");
mk_fat.addFileArg(kernel_bin);
mk_fat.addArg("system/manifest");
mk_fat.addFileArg(manifest);
mk_fat.addArg("boot/system.img");
mk_fat.addFileArg(capsule);
for (bundled) |item| {
mk_fat.addArg(item.path);
mk_fat.addFileArg(item.binary);
}
return fat_image;
}
+176
View File
@@ -0,0 +1,176 @@
//! The QEMU run steps (docs/build-packages-plan.md, phase 3): `run-x86-64`
//! boots the serial-enabled FAT image via UEFI/OVMF; `run-x86-64-gpu` adds a
//! virtio-gpu adapter for the native-present display path. OVMF firmware is
//! probed across distro/OS layouts (-Dovmf-code / -Dovmf-vars override).
const std = @import("std");
/// Wire up the `run-x86-64` and `run-x86-64-gpu` steps around the given
/// serial-enabled boot image (the guest boots that self-contained image
/// attached as USB storage, not the installed FHS zig-out).
pub fn addRunSteps(b: *std.Build, fat_image_serial: std.Build.LazyPath) void {
// Firmware lives in different places per OS/distro, so probe the known
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
// that exists. Override with -Dovmf-code / -Dovmf-vars if yours is elsewhere.
const ovmf_code = b.option(
[]const u8,
"ovmf-code",
"Path to the OVMF_CODE firmware image",
) orelse firstExisting(b.graph.io, &.{
"/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Architecture
"/usr/share/OVMF/OVMF_CODE_4M.fd", // Debian/Ubuntu
"/usr/share/OVMF/OVMF_CODE.fd", // older Debian/Ubuntu
"/usr/share/edk2-ovmf/x64/OVMF_CODE.fd", // Fedora
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Apple Silicon)
"/usr/local/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Intel)
});
const ovmf_vars = b.option(
[]const u8,
"ovmf-vars",
"Path to the OVMF_VARS firmware image (a writable copy is made)",
) orelse firstExisting(b.graph.io, &.{
"/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Architecture
"/usr/share/OVMF/OVMF_VARS_4M.fd", // Debian/Ubuntu
"/usr/share/OVMF/OVMF_VARS.fd", // older Debian/Ubuntu
"/usr/share/edk2-ovmf/x64/OVMF_VARS.fd", // Fedora
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Apple Silicon)
"/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel)
});
// The firmware needs to write NVRAM, so give it a writable copy of the vars.
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
const vars_out = vars_copy.addOutputFileArg("OVMF_VARS.4m.fd");
// Capture the guest's serial0 (danos's machine-readable log) to the qemu-test
// scratch area — a dev/host artifact, kept out of the FHS boot volume we mount.
// (/var/log/system is reserved for the kernel's own logging system later.) One
// timestamped file per run.
const log_dir = b.fmt("{s}/qemu-test", .{b.install_path});
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
const run_efi = b.addSystemCommand(&.{
"qemu-system-x86_64",
"-device",
"qemu-xhci,id=xhci",
"-device",
"usb-mouse,bus=xhci.0",
"-device",
"usb-kbd,bus=xhci.0",
"-machine",
"q35",
"-m",
"128M",
"-drive",
b.fmt("if=pflash,format=raw,readonly=on,file={s}", .{ovmf_code}),
});
run_efi.addArg("-drive");
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
// Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
// The serial-enabled variant, so serial0 carries the log for this dev boot.
run_efi.addArg("-drive");
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
run_efi.addArgs(&.{
"-device",
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
"-net",
"none",
// Emulated display advertising 1280x720 as its native (EDID preferred)
// resolution, so the kernel's native-resolution switch has something to
// find. `-vga none` avoids a second, default adapter.
"-vga",
"none",
"-device",
"VGA,edid=on,xres=1280,yres=720",
});
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ log_dir, timestamp(b) });
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
// We boot the self-contained `fat_image_serial` (added as a file arg above, so
// it's already a dependency) — not the installed FHS zig-out — so `run-x86-64`
// builds only the serial kernel, never the flashable one. Just make the serial
// scratch dir first.
run_efi.step.dependOn(&make_log_dir.step);
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
run_efi_step.dependOn(&run_efi.step);
// --- run-x86-64-gpu: the same boot plus a virtio-gpu adapter ---
// The VGA device still supplies the boot (GOP) framebuffer the compositor starts
// on; the virtio-gpu function is discovered by the device-manager stack, its
// driver announces a shared scanout, and the compositor upgrades off the GOP
// floor to fenced, tear-free native presents (docs/display-v2.md).
// This is the interactive twin of the `display-native` test case, and 512M
// matches it (the whole driver stack + the compositor's surfaces at once).
// QEMU shows one head per adapter: pick the virtio-gpu head in the View menu
// to watch the native output.
const run_gpu = b.addSystemCommand(&.{
"qemu-system-x86_64",
"-device",
"qemu-xhci,id=xhci",
"-device",
"usb-mouse,bus=xhci.0",
"-device",
"usb-kbd,bus=xhci.0",
"-machine",
"q35",
"-m",
"512M",
"-drive",
b.fmt("if=pflash,format=raw,readonly=on,file={s}", .{ovmf_code}),
});
run_gpu.addArg("-drive");
run_gpu.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
run_gpu.addArg("-drive");
run_gpu.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
run_gpu.addArgs(&.{
"-device",
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
"-net",
"none",
"-vga",
"none",
"-device",
"VGA,edid=on,xres=1280,yres=720",
"-device",
"virtio-gpu-pci",
});
const gpu_serial_log = b.fmt("{s}/run-x86-64-gpu-serial0-{s}.log", .{ log_dir, timestamp(b) });
run_gpu.addArgs(&.{ "-serial", b.fmt("file:{s}", .{gpu_serial_log}) });
run_gpu.step.dependOn(&make_log_dir.step);
const run_gpu_step = b.step("run-x86-64-gpu", "Boot in QEMU with a virtio-gpu adapter: the compositor upgrades to fenced (tear-free) native presents; watch the virtio-gpu head in QEMU's View menu");
run_gpu_step.dependOn(&run_gpu.step);
}
/// Return the first path in `candidates` that exists on the build host, else the
/// first candidate as a fallback so a missing-firmware error still names a
/// concrete (and, by convention, the primary) path. Used to locate OVMF firmware
/// across distro/OS layouts without configuration.
fn firstExisting(io: std.Io, candidates: []const []const u8) []const u8 {
for (candidates) |path| {
std.Io.Dir.accessAbsolute(io, path, .{}) catch continue;
return path;
}
return candidates[0];
}
/// A UTC timestamp like "20260708-153045", for naming a per-run artifact so
/// repeated runs don't clobber each other's logs. Resolved when `zig build`
/// runs, which is moments before QEMU launches.
fn timestamp(b: *std.Build) []const u8 {
const ns = std.Io.Clock.now(.real, b.graph.io).nanoseconds;
const secs: u64 = @intCast(@divFloor(ns, std.time.ns_per_s));
const es = std.time.epoch.EpochSeconds{ .secs = secs };
const yd = es.getEpochDay().calculateYearDay();
const md = yd.calculateMonthDay();
const ds = es.getDaySeconds();
return b.fmt("{d:0>4}{d:0>2}{d:0>2}-{d:0>2}{d:0>2}{d:0>2}", .{
yd.year,
md.month.numeric(),
@as(u32, md.day_index) + 1,
ds.getHoursIntoDay(),
ds.getMinutesIntoHour(),
ds.getSecondsIntoMinute(),
});
}
+14 -1
View File
@@ -263,9 +263,20 @@ test/ → /test the test tree: the QEMU harness (qemu_test.py, h
system/services/ beside the on-image test fixtures — vfs-test/ thread-test/
crash-test/ … — whose repo path IS their boot-volume path
(/test/system/services/<name>)
build-support/ the danos build API (build-time only, nothing on the image):
the shared user-binary recipe + default-import wiring every
build file consumes (docs/build-packages-plan.md)
build/ root-build helpers: image assembly (images.zig) + the QEMU
run steps (qemu.zig)
tools/ host-side build scripts
```
**Builds are packages** (docs/build-packages-plan.md): each `library/` domain owns a
`build.zig`/`build.zig.zon` exporting its modules (with a standalone `zig build test`),
every binary directory is a ~15-line package build, and the root `build.zig`
orchestrates — the kernel + loader, what ships, and the aggregate test step — with
image assembly in `build/images.zig` and the QEMU run steps in `build/qemu.zig`.
**Wire protocols live in `library/protocol/`**, one module per directory
(`library/protocol/vfs/vfs-protocol.zig` is the `vfs-protocol` module), imported by module
name. A protocol is the seam between a low-level driver and the higher-level service it
@@ -320,5 +331,7 @@ exception in [coding-standards.md](coding-standards.md) applies to that seam.
| System services (init, the `fat` filesystem, the device-manager) | `system/services/` |
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
| On-image test fixtures for the QEMU cases (`vfs-test`, `crash-test`, `thread-test`, …) → `/test/system/services` | `test/system/services/` |
| Build + `run-x86-64` (QEMU/OVMF) + `release-x86-64` (the flashable ISO) | `build.zig` |
| Build orchestration (kernel + loader, what ships, the aggregate test step) | `build.zig` (root; the shared user-binary recipe is `build-support/`, and each `library/` domain + binary package carries its own `build.zig`) |
| Image assembly + `release-x86-64` (the flashable ISO) | `build/images.zig` |
| `run-x86-64` / `run-x86-64-gpu` (QEMU/OVMF) | `build/qemu.zig` |
| QEMU integration test harness | `test/qemu_test.py` |
+177
View File
@@ -0,0 +1,177 @@
# Plan: packages — hierarchical builds for libraries and binaries
**Status: complete** (branch `claude/build-packages-plan-174144`). Phase 0
(`build-support`), phase 1 (all six library domains), phase 2 (every binary —
the pci-bus pilot first, then services, drivers, and test fixtures in waves;
multi-binary directories like ps2-bus and usb-hid are one package exporting
several artifacts, and the acpi/fdt discovery pair each export an artifact
named "discovery" that the root's -Ddiscovery picks between), and phase 3 (the
root split into `build/images.zig` + `build/qemu.zig`; the root `build.zig` is
~460 lines of orchestration, down from ~1,250). Every phase landed green: unit
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. 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
`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:
- **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.
- **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
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
three places in a 1,250-line file.
4. `lazyDependency` lets an image target build only what it ships: the /test
fixtures resolve only under -Dtest-case, and only the -Ddiscovery-selected
discovery package ever loads.
## 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 (done). Wave B: the
remaining drivers (done). Wave C: test fixtures (done). Root build shrank to
orchestration per wave. init's `-Dserial` heartbeat flag rides a dependency
option; a directory with several binaries (ps2-bus, usb-hid) is one package
exporting several artifacts.
**Phase 3 — root cleanup (done).** What remained of the root build split into
`build/images.zig` (the FHS install tree, boot manifest + capsule, FAT32
images, release ISO, check steps) and `build/qemu.zig` (the run steps + OVMF
probing), 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 new-driver checklist's build step
(docs/device-driver-development/new-driver-checklist.md, step 2) is already
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, 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`) 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.
- Package unit tests live in each package's own `test` step; the root
aggregate depends on every test-bearing package's step, so `zig build test`
at the root still runs everything.
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
`userBinary`/`userBinaryFromImports` bodies in build-support/build.zig. 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) — they moved into their binaries' packages with their waves,
discharging the carry-along obligation.
- Doc updates ride each phase: docs/README.md (repo layout + source map),
docs/device-driver-development/new-driver-checklist.md (step 2) and
devices-csv.md ("Adding a driver"), and the docs that cite the build recipe
(driver-model.md, threading.md, system-requirements.md) reference build
shapes that keep changing.
@@ -96,7 +96,15 @@ in different namespaces — against the right `bus` column.
## Adding a driver
1. Build the driver binary and bundle it at `/system/drivers/<name>` (build.zig).
(The step-by-step walkthrough with a worked example is
[new-driver-checklist.md](new-driver-checklist.md).)
1. Create `system/drivers/<name>/` with the driver source plus a ~15-line
package `build.zig` + `build.zig.zon` (copy an existing driver package,
e.g. `system/drivers/pci-bus/`; per-driver extras go through
`build_support.programModule`). Then bundle it at `/system/drivers/<name>`:
one dependency + one bundled entry in the root `build.zig`, one line in the
root `build.zig.zon`.
2. Add a row to `etc/devices.csv` naming the identity it binds and its full path.
No device-manager change is required — the registry is the seam.
@@ -20,9 +20,10 @@ Read [display.md](display.md) first for the *why*; this is the *what* and the *o
Follow [coding-standards.md](../coding-standards.md): spell out non-acronym abbreviations in
full, kebab-case file names, no `Co-Authored-By` trailers on commits. New user binaries
go through `addUserBinary` in [build.zig](../../build.zig) and get packed into the
initial-ramdisk; protocols are `b.addModule("…-protocol", …)` and imported into the
`runtime` module.
go through build-support's shared user-binary recipe and get packed into the
initial-ramdisk; protocols are modules exported by the `library/protocol` package.
(This section predates the build-packages split; see
[build-packages-plan.md](../build-packages-plan.md) for the current build shape.)
## How to verify along the way
@@ -18,9 +18,11 @@ lands on its own and ends in a **verifiable gate** — shaped for a `/loop` run,
Follow [coding-standards.md](../coding-standards.md): spell out non-acronym abbreviations,
kebab-case file names, no `Co-Authored-By` trailers. New user binaries go through
`addUserBinary` and get packed into the initial-ramdisk; protocols are
`b.addModule("…-protocol", …)` imported into `runtime`; new syscalls extend
[abi.zig](../../system/abi.zig) `SystemCall` + a `library/runtime` wrapper.
build-support's shared user-binary recipe and get packed into the initial-ramdisk;
protocols are modules exported by the `library/protocol` package; new syscalls extend
[abi.zig](../../system/abi.zig) `SystemCall` + a `library/kernel` wrapper.
(This section predates the build-packages split; see
[build-packages-plan.md](../build-packages-plan.md) for the current build shape.)
## How to verify along the way
+1 -1
View File
@@ -29,7 +29,7 @@ which one you're holding decides what you can do.
- **The PCI class-0x03 device is the raw controller** — BARs, config space, registers,
IO ports. It is what you actually *own* after boot. On QEMU's emulated adapter
([`-device VGA,edid=on`](../../build.zig), the Bochs VBE/DISPI model) the `base` GOP handed
([`-device VGA,edid=on`](../../build/qemu.zig), the Bochs VBE/DISPI model) the `base` GOP handed
you *is* that device's linear-framebuffer BAR — the same physical memory, seen through
a different door. On a real discrete GPU, GOP's `base` is an aperture inside the GPU's
VRAM BAR. danos already decodes this device
+11 -7
View File
@@ -138,13 +138,17 @@ 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: [`addUserBinary`](build.zig) 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(...)`. That's the *entire* mechanism — Zig modules
already give you everything else.
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*
logic module.** `usb-hid` imports `usb` (the transfer client) and `input-protocol`, never
@@ -0,0 +1,218 @@
# New driver: the minimum steps
The shortest path from "a device shows up in the boot log" to "my process is
running with its registers mapped". This is the checklist; the reasoning behind
every step lives in [Writing a driver](drivers.md), the matching rules in
[devices.csv](devices-csv.md), and interrupts in
[device interrupts](device-interrupts.md).
Worked example throughout: the Intel UHD 750 iGPU, which the boot log reports as
```
pci-bus: 0:2.0 bus=pci base=03 class=00 prog_if=00 vendor=8086 device=4C8A ...
```
## 1. Create the source file
`system/drivers/<name>/<name>.zig` — kebab-case, abbreviations spelled out
([coding standards](../coding-standards.md)). The directory name, the binary
name, and the `devices.csv` driver path must all agree; a mismatch fails
silently (the device-manager logs the spawn failure, nothing else happens).
The complete minimal driver — claims its device, logs every resource, maps the
register window, then sleeps in the harness loop:
```zig
//! /system/drivers/intel-uhd-graphics-750 — spawned by the device manager with
//! the device-tree id as argv[1]; claims that device and no other.
const std = @import("std");
const device = @import("driver");
const ipc = @import("ipc");
const memory = @import("memory");
const process = @import("process");
const service = @import("service");
/// No protocol yet: the kernel's IPC ceiling (MESSAGE_MAXIMUM) sizes the buffers.
const message_maximum = 256;
var controller_id: u64 = 0;
var register_base: usize = 0;
fn initialise(endpoint: ipc.Handle) bool {
_ = endpoint; // needed later, for irq binding and timers
if (!device.claim(controller_id)) {
std.log.err("unable to claim device {d}", .{controller_id});
return false;
}
// Fetch our own descriptor back for the device's resources.
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch return false;
defer memory.allocator().free(buffer);
const total = device.enumerate(buffer);
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
if (d.id == controller_id) break d;
} else {
std.log.err("device {d} not in the device tree", .{controller_id});
return false;
};
// Log every resource BEFORE choosing one (see step 5).
var register_index: u64 = 0;
for (descriptor.resources[0..@intCast(descriptor.resource_count)], 0..) |resource, index| {
std.log.info("resource {d}: kind={d} start=0x{x} len=0x{x}", .{
index, resource.kind, resource.start, resource.len,
});
// The 16 MiB window is GTTMMADR, the register BAR (this device also has
// a 256 MiB memory BAR, GMADR — "first memory resource" would be wrong).
if (resource.kind == @intFromEnum(device.ResourceKind.memory) and
resource.len == 16 * 1024 * 1024) register_index = index;
}
if (register_index == 0) {
std.log.err("register BAR not found", .{});
return false;
}
register_base = device.mmioMap(controller_id, register_index) orelse {
std.log.err("mmio_map failed", .{});
return false;
};
std.log.info("registers mapped at 0x{x}", .{register_base});
return true;
}
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = message;
_ = reply;
_ = sender;
_ = capability;
return 0; // no protocol yet; the zero-length ping is answered by the harness
}
pub fn main(init: process.Init) void {
const argument = init.arguments.get(1) orelse {
std.log.err("missing device id (argv[1])", .{});
return;
};
controller_id = std.fmt.parseInt(u64, argument, 10) catch {
std.log.err("malformed device id '{s}'", .{argument});
return;
};
service.run(message_maximum, .{
.init = initialise,
.on_message = onMessage,
// .on_notification only once an IRQ or timer is bound
});
}
```
`claim` is the capability gate: MMIO mapping, DMA grants, and IRQ binding all
require it, and it pins the IOMMU domain to this process
([drivers.md — claim before touch](drivers.md#the-capability-claim-before-touch)).
## 2. Create the build package and register it in the root build
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 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"),
.imports = &.{ "driver", "ipc", "memory", "process", "service" },
});
b.installArtifact(exe);
}
```
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
`virtio_gpu_package` to land in the right places),
```zig
const intel_uhd_graphics_750_exe = b.dependency("intel-uhd-graphics-750", .{}).artifact("intel-uhd-graphics-750");
```
```zig
.{ .path = "system/drivers/intel-uhd-graphics-750", .binary = intel_uhd_graphics_750_exe.getEmittedBin() },
```
and the path entry in the root `build.zig.zon`:
```zig
.@"intel-uhd-graphics-750" = .{ .path = "system/drivers/intel-uhd-graphics-750" },
```
Without the boot-tree row the binary never reaches the image and the
device-manager has nothing to spawn. (The package also builds standalone:
`cd system/drivers/intel-uhd-graphics-750 && zig build`.)
## 3. Add the match rule to `etc/devices.csv`
One row: bus, class triplet, vendor/device, driver path. **Copy the class
triplet from the pci-bus boot log line, not from another row** — for the iGPU
above the correct rule is
```
pci, 03, 00, 00, 8086, 4C8A, *, *, /system/drivers/intel-uhd-graphics-750
```
Field-by-field rules and the most-specific-wins policy: [devices.csv](devices-csv.md).
The registry is authoritative: an unmatched device is logged unbound, never
guessed — so a wrong nibble here means the driver simply never starts.
## 4. First contact: read, predict, verify
Before writing any register, read one whose value you can predict from state
the firmware already programmed (for a display controller: the pipe source
size of the live mode). Registers are volatile loads at `register_base +
offset`, where `offset` is what the device's manual lists:
```zig
fn read32(offset: usize) u32 {
return @as(*volatile u32, @ptrFromInt(register_base + offset)).*;
}
```
A matching read proves the whole chain — CSV match, spawn, claim, BAR choice,
mapping — with zero risk to the hardware.
## 5. Verify the plumbing
- `zig build test` still passes.
- On the image: `/var/log/<boot-stamp>/system/services/device-manager.log`
shows `spawned <name> for device <N>`, and
`/var/log/<boot-stamp>/system/drivers/<name>.log` holds the resource list and
your first read.
- If the driver did not spawn, diagnose in this order: binary on the image
(step 2) → CSV row matches the log line exactly (step 3) → path identical in
both (step 1).
## Later, when the device needs them
- **Interrupts**: MSI/MSI-X via the `pci` module, delivered as notifications to
`on_notification` — see [device interrupts](device-interrupts.md) and the
xHCI driver's `setupMsi` (QEMU trap documented there: enable MSI-X before
unmasking the device's own interrupt-enable bit).
- **DMA**: grant-backed buffers, bounded by the IOMMU domain established at
claim time ([driver model](driver-model.md)).
- **Children**: a bus driver publishes what it finds via `device_register`
([drivers.md — publishing children](drivers.md#publishing-children-device_register)).
- **A protocol**: replace `message_maximum` with the protocol's own maximum and
dispatch on the operation word in `onMessage` — every service under
`system/services/` is an example.
+3 -2
View File
@@ -24,8 +24,9 @@ EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64
```
The boot volume is **FHS-shaped** (see the repository-layout note in
[README.md](../README.md)): `build.zig` installs `boot/efi.zig` (built for the `uefi`
target) at `EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays
[README.md](../README.md)): the root `build.zig` compiles `boot/efi.zig` (built
for the `uefi` target) and `build/images.zig` places it at
`EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays
the rest out by FHS path: the kernel at `system/kernel`, init at
`system/services/init`, the pre-packed boot capsule at `boot/system.img`
([system-image.md](system-image.md)).
+6 -5
View File
@@ -22,11 +22,12 @@ lands on its own and ends in a **verifiable gate** — shaped for a `/loop` run,
## Conventions
Follow [coding-standards.md](../coding-standards.md): spell out non-acronym abbreviations,
kebab-case file names, no `Co-Authored-By` trailers. New user binaries go through
`addUserBinary` (with the new `threaded` flag where a binary spawns threads) and get
packed into the initial-ramdisk; new syscalls extend [abi.zig](../../system/abi.zig)
`SystemCall` + a `library/runtime` wrapper; test services live beside the code they
exercise and register a `ServiceId` if they must be looked up.
kebab-case file names, no `Co-Authored-By` trailers. New user binaries are
packages whose build.zig calls `build_support.userBinary` (with `.threaded =
true` where a binary spawns threads) and get packed into the initial-ramdisk;
new syscalls extend [abi.zig](../../system/abi.zig) `SystemCall` + a
`library/kernel` wrapper; test services live beside the code they exercise and
register a `ServiceId` if they must be looked up.
## How to verify along the way
+8 -6
View File
@@ -61,9 +61,10 @@ runtime — rebuilt in lockstep — knows the mapping.
backend would either bake danos syscall numbers into std (breaking ABI privacy and
renumbering) or fork std to route back through the runtime — a permanent rebase
cost that buys nothing the native type doesn't.
2. **Our user binaries are built `single_threaded = true`** ([build.zig](../../build.zig)
`addUserBinary`), which compiles threading out entirely and makes atomics and TLS
single-threaded. Threads need this flipped per binary regardless.
2. **Our user binaries are built `single_threaded = true`** (the shared recipe in
[build-support/build.zig](../../build-support/build.zig)), which compiles threading
out entirely and makes atomics and TLS single-threaded. Threads need this flipped
per binary regardless.
So we take the *shape* of `std.Thread`, not the *type*. The cost of replicating the
surface (spawn/join/Mutex/Condition) is small; the cost of the std type is the ABI
@@ -236,9 +237,10 @@ see the intro). Two scoped pieces, as built:
### Build: multi-threaded codegen, opt-in
A binary opts in by being added with `addThreadedUserBinary` — as `addUserBinary`,
but the shared implementation builds it `single_threaded = false` — so atomics and
(later) TLS are real. Threads and atomics are unsound in a `single_threaded` image,
A binary opts in with `.threaded = true` in its package's
`build_support.userBinary` call — the shared recipe in build-support then builds it
`single_threaded = false` — so atomics
and (later) TLS are real. Threads and atomics are unsound in a `single_threaded` image,
so a binary must opt in **before** it may call `Thread.spawn`. Everyone else
stays single-threaded and lean.
+3 -3
View File
@@ -95,9 +95,9 @@ hypervisor configured for UEFI firmware and an xHCI USB controller.
| Requirement | Detail | Source |
|---|---|---|
| **x86-64, 64-bit only** | Kernel and loader are built exclusively for `x86_64`; the loader rejects any non-x86-64 kernel ELF (`error.WrongArchitecture`). | `build.zig:481`, `boot/efi.zig:622` |
| **x86-64, 64-bit only** | Kernel and loader are built exclusively for `x86_64`; the loader rejects any non-x86-64 kernel ELF (`error.WrongArchitecture`). | `build-support/build.zig` (`freestandingTarget`), `boot/efi.zig:622` |
| **Long mode + PAE + NX** | AP trampoline sets `CR4.PAE`, `EFER.LME`, `EFER.NXE`; NX is used in kernel page-table entries. | `system/kernel/architecture/x86_64/trampoline.s:62` |
| **SSE / SSE2** | Baseline: the compiler emits SSE for ordinary struct copies. Trampoline enables `CR4.OSFXSR` + `OSXMMEXCPT` and clears `CR0.EM`. | `build.zig:477`, `trampoline.s:62` |
| **SSE / SSE2** | Baseline: the compiler emits SSE for ordinary struct copies. Trampoline enables `CR4.OSFXSR` + `OSXMMEXCPT` and clears `CR0.EM`. | `build-support/build.zig` (`freestandingTarget`), `trampoline.s:62` |
| **`syscall` / `sysret`** | Primary user↔kernel entry path. `EFER.SCE` enabled; `STAR`/`LSTAR`/`SFMASK` programmed per core. (`int 0x80` exists as a parallel gate.) | `architecture/x86_64/per-cpu.zig:71`, `isr.s:196` |
| **Local APIC (xAPIC)** | LAPIC accessed via MMIO at `0xFEE00000`. LAPIC ID read as a `u8` — classic xAPIC. **x2APIC is not supported** (no MSR path). | `apic.zig:67`, `apic.zig:646` |
| **CPUID + RDTSC** | CPUID leaf `0x15` for TSC frequency; RDTSC is the monotonic clock. | `apic.zig:333`, `apic.zig:113` |
@@ -108,7 +108,7 @@ hypervisor configured for UEFI firmware and an xHCI USB controller.
- **UEFI only.** A custom UEFI application loader is installed to
`\EFI\BOOT\BOOTX64.efi`. There is **no BIOS, multiboot, or limine** path. The
loader tolerates UEFI Class-3 machines with no legacy PIC/PIT.
(`build.zig:246`, `boot/efi.zig`)
(`build/images.zig` — the EFI/BOOT install — and `boot/efi.zig`)
- **ACPI is the hardware-discovery mechanism.** The RSDP is taken from the UEFI
configuration table (ACPI 2.0 GUID preferred, 1.0 fallback). Without a valid
RSDP there is **no device discovery** — no SMP, no IOAPIC routing, no PCI/USB.
+3 -1
View File
@@ -12,7 +12,9 @@ There are two layers:
`system/abi.zig`, `library/device/model/device-abi.zig`) now spans ~26 modules:
protocol and on-wire definitions (VFS, USB, virtio-gpu), the FAT engine, the
display compositor, PS/2 and HID decoding, the kernel log ring, and the
runtime's `time`/`thread` — the full list is the test step in `build.zig`.
runtime's `time`/`thread` — the list is distributed across the library-domain
and binary packages' own `test` steps, which the root `zig build test`
aggregates (docs/build-packages-plan.md).
These compile for the host and run natively.
- **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel
and check its behaviour. This is the interesting part.
+38
View File
@@ -0,0 +1,38 @@
//! The "client" library domain (library/client): userspace-service clients —
//! they talk to services over IPC, not to the kernel. Client modules end in
//! `-client` the way wire protocols end in `-protocol`, so a service, its
//! protocol, and its client never share a name (`display` the service,
//! `display-protocol` the wire contract, `display-client` a program's view).
const std = @import("std");
pub fn build(b: *std.Build) void {
const kernel = b.dependency("kernel", .{});
const protocol = b.dependency("protocol", .{});
const ipc = kernel.module("ipc");
const time = kernel.module("time");
_ = b.addModule("display-client", .{
.root_source_file = b.path("display/display-client.zig"),
.imports = &.{
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "display-protocol", .module = protocol.module("display-protocol") },
},
});
_ = b.addModule("input-client", .{
.root_source_file = b.path("input/input-client.zig"),
.imports = &.{
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "input-protocol", .module = protocol.module("input-protocol") },
},
});
// Standalone `zig build test`, kept for uniformity across the domains (the
// root aggregate depends on every domain's test step). The clients have no
// host-runnable unit tests yet — they are thin IPC conversation wrappers —
// so the step is empty until one grows some.
_ = b.step("test", "Run the client unit tests (none yet)");
}
+13
View File
@@ -0,0 +1,13 @@
.{
.name = .client,
.version = "0.0.0",
.fingerprint = 0xc74404553e73d4ff, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// The clients converse over ipc with time-bounded waits.
.kernel = .{ .path = "../kernel" },
// Each client speaks its service's wire protocol.
.protocol = .{ .path = "../protocol" },
},
.paths = .{""},
}
+20
View File
@@ -0,0 +1,20 @@
//! The "csv" library domain: 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 std = @import("std");
pub fn build(b: *std.Build) void {
_ = b.addModule("csv", .{ .root_source_file = b.path("csv.zig") });
// Standalone `zig build test` for this domain alone; the root build keeps
// its aggregate test step.
const test_step = b.step("test", "Run the csv unit tests");
const csv_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("csv.zig"),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(csv_tests).step);
}
+8
View File
@@ -0,0 +1,8 @@
.{
.name = .csv,
.version = "0.0.0",
.fingerprint = 0x8a4525791f4e5b6, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{""},
}
+137
View File
@@ -0,0 +1,137 @@
//! The "device" library domain (library/device): what a driver author imports.
//! The flat reference data (device-abi, pci-class, acpi-ids, usb-abi, usb-ids),
//! typed MMIO access, the driver-side client libraries (driver, pci, usb,
//! block), the AML interpreter, and the data-driven device registry.
const std = @import("std");
pub fn build(b: *std.Build) void {
const kernel = b.dependency("kernel", .{});
const protocol = b.dependency("protocol", .{});
const csv = b.dependency("csv", .{});
const abi = kernel.module("abi");
const system_call = kernel.module("system-call");
const ipc = kernel.module("ipc");
const time = kernel.module("time");
// The devices sub-project's public interface (the flat wire types),
// importable by user space, unlike the kernel-internal device model it
// also feeds (system/kernel/device-model.zig).
const device_abi = b.addModule("device-abi", .{
.root_source_file = b.path("model/device-abi.zig"),
});
// PCI class-code decoding (class/subclass/prog-IF -> names). Pure reference
// data, shared by kernel discovery and any user-space PCI tool.
const pci_class = b.addModule("pci-class", .{
.root_source_file = b.path("pci/pci-class.zig"),
});
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class.
_ = b.addModule("acpi-ids", .{
.root_source_file = b.path("acpi/acpi-ids.zig"),
});
// The AML interpreter, a build module so the ring-3 acpi service can run
// the same parser the kernel does (docs/discovery.md). Pure Zig, no kernel
// imports — one source, two builds.
_ = b.addModule("aml", .{
.root_source_file = b.path("acpi/aml/aml.zig"),
});
// The USB device-framework wire ABI (chapter-9 set-up packets, standard +
// class requests, descriptors) and the USB class-code taxonomy.
const usb_abi = b.addModule("usb-abi", .{
.root_source_file = b.path("usb/usb-abi.zig"),
});
const usb_ids = b.addModule("usb-ids", .{
.root_source_file = b.path("usb/usb-ids.zig"),
});
// Typed volatile MMIO register access + memory-ordering barriers, for
// drivers on top of an mmio_map grant. Depends only on `builtin`.
const mmio = b.addModule("mmio", .{
.root_source_file = b.path("mmio/mmio.zig"),
});
// The driver author's interface: device access + the device-manager hello
// handshake, folded together.
const driver = b.addModule("driver", .{
.root_source_file = b.path("driver/driver.zig"),
.imports = &.{
.{ .name = "abi", .module = abi },
.{ .name = "device-abi", .module = device_abi },
.{ .name = "system-call", .module = system_call },
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "device-manager-protocol", .module = protocol.module("device-manager-protocol") },
},
});
// A device driver's view of its claimed PCI function: config-space header
// fields, BAR decode + map, capability walks (legacy + extended), MSI/MSI-X
// programming, power state, and function-level reset — the generic PCI
// mechanics every leaf PCI driver used to re-derive inline.
_ = b.addModule("pci", .{
.root_source_file = b.path("pci/pci.zig"),
.imports = &.{
.{ .name = "driver", .module = driver },
.{ .name = "mmio", .module = mmio },
.{ .name = "pci-class", .module = pci_class },
.{ .name = "time", .module = time },
},
});
// The USB class-driver transfer client: open a device on the xHCI bus and
// drive it (control / interrupt / bulk). Re-exports usb-abi / usb-ids as
// usb.abi / usb.ids for a single USB import.
_ = b.addModule("usb", .{
.root_source_file = b.path("usb/usb.zig"),
.imports = &.{
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "usb-transfer-protocol", .module = protocol.module("usb-transfer-protocol") },
.{ .name = "usb-abi", .module = usb_abi },
.{ .name = "usb-ids", .module = usb_ids },
},
});
// The block-device client — a device type, so it lives here.
_ = b.addModule("block", .{
.root_source_file = b.path("block/block.zig"),
.imports = &.{
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "block-protocol", .module = protocol.module("block-protocol") },
},
});
// The device registry: parse /etc/devices.csv into match rules and bind a
// reported device to a driver. Pure logic (no hardware, no syscalls), so it
// unit-tests on the host; the device manager imports it.
_ = b.addModule("device-registry", .{
.root_source_file = b.path("registry/device-registry.zig"),
.imports = &.{.{ .name = "csv", .module = csv.module("csv") }},
});
// Standalone `zig build test` for this domain alone; the root build keeps
// its aggregate test step.
const test_step = b.step("test", "Run the device library unit tests");
for ([_][]const u8{
"model/device-abi.zig", // wire-type sizes
"pci/pci-class.zig", // class/subclass/prog-IF name decoding
"acpi/acpi-ids.zig", // _HID name decoding
"acpi/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch
"usb/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
"usb/usb-ids.zig", // class/subclass/protocol code assignments
"mmio/mmio.zig", // barriers assemble + registers round-trip
}) |root| {
const device_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(device_tests).step);
}
// The registry needs its csv import wired, so it doesn't fit the loop.
const registry_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("registry/device-registry.zig"),
.target = b.resolveTargetQuery(.{}),
.imports = &.{.{ .name = "csv", .module = csv.module("csv") }},
}),
});
test_step.dependOn(&b.addRunArtifact(registry_tests).step);
}
+15
View File
@@ -0,0 +1,15 @@
.{
.name = .device,
.version = "0.0.0",
.fingerprint = 0x92fb68eace23a4f, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// driver/block/usb/pci build on the kernel library's concern modules.
.kernel = .{ .path = "../kernel" },
// driver speaks device-manager-protocol; block/usb their transfer protocols.
.protocol = .{ .path = "../protocol" },
// device-registry parses /etc/devices.csv with the shared csv helpers.
.csv = .{ .path = "../csv" },
},
.paths = .{""},
}
+104
View File
@@ -0,0 +1,104 @@
//! The "kernel" library domain (library/kernel): the userspace private-ABI
//! library (kernel32-style), split by concern into directly-importable
//! modules. The graph is a DAG: memory depends on thread (heap needs
//! Thread.Mutex), and thread does its own raw mmap so there is no cycle.
//!
//! This package also exports `abi` — the kernel <-> user contract (SystemCall
//! numbers, mmap prot flags, page_size). Its source lives with the kernel in
//! system/abi.zig, outside this directory, but userspace's one view of it is
//! exported here so every consumer names the same module instance. Reaching
//! outside the package root means this package is valid only as an in-repo
//! path dependency (never fetchable by hash) — fine, since path dependencies
//! are the only way danos packages are consumed.
//!
//! The root shim (root.zig) and the user link script (user.ld) are plain
//! files, not modules; build-support reaches them through this package's
//! directory (Dependency.path).
const std = @import("std");
pub fn build(b: *std.Build) void {
const protocol = b.dependency("protocol", .{});
const abi = b.addModule("abi", .{
.root_source_file = b.path("../../system/abi.zig"),
});
const system_call = b.addModule("system-call", .{
.root_source_file = b.path("system-call.zig"),
.imports = &.{.{ .name = "abi", .module = abi }},
});
const ipc = b.addModule("ipc", .{
.root_source_file = b.path("ipc.zig"),
.imports = &.{ .{ .name = "abi", .module = abi }, .{ .name = "system-call", .module = system_call } },
});
const time = b.addModule("time", .{
.root_source_file = b.path("time.zig"),
.imports = &.{.{ .name = "system-call", .module = system_call }},
});
const thread = b.addModule("thread", .{
.root_source_file = b.path("thread.zig"),
.imports = &.{ .{ .name = "abi", .module = abi }, .{ .name = "system-call", .module = system_call } },
});
const logging = b.addModule("logging", .{
.root_source_file = b.path("logging.zig"),
.imports = &.{ .{ .name = "abi", .module = abi }, .{ .name = "system-call", .module = system_call } },
});
const process = b.addModule("process", .{
.root_source_file = b.path("process.zig"),
.imports = &.{
.{ .name = "abi", .module = abi },
.{ .name = "system-call", .module = system_call },
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
},
});
_ = b.addModule("file-system", .{
.root_source_file = b.path("file-system.zig"),
.imports = &.{
.{ .name = "abi", .module = abi },
.{ .name = "system-call", .module = system_call },
.{ .name = "ipc", .module = ipc },
.{ .name = "vfs-protocol", .module = protocol.module("vfs-protocol") },
},
});
_ = b.addModule("memory", .{
.root_source_file = b.path("memory/memory.zig"),
.imports = &.{
.{ .name = "abi", .module = abi },
.{ .name = "system-call", .module = system_call },
.{ .name = "ipc", .module = ipc },
.{ .name = "thread", .module = thread },
},
});
_ = b.addModule("service", .{
.root_source_file = b.path("service.zig"),
.imports = &.{
.{ .name = "abi", .module = abi },
.{ .name = "ipc", .module = ipc },
.{ .name = "process", .module = process },
},
});
_ = b.addModule("start", .{
.root_source_file = b.path("start.zig"),
.imports = &.{ .{ .name = "process", .module = process }, .{ .name = "logging", .module = logging } },
});
// Standalone `zig build test` for this domain alone; the root build keeps
// its aggregate test step. time and thread pull in the syscall wrappers,
// which need the `abi` module; their danos seams fall back to host
// primitives off the danos target, so they run with real host threads.
const test_step = b.step("test", "Run the kernel library unit tests");
for ([_][]const u8{
"time.zig", // Instant/Duration arithmetic
"thread.zig", // Mutex/Condition/RwLock/WaitGroup state machines
}) |root| {
const kernel_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(root),
.target = b.resolveTargetQuery(.{}),
.imports = &.{.{ .name = "abi", .module = abi }},
}),
});
test_step.dependOn(&b.addRunArtifact(kernel_tests).step);
}
}
+11
View File
@@ -0,0 +1,11 @@
.{
.name = .kernel,
.version = "0.0.0",
.fingerprint = 0x5dd29aab36503453, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// file-system speaks the VFS wire protocol.
.protocol = .{ .path = "../protocol" },
},
.paths = .{""},
}
+46
View File
@@ -0,0 +1,46 @@
//! The "protocol" library domain: the wire protocols — each service's public
//! interface, exposed as its own module (docs/driver-model.md). Both sides of
//! every conversation depend on the contract by name; neither reaches into the
//! other's files. Pure flat wire types: no protocol module imports anything.
//!
//! vfs-protocol : the VFS server <-> the file layer (unistd/stdio)
//! input-protocol : the input fan-out service <-> sources + subscribers
//! block-protocol : a filesystem <-> a block driver (usb-storage)
//! usb-transfer-protocol : a USB class driver <-> the xHCI bus driver
//! device-manager-protocol : the device manager <-> drivers + discovery
//! display-protocol : the compositor's client-facing surface
//! scanout-protocol : the compositor -> a native scanout driver (docs/display-v2.md)
//! power-protocol : system power's domain-named surface (docs/power.md)
const std = @import("std");
pub fn build(b: *std.Build) void {
for ([_]struct { name: []const u8, root: []const u8 }{
.{ .name = "vfs-protocol", .root = "vfs/vfs-protocol.zig" },
.{ .name = "input-protocol", .root = "input/input-protocol.zig" },
.{ .name = "block-protocol", .root = "block/block-protocol.zig" },
.{ .name = "usb-transfer-protocol", .root = "usb-transfer/usb-transfer-protocol.zig" },
.{ .name = "device-manager-protocol", .root = "device-manager/device-manager-protocol.zig" },
.{ .name = "display-protocol", .root = "display/display-protocol.zig" },
.{ .name = "scanout-protocol", .root = "scanout/scanout-protocol.zig" },
.{ .name = "power-protocol", .root = "power/power-protocol.zig" },
}) |protocol| {
_ = b.addModule(protocol.name, .{ .root_source_file = b.path(protocol.root) });
}
// Standalone `zig build test` for this domain alone; the root build keeps
// its aggregate test step.
const test_step = b.step("test", "Run the protocol unit tests");
for ([_][]const u8{
"vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"display/display-protocol.zig", // pack(): native pixel encoding per format
}) |root| {
const protocol_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(protocol_tests).step);
}
}
+8
View File
@@ -0,0 +1,8 @@
.{
.name = .protocol,
.version = "0.0.0",
.fingerprint = 0xc8c0bc4c4d551283, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{""},
}
+31
View File
@@ -0,0 +1,31 @@
//! The "xkeyboard-config" library domain: keyboard layouts compiled from the
//! X11 xkeyboard-config database into native Zig (keycode + modifiers ->
//! keysym/character). The `layouts` tables are generated by
//! tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API
//! over them.
const std = @import("std");
pub fn build(b: *std.Build) void {
const layouts = b.addModule("layouts", .{
.root_source_file = b.path("generated/layouts.zig"),
});
_ = b.addModule("xkeyboard-config", .{
.root_source_file = b.path("xkeyboard-config.zig"),
.imports = &.{.{ .name = "layouts", .module = layouts }},
});
// Standalone `zig build test` for this domain alone; the root build keeps
// its aggregate test step. The keycode->character assertions are the
// end-to-end proof that the xkb-data -> generator -> Zig-lookup pipeline
// is correct.
const test_step = b.step("test", "Run the xkeyboard-config unit tests");
const xkb_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("xkeyboard-config.zig"),
.target = b.resolveTargetQuery(.{}),
.imports = &.{.{ .name = "layouts", .module = layouts }},
}),
});
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
}
+8
View File
@@ -0,0 +1,8 @@
.{
.name = .xkeyboard_config,
.version = "0.0.0",
.fingerprint = 0xea5abe82f08b6eae, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{""},
}
+18
View File
@@ -0,0 +1,18 @@
//! The pci-bus driver as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "pci-bus",
.root_source_file = b.path("pci-bus.zig"),
.imports = &.{
"device-manager-protocol", "driver", "ipc", "logging", "memory", "pci-class",
"process", "service",
},
});
b.installArtifact(exe);
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .pci_bus,
.version = "0.0.0",
.fingerprint = 0x283fca121f0bb145, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+50
View File
@@ -0,0 +1,50 @@
//! The ps2-bus driver as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const ps2_bus_exe = build_support.userBinary(b, .{
.name = "ps2-bus",
.root_source_file = b.path("ps2-bus.zig"),
.imports = &.{ "acpi-ids", "driver", "ipc", "logging", "memory", "process", "service", "time" },
});
b.installArtifact(ps2_bus_exe);
const ps2_keyboard_exe = build_support.userBinary(b, .{
.name = "ps2-keyboard",
.root_source_file = b.path("keyboard.zig"),
.imports = &.{
"acpi-ids", "driver", "input-client", "input-protocol", "ipc", "logging", "memory",
"process", "time", "xkeyboard-config",
},
});
b.installArtifact(ps2_keyboard_exe);
const ps2_mouse_exe = build_support.userBinary(b, .{
.name = "ps2-mouse",
.root_source_file = b.path("mouse.zig"),
.imports = &.{
"acpi-ids", "driver", "input-client", "input-protocol", "ipc", "logging", "memory",
"process", "time",
},
});
b.installArtifact(ps2_mouse_exe);
// Standalone `zig build test`; the root aggregate depends on this step.
const test_step = b.step("test", "Run the ps2-bus unit tests");
for ([_][]const u8{
"scancode.zig", // set-2 decode + keyboard state machine
"mouse-packet.zig", // 3-byte mouse packet assembly
}) |test_root| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(test_root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
}
+18
View File
@@ -0,0 +1,18 @@
.{
.name = .ps2_bus,
.version = "0.0.0",
.fingerprint = 0x642a365353bf7de9, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.client = .{ .path = "../../../library/client" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
.@"xkeyboard-config" = .{ .path = "../../../library/xkeyboard-config" },
},
.paths = .{""},
}
+1 -1
View File
@@ -19,7 +19,7 @@ const device = @import("driver");
const ipc = @import("ipc");
const process = @import("process");
const time = @import("time");
const input = @import("input");
const input = @import("input-client");
const memory = @import("memory");
const logging = @import("logging");
const xkb = @import("xkeyboard-config");
+1 -1
View File
@@ -19,7 +19,7 @@ const device = @import("driver");
const ipc = @import("ipc");
const process = @import("process");
const time = @import("time");
const input = @import("input");
const input = @import("input-client");
const memory = @import("memory");
const logging = @import("logging");
const ps2 = @import("ps2-library.zig");
+42
View File
@@ -0,0 +1,42 @@
//! The usb-hid driver as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const usb_hid_keyboard_exe = build_support.userBinary(b, .{
.name = "usb-hid-keyboard",
.root_source_file = b.path("keyboard.zig"),
.imports = &.{
"driver", "input-client", "input-protocol", "ipc", "logging", "process", "service",
"usb", "usb-abi", "xkeyboard-config",
},
});
b.installArtifact(usb_hid_keyboard_exe);
const usb_hid_mouse_exe = build_support.userBinary(b, .{
.name = "usb-hid-mouse",
.root_source_file = b.path("mouse.zig"),
.imports = &.{
"driver", "input-client", "input-protocol", "ipc", "logging", "process", "service",
"usb", "usb-abi",
},
});
b.installArtifact(usb_hid_mouse_exe);
// Standalone `zig build test`; the root aggregate depends on this step.
const test_step = b.step("test", "Run the usb-hid unit tests");
for ([_][]const u8{
"hid-report.zig", // HID boot-report keyboard/mouse decode
}) |test_root| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(test_root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
}
+18
View File
@@ -0,0 +1,18 @@
.{
.name = .usb_hid,
.version = "0.0.0",
.fingerprint = 0x66328b738fffff01, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.client = .{ .path = "../../../library/client" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
.@"xkeyboard-config" = .{ .path = "../../../library/xkeyboard-config" },
},
.paths = .{""},
}
+1 -1
View File
@@ -18,7 +18,7 @@ const std = @import("std");
const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const input = @import("input");
const input = @import("input-client");
const device_manager = @import("driver");
const logging = @import("logging");
const usb = @import("usb");
+1 -1
View File
@@ -14,7 +14,7 @@ const std = @import("std");
const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const input = @import("input");
const input = @import("input-client");
const device_manager = @import("driver");
const logging = @import("logging");
const usb = @import("usb");
+33
View File
@@ -0,0 +1,33 @@
//! The usb-storage driver as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "usb-storage",
.root_source_file = b.path("usb-storage.zig"),
.imports = &.{
"block-protocol", "driver", "ipc", "logging", "memory", "process", "service",
"time", "usb",
},
});
b.installArtifact(exe);
// Standalone `zig build test`; the root aggregate depends on this step.
const test_step = b.step("test", "Run the usb-storage unit tests");
for ([_][]const u8{
"bulk-only-transport.zig", // CBW/CSW wrapper sizes
"scsi.zig", // SCSI CDB encodings (big-endian)
}) |test_root| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(test_root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .usb_storage,
.version = "0.0.0",
.fingerprint = 0xce09fdc4c50bb4fe, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+19
View File
@@ -0,0 +1,19 @@
//! The usb-xhci-bus driver as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "usb-xhci-bus",
.root_source_file = b.path("usb-xhci-bus.zig"),
.imports = &.{
"device-manager-protocol", "driver", "input-client", "ipc", "logging", "memory",
"mmio", "pci", "process", "service", "time", "usb-abi", "usb-ids",
"usb-transfer-protocol",
},
});
b.installArtifact(exe);
}
+17
View File
@@ -0,0 +1,17 @@
.{
.name = .usb_xhci_bus,
.version = "0.0.0",
.fingerprint = 0x46d21373f05f6b20, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.client = .{ .path = "../../../library/client" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+1 -1
View File
@@ -19,7 +19,7 @@ const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const time = @import("time");
const input = @import("input");
const input = @import("input-client");
const device_manager = @import("driver");
const memory = @import("memory");
const logging = @import("logging");
+33
View File
@@ -0,0 +1,33 @@
//! The virtio-gpu driver as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "virtio-gpu",
.root_source_file = b.path("virtio-gpu.zig"),
.imports = &.{
"display-protocol", "driver", "ipc", "logging", "memory", "mmio", "pci", "process",
"scanout-protocol", "service", "time",
},
});
b.installArtifact(exe);
// Standalone `zig build test`; the root aggregate depends on this step.
const test_step = b.step("test", "Run the virtio-gpu unit tests");
for ([_][]const u8{
"virtio-gpu-protocol.zig", // virtio-gpu command struct sizes
"virtio-pci.zig", // virtio 1.0 PCI transport struct sizes
}) |test_root| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(test_root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .virtio_gpu,
.version = "0.0.0",
.fingerprint = 0xfb704899c18b9a23, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+6 -11
View File
@@ -106,12 +106,6 @@ pub const PlatformInformation = struct {
/// INCLUDE_PCI_ALL (the catch-all unit; falls back to the first), or the AMD-Vi
/// IOMMU's control-register base from the first IVHD.
iommu_base: u64 = 0,
/// The unit's Version register (offset 0x00) — its low byte is major.minor;
/// reading it back nonzero confirms a real, mappable VT-d unit.
iommu_version: u32 = 0,
/// The unit's Capability register (offset 0x08): supported address widths, number
/// of domains, etc. Consumed by the IOMMU core when it enables translation.
iommu_capabilities: u64 = 0,
/// Whether the selected unit carries INCLUDE_PCI_ALL. False means every unit is
/// device-scoped (unusual) — the core still enables on the selected unit but
/// devices outside its scope remain untranslated.
@@ -502,7 +496,7 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
} else if (std.mem.eql(u8, &sig, &SPCR)) {
parseSpcr(header);
} else if (std.mem.eql(u8, &sig, &DMAR)) {
parseDmar(hal, header);
parseDmar(header);
} else if (std.mem.eql(u8, &sig, &IVRS)) {
parseIvrs(header);
} else if (std.mem.eql(u8, &sig, &SSDT)) {
@@ -822,7 +816,7 @@ const scope_path_offset = 6;
/// and records single-path endpoint RMRRs for the IOMMU core to pre-map before it
/// enables translation. Multi-hop RMRR scopes are skipped loudly: better a named gap
/// than a silent one.
fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
fn parseDmar(header: *const SystemDescriptorTableHeader) void {
const base: [*]align(1) const u8 = @ptrCast(header);
const total: usize = header.length;
@@ -840,13 +834,14 @@ fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
const replace = !platform_information.iommu_present or
(include_all and !platform_information.iommu_include_all);
if (replace) {
// Table facts only: the unit's registers are the IOMMU
// backend's business (it maps and validates them at
// detect) — discovery records where they live, never
// reads them.
if (platform_information.iommu_present) platform_information.iommu_extra_units += 1;
const regs = hal.mapMmio(register_base, abi.page_size, true);
platform_information.iommu_present = true;
platform_information.iommu_base = register_base;
platform_information.iommu_include_all = include_all;
platform_information.iommu_version = @as(*const volatile u32, @ptrFromInt(regs + 0x00)).*;
platform_information.iommu_capabilities = @as(*const volatile u64, @ptrFromInt(regs + 0x08)).*;
} else {
platform_information.iommu_extra_units += 1;
}
@@ -17,6 +17,11 @@ const io = @import("io.zig");
const smp = @import("smp.zig");
const pcpu = @import("per-cpu.zig");
/// The x86-64 IOMMU backends (Intel VT-d, AMD-Vi) behind their dispatch
/// surface — the architecture-neutral IOMMU core (system/kernel/iommu.zig)
/// reaches the hardware only through this.
pub const iommu = @import("iommu.zig");
/// The saved register/trap frame passed to a fault handler.
pub const CpuState = idt.CpuState;
@@ -1,5 +1,6 @@
//! system/kernel/iommu-amd.zig — AMD-Vi (AMD I/O Virtualization) backend for the IOMMU
//! core. The AMD analogue of iommu-intel.zig: it supplies the core's `Backend` vtable
//! AMD-Vi (AMD I/O Virtualization) backend for the IOMMU core, behind the
//! architecture boundary. The AMD analogue of iommu-intel.zig: it supplies
//! the architecture-neutral core's `Backend` vtable (iommu.zig beside this file)
//! with AMD-Vi's page-table entry bits and drives the device table, command buffer, and
//! event log.
//!
@@ -14,10 +15,7 @@
const std = @import("std");
const abi = @import("abi");
const boot_handoff = @import("boot-handoff");
const pmm = @import("pmm.zig");
const platform = @import("platform");
const architecture = @import("architecture");
const log = @import("log.zig");
const paging = @import("paging.zig");
const iommu = @import("iommu.zig");
const page_size = abi.page_size;
@@ -88,10 +86,10 @@ fn ram(physical: u64) [*]volatile u64 {
/// Map the register window, allocate the device table / command buffer / event log.
/// Returns the vtable, or null if the boot-time allocations fail.
pub fn detect(info: platform.PlatformInformation) ?iommu.Backend {
register_base = architecture.mapMmio(info.iommu_base, 16 * 1024, true);
pub fn detect(discovery: iommu.Discovery) ?iommu.Backend {
register_base = paging.mapMmio(discovery.register_base, 16 * 1024, true);
device_table = pmm.allocContiguous(device_table_pages, ~@as(u64, 0)) orelse return null;
device_table = iommu.environment.allocateContiguous(device_table_pages, ~@as(u64, 0)) orelse return null;
zero(device_table, device_table_pages); // all-zero DTE = V=0 = deny every device
command_buffer = allocZeroedFrame() orelse return null;
event_log = allocZeroedFrame() orelse return null;
@@ -100,6 +98,7 @@ pub fn detect(info: platform.PlatformInformation) ?iommu.Backend {
return iommu.Backend{
.levels = levels,
.supports_huge_pages = false, // 4 KiB leaves only (AMD superpage encoding deferred)
.enable = enable,
.makeLeaf = makeLeaf,
.makeTable = makeTable,
.isPresent = isPresent,
@@ -114,7 +113,7 @@ pub fn detect(info: platform.PlatformInformation) ?iommu.Backend {
/// Program the base registers and enable translation. The device table is already
/// zeroed (every device denied) except any entries `attach` wrote for RMRR/claimed
/// devices, so turning translation on blocks all other DMA and logs it.
pub fn enable() void {
fn enable() void {
write64(reg_device_table_base, (device_table & address_mask) | (device_table_pages - 1));
write64(reg_command_buffer_base, (command_buffer & address_mask) | (ring_length_code << 56));
write64(reg_event_log_base, (event_log & address_mask) | (ring_length_code << 56));
@@ -126,6 +125,12 @@ pub fn enable() void {
// Buffers first, then the master enable.
write64(reg_control, control_command_buffer_enable | control_event_log_enable);
write64(reg_control, control_command_buffer_enable | control_event_log_enable | control_iommu_enable);
iommu.environment.write("/system/kernel: iommu online (AMD-Vi) — UNTESTED on real AMD hardware (QEMU-verified only)\n");
var buffer: [48]u8 = undefined;
if (std.fmt.bufPrint(&buffer, " levels : {d} (48-bit)\n", .{levels})) |line|
iommu.environment.write(line)
else |_| {}
}
// --- Backend vtable ------------------------------------------------------------------
@@ -195,13 +200,14 @@ fn faultDrain() usize {
fn logFault(source: u16, address: u64) void {
if (fault_log_budget == 0) return;
fault_log_budget -= 1;
log.print("DANOS-IOMMU-FAULT: bdf={x:0>2}:{x:0>2}.{d} addr=0x{x} reason=amd-io-page-fault\n", .{
var buffer: [128]u8 = undefined;
if (std.fmt.bufPrint(&buffer, "DANOS-IOMMU-FAULT: bdf={x:0>2}:{x:0>2}.{d} addr=0x{x} reason=amd-io-page-fault\n", .{
source >> 8,
(source >> 3) & 0x1F,
source & 0x7,
address,
});
if (fault_log_budget == 0) log.write("DANOS-IOMMU-FAULT: (further faults suppressed)\n");
})) |line| iommu.environment.write(line) else |_| {}
if (fault_log_budget == 0) iommu.environment.write("DANOS-IOMMU-FAULT: (further faults suppressed)\n");
}
// --- command ring --------------------------------------------------------------------
@@ -240,7 +246,7 @@ fn completeAndWait() void {
if (spins > 100_000) {
if (!completion_warned) {
completion_warned = true;
log.write("/system/kernel: AMD-Vi COMPLETION_WAIT store not observed — proceeding (QEMU processes commands synchronously)\n");
iommu.environment.write("/system/kernel: AMD-Vi COMPLETION_WAIT store not observed — proceeding (QEMU processes commands synchronously)\n");
}
return;
}
@@ -248,7 +254,7 @@ fn completeAndWait() void {
}
fn allocZeroedFrame() ?u64 {
const frame = pmm.alloc() orelse return null;
const frame = iommu.environment.allocateFrame() orelse return null;
zero(frame, 1);
return frame;
}
@@ -1,7 +1,7 @@
//! system/kernel/iommu-intel.zig — Intel VT-d backend for the IOMMU core.
//! Intel VT-d backend for the IOMMU core, behind the architecture boundary.
//!
//! Provides the core (iommu.zig) with the VT-d hardware specifics behind its `Backend`
//! vtable: second-level page-table entry bits, the root/context table structure, the
//! Provides the architecture-neutral core (system/kernel/iommu.zig) with the VT-d
//! hardware specifics behind the `Backend` vtable (iommu.zig beside this file): second-level page-table entry bits, the root/context table structure, the
//! translation-enable and invalidation register sequences, and the fault drain. The
//! core owns the domain table and the page-table walk; this file owns the registers.
//!
@@ -15,10 +15,7 @@
const std = @import("std");
const abi = @import("abi");
const boot_handoff = @import("boot-handoff");
const pmm = @import("pmm.zig");
const platform = @import("platform");
const architecture = @import("architecture");
const log = @import("log.zig");
const paging = @import("paging.zig");
const iommu = @import("iommu.zig");
const page_size = abi.page_size;
@@ -66,6 +63,7 @@ const slpte_page_size: u64 = 1 << 7; // a 2 MiB leaf (== iommu.huge_leaf_bit)
const address_mask: u64 = 0x000F_FFFF_FFFF_F000;
var register_base: usize = 0;
var version: u32 = 0;
var capabilities: u64 = 0;
var extended_capabilities: u64 = 0;
var coherent: bool = true; // ECAP.C — whether clflush is unnecessary
@@ -97,11 +95,15 @@ fn tableAt(physical: u64) [*]volatile u64 {
}
/// Map the register window, read caps, pick the address width. Returns the vtable, or
/// null when the unit advertises no address width danos can drive.
pub fn detect(info: platform.PlatformInformation) ?iommu.Backend {
// Remap 16 KiB: FRCD and IOTLB registers can sit past the first page (CAP.FRO /
// ECAP.IRO are 16-byte-unit offsets). Idempotent with the detection-time mapping.
register_base = architecture.mapMmio(info.iommu_base, 16 * 1024, true);
/// null when the unit is not live or advertises no address width danos can drive.
pub fn detect(discovery: iommu.Discovery) ?iommu.Backend {
// Map 16 KiB: FRCD and IOTLB registers can sit past the first page (CAP.FRO /
// ECAP.IRO are 16-byte-unit offsets).
register_base = paging.mapMmio(discovery.register_base, 16 * 1024, true);
// The Version register's low byte is major.minor; reading it back nonzero is
// the live-mappable-unit sanity check (previously a kernel-test assertion).
version = read32(0x00);
if (version == 0) return null;
capabilities = read64(reg_cap);
extended_capabilities = read64(reg_ecap);
coherent = (extended_capabilities & ecap_coherent) != 0;
@@ -122,6 +124,7 @@ pub fn detect(info: platform.PlatformInformation) ?iommu.Backend {
return iommu.Backend{
.levels = levels,
.supports_huge_pages = true,
.enable = enable,
.makeLeaf = makeLeaf,
.makeTable = makeTable,
.isPresent = isPresent,
@@ -137,7 +140,7 @@ pub fn detect(info: platform.PlatformInformation) ?iommu.Backend {
/// and populated the RMRR domains (their context entries are live via `attach`), so at
/// this instant every OTHER device's context entry is not-present and will fault — which
/// for stale firmware bus-mastering is the desired evidence, not a bug.
pub fn enable() void {
fn enable() void {
write64(reg_rtaddr, root_table); // legacy mode (bits 11:10 = 00)
setGlobalCommand(gcmd_srtp);
spinStatus(gsts_rtps);
@@ -145,6 +148,15 @@ pub fn enable() void {
setGlobalCommand(gcmd_te);
spinStatus(gsts_tes);
gcmd_shadow |= gcmd_te;
iommu.environment.write("/system/kernel: iommu online (Intel VT-d)\n");
var buffer: [64]u8 = undefined;
if (std.fmt.bufPrint(&buffer, " version : 0x{x}\n", .{version})) |line|
iommu.environment.write(line)
else |_| {}
if (std.fmt.bufPrint(&buffer, " agaw : {d} levels\n", .{levels})) |line|
iommu.environment.write(line)
else |_| {}
}
// --- Backend vtable ------------------------------------------------------------------
@@ -241,16 +253,17 @@ fn faultDrain() usize {
fn logFault(source: u16, address: u64, reason: u8, is_read: bool) void {
if (fault_log_budget > 0) {
fault_log_budget -= 1;
log.print("DANOS-IOMMU-FAULT: bdf={x:0>2}:{x:0>2}.{d} addr=0x{x} reason=0x{x} write={d}\n", .{
var buffer: [128]u8 = undefined;
if (std.fmt.bufPrint(&buffer, "DANOS-IOMMU-FAULT: bdf={x:0>2}:{x:0>2}.{d} addr=0x{x} reason=0x{x} write={d}\n", .{
source >> 8,
(source >> 3) & 0x1F,
source & 0x7,
address,
reason,
@intFromBool(!is_read),
});
})) |line| iommu.environment.write(line) else |_| {}
if (fault_log_budget == 0)
log.write("DANOS-IOMMU-FAULT: (further faults suppressed)\n");
iommu.environment.write("DANOS-IOMMU-FAULT: (further faults suppressed)\n");
} else {
faults_suppressed += 1;
}
@@ -269,7 +282,7 @@ fn spinStatus(bit: u32) void {
while (read32(reg_gsts) & bit == 0) {
spins += 1;
if (spins > 10_000_000) {
log.write("/system/kernel: WARNING VT-d status bit never set — translation may be incomplete\n");
iommu.environment.write("/system/kernel: WARNING VT-d status bit never set — translation may be incomplete\n");
return;
}
}
@@ -306,7 +319,7 @@ fn iotlbOffset() usize {
}
fn allocZeroed() ?u64 {
const frame = pmm.alloc() orelse return null;
const frame = iommu.environment.allocateFrame() orelse return null;
const table = tableAt(frame);
var i: usize = 0;
while (i < 512) : (i += 1) table[i] = 0;
@@ -0,0 +1,77 @@
//! x86-64 IOMMU backends, behind the architecture boundary: Intel VT-d
//! (iommu-intel.zig) and AMD-Vi (iommu-amd.zig). The architecture-neutral
//! core (system/kernel/iommu.zig) owns the domain table and the shared
//! page-table walker; it hands this file the firmware discovery facts and an
//! environment (frame allocation + the log sink, injected the same way
//! enablePaging receives its frame hooks), and gets back a hardware vtable.
//! A new architecture supplies its own unit (ARM: the SMMU) from its own
//! directory with no core change.
const intel = @import("iommu-intel.zig");
const amd = @import("iommu-amd.zig");
/// What the platform's firmware tables reported: where the unit's registers
/// live, and which programming model its table implies (an IVRS table
/// describes AMD-Vi; a DMAR table describes Intel VT-d).
pub const Discovery = struct {
register_base: u64,
amd: bool,
};
/// What the backends need from the generic kernel, injected at detect so this
/// module never imports kernel internals: physical-frame allocation for the
/// hardware structures, and the kernel log sink (fault reports, warnings, the
/// enable banner).
pub const Environment = struct {
allocateFrame: *const fn () ?u64,
allocateContiguous: *const fn (count: usize, max_physical: u64) ?u64,
write: *const fn (bytes: []const u8) void,
};
/// The bit encodings and hardware operations a backend supplies to the shared
/// core. Entry helpers build the raw page-table entries for the backend's
/// format; the core walks the tree with them. The hardware ops act on a whole
/// domain (identified by its hardware domain id = core index + 1) or device
/// (by requester id / bdf).
pub const Backend = struct {
/// Number of page-table levels (3 or 4) the backend selected from hardware caps.
levels: u8,
/// Largest leaf the walker may emit: 4 KiB always, 2 MiB when the backend allows.
supports_huge_pages: bool,
/// Raw entry bits for a leaf mapping `physical` (with the given size), and for a
/// non-leaf entry pointing at `table_physical` at `level` (level counts down to 1
/// at the leaf's parent). `isPresent` tests a read-back entry.
makeLeaf: *const fn (physical: u64, huge: bool) u64,
makeTable: *const fn (table_physical: u64, level: u8) u64,
isPresent: *const fn (entry: u64) bool,
/// Flush a cache line holding IOMMU structures the hardware reads non-coherently
/// (VT-d with ECAP.C==0). A no-op where the unit snoops caches.
flushStructure: *const fn (address: usize) void,
/// Turn translation on (the core has already seeded any pre-claim domains)
/// and write the unit's identity lines to the log — the core follows with
/// the neutral posture lines.
enable: *const fn () void,
/// Point `bdf`'s translation structure at `domain` (hardware id) and invalidate the
/// context/device caches so the change takes effect.
attach: *const fn (bdf: u16, domain: u16, page_table_root: u64) void,
/// Return `bdf`'s translation structure to not-present + invalidate — all its DMA
/// faults afterward.
detach: *const fn (bdf: u16) void,
/// Invalidate cached translations for `domain` (after a map or unmap).
invalidateDomain: *const fn (domain: u16) void,
/// Pull pending faults out of the hardware, log them (rate-limited), return the
/// count seen this call.
faultDrain: *const fn () usize,
};
/// The injected kernel services, stored for the backends at detect time.
pub var environment: Environment = undefined;
/// Probe the discovered unit and return its vtable, or null when it is
/// unusable (the core stays fail-open and says so).
pub fn detect(discovery: Discovery, injected: Environment) ?Backend {
environment = injected;
return if (discovery.amd) amd.detect(discovery) else intel.detect(discovery);
}
+53 -104
View File
@@ -1,5 +1,6 @@
//! system/kernel/iommu.zig — vendor-neutral IOMMU core: per-device DMA translation
//! domains over an Intel VT-d or AMD-Vi backend.
//! system/kernel/iommu.zig — architecture-neutral IOMMU core: per-device DMA
//! translation domains over a backend the architecture module supplies
//! (x86-64: Intel VT-d or AMD-Vi, behind architecture/x86_64/iommu.zig).
//!
//! The problem this closes: without an IOMMU, a claimed bus-mastering device can DMA to
//! ANY physical address, so a compromised or buggy driver reaches all of memory through
@@ -13,11 +14,12 @@
//! physical address they program into hardware; a domain simply makes that same
//! address the ONLY thing the device can reach. No IOVA allocator, and every
//! driver's register-programming code is untouched.
//! - **Vendor-neutral**: this file owns the domain table and a shared 512-entry
//! page-table walker; a `Backend` vtable supplies the hardware specifics (VT-d in
//! iommu-intel.zig, AMD-Vi in iommu-amd.zig) — the entry-bit encodings, the
//! enable/invalidate register dances, and the fault drain.
//! - **Fail-open**: when no IOMMU is found, `kind == .none` and every entry point is a
//! - **Architecture-neutral**: this file owns the domain table and a shared
//! 512-entry page-table walker; the architecture module's `Backend` vtable
//! supplies the hardware specifics — the entry-bit encodings, the
//! enable/invalidate register dances, and the fault drain — with the frame
//! allocator and log sink injected the other way.
//! - **Fail-open**: when no IOMMU is found, nothing activates and every entry point is a
//! success no-op, so callers in process.zig stay unconditional and behavior is
//! byte-for-byte the pre-IOMMU kernel. The boot log states the posture.
//!
@@ -29,54 +31,18 @@ const abi = @import("abi");
const boot_handoff = @import("boot-handoff");
const pmm = @import("pmm.zig");
const platform = @import("platform");
const architecture = @import("architecture");
const devices_broker = @import("devices-broker.zig");
const log = @import("log.zig");
const intel = @import("iommu-intel.zig");
const amd = @import("iommu-amd.zig");
const page_size: u64 = abi.page_size;
const page_mask: u64 = page_size - 1;
const huge_page_size: u64 = 2 * 1024 * 1024;
pub const Kind = enum { none, intel_vtd, amd_vi };
/// One domain per claimed PCI function. 64 mirrors devices-broker's device cap.
pub const maximum_domains = 64;
pub const invalid_domain: u16 = 0xFFFF;
/// The bit encodings and hardware operations a backend supplies to the shared core.
/// Entry helpers build the raw page-table entries for the backend's format; the core
/// walks the tree with them. The hardware ops act on a whole domain (identified by its
/// hardware domain id = core index + 1) or device (by requester id / bdf).
pub const Backend = struct {
/// Number of page-table levels (3 or 4) the backend selected from hardware caps.
levels: u8,
/// Largest leaf the walker may emit: 4 KiB always, 2 MiB when the backend allows.
supports_huge_pages: bool,
/// Raw entry bits for a leaf mapping `physical` (with the given size), and for a
/// non-leaf entry pointing at `table_physical` at `level` (level counts down to 1
/// at the leaf's parent). `isPresent` tests a read-back entry.
makeLeaf: *const fn (physical: u64, huge: bool) u64,
makeTable: *const fn (table_physical: u64, level: u8) u64,
isPresent: *const fn (entry: u64) bool,
/// Flush a cache line holding IOMMU structures the hardware reads non-coherently
/// (VT-d with ECAP.C==0). A no-op where the unit snoops caches.
flushStructure: *const fn (address: usize) void,
/// Point `bdf`'s translation structure at `domain` (hardware id) and invalidate the
/// context/device caches so the change takes effect.
attach: *const fn (bdf: u16, domain: u16, page_table_root: u64) void,
/// Return `bdf`'s translation structure to not-present + invalidate — all its DMA
/// faults afterward.
detach: *const fn (bdf: u16) void,
/// Invalidate cached translations for `domain` (after a map or unmap).
invalidateDomain: *const fn (domain: u16) void,
/// Pull pending faults out of the hardware, log them (rate-limited), return the
/// count seen this call.
faultDrain: *const fn () usize,
};
const Domain = struct {
in_use: bool = false,
owner: u32 = 0, // task that owns the attached device
@@ -85,53 +51,44 @@ const Domain = struct {
rmrr: bool = false, // a firmware reserved-region domain (persists across claims)
};
var kind: Kind = .none;
var backend: Backend = undefined;
var active: bool = false;
var backend: architecture.iommu.Backend = undefined;
var domains: [maximum_domains]Domain = .{Domain{}} ** maximum_domains;
pub fn kindOf() Kind {
return kind;
}
pub fn enabled() bool {
return kind != .none;
return active;
}
/// Detect the IOMMU, pick a backend, pre-map firmware reserved regions, and enable
/// translation. Fail-open (kind stays .none) when no unit exists — the caller logs the
/// posture. Must run after platform discovery and before any user process starts.
/// Detect the IOMMU (the architecture module probes the discovered unit and
/// returns its backend), pre-map firmware reserved regions, and enable
/// translation. Fail-open (nothing activates) when no usable unit exists — the
/// caller logs the posture. Must run after platform discovery and before any
/// user process starts.
pub fn init() void {
const info = platform.platformInformation();
if (!info.iommu_present) {
kind = .none;
if (!info.iommu_present) return;
// A present-but-unusable unit stays fail-open with a logged reason rather
// than half-enabling. The backend receives the kernel services it needs
// (frames, the log sink) here — it never imports kernel internals.
backend = architecture.iommu.detect(.{
.register_base = info.iommu_base,
.amd = info.iommu_is_amd,
}, .{
.allocateFrame = pmm.alloc,
.allocateContiguous = pmm.allocContiguous,
.write = log.write,
}) orelse {
log.write("/system/kernel: WARNING IOMMU present but unusable — staying fail-open\n");
return;
}
// Pick the backend by vendor. A present-but-unusable unit stays fail-open with a
// logged reason rather than half-enabling.
if (info.iommu_is_amd) {
if (amd.detect(info)) |be| {
backend = be;
kind = .amd_vi;
} else {
kind = .none;
log.write("/system/kernel: WARNING AMD-Vi present but unusable — staying fail-open\n");
return;
}
} else {
if (intel.detect(info)) |be| {
backend = be;
kind = .intel_vtd;
} else {
kind = .none;
log.write("/system/kernel: WARNING IOMMU present but unusable — staying fail-open\n");
return;
}
}
};
active = true;
// The translation structures start empty: every device is denied until its driver
// claims it (confineDevice gives it a private domain). PCI functions are enumerated
// post-boot by the ring-3 pci-bus driver, so there is nothing to attach at init.
if (kind == .amd_vi) amd.enable() else intel.enable();
logEnabled(info);
// The backend writes its identity lines; the neutral posture lines follow.
backend.enable();
logPosture(info);
}
/// Per-claimed-device record: its private domain, so a driver's death tears down
@@ -147,7 +104,7 @@ var confined: [maximum_domains]Confined = .{Confined{}} ** maximum_domains;
/// the claim back (a claim that can't be confined must not stand). No-op success when no
/// IOMMU exists (fail-open).
pub fn confineDevice(device_id: u64, bdf: u16, owner: u32) bool {
if (kind == .none) return true;
if (!active) return true;
if (device_id >= confined.len) return true; // unusual id; leave it to fail-open
const domain = domainCreate(owner, bdf) orelse return false;
@@ -174,14 +131,14 @@ fn confinedOf(device_id: u64) ?*Confined {
/// Map a DMA region into a specific claimed device's domain (the device owner binding a
/// granted buffer). false if the device is not confined. No-op success without an IOMMU.
pub fn mapForDevice(device_id: u64, physical: u64, len: u64) bool {
if (kind == .none) return true;
if (!active) return true;
const c = confinedOf(device_id) orelse return false;
return map(c.domain, physical, len);
}
/// Unmap a DMA region from a specific claimed device's domain. No-op if not confined.
pub fn unmapForDevice(device_id: u64, physical: u64, len: u64) void {
if (kind == .none) return;
if (!active) return;
const c = confinedOf(device_id) orelse return;
unmap(c.domain, physical, len);
}
@@ -189,7 +146,7 @@ pub fn unmapForDevice(device_id: u64, physical: u64, len: u64) void {
/// Map a region into every claimed device owned by `owner` — the auto-bind of a task's
/// own freshly-`dma_alloc`'d buffer into the devices it drives.
pub fn mapRegionForOwner(owner: u32, physical: u64, len: u64) void {
if (kind == .none) return;
if (!active) return;
for (&confined) |*c| {
if (c.active and c.owner == owner) _ = map(c.domain, physical, len);
}
@@ -200,7 +157,7 @@ pub fn mapRegionForOwner(owner: u32, physical: u64, len: u64) void {
/// use-after-free this prevents. Cross-device because a granted buffer may be bound in a
/// domain other than its owner's.
pub fn unmapRegionEverywhere(physical: u64, len: u64) void {
if (kind == .none) return;
if (!active) return;
for (&confined) |*c| {
if (c.active) unmap(c.domain, physical, len);
}
@@ -210,7 +167,7 @@ pub fn unmapRegionEverywhere(physical: u64, len: u64) void {
/// device, free the tables) so their DMA is blocked again and a restarted driver
/// re-claims cleanly. Runs BEFORE the broker claims and the DMA frames are released.
pub fn releaseAllOwnedBy(owner: u32) void {
if (kind == .none) return;
if (!active) return;
for (&confined) |*c| {
if (c.active and c.owner == owner) {
detachDevice(c.bdf);
@@ -223,7 +180,7 @@ pub fn releaseAllOwnedBy(owner: u32) void {
/// Allocate an empty domain (an empty top-level table). null when the table is full.
pub fn domainCreate(owner: u32, bdf: u16) ?u16 {
if (kind == .none) return 0; // fail-open: a dummy id the no-op ops ignore
if (!active) return 0; // fail-open: a dummy id the no-op ops ignore
for (&domains, 0..) |*d, index| {
if (d.in_use) continue;
const root = allocTable() orelse return null;
@@ -236,7 +193,7 @@ pub fn domainCreate(owner: u32, bdf: u16) ?u16 {
/// Free a domain's page-table frames and its slot. Precondition: no device attached
/// (detach first).
pub fn domainDestroy(domain: u16) void {
if (kind == .none) return;
if (!active) return;
const d = &domains[domain];
if (!d.in_use) return;
freeTables(d.page_table_root, backend.levels);
@@ -245,7 +202,7 @@ pub fn domainDestroy(domain: u16) void {
/// Attach `bdf`'s device to `domain` and pre-load any RMRR range recorded for it.
pub fn attachDevice(domain: u16, bdf: u16) void {
if (kind == .none) return;
if (!active) return;
const d = &domains[domain];
d.bdf = bdf;
backend.attach(bdf, hardwareId(domain), d.page_table_root);
@@ -253,7 +210,7 @@ pub fn attachDevice(domain: u16, bdf: u16) void {
/// Return `bdf`'s device to not-present + invalidate.
pub fn detachDevice(bdf: u16) void {
if (kind == .none) return;
if (!active) return;
backend.detach(bdf);
}
@@ -261,7 +218,7 @@ pub fn detachDevice(bdf: u16) void {
/// Unconditional domain-selective invalidation after every map — correct under VT-d
/// caching-mode and free otherwise.
pub fn map(domain: u16, physical: u64, len: u64) bool {
if (kind == .none) return true;
if (!active) return true;
const d = &domains[domain];
if (!d.in_use) return false;
if (!mapRange(d.page_table_root, physical, len)) return false;
@@ -273,7 +230,7 @@ pub fn map(domain: u16, physical: u64, len: u64) bool {
/// invalidation before the caller returns the frames to pmm — a stale IOTLB entry
/// pointing at a reallocated frame is the use-after-free this ordering prevents.
pub fn unmap(domain: u16, physical: u64, len: u64) void {
if (kind == .none) return;
if (!active) return;
const d = &domains[domain];
if (!d.in_use) return;
unmapRange(d.page_table_root, physical, len);
@@ -283,14 +240,14 @@ pub fn unmap(domain: u16, physical: u64, len: u64) void {
/// Poll the hardware for translation faults, log them, return the count. Called by the
/// IOMMU test case and opportunistically after a device detaches.
pub fn faultDrain() usize {
if (kind == .none) return 0;
if (!active) return 0;
return backend.faultDrain();
}
/// The physical address `virtual` maps to in `domain`, or null if unmapped — a test
/// helper that walks the domain's page tables (identity mappings return `virtual`).
pub fn translationOf(domain: u16, virtual: u64) ?u64 {
if (kind == .none) return virtual;
if (!active) return virtual;
const d = &domains[domain];
if (!d.in_use) return null;
var table = d.page_table_root;
@@ -425,24 +382,16 @@ fn isHugeLeaf(entry: u64) bool {
/// The size-bit the backends set on a 2 MiB leaf (VT-d SL-PTE PS bit 7; AMD encodes a
/// leaf as next-level 0, so the core marks huge leaves with this software bit — an
/// ignored bit in both formats — to tell them apart from table pointers when freeing).
pub const huge_leaf_bit: u64 = 1 << 7;
const huge_leaf_bit: u64 = 1 << 7;
fn hardwareId(domain: u16) u16 {
return domain + 1; // id 0 is reserved by both architectures
}
fn logEnabled(info: platform.PlatformInformation) void {
if (kind == .amd_vi) {
log.write("/system/kernel: iommu online (AMD-Vi) — UNTESTED on real AMD hardware (QEMU-verified only)\n");
log.print(" levels : {d} (48-bit)\n", .{backend.levels});
return;
}
log.write("/system/kernel: iommu online (Intel VT-d)\n");
log.print(" version : 0x{x}\n", .{info.iommu_version});
log.print(" agaw : {d} levels\n", .{backend.levels});
fn logPosture(info: platform.PlatformInformation) void {
log.print(" rmrr : {d} region(s) premapped\n", .{info.rmrr_count});
if (info.rmrr_skipped > 0)
log.print(" rmrr : WARNING {d} scope(s) skipped — a device keeps an unmapped firmware buffer\n", .{info.rmrr_skipped});
if (info.iommu_extra_units > 0)
log.print(" units : WARNING {d} other DRHD(s) — their scoped devices are NOT translated\n", .{info.iommu_extra_units});
log.print(" units : WARNING {d} other unit(s) — their scoped devices are NOT translated\n", .{info.iommu_extra_units});
}
+26 -13
View File
@@ -1253,11 +1253,10 @@ fn iommuTest() void {
const pinfo = platform.platformInformation();
check("IOMMU found in the firmware tables", pinfo.iommu_present);
check("IOMMU unit has a register base", pinfo.iommu_base != 0);
// The VT-d version register is a live-unit sanity check; AMD-Vi (from IVRS) records
// no version, so gate it on the vendor.
if (!pinfo.iommu_is_amd)
check("VT-d version register reads back nonzero (real, mappable unit)", pinfo.iommu_version != 0);
log("DANOS-IOMMU: base=0x{x} version=0x{x} capabilities=0x{x}\n", .{ pinfo.iommu_base, pinfo.iommu_version, pinfo.iommu_capabilities });
// The live-unit sanity (the version register reading back nonzero) now
// gates detect itself: an unusable unit stays fail-open, so `enabled()`
// below subsumes the old vendor-gated register check.
log("DANOS-IOMMU: base=0x{x}\n", .{pinfo.iommu_base});
// Translation was enabled at boot (kernel.zig: iommu.init before any driver claims
// a device). The blanket domain keeps every device identity-mapped, so DMA still
@@ -1970,15 +1969,20 @@ fn initTest(boot_information: *const BootInformation) void {
};
check("init loaded and spawned as a process", spawned);
// Wait (real time) for at least two heartbeats — proving it runs, writes,
// and sleeps repeatedly (init sleeps ~1 s between beats).
// Wait (real time) until the LAST write is a heartbeat — proving init got
// through its boot chatter (heap ok, the /etc/init.csv lookup) and settled
// into its beat-and-sleep loop (~1 s between beats). Waiting on the text
// rather than a raw write count: the boot chatter alone satisfies a count,
// which is exactly the too-early check that used to fail here.
scheduler.setPriority(1);
const deadline = architecture.millis() + 8000;
while (process.write_count < 2 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
const prefix = "init: heartbeat";
const beat_ok = bufferHas(prefix);
const deadline = architecture.millis() + 8000;
var beat_ok = false;
while (!beat_ok and architecture.millis() < deadline) {
beat_ok = bufferHas(prefix);
scheduler.yield();
}
scheduler.setPriority(4);
check("init produced repeated heartbeats (>=2)", process.write_count >= 2);
check("heartbeat text arrived intact", beat_ok);
check("heartbeats came from user mode (CPL 3)", process.write_from_user);
@@ -2792,15 +2796,24 @@ fn initialRamdiskTest(boot_information: *const BootInformation) void {
process.write_count = 0;
process.write_from_user = false;
var programs: u32 = 0;
var spawned: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
// The FHS boot tree ferries data files too (/etc/devices.csv,
// /etc/init.csv — served read-only by the kernel VFS, never spawned);
// only the /system and /test trees hold programs, so only those count
// toward the spawn-everything sweep.
const is_program = std.mem.startsWith(u8, item.name, "/system/") or
std.mem.startsWith(u8, item.name, "/test/");
if (!is_program) continue;
programs += 1;
if (process.spawnProcess(item.blob, 4, &.{item.name})) spawned += 1 else |err| {
log("DANOS-INITRD-ERR: {s}: {s}\n", .{ item.name, @errorName(err) });
}
}
check("every initial_ramdisk binary spawned", spawned == rd.count);
check("every initial_ramdisk program spawned", programs >= 1 and spawned == programs);
// Wait for the spawned programs to run and make syscalls (they write + sleep).
scheduler.setPriority(1);
+22
View File
@@ -0,0 +1,22 @@
//! The acpi service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
//!
//! The artifact is named "discovery": one swappable process per firmware
//! fills the ramdisk's neutral `discovery` slot (docs/discovery.md); the
//! root's -Ddiscovery picks this package or `fdt`.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "discovery",
.root_source_file = b.path("acpi.zig"),
.imports = &.{
"acpi-ids", "aml", "device-manager-protocol", "driver", "ipc", "logging", "memory",
"power-protocol", "process", "service", "time",
},
});
b.installArtifact(exe);
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .acpi,
.version = "0.0.0",
.fingerprint = 0xf31e9a0903256b64, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+18
View File
@@ -0,0 +1,18 @@
//! The device-manager service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "device-manager",
.root_source_file = b.path("device-manager.zig"),
.imports = &.{
"device-manager-protocol", "device-registry", "driver", "file-system", "ipc",
"logging", "memory", "process", "service", "time",
},
});
b.installArtifact(exe);
}
@@ -0,0 +1,16 @@
.{
.name = .device_manager,
.version = "0.0.0",
.fingerprint = 0x7092fc24905cd147, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+15
View File
@@ -0,0 +1,15 @@
//! The display-demo service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "display-demo",
.root_source_file = b.path("display-demo.zig"),
.imports = &.{ "display-client", "logging", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .display_demo,
.version = "0.0.0",
.fingerprint = 0x5d2832e1e2880143, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.client = .{ .path = "../../../library/client" },
},
.paths = .{""},
}
@@ -11,7 +11,7 @@
//! is deliberately independent of the mouse.
const display = @import("display");
const display = @import("display-client");
const time = @import("time");
const logging = @import("logging");
pub fn main() void {
+33
View File
@@ -0,0 +1,33 @@
//! The display service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "display",
.root_source_file = b.path("display.zig"),
.imports = &.{
"display-client", "display-protocol", "driver", "input-client", "ipc", "logging",
"memory", "scanout-protocol", "service", "thread", "time",
},
.threaded = true, // real atomics/TLS (docs/threading.md)
});
b.installArtifact(exe);
// Standalone `zig build test`; the root aggregate depends on this step.
const test_step = b.step("test", "Run the display unit tests");
for ([_][]const u8{
"compositor.zig", // Rect math + fill/composite/blit-tile
}) |test_root| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(test_root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
}
+17
View File
@@ -0,0 +1,17 @@
.{
.name = .display,
.version = "0.0.0",
.fingerprint = 0xcd172a34b127a19, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.client = .{ .path = "../../../library/client" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+2 -2
View File
@@ -17,11 +17,11 @@
const std = @import("std");
const ipc = @import("ipc");
const input = @import("input");
const input = @import("input-client");
const Thread = @import("thread").Thread;
const service = @import("service");
const time = @import("time");
const display = @import("display");
const display = @import("display-client");
const memory = @import("memory");
const logging = @import("logging");
const compositor = @import("compositor.zig");
+33
View File
@@ -0,0 +1,33 @@
//! The fat service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "fat",
.root_source_file = b.path("fat.zig"),
.imports = &.{
"block", "file-system", "ipc", "logging", "memory", "process", "service", "time",
"vfs-protocol",
},
});
b.installArtifact(exe);
// Standalone `zig build test`; the root aggregate depends on this step.
const test_step = b.step("test", "Run the fat unit tests");
for ([_][]const u8{
"on-disk.zig", // FAT on-disk struct sizes + type detection
"engine.zig", // FAT read/write over a RAM-backed image
}) |test_root| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(test_root),
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .fat,
.version = "0.0.0",
.fingerprint = 0x98958143ecfe1636, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.device = .{ .path = "../../../library/device" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+20
View File
@@ -0,0 +1,20 @@
//! The fdt service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
//!
//! The artifact is named "discovery" like acpi's: the Raspberry Pis hand over
//! a flattened device tree, and the aarch64 target flips the root's
//! -Ddiscovery default when it lands (docs/arm.md). A placeholder until the
//! ARM bring-up.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "discovery",
.root_source_file = b.path("fdt.zig"),
.imports = &.{ "process" },
});
b.installArtifact(exe);
}
+14
View File
@@ -0,0 +1,14 @@
.{
.name = .fdt,
.version = "0.0.0",
.fingerprint = 0xe5e27506c7fc2eea, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
},
.paths = .{""},
}
+28
View File
@@ -0,0 +1,28 @@
//! init (PID 1) as a binary package (docs/build-packages-plan.md): this file
//! names the binary and EXACTLY the modules its source imports — the shared
//! recipe and the module-to-domain map live in build-support. The root build
//! consumes the artifact for the boot image and forwards its -Dserial here.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "init",
.root_source_file = b.path("init.zig"),
.imports = &.{
"csv", "file-system", "ipc", "logging", "memory", "power-protocol",
"process", "time",
},
});
// 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 root build forwards
// its top-level -Dserial as this dependency option.
const serial = b.option(bool, "serial", "Compile the serial liveness heartbeat in (forwarded from the root -Dserial)") orelse false;
const init_options = b.addOptions();
init_options.addOption(bool, "serial", serial);
build_support.programModule(exe).addImport("build_options", init_options.createModule());
b.installArtifact(exe);
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .init,
.version = "0.0.0",
.fingerprint = 0xc674e474eeeced43, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.csv = .{ .path = "../../../library/csv" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+15
View File
@@ -0,0 +1,15 @@
//! The input service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "input",
.root_source_file = b.path("input.zig"),
.imports = &.{ "input-client", "input-protocol", "ipc", "logging", "process", "service" },
});
b.installArtifact(exe);
}
+16
View File
@@ -0,0 +1,16 @@
.{
.name = .input,
.version = "0.0.0",
.fingerprint = 0xd82832d7113ed94e, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
.client = .{ .path = "../../../library/client" },
.protocol = .{ .path = "../../../library/protocol" },
},
.paths = .{""},
}
+1 -1
View File
@@ -23,7 +23,7 @@ const std = @import("std");
const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const input = @import("input");
const input = @import("input-client");
const logging = @import("logging");
const input_protocol = @import("input-protocol");
+15
View File
@@ -0,0 +1,15 @@
//! The logger service as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "logger",
.root_source_file = b.path("logger.zig"),
.imports = &.{ "file-system", "ipc", "logging", "service", "time" },
});
b.installArtifact(exe);
}
+14
View File
@@ -0,0 +1,14 @@
.{
.name = .logger,
.version = "0.0.0",
.fingerprint = 0x987e13f37b0eaea2, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../build-support" },
.kernel = .{ .path = "../../../library/kernel" },
},
.paths = .{""},
}
+15
View File
@@ -0,0 +1,15 @@
//! The args-echo test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "args-echo",
.root_source_file = b.path("args-echo.zig"),
.imports = &.{ "logging", "process" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,14 @@
.{
.name = .args_echo,
.version = "0.0.0",
.fingerprint = 0xcc0ad94cf777eeb, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
},
.paths = .{""},
}
+15
View File
@@ -0,0 +1,15 @@
//! The crash-test test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "crash-test",
.root_source_file = b.path("crash-test.zig"),
.imports = &.{ "device-manager-protocol", "driver", "ipc", "logging", "process", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,16 @@
.{
.name = .crash_test,
.version = "0.0.0",
.fingerprint = 0x611122f815277984, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.device = .{ .path = "../../../../library/device" },
.protocol = .{ .path = "../../../../library/protocol" },
},
.paths = .{""},
}
@@ -0,0 +1,15 @@
//! The device-list test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "device-list",
.root_source_file = b.path("device-list.zig"),
.imports = &.{ "device-manager-protocol", "ipc", "logging", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .device_list,
.version = "0.0.0",
.fingerprint = 0xcdf12d2276a06cb1, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.protocol = .{ .path = "../../../../library/protocol" },
},
.paths = .{""},
}
+15
View File
@@ -0,0 +1,15 @@
//! The fat-test test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "fat-test",
.root_source_file = b.path("fat-test.zig"),
.imports = &.{ "file-system", "logging", "process", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,14 @@
.{
.name = .fat_test,
.version = "0.0.0",
.fingerprint = 0x8dac87d9dc3aabca, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
},
.paths = .{""},
}
@@ -0,0 +1,15 @@
//! The input-source test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "input-source",
.root_source_file = b.path("input-source.zig"),
.imports = &.{ "input-client", "logging", "process", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .input_source,
.version = "0.0.0",
.fingerprint = 0x933ef62ed7d33db7, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.client = .{ .path = "../../../../library/client" },
},
.paths = .{""},
}
@@ -11,7 +11,7 @@
//! decoded hardware is a follow-up (see docs/input.md).
const std = @import("std");
const input = @import("input");
const input = @import("input-client");
const process = @import("process");
const time = @import("time");
const logging = @import("logging");
+15
View File
@@ -0,0 +1,15 @@
//! The input-test test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "input-test",
.root_source_file = b.path("input-test.zig"),
.imports = &.{ "input-client", "logging" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .input_test,
.version = "0.0.0",
.fingerprint = 0x9b12c53bde4d070e, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.client = .{ .path = "../../../../library/client" },
},
.paths = .{""},
}
@@ -7,7 +7,7 @@
//! device classes to one subscription — source -> service -> subscriber, per device.
const std = @import("std");
const input = @import("input");
const input = @import("input-client");
const logging = @import("logging");
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
@@ -0,0 +1,15 @@
//! The iommu-fault-test test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "iommu-fault-test",
.root_source_file = b.path("iommu-fault-test.zig"),
.imports = &.{ "driver", "logging", "mmio", "pci", "pci-class", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .iommu_fault_test,
.version = "0.0.0",
.fingerprint = 0xd7ec3a4e1a61696c, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.device = .{ .path = "../../../../library/device" },
},
.paths = .{""},
}
@@ -0,0 +1,15 @@
//! The pci-cap-test test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "pci-cap-test",
.root_source_file = b.path("pci-cap-test.zig"),
.imports = &.{ "driver", "ipc", "logging", "mmio", "pci", "pci-class", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .pci_cap_test,
.version = "0.0.0",
.fingerprint = 0x2d4f027d811c3953, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.device = .{ .path = "../../../../library/device" },
},
.paths = .{""},
}
@@ -0,0 +1,15 @@
//! The process-test test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "process-test",
.root_source_file = b.path("process-test.zig"),
.imports = &.{ "ipc", "logging", "process", "service", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,14 @@
.{
.name = .process_test,
.version = "0.0.0",
.fingerprint = 0x3796ac392c89622b, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
},
.paths = .{""},
}
@@ -0,0 +1,15 @@
//! The shared-memory-client test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! the shared recipe and the module-to-domain map live in build-support.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "shared-memory-client",
.root_source_file = b.path("shared-memory-client.zig"),
.imports = &.{ "ipc", "logging", "memory", "time" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,14 @@
.{
.name = .shared_memory_client,
.version = "0.0.0",
.fingerprint = 0xc3945d0308ff5720, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
},
.paths = .{""},
}

Some files were not shown because too many files have changed in this diff Show More