Add non-interactive Fruix installation flow

This commit is contained in:
2026-04-03 23:34:38 +02:00
parent 02a02e365d
commit 2517710282
8 changed files with 980 additions and 68 deletions

View File

@@ -1,5 +1,92 @@
# Progress
## 2026-04-03 — Phase 18.1 completed: Fruix now has a minimal non-interactive installation flow
Completed work:
- added in `modules/fruix/system/freebsd.scm`:
- `operating-system-install-spec`
- `install-operating-system`
- refactored rootfs staging with:
- `populate-rootfs-from-closure`
so image generation and installation can share the same declarative rootfs assembly logic
- the new installer now supports:
- raw image-file targets
- `/dev/...` block-device targets
- for raw image targets, Fruix now performs a repeatable install flow that:
- creates/truncates the target image
- attaches it with `mdconfig`
- creates GPT partitions
- formats an EFI partition and UFS root partition
- stages the system rootfs
- copies the selected closure and referenced `/frx/store` items into the installed root
- installs `loader.efi` to `EFI/BOOT/BOOTX64.EFI`
- writes install metadata to:
- `/var/lib/fruix/install.scm`
- added user-facing CLI support in `scripts/fruix.scm`:
- `fruix system install`
- new option:
- `--target PATH`
- install metadata emitted by the CLI now includes:
- target/target-kind/device fields
- install metadata path
- disk/root sizing
- declared/materialized FreeBSD source metadata
- closure/native/runtime store metadata
- added validation artifacts:
- `tests/system/phase18-install-operating-system.scm.in`
- `tests/system/run-phase18-system-install.sh`
- wrote:
- `docs/reports/phase18-minimal-installation-flow-freebsd.md`
Validation:
- `PASS phase18-system-install`
- regression re-check:
- `PASS phase17-source-revisions-qemu`
- validated a full install to a raw target image:
- target kind:
- `raw-file`
- disk capacity:
- `12g`
- root size:
- `10g`
- validated target layout and boot artifacts:
- GPT image created
- ESP filesystem:
- `msdosfs`
- root filesystem:
- `ufs`
- `EFI/BOOT/BOOTX64.EFI` present
- validated installed-system boot through the already-validated mode:
- `freebsd-init+rc.d-shepherd`
- validated after boot:
- `sshd` running
- `/usr/local/etc/rc.d/fruix-shepherd onestatus` reports running
- activation completed successfully
- `/run/current-system` points at the installed closure under `/frx/store`
- validated source-driven installation provenance from:
- Git ref:
- `stable/15`
- pinned commit:
- `332708a606f6bf0841c1d4a74c0d067f5640fe89`
- installed materialized source store:
- `/frx/store/a892afb425235de71c9da38884e2ebdba5dafd3a1993f432fe7c446f5af2151f-freebsd-source-stable15-install-source`
Operational note:
- I tested the idea of defaulting local QEMU/TCG validation to 8 vCPUs
- result:
- under TCG, higher SMP did not help this path and regressed boot responsiveness
- current harnesses therefore keep `QEMU_SMP` configurable but retain the conservative local default of `2`
Current assessment:
- Phase 18.1 is complete
- Fruix can now perform a repeatable, non-interactive installation of a declarative system onto a target image or disk without relying on ad hoc manual assembly
- the next step is Phase 18.2:
- build a minimal Fruix-managed installer environment that can boot into an install context and run this workflow from within that environment
## 2026-04-03 — Phase 17.3 completed: the repo now records Fruix FreeBSD source policy explicitly
Completed work:

View File

