Record FreeBSD base provenance in system artifacts

This commit is contained in:
2026-04-02 21:41:04 +02:00
parent 4b786c356f
commit a04e650326
11 changed files with 759 additions and 5 deletions

316
docs/PLAN_3.md Normal file
View File

@@ -0,0 +1,316 @@
# Fruix on FreeBSD, Path C: Short Hardening Pass, Then Native FreeBSD Base Builds
This document extends `docs/PLAN_2.md` after the completion of Phases 1 through 10 and the post-Phase-10 Shepherd/PID-1 and runtime-prefix cleanup work.
The project has already crossed the main feasibility threshold:
- Fruix can build declarative FreeBSD system artifacts
- those artifacts boot on the real XCP-ng VM
- the guest reaches DHCP and SSH
- both `freebsd-init+rc.d-shepherd` and `shepherd-pid1` have been validated
- the guest no longer depends on `/tmp/*-validate-install` runtime-compatibility shims
The next architectural question is about the **FreeBSD base itself**.
Today, Fruix still assembles the kernel, bootloader, libraries, rc assets, and userland largely by **copying curated artifacts from the build host** into `/frx/store`. That was the right bootstrap move, but it is now the largest remaining non-native part of the system.
Long-term, the desired direction is clear:
- build FreeBSD kernel/world as Fruix-managed artifacts
- keep them in `/frx/store`
- make system upgrades a declarative rebuild/redeploy story instead of a host-copy refresh
However, this plan does **not** recommend jumping directly into full self-hosted FreeBSD base builds inside the guest. Instead, it takes a narrower and safer sequence:
1. do a **short hardening pass** on the current working system
2. then begin **Option B** immediately as the next major architecture phase
3. start Option B on the **builder side** from `/usr/src`, not with guest self-hosting
This keeps momentum while avoiding two failure modes:
- over-polishing the current host-copy pipeline as if it were the end state
- mixing base-system build mechanics, runtime polish, boot debugging, and self-hosting into one oversized step
Throughout this plan, the canonical Fruix roots remain:
- `/frx/store`
- `/frx/var`
- `/frx/etc`
The user-facing system remains **Fruix**, with CLI `fruix`, while internal upstream-derived names continue to be renamed selectively rather than mechanically.
---
## Current State at the Start of Plan 3
Fruix on FreeBSD already has:
- a functioning content-addressed store and derivation path
- daemon-mediated builds with FreeBSD-aware isolation prototypes
- real package outputs in `/frx/store`
- a declarative FreeBSD operating-system model
- rootfs and image generation driven by `fruix system ...`
- real XCP-ng validation using the approved VM/VDI path
- a working FreeBSD guest with DHCP, SSH, activation, Shepherd, and ready markers
- validated `shepherd-pid1` boot on both local QEMU/TCG and the real XCP-ng VM
- removal of the guest runtime's dependence on `/tmp` compatibility-prefix symlinks
The main remaining architectural compromise is that the FreeBSD base layer is still defined mostly as a set of curated host copies:
- kernel from `/boot/kernel`
- bootloader and boot assets from `/boot`
- userland/runtime pieces from `/bin`, `/sbin`, `/usr/bin`, `/usr/sbin`, `/lib`, `/usr/lib`, and `/etc`
- headers from `/usr/src`
That means Fruix can currently rebuild and redeploy a working system, but it does **not** yet have a truly native upgrade story for the FreeBSD base itself.
---
## Guiding Decision for the Next Milestone
The next milestone should be:
- **just enough hardening** to make the current system a reliable platform for deeper work,
- followed immediately by the first **host-built FreeBSD base artifacts** in `/frx/store`.
This means the next work should optimize for:
- debuggability
- provenance
- repeatable deployment/validation
- clean separation between host-staged base artifacts and Fruix-built artifacts
- incremental replacement of the copy-based base layer
It should **not** optimize for:
- turning the current host-copy path into a heavily polished long-term abstraction
- immediate self-hosted `buildworld`/`buildkernel` inside the Fruix guest
- solving every usability concern before starting native base work
---
## Phase 12: Short Hardening Pass on the Existing Working Pipeline
The purpose of this phase is not to beautify the bootstrap path. It is to make the existing validated system reliable enough that FreeBSD base-build work can proceed without avoidable confusion.
### Intermediate Goal 12.1: Improve Deployment Provenance and Failure Diagnosis
**Verification Goal 12.1:** Make the current pipeline record exactly what FreeBSD base inputs and deployment artifacts were used for each generated system/image.
This should include at least:
- host `freebsd-version` / `uname` provenance
- `/usr/src` revision or identifying metadata when available
- the exact staged base-package store paths used in the system closure
- image metadata that clearly distinguishes:
- host-copied FreeBSD base artifacts
- Fruix-built Guile/Shepherd/system artifacts
- better collection of boot/runtime logs where possible from the existing QEMU and XCP-ng harnesses
**Success Criteria:** a generated Fruix system/image can be traced back to its host-side FreeBSD base inputs and runtime validation logs without ad hoc investigation.
### Intermediate Goal 12.2: Tighten Basic Runtime Completeness and Operator Diagnostics
**Verification Goal 12.2:** Close the most important remaining “prototype rough edges” in the current guest so it remains a dependable validation target while Option B begins.
This should focus on small, high-value improvements such as:
- clearer boot/service logs
- fewer known noisy runtime warnings where reasonably fixable
- more explicit validation of essential services and files
- better failure surfacing from activation and service startup
This phase should avoid broad feature creep. The target is not a polished distribution; it is a sharper debugging and validation baseline.
**Success Criteria:** the current FreeBSD guest remains reproducibly bootable and easier to debug, with fewer ambiguous failures during rebuild/redeploy cycles.
### Intermediate Goal 12.3: Make the Host-Staged FreeBSD Base Boundary Explicit
**Verification Goal 12.3:** Refine the current package/model layer so the “host-staged FreeBSD base” is treated as an explicit transitional boundary rather than an implicit permanent design.
This should include:
- clearer grouping or manifesting of host-staged base packages
- documentation of which components are currently copied from the host
- explicit notes on which ones are planned to be replaced first by native base builds
**Success Criteria:** the repo documents and code clearly separate transitional host-copy FreeBSD base handling from genuine Fruix-built artifacts.
---
## Phase 13: Option B Begins — Host-Built FreeBSD Kernel and World Artifacts in `/frx/store`
This is the real architectural pivot.
The goal is **not** yet to self-host FreeBSD base builds inside a Fruix guest. The goal is to teach Fruix to produce FreeBSD base artifacts as build outputs under `/frx/store`, using the builder host and `/usr/src` as the source of truth.
### Intermediate Goal 13.1: Model FreeBSD World and Kernel as Fruix Build Artifacts
**Verification Goal 13.1:** Introduce Fruix build descriptions for the FreeBSD base that can represent at least:
- a `freebsd-world` artifact
- a `freebsd-kernel` artifact
- required boot assets associated with the selected kernel/world build
The first version may still be specialized and FreeBSD-specific rather than fully generalized.
This step should determine and document:
- how `/usr/src` is treated as an input
- what build parameters affect output identity
- how kernel/world configuration is hashed into the output model
- how these outputs are split or grouped in `/frx/store`
**Success Criteria:** Fruix can describe native FreeBSD world/kernel build outputs as real store artifacts rather than only host-copy packages.
### Intermediate Goal 13.2: Build and Stage Minimal FreeBSD World/Kernel Outputs From `/usr/src`
**Verification Goal 13.2:** Produce the first concrete world/kernel build outputs from `/usr/src` and stage them into `/frx/store`.
The first target should be intentionally narrow:
- build a kernel matching the validated VM target
- build a minimal world sufficient for the current Fruix guest to boot
- stage install trees under content-addressed store paths
This phase should prefer correctness and repeatability over completeness. A minimal successful output split is acceptable if it is documented.
**Success Criteria:** `/frx/store` contains native Fruix-managed FreeBSD world/kernel outputs built from `/usr/src`, and their contents can be inspected as build results rather than copied host snapshots.
### Intermediate Goal 13.3: Boot a Fruix System Using Store-Built FreeBSD Base Artifacts
**Verification Goal 13.3:** Wire the operating-system/image pipeline so a generated system can boot using the newly built world/kernel outputs instead of the corresponding host-copy packages.
This should preserve the existing validation strategy:
- local QEMU/TCG + UEFI where useful
- real XCP-ng validation on the approved VM and existing VDI
The first boot target may still leave some secondary base components on the older copy path if necessary, as long as the transition boundary is explicit.
**Success Criteria:** a Fruix system/image boots successfully using native store-built FreeBSD base artifacts for at least the kernel and core world runtime.
---
## Phase 14: Incrementally Replace the Host-Copy FreeBSD Base Layer
Once a minimal native world/kernel path works, the rest of the host-copy base layer should be retired incrementally rather than in one giant switch.
### Intermediate Goal 14.1: Replace Kernel and Boot Assets First
**Verification Goal 14.1:** Move the system closure and image generator to prefer native store-built kernel and boot assets over host-copied `/boot/...` material.
This should include:
- kernel
- loader/boot assets as appropriate
- any required linker or boot metadata
**Success Criteria:** the image no longer relies on host-copied kernel/boot components for the validated boot path.
### Intermediate Goal 14.2: Replace the Core Runtime World Slice
**Verification Goal 14.2:** Move the essential userland/runtime components to the native world outputs, including the files required for:
- boot
- activation
- networking
- SSH
- service startup
- basic operator access
**Success Criteria:** the booted Fruix system reaches ready state using a native store-built core FreeBSD runtime rather than a hand-curated host copy set.
### Intermediate Goal 14.3: Revisit Headers, Toolchain, and Development Splits
**Verification Goal 14.3:** Define cleaner boundaries between:
- runtime world outputs
- development headers
- toolchain artifacts
- optional build/developer profiles
This step should reduce accidental coupling between “what the guest needs to boot” and “what the host needs to build software”.
**Success Criteria:** Fruix has a clearer and more maintainable model for FreeBSD runtime vs. development artifacts in `/frx/store`.
---
## Phase 15: Establish a Real Fruix Upgrade Story for the FreeBSD Base
After native base artifacts exist and are used by the system closure, Fruix can move from “rebuild from current host copies” toward a more honest upgrade story.
### Intermediate Goal 15.1: Make the FreeBSD Base Version a Declarative Input
**Verification Goal 15.1:** Define how a Fruix system declares which FreeBSD base/kernel source version it targets.
This may initially remain tied to locally available `/usr/src`, but it should move the model toward a declarative notion of:
- target FreeBSD branch/release
- kernel configuration
- world configuration
- boot/runtime variant
**Success Criteria:** FreeBSD base versioning becomes an explicit part of the Fruix system model rather than an implicit property of the builder host.
### Intermediate Goal 15.2: Support Rebuild/Redeploy/Rollback Across Base Versions
**Verification Goal 15.2:** Demonstrate that at least two distinct FreeBSD base builds can coexist in `/frx/store` and that the generated system can switch between them through the normal rebuild/redeploy flow.
This should preserve the Guix-inspired strengths:
- content-addressed outputs
- side-by-side versions
- rollback-friendly system closures
**Success Criteria:** Fruix can rebuild and redeploy a system against a newer FreeBSD base without mutating the old one in place.
### Intermediate Goal 15.3: Decide When to Pursue Self-Hosted Base Builds
**Verification Goal 15.3:** Reassess whether the next step should be:
- building FreeBSD base artifacts inside a Fruix-managed environment, or
- continuing to use the host builder while improving reproducibility and source acquisition
This decision should be made only after native world/kernel artifacts are already working in `/frx/store`.
**Success Criteria:** the project has a documented, evidence-based decision on whether and when to pursue self-hosted FreeBSD base builds.
---
## Strategic Notes
### Why this order?
This sequence is intended to preserve momentum and architectural clarity.
The short hardening pass prevents the next phase from being undermined by avoidable debugging ambiguity. But the hardening phase is intentionally short so the project does not over-invest in the transitional host-copy design.
Then Option B begins immediately in a way that is ambitious but still controlled:
- build FreeBSD base artifacts on the host
- store them in `/frx/store`
- boot from them
- replace the host-copy model incrementally
### What this plan does **not** require yet
This plan does **not** require, in its first native-base stages:
- guest self-hosting
- in-place `freebsd-update` integration
- complete replacement of every base file in one step
- abandoning the validated XCP-ng/QEMU test harnesses
- immediate adoption of `shepherd-pid1` as the only supported boot path
### What success will mean
Success under this plan means the project moves from:
- “Fruix assembles a FreeBSD system by copying a curated slice of the host”
into:
- “Fruix builds and stores FreeBSD base artifacts itself, then assembles and deploys systems from those declared outputs.”
That is the real bridge from the current prototype to a more genuinely native Fruix system on FreeBSD.

