167 lines
9.1 KiB
Zig
167 lines
9.1 KiB
Zig
//! 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;
|
|
}
|