@@ -31,6 +31,7 @@ Completed milestones include:
- **Side-by-side source revisions**: Fruix can now keep distinct FreeBSD source identities side by side in `/frx/store` and produce distinct native base outputs from them, even when the visible base version label is held constant.
- **Source-driven boot validation**: Fruix can now also boot systems built from distinct declared FreeBSD source revisions while preserving those source identities in image/build metadata.
- **Explicit source policy**: the repo now records how FreeBSD source objects are fetched, cached, identified, invalidated, and consumed by native base builds in `docs/freebsd-source-policy.md`.
- **Minimal installation workflow**: Fruix now has a non-interactive `fruix system install` path that can partition, format, populate, and boot a target image or disk from a declarative system closure.
- **Base upgrade story**: Fruix can now keep distinct declared base versions side by side in `/frx/store` and roll forward / back between them through the normal system deployment flow.
## Major pain points now behind us
@@ -45,7 +46,7 @@ Completed milestones include:
## Major pain points still ahead
- **True store-native runtime artifacts**: some historical build/install prefixes are still embedded in binaries and metadata. They are no longer required at runtime, but the local Guile/guile-extra/Shepherd build/install flow should still be moved to a genuinely store-native prefix from the start.
- **Installation workflow**: the major remaining near-term gap is no longer source identity itself, but turning the existing build/image machinery into a first-class installation story with clearer deployment semantics.
- **Installer environment**: Fruix now has a host-driven non-interactive install path, but it still lacks a dedicated Fruix-managed installer environment that can boot into an install context and run that workflow from within the target environment.
- **Boot-path simplification**: Fruix now supports both the legacy `freebsd-init+rc.d-shepherd` path and the more Guix-like `shepherd-pid1` path. We still need to decide whether Shepherd PID 1 becomes the preferred/default architecture.
- **Reduce transitional FreeBSD glue**: more of the current bootstrap/activation/runtime setup should become cleaner and less prototype-specific over time.
- **Tooling and platform constraints**: local bhyve remains blocked by missing nested virtualization under Xen, and XO permissions still prevent creating/importing new VDIs; current validation must keep reusing the approved VM/VDI path.
@@ -53,4 +54,4 @@ Completed milestones include:
## Bottom line
Fruix has crossed the most important threshold: it is no longer just a collection of isolated FreeBSD experiments. It can now build declarative FreeBSD system artifacts, boot them on the real target VM, reach the network, serve SSH, run Shepherd as PID 1, operate from `/frx` without depending on temporary runtime-prefix shims, build native FreeBSD base artifacts into `/frx/store`, roll forward / back between declared base versions, materialize declared FreeBSD source inputs into `/frx/store`, drive native base builds from those materialized source snapshots, boot systems from distinct source revisions, and explain the source provenance/invalidation rules explicitly. The biggest remaining work is no longer “can this boot?” but “how do those source-driven system artifacts become a real installation and deployment workflow?”
Fruix has crossed the most important threshold: it is no longer just a collection of isolated FreeBSD experiments. It can now build declarative FreeBSD system artifacts, boot them on the real target VM, reach the network, serve SSH, run Shepherd as PID 1, operate from `/frx` without depending on temporary runtime-prefix shims, build native FreeBSD base artifacts into `/frx/store`, roll forward / back between declared base versions, materialize declared FreeBSD source inputs into `/frx/store`, drive native base builds from those materialized source snapshots, boot systems from distinct source revisions, explain the source provenance/invalidation rules explicitly, and install a declarative system onto a target image through a repeatable Fruix workflow. The biggest remaining work is no longer “can this build/install at all?” but “how does this become a fuller installer/deployment/generation story?”

View File