View File

@@ -2619,3 +2619,95 @@ Next recommended step:
1. keep `shepherd-pid1` available as the stronger experimental boot architecture
2. start pushing the local Guile / guile-extra / Shepherd build/install process itself toward a truly store-native prefix layout
3. clean up the remaining historical prefix strings still present in binaries, libtool metadata, and pkg-config metadata where they still matter for developer/operator workflows
## 2026-04-02 — Phase 12.1: deployment provenance and diagnostic metadata improved
Completed work:
- wrote the next-stage plan document:
- `docs/PLAN_3.md`
- added a concise progress snapshot:
- `docs/PROG_SUMMARY.md`
- wrote the Phase 12.1 report:
- `docs/reports/phase12-provenance-diagnostics-freebsd.md`
- updated `modules/fruix/system/freebsd.scm` so generated system closures now carry explicit provenance files:
- `metadata/host-base-provenance.scm`
- `metadata/store-layout.scm`
- those closure metadata files now record at least:
- host `freebsd-version -kru`
- host `uname -a`
- `/usr/src` path
- `/usr/src` git revision/branch when available
- `newvers.sh` SHA256 as a fallback source-tree identifier
- exact host-staged FreeBSD base store paths
- exact Fruix runtime store paths
- selected init mode
- extended `scripts/fruix.scm` so `fruix system build` and `fruix system image` now emit explicit provenance/layout metadata including:
- `host_base_store_count`
- `host_base_stores`
- `fruix_runtime_store_count`
- `fruix_runtime_stores`
- `host_base_provenance_file`
- `store_layout_file`
- `host_freebsd_version`
- `host_uname`
- `usr_src_git_revision`
- `usr_src_git_branch`
- `usr_src_newvers_sha256`
- tightened the local closure/image validation harnesses so they now assert that this provenance metadata exists:
- `tests/system/run-phase7-system-closure.sh`
- `tests/system/run-phase8-system-image.sh`
- expanded the VM-oriented harnesses to collect more guest-side diagnostic tails where available:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
- now include additional capture for:
- `shepherd-bootstrap.out`
- `shepherd.log`
- recent `dmesg`
- stabilized local reproducibility checks by forcing:
- `GUILE_AUTO_COMPILE=0`
in the host-side closure/image harnesses when invoking `fruix` under `sudo env`
Validation:
- `tests/system/run-phase7-system-closure.sh` passes with the new provenance checks:
- workdir: `/tmp/phase12-1b-closure-1775157039`
- confirmed:
- `host_base_store_count=8`
- `fruix_runtime_store_count=3`
- `host_base_provenance_file=/frx/store/.../metadata/host-base-provenance.scm`
- `store_layout_file=/frx/store/.../metadata/store-layout.scm`
- `host_freebsd_version=15.0-STABLE`
- `tests/system/run-phase8-system-image.sh` passes with the new provenance checks:
- workdir: `/tmp/phase12-1b-image-1775157039`
- confirmed:
- `host_base_store_count=8`
- `fruix_runtime_store_count=3`
- `host_base_provenance_file=/frx/store/.../metadata/host-base-provenance.scm`
- `store_layout_file=/frx/store/.../metadata/store-layout.scm`
- `host_uname=FreeBSD fruixdev 15.0-STABLE ...`
- syntax-checked successfully:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
Important findings:
- the current host-staged FreeBSD base pipeline is still transitional, but it is now much more inspectable and self-describing
- one-shot reproducibility failures immediately after source edits were partly a host-side auto-compilation artifact; forcing `GUILE_AUTO_COMPILE=0` in the validation harnesses makes the closure/image checks more stable
- the project can now identify much more directly:
- which closure/image inputs came from the host-staged FreeBSD base
- which came from Fruix-built Guile/Shepherd runtime artifacts
- what `/usr/src` identity was available at build time
Current assessment:
- Fruix now has a better-documented and easier-to-debug working pipeline for the current host-staged FreeBSD base model
- this is the right amount of hardening before beginning native FreeBSD base-build work; it improves traceability without pretending the current host-copy model is final
Next recommended step:
1. continue with Phase 12.2 and tighten the guest-side runtime/operator diagnostics
2. remove or reduce the most distracting remaining boot/runtime rough edges where the fixes are small and local
3. keep the deployment path stable so Phase 13 can start from a sharper baseline

