diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 36b9e5e..07a9c73 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,5 +1,64 @@ # Progress +## 2026-04-03 — Phase 17.1 completed: side-by-side FreeBSD source revisions now coexist in `/frx/store` + +Completed work: + +- added Phase 17 operating-system templates for distinct source identities: + - `tests/system/phase17-git-source-operating-system.scm.in` + - `tests/system/phase17-txz-source-operating-system.scm.in` +- modeled the Git side with both: + - ref: `stable/15` + - pinned commit: + - `332708a606f6bf0841c1d4a74c0d067f5640fe89` +- modeled the archive side with: + - `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz` + - sha256: + - `83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0` +- added side-by-side source coexistence validation: + - `tests/system/run-phase17-source-coexistence.sh` +- the new harness builds: + - Git source build A + - `src.txz` source build + - Git source rebuild B + and verifies: + - Git rebuild stability when pinned by commit + - distinct closure paths for Git vs `src.txz` + - distinct materialized source stores + - distinct native kernel/bootloader/runtime outputs + - correct declared/materialized source metadata in closures and native build info + - continued use of the materialized source root instead of the unused declared transitional `source-root` +- wrote: + - `docs/reports/phase17-side-by-side-source-revisions-freebsd.md` + +Validation: + +- `PASS phase17-source-coexistence` +- validated side-by-side closures: + - Git closure: + - `/frx/store/d6cbcc76f57fa9c392a80fe20e7499f7a837aab4fb96ea056e624cde95bc70c8-fruix-system-fruix-freebsd` + - `src.txz` closure: + - `/frx/store/02268e19930facb32e12b6ec191f2e5704d1e81033baf3637a889ad15924ff88-fruix-system-fruix-freebsd` +- validated distinct materialized source stores: + - Git: + - `/frx/store/c9928605fa906b90a600dafeebe5005dd18ad3b8e62b7111d9d13ad60ee56490-freebsd-source-stable15-side-a` + - `src.txz`: + - `/frx/store/5eaeff5c6c55a95b6531d9cf2e1824cd4368d81c614608426bee1a5d2a664dc5-freebsd-source-release15-side-b` +- validated distinct native base outputs for the **same** version label: + - `15.0-source-side-by-side` +- validated effective source roots: + - Git: + - `.../tree` + - `src.txz`: + - `.../tree/usr/src` + +Current assessment: + +- Phase 17.1 is complete +- Fruix can now keep at least two distinct FreeBSD source revisions side by side in `/frx/store` as meaningful native base inputs/outputs +- the next step is Phase 17.2: + - boot systems built from at least two distinct declared source revisions and confirm that the booted metadata tracks those revisions + ## 2026-04-03 — Phase 16.3 completed: native FreeBSD base builds now consume materialized source inputs Completed work: diff --git a/docs/PROG_SUMMARY.md b/docs/PROG_SUMMARY.md index 0f80f13..1a4b810 100644 --- a/docs/PROG_SUMMARY.md +++ b/docs/PROG_SUMMARY.md @@ -28,6 +28,7 @@ Completed milestones include: - official `src.txz` archives such as `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz` into `/frx/store`, with cache-backed provenance under `/frx/var/cache/fruix/freebsd-source`. - **Source-driven native base builds**: native FreeBSD kernel/bootloader/runtime artifacts now consume those materialized source snapshots rather than ambient `/usr/src`, and their build metadata records both the declared source and the effective materialized source identity. +- **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. - **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 @@ -42,7 +43,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. -- **Side-by-side source revision validation**: Fruix now has the source boundary needed for this work, but it still needs stronger side-by-side revision boot validation and upgrade/rollback exercises across distinct fetched source revisions. +- **Source-revision boot validation**: Fruix now has side-by-side source-driven native outputs, but it still needs stronger boot validation and update/rollback exercises across distinct fetched source revisions. - **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. diff --git a/docs/reports/phase17-side-by-side-source-revisions-freebsd.md b/docs/reports/phase17-side-by-side-source-revisions-freebsd.md new file mode 100644 index 0000000..a1f45f2 --- /dev/null +++ b/docs/reports/phase17-side-by-side-source-revisions-freebsd.md @@ -0,0 +1,141 @@ +# Phase 17.1: side-by-side FreeBSD source revisions in `/frx/store` + +Date: 2026-04-03 + +## Goal + +Phase 17.1 verifies that Fruix can treat FreeBSD source revisions the same way it already treats other declarative inputs: + +- as explicit inputs, +- as side-by-side store objects, +- and as drivers of distinct native base outputs. + +The key question was no longer whether Fruix could materialize a source tree at all. Phase 16 already established that. + +The question here was stricter: + +- can two distinct FreeBSD source inputs coexist, +- can they both drive native base builds, +- and can they do so even when the user-facing base version label is the same? + +That last point matters because it proves the result is driven by source identity rather than by an arbitrary version-string rename. + +## Implementation + +Added two Phase 17 operating-system templates: + +- `tests/system/phase17-git-source-operating-system.scm.in` +- `tests/system/phase17-txz-source-operating-system.scm.in` + +These model two distinct source identities: + +- a Git source with both: + - ref: `stable/15` + - pinned commit: `332708a606f6bf0841c1d4a74c0d067f5640fe89` +- an official release archive source: + - `https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz` + - sha256: + - `83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0` + +Added validation harness: + +- `tests/system/run-phase17-source-coexistence.sh` + +## Validation design + +The harness builds three times: + +1. Git-backed system build +2. `src.txz`-backed system build +3. Git-backed rebuild + +The Git and `src.txz` systems intentionally share the same: + +- base name: + - `source-side-by-side` +- base version label: + - `15.0-source-side-by-side` + +while differing only in their declared source/release metadata. + +This means distinct outputs cannot be explained away by a version-label rename. + +The harness verifies: + +- Git rebuild stability when the Git source is pinned by commit +- distinct closure paths for Git vs `src.txz` +- distinct materialized source store paths +- distinct native kernel/bootloader/runtime store paths +- zero host-base stores in both builds +- one materialized source store in each closure +- correct closure metadata for: + - declared source + - materialized source + - materialized source store count/path +- correct native build info for: + - kernel + - runtime +- effective source roots: + - Git: `.../tree` + - `src.txz`: `.../tree/usr/src` +- the continued separation between: + - declared transitional `source-root` + - actual materialized source root used by the native build + +## Results + +Passing validation: + +- `PASS phase17-source-coexistence` + +Observed side-by-side closures: + +```text +git_closure=/frx/store/d6cbcc76f57fa9c392a80fe20e7499f7a837aab4fb96ea056e624cde95bc70c8-fruix-system-fruix-freebsd +git_closure_rebuild=/frx/store/d6cbcc76f57fa9c392a80fe20e7499f7a837aab4fb96ea056e624cde95bc70c8-fruix-system-fruix-freebsd +txz_closure=/frx/store/02268e19930facb32e12b6ec191f2e5704d1e81033baf3637a889ad15924ff88-fruix-system-fruix-freebsd +base_version_label=15.0-source-side-by-side +same_base_version_label_distinct_sources=ok +``` + +Observed source identities: + +```text +git_source_kind=git +git_source_ref=stable/15 +git_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89 +git_materialized_source_store=/frx/store/c9928605fa906b90a600dafeebe5005dd18ad3b8e62b7111d9d13ad60ee56490-freebsd-source-stable15-side-a +git_materialized_source_root=/frx/store/c9928605fa906b90a600dafeebe5005dd18ad3b8e62b7111d9d13ad60ee56490-freebsd-source-stable15-side-a/tree + +txz_source_kind=src-txz +txz_source_url=https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz +txz_source_sha256=83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0 +txz_materialized_source_store=/frx/store/5eaeff5c6c55a95b6531d9cf2e1824cd4368d81c614608426bee1a5d2a664dc5-freebsd-source-release15-side-b +txz_materialized_source_root=/frx/store/5eaeff5c6c55a95b6531d9cf2e1824cd4368d81c614608426bee1a5d2a664dc5-freebsd-source-release15-side-b/tree/usr/src +``` + +Observed native base outputs for the same version label: + +```text +git_native_base_stores=/frx/store/4b615431ec25c500a3bf0ed70ce39e2ebf4f584994a53756268e4383962bc86b-freebsd-native-kernel-15.0-source-side-by-side,/frx/store/3a5a0b2b88b4757cf9cb4e3040f992d8fdb5bd9a7f1b186da983854cd95392c5-freebsd-native-bootloader-15.0-source-side-by-side,/frx/store/177f78e7f2932986a380187eb09dc34cc2cd9a146c5ed1fe1f00aae15ddf78d9-freebsd-native-runtime-15.0-source-side-by-side + +txz_native_base_stores=/frx/store/0c5141a86fa9c1974102f2bd8766eb3ab787b97dcccb71f17d80aefbe8ed4f3e-freebsd-native-kernel-15.0-source-side-by-side,/frx/store/3de6592f50a735d8461662cb393fc413325ce24ded45d4bb494525896f8cb5eb-freebsd-native-bootloader-15.0-source-side-by-side,/frx/store/46d256305198ee7d745b9032c71085aba97d55fdf7a0d3d2017dd4455173205d-freebsd-native-runtime-15.0-source-side-by-side +``` + +Those outputs differ only by content-address prefix, not by the human-readable version suffix. That is exactly the property Phase 17.1 needed. + +## Key observation + +A moving Git ref by itself is not a reproducibility boundary. For the reproducible half of this validation, the Git source was pinned by commit while still retaining its `stable/15` ref metadata. + +This fits the intended Fruix model: + +- refs are useful selectors, +- commits are the stable Git identity boundary, +- archive SHA256 values are the stable `src.txz` identity boundary. + +## Result + +Phase 17.1 is complete. + +Fruix can now hold at least two distinct FreeBSD source revisions side by side in `/frx/store` and build distinct native FreeBSD base artifacts from them, even when the visible base version label is kept the same. diff --git a/tests/system/phase17-git-source-operating-system.scm.in b/tests/system/phase17-git-source-operating-system.scm.in new file mode 100644 index 0000000..5452a23 --- /dev/null +++ b/tests/system/phase17-git-source-operating-system.scm.in @@ -0,0 +1,91 @@ +(use-modules (fruix system freebsd) + (fruix packages freebsd)) + +(define phase17-source + (freebsd-source + #:name "__SOURCE_NAME__" + #:kind 'git + #:ref "__SOURCE_REF__" + #:commit "__SOURCE_COMMIT__")) + +(define phase17-base + (freebsd-base + #:name "__BASE_NAME__" + #:version-label "__BASE_VERSION_LABEL__" + #:release "__BASE_RELEASE__" + #:branch "__BASE_BRANCH__" + #:source phase17-source + #:source-root "__DECLARED_SOURCE_ROOT__" + #:target "amd64" + #:target-arch "amd64" + #:kernconf "GENERIC")) + +(define phase17-operating-system + (operating-system + #:host-name "fruix-freebsd" + #:freebsd-base phase17-base + #:kernel (freebsd-native-kernel-for phase17-base) + #:bootloader (freebsd-native-bootloader-for phase17-base) + #:base-packages (freebsd-native-system-packages-for phase17-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 'shepherd-pid1 + #:ready-marker "/var/lib/fruix/ready" + #:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__"))) diff --git a/tests/system/phase17-txz-source-operating-system.scm.in b/tests/system/phase17-txz-source-operating-system.scm.in new file mode 100644 index 0000000..a009c0d --- /dev/null +++ b/tests/system/phase17-txz-source-operating-system.scm.in @@ -0,0 +1,91 @@ +(use-modules (fruix system freebsd) + (fruix packages freebsd)) + +(define phase17-source + (freebsd-source + #:name "__SOURCE_NAME__" + #:kind 'src-txz + #:url "__SOURCE_URL__" + #:sha256 "__SOURCE_SHA256__")) + +(define phase17-base + (freebsd-base + #:name "__BASE_NAME__" + #:version-label "__BASE_VERSION_LABEL__" + #:release "__BASE_RELEASE__" + #:branch "__BASE_BRANCH__" + #:source phase17-source + #:source-root "__DECLARED_SOURCE_ROOT__" + #:target "amd64" + #:target-arch "amd64" + #:kernconf "GENERIC")) + +(define phase17-operating-system + (operating-system + #:host-name "fruix-freebsd" + #:freebsd-base phase17-base + #:kernel (freebsd-native-kernel-for phase17-base) + #:bootloader (freebsd-native-bootloader-for phase17-base) + #:base-packages (freebsd-native-system-packages-for phase17-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 'shepherd-pid1 + #:ready-marker "/var/lib/fruix/ready" + #:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__"))) diff --git a/tests/system/run-phase17-source-coexistence.sh b/tests/system/run-phase17-source-coexistence.sh new file mode 100755 index 0000000..d554d06 --- /dev/null +++ b/tests/system/run-phase17-source-coexistence.sh @@ -0,0 +1,379 @@ +#!/bin/sh +set -eu + +project_root=${PROJECT_ROOT:-$(pwd)} +script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +fruix_cmd=$project_root/bin/fruix +git_template=${GIT_TEMPLATE:-$script_dir/phase17-git-source-operating-system.scm.in} +txz_template=${TXZ_TEMPLATE:-$script_dir/phase17-txz-source-operating-system.scm.in} +system_name=${SYSTEM_NAME:-phase17-operating-system} +store_dir=${STORE_DIR:-/frx/store} +base_name=${BASE_NAME:-source-side-by-side} +base_version_label=${BASE_VERSION_LABEL:-15.0-source-side-by-side} +git_base_release=${GIT_BASE_RELEASE:-15.0-STABLE} +git_base_branch=${GIT_BASE_BRANCH:-stable/15} +git_source_name=${GIT_SOURCE_NAME:-stable15-side-a} +git_source_ref=${GIT_SOURCE_REF:-stable/15} +git_source_commit=${GIT_SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89} +git_declared_source_root=${GIT_DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-git} +txz_base_release=${TXZ_BASE_RELEASE:-15.0-RELEASE} +txz_base_branch=${TXZ_BASE_BRANCH:-releng/15.0} +txz_source_name=${TXZ_SOURCE_NAME:-release15-side-b} +txz_source_url=${TXZ_SOURCE_URL:-https://download.freebsd.org/releases/amd64/15.0-RELEASE/src.txz} +txz_source_sha256=${TXZ_SOURCE_SHA256:-83c3e8157b6d7afcae57167fda75693bf1e5f581ca149a6ecb2d398b71bdfab0} +txz_declared_source_root=${TXZ_DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-txz} +metadata_target=${METADATA_OUT:-} +root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub} + +[ -x "$fruix_cmd" ] || { + echo "fruix command is not executable: $fruix_cmd" >&2 + exit 1 +} +[ -f "$git_template" ] || { + echo "missing git operating-system template: $git_template" >&2 + exit 1 +} +[ -f "$txz_template" ] || { + echo "missing txz operating-system template: $txz_template" >&2 + exit 1 +} +[ -f "$root_authorized_key_file" ] || { + echo "missing root authorized key file: $root_authorized_key_file" >&2 + exit 1 +} + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase17-source-coexistence.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +cleanup_workdir() { + 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") +git_os=$workdir/git-operating-system.scm +txz_os=$workdir/txz-operating-system.scm +git_out_a=$workdir/git-build-a.txt +git_out_b=$workdir/git-build-b.txt +txz_out=$workdir/txz-build.txt +metadata_file=$workdir/phase17-source-coexistence-metadata.txt + +sed \ + -e "s|__BASE_NAME__|$base_name|g" \ + -e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \ + -e "s|__BASE_RELEASE__|$git_base_release|g" \ + -e "s|__BASE_BRANCH__|$git_base_branch|g" \ + -e "s|__SOURCE_NAME__|$git_source_name|g" \ + -e "s|__SOURCE_REF__|$git_source_ref|g" \ + -e "s|__SOURCE_COMMIT__|$git_source_commit|g" \ + -e "s|__DECLARED_SOURCE_ROOT__|$git_declared_source_root|g" \ + -e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \ + "$git_template" > "$git_os" + +sed \ + -e "s|__BASE_NAME__|$base_name|g" \ + -e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \ + -e "s|__BASE_RELEASE__|$txz_base_release|g" \ + -e "s|__BASE_BRANCH__|$txz_base_branch|g" \ + -e "s|__SOURCE_NAME__|$txz_source_name|g" \ + -e "s|__SOURCE_URL__|$txz_source_url|g" \ + -e "s|__SOURCE_SHA256__|$txz_source_sha256|g" \ + -e "s|__DECLARED_SOURCE_ROOT__|$txz_declared_source_root|g" \ + -e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \ + "$txz_template" > "$txz_os" + +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}" \ + "$@" +} + +build_os() { + os_file=$1 + out_file=$2 + action_env "$fruix_cmd" system build "$os_file" --system "$system_name" --store "$store_dir" >"$out_file" +} + +field() { + key=$1 + file=$2 + sed -n "s/^$key=//p" "$file" | tail -n 1 +} + +resolved_git_commit() { + file=$1 + grep -Eo '\(commit \. "[0-9a-f]{40}"\)' "$file" | head -n 1 | sed 's/.*"\([0-9a-f]\{40\}\)".*/\1/' +} + +build_os "$git_os" "$git_out_a" +build_os "$txz_os" "$txz_out" +build_os "$git_os" "$git_out_b" + +git_closure=$(field closure_path "$git_out_a") +git_closure_rebuild=$(field closure_path "$git_out_b") +txz_closure=$(field closure_path "$txz_out") +git_kernel_store=$(field kernel_store "$git_out_a") +txz_kernel_store=$(field kernel_store "$txz_out") +git_native_stores=$(field native_base_stores "$git_out_a") +txz_native_stores=$(field native_base_stores "$txz_out") +git_host_count=$(field host_base_store_count "$git_out_a") +txz_host_count=$(field host_base_store_count "$txz_out") +git_materialized_count=$(field materialized_source_store_count "$git_out_a") +txz_materialized_count=$(field materialized_source_store_count "$txz_out") +git_materialized_store=$(field materialized_source_stores "$git_out_a") +txz_materialized_store=$(field materialized_source_stores "$txz_out") +git_source_kind=$(field freebsd_source_kind "$git_out_a") +txz_source_kind=$(field freebsd_source_kind "$txz_out") +git_source_ref_out=$(field freebsd_source_ref "$git_out_a") +git_source_commit_out=$(field freebsd_source_commit "$git_out_a") +txz_source_url_out=$(field freebsd_source_url "$txz_out") +txz_source_sha256_out=$(field freebsd_source_sha256 "$txz_out") +git_source_file=$(field freebsd_source_file "$git_out_a") +txz_source_file=$(field freebsd_source_file "$txz_out") +git_source_materializations_file=$(field freebsd_source_materializations_file "$git_out_a") +txz_source_materializations_file=$(field freebsd_source_materializations_file "$txz_out") +git_store_layout_file=$(field store_layout_file "$git_out_a") +txz_store_layout_file=$(field store_layout_file "$txz_out") +git_base_name_out=$(field freebsd_base_name "$git_out_a") +txz_base_name_out=$(field freebsd_base_name "$txz_out") +git_base_version_out=$(field freebsd_base_version_label "$git_out_a") +txz_base_version_out=$(field freebsd_base_version_label "$txz_out") +git_base_release_out=$(field freebsd_base_release "$git_out_a") +txz_base_release_out=$(field freebsd_base_release "$txz_out") +git_base_source_root_out=$(field freebsd_base_source_root "$git_out_a") +txz_base_source_root_out=$(field freebsd_base_source_root "$txz_out") + +[ -n "$git_closure" ] || { echo "missing git closure" >&2; exit 1; } +[ -n "$txz_closure" ] || { echo "missing txz closure" >&2; exit 1; } +[ "$git_closure" = "$git_closure_rebuild" ] || { + echo "git closure path was not reproducible across rebuilds: $git_closure != $git_closure_rebuild" >&2 + exit 1 +} +[ "$git_closure" != "$txz_closure" ] || { + echo "git and txz closures unexpectedly match" >&2 + exit 1 +} +[ "$git_host_count" = 0 ] || { echo "git build has host base stores" >&2; exit 1; } +[ "$txz_host_count" = 0 ] || { echo "txz build has host base stores" >&2; exit 1; } +[ "$git_materialized_count" = 1 ] || { echo "expected one git materialized source store" >&2; exit 1; } +[ "$txz_materialized_count" = 1 ] || { echo "expected one txz materialized source store" >&2; exit 1; } +[ "$git_source_kind" = git ] || { echo "unexpected git source kind: $git_source_kind" >&2; exit 1; } +[ "$txz_source_kind" = src-txz ] || { echo "unexpected txz source kind: $txz_source_kind" >&2; exit 1; } +[ "$git_source_ref_out" = "$git_source_ref" ] || { echo "unexpected git source ref: $git_source_ref_out" >&2; exit 1; } +[ "$git_source_commit_out" = "$git_source_commit" ] || { echo "unexpected git source commit: $git_source_commit_out" >&2; exit 1; } +[ "$txz_source_url_out" = "$txz_source_url" ] || { echo "unexpected txz source URL: $txz_source_url_out" >&2; exit 1; } +[ "$txz_source_sha256_out" = "$txz_source_sha256" ] || { echo "unexpected txz source sha256: $txz_source_sha256_out" >&2; exit 1; } +[ "$git_base_name_out" = "$base_name" ] || { echo "unexpected git base name: $git_base_name_out" >&2; exit 1; } +[ "$txz_base_name_out" = "$base_name" ] || { echo "unexpected txz base name: $txz_base_name_out" >&2; exit 1; } +[ "$git_base_version_out" = "$base_version_label" ] || { echo "unexpected git base version label: $git_base_version_out" >&2; exit 1; } +[ "$txz_base_version_out" = "$base_version_label" ] || { echo "unexpected txz base version label: $txz_base_version_out" >&2; exit 1; } +[ "$git_base_release_out" = "$git_base_release" ] || { echo "unexpected git base release: $git_base_release_out" >&2; exit 1; } +[ "$txz_base_release_out" = "$txz_base_release" ] || { echo "unexpected txz base release: $txz_base_release_out" >&2; exit 1; } +[ "$git_base_source_root_out" = "$git_declared_source_root" ] || { echo "unexpected git declared source root: $git_base_source_root_out" >&2; exit 1; } +[ "$txz_base_source_root_out" = "$txz_declared_source_root" ] || { echo "unexpected txz declared source root: $txz_base_source_root_out" >&2; exit 1; } +[ "$git_materialized_store" != "$txz_materialized_store" ] || { + echo "git and txz materialized source stores unexpectedly match" >&2 + exit 1 +} +[ "$git_native_stores" != "$txz_native_stores" ] || { + echo "git and txz native store sets unexpectedly match" >&2 + exit 1 +} +[ "$git_kernel_store" != "$txz_kernel_store" ] || { + echo "git and txz kernel stores unexpectedly match" >&2 + exit 1 +} + +case "$git_kernel_store" in + /frx/store/*-freebsd-native-kernel-$base_version_label) : ;; + *) echo "unexpected git kernel store path: $git_kernel_store" >&2; exit 1 ;; +esac +case "$txz_kernel_store" in + /frx/store/*-freebsd-native-kernel-$base_version_label) : ;; + *) echo "unexpected txz kernel store path: $txz_kernel_store" >&2; exit 1 ;; +esac +case "$git_materialized_store" in + /frx/store/*-freebsd-source-$git_source_name) : ;; + *) echo "unexpected git materialized source store path: $git_materialized_store" >&2; exit 1 ;; +esac +case "$txz_materialized_store" in + /frx/store/*-freebsd-source-$txz_source_name) : ;; + *) echo "unexpected txz materialized source store path: $txz_materialized_store" >&2; exit 1 ;; +esac + +[ -f "$git_source_file" ] || { echo "missing git source file: $git_source_file" >&2; exit 1; } +[ -f "$txz_source_file" ] || { echo "missing txz source file: $txz_source_file" >&2; exit 1; } +[ -f "$git_source_materializations_file" ] || { echo "missing git source materializations file: $git_source_materializations_file" >&2; exit 1; } +[ -f "$txz_source_materializations_file" ] || { echo "missing txz source materializations file: $txz_source_materializations_file" >&2; exit 1; } +[ -f "$git_store_layout_file" ] || { echo "missing git store layout file: $git_store_layout_file" >&2; exit 1; } +[ -f "$txz_store_layout_file" ] || { echo "missing txz store layout file: $txz_store_layout_file" >&2; exit 1; } + +git_materialized_root=$git_materialized_store/tree +txz_materialized_root=$txz_materialized_store/tree/usr/src +[ -f "$git_materialized_root/Makefile" ] || { echo "git materialized source root missing Makefile" >&2; exit 1; } +[ -f "$txz_materialized_root/Makefile" ] || { echo "txz materialized source root missing Makefile" >&2; exit 1; } + +git_commit=$(resolved_git_commit "$git_source_materializations_file") +printf '%s\n' "$git_commit" | grep -E '^[0-9a-f]{40}$' >/dev/null || { + echo "failed to recover resolved git commit" >&2 + exit 1 +} + +grep -F "(source-root . \"$git_materialized_root\")" "$git_source_materializations_file" >/dev/null || { + echo "git source materializations file missing effective root" >&2 + exit 1 +} +[ "$git_commit" = "$git_source_commit" ] || { + echo "resolved git commit does not match the declared pinned commit: $git_commit != $git_source_commit" >&2 + exit 1 +} +grep -F "(commit . \"$git_commit\")" "$git_source_materializations_file" >/dev/null || { + echo "git source materializations file missing resolved commit" >&2 + exit 1 +} +grep -F "(sha256 . \"$txz_source_sha256\")" "$txz_source_materializations_file" >/dev/null || { + echo "txz source materializations file missing verified archive hash" >&2 + exit 1 +} +grep -F "(source-root . \"$txz_materialized_root\")" "$txz_source_materializations_file" >/dev/null || { + echo "txz source materializations file missing effective root" >&2 + exit 1 +} +grep -F "(materialized-source-store-count . 1)" "$git_store_layout_file" >/dev/null || { + echo "git store layout missing materialized source count" >&2 + exit 1 +} +grep -F "(materialized-source-store-count . 1)" "$txz_store_layout_file" >/dev/null || { + echo "txz store layout missing materialized source count" >&2 + exit 1 +} +grep -F "$git_materialized_store" "$git_store_layout_file" >/dev/null || { + echo "git store layout missing materialized source store path" >&2 + exit 1 +} +grep -F "$txz_materialized_store" "$txz_store_layout_file" >/dev/null || { + echo "txz store layout missing materialized source store path" >&2 + exit 1 +} + +git_runtime_store=$(printf '%s\n' "$git_native_stores" | tr ',' '\n' | grep "freebsd-native-runtime-$base_version_label$" | head -n 1) +txz_runtime_store=$(printf '%s\n' "$txz_native_stores" | tr ',' '\n' | grep "freebsd-native-runtime-$base_version_label$" | head -n 1) +[ -n "$git_runtime_store" ] || { echo "failed to recover git runtime store" >&2; exit 1; } +[ -n "$txz_runtime_store" ] || { echo "failed to recover txz runtime store" >&2; exit 1; } + +for path in "$git_kernel_store/.freebsd-native-build-info.scm" "$git_runtime_store/.freebsd-native-build-info.scm"; do + [ -f "$path" ] || { echo "missing git native build info file: $path" >&2; exit 1; } + grep -F "(materialized-source" "$path" >/dev/null || { + echo "git native build info missing materialized-source block in $path" >&2 + exit 1 + } + grep -F "(ref . \"$git_source_ref\")" "$path" >/dev/null || { + echo "git native build info missing source ref in $path" >&2 + exit 1 + } + grep -F "(commit . \"$git_commit\")" "$path" >/dev/null || { + echo "git native build info missing resolved commit in $path" >&2 + exit 1 + } + grep -F "(source-root . \"$git_materialized_root\")" "$path" >/dev/null || { + echo "git native build info missing materialized source root in $path" >&2 + exit 1 + } + grep -F "(store-path . \"$git_materialized_store\")" "$path" >/dev/null || { + echo "git native build info missing materialized source store path in $path" >&2 + exit 1 + } + if grep -F "(source-root . \"$git_declared_source_root\")" "$path" >/dev/null; then + echo "git native build info still records the unused declared source root in $path" >&2 + exit 1 + fi +done + +for path in "$txz_kernel_store/.freebsd-native-build-info.scm" "$txz_runtime_store/.freebsd-native-build-info.scm"; do + [ -f "$path" ] || { echo "missing txz native build info file: $path" >&2; exit 1; } + grep -F "(materialized-source" "$path" >/dev/null || { + echo "txz native build info missing materialized-source block in $path" >&2 + exit 1 + } + grep -F "(sha256 . \"$txz_source_sha256\")" "$path" >/dev/null || { + echo "txz native build info missing archive sha256 in $path" >&2 + exit 1 + } + grep -F "(source-root . \"$txz_materialized_root\")" "$path" >/dev/null || { + echo "txz native build info missing materialized source root in $path" >&2 + exit 1 + } + grep -F "(store-path . \"$txz_materialized_store\")" "$path" >/dev/null || { + echo "txz native build info missing materialized source store path in $path" >&2 + exit 1 + } + if grep -F "(source-root . \"$txz_declared_source_root\")" "$path" >/dev/null; then + echo "txz native build info still records the unused declared source root in $path" >&2 + exit 1 + fi +done + +git_closure_base=$(basename "$git_closure") +txz_closure_base=$(basename "$txz_closure") +cat >"$metadata_file" <