@@ -0,0 +1,167 @@
# Phase 18.1: minimal non-interactive Fruix installation flow on FreeBSD
Date: 2026-04-03
## Goal
Phase 18.1 turns Fruix's existing closure/rootfs/image machinery into a real installation workflow.
The goal is not a polished installer yet. The goal is a repeatable, non-interactive install path that can:
- take a declarative Fruix system,
- partition and format a target disk or image,
- populate it with the selected system closure,
- install boot assets,
- and leave the target bootable.
## Implementation
### New install spec and installer entry point
Added in `modules/fruix/system/freebsd.scm`:
- `operating-system-install-spec`
- `install-operating-system`
The installer currently supports:
- raw image-file targets
- `/dev/...` block-device targets
For raw image-file targets, Fruix now:
- creates/truncates the target image
- attaches it with `mdconfig`
- creates a GPT layout
- adds:
- an EFI partition
- a FreeBSD UFS root partition
- formats them with:
- `newfs_msdos`
- `newfs`
- mounts them
- stages the declarative Fruix rootfs
- copies the closure and referenced `/frx/store` items into the installed root
- installs `loader.efi` to `EFI/BOOT/BOOTX64.EFI`
- writes install metadata to:
- `/var/lib/fruix/install.scm`
### Rootfs staging was factored for reuse
Added internal helper:
- `populate-rootfs-from-closure`
This lets image generation and installation reuse the same rootfs staging logic while differing in how the final target is created.
### New CLI action
Added user-facing command support in `scripts/fruix.scm`:
- `fruix system install`
New system option:
- `--target PATH`
Install metadata now emits machine-readable fields including:
- `target`
- `target_kind`
- `target_device`
- `esp_device`
- `root_device`
- `install_metadata_path`
- `disk_capacity`
- `root_size`
- declared/materialized FreeBSD source metadata
- closure/native/runtime store metadata
### Validation harnesses
Added:
- `tests/system/phase18-install-operating-system.scm.in`
- `tests/system/run-phase18-system-install.sh`
The Phase 18 install validation uses the already-validated boot mode:
- `freebsd-init+rc.d-shepherd`
This keeps the install-flow validation focused on installation mechanics rather than on the separate Shepherd-as-PID-1 boot path.
## Validation
Passing validation:
- `PASS phase18-system-install`
- regression re-check:
- `PASS phase17-source-revisions-qemu`
Validated install result:
```text
target_image=/tmp/fruix-phase18-install.CyrgKc/installed.img
target_kind=raw-file
disk_capacity=12g
root_size=10g
closure_path=/frx/store/ee486985797103aa5d3eeeef7f2cf066bcbd6839cd81083dbe626a594e71a703-fruix-system-fruix-freebsd
freebsd_source_kind=git
freebsd_source_ref=stable/15
freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
materialized_source_store=/frx/store/a892afb425235de71c9da38884e2ebdba5dafd3a1993f432fe7c446f5af2151f-freebsd-source-stable15-install-source
native_base_store_count=3
install_metadata_path=/var/lib/fruix/install.scm
esp_fstype=msdosfs
root_fstype=ufs
shepherd_status=running
sshd_status=running
install_flow=non_interactive
init_mode=freebsd-init+rc.d-shepherd
install_target_boot=ok
```
The harness verified all of the following:
- GPT partitioning is created on the target image
- the installed ESP is a valid `msdosfs` filesystem
- the installed root partition is `ufs`
- `EFI/BOOT/BOOTX64.EFI` exists on the target
- `/run/current-system` points at the installed closure in `/frx/store`
- the installed closure exists under the target's `/frx/store`
- `/var/lib/fruix/install.scm` exists and records:
- closure path
- store items
- install spec
- materialized source provenance via the referenced closure/store items
- the installed system boots under local QEMU/UEFI/TCG
- after boot:
- `sshd` is running
- `/usr/local/etc/rc.d/fruix-shepherd onestatus` reports running
- activation completed successfully
## QEMU SMP note
I tested the idea of defaulting local QEMU validation to 8 vCPUs.
Result:
- for local `qemu-system-x86_64` under **TCG**, higher SMP did not help this validation path and in practice regressed boot responsiveness
- the harnesses therefore keep `QEMU_SMP` configurable but retain a conservative default of `2` for TCG-based local validation
This keeps the validated path reliable while still allowing manual override when useful.
## Result
Phase 18.1 is complete.
Fruix now has a real, repeatable, non-interactive installation workflow for FreeBSD systems:
- declarative system input
- native/source-driven closure output
- partition/format/install to a target image or disk
- bootable installed result
The next step is Phase 18.2:
- a minimal Fruix-managed installer environment that can boot into an install context and run this workflow from within that environment.

View File