40
docs/PROG_SUMMARY.md Normal file
View File

@@ -0,0 +1,40 @@
# Fruix FreeBSD Progress Summary
## What we have achieved so far
Fruix now has a working end-to-end prototype of a Guix-inspired system on FreeBSD, while preserving the Fruix product boundary (`fruix`, `/frx`) and the core model: declarative builds, content-addressed store paths, daemon-mediated privilege separation, garbage-collection roots, derivations, and reproducible system artifacts.
Completed milestones include:
- **Core runtime/build foundation**: validated Guile on FreeBSD, diagnosed the packaged-Guile subprocess crash, and established a working local Guile path with the upstream FreeBSD fix.
- **Store and daemon path**: built the first Fruix daemon/store workflow on FreeBSD, including jail/build-user isolation, store population under `/frx/store`, derivation generation, and minimal RPC integration.
- **Real package builds**: adapted enough of the GNU build/package path to build real package outputs into `/frx/store` and install a minimal profile.
- **System model**: introduced a declarative FreeBSD `operating-system` model, reproducible system closures, rootfs generation, and raw image generation as first-class system artifacts.
- **Bootable VM images**: built FreeBSD images that boot reproducibly and stage the full `/frx/store` into the guest image.
- **Real VM validation on XCP-ng**: proved the system boots on the real target VM, gets DHCP, starts SSH, and exposes a usable guest for validation.
- **User-facing CLI**: added `bin/fruix` with `fruix system build|rootfs|image`, making system artifact generation a real Fruix workflow instead of a phase-specific prototype.
- **Boot architecture progress**: validated both boot modes on the real XCP-ng VM:
- `freebsd-init+rc.d-shepherd`
- `shepherd-pid1`
- **Native runtime cleanup**: removed the guest runtimes dependence on `/tmp/*-validate-install` compatibility-prefix symlinks for Guile, guile-extra, and Shepherd. The guest now boots and runs Guile/Shepherd from the store-backed runtime layout without those temporary aliases.
## Major pain points now behind us
- **Guile subprocess instability on FreeBSD**: root-caused and bypassed with the validated local Guile build.
- **“Prototype only” store/build flow**: replaced with real derivations, store outputs, and system artifacts under `/frx/store`.
- **Non-booting early images**: fixed rootfs/image composition, GPT sizing issues for XCP-ng, and early rc/fstab problems.
- **No practical VM validation path**: when local bhyve proved impossible under Xen, validation successfully pivoted to the real XCP-ng VM and existing VDI.
- **Guest runtime crashes from locale/prefix issues**: fixed enough locale/runtime staging and later removed dependence on guest-side compatibility-prefix shims.
- **Shepherd only as a bridged service**: Fruix has now proven Shepherd can run as PID 1 on FreeBSD, not just behind FreeBSD `init` + `rc.d`.
## 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.
- **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.
- **Remaining ecosystem gaps**: some deeper Guix-like features are still immature on FreeBSD, especially around native package-management ergonomics and fully polished runtime/deployment behavior.
## 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, and operate from `/frx` without depending on temporary runtime-prefix shims. The biggest remaining work is no longer “can this boot?” but “how cleanly and natively can we make the runtime and boot architecture from here?”

