holy-image(7)
NAME
holy-image - image build and libc recovery boot tests
BUILD
make bootstrap-kernel packages an existing x86 kernel image as linux.holy. KERNEL_IMAGE, KERNEL_VERSION, ARCH and OUTPUT select its inputs and output; MODULES_DIR optionally names a staging tree for that release. The builder checks the x86 boot header, matches every .ko to modules.dep, verifies the package and scans each module's architecture and vermagic. It records the input and artifact hashes. This target packages an existing kernel; QEMU boot acceptance belongs to holygetiso.
make bootstrap-image builds a live ISO and runs its QEMU boot test. ARCH defaults to x86_64. ARCH=i686 supports BIOS optical ISOs with ROOT_STORAGE=ram or ext4 and an independently bootable BIOS GPT disk with ROOT_STORAGE=gpt-ext4. IMAGE_BOOT_TEST=build-only produces the image without a QEMU run, records "result untested" and returns 6. If the selected QEMU binary is missing, the builder takes the same path. Neither case counts as a release boot gate. The i686 ISO boots through El Torito; it is not a hybrid USB image. The i686 profile requires an i686 kernel and matching static BusyBox, dinit, mdevd, holypkg and C compiler inputs. The builder checks package and executable architectures before installation. The dual-libc profile also requires matching i686 glibc and musl runtime packages and C compilers. The GPT profile has no i686 UEFI loader; its tested boot path is BIOS. IMAGE_PROFILE defaults to dual-libc; static-core explicitly selects the earlier profile without dynamic libraries. LIBC_BOOT_STATE is present by default. For dual-libc, glibc, musl or both remove those runtime payloads and their loader links before creating initramfs, while retaining installed records and cached .holy artifacts. These are intentional damage fixtures, not package removals. LIBC_BOOT_STATE=remove-both requires an ext4 or gpt-ext4 dual-libc root. The first guest boot runs both dynamic probes, then holypkg removes both runtime packages with an explicit broken-dependency decision and reboots. Static BusyBox, dinit and holypkg run on the second boot, report broken providers, install the same two cached .holy artifacts through reviewed plans and repeat the probes. The QEMU report requires both boot contracts and an unchanged read-only base image. ROOT_STORAGE defaults to ram. ext4 selects a persistent recovery test for dual-libc and requires mke2fs and qemu-img. The kernel must include ext4 and virtio-blk, and the BusyBox package must include switch_root, sync and reboot. gpt-ext4 selects an independently bootable GPT disk with the same ext4 recovery contract. It additionally requires sfdisk, mkfs.fat and mtools; xorriso is only required for ISO profiles. NETWORK_RECOVERY defaults to off. The explicit fixture value requires a dual-libc RAM image with LIBC_BOOT_STATE=both. The builder omits both libc archives from the guest cache, retains copies for a local HTTPS fixture, and packages a generated test CA. The guest assigns the fixture address with static BusyBox ip, downloads both archives with static holypkg and verifies their SHA-256 digests before missing-only repair. The test CA key stays in the build output and is not included in the image or documentation bundle. The following make variables are required:
INPUTS AND ROOT
The builder snapshots the selected native packages, normalizes numeric ownership to root and directory modes to 0755, regenerates manifests and records the parent hashes and normalization in origin. These are new unsigned local artifacts. The original archives remain in inputs/. For the older BusyBox bootstrap package, its embedded-musl license moves to usr/share/licenses/busybox/musl.COPYRIGHT, with the mapping recorded in origin. The dynamic musl package owns its own license file.
The static core binary, selected kernel, Limine data and Holy boot configuration also become native packages. The holy-base metapackage requires the resulting eight packages, including holyinstall. INSTALL_TEST=1 also selects the doas package and records its setuid approval. The dual-libc profile adds the two runtime packages and two C probe packages. holyinstall asks holypkg to resolve the selected set, writes install.preview and a frozen install.plan, then applies that plan with one writer lock and one generation change into root/. The preview records selected providers and requirements; the plan records artifacts and architecture approvals. The builder prepares the profile's directories first. It does not extract foreign distro roots or modify the host package database.
For ROOT_STORAGE=ext4, mke2fs copies that prepared root into a 512 MiB raw root.ext4 without mounting it on the host. Limine passes holy.root=/dev/vda and holy.rootfstype=ext4. holy-init mounts the disk, moves runtime mounts and executes BusyBox switch_root followed by dinit. The ISO still provides kernel and initramfs; this is not yet an independently installed bootable disk.
ROOT_STORAGE=gpt-ext4 creates a new 1 GiB regular disk.raw. The builder uses holyinstall disk plan/apply and includes the reviewed plan hash in the boot plan. The GPT contains a 1 MiB BIOS Boot partition, a 256 MiB FAT32 ESP and a 765 MiB ext4 root. It verifies the partition layout, copies the kernel, initramfs and Limine configuration to the ESP, reads those files back and compares their bytes. It then uses holyinstall disk finalize-plan/apply to bind the ESP, root and disk images by SHA-256 before copying their bytes into the partitions. The finalize journal records the write stages. Limine BIOS stages use partition 1; on x86_64 the UEFI loader uses EFI/BOOT/BOOTX64.EFI. The kernel mounts /dev/vda3. Host filesystems are not mounted and physical disks are not accepted as output.
The GPT profile passes holy.esp=/dev/vda2. holy-init mounts that FAT partition at /boot before starting dinit, using nosuid,nodev,noexec and masks matching the kernel package's file and directory modes. The kernel must include vfat, the default FAT codepage and NLS charset. The boot probe checks the mount, kernel/initramfs/config presence and a plan-bound write that survives reboot. Installed package checking validates the mounted kernel payload against its recorded manifest. This exposes the actual boot files to subsequent operations; coordinated kernel/initramfs update transactions remain to be implemented.
The profile installs merged-/usr links, a locked root account and the dinit services boot, console, mdevd, coldplug and holy-test. mdevd reports readiness through a pipe before synchronous coldplug starts. The fixture checks that coldplug applied the /dev/null permission rule. This is device-manager coverage, not client libudev compatibility.
INITRAMFS AND ISO
The private dracut module copies the installed root with preserved ownership and file contents. dracut includes only the Holy module, with no host-only configuration, host runtime dependencies, microcode or kernel modules. The builder points dracut's sysroot at the installed root. It extracts the finished initramfs, compares payload bytes, modes and symlinks with that root, and checks each ELF with holypkg. Only dracut's module list, build parameters and generated loader caches may be additional files. The audit rejects unexpected loader aliases. dracut uses ldconfig -X so package symlinks remain unchanged. The audit also rejects non-static ELF outside the five declared libc and probe paths in dual-libc; static-core rejects all non-static ELF. The prior package-set scan validates the libc/probe graph and payload manifests. Omitted files are listed in the build record and remain absent from the image until guest recovery. holy-init mounts the live runtime filesystems and executes dinit. Limine loads the kernel and initramfs from the ISO. The default entry selects the guest test service with holy.test=1. Remove that argument in Limine's entry editor to reach the BusyBox console service.
The output includes packages/, inputs/, root/, initramfs.img, holy-ARCH.iso, build.log, build.record, root-check.record, a frozen plan and its boot-plan SHA-256. The record lists artifact hashes and the build result. Input retention permits repeating an experiment; bit-identical ISO reproduction is not established.
INSTALLER VM FIXTURE
INSTALL_TEST=1 selects an x86_64 static-core RAM ISO whose default service installs the selected base artifacts on a separate QEMU guest disk. The runner creates a blank, read-only 1 GiB raw disk and attaches a writable qcow2 overlay to the guest. The live guest uses holyinstall to partition that disk, create FAT32 and ext4 filesystems, and install Limine's BIOS stage. Its static storage tools come from STORAGE_TOOLS_PACKAGE, which the builder installs into the live image but excludes from the installed base. The guest mounts the target root, stages its local .holy archives, reviews and applies one holyinstall package plan containing doas and its setuid approval. When the live image has configured sources, the guest registers the same source IDs in the target root, copies embedded pinned mirrors, and binds source-attributed artifacts in that plan. It verifies the installed source records before declaring the package set committed. The source config remains at /etc/holy.conf on the installed disk. It generates the installed man bundle, and copies kernel, initramfs and Limine files from the ISO to the ESP. The runner shuts down that guest after serial success markers, then forks the installed overlay for separate BIOS and UEFI disk boots. Both guests check dinit, BusyBox, holypkg, package state, documentation and the mounted ESP. The install-test image contains a disposable local user and a static login probe. Each installed boot checks rejection of an incorrect password and an authenticated shell with UID 10001. The same PTY probe enters the password requested by doas and checks UID 0 from the policy's selected BusyBox command. The fixture account and scoped doas.conf are only present when INSTALL_TEST=1. The UEFI boot uses fresh writable firmware variables.
The holy-install-vm-7 report records each QEMU command, observed and missing serial markers, serial hashes, ISO and disk hashes, the guest disk-plan hash, base-image integrity and checks reached by the guest. The i686 BIOS install-to-disk fixture passed under QEMU/TCG. Its report lists the account menu, PAM/NSS, network and i686 UEFI as untested. INSTALL_FIRMWARE=bios runs only BIOS and records UEFI as untested; the x86_64 default both mode requires UEFI_CODE and UEFI_VARS, which default to the local QEMU edk2 images. This fixture verifies package installation and boot from a separate disk; it does not claim the whole interactive installer acceptance matrix. Run an existing fixture ISO with make check-install-vm ARCH=i686 ISO=FILE BOOT_PLAN=SHA256 for i686, or omit ARCH for x86_64. The i686 install fixture requires a BusyBox package with chown, login, getty, su, passwd, adduser and addgroup applets.
QEMU
make check-qemu ARCH=x86_64 ISO=FILE BOOT_PLAN=SHA256 runs the test separately. IMAGE_PROFILE defaults to dual-libc. LIBC_BOOT_STATE must match the image's declared initial state. The runner requires the corresponding recovery markers; a static-only boot cannot pass a dual-libc test. ARCH=i686 chooses qemu-system-i386 and requires an i686 guest image. The builder reads the x86 kernel boot header and rejects a kernel with the other target architecture before it creates an image. REPORT_DIR chooses an existing report parent directory, defaulting to the system temporary directory. QEMU_TIMEOUT bounds each boot in seconds (1..600, default 120). QEMU_PROBE_TIMEOUT bounds each guest stage (1..600, defaulting to QEMU_TIMEOUT). The guest emits stage-start markers; a repeated marker does not renew its deadline. The runner tracks repeated stages within each boot; the report names the boot and stage that ran or timed out. QEMU_ACCEL=auto selects usable KVM or TCG; kvm and tcg force the accelerator, with unavailable KVM returning 6.
FIRMWARE=bios is the default. FIRMWARE=uefi requires UEFI_CODE and UEFI_VARS. The runner copies both firmware inputs and gives the guest a new writable VARS file. It copies and hashes the ISO before starting QEMU. Networking is disabled by default, and no host writable disks or directories are attached. For NETWORK_RECOVERY=fixture, the runner creates a private network namespace, raises its loopback, serves the two frozen artifacts over HTTPS and connects QEMU user networking only to that namespace. Its host has no external route. The guest uses 10.0.2.15/24, resolves fixture.holy.test through the fixture DNS server and fetches both archives by that HTTPS name. The report records DNS queries, the CA and artifact hashes, HTTP requests and recovery markers. This fixture does not test a public CA bundle or external network access.
ROOT_DISK supplies a self-contained regular raw or qcow2 disk image for the persistent dual-libc test. An external qcow2 backing chain is rejected. The runner copies the input to a read-only base, creates a qcow2 overlay and attaches only that overlay through virtio-blk. It verifies the copied base hash after QEMU exits and records the overlay hash. QEMU_KEEP=1 retains the overlay and copied inputs for inspection. The default removes those temporary files after writing the report and serial log. Copying a live input is marked unverified for consistency; the caller must provide a stopped image or stable snapshot for a precise trial. ROOT_IMAGE alone remains an optional report input and does not attach a disk.
The guest verifies that / is ext4, completes recovery and its package probes, records the plan ID under /var/lib/holy-boot-test/reboot, calls static sync and requests reboot. On the second boot it requires that witness and intact restored libc; it repeats PID 1, shell, device, dynamic/IPC and package probes without another libc repair. The runner keeps each boot's markers separate, rejects repeated or out-of-order boot numbers and requires both complete contracts. Each boot has its own QEMU_TIMEOUT deadline. RAM tests still use one boot and QEMU's no-reboot mode.
Guest markers must confirm the expected plan and architecture, dinit as PID 1 by /proc/1/exe, a BusyBox shell probe, mdevd/coldplug and a complete local two-package dependency-set install/check/remove, including refusal to remove the referenced provider first. The guest checks the critical executables' static ELF classification. For static-core it also checks absence of the four expected dynamic loaders. For dual-libc it verifies the declared initial presence/absence of the runtime files, diagnoses broken providers, reads the local LZ4 archives and executes missing-only repair inside the guest. It runs allocation, thread and clock probes with each libc, checks both standard loader links and exchanges data through a pipe in both directions between the two ABIs. KERNEL_VERSION additionally requires the matching release marker. A failure marker overrides pass markers. The runner sends QMP quit after the probes or a boot timeout, waits up to five seconds, then kills a process which did not exit. Process launch or exit status alone cannot pass a boot test.
Each run retains serial.log, QEMU output, a text report and report.json (holy-qemu-report-2). Reports include argv, accelerator, firmware, input hashes, elapsed time, stage deadlines, exit reason and per-marker status. The report records whether temporary inputs and the writable overlay remain. Persistent tests additionally record both boot results, the first-boot completion time, root storage type and whether the read-only base stayed intact. The probe list records each stage's elapsed time and pass/fail/unknown status. Optional KERNEL_IMAGE, INITRAMFS and ROOT_IMAGE inputs add their hashes. They identify caller-supplied files, not guest measurements of running code.
Retained recovery matrix
make check-recovery-matrix REPORT=NEW_FILE REPORTS="REPORT_PATHS" validates eight retained QEMU report.json files: present, glibc, musl and both initial states, each under BIOS and UEFI. Each case must contain two complete ext4 boots. The validator checks per-boot serial evidence and reported checks, including PID 1 identity after reboot. It rehashes the retained ISO, raw root, recovery overlay and UEFI code snapshots. BIOS and UEFI runs of each initial state must use the same plan, ISO and raw root. Missing or duplicate cases, changed snapshots and incomplete second boots fail the matrix.
The command creates a new holy-recovery-matrix-1 JSON report containing each input report and serial hash, image identities, acceleration and elapsed time. The output must not exist. Run the guest tests before invoking this command; the validator reads retained evidence and does not start new VMs. Local hashes bind the retained files, not an authenticated publisher. Preserve each run directory alongside the summary for revalidation.
COVERAGE
BOOT_MEDIA=disk selects standalone disk boot in the QEMU runner and requires ROOT_DISK. ISO must be empty; the runner attaches no CD-ROM. Each run uses its own qcow2 overlay and records boot_media, two boot contracts and the unchanged copied base. gpt-ext4 produces disk.raw, disk.plan, disk-layout.json and limine.conf instead of an ISO. BIOS and UEFI remain separate test runs.
The bootstrap glibc package currently contains libc.so.6 and its loader with licenses, not the complete SDK, locale archive, NSS modules or auxiliary glibc libraries. The reference image still needs the installer, ordinary networking and public CA packaging. The ext4 fixture uses an ISO kernel; gpt-ext4 loads its kernel and initramfs from the disk's ESP. Both test recovery followed by reboot on a persistent root overlay. The GPT builder is not the interactive installer. Kernel updates still require integration with ESP contents and the generated initramfs. Those gates remain separate from this boot result. The glibc/musl/both damage fixtures retain package records; remove-both exercises package removal and reinstallation after a persistent reboot. The repository documentation bundle contains Holy pages. The image builder uses holypkg docs after its package transaction to generate /usr/share/holy/llm.txt from the installed man sources, including dinit. This generated file is separate from package-owned source pages; its SHA-256 is recorded in the frozen image plan and /etc/holy/docs.sha256. The output directory retains llm.txt and docs.record with the coverage summary. Packages without man pages and unsupported page encodings appear in that bundle.
On each boot, the guest verifies the shipped bundle's digest and regenerates it with static holypkg after any libc recovery. It compares the complete contents, normalizing only the final summary's database generation because package transactions between boots advance that counter. A documentation failure fails the boot contract. The bundle contains attributed roff sources rather than rendered upstream text. QEMU virtual devices do not establish physical GPU support.
make check-image-docs IMAGE_DIRECTORY=DIRECTORY tests an ext4/dual-libc/both build's documentation failures. It copies root.ext4 into two disposable disks, changes the generated bundle in one and the installed holypkg man source in the other, then boots each through the original ISO under BIOS/TCG. Both guests must stop the boot contract at the documentation stage. The original root image's hash must remain unchanged. The fixture requires debugfs and the QEMU tools; it never edits the original image or mounts a host disk.
HARDWARE
make check-hardware runs outside QEMU against a physical NVIDIA GPU using Nouveau and Mesa NVK. It checks the kernel driver and DRM render node, selects the NVK Vulkan device, presents 30 frames with vkcube, and measures OpenGL frames with glxgears. glxinfo must report an accelerated NVIDIA renderer. lspci, vulkaninfo, vkcube, glxinfo, glxgears, timeout, a display session and a writable render node are required.
The default HARDWARE_SCOPE=holy requires a valid installed Holy database on the running system. Missing inputs return 6. REPORT defaults to out/hardware.json; the holy-hardware-1 JSON records probe commands, output, elapsed times, driver selection and frame counts. A failure returns 1. HARDWARE_SCOPE=host permits a diagnostic probe on another distribution and marks release_gate=false. A host result cannot certify Holy's packages or boot image. The frame probes establish presentation and an FPS count, not pixel-correct rendering or broader game compatibility.
SOURCES
Limine configuration and ISO layout: https://github.com/limine-bootloader/limine/blob/v12.x/CONFIG.md and https://github.com/limine-bootloader/limine/blob/v12.x/USAGE.md. dracut module interface: https://github.com/dracut-ng/dracut-ng/blob/main/man/dracut.modules.7.adoc. QEMU block device and backing-file options: https://www.qemu.org/docs/master/system/invocation.html.
SEE ALSO
holy-init (8),holypkg (8)