diff --git a/docs/reports/phase18-installer-iso-freebsd.md b/docs/reports/phase18-installer-iso-freebsd.md new file mode 100644 index 0000000..b6ebf39 --- /dev/null +++ b/docs/reports/phase18-installer-iso-freebsd.md @@ -0,0 +1,139 @@ +# 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="md_image"` + - `mdroot_name="/boot/root.img"` + - `rootdev="ufs:/dev/md0"` + - `vfs.root.mountfrom="ufs:/dev/md0"` + - `vfs.root.mountfrom.options="rw"` + +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 store closure +- 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` + +## Current validation status + +### 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 +``` + +### Validation harness added + +Added: + +- `tests/system/run-phase18-installer-iso.sh` + +This harness is intended to validate the full Phase 18.3 flow: + +1. build installer ISO +2. boot it under QEMU/UEFI/TCG +3. install onto a second disk from inside the booted ISO environment +4. boot the installed target + +## Status + +Phase 18.3 implementation is now in place, with successful build-smoke validation and a dedicated end-to-end harness added. + +The remaining step is full end-to-end boot/install validation of the ISO path under QEMU/UEFI/TCG and, if practical, the broader validated virtualization path. diff --git a/modules/fruix/system/freebsd.scm b/modules/fruix/system/freebsd.scm index 4c0c8c7..f30743b 100644 --- a/modules/fruix/system/freebsd.scm +++ b/modules/fruix/system/freebsd.scm @@ -46,10 +46,12 @@ operating-system-install-spec operating-system-image-spec operating-system-installer-image-spec + operating-system-installer-iso-spec installer-operating-system materialize-operating-system materialize-rootfs install-operating-system materialize-bhyve-image materialize-installer-image + materialize-installer-iso default-minimal-operating-system)) diff --git a/modules/fruix/system/freebsd/build.scm b/modules/fruix/system/freebsd/build.scm index 90074b6..270bfc9 100644 --- a/modules/fruix/system/freebsd/build.scm +++ b/modules/fruix/system/freebsd/build.scm @@ -155,7 +155,7 @@ (define (native-build-root common) (string-append "/var/tmp/fruix-freebsd-native-build-" - (string-hash (object->string common)))) + (sha256-string (object->string common)))) (define (native-make-arguments common _build-root) (append @@ -254,7 +254,7 @@ (let* ((plan (freebsd-package-install-plan package)) (common (native-build-common-manifest plan)) (build-root (native-build-root common)) - (stage-root (string-append build-root "/stage-" (freebsd-package-name package) "-" (string-hash manifest))) + (stage-root (string-append build-root "/stage-" (freebsd-package-name package) "-" (sha256-string manifest))) (install-log (string-append build-root "/logs/install-" (freebsd-package-name package) ".log")) (final-stage-root (case (freebsd-package-build-system package) @@ -312,7 +312,7 @@ #:sha256 (build-plan-ref plan 'base-source-sha256 #f))) (define (source-cache-key source) - (string-hash (object->string (freebsd-source-spec source)))) + (sha256-string (object->string (freebsd-source-spec source)))) (define (materialize-freebsd-source/cached source store-dir source-cache) (let* ((key (source-cache-key source)) @@ -360,11 +360,11 @@ input-paths)) (effective-input-paths (filter identity effective-input-paths)) (manifest (package-manifest-string prepared-package effective-input-paths)) - (cache-key (string-hash manifest)) + (cache-key (sha256-string manifest)) (cached (hash-ref cache cache-key #f))) (if cached cached - (let* ((hash (string-hash manifest)) + (let* ((hash (sha256-string manifest)) (output-path (string-append store-dir "/" hash "-" (freebsd-package-name prepared-package) "-" @@ -419,6 +419,8 @@ (delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/shepherd/config.go")))) #t) +(define prefix-materializer-version "3") + (define (prefix-manifest-string source-path extra-files) (string-append "prefix-materializer-version=" prefix-materializer-version "\n" @@ -452,7 +454,7 @@ (define* (materialize-prefix source-path name version store-dir #:key (extra-files '())) (let* ((manifest (prefix-manifest-string source-path extra-files)) - (hash (string-hash manifest)) + (hash (sha256-string manifest)) (output-path (string-append store-dir "/" hash "-" name "-" version))) (unless (file-exists? output-path) (mkdir-p output-path) diff --git a/modules/fruix/system/freebsd/media.scm b/modules/fruix/system/freebsd/media.scm index 4cb1702..3fdccbe 100644 --- a/modules/fruix/system/freebsd/media.scm +++ b/modules/fruix/system/freebsd/media.scm @@ -10,15 +10,18 @@ #:use-module (ice-9 hash-table) #:use-module (srfi srfi-1) #:use-module (srfi srfi-13) + #:use-module (rnrs io ports) #:export (operating-system-install-spec operating-system-image-spec operating-system-installer-image-spec + operating-system-installer-iso-spec installer-operating-system materialize-operating-system materialize-rootfs install-operating-system materialize-bhyve-image - materialize-installer-image)) + materialize-installer-image + materialize-installer-iso)) (define (same-file-contents? a b) (zero? (system* "cmp" "-s" a b))) @@ -169,7 +172,7 @@ "\n") "\nreferences=\n" (string-join references "\n"))) - (hash (string-hash manifest)) + (hash (sha256-string manifest)) (closure-path (string-append store-dir "/" hash "-fruix-system-" (operating-system-host-name os)))) (unless (file-exists? closure-path) @@ -459,9 +462,39 @@ #:serial-console serial-console)) (target-install . ,target-install-spec)))) +(define* (operating-system-installer-iso-spec os + #:key + (install-target-device "/dev/vtbd1") + (installer-host-name (string-append (operating-system-host-name os) + "-installer")) + (root-size #f) + (iso-volume-label "FRUIX_INSTALLER") + (installer-root-partition-label "fruix-installer-root") + (target-efi-partition-label "efiboot") + (target-root-partition-label "fruix-root") + (serial-console "comconsole")) + (let ((target-install-spec (operating-system-install-spec os + #:target install-target-device + #:target-kind 'block-device + #:efi-size "64m" + #:root-size #f + #:disk-capacity #f + #:efi-partition-label target-efi-partition-label + #:root-partition-label target-root-partition-label + #:serial-console serial-console))) + `((installer-host-name . ,installer-host-name) + (install-target-device . ,install-target-device) + (boot-mode . uefi) + (image-format . iso9660) + (iso-volume-label . ,iso-volume-label) + (root-size . ,root-size) + (installer-root-partition-label . ,installer-root-partition-label) + (target-install . ,target-install-spec)))) + (define image-builder-version "2") (define install-builder-version "1") (define installer-image-builder-version "1") +(define installer-iso-builder-version "1") (define (operating-system-install-metadata-object install-spec closure-path store-items) `((install-version . ,install-builder-version) @@ -754,7 +787,7 @@ "\nstore-items=\n" (string-join store-items "\n") "\n")) - (hash (string-hash manifest)) + (hash (sha256-string manifest)) (image-store-path (string-append store-dir "/" hash "-fruix-bhyve-image-" (operating-system-host-name os))) (disk-image (string-append image-store-path "/disk.img")) @@ -908,7 +941,7 @@ "\ninstall-metadata=\n" (object->string install-metadata) "\n")) - (hash (string-hash manifest)) + (hash (sha256-string manifest)) (image-store-path (string-append store-dir "/" hash "-fruix-installer-image-" (operating-system-host-name installer-os))) (disk-image (string-append image-store-path "/disk.img")) @@ -1029,3 +1062,277 @@ (store-items . ,combined-store-items) (target-store-items . ,target-store-items) (installer-store-items . ,installer-store-items)))) + +(define (resolved-path path) + (let ((target (false-if-exception (readlink path)))) + (if target + (if (string-prefix? "/" target) + target + (string-append (dirname path) "/" target)) + path))) + +(define (copy-resolved-node source destination) + (copy-node (resolved-path source) destination)) + +(define (sanitize-iso-volume-label label) + (let* ((text (if (and (string? label) (not (string-null? label))) + label + "FRUIX_INSTALLER")) + (upper (string-upcase text)) + (chars (map (lambda (ch) + (if (or (char-alphabetic? ch) + (char-numeric? ch) + (memv ch '(#\_ #\-))) + ch + #\_)) + (string->list upper))) + (sanitized (list->string chars))) + (if (> (string-length sanitized) 32) + (substring sanitized 0 32) + sanitized))) + +(define (write-installer-iso-loader-conf source-path destination) + (let* ((mode (stat:perms (stat source-path))) + (base (call-with-input-file source-path get-string-all)) + (extra (string-append + "mdroot_load=\"YES\"\n" + "mdroot_type=\"md_image\"\n" + "mdroot_name=\"/boot/root.img\"\n" + "rootdev=\"ufs:/dev/md0\"\n" + "vfs.root.mountfrom=\"ufs:/dev/md0\"\n" + "vfs.root.mountfrom.options=\"rw\"\n"))) + (write-file destination + (string-append base + (if (or (string-null? base) + (char=? (string-ref base (- (string-length base) 1)) #\newline)) + "" + "\n") + extra)) + (chmod destination mode))) + +(define* (make-ufs-image output-path source-root label #:key size) + (apply run-command + (append (list "makefs" "-t" "ffs" "-T" "0" "-B" "little") + (if size + (list "-s" size) + '()) + (list "-o" (string-append "label=" label + ",version=2,bsize=32768,fsize=4096,density=16384") + output-path + source-root)))) + +(define (make-efi-boot-image loader-efi output-path) + (let ((stage-root (mktemp-directory "/tmp/fruix-installer-iso-esp.XXXXXX"))) + (dynamic-wind + (lambda () #t) + (lambda () + (mkdir-p (string-append stage-root "/EFI/BOOT")) + (copy-regular-file loader-efi + (string-append stage-root "/EFI/BOOT/BOOTX64.EFI")) + (run-command "makefs" "-t" "msdos" "-T" "0" + "-o" "fat_type=12" + "-o" "sectors_per_cluster=1" + "-o" "volume_label=EFISYS" + "-s" "2048k" + output-path stage-root)) + (lambda () + (when (file-exists? stage-root) + (delete-file-recursively stage-root)))))) + +(define (populate-installer-iso-boot-tree installer-closure-path iso-root root-image-path) + (let ((boot-root (string-append iso-root "/boot"))) + (mkdir-p (string-append boot-root "/kernel")) + (copy-resolved-node (string-append installer-closure-path "/boot/kernel/kernel") + (string-append boot-root "/kernel/kernel")) + (copy-resolved-node (string-append installer-closure-path "/boot/kernel/linker.hints") + (string-append boot-root "/kernel/linker.hints")) + (copy-resolved-node (string-append installer-closure-path "/boot/loader") + (string-append boot-root "/loader")) + (copy-resolved-node (string-append installer-closure-path "/boot/loader.efi") + (string-append boot-root "/loader.efi")) + (copy-resolved-node (string-append installer-closure-path "/boot/device.hints") + (string-append boot-root "/device.hints")) + (copy-resolved-node (string-append installer-closure-path "/boot/defaults") + (string-append boot-root "/defaults")) + (copy-resolved-node (string-append installer-closure-path "/boot/lua") + (string-append boot-root "/lua")) + (write-installer-iso-loader-conf (string-append installer-closure-path "/boot/loader.conf") + (string-append boot-root "/loader.conf")) + (copy-regular-file root-image-path + (string-append boot-root "/root.img")))) + +(define* (materialize-installer-iso os + #:key + (store-dir "/frx/store") + (guile-prefix "/tmp/guile-freebsd-validate-install") + (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") + (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (install-target-device "/dev/vtbd1") + (root-size #f) + (installer-host-name (string-append (operating-system-host-name os) + "-installer")) + (installer-root-partition-label "fruix-installer-root") + (target-efi-partition-label "efiboot") + (target-root-partition-label "fruix-root") + (serial-console "comconsole") + (iso-volume-label "FRUIX_INSTALLER")) + (let* ((installer-os (installer-operating-system os + #:host-name installer-host-name + #:root-partition-label installer-root-partition-label)) + (target-closure (materialize-operating-system os + #:store-dir store-dir + #:guile-prefix guile-prefix + #:guile-extra-prefix guile-extra-prefix + #:shepherd-prefix shepherd-prefix)) + (installer-closure (materialize-operating-system installer-os + #:store-dir store-dir + #:guile-prefix guile-prefix + #:guile-extra-prefix guile-extra-prefix + #:shepherd-prefix shepherd-prefix)) + (target-closure-path (assoc-ref target-closure 'closure-path)) + (installer-closure-path (assoc-ref installer-closure 'closure-path)) + (target-store-items (store-reference-closure (list target-closure-path))) + (installer-store-items (store-reference-closure (list installer-closure-path))) + (combined-store-items (delete-duplicates (append installer-store-items target-store-items))) + (sanitized-iso-volume-label (sanitize-iso-volume-label iso-volume-label)) + (installer-iso-spec (operating-system-installer-iso-spec os + #:install-target-device install-target-device + #:installer-host-name installer-host-name + #:root-size root-size + #:iso-volume-label sanitized-iso-volume-label + #:installer-root-partition-label installer-root-partition-label + #:target-efi-partition-label target-efi-partition-label + #:target-root-partition-label target-root-partition-label + #:serial-console serial-console)) + (target-install-spec (assoc-ref installer-iso-spec 'target-install)) + (install-metadata (operating-system-install-metadata-object target-install-spec + target-closure-path + target-store-items)) + (installer-plan-directory "/var/lib/fruix/installer") + (installer-state-path (string-append installer-plan-directory "/state")) + (installer-log-path "/var/log/fruix-installer.log") + (manifest (string-append + "installer-iso-builder-version=\n" + installer-iso-builder-version + "\ninstaller-iso-spec=\n" + (object->string installer-iso-spec) + "installer-closure-path=\n" + installer-closure-path + "\ntarget-closure-path=\n" + target-closure-path + "\ncombined-store-items=\n" + (string-join combined-store-items "\n") + "\ntarget-store-items=\n" + (string-join target-store-items "\n") + "\ninstall-metadata=\n" + (object->string install-metadata) + "\n")) + (hash (sha256-string manifest)) + (iso-store-path (string-append store-dir "/" hash "-fruix-installer-iso-" + (operating-system-host-name installer-os))) + (iso-image (string-append iso-store-path "/installer.iso")) + (boot-efi-image (string-append iso-store-path "/efiboot.img")) + (root-image (string-append iso-store-path "/root.img"))) + (unless (file-exists? iso-store-path) + (let* ((build-root (mktemp-directory "/tmp/fruix-installer-iso-build.XXXXXX")) + (installer-rootfs (string-append build-root "/installer-rootfs")) + (target-rootfs (string-append build-root "/target-rootfs")) + (image-rootfs (string-append build-root "/image-rootfs")) + (iso-root (string-append build-root "/iso-root")) + (temp-output (mktemp-directory (string-append store-dir "/.fruix-installer-iso.XXXXXX"))) + (temp-iso (string-append build-root "/installer.iso")) + (temp-esp (string-append build-root "/efiboot.img")) + (temp-root (string-append build-root "/root.img")) + (plan-root (string-append image-rootfs installer-plan-directory))) + (dynamic-wind + (lambda () #t) + (lambda () + (populate-rootfs-from-closure installer-os installer-rootfs installer-closure-path) + (populate-rootfs-from-closure os target-rootfs target-closure-path) + (copy-rootfs-for-image installer-rootfs image-rootfs) + (mkdir-p plan-root) + (mkdir-p (string-append image-rootfs "/usr/local/libexec")) + (mkdir-p (string-append image-rootfs "/usr/local/etc/rc.d")) + (mkdir-p (string-append plan-root "/target-rootfs")) + (copy-tree-contents target-rootfs (string-append plan-root "/target-rootfs")) + (copy-store-items-into-rootfs image-rootfs store-dir combined-store-items) + (write-file (string-append plan-root "/store-items") + (string-append (string-join (map path-basename target-store-items) "\n") "\n")) + (write-file (string-append plan-root "/install.scm") + (object->string install-metadata)) + (copy-regular-file (string-append target-closure-path "/boot/loader.efi") + (string-append plan-root "/loader.efi")) + (write-file (string-append plan-root "/target-device") + (string-append install-target-device "\n")) + (write-file (string-append plan-root "/efi-size") "64m\n") + (write-file (string-append plan-root "/efi-partition-label") + (string-append target-efi-partition-label "\n")) + (write-file (string-append plan-root "/root-partition-label") + (string-append target-root-partition-label "\n")) + (write-file (string-append plan-root "/state") "pending\n") + (write-file (string-append image-rootfs "/usr/local/libexec/fruix-installer-run") + (render-installer-run-script store-dir installer-plan-directory)) + (write-file (string-append image-rootfs "/usr/local/etc/rc.d/fruix-installer") + (render-installer-rc-script installer-plan-directory)) + (chmod (string-append image-rootfs "/usr/local/libexec/fruix-installer-run") #o555) + (chmod (string-append image-rootfs "/usr/local/etc/rc.d/fruix-installer") #o555) + (make-ufs-image temp-root image-rootfs installer-root-partition-label #:size root-size) + (populate-installer-iso-boot-tree installer-closure-path iso-root temp-root) + (make-efi-boot-image (resolved-path (string-append installer-closure-path "/boot/loader.efi")) temp-esp) + (run-command "makefs" "-t" "cd9660" "-T" "0" + "-o" (string-append "bootimage=efi;" temp-esp) + "-o" "no-emul-boot" + "-o" "platformid=efi" + "-o" "rockridge" + "-o" (string-append "label=" sanitized-iso-volume-label) + temp-iso iso-root) + (mkdir-p temp-output) + (copy-regular-file temp-iso (string-append temp-output "/installer.iso")) + (copy-regular-file temp-esp (string-append temp-output "/efiboot.img")) + (copy-regular-file temp-root (string-append temp-output "/root.img")) + (write-file (string-append temp-output "/installer-iso-spec.scm") + (object->string installer-iso-spec)) + (write-file (string-append temp-output "/installer-closure-path") installer-closure-path) + (write-file (string-append temp-output "/target-closure-path") target-closure-path) + (write-file (string-append temp-output "/.references") + (string-join combined-store-items "\n")) + (write-file (string-append temp-output "/.fruix-package") manifest) + (chmod temp-output #o755) + (for-each (lambda (path) + (chmod path #o644)) + (list (string-append temp-output "/installer.iso") + (string-append temp-output "/efiboot.img") + (string-append temp-output "/root.img") + (string-append temp-output "/installer-iso-spec.scm") + (string-append temp-output "/installer-closure-path") + (string-append temp-output "/target-closure-path") + (string-append temp-output "/.references") + (string-append temp-output "/.fruix-package"))) + (rename-file temp-output iso-store-path)) + (lambda () + (when (file-exists? build-root) + (delete-file-recursively build-root)))))) + `((iso-store-path . ,iso-store-path) + (iso-image . ,iso-image) + (boot-efi-image . ,boot-efi-image) + (root-image . ,root-image) + (installer-closure-path . ,installer-closure-path) + (target-closure-path . ,target-closure-path) + (closure-path . ,installer-closure-path) + (installer-iso-spec . ,installer-iso-spec) + (install-spec . ,target-install-spec) + (installer-state-path . ,installer-state-path) + (installer-log-path . ,installer-log-path) + (install-target-device . ,install-target-device) + (host-base-stores . ,(assoc-ref target-closure 'host-base-stores)) + (native-base-stores . ,(assoc-ref target-closure 'native-base-stores)) + (fruix-runtime-stores . ,(assoc-ref target-closure 'fruix-runtime-stores)) + (freebsd-base-file . ,(assoc-ref target-closure 'freebsd-base-file)) + (freebsd-source-file . ,(assoc-ref target-closure 'freebsd-source-file)) + (freebsd-source-materializations-file . ,(assoc-ref target-closure 'freebsd-source-materializations-file)) + (materialized-source-stores . ,(assoc-ref target-closure 'materialized-source-stores)) + (host-base-provenance-file . ,(assoc-ref target-closure 'host-base-provenance-file)) + (store-layout-file . ,(assoc-ref target-closure 'store-layout-file)) + (store-items . ,combined-store-items) + (target-store-items . ,target-store-items) + (installer-store-items . ,installer-store-items)))) diff --git a/modules/fruix/system/freebsd/source.scm b/modules/fruix/system/freebsd/source.scm index b4be051..a508e8e 100644 --- a/modules/fruix/system/freebsd/source.scm +++ b/modules/fruix/system/freebsd/source.scm @@ -36,7 +36,7 @@ (define (ensure-git-source-cache source cache-dir) (let* ((url (freebsd-source-url source)) (repo-dir (string-append cache-dir "/git/" - (string-hash (string-append "git:" url)) + (sha256-string (string-append "git:" url)) ".git"))) (mkdir-p (dirname repo-dir)) (unless (file-exists? repo-dir) @@ -84,7 +84,7 @@ (expected-sha256 (or (normalize-expected-sha256 source) (error "src-txz freebsd source requires sha256 for materialization" source))) (archive-path (string-append cache-dir "/archives/" - (string-hash (string-append "txz:" url)) + (sha256-string (string-append "txz:" url)) "-src.txz"))) (mkdir-p (dirname archive-path)) (when (file-exists? archive-path) @@ -150,7 +150,7 @@ (effective-source (assoc-ref resolution 'effective-source)) (identity (assoc-ref resolution 'identity)) (manifest (freebsd-source-manifest source effective-source identity)) - (hash (string-hash manifest)) + (hash (sha256-string manifest)) (output-path (string-append store-dir "/" hash "-freebsd-source-" (safe-name-fragment (freebsd-source-name source)))) (info-file (string-append output-path "/.freebsd-source-info.scm")) diff --git a/modules/fruix/system/freebsd/utils.scm b/modules/fruix/system/freebsd/utils.scm index 219198c..ab13445 100644 --- a/modules/fruix/system/freebsd/utils.scm +++ b/modules/fruix/system/freebsd/utils.scm @@ -13,7 +13,7 @@ command-output safe-command-output write-file - string-hash + sha256-string file-hash directory-entries path-signature @@ -63,7 +63,7 @@ (lambda (port) (display content port)))) -(define (string-hash text) +(define (sha256-string text) (let* ((tmp (string-append (getenv* "TMPDIR" "/tmp") "/fruix-system-hash.txt"))) (write-file tmp text) (command-output "sha256" "-q" tmp))) @@ -110,7 +110,7 @@ (stable-lines (filter (lambda (line) (not (string-prefix? "#" line))) (string-split mtree-output #\newline)))) - (string-hash (string-join stable-lines "\n")))) + (sha256-string (string-join stable-lines "\n")))) (define (copy-regular-file source destination) (let ((mode (stat:perms (stat source)))) diff --git a/scripts/fruix.scm b/scripts/fruix.scm index 29f505c..11ca552 100644 --- a/scripts/fruix.scm +++ b/scripts/fruix.scm @@ -20,6 +20,7 @@ System actions:\n\ build Materialize the Fruix system closure in /frx/store.\n\ image Materialize the Fruix disk image in /frx/store.\n\ installer Materialize a bootable Fruix installer image in /frx/store.\n\ + installer-iso Materialize a bootable Fruix installer ISO in /frx/store.\n\ install Install the Fruix system onto --target PATH.\n\ rootfs Materialize a rootfs tree at --rootfs DIR or ROOTFS-DIR.\n\ \n\ @@ -27,7 +28,7 @@ System options:\n\ --system NAME Scheme variable holding the operating-system object.\n\ --store DIR Store directory to use (default: /frx/store).\n\ --disk-capacity SIZE Disk capacity for 'image', 'installer', or raw-file 'install' targets.\n\ - --root-size SIZE Root filesystem size for 'image', 'installer', or 'install' (example: 6g).\n\ + --root-size SIZE Root filesystem size for 'image', 'installer', 'installer-iso', or 'install' (example: 6g).\n\ --target PATH Install target for 'install' (raw image file or /dev/... device).\n\ --install-target-device DEVICE\n\ Target block device used by the booted 'installer' environment.\n\ @@ -476,6 +477,71 @@ Common options:\n\ (target_store_item_count . ,(length target-store-items)) (installer_store_item_count . ,(length installer-store-items)))))) +(define (emit-system-installer-iso-metadata os-file resolved-symbol store-dir os result) + (let* ((installer-iso-spec (assoc-ref result 'installer-iso-spec)) + (store-items (assoc-ref result 'store-items)) + (target-store-items (assoc-ref result 'target-store-items)) + (installer-store-items (assoc-ref result 'installer-store-items)) + (host-base-stores (assoc-ref result 'host-base-stores)) + (native-base-stores (assoc-ref result 'native-base-stores)) + (fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores)) + (base (operating-system-freebsd-base os)) + (source (freebsd-base-source base)) + (host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read))) + (emit-metadata + `((action . "installer-iso") + (os_file . ,os-file) + (system_variable . ,resolved-symbol) + (store_dir . ,store-dir) + (freebsd_base_name . ,(freebsd-base-name base)) + (freebsd_base_version_label . ,(freebsd-base-version-label base)) + (freebsd_base_release . ,(freebsd-base-release base)) + (freebsd_base_branch . ,(freebsd-base-branch base)) + (freebsd_base_source_root . ,(freebsd-base-source-root base)) + (freebsd_base_target . ,(freebsd-base-target base)) + (freebsd_base_target_arch . ,(freebsd-base-target-arch base)) + (freebsd_base_kernconf . ,(freebsd-base-kernconf base)) + (freebsd_base_file . ,(assoc-ref result 'freebsd-base-file)) + (freebsd_source_name . ,(freebsd-source-name source)) + (freebsd_source_kind . ,(freebsd-source-kind source)) + (freebsd_source_url . ,(or (freebsd-source-url source) "")) + (freebsd_source_path . ,(or (freebsd-source-path source) "")) + (freebsd_source_ref . ,(or (freebsd-source-ref source) "")) + (freebsd_source_commit . ,(or (freebsd-source-commit source) "")) + (freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) "")) + (freebsd_source_file . ,(assoc-ref result 'freebsd-source-file)) + (freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file)) + (materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores))) + (materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ",")) + (installer_host_name . ,(assoc-ref installer-iso-spec 'installer-host-name)) + (install_target_device . ,(assoc-ref result 'install-target-device)) + (iso_volume_label . ,(assoc-ref installer-iso-spec 'iso-volume-label)) + (root_size . ,(assoc-ref installer-iso-spec 'root-size)) + (installer_state_path . ,(assoc-ref result 'installer-state-path)) + (installer_log_path . ,(assoc-ref result 'installer-log-path)) + (iso_store_path . ,(assoc-ref result 'iso-store-path)) + (iso_image . ,(assoc-ref result 'iso-image)) + (boot_efi_image . ,(assoc-ref result 'boot-efi-image)) + (root_image . ,(assoc-ref result 'root-image)) + (installer_closure_path . ,(assoc-ref result 'installer-closure-path)) + (target_closure_path . ,(assoc-ref result 'target-closure-path)) + (host_base_store_count . ,(length host-base-stores)) + (host_base_stores . ,(string-join host-base-stores ",")) + (native_base_store_count . ,(length native-base-stores)) + (native_base_stores . ,(string-join native-base-stores ",")) + (fruix_runtime_store_count . ,(length fruix-runtime-stores)) + (fruix_runtime_stores . ,(string-join fruix-runtime-stores ",")) + (host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file)) + (store_layout_file . ,(assoc-ref result 'store-layout-file)) + (host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru)) + (host_uname . ,(assoc-ref host-provenance 'uname)) + (usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision)) + (usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch)) + (usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256)) + (store_item_count . ,(length store-items)) + (target_store_item_count . ,(length target-store-items)) + (installer_store_item_count . ,(length installer-store-items)))))) + (define (main argv) (let* ((parsed (parse-arguments argv)) (command (assoc-ref parsed 'command)) @@ -491,7 +557,7 @@ Common options:\n\ (rootfs-opt (assoc-ref parsed 'rootfs)) (system-name (assoc-ref parsed 'system-name)) (requested-symbol (and system-name (string->symbol system-name)))) - (unless (member action '("build" "image" "installer" "install" "rootfs")) + (unless (member action '("build" "image" "installer" "installer-iso" "install" "rootfs")) (error "unknown system action" action)) (let* ((os-file (match positional ((file . _) file) @@ -561,6 +627,16 @@ Common options:\n\ #:install-target-device (or install-target-device "/dev/vtbd1") #:root-size (or root-size "10g") #:disk-capacity disk-capacity))) + ((string=? action "installer-iso") + (emit-system-installer-iso-metadata + os-file resolved-symbol store-dir os + (materialize-installer-iso os + #:store-dir store-dir + #:guile-prefix guile-prefix + #:guile-extra-prefix guile-extra-prefix + #:shepherd-prefix shepherd-prefix + #:install-target-device (or install-target-device "/dev/vtbd1") + #:root-size root-size))) ((string=? action "install") (unless target (error "install action requires TARGET or --target PATH")) diff --git a/tests/system/run-phase18-installer-iso.sh b/tests/system/run-phase18-installer-iso.sh new file mode 100755 index 0000000..18f9d5a --- /dev/null +++ b/tests/system/run-phase18-installer-iso.sh @@ -0,0 +1,412 @@ +#!/bin/sh +set -eu + +project_root=${PROJECT_ROOT:-$(pwd)} +script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +fruix_cmd=$project_root/bin/fruix +os_template=${OS_TEMPLATE:-$script_dir/phase18-installer-target-operating-system.scm.in} +system_name=${SYSTEM_NAME:-phase18-target-operating-system} +store_dir=${STORE_DIR:-/frx/store} +installer_root_size=${INSTALLER_ROOT_SIZE:-} +target_disk_capacity=${TARGET_DISK_CAPACITY:-12g} +install_target_device=${INSTALL_TARGET_DEVICE:-/dev/vtbd1} +qemu_smp=${QEMU_SMP:-2} +installer_memory=${INSTALLER_MEMORY:-6144} +installer_ssh_port=${INSTALLER_SSH_PORT:-10027} +target_ssh_port=${TARGET_SSH_PORT:-10028} +base_name=${BASE_NAME:-phase18-installer-iso-target} +base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-installer-iso-target} +base_release=${BASE_RELEASE:-15.0-STABLE} +base_branch=${BASE_BRANCH:-stable/15} +source_name=${SOURCE_NAME:-stable15-installer-iso-target-source} +source_ref=${SOURCE_REF:-stable/15} +source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89} +declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-installer-iso-target} +metadata_target=${METADATA_OUT:-} +root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub} +root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519} + +[ -x "$fruix_cmd" ] || { + echo "fruix command is not executable: $fruix_cmd" >&2 + exit 1 +} +[ -f "$os_template" ] || { + echo "missing operating-system template: $os_template" >&2 + exit 1 +} +[ -f "$root_authorized_key_file" ] || { + echo "missing root authorized key file: $root_authorized_key_file" >&2 + exit 1 +} +[ -f "$root_ssh_private_key_file" ] || { + echo "missing root SSH private key file: $root_ssh_private_key_file" >&2 + exit 1 +} +command -v qemu-system-x86_64 >/dev/null 2>&1 || { + echo "qemu-system-x86_64 is required" >&2 + exit 1 +} +[ -f /usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd ] || { + echo "missing QEMU UEFI firmware" >&2 + exit 1 +} + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase18-installer-iso.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +target_os_file=$workdir/phase18-installer-iso-target-operating-system.scm +installer_out=$workdir/installer-iso.txt +metadata_file=$workdir/phase18-installer-iso-metadata.txt +installer_serial_log=$workdir/installer-iso-serial.log +target_serial_log=$workdir/target-serial.log +installer_qemu_pidfile=$workdir/installer-qemu.pid +target_qemu_pidfile=$workdir/target-qemu.pid +installer_uefi_vars=$workdir/installer-vars.fd +target_uefi_vars=$workdir/target-vars.fd +installer_boot_iso=$workdir/installer-boot.iso +target_image=$workdir/installed-target.img +gpart_log=$workdir/gpart-show.txt +mnt_esp=$workdir/mnt-esp +mnt_root=$workdir/mnt-root +md_unit= + +cleanup_workdir() { + if [ -f "$installer_qemu_pidfile" ]; then + sudo kill "$(sudo cat "$installer_qemu_pidfile")" >/dev/null 2>&1 || true + fi + if [ -f "$target_qemu_pidfile" ]; then + sudo kill "$(sudo cat "$target_qemu_pidfile")" >/dev/null 2>&1 || true + fi + if [ -n "$md_unit" ]; then + sudo umount "$mnt_esp" >/dev/null 2>&1 || true + sudo umount "$mnt_root" >/dev/null 2>&1 || true + sudo mdconfig -d -u "$md_unit" >/dev/null 2>&1 || true + fi + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file") +sed \ + -e "s|__BASE_NAME__|$base_name|g" \ + -e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \ + -e "s|__BASE_RELEASE__|$base_release|g" \ + -e "s|__BASE_BRANCH__|$base_branch|g" \ + -e "s|__SOURCE_NAME__|$source_name|g" \ + -e "s|__SOURCE_REF__|$source_ref|g" \ + -e "s|__SOURCE_COMMIT__|$source_commit|g" \ + -e "s|__DECLARED_SOURCE_ROOT__|$declared_source_root|g" \ + -e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \ + "$os_template" > "$target_os_file" + +cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$installer_uefi_vars" +cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$target_uefi_vars" +truncate -s "$target_disk_capacity" "$target_image" +mkdir -p "$mnt_esp" "$mnt_root" + +action_env() { + sudo env \ + HOME="$HOME" \ + GUILE_AUTO_COMPILE=0 \ + FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \ + GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \ + GUILE_BIN="${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}" \ + GUILE_EXTRA_PREFIX="${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}" \ + SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \ + "$@" +} + +if [ -n "$installer_root_size" ]; then + action_env "$fruix_cmd" system installer-iso "$target_os_file" \ + --system "$system_name" \ + --store "$store_dir" \ + --install-target-device "$install_target_device" \ + --root-size "$installer_root_size" >"$installer_out" +else + action_env "$fruix_cmd" system installer-iso "$target_os_file" \ + --system "$system_name" \ + --store "$store_dir" \ + --install-target-device "$install_target_device" >"$installer_out" +fi + +field() { + sed -n "s/^$1=//p" "$installer_out" | tail -n 1 +} + +iso_store_path=$(field iso_store_path) +installer_iso_image=$(field iso_image) +installer_boot_efi_image=$(field boot_efi_image) +installer_root_image=$(field root_image) +installer_closure_path=$(field installer_closure_path) +target_closure_path=$(field target_closure_path) +installer_host_name=$(field installer_host_name) +install_target_device_out=$(field install_target_device) +installer_state_path=$(field installer_state_path) +installer_log_path=$(field installer_log_path) +iso_volume_label=$(field iso_volume_label) +root_size_out=$(field root_size) +freebsd_source_kind_out=$(field freebsd_source_kind) +freebsd_source_ref_out=$(field freebsd_source_ref) +freebsd_source_commit_out=$(field freebsd_source_commit) +freebsd_source_file=$(field freebsd_source_file) +freebsd_source_materializations_file=$(field freebsd_source_materializations_file) +materialized_source_store_count=$(field materialized_source_store_count) +materialized_source_stores=$(field materialized_source_stores) +host_base_store_count=$(field host_base_store_count) +native_base_store_count=$(field native_base_store_count) +native_base_stores=$(field native_base_stores) +store_item_count=$(field store_item_count) +target_store_item_count=$(field target_store_item_count) +installer_store_item_count=$(field installer_store_item_count) +store_layout_file=$(field store_layout_file) + +[ -d "$iso_store_path" ] || { echo "missing installer ISO store path: $iso_store_path" >&2; exit 1; } +[ -f "$installer_iso_image" ] || { echo "missing installer ISO image: $installer_iso_image" >&2; exit 1; } +[ -f "$installer_boot_efi_image" ] || { echo "missing installer EFI boot image: $installer_boot_efi_image" >&2; exit 1; } +[ -f "$installer_root_image" ] || { echo "missing installer root image: $installer_root_image" >&2; exit 1; } +[ -n "$installer_closure_path" ] || { echo "missing installer closure path" >&2; exit 1; } +[ -n "$target_closure_path" ] || { echo "missing target closure path" >&2; exit 1; } +[ "$install_target_device_out" = "$install_target_device" ] || { echo "unexpected install target device: $install_target_device_out" >&2; exit 1; } +[ "$installer_host_name" = fruix-freebsd-installer ] || { echo "unexpected installer host name: $installer_host_name" >&2; exit 1; } +[ -n "$iso_volume_label" ] || { echo "missing ISO volume label" >&2; exit 1; } +[ "$freebsd_source_kind_out" = git ] || { echo "unexpected source kind: $freebsd_source_kind_out" >&2; exit 1; } +[ "$freebsd_source_ref_out" = "$source_ref" ] || { echo "unexpected source ref: $freebsd_source_ref_out" >&2; exit 1; } +[ "$freebsd_source_commit_out" = "$source_commit" ] || { echo "unexpected source commit: $freebsd_source_commit_out" >&2; exit 1; } +[ "$materialized_source_store_count" = 1 ] || { echo "unexpected materialized source store count: $materialized_source_store_count" >&2; exit 1; } +[ "$host_base_store_count" = 0 ] || { echo "expected zero host base stores, got: $host_base_store_count" >&2; exit 1; } +[ "$native_base_store_count" = 3 ] || { echo "expected three native base stores, got: $native_base_store_count" >&2; exit 1; } +[ -f "$freebsd_source_file" ] || { echo "missing freebsd source file: $freebsd_source_file" >&2; exit 1; } +[ -f "$freebsd_source_materializations_file" ] || { echo "missing source materializations file: $freebsd_source_materializations_file" >&2; exit 1; } +[ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; } +case "$materialized_source_stores" in + /frx/store/*-freebsd-source-$source_name) : ;; + *) echo "unexpected materialized source store path: $materialized_source_stores" >&2; exit 1 ;; +esac +[ "$store_item_count" -ge "$target_store_item_count" ] || { echo "combined store item count smaller than target store item count" >&2; exit 1; } +[ "$installer_store_item_count" -ge 1 ] || { echo "expected installer store items" >&2; exit 1; } + +cp "$installer_iso_image" "$installer_boot_iso" + +target_closure_base=$(basename "$target_closure_path") +installer_closure_base=$(basename "$installer_closure_path") + +sudo qemu-system-x86_64 \ + -machine q35,accel=tcg \ + -cpu max \ + -m "$installer_memory" \ + -smp "$qemu_smp" \ + -display none \ + -serial "file:$installer_serial_log" \ + -monitor none \ + -pidfile "$installer_qemu_pidfile" \ + -daemonize \ + -boot d \ + -drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \ + -drive if=pflash,format=raw,file="$installer_uefi_vars" \ + -cdrom "$installer_boot_iso" \ + -drive if=virtio,format=raw,file="$target_image" \ + -netdev user,id=net0,hostfwd=tcp::${installer_ssh_port}-:22 \ + -device virtio-net-pci,netdev=net0 + +installer_guest() { + ssh -p "$installer_ssh_port" -i "$root_ssh_private_key_file" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o LogLevel=ERROR \ + -o ConnectTimeout=5 \ + root@127.0.0.1 "$@" +} + +installer_ssh_reached=0 +installer_state=missing +for attempt in $(jot 180 1 180); do + if installer_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then + installer_ssh_reached=1 + installer_state=$(installer_guest "cat '$installer_state_path' 2>/dev/null || echo missing") + [ "$installer_state" = done ] && break + fi + sleep 2 +done + +[ "$installer_ssh_reached" = 1 ] || { echo "installer ISO environment never became reachable over SSH" >&2; exit 1; } +[ "$installer_state" = done ] || { echo "installer ISO environment did not finish installation: $installer_state" >&2; exit 1; } + +installer_run_current_system=$(installer_guest 'readlink /run/current-system') +installer_sshd_status=$(installer_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped') +installer_activate_log=$(installer_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ') +installer_log=$(installer_guest "cat '$installer_log_path' 2>/dev/null || true" | tr '\n' ' ') + +[ "$installer_run_current_system" = "/frx/store/$installer_closure_base" ] || { echo "unexpected installer current-system target: $installer_run_current_system" >&2; exit 1; } +[ "$installer_sshd_status" = running ] || { echo "installer sshd is not running" >&2; exit 1; } +case "$installer_activate_log" in + *fruix-activate:done*) : ;; + *) echo "installer activation log does not show success" >&2; exit 1 ;; +esac +case "$installer_log" in + *fruix-installer:done*) : ;; + *) echo "installer log does not show completion" >&2; exit 1 ;; +esac + +sudo kill "$(sudo cat "$installer_qemu_pidfile")" >/dev/null 2>&1 || true +rm -f "$installer_qemu_pidfile" +sleep 2 + +md=$(sudo mdconfig -a -t vnode -f "$target_image") +md_unit=${md#md} +sudo gpart show -lp "/dev/$md" >"$gpart_log" +esp_fstype=$(sudo fstyp "/dev/${md}p1") +root_fstype=$(sudo fstyp "/dev/${md}p2") +[ "$esp_fstype" = msdosfs ] || { echo "unexpected target ESP filesystem: $esp_fstype" >&2; exit 1; } +[ "$root_fstype" = ufs ] || { echo "unexpected target root filesystem: $root_fstype" >&2; exit 1; } + +sudo mount -t msdosfs "/dev/${md}p1" "$mnt_esp" +sudo mount -t ufs -o ro "/dev/${md}p2" "$mnt_root" + +[ -f "$mnt_esp/EFI/BOOT/BOOTX64.EFI" ] || { echo "missing EFI boot file on installed target" >&2; exit 1; } +target_run_current_system=$(readlink "$mnt_root/run/current-system") +target_boot_loader=$(readlink "$mnt_root/boot/loader") +install_metadata_host=$(cat "$mnt_root/var/lib/fruix/install.scm") +[ "$target_run_current_system" = "/frx/store/$target_closure_base" ] || { echo "unexpected target /run/current-system target: $target_run_current_system" >&2; exit 1; } +[ "$target_boot_loader" = /run/current-system/boot/loader ] || { echo "unexpected target boot loader link: $target_boot_loader" >&2; exit 1; } +[ -d "$mnt_root/frx/store/$target_closure_base" ] || { echo "installed target closure missing from target root" >&2; exit 1; } +case "$install_metadata_host" in + *"$target_closure_path"*) : ;; + *) echo "installed target metadata does not record target closure path" >&2; exit 1 ;; +esac +case "$install_metadata_host" in + *"$materialized_source_stores"*) : ;; + *) echo "installed target metadata does not record materialized source store" >&2; exit 1 ;; +esac + +sudo umount "$mnt_esp" +sudo umount "$mnt_root" +sudo mdconfig -d -u "$md_unit" +md_unit= + +sudo qemu-system-x86_64 \ + -machine q35,accel=tcg \ + -cpu max \ + -m 2048 \ + -smp "$qemu_smp" \ + -display none \ + -serial "file:$target_serial_log" \ + -monitor none \ + -pidfile "$target_qemu_pidfile" \ + -daemonize \ + -drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \ + -drive if=pflash,format=raw,file="$target_uefi_vars" \ + -drive if=virtio,format=raw,file="$target_image" \ + -netdev user,id=net0,hostfwd=tcp::${target_ssh_port}-:22 \ + -device virtio-net-pci,netdev=net0 + +target_guest() { + ssh -p "$target_ssh_port" -i "$root_ssh_private_key_file" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o LogLevel=ERROR \ + -o ConnectTimeout=5 \ + root@127.0.0.1 "$@" +} + +for attempt in $(jot 120 1 120); do + if target_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then + break + fi + sleep 2 +done + +target_run_current_system_guest=$(target_guest 'readlink /run/current-system') +target_shepherd_status=$(target_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped') +target_sshd_status=$(target_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped') +target_install_metadata_guest=$(target_guest 'cat /var/lib/fruix/install.scm') +target_activate_log=$(target_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ') + +[ "$target_run_current_system_guest" = "/frx/store/$target_closure_base" ] || { echo "unexpected booted target current-system: $target_run_current_system_guest" >&2; exit 1; } +[ "$target_shepherd_status" = running ] || { echo "fruix-shepherd is not running in booted target" >&2; exit 1; } +[ "$target_sshd_status" = running ] || { echo "sshd is not running in booted target" >&2; exit 1; } +case "$target_install_metadata_guest" in + *"$target_closure_path"*) : ;; + *) echo "booted target metadata does not record target closure path" >&2; exit 1 ;; +esac +case "$target_install_metadata_guest" in + *"$materialized_source_stores"*) : ;; + *) echo "booted target metadata does not record materialized source store" >&2; exit 1 ;; +esac +case "$target_activate_log" in + *fruix-activate:done*) : ;; + *) echo "booted target activation log does not show success" >&2; exit 1 ;; +esac + +cat >"$metadata_file" <