View File

@@ -0,0 +1,161 @@
# Phase 12.1: deployment provenance and diagnostic metadata for the current FreeBSD pipeline
Date: 2026-04-02
## Goal
Before starting native FreeBSD base builds from `/usr/src`, the current host-staged pipeline needed better provenance and easier diagnosis.
The specific targets for this subphase were:
- record which host-side FreeBSD base inputs were used for a generated closure/image
- make the closure itself carry that provenance
- distinguish host-staged FreeBSD base stores from Fruix-built runtime stores
- improve the validation harnesses so later failures are easier to investigate
## Implementation
### 1. Closure-level provenance files
`modules/fruix/system/freebsd.scm` now generates and stores two explicit metadata files in the system closure:
- `metadata/host-base-provenance.scm`
- `metadata/store-layout.scm`
These record:
- host `freebsd-version -kru`
- host `uname -a`
- `/usr/src` path
- `/usr/src` git revision/branch when available
- `newvers.sh` SHA256 as a stable source-tree identifier fallback
- exact host-staged FreeBSD base store paths used by the closure
- exact Fruix runtime store paths used by the closure
- selected init mode
This makes the generated closure self-describing instead of requiring ad hoc host inspection.
### 2. `fruix system` metadata became more explicit
`scripts/fruix.scm` now emits additional machine-readable metadata for both:
- `fruix system build`
- `fruix system image`
New fields include:
- `host_base_store_count`
- `host_base_stores`
- `fruix_runtime_store_count`
- `fruix_runtime_stores`
- `host_base_provenance_file`
- `store_layout_file`
- `host_freebsd_version`
- `host_uname`
- `usr_src_git_revision`
- `usr_src_git_branch`
- `usr_src_newvers_sha256`
This gives later harnesses and operators a clean way to identify exactly what kind of system/image was produced.
### 3. Validation harnesses now assert provenance availability
Updated harnesses:
- `tests/system/run-phase7-system-closure.sh`
- `tests/system/run-phase8-system-image.sh`
They now validate that provenance fields are present and that the generated closure contains the new metadata files.
### 4. Runtime-diagnostic capture was expanded in the VM harnesses
Updated harnesses:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
These now collect additional guest-side diagnostic tails where available, including:
- `shepherd-bootstrap.out`
- `shepherd.log`
- recent `dmesg`
This does not change the boot architecture, but it improves post-failure visibility.
### 5. Host-side validation became more stable
The host-side closure/image harnesses now force:
- `GUILE_AUTO_COMPILE=0`
when invoking `fruix` under `sudo env`.
This avoided a misleading one-time validation drift caused by mixed compiled/source host state during back-to-back reproducibility checks immediately after source edits.
## Validation
### Phase 7 system closure
Passing run:
- `PASS phase7-system-closure`
- workdir: `/tmp/phase12-1b-closure-1775157039`
Key new metadata included:
```text
host_base_store_count=8
fruix_runtime_store_count=3
host_base_provenance_file=/frx/store/.../metadata/host-base-provenance.scm
store_layout_file=/frx/store/.../metadata/store-layout.scm
host_freebsd_version=15.0-STABLE
usr_src_newvers_sha256=d6f6e9ab352d3f6281e788c78a63ac311ab7a3a4bb5dfc0016ed0aadb90b5d9d
```
### Phase 8 system image
Passing run:
- `PASS phase8-system-image`
- workdir: `/tmp/phase12-1b-image-1775157039`
Key new metadata included:
```text
host_base_store_count=8
fruix_runtime_store_count=3
host_base_provenance_file=/frx/store/.../metadata/host-base-provenance.scm
store_layout_file=/frx/store/.../metadata/store-layout.scm
host_freebsd_version=15.0-STABLE
host_uname=FreeBSD fruixdev 15.0-STABLE ...
```
### Harness validation
Syntax-checked successfully:
- `tests/system/run-phase9-xcpng-boot.sh`
- `tests/system/run-phase11-shepherd-pid1-xcpng.sh`
- `tests/system/run-phase11-shepherd-pid1-qemu.sh`
## Assessment
This subphase does not replace the host-staged FreeBSD base model yet. Instead, it makes that transitional model much easier to reason about.
Generated systems/images now answer the key questions directly:
- which FreeBSD base inputs came from the host?
- which runtime artifacts came from Fruix-built Guile/Shepherd outputs?
- what `/usr/src` identity was available at build time?
- where should an operator look first when the VM boot/runtime path misbehaves?
That is sufficient to support the next hardening step and then the later move toward native FreeBSD world/kernel artifacts in `/frx/store`.
## Next recommended step
Proceed to Phase 12.2:
- improve runtime/operator diagnostics inside the guest itself
- reduce the remaining noisy boot rough edges where the fixes are small and local
- keep the validated deployment path stable while preparing for native base-build work