@@ -51,9 +51,11 @@
validate-operating-system
materialize-freebsd-source
operating-system-closure-spec
operating-system-install-spec
operating-system-image-spec
materialize-operating-system
materialize-rootfs
install-operating-system
materialize-bhyve-image
default-minimal-operating-system))
@@ -1833,18 +1835,7 @@
(mkdir-p (dirname link-name))
(symlink target link-name))
(define* (materialize-rootfs os rootfs
#: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"))
(let* ((closure (materialize-operating-system os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-path (assoc-ref closure 'closure-path)))
(define (populate-rootfs-from-closure os rootfs closure-path)
(when (file-exists? rootfs)
(delete-file-recursively rootfs))
(mkdir-p rootfs)
@@ -1894,7 +1885,48 @@
`((rootfs . ,rootfs)
(closure-path . ,closure-path)
(ready-marker . ,(operating-system-ready-marker os))
(rc-script . ,(string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd")))))
(rc-script . ,(string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd"))))
(define* (materialize-rootfs os rootfs
#: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"))
(let* ((closure (materialize-operating-system os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-path (assoc-ref closure 'closure-path)))
(populate-rootfs-from-closure os rootfs closure-path)))
(define* (operating-system-install-spec os
#:key
target
(target-kind 'raw-file)
(boot-mode 'uefi)
(partition-scheme 'gpt)
(efi-size "64m")
(root-size #f)
(disk-capacity #f)
(efi-partition-label "efiboot")
(root-partition-label "fruix-root")
(serial-console "comconsole"))
`((host-name . ,(operating-system-host-name os))
(freebsd-base . ,(freebsd-base-spec (operating-system-freebsd-base os)))
(install-mode . non-interactive)
(target . ,target)
(target-kind . ,target-kind)
(boot-mode . ,boot-mode)
(partition-scheme . ,partition-scheme)
(efi-size . ,efi-size)
(root-size . ,root-size)
(disk-capacity . ,disk-capacity)
(efi-partition-label . ,efi-partition-label)
(root-partition-label . ,root-partition-label)
(serial-console . ,serial-console)
(init-mode . ,(operating-system-init-mode os))))
(define* (operating-system-image-spec os
#:key
@@ -1966,6 +1998,7 @@
(command-output "mktemp" "-d" pattern))
(define image-builder-version "2")
(define install-builder-version "1")
(define (resize-gpt-image image disk-capacity)
(when disk-capacity
@@ -1978,6 +2011,133 @@
(lambda ()
(run-command "mdconfig" "-d" "-u" (string-drop md 2)))))))
(define* (install-operating-system os
#:key
target
(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")
(efi-size "64m")
(root-size #f)
(disk-capacity #f)
(efi-partition-label "efiboot")
(root-partition-label "fruix-root")
(serial-console "comconsole"))
(unless (and (string? target) (not (string-null? target)))
(error "install target must be a non-empty path" target))
(let* ((closure (materialize-operating-system os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-path (assoc-ref closure 'closure-path))
(store-items (store-reference-closure (list closure-path)))
(target-kind (if (string-prefix? "/dev/" target)
'block-device
'raw-file))
(install-spec (operating-system-install-spec os
#:target target
#:target-kind target-kind
#:efi-size efi-size
#:root-size root-size
#:disk-capacity disk-capacity
#:efi-partition-label efi-partition-label
#:root-partition-label root-partition-label
#:serial-console serial-console))
(build-root (mktemp-directory "/tmp/fruix-system-install.XXXXXX"))
(rootfs (string-append build-root "/rootfs"))
(mnt-root (string-append build-root "/mnt-root"))
(mnt-esp (string-append build-root "/mnt-esp"))
(install-metadata-relative-path "/var/lib/fruix/install.scm")
(target-device #f)
(target-md #f)
(esp-device #f)
(root-device #f)
(root-mounted? #f)
(esp-mounted? #f))
(dynamic-wind
(lambda () #t)
(lambda ()
(populate-rootfs-from-closure os rootfs closure-path)
(mkdir-p mnt-root)
(mkdir-p mnt-esp)
(case target-kind
((raw-file)
(unless disk-capacity
(error "raw-file install target requires --disk-capacity" target))
(mkdir-p (dirname target))
(delete-path-if-exists target)
(run-command "truncate" "-s" disk-capacity target)
(let ((md (command-output "mdconfig" "-a" "-t" "vnode" "-f" target)))
(set! target-md md)
(set! target-device (string-append "/dev/" md))))
((block-device)
(set! target-device target)))
(system* "sh" "-c"
(string-append "gpart destroy -F " target-device " >/dev/null 2>&1"))
(run-command "gpart" "create" "-s" "gpt" target-device)
(run-command "gpart" "add" "-a" "1m" "-s" efi-size
"-t" "efi" "-l" efi-partition-label target-device)
(if root-size
(run-command "gpart" "add" "-a" "1m" "-s" root-size
"-t" "freebsd-ufs" "-l" root-partition-label target-device)
(run-command "gpart" "add" "-a" "1m"
"-t" "freebsd-ufs" "-l" root-partition-label target-device))
(set! esp-device (string-append target-device "p1"))
(set! root-device (string-append target-device "p2"))
(run-command "newfs_msdos" "-L" "EFISYS" esp-device)
(run-command "newfs" "-U" "-L" root-partition-label root-device)
(run-command "mount" "-t" "ufs" root-device mnt-root)
(set! root-mounted? #t)
(run-command "mount" "-t" "msdosfs" esp-device mnt-esp)
(set! esp-mounted? #t)
(copy-tree-contents rootfs mnt-root)
(copy-store-items-into-rootfs mnt-root store-dir store-items)
(mkdir-p (string-append mnt-esp "/EFI/BOOT"))
(copy-regular-file (string-append closure-path "/boot/loader.efi")
(string-append mnt-esp "/EFI/BOOT/BOOTX64.EFI"))
(let ((install-metadata-file (string-append mnt-root install-metadata-relative-path)))
(write-file install-metadata-file
(object->string
`((install-version . ,install-builder-version)
(install-spec . ,install-spec)
(closure-path . ,closure-path)
(store-item-count . ,(length store-items))
(store-items . ,store-items))))
(chmod install-metadata-file #o644))
(run-command "sync")
`((target . ,target)
(target-kind . ,target-kind)
(target-device . ,target-device)
(esp-device . ,esp-device)
(root-device . ,root-device)
(install-spec . ,install-spec)
(install-metadata-path . ,install-metadata-relative-path)
(closure-path . ,closure-path)
(host-base-stores . ,(assoc-ref closure 'host-base-stores))
(native-base-stores . ,(assoc-ref closure 'native-base-stores))
(fruix-runtime-stores . ,(assoc-ref closure 'fruix-runtime-stores))
(freebsd-base-file . ,(assoc-ref closure 'freebsd-base-file))
(freebsd-source-file . ,(assoc-ref closure 'freebsd-source-file))
(freebsd-source-materializations-file . ,(assoc-ref closure 'freebsd-source-materializations-file))
(materialized-source-stores . ,(assoc-ref closure 'materialized-source-stores))
(host-base-provenance-file . ,(assoc-ref closure 'host-base-provenance-file))
(store-layout-file . ,(assoc-ref closure 'store-layout-file))
(store-items . ,store-items)))
(lambda ()
(when esp-mounted?
(system* "umount" mnt-esp)
(set! esp-mounted? #f))
(when root-mounted?
(system* "umount" mnt-root)
(set! root-mounted? #f))
(when target-md
(system* "mdconfig" "-d" "-u" (string-drop target-md 2))
(set! target-md #f))
(when (file-exists? build-root)
(delete-file-recursively build-root))))))
(define* (materialize-bhyve-image os
#:key
(store-dir "/frx/store")

View File

@@ -19,13 +19,15 @@ Commands:\n\
System actions:\n\
build Materialize the Fruix system closure in /frx/store.\n\
image Materialize the Fruix disk image 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\
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' (example: 30g).\n\
--root-size SIZE Root filesystem size for 'image' (example: 6g).\n\
--disk-capacity SIZE Disk capacity for 'image' or raw-file 'install' targets.\n\
--root-size SIZE Root filesystem size for 'image' or 'install' (example: 6g).\n\
--target PATH Install target for 'install' (raw image file or /dev/... device).\n\
--rootfs DIR Rootfs target for 'rootfs'.\n\
\n\
Source actions:\n\
@@ -123,6 +125,7 @@ Common options:\n\
(store-dir "/frx/store")
(disk-capacity #f)
(root-size #f)
(target #f)
(rootfs #f))
(match args
(()
@@ -134,33 +137,38 @@ Common options:\n\
(store-dir . ,store-dir)
(disk-capacity . ,disk-capacity)
(root-size . ,root-size)
(target . ,target)
(rootfs . ,rootfs))))
(("--help")
(usage 0))
(((? (lambda (arg) (string-prefix? "--system=" arg)) arg) . tail)
(loop tail positional (option-value arg "--system=") store-dir disk-capacity root-size rootfs))
(loop tail positional (option-value arg "--system=") store-dir disk-capacity root-size target rootfs))
(("--system" value . tail)
(loop tail positional value store-dir disk-capacity root-size rootfs))
(loop tail positional value store-dir disk-capacity root-size target rootfs))
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
(loop tail positional system-name (option-value arg "--store=") disk-capacity root-size rootfs))
(loop tail positional system-name (option-value arg "--store=") disk-capacity root-size target rootfs))
(("--store" value . tail)
(loop tail positional system-name value disk-capacity root-size rootfs))
(loop tail positional system-name value disk-capacity root-size target rootfs))
(((? (lambda (arg) (string-prefix? "--disk-capacity=" arg)) arg) . tail)
(loop tail positional system-name store-dir (option-value arg "--disk-capacity=") root-size rootfs))
(loop tail positional system-name store-dir (option-value arg "--disk-capacity=") root-size target rootfs))
(("--disk-capacity" value . tail)
(loop tail positional system-name store-dir value root-size rootfs))
(loop tail positional system-name store-dir value root-size target rootfs))
(((? (lambda (arg) (string-prefix? "--root-size=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity (option-value arg "--root-size=") rootfs))
(loop tail positional system-name store-dir disk-capacity (option-value arg "--root-size=") target rootfs))
(("--root-size" value . tail)
(loop tail positional system-name store-dir disk-capacity value rootfs))
(loop tail positional system-name store-dir disk-capacity value target rootfs))
(((? (lambda (arg) (string-prefix? "--target=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity root-size (option-value arg "--target=") rootfs))
(("--target" value . tail)
(loop tail positional system-name store-dir disk-capacity root-size value rootfs))
(((? (lambda (arg) (string-prefix? "--rootfs=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity root-size (option-value arg "--rootfs=")))
(loop tail positional system-name store-dir disk-capacity root-size target (option-value arg "--rootfs=")))
(("--rootfs" value . tail)
(loop tail positional system-name store-dir disk-capacity root-size value))
(loop tail positional system-name store-dir disk-capacity root-size target value))
(((? (lambda (arg) (string-prefix? "--" arg)) arg) . _)
(error "unknown option" arg))
((arg . tail)
(loop tail (cons arg positional) system-name store-dir disk-capacity root-size rootfs)))))
(loop tail (cons arg positional) system-name store-dir disk-capacity root-size target rootfs)))))
(define (parse-source-arguments action rest)
(let loop ((args rest)
@@ -276,6 +284,65 @@ Common options:\n\
(generated_file_count . ,(length generated-files))
(reference_count . ,(length references))))))
(define (emit-system-install-metadata os-file resolved-symbol store-dir os result)
(let* ((install-spec (assoc-ref result 'install-spec))
(store-items (assoc-ref result '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 . "install")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(target . ,(assoc-ref result 'target))
(target_kind . ,(assoc-ref result 'target-kind))
(target_device . ,(assoc-ref result 'target-device))
(esp_device . ,(assoc-ref result 'esp-device))
(root_device . ,(assoc-ref result 'root-device))
(install_metadata_path . ,(assoc-ref result 'install-metadata-path))
(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) ","))
(disk_capacity . ,(assoc-ref install-spec 'disk-capacity))
(root_size . ,(assoc-ref install-spec 'root-size))
(efi_size . ,(assoc-ref install-spec 'efi-size))
(closure_path . ,(assoc-ref result '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))))))
(define (emit-system-image-metadata os-file resolved-symbol store-dir os result)
(let* ((image-spec (assoc-ref result 'image-spec))
(store-items (assoc-ref result 'store-items))
@@ -342,14 +409,20 @@ Common options:\n\
(let* ((positional (assoc-ref parsed 'positional))
(disk-capacity (assoc-ref parsed 'disk-capacity))
(root-size (assoc-ref parsed 'root-size))
(target-opt (assoc-ref parsed 'target))
(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" "rootfs"))
(unless (member action '("build" "image" "install" "rootfs"))
(error "unknown system action" action))
(let* ((os-file (match positional
((file . _) file)
(() (error "missing operating-system file argument"))))
(target (or target-opt
(and (string=? action "install")
(match positional
((_ target-path) target-path)
(_ #f)))))
(rootfs (or rootfs-opt
(and (string=? action "rootfs")
(match positional
@@ -398,6 +471,19 @@ Common options:\n\
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:root-size (or root-size "256m")
#:disk-capacity disk-capacity)))
((string=? action "install")
(unless target
(error "install action requires TARGET or --target PATH"))
(emit-system-install-metadata
os-file resolved-symbol store-dir os
(install-operating-system os
#:target target
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:root-size root-size
#:disk-capacity disk-capacity))))))))))
((string=? command "source")
(let* ((positional (assoc-ref parsed 'positional))

View File

@@ -0,0 +1,91 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase18-source
(freebsd-source
#:name "__SOURCE_NAME__"
#:kind 'git
#:ref "__SOURCE_REF__"
#:commit "__SOURCE_COMMIT__"))
(define phase18-base
(freebsd-base
#:name "__BASE_NAME__"
#:version-label "__BASE_VERSION_LABEL__"
#:release "__BASE_RELEASE__"
#:branch "__BASE_BRANCH__"
#:source phase18-source
#:source-root "__DECLARED_SOURCE_ROOT__"
#:target "amd64"
#:target-arch "amd64"
#:kernconf "GENERIC"))
(define phase18-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:freebsd-base phase18-base
#:kernel (freebsd-native-kernel-for phase18-base)
#:bootloader (freebsd-native-bootloader-for phase18-base)
#:base-packages (freebsd-native-system-packages-for phase18-base)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:init-mode 'freebsd-init+rc.d-shepherd
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -8,6 +8,7 @@ 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}
ssh_port=${QEMU_SSH_PORT:-10022}
qemu_smp=${QEMU_SMP:-2}
disk_capacity=${DISK_CAPACITY:-5g}
cleanup=0
@@ -75,7 +76,7 @@ sudo qemu-system-x86_64 \
-machine q35,accel=tcg \
-cpu max \
-m 2048 \
-smp 2 \
-smp "$qemu_smp" \
-display none \
-serial "file:$serial_log" \
-monitor none \
@@ -157,6 +158,7 @@ raw_sha256=$raw_sha256
serial_log=$serial_log
qemu_pidfile=$qemu_pidfile
ssh_port=$ssh_port
qemu_smp=$qemu_smp
ready_marker=$ready_marker
run_current_system_target=$run_current_system_target
pid1_command=$pid1_command

View File

@@ -0,0 +1,318 @@
#!/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-install-operating-system.scm.in}
system_name=${SYSTEM_NAME:-phase18-operating-system}
store_dir=${STORE_DIR:-/frx/store}
disk_capacity=${DISK_CAPACITY:-12g}
root_size=${ROOT_SIZE:-10g}
qemu_smp=${QEMU_SMP:-2}
ssh_port=${QEMU_SSH_PORT:-10024}
base_name=${BASE_NAME:-phase18-install}
base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-install}
base_release=${BASE_RELEASE:-15.0-STABLE}
base_branch=${BASE_BRANCH:-stable/15}
source_name=${SOURCE_NAME:-stable15-install-source}
source_ref=${SOURCE_REF:-stable/15}
source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89}
declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-install}
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-install.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
phase18_os_file=$workdir/phase18-install-operating-system.scm
install_out=$workdir/install.txt
target_image=$workdir/installed.img
gpart_log=$workdir/gpart-show.txt
serial_log=$workdir/serial.log
qemu_pidfile=$workdir/qemu.pid
uefi_vars=$workdir/QEMU_UEFI_VARS.fd
metadata_file=$workdir/phase18-system-install-metadata.txt
mnt_esp=$workdir/mnt-esp
mnt_root=$workdir/mnt-root
md_unit=
cleanup_workdir() {
if [ -f "$qemu_pidfile" ]; then
sudo kill "$(sudo cat "$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" > "$phase18_os_file"
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$uefi_vars"
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}" \
"$@"
}
action_env "$fruix_cmd" system install "$phase18_os_file" \
--system "$system_name" \
--store "$store_dir" \
--target "$target_image" \
--disk-capacity "$disk_capacity" \
--root-size "$root_size" >"$install_out"
field() {
sed -n "s/^$1=//p" "$install_out" | tail -n 1
}
target_out=$(field target)
target_kind=$(field target_kind)
target_device=$(field target_device)
esp_device=$(field esp_device)
root_device=$(field root_device)
install_metadata_path=$(field install_metadata_path)
closure_path=$(field closure_path)
freebsd_base_name_out=$(field freebsd_base_name)
freebsd_base_version_label_out=$(field freebsd_base_version_label)
freebsd_base_release_out=$(field freebsd_base_release)
freebsd_base_branch_out=$(field freebsd_base_branch)
freebsd_base_source_root_out=$(field freebsd_base_source_root)
freebsd_source_name_out=$(field freebsd_source_name)
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)
store_layout_file=$(field store_layout_file)
[ "$target_out" = "$target_image" ] || { echo "unexpected target path: $target_out" >&2; exit 1; }
[ "$target_kind" = raw-file ] || { echo "unexpected target kind: $target_kind" >&2; exit 1; }
[ -n "$target_device" ] || { echo "missing target device" >&2; exit 1; }
[ -n "$esp_device" ] || { echo "missing esp device" >&2; exit 1; }
[ -n "$root_device" ] || { echo "missing root device" >&2; exit 1; }
[ "$install_metadata_path" = /var/lib/fruix/install.scm ] || { echo "unexpected install metadata path: $install_metadata_path" >&2; exit 1; }
[ -n "$closure_path" ] || { echo "missing closure path" >&2; exit 1; }
[ "$freebsd_base_name_out" = "$base_name" ] || { echo "unexpected base name: $freebsd_base_name_out" >&2; exit 1; }
[ "$freebsd_base_version_label_out" = "$base_version_label" ] || { echo "unexpected base version label: $freebsd_base_version_label_out" >&2; exit 1; }
[ "$freebsd_base_release_out" = "$base_release" ] || { echo "unexpected base release: $freebsd_base_release_out" >&2; exit 1; }
[ "$freebsd_base_branch_out" = "$base_branch" ] || { echo "unexpected base branch: $freebsd_base_branch_out" >&2; exit 1; }
[ "$freebsd_base_source_root_out" = "$declared_source_root" ] || { echo "unexpected declared source root: $freebsd_base_source_root_out" >&2; exit 1; }
[ "$freebsd_source_name_out" = "$source_name" ] || { echo "unexpected source name: $freebsd_source_name_out" >&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; }
[ -f "$target_image" ] || { echo "missing target image: $target_image" >&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
closure_base=$(basename "$closure_path")
md=$(sudo mdconfig -a -t vnode -f "$target_image")
md_unit=${md#md}
sudo mkdir -p "$mnt_esp" "$mnt_root"
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 ESP filesystem: $esp_fstype" >&2; exit 1; }
[ "$root_fstype" = ufs ] || { echo "unexpected 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; }
run_current_system_target=$(readlink "$mnt_root/run/current-system")
boot_loader_target=$(readlink "$mnt_root/boot/loader")
install_metadata_host=$(cat "$mnt_root$install_metadata_path")
[ "$run_current_system_target" = "/frx/store/$closure_base" ] || { echo "unexpected /run/current-system target: $run_current_system_target" >&2; exit 1; }
[ "$boot_loader_target" = /run/current-system/boot/loader ] || { echo "unexpected /boot/loader target: $boot_loader_target" >&2; exit 1; }
[ -d "$mnt_root/frx/store/$closure_base" ] || { echo "installed closure missing from target root" >&2; exit 1; }
case "$install_metadata_host" in
*"$closure_path"*) : ;;
*) echo "installed metadata file does not record closure path" >&2; exit 1 ;;
esac
case "$install_metadata_host" in
*"$materialized_source_stores"*) : ;;
*) echo "installed metadata file 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:$serial_log" \
-monitor none \
-pidfile "$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="$uefi_vars" \
-drive if=virtio,format=raw,file="$target_image" \
-netdev user,id=net0,hostfwd=tcp::${ssh_port}-:22 \
-device virtio-net-pci,netdev=net0
ssh_guest() {
ssh -p "$ssh_port" -i "$root_ssh_private_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
root@127.0.0.1 "$@"
}
for attempt in $(jot 120 1 120); do
if ssh_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
break
fi
sleep 2
done
run_current_system_guest=$(ssh_guest 'readlink /run/current-system')
shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
install_metadata_guest=$(ssh_guest 'cat /var/lib/fruix/install.scm')
activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
[ "$run_current_system_guest" = "/frx/store/$closure_base" ] || { echo "unexpected guest current-system target: $run_current_system_guest" >&2; exit 1; }
[ "$shepherd_status" = running ] || { echo "fruix-shepherd rc service is not running in installed system" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running in installed system" >&2; exit 1; }
case "$install_metadata_guest" in
*"$closure_path"*) : ;;
*) echo "guest install metadata does not record closure path" >&2; exit 1 ;;
esac
case "$install_metadata_guest" in
*"$materialized_source_stores"*) : ;;
*) echo "guest install metadata does not record materialized source store" >&2; exit 1 ;;
esac
case "$activate_log" in
*fruix-activate:done*) : ;;
*) echo "installed system activation log does not show success" >&2; exit 1 ;;
esac
cat >"$metadata_file" <<EOF
workdir=$workdir
phase18_os_file=$phase18_os_file
target_image=$target_image
target_kind=$target_kind
disk_capacity=$disk_capacity
root_size=$root_size
qemu_smp=$qemu_smp
closure_path=$closure_path
closure_base=$closure_base
freebsd_source_kind=$freebsd_source_kind_out
freebsd_source_ref=$freebsd_source_ref_out
freebsd_source_commit=$freebsd_source_commit_out
freebsd_source_file=$freebsd_source_file
freebsd_source_materializations_file=$freebsd_source_materializations_file
materialized_source_store_count=$materialized_source_store_count
materialized_source_store=$materialized_source_stores
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
store_item_count=$store_item_count
install_metadata_path=$install_metadata_path
gpart_log=$gpart_log
esp_fstype=$esp_fstype
root_fstype=$root_fstype
serial_log=$serial_log
ssh_port=$ssh_port
run_current_system_target=$run_current_system_guest
shepherd_status=$shepherd_status
sshd_status=$sshd_status
install_flow=non_interactive
init_mode=freebsd-init+rc.d-shepherd
install_target_boot=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase18-system-install\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"