Compare commits
3 Commits
e933bf3fd1
...
7db5c76541
| Author | SHA1 | Date | |
|---|---|---|---|
| 7db5c76541 | |||
| b037427c22 | |||
| a69864cc0c |
@@ -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
51
bin/fruix
Executable 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
466
docs/LLM_UI.md
Normal 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 I’d 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, I’d 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 I’d 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 I’d 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.
|
||||
@@ -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
|
||||
|
||||
149
docs/reports/phase10-fruix-system-command-freebsd.md
Normal file
149
docs/reports/phase10-fruix-system-command-freebsd.md
Normal 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
217
scripts/fruix.scm
Normal 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))
|
||||
102
tests/system/run-phase10-fruix-system-command.sh
Executable file
102
tests/system/run-phase10-fruix-system-command.sh
Executable 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
|
||||
Reference in New Issue
Block a user