Compare commits

...

3 Commits

Author SHA1 Message Date
7db5c76541 README 2026-04-02 10:39:33 +02:00
b037427c22 Add fruix system command for FreeBSD 2026-04-02 10:31:24 +02:00
a69864cc0c docs: LLM_UI 2026-04-02 10:25:56 +02:00
7 changed files with 1049 additions and 0 deletions

View File

@@ -1,3 +1,5 @@
# Fruix
Fruix is a Guix-like system running on FreeBSD but not GNU, using Shepherd, building GNU packages, with a BSD userland, and a functional store similar to Nix but not Nix.
Fruix is a system where everything that exists on the machine exists for a reason that can be explained.

51
bin/fruix Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/sh
set -eu
project_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
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}
script=$project_root/scripts/fruix.scm
if [ ! -x "$guile_bin" ]; then
echo "Guile binary is not executable: $guile_bin" >&2
exit 1
fi
ensure_built() {
if [ ! -d "$guile_extra_prefix/share/guile/site" ] || \
! GUILE_LOAD_PATH="$guile_extra_prefix/share/guile/site/3.0${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \
GUILE_LOAD_COMPILED_PATH="$guile_extra_prefix/lib/guile/3.0/site-ccache${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" \
GUILE_EXTENSIONS_PATH="$guile_extra_prefix/lib/guile/3.0/extensions${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" \
LD_LIBRARY_PATH="$guile_extra_prefix/lib:/tmp/guile-freebsd-validate-install/lib:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
"$guile_bin" -c '(catch #t (lambda () (use-modules (fibers)) (display "ok") (newline)) (lambda _ (display "missing") (newline)))' | grep -qx ok; then
METADATA_OUT= ENV_OUT= "$project_root/tests/shepherd/build-local-guile-fibers.sh"
fi
if [ ! -x "$shepherd_prefix/bin/shepherd" ] || [ ! -x "$shepherd_prefix/bin/herd" ]; then
METADATA_OUT= ENV_OUT= GUILE_EXTRA_PREFIX="$guile_extra_prefix" "$project_root/tests/shepherd/build-local-shepherd.sh"
fi
}
ensure_built
guile_prefix=$(CDPATH= cd -- "$(dirname "$guile_bin")/.." && pwd)
guile_lib_dir=$guile_prefix/lib
if [ -n "${GUILE_LOAD_PATH:-}" ]; then
guile_load_path="$project_root/modules:$guix_source_dir:$GUILE_LOAD_PATH"
else
guile_load_path="$project_root/modules:$guix_source_dir"
fi
exec env \
GUILE_AUTO_COMPILE=0 \
GUILE_LOAD_PATH="$guile_load_path" \
LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
GUILE_PREFIX="$guile_prefix" \
GUILE_EXTRA_PREFIX="$guile_extra_prefix" \
SHEPHERD_PREFIX="$shepherd_prefix" \
GUIX_SOURCE_DIR="$guix_source_dir" \
FRUIX_PROJECT_ROOT="$project_root" \
"$guile_bin" --no-auto-compile -s "$script" "$@"

466
docs/LLM_UI.md Normal file
View File

@@ -0,0 +1,466 @@
Dual output is exactly the right instinct. If you want a system to be self-documenting for both humans and LLMs, the key is this:
> **Do not treat documentation as prose attached afterward. Treat it as structured semantic data emitted by the same machinery that defines the system.**
Otherwise the docs drift, the machine lies, the humans improvise, and the model confidently invents nonsense. A beloved industry tradition.
## My preference, if Fruix wanted to be maximally legible to me
I would want **three layers at once** for every meaningful object in the system:
### 1. Operational output
The normal thing a human expects.
Example:
```text
$ fruix service status sshd
running since 2026-04-02T09:14:33Z
pid 812
generation 42
store item /frx/store/...-openssh-service
```
### 2. Structured machine-readable output
Explicit, stable, schema-like, with no pretty nonsense.
Example:
```json
{
"kind": "service-status",
"name": "sshd",
"state": "running",
"since": "2026-04-02T09:14:33Z",
"pid": 812,
"generation": 42,
"store_path": "/frx/store/...-openssh-service",
"definition_ref": "services.sshd"
}
```
### 3. Embedded semantic help/doc layer
Not a wall of text. Short, attached meaning.
Example:
```json
{
"doc": {
"summary": "OpenSSH daemon providing remote login access.",
"purpose": "Accepts inbound SSH connections for remote administration.",
"inputs": ["host keys", "network", "authorized keys config"],
"depends_on": ["network-online", "host-keys"],
"used_by": ["admin access", "remote deployment"],
"failure_modes": [
"missing host keys",
"port unavailable",
"invalid configuration"
],
"see_also": [
"fruix service logs sshd",
"fruix config explain services.sshd"
]
}
}
```
That combination is ideal. The command answers the immediate question, but also emits enough structure that an LLM can keep asking intelligent follow-ups without having to guess what “sshd” even is in Fruix-world.
---
## The most important design principle
Every object in Fruix should ideally answer these questions about itself:
* what are you?
* why do you exist?
* what created you?
* what do you depend on?
* what depends on you?
* how do I inspect you?
* how do I change you?
* what breaks if you fail?
* where is your definition?
* what commands are relevant next?
That is the difference between “a system with documentation” and “a system that can explain itself.”
## The command model I would love
I would strongly prefer **one canonical plain output mode**, plus **a structured explain mode**, rather than mixing prose into every default command.
So:
```text
fruix service status sshd
fruix service status sshd --json
fruix service explain sshd
```
Or maybe:
```text
fruix inspect service sshd
fruix inspect service sshd --json
fruix explain service sshd
```
This separation matters.
### Why not always dual-output in the same stream?
Because it becomes annoying for humans, brittle for scripts, and ugly for terminals. People say they want hybrid output until they actually have to read it every day.
Better pattern:
* default output for operators
* stable machine output for tooling/LLMs
* explicit explanation output for context
That said, you *can* still have a dual channel if done cleanly.
---
## My favorite model: result + attached explain block
Something like this:
```text
$ fruix service status sshd --with-doc
sshd: running
pid: 812
since: 2026-04-02T09:14:33Z
generation: 42
Explanation:
SSH daemon for remote login and administration.
Depends on network-online and host key material.
Configured from services.sshd in the system definition.
Relevant commands:
fruix service logs sshd
fruix config explain services.sshd
fruix service restart sshd
```
That is excellent for humans and still digestible for an LLM if the formatting is predictable.
But for automation, I would still want:
```text
fruix service status sshd --json
fruix service explain sshd --json
```
So in practice Id want **two parallel interfaces**, not one muddled one.
---
## What should be self-documenting in Fruix?
Not just commands. The **actual nouns** of the system.
### 1. Store paths
A store item should be explainable.
```text
fruix store explain /frx/store/845bd...-freebsd-bash-5.3.9
```
Should answer:
* package/runtime/component name
* version
* origin
* build inputs
* purpose
* references
* whether it is part of current system
* whether it is GC-rooted
* whether it is user-requested or dependency-only
### 2. Services
Explain:
* purpose
* definition origin
* dependencies
* restart policy
* logs
* ports/files touched
* activation behavior
### 3. System definitions
Explain:
* what modules/options contributed
* what options are set
* defaults vs overrides
* resulting closures
* affected services
* resulting boot entries/generations
### 4. Configuration options
This one matters a lot.
A good system should let me ask:
```text
fruix config explain services.sshd.enable
fruix config explain system.network.hostname
fruix config search ssh
```
And get:
* type
* default
* current value
* description
* examples
* constraints
* source module
* related options
This is one of the best things NixOS/Guix-style systems can do, and Fruix should absolutely lean into it.
### 5. Commands themselves
CLI subcommands should be introspectable too.
```text
fruix help gc
fruix help gc --json
fruix explain command gc
```
Because LLMs are often dropped into a black box and forced to infer the command surface. A command that can describe itself structurally is vastly easier to use safely.
---
## What would help me most as an LLM?
If Fruix wanted to be unusually legible to a model, Id want these properties.
### Stable schemas
If `--json` exists, make it boring and stable. No whimsical field names, no gratuitous nesting, no surprise changes. Humans love creative naming, then wonder why tooling breaks.
### Uniform “kind” fields
Every structured output should say what it is.
```json
{ "kind": "service-status", ... }
{ "kind": "config-option", ... }
{ "kind": "store-item", ... }
{ "kind": "system-generation", ... }
```
This helps tremendously.
### Embedded references
Objects should point to related objects.
For example, service status should include:
* definition reference
* related config path
* related store path
* dependency names
This lets a model traverse the system instead of hallucinating relationships.
### Short summaries written for machines and humans
Not marketing copy. Not essay paragraphs. Tight explanatory text.
Good:
> "Enables the OpenSSH daemon for inbound remote shell access."
Bad:
> "This module offers a powerful and flexible way to control secure access to your Fruix machine."
Nobody needs brochure language except people trying to justify headcount.
### Explicit provenance
For anything important, say where it came from:
* built from which derivation
* declared in which module
* set by user vs default
* inherited from which profile/system generation
That makes the system explainable instead of mystical.
---
## I would absolutely add a “why” command
This is the killer feature.
Examples:
```text
fruix why /frx/store/...-freebsd-bash-5.3.9
fruix why service sshd
fruix why config services.sshd.enable
```
Answers like:
```text
freebsd-bash-5.3.9 is present because:
- required by fruix-system-fruix-freebsd generation 42
- referenced by interactive-shell-runtime
- pulled in by admin profile
```
Or:
```text
services.sshd.enable is true because:
- set explicitly in hosts/fruix-freebsd.scm
- module openssh-service.scm contributes service definition
```
This is huge for both humans and LLMs. “Why is this here?” is half of systems debugging.
---
## My preferred architecture for integrated docs
I would not store docs only in markdown files. I would make them part of the typed object/module/option definitions themselves.
For each module/option/service/package, attach fields like:
* `summary`
* `description`
* `type`
* `default`
* `examples`
* `see-also`
* `danger-level`
* `depends-on`
* `used-by`
* `introduced-in`
* `notes`
Then generate from that:
* terminal help
* handbook pages
* man pages
* JSON explain output
* web docs
* maybe even shell completions with hints
That way there is **one source of truth**, many renderings.
This is the real win.
---
## A concrete Fruix interaction model Id love
Something like this command family:
```text
fruix inspect <object> [name]
fruix explain <object> [name]
fruix search <term>
fruix why <object> [name]
fruix graph <object> [name]
```
Examples:
```text
fruix inspect service sshd
fruix explain service sshd
fruix why service sshd
fruix graph service sshd
fruix inspect store /frx/store/...-freebsd-bash-5.3.9
fruix explain store /frx/store/...-freebsd-bash-5.3.9
fruix why store /frx/store/...-freebsd-bash-5.3.9
fruix inspect config services.sshd.enable
fruix explain config services.sshd.enable
fruix why config services.sshd.enable
```
That is clean, memorable, and composable.
---
## If you insist on dual output in one invocation
Then I would make it a structured container, not mixed prose spaghetti.
Maybe:
```text
fruix service status sshd --format annotated
```
Output:
```yaml
result:
name: sshd
state: running
pid: 812
since: 2026-04-02T09:14:33Z
generation: 42
doc:
summary: OpenSSH daemon for remote administration.
depends_on:
- network-online
- host-keys
config_path: services.sshd
see_also:
- fruix service logs sshd
- fruix config explain services.sshd
```
YAML is readable enough for humans and parseable enough for tools. Though for machine use Id still pick JSON.
---
## The single biggest preference of all
If I had to choose only one thing, it would be this:
> Every meaningful Fruix object should be introspectable by name or path, with both a stable machine schema and a concise semantic explanation.
That alone would make the system unusually legible to me, to users, and to future maintainers who have not yet been spiritually damaged by it.
## My concrete recommendation
Build this into Fruix from the start:
* `--json` on all important commands
* `explain` subcommands for commands, config, services, store items, generations
* `why` subcommands for dependency/provenance tracing
* structured doc fields embedded in module and option definitions
* generated docs from the same source
* concise summaries, not essay sludge
That is the sweet spot. Self-documenting, machine-friendly, human-usable, and hard to let drift.
If Fruix gets that right, an LLM dropped cold into the system could become useful very quickly instead of fumbling around like a junior admin in an unfamiliar datacenter.

View File

@@ -2291,3 +2291,65 @@ Next recommended step:
- login-class warnings around `daemon`
- the `gpart: Unknown command: show` rc noise
- residual syslog/cron/runtime polish issues where they still matter
## 2026-04-02 — Phase 10.1: added a real `fruix system` command
Completed work:
- started Optional Phase 10 with the first user-facing Fruix system-management command surface
- wrote the subphase report:
- `docs/reports/phase10-fruix-system-command-freebsd.md`
- added a real CLI wrapper:
- `bin/fruix`
- added the corresponding Guile entry point:
- `scripts/fruix.scm`
- added a dedicated validation harness:
- `tests/system/run-phase10-fruix-system-command.sh`
What the new command does:
- exposes declarative FreeBSD system artifact generation through a Fruix CLI instead of only phase-specific harness scripts
- currently supports:
- `fruix system build OS-FILE`
- `fruix system image OS-FILE`
- `fruix system rootfs OS-FILE ROOTFS-DIR`
- currently supports options:
- `--system NAME`
- `--store DIR`
- `--disk-capacity SIZE`
- `--rootfs DIR`
- `--help`
- loads an operating-system object from a Scheme file, validates it, and dispatches to:
- `materialize-operating-system`
- `materialize-rootfs`
- `materialize-bhyve-image`
- emits machine-readable `key=value` metadata for the produced artifacts
Important findings:
- the project already had the core declarative system machinery by the end of Phase 9; the missing piece here was a direct Fruix operator interface to that machinery
- a small dedicated wrapper in this repo is currently the most practical way to expose that functionality without waiting for deeper upstream command-framework integration
- keeping the command aligned with the already validated local FreeBSD Guile / Fibers / Shepherd toolchain avoids introducing a second, divergent runtime path
Validation:
- `tests/system/run-phase10-fruix-system-command.sh` passes
- passing run workdir:
- `/tmp/phase10-fruix-cmd-1775117490`
- the test validated that:
- `fruix system build` materializes a closure under `/frx/store/*-fruix-system-fruix-freebsd`
- the produced closure contains the activation path
- `fruix system image` materializes a disk image under `/frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img`
- the command returns expected metadata fields for both actions
Current assessment:
- Fruix now has a real user-facing system command in this repo, which is a concrete step from “prototype scripts” toward “OS tooling”
- this does not replace all earlier harnesses yet, but it establishes `fruix system` as the new canonical interface for system closure/rootfs/image materialization work
- system/image creation still typically requires `sudo` because the command writes into `/frx/store` and uses privileged image-building operations
Next recommended step:
1. continue Phase 10 by making more of the existing system workflows call `bin/fruix` directly instead of bespoke phase scripts
2. reduce the current runtime compatibility shims for locally built Guile / Shepherd prefixes and move toward a more native store-path-aware Fruix runtime arrangement
3. consider adding the next operator-facing subcommand on top of the now-working image path, such as a `vm`/deploy-oriented flow for the active XCP-ng workflow

View File

@@ -0,0 +1,149 @@
# Phase 10.1: Add a real `fruix system` command for FreeBSD system artifacts
Date: 2026-04-02
## Goal
Start Optional Phase 10 by replacing one of the remaining prototype-only workflows with a real user-facing Fruix command.
The concrete target for this subphase was:
- expose the declarative FreeBSD operating-system machinery through a true `fruix` CLI entry point,
- stop relying solely on ad hoc phase-specific harness scripts for closure/image materialization,
- and validate that the new command can build the existing declarative system outputs.
## Result
A minimal user-facing `fruix` command now exists in this repository.
New entry points:
- `bin/fruix`
- `scripts/fruix.scm`
Supported commands in this first cut:
- `fruix system build OS-FILE`
- `fruix system image OS-FILE`
- `fruix system rootfs OS-FILE ROOTFS-DIR`
Supported options:
- `--system NAME`
- `--store DIR`
- `--disk-capacity SIZE`
- `--rootfs DIR`
- `--help`
The command loads a declarative operating-system definition from a Scheme file, resolves the selected operating-system binding, and then calls the existing Fruix FreeBSD system materializers directly:
- `materialize-operating-system`
- `materialize-rootfs`
- `materialize-bhyve-image`
The command prints machine-readable `key=value` metadata for the produced artifacts.
## Why this matters
Up through Phase 9, the project had already achieved a real booted Fruix guest, but the operator-facing path for producing those artifacts still lived mostly in phase-specific test harnesses such as:
- `tests/system/run-phase7-system-closure.sh`
- `tests/system/run-phase8-system-image.sh`
- `tests/system/run-phase9-xcpng-boot.sh`
Those were valuable validation tools, but they were not yet a real Fruix interface.
This subphase moves the project one step closer to a genuine OS workflow by making system artifact generation available through a Fruix CLI surface.
## Implementation details
### `bin/fruix`
Added a shell wrapper that:
- locates the repository root,
- locates the Guile and Guile-extra validation prefixes already used elsewhere in the project,
- ensures the required local Guile/Fibers/Shepherd pieces exist,
- prepares `GUILE_LOAD_PATH` for:
- `modules/`
- `~/repos/guix`
- and runs the Guile entry point.
This keeps the new command aligned with the currently validated local FreeBSD Guile toolchain rather than inventing a separate runtime path.
### `scripts/fruix.scm`
Added the actual CLI implementation.
It currently:
- parses `fruix system ...` actions,
- supports `--help`,
- loads an operating-system definition file,
- resolves the selected OS variable via `--system NAME` or a small fallback list of conventional names,
- validates the operating-system object,
- dispatches to the existing FreeBSD system materializers,
- and emits stable `key=value` metadata.
### `tests/system/run-phase10-fruix-system-command.sh`
Added a dedicated validation harness for the new command.
The test verifies that:
1. `fruix system build` materializes a closure under `/frx/store`
2. the returned closure contains the generated activation path
3. `fruix system image` materializes a disk image under `/frx/store`
4. the command emits the expected metadata fields for both operations
## Validation
Successful validation run:
- `PASS phase10-fruix-system-command`
- workdir: `/tmp/phase10-fruix-cmd-1775117490`
The new command successfully produced both:
- a Fruix system closure path under `/frx/store/*-fruix-system-fruix-freebsd`
- a Fruix image path under `/frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img`
Example usage:
```sh
sudo env HOME="$HOME" ./bin/fruix system build \
tests/system/phase7-minimal-operating-system.scm \
--system phase7-operating-system
```
```sh
sudo env HOME="$HOME" ./bin/fruix system image \
tests/system/phase7-minimal-operating-system.scm \
--system phase7-operating-system \
--disk-capacity 5g
```
## Current limitations
This is intentionally a first Phase 10 step, not the final Fruix command surface.
Notable current limitations:
- system/image materialization still expects the currently validated local Guile/Shepherd build prefixes
- writing into `/frx/store` and building disk images still typically requires `sudo`
- the command currently targets only the FreeBSD system prototype path already implemented in `modules/fruix/system/freebsd.scm`
- it does not yet integrate with the larger upstream Guix command framework; it is a Fruix-native CLI wrapper in this repo
## Assessment
This subphase successfully replaces an important transitional layer with a more OS-like Fruix interface.
The project can now say not only that it can declaratively define and boot a Fruix FreeBSD system, but also that it has a direct user-facing command for materializing the resulting system artifacts.
## Recommended next step
Continue Phase 10 by reducing another transitional seam behind `fruix system`, most likely one of:
1. add a `fruix system vm` / deploy-oriented flow on top of the now-working image path
2. replace the current compatibility symlink handling for locally built Guile/Shepherd prefixes with a more native store-path-aware runtime arrangement
3. factor the phase-specific system test harnesses to call `bin/fruix` directly where practical, so the command becomes the canonical operator path rather than just an additional wrapper

217
scripts/fruix.scm Normal file
View File

@@ -0,0 +1,217 @@
#!/tmp/guile-freebsd-validate-install/bin/guile -s
!#
(use-modules (fruix system freebsd)
(ice-9 format)
(ice-9 match)
(srfi srfi-1)
(srfi srfi-13))
(define (usage code)
(format (if (= code 0) #t (current-error-port))
"Usage: fruix system ACTION OS-FILE [OPTIONS]\n\
\n\
Actions:\n\
build Materialize the Fruix system closure in /frx/store.\n\
image Materialize the Fruix disk image in /frx/store.\n\
rootfs Materialize a rootfs tree at --rootfs DIR or ROOTFS-DIR.\n\
\n\
Options:\n\
--system NAME Scheme variable holding the operating-system object.\n\
--store DIR Store directory to use (default: /frx/store).\n\
--disk-capacity SIZE Disk capacity for 'image' (example: 30g).\n\
--rootfs DIR Rootfs target for 'rootfs'.\n\
--help Show this help.\n")
(exit code))
(define (option-value arg prefix)
(and (string-prefix? prefix arg)
(substring arg (string-length prefix))))
(define (stringify value)
(cond ((string? value) value)
((symbol? value) (symbol->string value))
((number? value) (number->string value))
((boolean? value) (if value "true" "false"))
(else (call-with-output-string (lambda (port) (write value port))))))
(define (emit-metadata fields)
(for-each (lambda (field)
(format #t "~a=~a~%" (car field) (stringify (cdr field))))
fields))
(define (lookup-bound-value module symbol)
(let ((var (module-variable module symbol)))
(and var (variable-ref var))))
(define candidate-operating-system-symbols
'(operating-system
phase10-operating-system
phase9-operating-system
phase8-operating-system
phase7-operating-system
default-operating-system
os))
(define (resolve-operating-system-symbol module requested)
(or requested
(find (lambda (symbol)
(let ((value (lookup-bound-value module symbol)))
(and value (operating-system? value))))
candidate-operating-system-symbols)
(error "could not infer operating-system variable; use --system NAME")))
(define (load-operating-system-from-file file requested-symbol)
(unless (file-exists? file)
(error "operating-system file does not exist" file))
(primitive-load file)
(let* ((module (current-module))
(symbol (resolve-operating-system-symbol module requested-symbol))
(value (lookup-bound-value module symbol)))
(unless (and value (operating-system? value))
(error "resolved variable is not an operating-system" symbol))
(validate-operating-system value)
(values value symbol)))
(define (parse-arguments argv)
(match argv
((_)
(usage 1))
((_ "--help")
(usage 0))
((_ "help")
(usage 0))
((_ "system" "--help")
(usage 0))
((_ "system" action . rest)
(let loop ((args rest)
(positional '())
(system-name #f)
(store-dir "/frx/store")
(disk-capacity #f)
(rootfs #f))
(match args
(()
(let ((positional (reverse positional)))
`((action . ,action)
(positional . ,positional)
(system-name . ,system-name)
(store-dir . ,store-dir)
(disk-capacity . ,disk-capacity)
(rootfs . ,rootfs))))
(("--help")
(usage 0))
(((? (lambda (arg) (string-prefix? "--system=" arg)) arg) . tail)
(loop tail positional (option-value arg "--system=") store-dir disk-capacity rootfs))
(("--system" value . tail)
(loop tail positional value store-dir disk-capacity rootfs))
(((? (lambda (arg) (string-prefix? "--store=" arg)) arg) . tail)
(loop tail positional system-name (option-value arg "--store=") disk-capacity rootfs))
(("--store" value . tail)
(loop tail positional system-name value disk-capacity rootfs))
(((? (lambda (arg) (string-prefix? "--disk-capacity=" arg)) arg) . tail)
(loop tail positional system-name store-dir (option-value arg "--disk-capacity=") rootfs))
(("--disk-capacity" value . tail)
(loop tail positional system-name store-dir value rootfs))
(((? (lambda (arg) (string-prefix? "--rootfs=" arg)) arg) . tail)
(loop tail positional system-name store-dir disk-capacity (option-value arg "--rootfs=")))
(("--rootfs" value . tail)
(loop tail positional system-name store-dir disk-capacity value))
(((? (lambda (arg) (string-prefix? "--" arg)) arg) . _)
(error "unknown option" arg))
((arg . tail)
(loop tail (cons arg positional) system-name store-dir disk-capacity rootfs)))))
((_ . _)
(usage 1))))
(define (main argv)
(let* ((parsed (parse-arguments argv))
(action (assoc-ref parsed 'action))
(positional (assoc-ref parsed 'positional))
(store-dir (assoc-ref parsed 'store-dir))
(disk-capacity (assoc-ref parsed 'disk-capacity))
(rootfs-opt (assoc-ref parsed 'rootfs))
(system-name (assoc-ref parsed 'system-name))
(requested-symbol (and system-name (string->symbol system-name))))
(cond
((member action '("build" "image" "rootfs")) #t)
(else (error "unknown system action" action)))
(let* ((os-file (match positional
((file . _) file)
(() (error "missing operating-system file argument"))))
(rootfs (or rootfs-opt
(and (string=? action "rootfs")
(match positional
((_ dir) dir)
((_ _ dir . _) dir)
(_ #f))))))
(call-with-values
(lambda ()
(load-operating-system-from-file os-file requested-symbol))
(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")))
(cond
((string=? action "build")
(let* ((result (materialize-operating-system os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(closure-path (assoc-ref result 'closure-path))
(generated-files (assoc-ref result 'generated-files))
(references (assoc-ref result 'references)))
(emit-metadata
`((action . "build")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(closure_path . ,closure-path)
(kernel_store . ,(assoc-ref result 'kernel-store))
(bootloader_store . ,(assoc-ref result 'bootloader-store))
(guile_store . ,(assoc-ref result 'guile-store))
(guile_extra_store . ,(assoc-ref result 'guile-extra-store))
(shepherd_store . ,(assoc-ref result 'shepherd-store))
(generated_file_count . ,(length generated-files))
(reference_count . ,(length references))))))
((string=? action "rootfs")
(unless rootfs
(error "rootfs action requires ROOTFS-DIR or --rootfs DIR"))
(let ((result (materialize-rootfs os rootfs
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix)))
(emit-metadata
`((action . "rootfs")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(rootfs . ,(assoc-ref result 'rootfs))
(closure_path . ,(assoc-ref result 'closure-path))
(ready_marker . ,(assoc-ref result 'ready-marker))
(rc_script . ,(assoc-ref result 'rc-script))))))
((string=? action "image")
(let* ((result (materialize-bhyve-image os
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:disk-capacity disk-capacity))
(image-spec (assoc-ref result 'image-spec))
(store-items (assoc-ref result 'store-items)))
(emit-metadata
`((action . "image")
(os_file . ,os-file)
(system_variable . ,resolved-symbol)
(store_dir . ,store-dir)
(disk_capacity . ,(assoc-ref image-spec 'disk-capacity))
(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))
(closure_path . ,(assoc-ref result 'closure-path))
(store_item_count . ,(length store-items)))))))))))))
(main (command-line))

View File

@@ -0,0 +1,102 @@
#!/bin/sh
set -eu
project_root=${PROJECT_ROOT:-$(pwd)}
os_file=${OS_FILE:-$project_root/tests/system/phase7-minimal-operating-system.scm}
system_name=${SYSTEM_NAME:-phase7-operating-system}
store_dir=${STORE_DIR:-/frx/store}
disk_capacity=${DISK_CAPACITY:-5g}
metadata_target=${METADATA_OUT:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase10-system-command.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
build_out=$workdir/build.txt
image_out=$workdir/image.txt
metadata_file=$workdir/phase10-fruix-system-command-metadata.txt
sudo env \
HOME="$HOME" \
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}" \
"$project_root/bin/fruix" system build "$os_file" --system "$system_name" --store "$store_dir" >"$build_out"
closure_path=$(sed -n 's/^closure_path=//p' "$build_out")
generated_file_count=$(sed -n 's/^generated_file_count=//p' "$build_out")
reference_count=$(sed -n 's/^reference_count=//p' "$build_out")
case "$closure_path" in
/frx/store/*-fruix-system-fruix-freebsd) : ;;
*) echo "unexpected closure path: $closure_path" >&2; exit 1 ;;
esac
[ -x "$closure_path/activate" ] || { echo "missing activate script in closure" >&2; exit 1; }
[ -n "$generated_file_count" ] || { echo "missing generated_file_count" >&2; exit 1; }
[ -n "$reference_count" ] || { echo "missing reference_count" >&2; exit 1; }
sudo env \
HOME="$HOME" \
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}" \
"$project_root/bin/fruix" system image "$os_file" --system "$system_name" --store "$store_dir" --disk-capacity "$disk_capacity" >"$image_out"
image_store_path=$(sed -n 's/^image_store_path=//p' "$image_out")
disk_image=$(sed -n 's/^disk_image=//p' "$image_out")
disk_capacity_reported=$(sed -n 's/^disk_capacity=//p' "$image_out")
store_item_count=$(sed -n 's/^store_item_count=//p' "$image_out")
case "$image_store_path" in
/frx/store/*-fruix-bhyve-image-fruix-freebsd) : ;;
*) 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) : ;;
*) echo "unexpected disk image path: $disk_image" >&2; exit 1 ;;
esac
[ -f "$disk_image" ] || { echo "missing disk image" >&2; exit 1; }
[ -n "$disk_capacity_reported" ] || { echo "missing disk capacity report" >&2; exit 1; }
[ -n "$store_item_count" ] || { echo "missing store item count" >&2; exit 1; }
cat >"$metadata_file" <<EOF
workdir=$workdir
os_file=$os_file
system_name=$system_name
store_dir=$store_dir
closure_path=$closure_path
generated_file_count=$generated_file_count
reference_count=$reference_count
image_store_path=$image_store_path
disk_image=$disk_image
disk_capacity=$disk_capacity_reported
store_item_count=$store_item_count
EOF
if [ -n "$metadata_target" ]; then
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase10-fruix-system-command\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