View File

@@ -184,6 +184,9 @@
(error (format #f "command failed: ~a ~s => ~a" program args status)))
(trim-trailing-newlines output)))
(define (safe-command-output program . args)
(false-if-exception (apply command-output program args)))
(define (write-file path content)
(mkdir-p (dirname path))
(call-with-output-file path
@@ -198,6 +201,23 @@
(define (file-hash path)
(command-output "sha256" "-q" path))
(define (host-freebsd-provenance)
(let ((src-git? (file-exists? "/usr/src/.git"))
(newvers "/usr/src/sys/conf/newvers.sh"))
`((freebsd-release . ,freebsd-release)
(freebsd-version-kru . ,(or (safe-command-output "freebsd-version" "-kru") "unknown"))
(uname . ,(or (safe-command-output "uname" "-a") "unknown"))
(usr-src-path . "/usr/src")
(usr-src-git-revision . ,(or (and src-git?
(safe-command-output "git" "-C" "/usr/src" "rev-parse" "HEAD"))
"absent"))
(usr-src-git-branch . ,(or (and src-git?
(safe-command-output "git" "-C" "/usr/src" "rev-parse" "--abbrev-ref" "HEAD"))
"absent"))
(usr-src-newvers-sha256 . ,(if (file-exists? newvers)
(file-hash newvers)
"absent")))))
(define (directory-entries path)
(sort (filter (lambda (entry)
(not (member entry '("." ".."))))
@@ -917,6 +937,8 @@
"etc/shells"
"etc/motd"
"etc/ttys"
"metadata/host-base-provenance.scm"
"metadata/store-layout.scm"
"activate"
"shepherd/init.scm")
(if (pid1-init-mode? os)
@@ -1051,16 +1073,29 @@
#:extra-files (append guile-runtime-extra-files
guile-extra-runtime-files)))
(shepherd-store (materialize-prefix shepherd-prefix "fruix-shepherd-runtime" "1.0.9" store-dir))
(host-base-stores (delete-duplicates (append (list kernel-store bootloader-store)
base-package-stores)))
(fruix-runtime-stores (list guile-store guile-extra-store shepherd-store))
(metadata-files
`(("metadata/host-base-provenance.scm"
. ,(object->string (host-freebsd-provenance)))
("metadata/store-layout.scm"
. ,(object->string
`((host-base-store-count . ,(length host-base-stores))
(host-base-stores . ,host-base-stores)
(fruix-runtime-store-count . ,(length fruix-runtime-stores))
(fruix-runtime-stores . ,fruix-runtime-stores)
(init-mode . ,(operating-system-init-mode os)))))))
(generated-files (append (operating-system-generated-files os
#:guile-store guile-store
#:guile-extra-store guile-extra-store
#:shepherd-store shepherd-store)
metadata-files
`(("usr/local/etc/rc.d/fruix-activate"
. ,(render-activation-rc-script))
("usr/local/etc/rc.d/fruix-shepherd"
. ,(render-rc-script shepherd-store guile-store guile-extra-store)))))
(references (append (list kernel-store bootloader-store guile-store guile-extra-store shepherd-store)
base-package-stores))
(references (delete-duplicates (append host-base-stores fruix-runtime-stores)))
(manifest (string-append
"closure-spec=\n"
(object->string (operating-system-closure-spec os))
@@ -1112,6 +1147,10 @@
(guile-extra-store . ,guile-extra-store)
(shepherd-store . ,shepherd-store)
(base-package-stores . ,base-package-stores)
(host-base-stores . ,host-base-stores)
(fruix-runtime-stores . ,fruix-runtime-stores)
(host-base-provenance-file . ,(string-append closure-path "/metadata/host-base-provenance.scm"))
(store-layout-file . ,(string-append closure-path "/metadata/store-layout.scm"))
(generated-files . ,(map car generated-files))
(references . ,references))))
@@ -1369,5 +1408,9 @@
(esp-image . ,esp-image)
(root-image . ,root-image)
(closure-path . ,closure-path)
(host-base-stores . ,(assoc-ref closure 'host-base-stores))
(fruix-runtime-stores . ,(assoc-ref closure 'fruix-runtime-stores))
(host-base-provenance-file . ,(assoc-ref closure 'host-base-provenance-file))
(store-layout-file . ,(assoc-ref closure 'store-layout-file))
(image-spec . ,image-spec)
(store-items . ,store-items))))

View File

@@ -162,7 +162,10 @@ Options:\n\
(closure-path (assoc-ref result 'closure-path))
(generated-files (assoc-ref result 'generated-files))
(references (assoc-ref result 'references))
(base-package-stores (assoc-ref result 'base-package-stores)))
(base-package-stores (assoc-ref result 'base-package-stores))
(host-base-stores (assoc-ref result 'host-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "build")
(os_file . ,os-file)
@@ -177,6 +180,17 @@ Options:\n\
(shepherd_store . ,(assoc-ref result 'shepherd-store))
(base_package_store_count . ,(length base-package-stores))
(base_package_stores . ,(string-join base-package-stores ","))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-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))
(generated_file_count . ,(length generated-files))
(reference_count . ,(length references))))))
((string=? action "rootfs")
@@ -204,7 +218,10 @@ Options:\n\
#:shepherd-prefix shepherd-prefix
#:disk-capacity disk-capacity))
(image-spec (assoc-ref result 'image-spec))
(store-items (assoc-ref result 'store-items)))
(store-items (assoc-ref result 'store-items))
(host-base-stores (assoc-ref result 'host-base-stores))
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
(emit-metadata
`((action . "image")
(os_file . ,os-file)
@@ -216,6 +233,17 @@ Options:\n\
(esp_image . ,(assoc-ref result 'esp-image))
(root_image . ,(assoc-ref result 'root-image))
(closure_path . ,(assoc-ref result 'closure-path))
(host_base_store_count . ,(length host-base-stores))
(host_base_stores . ,(string-join host-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)))))))))))))
(main (command-line))

View File

@@ -110,6 +110,9 @@ shepherd_socket=$(ssh_guest 'test -S /var/run/shepherd.sock && echo present || e
shepherd_status=$(ssh_guest 'test -f /var/run/shepherd.pid && kill -0 "$(cat /var/run/shepherd.pid)" >/dev/null 2>&1 && echo running || echo stopped')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ')
shepherd_bootstrap_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd-bootstrap.out 2>/dev/null || true" | tr '\n' ' ')
shepherd_log_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd.log 2>/dev/null || true" | tr '\n' ' ')
guest_dmesg_tail=$(ssh_guest "dmesg | awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }'" | tr '\n' ' ')
uname_output=$(ssh_guest 'uname -sr')
operator_home_listing=$(ssh_guest 'ls -d /home/operator')
@@ -149,6 +152,9 @@ shepherd_socket=$shepherd_socket
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
shepherd_bootstrap_tail=$shepherd_bootstrap_tail
shepherd_log_tail=$shepherd_log_tail
guest_dmesg_tail=$guest_dmesg_tail
uname_output=$uname_output
operator_home_listing=$operator_home_listing
boot_backend=qemu-uefi-tcg

View File

@@ -149,6 +149,9 @@ shepherd_pid=$(ssh_guest 'cat /var/run/shepherd.pid')
shepherd_socket=$(ssh_guest 'test -S /var/run/shepherd.sock && echo present || echo missing')
shepherd_status=$(ssh_guest 'test -f /var/run/shepherd.pid && kill -0 "$(cat /var/run/shepherd.pid)" >/dev/null 2>&1 && echo running || echo stopped')
logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ')
shepherd_bootstrap_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd-bootstrap.out 2>/dev/null || true" | tr '\n' ' ')
shepherd_log_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd.log 2>/dev/null || true" | tr '\n' ' ')
guest_dmesg_tail=$(ssh_guest "dmesg | awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }'" | tr '\n' ' ')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
uname_output=$(ssh_guest 'uname -sr')
operator_home_listing=$(ssh_guest 'ls -d /home/operator')
@@ -199,6 +202,9 @@ shepherd_socket=$shepherd_socket
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
shepherd_bootstrap_tail=$shepherd_bootstrap_tail
shepherd_log_tail=$shepherd_log_tail
guest_dmesg_tail=$guest_dmesg_tail
uname_output=$uname_output
operator_home_listing=$operator_home_listing
compat_prefix_shims=$compat_prefix_shims

View File

@@ -40,6 +40,7 @@ metadata_file=$workdir/phase7-system-closure-metadata.txt
action_env() {
sudo env \
HOME="$HOME" \
GUILE_AUTO_COMPILE=0 \
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}" \
@@ -64,6 +65,17 @@ guile_extra_store=$(sed -n 's/^guile_extra_store=//p' "$build_out_a")
shepherd_store=$(sed -n 's/^shepherd_store=//p' "$build_out_a")
base_package_store_count=$(sed -n 's/^base_package_store_count=//p' "$build_out_a")
base_package_stores=$(sed -n 's/^base_package_stores=//p' "$build_out_a")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$build_out_a")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$build_out_a")
fruix_runtime_store_count=$(sed -n 's/^fruix_runtime_store_count=//p' "$build_out_a")
fruix_runtime_stores=$(sed -n 's/^fruix_runtime_stores=//p' "$build_out_a")
host_base_provenance_file=$(sed -n 's/^host_base_provenance_file=//p' "$build_out_a")
store_layout_file=$(sed -n 's/^store_layout_file=//p' "$build_out_a")
host_freebsd_version=$(sed -n 's/^host_freebsd_version=//p' "$build_out_a")
host_uname=$(sed -n 's/^host_uname=//p' "$build_out_a")
usr_src_git_revision=$(sed -n 's/^usr_src_git_revision=//p' "$build_out_a")
usr_src_git_branch=$(sed -n 's/^usr_src_git_branch=//p' "$build_out_a")
usr_src_newvers_sha256=$(sed -n 's/^usr_src_newvers_sha256=//p' "$build_out_a")
reference_count=$(sed -n 's/^reference_count=//p' "$build_out_a")
generated_file_count=$(sed -n 's/^generated_file_count=//p' "$build_out_a")
@@ -100,7 +112,9 @@ for path in \
"$profile_bin_sh" \
"$profile_sbin_init" \
"$profile_rc" \
"$closure_path/parameters.scm"
"$closure_path/parameters.scm" \
"$host_base_provenance_file" \
"$store_layout_file"
do
[ -e "$path" ] || {
echo "required path missing: $path" >&2
@@ -111,8 +125,12 @@ done
[ -x "$activation_script" ] || { echo "activation script is not executable" >&2; exit 1; }
[ -x "$rc_script" ] || { echo "fruix shepherd rc script is not executable" >&2; exit 1; }
[ -n "$base_package_store_count" ] || { echo "missing base package store count" >&2; exit 1; }
[ -n "$host_base_store_count" ] || { echo "missing host base store count" >&2; exit 1; }
[ -n "$fruix_runtime_store_count" ] || { echo "missing Fruix runtime store count" >&2; exit 1; }
[ -n "$generated_file_count" ] || { echo "missing generated file count" >&2; exit 1; }
[ -n "$reference_count" ] || { echo "missing reference count" >&2; exit 1; }
[ -n "$host_freebsd_version" ] || { echo "missing host freebsd version provenance" >&2; exit 1; }
[ -n "$host_uname" ] || { echo "missing host uname provenance" >&2; exit 1; }
case "$boot_loader_target" in
/frx/store/*/boot/loader) : ;;
*) echo "unexpected /boot/loader target in closure: $boot_loader_target" >&2; exit 1 ;;
@@ -146,6 +164,17 @@ base_package_store_count=$base_package_store_count
base_package_stores=$base_package_stores
reference_count=$reference_count
generated_file_count=$generated_file_count
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
fruix_runtime_store_count=$fruix_runtime_store_count
fruix_runtime_stores=$fruix_runtime_stores
host_base_provenance_file=$host_base_provenance_file
store_layout_file=$store_layout_file
host_freebsd_version=$host_freebsd_version
host_uname=$host_uname
usr_src_git_revision=$usr_src_git_revision
usr_src_git_branch=$usr_src_git_branch
usr_src_newvers_sha256=$usr_src_newvers_sha256
rc_script=$rc_script
shepherd_config=$shepherd_config
activation_script=$activation_script

View File

@@ -49,6 +49,7 @@ trap cleanup_workdir EXIT INT TERM
action_env() {
sudo env \
HOME="$HOME" \
GUILE_AUTO_COMPILE=0 \
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}" \
@@ -71,6 +72,17 @@ disk_image=$(sed -n 's/^disk_image=//p' "$build_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$build_metadata")
disk_capacity_reported=$(sed -n 's/^disk_capacity=//p' "$build_metadata")
store_item_count=$(sed -n 's/^store_item_count=//p' "$build_metadata")
host_base_store_count=$(sed -n 's/^host_base_store_count=//p' "$build_metadata")
host_base_stores=$(sed -n 's/^host_base_stores=//p' "$build_metadata")
fruix_runtime_store_count=$(sed -n 's/^fruix_runtime_store_count=//p' "$build_metadata")
fruix_runtime_stores=$(sed -n 's/^fruix_runtime_stores=//p' "$build_metadata")
host_base_provenance_file=$(sed -n 's/^host_base_provenance_file=//p' "$build_metadata")
store_layout_file=$(sed -n 's/^store_layout_file=//p' "$build_metadata")
host_freebsd_version=$(sed -n 's/^host_freebsd_version=//p' "$build_metadata")
host_uname=$(sed -n 's/^host_uname=//p' "$build_metadata")
usr_src_git_revision=$(sed -n 's/^usr_src_git_revision=//p' "$build_metadata")
usr_src_git_branch=$(sed -n 's/^usr_src_git_branch=//p' "$build_metadata")
usr_src_newvers_sha256=$(sed -n 's/^usr_src_newvers_sha256=//p' "$build_metadata")
raw_sha256=$(sha256 -q "$disk_image")
image_size_bytes=$(stat -f '%z' "$disk_image")
closure_base=$(basename "$closure_path")
@@ -111,6 +123,10 @@ loader_conf_image=$mnt_root/frx/store/$closure_base/boot/loader.conf
rc_conf_image=$mnt_root/frx/store/$closure_base/etc/rc.conf
grep -F 'comconsole' "$loader_conf_image" >/dev/null || { echo "loader.conf is missing serial console config" >&2; exit 1; }
grep -F 'hostname="fruix-freebsd"' "$rc_conf_image" >/dev/null || { echo "rc.conf is missing hostname" >&2; exit 1; }
[ -f "$host_base_provenance_file" ] || { echo "missing host base provenance file: $host_base_provenance_file" >&2; exit 1; }
[ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; }
[ -n "$host_freebsd_version" ] || { echo "missing host freebsd version provenance" >&2; exit 1; }
[ -n "$host_uname" ] || { echo "missing host uname provenance" >&2; exit 1; }
cat >"$metadata_file" <<EOF
workdir=$workdir
@@ -124,6 +140,17 @@ raw_sha256=$raw_sha256
image_size_bytes=$image_size_bytes
disk_capacity=$disk_capacity_reported
store_item_count=$store_item_count
host_base_store_count=$host_base_store_count
host_base_stores=$host_base_stores
fruix_runtime_store_count=$fruix_runtime_store_count
fruix_runtime_stores=$fruix_runtime_stores
host_base_provenance_file=$host_base_provenance_file
store_layout_file=$store_layout_file
host_freebsd_version=$host_freebsd_version
host_uname=$host_uname
usr_src_git_revision=$usr_src_git_revision
usr_src_git_branch=$usr_src_git_branch
usr_src_newvers_sha256=$usr_src_newvers_sha256
gpart_log=$gpart_log
esp_fstype=$esp_fstype
root_fstype=$root_fstype

View File

@@ -148,6 +148,9 @@ run_current_system_target=$(ssh_guest 'readlink /run/current-system')
rc_conf_hostname=$(ssh_guest 'grep "^hostname=" /etc/rc.conf | cut -d"\"" -f2')
shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ')
shepherd_bootstrap_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd-bootstrap.out 2>/dev/null || true" | tr '\n' ' ')
shepherd_log_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd.log 2>/dev/null || true" | tr '\n' ' ')
guest_dmesg_tail=$(ssh_guest "dmesg | awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }'" | tr '\n' ' ')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
uname_output=$(ssh_guest 'uname -sr')
operator_home_listing=$(ssh_guest 'ls -d /home/operator')
@@ -194,6 +197,9 @@ run_current_system_target=$run_current_system_target
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
shepherd_bootstrap_tail=$shepherd_bootstrap_tail
shepherd_log_tail=$shepherd_log_tail
guest_dmesg_tail=$guest_dmesg_tail
uname_output=$uname_output
operator_home_listing=$operator_home_listing
compat_prefix_shims=$compat_prefix_shims