# Phase 18.3: bootable Fruix installer ISO on FreeBSD Date: 2026-04-04 ## Goal Phase 18.3 extends the Phase 18.2 installer-environment work from a disk-image-style installer into a UEFI-bootable ISO artifact. The intended first ISO is deliberately narrow: - UEFI only - serial-console-friendly - non-interactive install flow reused from Phase 18.1/18.2 - target disk installation still performed by the same Fruix-managed in-guest installer logic ## Implementation ### New API Added in `modules/fruix/system/freebsd.scm`: - `operating-system-installer-iso-spec` - `materialize-installer-iso` The system module split done immediately before this phase was also exercised during this work. ### New CLI action Added in `scripts/fruix.scm`: - `fruix system installer-iso` This action emits metadata for: - ISO store path - ISO image path - EFI boot image path - installer root image path - installer and target closure paths - installer state/log paths - declared/materialized FreeBSD source metadata - store closure counts ### ISO boot model The ISO does not try to run the Fruix installer directly from a read-only cd9660 root. Instead it uses a small UEFI El Torito boot image plus an in-memory installer root image: 1. a small FAT EFI boot image contains `EFI/BOOT/BOOTX64.EFI` 2. the ISO root contains real boot assets under `/boot` 3. the ISO root also contains `/boot/root.img` 4. `loader.conf` on the ISO is augmented with: - `mdroot_load="YES"` - `mdroot_type="mfs_root"` - `mdroot_name="/boot/root.img"` - `vfs.root.mountfrom="ufs:/dev/md0"` - `vfs.root.mountfrom.options="rw"` A practical loader detail surfaced during validation: - setting `rootdev` or `currdev` to `md0:` in the ISO loader path is wrong for this loader configuration and caused an early EFI-loader crash before kernel handoff - the reliable ISO path is to let loader keep its current device on the CD media, preload `/boot/root.img`, and pass only `vfs.root.mountfrom=ufs:/dev/md0` This preserves the existing Fruix installer environment semantics while avoiding the need to make the whole installer operate directly from a read-only ISO root. ### Installer root image contents `materialize-installer-iso` stages the same installer payload model already validated in Phase 18.2: - installer closure - target closure - target runtime store closure needed for installation/boot - staged target rootfs under `/var/lib/fruix/installer/target-rootfs` - installer plan and state files under `/var/lib/fruix/installer` - installer helper scripts: - `/usr/local/libexec/fruix-installer-run` - `/usr/local/etc/rc.d/fruix-installer` The ISO root image is then built as a UFS image and embedded as `/boot/root.img`. ### Split-regression fixes found during this work While exercising the refactored split modules, two issues surfaced and were fixed: 1. `string-hash` name-clash warnings - the old helper name collided with Guile/SRFI bindings - it was renamed to `sha256-string` 2. missing `prefix-materializer-version` - this constant was accidentally omitted when `modules/fruix/system/freebsd.scm` was split - the missing definition was restored in `modules/fruix/system/freebsd/build.scm` ## Validation ### Completed smoke validation A host-side smoke build was completed successfully for the new ISO builder using a host-staged operating-system definition: - command pattern: - `fruix system installer-iso ...` - result: - successful ISO materialization in a temporary store - artifact checks performed: - `etdump` reports an EFI El Torito boot entry - the ISO contains: - `boot/kernel/kernel` - `boot/kernel/linker.hints` - `boot/loader.conf` - `boot/loader.efi` - `boot/root.img` - `boot/loader.conf` inside the ISO contains the expected `mdroot_*` and `vfs.root.mountfrom` entries Example smoke-build metadata: ```text action=installer-iso iso_volume_label=FRUIX_INSTALLER iso_store_path=/tmp/...-fruix-installer-iso-fruix-freebsd-installer iso_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/installer.iso boot_efi_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/efiboot.img root_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/root.img installer_closure_path=/tmp/...-fruix-system-fruix-freebsd-installer target_closure_path=/tmp/...-fruix-system-fruix-freebsd ``` ### End-to-end harness validation Added: - `tests/system/run-phase18-installer-iso.sh` This harness validates the full Phase 18.3 flow: 1. build installer ISO 2. boot it under QEMU/UEFI/TCG 3. install onto a target disk from inside the booted ISO environment 4. boot the installed target Passing validation: - `PASS phase18-installer-iso` Validated result summary: ```text installer_iso_store_path=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer installer_iso_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/installer.iso installer_boot_efi_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/efiboot.img installer_root_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/root.img install_target_device=/dev/vtbd0 freebsd_source_kind=git freebsd_source_ref=stable/15 freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89 materialized_source_store=/frx/store/459499e0eb29f4c73ad455060dd2502d21fb56f205c0a676831cf723b3a0c378-freebsd-source-stable15-installer-iso-target-source installer_state=done installer_sshd_status=running target_esp_fstype=msdosfs target_root_fstype=ufs target_shepherd_status=running target_sshd_status=running installer_iso_boot=ok installer_iso_install=ok installed_target_boot=ok ``` Notable QEMU-specific ISO validation detail: - unlike the disk-image-style installer environment from Phase 18.2, the ISO boots from `cd0`, so the target virtio disk appears as: - `/dev/vtbd0` - the earlier installer-environment default: - `/dev/vtbd1` remains correct for the disk-image installer, but not for the ISO path The harness verified all of the following: 1. `fruix system installer-iso` produces a bootable ISO artifact in `/frx/store` 2. the ISO boots successfully under QEMU/UEFI/TCG 3. the booted installer ISO environment becomes reachable over SSH 4. `/run/current-system` inside the installer ISO points at the installer closure 5. the installer rc.d job reaches: - `state=done` 6. the installer log records: - `fruix-installer:done` 7. the installed target disk contains: - GPT partitioning - EFI filesystem: `msdosfs` - root filesystem: `ufs` - `EFI/BOOT/BOOTX64.EFI` - `/var/lib/fruix/install.scm` 8. the installed target then boots successfully as its own Fruix system under QEMU/UEFI/TCG 9. after target boot: - `/run/current-system` points at the target closure - shepherd is running - `sshd` is running - activation completed successfully ### Real XCP-ng validation Added: - `tests/system/run-phase18-installer-iso-xcpng.sh` This harness validates the same installer-iso workflow on the approved real XCP-ng path: - VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289` - ISO SR: `537a6219-8452-7cb5-8d56-5eed6910c7a2` - target VDIs: - `0f1f90d3-48ca-4fa2-91d8-fc6339b95743` - `7061d761-3639-4bec-87f7-2ba1af924eaa` Because the current `xo-cli disk.import @=/path/to.iso` path returned an HTTP 500 error in this environment, the harness imports the ISO into the ISO SR via a temporary local HTTP URL, then inserts the resulting ISO VDI into the VM's CD drive. Passing validation: - `PASS phase18-installer-iso-xcpng` Validated result summary: ```text vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289 iso_id= guest_ip=192.168.213.62 installer_state=done installer_target_device=/dev/ada0 kern_disks=cd0 ada1 ada0 installer_run_current_system=/frx/store/16969e825dbb65b5c27180030d4a7d98821893460fb3dccdc863ff6156ed61e0-fruix-system-fruix-freebsd-installer installer_sshd_status=running target_run_current_system=/frx/store/a98d3af6a1afbc4a927d47cea6458d5a70747b051ed994e5d9ff1ae79c4f2b42-fruix-system-fruix-freebsd target_sshd_status=running target_shepherd_status=running ``` Important XCP-ng-specific details: - the installer ISO still boots from: - `cd0` - on this Xen HVM path, the primary target disk is exposed through Xen block front as `xbd0` and appears to FreeBSD as: - `/dev/ada0` - therefore the XCP-ng installer-iso path must target: - `/dev/ada0` rather than QEMU's: - `/dev/vtbd0` - the visible EFI console can appear to stop at: - `console vidconsole is unavailable` but boot still continues and the installer becomes reachable over SSH; that message was not the actual failure mode on XCP-ng The harness verified all of the following on the real VM path: 1. `fruix system installer-iso` builds a bootable ISO with `--install-target-device /dev/ada0` 2. the ISO can be imported into the operator-approved ISO SR and attached to the approved VM 3. the VM boots the Fruix installer ISO successfully under UEFI 4. the installer environment becomes reachable over SSH 5. inside the installer guest: - `kern.disks` includes `cd0` and `ada0` - `/run/current-system` points at the installer closure - the installer reaches `state=done` 6. the installed target on `ada0` is partitioned and formatted correctly 7. after ejecting the ISO and rebooting, the installed target boots successfully on the same XCP-ng VM 8. after target boot: - `/run/current-system` points at the target closure - shepherd is running - `sshd` is running - activation completed successfully - `/var/lib/fruix/install.scm` still records the materialized source store provenance ## Result Phase 18.3 is complete. Fruix now has a validated bootable UEFI installer ISO on FreeBSD that can: - boot into a Fruix-managed installer environment from ISO media - perform the non-interactive installation flow onto a target disk - boot the installed target successfully - and do so on both: - local `QEMU/UEFI/TCG` - the approved real `XCP-ng` VM path