//! 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 /volumes/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; }