Compare commits
19 Commits
2517710282
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a1a6c3b81 | |||
| cb9e7332f4 | |||
| db4d5bdf4c | |||
| 0e8b30434f | |||
| f41a916f45 | |||
| 006ffee615 | |||
| 4614592a25 | |||
| a3dd5556ae | |||
| 9e9a0b59fc | |||
| 4975084baa | |||
| 9dae4e5c84 | |||
| b3b1ba2489 | |||
| e86f74af97 | |||
| 43c155bb9f | |||
| 604ad82f4f | |||
| 1970c5c181 | |||
| ebe064a652 | |||
| 56d9d6a54b | |||
| 1d0090752d |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/TODO.md
|
||||
384
docs/GUIX_DIFFERENCES.md
Normal file
384
docs/GUIX_DIFFERENCES.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# Fruix differences for Guix sysadmins
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
This document is aimed at operators who already know Guix well and want a quick map of where Fruix behaves similarly and where it intentionally differs.
|
||||
|
||||
The short version is:
|
||||
|
||||
- Fruix is strongly **Guix-inspired**
|
||||
- it tries to preserve the important semantic properties
|
||||
- but it does **not** copy Guix mechanically where FreeBSD or Fruix-specific concerns make a different representation clearer
|
||||
|
||||
## Big picture
|
||||
|
||||
Fruix keeps the core Guix ideas you would expect:
|
||||
|
||||
- declarative inputs
|
||||
- content-addressed store paths
|
||||
- immutable build outputs
|
||||
- rollback-friendly deployment identity
|
||||
- provenance tied to the deployed closure rather than mutable in-place state
|
||||
|
||||
But Fruix differs in at least four major ways today:
|
||||
|
||||
1. it targets **FreeBSD**, not GNU/Linux
|
||||
2. its system frontend is currently smaller:
|
||||
- `fruix system build|rootfs|image|install|installer|installer-iso`
|
||||
3. it treats **FreeBSD source provenance** as an explicit deployment concern
|
||||
4. its installed-system generation model is still earlier-stage than Guix's mature system-generation workflow
|
||||
|
||||
## Similarities to Guix
|
||||
|
||||
If you know Guix System, these Fruix properties should feel familiar.
|
||||
|
||||
### Immutable deployment identity
|
||||
|
||||
A deployed Fruix system is identified primarily by its closure path in `/frx/store`, not by mutable files under `/etc` or `/usr/local`.
|
||||
|
||||
### `/run/current-system`
|
||||
|
||||
Fruix keeps the Guix-like runtime convention:
|
||||
|
||||
- `/run/current-system`
|
||||
|
||||
That path remains the active runtime boundary used by activation and service wiring.
|
||||
|
||||
### Rollback-friendly semantics
|
||||
|
||||
Fruix avoids in-place mutation of an older deployed closure.
|
||||
|
||||
The validated rollback story now has two layers:
|
||||
|
||||
- declaration-level rollback by rebuilding/redeploying an earlier declaration
|
||||
- installed-system rollback between already-recorded generations on the target itself
|
||||
|
||||
That is Guix-like in spirit, although Fruix still exposes a smaller installed-system workflow than Guix's more mature `reconfigure` model.
|
||||
|
||||
### Generation-style metadata and roots
|
||||
|
||||
Fruix now records explicit installed-system generation state under:
|
||||
|
||||
- `/var/lib/fruix/system`
|
||||
|
||||
and explicit retention roots under:
|
||||
|
||||
- `/frx/var/fruix/gcroots`
|
||||
|
||||
This preserves the basic Guix idea that deployment state and reachability should be represented explicitly rather than inferred from whatever happens to be on disk.
|
||||
|
||||
## Important differences from Guix
|
||||
|
||||
## 1. Fruix does not mirror Guix's on-disk generation layout 1:1
|
||||
|
||||
This is intentional.
|
||||
|
||||
Guix heavily reuses its profile-generation model and represents a lot of meaning through symlink structure such as profile links and system generation links.
|
||||
|
||||
Fruix keeps the **semantics** but uses a more explicit metadata-oriented layout for installed systems.
|
||||
|
||||
Current Fruix layout starts as:
|
||||
|
||||
```text
|
||||
/var/lib/fruix/system/
|
||||
current -> generations/1
|
||||
current-generation
|
||||
generations/
|
||||
1/
|
||||
closure -> /frx/store/...-fruix-system-...
|
||||
metadata.scm
|
||||
provenance.scm
|
||||
install.scm
|
||||
```
|
||||
|
||||
After a validated installed-system switch, Fruix also records:
|
||||
|
||||
```text
|
||||
/var/lib/fruix/system/
|
||||
rollback -> generations/1
|
||||
rollback-generation
|
||||
generations/
|
||||
2/
|
||||
closure -> /frx/store/...-fruix-system-...
|
||||
metadata.scm
|
||||
provenance.scm
|
||||
install.scm
|
||||
```
|
||||
|
||||
Why Fruix does this:
|
||||
|
||||
- it makes deployment state easier to inspect directly
|
||||
- it gives FreeBSD-specific install and provenance details a clearer home
|
||||
- it keeps room for richer operator tooling later without losing the Guix properties
|
||||
|
||||
So the rule of thumb is:
|
||||
|
||||
- **same semantics as Guix where practical**
|
||||
- **not necessarily the same directory names or symlink vocabulary**
|
||||
|
||||
## 2. Fruix currently keeps `/run/current-system` simple
|
||||
|
||||
Even though Fruix now records explicit generation metadata under `/var/lib/fruix/system`, it still keeps:
|
||||
|
||||
- `/run/current-system -> /frx/store/...`
|
||||
|
||||
rather than making `/run/current-system` point through a generation directory first.
|
||||
|
||||
This was chosen to preserve compatibility with the already-validated activation and runtime model while adding explicit generation metadata separately.
|
||||
|
||||
## 3. Fruix treats source provenance more explicitly
|
||||
|
||||
Guix sysadmins are used to derivation/store provenance. Fruix adds an extra emphasis because its current FreeBSD story depends on explicit source selection and materialization.
|
||||
|
||||
Fruix routinely records:
|
||||
|
||||
- declared FreeBSD source object metadata
|
||||
- materialized source store paths
|
||||
- source materialization metadata files
|
||||
- closure-level store layout metadata
|
||||
- install metadata on the target system
|
||||
|
||||
This is more prominent in Fruix than most Guix system docs because FreeBSD base/source identity has been an active design concern for this project.
|
||||
|
||||
## 4. Fruix has installer-media workflows as first-class system actions
|
||||
|
||||
Guix has installation media and image workflows, but Fruix's current system frontend makes these especially explicit because they are still part of the active architectural bring-up story.
|
||||
|
||||
Current Fruix actions include:
|
||||
|
||||
- `fruix system install`
|
||||
- `fruix system installer`
|
||||
- `fruix system installer-iso`
|
||||
|
||||
That is a little more deployment-medium-oriented than the mental model many Guix users start with.
|
||||
|
||||
## 5. Device naming is more environment-sensitive than Guix users may expect
|
||||
|
||||
Because Fruix is on FreeBSD, the install target device naming is not the same as on Linux.
|
||||
|
||||
Validated examples:
|
||||
|
||||
- installer disk-image path under QEMU:
|
||||
- `/dev/vtbd1`
|
||||
- installer ISO path under QEMU:
|
||||
- `/dev/vtbd0`
|
||||
- installer ISO path under XCP-ng:
|
||||
- `/dev/ada0`
|
||||
|
||||
So compared with Guix-on-Linux intuition, Fruix operators should be more explicit about target-device selection during installation and installer-media validation.
|
||||
|
||||
## 6. Fruix now has a minimal installed-system generation command surface, but it is still smaller than Guix's
|
||||
|
||||
This remains the biggest operational gap, but it is no longer a complete gap.
|
||||
|
||||
Installed Fruix systems now provide a larger in-guest helper surface:
|
||||
|
||||
- `fruix system build`
|
||||
- `fruix system reconfigure`
|
||||
- `fruix system status`
|
||||
- `fruix system switch /frx/store/...-fruix-system-...`
|
||||
- `fruix system rollback`
|
||||
|
||||
What this gives you today:
|
||||
|
||||
- explicit current-generation tracking
|
||||
- explicit rollback-generation tracking
|
||||
- in-place switching between already-staged closures on the installed target
|
||||
- rollback without reinstalling the whole system image again
|
||||
- a validated in-node build path that can read the embedded current declaration inputs
|
||||
- a validated in-node `reconfigure` path that can build a candidate closure locally and stage it as the next generation
|
||||
|
||||
What it still does **not** give you yet compared with Guix:
|
||||
|
||||
- a mature end-to-end `upgrade` story for advancing declared inputs automatically
|
||||
- automatic closure transfer/fetch as part of `switch`
|
||||
- a higher-level `deploy` workflow across multiple machines or targets
|
||||
- the broader generation-management UX Guix operators expect
|
||||
|
||||
So if you come from Guix, assume that Fruix now has:
|
||||
|
||||
- strong closure/store semantics
|
||||
- explicit install artifacts
|
||||
- explicit generation metadata roots
|
||||
- a real installed-system build/reconfigure/switch/rollback surface
|
||||
- but not yet the fuller long-term node/deployment UX that Guix users may expect
|
||||
|
||||
## 7. Fruix keeps Guix-like store semantics, but not Guix/Nix hash-prefix machinery exactly
|
||||
|
||||
Fruix still uses immutable store paths under:
|
||||
|
||||
- `/frx/store`
|
||||
|
||||
and it still treats a store path as a deployment identity boundary.
|
||||
|
||||
But Fruix now intentionally differs from Guix/Nix in how the visible store-path prefix is constructed.
|
||||
|
||||
Current Fruix policy is:
|
||||
|
||||
- centralize store-path naming behind shared helpers
|
||||
- hash a small semantic identity record rather than copying Nix's historical path-hash formula exactly
|
||||
- include at least:
|
||||
- object kind
|
||||
- logical/display name
|
||||
- output name
|
||||
- payload or manifest identity
|
||||
- hash-scheme version marker
|
||||
- truncate the visible SHA-256 prefix to **160 bits**
|
||||
- render that visible prefix as **40 hex characters**
|
||||
|
||||
Why Fruix does this instead of copying Guix/Nix exactly:
|
||||
|
||||
- the main goal was shorter store prefixes, not Nix compatibility for its own sake
|
||||
- distinct outputs should have distinct identities because `out`, `lib`, `debug`, and `doc` are semantically different artifacts
|
||||
- Fruix wanted one central policy point that can be swapped later without touching every materializer again
|
||||
- Fruix did **not** want to inherit Nix's legacy details unless they provide clear value here
|
||||
- custom base32 alphabet and bit ordering
|
||||
- compressed/XOR-folded path hashes
|
||||
- exact historical `output:out` / `source` path-hash conventions
|
||||
|
||||
So compared with Guix:
|
||||
|
||||
- the important semantic property is the same:
|
||||
- different store objects should get different immutable identities
|
||||
- the exact printable prefix algorithm is intentionally simpler in Fruix today
|
||||
|
||||
For Guix-familiar operators, the practical takeaway is:
|
||||
|
||||
- still think of `/frx/store/...` paths as immutable deployment identities
|
||||
- do **not** assume Fruix store prefixes are byte-for-byte comparable to Guix/Nix ones
|
||||
- expect Fruix to prefer a simpler, centralized naming policy unless exact Guix/Nix behavior becomes necessary later
|
||||
|
||||
## 8. Fruix can expose separate in-system development and build overlays
|
||||
|
||||
For the validated Phase 20 path, Fruix now distinguishes three layers instead of two:
|
||||
|
||||
- runtime profile
|
||||
- development profile
|
||||
- build profile
|
||||
|
||||
On those systems, Fruix exposes:
|
||||
|
||||
- development:
|
||||
- `/run/current-system/development-profile`
|
||||
- `/run/current-development`
|
||||
- `/usr/local/bin/fruix-development-environment`
|
||||
- build:
|
||||
- `/run/current-system/build-profile`
|
||||
- `/run/current-build`
|
||||
- `/usr/local/bin/fruix-build-environment`
|
||||
- canonical base-build compatibility links:
|
||||
- `/usr/include -> /run/current-system/build-profile/usr/include`
|
||||
- `/usr/share/mk -> /run/current-system/build-profile/usr/share/mk`
|
||||
|
||||
The intent is:
|
||||
|
||||
- keep the main runtime profile lean
|
||||
- keep the development helper interactive and convenient
|
||||
- keep the build helper narrower, more sanitized, and closer to the actual `buildworld` / `buildkernel` contract
|
||||
- avoid treating a development-heavy or build-heavy image as the default runtime shape
|
||||
|
||||
Compared with Guix, this is conceptually similar to keeping development-oriented and build-oriented state separate from the main runtime identity, but Fruix currently expresses it as system-attached overlays rather than through Guix's broader profile/tooling model.
|
||||
|
||||
Fruix still has the guest self-hosted native-build prototype helper at:
|
||||
|
||||
- `/usr/local/bin/fruix-self-hosted-native-build`
|
||||
|
||||
But that helper now explicitly uses the build helper contract instead of trying to reuse the full interactive development shell. The practical Fruix takeaway is:
|
||||
|
||||
- the development overlay makes native base work possible inside the system
|
||||
- the build overlay gives Fruix a stricter, more reproducible contract for real base builds
|
||||
|
||||
Fruix now also makes an explicit distinction between:
|
||||
|
||||
- mutable native-build staging/results under:
|
||||
- `/var/lib/fruix/native-builds`
|
||||
- immutable promoted native-build identities under:
|
||||
- `/frx/store`
|
||||
|
||||
So a guest self-hosted run can stage a result tree locally, while the host can later promote that result into first-class Fruix store objects such as:
|
||||
|
||||
- `/frx/store/...-fruix-native-world-...`
|
||||
- `/frx/store/...-fruix-native-kernel-...`
|
||||
- `/frx/store/...-fruix-native-headers-...`
|
||||
- `/frx/store/...-fruix-native-bootloader-...`
|
||||
- `/frx/store/...-fruix-native-build-result-...`
|
||||
|
||||
Compared with Guix, this is a more explicit split between:
|
||||
|
||||
- a mutable result/staging area for native build execution
|
||||
- and the immutable store identities that Fruix treats as the real promoted result
|
||||
|
||||
Fruix also now models native-build placement more explicitly as an executor choice.
|
||||
|
||||
Current executor kinds are:
|
||||
|
||||
- `host`
|
||||
- `ssh-guest`
|
||||
- `self-hosted`
|
||||
|
||||
So instead of treating:
|
||||
|
||||
- host-driven builds
|
||||
- host-initiated guest builds
|
||||
- guest self-hosted builds
|
||||
|
||||
as three unrelated architectural forks, Fruix is moving toward one native-build result model with different executor policies attached to it.
|
||||
|
||||
Fruix also now lets system declarations consume a promoted native-build result bundle directly.
|
||||
|
||||
That means Fruix can now distinguish more explicitly between:
|
||||
|
||||
- a mutable build/result root used during execution
|
||||
- an immutable promoted native-build identity in `/frx/store`
|
||||
- and a later system declaration that points at that promoted identity as an input
|
||||
|
||||
Compared with Guix, this is still Guix-like in the important sense that the deployed system is identified by immutable store objects, but Fruix is making the FreeBSD native-base result set itself a more explicit first-class declaration input than it was before.
|
||||
|
||||
## Where Fruix is intentionally trying to improve on Guix's representation
|
||||
|
||||
Fruix is not trying to improve on Guix's core semantics. Guix already got those right.
|
||||
|
||||
Where Fruix is intentionally experimenting is mostly the **representation layer**:
|
||||
|
||||
- make generation state more legible to operators
|
||||
- make provenance more visible without needing to reconstruct it mentally from symlink layout alone
|
||||
- separate:
|
||||
- runtime entry point (`/run/current-system`)
|
||||
- installed deployment state (`/var/lib/fruix/system`)
|
||||
- retention roots (`/frx/var/fruix/gcroots`)
|
||||
|
||||
That is why Fruix currently prefers a small per-generation metadata directory instead of only a bare generation link.
|
||||
|
||||
## Practical operator advice for Guix users
|
||||
|
||||
If you are already comfortable with Guix, the safest Fruix mental model today is:
|
||||
|
||||
1. think in terms of immutable closures and declarations first
|
||||
2. use `fruix system build` as the canonical starting point
|
||||
3. treat image/install/installer/installer-iso as deployment materializers built from the same declaration
|
||||
4. identify a deployment by:
|
||||
- closure path
|
||||
- source provenance metadata
|
||||
- install metadata
|
||||
5. think of rollback in two layers:
|
||||
- if the target already has the desired closure staged locally:
|
||||
- use `fruix system rollback`
|
||||
- otherwise:
|
||||
- redeploy the earlier declaration again
|
||||
|
||||
## Status summary
|
||||
|
||||
Today Fruix is closest to Guix in:
|
||||
|
||||
- store and closure semantics
|
||||
- declarative deployment identity
|
||||
- rollback-friendly immutability
|
||||
- `/run/current-system` runtime model
|
||||
|
||||
It differs most from Guix in:
|
||||
|
||||
- FreeBSD platform details
|
||||
- source-provenance emphasis
|
||||
- installer-medium-oriented workflows
|
||||
- generation-layout representation
|
||||
- and an installed-system generation command surface that now exists, but is still much smaller than Guix's
|
||||
4285
docs/PROGRESS.md
4285
docs/PROGRESS.md
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ Completed milestones include:
|
||||
- **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.
|
||||
- **Minimal installer environment**: Fruix can now also build and boot a dedicated installer image that carries a selected target closure, installs it onto a second disk from inside the guest, and leaves the installed target bootable.
|
||||
- **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
|
||||
@@ -46,7 +47,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.
|
||||
- **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.
|
||||
- **Installer media beyond disk images**: Fruix now has both a host-driven install path and a bootable installer environment, but it still lacks a UEFI installer ISO and a more polished operator-facing installation medium.
|
||||
- **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.
|
||||
@@ -54,4 +55,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, 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?”
|
||||
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, install a declarative system onto a target image through a repeatable Fruix workflow, and boot a dedicated Fruix-managed installer environment that performs that installation from inside the guest. The biggest remaining work is no longer “can this build/install at all?” but “how does this become a fuller installer/deployment/generation/installer-media story?”
|
||||
|
||||
191
docs/reports/phase18-installer-environment-freebsd.md
Normal file
191
docs/reports/phase18-installer-environment-freebsd.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Phase 18.2: minimal Fruix-managed installer environment on FreeBSD
|
||||
|
||||
Date: 2026-04-04
|
||||
|
||||
## Goal
|
||||
|
||||
Phase 18.2 builds on the Phase 18.1 host-driven install primitive.
|
||||
|
||||
The goal here is not a polished live installer. The goal is a small Fruix-managed environment that can:
|
||||
|
||||
- boot as its own Fruix system,
|
||||
- carry a selected target Fruix system closure and rootfs payload,
|
||||
- install that target system onto a second disk from inside the booted environment,
|
||||
- and leave the installed target bootable.
|
||||
|
||||
## Implementation
|
||||
|
||||
### New installer-environment API
|
||||
|
||||
Added in `modules/fruix/system/freebsd.scm`:
|
||||
|
||||
- `installer-operating-system`
|
||||
- `operating-system-installer-image-spec`
|
||||
- `materialize-installer-image`
|
||||
|
||||
The installer environment is derived from the selected target operating system, but with installer-specific behavior:
|
||||
|
||||
- host name defaults to:
|
||||
- `<target-host-name>-installer`
|
||||
- init mode is kept on the currently most stable installer path:
|
||||
- `freebsd-init+rc.d-shepherd`
|
||||
- the installer image root label is distinct:
|
||||
- `fruix-installer-root`
|
||||
- `sshd` is enabled for operator/debug access
|
||||
- installer accounts needed for SSH/DHCP are ensured if absent:
|
||||
- `sshd`
|
||||
- `_dhcp`
|
||||
|
||||
### Bootable installer image contents
|
||||
|
||||
`materialize-installer-image` now produces a bootable image that contains:
|
||||
|
||||
- the installer system closure and its runtime store closure
|
||||
- the selected target system closure
|
||||
- the selected target system's referenced store items
|
||||
- a prebuilt target rootfs tree staged under:
|
||||
- `/var/lib/fruix/installer/target-rootfs`
|
||||
- installer plan/state files under:
|
||||
- `/var/lib/fruix/installer`
|
||||
- installer helper scripts:
|
||||
- `/usr/local/libexec/fruix-installer-run`
|
||||
- `/usr/local/etc/rc.d/fruix-installer`
|
||||
|
||||
The booted installer environment runs a background rc.d job that:
|
||||
|
||||
- partitions the selected target disk
|
||||
- creates EFI + UFS filesystems
|
||||
- copies the staged target rootfs onto the target
|
||||
- copies only the target system's required store items into the target `/frx/store`
|
||||
- installs the target's `loader.efi`
|
||||
- writes `/var/lib/fruix/install.scm` on the target
|
||||
- records installer state in:
|
||||
- `/var/lib/fruix/installer/state`
|
||||
- logs to:
|
||||
- `/var/log/fruix-installer.log`
|
||||
|
||||
### New CLI action
|
||||
|
||||
Added in `scripts/fruix.scm`:
|
||||
|
||||
- `fruix system installer`
|
||||
|
||||
Added option:
|
||||
|
||||
- `--install-target-device DEVICE`
|
||||
|
||||
This action materializes a bootable installer image in `/frx/store` and emits metadata for:
|
||||
|
||||
- installer image paths
|
||||
- installer closure path
|
||||
- target closure path
|
||||
- target install device
|
||||
- installer state/log paths
|
||||
- declared/materialized FreeBSD source metadata
|
||||
- target/native/runtime store metadata
|
||||
|
||||
### FreeBSD virtio target-device detail
|
||||
|
||||
A practical detail surfaced during validation:
|
||||
|
||||
- the correct FreeBSD virtio block device node for the second QEMU disk is:
|
||||
- `/dev/vtbd1`
|
||||
|
||||
The earlier Linux-flavored guess:
|
||||
|
||||
- `/dev/vtblk1`
|
||||
|
||||
was wrong for the actual FreeBSD device node namespace in this environment.
|
||||
|
||||
The installer defaults were updated accordingly.
|
||||
|
||||
### Small image-builder correctness fix
|
||||
|
||||
While doing this work I also fixed `materialize-bhyve-image` so its generated UFS filesystem label respects the requested:
|
||||
|
||||
- `root-partition-label`
|
||||
|
||||
instead of always hardcoding:
|
||||
|
||||
- `fruix-root`
|
||||
|
||||
This matters for the installer image because it needs a distinct root label while the target disk still uses the normal target label.
|
||||
|
||||
## Validation
|
||||
|
||||
Added validation artifacts:
|
||||
|
||||
- `tests/system/phase18-installer-target-operating-system.scm.in`
|
||||
- `tests/system/run-phase18-installer-environment.sh`
|
||||
|
||||
Passing validations:
|
||||
|
||||
- `PASS phase18-installer-environment`
|
||||
- regression re-check:
|
||||
- `PASS phase18-system-install`
|
||||
- regression re-check:
|
||||
- `PASS phase17-source-revisions-qemu`
|
||||
|
||||
Validated installer-environment result:
|
||||
|
||||
```text
|
||||
installer_image_store_path=/frx/store/fb038dbf5dac2ad1bb767a264d3a268915f489b936dc5dd32425645102d3da48-fruix-installer-image-fruix-freebsd-installer
|
||||
installer_disk_image=/frx/store/fb038dbf5dac2ad1bb767a264d3a268915f489b936dc5dd32425645102d3da48-fruix-installer-image-fruix-freebsd-installer/disk.img
|
||||
installer_disk_capacity=16g
|
||||
installer_root_size=14g
|
||||
target_disk_capacity=12g
|
||||
install_target_device=/dev/vtbd1
|
||||
installer_closure_path=/frx/store/ea821f20b579684877fdc86a2a1e80485cf2b12d9d32f74f42e368d738c2ad4d-fruix-system-fruix-freebsd-installer
|
||||
target_closure_path=/frx/store/7ee225db532b6973e385f8507d2d61aec3cd3aeb0864f983c2ae4b6e149ef3b0-fruix-system-fruix-freebsd
|
||||
freebsd_source_kind=git
|
||||
freebsd_source_ref=stable/15
|
||||
freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
|
||||
materialized_source_store=/frx/store/7563df2714ae7fa9bd40b83c74512ffe2cb2ad91b297915591b55c76edbb2fcb-freebsd-source-stable15-installer-target-source
|
||||
installer_state=done
|
||||
installer_sshd_status=running
|
||||
target_esp_fstype=msdosfs
|
||||
target_root_fstype=ufs
|
||||
target_shepherd_status=running
|
||||
target_sshd_status=running
|
||||
installer_environment_boot=ok
|
||||
installer_environment_install=ok
|
||||
installed_target_boot=ok
|
||||
```
|
||||
|
||||
The harness verified all of the following:
|
||||
|
||||
1. `fruix system installer` produces a bootable installer image in `/frx/store`
|
||||
2. validation boots a workdir copy of that installer disk image so the store artifact itself is not mutated during the boot/install run
|
||||
3. the installer environment boots successfully under QEMU/UEFI/TCG
|
||||
4. the installer environment becomes reachable over SSH
|
||||
5. `/run/current-system` inside the installer environment points at the installer closure
|
||||
6. the installer rc.d job reaches:
|
||||
- `state=done`
|
||||
7. the installer log records:
|
||||
- `fruix-installer:done`
|
||||
8. the target raw disk is transformed into a valid GPT-installed Fruix target with:
|
||||
- EFI filesystem: `msdosfs`
|
||||
- root filesystem: `ufs`
|
||||
- `EFI/BOOT/BOOTX64.EFI` present
|
||||
- `/var/lib/fruix/install.scm` present
|
||||
9. the installed target then boots successfully as its own Fruix system under QEMU/UEFI/TCG
|
||||
10. after target boot:
|
||||
- `/run/current-system` points at the target closure
|
||||
- `/usr/local/etc/rc.d/fruix-shepherd onestatus` reports running
|
||||
- `sshd` is running
|
||||
- activation completed successfully
|
||||
|
||||
## Result
|
||||
|
||||
Phase 18.2 is complete.
|
||||
|
||||
Fruix now has a real installer substrate on FreeBSD:
|
||||
|
||||
- a bootable Fruix-managed installer image
|
||||
- a target closure bundled inside that installer environment
|
||||
- in-guest non-interactive installation onto a second disk
|
||||
- validated boot of the installed result
|
||||
|
||||
The next step is Phase 18.3:
|
||||
|
||||
- produce a bootable installer ISO for UEFI systems, rather than only a disk-image-style installer environment.
|
||||
277
docs/reports/phase18-installer-iso-freebsd.md
Normal file
277
docs/reports/phase18-installer-iso-freebsd.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Phase 18.3: bootable Fruix installer ISO on FreeBSD
|
||||
|
||||
Date: 2026-04-04
|
||||
|
||||
## Goal
|
||||
|
||||
Phase 18.3 extends the Phase 18.2 installer-environment work from a disk-image-style installer into a UEFI-bootable ISO artifact.
|
||||
|
||||
The intended first ISO is deliberately narrow:
|
||||
|
||||
- UEFI only
|
||||
- serial-console-friendly
|
||||
- non-interactive install flow reused from Phase 18.1/18.2
|
||||
- target disk installation still performed by the same Fruix-managed in-guest installer logic
|
||||
|
||||
## Implementation
|
||||
|
||||
### New API
|
||||
|
||||
Added in `modules/fruix/system/freebsd.scm`:
|
||||
|
||||
- `operating-system-installer-iso-spec`
|
||||
- `materialize-installer-iso`
|
||||
|
||||
The system module split done immediately before this phase was also exercised during this work.
|
||||
|
||||
### New CLI action
|
||||
|
||||
Added in `scripts/fruix.scm`:
|
||||
|
||||
- `fruix system installer-iso`
|
||||
|
||||
This action emits metadata for:
|
||||
|
||||
- ISO store path
|
||||
- ISO image path
|
||||
- EFI boot image path
|
||||
- installer root image path
|
||||
- installer and target closure paths
|
||||
- installer state/log paths
|
||||
- declared/materialized FreeBSD source metadata
|
||||
- store closure counts
|
||||
|
||||
### ISO boot model
|
||||
|
||||
The ISO does not try to run the Fruix installer directly from a read-only cd9660 root.
|
||||
|
||||
Instead it uses a small UEFI El Torito boot image plus an in-memory installer root image:
|
||||
|
||||
1. a small FAT EFI boot image contains `EFI/BOOT/BOOTX64.EFI`
|
||||
2. the ISO root contains real boot assets under `/boot`
|
||||
3. the ISO root also contains `/boot/root.img`
|
||||
4. `loader.conf` on the ISO is augmented with:
|
||||
- `mdroot_load="YES"`
|
||||
- `mdroot_type="mfs_root"`
|
||||
- `mdroot_name="/boot/root.img"`
|
||||
- `vfs.root.mountfrom="ufs:/dev/md0"`
|
||||
- `vfs.root.mountfrom.options="rw"`
|
||||
|
||||
A practical loader detail surfaced during validation:
|
||||
|
||||
- setting `rootdev` or `currdev` to `md0:` in the ISO loader path is wrong for this loader configuration and caused an early EFI-loader crash before kernel handoff
|
||||
- the reliable ISO path is to let loader keep its current device on the CD media, preload `/boot/root.img`, and pass only `vfs.root.mountfrom=ufs:/dev/md0`
|
||||
|
||||
This preserves the existing Fruix installer environment semantics while avoiding the need to make the whole installer operate directly from a read-only ISO root.
|
||||
|
||||
### Installer root image contents
|
||||
|
||||
`materialize-installer-iso` stages the same installer payload model already validated in Phase 18.2:
|
||||
|
||||
- installer closure
|
||||
- target closure
|
||||
- target runtime store closure needed for installation/boot
|
||||
- staged target rootfs under `/var/lib/fruix/installer/target-rootfs`
|
||||
- installer plan and state files under `/var/lib/fruix/installer`
|
||||
- installer helper scripts:
|
||||
- `/usr/local/libexec/fruix-installer-run`
|
||||
- `/usr/local/etc/rc.d/fruix-installer`
|
||||
|
||||
The ISO root image is then built as a UFS image and embedded as `/boot/root.img`.
|
||||
|
||||
### Split-regression fixes found during this work
|
||||
|
||||
While exercising the refactored split modules, two issues surfaced and were fixed:
|
||||
|
||||
1. `string-hash` name-clash warnings
|
||||
- the old helper name collided with Guile/SRFI bindings
|
||||
- it was renamed to `sha256-string`
|
||||
2. missing `prefix-materializer-version`
|
||||
- this constant was accidentally omitted when `modules/fruix/system/freebsd.scm` was split
|
||||
- the missing definition was restored in `modules/fruix/system/freebsd/build.scm`
|
||||
|
||||
## Validation
|
||||
|
||||
### Completed smoke validation
|
||||
|
||||
A host-side smoke build was completed successfully for the new ISO builder using a host-staged operating-system definition:
|
||||
|
||||
- command pattern:
|
||||
- `fruix system installer-iso ...`
|
||||
- result:
|
||||
- successful ISO materialization in a temporary store
|
||||
- artifact checks performed:
|
||||
- `etdump` reports an EFI El Torito boot entry
|
||||
- the ISO contains:
|
||||
- `boot/kernel/kernel`
|
||||
- `boot/kernel/linker.hints`
|
||||
- `boot/loader.conf`
|
||||
- `boot/loader.efi`
|
||||
- `boot/root.img`
|
||||
- `boot/loader.conf` inside the ISO contains the expected `mdroot_*` and `vfs.root.mountfrom` entries
|
||||
|
||||
Example smoke-build metadata:
|
||||
|
||||
```text
|
||||
action=installer-iso
|
||||
iso_volume_label=FRUIX_INSTALLER
|
||||
iso_store_path=/tmp/...-fruix-installer-iso-fruix-freebsd-installer
|
||||
iso_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/installer.iso
|
||||
boot_efi_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/efiboot.img
|
||||
root_image=/tmp/...-fruix-installer-iso-fruix-freebsd-installer/root.img
|
||||
installer_closure_path=/tmp/...-fruix-system-fruix-freebsd-installer
|
||||
target_closure_path=/tmp/...-fruix-system-fruix-freebsd
|
||||
```
|
||||
|
||||
### End-to-end harness validation
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase18-installer-iso.sh`
|
||||
|
||||
This harness validates the full Phase 18.3 flow:
|
||||
|
||||
1. build installer ISO
|
||||
2. boot it under QEMU/UEFI/TCG
|
||||
3. install onto a target disk from inside the booted ISO environment
|
||||
4. boot the installed target
|
||||
|
||||
Passing validation:
|
||||
|
||||
- `PASS phase18-installer-iso`
|
||||
|
||||
Validated result summary:
|
||||
|
||||
```text
|
||||
installer_iso_store_path=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer
|
||||
installer_iso_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/installer.iso
|
||||
installer_boot_efi_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/efiboot.img
|
||||
installer_root_image=/frx/store/...-fruix-installer-iso-fruix-freebsd-installer/root.img
|
||||
install_target_device=/dev/vtbd0
|
||||
freebsd_source_kind=git
|
||||
freebsd_source_ref=stable/15
|
||||
freebsd_source_commit=332708a606f6bf0841c1d4a74c0d067f5640fe89
|
||||
materialized_source_store=/frx/store/459499e0eb29f4c73ad455060dd2502d21fb56f205c0a676831cf723b3a0c378-freebsd-source-stable15-installer-iso-target-source
|
||||
installer_state=done
|
||||
installer_sshd_status=running
|
||||
target_esp_fstype=msdosfs
|
||||
target_root_fstype=ufs
|
||||
target_shepherd_status=running
|
||||
target_sshd_status=running
|
||||
installer_iso_boot=ok
|
||||
installer_iso_install=ok
|
||||
installed_target_boot=ok
|
||||
```
|
||||
|
||||
Notable QEMU-specific ISO validation detail:
|
||||
|
||||
- unlike the disk-image-style installer environment from Phase 18.2, the ISO boots from `cd0`, so the target virtio disk appears as:
|
||||
- `/dev/vtbd0`
|
||||
- the earlier installer-environment default:
|
||||
- `/dev/vtbd1`
|
||||
remains correct for the disk-image installer, but not for the ISO path
|
||||
|
||||
The harness verified all of the following:
|
||||
|
||||
1. `fruix system installer-iso` produces a bootable ISO artifact in `/frx/store`
|
||||
2. the ISO boots successfully under QEMU/UEFI/TCG
|
||||
3. the booted installer ISO environment becomes reachable over SSH
|
||||
4. `/run/current-system` inside the installer ISO points at the installer closure
|
||||
5. the installer rc.d job reaches:
|
||||
- `state=done`
|
||||
6. the installer log records:
|
||||
- `fruix-installer:done`
|
||||
7. the installed target disk contains:
|
||||
- GPT partitioning
|
||||
- EFI filesystem: `msdosfs`
|
||||
- root filesystem: `ufs`
|
||||
- `EFI/BOOT/BOOTX64.EFI`
|
||||
- `/var/lib/fruix/install.scm`
|
||||
8. the installed target then boots successfully as its own Fruix system under QEMU/UEFI/TCG
|
||||
9. after target boot:
|
||||
- `/run/current-system` points at the target closure
|
||||
- shepherd is running
|
||||
- `sshd` is running
|
||||
- activation completed successfully
|
||||
|
||||
### Real XCP-ng validation
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase18-installer-iso-xcpng.sh`
|
||||
|
||||
This harness validates the same installer-iso workflow on the approved real XCP-ng path:
|
||||
|
||||
- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- ISO SR: `537a6219-8452-7cb5-8d56-5eed6910c7a2`
|
||||
- target VDIs:
|
||||
- `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
- `7061d761-3639-4bec-87f7-2ba1af924eaa`
|
||||
|
||||
Because the current `xo-cli disk.import @=/path/to.iso` path returned an HTTP 500 error in this environment, the harness imports the ISO into the ISO SR via a temporary local HTTP URL, then inserts the resulting ISO VDI into the VM's CD drive.
|
||||
|
||||
Passing validation:
|
||||
|
||||
- `PASS phase18-installer-iso-xcpng`
|
||||
|
||||
Validated result summary:
|
||||
|
||||
```text
|
||||
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
|
||||
iso_id=<temporary-imported-iso-vdi>
|
||||
guest_ip=192.168.213.62
|
||||
installer_state=done
|
||||
installer_target_device=/dev/ada0
|
||||
kern_disks=cd0 ada1 ada0
|
||||
installer_run_current_system=/frx/store/16969e825dbb65b5c27180030d4a7d98821893460fb3dccdc863ff6156ed61e0-fruix-system-fruix-freebsd-installer
|
||||
installer_sshd_status=running
|
||||
target_run_current_system=/frx/store/a98d3af6a1afbc4a927d47cea6458d5a70747b051ed994e5d9ff1ae79c4f2b42-fruix-system-fruix-freebsd
|
||||
target_sshd_status=running
|
||||
target_shepherd_status=running
|
||||
```
|
||||
|
||||
Important XCP-ng-specific details:
|
||||
|
||||
- the installer ISO still boots from:
|
||||
- `cd0`
|
||||
- on this Xen HVM path, the primary target disk is exposed through Xen block front as `xbd0` and appears to FreeBSD as:
|
||||
- `/dev/ada0`
|
||||
- therefore the XCP-ng installer-iso path must target:
|
||||
- `/dev/ada0`
|
||||
rather than QEMU's:
|
||||
- `/dev/vtbd0`
|
||||
- the visible EFI console can appear to stop at:
|
||||
- `console vidconsole is unavailable`
|
||||
but boot still continues and the installer becomes reachable over SSH; that message was not the actual failure mode on XCP-ng
|
||||
|
||||
The harness verified all of the following on the real VM path:
|
||||
|
||||
1. `fruix system installer-iso` builds a bootable ISO with `--install-target-device /dev/ada0`
|
||||
2. the ISO can be imported into the operator-approved ISO SR and attached to the approved VM
|
||||
3. the VM boots the Fruix installer ISO successfully under UEFI
|
||||
4. the installer environment becomes reachable over SSH
|
||||
5. inside the installer guest:
|
||||
- `kern.disks` includes `cd0` and `ada0`
|
||||
- `/run/current-system` points at the installer closure
|
||||
- the installer reaches `state=done`
|
||||
6. the installed target on `ada0` is partitioned and formatted correctly
|
||||
7. after ejecting the ISO and rebooting, the installed target boots successfully on the same XCP-ng VM
|
||||
8. after target boot:
|
||||
- `/run/current-system` points at the target closure
|
||||
- shepherd is running
|
||||
- `sshd` is running
|
||||
- activation completed successfully
|
||||
- `/var/lib/fruix/install.scm` still records the materialized source store provenance
|
||||
|
||||
## Result
|
||||
|
||||
Phase 18.3 is complete.
|
||||
|
||||
Fruix now has a validated bootable UEFI installer ISO on FreeBSD that can:
|
||||
|
||||
- boot into a Fruix-managed installer environment from ISO media
|
||||
- perform the non-interactive installation flow onto a target disk
|
||||
- boot the installed target successfully
|
||||
- and do so on both:
|
||||
- local `QEMU/UEFI/TCG`
|
||||
- the approved real `XCP-ng` VM path
|
||||
143
docs/reports/phase19-deployment-workflow-freebsd.md
Normal file
143
docs/reports/phase19-deployment-workflow-freebsd.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Phase 19.1: canonical Fruix deployment workflow on FreeBSD
|
||||
|
||||
Date: 2026-04-04
|
||||
|
||||
## Goal
|
||||
|
||||
Phase 19.1 is about turning Fruix's already-validated closure/image/install behavior into a clear operator-facing deployment story.
|
||||
|
||||
The verification target here is documentation clarity rather than a new low-level boot primitive.
|
||||
|
||||
The repo needed a single coherent explanation of how Fruix expects operators to:
|
||||
|
||||
- build a system closure
|
||||
- materialize a rootfs or image
|
||||
- install directly to an image or block device
|
||||
- use the installer image and installer ISO paths
|
||||
- roll forward to a candidate declaration
|
||||
- roll back to an earlier declaration
|
||||
|
||||
## Result
|
||||
|
||||
Phase 19.1 is complete.
|
||||
|
||||
The repository now documents a first-class Fruix deployment workflow in:
|
||||
|
||||
- `docs/system-deployment-workflow.md`
|
||||
|
||||
That document defines the current canonical command surface and explains how the already-existing validated paths fit together operationally.
|
||||
|
||||
## What was documented
|
||||
|
||||
### Canonical frontend
|
||||
|
||||
The documented user-facing frontend is now explicitly:
|
||||
|
||||
- `./bin/fruix system ...`
|
||||
|
||||
This includes the currently supported deployment-oriented actions:
|
||||
|
||||
- `build`
|
||||
- `rootfs`
|
||||
- `image`
|
||||
- `install`
|
||||
- `installer`
|
||||
- `installer-iso`
|
||||
|
||||
### Canonical deployment model
|
||||
|
||||
The workflow document now defines Fruix's current deployment model as:
|
||||
|
||||
1. declare a system in Scheme
|
||||
2. build the system closure in `/frx/store`
|
||||
3. materialize the artifact appropriate to the target environment
|
||||
4. boot or install that artifact
|
||||
5. treat the resulting closure path and emitted provenance metadata as the deployment identity
|
||||
|
||||
### Roll-forward and rollback semantics
|
||||
|
||||
The document makes explicit an important current design point:
|
||||
|
||||
- Fruix rollback is already real at the declaration/closure/deployment layer
|
||||
- but it is not yet a first-class installed-system generation switch operation
|
||||
|
||||
So the documented rollback workflow today is:
|
||||
|
||||
- retain the earlier declaration
|
||||
- rebuild or rematerialize it
|
||||
- redeploy or reboot that earlier closure again
|
||||
|
||||
That matches what Fruix has already validated in earlier phases.
|
||||
|
||||
### Platform-specific installer target-device detail
|
||||
|
||||
The workflow document also records the now-important target-device distinctions between validated environments:
|
||||
|
||||
- installer disk-image path under QEMU:
|
||||
- `/dev/vtbd1`
|
||||
- installer ISO path under QEMU:
|
||||
- `/dev/vtbd0`
|
||||
- installer ISO path under XCP-ng:
|
||||
- `/dev/ada0`
|
||||
|
||||
That makes the deployment story less harness-specific and more operator-explicit.
|
||||
|
||||
## Why this satisfies Phase 19.1
|
||||
|
||||
Before this phase, Fruix already had the machinery for:
|
||||
|
||||
- building declarative system closures
|
||||
- generating bootable images
|
||||
- performing direct non-interactive installation
|
||||
- booting a Fruix installer environment
|
||||
- booting and installing from a Fruix installer ISO
|
||||
- rollback-friendly redeploy of earlier declarations
|
||||
|
||||
What was missing was a repo-level explanation that unified those into a single operator workflow.
|
||||
|
||||
The new document closes that gap by connecting:
|
||||
|
||||
- Phase 10 command-surface work
|
||||
- Phase 15 redeploy/rollback validation
|
||||
- Phase 18 install and installer-media validation
|
||||
- and the recent QEMU + XCP-ng installer ISO validation
|
||||
|
||||
## Current boundaries now made explicit
|
||||
|
||||
The documentation intentionally records what Fruix has **not** solved yet:
|
||||
|
||||
- installed-system generation links
|
||||
- explicit rollback targets and generation metadata
|
||||
- a first-class `switch` or `reconfigure` command
|
||||
- installed-system rollback as an in-place operator workflow
|
||||
- GC-root management for installed systems
|
||||
|
||||
Those are left for later Phase 19 steps rather than being blurred into the current deployment story.
|
||||
|
||||
## References to existing validation
|
||||
|
||||
The documented workflow rests on already-passing validation paths, including:
|
||||
|
||||
- `PASS phase18-system-install`
|
||||
- `PASS phase18-installer-environment`
|
||||
- `PASS phase18-installer-iso`
|
||||
- `PASS phase18-installer-iso-xcpng`
|
||||
- `PASS phase15-base-rollback-qemu`
|
||||
- `PASS phase15-base-rollback-xcpng`
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phase 19.1 is now complete.
|
||||
|
||||
Fruix has a documented canonical deployment workflow for FreeBSD covering:
|
||||
|
||||
- build
|
||||
- image generation
|
||||
- direct install
|
||||
- installer-media install
|
||||
- roll-forward
|
||||
- rollback by redeploying an earlier declaration
|
||||
|
||||
The next step is Phase 19.2:
|
||||
|
||||
- model installed-system generations, rollback targets, and deployment roots more explicitly.
|
||||
192
docs/reports/phase19-generation-layout-freebsd.md
Normal file
192
docs/reports/phase19-generation-layout-freebsd.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Phase 19.2: explicit installed-system generation layout on FreeBSD
|
||||
|
||||
Date: 2026-04-04
|
||||
|
||||
## Goal
|
||||
|
||||
Phase 19.2 is about making Fruix's installed-system generation model more explicit.
|
||||
|
||||
The target here is not yet a full Guix-equivalent in-place `switch-generation` workflow.
|
||||
|
||||
The immediate goal is to stop relying mainly on harness knowledge and implicit symlink expectations by recording installed deployment state more explicitly on disk.
|
||||
|
||||
## Decision
|
||||
|
||||
Fruix now follows this design direction:
|
||||
|
||||
- keep Guix-like **semantics**
|
||||
- do not mirror Guix's installed-system/profile layout **mechanically 1:1**
|
||||
|
||||
What Fruix preserves from Guix:
|
||||
|
||||
- immutable closure identity
|
||||
- rollback-friendly deployment semantics
|
||||
- explicit current deployment pointer
|
||||
- GC-root-style retention links
|
||||
- `/run/current-system` as the active runtime boundary
|
||||
|
||||
What Fruix intentionally changes:
|
||||
|
||||
- installed-system generation state is represented as a small metadata-bearing directory
|
||||
- the generation model is recorded under a Fruix-native path
|
||||
- deployment metadata and provenance are easier to inspect directly without reconstructing intent from symlink layout alone
|
||||
|
||||
## Implemented layout
|
||||
|
||||
Installed systems now record an explicit generation root under:
|
||||
|
||||
- `/var/lib/fruix/system`
|
||||
|
||||
Current validated initial layout:
|
||||
|
||||
```text
|
||||
/var/lib/fruix/system/
|
||||
current -> generations/1
|
||||
current-generation
|
||||
generations/
|
||||
1/
|
||||
closure -> /frx/store/...-fruix-system-...
|
||||
metadata.scm
|
||||
provenance.scm
|
||||
install.scm
|
||||
```
|
||||
|
||||
Installed systems now also create explicit retention roots under:
|
||||
|
||||
- `/frx/var/fruix/gcroots`
|
||||
|
||||
Current validated initial layout:
|
||||
|
||||
```text
|
||||
/frx/var/fruix/gcroots/
|
||||
current-system -> /frx/store/...-fruix-system-...
|
||||
system-1 -> /frx/store/...-fruix-system-...
|
||||
```
|
||||
|
||||
Important compatibility point:
|
||||
|
||||
- `/run/current-system` still points directly at the active closure in `/frx/store`
|
||||
|
||||
That means the new explicit generation model strengthens deployment metadata without changing the already-validated runtime contract used by activation, rc.d integration, and service startup.
|
||||
|
||||
## Code changes
|
||||
|
||||
### `modules/fruix/system/freebsd/media.scm`
|
||||
|
||||
Added explicit generation-layout helpers:
|
||||
|
||||
- generation metadata object writer
|
||||
- generation provenance object writer
|
||||
- generation layout population for staged rootfs trees
|
||||
|
||||
The system rootfs staging path now creates explicit generation state during rootfs population.
|
||||
|
||||
That affects:
|
||||
|
||||
- direct rootfs materialization
|
||||
- direct image materialization
|
||||
- direct installation targets
|
||||
- target rootfs payloads staged inside installer images
|
||||
- target rootfs payloads staged inside installer ISOs
|
||||
|
||||
The direct install path now also refreshes the generation layout after writing:
|
||||
|
||||
- `/var/lib/fruix/install.scm`
|
||||
|
||||
so the generation directory carries the same install metadata.
|
||||
|
||||
### Installer runtime path
|
||||
|
||||
The generated installer runtime script now also copies install metadata into:
|
||||
|
||||
- `/var/lib/fruix/system/generations/1/install.scm`
|
||||
|
||||
on the installed target.
|
||||
|
||||
This keeps direct-install and installer-mediated installs aligned.
|
||||
|
||||
## New validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase19-generation-layout-qemu.sh`
|
||||
|
||||
This harness builds on the already-passing direct install validation from Phase 18.1 and then verifies the new explicit generation layout on the installed target image.
|
||||
|
||||
Passing validation:
|
||||
|
||||
- `PASS phase19-generation-layout-qemu`
|
||||
|
||||
Validated result summary:
|
||||
|
||||
```text
|
||||
closure_path=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
|
||||
current_generation=1
|
||||
current_link=generations/1
|
||||
generation_closure=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
|
||||
gcroot_current=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
|
||||
gcroot_generation=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
|
||||
run_current_system_target=/frx/store/882fb4a9fbb05f08e77de29f70ca50f3c01dd29141e72688d32770a3172747e7-fruix-system-fruix-freebsd
|
||||
generation_layout=explicit
|
||||
generation_layout_validation=ok
|
||||
```
|
||||
|
||||
The harness verified all of the following:
|
||||
|
||||
1. the installed target contains:
|
||||
- `/var/lib/fruix/system`
|
||||
2. the current generation pointer exists and resolves to:
|
||||
- `generations/1`
|
||||
3. the generation directory contains:
|
||||
- `closure`
|
||||
- `metadata.scm`
|
||||
- `provenance.scm`
|
||||
- `install.scm`
|
||||
4. the generation closure link points at the installed closure in `/frx/store`
|
||||
5. the generation metadata records:
|
||||
- closure path
|
||||
- install metadata path
|
||||
6. the generation install metadata records:
|
||||
- closure path
|
||||
- materialized source provenance
|
||||
7. explicit retention roots exist under:
|
||||
- `/frx/var/fruix/gcroots/current-system`
|
||||
- `/frx/var/fruix/gcroots/system-1`
|
||||
8. those GC-root-style links point at the same active closure
|
||||
9. `/run/current-system` still points directly at the active closure path
|
||||
10. existing install boot validation remains intact
|
||||
|
||||
## Relationship to Guix
|
||||
|
||||
Fruix now has an explicit installed-system generation model, but it is still intentionally not a byte-for-byte clone of Guix's on-disk conventions.
|
||||
|
||||
The design choice is:
|
||||
|
||||
- preserve Guix's deployment semantics
|
||||
- use a Fruix-native metadata-oriented representation where that improves clarity for operators and debugging
|
||||
|
||||
That decision is documented separately in:
|
||||
|
||||
- `docs/GUIX_DIFFERENCES.md`
|
||||
|
||||
## Current limitations
|
||||
|
||||
This phase does **not** yet add:
|
||||
|
||||
- multi-generation switching in place
|
||||
- a `switch`/`reconfigure` command
|
||||
- an operator-facing rollback command that flips from current to a previous installed generation without redeploy
|
||||
- explicit `rollback` link management beyond the initial current-generation layout
|
||||
|
||||
Those belong to later Phase 19 work.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phase 19.2 is complete.
|
||||
|
||||
Fruix now has a clearer, explicit installed-system generation and retention-root model on FreeBSD:
|
||||
|
||||
- generation metadata is recorded under `/var/lib/fruix/system`
|
||||
- retention links are recorded under `/frx/var/fruix/gcroots`
|
||||
- `/run/current-system` remains stable as the runtime boundary
|
||||
- and the model is documented in Fruix-native terms for Guix-familiar operators
|
||||
226
docs/reports/phase19-installed-system-rollback-freebsd.md
Normal file
226
docs/reports/phase19-installed-system-rollback-freebsd.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Phase 19.3: installed-system rollback workflow on FreeBSD
|
||||
|
||||
Date: 2026-04-04
|
||||
|
||||
## Goal
|
||||
|
||||
Phase 19.3 is about validating installed-system rollback through the intended operator-facing workflow, not only through host-side build/image redeploy harnesses.
|
||||
|
||||
The key question was:
|
||||
|
||||
- can an already-installed Fruix system move between recorded generations coherently, using an operator-facing command surface on the target itself?
|
||||
|
||||
## Decision
|
||||
|
||||
The current Fruix solution is intentionally modest.
|
||||
|
||||
Fruix now provides a small installed-system helper on the target itself:
|
||||
|
||||
- `/usr/local/bin/fruix`
|
||||
|
||||
Validated in-guest commands:
|
||||
|
||||
- `fruix system status`
|
||||
- `fruix system switch /frx/store/...-fruix-system-...`
|
||||
- `fruix system rollback`
|
||||
|
||||
Important scope choice:
|
||||
|
||||
- `switch` assumes the candidate closure is already present on the target's `/frx/store`
|
||||
- Fruix does **not** yet fetch or transfer that closure onto the target automatically
|
||||
|
||||
That keeps Phase 19.3 focused on generation-state correctness rather than introducing a larger store-transfer story prematurely.
|
||||
|
||||
## Implemented model
|
||||
|
||||
Installed systems now support the following validated operator pattern:
|
||||
|
||||
1. build a candidate closure with the host-side Fruix frontend
|
||||
2. stage that closure into the installed system's `/frx/store`
|
||||
3. run:
|
||||
- `fruix system switch /frx/store/...candidate...`
|
||||
4. reboot into the candidate generation
|
||||
5. if needed, run:
|
||||
- `fruix system rollback`
|
||||
6. reboot into the recorded rollback generation
|
||||
|
||||
The installed system now records explicit rollback state under:
|
||||
|
||||
```text
|
||||
/var/lib/fruix/system/
|
||||
current -> generations/N
|
||||
current-generation
|
||||
rollback -> generations/M
|
||||
rollback-generation
|
||||
generations/
|
||||
...
|
||||
```
|
||||
|
||||
and explicit rollback reachability under:
|
||||
|
||||
```text
|
||||
/frx/var/fruix/gcroots/
|
||||
current-system -> /frx/store/...current...
|
||||
rollback-system -> /frx/store/...rollback...
|
||||
system-1 -> ...
|
||||
system-2 -> ...
|
||||
```
|
||||
|
||||
## Code changes
|
||||
|
||||
### `modules/fruix/system/freebsd/render.scm`
|
||||
|
||||
Added a generated in-guest Fruix deployment helper script under:
|
||||
|
||||
- `usr/local/bin/fruix`
|
||||
|
||||
That helper now:
|
||||
|
||||
- reports installed-system state with `fruix system status`
|
||||
- stages a new current generation with `fruix system switch`
|
||||
- stages the recorded rollback generation with `fruix system rollback`
|
||||
- updates:
|
||||
- `/var/lib/fruix/system/current`
|
||||
- `/var/lib/fruix/system/current-generation`
|
||||
- `/var/lib/fruix/system/rollback`
|
||||
- `/var/lib/fruix/system/rollback-generation`
|
||||
- `/frx/var/fruix/gcroots/current-system`
|
||||
- `/frx/var/fruix/gcroots/rollback-system`
|
||||
- `/frx/var/fruix/gcroots/system-N`
|
||||
- refreshes the ESP bootloader file from the selected closure's `boot/loader.efi`
|
||||
|
||||
A practical implementation detail mattered here:
|
||||
|
||||
- replacing `/run/current-system` with a remove-then-recreate strategy caused the live shell environment to break while the link was absent
|
||||
- switching that update to an atomic symlink replacement path for `/run/current-system` avoided that gap and made the in-guest operator command reliable
|
||||
|
||||
### `modules/fruix/system/freebsd/media.scm`
|
||||
|
||||
Updated installed rootfs staging so that installed targets expose:
|
||||
|
||||
- `/usr/local/bin/fruix -> /run/current-system/usr/local/bin/fruix`
|
||||
|
||||
Also bumped the explicit generation-layout version from:
|
||||
|
||||
- `1` to `2`
|
||||
|
||||
because the installed-system model now includes operator-driven switch/rollback state as part of the validated layout story.
|
||||
|
||||
### `modules/fruix/system/freebsd/model.scm`
|
||||
|
||||
Updated generated-file metadata so the system closure records:
|
||||
|
||||
- `usr/local/bin/fruix`
|
||||
|
||||
as part of the generated operating-system file set.
|
||||
|
||||
## New validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase19-installed-system-rollback-qemu.sh`
|
||||
|
||||
This harness validates the actual installed-system operator flow on local `QEMU/UEFI/TCG`.
|
||||
|
||||
## Validation flow
|
||||
|
||||
The harness now performs all of the following:
|
||||
|
||||
1. installs a current system image directly to a target disk image
|
||||
2. builds a distinct candidate closure
|
||||
- in the validated harness this differs by host name so the closure identity changes cleanly without needing a heavier base-version rebuild
|
||||
3. stages the candidate closure and its referenced store items into the installed target's `/frx/store`
|
||||
4. boots the installed current system
|
||||
5. validates initial state:
|
||||
- current generation = `1`
|
||||
- current closure = current installed closure
|
||||
- no rollback generation yet recorded
|
||||
6. runs:
|
||||
- `fruix system switch /frx/store/...candidate...`
|
||||
7. validates staged switch state:
|
||||
- current generation = `2`
|
||||
- rollback generation = `1`
|
||||
- current closure = candidate closure
|
||||
- rollback closure = original current closure
|
||||
- generation 2 metadata/install files were written
|
||||
8. reboots and validates boot into the candidate closure
|
||||
9. runs:
|
||||
- `fruix system rollback`
|
||||
10. validates staged rollback state:
|
||||
- current generation = `1`
|
||||
- rollback generation = `2`
|
||||
- current closure = original closure
|
||||
- rollback closure = candidate closure
|
||||
11. reboots and validates boot back into the original current system
|
||||
12. confirms post-rollback service state:
|
||||
- `fruix-shepherd` running
|
||||
- `sshd` running
|
||||
- activation log still shows success
|
||||
|
||||
## Passing validation
|
||||
|
||||
Passing result:
|
||||
|
||||
- `PASS phase19-installed-system-rollback-qemu`
|
||||
|
||||
Validated metadata summary:
|
||||
|
||||
```text
|
||||
current_closure_path=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
|
||||
candidate_closure_path=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
|
||||
current_host_name=fruix-freebsd-current
|
||||
candidate_host_name=fruix-freebsd-canary
|
||||
final_current_generation=1
|
||||
final_current_closure=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
|
||||
final_rollback_generation=2
|
||||
final_rollback_closure=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
|
||||
installed_system_switch=ok
|
||||
installed_system_rollback=ok
|
||||
```
|
||||
|
||||
## Regression checks
|
||||
|
||||
After landing the installed-system switch/rollback workflow, the following regression checks still pass:
|
||||
|
||||
- `PASS phase19-generation-layout-qemu`
|
||||
- `PASS phase18-installer-iso`
|
||||
|
||||
That means the new in-guest generation-management path did not regress:
|
||||
|
||||
- the previously validated explicit generation layout
|
||||
- or the UEFI installer ISO boot/install path
|
||||
|
||||
## Relationship to Guix
|
||||
|
||||
This phase does **not** claim that Fruix now matches Guix's full installed-system UX.
|
||||
|
||||
What Fruix now has is:
|
||||
|
||||
- explicit generation state on disk
|
||||
- explicit current/rollback pointers
|
||||
- a minimal installed-system operator command surface
|
||||
- validated switching and rollback between already-staged closures
|
||||
|
||||
What still remains compared with Guix:
|
||||
|
||||
- building/staging the candidate closure from inside the target system itself
|
||||
- automatic closure transfer/fetch as part of `switch`
|
||||
- a richer long-term generation lifecycle policy
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phase 19.3 is complete.
|
||||
|
||||
Fruix now validates an actual installed-system rollback workflow on FreeBSD:
|
||||
|
||||
- the target system itself can report current/rollback state
|
||||
- it can switch to a staged candidate generation
|
||||
- it can reboot into that candidate generation
|
||||
- it can roll back to the recorded prior generation
|
||||
- and it can reboot into the restored current system
|
||||
|
||||
That closes the Phase 19 deployment story from:
|
||||
|
||||
- documented deployment workflow
|
||||
- to explicit generation layout
|
||||
- to validated installed-system operator rollback behavior
|
||||
160
docs/reports/phase20-development-environment-freebsd.md
Normal file
160
docs/reports/phase20-development-environment-freebsd.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Phase 20.1: Fruix-managed development environment for native FreeBSD base work
|
||||
|
||||
Date: 2026-04-05
|
||||
|
||||
## Goal
|
||||
|
||||
Validate that a booted Fruix-managed FreeBSD system can expose a usable development environment for deeper native base work without collapsing the runtime/development boundary back into one broad profile.
|
||||
|
||||
This step explicitly builds on the Phase 14 split between:
|
||||
|
||||
- native runtime/boot artifacts for the running system
|
||||
- separate development-facing artifacts such as headers and toolchain pieces
|
||||
|
||||
The goal is **not** full self-hosting yet.
|
||||
|
||||
It is to prove that a running Fruix system can expose the tools and paths needed for native FreeBSD build work in a controlled way.
|
||||
|
||||
## Implementation
|
||||
|
||||
### New operating-system field
|
||||
|
||||
`modules/fruix/system/freebsd/model.scm` now supports:
|
||||
|
||||
- `#:development-packages`
|
||||
- `operating-system-development-packages`
|
||||
|
||||
The default remains empty, so existing systems do not change unless they opt in.
|
||||
|
||||
### Separate development profile inside the system closure
|
||||
|
||||
`modules/fruix/system/freebsd/media.scm` now materializes an additional profile tree when `development-packages` is non-empty:
|
||||
|
||||
- `/frx/store/...-fruix-system-.../development-profile`
|
||||
|
||||
The main runtime tree remains:
|
||||
|
||||
- `/frx/store/...-fruix-system-.../profile`
|
||||
|
||||
This preserves the runtime/development split:
|
||||
|
||||
- runtime stays under `profile`
|
||||
- development tooling stays under `development-profile`
|
||||
|
||||
The development stores are also now part of the closure references and recorded in `metadata/store-layout.scm`.
|
||||
|
||||
### In-guest development environment helper
|
||||
|
||||
Opt-in systems with development packages now ship:
|
||||
|
||||
- `/usr/local/bin/fruix-development-environment`
|
||||
|
||||
and expose a stable runtime link:
|
||||
|
||||
- `/run/current-development -> /run/current-system/development-profile`
|
||||
|
||||
The helper emits shell exports for the active development profile, including at least:
|
||||
|
||||
- `FRUIX_DEVELOPMENT_PROFILE`
|
||||
- `FRUIX_DEVELOPMENT_INCLUDE`
|
||||
- `FRUIX_DEVELOPMENT_SHARE_MK`
|
||||
- `FRUIX_CC`
|
||||
- `FRUIX_CXX`
|
||||
- `FRUIX_AR`
|
||||
- `FRUIX_RANLIB`
|
||||
- `FRUIX_NM`
|
||||
- `FRUIX_BMAKE`
|
||||
- `CPPFLAGS`
|
||||
- `MAKEFLAGS`
|
||||
- `PATH`
|
||||
|
||||
Intended use:
|
||||
|
||||
```sh
|
||||
eval "$(/usr/local/bin/fruix-development-environment)"
|
||||
```
|
||||
|
||||
### Chosen development overlay for this phase
|
||||
|
||||
For the validated Phase 20.1 guest path, the development overlay is intentionally narrow:
|
||||
|
||||
- `freebsd-native-headers`
|
||||
- `freebsd-clang-toolchain`
|
||||
|
||||
This was chosen deliberately.
|
||||
|
||||
The earlier standalone Phase 14 development-profile package set was designed for broad profile composition, but for a running Fruix system it unnecessarily duplicated runtime pieces already present in the system profile.
|
||||
|
||||
For native base work, the important additions here are:
|
||||
|
||||
- `usr/include`
|
||||
- `usr/share/mk`
|
||||
- Clang/binutils-style frontend tools
|
||||
|
||||
while the running system already supplies:
|
||||
|
||||
- base runtime
|
||||
- `/usr/bin/make`
|
||||
- system libraries and boot/runtime state
|
||||
|
||||
That keeps the Phase 20.1 environment focused on native base development rather than reintroducing a broad mixed profile.
|
||||
|
||||
## New files
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/phase20-development-operating-system.scm.in`
|
||||
- `tests/system/run-phase20-development-environment-xcpng.sh`
|
||||
|
||||
## Validation
|
||||
|
||||
Passing run:
|
||||
|
||||
- `PASS phase20-development-environment-xcpng`
|
||||
- workdir: `/tmp/fruix-phase20-development-xcpng`
|
||||
|
||||
Validated on the approved real XCP-ng path:
|
||||
|
||||
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Representative result:
|
||||
|
||||
```text
|
||||
closure_path=/frx/store/c0ad43d7ef72323d4270a4f1e96ca1f5cc99566c-fruix-system-fruix-freebsd
|
||||
development_profile_path=/frx/store/c0ad43d7ef72323d4270a4f1e96ca1f5cc99566c-fruix-system-fruix-freebsd/development-profile
|
||||
development_profile_guest=/run/current-system/development-profile
|
||||
cc_version=FreeBSD clang version 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd708029e0b2)
|
||||
hello_direct=hello-from-direct-dev-env
|
||||
hello_make=hello-from-make-dev-env
|
||||
development_environment=ok
|
||||
```
|
||||
|
||||
The harness verified all of the following on the real booted Fruix guest:
|
||||
|
||||
- runtime boot/regression still succeeds on XCP-ng
|
||||
- the closure contains a separate `development-profile`
|
||||
- runtime `profile` does **not** regain headers or `usr/share/mk`
|
||||
- `/usr/local/bin/fruix-development-environment` exists and emits the expected exports
|
||||
- `/run/current-development` points at `/run/current-system/development-profile`
|
||||
- direct compilation works with the exported Clang toolchain and headers
|
||||
- a simple native FreeBSD `make` build using `.include <bsd.prog.mk>` also succeeds
|
||||
|
||||
## Result
|
||||
|
||||
Phase 20.1 is complete.
|
||||
|
||||
Fruix now validates a real booted FreeBSD system path where:
|
||||
|
||||
- the running system remains lean and runtime-focused
|
||||
- native development artifacts are exposed separately and explicitly
|
||||
- the guest can compile code directly with the Fruix-provided toolchain
|
||||
- the guest can also drive a simple `bsd.prog.mk` build using the exported development environment
|
||||
|
||||
This is enough to say that Fruix can host a controlled native FreeBSD base-development environment, without yet claiming full self-hosting.
|
||||
|
||||
## Next step
|
||||
|
||||
Per `docs/PLAN_4.md`, the next planned step is:
|
||||
|
||||
- **Phase 20.2** — run host-initiated native base builds inside a Fruix-managed environment
|
||||
169
docs/reports/phase20-host-initiated-native-builds-freebsd.md
Normal file
169
docs/reports/phase20-host-initiated-native-builds-freebsd.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Phase 20.2: host-initiated native base builds inside a Fruix-managed environment
|
||||
|
||||
Date: 2026-04-05
|
||||
|
||||
## Goal
|
||||
|
||||
Validate the next step after Phase 20.1:
|
||||
|
||||
- the host still orchestrates the outer loop
|
||||
- the actual FreeBSD native base build work runs inside a booted Fruix-managed system
|
||||
|
||||
This is the intermediate path between:
|
||||
|
||||
- purely host-side native base builds
|
||||
- any future claim of guest self-hosting
|
||||
|
||||
The target here was not a new self-hosted package manager story.
|
||||
|
||||
It was narrower:
|
||||
|
||||
- boot a Fruix-managed FreeBSD system on the approved real XCP-ng path
|
||||
- expose the development environment required for native base work
|
||||
- run real `buildworld` / `buildkernel` / staged install steps inside that Fruix guest
|
||||
- confirm that the resulting staged artifacts are the expected FreeBSD base slices
|
||||
|
||||
## Implementation
|
||||
|
||||
### Canonical development-path compatibility links
|
||||
|
||||
Phase 20.1 proved that Fruix could keep development content separate in:
|
||||
|
||||
- `/run/current-system/development-profile`
|
||||
- `/run/current-development`
|
||||
|
||||
Phase 20.2 exposed an additional practical requirement:
|
||||
|
||||
- FreeBSD native base builds still expect canonical system paths such as:
|
||||
- `/usr/include`
|
||||
- `/usr/share/mk`
|
||||
|
||||
For development-enabled systems, `populate-rootfs-from-closure` now also exposes:
|
||||
|
||||
- `/usr/include -> /run/current-system/development-profile/usr/include`
|
||||
- `/usr/share/mk -> /run/current-system/development-profile/usr/share/mk`
|
||||
|
||||
This keeps the development profile separate while still satisfying the buildworld/buildkernel assumptions of the native FreeBSD build system.
|
||||
|
||||
### Media builder invalidation
|
||||
|
||||
Because this changed the visible rootfs layout of booted systems, the media builder versions were bumped in `modules/fruix/system/freebsd/media.scm`:
|
||||
|
||||
- `image-builder-version`
|
||||
- `install-builder-version`
|
||||
- `installer-image-builder-version`
|
||||
- `installer-iso-builder-version`
|
||||
|
||||
That ensured booted images and future installed targets actually pick up the new compatibility links.
|
||||
|
||||
### New validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase20-host-initiated-native-build-xcpng.sh`
|
||||
|
||||
This harness reuses the validated Phase 20.1 XCP-ng path first, then performs the 20.2-native-build step over SSH from the host.
|
||||
|
||||
The guest build flow is:
|
||||
|
||||
1. boot the development-enabled Fruix guest on XCP-ng
|
||||
2. recover the materialized source store from `/run/current-system/metadata/store-layout.scm`
|
||||
3. run real FreeBSD native build commands inside the guest:
|
||||
- `make -j8 buildworld`
|
||||
- `make -j8 buildkernel`
|
||||
- `make DESTDIR=... installworld`
|
||||
- `make DESTDIR=... distribution`
|
||||
- `make DESTDIR=... installkernel`
|
||||
4. stage narrower artifact slices from the staged output:
|
||||
- headers slice
|
||||
- bootloader slice
|
||||
- kernel stage
|
||||
|
||||
### Why `DB_FROM_SRC=yes` is used for staged install steps
|
||||
|
||||
The development-enabled Fruix guest is intentionally lean and does not carry the full ambient host account database.
|
||||
|
||||
`installworld` on modern FreeBSD checks for required users/groups unless `DB_FROM_SRC` is defined. For staged installs into `DESTDIR`, the appropriate controlled input is the source tree's own account database under `etc/`, not the minimal running guest's `/etc/master.passwd`.
|
||||
|
||||
So the validated Phase 20.2 staged install path uses:
|
||||
|
||||
- `DB_FROM_SRC=yes`
|
||||
|
||||
for:
|
||||
|
||||
- `installworld`
|
||||
- `distribution`
|
||||
- `installkernel`
|
||||
|
||||
That keeps the staged install driven by the declared source input rather than by accidental guest-local account state.
|
||||
|
||||
## Validation
|
||||
|
||||
Passing run:
|
||||
|
||||
- `PASS phase20-host-initiated-native-build-xcpng`
|
||||
- workdir: `/tmp/fruix-phase20-host-initiated-native-build-xcpng`
|
||||
|
||||
Validated on the approved real XCP-ng path:
|
||||
|
||||
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Representative result:
|
||||
|
||||
```text
|
||||
build_jobs=8
|
||||
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
|
||||
source_root=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default/tree
|
||||
build_root=/var/tmp/fruix-phase20-native-build
|
||||
world_stage=/var/tmp/fruix-phase20-native-build/stage-world
|
||||
kernel_stage=/var/tmp/fruix-phase20-native-build/stage-kernel
|
||||
headers_stage=/var/tmp/fruix-phase20-native-build/artifact-headers
|
||||
bootloader_stage=/var/tmp/fruix-phase20-native-build/artifact-bootloader
|
||||
build_root_size=7.6G
|
||||
world_stage_size=672M
|
||||
kernel_stage_size=739M
|
||||
headers_stage_size=32M
|
||||
bootloader_stage_size=1.3M
|
||||
sha_kernel=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
|
||||
sha_loader=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
|
||||
sha_param=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
|
||||
host_initiated_native_build=ok
|
||||
```
|
||||
|
||||
The harness verified all of the following:
|
||||
|
||||
- the guest still boots and passes the Phase 20.1 development-environment checks first
|
||||
- development-enabled systems expose canonical native-build compatibility links at:
|
||||
- `/usr/include`
|
||||
- `/usr/share/mk`
|
||||
- the guest can recover the declared materialized FreeBSD source store from system metadata
|
||||
- real FreeBSD `buildworld` succeeds inside the booted Fruix guest
|
||||
- real FreeBSD `buildkernel` succeeds inside the booted Fruix guest
|
||||
- staged `installworld`, `distribution`, and `installkernel` also succeed inside the guest
|
||||
- the staged outputs contain the expected artifact shapes:
|
||||
- `boot/kernel/kernel`
|
||||
- `usr/include/sys/param.h`
|
||||
- `usr/share/mk/bsd.prog.mk`
|
||||
- `boot/loader.efi`
|
||||
- `boot/defaults/loader.conf`
|
||||
- `boot/lua/loader.lua`
|
||||
|
||||
## Result
|
||||
|
||||
Phase 20.2 is complete.
|
||||
|
||||
Fruix now validates a real host-orchestrated path where:
|
||||
|
||||
- the host boots and reaches a Fruix-managed development-enabled guest
|
||||
- the guest uses its own Fruix-exposed development paths and declared source store
|
||||
- the native FreeBSD base build work runs inside that Fruix-managed environment
|
||||
- the host remains the outer orchestrator and result collector
|
||||
|
||||
This materially narrows the gap to any future self-hosting experiment while still avoiding the complexity jump to a full guest-driven package/deployment loop.
|
||||
|
||||
## Next step
|
||||
|
||||
Per `docs/PLAN_4.md`, the next planned step is:
|
||||
|
||||
- **Phase 20.3** — reassess and potentially prototype guest self-hosted base builds
|
||||
202
docs/reports/phase20-native-build-executor-model-freebsd.md
Normal file
202
docs/reports/phase20-native-build-executor-model-freebsd.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Post-Phase 20: native build executor model
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
## Goal
|
||||
|
||||
Turn the now-proven native base-build placement options into one Fruix abstraction instead of treating them as unrelated paths.
|
||||
|
||||
The desired model is:
|
||||
|
||||
- same declared source identity
|
||||
- same expected artifact kinds
|
||||
- same staged-result shape
|
||||
- same promotion/provenance shape
|
||||
- different executor policy
|
||||
|
||||
So the question becomes:
|
||||
|
||||
- where should the build run?
|
||||
|
||||
and the answer is expressed as executor policy rather than as a separate architecture each time.
|
||||
|
||||
## Executor model
|
||||
|
||||
Fruix now has an explicit native-build executor model with current executor kinds:
|
||||
|
||||
- `host`
|
||||
- `ssh-guest`
|
||||
- `self-hosted`
|
||||
|
||||
and intended future extension points:
|
||||
|
||||
- `jail`
|
||||
- `remote-builder`
|
||||
|
||||
The new executor model is implemented in:
|
||||
|
||||
- `modules/fruix/system/freebsd/executor.scm`
|
||||
|
||||
It defines a structured executor object that records at least:
|
||||
|
||||
- `kind`
|
||||
- `name`
|
||||
- `version`
|
||||
- `properties`
|
||||
|
||||
## What changed
|
||||
|
||||
### 1. Structured executor metadata
|
||||
|
||||
Native-build result metadata is no longer limited to a flat string such as:
|
||||
|
||||
- `guest-self-hosted`
|
||||
|
||||
Instead, result/promotion objects now carry a structured executor description.
|
||||
|
||||
Representative executor objects now look like:
|
||||
|
||||
```scheme
|
||||
((kind . self-hosted)
|
||||
(name . "guest-self-hosted")
|
||||
(version . "4")
|
||||
(properties . (...)))
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```scheme
|
||||
((kind . ssh-guest)
|
||||
(name . "ssh-guest")
|
||||
(version . "1")
|
||||
(properties . (...)))
|
||||
```
|
||||
|
||||
Promoted store metadata also now records compatibility fields derived from that executor object:
|
||||
|
||||
- `executor-kind`
|
||||
- `executor-name`
|
||||
- `executor-version`
|
||||
|
||||
### 2. Shared staged-result shape across executors
|
||||
|
||||
Both validated executor paths now converge on the same mutable staging layout under:
|
||||
|
||||
- `/var/lib/fruix/native-builds/<run-id>`
|
||||
|
||||
with promoted artifacts staged under:
|
||||
|
||||
- `artifacts/world`
|
||||
- `artifacts/kernel`
|
||||
- `artifacts/headers`
|
||||
- `artifacts/bootloader`
|
||||
|
||||
and promotion metadata recorded in:
|
||||
|
||||
- `promotion.scm`
|
||||
|
||||
The heavy build work remains executor-specific, but the result shape is now shared.
|
||||
|
||||
### 3. Shared immutable promotion path
|
||||
|
||||
Both validated executor paths now promote through the same command:
|
||||
|
||||
```sh
|
||||
fruix native-build promote RESULT_ROOT
|
||||
```
|
||||
|
||||
That promotion creates immutable store identities for:
|
||||
|
||||
- `world`
|
||||
- `kernel`
|
||||
- `headers`
|
||||
- `bootloader`
|
||||
- result bundle
|
||||
|
||||
under `/frx/store/...`.
|
||||
|
||||
## Validated executor policies
|
||||
|
||||
### `self-hosted`
|
||||
|
||||
The self-hosted guest helper now emits the structured executor metadata directly from:
|
||||
|
||||
- `/usr/local/bin/fruix-self-hosted-native-build`
|
||||
|
||||
Validated self-hosted promotion flow:
|
||||
|
||||
- `PASS phase20-self-hosted-native-build-xcpng`
|
||||
- `PASS phase20-native-build-store-promotion-xcpng`
|
||||
|
||||
Representative promoted result:
|
||||
|
||||
```text
|
||||
result_store=/frx/store/0423193b9bd5e652bdb9d94d077e40dfcc3e9e78-fruix-native-build-result-15.0-STABLE-guest-self-hosted
|
||||
world_store=/frx/store/5f67e95058186147206ff6f5da2243a09212e358-fruix-native-world-15.0-STABLE-guest-self-hosted
|
||||
kernel_store=/frx/store/3a32797cc187a90e8273f205eababae6246568d9-fruix-native-kernel-15.0-STABLE-guest-self-hosted
|
||||
headers_store=/frx/store/1d54d9814461f003e91add8cd37e94ac5f3d04ce-fruix-native-headers-15.0-STABLE-guest-self-hosted
|
||||
bootloader_store=/frx/store/49f4885f0f05a324ac826f2618d4c7a923ca30d2-fruix-native-bootloader-15.0-STABLE-guest-self-hosted
|
||||
```
|
||||
|
||||
### `ssh-guest`
|
||||
|
||||
The host-initiated guest path now also stages a shared native-build result root under:
|
||||
|
||||
- `/var/lib/fruix/native-builds/<run-id>`
|
||||
|
||||
instead of stopping at temporary build directories alone.
|
||||
|
||||
Its promotion metadata records executor policy as:
|
||||
|
||||
- `kind = ssh-guest`
|
||||
- `name = ssh-guest`
|
||||
- `version = 1`
|
||||
|
||||
with executor properties including:
|
||||
|
||||
- `transport = ssh`
|
||||
- `orchestrator = host`
|
||||
- guest addressing / VM identity metadata
|
||||
|
||||
Validated host-initiated promotion flow:
|
||||
|
||||
- `PASS phase20-host-initiated-native-build-xcpng`
|
||||
- `PASS phase20-host-initiated-native-build-store-promotion-xcpng`
|
||||
|
||||
Representative promoted result:
|
||||
|
||||
```text
|
||||
result_store=/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest
|
||||
world_store=/frx/store/89c7a71c3df148a1f99b13d57fd6be88243eb2cb-fruix-native-world-15.0-STABLE-ssh-guest
|
||||
kernel_store=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest
|
||||
headers_store=/frx/store/dd7f39f526bca4849caf1eaf96ae25d29b43493c-fruix-native-headers-15.0-STABLE-ssh-guest
|
||||
bootloader_store=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest
|
||||
```
|
||||
|
||||
## Important result
|
||||
|
||||
The same declared source and artifact contract now works across two different executor policies:
|
||||
|
||||
- `ssh-guest`
|
||||
- `self-hosted`
|
||||
|
||||
while preserving the same Fruix-native staging/promotion split:
|
||||
|
||||
- mutable result roots under `/var/lib/fruix/native-builds/...`
|
||||
- immutable promoted identities under `/frx/store/...`
|
||||
|
||||
That is the core architectural win of the executor model.
|
||||
|
||||
## What is not yet fully unified
|
||||
|
||||
The `host` executor kind now exists in the model, but the fully shared staged-result-plus-promotion workflow is not yet wired through the existing host-local native build path.
|
||||
|
||||
So the executor model is introduced and real-VM validated across two policies, but not yet uniformly productized across every native-build entry point.
|
||||
|
||||
## Result
|
||||
|
||||
Fruix now treats native-build placement as executor policy rather than as an architectural fork.
|
||||
|
||||
That means the next step is no longer to invent another build path from scratch.
|
||||
|
||||
It is to keep extending the same executor/result/promotion model so additional execution policies can plug into the same Fruix-native object story.
|
||||
190
docs/reports/phase20-native-build-store-promotion-freebsd.md
Normal file
190
docs/reports/phase20-native-build-store-promotion-freebsd.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Post-Phase 20: native build result promotion into first-class Fruix store objects
|
||||
|
||||
Date: 2026-04-05
|
||||
|
||||
## Goal
|
||||
|
||||
Make native FreeBSD base-build results feel like real Fruix objects instead of stopping at mutable staged files under:
|
||||
|
||||
- `/var/lib/fruix/native-builds/...`
|
||||
|
||||
The desired model is:
|
||||
|
||||
- `/var/lib/fruix/native-builds/...` remains a staging/result area
|
||||
- `/frx/store/...` remains the real immutable identity
|
||||
|
||||
Validated artifact identities:
|
||||
|
||||
- `world`
|
||||
- `kernel`
|
||||
- `headers`
|
||||
- `bootloader`
|
||||
|
||||
## What changed
|
||||
|
||||
### Promotion metadata in guest result roots
|
||||
|
||||
The guest self-hosted helper now emits a promotion description file at:
|
||||
|
||||
- `/var/lib/fruix/native-builds/<run-id>/promotion.scm`
|
||||
|
||||
That metadata records at least:
|
||||
|
||||
- executor / executor-version
|
||||
- run-id / guest-host-name
|
||||
- closure path
|
||||
- development profile path
|
||||
- declared FreeBSD base metadata
|
||||
- source store provenance
|
||||
- build policy
|
||||
- artifact entries for:
|
||||
- `world`
|
||||
- `kernel`
|
||||
- `headers`
|
||||
- `bootloader`
|
||||
|
||||
The helper also stages a promotable `world` artifact tree in addition to the already validated narrower artifacts.
|
||||
|
||||
### Host-side promotion API
|
||||
|
||||
Fruix now exports:
|
||||
|
||||
- `promote-native-build-result`
|
||||
|
||||
and the CLI now exposes:
|
||||
|
||||
- `fruix native-build promote RESULT_ROOT [--store DIR]`
|
||||
|
||||
### Promoted store object layout
|
||||
|
||||
Promotion now creates immutable store objects for:
|
||||
|
||||
- `/frx/store/...-fruix-native-world-...`
|
||||
- `/frx/store/...-fruix-native-kernel-...`
|
||||
- `/frx/store/...-fruix-native-headers-...`
|
||||
- `/frx/store/...-fruix-native-bootloader-...`
|
||||
|
||||
Each promoted artifact store records:
|
||||
|
||||
- `.fruix-native-build-object.scm`
|
||||
- `.references`
|
||||
|
||||
Promotion also creates a result-bundle store object:
|
||||
|
||||
- `/frx/store/...-fruix-native-build-result-...`
|
||||
|
||||
That bundle records:
|
||||
|
||||
- `.fruix-native-build-result.scm`
|
||||
- `artifacts/world`
|
||||
- `artifacts/kernel`
|
||||
- `artifacts/headers`
|
||||
- `artifacts/bootloader`
|
||||
|
||||
where the `artifacts/*` entries are symlinks to the promoted artifact stores.
|
||||
|
||||
### Identity policy
|
||||
|
||||
Artifact identity is now based on Fruix metadata plus a tree-content signature of the staged artifact tree.
|
||||
|
||||
That means promotion identity depends on both:
|
||||
|
||||
- the explicit Fruix-native build metadata
|
||||
- the actual content of the promoted artifact tree
|
||||
|
||||
## Validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase20-native-build-store-promotion-xcpng.sh`
|
||||
|
||||
This harness:
|
||||
|
||||
1. boots the approved real XCP-ng guest path
|
||||
2. runs the validated in-guest self-hosted native build helper
|
||||
3. imports the guest result root back to the host
|
||||
4. runs `fruix native-build promote`
|
||||
5. verifies promoted store paths, metadata, symlink structure, and representative hashes
|
||||
|
||||
## Validation
|
||||
|
||||
Passing run:
|
||||
|
||||
- `PASS phase20-native-build-store-promotion-xcpng`
|
||||
- workdir: `/tmp/current-phase20-native-build-store-promotion-xcpng`
|
||||
|
||||
Approved real XCP-ng path:
|
||||
|
||||
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Representative metadata:
|
||||
|
||||
```text
|
||||
run_id=20260405T213444Z
|
||||
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
|
||||
guest_result_root=/var/lib/fruix/native-builds/20260405T213444Z
|
||||
result_store=/frx/store/c6329a0053720b05aff3274b8b1d522c909f475d-fruix-native-build-result-15.0-STABLE-guest-self-hosted
|
||||
world_store=/frx/store/dfe37b36f6537a95ceea16ea62001b2ca5617eb7-fruix-native-world-15.0-STABLE-guest-self-hosted
|
||||
kernel_store=/frx/store/0ab7cbceca240ab2c3b91e83e059844ea792e49e-fruix-native-kernel-15.0-STABLE-guest-self-hosted
|
||||
headers_store=/frx/store/5bbeae9266687a229f1c6d176a08886c35243ff0-fruix-native-headers-15.0-STABLE-guest-self-hosted
|
||||
bootloader_store=/frx/store/bd49a508bd7a3b94a2535d6774f31c993c406552-fruix-native-bootloader-15.0-STABLE-guest-self-hosted
|
||||
artifact_store_count=4
|
||||
sha_kernel=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
|
||||
sha_loader=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
|
||||
sha_param=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
|
||||
promoted_kernel_sha=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
|
||||
promoted_loader_sha=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
|
||||
promoted_param_sha=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
|
||||
native_build_store_promotion=ok
|
||||
```
|
||||
|
||||
Validated facts:
|
||||
|
||||
- guest self-hosted native-build results remain available under `/var/lib/fruix/native-builds/...` as mutable staging/results
|
||||
- those staged results are now sufficient to promote first-class Fruix identities on the host
|
||||
- promotion creates four immutable artifact store objects plus one immutable result-bundle store object
|
||||
- promoted metadata retains executor/source/build-policy/provenance information explicitly
|
||||
- promoted kernel, bootloader, and headers hashes match the already validated staged artifacts
|
||||
- the promoted `world` artifact is now also preserved as a first-class Fruix store object instead of remaining only as an unpromoted staged tree
|
||||
|
||||
## Important implementation note
|
||||
|
||||
The first full promotion attempt exposed an incorrect assumption in the validation surface:
|
||||
|
||||
- the promoted/staged `world` artifact should be checked at:
|
||||
- `bin/sh`
|
||||
- not at:
|
||||
- `usr/bin/sh`
|
||||
|
||||
That path expectation was corrected in:
|
||||
|
||||
- helper validation
|
||||
- emitted promotion metadata
|
||||
- the end-to-end promotion harness
|
||||
|
||||
## Result
|
||||
|
||||
Fruix now has a validated native-build object model with a clear split:
|
||||
|
||||
- mutable native-build result roots under `/var/lib/fruix/native-builds/...`
|
||||
- immutable promoted identities under `/frx/store/...`
|
||||
|
||||
That makes native FreeBSD base builds feel substantially more Fruix-native:
|
||||
|
||||
- build outputs have explicit immutable identities
|
||||
- metadata is Fruix-native rather than implied only by ad hoc directory layout
|
||||
- executor/source/provenance/build-policy remain attached to the promoted result
|
||||
- the staged result area and the real store identity are now intentionally distinct
|
||||
|
||||
## Next direction
|
||||
|
||||
This suggests the next product step is not merely “more self-hosting”.
|
||||
|
||||
It is to generalize this result model so different execution modes can converge on the same promoted object story, for example:
|
||||
|
||||
- host-initiated in-guest builds
|
||||
- guest self-hosted builds
|
||||
- future executor variants
|
||||
|
||||
That would move Fruix closer to a shared executor model rather than treating each validation path as a one-off harness.
|
||||
201
docs/reports/phase20-self-hosted-native-builds-freebsd.md
Normal file
201
docs/reports/phase20-self-hosted-native-builds-freebsd.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Phase 20.3: controlled guest self-hosted native base-build prototype
|
||||
|
||||
Date: 2026-04-05
|
||||
|
||||
## Goal
|
||||
|
||||
Reassess guest self-hosting now that Fruix has already completed the earlier source, installation, generation-layout, rollback, development-overlay, and host-initiated in-guest native-build steps.
|
||||
|
||||
Phase 20.3 asked for real evidence about:
|
||||
|
||||
- what self-hosting would improve
|
||||
- what it would cost in complexity
|
||||
- how it fits with the Fruix source/deployment model already in place
|
||||
|
||||
## What changed
|
||||
|
||||
### New in-guest helper
|
||||
|
||||
Development-enabled systems now also ship:
|
||||
|
||||
- `/usr/local/bin/fruix-self-hosted-native-build`
|
||||
|
||||
This helper performs a controlled in-guest native FreeBSD base build using the system's own declared materialized source store recorded in:
|
||||
|
||||
- `/run/current-system/metadata/store-layout.scm`
|
||||
|
||||
The helper:
|
||||
|
||||
1. verifies the development overlay is present
|
||||
2. verifies the canonical compatibility links exist:
|
||||
- `/usr/include`
|
||||
- `/usr/share/mk`
|
||||
3. recovers the materialized FreeBSD source store from current-system metadata
|
||||
4. runs:
|
||||
- `buildworld`
|
||||
- `buildkernel`
|
||||
- `installworld`
|
||||
- `distribution`
|
||||
- `installkernel`
|
||||
5. stages narrower artifact outputs under:
|
||||
- `/var/lib/fruix/native-builds/<run-id>/artifacts/`
|
||||
6. records metadata and status under:
|
||||
- `/var/lib/fruix/native-builds/<run-id>/`
|
||||
- `/var/lib/fruix/native-builds/latest`
|
||||
|
||||
The heavy object/stage work stays under:
|
||||
|
||||
- `/var/tmp/fruix-self-hosted-native-builds/<run-id>`
|
||||
|
||||
so the installed-system result area remains smaller and more legible.
|
||||
|
||||
### Important environment fix discovered during prototyping
|
||||
|
||||
The first prototype attempt failed even though Phase 20.2 had already succeeded.
|
||||
|
||||
Cause:
|
||||
|
||||
- directly evaluating `fruix-development-environment` before `buildworld` exported development-oriented variables like:
|
||||
- `MAKEFLAGS`
|
||||
- `CPPFLAGS`
|
||||
- `CFLAGS`
|
||||
- `CXXFLAGS`
|
||||
- `LDFLAGS`
|
||||
- those are appropriate for smaller development builds, but they polluted FreeBSD's world/kernel bootstrap environment and broke the LLVM bootstrap phase
|
||||
|
||||
Representative failure:
|
||||
|
||||
- missing generated LLVM config headers during bootstrap (`llvm/Config/abi-breaking.h`)
|
||||
|
||||
The validated fix was to make the self-hosted helper explicitly sanitize that environment first:
|
||||
|
||||
- reset `PATH` to the normal base paths
|
||||
- unset development-shell variables such as:
|
||||
- `MAKEFLAGS`
|
||||
- `CC`, `CXX`, `AR`, `RANLIB`, `NM`
|
||||
- `CPPFLAGS`, `CFLAGS`, `CXXFLAGS`, `LDFLAGS`
|
||||
- `FRUIX_DEVELOPMENT_*`
|
||||
- `FRUIX_*` tool variables
|
||||
|
||||
So the final 20.3 result is not “just reuse the development shell wholesale”.
|
||||
|
||||
It is more precise:
|
||||
|
||||
- use the development overlay for canonical paths and available content
|
||||
- but run the real base-build steps in a cleaner, purpose-built helper environment
|
||||
|
||||
### Closure invalidation
|
||||
|
||||
To ensure the updated helper actually affects generated system closures, the operating-system closure spec now also records helper-version markers for development-enabled systems.
|
||||
|
||||
That ensures guest images pick up helper changes instead of silently reusing an older cached closure path.
|
||||
|
||||
### Validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/run-phase20-self-hosted-native-build-xcpng.sh`
|
||||
|
||||
This harness:
|
||||
|
||||
1. boots the validated development-enabled Fruix guest on the approved XCP-ng path
|
||||
2. verifies the new helper exists in the guest
|
||||
3. invokes the helper from inside the guest
|
||||
4. verifies the recorded result/status/`latest` pointer
|
||||
5. validates the resulting staged artifact metadata and hashes
|
||||
|
||||
## Validation
|
||||
|
||||
Passing run:
|
||||
|
||||
- `PASS phase20-self-hosted-native-build-xcpng`
|
||||
- workdir: `/tmp/fruix-phase20-self-hosted-native-build-xcpng`
|
||||
|
||||
Validated on the approved real XCP-ng path:
|
||||
|
||||
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Representative metadata:
|
||||
|
||||
```text
|
||||
run_id=20260405T150359Z
|
||||
helper_version=2
|
||||
build_jobs=8
|
||||
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
|
||||
build_root=/var/tmp/fruix-self-hosted-native-builds/20260405T150359Z
|
||||
result_root=/var/lib/fruix/native-builds/20260405T150359Z
|
||||
latest_link=/var/lib/fruix/native-builds/latest
|
||||
latest_target=/var/lib/fruix/native-builds/20260405T150359Z
|
||||
status_value=ok
|
||||
build_root_size=7.5G
|
||||
result_root_size=343M
|
||||
kernel_artifact_size=158M
|
||||
headers_artifact_size=32M
|
||||
bootloader_artifact_size=1.3M
|
||||
sha_kernel=16950f116a52134b98e2f8e0dacc556e18fe254e4a0ac2c1741422dde281a341
|
||||
sha_loader=ea417846167ece270ada611624dca622ca38bd30125b9a125cd8ebb8b3600313
|
||||
sha_param=9eb140ca7d9666f3d484a4174c9acd94b45427db6292b4e17de19af2c6aa5219
|
||||
self_hosted_native_build=ok
|
||||
```
|
||||
|
||||
Validated facts:
|
||||
|
||||
- the development-enabled Fruix guest can now run a controlled self-hosted native base-build helper from inside the installed system itself
|
||||
- the helper can recover the declared source store from current-system metadata without host-side parsing
|
||||
- `buildworld` and `buildkernel` succeed in the guest
|
||||
- staged `installworld`, `distribution`, and `installkernel` succeed in the guest
|
||||
- the helper records a stable result directory and `latest` pointer under:
|
||||
- `/var/lib/fruix/native-builds`
|
||||
- the resulting artifact hashes match the earlier validated Phase 20.2 host-initiated in-guest path
|
||||
|
||||
## What self-hosting improved
|
||||
|
||||
The prototype demonstrates a few real improvements:
|
||||
|
||||
- the build recipe itself now lives inside the Fruix-managed system, not only in a host-side SSH harness
|
||||
- the guest can derive its own declared source input from current-system metadata
|
||||
- result/state recording now has a Fruix-native installed-system location:
|
||||
- `/var/lib/fruix/native-builds`
|
||||
- the host no longer needs to spell out every `make` phase just to validate the in-guest path
|
||||
|
||||
## What it cost in complexity
|
||||
|
||||
The prototype also made the extra complexity visible:
|
||||
|
||||
- the guest helper needs its own controlled environment contract
|
||||
- a naive reuse of the development-shell exports was wrong for real `buildworld`
|
||||
- helper-version invalidation had to be made explicit so closure caching would not hide helper changes
|
||||
- the in-guest result/staging model now needs its own operator-facing conventions
|
||||
|
||||
So the experiment did not eliminate complexity.
|
||||
|
||||
It mostly moved some of it from the host harness into an explicit in-guest helper contract.
|
||||
|
||||
## Decision after the prototype
|
||||
|
||||
Phase 20.3 is complete because Fruix now has a **first controlled guest self-hosted native base-build prototype**.
|
||||
|
||||
However, the evidence does **not** suggest replacing the Phase 20.2 path as the default operator workflow yet.
|
||||
|
||||
The current recommendation is:
|
||||
|
||||
- keep the **host-initiated in-guest native-build path** as the simpler default validation and orchestration flow
|
||||
- keep the new **self-hosted helper** as a controlled prototype and stepping stone toward deeper guest-driven workflows
|
||||
|
||||
That fits the existing Fruix model well:
|
||||
|
||||
- source identity still comes from declared store-backed metadata
|
||||
- deployment identity still comes from immutable closures under `/frx/store`
|
||||
- the guest-side prototype adds a narrower in-system build/result workflow without replacing the existing deployment story
|
||||
|
||||
## Result
|
||||
|
||||
Phase 20.3 is complete.
|
||||
|
||||
Fruix now has:
|
||||
|
||||
- a validated host-orchestrated in-guest native base-build workflow
|
||||
- and a validated first controlled guest self-hosted native base-build prototype
|
||||
|
||||
That answers the Phase 20.3 question with real evidence instead of only prior caution.
|
||||
223
docs/reports/postphase20-build-profile-separation-freebsd.md
Normal file
223
docs/reports/postphase20-build-profile-separation-freebsd.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Post-Phase 20: separate development from native base-build environment
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
## Goal
|
||||
|
||||
Tighten the distinction that Phase 20.3 exposed in practice:
|
||||
|
||||
- an interactive development shell is not the same thing as a reliable native base-build environment
|
||||
|
||||
Fruix now models three layers instead of two:
|
||||
|
||||
- runtime profile
|
||||
- development profile
|
||||
- build profile
|
||||
|
||||
## Why this change was needed
|
||||
|
||||
The earlier self-hosted native-build prototype proved that simply reusing the exported development environment for `buildworld` / `buildkernel` was too loose.
|
||||
|
||||
Development-oriented exports like:
|
||||
|
||||
- `MAKEFLAGS`
|
||||
- `CPPFLAGS`
|
||||
- `CFLAGS`
|
||||
- `CXXFLAGS`
|
||||
- `LDFLAGS`
|
||||
|
||||
are useful for interactive compilation work, but they are the wrong contract for a real FreeBSD world/kernel bootstrap.
|
||||
|
||||
Phase 20.3 previously worked around that by manually sanitizing the shell before running the build.
|
||||
|
||||
This change makes that separation explicit in the product model instead of keeping it as an ad hoc helper detail.
|
||||
|
||||
## Implementation
|
||||
|
||||
### New operating-system layer
|
||||
|
||||
`modules/fruix/system/freebsd/model.scm` now supports:
|
||||
|
||||
- `#:build-packages`
|
||||
- `operating-system-build-packages`
|
||||
|
||||
So a build-capable system can now carry both:
|
||||
|
||||
- `development-packages`
|
||||
- `build-packages`
|
||||
|
||||
separately.
|
||||
|
||||
### New build profile inside the system closure
|
||||
|
||||
When `build-packages` is non-empty, Fruix now materializes:
|
||||
|
||||
- `/frx/store/...-fruix-system-.../build-profile`
|
||||
|
||||
alongside the existing:
|
||||
|
||||
- `/frx/store/...-fruix-system-.../profile`
|
||||
- `/frx/store/...-fruix-system-.../development-profile`
|
||||
|
||||
Store-layout metadata now records both development-package stores and build-package stores explicitly.
|
||||
|
||||
### New in-guest build helper
|
||||
|
||||
Build-capable systems now ship:
|
||||
|
||||
- `/usr/local/bin/fruix-build-environment`
|
||||
|
||||
and expose a stable runtime link:
|
||||
|
||||
- `/run/current-build -> /run/current-system/build-profile`
|
||||
|
||||
That helper intentionally emits a stricter environment contract than the interactive development helper.
|
||||
|
||||
It clears development-shell variables such as:
|
||||
|
||||
- `MAKEOBJDIRPREFIX`
|
||||
- `MAKEFLAGS`
|
||||
- `CC`
|
||||
- `CXX`
|
||||
- `AR`
|
||||
- `RANLIB`
|
||||
- `NM`
|
||||
- `CPPFLAGS`
|
||||
- `CFLAGS`
|
||||
- `CXXFLAGS`
|
||||
- `LDFLAGS`
|
||||
|
||||
and also clears development-helper exports such as:
|
||||
|
||||
- `FRUIX_DEVELOPMENT_PROFILE`
|
||||
- `FRUIX_CC`
|
||||
- `FRUIX_CXX`
|
||||
- `FRUIX_AR`
|
||||
- `FRUIX_RANLIB`
|
||||
- `FRUIX_NM`
|
||||
- `FRUIX_BMAKE`
|
||||
|
||||
Then it exports build-specific values including at least:
|
||||
|
||||
- `FRUIX_BUILD_PROFILE`
|
||||
- `FRUIX_BUILD_INCLUDE`
|
||||
- `FRUIX_BUILD_SHARE_MK`
|
||||
- `FRUIX_BUILD_BIN`
|
||||
- `FRUIX_BUILD_USR_BIN`
|
||||
- `FRUIX_BUILD_CC`
|
||||
- `FRUIX_BUILD_CXX`
|
||||
- `FRUIX_BUILD_AR`
|
||||
- `FRUIX_BUILD_RANLIB`
|
||||
- `FRUIX_BUILD_NM`
|
||||
- `FRUIX_BMAKE`
|
||||
- `PATH`
|
||||
|
||||
Intended use:
|
||||
|
||||
```sh
|
||||
eval "$(/usr/local/bin/fruix-build-environment)"
|
||||
```
|
||||
|
||||
### Canonical build compatibility links now come from the build profile
|
||||
|
||||
For native base-build compatibility, build-capable systems now expose:
|
||||
|
||||
- `/usr/include -> /run/current-system/build-profile/usr/include`
|
||||
- `/usr/share/mk -> /run/current-system/build-profile/usr/share/mk`
|
||||
|
||||
This means the running system can still keep development and build content separate while offering the canonical paths that FreeBSD native build machinery expects.
|
||||
|
||||
### Self-hosted native-build helper now uses the build helper contract
|
||||
|
||||
`/usr/local/bin/fruix-self-hosted-native-build` now:
|
||||
|
||||
- requires `build-profile`
|
||||
- requires `/usr/local/bin/fruix-build-environment`
|
||||
- evaluates the build helper contract before `buildworld`
|
||||
- verifies the canonical compatibility links point at `build-profile`
|
||||
|
||||
This replaces the earlier approach where the helper had to reconstruct sanitization manually around a development-oriented environment.
|
||||
|
||||
### Promotion metadata now records build profile provenance
|
||||
|
||||
Promotion metadata emitted for self-hosted native-build results now records:
|
||||
|
||||
- `build-profile`
|
||||
|
||||
Promoted artifact/result metadata also now preserves that field.
|
||||
|
||||
## Validation
|
||||
|
||||
Validated on the approved real XCP-ng path:
|
||||
|
||||
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Passing runs:
|
||||
|
||||
- `PASS phase20-development-environment-xcpng`
|
||||
- `PASS phase20-self-hosted-native-build-xcpng`
|
||||
- `PASS phase20-native-build-store-promotion-xcpng`
|
||||
- `PASS phase20-host-initiated-native-build-xcpng`
|
||||
- `PASS phase20-host-initiated-native-build-store-promotion-xcpng`
|
||||
|
||||
## Representative validated results
|
||||
|
||||
Development/build environment validation:
|
||||
|
||||
```text
|
||||
development_profile_guest=/run/current-system/development-profile
|
||||
build_profile_guest=/run/current-system/build-profile
|
||||
development_env_script=/frx/store/...-fruix-system-.../usr/local/bin/fruix-development-environment
|
||||
build_env_script=/frx/store/...-fruix-system-.../usr/local/bin/fruix-build-environment
|
||||
development_environment=ok
|
||||
```
|
||||
|
||||
Self-hosted native-build validation:
|
||||
|
||||
```text
|
||||
helper_version=5
|
||||
executor_kind=self-hosted
|
||||
executor_name=guest-self-hosted
|
||||
executor_version=5
|
||||
build_profile=/run/current-system/build-profile
|
||||
source_store=/frx/store/12d7704362e95afc2697db63f168b878e082b372-freebsd-source-default
|
||||
self_hosted_native_build=ok
|
||||
```
|
||||
|
||||
Promotion validation:
|
||||
|
||||
```text
|
||||
executor_kind=self-hosted
|
||||
executor_name=guest-self-hosted
|
||||
executor_version=5
|
||||
result_store=/frx/store/3ce6aefd564bc51f2465dcbb5c261355be4c7076-fruix-native-build-result-15.0-STABLE-guest-self-hosted
|
||||
native_build_store_promotion=ok
|
||||
```
|
||||
|
||||
Host-initiated path revalidation with the new build-profile split also succeeded:
|
||||
|
||||
```text
|
||||
executor_kind=ssh-guest
|
||||
executor_name=ssh-guest
|
||||
executor_version=1
|
||||
result_store=/frx/store/93a4837d2588acfde2262010b296df9c7b7b367a-fruix-native-build-result-15.0-STABLE-ssh-guest
|
||||
host_initiated_native_build_store_promotion=ok
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
Fruix now expresses an important product distinction more honestly:
|
||||
|
||||
- development is for interactive work
|
||||
- build is for a stricter native base-build contract
|
||||
|
||||
That reduces special-case cleanup inside the self-hosted helper and gives Fruix a clearer path for future operator-facing commands such as:
|
||||
|
||||
- `fruix system build-base`
|
||||
- `fruix system upgrade`
|
||||
|
||||
because the system model now has an explicit place to say:
|
||||
|
||||
- what is available for interactive development
|
||||
- what is available for real native base builds
|
||||
178
docs/reports/postphase20-installed-node-management-freebsd.md
Normal file
178
docs/reports/postphase20-installed-node-management-freebsd.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Post-Phase 20: installed systems as real Fruix nodes
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
## Goal
|
||||
|
||||
Make a Fruix-installed machine feel like a managed Fruix node instead of only a deployed image with switch/rollback helpers.
|
||||
|
||||
The immediate target was not yet the full long-term command surface of:
|
||||
|
||||
- `fruix system upgrade`
|
||||
- `fruix system build-base`
|
||||
- `fruix system deploy`
|
||||
|
||||
but it was enough to cross an important product threshold:
|
||||
|
||||
- installed nodes can now remember their own declaration inputs
|
||||
- installed nodes can build from those inputs locally
|
||||
- installed nodes can reconfigure themselves into a newly built generation
|
||||
- installed nodes can still roll back cleanly
|
||||
|
||||
## What changed
|
||||
|
||||
### Canonical declaration state now ships inside the system closure
|
||||
|
||||
Fruix system closures now carry explicit declaration metadata:
|
||||
|
||||
- `metadata/system-declaration.scm`
|
||||
- `metadata/system-declaration-info.scm`
|
||||
- `metadata/system-declaration-system`
|
||||
|
||||
That gives an installed system a canonical local answer to:
|
||||
|
||||
- what declaration source produced me?
|
||||
- what top-level system variable should be used?
|
||||
|
||||
This declaration metadata is also recorded through the installed generation layout metadata.
|
||||
|
||||
### Bundled Fruix node CLI sources now ship inside the closure
|
||||
|
||||
Installed system closures now also carry a self-contained Fruix node CLI source bundle under:
|
||||
|
||||
- `share/fruix/node/scripts/fruix.scm`
|
||||
- `share/fruix/node/modules/...`
|
||||
- `share/fruix/node/guix/guix/build/utils.scm`
|
||||
|
||||
This gives the installed node enough local Fruix/Guix Scheme source to run Fruix system actions from the node itself.
|
||||
|
||||
### Installed `fruix` helper gained local build/reconfigure support
|
||||
|
||||
The installed helper at:
|
||||
|
||||
- `/usr/local/bin/fruix`
|
||||
|
||||
now supports:
|
||||
|
||||
- `fruix system build`
|
||||
- `fruix system reconfigure`
|
||||
- `fruix system status`
|
||||
- `fruix system switch`
|
||||
- `fruix system rollback`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `fruix system build` with no extra arguments uses:
|
||||
- `/run/current-system/metadata/system-declaration.scm`
|
||||
- `/run/current-system/metadata/system-declaration-system`
|
||||
- `fruix system reconfigure` with no extra arguments builds from that same embedded declaration and then stages a switch to the resulting closure
|
||||
- both commands can also take an explicit declaration file plus `--system NAME`, using the same general CLI shape as the host-side Fruix frontend
|
||||
|
||||
### In-node builds now reuse the installed Fruix runtime stores
|
||||
|
||||
A crucial implementation fix was needed here.
|
||||
|
||||
An installed node should not try to reconstruct the Guile/Shepherd runtime prefixes from host-side `/tmp/...` build roots or host `/usr/local/lib/...` assumptions.
|
||||
|
||||
Instead, in-node Fruix builds now explicitly reuse the installed runtime stores already referenced by the current system closure.
|
||||
|
||||
That allows the in-node build path to work on the real installed system rather than depending on build-host-only paths.
|
||||
|
||||
## Validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/postphase20-installed-node-operating-system.scm.in`
|
||||
- `tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh`
|
||||
|
||||
This harness validates the new installed-node workflow on the approved real XCP-ng path.
|
||||
|
||||
## Validation performed
|
||||
|
||||
Approved real XCP-ng path:
|
||||
|
||||
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Promoted native-base result consumed by the installed node declaration:
|
||||
|
||||
- `/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest`
|
||||
|
||||
Validated flow:
|
||||
|
||||
1. boot a Fruix-installed node built from the promoted native-base result
|
||||
2. confirm the installed node exposes:
|
||||
- embedded declaration metadata
|
||||
- bundled node CLI sources
|
||||
3. run in-guest:
|
||||
- `fruix system build`
|
||||
using the node's own embedded declaration inputs
|
||||
4. copy a candidate declaration to the guest
|
||||
5. run in-guest:
|
||||
- `fruix system build /root/candidate.scm --system postphase20-installed-node-operating-system`
|
||||
6. run in-guest:
|
||||
- `fruix system reconfigure /root/candidate.scm --system postphase20-installed-node-operating-system`
|
||||
7. reboot and verify the candidate generation boots
|
||||
8. run in-guest:
|
||||
- `fruix system rollback`
|
||||
9. reboot and verify the original generation boots again
|
||||
|
||||
Passing run:
|
||||
|
||||
- `PASS postphase20-installed-node-build-reconfigure-xcpng`
|
||||
|
||||
Representative metadata:
|
||||
|
||||
```text
|
||||
closure_path=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current
|
||||
current_built_closure=/frx/store/18ee10925a15b48c676463a3359c45ff766e16a0-fruix-system-fruix-node-current
|
||||
candidate_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
|
||||
reconfigure_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
|
||||
reconfigure_current_generation=2
|
||||
reconfigure_current_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
|
||||
reconfigure_rollback_generation=1
|
||||
reconfigure_rollback_closure=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current
|
||||
candidate_hostname=fruix-node-canary
|
||||
rollback_current_generation=1
|
||||
rollback_current_closure=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current
|
||||
rollback_rollback_generation=2
|
||||
rollback_rollback_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary
|
||||
rollback_hostname=fruix-node-current
|
||||
installed_node_build_reconfigure=ok
|
||||
```
|
||||
|
||||
## Important observation
|
||||
|
||||
The no-argument in-node build of the current declaration did not necessarily reproduce the exact original current closure path.
|
||||
|
||||
That is expected with the current model because closure metadata still records builder-local provenance such as the current build host context.
|
||||
|
||||
So the significant validated fact is not strict bit-for-bit closure reuse.
|
||||
|
||||
It is that the installed node can now:
|
||||
|
||||
- read its own declaration inputs locally
|
||||
- build a valid local candidate closure from them
|
||||
- build a different candidate declaration locally
|
||||
- reconfigure itself into that candidate generation
|
||||
- boot the candidate generation
|
||||
- roll back and boot the prior generation again
|
||||
|
||||
## Result
|
||||
|
||||
Fruix-installed machines are now meaningfully closer to real Fruix nodes.
|
||||
|
||||
They no longer only support:
|
||||
|
||||
- `status`
|
||||
- `switch`
|
||||
- `rollback`
|
||||
|
||||
They now also support a validated local node workflow for:
|
||||
|
||||
- reading embedded declaration inputs
|
||||
- building a local candidate closure
|
||||
- reconfiguring into that locally built candidate
|
||||
- rolling back through the same installed generation model
|
||||
|
||||
This is the first concrete step toward a fuller operator-facing Fruix node command surface.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Post-Phase 20: promoted native base result sets as declaration inputs
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
## Goal
|
||||
|
||||
Move Fruix from:
|
||||
|
||||
- “the guest built a kernel”
|
||||
|
||||
to:
|
||||
|
||||
- “Fruix materialized a native-base result set with identity X”
|
||||
|
||||
and then prove that a normal Fruix system declaration can consume that promoted result set directly.
|
||||
|
||||
Concretely, the desired product behavior was:
|
||||
|
||||
1. build runs somewhere
|
||||
2. result is recorded in a staging/result root
|
||||
3. Fruix validates the result shape and metadata
|
||||
4. Fruix promotes it into store objects
|
||||
5. system declarations can refer to those promoted artifacts
|
||||
|
||||
Before this step, Fruix already had validated steps 1 through 4.
|
||||
|
||||
This step implemented and validated step 5.
|
||||
|
||||
## What changed
|
||||
|
||||
### First-class promoted result reference in system declarations
|
||||
|
||||
Fruix now has a first-class promoted native-build result reference object.
|
||||
|
||||
Current declaration-level entry points are:
|
||||
|
||||
- `promoted-native-build-result`
|
||||
- `operating-system-from-promoted-native-build-result`
|
||||
|
||||
A declaration can now point at a promoted native-build result bundle in `/frx/store`, for example:
|
||||
|
||||
```scheme
|
||||
(define promoted
|
||||
(promoted-native-build-result
|
||||
#:store-path "/frx/store/...-fruix-native-build-result-..."))
|
||||
|
||||
(define os
|
||||
(operating-system-from-promoted-native-build-result
|
||||
promoted
|
||||
...))
|
||||
```
|
||||
|
||||
### Promoted result bundles now drive system composition
|
||||
|
||||
From a promoted result bundle, Fruix now derives:
|
||||
|
||||
- the effective FreeBSD base metadata used by the system declaration
|
||||
- kernel package identity from the promoted kernel artifact store
|
||||
- bootloader package identity from the promoted bootloader artifact store
|
||||
- base-package identity from the promoted world artifact store
|
||||
- optional development-package identity from the promoted headers artifact store
|
||||
|
||||
This means a system declaration can now be based on a promoted native-build result set rather than only on source-driven base-package reconstruction.
|
||||
|
||||
### Closure metadata now records promoted-result provenance explicitly
|
||||
|
||||
When a system declaration uses a promoted native-build result set, Fruix now records that explicitly in the resulting closure metadata.
|
||||
|
||||
New closure metadata includes:
|
||||
|
||||
- `metadata/promoted-native-build-result.scm`
|
||||
- `metadata/store-layout.scm` entries for:
|
||||
- promoted result summary
|
||||
- promoted artifact stores
|
||||
|
||||
The system closure also retains the promoted result bundle itself as an explicit reference in:
|
||||
|
||||
- `/frx/store/...-fruix-system-.../.references`
|
||||
|
||||
So the resulting deployed closure does not merely happen to contain files copied from a promoted artifact; it explicitly records which promoted result set it came from.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
The key plumbing additions were:
|
||||
|
||||
- a promoted native-build result record in:
|
||||
- `modules/fruix/system/freebsd/model.scm`
|
||||
- promoted-result readers/helpers in:
|
||||
- `modules/fruix/system/freebsd/build.scm`
|
||||
- direct system-construction helper:
|
||||
- `operating-system-from-promoted-native-build-result`
|
||||
- support for consuming promoted artifact stores directly when materializing packages
|
||||
- closure/store-layout metadata wiring in:
|
||||
- `modules/fruix/system/freebsd/media.scm`
|
||||
|
||||
The important product-level effect is that promoted native-build results are no longer only post-build artifacts. They are now declaration inputs.
|
||||
|
||||
## Validation harness
|
||||
|
||||
Added:
|
||||
|
||||
- `tests/system/phase20-promoted-native-base-operating-system.scm.in`
|
||||
- `tests/system/run-phase20-promoted-native-base-declaration-xcpng.sh`
|
||||
|
||||
The harness supports two modes:
|
||||
|
||||
- use an explicit `RESULT_STORE=/frx/store/...-fruix-native-build-result-...`
|
||||
- or, if no result store is supplied, first run the validated host-initiated promotion path and consume that result
|
||||
|
||||
## Validation performed
|
||||
|
||||
Validated on the approved real XCP-ng path using the promoted `ssh-guest` result bundle:
|
||||
|
||||
- result bundle:
|
||||
- `/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest`
|
||||
- VM:
|
||||
- `90490f2e-e8fc-4b7a-388e-5c26f0157289`
|
||||
- VDI:
|
||||
- `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
|
||||
|
||||
Passing run:
|
||||
|
||||
- `PASS phase20-promoted-native-base-declaration-xcpng`
|
||||
|
||||
Representative metadata:
|
||||
|
||||
```text
|
||||
closure_path=/frx/store/ac1e6dcfe67d3cde49d4fd5da97740f6244276b4-fruix-system-fruix-freebsd
|
||||
result_store=/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest
|
||||
executor_kind=ssh-guest
|
||||
executor_name=ssh-guest
|
||||
executor_version=1
|
||||
world_store=/frx/store/89c7a71c3df148a1f99b13d57fd6be88243eb2cb-fruix-native-world-15.0-STABLE-ssh-guest
|
||||
kernel_store=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest
|
||||
headers_store=/frx/store/dd7f39f526bca4849caf1eaf96ae25d29b43493c-fruix-native-headers-15.0-STABLE-ssh-guest
|
||||
bootloader_store=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest
|
||||
kernel_link=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest/boot/kernel/kernel
|
||||
bootloader_link=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest/boot/loader.efi
|
||||
guest_kernel_link=/frx/store/93bac81122022b40438d356146a6854b4ee48513-fruix-native-kernel-15.0-STABLE-ssh-guest/boot/kernel/kernel
|
||||
guest_bootloader_link=/frx/store/78b1c6b0b5c0c2c1549f5f42f3d64b6d9293669b-fruix-native-bootloader-15.0-STABLE-ssh-guest/boot/loader.efi
|
||||
guest_uname=FreeBSD 15.0-STABLE
|
||||
promoted_native_base_declaration=ok
|
||||
```
|
||||
|
||||
## Validated facts
|
||||
|
||||
The real-VM validation proved that Fruix can now:
|
||||
|
||||
- consume a promoted native-build result bundle directly from a declaration
|
||||
- materialize a normal system closure from that promoted identity
|
||||
- retain explicit promoted-result provenance in closure metadata
|
||||
- boot the resulting system successfully on the approved XCP-ng path
|
||||
- keep the closure's kernel and bootloader linked directly to the promoted artifact stores
|
||||
- preserve the promoted result-bundle store itself as an explicit closure reference
|
||||
|
||||
So the operator-facing story is now materially better:
|
||||
|
||||
- not only “a build happened somewhere”
|
||||
- but “this system declaration is based on promoted native-base result X”
|
||||
|
||||
## Result
|
||||
|
||||
Promoted native-build result sets are now first-class Fruix declaration inputs.
|
||||
|
||||
That is the point where the native FreeBSD base path stops being only a build/promotion harness and starts acting more like real product behavior.
|
||||
4227
docs/reports/progress-log-through-phase18-2-freebsd.md
Normal file
4227
docs/reports/progress-log-through-phase18-2-freebsd.md
Normal file
File diff suppressed because it is too large
Load Diff
794
docs/system-deployment-workflow.md
Normal file
794
docs/system-deployment-workflow.md
Normal file
@@ -0,0 +1,794 @@
|
||||
# Fruix system deployment workflow
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the current canonical Fruix workflow for:
|
||||
|
||||
- building a declarative system closure
|
||||
- materializing deployable artifacts
|
||||
- installing a declarative system onto an image or disk
|
||||
- booting through installer media
|
||||
- rolling forward to a candidate system
|
||||
- switching an installed system to a staged candidate generation
|
||||
- rolling an installed system back to an earlier recorded generation
|
||||
|
||||
This is now the Phase 19 operator-facing view of the system model as validated through explicit installed-system generation switching and rollback.
|
||||
|
||||
## Core model
|
||||
|
||||
A Fruix system workflow starts from a Scheme file that binds an `operating-system` object.
|
||||
|
||||
Today, the canonical frontend is:
|
||||
|
||||
- `./bin/fruix system ...`
|
||||
|
||||
The important output objects are:
|
||||
|
||||
- **system closure**
|
||||
- a content-addressed store item under `/frx/store/*-fruix-system-<host-name>`
|
||||
- includes boot assets, activation logic, profile tree, metadata, and references
|
||||
- **rootfs tree**
|
||||
- a materialized runtime tree for inspection or image staging
|
||||
- **disk image**
|
||||
- a bootable GPT/UEFI raw disk image
|
||||
- **installer image**
|
||||
- a bootable Fruix installer disk image that installs a selected target system from inside the guest
|
||||
- **installer ISO**
|
||||
- a bootable UEFI ISO with an embedded installer mdroot payload
|
||||
- **install metadata**
|
||||
- `/var/lib/fruix/install.scm` on installed targets
|
||||
- records the selected closure path, install spec, and referenced store items including source provenance
|
||||
|
||||
The current deployment story is therefore already declaration-driven and content-addressed, even before first-class installed-system generations are modeled more explicitly.
|
||||
|
||||
## Canonical command surface
|
||||
|
||||
### Build a system closure
|
||||
|
||||
```sh
|
||||
sudo env HOME="$HOME" \
|
||||
GUILE_AUTO_COMPILE=0 \
|
||||
GUIX_SOURCE_DIR="$HOME/repos/guix" \
|
||||
GUILE_BIN="/tmp/guile-freebsd-validate-install/bin/guile" \
|
||||
GUILE_EXTRA_PREFIX="/tmp/guile-gnutls-freebsd-validate-install" \
|
||||
SHEPHERD_PREFIX="/tmp/shepherd-freebsd-validate-install" \
|
||||
./bin/fruix system build path/to/system.scm --system my-operating-system
|
||||
```
|
||||
|
||||
Primary result:
|
||||
|
||||
- `closure_path=/frx/store/...-fruix-system-...`
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- validate the declarative system composition itself
|
||||
- inspect provenance/layout metadata
|
||||
- compare candidate and current closure paths
|
||||
- drive later rootfs/image/install steps from the same declaration
|
||||
|
||||
### Materialize a rootfs tree
|
||||
|
||||
```sh
|
||||
sudo env HOME="$HOME" ... \
|
||||
./bin/fruix system rootfs path/to/system.scm ./rootfs --system my-operating-system
|
||||
```
|
||||
|
||||
Primary result:
|
||||
|
||||
- `rootfs=...`
|
||||
- `closure_path=/frx/store/...`
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- inspect the runtime filesystem layout directly
|
||||
- stage a tree for debugging
|
||||
- validate `/run/current-system`-style symlink layout without booting a full image
|
||||
|
||||
### Materialize a bootable disk image
|
||||
|
||||
```sh
|
||||
sudo env HOME="$HOME" ... \
|
||||
./bin/fruix system image path/to/system.scm \
|
||||
--system my-operating-system \
|
||||
--root-size 6g
|
||||
```
|
||||
|
||||
Primary result:
|
||||
|
||||
- `disk_image=/frx/store/.../disk.img`
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- boot the system directly as a VM image
|
||||
- test a candidate deployment under QEMU or XCP-ng
|
||||
- validate a roll-forward or rollback candidate by image boot
|
||||
|
||||
### Install directly to an image file or block device
|
||||
|
||||
```sh
|
||||
sudo env HOME="$HOME" ... \
|
||||
./bin/fruix system install path/to/system.scm \
|
||||
--system my-operating-system \
|
||||
--target ./installed.img \
|
||||
--disk-capacity 12g \
|
||||
--root-size 10g
|
||||
```
|
||||
|
||||
Primary result:
|
||||
|
||||
- `target=...`
|
||||
- `target_kind=raw-file` or `block-device`
|
||||
- `install_metadata_path=/var/lib/fruix/install.scm`
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- produce an installed target image without booting an installer guest
|
||||
- validate installation mechanics directly
|
||||
- populate a raw image or a real `/dev/...` target
|
||||
|
||||
### Materialize a bootable installer disk image
|
||||
|
||||
```sh
|
||||
sudo env HOME="$HOME" ... \
|
||||
./bin/fruix system installer path/to/system.scm \
|
||||
--system my-operating-system \
|
||||
--install-target-device /dev/vtbd1 \
|
||||
--root-size 10g
|
||||
```
|
||||
|
||||
Primary result:
|
||||
|
||||
- `installer_disk_image=/frx/store/.../disk.img`
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- boot a Fruix installer environment as a disk image
|
||||
- let the in-guest installer partition and install onto a second disk
|
||||
- validate non-interactive installation from inside a booted Fruix guest
|
||||
|
||||
### Materialize a bootable installer ISO
|
||||
|
||||
```sh
|
||||
sudo env HOME="$HOME" ... \
|
||||
./bin/fruix system installer-iso path/to/system.scm \
|
||||
--system my-operating-system \
|
||||
--install-target-device /dev/vtbd0
|
||||
```
|
||||
|
||||
Primary result:
|
||||
|
||||
- `iso_image=/frx/store/.../installer.iso`
|
||||
- `boot_efi_image=/frx/store/.../efiboot.img`
|
||||
- `root_image=/frx/store/.../root.img`
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- boot through UEFI ISO media instead of a writable installer disk image
|
||||
- install from an ISO-attached Fruix environment
|
||||
- test the same install model on more realistic VM paths
|
||||
|
||||
### Installed-system generation commands
|
||||
|
||||
Installed Fruix systems now also ship a small in-guest deployment helper at:
|
||||
|
||||
- `/usr/local/bin/fruix`
|
||||
|
||||
Current validated in-guest commands are:
|
||||
|
||||
```sh
|
||||
fruix system build
|
||||
fruix system reconfigure
|
||||
fruix system status
|
||||
fruix system switch /frx/store/...-fruix-system-...
|
||||
fruix system rollback
|
||||
```
|
||||
|
||||
Installed systems now carry canonical declaration state in:
|
||||
|
||||
- `/run/current-system/metadata/system-declaration.scm`
|
||||
- `/run/current-system/metadata/system-declaration-info.scm`
|
||||
- `/run/current-system/metadata/system-declaration-system`
|
||||
|
||||
So the in-guest helper can now build from the node's own embedded declaration inputs.
|
||||
|
||||
Current validated build/reconfigure behavior is:
|
||||
|
||||
- `fruix system build`
|
||||
- with no extra arguments, builds from the embedded current declaration
|
||||
- `fruix system reconfigure`
|
||||
- with no extra arguments, builds from the embedded current declaration and stages a switch to the resulting closure
|
||||
- both commands can also take an explicit declaration file plus `--system NAME`
|
||||
|
||||
Current intended usage now has two validated patterns.
|
||||
|
||||
### Pattern A: build elsewhere, then switch/rollback locally
|
||||
|
||||
1. build a candidate closure on the operator side with `./bin/fruix system build`
|
||||
2. ensure that candidate closure is present on the installed target's `/frx/store`
|
||||
3. run `fruix system switch /frx/store/...` on the installed system
|
||||
4. reboot into the staged candidate generation
|
||||
5. if needed, run `fruix system rollback`
|
||||
6. reboot back into the recorded rollback generation
|
||||
|
||||
Important current limitation of this lower-level pattern:
|
||||
|
||||
- `fruix system switch` does **not** yet fetch or copy the candidate closure onto the target for you
|
||||
- it assumes the selected closure is already present in the installed system's `/frx/store`
|
||||
|
||||
### Pattern B: build and reconfigure from the node itself
|
||||
|
||||
1. inspect or edit the node declaration inputs
|
||||
- embedded current declaration, or
|
||||
- an explicit replacement declaration file
|
||||
2. run:
|
||||
|
||||
```sh
|
||||
fruix system build
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```sh
|
||||
fruix system build /path/to/candidate.scm --system my-operating-system
|
||||
```
|
||||
|
||||
3. stage a local generation update with:
|
||||
|
||||
```sh
|
||||
fruix system reconfigure
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```sh
|
||||
fruix system reconfigure /path/to/candidate.scm --system my-operating-system
|
||||
```
|
||||
|
||||
4. reboot into the staged generation
|
||||
5. if needed, run `fruix system rollback`
|
||||
6. reboot back into the recorded prior generation
|
||||
|
||||
### In-guest development and build environments
|
||||
|
||||
Opt-in systems can now expose two separate overlays above the main runtime profile:
|
||||
|
||||
- development:
|
||||
- `/run/current-system/development-profile`
|
||||
- `/run/current-development`
|
||||
- `/usr/local/bin/fruix-development-environment`
|
||||
- build:
|
||||
- `/run/current-system/build-profile`
|
||||
- `/run/current-build`
|
||||
- `/usr/local/bin/fruix-build-environment`
|
||||
|
||||
Intended use:
|
||||
|
||||
```sh
|
||||
eval "$(/usr/local/bin/fruix-development-environment)"
|
||||
```
|
||||
|
||||
for interactive development work, and:
|
||||
|
||||
```sh
|
||||
eval "$(/usr/local/bin/fruix-build-environment)"
|
||||
```
|
||||
|
||||
for a narrower native base-build contract.
|
||||
|
||||
The current split is:
|
||||
|
||||
- runtime profile
|
||||
- development profile
|
||||
- build profile
|
||||
|
||||
The development helper remains intentionally interactive and currently exposes at least:
|
||||
|
||||
- native headers under `usr/include`
|
||||
- FreeBSD `share/mk` files for `bsd.*.mk`
|
||||
- Clang toolchain commands such as `cc`, `c++`, `ar`, `ranlib`, and `nm`
|
||||
- `MAKEFLAGS` pointing at the development profile's `usr/share/mk`
|
||||
|
||||
The build helper is intentionally more sanitized and less interactive. It clears development-shell variables such as:
|
||||
|
||||
- `MAKEFLAGS`
|
||||
- `CPPFLAGS`
|
||||
- `CFLAGS`
|
||||
- `CXXFLAGS`
|
||||
- `LDFLAGS`
|
||||
|
||||
and then exposes build-oriented paths such as:
|
||||
|
||||
- `FRUIX_BUILD_PROFILE`
|
||||
- `FRUIX_BUILD_INCLUDE`
|
||||
- `FRUIX_BUILD_SHARE_MK`
|
||||
- `FRUIX_BUILD_CC`
|
||||
- `FRUIX_BUILD_CXX`
|
||||
- `FRUIX_BUILD_AR`
|
||||
- `FRUIX_BUILD_RANLIB`
|
||||
- `FRUIX_BUILD_NM`
|
||||
- `FRUIX_BMAKE`
|
||||
|
||||
For native base-build compatibility, build-enabled systems now expose canonical links at:
|
||||
|
||||
- `/usr/include -> /run/current-system/build-profile/usr/include`
|
||||
- `/usr/share/mk -> /run/current-system/build-profile/usr/share/mk`
|
||||
|
||||
So Fruix now separates interactive development support from the stricter environment used for `buildworld` / `buildkernel` style work, instead of treating them as one overlay.
|
||||
|
||||
### Host-initiated native base builds inside a Fruix-managed guest
|
||||
|
||||
The currently validated intermediate path toward self-hosting is still host-orchestrated.
|
||||
|
||||
The host:
|
||||
|
||||
1. boots a development-enabled Fruix guest
|
||||
2. connects over SSH
|
||||
3. recovers the materialized FreeBSD source store from system metadata
|
||||
4. runs native FreeBSD build commands inside the guest
|
||||
5. collects and records the staged outputs
|
||||
|
||||
The validated build sequence inside the guest is:
|
||||
|
||||
- `make -jN buildworld`
|
||||
- `make -jN buildkernel`
|
||||
- `make DESTDIR=... installworld`
|
||||
- `make DESTDIR=... distribution`
|
||||
- `make DESTDIR=... installkernel`
|
||||
|
||||
For staged install steps, the validated path uses:
|
||||
|
||||
- `DB_FROM_SRC=yes`
|
||||
|
||||
so the staged install is driven by the declared source tree's account database rather than by accidental guest-local `/etc/master.passwd` contents.
|
||||
|
||||
This is the current Phase 20.2 answer to “where should native base builds run?”
|
||||
|
||||
- **inside** a Fruix-managed FreeBSD environment
|
||||
- but still with the **host** driving the outer orchestration loop
|
||||
|
||||
### Controlled guest self-hosted native-build prototype
|
||||
|
||||
Fruix now also has a narrower in-guest prototype helper at:
|
||||
|
||||
- `/usr/local/bin/fruix-self-hosted-native-build`
|
||||
|
||||
Intended use:
|
||||
|
||||
```sh
|
||||
FRUIX_SELF_HOSTED_NATIVE_BUILD_JOBS=8 \
|
||||
/usr/local/bin/fruix-self-hosted-native-build
|
||||
```
|
||||
|
||||
That helper:
|
||||
|
||||
1. evaluates the build helper and verifies the build overlay plus canonical compatibility links
|
||||
2. recovers the materialized FreeBSD source store from:
|
||||
- `/run/current-system/metadata/store-layout.scm`
|
||||
3. runs the native FreeBSD build/install phases inside the guest
|
||||
4. records staged results under:
|
||||
- `/var/lib/fruix/native-builds/<run-id>`
|
||||
- `/var/lib/fruix/native-builds/latest`
|
||||
5. emits promotion metadata for first-class artifact identities covering:
|
||||
- `world`
|
||||
- `kernel`
|
||||
- `headers`
|
||||
- `bootloader`
|
||||
6. keeps the heavier object/stage work under:
|
||||
- `/var/tmp/fruix-self-hosted-native-builds/<run-id>`
|
||||
|
||||
Important current detail:
|
||||
|
||||
- the self-hosted helper now uses the separate `fruix-build-environment` contract instead of reusing the interactive development helper wholesale
|
||||
- that build helper intentionally clears development-shell exports such as `MAKEFLAGS`, `CPPFLAGS`, `CFLAGS`, `CXXFLAGS`, and `LDFLAGS` before `buildworld`
|
||||
- this keeps the base-build path closer to the exact contract needed for real world/kernel bootstrap work
|
||||
|
||||
So the validated Phase 20.3 answer is:
|
||||
|
||||
- a controlled guest self-hosted base-build prototype now works
|
||||
- but the simpler default operator flow should still be the Phase 20.2 host-initiated in-guest path unless there is a specific reason to push the build loop farther into the guest
|
||||
|
||||
### Promoting native-build results into first-class Fruix store objects
|
||||
|
||||
The guest-side result root is now explicitly a **staging/result area**, not the final immutable identity.
|
||||
|
||||
Current validated flow:
|
||||
|
||||
1. run the in-guest helper so the guest records a result under:
|
||||
- `/var/lib/fruix/native-builds/<run-id>`
|
||||
2. copy that result root back to the host
|
||||
3. run:
|
||||
|
||||
```sh
|
||||
fruix native-build promote RESULT_ROOT
|
||||
```
|
||||
|
||||
The promotion step creates immutable `/frx/store` identities for:
|
||||
|
||||
- `world`
|
||||
- `kernel`
|
||||
- `headers`
|
||||
- `bootloader`
|
||||
|
||||
and also creates a result-bundle store object that references those promoted artifact stores.
|
||||
|
||||
Current metadata split:
|
||||
|
||||
- mutable staging/result root:
|
||||
- `/var/lib/fruix/native-builds/<run-id>`
|
||||
- immutable artifact stores:
|
||||
- `/frx/store/...-fruix-native-world-...`
|
||||
- `/frx/store/...-fruix-native-kernel-...`
|
||||
- `/frx/store/...-fruix-native-headers-...`
|
||||
- `/frx/store/...-fruix-native-bootloader-...`
|
||||
- immutable result bundle:
|
||||
- `/frx/store/...-fruix-native-build-result-...`
|
||||
|
||||
The promoted store objects record explicit Fruix-native metadata including at least:
|
||||
|
||||
- executor kind / name / version
|
||||
- run-id / guest-host-name
|
||||
- closure path
|
||||
- source store provenance
|
||||
- build policy
|
||||
- artifact kind
|
||||
- required-file expectations
|
||||
- recorded content signatures and hashes
|
||||
|
||||
This is the current Fruix-native answer to the question:
|
||||
|
||||
- where should mutable native-build state live?
|
||||
- `/var/lib/fruix/native-builds/...`
|
||||
- where should immutable native-build identity live?
|
||||
- `/frx/store/...`
|
||||
|
||||
### Using promoted native-build results in system declarations
|
||||
|
||||
Fruix system declarations can now refer directly to a promoted native-build result bundle.
|
||||
|
||||
Current declaration-level helpers are:
|
||||
|
||||
- `promoted-native-build-result`
|
||||
- `operating-system-from-promoted-native-build-result`
|
||||
|
||||
Representative pattern:
|
||||
|
||||
```scheme
|
||||
(define promoted
|
||||
(promoted-native-build-result
|
||||
#:store-path "/frx/store/...-fruix-native-build-result-..."))
|
||||
|
||||
(define os
|
||||
(operating-system-from-promoted-native-build-result
|
||||
promoted
|
||||
#:host-name "fruix-freebsd"
|
||||
...))
|
||||
```
|
||||
|
||||
That now gives Fruix a more product-like story:
|
||||
|
||||
1. a build runs under some executor policy
|
||||
2. Fruix records the staged mutable result
|
||||
3. Fruix promotes it into immutable store identities
|
||||
4. a later system declaration can point at that promoted result identity
|
||||
5. Fruix materializes and boots a normal system from that promoted identity
|
||||
|
||||
The resulting closure now records that provenance explicitly through:
|
||||
|
||||
- `metadata/promoted-native-build-result.scm`
|
||||
- `metadata/store-layout.scm`
|
||||
- closure references that retain the selected result-bundle store path
|
||||
|
||||
So the operator-facing statement is now:
|
||||
|
||||
- “this Fruix system is based on promoted native-base result X”
|
||||
|
||||
not only:
|
||||
|
||||
- “some earlier build happened and its files were copied somewhere.”
|
||||
|
||||
### Native-build executor model
|
||||
|
||||
Fruix now has an explicit executor model for native base builds.
|
||||
|
||||
Current executor kinds are:
|
||||
|
||||
- `host`
|
||||
- `ssh-guest`
|
||||
- `self-hosted`
|
||||
|
||||
and the intended future extension points are:
|
||||
|
||||
- `jail`
|
||||
- `remote-builder`
|
||||
|
||||
The important change is architectural:
|
||||
|
||||
- declared source identity stays the same
|
||||
- expected artifact kinds stay the same
|
||||
- result/promotion metadata shape stays the same
|
||||
- only the executor policy changes
|
||||
|
||||
So “where the build runs” is now treated as executor policy rather than as a separate native-build architecture each time.
|
||||
|
||||
Current end-to-end validated executors for the staged-result-plus-promotion model are:
|
||||
|
||||
- `ssh-guest`
|
||||
- `self-hosted`
|
||||
|
||||
Both now converge on the same Fruix-native flow:
|
||||
|
||||
1. run the build under a selected executor
|
||||
2. stage a result root under `/var/lib/fruix/native-builds/...`
|
||||
3. emit the same promotion/provenance shape
|
||||
4. promote the result into immutable `/frx/store/...` objects
|
||||
|
||||
## Deployment patterns
|
||||
|
||||
### 1. Build-first workflow
|
||||
|
||||
The default Fruix operator workflow starts by building the closure first:
|
||||
|
||||
1. edit the system declaration
|
||||
2. run `fruix system build`
|
||||
3. inspect emitted metadata
|
||||
4. if needed, produce one of:
|
||||
- `rootfs`
|
||||
- `image`
|
||||
- `install`
|
||||
- `installer`
|
||||
- `installer-iso`
|
||||
|
||||
This keeps the declaration-to-closure boundary explicit.
|
||||
|
||||
### 2. VM image deployment workflow
|
||||
|
||||
Use this when you want to boot a system directly rather than through an installer.
|
||||
|
||||
1. run `fruix system image`
|
||||
2. boot the image in QEMU or convert/import it for XCP-ng
|
||||
3. validate:
|
||||
- `/run/current-system`
|
||||
- shepherd/sshd state
|
||||
- activation log
|
||||
4. keep the closure path from the build metadata as the deployment identity
|
||||
|
||||
This is the current canonical direct deployment path for already-built images.
|
||||
|
||||
### 3. Direct installation workflow
|
||||
|
||||
Use this when you want an installed target image or disk without a booted installer guest.
|
||||
|
||||
1. run `fruix system install --target ...`
|
||||
2. let Fruix partition, format, populate, and install the target
|
||||
3. boot the installed result
|
||||
4. validate `/var/lib/fruix/install.scm` and target services
|
||||
|
||||
This is the most direct install path.
|
||||
|
||||
### 4. Installer-environment workflow
|
||||
|
||||
Use this when the install itself should happen from inside a booted Fruix environment.
|
||||
|
||||
1. run `fruix system installer`
|
||||
2. boot the installer disk image
|
||||
3. let the in-guest installer run onto the selected target device
|
||||
4. boot the installed target
|
||||
|
||||
This is useful when the installer environment itself is part of what needs validation.
|
||||
|
||||
### 5. Installer-ISO workflow
|
||||
|
||||
Use this when the desired operator artifact is a bootable UEFI ISO.
|
||||
|
||||
1. run `fruix system installer-iso`
|
||||
2. boot the ISO under the target virtualization path
|
||||
3. let the in-guest installer run onto the selected target device
|
||||
4. eject the ISO and reboot the installed target
|
||||
|
||||
This is now validated on both:
|
||||
|
||||
- local `QEMU/UEFI/TCG`
|
||||
- the approved real `XCP-ng` VM path
|
||||
|
||||
## Install-target device conventions
|
||||
|
||||
The install target device is not identical across all boot styles.
|
||||
|
||||
Current validated defaults are:
|
||||
|
||||
- direct installer disk-image path under QEMU:
|
||||
- `/dev/vtbd1`
|
||||
- installer ISO path under QEMU:
|
||||
- `/dev/vtbd0`
|
||||
- installer ISO path under XCP-ng:
|
||||
- `/dev/ada0`
|
||||
|
||||
Therefore the canonical workflow is:
|
||||
|
||||
- always treat `--install-target-device` as an explicit deployment parameter when moving between virtualization environments
|
||||
|
||||
Do not assume that a device name validated in one harness is portable to another.
|
||||
|
||||
## Installed-system generation layout
|
||||
|
||||
Installed Fruix systems now record an explicit first-generation deployment layout under:
|
||||
|
||||
- `/var/lib/fruix/system`
|
||||
|
||||
Initial installed shape:
|
||||
|
||||
```text
|
||||
/var/lib/fruix/system/
|
||||
current -> generations/1
|
||||
current-generation
|
||||
generations/
|
||||
1/
|
||||
closure -> /frx/store/...-fruix-system-...
|
||||
metadata.scm
|
||||
provenance.scm
|
||||
install.scm # present on installed targets
|
||||
```
|
||||
|
||||
After a validated in-place switch, the layout extends to:
|
||||
|
||||
```text
|
||||
/var/lib/fruix/system/
|
||||
current -> generations/2
|
||||
current-generation
|
||||
rollback -> generations/1
|
||||
rollback-generation
|
||||
generations/
|
||||
1/
|
||||
...
|
||||
2/
|
||||
closure -> /frx/store/...-fruix-system-...
|
||||
metadata.scm
|
||||
provenance.scm
|
||||
install.scm # deployment metadata for the switch operation
|
||||
```
|
||||
|
||||
Installed systems also now create explicit GC-root-style deployment links under:
|
||||
|
||||
- `/frx/var/fruix/gcroots`
|
||||
|
||||
Current validated shape:
|
||||
|
||||
```text
|
||||
/frx/var/fruix/gcroots/
|
||||
current-system -> /frx/store/...-fruix-system-...
|
||||
rollback-system -> /frx/store/...-fruix-system-...
|
||||
system-1 -> /frx/store/...-fruix-system-...
|
||||
system-2 -> /frx/store/...-fruix-system-...
|
||||
```
|
||||
|
||||
Important detail:
|
||||
|
||||
- `/run/current-system` still points directly at the active closure path in `/frx/store`
|
||||
- the explicit generation layout therefore adds deployment metadata and retention roots without changing the already-validated runtime contract used by activation, rc.d wiring, and tests
|
||||
|
||||
## Roll-forward workflow
|
||||
|
||||
The current Fruix roll-forward model now has two validated layers.
|
||||
|
||||
### Declaration/deployment roll-forward
|
||||
|
||||
Canonical process:
|
||||
|
||||
1. keep the current known-good system declaration
|
||||
2. prepare a candidate declaration
|
||||
- this may differ by FreeBSD base identity
|
||||
- source revision
|
||||
- services
|
||||
- users/groups
|
||||
- or other operating-system fields
|
||||
3. run `fruix system build` for the candidate
|
||||
4. materialize either:
|
||||
- `fruix system image`
|
||||
- `fruix system install`
|
||||
- `fruix system installer`
|
||||
- `fruix system installer-iso`
|
||||
5. boot or install the candidate
|
||||
6. validate the candidate closure in the booted system
|
||||
|
||||
### Installed-system generation roll-forward
|
||||
|
||||
When the candidate closure is already present on an installed target:
|
||||
|
||||
1. run `fruix system switch /frx/store/...candidate...`
|
||||
2. confirm the staged state with `fruix system status`
|
||||
3. reboot into the candidate generation
|
||||
4. validate the new active closure after reboot
|
||||
|
||||
The important property is still that the candidate closure appears beside the earlier one in `/frx/store` rather than mutating it in place.
|
||||
|
||||
## Rollback workflow
|
||||
|
||||
The current canonical rollback workflow also now has two validated layers.
|
||||
|
||||
### Declaration/deployment rollback
|
||||
|
||||
You can still roll back by redeploying the earlier declaration:
|
||||
|
||||
1. retain the earlier declaration that produced the known-good closure
|
||||
2. rebuild or rematerialize that earlier declaration
|
||||
3. redeploy or reboot that earlier artifact again
|
||||
|
||||
Concretely, the usual declaration-level rollback choices are:
|
||||
|
||||
- rebuild the earlier declaration with `fruix system build` and confirm the old closure path reappears
|
||||
- boot the earlier declaration again through `fruix system image`
|
||||
- reinstall the earlier declaration through `fruix system install`, `installer`, or `installer-iso` if the deployment medium itself must change
|
||||
|
||||
### Installed-system generation rollback
|
||||
|
||||
When an installed target already has both the current and rollback generations recorded:
|
||||
|
||||
1. run `fruix system rollback`
|
||||
2. confirm the staged state with `fruix system status`
|
||||
3. reboot into the rollback generation
|
||||
4. validate the restored active closure after reboot
|
||||
|
||||
This installed-system rollback path is now validated on local `QEMU/UEFI/TCG`.
|
||||
|
||||
### Important scope note
|
||||
|
||||
This is still not yet the same thing as Guix's full `reconfigure`/generation UX.
|
||||
|
||||
Current installed-system rollback is intentionally modest:
|
||||
|
||||
- it switches between already-recorded generations on the target
|
||||
- it does not yet fetch candidate closures onto the machine for you
|
||||
- it does not yet expose a richer history-management or generation-pruning policy
|
||||
|
||||
Still pending:
|
||||
|
||||
- operator-facing closure transfer or fetch onto installed systems
|
||||
- multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers
|
||||
- a fuller `reconfigure`-style installed-system UX
|
||||
|
||||
## Provenance and deployment identity
|
||||
|
||||
For any serious deployment or rollback decision, the canonical identity is not merely the host name. It is the emitted metadata:
|
||||
|
||||
- `closure_path`
|
||||
- declared FreeBSD base/source metadata
|
||||
- materialized source store paths
|
||||
- install metadata at `/var/lib/fruix/install.scm`
|
||||
- store item counts and reference lists
|
||||
|
||||
Operators should retain metadata from successful candidate and current deployments because Fruix already emits enough data to answer:
|
||||
|
||||
- which declaration was built
|
||||
- which closure booted
|
||||
- which source snapshot was materialized
|
||||
- which target device or image was installed
|
||||
|
||||
## Current limitations
|
||||
|
||||
The deployment workflow is now coherent, and Fruix now has a validated installed-system switch/rollback path, but it is still not the final generation-management story.
|
||||
|
||||
Not yet first-class:
|
||||
|
||||
- host-side closure transfer/fetch onto installed systems as part of `fruix system switch`
|
||||
- a fuller `reconfigure` workflow that builds and stages the new closure from inside the target environment
|
||||
- multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers
|
||||
- generation pruning and retention policy independent of full redeploy
|
||||
|
||||
Those are the next logical steps after the current explicit-generation switch/rollback model.
|
||||
|
||||
## Summary
|
||||
|
||||
The current canonical Fruix deployment model is:
|
||||
|
||||
- **declare** a system in Scheme
|
||||
- **build** the closure with `fruix system build`
|
||||
- **materialize** the artifact appropriate to the deployment target
|
||||
- **boot or install** that artifact
|
||||
- **identify deployments by closure path and provenance metadata**
|
||||
- on installed systems, **switch** to a staged candidate with `fruix system switch`
|
||||
- on installed systems, **roll back** to the recorded rollback generation with `fruix system rollback`
|
||||
- still use declaration/redeploy rollback when the target does not already have the desired closure staged locally
|
||||
|
||||
That is the operator-facing workflow Fruix should document and use while its installed-system generation UX remains simpler than Guix's mature in-place system-generation workflow.
|
||||
File diff suppressed because it is too large
Load Diff
933
modules/fruix/system/freebsd/build.scm
Normal file
933
modules/fruix/system/freebsd/build.scm
Normal file
@@ -0,0 +1,933 @@
|
||||
(define-module (fruix system freebsd build)
|
||||
#:use-module (fruix packages freebsd)
|
||||
#:use-module (fruix system freebsd model)
|
||||
#:use-module (fruix system freebsd source)
|
||||
#:use-module (fruix system freebsd executor)
|
||||
#:use-module (fruix system freebsd utils)
|
||||
#:use-module (guix build utils)
|
||||
#:use-module (ice-9 format)
|
||||
#:use-module (ice-9 hash-table)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-13)
|
||||
#:export (host-freebsd-provenance
|
||||
promoted-native-build-result
|
||||
promoted-native-build-result->freebsd-base
|
||||
promoted-native-build-result-artifact-store
|
||||
promoted-native-build-result-kernel-package
|
||||
promoted-native-build-result-bootloader-package
|
||||
promoted-native-build-result-base-packages
|
||||
promoted-native-build-result-development-packages
|
||||
operating-system-from-promoted-native-build-result
|
||||
materialize-freebsd-package
|
||||
promote-native-build-result
|
||||
materialize-prefix))
|
||||
|
||||
(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 native-freebsd-build-version "1")
|
||||
|
||||
(define (freebsd-native-build-system? build-system)
|
||||
(not (not (memq build-system '(freebsd-kernel-build-system freebsd-world-build-system)))))
|
||||
|
||||
(define (build-plan-ref plan key default)
|
||||
(match (assoc key plan)
|
||||
((_ . value) value)
|
||||
(#f default)))
|
||||
|
||||
(define (make-flag->pair flag)
|
||||
(match (string-split flag #\=)
|
||||
((name value ...) (cons name (string-join value "=")))
|
||||
((name) (cons name "yes"))
|
||||
(_ (error (format #f "invalid make flag: ~a" flag)))))
|
||||
|
||||
(define (native-build-kernconf-path plan)
|
||||
(or (build-plan-ref plan 'kernconf-path #f)
|
||||
(string-append (build-plan-ref plan 'source-root "/usr/src")
|
||||
"/sys/"
|
||||
(build-plan-ref plan 'target-arch "amd64")
|
||||
"/conf/"
|
||||
(build-plan-ref plan 'kernconf "GENERIC"))))
|
||||
|
||||
(define (native-build-common-manifest plan)
|
||||
(let* ((source-root (build-plan-ref plan 'source-root "/usr/src"))
|
||||
(target (build-plan-ref plan 'target "amd64"))
|
||||
(target-arch (build-plan-ref plan 'target-arch "amd64"))
|
||||
(kernconf (build-plan-ref plan 'kernconf "GENERIC"))
|
||||
(make-flags (build-plan-ref plan 'make-flags '()))
|
||||
(kernconf-path (native-build-kernconf-path plan)))
|
||||
(unless (file-exists? source-root)
|
||||
(error (format #f "native FreeBSD source root does not exist: ~a" source-root)))
|
||||
(unless (file-exists? kernconf-path)
|
||||
(error (format #f "native FreeBSD kernconf does not exist: ~a" kernconf-path)))
|
||||
`((build-version . ,native-freebsd-build-version)
|
||||
(source-root . ,source-root)
|
||||
(source-tree-identity-mode . "mtree:type,link,size,mode,sha256digest")
|
||||
(source-tree-sha256 . ,(or (build-plan-ref plan 'materialized-source-tree-sha256 #f)
|
||||
(native-build-source-tree-sha256 source-root)))
|
||||
(target . ,target)
|
||||
(target-arch . ,target-arch)
|
||||
(kernconf . ,kernconf)
|
||||
(kernconf-path . ,kernconf-path)
|
||||
(kernconf-sha256 . ,(file-hash kernconf-path))
|
||||
(make-flags . ,make-flags))))
|
||||
|
||||
(define (native-build-declared-base plan)
|
||||
`((name . ,(build-plan-ref plan 'base-name "default"))
|
||||
(version-label . ,(build-plan-ref plan 'base-version-label freebsd-release))
|
||||
(release . ,(build-plan-ref plan 'base-release freebsd-release))
|
||||
(branch . ,(build-plan-ref plan 'base-branch "unknown"))))
|
||||
|
||||
(define (native-build-declared-source plan)
|
||||
`((name . ,(build-plan-ref plan 'base-source-name "default"))
|
||||
(kind . ,(build-plan-ref plan 'base-source-kind 'local-tree))
|
||||
(url . ,(build-plan-ref plan 'base-source-url #f))
|
||||
(path . ,(build-plan-ref plan 'base-source-path #f))
|
||||
(ref . ,(build-plan-ref plan 'base-source-ref #f))
|
||||
(commit . ,(build-plan-ref plan 'base-source-commit #f))
|
||||
(sha256 . ,(build-plan-ref plan 'base-source-sha256 #f))))
|
||||
|
||||
(define (native-build-materialized-source plan)
|
||||
`((store-path . ,(build-plan-ref plan 'materialized-source-store #f))
|
||||
(source-root . ,(build-plan-ref plan 'source-root "/usr/src"))
|
||||
(info-file . ,(build-plan-ref plan 'materialized-source-info-file #f))
|
||||
(tree-sha256 . ,(build-plan-ref plan 'materialized-source-tree-sha256 #f))
|
||||
(cache-path . ,(build-plan-ref plan 'materialized-source-cache-path #f))
|
||||
(effective-source . ((kind . ,(build-plan-ref plan 'effective-source-kind #f))
|
||||
(url . ,(build-plan-ref plan 'effective-source-url #f))
|
||||
(path . ,(build-plan-ref plan 'effective-source-path #f))
|
||||
(ref . ,(build-plan-ref plan 'effective-source-ref #f))
|
||||
(commit . ,(build-plan-ref plan 'effective-source-commit #f))
|
||||
(sha256 . ,(build-plan-ref plan 'effective-source-sha256 #f))))))
|
||||
|
||||
(define (native-build-manifest-string package input-paths)
|
||||
(let* ((plan (freebsd-package-install-plan package))
|
||||
(common (native-build-common-manifest plan))
|
||||
(declared-base (native-build-declared-base plan))
|
||||
(declared-source (native-build-declared-source plan))
|
||||
(materialized-source (native-build-materialized-source plan))
|
||||
(keep-paths (build-plan-ref plan 'keep-paths '()))
|
||||
(prune-paths (build-plan-ref plan 'prune-paths '())))
|
||||
(string-append
|
||||
"name=" (freebsd-package-name package) "\n"
|
||||
"version=" (freebsd-package-version package) "\n"
|
||||
"build-system=" (symbol->string (freebsd-package-build-system package)) "\n"
|
||||
"inputs=" (string-join input-paths ",") "\n"
|
||||
"declared-base=\n"
|
||||
(object->string declared-base)
|
||||
"\ndeclared-source=\n"
|
||||
(object->string declared-source)
|
||||
"\nmaterialized-source=\n"
|
||||
(object->string materialized-source)
|
||||
"\nnative-build-common=\n"
|
||||
(object->string common)
|
||||
"\nkeep-paths=\n"
|
||||
(object->string keep-paths)
|
||||
"\nprune-paths=\n"
|
||||
(object->string prune-paths))))
|
||||
|
||||
(define (copy-build-manifest-string package input-paths)
|
||||
(string-append
|
||||
"name=" (freebsd-package-name package) "\n"
|
||||
"version=" (freebsd-package-version package) "\n"
|
||||
"build-system=" (symbol->string (freebsd-package-build-system package)) "\n"
|
||||
"inputs=" (string-join input-paths ",") "\n"
|
||||
"install-plan-signature=\n"
|
||||
(string-join (map install-plan-signature
|
||||
(freebsd-package-install-plan package))
|
||||
"\n")))
|
||||
|
||||
(define (package-manifest-string package input-paths)
|
||||
(if (freebsd-native-build-system? (freebsd-package-build-system package))
|
||||
(native-build-manifest-string package input-paths)
|
||||
(copy-build-manifest-string package input-paths)))
|
||||
|
||||
(define (current-build-jobs)
|
||||
(or (getenv "FRUIX_FREEBSD_BUILD_JOBS")
|
||||
(safe-command-output "sysctl" "-n" "hw.ncpu")
|
||||
"1"))
|
||||
|
||||
(define (native-build-root common)
|
||||
(string-append "/var/tmp/fruix-freebsd-native-build-"
|
||||
(sha256-string (object->string common))))
|
||||
|
||||
(define (native-make-arguments common _build-root)
|
||||
(append
|
||||
(list "-C" (assoc-ref common 'source-root)
|
||||
(string-append "TARGET=" (assoc-ref common 'target))
|
||||
(string-append "TARGET_ARCH=" (assoc-ref common 'target-arch))
|
||||
(string-append "KERNCONF=" (assoc-ref common 'kernconf)))
|
||||
(assoc-ref common 'make-flags)))
|
||||
|
||||
(define* (make-command-string common build-root target #:key (parallel? #f) (destdir #f))
|
||||
(string-join
|
||||
(append
|
||||
(list "env" (string-append "MAKEOBJDIRPREFIX=" build-root "/obj") "make")
|
||||
(if parallel?
|
||||
(list (string-append "-j" (current-build-jobs)))
|
||||
'())
|
||||
(native-make-arguments common build-root)
|
||||
(if destdir
|
||||
(list (string-append "DESTDIR=" destdir))
|
||||
'())
|
||||
(list target))
|
||||
" "))
|
||||
|
||||
(define (run-command/log log-file command)
|
||||
(mkdir-p (dirname log-file))
|
||||
(let ((status (system* "sh" "-c" (string-append command " >" log-file " 2>&1"))))
|
||||
(unless (zero? status)
|
||||
(error (format #f "command failed; see ~a: ~a" log-file command)))))
|
||||
|
||||
(define (ensure-native-build-root common build-root)
|
||||
(mkdir-p build-root)
|
||||
(mkdir-p (string-append build-root "/logs"))
|
||||
(mkdir-p (string-append build-root "/stamps"))
|
||||
(write-file (string-append build-root "/build-parameters.scm")
|
||||
(object->string common)))
|
||||
|
||||
(define (ensure-native-buildworld common build-root)
|
||||
(let ((stamp (string-append build-root "/stamps/buildworld.done")))
|
||||
(ensure-native-build-root common build-root)
|
||||
(unless (file-exists? stamp)
|
||||
(run-command/log (string-append build-root "/logs/buildworld.log")
|
||||
(make-command-string common build-root "buildworld" #:parallel? #t))
|
||||
(write-file stamp "ok\n"))))
|
||||
|
||||
(define (ensure-native-buildkernel common build-root)
|
||||
(let ((stamp (string-append build-root "/stamps/buildkernel-" (assoc-ref common 'kernconf) ".done")))
|
||||
(ensure-native-buildworld common build-root)
|
||||
(unless (file-exists? stamp)
|
||||
(run-command/log (string-append build-root "/logs/buildkernel-" (assoc-ref common 'kernconf) ".log")
|
||||
(make-command-string common build-root "buildkernel" #:parallel? #t))
|
||||
(write-file stamp "ok\n"))))
|
||||
|
||||
(define (prune-stage-paths stage-root paths)
|
||||
(for-each (lambda (path)
|
||||
(delete-path-if-exists (string-append stage-root "/" path)))
|
||||
paths))
|
||||
|
||||
(define (select-stage-paths stage-root paths)
|
||||
(let ((selected-root (string-append stage-root ".selected")))
|
||||
(delete-path-if-exists selected-root)
|
||||
(mkdir-p selected-root)
|
||||
(for-each (lambda (path)
|
||||
(let ((source (string-append stage-root "/" path))
|
||||
(target (string-append selected-root "/" path)))
|
||||
(unless (or (file-exists? source)
|
||||
(false-if-exception (readlink source)))
|
||||
(error (format #f "native stage path is missing: ~a" source)))
|
||||
(copy-node source target)))
|
||||
paths)
|
||||
selected-root))
|
||||
(define (native-build-output-metadata package common build-root stage-root)
|
||||
(let ((plan (freebsd-package-install-plan package)))
|
||||
`((package . ,(freebsd-package-name package))
|
||||
(version . ,(freebsd-package-version package))
|
||||
(declared-base . ,(native-build-declared-base plan))
|
||||
(declared-source . ,(native-build-declared-source plan))
|
||||
(materialized-source . ,(native-build-materialized-source plan))
|
||||
(build-system . ,(freebsd-package-build-system package))
|
||||
(source-root . ,(assoc-ref common 'source-root))
|
||||
(source-tree-sha256 . ,(assoc-ref common 'source-tree-sha256))
|
||||
(target . ,(assoc-ref common 'target))
|
||||
(target-arch . ,(assoc-ref common 'target-arch))
|
||||
(kernconf . ,(assoc-ref common 'kernconf))
|
||||
(kernconf-path . ,(assoc-ref common 'kernconf-path))
|
||||
(kernconf-sha256 . ,(assoc-ref common 'kernconf-sha256))
|
||||
(make-flags . ,(assoc-ref common 'make-flags))
|
||||
(keep-paths . ,(build-plan-ref plan 'keep-paths '()))
|
||||
(prune-paths . ,(build-plan-ref plan 'prune-paths '()))
|
||||
(build-root . ,build-root)
|
||||
(stage-root . ,stage-root)
|
||||
(buildworld-log . ,(string-append build-root "/logs/buildworld.log"))
|
||||
(buildkernel-log . ,(string-append build-root "/logs/buildkernel-" (assoc-ref common 'kernconf) ".log"))
|
||||
(install-log . ,(string-append build-root "/logs/install-" (freebsd-package-name package) ".log")))))
|
||||
|
||||
(define (materialize-native-freebsd-package package input-paths manifest output-path)
|
||||
(let* ((plan (freebsd-package-install-plan package))
|
||||
(common (native-build-common-manifest plan))
|
||||
(build-root (native-build-root common))
|
||||
(stage-root (string-append build-root "/stage-" (freebsd-package-name package) "-" (sha256-string manifest)))
|
||||
(install-log (string-append build-root "/logs/install-" (freebsd-package-name package) ".log"))
|
||||
(final-stage-root
|
||||
(case (freebsd-package-build-system package)
|
||||
((freebsd-world-build-system)
|
||||
(ensure-native-buildworld common build-root)
|
||||
(delete-path-if-exists stage-root)
|
||||
(mkdir-p stage-root)
|
||||
(run-command/log install-log
|
||||
(string-append (make-command-string common build-root "installworld" #:destdir stage-root)
|
||||
" && "
|
||||
(make-command-string common build-root "distribution" #:destdir stage-root)))
|
||||
(let* ((keep-paths (build-plan-ref plan 'keep-paths '()))
|
||||
(selected-root (if (null? keep-paths)
|
||||
stage-root
|
||||
(select-stage-paths stage-root keep-paths))))
|
||||
(prune-stage-paths selected-root (build-plan-ref plan 'prune-paths '()))
|
||||
selected-root))
|
||||
((freebsd-kernel-build-system)
|
||||
(ensure-native-buildkernel common build-root)
|
||||
(delete-path-if-exists stage-root)
|
||||
(mkdir-p stage-root)
|
||||
(run-command/log install-log
|
||||
(make-command-string common build-root "installkernel" #:destdir stage-root))
|
||||
stage-root)
|
||||
(else
|
||||
(error (format #f "unsupported native FreeBSD build system: ~a"
|
||||
(freebsd-package-build-system package)))))))
|
||||
(mkdir-p output-path)
|
||||
(stage-tree-into-output final-stage-root output-path)
|
||||
(write-file (string-append output-path "/.references")
|
||||
(string-join input-paths "\n"))
|
||||
(write-file (string-append output-path "/.fruix-package") manifest)
|
||||
(write-file (string-append output-path "/.freebsd-native-build-info.scm")
|
||||
(object->string (native-build-output-metadata package common build-root final-stage-root)))))
|
||||
|
||||
(define (package-with-install-plan package install-plan)
|
||||
(freebsd-package
|
||||
#:name (freebsd-package-name package)
|
||||
#:version (freebsd-package-version package)
|
||||
#:build-system (freebsd-package-build-system package)
|
||||
#:inputs (freebsd-package-inputs package)
|
||||
#:home-page (freebsd-package-home-page package)
|
||||
#:synopsis (freebsd-package-synopsis package)
|
||||
#:description (freebsd-package-description package)
|
||||
#:license (freebsd-package-license package)
|
||||
#:install-plan install-plan))
|
||||
|
||||
(define (plan-freebsd-source plan)
|
||||
(freebsd-source #:name (build-plan-ref plan 'base-source-name "default")
|
||||
#:kind (build-plan-ref plan 'base-source-kind 'local-tree)
|
||||
#:url (build-plan-ref plan 'base-source-url #f)
|
||||
#:path (build-plan-ref plan 'base-source-path #f)
|
||||
#:ref (build-plan-ref plan 'base-source-ref #f)
|
||||
#:commit (build-plan-ref plan 'base-source-commit #f)
|
||||
#:sha256 (build-plan-ref plan 'base-source-sha256 #f)))
|
||||
|
||||
(define (source-cache-key source)
|
||||
(sha256-string (object->string (freebsd-source-spec source))))
|
||||
|
||||
(define (materialize-freebsd-source/cached source store-dir source-cache)
|
||||
(let* ((key (source-cache-key source))
|
||||
(cached (hash-ref source-cache key #f)))
|
||||
(or cached
|
||||
(let ((result (materialize-freebsd-source source #:store-dir store-dir)))
|
||||
(hash-set! source-cache key result)
|
||||
result))))
|
||||
|
||||
(define (plan-with-materialized-source plan source-result)
|
||||
(let* ((effective (assoc-ref source-result 'effective-source))
|
||||
(overrides
|
||||
`((source-root . ,(assoc-ref source-result 'source-root))
|
||||
(materialized-source-store . ,(assoc-ref source-result 'source-store-path))
|
||||
(materialized-source-info-file . ,(assoc-ref source-result 'source-info-file))
|
||||
(materialized-source-tree-sha256 . ,(assoc-ref source-result 'source-tree-sha256))
|
||||
(materialized-source-cache-path . ,(assoc-ref source-result 'cache-path))
|
||||
(effective-source-kind . ,(assoc-ref effective 'kind))
|
||||
(effective-source-url . ,(assoc-ref effective 'url))
|
||||
(effective-source-path . ,(assoc-ref effective 'path))
|
||||
(effective-source-ref . ,(assoc-ref effective 'ref))
|
||||
(effective-source-commit . ,(assoc-ref effective 'commit))
|
||||
(effective-source-sha256 . ,(assoc-ref effective 'sha256)))))
|
||||
(append overrides plan)))
|
||||
|
||||
(define* (materialize-freebsd-package package store-dir cache #:optional source-cache)
|
||||
(if (existing-store-package? package)
|
||||
(validate-existing-store-package package)
|
||||
(let* ((source-cache (or source-cache (make-hash-table)))
|
||||
(input-paths (map (lambda (input)
|
||||
(materialize-freebsd-package input store-dir cache source-cache))
|
||||
(freebsd-package-inputs package)))
|
||||
(prepared-package
|
||||
(if (freebsd-native-build-package? package)
|
||||
(let* ((source (plan-freebsd-source (freebsd-package-install-plan package)))
|
||||
(source-result (materialize-freebsd-source/cached source store-dir source-cache))
|
||||
(plan (plan-with-materialized-source (freebsd-package-install-plan package)
|
||||
source-result)))
|
||||
(package-with-install-plan package plan))
|
||||
package))
|
||||
(effective-input-paths
|
||||
(if (freebsd-native-build-package? package)
|
||||
(cons (build-plan-ref (freebsd-package-install-plan prepared-package)
|
||||
'materialized-source-store
|
||||
#f)
|
||||
input-paths)
|
||||
input-paths))
|
||||
(effective-input-paths (filter identity effective-input-paths))
|
||||
(manifest (package-manifest-string prepared-package effective-input-paths))
|
||||
(cache-key (sha256-string manifest))
|
||||
(cached (hash-ref cache cache-key #f)))
|
||||
(if cached
|
||||
cached
|
||||
(let* ((display-name (string-append (freebsd-package-name prepared-package)
|
||||
"-"
|
||||
(freebsd-package-version prepared-package)))
|
||||
(output-path (make-store-path store-dir display-name manifest
|
||||
#:kind 'freebsd-package)))
|
||||
(unless (file-exists? output-path)
|
||||
(case (freebsd-package-build-system prepared-package)
|
||||
((copy-build-system)
|
||||
(mkdir-p output-path)
|
||||
(for-each (lambda (entry)
|
||||
(materialize-plan-entry output-path entry))
|
||||
(freebsd-package-install-plan prepared-package))
|
||||
(write-file (string-append output-path "/.references")
|
||||
(string-join effective-input-paths "\n"))
|
||||
(write-file (string-append output-path "/.fruix-package") manifest))
|
||||
((freebsd-world-build-system freebsd-kernel-build-system)
|
||||
(materialize-native-freebsd-package prepared-package effective-input-paths manifest output-path))
|
||||
(else
|
||||
(error (format #f "unsupported package build system: ~a"
|
||||
(freebsd-package-build-system prepared-package))))))
|
||||
(hash-set! cache cache-key output-path)
|
||||
output-path)))))
|
||||
|
||||
|
||||
(define native-build-result-promotion-version "1")
|
||||
|
||||
(define (native-build-result-ref data key default)
|
||||
(match (assoc key data)
|
||||
((_ . value) value)
|
||||
(#f default)))
|
||||
|
||||
(define (native-build-result-executor result)
|
||||
(let* ((executor (native-build-result-ref result 'executor #f))
|
||||
(legacy-version (native-build-result-ref result 'executor-version "legacy")))
|
||||
(cond
|
||||
((native-build-executor? executor)
|
||||
executor)
|
||||
((string? executor)
|
||||
(let ((normalized (normalize-native-build-executor executor)))
|
||||
`((kind . ,(native-build-executor-kind normalized))
|
||||
(name . ,(native-build-executor-name normalized))
|
||||
(version . ,legacy-version)
|
||||
(properties . ,(native-build-executor-properties normalized)))))
|
||||
(else
|
||||
(native-build-executor #:kind 'unknown
|
||||
#:name "unknown"
|
||||
#:version legacy-version)))))
|
||||
|
||||
(define (native-build-result-executor-kind result)
|
||||
(native-build-executor-kind (native-build-result-executor result)))
|
||||
|
||||
(define (native-build-result-executor-name result)
|
||||
(native-build-executor-name (native-build-result-executor result)))
|
||||
|
||||
(define (native-build-result-executor-version result)
|
||||
(native-build-executor-version (native-build-result-executor result)))
|
||||
|
||||
(define (read-native-build-result result-root)
|
||||
(let ((promotion-file (string-append result-root "/promotion.scm")))
|
||||
(unless (file-exists? promotion-file)
|
||||
(error "native build result is missing promotion.scm" result-root))
|
||||
(let ((result (call-with-input-file promotion-file read)))
|
||||
(unless (equal? (native-build-result-ref result 'native-build-result-version #f)
|
||||
native-build-result-promotion-version)
|
||||
(error "unsupported native build result promotion version" promotion-file))
|
||||
result)))
|
||||
|
||||
(define existing-store-package-build-system 'existing-store-item-build-system)
|
||||
|
||||
(define (existing-store-package? package)
|
||||
(eq? (freebsd-package-build-system package)
|
||||
existing-store-package-build-system))
|
||||
|
||||
(define (existing-store-package-ref package key default)
|
||||
(build-plan-ref (freebsd-package-install-plan package) key default))
|
||||
|
||||
(define (validate-existing-store-package package)
|
||||
(let* ((store-path (existing-store-package-ref package 'store-path #f))
|
||||
(required-file (existing-store-package-ref package 'required-file #f))
|
||||
(metadata-file (existing-store-package-ref package 'metadata-file #f)))
|
||||
(unless (and (string? store-path) (file-exists? store-path))
|
||||
(error "existing-store package is missing a valid store path" package store-path))
|
||||
(when metadata-file
|
||||
(unless (file-exists? metadata-file)
|
||||
(error "existing-store package metadata file is missing" package metadata-file)))
|
||||
(when required-file
|
||||
(unless (file-exists? (string-append store-path "/" required-file))
|
||||
(error "existing-store package is missing required file"
|
||||
package
|
||||
(string-append store-path "/" required-file))))
|
||||
store-path))
|
||||
|
||||
(define (normalize-promoted-native-build-result value)
|
||||
(cond
|
||||
((promoted-native-build-result? value)
|
||||
value)
|
||||
((string? value)
|
||||
(promoted-native-build-result #:store-path value))
|
||||
(else
|
||||
(error "expected a promoted native build result or store path" value))))
|
||||
|
||||
(define (read-promoted-native-build-artifact-metadata metadata-file)
|
||||
(unless (file-exists? metadata-file)
|
||||
(error "promoted native build artifact metadata file is missing" metadata-file))
|
||||
(let ((metadata (call-with-input-file metadata-file read)))
|
||||
(unless (equal? (native-build-result-ref metadata 'native-build-object-version #f)
|
||||
native-build-result-promotion-version)
|
||||
(error "unsupported promoted native build object version" metadata-file))
|
||||
(unless (eq? (native-build-result-ref metadata 'object-kind #f) 'artifact)
|
||||
(error "promoted native build object is not an artifact" metadata-file))
|
||||
metadata))
|
||||
|
||||
(define (promoted-native-build-result-artifact-entry result artifact-kind)
|
||||
(let* ((metadata (promoted-native-build-result-metadata result))
|
||||
(artifacts (native-build-result-ref metadata 'artifacts '()))
|
||||
(entry (find (lambda (item)
|
||||
(eq? (native-build-result-ref item 'artifact-kind #f)
|
||||
artifact-kind))
|
||||
artifacts)))
|
||||
(unless entry
|
||||
(error "promoted native build result is missing artifact entry" artifact-kind))
|
||||
entry))
|
||||
|
||||
(define (promoted-native-build-result-artifact-store result artifact-kind)
|
||||
(let* ((result (normalize-promoted-native-build-result result))
|
||||
(entry (promoted-native-build-result-artifact-entry result artifact-kind))
|
||||
(store-path (native-build-result-ref entry 'store-path #f))
|
||||
(metadata-file (native-build-result-ref entry 'metadata-file #f))
|
||||
(artifact-metadata (and metadata-file
|
||||
(read-promoted-native-build-artifact-metadata metadata-file)))
|
||||
(required-file (and artifact-metadata
|
||||
(native-build-result-ref artifact-metadata 'required-file #f))))
|
||||
(unless (and (string? store-path) (file-exists? store-path))
|
||||
(error "promoted native build result is missing artifact store" artifact-kind store-path))
|
||||
(when artifact-metadata
|
||||
(unless (eq? (native-build-result-ref artifact-metadata 'artifact-kind #f)
|
||||
artifact-kind)
|
||||
(error "promoted native build artifact metadata kind mismatch"
|
||||
artifact-kind
|
||||
metadata-file)))
|
||||
(when required-file
|
||||
(unless (file-exists? (string-append store-path "/" required-file))
|
||||
(error "promoted native build artifact store is missing required file"
|
||||
artifact-kind
|
||||
(string-append store-path "/" required-file))))
|
||||
store-path))
|
||||
|
||||
(define* (promoted-native-build-result #:key store-path)
|
||||
(unless (and (string? store-path) (file-exists? store-path))
|
||||
(error "promoted native build result store path does not exist" store-path))
|
||||
(let* ((metadata-file (string-append store-path "/.fruix-native-build-result.scm")))
|
||||
(unless (file-exists? metadata-file)
|
||||
(error "promoted native build result store is missing metadata" metadata-file))
|
||||
(let* ((metadata (call-with-input-file metadata-file read))
|
||||
(result (make-promoted-native-build-result store-path metadata-file metadata)))
|
||||
(unless (equal? (native-build-result-ref metadata 'native-build-result-version #f)
|
||||
native-build-result-promotion-version)
|
||||
(error "unsupported promoted native build result version" metadata-file))
|
||||
(unless (eq? (native-build-result-ref metadata 'object-kind #f) 'result-bundle)
|
||||
(error "promoted native build result store does not contain a result bundle" metadata-file))
|
||||
(for-each (lambda (artifact-kind)
|
||||
(promoted-native-build-result-artifact-store result artifact-kind))
|
||||
'(world kernel headers bootloader))
|
||||
result)))
|
||||
|
||||
(define (promoted-native-build-result->freebsd-base result)
|
||||
(let* ((result (normalize-promoted-native-build-result result))
|
||||
(metadata (promoted-native-build-result-metadata result))
|
||||
(base (native-build-result-ref metadata 'freebsd-base '()))
|
||||
(source (native-build-result-ref metadata 'source '()))
|
||||
(source-root (or (native-build-result-ref source 'source-root #f)
|
||||
(native-build-result-ref base 'source-root #f)
|
||||
"/usr/src"))
|
||||
(source-name (string-append "promoted-native-build-result-source-"
|
||||
(path-basename
|
||||
(promoted-native-build-result-store-path result))))
|
||||
(synthetic-source (freebsd-source #:name source-name
|
||||
#:kind 'local-tree
|
||||
#:path source-root)))
|
||||
(freebsd-base #:name (native-build-result-ref base 'name "promoted-native-build-result")
|
||||
#:version-label (native-build-result-ref base 'version-label "unknown")
|
||||
#:release (native-build-result-ref base 'release "unknown")
|
||||
#:branch (native-build-result-ref base 'branch "unknown")
|
||||
#:source synthetic-source
|
||||
#:source-root (native-build-result-ref base 'source-root source-root)
|
||||
#:target (native-build-result-ref base 'target "amd64")
|
||||
#:target-arch (native-build-result-ref base 'target-arch "amd64")
|
||||
#:kernconf (native-build-result-ref base 'kernconf "GENERIC"))))
|
||||
|
||||
(define (promoted-native-build-result-artifact-package result artifact-kind
|
||||
package-name synopsis description)
|
||||
(let* ((result (normalize-promoted-native-build-result result))
|
||||
(metadata (promoted-native-build-result-metadata result))
|
||||
(base (native-build-result-ref metadata 'freebsd-base '()))
|
||||
(entry (promoted-native-build-result-artifact-entry result artifact-kind))
|
||||
(store-path (promoted-native-build-result-artifact-store result artifact-kind))
|
||||
(metadata-file (native-build-result-ref entry 'metadata-file #f))
|
||||
(artifact-metadata (and metadata-file
|
||||
(read-promoted-native-build-artifact-metadata metadata-file)))
|
||||
(required-file (and artifact-metadata
|
||||
(native-build-result-ref artifact-metadata 'required-file #f))))
|
||||
(freebsd-package
|
||||
#:name package-name
|
||||
#:version (native-build-result-ref base 'version-label "unknown")
|
||||
#:build-system existing-store-package-build-system
|
||||
#:home-page "https://www.freebsd.org/"
|
||||
#:synopsis synopsis
|
||||
#:description description
|
||||
#:license 'bsd-2
|
||||
#:install-plan `((store-path . ,store-path)
|
||||
(metadata-file . ,metadata-file)
|
||||
(required-file . ,required-file)
|
||||
(artifact-kind . ,artifact-kind)
|
||||
(result-store . ,(promoted-native-build-result-store-path result))))))
|
||||
|
||||
(define (promoted-native-build-result-world-package result)
|
||||
(promoted-native-build-result-artifact-package
|
||||
result
|
||||
'world
|
||||
"freebsd-promoted-world"
|
||||
"Promoted Fruix-native FreeBSD world artifact"
|
||||
"FreeBSD world artifact imported from a promoted Fruix native-build result bundle."))
|
||||
|
||||
(define (promoted-native-build-result-kernel-package result)
|
||||
(promoted-native-build-result-artifact-package
|
||||
result
|
||||
'kernel
|
||||
"freebsd-promoted-kernel"
|
||||
"Promoted Fruix-native FreeBSD kernel artifact"
|
||||
"FreeBSD kernel artifact imported from a promoted Fruix native-build result bundle."))
|
||||
|
||||
(define (promoted-native-build-result-bootloader-package result)
|
||||
(promoted-native-build-result-artifact-package
|
||||
result
|
||||
'bootloader
|
||||
"freebsd-promoted-bootloader"
|
||||
"Promoted Fruix-native FreeBSD bootloader artifact"
|
||||
"FreeBSD bootloader artifact imported from a promoted Fruix native-build result bundle."))
|
||||
|
||||
(define (promoted-native-build-result-headers-package result)
|
||||
(promoted-native-build-result-artifact-package
|
||||
result
|
||||
'headers
|
||||
"freebsd-promoted-headers"
|
||||
"Promoted Fruix-native FreeBSD headers artifact"
|
||||
"FreeBSD headers artifact imported from a promoted Fruix native-build result bundle."))
|
||||
|
||||
(define (promoted-native-build-result-base-packages result)
|
||||
(list (promoted-native-build-result-world-package result)))
|
||||
|
||||
(define (promoted-native-build-result-development-packages result)
|
||||
(list (promoted-native-build-result-headers-package result)))
|
||||
|
||||
(define* (operating-system-from-promoted-native-build-result result
|
||||
#:key
|
||||
(host-name #f)
|
||||
(freebsd-base #f)
|
||||
(kernel #f)
|
||||
(bootloader #f)
|
||||
(base-packages #f)
|
||||
(development-packages #f)
|
||||
(users #f)
|
||||
(groups #f)
|
||||
(file-systems #f)
|
||||
(services #f)
|
||||
(loader-entries #f)
|
||||
(rc-conf-entries #f)
|
||||
(init-mode #f)
|
||||
(ready-marker #f)
|
||||
(root-authorized-keys #f))
|
||||
(let* ((result (normalize-promoted-native-build-result result))
|
||||
(defaults default-minimal-operating-system)
|
||||
(fallback (lambda (value thunk)
|
||||
(if (eq? value #f) (thunk) value))))
|
||||
(operating-system
|
||||
#:host-name (fallback host-name (lambda () (operating-system-host-name defaults)))
|
||||
#:freebsd-base (fallback freebsd-base (lambda ()
|
||||
(promoted-native-build-result->freebsd-base result)))
|
||||
#:native-build-result result
|
||||
#:kernel (fallback kernel (lambda ()
|
||||
(promoted-native-build-result-kernel-package result)))
|
||||
#:bootloader (fallback bootloader (lambda ()
|
||||
(promoted-native-build-result-bootloader-package result)))
|
||||
#:base-packages (fallback base-packages (lambda ()
|
||||
(promoted-native-build-result-base-packages result)))
|
||||
#:development-packages (fallback development-packages (lambda ()
|
||||
(operating-system-development-packages defaults)))
|
||||
#:users (fallback users (lambda () (operating-system-users defaults)))
|
||||
#:groups (fallback groups (lambda () (operating-system-groups defaults)))
|
||||
#:file-systems (fallback file-systems (lambda () (operating-system-file-systems defaults)))
|
||||
#:services (fallback services (lambda () (operating-system-services defaults)))
|
||||
#:loader-entries (fallback loader-entries (lambda () (operating-system-loader-entries defaults)))
|
||||
#:rc-conf-entries (fallback rc-conf-entries (lambda () (operating-system-rc-conf-entries defaults)))
|
||||
#:init-mode (fallback init-mode (lambda () (operating-system-init-mode defaults)))
|
||||
#:ready-marker (fallback ready-marker (lambda () (operating-system-ready-marker defaults)))
|
||||
#:root-authorized-keys (fallback root-authorized-keys (lambda ()
|
||||
(operating-system-root-authorized-keys defaults))))))
|
||||
|
||||
(define (native-build-artifact-entry result artifact-kind)
|
||||
(let* ((artifacts (native-build-result-ref result 'artifacts '()))
|
||||
(entry (assoc artifact-kind artifacts)))
|
||||
(unless entry
|
||||
(error "native build result is missing artifact entry" artifact-kind))
|
||||
(cdr entry)))
|
||||
|
||||
(define (native-build-artifact-root result-root result artifact-kind)
|
||||
(let* ((entry (native-build-artifact-entry result artifact-kind))
|
||||
(relative-path (native-build-result-ref entry 'path #f))
|
||||
(required-file (native-build-result-ref entry 'required-file #f))
|
||||
(artifact-root (and relative-path
|
||||
(string-append result-root "/" relative-path))))
|
||||
(unless (and artifact-root (file-exists? artifact-root))
|
||||
(error "native build result is missing artifact tree" artifact-kind artifact-root))
|
||||
(when required-file
|
||||
(unless (file-exists? (string-append artifact-root "/" required-file))
|
||||
(error "native build artifact is missing required file"
|
||||
artifact-kind
|
||||
(string-append artifact-root "/" required-file))))
|
||||
artifact-root))
|
||||
|
||||
(define (native-build-existing-store-references result store-dir)
|
||||
(filter identity
|
||||
(map (lambda (path)
|
||||
(and (string? path)
|
||||
(string-prefix? (string-append store-dir "/") path)
|
||||
(file-exists? path)
|
||||
path))
|
||||
(list (native-build-result-ref result 'closure-path #f)
|
||||
(let ((source (native-build-result-ref result 'source '())))
|
||||
(native-build-result-ref source 'store-path #f))))))
|
||||
|
||||
(define (native-build-artifact-display-name result artifact-kind)
|
||||
(let* ((base (native-build-result-ref result 'freebsd-base '()))
|
||||
(version-label (native-build-result-ref base 'version-label "unknown"))
|
||||
(executor-name (native-build-result-executor-name result)))
|
||||
(string-append "fruix-native-"
|
||||
(symbol->string artifact-kind)
|
||||
"-"
|
||||
version-label
|
||||
"-"
|
||||
executor-name)))
|
||||
|
||||
(define (native-build-promoted-artifact-metadata result artifact-kind content-signature)
|
||||
(let* ((entry (native-build-artifact-entry result artifact-kind))
|
||||
(executor (native-build-result-executor result))
|
||||
(build-profile (native-build-result-ref result 'build-profile
|
||||
(native-build-result-ref result 'development-profile ""))))
|
||||
`((native-build-object-version . ,native-build-result-promotion-version)
|
||||
(object-kind . artifact)
|
||||
(artifact-kind . ,artifact-kind)
|
||||
(executor . ,executor)
|
||||
(executor-kind . ,(native-build-result-executor-kind result))
|
||||
(executor-name . ,(native-build-result-executor-name result))
|
||||
(executor-version . ,(native-build-result-executor-version result))
|
||||
(run-id . ,(native-build-result-ref result 'run-id "unknown"))
|
||||
(guest-host-name . ,(native-build-result-ref result 'guest-host-name "unknown"))
|
||||
(closure-path . ,(native-build-result-ref result 'closure-path ""))
|
||||
(development-profile . ,(native-build-result-ref result 'development-profile ""))
|
||||
(build-profile . ,build-profile)
|
||||
(freebsd-base . ,(native-build-result-ref result 'freebsd-base '()))
|
||||
(source . ,(native-build-result-ref result 'source '()))
|
||||
(build-policy . ,(native-build-result-ref result 'build-policy '()))
|
||||
(required-file . ,(native-build-result-ref entry 'required-file ""))
|
||||
(recorded-sha256 . ,(native-build-result-ref entry 'recorded-sha256 ""))
|
||||
(content-signature . ,content-signature))))
|
||||
|
||||
(define (promote-native-build-artifact result-root result store-dir artifact-kind)
|
||||
(let* ((artifact-root (native-build-artifact-root result-root result artifact-kind))
|
||||
(content-signature (tree-content-signature artifact-root))
|
||||
(metadata (native-build-promoted-artifact-metadata result artifact-kind content-signature))
|
||||
(payload (object->string metadata))
|
||||
(display-name (native-build-artifact-display-name result artifact-kind))
|
||||
(output-path (make-store-path store-dir display-name payload
|
||||
#:kind 'native-build-artifact
|
||||
#:output artifact-kind))
|
||||
(references (native-build-existing-store-references result store-dir)))
|
||||
(unless (file-exists? output-path)
|
||||
(mkdir-p output-path)
|
||||
(stage-tree-into-output artifact-root output-path)
|
||||
(write-file (string-append output-path "/.references")
|
||||
(string-join references "\n"))
|
||||
(write-file (string-append output-path "/.fruix-native-build-object.scm")
|
||||
payload))
|
||||
`((artifact-kind . ,artifact-kind)
|
||||
(artifact-root . ,artifact-root)
|
||||
(store-path . ,output-path)
|
||||
(content-signature . ,content-signature)
|
||||
(metadata-file . ,(string-append output-path "/.fruix-native-build-object.scm")))) )
|
||||
|
||||
(define (native-build-result-display-name result)
|
||||
(let* ((base (native-build-result-ref result 'freebsd-base '()))
|
||||
(version-label (native-build-result-ref base 'version-label "unknown"))
|
||||
(executor-name (native-build-result-executor-name result)))
|
||||
(string-append "fruix-native-build-result-" version-label "-" executor-name)))
|
||||
|
||||
(define (native-build-promoted-result-object result promoted-artifacts)
|
||||
(let ((executor (native-build-result-executor result))
|
||||
(build-profile (native-build-result-ref result 'build-profile
|
||||
(native-build-result-ref result 'development-profile ""))))
|
||||
`((native-build-result-version . ,native-build-result-promotion-version)
|
||||
(object-kind . result-bundle)
|
||||
(executor . ,executor)
|
||||
(executor-kind . ,(native-build-result-executor-kind result))
|
||||
(executor-name . ,(native-build-result-executor-name result))
|
||||
(executor-version . ,(native-build-result-executor-version result))
|
||||
(run-id . ,(native-build-result-ref result 'run-id "unknown"))
|
||||
(guest-host-name . ,(native-build-result-ref result 'guest-host-name "unknown"))
|
||||
(closure-path . ,(native-build-result-ref result 'closure-path ""))
|
||||
(development-profile . ,(native-build-result-ref result 'development-profile ""))
|
||||
(build-profile . ,build-profile)
|
||||
(freebsd-base . ,(native-build-result-ref result 'freebsd-base '()))
|
||||
(source . ,(native-build-result-ref result 'source '()))
|
||||
(build-policy . ,(native-build-result-ref result 'build-policy '()))
|
||||
(artifact-count . ,(length promoted-artifacts))
|
||||
(artifacts . ,(map (lambda (entry)
|
||||
`((artifact-kind . ,(assoc-ref entry 'artifact-kind))
|
||||
(store-path . ,(assoc-ref entry 'store-path))
|
||||
(content-signature . ,(assoc-ref entry 'content-signature))
|
||||
(metadata-file . ,(assoc-ref entry 'metadata-file))))
|
||||
promoted-artifacts)))))
|
||||
|
||||
(define* (promote-native-build-result result-root #:key (store-dir "/frx/store"))
|
||||
(let* ((result (read-native-build-result result-root))
|
||||
(promoted-artifacts (map (lambda (artifact-kind)
|
||||
(promote-native-build-artifact result-root result store-dir artifact-kind))
|
||||
'(world kernel headers bootloader)))
|
||||
(result-object (native-build-promoted-result-object result promoted-artifacts))
|
||||
(payload (object->string result-object))
|
||||
(display-name (native-build-result-display-name result))
|
||||
(result-store (make-store-path store-dir display-name payload
|
||||
#:kind 'native-build-result))
|
||||
(result-references (append (map (lambda (entry)
|
||||
(assoc-ref entry 'store-path))
|
||||
promoted-artifacts)
|
||||
(native-build-existing-store-references result store-dir))))
|
||||
(unless (file-exists? result-store)
|
||||
(mkdir-p (string-append result-store "/artifacts"))
|
||||
(for-each (lambda (entry)
|
||||
(symlink (assoc-ref entry 'store-path)
|
||||
(string-append result-store
|
||||
"/artifacts/"
|
||||
(symbol->string (assoc-ref entry 'artifact-kind)))))
|
||||
promoted-artifacts)
|
||||
(write-file (string-append result-store "/.references")
|
||||
(string-join result-references "\n"))
|
||||
(write-file (string-append result-store "/.fruix-native-build-result.scm")
|
||||
payload))
|
||||
`((result-root . ,result-root)
|
||||
(executor-kind . ,(native-build-result-executor-kind result))
|
||||
(executor-name . ,(native-build-result-executor-name result))
|
||||
(executor-version . ,(native-build-result-executor-version result))
|
||||
(result-store . ,result-store)
|
||||
(result-metadata-file . ,(string-append result-store "/.fruix-native-build-result.scm"))
|
||||
(artifact-store-count . ,(length promoted-artifacts))
|
||||
(artifact-stores . ,(map (lambda (entry) (assoc-ref entry 'store-path)) promoted-artifacts))
|
||||
(world-store . ,(assoc-ref (find (lambda (entry)
|
||||
(eq? (assoc-ref entry 'artifact-kind) 'world))
|
||||
promoted-artifacts)
|
||||
'store-path))
|
||||
(kernel-store . ,(assoc-ref (find (lambda (entry)
|
||||
(eq? (assoc-ref entry 'artifact-kind) 'kernel))
|
||||
promoted-artifacts)
|
||||
'store-path))
|
||||
(headers-store . ,(assoc-ref (find (lambda (entry)
|
||||
(eq? (assoc-ref entry 'artifact-kind) 'headers))
|
||||
promoted-artifacts)
|
||||
'store-path))
|
||||
(bootloader-store . ,(assoc-ref (find (lambda (entry)
|
||||
(eq? (assoc-ref entry 'artifact-kind) 'bootloader))
|
||||
promoted-artifacts)
|
||||
'store-path)))))
|
||||
|
||||
(define (sanitize-materialized-prefix name output-path)
|
||||
(cond
|
||||
((string=? name "fruix-guile-extra")
|
||||
(rewrite-text-file
|
||||
(string-append output-path "/share/guile/site/3.0/fibers/config.scm")
|
||||
'(("((getenv \"FIBERS_BUILD_DIR\")\n => (lambda (builddir) (in-vicinity builddir \".libs\")))\n (else \"/tmp/guile-gnutls-freebsd-validate-install/lib/guile/3.0/extensions\"))"
|
||||
. "((getenv \"FIBERS_BUILD_DIR\")\n => (lambda (builddir) (in-vicinity builddir \".libs\")))\n ((getenv \"GUILE_EXTENSIONS_PATH\"))\n (else \"/usr/local/lib/guile/3.0/extensions\"))")))
|
||||
(rewrite-text-file
|
||||
(string-append output-path "/share/guile/site/3.0/gnutls.scm")
|
||||
'(("\"/tmp/guile-gnutls-freebsd-validate-install/lib/guile/3.0/extensions\""
|
||||
. "(or (getenv \"GUILE_EXTENSIONS_PATH\") \"/usr/local/lib/guile/3.0/extensions\")")))
|
||||
(delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/fibers/config.go"))
|
||||
(delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/gnutls.go")))
|
||||
((string=? name "fruix-shepherd-runtime")
|
||||
(rewrite-text-file
|
||||
(string-append output-path "/share/guile/site/3.0/shepherd/config.scm")
|
||||
'(("(define Prefix-dir \"/tmp/shepherd-freebsd-validate-install\")"
|
||||
. "(define Prefix-dir \"/frx\")")
|
||||
("(define %localstatedir \"/tmp/shepherd-freebsd-validate-install/var\")"
|
||||
. "(define %localstatedir \"/var\")")
|
||||
("(define %runstatedir \"/tmp/shepherd-freebsd-validate-install/var/run\")"
|
||||
. "(define %runstatedir \"/var/run\")")
|
||||
("(define %sysconfdir \"/tmp/shepherd-freebsd-validate-install/etc\")"
|
||||
. "(define %sysconfdir \"/etc\")")
|
||||
("(define %localedir \"/tmp/shepherd-freebsd-validate-install/share/locale\")"
|
||||
. "(define %localedir \"/usr/share/locale\")")
|
||||
("(define %pkglibdir \"/tmp/shepherd-freebsd-validate-install/lib/shepherd\")"
|
||||
. "(define %pkglibdir \"/usr/local/lib/shepherd\")")))
|
||||
(delete-file-if-exists (string-append output-path "/lib/guile/3.0/site-ccache/shepherd/config.go"))))
|
||||
#t)
|
||||
|
||||
(define prefix-materializer-version "3")
|
||||
|
||||
(define (prefix-manifest-string source-path extra-files)
|
||||
(string-append
|
||||
"prefix-materializer-version=" prefix-materializer-version "\n"
|
||||
"prefix-source=" source-path "\n"
|
||||
(path-signature source-path)
|
||||
(if (null? extra-files)
|
||||
""
|
||||
(string-append
|
||||
"\nextra-files=\n"
|
||||
(string-join
|
||||
(map (lambda (entry)
|
||||
(string-append (cdr entry) "\n" (path-signature (car entry))))
|
||||
extra-files)
|
||||
"\n")))))
|
||||
|
||||
(define (copy-extra-node source destination)
|
||||
(let ((kind (stat:type (lstat source))))
|
||||
(mkdir-p (dirname destination))
|
||||
(case kind
|
||||
((symlink)
|
||||
(unless (or (file-exists? destination)
|
||||
(false-if-exception (readlink destination)))
|
||||
(let ((target (readlink source)))
|
||||
(symlink target destination)
|
||||
(unless (string-prefix? "/" target)
|
||||
(copy-extra-node (string-append (dirname source) "/" target)
|
||||
(string-append (dirname destination) "/" target))))))
|
||||
(else
|
||||
(unless (file-exists? destination)
|
||||
(copy-node source destination))))))
|
||||
|
||||
(define* (materialize-prefix source-path name version store-dir #:key (extra-files '()))
|
||||
(let* ((manifest (prefix-manifest-string source-path extra-files))
|
||||
(display-name (string-append name "-" version))
|
||||
(output-path (make-store-path store-dir display-name manifest
|
||||
#:kind 'prefix)))
|
||||
(unless (file-exists? output-path)
|
||||
(mkdir-p output-path)
|
||||
(for-each (lambda (entry)
|
||||
(copy-node (string-append source-path "/" entry)
|
||||
(string-append output-path "/" entry)))
|
||||
(directory-entries source-path))
|
||||
(for-each (lambda (entry)
|
||||
(copy-extra-node (car entry)
|
||||
(string-append output-path "/" (cdr entry))))
|
||||
extra-files)
|
||||
(sanitize-materialized-prefix name output-path)
|
||||
(write-file (string-append output-path "/.fruix-package") manifest))
|
||||
output-path))
|
||||
|
||||
121
modules/fruix/system/freebsd/executor.scm
Normal file
121
modules/fruix/system/freebsd/executor.scm
Normal file
@@ -0,0 +1,121 @@
|
||||
(define-module (fruix system freebsd executor)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:export (native-build-executor-model-version
|
||||
native-build-executor
|
||||
native-build-executor?
|
||||
native-build-executor-ref
|
||||
native-build-executor-kind
|
||||
native-build-executor-name
|
||||
native-build-executor-version
|
||||
native-build-executor-properties
|
||||
normalize-native-build-executor
|
||||
host-native-build-executor
|
||||
ssh-guest-native-build-executor
|
||||
self-hosted-native-build-executor))
|
||||
|
||||
(define native-build-executor-model-version "1")
|
||||
|
||||
(define (association-list? value)
|
||||
(and (list? value)
|
||||
(every pair? value)))
|
||||
|
||||
(define (executor-name kind provided-name)
|
||||
(or provided-name
|
||||
(symbol->string kind)))
|
||||
|
||||
(define* (native-build-executor #:key kind name
|
||||
(version native-build-executor-model-version)
|
||||
(properties '()))
|
||||
(unless (symbol? kind)
|
||||
(error "native build executor kind must be a symbol" kind))
|
||||
(unless (string? (executor-name kind name))
|
||||
(error "native build executor name must be a string" name))
|
||||
(unless (string? version)
|
||||
(error "native build executor version must be a string" version))
|
||||
(unless (association-list? properties)
|
||||
(error "native build executor properties must be an association list" properties))
|
||||
`((kind . ,kind)
|
||||
(name . ,(executor-name kind name))
|
||||
(version . ,version)
|
||||
(properties . ,properties)))
|
||||
|
||||
(define (native-build-executor-ref executor key default)
|
||||
(match (assoc key executor)
|
||||
((_ . value) value)
|
||||
(#f default)))
|
||||
|
||||
(define (native-build-executor? value)
|
||||
(and (association-list? value)
|
||||
(symbol? (native-build-executor-ref value 'kind #f))
|
||||
(string? (native-build-executor-ref value 'name #f))
|
||||
(string? (native-build-executor-ref value 'version #f))
|
||||
(association-list? (native-build-executor-ref value 'properties '()))))
|
||||
|
||||
(define (native-build-executor-kind executor)
|
||||
(native-build-executor-ref executor 'kind 'unknown))
|
||||
|
||||
(define (native-build-executor-name executor)
|
||||
(native-build-executor-ref executor 'name "unknown"))
|
||||
|
||||
(define (native-build-executor-version executor)
|
||||
(native-build-executor-ref executor 'version "unknown"))
|
||||
|
||||
(define (native-build-executor-properties executor)
|
||||
(native-build-executor-ref executor 'properties '()))
|
||||
|
||||
(define (legacy-executor-kind name)
|
||||
(cond
|
||||
((member name '("host")) 'host)
|
||||
((member name '("ssh-guest" "guest-ssh" "guest-host-initiated")) 'ssh-guest)
|
||||
((member name '("self-hosted" "guest-self-hosted")) 'self-hosted)
|
||||
((member name '("jail")) 'jail)
|
||||
((member name '("remote-builder")) 'remote-builder)
|
||||
(else 'legacy)))
|
||||
|
||||
(define (normalize-native-build-executor value)
|
||||
(cond
|
||||
((native-build-executor? value)
|
||||
value)
|
||||
((string? value)
|
||||
(native-build-executor #:kind (legacy-executor-kind value)
|
||||
#:name value
|
||||
#:version "legacy"))
|
||||
(else
|
||||
(error "unsupported native build executor representation" value))))
|
||||
|
||||
(define* (host-native-build-executor #:key (name "host")
|
||||
host-name working-directory)
|
||||
(native-build-executor
|
||||
#:kind 'host
|
||||
#:name name
|
||||
#:properties (filter-map identity
|
||||
`((host-name . ,host-name)
|
||||
(working-directory . ,working-directory)))))
|
||||
|
||||
(define* (ssh-guest-native-build-executor #:key (name "ssh-guest")
|
||||
transport orchestrator
|
||||
guest-host-name guest-ip vm-id vdi-id)
|
||||
(native-build-executor
|
||||
#:kind 'ssh-guest
|
||||
#:name name
|
||||
#:properties (filter-map identity
|
||||
`((transport . ,(or transport "ssh"))
|
||||
(orchestrator . ,(or orchestrator "host"))
|
||||
(guest-host-name . ,guest-host-name)
|
||||
(guest-ip . ,guest-ip)
|
||||
(vm-id . ,vm-id)
|
||||
(vdi-id . ,vdi-id)))))
|
||||
|
||||
(define* (self-hosted-native-build-executor #:key (name "self-hosted")
|
||||
helper-path helper-version
|
||||
guest-host-name build-root-base result-root-base)
|
||||
(native-build-executor
|
||||
#:kind 'self-hosted
|
||||
#:name name
|
||||
#:version (or helper-version native-build-executor-model-version)
|
||||
#:properties (filter-map identity
|
||||
`((helper-path . ,helper-path)
|
||||
(guest-host-name . ,guest-host-name)
|
||||
(build-root-base . ,build-root-base)
|
||||
(result-root-base . ,result-root-base)))))
|
||||
1627
modules/fruix/system/freebsd/media.scm
Normal file
1627
modules/fruix/system/freebsd/media.scm
Normal file
File diff suppressed because it is too large
Load Diff
446
modules/fruix/system/freebsd/model.scm
Normal file
446
modules/fruix/system/freebsd/model.scm
Normal file
@@ -0,0 +1,446 @@
|
||||
(define-module (fruix system freebsd model)
|
||||
#:use-module (fruix packages freebsd)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-9)
|
||||
#:use-module (srfi srfi-13)
|
||||
#:export (user-group
|
||||
user-group?
|
||||
user-group-name
|
||||
user-group-gid
|
||||
user-group-system?
|
||||
user-account
|
||||
user-account?
|
||||
user-account-name
|
||||
user-account-uid
|
||||
user-account-group
|
||||
user-account-supplementary-groups
|
||||
user-account-comment
|
||||
user-account-home
|
||||
user-account-shell
|
||||
user-account-system?
|
||||
file-system
|
||||
file-system?
|
||||
file-system-device
|
||||
file-system-mount-point
|
||||
file-system-type
|
||||
file-system-options
|
||||
file-system-needed-for-boot?
|
||||
make-promoted-native-build-result
|
||||
promoted-native-build-result?
|
||||
promoted-native-build-result-store-path
|
||||
promoted-native-build-result-metadata-file
|
||||
promoted-native-build-result-metadata
|
||||
promoted-native-build-result-spec
|
||||
operating-system
|
||||
operating-system?
|
||||
operating-system-host-name
|
||||
operating-system-freebsd-base
|
||||
operating-system-native-build-result
|
||||
operating-system-kernel
|
||||
operating-system-bootloader
|
||||
operating-system-base-packages
|
||||
operating-system-development-packages
|
||||
operating-system-build-packages
|
||||
operating-system-users
|
||||
operating-system-groups
|
||||
operating-system-file-systems
|
||||
operating-system-services
|
||||
operating-system-loader-entries
|
||||
operating-system-rc-conf-entries
|
||||
operating-system-init-mode
|
||||
operating-system-ready-marker
|
||||
operating-system-root-authorized-keys
|
||||
default-minimal-operating-system
|
||||
freebsd-source-spec
|
||||
freebsd-base-spec
|
||||
validate-freebsd-source
|
||||
validate-operating-system
|
||||
pid1-init-mode?
|
||||
effective-loader-entries
|
||||
rc-conf-entry-value
|
||||
sshd-enabled?
|
||||
operating-system-generated-file-names
|
||||
operating-system-closure-spec))
|
||||
|
||||
(define-record-type <user-group>
|
||||
(make-user-group name gid system?)
|
||||
user-group?
|
||||
(name user-group-name)
|
||||
(gid user-group-gid)
|
||||
(system? user-group-system?))
|
||||
|
||||
(define* (user-group #:key name gid (system? #t))
|
||||
(make-user-group name gid system?))
|
||||
|
||||
(define-record-type <user-account>
|
||||
(make-user-account name uid group supplementary-groups comment home shell system?)
|
||||
user-account?
|
||||
(name user-account-name)
|
||||
(uid user-account-uid)
|
||||
(group user-account-group)
|
||||
(supplementary-groups user-account-supplementary-groups)
|
||||
(comment user-account-comment)
|
||||
(home user-account-home)
|
||||
(shell user-account-shell)
|
||||
(system? user-account-system?))
|
||||
|
||||
(define* (user-account #:key name uid group (supplementary-groups '())
|
||||
(comment "Fruix user") (home "/nonexistent")
|
||||
(shell "/usr/sbin/nologin") (system? #t))
|
||||
(make-user-account name uid group supplementary-groups comment home shell system?))
|
||||
|
||||
(define-record-type <file-system>
|
||||
(make-file-system device mount-point type options needed-for-boot?)
|
||||
file-system?
|
||||
(device file-system-device)
|
||||
(mount-point file-system-mount-point)
|
||||
(type file-system-type)
|
||||
(options file-system-options)
|
||||
(needed-for-boot? file-system-needed-for-boot?))
|
||||
|
||||
(define* (file-system #:key device mount-point type (options "rw")
|
||||
(needed-for-boot? #f))
|
||||
(make-file-system device mount-point type options needed-for-boot?))
|
||||
|
||||
(define-record-type <promoted-native-build-result>
|
||||
(make-promoted-native-build-result store-path metadata-file metadata)
|
||||
promoted-native-build-result?
|
||||
(store-path promoted-native-build-result-store-path)
|
||||
(metadata-file promoted-native-build-result-metadata-file)
|
||||
(metadata promoted-native-build-result-metadata))
|
||||
|
||||
(define (promoted-native-build-result-metadata-ref metadata key default)
|
||||
(match (assoc key metadata)
|
||||
((_ . value) value)
|
||||
(#f default)))
|
||||
|
||||
(define (promoted-native-build-result-artifact-spec metadata artifact-kind)
|
||||
(find (lambda (entry)
|
||||
(eq? (promoted-native-build-result-metadata-ref entry 'artifact-kind #f)
|
||||
artifact-kind))
|
||||
(promoted-native-build-result-metadata-ref metadata 'artifacts '())))
|
||||
|
||||
(define (promoted-native-build-result-spec result)
|
||||
(let* ((metadata (promoted-native-build-result-metadata result))
|
||||
(base (promoted-native-build-result-metadata-ref metadata 'freebsd-base '()))
|
||||
(source (promoted-native-build-result-metadata-ref metadata 'source '())))
|
||||
`((store-path . ,(promoted-native-build-result-store-path result))
|
||||
(metadata-file . ,(promoted-native-build-result-metadata-file result))
|
||||
(executor-kind . ,(promoted-native-build-result-metadata-ref metadata 'executor-kind #f))
|
||||
(executor-name . ,(promoted-native-build-result-metadata-ref metadata 'executor-name #f))
|
||||
(executor-version . ,(promoted-native-build-result-metadata-ref metadata 'executor-version #f))
|
||||
(run-id . ,(promoted-native-build-result-metadata-ref metadata 'run-id #f))
|
||||
(version-label . ,(promoted-native-build-result-metadata-ref base 'version-label #f))
|
||||
(release . ,(promoted-native-build-result-metadata-ref base 'release #f))
|
||||
(branch . ,(promoted-native-build-result-metadata-ref base 'branch #f))
|
||||
(source-store . ,(promoted-native-build-result-metadata-ref source 'store-path #f))
|
||||
(source-root . ,(promoted-native-build-result-metadata-ref source 'source-root #f))
|
||||
(artifact-count . ,(promoted-native-build-result-metadata-ref metadata 'artifact-count 0))
|
||||
(world-store . ,(promoted-native-build-result-metadata-ref
|
||||
(promoted-native-build-result-artifact-spec metadata 'world)
|
||||
'store-path
|
||||
#f))
|
||||
(kernel-store . ,(promoted-native-build-result-metadata-ref
|
||||
(promoted-native-build-result-artifact-spec metadata 'kernel)
|
||||
'store-path
|
||||
#f))
|
||||
(headers-store . ,(promoted-native-build-result-metadata-ref
|
||||
(promoted-native-build-result-artifact-spec metadata 'headers)
|
||||
'store-path
|
||||
#f))
|
||||
(bootloader-store . ,(promoted-native-build-result-metadata-ref
|
||||
(promoted-native-build-result-artifact-spec metadata 'bootloader)
|
||||
'store-path
|
||||
#f)))))
|
||||
|
||||
(define-record-type <operating-system>
|
||||
(make-operating-system host-name freebsd-base native-build-result kernel bootloader
|
||||
base-packages development-packages build-packages users groups file-systems
|
||||
services loader-entries rc-conf-entries init-mode ready-marker
|
||||
root-authorized-keys)
|
||||
operating-system?
|
||||
(host-name operating-system-host-name)
|
||||
(freebsd-base operating-system-freebsd-base)
|
||||
(native-build-result operating-system-native-build-result)
|
||||
(kernel operating-system-kernel)
|
||||
(bootloader operating-system-bootloader)
|
||||
(base-packages operating-system-base-packages)
|
||||
(development-packages operating-system-development-packages)
|
||||
(build-packages operating-system-build-packages)
|
||||
(users operating-system-users)
|
||||
(groups operating-system-groups)
|
||||
(file-systems operating-system-file-systems)
|
||||
(services operating-system-services)
|
||||
(loader-entries operating-system-loader-entries)
|
||||
(rc-conf-entries operating-system-rc-conf-entries)
|
||||
(init-mode operating-system-init-mode)
|
||||
(ready-marker operating-system-ready-marker)
|
||||
(root-authorized-keys operating-system-root-authorized-keys))
|
||||
|
||||
(define* (operating-system #:key
|
||||
(host-name "fruix-freebsd")
|
||||
(freebsd-base %default-freebsd-base)
|
||||
(native-build-result #f)
|
||||
(kernel freebsd-kernel)
|
||||
(bootloader freebsd-bootloader)
|
||||
(base-packages %freebsd-system-packages)
|
||||
(development-packages '())
|
||||
(build-packages '())
|
||||
(users (list (user-account #:name "root"
|
||||
#:uid 0
|
||||
#:group "wheel"
|
||||
#:comment "Charlie &"
|
||||
#:home "/root"
|
||||
#:shell "/bin/sh"
|
||||
#:system? #t)
|
||||
(user-account #:name "operator"
|
||||
#:uid 1000
|
||||
#:group "operator"
|
||||
#:supplementary-groups '("wheel")
|
||||
#:comment "Fruix Operator"
|
||||
#:home "/home/operator"
|
||||
#:shell "/bin/sh"
|
||||
#:system? #f)))
|
||||
(groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
|
||||
(user-group #:name "operator" #:gid 1000 #:system? #f)))
|
||||
(file-systems (list (file-system #:device "/dev/ufs/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"
|
||||
#:needed-for-boot? #f)))
|
||||
(services '(shepherd ready-marker))
|
||||
(loader-entries '(("autoboot_delay" . "1")
|
||||
("console" . "comconsole")))
|
||||
(rc-conf-entries '(("clear_tmp_enable" . "YES")
|
||||
("sendmail_enable" . "NONE")
|
||||
("sshd_enable" . "NO")))
|
||||
(init-mode 'freebsd-init+rc.d-shepherd)
|
||||
(ready-marker "/var/lib/fruix/ready")
|
||||
(root-authorized-keys '()))
|
||||
(make-operating-system host-name freebsd-base native-build-result kernel bootloader
|
||||
base-packages development-packages build-packages users groups file-systems
|
||||
services loader-entries rc-conf-entries init-mode ready-marker
|
||||
root-authorized-keys))
|
||||
|
||||
(define default-minimal-operating-system (operating-system))
|
||||
|
||||
(define (package-names packages)
|
||||
(map freebsd-package-name packages))
|
||||
|
||||
(define (freebsd-source-spec source)
|
||||
`((name . ,(freebsd-source-name source))
|
||||
(kind . ,(freebsd-source-kind source))
|
||||
(url . ,(freebsd-source-url source))
|
||||
(path . ,(freebsd-source-path source))
|
||||
(ref . ,(freebsd-source-ref source))
|
||||
(commit . ,(freebsd-source-commit source))
|
||||
(sha256 . ,(freebsd-source-sha256 source))))
|
||||
|
||||
(define (freebsd-base-spec base)
|
||||
`((name . ,(freebsd-base-name base))
|
||||
(version-label . ,(freebsd-base-version-label base))
|
||||
(release . ,(freebsd-base-release base))
|
||||
(branch . ,(freebsd-base-branch base))
|
||||
(source-root . ,(freebsd-base-source-root base))
|
||||
(source . ,(freebsd-source-spec (freebsd-base-source base)))
|
||||
(target . ,(freebsd-base-target base))
|
||||
(target-arch . ,(freebsd-base-target-arch base))
|
||||
(kernconf . ,(freebsd-base-kernconf base))
|
||||
(make-flags . ,(freebsd-base-make-flags base))))
|
||||
|
||||
|
||||
(define (duplicate-elements values)
|
||||
(let loop ((rest values) (seen '()) (duplicates '()))
|
||||
(match rest
|
||||
(() (reverse duplicates))
|
||||
((head . tail)
|
||||
(if (member head seen)
|
||||
(loop tail seen (if (member head duplicates) duplicates (cons head duplicates)))
|
||||
(loop tail (cons head seen) duplicates))))))
|
||||
|
||||
(define (non-empty-string? value)
|
||||
(and (string? value)
|
||||
(not (string-null? value))))
|
||||
|
||||
(define (validate-freebsd-source source)
|
||||
(unless (freebsd-source? source)
|
||||
(error "freebsd base source must be a <freebsd-source> record"))
|
||||
(let ((kind (freebsd-source-kind source)))
|
||||
(unless (member kind '(local-tree git src-txz))
|
||||
(error "unsupported freebsd source kind" kind))
|
||||
(case kind
|
||||
((local-tree)
|
||||
(unless (non-empty-string? (freebsd-source-path source))
|
||||
(error "local-tree freebsd source must declare a path" source)))
|
||||
((git)
|
||||
(unless (non-empty-string? (freebsd-source-url source))
|
||||
(error "git freebsd source must declare a URL" source))
|
||||
(unless (or (non-empty-string? (freebsd-source-ref source))
|
||||
(non-empty-string? (freebsd-source-commit source)))
|
||||
(error "git freebsd source must declare a ref or commit" source)))
|
||||
((src-txz)
|
||||
(unless (non-empty-string? (freebsd-source-url source))
|
||||
(error "src-txz freebsd source must declare a URL" source))
|
||||
(unless (non-empty-string? (freebsd-source-sha256 source))
|
||||
(error "src-txz freebsd source must declare a sha256" source)))))
|
||||
#t)
|
||||
|
||||
(define (validate-operating-system os)
|
||||
(let* ((host-name (operating-system-host-name os))
|
||||
(base (operating-system-freebsd-base os))
|
||||
(native-build-result (operating-system-native-build-result os))
|
||||
(base-packages (operating-system-base-packages os))
|
||||
(development-packages (operating-system-development-packages os))
|
||||
(build-packages (operating-system-build-packages os))
|
||||
(users (operating-system-users os))
|
||||
(groups (operating-system-groups os))
|
||||
(file-systems (operating-system-file-systems os))
|
||||
(user-names (map user-account-name users))
|
||||
(group-names (map user-group-name groups))
|
||||
(mount-points (map file-system-mount-point file-systems))
|
||||
(init-mode (operating-system-init-mode os)))
|
||||
(when (string-null? host-name)
|
||||
(error "operating-system host-name must not be empty"))
|
||||
(unless (freebsd-base? base)
|
||||
(error "operating-system freebsd-base must be a <freebsd-base> record"))
|
||||
(when native-build-result
|
||||
(unless (promoted-native-build-result? native-build-result)
|
||||
(error "operating-system native-build-result must be a <promoted-native-build-result> record")))
|
||||
(unless (every freebsd-package? base-packages)
|
||||
(error "operating-system base-packages must be a list of <freebsd-package> records"))
|
||||
(unless (every freebsd-package? development-packages)
|
||||
(error "operating-system development-packages must be a list of <freebsd-package> records"))
|
||||
(unless (every freebsd-package? build-packages)
|
||||
(error "operating-system build-packages must be a list of <freebsd-package> records"))
|
||||
(validate-freebsd-source (freebsd-base-source base))
|
||||
(let ((dups (duplicate-elements user-names)))
|
||||
(unless (null? dups)
|
||||
(error "duplicate user names in operating-system" dups)))
|
||||
(let ((dups (duplicate-elements group-names)))
|
||||
(unless (null? dups)
|
||||
(error "duplicate group names in operating-system" dups)))
|
||||
(unless (member "/" mount-points)
|
||||
(error "operating-system must declare a root file-system"))
|
||||
(unless (member "root" user-names)
|
||||
(error "operating-system must declare a root user"))
|
||||
(unless (member "wheel" group-names)
|
||||
(error "operating-system must declare a wheel group"))
|
||||
(unless (member init-mode '(freebsd-init+rc.d-shepherd shepherd-pid1))
|
||||
(error "unsupported operating-system init-mode" init-mode))
|
||||
#t))
|
||||
|
||||
(define (pid1-init-mode? os)
|
||||
(eq? (operating-system-init-mode os) 'shepherd-pid1))
|
||||
|
||||
(define (effective-loader-entries os)
|
||||
(append (if (pid1-init-mode? os)
|
||||
'(("init_exec" . "/run/current-system/boot/fruix-pid1"))
|
||||
'())
|
||||
(operating-system-loader-entries os)))
|
||||
|
||||
(define (rc-conf-entry-value os key)
|
||||
(let ((entry (assoc key (operating-system-rc-conf-entries os))))
|
||||
(and entry (cdr entry))))
|
||||
|
||||
(define (sshd-enabled? os)
|
||||
(let ((value (rc-conf-entry-value os "sshd_enable")))
|
||||
(and value
|
||||
(member (string-upcase value) '("YES" "TRUE" "1")))))
|
||||
|
||||
|
||||
(define (operating-system-generated-file-names os)
|
||||
(append
|
||||
'("boot/loader.conf"
|
||||
"etc/rc.conf"
|
||||
"etc/fstab"
|
||||
"etc/hosts"
|
||||
"etc/passwd"
|
||||
"etc/master.passwd"
|
||||
"etc/group"
|
||||
"etc/login.conf"
|
||||
"etc/shells"
|
||||
"etc/motd"
|
||||
"etc/ttys"
|
||||
"metadata/freebsd-base.scm"
|
||||
"metadata/host-base-provenance.scm"
|
||||
"metadata/store-layout.scm"
|
||||
"metadata/system-declaration.scm"
|
||||
"metadata/system-declaration-info.scm"
|
||||
"metadata/system-declaration-system"
|
||||
"activate"
|
||||
"shepherd/init.scm"
|
||||
"share/fruix/node/scripts/fruix.scm"
|
||||
"usr/local/bin/fruix")
|
||||
(if (operating-system-native-build-result os)
|
||||
'("metadata/promoted-native-build-result.scm")
|
||||
'())
|
||||
(if (null? (operating-system-development-packages os))
|
||||
'()
|
||||
'("usr/local/bin/fruix-development-environment"))
|
||||
(if (null? (operating-system-build-packages os))
|
||||
'()
|
||||
'("usr/local/bin/fruix-build-environment"
|
||||
"usr/local/bin/fruix-self-hosted-native-build"))
|
||||
(if (pid1-init-mode? os)
|
||||
'("boot/fruix-pid1")
|
||||
'())
|
||||
(if (sshd-enabled? os)
|
||||
'("etc/ssh/sshd_config")
|
||||
'())
|
||||
(if (null? (operating-system-root-authorized-keys os))
|
||||
'()
|
||||
'("root/.ssh/authorized_keys"))))
|
||||
|
||||
|
||||
(define (operating-system-closure-spec os)
|
||||
(validate-operating-system os)
|
||||
`((host-name . ,(operating-system-host-name os))
|
||||
(freebsd-base . ,(freebsd-base-spec (operating-system-freebsd-base os)))
|
||||
(promoted-native-build-result
|
||||
. ,(and (operating-system-native-build-result os)
|
||||
(promoted-native-build-result-spec
|
||||
(operating-system-native-build-result os))))
|
||||
(kernel-package . ,(freebsd-package-name (operating-system-kernel os)))
|
||||
(bootloader-package . ,(freebsd-package-name (operating-system-bootloader os)))
|
||||
(base-package-count . ,(length (operating-system-base-packages os)))
|
||||
(base-packages . ,(package-names (operating-system-base-packages os)))
|
||||
(development-package-count . ,(length (operating-system-development-packages os)))
|
||||
(development-packages . ,(package-names (operating-system-development-packages os)))
|
||||
(build-package-count . ,(length (operating-system-build-packages os)))
|
||||
(build-packages . ,(package-names (operating-system-build-packages os)))
|
||||
(installed-system-command-surface-version . "2")
|
||||
(bundled-fruix-node-cli-version . "1")
|
||||
(development-environment-helper-version
|
||||
. ,(if (null? (operating-system-development-packages os)) #f "1"))
|
||||
(build-environment-helper-version
|
||||
. ,(if (null? (operating-system-build-packages os)) #f "1"))
|
||||
(self-hosted-native-build-helper-version
|
||||
. ,(if (null? (operating-system-build-packages os)) #f "5"))
|
||||
(user-count . ,(length (operating-system-users os)))
|
||||
(users . ,(map user-account-name (operating-system-users os)))
|
||||
(group-count . ,(length (operating-system-groups os)))
|
||||
(groups . ,(map user-group-name (operating-system-groups os)))
|
||||
(file-system-count . ,(length (operating-system-file-systems os)))
|
||||
(file-systems . ,(map (lambda (fs)
|
||||
`((device . ,(file-system-device fs))
|
||||
(mount-point . ,(file-system-mount-point fs))
|
||||
(type . ,(file-system-type fs))
|
||||
(options . ,(file-system-options fs))
|
||||
(needed-for-boot? . ,(file-system-needed-for-boot? fs))))
|
||||
(operating-system-file-systems os)))
|
||||
(services . ,(operating-system-services os))
|
||||
(generated-files . ,(operating-system-generated-file-names os))
|
||||
(init-mode . ,(operating-system-init-mode os))
|
||||
(ready-marker . ,(operating-system-ready-marker os))))
|
||||
|
||||
1233
modules/fruix/system/freebsd/render.scm
Normal file
1233
modules/fruix/system/freebsd/render.scm
Normal file
File diff suppressed because it is too large
Load Diff
204
modules/fruix/system/freebsd/source.scm
Normal file
204
modules/fruix/system/freebsd/source.scm
Normal file
@@ -0,0 +1,204 @@
|
||||
(define-module (fruix system freebsd source)
|
||||
#:use-module (fruix packages freebsd)
|
||||
#:use-module (fruix system freebsd model)
|
||||
#:use-module (fruix system freebsd utils)
|
||||
#:use-module (guix build utils)
|
||||
#:use-module (srfi srfi-13)
|
||||
#:export (materialize-freebsd-source
|
||||
freebsd-source-materialization-spec))
|
||||
|
||||
(define freebsd-source-materializer-version "2")
|
||||
|
||||
(define (string-downcase* value)
|
||||
(list->string (map char-downcase (string->list value))))
|
||||
|
||||
(define (safe-name-fragment value)
|
||||
(let* ((text (if (and (string? value) (not (string-null? value))) value "source"))
|
||||
(chars (map (lambda (ch)
|
||||
(if (or (char-alphabetic? ch)
|
||||
(char-numeric? ch)
|
||||
(memv ch '(#\- #\_ #\.)))
|
||||
ch
|
||||
#\-))
|
||||
(string->list text))))
|
||||
(list->string chars)))
|
||||
|
||||
(define (freebsd-source-manifest source effective-source identity)
|
||||
(string-append
|
||||
"materializer-version=" freebsd-source-materializer-version "\n"
|
||||
"declared-source=\n"
|
||||
(object->string (freebsd-source-spec source))
|
||||
"\neffective-source=\n"
|
||||
(object->string (freebsd-source-spec effective-source))
|
||||
"\nidentity=\n"
|
||||
(object->string identity)))
|
||||
|
||||
(define (ensure-git-source-cache source cache-dir)
|
||||
(let* ((url (freebsd-source-url source))
|
||||
(repo-dir (string-append cache-dir "/git/"
|
||||
(sha256-string (string-append "git:" url))
|
||||
".git")))
|
||||
(mkdir-p (dirname repo-dir))
|
||||
(unless (file-exists? repo-dir)
|
||||
(unless (zero? (system* "git" "init" "--quiet" "--bare" repo-dir))
|
||||
(error "failed to initialize git source cache" repo-dir))
|
||||
(unless (zero? (system* "git" "-C" repo-dir "remote" "add" "origin" url))
|
||||
(error "failed to add git source remote" url)))
|
||||
(let ((current-url (safe-command-output "git" "-C" repo-dir "remote" "get-url" "origin")))
|
||||
(unless (and current-url (string=? current-url url))
|
||||
(unless (zero? (system* "git" "-C" repo-dir "remote" "set-url" "origin" url))
|
||||
(error "failed to update git source remote" url))))
|
||||
repo-dir))
|
||||
|
||||
(define (resolve-git-freebsd-source source cache-dir)
|
||||
(let* ((selector (or (freebsd-source-commit source)
|
||||
(freebsd-source-ref source)
|
||||
(error "git freebsd source requires a ref or commit" source)))
|
||||
(repo-dir (ensure-git-source-cache source cache-dir)))
|
||||
(unless (zero? (system* "git" "-C" repo-dir "fetch" "--quiet" "--depth" "1" "origin" selector))
|
||||
(error "failed to fetch git freebsd source" selector))
|
||||
(let ((resolved-commit (command-output "git" "-C" repo-dir "rev-parse" "FETCH_HEAD")))
|
||||
`((cache-path . ,repo-dir)
|
||||
(effective-source . ,(freebsd-source #:name (freebsd-source-name source)
|
||||
#:kind 'git
|
||||
#:url (freebsd-source-url source)
|
||||
#:ref (freebsd-source-ref source)
|
||||
#:commit resolved-commit
|
||||
#:sha256 #f))
|
||||
(identity . ((resolved-commit . ,resolved-commit)))
|
||||
(populate-tree . ,(lambda (tree-root)
|
||||
(let ((archive-path (string-append (dirname tree-root) "/git-export.tar")))
|
||||
(unless (zero? (system* "git" "-C" repo-dir "archive"
|
||||
"--format=tar" "-o" archive-path resolved-commit))
|
||||
(error "failed to archive git freebsd source" resolved-commit))
|
||||
(unless (zero? (system* "tar" "-xpf" archive-path "-C" tree-root))
|
||||
(error "failed to extract archived git freebsd source" archive-path))
|
||||
(delete-path-if-exists archive-path))))))))
|
||||
|
||||
(define (normalize-expected-sha256 source)
|
||||
(let ((sha256 (freebsd-source-sha256 source)))
|
||||
(and sha256 (string-downcase* sha256))))
|
||||
|
||||
(define (resolve-txz-freebsd-source source cache-dir)
|
||||
(let* ((url (freebsd-source-url source))
|
||||
(expected-sha256 (or (normalize-expected-sha256 source)
|
||||
(error "src-txz freebsd source requires sha256 for materialization" source)))
|
||||
(archive-path (string-append cache-dir "/archives/"
|
||||
(sha256-string (string-append "txz:" url))
|
||||
"-src.txz")))
|
||||
(mkdir-p (dirname archive-path))
|
||||
(when (file-exists? archive-path)
|
||||
(let ((actual (string-downcase* (file-hash archive-path))))
|
||||
(unless (string=? actual expected-sha256)
|
||||
(delete-file archive-path))))
|
||||
(unless (file-exists? archive-path)
|
||||
(unless (zero? (system* "fetch" "-q" "-o" archive-path url))
|
||||
(error "failed to download FreeBSD src.txz source" url)))
|
||||
(let ((actual-sha256 (string-downcase* (file-hash archive-path))))
|
||||
(unless (string=? actual-sha256 expected-sha256)
|
||||
(error "downloaded src.txz hash mismatch" url expected-sha256 actual-sha256))
|
||||
`((cache-path . ,archive-path)
|
||||
(effective-source . ,(freebsd-source #:name (freebsd-source-name source)
|
||||
#:kind 'src-txz
|
||||
#:url url
|
||||
#:path #f
|
||||
#:ref #f
|
||||
#:commit #f
|
||||
#:sha256 actual-sha256))
|
||||
(identity . ((archive-sha256 . ,actual-sha256)))
|
||||
(populate-tree . ,(lambda (tree-root)
|
||||
(unless (zero? (system* "tar" "-xpf" archive-path "-C" tree-root))
|
||||
(error "failed to extract FreeBSD src.txz source" archive-path))))))))
|
||||
|
||||
(define (resolve-local-freebsd-source source)
|
||||
(let* ((path (freebsd-source-path source))
|
||||
(tree-sha256 (native-build-source-tree-sha256 path)))
|
||||
`((cache-path . #f)
|
||||
(effective-source . ,(freebsd-source #:name (freebsd-source-name source)
|
||||
#:kind 'local-tree
|
||||
#:url #f
|
||||
#:path path
|
||||
#:ref #f
|
||||
#:commit #f
|
||||
#:sha256 tree-sha256))
|
||||
(identity . ((tree-sha256 . ,tree-sha256)))
|
||||
(populate-tree . ,(lambda (tree-root)
|
||||
(copy-tree-contents path tree-root))))))
|
||||
|
||||
(define (detect-materialized-source-relative-root tree-root)
|
||||
(cond
|
||||
((file-exists? (string-append tree-root "/Makefile"))
|
||||
"tree")
|
||||
((file-exists? (string-append tree-root "/usr/src/Makefile"))
|
||||
"tree/usr/src")
|
||||
(else
|
||||
"tree")))
|
||||
|
||||
(define* (materialize-freebsd-source source #:key
|
||||
(store-dir "/frx/store")
|
||||
(cache-dir "/frx/var/cache/fruix/freebsd-source"))
|
||||
(validate-freebsd-source source)
|
||||
(let* ((resolution (case (freebsd-source-kind source)
|
||||
((local-tree)
|
||||
(resolve-local-freebsd-source source))
|
||||
((git)
|
||||
(resolve-git-freebsd-source source cache-dir))
|
||||
((src-txz)
|
||||
(resolve-txz-freebsd-source source cache-dir))
|
||||
(else
|
||||
(error "unsupported freebsd source kind" (freebsd-source-kind source)))))
|
||||
(effective-source (assoc-ref resolution 'effective-source))
|
||||
(identity (assoc-ref resolution 'identity))
|
||||
(manifest (freebsd-source-manifest source effective-source identity))
|
||||
(display-name (string-append "freebsd-source-"
|
||||
(safe-name-fragment (freebsd-source-name source))))
|
||||
(output-path (make-store-path store-dir display-name manifest
|
||||
#:kind 'freebsd-source))
|
||||
(info-file (string-append output-path "/.freebsd-source-info.scm"))
|
||||
(cache-path (assoc-ref resolution 'cache-path))
|
||||
(populate-tree (assoc-ref resolution 'populate-tree)))
|
||||
(unless (file-exists? output-path)
|
||||
(let* ((temp-output (string-append output-path ".tmp"))
|
||||
(temp-tree-root (string-append temp-output "/tree")))
|
||||
(delete-path-if-exists temp-output)
|
||||
(mkdir-p temp-tree-root)
|
||||
(populate-tree temp-tree-root)
|
||||
(let* ((relative-root (detect-materialized-source-relative-root temp-tree-root))
|
||||
(source-root (string-append output-path "/" relative-root))
|
||||
(temp-source-root (string-append temp-output "/" relative-root))
|
||||
(tree-sha256 (native-build-source-tree-sha256 temp-source-root)))
|
||||
(write-file (string-append temp-output "/.references") "")
|
||||
(write-file (string-append temp-output "/.fruix-source") manifest)
|
||||
(write-file (string-append temp-output "/.freebsd-source-info.scm")
|
||||
(object->string
|
||||
`((materializer-version . ,freebsd-source-materializer-version)
|
||||
(declared-source . ,(freebsd-source-spec source))
|
||||
(effective-source . ,(freebsd-source-spec effective-source))
|
||||
(identity . ,identity)
|
||||
(source-store . ,output-path)
|
||||
(source-root . ,source-root)
|
||||
(source-tree-sha256 . ,tree-sha256)
|
||||
(cache-path . ,cache-path)))))
|
||||
(rename-file temp-output output-path)))
|
||||
(call-with-input-file info-file
|
||||
(lambda (port)
|
||||
(let* ((info (read port))
|
||||
(effective (assoc-ref info 'effective-source)))
|
||||
`((source-store-path . ,output-path)
|
||||
(source-root . ,(assoc-ref info 'source-root))
|
||||
(source-info-file . ,info-file)
|
||||
(source-tree-sha256 . ,(assoc-ref info 'source-tree-sha256))
|
||||
(cache-path . ,(assoc-ref info 'cache-path))
|
||||
(effective-source . ,effective)
|
||||
(effective-commit . ,(assoc-ref effective 'commit))
|
||||
(effective-sha256 . ,(assoc-ref effective 'sha256))))))))
|
||||
|
||||
|
||||
(define (freebsd-source-materialization-spec result)
|
||||
`((source-store-path . ,(assoc-ref result 'source-store-path))
|
||||
(source-root . ,(assoc-ref result 'source-root))
|
||||
(source-info-file . ,(assoc-ref result 'source-info-file))
|
||||
(source-tree-sha256 . ,(assoc-ref result 'source-tree-sha256))
|
||||
(cache-path . ,(assoc-ref result 'cache-path))
|
||||
(effective-source . ,(assoc-ref result 'effective-source))))
|
||||
|
||||
304
modules/fruix/system/freebsd/utils.scm
Normal file
304
modules/fruix/system/freebsd/utils.scm
Normal file
@@ -0,0 +1,304 @@
|
||||
(define-module (fruix system freebsd utils)
|
||||
#:use-module (guix build utils)
|
||||
#:use-module (ice-9 ftw)
|
||||
#:use-module (ice-9 format)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (ice-9 popen)
|
||||
#:use-module (ice-9 hash-table)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-13)
|
||||
#:use-module (rnrs io ports)
|
||||
#:export (getenv*
|
||||
trim-trailing-newlines
|
||||
command-output
|
||||
safe-command-output
|
||||
write-file
|
||||
sha256-string
|
||||
store-hash-string
|
||||
make-store-path
|
||||
file-hash
|
||||
directory-entries
|
||||
path-signature
|
||||
tree-content-signature
|
||||
install-plan-signature
|
||||
native-build-source-tree-sha256
|
||||
copy-regular-file
|
||||
copy-node
|
||||
materialize-plan-entry
|
||||
delete-path-if-exists
|
||||
stage-tree-into-output
|
||||
string-replace-all
|
||||
rewrite-text-file
|
||||
delete-file-if-exists
|
||||
copy-tree-contents
|
||||
path-basename
|
||||
read-lines
|
||||
run-command
|
||||
store-reference-closure
|
||||
copy-store-items-into-rootfs
|
||||
copy-rootfs-for-image
|
||||
mktemp-directory))
|
||||
|
||||
(define (getenv* name default)
|
||||
(or (getenv name) default))
|
||||
|
||||
(define (trim-trailing-newlines str)
|
||||
(let loop ((len (string-length str)))
|
||||
(if (and (> len 0)
|
||||
(char=? (string-ref str (- len 1)) #\newline))
|
||||
(loop (- len 1))
|
||||
(substring str 0 len))))
|
||||
|
||||
(define (command-output program . args)
|
||||
(let* ((port (apply open-pipe* OPEN_READ program args))
|
||||
(output (get-string-all port))
|
||||
(status (close-pipe port)))
|
||||
(unless (zero? status)
|
||||
(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
|
||||
(lambda (port)
|
||||
(display content port))))
|
||||
|
||||
(define (sha256-string text)
|
||||
(let* ((tmp (string-append (getenv* "TMPDIR" "/tmp") "/fruix-system-hash.txt")))
|
||||
(write-file tmp text)
|
||||
(command-output "sha256" "-q" tmp)))
|
||||
|
||||
(define store-hash-visible-length 40)
|
||||
(define store-hash-scheme-version "1")
|
||||
|
||||
(define (store-identity-field value)
|
||||
(cond ((symbol? value)
|
||||
(symbol->string value))
|
||||
((string? value)
|
||||
value)
|
||||
(else
|
||||
(object->string value))))
|
||||
|
||||
(define* (store-hash-string payload #:key (kind 'item) name (output "out"))
|
||||
(let* ((identity `((scheme . "fruix-store-path")
|
||||
(version . ,store-hash-scheme-version)
|
||||
(kind . ,(store-identity-field kind))
|
||||
(name . ,(store-identity-field (or name "")))
|
||||
(output . ,(store-identity-field output))
|
||||
(payload . ,payload)))
|
||||
(digest (sha256-string (object->string identity))))
|
||||
(string-take digest store-hash-visible-length)))
|
||||
|
||||
(define* (make-store-path store-dir display-name payload
|
||||
#:key
|
||||
(kind 'item)
|
||||
name
|
||||
(output "out"))
|
||||
(string-append store-dir "/"
|
||||
(store-hash-string payload
|
||||
#:kind kind
|
||||
#:name (or name display-name)
|
||||
#:output output)
|
||||
"-"
|
||||
display-name))
|
||||
|
||||
(define (file-hash path)
|
||||
(command-output "sha256" "-q" path))
|
||||
|
||||
|
||||
(define (directory-entries path)
|
||||
(sort (filter (lambda (entry)
|
||||
(not (member entry '("." ".."))))
|
||||
(scandir path))
|
||||
string<?))
|
||||
|
||||
(define (path-signature path)
|
||||
(let ((st (lstat path)))
|
||||
(case (stat:type st)
|
||||
((regular)
|
||||
(string-append "file:" path ":" (file-hash path)))
|
||||
((symlink)
|
||||
(string-append "symlink:" path ":" (readlink path)))
|
||||
((directory)
|
||||
(string-join
|
||||
(cons (string-append "directory:" path)
|
||||
(apply append
|
||||
(map (lambda (entry)
|
||||
(list (path-signature (string-append path "/" entry))))
|
||||
(directory-entries path))))
|
||||
"\n"))
|
||||
(else
|
||||
(string-append "other:" path ":" (symbol->string (stat:type st)))))))
|
||||
|
||||
(define (tree-content-signature root)
|
||||
(define (walk path relative)
|
||||
(let ((st (lstat path)))
|
||||
(case (stat:type st)
|
||||
((regular)
|
||||
(string-append "file:" relative ":" (file-hash path)))
|
||||
((symlink)
|
||||
(string-append "symlink:" relative ":" (readlink path)))
|
||||
((directory)
|
||||
(string-join
|
||||
(cons (string-append "directory:" relative)
|
||||
(apply append
|
||||
(map (lambda (entry)
|
||||
(let ((child-relative (if (string=? relative ".")
|
||||
entry
|
||||
(string-append relative "/" entry))))
|
||||
(list (walk (string-append path "/" entry)
|
||||
child-relative))))
|
||||
(directory-entries path))))
|
||||
"\n"))
|
||||
(else
|
||||
(string-append "other:" relative ":" (symbol->string (stat:type st)))))))
|
||||
(walk root "."))
|
||||
|
||||
(define (install-plan-signature entry)
|
||||
(match entry
|
||||
(('file source target)
|
||||
(string-append "file-target:" target "\n" (path-signature source)))
|
||||
(('directory source target)
|
||||
(string-append "directory-target:" target "\n" (path-signature source)))
|
||||
(_
|
||||
(error (format #f "unsupported install plan entry: ~s" entry)))))
|
||||
|
||||
(define (native-build-source-tree-sha256 source-root)
|
||||
(let* ((mtree-output (command-output "mtree" "-c" "-k" "type,link,size,mode,sha256digest" "-p" source-root))
|
||||
(stable-lines (filter (lambda (line)
|
||||
(not (string-prefix? "#" line)))
|
||||
(string-split mtree-output #\newline))))
|
||||
(sha256-string (string-join stable-lines "\n"))))
|
||||
|
||||
(define (copy-regular-file source destination)
|
||||
(let ((mode (stat:perms (stat source))))
|
||||
(copy-file source destination)
|
||||
(chmod destination mode)))
|
||||
|
||||
(define (copy-node source destination)
|
||||
(let ((kind (stat:type (lstat source))))
|
||||
(mkdir-p (dirname destination))
|
||||
(case kind
|
||||
((directory)
|
||||
(mkdir-p destination)
|
||||
(for-each (lambda (entry)
|
||||
(copy-node (string-append source "/" entry)
|
||||
(string-append destination "/" entry)))
|
||||
(directory-entries source)))
|
||||
((symlink)
|
||||
(symlink (readlink source) destination))
|
||||
(else
|
||||
(copy-regular-file source destination)))))
|
||||
|
||||
(define (materialize-plan-entry output-path entry)
|
||||
(match entry
|
||||
(('file source target)
|
||||
(copy-node source (string-append output-path "/" target)))
|
||||
(('directory source target)
|
||||
(copy-node source (string-append output-path "/" target)))
|
||||
(_
|
||||
(error (format #f "unsupported install plan entry: ~s" entry)))))
|
||||
|
||||
(define (clear-file-flags path)
|
||||
(false-if-exception (system* "chflags" "-R" "noschg,nouchg" path)))
|
||||
|
||||
(define (delete-path-if-exists path)
|
||||
(when (or (file-exists? path) (false-if-exception (readlink path)))
|
||||
(clear-file-flags path)
|
||||
(let ((kind (stat:type (lstat path))))
|
||||
(case kind
|
||||
((directory) (delete-file-recursively path))
|
||||
(else (delete-file path))))))
|
||||
|
||||
(define (stage-tree-into-output stage-root output-path)
|
||||
(mkdir-p output-path)
|
||||
(for-each (lambda (entry)
|
||||
(copy-node (string-append stage-root "/" entry)
|
||||
(string-append output-path "/" entry)))
|
||||
(directory-entries stage-root)))
|
||||
|
||||
|
||||
(define (string-replace-all str old new)
|
||||
(let ((old-len (string-length old)))
|
||||
(let loop ((start 0) (chunks '()))
|
||||
(let ((index (string-contains str old start)))
|
||||
(if index
|
||||
(loop (+ index old-len)
|
||||
(cons new
|
||||
(cons (substring str start index) chunks)))
|
||||
(apply string-append
|
||||
(reverse (cons (substring str start) chunks))))))))
|
||||
|
||||
(define (rewrite-text-file path replacements)
|
||||
(when (file-exists? path)
|
||||
(let* ((mode (stat:perms (stat path)))
|
||||
(original (call-with-input-file path get-string-all))
|
||||
(updated (fold (lambda (replacement text)
|
||||
(string-replace-all text (car replacement) (cdr replacement)))
|
||||
original
|
||||
replacements)))
|
||||
(unless (string=? original updated)
|
||||
(write-file path updated)
|
||||
(chmod path mode)))))
|
||||
|
||||
(define (delete-file-if-exists path)
|
||||
(when (file-exists? path)
|
||||
(delete-file path)))
|
||||
|
||||
|
||||
(define (copy-tree-contents source-root target-root)
|
||||
(mkdir-p target-root)
|
||||
(for-each (lambda (entry)
|
||||
(copy-node (string-append source-root "/" entry)
|
||||
(string-append target-root "/" entry)))
|
||||
(directory-entries source-root)))
|
||||
|
||||
|
||||
(define (path-basename path)
|
||||
(let ((parts (filter (lambda (part) (not (string-null? part)))
|
||||
(string-split path #\/))))
|
||||
(if (null? parts)
|
||||
path
|
||||
(last parts))))
|
||||
|
||||
(define (read-lines path)
|
||||
(if (file-exists? path)
|
||||
(filter (lambda (line) (not (string-null? line)))
|
||||
(string-split (call-with-input-file path get-string-all) #\newline))
|
||||
'()))
|
||||
|
||||
(define (run-command . args)
|
||||
(let ((status (apply system* args)))
|
||||
(unless (zero? status)
|
||||
(error "command failed" args status))
|
||||
#t))
|
||||
|
||||
(define (store-reference-closure roots)
|
||||
(let ((seen (make-hash-table))
|
||||
(result '()))
|
||||
(define (visit item)
|
||||
(unless (hash-ref seen item #f)
|
||||
(hash-set! seen item #t)
|
||||
(set! result (cons item result))
|
||||
(for-each visit (read-lines (string-append item "/.references")))))
|
||||
(for-each visit roots)
|
||||
(reverse result)))
|
||||
|
||||
(define (copy-store-items-into-rootfs rootfs store-dir items)
|
||||
(let ((store-root (string-append rootfs store-dir)))
|
||||
(mkdir-p store-root)
|
||||
(for-each (lambda (item)
|
||||
(copy-node item (string-append store-root "/" (path-basename item))))
|
||||
items)))
|
||||
|
||||
(define (copy-rootfs-for-image source-rootfs image-rootfs)
|
||||
(when (file-exists? image-rootfs)
|
||||
(delete-file-recursively image-rootfs))
|
||||
(copy-node source-rootfs image-rootfs))
|
||||
|
||||
(define (mktemp-directory pattern)
|
||||
(command-output "mktemp" "-d" pattern))
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
(ice-9 format)
|
||||
(ice-9 match)
|
||||
(srfi srfi-1)
|
||||
(srfi srfi-13))
|
||||
(srfi srfi-13)
|
||||
(rnrs io ports))
|
||||
|
||||
(define (usage code)
|
||||
(format (if (= code 0) #t (current-error-port))
|
||||
@@ -15,24 +16,35 @@
|
||||
Commands:\n\
|
||||
system ACTION ... Build or materialize Fruix system artifacts.\n\
|
||||
source ACTION ... Fetch or snapshot declarative FreeBSD source inputs.\n\
|
||||
native-build ACTION ... Promote native build results into Fruix store objects.\n\
|
||||
\n\
|
||||
System actions:\n\
|
||||
build Materialize the Fruix system closure in /frx/store.\n\
|
||||
image Materialize the Fruix disk image in /frx/store.\n\
|
||||
installer Materialize a bootable Fruix installer image in /frx/store.\n\
|
||||
installer-iso Materialize a bootable Fruix installer ISO in /frx/store.\n\
|
||||
install Install the Fruix system onto --target PATH.\n\
|
||||
rootfs Materialize a rootfs tree at --rootfs DIR or ROOTFS-DIR.\n\
|
||||
\n\
|
||||
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' or raw-file 'install' targets.\n\
|
||||
--root-size SIZE Root filesystem size for 'image' or 'install' (example: 6g).\n\
|
||||
--disk-capacity SIZE Disk capacity for 'image', 'installer', or raw-file 'install' targets.\n\
|
||||
--root-size SIZE Root filesystem size for 'image', 'installer', 'installer-iso', or 'install' (example: 6g).\n\
|
||||
--target PATH Install target for 'install' (raw image file or /dev/... device).\n\
|
||||
--install-target-device DEVICE\n\
|
||||
Target block device used by the booted 'installer' environment.\n\
|
||||
--rootfs DIR Rootfs target for 'rootfs'.\n\
|
||||
\n\
|
||||
Source actions:\n\
|
||||
materialize Materialize a declared FreeBSD source tree in /frx/store.\n\
|
||||
\n\
|
||||
Native-build actions:\n\
|
||||
promote Promote a native build result root into /frx/store.\n\
|
||||
\n\
|
||||
Native-build options:\n\
|
||||
--store DIR Store directory to use (default: /frx/store).\n\
|
||||
\n\
|
||||
Source options:\n\
|
||||
--source NAME Scheme variable holding the freebsd-source object.\n\
|
||||
--store DIR Store directory to use (default: /frx/store).\n\
|
||||
@@ -58,6 +70,9 @@ Common options:\n\
|
||||
(format #t "~a=~a~%" (car field) (stringify (cdr field))))
|
||||
fields))
|
||||
|
||||
(define (read-file-string file)
|
||||
(call-with-input-file file get-string-all))
|
||||
|
||||
(define (lookup-bound-value module symbol)
|
||||
(let ((var (module-variable module symbol)))
|
||||
(and var (variable-ref var))))
|
||||
@@ -126,6 +141,7 @@ Common options:\n\
|
||||
(disk-capacity #f)
|
||||
(root-size #f)
|
||||
(target #f)
|
||||
(install-target-device #f)
|
||||
(rootfs #f))
|
||||
(match args
|
||||
(()
|
||||
@@ -138,37 +154,44 @@ Common options:\n\
|
||||
(disk-capacity . ,disk-capacity)
|
||||
(root-size . ,root-size)
|
||||
(target . ,target)
|
||||
(install-target-device . ,install-target-device)
|
||||
(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 target rootfs))
|
||||
(loop tail positional (option-value arg "--system=") store-dir disk-capacity root-size target install-target-device rootfs))
|
||||
(("--system" value . tail)
|
||||
(loop tail positional value store-dir disk-capacity root-size target rootfs))
|
||||
(loop tail positional value store-dir disk-capacity root-size target install-target-device rootfs))
|
||||
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
|
||||
(loop tail positional system-name (option-value arg "--store=") disk-capacity root-size target rootfs))
|
||||
(loop tail positional system-name (option-value arg "--store=") disk-capacity root-size target install-target-device rootfs))
|
||||
(("--store" value . tail)
|
||||
(loop tail positional system-name value disk-capacity root-size target rootfs))
|
||||
(loop tail positional system-name value disk-capacity root-size target install-target-device rootfs))
|
||||
(((? (lambda (arg) (string-prefix? "--disk-capacity=" arg)) arg) . tail)
|
||||
(loop tail positional system-name store-dir (option-value arg "--disk-capacity=") root-size target rootfs))
|
||||
(loop tail positional system-name store-dir (option-value arg "--disk-capacity=") root-size target install-target-device rootfs))
|
||||
(("--disk-capacity" value . tail)
|
||||
(loop tail positional system-name store-dir value root-size target rootfs))
|
||||
(loop tail positional system-name store-dir value root-size target install-target-device rootfs))
|
||||
(((? (lambda (arg) (string-prefix? "--root-size=" arg)) arg) . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity (option-value arg "--root-size=") target rootfs))
|
||||
(loop tail positional system-name store-dir disk-capacity (option-value arg "--root-size=") target install-target-device rootfs))
|
||||
(("--root-size" value . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity value target rootfs))
|
||||
(loop tail positional system-name store-dir disk-capacity value target install-target-device 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))
|
||||
(loop tail positional system-name store-dir disk-capacity root-size (option-value arg "--target=") install-target-device rootfs))
|
||||
(("--target" value . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity root-size value rootfs))
|
||||
(loop tail positional system-name store-dir disk-capacity root-size value install-target-device rootfs))
|
||||
(((? (lambda (arg) (string-prefix? "--install-target-device=" arg)) arg) . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity root-size target
|
||||
(option-value arg "--install-target-device=") rootfs))
|
||||
(("--install-target-device" value . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity root-size target value rootfs))
|
||||
(((? (lambda (arg) (string-prefix? "--rootfs=" arg)) arg) . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity root-size target (option-value arg "--rootfs=")))
|
||||
(loop tail positional system-name store-dir disk-capacity root-size target install-target-device
|
||||
(option-value arg "--rootfs=")))
|
||||
(("--rootfs" value . tail)
|
||||
(loop tail positional system-name store-dir disk-capacity root-size target value))
|
||||
(loop tail positional system-name store-dir disk-capacity root-size target install-target-device 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 target rootfs)))))
|
||||
(loop tail (cons arg positional) system-name store-dir disk-capacity root-size target install-target-device rootfs)))))
|
||||
|
||||
(define (parse-source-arguments action rest)
|
||||
(let loop ((args rest)
|
||||
@@ -204,6 +227,28 @@ Common options:\n\
|
||||
((arg . tail)
|
||||
(loop tail (cons arg positional) source-name store-dir cache-dir)))))
|
||||
|
||||
(define (parse-native-build-arguments action rest)
|
||||
(let loop ((args rest)
|
||||
(positional '())
|
||||
(store-dir "/frx/store"))
|
||||
(match args
|
||||
(()
|
||||
(let ((positional (reverse positional)))
|
||||
`((command . "native-build")
|
||||
(action . ,action)
|
||||
(positional . ,positional)
|
||||
(store-dir . ,store-dir))))
|
||||
(("--help")
|
||||
(usage 0))
|
||||
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
|
||||
(loop tail positional (option-value arg "--store=")))
|
||||
(("--store" value . tail)
|
||||
(loop tail positional value))
|
||||
(((? (lambda (arg) (string-prefix? "--" arg)) arg) . _)
|
||||
(error "unknown option" arg))
|
||||
((arg . tail)
|
||||
(loop tail (cons arg positional) store-dir)))))
|
||||
|
||||
(define (parse-arguments argv)
|
||||
(match argv
|
||||
((_)
|
||||
@@ -216,10 +261,14 @@ Common options:\n\
|
||||
(usage 0))
|
||||
((_ "source" "--help")
|
||||
(usage 0))
|
||||
((_ "native-build" "--help")
|
||||
(usage 0))
|
||||
((_ "system" action . rest)
|
||||
(parse-system-arguments action rest))
|
||||
((_ "source" action . rest)
|
||||
(parse-source-arguments action rest))
|
||||
((_ "native-build" action . rest)
|
||||
(parse-native-build-arguments action rest))
|
||||
((_ . _)
|
||||
(usage 1))))
|
||||
|
||||
@@ -399,6 +448,154 @@ Common options:\n\
|
||||
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
|
||||
(store_item_count . ,(length store-items))))))
|
||||
|
||||
(define (emit-system-installer-metadata os-file resolved-symbol store-dir os result)
|
||||
(let* ((installer-image-spec (assoc-ref result 'installer-image-spec))
|
||||
(image-spec (assoc-ref result 'image-spec))
|
||||
(store-items (assoc-ref result 'store-items))
|
||||
(target-store-items (assoc-ref result 'target-store-items))
|
||||
(installer-store-items (assoc-ref result 'installer-store-items))
|
||||
(host-base-stores (assoc-ref result 'host-base-stores))
|
||||
(native-base-stores (assoc-ref result 'native-base-stores))
|
||||
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
|
||||
(base (operating-system-freebsd-base os))
|
||||
(source (freebsd-base-source base))
|
||||
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
|
||||
(emit-metadata
|
||||
`((action . "installer")
|
||||
(os_file . ,os-file)
|
||||
(system_variable . ,resolved-symbol)
|
||||
(store_dir . ,store-dir)
|
||||
(freebsd_base_name . ,(freebsd-base-name base))
|
||||
(freebsd_base_version_label . ,(freebsd-base-version-label base))
|
||||
(freebsd_base_release . ,(freebsd-base-release base))
|
||||
(freebsd_base_branch . ,(freebsd-base-branch base))
|
||||
(freebsd_base_source_root . ,(freebsd-base-source-root base))
|
||||
(freebsd_base_target . ,(freebsd-base-target base))
|
||||
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
|
||||
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
|
||||
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
|
||||
(freebsd_source_name . ,(freebsd-source-name source))
|
||||
(freebsd_source_kind . ,(freebsd-source-kind source))
|
||||
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
|
||||
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
|
||||
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
|
||||
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
|
||||
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
|
||||
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
|
||||
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
|
||||
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
|
||||
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
|
||||
(disk_capacity . ,(assoc-ref image-spec 'disk-capacity))
|
||||
(root_size . ,(assoc-ref image-spec 'root-size))
|
||||
(installer_host_name . ,(assoc-ref installer-image-spec 'installer-host-name))
|
||||
(install_target_device . ,(assoc-ref result 'install-target-device))
|
||||
(installer_state_path . ,(assoc-ref result 'installer-state-path))
|
||||
(installer_log_path . ,(assoc-ref result 'installer-log-path))
|
||||
(image_store_path . ,(assoc-ref result 'image-store-path))
|
||||
(disk_image . ,(assoc-ref result 'disk-image))
|
||||
(esp_image . ,(assoc-ref result 'esp-image))
|
||||
(root_image . ,(assoc-ref result 'root-image))
|
||||
(installer_closure_path . ,(assoc-ref result 'installer-closure-path))
|
||||
(target_closure_path . ,(assoc-ref result 'target-closure-path))
|
||||
(host_base_store_count . ,(length host-base-stores))
|
||||
(host_base_stores . ,(string-join host-base-stores ","))
|
||||
(native_base_store_count . ,(length native-base-stores))
|
||||
(native_base_stores . ,(string-join native-base-stores ","))
|
||||
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
|
||||
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
|
||||
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
|
||||
(store_layout_file . ,(assoc-ref result 'store-layout-file))
|
||||
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
|
||||
(host_uname . ,(assoc-ref host-provenance 'uname))
|
||||
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
|
||||
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
|
||||
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
|
||||
(store_item_count . ,(length store-items))
|
||||
(target_store_item_count . ,(length target-store-items))
|
||||
(installer_store_item_count . ,(length installer-store-items))))))
|
||||
|
||||
(define (emit-system-installer-iso-metadata os-file resolved-symbol store-dir os result)
|
||||
(let* ((installer-iso-spec (assoc-ref result 'installer-iso-spec))
|
||||
(store-items (assoc-ref result 'store-items))
|
||||
(target-store-items (assoc-ref result 'target-store-items))
|
||||
(installer-store-items (assoc-ref result 'installer-store-items))
|
||||
(host-base-stores (assoc-ref result 'host-base-stores))
|
||||
(native-base-stores (assoc-ref result 'native-base-stores))
|
||||
(fruix-runtime-stores (assoc-ref result 'fruix-runtime-stores))
|
||||
(base (operating-system-freebsd-base os))
|
||||
(source (freebsd-base-source base))
|
||||
(host-provenance (call-with-input-file (assoc-ref result 'host-base-provenance-file) read)))
|
||||
(emit-metadata
|
||||
`((action . "installer-iso")
|
||||
(os_file . ,os-file)
|
||||
(system_variable . ,resolved-symbol)
|
||||
(store_dir . ,store-dir)
|
||||
(freebsd_base_name . ,(freebsd-base-name base))
|
||||
(freebsd_base_version_label . ,(freebsd-base-version-label base))
|
||||
(freebsd_base_release . ,(freebsd-base-release base))
|
||||
(freebsd_base_branch . ,(freebsd-base-branch base))
|
||||
(freebsd_base_source_root . ,(freebsd-base-source-root base))
|
||||
(freebsd_base_target . ,(freebsd-base-target base))
|
||||
(freebsd_base_target_arch . ,(freebsd-base-target-arch base))
|
||||
(freebsd_base_kernconf . ,(freebsd-base-kernconf base))
|
||||
(freebsd_base_file . ,(assoc-ref result 'freebsd-base-file))
|
||||
(freebsd_source_name . ,(freebsd-source-name source))
|
||||
(freebsd_source_kind . ,(freebsd-source-kind source))
|
||||
(freebsd_source_url . ,(or (freebsd-source-url source) ""))
|
||||
(freebsd_source_path . ,(or (freebsd-source-path source) ""))
|
||||
(freebsd_source_ref . ,(or (freebsd-source-ref source) ""))
|
||||
(freebsd_source_commit . ,(or (freebsd-source-commit source) ""))
|
||||
(freebsd_source_sha256 . ,(or (freebsd-source-sha256 source) ""))
|
||||
(freebsd_source_file . ,(assoc-ref result 'freebsd-source-file))
|
||||
(freebsd_source_materializations_file . ,(assoc-ref result 'freebsd-source-materializations-file))
|
||||
(materialized_source_store_count . ,(length (assoc-ref result 'materialized-source-stores)))
|
||||
(materialized_source_stores . ,(string-join (assoc-ref result 'materialized-source-stores) ","))
|
||||
(installer_host_name . ,(assoc-ref installer-iso-spec 'installer-host-name))
|
||||
(install_target_device . ,(assoc-ref result 'install-target-device))
|
||||
(iso_volume_label . ,(assoc-ref installer-iso-spec 'iso-volume-label))
|
||||
(root_size . ,(assoc-ref installer-iso-spec 'root-size))
|
||||
(installer_state_path . ,(assoc-ref result 'installer-state-path))
|
||||
(installer_log_path . ,(assoc-ref result 'installer-log-path))
|
||||
(iso_store_path . ,(assoc-ref result 'iso-store-path))
|
||||
(iso_image . ,(assoc-ref result 'iso-image))
|
||||
(boot_efi_image . ,(assoc-ref result 'boot-efi-image))
|
||||
(root_image . ,(assoc-ref result 'root-image))
|
||||
(installer_closure_path . ,(assoc-ref result 'installer-closure-path))
|
||||
(target_closure_path . ,(assoc-ref result 'target-closure-path))
|
||||
(host_base_store_count . ,(length host-base-stores))
|
||||
(host_base_stores . ,(string-join host-base-stores ","))
|
||||
(native_base_store_count . ,(length native-base-stores))
|
||||
(native_base_stores . ,(string-join native-base-stores ","))
|
||||
(fruix_runtime_store_count . ,(length fruix-runtime-stores))
|
||||
(fruix_runtime_stores . ,(string-join fruix-runtime-stores ","))
|
||||
(host_base_provenance_file . ,(assoc-ref result 'host-base-provenance-file))
|
||||
(store_layout_file . ,(assoc-ref result 'store-layout-file))
|
||||
(host_freebsd_version . ,(assoc-ref host-provenance 'freebsd-version-kru))
|
||||
(host_uname . ,(assoc-ref host-provenance 'uname))
|
||||
(usr_src_git_revision . ,(assoc-ref host-provenance 'usr-src-git-revision))
|
||||
(usr_src_git_branch . ,(assoc-ref host-provenance 'usr-src-git-branch))
|
||||
(usr_src_newvers_sha256 . ,(assoc-ref host-provenance 'usr-src-newvers-sha256))
|
||||
(store_item_count . ,(length store-items))
|
||||
(target_store_item_count . ,(length target-store-items))
|
||||
(installer_store_item_count . ,(length installer-store-items))))))
|
||||
|
||||
(define (emit-native-build-promotion-metadata store-dir result-root result)
|
||||
(emit-metadata
|
||||
`((action . "promote")
|
||||
(result_root . ,result-root)
|
||||
(store_dir . ,store-dir)
|
||||
(executor_kind . ,(assoc-ref result 'executor-kind))
|
||||
(executor_name . ,(assoc-ref result 'executor-name))
|
||||
(executor_version . ,(assoc-ref result 'executor-version))
|
||||
(result_store . ,(assoc-ref result 'result-store))
|
||||
(result_metadata_file . ,(assoc-ref result 'result-metadata-file))
|
||||
(artifact_store_count . ,(assoc-ref result 'artifact-store-count))
|
||||
(artifact_stores . ,(string-join (assoc-ref result 'artifact-stores) ","))
|
||||
(world_store . ,(assoc-ref result 'world-store))
|
||||
(kernel_store . ,(assoc-ref result 'kernel-store))
|
||||
(headers_store . ,(assoc-ref result 'headers-store))
|
||||
(bootloader_store . ,(assoc-ref result 'bootloader-store)))))
|
||||
|
||||
(define (main argv)
|
||||
(let* ((parsed (parse-arguments argv))
|
||||
(command (assoc-ref parsed 'command))
|
||||
@@ -410,10 +607,11 @@ Common options:\n\
|
||||
(disk-capacity (assoc-ref parsed 'disk-capacity))
|
||||
(root-size (assoc-ref parsed 'root-size))
|
||||
(target-opt (assoc-ref parsed 'target))
|
||||
(install-target-device (assoc-ref parsed 'install-target-device))
|
||||
(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" "install" "rootfs"))
|
||||
(unless (member action '("build" "image" "installer" "installer-iso" "install" "rootfs"))
|
||||
(error "unknown system action" action))
|
||||
(let* ((os-file (match positional
|
||||
((file . _) file)
|
||||
@@ -435,7 +633,11 @@ Common options:\n\
|
||||
(lambda (os resolved-symbol)
|
||||
(let* ((guile-prefix (or (getenv "GUILE_PREFIX") "/tmp/guile-freebsd-validate-install"))
|
||||
(guile-extra-prefix (or (getenv "GUILE_EXTRA_PREFIX") "/tmp/guile-gnutls-freebsd-validate-install"))
|
||||
(shepherd-prefix (or (getenv "SHEPHERD_PREFIX") "/tmp/shepherd-freebsd-validate-install")))
|
||||
(shepherd-prefix (or (getenv "SHEPHERD_PREFIX") "/tmp/shepherd-freebsd-validate-install"))
|
||||
(guile-store-path (getenv "FRUIX_GUILE_STORE"))
|
||||
(guile-extra-store-path (getenv "FRUIX_GUILE_EXTRA_STORE"))
|
||||
(shepherd-store-path (getenv "FRUIX_SHEPHERD_STORE"))
|
||||
(declaration-source (read-file-string os-file)))
|
||||
(cond
|
||||
((string=? action "build")
|
||||
(emit-system-build-metadata
|
||||
@@ -444,7 +646,13 @@ Common options:\n\
|
||||
#:store-dir store-dir
|
||||
#:guile-prefix guile-prefix
|
||||
#:guile-extra-prefix guile-extra-prefix
|
||||
#:shepherd-prefix shepherd-prefix)))
|
||||
#:shepherd-prefix shepherd-prefix
|
||||
#:guile-store-path guile-store-path
|
||||
#:guile-extra-store-path guile-extra-store-path
|
||||
#:shepherd-store-path shepherd-store-path
|
||||
#:declaration-source declaration-source
|
||||
#:declaration-origin os-file
|
||||
#:declaration-system-symbol resolved-symbol)))
|
||||
((string=? action "rootfs")
|
||||
(unless rootfs
|
||||
(error "rootfs action requires ROOTFS-DIR or --rootfs DIR"))
|
||||
@@ -452,7 +660,10 @@ Common options:\n\
|
||||
#:store-dir store-dir
|
||||
#:guile-prefix guile-prefix
|
||||
#:guile-extra-prefix guile-extra-prefix
|
||||
#:shepherd-prefix shepherd-prefix)))
|
||||
#:shepherd-prefix shepherd-prefix
|
||||
#:declaration-source declaration-source
|
||||
#:declaration-origin os-file
|
||||
#:declaration-system-symbol resolved-symbol)))
|
||||
(emit-metadata
|
||||
`((action . "rootfs")
|
||||
(os_file . ,os-file)
|
||||
@@ -470,8 +681,38 @@ Common options:\n\
|
||||
#:guile-prefix guile-prefix
|
||||
#:guile-extra-prefix guile-extra-prefix
|
||||
#:shepherd-prefix shepherd-prefix
|
||||
#:declaration-source declaration-source
|
||||
#:declaration-origin os-file
|
||||
#:declaration-system-symbol resolved-symbol
|
||||
#:root-size (or root-size "256m")
|
||||
#:disk-capacity disk-capacity)))
|
||||
((string=? action "installer")
|
||||
(emit-system-installer-metadata
|
||||
os-file resolved-symbol store-dir os
|
||||
(materialize-installer-image os
|
||||
#:store-dir store-dir
|
||||
#:guile-prefix guile-prefix
|
||||
#:guile-extra-prefix guile-extra-prefix
|
||||
#:shepherd-prefix shepherd-prefix
|
||||
#:declaration-source declaration-source
|
||||
#:declaration-origin os-file
|
||||
#:declaration-system-symbol resolved-symbol
|
||||
#:install-target-device (or install-target-device "/dev/vtbd1")
|
||||
#:root-size (or root-size "10g")
|
||||
#:disk-capacity disk-capacity)))
|
||||
((string=? action "installer-iso")
|
||||
(emit-system-installer-iso-metadata
|
||||
os-file resolved-symbol store-dir os
|
||||
(materialize-installer-iso os
|
||||
#:store-dir store-dir
|
||||
#:guile-prefix guile-prefix
|
||||
#:guile-extra-prefix guile-extra-prefix
|
||||
#:shepherd-prefix shepherd-prefix
|
||||
#:declaration-source declaration-source
|
||||
#:declaration-origin os-file
|
||||
#:declaration-system-symbol resolved-symbol
|
||||
#:install-target-device (or install-target-device "/dev/vtbd0")
|
||||
#:root-size root-size)))
|
||||
((string=? action "install")
|
||||
(unless target
|
||||
(error "install action requires TARGET or --target PATH"))
|
||||
@@ -483,6 +724,9 @@ Common options:\n\
|
||||
#:guile-prefix guile-prefix
|
||||
#:guile-extra-prefix guile-extra-prefix
|
||||
#:shepherd-prefix shepherd-prefix
|
||||
#:declaration-source declaration-source
|
||||
#:declaration-origin os-file
|
||||
#:declaration-system-symbol resolved-symbol
|
||||
#:root-size root-size
|
||||
#:disk-capacity disk-capacity))))))))))
|
||||
((string=? command "source")
|
||||
@@ -527,6 +771,16 @@ Common options:\n\
|
||||
(materialized_source_ref . ,(or (assoc-ref effective 'ref) ""))
|
||||
(materialized_source_commit . ,(or (assoc-ref result 'effective-commit) ""))
|
||||
(materialized_source_sha256 . ,(or (assoc-ref result 'effective-sha256) ""))))))))))
|
||||
((string=? command "native-build")
|
||||
(let ((positional (assoc-ref parsed 'positional)))
|
||||
(unless (string=? action "promote")
|
||||
(error "unknown native-build action" action))
|
||||
(let ((result-root (match positional
|
||||
((path . _) path)
|
||||
(() (error "missing native build result root argument")))))
|
||||
(emit-native-build-promotion-metadata
|
||||
store-dir result-root
|
||||
(promote-native-build-result result-root #:store-dir store-dir)))))
|
||||
(else
|
||||
(usage 1)))))
|
||||
|
||||
|
||||
@@ -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-target-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__")))
|
||||
@@ -0,0 +1,91 @@
|
||||
(use-modules (fruix system freebsd)
|
||||
(fruix packages freebsd))
|
||||
|
||||
(define phase19-source
|
||||
(freebsd-source
|
||||
#:name "__SOURCE_NAME__"
|
||||
#:kind 'git
|
||||
#:ref "__SOURCE_REF__"
|
||||
#:commit "__SOURCE_COMMIT__"))
|
||||
|
||||
(define phase19-base
|
||||
(freebsd-base
|
||||
#:name "__BASE_NAME__"
|
||||
#:version-label "__BASE_VERSION_LABEL__"
|
||||
#:release "__BASE_RELEASE__"
|
||||
#:branch "__BASE_BRANCH__"
|
||||
#:source phase19-source
|
||||
#:source-root "__DECLARED_SOURCE_ROOT__"
|
||||
#:target "amd64"
|
||||
#:target-arch "amd64"
|
||||
#:kernconf "GENERIC"))
|
||||
|
||||
(define phase19-operating-system
|
||||
(operating-system
|
||||
#:host-name "__HOST_NAME__"
|
||||
#:freebsd-base phase19-base
|
||||
#:kernel (freebsd-native-kernel-for phase19-base)
|
||||
#:bootloader (freebsd-native-bootloader-for phase19-base)
|
||||
#:base-packages (freebsd-native-system-packages-for phase19-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__")))
|
||||
75
tests/system/phase20-development-operating-system.scm.in
Normal file
75
tests/system/phase20-development-operating-system.scm.in
Normal file
@@ -0,0 +1,75 @@
|
||||
(use-modules (fruix system freebsd)
|
||||
(fruix packages freebsd))
|
||||
|
||||
(define phase20-operating-system
|
||||
(operating-system
|
||||
#:host-name "fruix-freebsd"
|
||||
#:kernel freebsd-native-kernel
|
||||
#:bootloader freebsd-native-bootloader
|
||||
#:base-packages %freebsd-native-system-packages
|
||||
#:development-packages (list freebsd-native-headers
|
||||
freebsd-clang-toolchain)
|
||||
#:build-packages (list freebsd-native-headers
|
||||
freebsd-clang-toolchain)
|
||||
#: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__")))
|
||||
@@ -0,0 +1,73 @@
|
||||
(use-modules (fruix system freebsd)
|
||||
(fruix packages freebsd))
|
||||
|
||||
(define phase20-promoted-native-build-result
|
||||
(promoted-native-build-result
|
||||
#:store-path "__PROMOTED_RESULT_STORE__"))
|
||||
|
||||
(define phase20-promoted-native-base-operating-system
|
||||
(operating-system-from-promoted-native-build-result
|
||||
phase20-promoted-native-build-result
|
||||
#:host-name "fruix-freebsd"
|
||||
#: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__")))
|
||||
@@ -0,0 +1,73 @@
|
||||
(use-modules (fruix system freebsd)
|
||||
(fruix packages freebsd))
|
||||
|
||||
(define postphase20-promoted-native-build-result
|
||||
(promoted-native-build-result
|
||||
#:store-path "__PROMOTED_RESULT_STORE__"))
|
||||
|
||||
(define postphase20-installed-node-operating-system
|
||||
(operating-system-from-promoted-native-build-result
|
||||
postphase20-promoted-native-build-result
|
||||
#:host-name "__HOST_NAME__"
|
||||
#: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__")))
|
||||
399
tests/system/run-phase18-installer-environment.sh
Executable file
399
tests/system/run-phase18-installer-environment.sh
Executable file
@@ -0,0 +1,399 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
project_root=${PROJECT_ROOT:-$(pwd)}
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
fruix_cmd=$project_root/bin/fruix
|
||||
os_template=${OS_TEMPLATE:-$script_dir/phase18-installer-target-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase18-target-operating-system}
|
||||
store_dir=${STORE_DIR:-/frx/store}
|
||||
installer_disk_capacity=${INSTALLER_DISK_CAPACITY:-16g}
|
||||
installer_root_size=${INSTALLER_ROOT_SIZE:-14g}
|
||||
target_disk_capacity=${TARGET_DISK_CAPACITY:-12g}
|
||||
install_target_device=${INSTALL_TARGET_DEVICE:-/dev/vtbd1}
|
||||
qemu_smp=${QEMU_SMP:-2}
|
||||
installer_ssh_port=${INSTALLER_SSH_PORT:-10025}
|
||||
target_ssh_port=${TARGET_SSH_PORT:-10026}
|
||||
base_name=${BASE_NAME:-phase18-installer-target}
|
||||
base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-installer-target}
|
||||
base_release=${BASE_RELEASE:-15.0-STABLE}
|
||||
base_branch=${BASE_BRANCH:-stable/15}
|
||||
source_name=${SOURCE_NAME:-stable15-installer-target-source}
|
||||
source_ref=${SOURCE_REF:-stable/15}
|
||||
source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89}
|
||||
declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-installer-target}
|
||||
metadata_target=${METADATA_OUT:-}
|
||||
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
|
||||
root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}
|
||||
|
||||
[ -x "$fruix_cmd" ] || {
|
||||
echo "fruix command is not executable: $fruix_cmd" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$os_template" ] || {
|
||||
echo "missing operating-system template: $os_template" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$root_authorized_key_file" ] || {
|
||||
echo "missing root authorized key file: $root_authorized_key_file" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$root_ssh_private_key_file" ] || {
|
||||
echo "missing root SSH private key file: $root_ssh_private_key_file" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v qemu-system-x86_64 >/dev/null 2>&1 || {
|
||||
echo "qemu-system-x86_64 is required" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f /usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd ] || {
|
||||
echo "missing QEMU UEFI firmware" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup=0
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase18-installer.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
target_os_file=$workdir/phase18-installer-target-operating-system.scm
|
||||
installer_out=$workdir/installer.txt
|
||||
metadata_file=$workdir/phase18-installer-environment-metadata.txt
|
||||
installer_serial_log=$workdir/installer-serial.log
|
||||
target_serial_log=$workdir/target-serial.log
|
||||
installer_qemu_pidfile=$workdir/installer-qemu.pid
|
||||
target_qemu_pidfile=$workdir/target-qemu.pid
|
||||
installer_uefi_vars=$workdir/installer-vars.fd
|
||||
target_uefi_vars=$workdir/target-vars.fd
|
||||
installer_boot_image=$workdir/installer-boot.img
|
||||
target_image=$workdir/installed-target.img
|
||||
gpart_log=$workdir/gpart-show.txt
|
||||
mnt_esp=$workdir/mnt-esp
|
||||
mnt_root=$workdir/mnt-root
|
||||
md_unit=
|
||||
|
||||
cleanup_workdir() {
|
||||
if [ -f "$installer_qemu_pidfile" ]; then
|
||||
sudo kill "$(sudo cat "$installer_qemu_pidfile")" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -f "$target_qemu_pidfile" ]; then
|
||||
sudo kill "$(sudo cat "$target_qemu_pidfile")" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$md_unit" ]; then
|
||||
sudo umount "$mnt_esp" >/dev/null 2>&1 || true
|
||||
sudo umount "$mnt_root" >/dev/null 2>&1 || true
|
||||
sudo mdconfig -d -u "$md_unit" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_workdir EXIT INT TERM
|
||||
|
||||
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
|
||||
sed \
|
||||
-e "s|__BASE_NAME__|$base_name|g" \
|
||||
-e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \
|
||||
-e "s|__BASE_RELEASE__|$base_release|g" \
|
||||
-e "s|__BASE_BRANCH__|$base_branch|g" \
|
||||
-e "s|__SOURCE_NAME__|$source_name|g" \
|
||||
-e "s|__SOURCE_REF__|$source_ref|g" \
|
||||
-e "s|__SOURCE_COMMIT__|$source_commit|g" \
|
||||
-e "s|__DECLARED_SOURCE_ROOT__|$declared_source_root|g" \
|
||||
-e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \
|
||||
"$os_template" > "$target_os_file"
|
||||
|
||||
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$installer_uefi_vars"
|
||||
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$target_uefi_vars"
|
||||
truncate -s "$target_disk_capacity" "$target_image"
|
||||
mkdir -p "$mnt_esp" "$mnt_root"
|
||||
|
||||
action_env() {
|
||||
sudo env \
|
||||
HOME="$HOME" \
|
||||
GUILE_AUTO_COMPILE=0 \
|
||||
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
|
||||
GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \
|
||||
GUILE_BIN="${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}" \
|
||||
GUILE_EXTRA_PREFIX="${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}" \
|
||||
SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
action_env "$fruix_cmd" system installer "$target_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" \
|
||||
--install-target-device "$install_target_device" \
|
||||
--disk-capacity "$installer_disk_capacity" \
|
||||
--root-size "$installer_root_size" >"$installer_out"
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$installer_out" | tail -n 1
|
||||
}
|
||||
|
||||
image_store_path=$(field image_store_path)
|
||||
installer_disk_image=$(field disk_image)
|
||||
installer_esp_image=$(field esp_image)
|
||||
installer_root_image=$(field root_image)
|
||||
installer_closure_path=$(field installer_closure_path)
|
||||
target_closure_path=$(field target_closure_path)
|
||||
installer_host_name=$(field installer_host_name)
|
||||
install_target_device_out=$(field install_target_device)
|
||||
installer_state_path=$(field installer_state_path)
|
||||
installer_log_path=$(field installer_log_path)
|
||||
freebsd_source_kind_out=$(field freebsd_source_kind)
|
||||
freebsd_source_ref_out=$(field freebsd_source_ref)
|
||||
freebsd_source_commit_out=$(field freebsd_source_commit)
|
||||
freebsd_source_file=$(field freebsd_source_file)
|
||||
freebsd_source_materializations_file=$(field freebsd_source_materializations_file)
|
||||
materialized_source_store_count=$(field materialized_source_store_count)
|
||||
materialized_source_stores=$(field materialized_source_stores)
|
||||
host_base_store_count=$(field host_base_store_count)
|
||||
native_base_store_count=$(field native_base_store_count)
|
||||
native_base_stores=$(field native_base_stores)
|
||||
store_item_count=$(field store_item_count)
|
||||
target_store_item_count=$(field target_store_item_count)
|
||||
installer_store_item_count=$(field installer_store_item_count)
|
||||
store_layout_file=$(field store_layout_file)
|
||||
|
||||
[ -d "$image_store_path" ] || { echo "missing installer image store path: $image_store_path" >&2; exit 1; }
|
||||
[ -f "$installer_disk_image" ] || { echo "missing installer disk image: $installer_disk_image" >&2; exit 1; }
|
||||
[ -f "$installer_esp_image" ] || { echo "missing installer ESP image: $installer_esp_image" >&2; exit 1; }
|
||||
[ -f "$installer_root_image" ] || { echo "missing installer root image: $installer_root_image" >&2; exit 1; }
|
||||
[ -n "$installer_closure_path" ] || { echo "missing installer closure path" >&2; exit 1; }
|
||||
[ -n "$target_closure_path" ] || { echo "missing target closure path" >&2; exit 1; }
|
||||
[ "$install_target_device_out" = "$install_target_device" ] || { echo "unexpected install target device: $install_target_device_out" >&2; exit 1; }
|
||||
[ "$installer_host_name" = fruix-freebsd-installer ] || { echo "unexpected installer host name: $installer_host_name" >&2; exit 1; }
|
||||
[ "$freebsd_source_kind_out" = git ] || { echo "unexpected source kind: $freebsd_source_kind_out" >&2; exit 1; }
|
||||
[ "$freebsd_source_ref_out" = "$source_ref" ] || { echo "unexpected source ref: $freebsd_source_ref_out" >&2; exit 1; }
|
||||
[ "$freebsd_source_commit_out" = "$source_commit" ] || { echo "unexpected source commit: $freebsd_source_commit_out" >&2; exit 1; }
|
||||
[ "$materialized_source_store_count" = 1 ] || { echo "unexpected materialized source store count: $materialized_source_store_count" >&2; exit 1; }
|
||||
[ "$host_base_store_count" = 0 ] || { echo "expected zero host base stores, got: $host_base_store_count" >&2; exit 1; }
|
||||
[ "$native_base_store_count" = 3 ] || { echo "expected three native base stores, got: $native_base_store_count" >&2; exit 1; }
|
||||
[ -f "$freebsd_source_file" ] || { echo "missing freebsd source file: $freebsd_source_file" >&2; exit 1; }
|
||||
[ -f "$freebsd_source_materializations_file" ] || { echo "missing source materializations file: $freebsd_source_materializations_file" >&2; exit 1; }
|
||||
[ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; }
|
||||
case "$materialized_source_stores" in
|
||||
/frx/store/*-freebsd-source-$source_name) : ;;
|
||||
*) echo "unexpected materialized source store path: $materialized_source_stores" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "$store_item_count" -ge "$target_store_item_count" ] || { echo "combined store item count smaller than target store item count" >&2; exit 1; }
|
||||
[ "$installer_store_item_count" -ge 1 ] || { echo "expected installer store items" >&2; exit 1; }
|
||||
|
||||
cp "$installer_disk_image" "$installer_boot_image"
|
||||
|
||||
target_closure_base=$(basename "$target_closure_path")
|
||||
installer_closure_base=$(basename "$installer_closure_path")
|
||||
|
||||
sudo qemu-system-x86_64 \
|
||||
-machine q35,accel=tcg \
|
||||
-cpu max \
|
||||
-m 2048 \
|
||||
-smp "$qemu_smp" \
|
||||
-display none \
|
||||
-serial "file:$installer_serial_log" \
|
||||
-monitor none \
|
||||
-pidfile "$installer_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="$installer_uefi_vars" \
|
||||
-drive if=virtio,format=raw,file="$installer_boot_image" \
|
||||
-drive if=virtio,format=raw,file="$target_image" \
|
||||
-netdev user,id=net0,hostfwd=tcp::${installer_ssh_port}-:22 \
|
||||
-device virtio-net-pci,netdev=net0
|
||||
|
||||
installer_guest() {
|
||||
ssh -p "$installer_ssh_port" -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR \
|
||||
-o ConnectTimeout=5 \
|
||||
root@127.0.0.1 "$@"
|
||||
}
|
||||
|
||||
installer_ssh_reached=0
|
||||
installer_state=missing
|
||||
for attempt in $(jot 150 1 150); do
|
||||
if installer_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
|
||||
installer_ssh_reached=1
|
||||
installer_state=$(installer_guest "cat '$installer_state_path' 2>/dev/null || echo missing")
|
||||
[ "$installer_state" = done ] && break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
[ "$installer_ssh_reached" = 1 ] || { echo "installer environment never became reachable over SSH" >&2; exit 1; }
|
||||
[ "$installer_state" = done ] || { echo "installer environment did not finish installation: $installer_state" >&2; exit 1; }
|
||||
|
||||
installer_run_current_system=$(installer_guest 'readlink /run/current-system')
|
||||
installer_sshd_status=$(installer_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
installer_activate_log=$(installer_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||
installer_log=$(installer_guest "cat '$installer_log_path' 2>/dev/null || true" | tr '\n' ' ')
|
||||
|
||||
[ "$installer_run_current_system" = "/frx/store/$installer_closure_base" ] || { echo "unexpected installer current-system target: $installer_run_current_system" >&2; exit 1; }
|
||||
[ "$installer_sshd_status" = running ] || { echo "installer sshd is not running" >&2; exit 1; }
|
||||
case "$installer_activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "installer activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$installer_log" in
|
||||
*fruix-installer:done*) : ;;
|
||||
*) echo "installer log does not show completion" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
sudo kill "$(sudo cat "$installer_qemu_pidfile")" >/dev/null 2>&1 || true
|
||||
rm -f "$installer_qemu_pidfile"
|
||||
sleep 2
|
||||
|
||||
md=$(sudo mdconfig -a -t vnode -f "$target_image")
|
||||
md_unit=${md#md}
|
||||
sudo gpart show -lp "/dev/$md" >"$gpart_log"
|
||||
esp_fstype=$(sudo fstyp "/dev/${md}p1")
|
||||
root_fstype=$(sudo fstyp "/dev/${md}p2")
|
||||
[ "$esp_fstype" = msdosfs ] || { echo "unexpected target ESP filesystem: $esp_fstype" >&2; exit 1; }
|
||||
[ "$root_fstype" = ufs ] || { echo "unexpected target root filesystem: $root_fstype" >&2; exit 1; }
|
||||
|
||||
sudo mount -t msdosfs "/dev/${md}p1" "$mnt_esp"
|
||||
sudo mount -t ufs -o ro "/dev/${md}p2" "$mnt_root"
|
||||
|
||||
[ -f "$mnt_esp/EFI/BOOT/BOOTX64.EFI" ] || { echo "missing EFI boot file on installed target" >&2; exit 1; }
|
||||
target_run_current_system=$(readlink "$mnt_root/run/current-system")
|
||||
target_boot_loader=$(readlink "$mnt_root/boot/loader")
|
||||
install_metadata_host=$(cat "$mnt_root/var/lib/fruix/install.scm")
|
||||
[ "$target_run_current_system" = "/frx/store/$target_closure_base" ] || { echo "unexpected target /run/current-system target: $target_run_current_system" >&2; exit 1; }
|
||||
[ "$target_boot_loader" = /run/current-system/boot/loader ] || { echo "unexpected target boot loader link: $target_boot_loader" >&2; exit 1; }
|
||||
[ -d "$mnt_root/frx/store/$target_closure_base" ] || { echo "installed target closure missing from target root" >&2; exit 1; }
|
||||
case "$install_metadata_host" in
|
||||
*"$target_closure_path"*) : ;;
|
||||
*) echo "installed target metadata does not record target closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$install_metadata_host" in
|
||||
*"$materialized_source_stores"*) : ;;
|
||||
*) echo "installed target metadata does not record materialized source store" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
sudo umount "$mnt_esp"
|
||||
sudo umount "$mnt_root"
|
||||
sudo mdconfig -d -u "$md_unit"
|
||||
md_unit=
|
||||
|
||||
sudo qemu-system-x86_64 \
|
||||
-machine q35,accel=tcg \
|
||||
-cpu max \
|
||||
-m 2048 \
|
||||
-smp "$qemu_smp" \
|
||||
-display none \
|
||||
-serial "file:$target_serial_log" \
|
||||
-monitor none \
|
||||
-pidfile "$target_qemu_pidfile" \
|
||||
-daemonize \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \
|
||||
-drive if=pflash,format=raw,file="$target_uefi_vars" \
|
||||
-drive if=virtio,format=raw,file="$target_image" \
|
||||
-netdev user,id=net0,hostfwd=tcp::${target_ssh_port}-:22 \
|
||||
-device virtio-net-pci,netdev=net0
|
||||
|
||||
target_guest() {
|
||||
ssh -p "$target_ssh_port" -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR \
|
||||
-o ConnectTimeout=5 \
|
||||
root@127.0.0.1 "$@"
|
||||
}
|
||||
|
||||
for attempt in $(jot 120 1 120); do
|
||||
if target_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
target_run_current_system_guest=$(target_guest 'readlink /run/current-system')
|
||||
target_shepherd_status=$(target_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
target_sshd_status=$(target_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
target_install_metadata_guest=$(target_guest 'cat /var/lib/fruix/install.scm')
|
||||
target_activate_log=$(target_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||
|
||||
[ "$target_run_current_system_guest" = "/frx/store/$target_closure_base" ] || { echo "unexpected booted target current-system: $target_run_current_system_guest" >&2; exit 1; }
|
||||
[ "$target_shepherd_status" = running ] || { echo "fruix-shepherd is not running in booted target" >&2; exit 1; }
|
||||
[ "$target_sshd_status" = running ] || { echo "sshd is not running in booted target" >&2; exit 1; }
|
||||
case "$target_install_metadata_guest" in
|
||||
*"$target_closure_path"*) : ;;
|
||||
*) echo "booted target metadata does not record target closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_install_metadata_guest" in
|
||||
*"$materialized_source_stores"*) : ;;
|
||||
*) echo "booted target metadata does not record materialized source store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "booted target activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
target_os_file=$target_os_file
|
||||
installer_image_store_path=$image_store_path
|
||||
installer_disk_image=$installer_disk_image
|
||||
installer_boot_image=$installer_boot_image
|
||||
installer_disk_capacity=$installer_disk_capacity
|
||||
installer_root_size=$installer_root_size
|
||||
target_image=$target_image
|
||||
target_disk_capacity=$target_disk_capacity
|
||||
install_target_device=$install_target_device
|
||||
qemu_smp=$qemu_smp
|
||||
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
|
||||
installer_closure_path=$installer_closure_path
|
||||
target_closure_path=$target_closure_path
|
||||
native_base_store_count=$native_base_store_count
|
||||
native_base_stores=$native_base_stores
|
||||
store_item_count=$store_item_count
|
||||
target_store_item_count=$target_store_item_count
|
||||
installer_store_item_count=$installer_store_item_count
|
||||
installer_state_path=$installer_state_path
|
||||
installer_log_path=$installer_log_path
|
||||
installer_state=$installer_state
|
||||
installer_run_current_system=$installer_run_current_system
|
||||
installer_sshd_status=$installer_sshd_status
|
||||
installer_serial_log=$installer_serial_log
|
||||
target_esp_fstype=$esp_fstype
|
||||
target_root_fstype=$root_fstype
|
||||
gpart_log=$gpart_log
|
||||
target_run_current_system=$target_run_current_system_guest
|
||||
target_shepherd_status=$target_shepherd_status
|
||||
target_sshd_status=$target_sshd_status
|
||||
target_serial_log=$target_serial_log
|
||||
installer_environment_boot=ok
|
||||
installer_environment_install=ok
|
||||
installed_target_boot=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase18-installer-environment\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"
|
||||
455
tests/system/run-phase18-installer-iso-xcpng.sh
Executable file
455
tests/system/run-phase18-installer-iso-xcpng.sh
Executable file
@@ -0,0 +1,455 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
fruix_cmd=$repo_root/bin/fruix
|
||||
vm_id=${VM_ID:-90490f2e-e8fc-4b7a-388e-5c26f0157289}
|
||||
iso_sr_id=${ISO_SR_ID:-537a6219-8452-7cb5-8d56-5eed6910c7a2}
|
||||
os_template=${OS_TEMPLATE:-$script_dir/phase18-installer-target-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase18-target-operating-system}
|
||||
store_dir=${STORE_DIR:-/frx/store}
|
||||
install_target_device=${INSTALL_TARGET_DEVICE:-/dev/ada0}
|
||||
installer_root_size=${INSTALLER_ROOT_SIZE:-}
|
||||
base_name=${BASE_NAME:-phase18-installer-iso-target}
|
||||
base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-installer-iso-target}
|
||||
base_release=${BASE_RELEASE:-15.0-STABLE}
|
||||
base_branch=${BASE_BRANCH:-stable/15}
|
||||
source_name=${SOURCE_NAME:-stable15-installer-iso-target-source}
|
||||
source_ref=${SOURCE_REF:-stable/15}
|
||||
source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89}
|
||||
declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-installer-iso-target}
|
||||
metadata_target=${METADATA_OUT:-}
|
||||
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
|
||||
root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}
|
||||
iso_http_port=${ISO_HTTP_PORT:-$(jot -r 1 18080 18999)}
|
||||
keep_imported_iso=${KEEP_IMPORTED_ISO:-0}
|
||||
|
||||
[ -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 node >/dev/null 2>&1 || {
|
||||
echo "node is required to serve the ISO for XO import" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v xo-cli >/dev/null 2>&1 || {
|
||||
echo "xo-cli is required" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "jq is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup=0
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase18-installer-iso-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
target_os_file=$workdir/phase18-installer-iso-target-operating-system.scm
|
||||
installer_out=$workdir/installer-iso.txt
|
||||
metadata_file=$workdir/phase18-installer-iso-xcpng-metadata.txt
|
||||
vm_info_json=$workdir/vm-info.json
|
||||
vdi_info_json=$workdir/vdi-info.json
|
||||
installer_log_file=$workdir/installer.log
|
||||
installer_gpart_file=$workdir/installer-gpart.txt
|
||||
installer_kern_disks_file=$workdir/installer-kern-disks.txt
|
||||
target_install_metadata_file=$workdir/target-install.scm
|
||||
server_script=$workdir/serve-iso.mjs
|
||||
server_log=$workdir/serve-iso.log
|
||||
arp_scan_log=$workdir/arp-scan.log
|
||||
|
||||
server_pid=
|
||||
imported_iso_id=
|
||||
imported_iso_name=
|
||||
guest_ip=
|
||||
vm_mac=
|
||||
|
||||
cleanup_workdir() {
|
||||
if [ -n "$server_pid" ]; then
|
||||
kill "$server_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
xo-cli vm.ejectCd id=$vm_id >/dev/null 2>&1 || true
|
||||
if [ -n "$imported_iso_id" ] && [ "$keep_imported_iso" -ne 1 ]; then
|
||||
xo-cli vdi.delete id=$imported_iso_id >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_workdir EXIT INT TERM
|
||||
|
||||
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
|
||||
sed \
|
||||
-e "s|__BASE_NAME__|$base_name|g" \
|
||||
-e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \
|
||||
-e "s|__BASE_RELEASE__|$base_release|g" \
|
||||
-e "s|__BASE_BRANCH__|$base_branch|g" \
|
||||
-e "s|__SOURCE_NAME__|$source_name|g" \
|
||||
-e "s|__SOURCE_REF__|$source_ref|g" \
|
||||
-e "s|__SOURCE_COMMIT__|$source_commit|g" \
|
||||
-e "s|__DECLARED_SOURCE_ROOT__|$declared_source_root|g" \
|
||||
-e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \
|
||||
"$os_template" > "$target_os_file"
|
||||
|
||||
action_env() {
|
||||
sudo env \
|
||||
HOME="$HOME" \
|
||||
GUILE_AUTO_COMPILE=0 \
|
||||
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
|
||||
GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \
|
||||
GUILE_BIN="${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}" \
|
||||
GUILE_EXTRA_PREFIX="${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}" \
|
||||
SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
if [ -n "$installer_root_size" ]; then
|
||||
action_env "$fruix_cmd" system installer-iso "$target_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" \
|
||||
--install-target-device "$install_target_device" \
|
||||
--root-size "$installer_root_size" >"$installer_out"
|
||||
else
|
||||
action_env "$fruix_cmd" system installer-iso "$target_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" \
|
||||
--install-target-device "$install_target_device" >"$installer_out"
|
||||
fi
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$installer_out" | tail -n 1
|
||||
}
|
||||
|
||||
iso_store_path=$(field iso_store_path)
|
||||
installer_iso_image=$(field iso_image)
|
||||
installer_boot_efi_image=$(field boot_efi_image)
|
||||
installer_root_image=$(field root_image)
|
||||
installer_closure_path=$(field installer_closure_path)
|
||||
target_closure_path=$(field target_closure_path)
|
||||
installer_host_name=$(field installer_host_name)
|
||||
install_target_device_out=$(field install_target_device)
|
||||
installer_state_path=$(field installer_state_path)
|
||||
installer_log_path=$(field installer_log_path)
|
||||
iso_volume_label=$(field iso_volume_label)
|
||||
root_size_out=$(field root_size)
|
||||
freebsd_source_kind_out=$(field freebsd_source_kind)
|
||||
freebsd_source_ref_out=$(field freebsd_source_ref)
|
||||
freebsd_source_commit_out=$(field freebsd_source_commit)
|
||||
freebsd_source_file=$(field freebsd_source_file)
|
||||
freebsd_source_materializations_file=$(field freebsd_source_materializations_file)
|
||||
materialized_source_store_count=$(field materialized_source_store_count)
|
||||
materialized_source_stores=$(field materialized_source_stores)
|
||||
host_base_store_count=$(field host_base_store_count)
|
||||
native_base_store_count=$(field native_base_store_count)
|
||||
native_base_stores=$(field native_base_stores)
|
||||
store_item_count=$(field store_item_count)
|
||||
target_store_item_count=$(field target_store_item_count)
|
||||
installer_store_item_count=$(field installer_store_item_count)
|
||||
store_layout_file=$(field store_layout_file)
|
||||
|
||||
[ -d "$iso_store_path" ] || { echo "missing installer ISO store path: $iso_store_path" >&2; exit 1; }
|
||||
[ -f "$installer_iso_image" ] || { echo "missing installer ISO image: $installer_iso_image" >&2; exit 1; }
|
||||
[ -f "$installer_boot_efi_image" ] || { echo "missing installer EFI boot image: $installer_boot_efi_image" >&2; exit 1; }
|
||||
[ -f "$installer_root_image" ] || { echo "missing installer root image: $installer_root_image" >&2; exit 1; }
|
||||
[ -n "$installer_closure_path" ] || { echo "missing installer closure path" >&2; exit 1; }
|
||||
[ -n "$target_closure_path" ] || { echo "missing target closure path" >&2; exit 1; }
|
||||
[ "$install_target_device_out" = "$install_target_device" ] || { echo "unexpected install target device: $install_target_device_out" >&2; exit 1; }
|
||||
[ "$installer_host_name" = fruix-freebsd-installer ] || { echo "unexpected installer host name: $installer_host_name" >&2; exit 1; }
|
||||
[ -n "$iso_volume_label" ] || { echo "missing ISO volume label" >&2; exit 1; }
|
||||
[ "$freebsd_source_kind_out" = git ] || { echo "unexpected source kind: $freebsd_source_kind_out" >&2; exit 1; }
|
||||
[ "$freebsd_source_ref_out" = "$source_ref" ] || { echo "unexpected source ref: $freebsd_source_ref_out" >&2; exit 1; }
|
||||
[ "$freebsd_source_commit_out" = "$source_commit" ] || { echo "unexpected source commit: $freebsd_source_commit_out" >&2; exit 1; }
|
||||
[ "$materialized_source_store_count" = 1 ] || { echo "unexpected materialized source store count: $materialized_source_store_count" >&2; exit 1; }
|
||||
[ "$host_base_store_count" = 0 ] || { echo "expected zero host base stores, got: $host_base_store_count" >&2; exit 1; }
|
||||
[ "$native_base_store_count" = 3 ] || { echo "expected three native base stores, got: $native_base_store_count" >&2; exit 1; }
|
||||
[ -f "$freebsd_source_file" ] || { echo "missing freebsd source file: $freebsd_source_file" >&2; exit 1; }
|
||||
[ -f "$freebsd_source_materializations_file" ] || { echo "missing source materializations file: $freebsd_source_materializations_file" >&2; exit 1; }
|
||||
[ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; }
|
||||
case "$materialized_source_stores" in
|
||||
/frx/store/*-freebsd-source-$source_name) : ;;
|
||||
*) echo "unexpected materialized source store path: $materialized_source_stores" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "$store_item_count" -ge "$target_store_item_count" ] || { echo "combined store item count smaller than target store item count" >&2; exit 1; }
|
||||
[ "$installer_store_item_count" -ge 1 ] || { echo "expected installer store items" >&2; exit 1; }
|
||||
|
||||
xo-cli list-objects id=$vm_id >"$vm_info_json"
|
||||
vdi_id=$(xo-cli list-objects type=VBD | jq -r '.[] | select(.VM=="'$vm_id'" and .is_cd_drive==false and .position=="0") | .VDI' | head -n 1)
|
||||
secondary_vdi_id=$(xo-cli list-objects type=VBD | jq -r '.[] | select(.VM=="'$vm_id'" and .is_cd_drive==false and .position=="1") | .VDI' | head -n 1)
|
||||
[ -n "$vdi_id" ] || { echo "failed to discover primary target VDI for VM $vm_id" >&2; exit 1; }
|
||||
xo-cli list-objects type=VDI | jq '[.[] | select(.id=="'$vdi_id'")]' >"$vdi_info_json"
|
||||
vdi_size=$(jq -r '.[0].size' "$vdi_info_json")
|
||||
[ -n "$vdi_size" ] || { echo "failed to discover VDI size for $vdi_id" >&2; exit 1; }
|
||||
|
||||
vif_id=$(jq -r '.[0].VIFs[0]' "$vm_info_json")
|
||||
if [ -n "$vif_id" ] && [ "$vif_id" != null ]; then
|
||||
vm_mac=$(xo-cli list-objects type=VIF | jq -r '.[] | select(.id=="'$vif_id'") | .MAC' | tr 'A-Z' 'a-z')
|
||||
else
|
||||
vm_mac=
|
||||
fi
|
||||
[ -n "$vm_mac" ] || { echo "failed to discover VM MAC address" >&2; exit 1; }
|
||||
|
||||
host_interface=$(route -n get default | awk '/interface:/{print $2; exit}')
|
||||
host_ip=$(ifconfig "$host_interface" | awk '/inet /{print $2; exit}')
|
||||
subnet_prefix=${host_ip%.*}
|
||||
|
||||
cat >"$server_script" <<'EOF'
|
||||
import { createServer } from 'node:http'
|
||||
import { createReadStream, statSync } from 'node:fs'
|
||||
const file = process.argv[2]
|
||||
const port = Number(process.argv[3])
|
||||
const size = statSync(file).size
|
||||
createServer((req, res) => {
|
||||
if (req.url !== '/installer.iso') {
|
||||
res.statusCode = 404
|
||||
res.end('not found\n')
|
||||
return
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': size,
|
||||
'Content-Disposition': 'attachment; filename="installer.iso"'
|
||||
})
|
||||
createReadStream(file).pipe(res)
|
||||
}).listen(port, '0.0.0.0', () => {
|
||||
console.log(`listening ${port}`)
|
||||
})
|
||||
EOF
|
||||
|
||||
node "$server_script" "$installer_iso_image" "$iso_http_port" >"$server_log" 2>&1 &
|
||||
server_pid=$!
|
||||
for _ in $(jot 50 1 50); do
|
||||
if grep -q 'listening' "$server_log"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if ! kill -0 "$server_pid" >/dev/null 2>&1; then
|
||||
echo "temporary ISO HTTP server failed to start" >&2
|
||||
cat "$server_log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
iso_import_url="http://$host_ip:$iso_http_port/installer.iso"
|
||||
imported_iso_name="fruix-installer-iso-ada0-$(date +%s).iso"
|
||||
imported_iso_id=$(xo-cli disk.import name="$imported_iso_name" sr="$iso_sr_id" type=iso url="$iso_import_url")
|
||||
kill "$server_pid" >/dev/null 2>&1 || true
|
||||
server_pid=
|
||||
|
||||
refresh_guest_ip() {
|
||||
guest_ip=$(arp -an | awk -v mac="$vm_mac" 'tolower($4)==mac {gsub(/[()]/,"",$2); print $2; exit}')
|
||||
}
|
||||
|
||||
ping_sweep() {
|
||||
: >"$arp_scan_log"
|
||||
for host in $(jot 254 1 254); do
|
||||
ip=$subnet_prefix.$host
|
||||
(
|
||||
ping -c 1 -W 1000 "$ip" >/dev/null 2>&1 && echo "$ip" >>"$arp_scan_log"
|
||||
) &
|
||||
done
|
||||
wait
|
||||
}
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
wait_for_guest_command() {
|
||||
probe=$1
|
||||
attempts=$2
|
||||
delay=$3
|
||||
guest_ip=
|
||||
for attempt in $(jot "$attempts" 1 "$attempts"); do
|
||||
refresh_guest_ip
|
||||
if [ -z "$guest_ip" ]; then
|
||||
ping_sweep
|
||||
refresh_guest_ip
|
||||
fi
|
||||
if [ -n "$guest_ip" ]; then
|
||||
if ssh_guest "$probe" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep "$delay"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
xo-cli vm.stop id=$vm_id force=true >/dev/null 2>&1 || true
|
||||
xo-cli vm.insertCd id=$vm_id cd_id=$imported_iso_id force=true >"$workdir/insert-cd.out"
|
||||
xo-cli vm.setBootOrder vm=$vm_id order=dcn >"$workdir/set-boot-order.out"
|
||||
xo-cli vm.start id=$vm_id >"$workdir/vm-start-installer.out"
|
||||
|
||||
wait_for_guest_command 'test -e /var/lib/fruix/installer/state' 90 5 || {
|
||||
echo "installer ISO guest never became reachable over SSH" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
installer_state=missing
|
||||
for attempt in $(jot 180 1 180); do
|
||||
if ssh_guest 'test -e /var/lib/fruix/installer/state' >/dev/null 2>&1; then
|
||||
installer_state=$(ssh_guest "cat '$installer_state_path' 2>/dev/null || echo missing")
|
||||
[ "$installer_state" = done ] && break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
installer_run_current_system=$(ssh_guest 'readlink /run/current-system')
|
||||
installer_sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
installer_log=$(ssh_guest "cat '$installer_log_path' 2>/dev/null || true")
|
||||
installer_target_device=$(ssh_guest 'cat /var/lib/fruix/installer/target-device')
|
||||
installer_kern_disks=$(ssh_guest 'sysctl -n kern.disks')
|
||||
installer_gpart=$(ssh_guest 'gpart show ada0')
|
||||
installer_esp_fstype=$(ssh_guest 'fstyp /dev/ada0p1')
|
||||
installer_root_fstype=$(ssh_guest 'fstyp /dev/ada0p2')
|
||||
printf '%s\n' "$installer_log" >"$installer_log_file"
|
||||
printf '%s\n' "$installer_kern_disks" >"$installer_kern_disks_file"
|
||||
printf '%s\n' "$installer_gpart" >"$installer_gpart_file"
|
||||
|
||||
[ "$installer_state" = done ] || { echo "installer ISO environment did not finish installation: $installer_state" >&2; exit 1; }
|
||||
[ "$installer_run_current_system" = "/frx/store/$(basename "$installer_closure_path")" ] || { echo "unexpected installer current-system target: $installer_run_current_system" >&2; exit 1; }
|
||||
[ "$installer_sshd_status" = running ] || { echo "installer sshd is not running" >&2; exit 1; }
|
||||
[ "$installer_target_device" = "$install_target_device" ] || { echo "unexpected installer target device in guest: $installer_target_device" >&2; exit 1; }
|
||||
[ "$installer_esp_fstype" = msdosfs ] || { echo "unexpected target ESP filesystem: $installer_esp_fstype" >&2; exit 1; }
|
||||
[ "$installer_root_fstype" = ufs ] || { echo "unexpected target root filesystem: $installer_root_fstype" >&2; exit 1; }
|
||||
case "$installer_log" in
|
||||
*fruix-installer:done*) : ;;
|
||||
*) echo "installer log does not show completion" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$installer_kern_disks" in
|
||||
*cd0*ada0*) : ;;
|
||||
*) echo "unexpected installer kern.disks output: $installer_kern_disks" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$installer_gpart" in
|
||||
*ada0*GPT*) : ;;
|
||||
*) echo "unexpected gpart output for ada0" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
xo-cli vm.ejectCd id=$vm_id >"$workdir/eject-cd.out"
|
||||
xo-cli vm.restart id=$vm_id force=true >"$workdir/vm-restart-target.out"
|
||||
|
||||
wait_for_guest_command 'test -f /var/lib/fruix/ready' 120 5 || {
|
||||
echo "installed target never became reachable over SSH" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
target_run_current_system=$(ssh_guest 'readlink /run/current-system')
|
||||
target_shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
target_sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
target_activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||
target_install_metadata=$(ssh_guest 'cat /var/lib/fruix/install.scm')
|
||||
printf '%s\n' "$target_install_metadata" >"$target_install_metadata_file"
|
||||
|
||||
[ "$target_run_current_system" = "/frx/store/$(basename "$target_closure_path")" ] || { echo "unexpected booted target current-system: $target_run_current_system" >&2; exit 1; }
|
||||
[ "$target_shepherd_status" = running ] || { echo "fruix-shepherd is not running in booted target" >&2; exit 1; }
|
||||
[ "$target_sshd_status" = running ] || { echo "sshd is not running in booted target" >&2; exit 1; }
|
||||
case "$target_install_metadata" in
|
||||
*"$target_closure_path"*) : ;;
|
||||
*) echo "booted target metadata does not record target closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_install_metadata" in
|
||||
*"$materialized_source_stores"*) : ;;
|
||||
*) echo "booted target metadata does not record materialized source store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_install_metadata" in
|
||||
*"$install_target_device"*) : ;;
|
||||
*) echo "booted target metadata does not record install target device" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "booted target activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
vm_id=$vm_id
|
||||
primary_vdi_id=$vdi_id
|
||||
secondary_vdi_id=$secondary_vdi_id
|
||||
vdi_size=$vdi_size
|
||||
iso_sr_id=$iso_sr_id
|
||||
imported_iso_id=$imported_iso_id
|
||||
imported_iso_name=$imported_iso_name
|
||||
guest_ip=$guest_ip
|
||||
target_os_file=$target_os_file
|
||||
installer_iso_store_path=$iso_store_path
|
||||
installer_iso_image=$installer_iso_image
|
||||
installer_boot_efi_image=$installer_boot_efi_image
|
||||
installer_root_image=$installer_root_image
|
||||
install_target_device=$install_target_device
|
||||
installer_root_size=$root_size_out
|
||||
iso_volume_label=$iso_volume_label
|
||||
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
|
||||
installer_closure_path=$installer_closure_path
|
||||
target_closure_path=$target_closure_path
|
||||
native_base_store_count=$native_base_store_count
|
||||
native_base_stores=$native_base_stores
|
||||
store_item_count=$store_item_count
|
||||
target_store_item_count=$target_store_item_count
|
||||
installer_store_item_count=$installer_store_item_count
|
||||
installer_state_path=$installer_state_path
|
||||
installer_log_path=$installer_log_path
|
||||
installer_state=$installer_state
|
||||
installer_run_current_system=$installer_run_current_system
|
||||
installer_sshd_status=$installer_sshd_status
|
||||
installer_target_device=$installer_target_device
|
||||
installer_kern_disks=$installer_kern_disks
|
||||
installer_log_file=$installer_log_file
|
||||
installer_gpart_file=$installer_gpart_file
|
||||
installer_kern_disks_file=$installer_kern_disks_file
|
||||
target_esp_fstype=$installer_esp_fstype
|
||||
target_root_fstype=$installer_root_fstype
|
||||
target_run_current_system=$target_run_current_system
|
||||
target_shepherd_status=$target_shepherd_status
|
||||
target_sshd_status=$target_sshd_status
|
||||
target_install_metadata_file=$target_install_metadata_file
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
installer_iso_boot=ok
|
||||
installer_iso_install=ok
|
||||
installed_target_boot=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase18-installer-iso-xcpng\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"
|
||||
412
tests/system/run-phase18-installer-iso.sh
Executable file
412
tests/system/run-phase18-installer-iso.sh
Executable file
@@ -0,0 +1,412 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
project_root=${PROJECT_ROOT:-$(pwd)}
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
fruix_cmd=$project_root/bin/fruix
|
||||
os_template=${OS_TEMPLATE:-$script_dir/phase18-installer-target-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase18-target-operating-system}
|
||||
store_dir=${STORE_DIR:-/frx/store}
|
||||
installer_root_size=${INSTALLER_ROOT_SIZE:-}
|
||||
target_disk_capacity=${TARGET_DISK_CAPACITY:-12g}
|
||||
install_target_device=${INSTALL_TARGET_DEVICE:-/dev/vtbd0}
|
||||
qemu_smp=${QEMU_SMP:-2}
|
||||
installer_memory=${INSTALLER_MEMORY:-6144}
|
||||
installer_ssh_port=${INSTALLER_SSH_PORT:-10027}
|
||||
target_ssh_port=${TARGET_SSH_PORT:-10028}
|
||||
base_name=${BASE_NAME:-phase18-installer-iso-target}
|
||||
base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-installer-iso-target}
|
||||
base_release=${BASE_RELEASE:-15.0-STABLE}
|
||||
base_branch=${BASE_BRANCH:-stable/15}
|
||||
source_name=${SOURCE_NAME:-stable15-installer-iso-target-source}
|
||||
source_ref=${SOURCE_REF:-stable/15}
|
||||
source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89}
|
||||
declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-installer-iso-target}
|
||||
metadata_target=${METADATA_OUT:-}
|
||||
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
|
||||
root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}
|
||||
|
||||
[ -x "$fruix_cmd" ] || {
|
||||
echo "fruix command is not executable: $fruix_cmd" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$os_template" ] || {
|
||||
echo "missing operating-system template: $os_template" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$root_authorized_key_file" ] || {
|
||||
echo "missing root authorized key file: $root_authorized_key_file" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$root_ssh_private_key_file" ] || {
|
||||
echo "missing root SSH private key file: $root_ssh_private_key_file" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v qemu-system-x86_64 >/dev/null 2>&1 || {
|
||||
echo "qemu-system-x86_64 is required" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f /usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd ] || {
|
||||
echo "missing QEMU UEFI firmware" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup=0
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase18-installer-iso.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
target_os_file=$workdir/phase18-installer-iso-target-operating-system.scm
|
||||
installer_out=$workdir/installer-iso.txt
|
||||
metadata_file=$workdir/phase18-installer-iso-metadata.txt
|
||||
installer_serial_log=$workdir/installer-iso-serial.log
|
||||
target_serial_log=$workdir/target-serial.log
|
||||
installer_qemu_pidfile=$workdir/installer-qemu.pid
|
||||
target_qemu_pidfile=$workdir/target-qemu.pid
|
||||
installer_uefi_vars=$workdir/installer-vars.fd
|
||||
target_uefi_vars=$workdir/target-vars.fd
|
||||
installer_boot_iso=$workdir/installer-boot.iso
|
||||
target_image=$workdir/installed-target.img
|
||||
gpart_log=$workdir/gpart-show.txt
|
||||
mnt_esp=$workdir/mnt-esp
|
||||
mnt_root=$workdir/mnt-root
|
||||
md_unit=
|
||||
|
||||
cleanup_workdir() {
|
||||
if [ -f "$installer_qemu_pidfile" ]; then
|
||||
sudo kill "$(sudo cat "$installer_qemu_pidfile")" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -f "$target_qemu_pidfile" ]; then
|
||||
sudo kill "$(sudo cat "$target_qemu_pidfile")" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$md_unit" ]; then
|
||||
sudo umount "$mnt_esp" >/dev/null 2>&1 || true
|
||||
sudo umount "$mnt_root" >/dev/null 2>&1 || true
|
||||
sudo mdconfig -d -u "$md_unit" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_workdir EXIT INT TERM
|
||||
|
||||
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
|
||||
sed \
|
||||
-e "s|__BASE_NAME__|$base_name|g" \
|
||||
-e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \
|
||||
-e "s|__BASE_RELEASE__|$base_release|g" \
|
||||
-e "s|__BASE_BRANCH__|$base_branch|g" \
|
||||
-e "s|__SOURCE_NAME__|$source_name|g" \
|
||||
-e "s|__SOURCE_REF__|$source_ref|g" \
|
||||
-e "s|__SOURCE_COMMIT__|$source_commit|g" \
|
||||
-e "s|__DECLARED_SOURCE_ROOT__|$declared_source_root|g" \
|
||||
-e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \
|
||||
"$os_template" > "$target_os_file"
|
||||
|
||||
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$installer_uefi_vars"
|
||||
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$target_uefi_vars"
|
||||
truncate -s "$target_disk_capacity" "$target_image"
|
||||
mkdir -p "$mnt_esp" "$mnt_root"
|
||||
|
||||
action_env() {
|
||||
sudo env \
|
||||
HOME="$HOME" \
|
||||
GUILE_AUTO_COMPILE=0 \
|
||||
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
|
||||
GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \
|
||||
GUILE_BIN="${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}" \
|
||||
GUILE_EXTRA_PREFIX="${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}" \
|
||||
SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
if [ -n "$installer_root_size" ]; then
|
||||
action_env "$fruix_cmd" system installer-iso "$target_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" \
|
||||
--install-target-device "$install_target_device" \
|
||||
--root-size "$installer_root_size" >"$installer_out"
|
||||
else
|
||||
action_env "$fruix_cmd" system installer-iso "$target_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" \
|
||||
--install-target-device "$install_target_device" >"$installer_out"
|
||||
fi
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$installer_out" | tail -n 1
|
||||
}
|
||||
|
||||
iso_store_path=$(field iso_store_path)
|
||||
installer_iso_image=$(field iso_image)
|
||||
installer_boot_efi_image=$(field boot_efi_image)
|
||||
installer_root_image=$(field root_image)
|
||||
installer_closure_path=$(field installer_closure_path)
|
||||
target_closure_path=$(field target_closure_path)
|
||||
installer_host_name=$(field installer_host_name)
|
||||
install_target_device_out=$(field install_target_device)
|
||||
installer_state_path=$(field installer_state_path)
|
||||
installer_log_path=$(field installer_log_path)
|
||||
iso_volume_label=$(field iso_volume_label)
|
||||
root_size_out=$(field root_size)
|
||||
freebsd_source_kind_out=$(field freebsd_source_kind)
|
||||
freebsd_source_ref_out=$(field freebsd_source_ref)
|
||||
freebsd_source_commit_out=$(field freebsd_source_commit)
|
||||
freebsd_source_file=$(field freebsd_source_file)
|
||||
freebsd_source_materializations_file=$(field freebsd_source_materializations_file)
|
||||
materialized_source_store_count=$(field materialized_source_store_count)
|
||||
materialized_source_stores=$(field materialized_source_stores)
|
||||
host_base_store_count=$(field host_base_store_count)
|
||||
native_base_store_count=$(field native_base_store_count)
|
||||
native_base_stores=$(field native_base_stores)
|
||||
store_item_count=$(field store_item_count)
|
||||
target_store_item_count=$(field target_store_item_count)
|
||||
installer_store_item_count=$(field installer_store_item_count)
|
||||
store_layout_file=$(field store_layout_file)
|
||||
|
||||
[ -d "$iso_store_path" ] || { echo "missing installer ISO store path: $iso_store_path" >&2; exit 1; }
|
||||
[ -f "$installer_iso_image" ] || { echo "missing installer ISO image: $installer_iso_image" >&2; exit 1; }
|
||||
[ -f "$installer_boot_efi_image" ] || { echo "missing installer EFI boot image: $installer_boot_efi_image" >&2; exit 1; }
|
||||
[ -f "$installer_root_image" ] || { echo "missing installer root image: $installer_root_image" >&2; exit 1; }
|
||||
[ -n "$installer_closure_path" ] || { echo "missing installer closure path" >&2; exit 1; }
|
||||
[ -n "$target_closure_path" ] || { echo "missing target closure path" >&2; exit 1; }
|
||||
[ "$install_target_device_out" = "$install_target_device" ] || { echo "unexpected install target device: $install_target_device_out" >&2; exit 1; }
|
||||
[ "$installer_host_name" = fruix-freebsd-installer ] || { echo "unexpected installer host name: $installer_host_name" >&2; exit 1; }
|
||||
[ -n "$iso_volume_label" ] || { echo "missing ISO volume label" >&2; exit 1; }
|
||||
[ "$freebsd_source_kind_out" = git ] || { echo "unexpected source kind: $freebsd_source_kind_out" >&2; exit 1; }
|
||||
[ "$freebsd_source_ref_out" = "$source_ref" ] || { echo "unexpected source ref: $freebsd_source_ref_out" >&2; exit 1; }
|
||||
[ "$freebsd_source_commit_out" = "$source_commit" ] || { echo "unexpected source commit: $freebsd_source_commit_out" >&2; exit 1; }
|
||||
[ "$materialized_source_store_count" = 1 ] || { echo "unexpected materialized source store count: $materialized_source_store_count" >&2; exit 1; }
|
||||
[ "$host_base_store_count" = 0 ] || { echo "expected zero host base stores, got: $host_base_store_count" >&2; exit 1; }
|
||||
[ "$native_base_store_count" = 3 ] || { echo "expected three native base stores, got: $native_base_store_count" >&2; exit 1; }
|
||||
[ -f "$freebsd_source_file" ] || { echo "missing freebsd source file: $freebsd_source_file" >&2; exit 1; }
|
||||
[ -f "$freebsd_source_materializations_file" ] || { echo "missing source materializations file: $freebsd_source_materializations_file" >&2; exit 1; }
|
||||
[ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; }
|
||||
case "$materialized_source_stores" in
|
||||
/frx/store/*-freebsd-source-$source_name) : ;;
|
||||
*) echo "unexpected materialized source store path: $materialized_source_stores" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "$store_item_count" -ge "$target_store_item_count" ] || { echo "combined store item count smaller than target store item count" >&2; exit 1; }
|
||||
[ "$installer_store_item_count" -ge 1 ] || { echo "expected installer store items" >&2; exit 1; }
|
||||
|
||||
cp "$installer_iso_image" "$installer_boot_iso"
|
||||
|
||||
target_closure_base=$(basename "$target_closure_path")
|
||||
installer_closure_base=$(basename "$installer_closure_path")
|
||||
|
||||
sudo qemu-system-x86_64 \
|
||||
-machine q35,accel=tcg \
|
||||
-cpu max \
|
||||
-m "$installer_memory" \
|
||||
-smp "$qemu_smp" \
|
||||
-display none \
|
||||
-serial "file:$installer_serial_log" \
|
||||
-monitor none \
|
||||
-pidfile "$installer_qemu_pidfile" \
|
||||
-daemonize \
|
||||
-boot d \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \
|
||||
-drive if=pflash,format=raw,file="$installer_uefi_vars" \
|
||||
-cdrom "$installer_boot_iso" \
|
||||
-drive if=virtio,format=raw,file="$target_image" \
|
||||
-netdev user,id=net0,hostfwd=tcp::${installer_ssh_port}-:22 \
|
||||
-device virtio-net-pci,netdev=net0
|
||||
|
||||
installer_guest() {
|
||||
ssh -p "$installer_ssh_port" -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR \
|
||||
-o ConnectTimeout=5 \
|
||||
root@127.0.0.1 "$@"
|
||||
}
|
||||
|
||||
installer_ssh_reached=0
|
||||
installer_state=missing
|
||||
for attempt in $(jot 180 1 180); do
|
||||
if installer_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
|
||||
installer_ssh_reached=1
|
||||
installer_state=$(installer_guest "cat '$installer_state_path' 2>/dev/null || echo missing")
|
||||
[ "$installer_state" = done ] && break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
[ "$installer_ssh_reached" = 1 ] || { echo "installer ISO environment never became reachable over SSH" >&2; exit 1; }
|
||||
[ "$installer_state" = done ] || { echo "installer ISO environment did not finish installation: $installer_state" >&2; exit 1; }
|
||||
|
||||
installer_run_current_system=$(installer_guest 'readlink /run/current-system')
|
||||
installer_sshd_status=$(installer_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
installer_activate_log=$(installer_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||
installer_log=$(installer_guest "cat '$installer_log_path' 2>/dev/null || true" | tr '\n' ' ')
|
||||
|
||||
[ "$installer_run_current_system" = "/frx/store/$installer_closure_base" ] || { echo "unexpected installer current-system target: $installer_run_current_system" >&2; exit 1; }
|
||||
[ "$installer_sshd_status" = running ] || { echo "installer sshd is not running" >&2; exit 1; }
|
||||
case "$installer_activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "installer activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$installer_log" in
|
||||
*fruix-installer:done*) : ;;
|
||||
*) echo "installer log does not show completion" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
sudo kill "$(sudo cat "$installer_qemu_pidfile")" >/dev/null 2>&1 || true
|
||||
rm -f "$installer_qemu_pidfile"
|
||||
sleep 2
|
||||
|
||||
md=$(sudo mdconfig -a -t vnode -f "$target_image")
|
||||
md_unit=${md#md}
|
||||
sudo gpart show -lp "/dev/$md" >"$gpart_log"
|
||||
esp_fstype=$(sudo fstyp "/dev/${md}p1")
|
||||
root_fstype=$(sudo fstyp "/dev/${md}p2")
|
||||
[ "$esp_fstype" = msdosfs ] || { echo "unexpected target ESP filesystem: $esp_fstype" >&2; exit 1; }
|
||||
[ "$root_fstype" = ufs ] || { echo "unexpected target root filesystem: $root_fstype" >&2; exit 1; }
|
||||
|
||||
sudo mount -t msdosfs "/dev/${md}p1" "$mnt_esp"
|
||||
sudo mount -t ufs -o ro "/dev/${md}p2" "$mnt_root"
|
||||
|
||||
[ -f "$mnt_esp/EFI/BOOT/BOOTX64.EFI" ] || { echo "missing EFI boot file on installed target" >&2; exit 1; }
|
||||
target_run_current_system=$(readlink "$mnt_root/run/current-system")
|
||||
target_boot_loader=$(readlink "$mnt_root/boot/loader")
|
||||
install_metadata_host=$(cat "$mnt_root/var/lib/fruix/install.scm")
|
||||
[ "$target_run_current_system" = "/frx/store/$target_closure_base" ] || { echo "unexpected target /run/current-system target: $target_run_current_system" >&2; exit 1; }
|
||||
[ "$target_boot_loader" = /run/current-system/boot/loader ] || { echo "unexpected target boot loader link: $target_boot_loader" >&2; exit 1; }
|
||||
[ -d "$mnt_root/frx/store/$target_closure_base" ] || { echo "installed target closure missing from target root" >&2; exit 1; }
|
||||
case "$install_metadata_host" in
|
||||
*"$target_closure_path"*) : ;;
|
||||
*) echo "installed target metadata does not record target closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$install_metadata_host" in
|
||||
*"$materialized_source_stores"*) : ;;
|
||||
*) echo "installed target metadata does not record materialized source store" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
sudo umount "$mnt_esp"
|
||||
sudo umount "$mnt_root"
|
||||
sudo mdconfig -d -u "$md_unit"
|
||||
md_unit=
|
||||
|
||||
sudo qemu-system-x86_64 \
|
||||
-machine q35,accel=tcg \
|
||||
-cpu max \
|
||||
-m 2048 \
|
||||
-smp "$qemu_smp" \
|
||||
-display none \
|
||||
-serial "file:$target_serial_log" \
|
||||
-monitor none \
|
||||
-pidfile "$target_qemu_pidfile" \
|
||||
-daemonize \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \
|
||||
-drive if=pflash,format=raw,file="$target_uefi_vars" \
|
||||
-drive if=virtio,format=raw,file="$target_image" \
|
||||
-netdev user,id=net0,hostfwd=tcp::${target_ssh_port}-:22 \
|
||||
-device virtio-net-pci,netdev=net0
|
||||
|
||||
target_guest() {
|
||||
ssh -p "$target_ssh_port" -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o LogLevel=ERROR \
|
||||
-o ConnectTimeout=5 \
|
||||
root@127.0.0.1 "$@"
|
||||
}
|
||||
|
||||
for attempt in $(jot 120 1 120); do
|
||||
if target_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
target_run_current_system_guest=$(target_guest 'readlink /run/current-system')
|
||||
target_shepherd_status=$(target_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
target_sshd_status=$(target_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||
target_install_metadata_guest=$(target_guest 'cat /var/lib/fruix/install.scm')
|
||||
target_activate_log=$(target_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||
|
||||
[ "$target_run_current_system_guest" = "/frx/store/$target_closure_base" ] || { echo "unexpected booted target current-system: $target_run_current_system_guest" >&2; exit 1; }
|
||||
[ "$target_shepherd_status" = running ] || { echo "fruix-shepherd is not running in booted target" >&2; exit 1; }
|
||||
[ "$target_sshd_status" = running ] || { echo "sshd is not running in booted target" >&2; exit 1; }
|
||||
case "$target_install_metadata_guest" in
|
||||
*"$target_closure_path"*) : ;;
|
||||
*) echo "booted target metadata does not record target closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_install_metadata_guest" in
|
||||
*"$materialized_source_stores"*) : ;;
|
||||
*) echo "booted target metadata does not record materialized source store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$target_activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "booted target activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
target_os_file=$target_os_file
|
||||
installer_iso_store_path=$iso_store_path
|
||||
installer_iso_image=$installer_iso_image
|
||||
installer_boot_iso=$installer_boot_iso
|
||||
installer_boot_efi_image=$installer_boot_efi_image
|
||||
installer_root_image=$installer_root_image
|
||||
installer_memory=$installer_memory
|
||||
installer_root_size=$root_size_out
|
||||
target_image=$target_image
|
||||
target_disk_capacity=$target_disk_capacity
|
||||
install_target_device=$install_target_device
|
||||
qemu_smp=$qemu_smp
|
||||
iso_volume_label=$iso_volume_label
|
||||
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
|
||||
installer_closure_path=$installer_closure_path
|
||||
target_closure_path=$target_closure_path
|
||||
native_base_store_count=$native_base_store_count
|
||||
native_base_stores=$native_base_stores
|
||||
store_item_count=$store_item_count
|
||||
target_store_item_count=$target_store_item_count
|
||||
installer_store_item_count=$installer_store_item_count
|
||||
installer_state_path=$installer_state_path
|
||||
installer_log_path=$installer_log_path
|
||||
installer_state=$installer_state
|
||||
installer_run_current_system=$installer_run_current_system
|
||||
installer_sshd_status=$installer_sshd_status
|
||||
installer_serial_log=$installer_serial_log
|
||||
target_esp_fstype=$esp_fstype
|
||||
target_root_fstype=$root_fstype
|
||||
gpart_log=$gpart_log
|
||||
target_run_current_system=$target_run_current_system_guest
|
||||
target_shepherd_status=$target_shepherd_status
|
||||
target_sshd_status=$target_sshd_status
|
||||
target_serial_log=$target_serial_log
|
||||
installer_iso_boot=ok
|
||||
installer_iso_install=ok
|
||||
installed_target_boot=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase18-installer-iso\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"
|
||||
160
tests/system/run-phase19-generation-layout-qemu.sh
Executable file
160
tests/system/run-phase19-generation-layout-qemu.sh
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
metadata_target=${METADATA_OUT:-}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase19-generation-layout-qemu.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
inner_workdir=$workdir/phase18-install
|
||||
inner_metadata=$workdir/phase18-system-install-metadata.txt
|
||||
metadata_file=$workdir/phase19-generation-layout-qemu-metadata.txt
|
||||
mnt_esp=$workdir/mnt-esp
|
||||
mnt_root=$workdir/mnt-root
|
||||
md_unit=
|
||||
|
||||
cleanup_workdir() {
|
||||
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
|
||||
|
||||
mkdir -p "$mnt_esp" "$mnt_root"
|
||||
|
||||
KEEP_WORKDIR=1 WORKDIR="$inner_workdir" METADATA_OUT="$inner_metadata" \
|
||||
"$repo_root/tests/system/run-phase18-system-install.sh" >/dev/null
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$inner_metadata" | tail -n 1
|
||||
}
|
||||
|
||||
target_image=$(field target_image)
|
||||
closure_path=$(field closure_path)
|
||||
materialized_source_store=$(field materialized_source_store)
|
||||
install_metadata_path=$(field install_metadata_path)
|
||||
run_current_system_target_reported=$(field run_current_system_target)
|
||||
serial_log=$(field serial_log)
|
||||
closure_base=$(basename "$closure_path")
|
||||
|
||||
guest_md=$(sudo mdconfig -a -t vnode -f "$target_image")
|
||||
md_unit=${guest_md#md}
|
||||
sudo mount -t msdosfs "/dev/${guest_md}p1" "$mnt_esp"
|
||||
sudo mount -t ufs -o ro "/dev/${guest_md}p2" "$mnt_root"
|
||||
|
||||
system_root=$mnt_root/var/lib/fruix/system
|
||||
generation_root=$system_root/generations/1
|
||||
gcroots_root=$mnt_root/frx/var/fruix/gcroots
|
||||
|
||||
[ -d "$system_root" ] || { echo "missing explicit system generation root" >&2; exit 1; }
|
||||
[ -d "$generation_root" ] || { echo "missing generation 1 directory" >&2; exit 1; }
|
||||
[ -f "$system_root/current-generation" ] || { echo "missing current-generation file" >&2; exit 1; }
|
||||
[ -L "$system_root/current" ] || { echo "missing current generation link" >&2; exit 1; }
|
||||
[ -L "$generation_root/closure" ] || { echo "missing generation closure link" >&2; exit 1; }
|
||||
[ -f "$generation_root/metadata.scm" ] || { echo "missing generation metadata file" >&2; exit 1; }
|
||||
[ -f "$generation_root/provenance.scm" ] || { echo "missing generation provenance file" >&2; exit 1; }
|
||||
[ -f "$generation_root/install.scm" ] || { echo "missing generation install metadata file" >&2; exit 1; }
|
||||
[ -L "$gcroots_root/current-system" ] || { echo "missing current-system gc root" >&2; exit 1; }
|
||||
[ -L "$gcroots_root/system-1" ] || { echo "missing system-1 gc root" >&2; exit 1; }
|
||||
|
||||
current_generation=$(tr -d '\n' < "$system_root/current-generation")
|
||||
current_link=$(readlink "$system_root/current")
|
||||
generation_closure=$(readlink "$generation_root/closure")
|
||||
gcroot_current=$(readlink "$gcroots_root/current-system")
|
||||
gcroot_generation=$(readlink "$gcroots_root/system-1")
|
||||
run_current_system_target=$(readlink "$mnt_root/run/current-system")
|
||||
generation_metadata=$(cat "$generation_root/metadata.scm")
|
||||
generation_provenance=$(cat "$generation_root/provenance.scm")
|
||||
generation_install=$(cat "$generation_root/install.scm")
|
||||
install_metadata_host=$(cat "$mnt_root$install_metadata_path")
|
||||
|
||||
[ "$current_generation" = 1 ] || { echo "unexpected current generation: $current_generation" >&2; exit 1; }
|
||||
[ "$current_link" = generations/1 ] || { echo "unexpected current link target: $current_link" >&2; exit 1; }
|
||||
[ "$generation_closure" = "/frx/store/$closure_base" ] || { echo "unexpected generation closure target: $generation_closure" >&2; exit 1; }
|
||||
[ "$gcroot_current" = "/frx/store/$closure_base" ] || { echo "unexpected current-system gc root: $gcroot_current" >&2; exit 1; }
|
||||
[ "$gcroot_generation" = "/frx/store/$closure_base" ] || { echo "unexpected system-1 gc root: $gcroot_generation" >&2; exit 1; }
|
||||
[ "$run_current_system_target" = "/frx/store/$closure_base" ] || { echo "unexpected /run/current-system target: $run_current_system_target" >&2; exit 1; }
|
||||
[ "$run_current_system_target_reported" = "/frx/store/$closure_base" ] || { echo "unexpected reported guest current-system target: $run_current_system_target_reported" >&2; exit 1; }
|
||||
|
||||
case "$generation_metadata" in
|
||||
*"$closure_path"*) : ;;
|
||||
*) echo "generation metadata does not record closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$generation_metadata" in
|
||||
*"$install_metadata_path"*) : ;;
|
||||
*) echo "generation metadata does not record install metadata path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$generation_provenance" in
|
||||
*"$closure_path/metadata/store-layout.scm"*) : ;;
|
||||
*) echo "generation provenance does not reference store layout metadata" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$generation_install" in
|
||||
*"$closure_path"*) : ;;
|
||||
*) echo "generation install metadata does not record closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$generation_install" in
|
||||
*"$materialized_source_store"*) : ;;
|
||||
*) echo "generation install metadata does not record materialized source store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$install_metadata_host" in
|
||||
*"$closure_path"*) : ;;
|
||||
*) echo "installed target metadata does not record closure path" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
sudo umount "$mnt_esp"
|
||||
sudo umount "$mnt_root"
|
||||
sudo mdconfig -d -u "$md_unit"
|
||||
md_unit=
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_workdir=$inner_workdir
|
||||
inner_metadata=$inner_metadata
|
||||
target_image=$target_image
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
materialized_source_store=$materialized_source_store
|
||||
install_metadata_path=$install_metadata_path
|
||||
system_root=$system_root
|
||||
generation_root=$generation_root
|
||||
gcroots_root=$gcroots_root
|
||||
current_generation=$current_generation
|
||||
current_link=$current_link
|
||||
generation_closure=$generation_closure
|
||||
gcroot_current=$gcroot_current
|
||||
gcroot_generation=$gcroot_generation
|
||||
run_current_system_target=$run_current_system_target
|
||||
serial_log=$serial_log
|
||||
generation_layout=explicit
|
||||
boot_backend=qemu-uefi-tcg
|
||||
generation_layout_validation=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase19-generation-layout-qemu\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"
|
||||
388
tests/system/run-phase19-installed-system-rollback-qemu.sh
Executable file
388
tests/system/run-phase19-installed-system-rollback-qemu.sh
Executable file
@@ -0,0 +1,388 @@
|
||||
#!/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/phase19-generation-rollback-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase19-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:-phase19-generation-layout}
|
||||
base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-generation-layout}
|
||||
base_release=${BASE_RELEASE:-15.0-STABLE}
|
||||
base_branch=${BASE_BRANCH:-stable/15}
|
||||
source_name=${SOURCE_NAME:-stable15-generation-layout-source}
|
||||
source_ref=${SOURCE_REF:-stable/15}
|
||||
source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89}
|
||||
declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-generation-layout}
|
||||
current_host_name=${CURRENT_HOST_NAME:-fruix-freebsd-current}
|
||||
candidate_host_name=${CANDIDATE_HOST_NAME:-fruix-freebsd-canary}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
[ -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
|
||||
}
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase19-installed-system-rollback-qemu.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
current_os_file=$workdir/current-operating-system.scm
|
||||
candidate_os_file=$workdir/candidate-operating-system.scm
|
||||
current_install_out=$workdir/current-install.txt
|
||||
candidate_build_out=$workdir/candidate-build.txt
|
||||
target_image=$workdir/installed.img
|
||||
candidate_store_items=$workdir/candidate-store-items.txt
|
||||
stage_log=$workdir/stage-candidate-store.txt
|
||||
serial_log=$workdir/serial.log
|
||||
qemu_pidfile=$workdir/qemu.pid
|
||||
uefi_vars=$workdir/QEMU_UEFI_VARS.fd
|
||||
metadata_file=$workdir/phase19-installed-system-rollback-qemu-metadata.txt
|
||||
switch_status_file=$workdir/switch-status.txt
|
||||
rollback_status_file=$workdir/rollback-status.txt
|
||||
boot_current_status_file=$workdir/boot-current-status.txt
|
||||
boot_candidate_status_file=$workdir/boot-candidate-status.txt
|
||||
boot_rollback_status_file=$workdir/boot-rollback-status.txt
|
||||
generation2_metadata_file=$workdir/generation2-metadata.scm
|
||||
generation2_install_file=$workdir/generation2-install.scm
|
||||
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_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
|
||||
|
||||
render_os() {
|
||||
output=$1
|
||||
host_name=$2
|
||||
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|__HOST_NAME__|$host_name|g" \
|
||||
-e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \
|
||||
"$os_template" > "$output"
|
||||
}
|
||||
|
||||
render_os "$current_os_file" "$current_host_name"
|
||||
render_os "$candidate_os_file" "$candidate_host_name"
|
||||
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$uefi_vars"
|
||||
mkdir -p "$mnt_root"
|
||||
|
||||
action_env() {
|
||||
sudo env \
|
||||
HOME="$HOME" \
|
||||
GUILE_AUTO_COMPILE=0 \
|
||||
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
|
||||
GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \
|
||||
GUILE_BIN="${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}" \
|
||||
GUILE_EXTRA_PREFIX="${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}" \
|
||||
SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
field() {
|
||||
name=$1
|
||||
file=$2
|
||||
sed -n "s/^$name=//p" "$file" | tail -n 1
|
||||
}
|
||||
|
||||
status_field() {
|
||||
name=$1
|
||||
file=$2
|
||||
sed -n "s/^$name=//p" "$file" | tail -n 1
|
||||
}
|
||||
|
||||
wait_for_ssh() {
|
||||
for attempt in $(jot 120 1 120); do
|
||||
if ssh_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
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 "$@"
|
||||
}
|
||||
|
||||
reboot_guest() {
|
||||
ssh_guest 'shutdown -r now >/dev/null 2>&1 || reboot >/dev/null 2>&1 || true' >/dev/null 2>&1 || true
|
||||
sleep 5
|
||||
wait_for_ssh || {
|
||||
echo "guest did not return over SSH after reboot" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
capture_status() {
|
||||
output_file=$1
|
||||
ssh_guest '/usr/local/bin/fruix system status' > "$output_file"
|
||||
}
|
||||
|
||||
action_env "$fruix_cmd" system install "$current_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" \
|
||||
--target "$target_image" \
|
||||
--disk-capacity "$disk_capacity" \
|
||||
--root-size "$root_size" > "$current_install_out"
|
||||
|
||||
current_closure_path=$(field closure_path "$current_install_out")
|
||||
install_metadata_path=$(field install_metadata_path "$current_install_out")
|
||||
materialized_source_store=$(field materialized_source_stores "$current_install_out")
|
||||
[ -n "$current_closure_path" ] || { echo "missing current closure path" >&2; exit 1; }
|
||||
[ "$install_metadata_path" = /var/lib/fruix/install.scm ] || { echo "unexpected install metadata path: $install_metadata_path" >&2; exit 1; }
|
||||
|
||||
action_env "$fruix_cmd" system build "$candidate_os_file" \
|
||||
--system "$system_name" \
|
||||
--store "$store_dir" > "$candidate_build_out"
|
||||
|
||||
candidate_closure_path=$(field closure_path "$candidate_build_out")
|
||||
[ -n "$candidate_closure_path" ] || { echo "missing candidate closure path" >&2; exit 1; }
|
||||
[ "$candidate_closure_path" != "$current_closure_path" ] || { echo "candidate closure unexpectedly matches current closure" >&2; exit 1; }
|
||||
|
||||
{
|
||||
printf '%s\n' "$candidate_closure_path"
|
||||
cat "$candidate_closure_path/.references"
|
||||
} | awk 'NF { print }' | sort -u > "$candidate_store_items"
|
||||
|
||||
candidate_store_item_count=$(wc -l < "$candidate_store_items" | tr -d ' ')
|
||||
|
||||
md=$(sudo mdconfig -a -t vnode -f "$target_image")
|
||||
md_unit=${md#md}
|
||||
sudo mount -t ufs "/dev/${md}p2" "$mnt_root"
|
||||
sudo mkdir -p "$mnt_root/frx/store"
|
||||
: > "$stage_log"
|
||||
while IFS= read -r item; do
|
||||
[ -n "$item" ] || continue
|
||||
item_base=$(basename "$item")
|
||||
if [ -e "$mnt_root/frx/store/$item_base" ]; then
|
||||
printf 'already-present=%s\n' "$item_base" >> "$stage_log"
|
||||
continue
|
||||
fi
|
||||
printf 'copy=%s\n' "$item_base" >> "$stage_log"
|
||||
sudo sh -c "cd '$store_dir' && pax -rw -pe '$item_base' '$mnt_root/frx/store'"
|
||||
done < "$candidate_store_items"
|
||||
sudo sync
|
||||
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
|
||||
|
||||
wait_for_ssh || {
|
||||
echo "guest never became reachable over SSH" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
capture_status "$boot_current_status_file"
|
||||
boot_current_closure=$(ssh_guest 'readlink /run/current-system')
|
||||
boot_current_hostname=$(ssh_guest 'hostname')
|
||||
boot_current_generation=$(status_field current_generation "$boot_current_status_file")
|
||||
boot_current_link=$(status_field current_link "$boot_current_status_file")
|
||||
boot_current_rollback_generation=$(status_field rollback_generation "$boot_current_status_file")
|
||||
[ "$boot_current_closure" = "$current_closure_path" ] || { echo "unexpected current closure after initial boot: $boot_current_closure" >&2; exit 1; }
|
||||
[ "$boot_current_hostname" = "$current_host_name" ] || { echo "unexpected hostname after initial boot: $boot_current_hostname" >&2; exit 1; }
|
||||
[ "$boot_current_generation" = 1 ] || { echo "unexpected initial generation: $boot_current_generation" >&2; exit 1; }
|
||||
[ "$boot_current_link" = generations/1 ] || { echo "unexpected initial current link: $boot_current_link" >&2; exit 1; }
|
||||
[ -z "$boot_current_rollback_generation" ] || { echo "rollback generation should be empty before switch" >&2; exit 1; }
|
||||
ssh_guest 'test -x /usr/local/bin/fruix'
|
||||
|
||||
ssh_guest "/usr/local/bin/fruix system switch $candidate_closure_path" > "$switch_status_file"
|
||||
switch_current_generation=$(status_field current_generation "$switch_status_file")
|
||||
switch_current_link=$(status_field current_link "$switch_status_file")
|
||||
switch_current_closure=$(status_field current_closure "$switch_status_file")
|
||||
switch_rollback_generation=$(status_field rollback_generation "$switch_status_file")
|
||||
switch_rollback_link=$(status_field rollback_link "$switch_status_file")
|
||||
switch_rollback_closure=$(status_field rollback_closure "$switch_status_file")
|
||||
[ "$switch_current_generation" = 2 ] || { echo "unexpected generation after switch: $switch_current_generation" >&2; exit 1; }
|
||||
[ "$switch_current_link" = generations/2 ] || { echo "unexpected current link after switch: $switch_current_link" >&2; exit 1; }
|
||||
[ "$switch_current_closure" = "$candidate_closure_path" ] || { echo "unexpected current closure after switch: $switch_current_closure" >&2; exit 1; }
|
||||
[ "$switch_rollback_generation" = 1 ] || { echo "unexpected rollback generation after switch: $switch_rollback_generation" >&2; exit 1; }
|
||||
[ "$switch_rollback_link" = generations/1 ] || { echo "unexpected rollback link after switch: $switch_rollback_link" >&2; exit 1; }
|
||||
[ "$switch_rollback_closure" = "$current_closure_path" ] || { echo "unexpected rollback closure after switch: $switch_rollback_closure" >&2; exit 1; }
|
||||
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/current-system')" = "$candidate_closure_path" ] || { echo "unexpected current-system gc root after switch" >&2; exit 1; }
|
||||
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/rollback-system')" = "$current_closure_path" ] || { echo "unexpected rollback-system gc root after switch" >&2; exit 1; }
|
||||
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/system-2')" = "$candidate_closure_path" ] || { echo "unexpected system-2 gc root after switch" >&2; exit 1; }
|
||||
ssh_guest 'test -f /var/lib/fruix/system/generations/2/metadata.scm'
|
||||
ssh_guest 'test -f /var/lib/fruix/system/generations/2/provenance.scm'
|
||||
ssh_guest 'test -f /var/lib/fruix/system/generations/2/install.scm'
|
||||
ssh_guest "cat /var/lib/fruix/system/generations/2/metadata.scm" > "$generation2_metadata_file"
|
||||
ssh_guest "cat /var/lib/fruix/system/generations/2/install.scm" > "$generation2_install_file"
|
||||
case "$(cat "$generation2_metadata_file")" in
|
||||
*"$candidate_closure_path"*"$current_closure_path"*) : ;;
|
||||
*) echo "generation 2 metadata does not record both candidate and previous closure paths" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$(cat "$generation2_install_file")" in
|
||||
*"(deployment-kind . \"switch\")"*"$candidate_closure_path"*) : ;;
|
||||
*) echo "generation 2 install metadata does not record switch provenance" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
reboot_guest
|
||||
capture_status "$boot_candidate_status_file"
|
||||
boot_candidate_closure=$(ssh_guest 'readlink /run/current-system')
|
||||
boot_candidate_hostname=$(ssh_guest 'hostname')
|
||||
boot_candidate_generation=$(status_field current_generation "$boot_candidate_status_file")
|
||||
boot_candidate_rollback_generation=$(status_field rollback_generation "$boot_candidate_status_file")
|
||||
boot_candidate_rollback_closure=$(status_field rollback_closure "$boot_candidate_status_file")
|
||||
[ "$boot_candidate_closure" = "$candidate_closure_path" ] || { echo "unexpected closure after switch reboot: $boot_candidate_closure" >&2; exit 1; }
|
||||
[ "$boot_candidate_hostname" = "$candidate_host_name" ] || { echo "unexpected hostname after switch reboot: $boot_candidate_hostname" >&2; exit 1; }
|
||||
[ "$boot_candidate_generation" = 2 ] || { echo "unexpected generation after switch reboot: $boot_candidate_generation" >&2; exit 1; }
|
||||
[ "$boot_candidate_rollback_generation" = 1 ] || { echo "unexpected rollback generation after switch reboot: $boot_candidate_rollback_generation" >&2; exit 1; }
|
||||
[ "$boot_candidate_rollback_closure" = "$current_closure_path" ] || { echo "unexpected rollback closure after switch reboot: $boot_candidate_rollback_closure" >&2; exit 1; }
|
||||
|
||||
ssh_guest '/usr/local/bin/fruix system rollback' > "$rollback_status_file"
|
||||
rollback_current_generation=$(status_field current_generation "$rollback_status_file")
|
||||
rollback_current_link=$(status_field current_link "$rollback_status_file")
|
||||
rollback_current_closure=$(status_field current_closure "$rollback_status_file")
|
||||
rollback_rollback_generation=$(status_field rollback_generation "$rollback_status_file")
|
||||
rollback_rollback_link=$(status_field rollback_link "$rollback_status_file")
|
||||
rollback_rollback_closure=$(status_field rollback_closure "$rollback_status_file")
|
||||
[ "$rollback_current_generation" = 1 ] || { echo "unexpected generation after rollback: $rollback_current_generation" >&2; exit 1; }
|
||||
[ "$rollback_current_link" = generations/1 ] || { echo "unexpected current link after rollback: $rollback_current_link" >&2; exit 1; }
|
||||
[ "$rollback_current_closure" = "$current_closure_path" ] || { echo "unexpected current closure after rollback: $rollback_current_closure" >&2; exit 1; }
|
||||
[ "$rollback_rollback_generation" = 2 ] || { echo "unexpected rollback generation after rollback: $rollback_rollback_generation" >&2; exit 1; }
|
||||
[ "$rollback_rollback_link" = generations/2 ] || { echo "unexpected rollback link after rollback: $rollback_rollback_link" >&2; exit 1; }
|
||||
[ "$rollback_rollback_closure" = "$candidate_closure_path" ] || { echo "unexpected rollback closure after rollback: $rollback_rollback_closure" >&2; exit 1; }
|
||||
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/current-system')" = "$current_closure_path" ] || { echo "unexpected current-system gc root after rollback" >&2; exit 1; }
|
||||
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/rollback-system')" = "$candidate_closure_path" ] || { echo "unexpected rollback-system gc root after rollback" >&2; exit 1; }
|
||||
|
||||
reboot_guest
|
||||
capture_status "$boot_rollback_status_file"
|
||||
boot_rollback_closure=$(ssh_guest 'readlink /run/current-system')
|
||||
boot_rollback_hostname=$(ssh_guest 'hostname')
|
||||
boot_rollback_generation=$(status_field current_generation "$boot_rollback_status_file")
|
||||
boot_rollback_rollback_generation=$(status_field rollback_generation "$boot_rollback_status_file")
|
||||
boot_rollback_rollback_closure=$(status_field rollback_closure "$boot_rollback_status_file")
|
||||
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')
|
||||
activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||
[ "$boot_rollback_closure" = "$current_closure_path" ] || { echo "unexpected closure after rollback reboot: $boot_rollback_closure" >&2; exit 1; }
|
||||
[ "$boot_rollback_hostname" = "$current_host_name" ] || { echo "unexpected hostname after rollback reboot: $boot_rollback_hostname" >&2; exit 1; }
|
||||
[ "$boot_rollback_generation" = 1 ] || { echo "unexpected generation after rollback reboot: $boot_rollback_generation" >&2; exit 1; }
|
||||
[ "$boot_rollback_rollback_generation" = 2 ] || { echo "unexpected rollback generation after rollback reboot: $boot_rollback_rollback_generation" >&2; exit 1; }
|
||||
[ "$boot_rollback_rollback_closure" = "$candidate_closure_path" ] || { echo "unexpected rollback closure after rollback reboot: $boot_rollback_rollback_closure" >&2; exit 1; }
|
||||
[ "$shepherd_status" = running ] || { echo "fruix-shepherd is not running after rollback reboot" >&2; exit 1; }
|
||||
[ "$sshd_status" = running ] || { echo "sshd is not running after rollback reboot" >&2; exit 1; }
|
||||
case "$activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "activation log does not show success after rollback workflow" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
current_os_file=$current_os_file
|
||||
candidate_os_file=$candidate_os_file
|
||||
target_image=$target_image
|
||||
current_closure_path=$current_closure_path
|
||||
candidate_closure_path=$candidate_closure_path
|
||||
current_host_name=$current_host_name
|
||||
candidate_host_name=$candidate_host_name
|
||||
candidate_store_item_count=$candidate_store_item_count
|
||||
candidate_store_items=$candidate_store_items
|
||||
stage_log=$stage_log
|
||||
serial_log=$serial_log
|
||||
install_metadata_path=$install_metadata_path
|
||||
materialized_source_store=$materialized_source_store
|
||||
switch_status_file=$switch_status_file
|
||||
rollback_status_file=$rollback_status_file
|
||||
boot_current_status_file=$boot_current_status_file
|
||||
boot_candidate_status_file=$boot_candidate_status_file
|
||||
boot_rollback_status_file=$boot_rollback_status_file
|
||||
generation2_metadata_file=$generation2_metadata_file
|
||||
generation2_install_file=$generation2_install_file
|
||||
final_current_generation=$boot_rollback_generation
|
||||
final_current_closure=$boot_rollback_closure
|
||||
final_rollback_generation=$boot_rollback_rollback_generation
|
||||
final_rollback_closure=$boot_rollback_rollback_closure
|
||||
shepherd_status=$shepherd_status
|
||||
sshd_status=$sshd_status
|
||||
boot_backend=qemu-uefi-tcg
|
||||
installed_system_switch=ok
|
||||
installed_system_rollback=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase19-installed-system-rollback-qemu\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"
|
||||
260
tests/system/run-phase20-development-environment-xcpng.sh
Executable file
260
tests/system/run-phase20-development-environment-xcpng.sh
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-development-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase20-operating-system}
|
||||
root_size=${ROOT_SIZE:-8g}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase20-development-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
inner_metadata=$workdir/phase20-development-inner-metadata.txt
|
||||
metadata_file=$workdir/phase20-development-environment-xcpng-metadata.txt
|
||||
|
||||
action_cleanup() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap action_cleanup EXIT INT TERM
|
||||
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
|
||||
"$repo_root/tests/system/run-phase11-shepherd-pid1-xcpng.sh"
|
||||
|
||||
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
|
||||
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
|
||||
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
|
||||
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
|
||||
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
|
||||
|
||||
development_profile_path=$closure_path/development-profile
|
||||
build_profile_path=$closure_path/build-profile
|
||||
runtime_profile_path=$closure_path/profile
|
||||
development_env_script=$closure_path/usr/local/bin/fruix-development-environment
|
||||
build_env_script=$closure_path/usr/local/bin/fruix-build-environment
|
||||
|
||||
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
|
||||
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
|
||||
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
|
||||
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
|
||||
case "$activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
for path in \
|
||||
"$development_profile_path/bin/cc" \
|
||||
"$development_profile_path/bin/c++" \
|
||||
"$development_profile_path/bin/ar" \
|
||||
"$development_profile_path/usr/include/sys/param.h" \
|
||||
"$development_profile_path/usr/share/mk/bsd.prog.mk" \
|
||||
"$build_profile_path/bin/cc" \
|
||||
"$build_profile_path/usr/include/sys/param.h" \
|
||||
"$build_profile_path/usr/share/mk/bsd.prog.mk" \
|
||||
"$development_env_script" \
|
||||
"$build_env_script"
|
||||
do
|
||||
[ -e "$path" ] || {
|
||||
echo "required development environment path missing: $path" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[ ! -e "$runtime_profile_path/include" ] || { echo "runtime profile unexpectedly contains headers" >&2; exit 1; }
|
||||
[ ! -e "$runtime_profile_path/usr/share/mk" ] || { echo "runtime profile unexpectedly contains /usr/share/mk" >&2; exit 1; }
|
||||
[ ! -e "$runtime_profile_path/bin/cc" ] || { echo "runtime profile unexpectedly contains cc" >&2; exit 1; }
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
guest_dev_metadata=$(ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" 'sh -s' <<'EOF'
|
||||
set -eu
|
||||
[ -x /usr/local/bin/fruix-development-environment ]
|
||||
[ -x /usr/local/bin/fruix-build-environment ]
|
||||
[ -L /run/current-development ]
|
||||
[ "$(readlink /run/current-development)" = "/run/current-system/development-profile" ]
|
||||
[ -L /run/current-build ]
|
||||
[ "$(readlink /run/current-build)" = "/run/current-system/build-profile" ]
|
||||
exports=$(/usr/local/bin/fruix-development-environment)
|
||||
printf '%s\n' "$exports" | grep '^export FRUIX_DEVELOPMENT_PROFILE="/run/current-system/development-profile"$' >/dev/null
|
||||
printf '%s\n' "$exports" | grep '^export MAKEFLAGS="-m /run/current-system/development-profile/usr/share/mk"$' >/dev/null
|
||||
eval "$exports"
|
||||
[ -d "$FRUIX_DEVELOPMENT_PROFILE" ]
|
||||
[ -x "$FRUIX_CC" ]
|
||||
[ -x "$FRUIX_CXX" ]
|
||||
[ -x "$FRUIX_AR" ]
|
||||
[ -f "$FRUIX_DEVELOPMENT_INCLUDE/sys/param.h" ]
|
||||
[ -f "$FRUIX_DEVELOPMENT_SHARE_MK/bsd.prog.mk" ]
|
||||
cc_version=$($FRUIX_CC --version | awk 'NR==1 { print; exit }')
|
||||
build_exports=$(/usr/local/bin/fruix-build-environment)
|
||||
printf '%s\n' "$build_exports" | grep '^unset MAKEOBJDIRPREFIX MAKEFLAGS CC CXX AR RANLIB NM CPPFLAGS CFLAGS CXXFLAGS LDFLAGS$' >/dev/null
|
||||
printf '%s\n' "$build_exports" | grep '^export FRUIX_BUILD_PROFILE="/run/current-system/build-profile"$' >/dev/null
|
||||
eval "$build_exports"
|
||||
[ -d "$FRUIX_BUILD_PROFILE" ]
|
||||
[ -f "$FRUIX_BUILD_INCLUDE/sys/param.h" ]
|
||||
[ -f "$FRUIX_BUILD_SHARE_MK/bsd.prog.mk" ]
|
||||
[ "${MAKEFLAGS-unset}" = unset ]
|
||||
[ "${CPPFLAGS-unset}" = unset ]
|
||||
[ "${CFLAGS-unset}" = unset ]
|
||||
[ "${LDFLAGS-unset}" = unset ]
|
||||
[ "$FRUIX_BMAKE" = make ]
|
||||
build_profile_value=$FRUIX_BUILD_PROFILE
|
||||
eval "$exports"
|
||||
tmp=/tmp/fruix-phase20-development-env
|
||||
rm -rf "$tmp"
|
||||
mkdir -p "$tmp/direct" "$tmp/mk"
|
||||
cat > "$tmp/direct/hello.c" <<'EOS'
|
||||
#include <stdio.h>
|
||||
int main(void){puts("hello-from-direct-dev-env");return 0;}
|
||||
EOS
|
||||
$FRUIX_CC $CPPFLAGS $LDFLAGS "$tmp/direct/hello.c" -o "$tmp/direct/hello"
|
||||
hello_direct=$($tmp/direct/hello)
|
||||
cat > "$tmp/mk/hello.c" <<'EOS'
|
||||
#include <stdio.h>
|
||||
int main(void){puts("hello-from-make-dev-env");return 0;}
|
||||
EOS
|
||||
cat > "$tmp/mk/Makefile" <<'EOS'
|
||||
PROG=hello
|
||||
SRCS=hello.c
|
||||
NO_MAN=yes
|
||||
.include <bsd.prog.mk>
|
||||
EOS
|
||||
cat > "$tmp/mk/hello.1" <<'EOS'
|
||||
.Dd April 5, 2026
|
||||
.Dt HELLO 1
|
||||
.Sh NAME
|
||||
.Nm hello
|
||||
.Nd phase20 development environment smoke binary
|
||||
EOS
|
||||
cd "$tmp/mk"
|
||||
make clean >/dev/null 2>&1 || true
|
||||
make > "$tmp/mk/make.log" 2>&1
|
||||
hello_make=$(./hello)
|
||||
make_log_tail=$(tail -n 20 "$tmp/mk/make.log" | tr '\n' ' ')
|
||||
exports_flat=$(printf '%s' "$exports" | tr '\n' ' ')
|
||||
printf 'development_profile=%s\n' "$FRUIX_DEVELOPMENT_PROFILE"
|
||||
printf 'build_profile=%s\n' "$build_profile_value"
|
||||
printf 'cc_version=%s\n' "$cc_version"
|
||||
printf 'hello_direct=%s\n' "$hello_direct"
|
||||
printf 'hello_make=%s\n' "$hello_make"
|
||||
build_exports_flat=$(printf '%s' "$build_exports" | tr '\n' ' ')
|
||||
printf 'exports=%s\n' "$exports_flat"
|
||||
printf 'build_exports=%s\n' "$build_exports_flat"
|
||||
printf 'make_log_tail=%s\n' "$make_log_tail"
|
||||
EOF
|
||||
)
|
||||
|
||||
development_profile=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^development_profile=//p')
|
||||
build_profile=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^build_profile=//p')
|
||||
cc_version=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^cc_version=//p')
|
||||
hello_direct=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^hello_direct=//p')
|
||||
hello_make=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^hello_make=//p')
|
||||
development_exports=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^exports=//p')
|
||||
build_exports=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^build_exports=//p')
|
||||
make_log_tail=$(printf '%s\n' "$guest_dev_metadata" | sed -n 's/^make_log_tail=//p')
|
||||
|
||||
[ "$development_profile" = "/run/current-system/development-profile" ] || {
|
||||
echo "unexpected guest development profile path: $development_profile" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$build_profile" = "/run/current-system/build-profile" ] || {
|
||||
echo "unexpected guest build profile path: $build_profile" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$cc_version" in
|
||||
*"FreeBSD clang version"*) : ;;
|
||||
*) echo "unexpected cc version output: $cc_version" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "$hello_direct" = hello-from-direct-dev-env ] || {
|
||||
echo "unexpected direct compile output: $hello_direct" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$hello_make" = hello-from-make-dev-env ] || {
|
||||
echo "unexpected make-based compile output: $hello_make" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$development_exports" in
|
||||
*'export FRUIX_CC="/run/current-system/development-profile/bin/cc"'*) : ;;
|
||||
*) echo "development environment exports do not include FRUIX_CC" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$build_exports" in
|
||||
*'export FRUIX_BUILD_PROFILE="/run/current-system/build-profile"'*) : ;;
|
||||
*) echo "build environment exports do not include FRUIX_BUILD_PROFILE" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_metadata=$inner_metadata
|
||||
phase8_metadata=$phase8_metadata
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
development_profile_path=$development_profile_path
|
||||
build_profile_path=$build_profile_path
|
||||
development_env_script=$development_env_script
|
||||
build_env_script=$build_env_script
|
||||
shepherd_pid=$shepherd_pid
|
||||
sshd_status=$sshd_status
|
||||
compat_prefix_shims=$compat_prefix_shims
|
||||
guile_module_smoke=$guile_module_smoke
|
||||
development_profile_guest=$development_profile
|
||||
build_profile_guest=$build_profile
|
||||
cc_version=$cc_version
|
||||
hello_direct=$hello_direct
|
||||
hello_make=$hello_make
|
||||
development_exports=$development_exports
|
||||
build_exports=$build_exports
|
||||
make_log_tail=$make_log_tail
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
development_environment=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase20-development-environment-xcpng\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"
|
||||
239
tests/system/run-phase20-host-initiated-native-build-store-promotion-xcpng.sh
Executable file
239
tests/system/run-phase20-host-initiated-native-build-store-promotion-xcpng.sh
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-development-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase20-operating-system}
|
||||
root_size=${ROOT_SIZE:-20g}
|
||||
store_dir=${STORE_DIR:-/frx/store}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase20-host-initiated-native-build-store-promotion-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
inner_metadata=$workdir/phase20-host-initiated-native-build-store-promotion-inner-metadata.txt
|
||||
promotion_out=$workdir/native-build-promote.txt
|
||||
metadata_file=$workdir/phase20-host-initiated-native-build-store-promotion-xcpng-metadata.txt
|
||||
import_root=$workdir/import
|
||||
|
||||
action_cleanup() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap action_cleanup EXIT INT TERM
|
||||
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
|
||||
"$repo_root/tests/system/run-phase20-host-initiated-native-build-xcpng.sh"
|
||||
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
run_id=$(sed -n 's/^run_id=//p' "$inner_metadata")
|
||||
source_store=$(sed -n 's/^source_store=//p' "$inner_metadata")
|
||||
result_root=$(sed -n 's/^result_root=//p' "$inner_metadata")
|
||||
promotion_file=$(sed -n 's/^promotion_file=//p' "$inner_metadata")
|
||||
world_artifact=$(sed -n 's/^world_artifact=//p' "$inner_metadata")
|
||||
kernel_artifact=$(sed -n 's/^kernel_artifact=//p' "$inner_metadata")
|
||||
headers_artifact=$(sed -n 's/^headers_artifact=//p' "$inner_metadata")
|
||||
bootloader_artifact=$(sed -n 's/^bootloader_artifact=//p' "$inner_metadata")
|
||||
sha_kernel=$(sed -n 's/^sha_kernel=//p' "$inner_metadata")
|
||||
sha_loader=$(sed -n 's/^sha_loader=//p' "$inner_metadata")
|
||||
sha_param=$(sed -n 's/^sha_param=//p' "$inner_metadata")
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
mkdir -p "$import_root"
|
||||
result_base=$(basename "$result_root")
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "tar -C '$(dirname "$result_root")' -cf - '$result_base'" | tar -C "$import_root" -xf -
|
||||
local_result_root=$import_root/$result_base
|
||||
[ -d "$local_result_root" ] || { echo "failed to import native build result root" >&2; exit 1; }
|
||||
[ -f "$local_result_root/promotion.scm" ] || { echo "imported result is missing promotion.scm" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/world/bin/sh" ] || { echo "imported result is missing world artifact" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/kernel/boot/kernel/kernel" ] || { echo "imported result is missing kernel artifact" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/headers/usr/include/sys/param.h" ] || { echo "imported result is missing headers artifact" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/bootloader/boot/loader.efi" ] || { echo "imported result is missing bootloader artifact" >&2; exit 1; }
|
||||
|
||||
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}" \
|
||||
SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
action_env "$repo_root/bin/fruix" native-build promote "$local_result_root" --store "$store_dir" >"$promotion_out"
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$promotion_out" | tail -n 1
|
||||
}
|
||||
|
||||
executor_kind=$(field executor_kind)
|
||||
executor_name=$(field executor_name)
|
||||
executor_version=$(field executor_version)
|
||||
result_store=$(field result_store)
|
||||
result_metadata_file=$(field result_metadata_file)
|
||||
artifact_store_count=$(field artifact_store_count)
|
||||
artifact_stores=$(field artifact_stores)
|
||||
world_store=$(field world_store)
|
||||
kernel_store=$(field kernel_store)
|
||||
headers_store=$(field headers_store)
|
||||
bootloader_store=$(field bootloader_store)
|
||||
|
||||
[ "$executor_kind" = ssh-guest ] || { echo "unexpected executor kind: $executor_kind" >&2; exit 1; }
|
||||
[ "$executor_name" = ssh-guest ] || { echo "unexpected executor name: $executor_name" >&2; exit 1; }
|
||||
[ "$executor_version" = 1 ] || { echo "unexpected executor version: $executor_version" >&2; exit 1; }
|
||||
[ "$artifact_store_count" = 4 ] || { echo "unexpected artifact store count: $artifact_store_count" >&2; exit 1; }
|
||||
case "$result_store" in
|
||||
/frx/store/*-fruix-native-build-result-*-ssh-guest) : ;;
|
||||
*) echo "unexpected result store path: $result_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$world_store" in
|
||||
/frx/store/*-fruix-native-world-*-ssh-guest) : ;;
|
||||
*) echo "unexpected world store path: $world_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$kernel_store" in
|
||||
/frx/store/*-fruix-native-kernel-*-ssh-guest) : ;;
|
||||
*) echo "unexpected kernel store path: $kernel_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$headers_store" in
|
||||
/frx/store/*-fruix-native-headers-*-ssh-guest) : ;;
|
||||
*) echo "unexpected headers store path: $headers_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$bootloader_store" in
|
||||
/frx/store/*-fruix-native-bootloader-*-ssh-guest) : ;;
|
||||
*) echo "unexpected bootloader store path: $bootloader_store" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
[ -f "$result_metadata_file" ] || { echo "missing result metadata file: $result_metadata_file" >&2; exit 1; }
|
||||
[ -f "$world_store/.fruix-native-build-object.scm" ] || { echo "missing world store metadata" >&2; exit 1; }
|
||||
[ -f "$kernel_store/.fruix-native-build-object.scm" ] || { echo "missing kernel store metadata" >&2; exit 1; }
|
||||
[ -f "$headers_store/.fruix-native-build-object.scm" ] || { echo "missing headers store metadata" >&2; exit 1; }
|
||||
[ -f "$bootloader_store/.fruix-native-build-object.scm" ] || { echo "missing bootloader store metadata" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/world" ] || { echo "missing promoted world artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/kernel" ] || { echo "missing promoted kernel artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/headers" ] || { echo "missing promoted headers artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/bootloader" ] || { echo "missing promoted bootloader artifact link" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/world")" = "$world_store" ] || { echo "world artifact link mismatch" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/kernel")" = "$kernel_store" ] || { echo "kernel artifact link mismatch" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/headers")" = "$headers_store" ] || { echo "headers artifact link mismatch" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/bootloader")" = "$bootloader_store" ] || { echo "bootloader artifact link mismatch" >&2; exit 1; }
|
||||
[ -f "$world_store/bin/sh" ] || { echo "promoted world store missing /bin/sh" >&2; exit 1; }
|
||||
[ -f "$kernel_store/boot/kernel/kernel" ] || { echo "promoted kernel store missing kernel" >&2; exit 1; }
|
||||
[ -f "$headers_store/usr/include/sys/param.h" ] || { echo "promoted headers store missing param.h" >&2; exit 1; }
|
||||
[ -f "$bootloader_store/boot/loader.efi" ] || { echo "promoted bootloader store missing loader.efi" >&2; exit 1; }
|
||||
|
||||
promoted_kernel_sha=$(sha256 -q "$kernel_store/boot/kernel/kernel")
|
||||
promoted_loader_sha=$(sha256 -q "$bootloader_store/boot/loader.efi")
|
||||
promoted_param_sha=$(sha256 -q "$headers_store/usr/include/sys/param.h")
|
||||
[ "$promoted_kernel_sha" = "$sha_kernel" ] || { echo "kernel sha mismatch after promotion" >&2; exit 1; }
|
||||
[ "$promoted_loader_sha" = "$sha_loader" ] || { echo "loader sha mismatch after promotion" >&2; exit 1; }
|
||||
[ "$promoted_param_sha" = "$sha_param" ] || { echo "param.h sha mismatch after promotion" >&2; exit 1; }
|
||||
|
||||
grep -F '(executor-kind . ssh-guest)' "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing ssh-guest executor kind" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(executor-name . "ssh-guest")' "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing ssh-guest executor name" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$source_store" "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing source store provenance" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(artifact-kind . kernel)' "$kernel_store/.fruix-native-build-object.scm" >/dev/null || {
|
||||
echo "kernel store metadata is missing artifact kind" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(artifact-kind . world)' "$world_store/.fruix-native-build-object.scm" >/dev/null || {
|
||||
echo "world store metadata is missing artifact kind" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_metadata=$inner_metadata
|
||||
promotion_out=$promotion_out
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
run_id=$run_id
|
||||
source_store=$source_store
|
||||
guest_result_root=$result_root
|
||||
guest_promotion_file=$promotion_file
|
||||
guest_world_artifact=$world_artifact
|
||||
guest_kernel_artifact=$kernel_artifact
|
||||
guest_headers_artifact=$headers_artifact
|
||||
guest_bootloader_artifact=$bootloader_artifact
|
||||
local_result_root=$local_result_root
|
||||
store_dir=$store_dir
|
||||
executor_kind=$executor_kind
|
||||
executor_name=$executor_name
|
||||
executor_version=$executor_version
|
||||
result_store=$result_store
|
||||
result_metadata_file=$result_metadata_file
|
||||
artifact_store_count=$artifact_store_count
|
||||
artifact_stores=$artifact_stores
|
||||
world_store=$world_store
|
||||
kernel_store=$kernel_store
|
||||
headers_store=$headers_store
|
||||
bootloader_store=$bootloader_store
|
||||
sha_kernel=$sha_kernel
|
||||
sha_loader=$sha_loader
|
||||
sha_param=$sha_param
|
||||
promoted_kernel_sha=$promoted_kernel_sha
|
||||
promoted_loader_sha=$promoted_loader_sha
|
||||
promoted_param_sha=$promoted_param_sha
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
host_initiated_native_build_store_promotion=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase20-host-initiated-native-build-store-promotion-xcpng\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"
|
||||
435
tests/system/run-phase20-host-initiated-native-build-xcpng.sh
Executable file
435
tests/system/run-phase20-host-initiated-native-build-xcpng.sh
Executable file
@@ -0,0 +1,435 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-development-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase20-operating-system}
|
||||
root_size=${ROOT_SIZE:-20g}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase20-native-build-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
inner_metadata=$workdir/phase20-native-build-inner-metadata.txt
|
||||
metadata_file=$workdir/phase20-host-initiated-native-build-xcpng-metadata.txt
|
||||
|
||||
action_cleanup() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap action_cleanup EXIT INT TERM
|
||||
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
|
||||
"$repo_root/tests/system/run-phase20-development-environment-xcpng.sh"
|
||||
|
||||
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
|
||||
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
|
||||
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
|
||||
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
|
||||
|
||||
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
|
||||
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
|
||||
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
|
||||
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
guest_build_jobs=${GUEST_BUILD_JOBS:-$(ssh_guest 'sysctl -n hw.ncpu')}
|
||||
case "$guest_build_jobs" in
|
||||
''|*[!0-9]*)
|
||||
echo "invalid guest build job count: $guest_build_jobs" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
native_build_metadata=$(ssh_guest env BUILD_JOBS="$guest_build_jobs" GUEST_IP="$guest_ip" VM_ID="$vm_id" VDI_ID="$vdi_id" sh -s <<'EOF'
|
||||
set -eu
|
||||
[ -L /run/current-development ]
|
||||
[ -L /run/current-build ]
|
||||
[ -L /usr/include ]
|
||||
[ "$(readlink /usr/include)" = "/run/current-system/build-profile/usr/include" ]
|
||||
[ -L /usr/share/mk ]
|
||||
[ "$(readlink /usr/share/mk)" = "/run/current-system/build-profile/usr/share/mk" ]
|
||||
guest_host_name=$(hostname)
|
||||
closure=$(readlink /run/current-system)
|
||||
source_store=$(sed -n 's/.*"\(\/frx\/store\/[^"]*-freebsd-source-[^"]*\)".*/\1/p' "$closure/metadata/store-layout.scm" | head -n 1)
|
||||
source_root="$source_store/tree"
|
||||
build_common='TARGET=amd64 TARGET_ARCH=amd64 KERNCONF=GENERIC __MAKE_CONF=/dev/null SRCCONF=/dev/null SRC_ENV_CONF=/dev/null MK_DEBUG_FILES=no MK_TESTS=no'
|
||||
install_common="$build_common DB_FROM_SRC=yes"
|
||||
build_root=/var/tmp/fruix-phase20-native-build
|
||||
run_id=${FRUIX_HOST_INITIATED_NATIVE_BUILD_ID:-$(date -u +%Y%m%dT%H%M%SZ)}
|
||||
result_root_base=/var/lib/fruix/native-builds
|
||||
result_root=$result_root_base/$run_id
|
||||
logdir=$build_root/logs
|
||||
status_file=$result_root/status
|
||||
guest_metadata_file=$result_root/metadata.txt
|
||||
promotion_file=$result_root/promotion.scm
|
||||
world_stage=$build_root/stage-world
|
||||
kernel_stage=$build_root/stage-kernel
|
||||
headers_stage=$build_root/artifact-headers
|
||||
bootloader_stage=$build_root/artifact-bootloader
|
||||
world_artifact=$result_root/artifacts/world
|
||||
kernel_artifact=$result_root/artifacts/kernel
|
||||
headers_artifact=$result_root/artifacts/headers
|
||||
bootloader_artifact=$result_root/artifacts/bootloader
|
||||
latest_link=$result_root_base/latest
|
||||
rm -rf "$build_root" "$result_root"
|
||||
mkdir -p "$logdir" "$result_root" "$world_artifact" "$kernel_artifact/boot" "$headers_artifact/usr" "$bootloader_artifact/boot"
|
||||
printf 'running\n' > "$status_file"
|
||||
fail_mark() {
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
printf 'failed\n' > "$status_file"
|
||||
fi
|
||||
}
|
||||
trap fail_mark EXIT HUP INT TERM
|
||||
export MAKEOBJDIRPREFIX="$build_root/obj"
|
||||
make -j"$BUILD_JOBS" -C "$source_root" TARGET=amd64 TARGET_ARCH=amd64 KERNCONF=GENERIC __MAKE_CONF=/dev/null SRCCONF=/dev/null SRC_ENV_CONF=/dev/null MK_DEBUG_FILES=no MK_TESTS=no buildworld > "$logdir/buildworld.log" 2>&1
|
||||
make -j"$BUILD_JOBS" -C "$source_root" TARGET=amd64 TARGET_ARCH=amd64 KERNCONF=GENERIC __MAKE_CONF=/dev/null SRCCONF=/dev/null SRC_ENV_CONF=/dev/null MK_DEBUG_FILES=no MK_TESTS=no buildkernel > "$logdir/buildkernel.log" 2>&1
|
||||
make -C "$source_root" $install_common DESTDIR="$world_stage" installworld > "$logdir/installworld.log" 2>&1
|
||||
make -C "$source_root" $install_common DESTDIR="$world_stage" distribution > "$logdir/distribution.log" 2>&1
|
||||
make -C "$source_root" $install_common DESTDIR="$kernel_stage" installkernel > "$logdir/installkernel.log" 2>&1
|
||||
mkdir -p "$headers_stage/usr" "$bootloader_stage/boot"
|
||||
cp -a "$world_stage/." "$world_artifact/"
|
||||
cp -a "$kernel_stage/boot/kernel" "$kernel_artifact/boot/kernel"
|
||||
cp -a "$world_stage/usr/include" "$headers_stage/usr/include"
|
||||
mkdir -p "$headers_stage/usr/share"
|
||||
cp -a "$world_stage/usr/share/mk" "$headers_stage/usr/share/mk"
|
||||
cp -a "$headers_stage/usr/." "$headers_artifact/usr/"
|
||||
cp -a "$world_stage/boot/loader" "$bootloader_stage/boot/loader"
|
||||
cp -a "$world_stage/boot/loader.efi" "$bootloader_stage/boot/loader.efi"
|
||||
cp -a "$world_stage/boot/device.hints" "$bootloader_stage/boot/device.hints"
|
||||
cp -a "$world_stage/boot/defaults" "$bootloader_stage/boot/defaults"
|
||||
cp -a "$world_stage/boot/lua" "$bootloader_stage/boot/lua"
|
||||
cp -a "$bootloader_stage/boot/." "$bootloader_artifact/boot/"
|
||||
[ -f "$world_artifact/bin/sh" ]
|
||||
[ -f "$kernel_artifact/boot/kernel/kernel" ]
|
||||
[ -f "$headers_artifact/usr/include/sys/param.h" ]
|
||||
[ -f "$headers_artifact/usr/share/mk/bsd.prog.mk" ]
|
||||
[ -f "$bootloader_artifact/boot/loader.efi" ]
|
||||
[ -f "$bootloader_artifact/boot/defaults/loader.conf" ]
|
||||
[ -f "$bootloader_artifact/boot/lua/loader.lua" ]
|
||||
sha_kernel=$(sha256 -q "$kernel_artifact/boot/kernel/kernel")
|
||||
sha_loader=$(sha256 -q "$bootloader_artifact/boot/loader.efi")
|
||||
sha_param=$(sha256 -q "$headers_artifact/usr/include/sys/param.h")
|
||||
buildworld_tail=$(tail -n 20 "$logdir/buildworld.log" | tr '\n' ' ')
|
||||
buildkernel_tail=$(tail -n 20 "$logdir/buildkernel.log" | tr '\n' ' ')
|
||||
installworld_tail=$(tail -n 20 "$logdir/installworld.log" | tr '\n' ' ')
|
||||
distribution_tail=$(tail -n 20 "$logdir/distribution.log" | tr '\n' ' ')
|
||||
installkernel_tail=$(tail -n 20 "$logdir/installkernel.log" | tr '\n' ' ')
|
||||
root_df=$(df -h / | tail -n 1 | tr -s ' ' | tr '\t' ' ')
|
||||
build_root_size=$(du -sh "$build_root" | awk '{print $1}')
|
||||
result_root_size=$(du -sh "$result_root" | awk '{print $1}')
|
||||
world_stage_size=$(du -sh "$world_stage" | awk '{print $1}')
|
||||
kernel_stage_size=$(du -sh "$kernel_stage" | awk '{print $1}')
|
||||
headers_stage_size=$(du -sh "$headers_stage" | awk '{print $1}')
|
||||
bootloader_stage_size=$(du -sh "$bootloader_stage" | awk '{print $1}')
|
||||
world_artifact_size=$(du -sh "$world_artifact" | awk '{print $1}')
|
||||
kernel_artifact_size=$(du -sh "$kernel_artifact" | awk '{print $1}')
|
||||
headers_artifact_size=$(du -sh "$headers_artifact" | awk '{print $1}')
|
||||
bootloader_artifact_size=$(du -sh "$bootloader_artifact" | awk '{print $1}')
|
||||
rm -f "$latest_link"
|
||||
ln -s "$result_root" "$latest_link"
|
||||
cat >"$promotion_file" <<EOF2
|
||||
((native-build-result-version . "1")
|
||||
(executor . ((kind . ssh-guest)
|
||||
(name . "ssh-guest")
|
||||
(version . "1")
|
||||
(properties . ((transport . "ssh")
|
||||
(orchestrator . "host")
|
||||
(guest-host-name . "$guest_host_name")
|
||||
(guest-ip . "$GUEST_IP")
|
||||
(vm-id . "$VM_ID")
|
||||
(vdi-id . "$VDI_ID")))))
|
||||
(run-id . "$run_id")
|
||||
(guest-host-name . "$guest_host_name")
|
||||
(closure-path . "$closure")
|
||||
(build-profile . "/run/current-system/build-profile")
|
||||
(freebsd-base . ((name . "default")
|
||||
(version-label . "15.0-STABLE")
|
||||
(release . "15.0-STABLE")
|
||||
(branch . "stable/15")
|
||||
(source-root . "/usr/src")
|
||||
(target . "amd64")
|
||||
(target-arch . "amd64")
|
||||
(kernconf . "GENERIC")))
|
||||
(source . ((store-path . "$source_store")
|
||||
(source-root . "$source_root")))
|
||||
(build-policy . ((jobs . "$BUILD_JOBS")
|
||||
(build-common . "$build_common")
|
||||
(install-common . "$install_common")))
|
||||
(artifacts . ((world . ((path . "artifacts/world")
|
||||
(required-file . "bin/sh")))
|
||||
(kernel . ((path . "artifacts/kernel")
|
||||
(required-file . "boot/kernel/kernel")
|
||||
(recorded-sha256 . "$sha_kernel")))
|
||||
(headers . ((path . "artifacts/headers")
|
||||
(required-file . "usr/include/sys/param.h")
|
||||
(recorded-sha256 . "$sha_param")))
|
||||
(bootloader . ((path . "artifacts/bootloader")
|
||||
(required-file . "boot/loader.efi")
|
||||
(recorded-sha256 . "$sha_loader"))))))
|
||||
EOF2
|
||||
cat >"$guest_metadata_file" <<EOF2
|
||||
run_id=$run_id
|
||||
executor_kind=ssh-guest
|
||||
executor_name=ssh-guest
|
||||
executor_version=1
|
||||
guest_host_name=$guest_host_name
|
||||
closure_path=$closure
|
||||
source_store=$source_store
|
||||
source_root=$source_root
|
||||
build_jobs=$BUILD_JOBS
|
||||
build_common=$build_common
|
||||
install_common=$install_common
|
||||
build_root=$build_root
|
||||
result_root=$result_root
|
||||
logdir=$logdir
|
||||
status_file=$status_file
|
||||
metadata_file=$guest_metadata_file
|
||||
promotion_file=$promotion_file
|
||||
world_stage=$world_stage
|
||||
kernel_stage=$kernel_stage
|
||||
headers_stage=$headers_stage
|
||||
bootloader_stage=$bootloader_stage
|
||||
world_artifact=$world_artifact
|
||||
kernel_artifact=$kernel_artifact
|
||||
headers_artifact=$headers_artifact
|
||||
bootloader_artifact=$bootloader_artifact
|
||||
latest_link=$latest_link
|
||||
root_df=$root_df
|
||||
build_root_size=$build_root_size
|
||||
result_root_size=$result_root_size
|
||||
world_stage_size=$world_stage_size
|
||||
kernel_stage_size=$kernel_stage_size
|
||||
headers_stage_size=$headers_stage_size
|
||||
bootloader_stage_size=$bootloader_stage_size
|
||||
world_artifact_size=$world_artifact_size
|
||||
kernel_artifact_size=$kernel_artifact_size
|
||||
headers_artifact_size=$headers_artifact_size
|
||||
bootloader_artifact_size=$bootloader_artifact_size
|
||||
buildworld_log=$logdir/buildworld.log
|
||||
buildkernel_log=$logdir/buildkernel.log
|
||||
installworld_log=$logdir/installworld.log
|
||||
distribution_log=$logdir/distribution.log
|
||||
installkernel_log=$logdir/installkernel.log
|
||||
sha_kernel=$sha_kernel
|
||||
sha_loader=$sha_loader
|
||||
sha_param=$sha_param
|
||||
buildworld_tail=$buildworld_tail
|
||||
buildkernel_tail=$buildkernel_tail
|
||||
installworld_tail=$installworld_tail
|
||||
distribution_tail=$distribution_tail
|
||||
installkernel_tail=$installkernel_tail
|
||||
host_initiated_native_build=ok
|
||||
EOF2
|
||||
printf 'ok\n' > "$status_file"
|
||||
cat "$guest_metadata_file"
|
||||
EOF
|
||||
)
|
||||
|
||||
run_id=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^run_id=//p')
|
||||
executor_kind=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^executor_kind=//p')
|
||||
executor_name=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^executor_name=//p')
|
||||
executor_version=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^executor_version=//p')
|
||||
guest_host_name=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^guest_host_name=//p')
|
||||
source_store=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^source_store=//p')
|
||||
source_root=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^source_root=//p')
|
||||
build_jobs=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^build_jobs=//p')
|
||||
build_common=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^build_common=//p')
|
||||
install_common=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^install_common=//p')
|
||||
build_root=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^build_root=//p')
|
||||
result_root=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^result_root=//p')
|
||||
logdir=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^logdir=//p')
|
||||
status_file=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^status_file=//p')
|
||||
guest_metadata_file=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^metadata_file=//p')
|
||||
promotion_file=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^promotion_file=//p')
|
||||
buildworld_log=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^buildworld_log=//p')
|
||||
buildkernel_log=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^buildkernel_log=//p')
|
||||
installworld_log=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^installworld_log=//p')
|
||||
distribution_log=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^distribution_log=//p')
|
||||
installkernel_log=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^installkernel_log=//p')
|
||||
world_stage=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^world_stage=//p')
|
||||
kernel_stage=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^kernel_stage=//p')
|
||||
headers_stage=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^headers_stage=//p')
|
||||
bootloader_stage=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^bootloader_stage=//p')
|
||||
world_artifact=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^world_artifact=//p')
|
||||
kernel_artifact=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^kernel_artifact=//p')
|
||||
headers_artifact=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^headers_artifact=//p')
|
||||
bootloader_artifact=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^bootloader_artifact=//p')
|
||||
latest_link=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^latest_link=//p')
|
||||
root_df=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^root_df=//p')
|
||||
build_root_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^build_root_size=//p')
|
||||
result_root_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^result_root_size=//p')
|
||||
world_stage_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^world_stage_size=//p')
|
||||
kernel_stage_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^kernel_stage_size=//p')
|
||||
headers_stage_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^headers_stage_size=//p')
|
||||
bootloader_stage_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^bootloader_stage_size=//p')
|
||||
world_artifact_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^world_artifact_size=//p')
|
||||
kernel_artifact_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^kernel_artifact_size=//p')
|
||||
headers_artifact_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^headers_artifact_size=//p')
|
||||
bootloader_artifact_size=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^bootloader_artifact_size=//p')
|
||||
sha_kernel=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^sha_kernel=//p')
|
||||
sha_loader=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^sha_loader=//p')
|
||||
sha_param=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^sha_param=//p')
|
||||
buildworld_tail=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^buildworld_tail=//p')
|
||||
buildkernel_tail=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^buildkernel_tail=//p')
|
||||
installworld_tail=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^installworld_tail=//p')
|
||||
distribution_tail=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^distribution_tail=//p')
|
||||
installkernel_tail=$(printf '%s\n' "$native_build_metadata" | sed -n 's/^installkernel_tail=//p')
|
||||
|
||||
status_value=$(ssh_guest "cat '$status_file'")
|
||||
latest_target=$(ssh_guest "readlink '$latest_link'")
|
||||
ssh_guest "[ -f '$promotion_file' ]"
|
||||
ssh_guest "[ -f '$world_artifact/bin/sh' ]"
|
||||
|
||||
[ "$executor_kind" = ssh-guest ] || { echo "unexpected executor kind: $executor_kind" >&2; exit 1; }
|
||||
[ "$executor_name" = ssh-guest ] || { echo "unexpected executor name: $executor_name" >&2; exit 1; }
|
||||
[ "$executor_version" = 1 ] || { echo "unexpected executor version: $executor_version" >&2; exit 1; }
|
||||
[ "$status_value" = ok ] || { echo "host-initiated build status is not ok: $status_value" >&2; exit 1; }
|
||||
[ "$latest_target" = "$result_root" ] || { echo "latest link target mismatch: $latest_target" >&2; exit 1; }
|
||||
|
||||
case "$source_store" in
|
||||
/frx/store/*-freebsd-source-*) : ;;
|
||||
*) echo "unexpected source store path: $source_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$source_root" in
|
||||
/frx/store/*-freebsd-source-*/*) : ;;
|
||||
*) echo "unexpected source root path: $source_root" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$build_root" in
|
||||
/var/tmp/fruix-phase20-native-build) : ;;
|
||||
*) echo "unexpected build root: $build_root" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$result_root" in
|
||||
/var/lib/fruix/native-builds/*) : ;;
|
||||
*) echo "unexpected result root: $result_root" >&2; exit 1 ;;
|
||||
esac
|
||||
printf '%s\n' "$sha_kernel" | grep -E '^[0-9a-f]{64}$' >/dev/null || {
|
||||
echo "invalid kernel sha256: $sha_kernel" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$sha_loader" | grep -E '^[0-9a-f]{64}$' >/dev/null || {
|
||||
echo "invalid loader sha256: $sha_loader" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$sha_param" | grep -E '^[0-9a-f]{64}$' >/dev/null || {
|
||||
echo "invalid param.h sha256: $sha_param" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$buildworld_tail" in
|
||||
*'World build completed on'*) : ;;
|
||||
*) echo "buildworld log does not show completion" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$buildkernel_tail" in
|
||||
*'Kernel(s) GENERIC built in'*) : ;;
|
||||
*) echo "buildkernel log does not show successful kernel build" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_metadata=$inner_metadata
|
||||
phase8_metadata=$phase8_metadata
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
run_id=$run_id
|
||||
executor_kind=$executor_kind
|
||||
executor_name=$executor_name
|
||||
executor_version=$executor_version
|
||||
guest_host_name=$guest_host_name
|
||||
build_jobs=$build_jobs
|
||||
source_store=$source_store
|
||||
source_root=$source_root
|
||||
build_common=$build_common
|
||||
install_common=$install_common
|
||||
build_root=$build_root
|
||||
result_root=$result_root
|
||||
logdir=$logdir
|
||||
status_file=$status_file
|
||||
guest_metadata_file=$guest_metadata_file
|
||||
promotion_file=$promotion_file
|
||||
buildworld_log=$buildworld_log
|
||||
buildkernel_log=$buildkernel_log
|
||||
installworld_log=$installworld_log
|
||||
distribution_log=$distribution_log
|
||||
installkernel_log=$installkernel_log
|
||||
world_stage=$world_stage
|
||||
kernel_stage=$kernel_stage
|
||||
headers_stage=$headers_stage
|
||||
bootloader_stage=$bootloader_stage
|
||||
world_artifact=$world_artifact
|
||||
kernel_artifact=$kernel_artifact
|
||||
headers_artifact=$headers_artifact
|
||||
bootloader_artifact=$bootloader_artifact
|
||||
latest_link=$latest_link
|
||||
latest_target=$latest_target
|
||||
status_value=$status_value
|
||||
root_df=$root_df
|
||||
build_root_size=$build_root_size
|
||||
result_root_size=$result_root_size
|
||||
world_stage_size=$world_stage_size
|
||||
kernel_stage_size=$kernel_stage_size
|
||||
headers_stage_size=$headers_stage_size
|
||||
bootloader_stage_size=$bootloader_stage_size
|
||||
world_artifact_size=$world_artifact_size
|
||||
kernel_artifact_size=$kernel_artifact_size
|
||||
headers_artifact_size=$headers_artifact_size
|
||||
bootloader_artifact_size=$bootloader_artifact_size
|
||||
sha_kernel=$sha_kernel
|
||||
sha_loader=$sha_loader
|
||||
sha_param=$sha_param
|
||||
buildworld_tail=$buildworld_tail
|
||||
buildkernel_tail=$buildkernel_tail
|
||||
installworld_tail=$installworld_tail
|
||||
distribution_tail=$distribution_tail
|
||||
installkernel_tail=$installkernel_tail
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
host_initiated_native_build=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase20-host-initiated-native-build-xcpng\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"
|
||||
248
tests/system/run-phase20-native-build-store-promotion-xcpng.sh
Executable file
248
tests/system/run-phase20-native-build-store-promotion-xcpng.sh
Executable file
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-development-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase20-operating-system}
|
||||
root_size=${ROOT_SIZE:-20g}
|
||||
store_dir=${STORE_DIR:-/frx/store}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase20-native-build-store-promotion-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
inner_metadata=$workdir/phase20-native-build-store-promotion-inner-metadata.txt
|
||||
existing_inner_metadata=${EXISTING_INNER_METADATA:-}
|
||||
promotion_out=$workdir/native-build-promote.txt
|
||||
metadata_file=$workdir/phase20-native-build-store-promotion-xcpng-metadata.txt
|
||||
import_root=$workdir/import
|
||||
|
||||
action_cleanup() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap action_cleanup EXIT INT TERM
|
||||
|
||||
if [ -n "$existing_inner_metadata" ]; then
|
||||
cp "$existing_inner_metadata" "$inner_metadata"
|
||||
else
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
|
||||
"$repo_root/tests/system/run-phase20-self-hosted-native-build-xcpng.sh"
|
||||
fi
|
||||
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
run_id=$(sed -n 's/^run_id=//p' "$inner_metadata")
|
||||
source_store=$(sed -n 's/^source_store=//p' "$inner_metadata")
|
||||
result_root=$(sed -n 's/^result_root=//p' "$inner_metadata")
|
||||
promotion_file=$(sed -n 's/^promotion_file=//p' "$inner_metadata")
|
||||
world_artifact=$(sed -n 's/^world_artifact=//p' "$inner_metadata")
|
||||
kernel_artifact=$(sed -n 's/^kernel_artifact=//p' "$inner_metadata")
|
||||
headers_artifact=$(sed -n 's/^headers_artifact=//p' "$inner_metadata")
|
||||
bootloader_artifact=$(sed -n 's/^bootloader_artifact=//p' "$inner_metadata")
|
||||
sha_kernel=$(sed -n 's/^sha_kernel=//p' "$inner_metadata")
|
||||
sha_loader=$(sed -n 's/^sha_loader=//p' "$inner_metadata")
|
||||
sha_param=$(sed -n 's/^sha_param=//p' "$inner_metadata")
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
mkdir -p "$import_root"
|
||||
result_base=$(basename "$result_root")
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "tar -C '$(dirname "$result_root")' -cf - '$result_base'" | tar -C "$import_root" -xf -
|
||||
local_result_root=$import_root/$result_base
|
||||
[ -d "$local_result_root" ] || { echo "failed to import native build result root" >&2; exit 1; }
|
||||
[ -f "$local_result_root/promotion.scm" ] || { echo "imported result is missing promotion.scm" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/world/bin/sh" ] || { echo "imported result is missing world artifact" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/kernel/boot/kernel/kernel" ] || { echo "imported result is missing kernel artifact" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/headers/usr/include/sys/param.h" ] || { echo "imported result is missing headers artifact" >&2; exit 1; }
|
||||
[ -f "$local_result_root/artifacts/bootloader/boot/loader.efi" ] || { echo "imported result is missing bootloader artifact" >&2; exit 1; }
|
||||
|
||||
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}" \
|
||||
SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
action_env "$repo_root/bin/fruix" native-build promote "$local_result_root" --store "$store_dir" >"$promotion_out"
|
||||
|
||||
field() {
|
||||
sed -n "s/^$1=//p" "$promotion_out" | tail -n 1
|
||||
}
|
||||
|
||||
executor_kind=$(field executor_kind)
|
||||
executor_name=$(field executor_name)
|
||||
executor_version=$(field executor_version)
|
||||
result_store=$(field result_store)
|
||||
result_metadata_file=$(field result_metadata_file)
|
||||
artifact_store_count=$(field artifact_store_count)
|
||||
artifact_stores=$(field artifact_stores)
|
||||
world_store=$(field world_store)
|
||||
kernel_store=$(field kernel_store)
|
||||
headers_store=$(field headers_store)
|
||||
bootloader_store=$(field bootloader_store)
|
||||
|
||||
[ "$executor_kind" = self-hosted ] || { echo "unexpected executor kind: $executor_kind" >&2; exit 1; }
|
||||
[ "$executor_name" = guest-self-hosted ] || { echo "unexpected executor name: $executor_name" >&2; exit 1; }
|
||||
[ "$executor_version" = 5 ] || { echo "unexpected executor version: $executor_version" >&2; exit 1; }
|
||||
[ "$artifact_store_count" = 4 ] || { echo "unexpected artifact store count: $artifact_store_count" >&2; exit 1; }
|
||||
case "$result_store" in
|
||||
/frx/store/*-fruix-native-build-result-*-guest-self-hosted) : ;;
|
||||
*) echo "unexpected result store path: $result_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$world_store" in
|
||||
/frx/store/*-fruix-native-world-*-guest-self-hosted) : ;;
|
||||
*) echo "unexpected world store path: $world_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$kernel_store" in
|
||||
/frx/store/*-fruix-native-kernel-*-guest-self-hosted) : ;;
|
||||
*) echo "unexpected kernel store path: $kernel_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$headers_store" in
|
||||
/frx/store/*-fruix-native-headers-*-guest-self-hosted) : ;;
|
||||
*) echo "unexpected headers store path: $headers_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$bootloader_store" in
|
||||
/frx/store/*-fruix-native-bootloader-*-guest-self-hosted) : ;;
|
||||
*) echo "unexpected bootloader store path: $bootloader_store" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
[ -f "$result_metadata_file" ] || { echo "missing result metadata file: $result_metadata_file" >&2; exit 1; }
|
||||
[ -f "$world_store/.fruix-native-build-object.scm" ] || { echo "missing world store metadata" >&2; exit 1; }
|
||||
[ -f "$kernel_store/.fruix-native-build-object.scm" ] || { echo "missing kernel store metadata" >&2; exit 1; }
|
||||
[ -f "$headers_store/.fruix-native-build-object.scm" ] || { echo "missing headers store metadata" >&2; exit 1; }
|
||||
[ -f "$bootloader_store/.fruix-native-build-object.scm" ] || { echo "missing bootloader store metadata" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/world" ] || { echo "missing promoted world artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/kernel" ] || { echo "missing promoted kernel artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/headers" ] || { echo "missing promoted headers artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/bootloader" ] || { echo "missing promoted bootloader artifact link" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/world")" = "$world_store" ] || { echo "world artifact link mismatch" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/kernel")" = "$kernel_store" ] || { echo "kernel artifact link mismatch" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/headers")" = "$headers_store" ] || { echo "headers artifact link mismatch" >&2; exit 1; }
|
||||
[ "$(readlink "$result_store/artifacts/bootloader")" = "$bootloader_store" ] || { echo "bootloader artifact link mismatch" >&2; exit 1; }
|
||||
[ -f "$world_store/bin/sh" ] || { echo "promoted world store missing /bin/sh" >&2; exit 1; }
|
||||
[ -f "$kernel_store/boot/kernel/kernel" ] || { echo "promoted kernel store missing kernel" >&2; exit 1; }
|
||||
[ -f "$headers_store/usr/include/sys/param.h" ] || { echo "promoted headers store missing param.h" >&2; exit 1; }
|
||||
[ -f "$bootloader_store/boot/loader.efi" ] || { echo "promoted bootloader store missing loader.efi" >&2; exit 1; }
|
||||
|
||||
promoted_kernel_sha=$(sha256 -q "$kernel_store/boot/kernel/kernel")
|
||||
promoted_loader_sha=$(sha256 -q "$bootloader_store/boot/loader.efi")
|
||||
promoted_param_sha=$(sha256 -q "$headers_store/usr/include/sys/param.h")
|
||||
[ "$promoted_kernel_sha" = "$sha_kernel" ] || { echo "kernel sha mismatch after promotion" >&2; exit 1; }
|
||||
[ "$promoted_loader_sha" = "$sha_loader" ] || { echo "loader sha mismatch after promotion" >&2; exit 1; }
|
||||
[ "$promoted_param_sha" = "$sha_param" ] || { echo "param.h sha mismatch after promotion" >&2; exit 1; }
|
||||
|
||||
grep -F '(executor-kind . self-hosted)' "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing self-hosted executor kind" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(executor-name . "guest-self-hosted")' "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing guest-self-hosted executor name" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$source_store" "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing source store provenance" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(build-profile . "/run/current-system/build-profile")' "$result_metadata_file" >/dev/null || {
|
||||
echo "result metadata file is missing build-profile provenance" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(artifact-kind . kernel)' "$kernel_store/.fruix-native-build-object.scm" >/dev/null || {
|
||||
echo "kernel store metadata is missing artifact kind" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F '(artifact-kind . world)' "$world_store/.fruix-native-build-object.scm" >/dev/null || {
|
||||
echo "world store metadata is missing artifact kind" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_metadata=$inner_metadata
|
||||
promotion_out=$promotion_out
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
run_id=$run_id
|
||||
source_store=$source_store
|
||||
guest_result_root=$result_root
|
||||
guest_promotion_file=$promotion_file
|
||||
guest_world_artifact=$world_artifact
|
||||
guest_kernel_artifact=$kernel_artifact
|
||||
guest_headers_artifact=$headers_artifact
|
||||
guest_bootloader_artifact=$bootloader_artifact
|
||||
local_result_root=$local_result_root
|
||||
store_dir=$store_dir
|
||||
executor_kind=$executor_kind
|
||||
executor_name=$executor_name
|
||||
executor_version=$executor_version
|
||||
result_store=$result_store
|
||||
result_metadata_file=$result_metadata_file
|
||||
artifact_store_count=$artifact_store_count
|
||||
artifact_stores=$artifact_stores
|
||||
world_store=$world_store
|
||||
kernel_store=$kernel_store
|
||||
headers_store=$headers_store
|
||||
bootloader_store=$bootloader_store
|
||||
sha_kernel=$sha_kernel
|
||||
sha_loader=$sha_loader
|
||||
sha_param=$sha_param
|
||||
promoted_kernel_sha=$promoted_kernel_sha
|
||||
promoted_loader_sha=$promoted_loader_sha
|
||||
promoted_param_sha=$promoted_param_sha
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
native_build_store_promotion=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase20-native-build-store-promotion-xcpng\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"
|
||||
227
tests/system/run-phase20-promoted-native-base-declaration-xcpng.sh
Executable file
227
tests/system/run-phase20-promoted-native-base-declaration-xcpng.sh
Executable file
@@ -0,0 +1,227 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-promoted-native-base-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase20-promoted-native-base-operating-system}
|
||||
root_size=${ROOT_SIZE:-12g}
|
||||
result_store=${RESULT_STORE:-}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase20-promoted-native-base-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
promotion_metadata=$workdir/phase20-promoted-native-base-promotion-metadata.txt
|
||||
prepared_template=$workdir/phase20-promoted-native-base-operating-system.scm.in
|
||||
inner_metadata=$workdir/phase20-promoted-native-base-inner-metadata.txt
|
||||
metadata_file=$workdir/phase20-promoted-native-base-declaration-xcpng-metadata.txt
|
||||
|
||||
cleanup_workdir() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_workdir EXIT INT TERM
|
||||
|
||||
if [ -z "$result_store" ]; then
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/promotion" METADATA_OUT="$promotion_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
ROOT_SIZE=20g \
|
||||
"$repo_root/tests/system/run-phase20-host-initiated-native-build-store-promotion-xcpng.sh"
|
||||
result_store=$(sed -n 's/^result_store=//p' "$promotion_metadata")
|
||||
fi
|
||||
|
||||
[ -n "$result_store" ] || { echo "missing promoted result store" >&2; exit 1; }
|
||||
[ -d "$result_store" ] || { echo "promoted result store does not exist: $result_store" >&2; exit 1; }
|
||||
[ -f "$result_store/.fruix-native-build-result.scm" ] || {
|
||||
echo "promoted result store is missing .fruix-native-build-result.scm" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -L "$result_store/artifacts/world" ] || { echo "promoted result store is missing world artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/kernel" ] || { echo "promoted result store is missing kernel artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/headers" ] || { echo "promoted result store is missing headers artifact link" >&2; exit 1; }
|
||||
[ -L "$result_store/artifacts/bootloader" ] || { echo "promoted result store is missing bootloader artifact link" >&2; exit 1; }
|
||||
|
||||
world_store=$(readlink "$result_store/artifacts/world")
|
||||
kernel_store=$(readlink "$result_store/artifacts/kernel")
|
||||
headers_store=$(readlink "$result_store/artifacts/headers")
|
||||
bootloader_store=$(readlink "$result_store/artifacts/bootloader")
|
||||
result_metadata_file=$result_store/.fruix-native-build-result.scm
|
||||
executor_kind=$(grep -o '(executor-kind \. [^)]*)' "$result_metadata_file" | head -n 1 | sed 's/(executor-kind \. //; s/)$//')
|
||||
executor_name=$(grep -o '(executor-name \. "[^"]*")' "$result_metadata_file" | head -n 1 | sed 's/(executor-name \. "//; s/")$//')
|
||||
executor_version=$(grep -o '(executor-version \. "[^"]*")' "$result_metadata_file" | head -n 1 | sed 's/(executor-version \. "//; s/")$//')
|
||||
|
||||
[ -f "$world_store/bin/sh" ] || { echo "promoted world store is missing /bin/sh" >&2; exit 1; }
|
||||
[ -f "$kernel_store/boot/kernel/kernel" ] || { echo "promoted kernel store is missing kernel" >&2; exit 1; }
|
||||
[ -f "$headers_store/usr/include/sys/param.h" ] || { echo "promoted headers store is missing param.h" >&2; exit 1; }
|
||||
[ -f "$bootloader_store/boot/loader.efi" ] || { echo "promoted bootloader store is missing loader.efi" >&2; exit 1; }
|
||||
|
||||
sed "s|__PROMOTED_RESULT_STORE__|$result_store|g" "$os_template" > "$prepared_template"
|
||||
|
||||
action_metadata=${promotion_metadata:-}
|
||||
ROOT_SIZE="$root_size" KEEP_WORKDIR=1 WORKDIR="$workdir/boot" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$prepared_template" SYSTEM_NAME="$system_name" \
|
||||
"$repo_root/tests/system/run-phase11-shepherd-pid1-xcpng.sh"
|
||||
|
||||
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
|
||||
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
|
||||
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
|
||||
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
|
||||
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
|
||||
|
||||
promoted_result_file=$closure_path/metadata/promoted-native-build-result.scm
|
||||
store_layout_file=$closure_path/metadata/store-layout.scm
|
||||
|
||||
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
|
||||
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
|
||||
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
|
||||
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
|
||||
case "$activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
[ -f "$promoted_result_file" ] || { echo "closure is missing promoted native build result metadata" >&2; exit 1; }
|
||||
[ -f "$store_layout_file" ] || { echo "closure is missing store layout metadata" >&2; exit 1; }
|
||||
grep -F "$result_store" "$promoted_result_file" >/dev/null || {
|
||||
echo "closure promoted result metadata does not reference the selected result store" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$result_store" "$closure_path/.references" >/dev/null || {
|
||||
echo "closure references do not retain the promoted result store" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$kernel_store" "$promoted_result_file" >/dev/null || {
|
||||
echo "closure promoted result metadata does not reference the promoted kernel store" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$bootloader_store" "$promoted_result_file" >/dev/null || {
|
||||
echo "closure promoted result metadata does not reference the promoted bootloader store" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$result_store" "$store_layout_file" >/dev/null || {
|
||||
echo "store layout metadata does not record the promoted result store" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
kernel_link=$(readlink "$closure_path/boot/kernel/kernel")
|
||||
bootloader_link=$(readlink "$closure_path/boot/loader.efi")
|
||||
[ "$kernel_link" = "$kernel_store/boot/kernel/kernel" ] || {
|
||||
echo "closure kernel link does not target the promoted kernel store" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$bootloader_link" = "$bootloader_store/boot/loader.efi" ] || {
|
||||
echo "closure bootloader link does not target the promoted bootloader store" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
guest_promoted_metadata=$(ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" 'sh -s' <<EOF
|
||||
set -eu
|
||||
[ -f /run/current-system/metadata/promoted-native-build-result.scm ]
|
||||
grep -F '$result_store' /run/current-system/metadata/promoted-native-build-result.scm >/dev/null
|
||||
grep -F '$result_store' /run/current-system/.references >/dev/null
|
||||
kernel_link=$(readlink /run/current-system/boot/kernel/kernel)
|
||||
bootloader_link=$(readlink /run/current-system/boot/loader.efi)
|
||||
[ "$kernel_link" = "$kernel_store/boot/kernel/kernel" ]
|
||||
[ "$bootloader_link" = "$bootloader_store/boot/loader.efi" ]
|
||||
[ -x /bin/sh ]
|
||||
[ -x /usr/sbin/sshd ]
|
||||
printf 'kernel_link=%s\n' "$kernel_link"
|
||||
printf 'bootloader_link=%s\n' "$bootloader_link"
|
||||
printf 'uname=%s\n' "$(uname -sr)"
|
||||
printf 'promoted_result_store=%s\n' '$result_store'
|
||||
EOF
|
||||
)
|
||||
|
||||
guest_kernel_link=$(printf '%s\n' "$guest_promoted_metadata" | sed -n 's/^kernel_link=//p')
|
||||
guest_bootloader_link=$(printf '%s\n' "$guest_promoted_metadata" | sed -n 's/^bootloader_link=//p')
|
||||
guest_uname=$(printf '%s\n' "$guest_promoted_metadata" | sed -n 's/^uname=//p')
|
||||
|
||||
[ "$guest_kernel_link" = "$kernel_store/boot/kernel/kernel" ] || {
|
||||
echo "guest kernel link does not target the promoted kernel store" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$guest_bootloader_link" = "$bootloader_store/boot/loader.efi" ] || {
|
||||
echo "guest bootloader link does not target the promoted bootloader store" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
promotion_metadata=$promotion_metadata
|
||||
inner_metadata=$inner_metadata
|
||||
phase8_metadata=$phase8_metadata
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
result_store=$result_store
|
||||
result_metadata_file=$result_metadata_file
|
||||
executor_kind=$executor_kind
|
||||
executor_name=$executor_name
|
||||
executor_version=$executor_version
|
||||
world_store=$world_store
|
||||
kernel_store=$kernel_store
|
||||
headers_store=$headers_store
|
||||
bootloader_store=$bootloader_store
|
||||
promoted_result_file=$promoted_result_file
|
||||
store_layout_file=$store_layout_file
|
||||
kernel_link=$kernel_link
|
||||
bootloader_link=$bootloader_link
|
||||
guest_kernel_link=$guest_kernel_link
|
||||
guest_bootloader_link=$guest_bootloader_link
|
||||
guest_uname=$guest_uname
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
promoted_native_base_declaration=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase20-promoted-native-base-declaration-xcpng\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"
|
||||
266
tests/system/run-phase20-self-hosted-native-build-xcpng.sh
Executable file
266
tests/system/run-phase20-self-hosted-native-build-xcpng.sh
Executable file
@@ -0,0 +1,266 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/phase20-development-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-phase20-operating-system}
|
||||
root_size=${ROOT_SIZE:-20g}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-phase20-self-hosted-native-build-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
inner_metadata=$workdir/phase20-self-hosted-inner-metadata.txt
|
||||
metadata_file=$workdir/phase20-self-hosted-native-build-xcpng-metadata.txt
|
||||
|
||||
action_cleanup() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap action_cleanup EXIT INT TERM
|
||||
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/inner" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$os_template" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
|
||||
"$repo_root/tests/system/run-phase20-development-environment-xcpng.sh"
|
||||
|
||||
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
|
||||
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
|
||||
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
|
||||
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
|
||||
|
||||
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
|
||||
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
|
||||
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
|
||||
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
guest_build_jobs=${GUEST_BUILD_JOBS:-$(ssh_guest 'sysctl -n hw.ncpu')}
|
||||
case "$guest_build_jobs" in
|
||||
''|*[!0-9]*)
|
||||
echo "invalid guest build job count: $guest_build_jobs" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
ssh_guest '[ -x /usr/local/bin/fruix-build-environment ]'
|
||||
ssh_guest '[ -x /usr/local/bin/fruix-self-hosted-native-build ]'
|
||||
ssh_guest '[ -L /run/current-build ]'
|
||||
ssh_guest '[ -L /usr/include ]'
|
||||
ssh_guest '[ -L /usr/share/mk ]'
|
||||
|
||||
self_hosted_metadata=$(ssh_guest env FRUIX_SELF_HOSTED_NATIVE_BUILD_JOBS="$guest_build_jobs" /usr/local/bin/fruix-self-hosted-native-build)
|
||||
|
||||
run_id=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^run_id=//p')
|
||||
helper_version=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^helper_version=//p')
|
||||
executor_kind=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^executor_kind=//p')
|
||||
executor_name=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^executor_name=//p')
|
||||
executor_version=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^executor_version=//p')
|
||||
build_profile=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^build_profile=//p')
|
||||
source_store=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^source_store=//p')
|
||||
source_root=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^source_root=//p')
|
||||
build_jobs=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^build_jobs=//p')
|
||||
build_common=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^build_common=//p')
|
||||
install_common=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^install_common=//p')
|
||||
build_root=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^build_root=//p')
|
||||
result_root=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^result_root=//p')
|
||||
logdir=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^logdir=//p')
|
||||
status_file=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^status_file=//p')
|
||||
guest_metadata_file=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^metadata_file=//p')
|
||||
promotion_file=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^promotion_file=//p')
|
||||
world_stage=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^world_stage=//p')
|
||||
kernel_stage=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^kernel_stage=//p')
|
||||
world_artifact=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^world_artifact=//p')
|
||||
kernel_artifact=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^kernel_artifact=//p')
|
||||
headers_artifact=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^headers_artifact=//p')
|
||||
bootloader_artifact=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^bootloader_artifact=//p')
|
||||
latest_link=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^latest_link=//p')
|
||||
root_df=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^root_df=//p')
|
||||
build_root_size=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^build_root_size=//p')
|
||||
result_root_size=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^result_root_size=//p')
|
||||
world_artifact_size=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^world_artifact_size=//p')
|
||||
kernel_artifact_size=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^kernel_artifact_size=//p')
|
||||
headers_artifact_size=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^headers_artifact_size=//p')
|
||||
bootloader_artifact_size=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^bootloader_artifact_size=//p')
|
||||
sha_kernel=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^sha_kernel=//p')
|
||||
sha_loader=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^sha_loader=//p')
|
||||
sha_param=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^sha_param=//p')
|
||||
buildworld_tail=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^buildworld_tail=//p')
|
||||
buildkernel_tail=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^buildkernel_tail=//p')
|
||||
installworld_tail=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^installworld_tail=//p')
|
||||
distribution_tail=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^distribution_tail=//p')
|
||||
installkernel_tail=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^installkernel_tail=//p')
|
||||
self_hosted_native_build=$(printf '%s\n' "$self_hosted_metadata" | sed -n 's/^self_hosted_native_build=//p')
|
||||
|
||||
status_value=$(ssh_guest "cat '$status_file'")
|
||||
latest_target=$(ssh_guest "readlink '$latest_link'")
|
||||
ssh_guest "[ -f '$promotion_file' ]"
|
||||
ssh_guest "[ -f '$world_artifact/bin/sh' ]"
|
||||
|
||||
[ "$helper_version" = 5 ] || { echo "unexpected helper version: $helper_version" >&2; exit 1; }
|
||||
[ "$executor_kind" = self-hosted ] || { echo "unexpected executor kind: $executor_kind" >&2; exit 1; }
|
||||
[ "$executor_name" = guest-self-hosted ] || { echo "unexpected executor name: $executor_name" >&2; exit 1; }
|
||||
[ "$executor_version" = 5 ] || { echo "unexpected executor version: $executor_version" >&2; exit 1; }
|
||||
[ "$build_profile" = /run/current-system/build-profile ] || { echo "unexpected build profile: $build_profile" >&2; exit 1; }
|
||||
[ "$build_jobs" = "$guest_build_jobs" ] || { echo "unexpected build job count: $build_jobs" >&2; exit 1; }
|
||||
[ "$status_value" = ok ] || { echo "self-hosted build status is not ok: $status_value" >&2; exit 1; }
|
||||
[ "$latest_target" = "$result_root" ] || { echo "latest link target mismatch: $latest_target" >&2; exit 1; }
|
||||
[ "$self_hosted_native_build" = ok ] || { echo "self-hosted build marker missing" >&2; exit 1; }
|
||||
|
||||
case "$source_store" in
|
||||
/frx/store/*-freebsd-source-*) : ;;
|
||||
*) echo "unexpected source store path: $source_store" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$source_root" in
|
||||
/frx/store/*-freebsd-source-*/*) : ;;
|
||||
*) echo "unexpected source root path: $source_root" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$build_root" in
|
||||
/var/tmp/fruix-self-hosted-native-builds/*) : ;;
|
||||
*) echo "unexpected build root: $build_root" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$result_root" in
|
||||
/var/lib/fruix/native-builds/*) : ;;
|
||||
*) echo "unexpected result root: $result_root" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$latest_link" in
|
||||
/var/lib/fruix/native-builds/latest) : ;;
|
||||
*) echo "unexpected latest link path: $latest_link" >&2; exit 1 ;;
|
||||
esac
|
||||
printf '%s\n' "$run_id" | grep -E '^[0-9]{8}T[0-9]{6}Z$' >/dev/null || {
|
||||
echo "unexpected run id: $run_id" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$sha_kernel" | grep -E '^[0-9a-f]{64}$' >/dev/null || {
|
||||
echo "invalid kernel sha256: $sha_kernel" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$sha_loader" | grep -E '^[0-9a-f]{64}$' >/dev/null || {
|
||||
echo "invalid loader sha256: $sha_loader" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$sha_param" | grep -E '^[0-9a-f]{64}$' >/dev/null || {
|
||||
echo "invalid param.h sha256: $sha_param" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$buildworld_tail" in
|
||||
*'World build completed on'*) : ;;
|
||||
*) echo "buildworld log does not show completion" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$buildkernel_tail" in
|
||||
*'Kernel(s) GENERIC built in'*) : ;;
|
||||
*) echo "buildkernel log does not show successful kernel build" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$installworld_tail" in
|
||||
*'Install world completed in'*) : ;;
|
||||
*) echo "installworld log does not show successful completion" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$installkernel_tail" in
|
||||
*'Install kernel(s) GENERIC completed in'*) : ;;
|
||||
*) echo "installkernel log does not show successful completion" >&2; exit 1 ;;
|
||||
esac
|
||||
printf '%s\n' "$build_common" | grep -F 'TARGET=amd64 TARGET_ARCH=amd64 KERNCONF=GENERIC' >/dev/null || {
|
||||
echo "unexpected build_common flags: $build_common" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$install_common" | grep -F 'DB_FROM_SRC=yes' >/dev/null || {
|
||||
echo "install_common is missing DB_FROM_SRC=yes" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat >"$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_metadata=$inner_metadata
|
||||
phase8_metadata=$phase8_metadata
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
run_id=$run_id
|
||||
helper_version=$helper_version
|
||||
executor_kind=$executor_kind
|
||||
executor_name=$executor_name
|
||||
executor_version=$executor_version
|
||||
build_jobs=$build_jobs
|
||||
build_profile=$build_profile
|
||||
source_store=$source_store
|
||||
source_root=$source_root
|
||||
build_common=$build_common
|
||||
install_common=$install_common
|
||||
build_root=$build_root
|
||||
result_root=$result_root
|
||||
logdir=$logdir
|
||||
status_file=$status_file
|
||||
guest_metadata_file=$guest_metadata_file
|
||||
promotion_file=$promotion_file
|
||||
world_stage=$world_stage
|
||||
kernel_stage=$kernel_stage
|
||||
world_artifact=$world_artifact
|
||||
kernel_artifact=$kernel_artifact
|
||||
headers_artifact=$headers_artifact
|
||||
bootloader_artifact=$bootloader_artifact
|
||||
latest_link=$latest_link
|
||||
latest_target=$latest_target
|
||||
status_value=$status_value
|
||||
root_df=$root_df
|
||||
build_root_size=$build_root_size
|
||||
result_root_size=$result_root_size
|
||||
world_artifact_size=$world_artifact_size
|
||||
kernel_artifact_size=$kernel_artifact_size
|
||||
headers_artifact_size=$headers_artifact_size
|
||||
bootloader_artifact_size=$bootloader_artifact_size
|
||||
sha_kernel=$sha_kernel
|
||||
sha_loader=$sha_loader
|
||||
sha_param=$sha_param
|
||||
buildworld_tail=$buildworld_tail
|
||||
buildkernel_tail=$buildkernel_tail
|
||||
installworld_tail=$installworld_tail
|
||||
distribution_tail=$distribution_tail
|
||||
installkernel_tail=$installkernel_tail
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
self_hosted_native_build=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS phase20-self-hosted-native-build-xcpng\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"
|
||||
@@ -104,11 +104,11 @@ image_size_bytes=$(stat -f '%z' "$disk_image")
|
||||
closure_base=$(basename "$closure_path")
|
||||
|
||||
case "$image_store_path" in
|
||||
/frx/store/*-fruix-bhyve-image-fruix-freebsd) : ;;
|
||||
/frx/store/*-fruix-bhyve-image-*) : ;;
|
||||
*) echo "unexpected image store path: $image_store_path" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$disk_image" in
|
||||
/frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img) : ;;
|
||||
/frx/store/*-fruix-bhyve-image-*/disk.img) : ;;
|
||||
*) echo "unexpected disk image path: $disk_image" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
@@ -142,7 +142,7 @@ if [ -L "$mnt_root/etc/master.passwd" ]; then master_passwd_kind=symlink; elif [
|
||||
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; }
|
||||
grep -E '^hostname=".+"$' "$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; }
|
||||
|
||||
271
tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh
Executable file
271
tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh
Executable file
@@ -0,0 +1,271 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=${PROJECT_ROOT:-$(pwd)}
|
||||
os_template=${OS_TEMPLATE:-$repo_root/tests/system/postphase20-installed-node-operating-system.scm.in}
|
||||
system_name=${SYSTEM_NAME:-postphase20-installed-node-operating-system}
|
||||
result_store=${RESULT_STORE:-/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest}
|
||||
root_size=${ROOT_SIZE:-12g}
|
||||
current_host_name=${CURRENT_HOST_NAME:-fruix-node-current}
|
||||
candidate_host_name=${CANDIDATE_HOST_NAME:-fruix-node-canary}
|
||||
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}
|
||||
cleanup=0
|
||||
|
||||
if [ -n "${WORKDIR:-}" ]; then
|
||||
workdir=$WORKDIR
|
||||
mkdir -p "$workdir"
|
||||
else
|
||||
workdir=$(mktemp -d /tmp/fruix-postphase20-installed-node-xcpng.XXXXXX)
|
||||
cleanup=1
|
||||
fi
|
||||
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
|
||||
cleanup=0
|
||||
fi
|
||||
|
||||
current_os_file=$workdir/current-operating-system.scm
|
||||
candidate_os_file=$workdir/candidate-operating-system.scm
|
||||
inner_metadata=$workdir/postphase20-installed-node-inner-metadata.txt
|
||||
current_build_out=$workdir/current-build.txt
|
||||
candidate_build_out=$workdir/candidate-build.txt
|
||||
reconfigure_out=$workdir/reconfigure.txt
|
||||
rollback_out=$workdir/rollback.txt
|
||||
post_reconfigure_status=$workdir/post-reconfigure-status.txt
|
||||
post_boot_candidate_status=$workdir/post-boot-candidate-status.txt
|
||||
post_boot_rollback_status=$workdir/post-boot-rollback-status.txt
|
||||
metadata_file=$workdir/postphase20-installed-node-build-reconfigure-xcpng-metadata.txt
|
||||
|
||||
cleanup_workdir() {
|
||||
if [ "$cleanup" -eq 1 ]; then
|
||||
rm -rf "$workdir"
|
||||
fi
|
||||
}
|
||||
trap cleanup_workdir EXIT INT TERM
|
||||
|
||||
[ -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; }
|
||||
[ -d "$result_store" ] || { echo "promoted result store does not exist: $result_store" >&2; exit 1; }
|
||||
|
||||
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
|
||||
|
||||
render_os() {
|
||||
output=$1
|
||||
host_name=$2
|
||||
sed \
|
||||
-e "s|__PROMOTED_RESULT_STORE__|$result_store|g" \
|
||||
-e "s|__HOST_NAME__|$host_name|g" \
|
||||
-e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \
|
||||
"$os_template" > "$output"
|
||||
}
|
||||
|
||||
render_os "$current_os_file" "$current_host_name"
|
||||
render_os "$candidate_os_file" "$candidate_host_name"
|
||||
|
||||
KEEP_WORKDIR=1 WORKDIR="$workdir/boot" METADATA_OUT="$inner_metadata" \
|
||||
ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \
|
||||
ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \
|
||||
OS_TEMPLATE="$current_os_file" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \
|
||||
"$repo_root/tests/system/run-phase11-shepherd-pid1-xcpng.sh"
|
||||
|
||||
phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata")
|
||||
closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata")
|
||||
closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata")
|
||||
guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata")
|
||||
vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata")
|
||||
vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata")
|
||||
shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata")
|
||||
sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata")
|
||||
compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata")
|
||||
guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata")
|
||||
activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata")
|
||||
|
||||
[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; }
|
||||
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
|
||||
[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; }
|
||||
[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; }
|
||||
case "$activate_log" in
|
||||
*fruix-activate:done*) : ;;
|
||||
*) echo "activation log does not show success" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
for path in \
|
||||
"$closure_path/metadata/system-declaration.scm" \
|
||||
"$closure_path/metadata/system-declaration-info.scm" \
|
||||
"$closure_path/metadata/system-declaration-system" \
|
||||
"$closure_path/share/fruix/node/scripts/fruix.scm" \
|
||||
"$closure_path/share/fruix/node/modules/fruix/system/freebsd/render.scm" \
|
||||
"$closure_path/share/fruix/node/guix/guix/build/utils.scm"
|
||||
do
|
||||
[ -f "$path" ] || {
|
||||
echo "required installed-node path missing: $path" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
grep -F "$current_host_name" "$closure_path/metadata/system-declaration.scm" >/dev/null || {
|
||||
echo "embedded declaration does not mention current host name" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -F "$system_name" "$closure_path/metadata/system-declaration-system" >/dev/null || {
|
||||
echo "embedded declaration system name is missing" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ssh_guest() {
|
||||
ssh -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
root@"$guest_ip" "$@"
|
||||
}
|
||||
|
||||
scp_guest() {
|
||||
scp -O -i "$root_ssh_private_key_file" \
|
||||
-o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=5 \
|
||||
"$@"
|
||||
}
|
||||
|
||||
wait_for_ssh() {
|
||||
for attempt in $(jot 120 1 120); do
|
||||
if ssh_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
reboot_guest() {
|
||||
ssh_guest 'shutdown -r now >/dev/null 2>&1 || reboot >/dev/null 2>&1 || true' >/dev/null 2>&1 || true
|
||||
sleep 5
|
||||
wait_for_ssh || {
|
||||
echo "guest did not return over SSH after reboot" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
ssh_guest 'sh -s' <<EOF
|
||||
set -eu
|
||||
[ -x /usr/local/bin/fruix ]
|
||||
[ -f /run/current-system/metadata/system-declaration.scm ]
|
||||
[ -f /run/current-system/metadata/system-declaration-info.scm ]
|
||||
[ -f /run/current-system/metadata/system-declaration-system ]
|
||||
[ -f /run/current-system/share/fruix/node/scripts/fruix.scm ]
|
||||
[ -f /run/current-system/share/fruix/node/modules/fruix/system/freebsd/media.scm ]
|
||||
[ -f /run/current-system/share/fruix/node/guix/guix/build/utils.scm ]
|
||||
grep -F '$system_name' /run/current-system/metadata/system-declaration-system >/dev/null
|
||||
EOF
|
||||
|
||||
ssh_guest '/usr/local/bin/fruix system build' > "$current_build_out"
|
||||
current_built_closure=$(sed -n 's/^closure_path=//p' "$current_build_out" | tail -n 1)
|
||||
[ -n "$current_built_closure" ] || { echo "missing closure_path from in-system build output" >&2; exit 1; }
|
||||
case "$current_built_closure" in
|
||||
/frx/store/*-fruix-system-$current_host_name) : ;;
|
||||
*)
|
||||
echo "in-system build of current declaration produced an unexpected closure path: $current_built_closure" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
scp_guest "$candidate_os_file" root@"$guest_ip":/root/candidate.scm >/dev/null
|
||||
ssh_guest "/usr/local/bin/fruix system build /root/candidate.scm --system $system_name" > "$candidate_build_out"
|
||||
candidate_closure=$(sed -n 's/^closure_path=//p' "$candidate_build_out" | tail -n 1)
|
||||
[ -n "$candidate_closure" ] || { echo "missing candidate closure_path from in-system build output" >&2; exit 1; }
|
||||
[ "$candidate_closure" != "$closure_path" ] || {
|
||||
echo "candidate closure unexpectedly matches current closure" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ssh_guest "/usr/local/bin/fruix system reconfigure /root/candidate.scm --system $system_name" > "$reconfigure_out"
|
||||
reconfigure_closure=$(sed -n 's/^reconfigure_closure=//p' "$reconfigure_out" | tail -n 1)
|
||||
reconfigure_current_generation=$(sed -n 's/^current_generation=//p' "$reconfigure_out" | tail -n 1)
|
||||
reconfigure_current_closure=$(sed -n 's/^current_closure=//p' "$reconfigure_out" | tail -n 1)
|
||||
reconfigure_rollback_generation=$(sed -n 's/^rollback_generation=//p' "$reconfigure_out" | tail -n 1)
|
||||
reconfigure_rollback_closure=$(sed -n 's/^rollback_closure=//p' "$reconfigure_out" | tail -n 1)
|
||||
[ "$reconfigure_closure" = "$candidate_closure" ] || { echo "reconfigure closure mismatch" >&2; exit 1; }
|
||||
[ "$reconfigure_current_generation" = 2 ] || { echo "unexpected current generation after reconfigure: $reconfigure_current_generation" >&2; exit 1; }
|
||||
[ "$reconfigure_current_closure" = "$candidate_closure" ] || { echo "unexpected current closure after reconfigure" >&2; exit 1; }
|
||||
[ "$reconfigure_rollback_generation" = 1 ] || { echo "unexpected rollback generation after reconfigure: $reconfigure_rollback_generation" >&2; exit 1; }
|
||||
[ "$reconfigure_rollback_closure" = "$closure_path" ] || { echo "unexpected rollback closure after reconfigure" >&2; exit 1; }
|
||||
ssh_guest '/usr/local/bin/fruix system status' > "$post_reconfigure_status"
|
||||
|
||||
reboot_guest
|
||||
candidate_hostname=$(ssh_guest 'hostname')
|
||||
candidate_run_current=$(ssh_guest 'readlink /run/current-system')
|
||||
ssh_guest '/usr/local/bin/fruix system status' > "$post_boot_candidate_status"
|
||||
[ "$candidate_hostname" = "$candidate_host_name" ] || { echo "unexpected host name after candidate boot: $candidate_hostname" >&2; exit 1; }
|
||||
[ "$candidate_run_current" = "$candidate_closure" ] || { echo "unexpected current closure after candidate boot: $candidate_run_current" >&2; exit 1; }
|
||||
|
||||
ssh_guest '/usr/local/bin/fruix system rollback' > "$rollback_out"
|
||||
rollback_current_generation=$(sed -n 's/^current_generation=//p' "$rollback_out" | tail -n 1)
|
||||
rollback_current_closure=$(sed -n 's/^current_closure=//p' "$rollback_out" | tail -n 1)
|
||||
rollback_rollback_generation=$(sed -n 's/^rollback_generation=//p' "$rollback_out" | tail -n 1)
|
||||
rollback_rollback_closure=$(sed -n 's/^rollback_closure=//p' "$rollback_out" | tail -n 1)
|
||||
[ "$rollback_current_generation" = 1 ] || { echo "unexpected current generation after rollback: $rollback_current_generation" >&2; exit 1; }
|
||||
[ "$rollback_current_closure" = "$closure_path" ] || { echo "unexpected current closure after rollback" >&2; exit 1; }
|
||||
[ "$rollback_rollback_generation" = 2 ] || { echo "unexpected rollback generation after rollback: $rollback_rollback_generation" >&2; exit 1; }
|
||||
[ "$rollback_rollback_closure" = "$candidate_closure" ] || { echo "unexpected rollback closure after rollback" >&2; exit 1; }
|
||||
|
||||
reboot_guest
|
||||
rollback_hostname=$(ssh_guest 'hostname')
|
||||
rollback_run_current=$(ssh_guest 'readlink /run/current-system')
|
||||
ssh_guest '/usr/local/bin/fruix system status' > "$post_boot_rollback_status"
|
||||
[ "$rollback_hostname" = "$current_host_name" ] || { echo "unexpected host name after rollback boot: $rollback_hostname" >&2; exit 1; }
|
||||
[ "$rollback_run_current" = "$closure_path" ] || { echo "unexpected current closure after rollback boot: $rollback_run_current" >&2; exit 1; }
|
||||
|
||||
cat > "$metadata_file" <<EOF
|
||||
workdir=$workdir
|
||||
inner_metadata=$inner_metadata
|
||||
phase8_metadata=$phase8_metadata
|
||||
closure_path=$closure_path
|
||||
closure_base=$closure_base
|
||||
vm_id=$vm_id
|
||||
vdi_id=$vdi_id
|
||||
guest_ip=$guest_ip
|
||||
root_size=$root_size
|
||||
result_store=$result_store
|
||||
current_host_name=$current_host_name
|
||||
candidate_host_name=$candidate_host_name
|
||||
current_build_out=$current_build_out
|
||||
current_built_closure=$current_built_closure
|
||||
candidate_build_out=$candidate_build_out
|
||||
candidate_closure=$candidate_closure
|
||||
reconfigure_out=$reconfigure_out
|
||||
reconfigure_closure=$reconfigure_closure
|
||||
reconfigure_current_generation=$reconfigure_current_generation
|
||||
reconfigure_current_closure=$reconfigure_current_closure
|
||||
reconfigure_rollback_generation=$reconfigure_rollback_generation
|
||||
reconfigure_rollback_closure=$reconfigure_rollback_closure
|
||||
candidate_hostname=$candidate_hostname
|
||||
candidate_run_current=$candidate_run_current
|
||||
rollback_out=$rollback_out
|
||||
rollback_current_generation=$rollback_current_generation
|
||||
rollback_current_closure=$rollback_current_closure
|
||||
rollback_rollback_generation=$rollback_rollback_generation
|
||||
rollback_rollback_closure=$rollback_rollback_closure
|
||||
rollback_hostname=$rollback_hostname
|
||||
rollback_run_current=$rollback_run_current
|
||||
boot_backend=xcp-ng-xo-cli
|
||||
init_mode=shepherd-pid1
|
||||
installed_node_build_reconfigure=ok
|
||||
EOF
|
||||
|
||||
if [ -n "$metadata_target" ]; then
|
||||
mkdir -p "$(dirname "$metadata_target")"
|
||||
cp "$metadata_file" "$metadata_target"
|
||||
fi
|
||||
|
||||
printf 'PASS postphase20-installed-node-build-reconfigure-xcpng\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"
|
||||
Reference in New Issue
Block a user