update docs

This commit is contained in:
2026-09-22 13:23:34 +08:00
parent 99bc3d15c5
commit 8a4788fca8
126 changed files with 7198 additions and 2425 deletions
+5 -3
View File
@@ -1,6 +1,6 @@
# FDS/OS implementation scope
Read `docs/master-plan.md` and `docs/build-host.md` before changing the build.
Read `docs/developer/master-plan.md` and `docs/developer/build-host.md` before changing the build.
The user has explicitly authorized implementation through M12. Proceed through
the remaining milestones in order, keeping the repository buildable and recording
actual acceptance evidence. Defer tests that require the physical Raspberry Pi
@@ -34,10 +34,12 @@ claim Pi boot or physical power-cycle recovery is verified.
# Workstation software and emulator follow-up
Read `docs/workstation.md`, `docs/software-format.md` and
`docs/workstation-tooling-plan.md` for this extension. Software builds and new
`docs/developer/workstation-tooling-plan.md` for this extension. Software builds and new
PROGRAM cartridge creation run on generic Linux workstations. The public native
Clap tools are `fds-cartridge` and `fds-emulator`. Software images have GPT
metadata partition 1 plus m EROFS payload partitions holding xz tarballs. Keep
metadata partition 1 plus m EROFS payload partitions containing installed Void
package trees. Build source templates with xbps-src on the workstation; do not
create new xz software bundles or extract programs at guest launch. Keep
legacy reading and base Dasung integration. Do not mutate the frozen 0.1.0 release
or treat its historical acceptance as evidence for changed sources. Validate
with `make workstation-test` and `make emulator-test` plus relevant guest checks.
Generated
+67
View File
@@ -308,6 +308,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fds-boottrace"
version = "0.1.0"
@@ -364,6 +374,17 @@ dependencies = [
"toml",
]
[[package]]
name = "fds-control"
version = "0.1.0"
dependencies = [
"clap",
"fds-common",
"libc",
"serde_json",
"x11rb",
]
[[package]]
name = "fds-release"
version = "0.1.0"
@@ -459,6 +480,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix",
"windows-link",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -520,6 +551,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
@@ -602,6 +639,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "semver"
version = "1.0.28"
@@ -833,6 +883,23 @@ dependencies = [
"memchr",
]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "zeroize"
version = "1.9.0"
+1
View File
@@ -7,6 +7,7 @@ members = [
"rust/fds-release",
"rust/fds-software",
"rust/fds-workstation",
"rust/fds-control",
"tests/helpers",
]
+15 -2
View File
@@ -2,11 +2,14 @@ SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
.NOTPARALLEL:
.PHONY: console-vm cartridge-test data-test desktop-test
.PHONY: console-vm cartridge-test data-test desktop-test clean clean-preview clean-test
.PHONY: help bootstrap smoke-test check dasung dasung-test rootfs rootfs-test init-test vm tooling tooling-test kernel initramfs system-card boot-volume boot-test console-test performance-test
help:
@printf '%s\n' 'FDS/OS — static Rust tools and native s6' \
'make clean-preview Show obsolete build/test directories and Rust output selected for cleanup' \
'make clean Remove that output; keep current images, latest tests, caches and releases' \
'make clean-test Test cleanup protections using small disposable fixtures' \
'make workstation Build native Linux software/cartridge and QEMU command-line tools' \
'make workstation-test Verify software builds, multi-partition images and file write/readback' \
'make emulator-test Boot FDS and test cartridge hotplug with the public emulator' \
@@ -51,6 +54,15 @@ help:
'make init-test Boot an ARM VM and verify s6 PID 1 and service control' \
'make vm Open a temporary root shell in the verified test VM'
clean-preview:
./tools/clean-builds --dry-run
clean:
./tools/clean-builds
clean-test:
python3 tests/integration/clean-checks.py
bootstrap:
@mkdir -p out/logs
./tools/bootstrap-host 2>&1 | tee out/logs/bootstrap.log
@@ -81,6 +93,7 @@ smoke-test:
check:
./tests/integration/m0-checks
cargo fmt --all -- --check
$(MAKE) clean-test
dasung:
@mkdir -p out/logs
@@ -235,7 +248,7 @@ workstation:
workstation-test: workstation
cargo test --locked --offline --target $$(rustc -vV | sed -n 's/^host: //p') -p fds-common -p fds-burn -p fds-software -p fds-workstation
python3 tests/integration/workstation-images.py --cli out/workstation/fds-cartridge --image-tool-runner tools/in-image-tools --cc "$(CURDIR)/tools/in-void" aarch64-linux-gnu-gcc 2>&1 | tee out/logs/workstation-images.log
python3 tests/integration/workstation-images.py --cli out/workstation/fds-cartridge --image-tool-runner tools/in-image-tools --xbps-tool-runner tools/in-void --xbps-bin "$(CURDIR)/.host/xbps/usr/bin" 2>&1 | tee out/logs/workstation-images.log
emulator-test: workstation
python3 tests/integration/workstation-emulator.py --cli out/workstation/fds-emulator --qemu-runner tools/in-void 2>&1 | tee out/logs/workstation-emulator.log
+52 -272
View File
@@ -1,293 +1,73 @@
# FDS/OS — Felis Data Systems Operating System
# FDS/OS
FDS/OS is an operating system project for the **Felis FP-85**, a planned retro
portable computer built around a Raspberry Pi 5, a Dasung Paperlike 13K grayscale E-Ink display, a removable
battery, and 12 USB cartridge bays.
FDS/OS is a Linux operating system for the Felis FP-85, a Raspberry Pi 5 computer
with twelve USB cartridge bays and a Dasung Paperlike 13K display. It combines
Void Linux packages, native s6 services and static Rust system tools with a
simple cartridge workflow: insert software, run it, and eject it when finished.
The idea is to make the computer's software and data removable. A **SYSTEM**
cartridge carries the operating system, a **DATA** cartridge carries the user's
files, and **PROGRAM** or **ENVIRONMENT** cartridges add applications or select
an environment such as WindowMaker. The internal NVMe holds the boot machinery,
recovery environment, and machine configuration. Swapping a SYSTEM cartridge is
the intended update and rollback mechanism.
## How the system fits together
The project aims to combine familiar Linux software with fast, predictable
startup and shutdown. It uses the Void/XBPS package ecosystem for ordinary Linux
applications, uses native s6 service management, and builds FDS-owned system
tools as small, self-contained Rust executables.
## What you can use today
**M0–M12 software acceptance is complete, including a signed local FDS/OS 0.1.0 release.**
Current development also adds [Linux workstation software cartridges and a QEMU emulator](docs/workstation.md). These changes are separate from the frozen 0.1.0 release.
Physical Pi testing is deferred; see
the [implementation ledger](docs/implementation-status.md).
The cross-build environment is implemented and verified on x86_64 Arch Linux. You can build and inspect ARM Linux software from your
Arch machine without owning a Pi.
| Available now | What it gives you |
| Component | Purpose |
| --- | --- |
| `out/fds-os-0.1.0/` | Signed local images, preserved source/build inputs and independent-build comparison; start with its `README.md` |
| `make workstation` | Native Linux `fds-cartridge` and `fds-emulator`: build xz bundles, create/verify 1+m GPT images, preview USB writes, and hotplug images into twelve virtual bays |
| `make bootstrap` | Project-local XBPS tools, a Void build container, and the pinned Rust toolchain |
| `make all` / `make packages` | The complete image set, or just the FDS base packages; see [Complete builds](docs/reproducible-builds.md#prepare-the-snapshot) |
| `make smoke-test` | An ARM glibc XBPS package and an ARM static-musl Rust executable, with automated checks |
| `make check` | Checks for invalid binaries, source pin drift, unsafe overlay replacement, and Rust formatting |
| `make tooling` / `make tooling-test` | Static ARM `fds` and stage0 diagnostic tools, with parsing/discovery tests |
| `make dasung` / `make dasung-test` | Static ARM monitor daemon, base XBPS package, s6 definitions, and simulated monitor tests |
| `make rootfs PROFILE=cli` / `make rootfs-test` | Configured ARM rootfs archive, emulated shell access, and archive acceptance checks |
| `make init-test` / `make vm` | Full ARM boot test, native s6 PID 1, service control, and a temporary development shell |
| `make kernel`, `make initramfs`, `make system-card`, `make boot-volume` | Pi kernel, early userspace, GPT/EROFS SYSTEM and FAT32 boot partition; see [Boot images](docs/boot.md) |
| `make boot-test` | Actual stage0 handoff, missing-media insertion, recovery and archive-format checks in an ARM VM |
| `make console-vm`, `make console-test`, `make performance-test` | Ordinary-user FDS console, boot tracing and measured VM optimization comparison; see [M5 evidence](docs/m5-validation.md) |
| `make cartridge-test` | Virtual USB bay mapping, hardware recognition, metadata/mount rejection checks and safe read-only eject |
| `make data-test` | Writable `/data`, managed background programs, sustained writes, safe eject and independent ext4 checks; see [DATA usage](docs/data.md) |
| `make rootfs PROFILE=development` / `make desktop-test` | WindowMaker, grayscale styling, trusted ENVIRONMENT activation, managed PROGRAM execution and on-demand Ethernet; see [Desktop usage](docs/desktop.md) |
| `make media-image-test` / `make media-test` | SYSTEM/DATA/ENVIRONMENT creation, legacy PROGRAM compatibility, confirmed writes, readback verification and boot of a newly written SYSTEM; see [Media tools](docs/media-tools.md) |
| `make recovery` / `make recovery-test` | Separate recovery rootfs and EROFS, local maintenance console, DATA checks/repairs and replacement SYSTEM workflow; software checks passed, see [Recovery](docs/recovery.md) |
| `make internal-image` / `make internal-test` | Complete internal GPT image and persistent bay/hardware settings; see [Internal storage](docs/internal-storage.md) |
| `make development-test` | Native C/C++/Rust builds, debuggers, build systems, Git and Vim inside the ARM development image |
| `make signing` / `make signing-test` | Host and ARM release signing/verification tools, checked independently against OpenSSL |
| `make eeprom` / `make eeprom-test` | Reversible, verified EEPROM configuration files without applying anything to hardware; see [EEPROM](docs/eeprom.md) |
| Source pinning and input records | A fixed Void source commit plus logs and hashes of the actual build inputs |
| Internal NVMe | Holds boot files, recovery and persistent machine settings |
| SYSTEM cartridge | Supplies the read-only operating system; swapping it changes the OS |
| PROGRAM cartridge | Supplies applications and their dependencies, ready to execute |
| DATA cartridge | Stores writable files at `/data` |
| ENVIRONMENT cartridge | Selects an installed environment, such as WindowMaker |
**The kernel → stage0 → SYSTEM → s6 path boots in an ARM virtual machine.**
The filesystem includes native s6 init, runtime mounts, console and device
services, and Dasung supervision. The ordinary-user FDS console and boot tracing are available. M6 adds the cartridge daemon and `fds bays`, `fds bay`, `fds cartridge`, and
`fds eject`; see [Cartridge usage](docs/cartridges.md). The complete internal NVMe image and independent recovery are implemented;
[release signature tooling](docs/releases.md) has passed host and ARM checks.
[Frozen inputs, offline rebuilds and complete release assembly](docs/reproducible-builds.md)
passed two independent offline builds with all 27 artifacts identical. The local
release is at `out/fds-os-0.1.0/`; [M12 validation](docs/m12-validation.md#final-local-release-acceptance)
records the signing identity, verification commands and exact evidence.
M10's `fds poweroff` and
`fds reboot` have passed software validation; see [Shutdown usage](docs/power.md). The `fds`
tool also provides identity, boot tracing and manifest inspection; see [Rust tooling](docs/tooling.md). See the [roadmap](docs/roadmap.md).
The default interface is a console. WindowMaker provides an optional grayscale
retro desktop and an FDS Control panel for inspecting bays, launching programs
and ejecting cartridges. The same theme is used on ordinary screens and E-Ink.
The Dasung controller is part of the base system and starts independently of
the desktop or any ENVIRONMENT cartridge.
## Start here
## Choose a starting point
- **I want to build software cartridges or try virtual insertion/removal:** follow the [workstation walkthrough](docs/workstation.md).
- **I want to understand the project:** read [Architecture](docs/architecture.md).
- **I want to try the working code:** follow [Your first build](docs/getting-started.md).
- **I want to build the OS filesystem:** follow [Rootfs](docs/rootfs.md).
- **I want to boot it and control services:** follow [Native init and ARM VM](docs/init.md).
- **I want to build the Pi boot files and SYSTEM image:** follow [Boot images](docs/boot.md).
- **I want to use the desktop, applications or Ethernet:** follow [Desktop usage](docs/desktop.md).
- **I want to create or write a cartridge:** follow [Media tools](docs/media-tools.md).
- **I want persistent bay settings and an internal NVMe image:** follow [Internal storage](docs/internal-storage.md).
- **SYSTEM will not boot, or DATA needs checking:** follow [Recovery](docs/recovery.md).
- **I have already built it and want to develop:** use the [Development guide](docs/development.md).
- **A command failed:** look up its message in [Troubleshooting](docs/troubleshooting.md).
- **I do not know the terminology:** keep the [Glossary](docs/glossary.md) open.
- **Build and try FDS:** follow [Build and start FDS](docs/getting-started.md).
- **Build an application cartridge:** use the [software and emulator walkthrough](docs/workstation.md).
- **Use the computer:** read [Using cartridges](docs/cartridges.md), [DATA](docs/data.md), and [the desktop guide](docs/desktop.md).
- **Install or maintain a machine:** see [internal storage](docs/internal-storage.md), [release verification](docs/releases.md), and [recovery](docs/recovery.md).
- **Free build space:** run `make clean-preview`, then `make clean`; see [cleanup](docs/cleanup.md).
The [documentation index](docs/README.md) organizes the remaining guides. The
[master plan](docs/master-plan.md) is the full specification, rather than the
recommended starting point for learning to use the repository.
The [user manual](docs/README.md) is organized around tasks. Build internals,
design decisions, hardware procedures and validation records live separately in
[developer notes](docs/developer/README.md).
## Quick start: software cartridges and emulation
## A typical software session
On a Linux workstation, build the native tools with `make workstation`, then
follow [the complete walkthrough](docs/workstation.md) to package the included
hello/report examples, assemble a cartridge, boot FDS and insert it. This tooling
uses normal Linux QEMU, EROFS and xz utilities; it does not run on the Pi.
Building the complete OS images still uses the environment below.
## Quick start on Arch Linux
You need an **x86_64 Arch Linux host**, a regular user account with access to sudo
for installing prerequisites, a network connection, and several GB of free disk
space. The kernel must permit unprivileged user namespaces. A Pi, Docker, and a
separate musl C compiler are not required for the current smoketest.
Use a checkout path without spaces. In the current workspace:
These commands run on FDS after inserting the example PROGRAM cartridge in bay 1:
```sh
cd /home/felis/source/fds
```
On another machine, enter your own FDS checkout directory instead. Use an actual
Git checkout so the pinned Void submodule can be initialized for a normal online
build. A complete [frozen-input snapshot](docs/reproducible-builds.md) instead
restores the source and its bundled Void checkout for offline building; a bare
source archive alone does not supply those inputs. This repository does not currently
configure a public Git remote, so no public clone URL is assumed here.
Install the prerequisites once:
```sh
sudo pacman -S --needed bash coreutils binutils git curl make file tar xz gzip zstd \
bubblewrap rustup ca-certificates findutils diffutils grep sed gawk util-linux
```
Then run these commands **in order, as your regular user**:
```sh
make bootstrap
make smoke-test
make check
```
`make bootstrap` downloads and prepares the build tools. `make smoke-test`
compiles the two ARM artifacts and verifies their architectures and library
requirements. `make check` exercises the validation failure cases and checks
formatting. If a command fails, stop at that step and use the linked
[troubleshooting guide](docs/troubleshooting.md); a partially completed run is
not a successful build.
The first run downloads hundreds of MB of compiler and build dependencies and
may take a while on a slow connection. Later runs reuse caches, although XBPS
operations may still access the network. `cargo --offline` applies only to Rust
crate downloads, not to the whole pipeline.
Successful smoke-test output ends with:
```text
PASS: aarch64 static ELF
...
PASS: XBPS package hello-2.12.3_1 architecture=aarch64 (glibc)
SKIP: ARM execution (optional qemu-aarch64 not installed); ELF verification passed
PASS: M0 smoke test complete
```
The QEMU line is a documented optional skip. If QEMU is installed, a runtime PASS
replaces it. The [first-build guide](docs/getting-started.md) explains how to run
the executable with QEMU and interpret every result.
To build the current OS filesystem and boot its development VM, continue with:
```sh
sudo pacman -S --needed python e2fsprogs libarchive lz4
make rootfs PROFILE=cli
make rootfs-test
make init-test
make vm
```
Python must be version 3.14 or newer. Additional emulation tools are installed
inside the project-local build container. `make init-test` boots ARM Linux,
checks native init and service control, and powers off. `make vm` repeats those
checks and opens a temporary root shell; type `exit` to shut it down. Nothing is
flashed to a physical disk. The [VM guide](docs/init.md) explains the console,
service commands, logs, and limits in detail.
To build the complete operating-system image set and try its ordinary user
console, continue from that prepared workstation:
The complete images require substantially more space than the initial smoke
test: the offline diagnostic build occupied about 24 GB, before additional VM
test copies. See [build-space observations](docs/reproducible-builds.md#what-is-frozen)
before preparing two independent release builds.
```sh
make all
make init-test
make boot-test
make console-vm
```
`make all` builds CLI and development SYSTEM cartridges, independent recovery
and the internal disk. `make init-test` prepares the full QEMU environment and
checks native init. `make boot-test` checks startup, missing-media recovery and
all initramfs formats. `make console-vm` opens the actual `FDS>` console.
Enter these commands **inside that console**:
```sh
fds info
fds bays
fds boot-profile
fds poweroff
fds bay 01
hello 'Hello from FDS'
fds run 01 -- demo.report:report
fds eject 01
```
This VM has no physical USB devices attached, so empty cartridge bays are
expected. Its home directory is temporary; use a DATA cartridge for persistent
files on the real machine. `fds poweroff` performs an orderly shutdown. The
[boot guide](docs/boot.md#use-the-ordinary-fds-console) explains console controls,
and the [internal-storage guide](docs/internal-storage.md) covers physical
installation when the Pi hardware is ready.
`hello` runs in your current terminal. `fds run` starts a managed background
command. Both forms are tracked for eject. Wait for **SAFE** before removing
media; use `fds poweroff` to shut down the whole computer.
## Dasung monitor support
## Build tools
The existing Rust `dasungd` from the monitor troubleshooting task is integrated
as a required base-system package, with a native s6 boot service. Build it with
`make dasung`, then run `make dasung-test`. The [Dasung guide](docs/dasung.md)
explains the exact monitor profile, dependencies, configuration, and Pi validation
still needed. M2 starts the controller under native s6 in the ARM VM; the VM has no physical monitor.
Run these from the repository root:
## What the build produces
| Path | Contents | How to use it |
| --- | --- | --- |
| `out/fds-internal.img` | Complete GPT disk with BOOT, independent RECOVERY and persistent machine-settings partitions | Use the [internal installation procedure](docs/internal-storage.md); this belongs on the internal NVMe |
| `out/fds-system-cli.img` | Immutable CLI SYSTEM cartridge, including base Dasung support | Build with `make system-card PROFILE=cli`; use the [media writer](docs/media-tools.md) to write a SYSTEM cartridge |
| `out/fds-system-development.img` | SYSTEM with native compilers, debugging tools and the optional desktop runtime | Build with `make rootfs PROFILE=development` followed by `make system-card PROFILE=development` |
| `out/fds-boot.img` / `out/fds-recovery.img` | Individual FAT32 BOOT and EROFS RECOVERY partition payloads | Normally use the complete internal image; these are component images, not interchangeable whole disks |
| `out/fds-initramfs.img` | Early boot program and base display support | Included in BOOT; four compressed/uncompressed formats are also under `out/initramfs/` |
| `out/rootfs-aarch64.tar` | Configured ARM glibc userspace, including Dasung | Follow [M1 rootfs](docs/rootfs.md) to build, inspect, and run it |
| `out/m2-vm-latest/` | Last successful ARM test disk, serial log, environment and hashes | Use `make vm` to boot it; see [Native init](docs/init.md) |
| `out/fds-smoketest` | Static-musl AArch64 Rust executable | Inspect with `file`/`readelf`; optionally run with `qemu-aarch64` |
| `out/packages/hello-2.12.3_1.aarch64.xbps` | GNU hello packaged for ARM glibc | Query or extract it with the [package guide](docs/packages.md) |
| `out/packages/aarch64-repodata` | Local package index | Lets XBPS query the exported package |
| `out/logs/` | Bootstrap and build logs | Diagnose a failed or apparently stalled step |
| `out/manifests/` | Package inventories and SHA-256 digests | Inspect which inputs and outputs were used |
`make all` produces the complete image set above in order. It prepares files and
does not write physical disks. Build and test commands share a build container;
run them sequentially. Stable paths point to the latest successful versioned
build directories. See [Offline rebuilds](docs/reproducible-builds.md) for the
separate frozen-input and release-verification workflow.
The rootfs tar and individual binaries are not flashable Pi images. The separate
VM disk boots with the generic ARM test kernel. The Rust
executable is ARM code and normally cannot run directly on the x86_64 host.
The XBPS package is not an Arch package and is not installed with pacman.
## How the build relates to the future machine
```text
Working today: x86_64 Arch workstation
├─ pinned Void + xbps-src + GNU cross compiler → aarch64 glibc package
└─ pinned Rust + bundled musl + rust-lld → static AArch64 executable
Pi boot path, software-tested in a VM; physical validation pending:
internal NVMe kernel/initramfs → SYSTEM cartridge → native s6 → FDS console
```
The first line supplies conventional Linux packages. The second supplies FDS's
own control tools without a dependency on the installed glibc version. Both
produce ARM code; only their library/linking strategy differs. See
[the architecture guide](docs/architecture.md#two-library-strategies).
## Where to find things
| Directory or file | Purpose today |
| Command | Result |
| --- | --- |
| `rust/fds-smoketest/`, `rust/dasungd/`, `rust/fds-common/`, `rust/fds-cli/`, `rust/fds-stage0/` | Static-linking smoketest, monitor controller, shared contracts, CLI and early boot |
| `tools/` | Bootstrap, package build, overlay preparation, and ELF checks |
| `config/` | Pinned host tool download and Void build configuration |
| `vendor/void-packages/` | Unmodified upstream source at the recorded commit |
| `tests/integration/m0-checks` | Executable validation and protection checks |
| `docs/` | Tutorials, design explanations, references, and validation evidence |
| `packages/fds-dasungd/`, `s6/source/`, `image/base-packages.list` | Monitor package, native service definitions, and mandatory base inclusion |
| `packages/fds-base/`, `packages/fds-base-files/`, `packages/fds-init/` | Base dependencies, identity, accounts, layout and native init |
| `packages/fds-kernel/`, `packages/fds-cli/`, `image/` | Pi kernel delta, static CLI package and filesystem/boot builders |
| `profiles/` | CLI, development and recovery package selections |
| `rust/fds-release/`, `tools/frozen-inputs`, `tools/assemble-release` | Signature verification, input preservation and local release assembly |
| `make workstation` | Native Linux `fds-cartridge` and `fds-emulator` executables |
| `make bootstrap` | Prepares the complete OS build environment on x86_64 Arch Linux |
| `make rootfs PROFILE=cli` | Builds the ARM base filesystem and its packages |
| `make system-card PROFILE=cli` | Creates a whole-disk SYSTEM cartridge image |
| `make initramfs` | Creates early boot userspace |
| `make all` | Builds the complete CLI/development, recovery and internal image set |
| `make clean` | Removes obsolete generated workspaces and Rust build output |
Generated `.host/`, `target/`, and `out/` contents are ignored by Git. Rustup also
installs the pinned toolchain in your user account's Rustup directory. The
[build reference](docs/build-host.md) details prerequisites, storage, and pins.
New software cartridges are built from metadata and Void source packages using
`xbps-src` on a Linux workstation. Their EROFS partitions contain installed
programs and dependencies directly. FDS does not extract those programs at launch.
## Project status and evidence
The [M0 validation report](docs/m0-validation.md) records the actual build commands,
results, and initial artifact hashes. QEMU execution and Pi hardware were not
validated in that initial run. Later milestones measured VM boot and shutdown;
physical Pi performance remains unmeasured. Source and toolchain pins are in
place, and the complete frozen-input/offline release workflow is undergoing M12
acceptance. See [current evidence](docs/m12-validation.md) for the distinction.
The [M1 guide](docs/rootfs.md) explains rootfs usage; [M1 validation](docs/m1-validation.md) records the results.
M2 usage is in [Native init and ARM VM](docs/init.md), with results in [M2 validation](docs/m2-validation.md).
M3 adds [static-musl FDS tooling](docs/tooling.md). Work continues through M12
under [AGENTS.md](AGENTS.md), with physical hardware checks deferred. Project-owned
source comments, documentation, diagnostics, and development discussions use English.
The workstation walkthrough includes prerequisites, complete example commands,
expected outputs, emulator lifecycle and the confirmed USB-writing workflow.
Append `--help` to either native tool for its command reference; run `make help`
for the complete build target list.
+21 -82
View File
@@ -1,86 +1,25 @@
# Documentation guide
# FDS/OS user manual
FDS/OS builds bootable ARM SYSTEM images, an internal NVMe disk, independent
recovery, and a development environment. Native s6 manages the base system,
including Dasung support; cartridge tools manage DATA, programs and optional
desktop/network activation. M0–M12 software checks and local release
verification have passed. Physical Pi validation is deferred. The guides
below explain how to build, use and test the implemented system.
FDS/OS runs a cartridge-based Linux computer. Start with the overview, then choose
the guide for building software, trying the emulator, or operating the machine.
Commands in workstation guides run on your Linux computer; commands in the
operating guides run at the FDS console unless stated otherwise.
## If this is your first visit
1. Read the [project overview](../README.md) for the intended computer and current capabilities.
2. Follow [Your first build](getting-started.md) to create and inspect the working artifacts.
3. Read [Architecture](architecture.md) to understand the relationship between the build host, Linux packages, FDS tools, and cartridges.
4. Use the [Development guide](development.md) for subsequent changes and rebuilds.
The [Glossary](glossary.md) explains terms such as ABI, sysroot, EROFS, and
masterdir. No prior Void or s6 experience is assumed by the first-build guide.
## Working with the repository today
| Guide | Question it answers |
| Guide | What you will learn |
| --- | --- |
| [Workstation and emulator](workstation.md) | How do I build software, create a 1+m cartridge image, boot FDS, insert/remove it and write USB? |
| [Software cartridge format](software-format.md) | What goes in metadata, payload partitions and xz tarballs, and how does the guest run them? |
| [Workstation validation](workstation-validation.md) | Which host builds, whole-image writes and public QEMU lifecycle tests actually passed? |
| [Your first build](getting-started.md) | What do I install, what do I run, and how do I know it worked? |
| [M1 root filesystem](rootfs.md) | How do I build the OS filesystem, run its ARM shell, inspect packages, and test the archive? |
| [FDS Rust tooling](tooling.md) | How do I build and run the static FDS command and boot-discovery tools? |
| [Clap migration validation](clap-validation.md) | Which parser, complete-image, VM and startup checks passed after standardizing the Rust command lines? |
| [Internal storage](internal-storage.md) | How do I build the full NVMe disk, persist bay settings and save diagnostics? |
| [Release signatures](releases.md) | How do I create a signing key, sign artifacts and verify a download with a trusted key? |
| [Frozen inputs and offline rebuilds](reproducible-builds.md) | How do I preserve inputs, build without network access, compare results and assemble a local release? |
| [Boot images](boot.md) | How do I create the Pi boot partition, initramfs and SYSTEM cartridge, and test insertion/recovery? |
| [Desktop, PROGRAM and Ethernet](desktop.md) | How do I start WindowMaker, run cartridge programs, and enable networking? |
| [Media tools](media-tools.md) | How are images created, inspected and confirmed before cartridge writes? |
| [Pi 5 EEPROM configuration](eeprom.md) | How do I prepare, review and restore bootloader settings without flashing the build host? |
| [Twelve-bay stress tests](stress-testing.md) | How do I run the twelve-device VM checks and prepare physical tests? |
| [Shutdown and reboot](power.md) | How does shutdown protect DATA, and what should I do when it is blocked? |
| [Writable DATA](data.md) | Where do user files live, how do managed programs work, and when is removal safe? |
| [Implementation through M12](implementation-status.md) | What is complete, pending, and deferred to physical hardware? |
| [M12 software evidence](m12-validation.md) | Which checks, offline comparisons and signed local artifacts establish M12 software acceptance? |
| [Native init and ARM VM](init.md) | How do I boot the system, open its shell, and start or stop a service? |
| [M1 validation](m1-validation.md) | What did the original rootfs milestone verify? |
| [M2 validation](m2-validation.md) | Which full ARM boot and service-control checks passed? |
| [M3 validation](m3-validation.md) | Which static tooling and ARM parsing/discovery checks passed? |
| [M4 software validation](m4-validation.md) | Which image, module, stage0, insertion and recovery tests passed? |
| [M5 software validation](m5-validation.md) | Which ordinary-console, trace, emulation and optimization checks passed? |
| [M6 software validation](m6-validation.md) | Which virtual USB, mapping, metadata, mount, IPC and eject checks passed? |
| [M7 DATA validation](m7-validation.md) | Did sustained writes survive safe eject, reinsertion and independent filesystem checks? |
| [M8 desktop/network validation](m8-validation.md) | Did desktop transitions, PROGRAM execution, fonts and Ethernet work in the ARM VM? |
| [M9 media-tool validation](m9-validation.md) | Did ARM image creation, confirmed cartridge writes, verification and boot of a newly written SYSTEM pass? |
| [M10 shutdown validation](m10-validation.md) | Which ordered halt/reboot, busy refusal, crash and writeback-error tests passed? |
| [M11 stress validation](m11-validation.md) | Which twelve-device and media failure tests have passed, and what remains? |
| [Build host reference](build-host.md) | Which versions, dependencies, environment settings, and checks does the pipeline use? |
| [Development](development.md) | How do I rebuild only the component I changed, configure the build, and prepare a change? |
| [Dasung monitor](dasung.md) | How is the existing monitor daemon built, packaged, configured, and tested for the base system? |
| [Dasung validation](dasung-validation.md) | Which builds, simulator cases, and native s6 checks actually passed? |
| [Packages](packages.md) | What is an XBPS package, and how do I inspect the one I built? |
| [Troubleshooting](troubleshooting.md) | Why did a command fail, and what should I check next? |
| [M0 validation](m0-validation.md) | What has actually been built and verified on the development host? |
| [Overview](../README.md) | How the computer, internal storage and cartridges fit together |
| [Build and start FDS](getting-started.md) | Prepare the OS build workstation, create images and boot the emulator |
| [Software and emulator walkthrough](workstation.md) | Build Void source packages, assemble cartridges, insert them and write USB media |
| [Using cartridges](cartridges.md) | Inspect bays, run applications, resolve command names and eject safely |
| [DATA and files](data.md) | Use persistent storage and manage active DATA |
| [Desktop and control panel](desktop.md) | Open WindowMaker, manage cartridges graphically and customize the theme |
| [Software format](software-format.md) | Write source recipes and understand the installed-program image layout |
| [Internal storage](internal-storage.md) | Install boot/recovery storage and manage machine settings |
| [Recovery](recovery.md) | Inspect failed media and check or repair DATA |
| [Release verification](releases.md) | Verify downloaded images before use |
| [Dasung display](dasung.md) | Inspect and control the built-in Paperlike daemon |
| [Cleanup](cleanup.md) | Reclaim obsolete build output without losing current images or personal data |
| [Troubleshooting](troubleshooting.md) | Diagnose build, cartridge, desktop and emulator problems |
## Understanding the operating system
These pages explain the implemented behavior and distinguish software evidence
from properties that still need physical hardware tests.
| Guide | Subject |
| --- | --- |
| [Architecture](architecture.md) | Machine model, two library strategies, host/target split, writable state |
| [Boot](boot.md) | From internal NVMe to a removable SYSTEM and the first console |
| [Cartridges](cartridges.md) | Cartridge classes, physical bays, insertion, ejection, and manifests |
| [Services](services.md) | Native s6, service dependencies, readiness, and shutdown |
| [Performance](performance.md) | Timing targets, measurement boundaries, and regression reporting |
| [Recovery](recovery.md) | Build and enter independent recovery, inspect cartridges, check/repair DATA and prepare replacement SYSTEM media |
## Planning and reference
- [Roadmap](roadmap.md): current milestone, remaining milestones, and acceptance criteria.
- [Master plan](master-plan.md): the complete 67-section specification; examples describe the final design unless stated otherwise.
- [Project instructions](../AGENTS.md): implementation scope and conventions.
A `sh` code block in a current-workflow guide is intended to be run from the
repository root unless the surrounding text says otherwise. Planned runtime
examples use `text` blocks and are labeled as future behavior. Run build commands
sequentially; they share one Void build directory.
Implementation plans, dependency rationale, measurements, hardware procedures and
historical test results are maintained separately in [developer notes](developer/README.md).
+71 -170
View File
@@ -1,197 +1,98 @@
# Using cartridges and configuring bays
# Use cartridges and run programs
[Documentation index](README.md) · [Boot images](boot.md) · [Services](services.md)
A bay number identifies a physical slot, from `01` through `12`. It stays tied
to the slot rather than the order in which Linux discovers USB disks.
M6 adds the base `fds-cartridged` service, twelve bay states, USB hardware
recognition, strict metadata inspection, read-only storage mounts and eject.
Its [ARM VM acceptance checks passed](m6-validation.md). [Writable DATA integration](data.md) has passed M7 software acceptance;
[desktop/program activation](desktop.md) passed M8 and [media creation tools](media-tools.md)
passed M9 software acceptance. [M10 ordered shutdown](power.md) has passed software acceptance.
| Cartridge | Role |
| --- | --- |
| SYSTEM | Supplies the read-only operating system used at boot |
| PROGRAM | Supplies applications and their runtime dependencies |
| DATA | Stores your writable files at `/data` |
| ENVIRONMENT | Requests an installed desktop profile, such as WindowMaker |
## Inspect the current machine
## Inspect a bay
At the FDS console:
Insert the cartridge, then run:
```sh
fds bays
fds bay 3
fds cartridge 3
fds --json bays
fds bay 01
```
The ordinary `fds` user can use these commands. `bays` shows all twelve bays,
including empty and unconfigured ones. `cartridge` includes validated metadata
and the current mount path. `--json` exposes a versioned machine-readable report.
`fds rescan` explicitly refreshes inventory; normal insert/remove events trigger
refresh automatically. The console does not wait for the inventory scan.
USB discovery takes place asynchronously. `fds bay` shows its state, identity,
mount location, running process count and software catalogue. `fds --json bay 01`
returns structured details, including command aliases. `fds rescan` refreshes
the inventory. No application starts merely because a PROGRAM cartridge is inserted.
| State | Meaning and next action |
| --- | --- |
| UNCONFIGURED | No measured controller/port mapping exists for this bay |
| EMPTY | Configured bay with no detected USB device |
| HARDWARE | USB device without storage; catalog name or VID/PID is shown |
| UNRECOGNIZED STORAGE | Storage exists but no named FDS partition is available |
| MOUNTED READ ONLY | Manifest validated; filesystem available at the displayed path |
| MOUNTED READ WRITE | Active DATA is writable at `/data`; eject before removal |
| PROTECTED | The active SYSTEM root; eject is refused |
| AMBIGUOUS | Multiple devices or named partitions match; nothing is selected arbitrarily |
| ERROR | Inspection, mounting or metadata validation failed; read the diagnostic |
| SAFE | The requested unmount succeeded; remove the cartridge |
| EMPTY | No cartridge is detected |
| MOUNTED READ ONLY | Contents are available; PROGRAM commands can run |
| MOUNTED READ WRITE | DATA is active at `/data` |
| SAFE | Storage is released; remove the cartridge |
| PROTECTED | This cartridge supplies the running SYSTEM; shut down before removal |
| UNCONFIGURED | The physical slot needs a bay mapping |
| ERROR / AMBIGUOUS | Read the detail; correct the media or mapping before use |
An unrecognized storage state may briefly appear while the kernel discovers its
partitions. It is not permission to remove a device being used elsewhere.
Errors and ambiguous media never produce SAFE. An additional mount of the same filesystem also blocks managed eject until
it is unmounted.
## Run a software command
## Calibrate the physical bay map
The shipped `/etc/fds/bays.toml` is deliberately empty: no Pi bay wiring has yet
been measured. This produces UNCONFIGURED states, not invented assignments.
1. On the assembled machine, insert one known USB device into one physical bay.
2. Run `fds topology`. This diagnostic explicitly shows controller/port paths;
ordinary bay commands omit those implementation details.
3. Record that path and repeat for each bay. Test both USB 2 and USB 3 devices,
because their companion root hubs can have different logical port paths.
4. Put the measured map in your machine configuration directory and build the
[internal image](internal-storage.md), or use `fds machine install DIRECTORY`
from recovery and reboot. Machine settings persist independently of SYSTEM.
The packaged `/etc/fds/` files are fallback defaults for missing/invalid internal
storage; `fds machine status` shows which source is active.
5. Verify all twelve devices together, in different insertion orders and after
reboot. That physical acceptance remains deferred.
A **syntax example only**, using identities that must be replaced by observations:
```toml
[front]
hub = "platform/example-controller:usb2/1"
[front.ports]
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
6 = 6
[front_superspeed]
hub = "platform/example-controller:usb3/1"
[front_superspeed.ports]
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
6 = 6
```
`hub` names the controller and parent port chain. Its numbered `ports` map the
next downstream port to a bay number from 1 through 12. Additional groups can
map the rear hub. Explicit USB 2/3 aliases may refer to the same bay; duplicate
ports, overlapping parent/child mappings, and duplicate bays within one hub are
rejected. If two devices simultaneously match aliases for one bay, it becomes
AMBIGUOUS. A hub inserted into a cartridge bay with multiple downstream devices
also needs an explicit future composite-device policy; M6 refuses to guess.
The identity excludes the Linux USB bus number and `/dev/sdX` enumeration order.
It includes the controller's sysfs path, USB protocol generation and port chain.
Moving a hub to another controller or changing wiring requires recalibration.
## Storage formats and metadata
| Class | GPT partition name | Filesystem and current behavior |
| --- | --- | --- |
| SYSTEM | FDS_SYSTEM | EROFS; current root is protected, additional media read-only |
| DATA | FDS_DATA | ext4; inspected read-only first, then activated at `/data` under the M7 policy |
| PROGRAM | FDS_METADATA + FDS_PAYLOAD02… | Metadata and xz bundles in `1+m` EROFS partitions; see [software format](software-format.md) |
| Legacy PROGRAM | FDS_PROGRAM | Existing single EROFS application tree; still readable |
| ENVIRONMENT | FDS_ENVIRONMENT | EROFS; declarative selection of a trusted built-in profile |
| UTILITY | FDS_UTILITY | EROFS; inspection only, no automatic actions |
| HARDWARE | None required | VID/PID, optional serial, device/interface class and bay |
Storage contains `/FDS/CARTRIDGE.TOML`. For example:
```toml
format = 1
[cartridge]
id = "fds.windowmaker"
name = "WINDOW SYSTEM"
class = "environment"
version = "0.1"
[media]
writable = false
[activation]
profile = "windowmaker"
```
The manifest class must match the GPT partition name. SYSTEM, PROGRAM and
ENVIRONMENT are read-only; DATA declares writable media during the initial read-only inspection. Unknown keys, root commands, unsafe identifiers, control
characters, symlink metadata and nonregular metadata files are rejected.
The parser accepts at most 64 KiB. No `/FDS/autorun.sh` is run.
Inspect a standalone manifest with `fds inspect /path/to/CARTRIDGE.TOML`.
For mounted storage, `fds cartridge N` reports the daemon's validated copy.
Media is inspected in a root-only staging directory, then published at
`/run/fds/media/NN` after validation, with nodev, nosuid and noexec. Device
major/minor and kernel disk sequence are checked before mounting an opened
block-device descriptor, preventing stale enumeration names from selecting a
replacement disk.
## Eject a mounted cartridge
For the included hello/report cartridge:
```sh
fds eject 2
hello 'Hello from my cartridge'
report
fds run 01 -- demo.hello:hello 'Hello from my cartridge'
```
Leave any shell working directory inside that cartridge and close files first.
SAFE is emitted only after an ordinary unmount succeeds. Busy mounts return an
error; forced or lazy unmount is never used to declare safe removal. Once SAFE,
the daemon keeps that insertion unmounted until removal and reinsertion, including
across a service restart. Its root-owned volatile marker is tied to the kernel
disk sequence, so a replacement disk is not mistaken for ejected media.
The active SYSTEM cannot be ejected. Swap SYSTEM only after shutting down.
Direct commands run in the foreground with your terminal, input/output streams,
arguments, working directory and exit code. Pipes and redirection work normally.
`fds run` starts a managed background program and prints its PID; its output is
written to `/run/log/cartridged/current`. Both forms run as the ordinary FDS user
and are tracked for cartridge removal.
Pulling a mounted cartridge without eject is surprise removal. The daemon clears
its inventory and detaches a vanished read-only mount where needed; that cleanup
is not a successful eject. [M7 DATA handling](data.md) extends ejection to writable DATA, consumer
tracking and syncfs. The temporary `/home/fds` remains usable without DATA and is lost at
power-off.
New cartridges run directly from read-only payload partitions. They require no
program extraction or compilation on FDS. The stable `/run/fds/bin` directory is
already in the console and terminal PATH, so existing shells see inserted commands.
## Hardware recognition
If cartridges export the same command name, the lowest numbered bay wins.
Within a bay, the first software selector in lexical order wins. System commands
appear earlier in PATH and retain their usual meanings. To select a specific
cartridge command, use its qualified alias:
Edit `packages/fds-cartridged/files/hardware-catalog.toml`, then rebuild SYSTEM.
Entries name devices, never executable actions:
```toml
[[device]]
name = "MY SERIAL ADAPTER"
vendor = "1234"
product = "5678"
# Optional exact restrictions:
serial = "UNIT-1"
class = "02"
```sh
b01:demo.hello:hello 'Explicit bay and software'
fds run 01 -- demo.hello:hello
```
Replace these example IDs with observed values. Device or interface class may
match `class`. Multiple matching catalog entries produce an error; unknown
hardware remains identified by VID/PID without being mistaken for empty media.
Dasung control remains in the independent base monitor service.
Legacy PROGRAM media uses `b01:COMMAND` for a qualified direct alias. Commands
are removed on eject/unplug; a shared name falls back to the next available bay.
After the final cartridge is removed, a shell may remember the old executable
path; `hash -r` clears Bash's command cache.
## Implementation and diagnostics
## Eject safely
The daemon subscribes to kernel USB/block events before taking its first snapshot.
A receive-buffer overflow causes a fresh snapshot. IPC uses a bounded Unix socket
at `/run/fds/control.sock`, mode 0660 and group `fds`, with peer UID checks for
root and the ordinary FDS user. Slow clients have individual deadlines and do not
block other clients. No asynchronous runtime or new external Rust dependency is
introduced: the existing std, libc, serde, serde_json and toml components suffice.
```sh
cd "$HOME"
fds eject 01
fds bay 01
```
Root diagnostics: `s6-svstat /run/service/cartridged` and
`cat /run/log/cartridged/current`. Logs are bounded and volatile. Restart through
`s6-rc -l /run/s6-rc -d change cartridged`, then the corresponding `-u` command.
Close users of cartridge mounts first; restart cleanup refuses busy leftovers.
Eject stops managed programs, flushes writable storage and releases its mounts.
Remove the cartridge after **SAFE** is reported. If eject fails, close files,
shells or extra mounts using that cartridge and retry. Never treat a timeout as
permission to pull writable media. Use `fds poweroff` to stop the whole computer;
swap the running SYSTEM only after shutdown.
On the build host, `make cartridge-test` runs fixtures and full virtual USB tests.
These exercise actual kernel events and mounts but cannot establish physical
wiring, USB power stability, or Pi port behavior. M11 expands to twelve-device
stress, and physical calibration must be recorded separately.
The [control panel](desktop.md) provides the same safe-eject action. For writable
storage behavior, see [DATA and files](data.md).
## Configure physical bay numbering
Run `fds topology` with one identifiable USB device inserted in each slot in
turn. Record the controller/hub topology for both USB 2 and USB 3 connections,
then map those stable identities in the machine's `bays.toml`. Do not map
`/dev/sda` names or enumeration order. The emulator supplies its own known map.
Use [machine settings](internal-storage.md) to validate, export and install the
configuration. The detailed wiring/calibration procedure is in the
[hardware engineering reference](developer/cartridges.md).
+90
View File
@@ -0,0 +1,90 @@
# Reclaim build space
Run these from the repository root after stopping builds and test VMs:
```sh
make clean-preview
make clean
```
The preview lists every selected directory, its combined allocated footprint,
and the generated directories being kept. It does not delete files. `make clean`
recalculates the selection and deletes it without a confirmation prompt. Repeat
it whenever old build/test workspaces accumulate. It needs Python 3, Git and GNU
`du`, already used by the build host; it needs no Rust compilation, downloads,
root privileges or new Python packages.
## What is removed
- Old generated rootfs, kernel, initramfs, SYSTEM, boot, recovery and internal
image workspaces that are no longer referenced by published output links.
- Old generated integration-test directories, including their disk images,
disposable test DATA overlays and local diagnostic logs. The current results
referenced by `*-latest` links or workstation current-pointer files are kept.
- Old generated package staging copies and service compilation directories.
- This checkout's `target/` Rust compilation output. Exported executables in
`out/` and `out/workstation/` remain available. The next Rust build recompiles
as needed using the retained Cargo source cache.
The command recognizes the specific temporary-directory names produced by the
repository's builders and tests. It never treats all of `out/` as disposable.
Unknown names are left alone, including ad hoc diagnostic experiments.
## What is kept
- Current rootfs archives for every profile, kernel, initramfs, boot, recovery,
internal disk and SYSTEM images, through their published symlinks. Links in
`out/manifests/` and links within retained workspaces also retain their targets.
- Latest published test runs, the current workstation image/emulator fixtures,
the current Dasung s6 database, and the newest `m9-images.*` fixture needed by
the media tests. These can still occupy tens of GiB.
- Signed releases such as `out/fds-os-0.1.0/`, input snapshots such as
`out/inputs-m12-v5/`, and independent restored trees such as
`out/rebuild-m12-v5-a/`. These remain available for release reproduction.
- User-created cartridge images, installed software trees and personal emulator sessions
such as `out/my-emulator/`, including their persistent DATA overlays.
- `out/logs/`, `out/manifests/`, packages, downloads, `out/cache/`, `.host/`, the
Void checkout/build container, and Cargo/Rustup caches outside this checkout.
- Git-tracked files and any recognized workspace containing a `.fds-keep` entry.
To retain an older generated workspace for investigation, put a marker in it
before cleaning:
```sh
touch out/m8-vm.YOUR_RUN/.fds-keep
make clean-preview
```
Replace `YOUR_RUN` with the actual directory suffix. Remove that marker when the
workspace is disposable again. Reserve the generated names for the build/test
tools; store personal sessions and images under your own names.
## Build-space management
Image builds and VM suites retain workspaces for diagnosis and preserve the last
successful published image. Repeat `make clean-preview` and `make clean` after
completed build batches. Cleanup is explicit; there is no automatic retention
limit during a build. Full image builds require tens of GiB per independent tree.
## Running safely and rebuilding
Use cleanup sequentially, just like the existing shared-container builds. It
holds the existing rootfs/base-package/image-tool locks, excludes another
cleanup, and refuses mounted candidate paths or observable active build/VM
processes. These checks are safeguards, not permission to start a separate build
while cleanup is running. It never follows a candidate symlink into another
directory, and refuses a symlinked `out/` entirely. Only the checkout containing
the script is eligible; there is no arbitrary deletion-path option.
Existing published images and native tools remain usable after cleanup. To
refresh exported binaries, run `make tooling` or `make workstation`. To rebuild
the smoke-test artifacts and rerun build checks:
```sh
make smoke-test
make check
```
After cleanup, run the smoke test before `make check`, since the checks need its
rebuilt executable. Cleanup implementation and validation details are in the
[developer notes](developer/cleanup.md).
+30 -168
View File
@@ -1,185 +1,47 @@
# Dasung Paperlike 13K base-system integration
# Dasung Paperlike display
[Documentation index](README.md) · [First build](getting-started.md) · [Services](services.md)
FDS includes `dasungd` for the configured Dasung Paperlike 13K grayscale monitor.
The controller starts during early boot and remains supervised in the base system,
including console-only use. No GUI or ENVIRONMENT cartridge is required.
FDS includes the Rust **dasungd** controller from the project's earlier
**Fix Dasung monitor black screen** task. It targets the user's **Paperlike 13K
grayscale**, serial `L56051794302`, with the confirmed 3200 × 2400 timing at about
37 Hz. This is core display support: its package is required for the base SYSTEM
image, and its service belongs to the base boot bundle, including console-only use.
It does not require an ENVIRONMENT cartridge, WindowMaker, or a graphical session.
The supplied profile uses the dedicated monitor's EDID and USB companion identity,
with raw mode 1 and contrast 4 as startup defaults. It is specific to that monitor;
do not use its identity as a blanket match for other CH340 USB devices.
**Implementation boundary:** the daemon, ARM package, and s6 service definitions
are included by the [rootfs assembler](rootfs.md). [M2 native init](init.md) starts
the controller in the base boot graph, including in the ARM VM with no monitor.
The [M4 boot path](boot.md) starts an early copy before SYSTEM exists, then stops
and reaps it before base s6 starts the normal daemon. Software handoff is tested
in a VM; physical Pi display and monitor recovery remain unverified. The final
s6 database is compiled at image build time.
## Inspect the controller
## Build it on the workstation
Complete `make bootstrap` as described in [Your first build](getting-started.md),
then run from the repository root, as your regular user:
The controller's socket is restricted to root. From a root maintenance console
on FDS:
```sh
make dasung
make dasung-test
```
The first command prepares the additional C cross compiler inside the existing
Void build container, fetches the crates pinned in `Cargo.lock`, and cross-builds
static-musl `dasungd`. It verifies the ELF, builds the `fds-dasungd` XBPS overlay,
indexes the output, and compiles the actual s6 source into a validation database.
The second runs unit tests and a simulated serial monitor, then inspects the
package and compiles the service definitions extracted from that package. It also
runs that s6 graph with a native test binary, a read-only container root, and no
physical device access to check startup, logging, permissions, restart, and stop.
Python 3 is needed **only on the host for the simulator**. If absent on Arch,
install `python` with pacman. No Python interpreter is shipped for the daemon.
The first additional toolchain download is approximately 243 MB and needs roughly
1 GB of extra container space. Builds still depend on rolling Void binary inputs.
Run these commands sequentially with other FDS builds; they share the container.
| Output | Purpose |
| --- | --- |
| `out/dasungd` | Static AArch64 executable; no target glibc, musl, libusb, or libudev shared library dependency |
| `out/packages/fds-dasungd-0.1.0_1.aarch64.xbps` | Target package with executable, config, exact EDID, s6 source, udev rule, and licenses |
| `out/manifests/dasung-s6-database.txt` | Path to the service database compiled on the host for validation |
| `out/manifests/dasung-artifacts.sha256` | Executable and package hashes |
| `out/manifests/dasung-build-packages.txt` | Installed build-container input versions |
| `out/logs/dasung-build.log`, `dasung-checks.log` | Build and test evidence |
The standalone validation database contains only the Dasung subset. The M2
image assembler merges it with the init package and compiles the full current
graph while building the rootfs. No daemon is started on the workstation by these targets;
the integration test uses its own temporary socket and pseudo-terminal.
## What goes into the base system
`image/base-packages.list` makes `fds-dasungd` a mandatory hardware package.
`packages/fds-dasungd/template` is the active FDS-owned XBPS overlay. The upstream
Void tree remains pinned and unchanged. Source is maintained at `rust/dasungd/`;
the original task directory is not required for future builds.
The package installs:
- `/usr/bin/dasungd` and `/etc/dasungd.toml`.
- `/usr/lib/firmware/edid/dasung-paperlike13k-37hz.bin`.
- `/etc/s6-rc/source/dasungd`, its runtime-directory and log services, and the
`boot/contents.d/dasungd` membership entry.
- `/usr/lib/udev/rules.d/99-dasung-spi.rules`, adapted to eudev syntax.
- Documentation and the controller/libusb licenses.
s6 supervises the foreground daemon. A short oneshot creates private writable
runtime/log directories; it does not wait for the display. The daemon starts with
no monitor attached and retries discovery. Its initial service transition does
not wait for a monitor reply, a desktop, the network, or all cartridge bays.
The log service rotates a small log under `/run/log/dasungd`.
The init package adds a dependency on `runtime-fs` so the controller starts with
its kernel interfaces mounted. The service runs as root because it claims USB interfaces. Its socket
is under a root-owned mode-0750 directory; runtime control is restricted to root.
A future FDS CLI can expose selected operations through an explicit policy.
No world-writable USB rule, systemd unit, or runit service is installed.
## Confirmed profile and Pi-specific choices
| Property | Recorded value |
| --- | --- |
| Monitor | Paperlike 13K grayscale; `L56051794302` |
| Resolution | 3200 × 2400 |
| Pixel clock | **304210 kHz** (304.21 MHz) |
| Horizontal active / sync start / sync end / total | 3200 / 3248 / 3280 / 3360 |
| Vertical active / sync start / sync end / total | 2400 / 2423 / 2427 / 2447 |
| Sync polarities | Positive horizontal and vertical |
| EDID SHA-256 | `b6c1e0a8d315d9cf5c2fb83784a177530bcc085c2d0724dc7478a9c12449e4f4` |
| Control transport | USB UART `1a86:7523`, with companion SPI bridge `1a86:5512` |
| Startup parameters | Raw mode 1 and contrast 4, previously read back on this unit |
| Keepalive | `20 01` every two seconds; minimum 150 ms between command writes |
The video timing was confirmed on the original AMD workstation, **not on a Pi**.
The original USB-C video connection is not a Pi cabling prescription. Pi connector,
cable/adapter, mode acceptance, and picture stability require hardware validation.
Desktop 2× scaling is separate from EDID timing; a console uses its own font size.
FDS configures `display.enabled=false` and `hotplug="none"`: the daemon handles
USB control without applying the source project's AMD debugfs disconnect/reconnect
procedure. The Pi boot/display integration must supply the appropriate KMS/firmware
EDID configuration for its real connector. The Pi boot-volume builder now supplies
an unqualified firmware EDID override for this dedicated monitor; it does not
assume `DP-2` or a numbered HDMI connector. See the [boot guide](boot.md) for
the exact command line and the remaining physical mode test.
The daemon requires the configured monitor's DRM EDID and the matching USB
companion topology before claiming a device. Multiple matching UARTs are rejected;
`usb_path` can disambiguate a verified Pi topology. Do not substitute a blanket
match for all CH340 adapters or copy this unit's identity to a different monitor.
The original monitor's SPI driver interaction caused a dark picture after a power
cycle. `image/kernel/dasung.config` requires `CONFIG_SPI_CH341=n`; the Pi kernel
builder enforces this before boot, including the initramfs environment. The scoped
udev rule is secondary protection: it cannot prevent a built-in or already-loaded
SPI driver from probing. The daemon also reserves the companion interface without
sending SPI data and refuses to take it from an already-bound kernel driver.
## Runtime usage once installed in an FDS image
These are target commands, **not commands to run against the workstation's live
monitor during the build**:
```text
dasungd status
dasungd query
```
`status` reports whether the monitor is connected and responsive, the age of its
last reply and cached parameters. `query` requests current values. To refresh
the display or adjust contrast:
```sh
dasungd refresh
dasungd set contrast 4
dasungd set mode 1 --save
dasungd forget mode
```
`status` returns JSON including `connected`, `responsive`, reply age, and cached
parameters. A successful set means a packet was sent; use query/status to check
observed values. It is not proof of physical image quality. Color-model mode names
are not assigned to this grayscale monitor's raw numeric modes.
A successful set means the command was sent; use query/status to read back the
result. The grayscale model uses raw numeric modes. Saved overrides in
`/run/dasungd/settings.json` survive a daemon restart during the same boot, but
not power loss. Image configuration defaults are in `/etc/dasungd.toml`.
The default socket is `/run/dasungd/control.sock`. External programs can use its
newline-delimited JSON protocol; the daemon remains the only USB owner:
## Diagnose a connection problem
```json
{"op":"status"}
{"op":"set","parameter":"contrast","value":4,"save":true}
```
Inspect `/run/log/dasungd/current` and the reported status. Discovery requires
the configured DRM EDID and matching USB companion topology. An ambiguous match
is rejected. Set `usb_path` only after identifying the intended device topology.
In FDS's current payload, saved overrides live in `/run/dasungd/settings.json`.
They survive a daemon restart **within the same boot**, but not power loss.
Mode 1/contrast 4 are persistent image configuration defaults. Once persistent
machine-state mounts are implemented, point `state_file` at that internal writable
storage to retain user overrides across boots. Do not put it on read-only SYSTEM
or make the base display daemon depend on a removable DATA cartridge.
## Dependencies and source provenance
The imported Rust dependencies are retained and pinned by the workspace lockfile:
`rusb` wraps USB access; `libc` supports Linux serial/locking calls; `ctrlc` handles
termination; `clap` provides CLI parsing; `serde`, `serde_json`, and `toml` support
configuration and the local socket; `anyhow` supplies contextual errors.
Transitive versions are recorded in `Cargo.lock`.
The `vendored` feature builds libusb from the pinned `libusb1-sys` crate. The Void
`cross-aarch64-linux-musl` toolchain supplies C headers/compiler for that library;
Rust's bundled musl target alone was enough for the old pure-Rust smoke-test, but
is insufficient for this C dependency. `LIBUSB_NO_PKG_CONFIG` and
`LIBUDEV_NO_PKG_CONFIG` prevent accidental host/shared-library linkage. libusb uses
its Linux netlink backend. No libusb/libudev shared runtime package is needed.
The target package depends on GNU coreutils for directory preparation, execline
for service launch, s6/s6-rc for supervision, and eudev for device rules. These
match the planned base userspace. The container's s6-rc is a build-time compiler,
not a target PID 1 running on Arch.
[Import provenance](../rust/dasungd/IMPORT.md) records the original source hashes.
[Original validation](../rust/dasungd/VALIDATION.md) records the live handoff and
remaining physical test. Protocol and reconnect tests use simulated hardware;
Pi boot, image quality, physical disconnect/reconnect, and corrected cold-power
recovery must still be verified on the actual target. No boot benchmark is claimed.
FDS uses the daemon for USB control. Its configuration disables the original
workstation-specific display hotplug procedure. Video mode and cabling are part
of the machine's boot/display configuration. The detailed protocol, build
rationale and physical display procedures are in the
[developer reference](developer/dasung.md).
+29 -115
View File
@@ -1,138 +1,52 @@
# Writable DATA and managed programs
# DATA and persistent files
[Documentation index](README.md) · [Cartridges](cartridges.md) · [Services](services.md)
M7 implements writable DATA. Its sustained-write, safe-eject and surrounding
image regression checks [passed in the ARM VM](m7-validation.md). A DATA
cartridge uses a GPT partition named `FDS_DATA`, ext4, and a validated
`/FDS/CARTRIDGE.TOML` declaring class `data` and `writable = true`.
Use the [M9 media tools](media-tools.md) to create new DATA media. The virtual
tests construct disposable image files and never format a workstation disk.
## Where your files live
`/home/fds` remains a temporary home, whether DATA is present or absent. Its
contents disappear at power-off. Persistent user files belong under `/data`.
Separating these paths lets you eject DATA without hiding the active shell's
home directory or substituting another cartridge underneath it.
After inserting a DATA cartridge into a configured bay:
FDS keeps SYSTEM read-only. Store documents, source code and other persistent
files on a DATA cartridge. With one healthy DATA cartridge inserted, FDS mounts
it at `/data` and gives the `fds` user access to its files.
```sh
fds bays
fds cartridge 2
ls /data
fds bay 02
mkdir -p /data/projects
printf 'My first FDS file\n' >/data/projects/hello.txt
cat /data/projects/hello.txt
```
If exactly one valid DATA candidate is available and none is active, the daemon
activates it at `/data`. The filesystem must grant UID/GID 1000 the intended
write permissions; insertion does not recursively change ownership of user files.
M9's DATA formatter supplies that ownership for fresh media.
An active DATA session stays attached to its current cartridge when another is
inserted. If a discovery snapshot contains multiple candidates with none active,
they remain read-only until you select one:
Confirm that the bay reports `MOUNTED READ WRITE` before writing. Only one DATA
cartridge is active at a time. To select DATA explicitly:
```sh
fds data use 4
fds data use 02
```
Eject the existing active DATA before selecting a replacement. The daemon never
changes `/data` underneath an active session. EMPTY, ERROR and SAFE states retain
the meanings in the [cartridge guide](cartridges.md); MOUNTED READ WRITE means
that the displayed DATA filesystem is available for user writes.
If another DATA is active, eject it first. In recovery, DATA starts read-only and
requires this explicit activation before it can be used for ordinary writes.
## Run a program that participates in eject
## Programs using DATA
Ordinary console commands work as usual. They are not automatically killed when
you request eject: an open file or working directory may make eject fail as busy.
For a background task that should stop when its DATA is ejected, use:
Run a system command under the DATA bay's process tracking when it should stop
as part of ejecting that DATA:
```sh
fds run 2 -- /usr/bin/bash -c 'date > /data/managed-example.txt'
fds run 02 -- /usr/bin/tail -f /data/application.log
```
This starts a **background** managed program and prints its process ID. It has no
interactive input. Output goes to the bounded, volatile cartridge service log:
This background command uses `/data` as its working directory. Its output goes
to the cartridge service log. A directly launched PROGRAM is tracked under its
PROGRAM bay; close it if it keeps files open on a different DATA cartridge.
```sh
tail /run/log/cartridged/current
fds cartridge 2
```
The executable path must be absolute. The child starts with `/data` as its working
directory, the ordinary FDS UID/GID, no supplementary groups, no effective
capabilities, a small explicit environment, and `no_new_privs`. Programs cannot
gain privileges through setuid executables. The limit is 256 managed processes
per bay. This interface accepts healthy writable DATA; the [desktop guide](desktop.md)
also explains M8's explicit PROGRAM-media launch interface.
A root-owned Linux cgroup tracks the program and its descendants, including
children that outlive their parent. Cgroups are kernel process tracking, not a
new init system or runtime package. The configured kernel already includes the
required cgroup v2 and process-limit support. The daemon sets up its hierarchy
independently of the console. See the [kernel cgroup interface](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html).
## Eject and remove DATA
## Finish a session
```sh
cd "$HOME"
fds eject 2
fds eject 02
```
Eject checks for additional mounts, stops managed consumers, flushes the DATA
filesystem with `syncfs`, and performs a normal unmount. Only successful completion
produces SAFE. A retained filesystem descriptor observes writeback errors during
the session. M10 additionally makes the filesystem read-only and checks writeback
again before closing that descriptor for unmount, preventing an error-reporting
gap when a busy mount needs a later retry.
Linux's [syncfs interface](https://man7.org/linux/man-pages/man2/syncfs.2.html)
reports filesystem errors that must not be ignored.
Wait for SAFE, then remove the cartridge. `fds poweroff` performs the storage
shutdown sequence for all active bays. Unexpected power loss or pulling a drive
during a write can damage files; [recovery](recovery.md) explains checking DATA.
Managed programs receive TERM first. The daemon waits on actual cgroup exit
notifications; programs that have not exited after a one-second deadline are
killed as a group, including newly forked descendants. A further bounded exit
check must succeed. These are failure deadlines, not delays imposed on a program
that has already stopped. Requesting eject therefore ends those background jobs;
it does not promise that unfinished application work is completed.
An unmanaged busy shell/file, extra mount, failed flush or failed unmount prevents
SAFE. Close the reported use and retry. A recorded DATA writeback fault requires
investigation or recovery; a later successful call is not used to erase it and
pretend the failed write succeeded. The daemon never uses lazy/forced unmount to
claim successful eject. A successfully ejected insertion remains unmounted across
a daemon restart until removal/reinsertion.
M10 records writable sessions and faults outside the daemon process. If it crashes
before verified unmount, the same insertion stays quarantined and read-only on
restart. A later flush cannot reconstruct the lost error history. See
[Shutdown and recovery records](power.md#data-recovery-records) for the current
implementation and its acceptance status.
Pulling DATA during writes is an error. The daemon stops its managed consumers,
cleans up the vanished mount and logs that writes may have been lost. This path
never produces SAFE and makes no filesystem-cleanliness promise. Use recovery
inspection/repair before trusting media that was removed during I/O.
## Build and verify on the workstation
After changing DATA code or image configuration, build fresh inputs in order:
```sh
make rootfs PROFILE=cli
make initramfs
make system-card
make data-test
```
The test boots disposable virtual media, writes continuously, verifies ordinary
user privileges and descendant shutdown, checks busy and additional mounts,
reopens persisted data, tests multiple candidates and simulates surprise removal.
After safe eject it extracts the ext4 partition into an ordinary file, runs
read-only `e2fsck -fn`, and compares the sustained-write payload byte-for-byte.
No host mount or physical USB device is used.
Run `make cartridge-test`, `make init-test`, `make boot-test` and
`make console-test` for the surrounding regressions. Test logs and VM evidence
are written under `out/`. Real flash-controller caches, battery loss, USB power
and physical Pi eject/shutdown latency remain hardware acceptance work.
In the emulator, writes go to a separate session overlay. Eject preserves that
overlay, and reinserting the original image creates a new overlay. See
[exporting emulator DATA](workstation.md#save-emulator-data) before moving or
deleting a session.
+49 -194
View File
@@ -1,212 +1,67 @@
# Desktop, programs, and networking
# WindowMaker and FDS Control
[Documentation index](README.md) · [Cartridge guide](cartridges.md) · [DATA guide](data.md)
WindowMaker provides the FDS desktop. Its default appearance is grayscale on
all displays: white backgrounds, black borders and selected titles, gray widgets,
Terminus text, and no animations. No E-Ink monitor detection or special cartridge
is needed to select this theme.
M8 software checks passed in the ARM virtual machine. The [validation report](m8-validation.md)
records desktop, program and networking evidence. Raspberry Pi video output and
Dasung display quality remain deferred physical checks.
## Open the desktop
## What starts when you boot
FDS opens its ordinary-user console first. Xorg, WindowMaker, and the DHCP client
are installed in SYSTEM but are absent from the boot bundle. Dasung control
remains in the base boot bundle, including when no desktop is running.
The default graphical backend is Xorg on virtual terminal 2, using its built-in
modesetting driver. The physical keyboard and pointer use libinput. Console
access remains on the original terminal. There is no display manager. Physical
VC4 output, monitor resolution, and E-Ink appearance still require Pi testing.
## Start and stop the desktop
At `FDS>`:
At the FDS console:
```sh
fds profiles
fds profile activate windowmaker
fds profiles
```
Activation is asynchronous. The second status command reports the selected
profile and an activation-to-ready duration once WindowMaker is ready. This
measurement starts at the activation request, so time spent deciding when to
start the desktop is excluded. `fds boot-profile` also records the first desktop
readiness event for the boot. Neither measure includes firmware or power-on.
FDS opens a terminal and **FDS Control**. You can reopen the panel from the
WindowMaker root menu's **FDS Control** item, or run `fds-control` in a terminal.
An ENVIRONMENT cartridge requesting the `windowmaker` profile can also start
the desktop. The Dasung controller runs independently of this session.
The desktop opens an FDS terminal. Right-click its white background to open the
menu, launch another terminal, enable/disable Ethernet, or return to the console.
To stop it from either a terminal or the console:
## Manage cartridges in the panel
![FDS Control with a PROGRAM cartridge and its terminal output](images/fds-control.png)
1. Select a bay in the left column. The right column shows its identity, state,
running process count and available software commands.
2. Select a command, then choose **Run in terminal**. The terminal stays open
after the command exits so you can read its output; close it when finished.
3. Choose **Safe eject** to stop that bay's programs and release its storage.
Wait for **SAFE** before removing the cartridge.
4. Use **Rescan** to refresh discovery. The panel also checks for changes
automatically and updates the display when the visible state changes.
For an inactive DATA cartridge, the primary button becomes **Use DATA**. Activate
it there to make it available at `/data`; eject another active DATA first.
Errors appear in the status area. Actions are unavailable when their prerequisites
are missing, such as running an application from an empty bay or ejecting the
active SYSTEM. The panel uses the same cartridge service as `fds`.
Keyboard controls: **Tab** moves focus between bays, programs and buttons;
**Up/Down** changes the selected bay or program; **Enter** activates the focused
control; **R** rescans; **Esc** closes the panel. Closing the panel does not eject cartridges or close
application terminals; use Safe eject when you want to release a cartridge.
## Return to the console
Choose **Return to console** in the root menu, or run:
```sh
fds profile deactivate
```
Stopping the desktop closes its terminal and all processes descended from its
session. Save work first. Files in `/home/fds` survive desktop restarts during the
same boot; the home directory is still temporary and disappears at shutdown.
Use [DATA](data.md) for persistent files. Removing an active DATA cartridge also
stops the desktop before flushing DATA, because desktop applications may use it.
This closes the desktop session. It does not shut down FDS. Use `fds poweroff`
when you want to turn the computer off.
`fds-profile status`, `fds-profile activate windowmaker`, and
`fds-profile deactivate` provide the standalone static helper interface.
## Customize the appearance
## ENVIRONMENT cartridges
System defaults are installed in `/etc/WindowMaker/`. Your files in
`$HOME/GNUstep/Defaults/` override those defaults. The session copies the FDS
settings into a new home once and preserves existing preferences. Use
WindowMaker's settings tools or edit those preference files to change fonts,
colors and behavior. Plain `wmaker` also reads the FDS system defaults.
An ENVIRONMENT cartridge is a read-only EROFS partition named `FDS_ENVIRONMENT`.
Its `FDS/CARTRIDGE.TOML` uses the [documented manifest format](cartridges.md), with:
```toml
[activation]
profile = "windowmaker"
```
When exactly one eligible ENVIRONMENT cartridge is present, the daemon requests
WindowMaker after console readiness. The graphical runtime comes from SYSTEM;
the cartridge provides a declarative request, never a privileged executable.
Unknown profile names do not execute anything. If several eligible cartridges
arrive together, select a profile explicitly instead of relying on bay ordering.
Eject its bay with `fds eject N`. The desktop stops before SAFE is reported.
Surprise removal also stops its associated desktop. Manually deactivating while
the cartridge remains inserted suppresses automatic reactivation until removal
and reinsertion. A manually started desktop is independent of ENVIRONMENT media.
A cartridge-service restart reevaluates mounted media and retires old optional
sessions; this is a maintenance operation, not a way to preserve desktop jobs.
## E-Ink defaults
`fds-eink` installs the WindowMaker policy and Terminus fonts: a white background,
black text and borders, grayscale controls, outline movement/resizing, no
animations, no blinking decorations, no app-icon bounce, no dock, no compositor,
and no periodically updating clock. X11 compositing is disabled. The terminal
uses black on white and a nonblinking cursor.
Defaults are copied into the ordinary user's `~/GNUstep/Defaults` at the first
session. Later sessions preserve edits there. They are temporary unless you
explicitly store a copy on DATA. The authoritative image defaults live in
`/usr/share/fds/eink`; edit their package sources and rebuild SYSTEM to change the
machine-wide defaults.
Font caches and X11 font indexes are generated while building the image.
An active X server's keyboard map is session state, generated only on desktop
activation. That is separate from global boot-time cache generation.
## Launch a software cartridge
Build current software cartridges on a [Linux workstation](workstation.md). They
contain metadata plus xz software bundles in payload partitions. In FDS:
```sh
fds bay 4
fds run 4 -- demo.hello:hello
fds eject 4
```
Use the software id and command listed by `fds bay`. The guest verifies and
extracts the bundle into a read-only temporary cache, then starts it as UID 1000.
There are no guest software build hooks.
### Legacy PROGRAM compatibility
Existing PROGRAM media contains a read-only EROFS partition named `FDS_PROGRAM`, metadata
in `FDS/CARTRIDGE.TOML`, and its self-contained application files:
```text
app/bin/editor
app/lib/
app/share/
```
FDS validates and mounts it at `/run/fds/apps/<cartridge-id>`. Insertion does not
run anything, and the mount initially disallows execution. To launch an executable
named `editor` from bay 4:
```sh
fds cartridge 4
fds run 4 -- editor
```
Use a simple executable name from `app/bin`, followed by that program's arguments.
The daemon enables read-only execution and starts it as UID 1000, with no added
privileges. Executable paths must remain within the cartridge's `app` directory.
Programs receive `FDS_APP`, a PATH including `app/bin`, `LD_LIBRARY_PATH` including
`app/lib`, and `XDG_DATA_DIRS` including `app/share`. Bundled libraries can also use
an executable-relative RPATH. FDS does not resolve dependencies across cartridges.
Programs are background jobs. Graphical programs can use the active authenticated
X display. Standard output/error goes to the bounded, temporary cartridge log:
```sh
tail /run/log/cartridged/current
fds bay 4
fds eject 4
```
The bay status reports managed processes. Eject stops the whole process group,
including forked descendants, before unmounting. The existing DATA form remains
`fds run N -- /absolute/system/command arguments`, using `/data` as its working
directory. DATA itself remains mounted with execution disabled.
## Ethernet on demand
External networking is off until explicitly requested or a mapped USB Ethernet cartridge
produces a real network interface. Interfaces are associated with their USB
ancestors; a descriptive label alone does not start networking. Wi-Fi setup is
not implemented by this Ethernet policy. The local loopback interface is always
up for applications on the same machine; it does not enable external traffic.
```sh
fds network on
fds profiles
ip -brief address
ip route
fds network off
```
Enabling networking starts DHCP without waiting for a lease. An absent server
cannot delay `FDS>`. Explicit activation selects available Ethernet interfaces;
automatic activation selects interfaces under mapped cartridge USB devices.
Stopping networking stops DHCP, brings the managed interfaces down, and clears
temporary DNS settings. This initial profile uses IPv4 DHCP; it disables kernel
IPv6 autoconfiguration on those interfaces. After an explicit stop, an inserted Ethernet
cartridge remains suppressed until it is removed, or you enable networking again.
Leases, DNS settings, and logs live under `/run`; nothing makes SYSTEM writable.
`/etc/resolv.conf` points to `/run/fds/resolv.conf`. Inspect
`/run/log/network/current` when an address is missing.
## Development and troubleshooting
The production console image includes the real desktop stack. The development
image adds Xvfb and X11 diagnostics for a virtual display without a connected GPU:
```sh
make rootfs PROFILE=development
make desktop-test
```
The test creates its own disposable image selecting Xvfb; the normal development
image still selects Xorg. Never mistake an Xvfb pass for Pi graphics verification.
Changing the SYSTEM image profile requires building that rootfs first; the image
builder rejects a profile label that disagrees with its embedded identity.
If desktop activation fails, `fds profiles` reports it. Read
`/run/log/xserver/current` and `/run/log/desktop/current`, then deactivate before
retrying. Xorg's detailed log is `/run/log/xserver/Xorg.0.log`. X access requires
an authority cookie readable only by root and the FDS group; do not use `xhost +`
or disable access control as a workaround.
Dependency rationale: Xorg supplies the display server and modesetting driver;
libinput supplies keyboard/pointer input; WindowMaker supplies window management;
Terminus and fontconfig supply the typography; xterm supplies a terminal; the static FDS helper
observes X11 property events for readiness without polling delays; xset/xsetroot apply the
static session settings. Xvfb, xdotool, xwd, xwininfo, xdpyinfo, xprop, and xauth are
optional development diagnostics. `fds-dhcpcd` builds the pinned upstream DHCP
client with privilege separation and volatile state paths, omitting its runit
service files. All FDS control helpers remain static Rust/musl binaries.
The readiness observer uses the [X11 core protocol](https://xorg.freedesktop.org/archive/X11R7.7/doc/xproto/x11protocol.html)
and establishes its property subscription before launching the window manager.
Font caches are generated with the image's normalized timestamp, following
[fontconfig's reproducible-cache support](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user.html),
so exporting the filesystem does not invalidate their directory timestamps.
The root menu includes Ethernet on/off controls. Equivalent console commands
are `fds network on`, `fds network off`, and `fds profiles` to inspect the result.
+80
View File
@@ -0,0 +1,80 @@
# Developer notes
This is the engineering side of FDS: how the pieces are built, why choices were
made, and what we actually measured. The [user manual](../README.md) describes
how to use the current interfaces without the milestone history.
Start with [the active cartridge/desktop revision](current-revision.md). Its
acceptance is separate from the frozen 0.1.0 release. Old test reports describe
the inputs named in those reports; a changed source tree needs fresh checks.
## Design and build references
- [Master plan](master-plan.md)
- [Build host and dependencies](build-host.md)
- [Development workflow](development.md)
- [Workstation tooling plan](workstation-tooling-plan.md)
- [Architecture](architecture.md), [packages](packages.md), [rootfs](rootfs.md), [init](init.md), [services](services.md)
- [Reproducible builds and frozen releases](reproducible-builds.md)
- [Dasung integration and physical monitor procedure](dasung.md)
- [Physical bay calibration and stress procedures](stress-testing.md)
- [Implementation ledger](implementation-status.md) and [roadmap](roadmap.md)
## Acceptance history
- [clap-validation](clap-validation.md)
- [dasung-validation](dasung-validation.md)
- [m0-validation](m0-validation.md)
- [m1-validation](m1-validation.md)
- [m10-validation](m10-validation.md)
- [m11-validation](m11-validation.md)
- [m12-validation](m12-validation.md)
- [m2-validation](m2-validation.md)
- [m3-validation](m3-validation.md)
- [m4-validation](m4-validation.md)
- [m5-validation](m5-validation.md)
- [m6-validation](m6-validation.md)
- [m7-validation](m7-validation.md)
- [m8-validation](m8-validation.md)
- [m9-validation](m9-validation.md)
- [workstation-validation](workstation-validation.md)
## Earlier guides and working notes
These retain useful implementation details and the historical interfaces that
went with them. Use the user manual for current commands, especially software
cartridge creation: new payloads contain installed Void package trees.
- [architecture](architecture.md)
- [boot](boot.md)
- [build-host](build-host.md)
- [cartridges](cartridges.md)
- [cleanup](cleanup.md)
- [dasung](dasung.md)
- [data](data.md)
- [desktop](desktop.md)
- [development](development.md)
- [eeprom](eeprom.md)
- [getting-started](getting-started.md)
- [glossary](glossary.md)
- [implementation-status](implementation-status.md)
- [init](init.md)
- [internal-storage](internal-storage.md)
- [m4-work](m4-work.md)
- [master-plan](master-plan.md)
- [media-tools](media-tools.md)
- [packages](packages.md)
- [performance](performance.md)
- [power](power.md)
- [recovery](recovery.md)
- [releases](releases.md)
- [reproducible-builds](reproducible-builds.md)
- [roadmap](roadmap.md)
- [rootfs](rootfs.md)
- [services](services.md)
- [software-format](software-format.md)
- [stress-testing](stress-testing.md)
- [tooling](tooling.md)
- [troubleshooting](troubleshooting.md)
- [workstation-tooling-plan](workstation-tooling-plan.md)
- [workstation](workstation.md)
@@ -1,5 +1,8 @@
# Understanding FDS/OS
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Roadmap](roadmap.md) · [Glossary](glossary.md)
FDS/OS is designed for a Raspberry Pi 5 portable computer whose operating system,
+3
View File
@@ -1,5 +1,8 @@
# Boot images and stage0
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Implementation status](implementation-status.md)
FDS keeps the machine's boot files on internal NVMe and its operating system on a
@@ -1,5 +1,8 @@
# M0: Arch Linux x86_64 build host
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Troubleshooting](troubleshooting.md)
This is the technical reference for the build foundation used by later milestones.
@@ -41,15 +44,20 @@ explicitly use `XBPS_ARCH=x86_64` for the glibc build container, then run
`./xbps-src -a aarch64 pkg hello` to build the ARM glibc package. Local ARM package
indexing also explicitly sets `XBPS_ARCH=aarch64`.
## Separate generic Linux workstation tools
## Generic Linux workstation tools and desktop additions
`make workstation` builds native `fds-cartridge` and `fds-emulator` without the
Void/Arch bootstrap. Their runtime dependencies are QEMU (`qemu-system-aarch64`
and `qemu-img`), erofs-utils, xz and bubblewrap. A software recipe chooses its
cross compiler. The shared archive reader adds Rust `tar` 0.4.46 and its
`filetime` dependency; both are locked. No new daemon or host build tools are
installed in the Pi base image. Follow [the workstation guide](workstation.md).
The full OS build prerequisites below retain their existing scope.
`make workstation` builds native `fds-cartridge` and `fds-emulator`. Runtime
requirements are QEMU, erofs-utils, bubblewrap and native XBPS with a prepared
Void source checkout for software creation. `fds-cartridge` invokes xbps-src,
installs runtime dependencies and creates direct EROFS trees. The Rust tar reader
and xz utility remain for legacy cartridges and package/source archives. See
[the current workstation guide](../workstation.md).
The target `fds-control` panel uses x11rb 0.13.2's pure-Rust core X11 connection.
It adds no shared GUI toolkit or background service. Its protocol, hostname and
OS-access dependencies are locked and included in Rust license notices. Both
the panel and foreground launcher are packaged in fds-cli as static ARM binaries.
See [the revision rationale](current-revision.md).
## Host dependencies
@@ -173,6 +181,11 @@ runtime or Pi boot test.
## Inputs, caches, and overlays
Use `make clean-preview` followed by `make clean` when repeated builds and VM
tests accumulate. [Cleanup](cleanup.md) explains the retained current images,
release/input archives and caches. Full image builds and VM suites need tens of
GiB per working tree, beyond the initial M0 smoke test's requirements.
- `.host/xbps/`: project-local host tools, not installed into `/usr`.
- `vendor/void-packages/masterdir-x86_64/`: glibc build container.
- `vendor/void-packages/hostdir/`: download caches, cross packages, and build output.
+200
View File
@@ -0,0 +1,200 @@
# Using cartridges and configuring bays
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Boot images](boot.md) · [Services](services.md)
M6 adds the base `fds-cartridged` service, twelve bay states, USB hardware
recognition, strict metadata inspection, read-only storage mounts and eject.
Its [ARM VM acceptance checks passed](m6-validation.md). [Writable DATA integration](data.md) has passed M7 software acceptance;
[desktop/program activation](desktop.md) passed M8 and [media creation tools](media-tools.md)
passed M9 software acceptance. [M10 ordered shutdown](power.md) has passed software acceptance.
## Inspect the current machine
At the FDS console:
```sh
fds bays
fds bay 3
fds cartridge 3
fds --json bays
```
The ordinary `fds` user can use these commands. `bays` shows all twelve bays,
including empty and unconfigured ones. `cartridge` includes validated metadata
and the current mount path. `--json` exposes a versioned machine-readable report.
`fds rescan` explicitly refreshes inventory; normal insert/remove events trigger
refresh automatically. The console does not wait for the inventory scan.
| State | Meaning and next action |
| --- | --- |
| UNCONFIGURED | No measured controller/port mapping exists for this bay |
| EMPTY | Configured bay with no detected USB device |
| HARDWARE | USB device without storage; catalog name or VID/PID is shown |
| UNRECOGNIZED STORAGE | Storage exists but no named FDS partition is available |
| MOUNTED READ ONLY | Manifest validated; filesystem available at the displayed path |
| MOUNTED READ WRITE | Active DATA is writable at `/data`; eject before removal |
| PROTECTED | The active SYSTEM root; eject is refused |
| AMBIGUOUS | Multiple devices or named partitions match; nothing is selected arbitrarily |
| ERROR | Inspection, mounting or metadata validation failed; read the diagnostic |
| SAFE | The requested unmount succeeded; remove the cartridge |
An unrecognized storage state may briefly appear while the kernel discovers its
partitions. It is not permission to remove a device being used elsewhere.
Errors and ambiguous media never produce SAFE. An additional mount of the same filesystem also blocks managed eject until
it is unmounted.
## Calibrate the physical bay map
The shipped `/etc/fds/bays.toml` is deliberately empty: no Pi bay wiring has yet
been measured. This produces UNCONFIGURED states, not invented assignments.
1. On the assembled machine, insert one known USB device into one physical bay.
2. Run `fds topology`. This diagnostic explicitly shows controller/port paths;
ordinary bay commands omit those implementation details.
3. Record that path and repeat for each bay. Test both USB 2 and USB 3 devices,
because their companion root hubs can have different logical port paths.
4. Put the measured map in your machine configuration directory and build the
[internal image](internal-storage.md), or use `fds machine install DIRECTORY`
from recovery and reboot. Machine settings persist independently of SYSTEM.
The packaged `/etc/fds/` files are fallback defaults for missing/invalid internal
storage; `fds machine status` shows which source is active.
5. Verify all twelve devices together, in different insertion orders and after
reboot. That physical acceptance remains deferred.
A **syntax example only**, using identities that must be replaced by observations:
```toml
[front]
hub = "platform/example-controller:usb2/1"
[front.ports]
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
6 = 6
[front_superspeed]
hub = "platform/example-controller:usb3/1"
[front_superspeed.ports]
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
6 = 6
```
`hub` names the controller and parent port chain. Its numbered `ports` map the
next downstream port to a bay number from 1 through 12. Additional groups can
map the rear hub. Explicit USB 2/3 aliases may refer to the same bay; duplicate
ports, overlapping parent/child mappings, and duplicate bays within one hub are
rejected. If two devices simultaneously match aliases for one bay, it becomes
AMBIGUOUS. A hub inserted into a cartridge bay with multiple downstream devices
also needs an explicit future composite-device policy; M6 refuses to guess.
The identity excludes the Linux USB bus number and `/dev/sdX` enumeration order.
It includes the controller's sysfs path, USB protocol generation and port chain.
Moving a hub to another controller or changing wiring requires recalibration.
## Storage formats and metadata
| Class | GPT partition name | Filesystem and current behavior |
| --- | --- | --- |
| SYSTEM | FDS_SYSTEM | EROFS; current root is protected, additional media read-only |
| DATA | FDS_DATA | ext4; inspected read-only first, then activated at `/data` under the M7 policy |
| PROGRAM | FDS_METADATA + FDS_PAYLOAD02… | Metadata and xz bundles in `1+m` EROFS partitions; see [software format](software-format.md) |
| Legacy PROGRAM | FDS_PROGRAM | Existing single EROFS application tree; still readable |
| ENVIRONMENT | FDS_ENVIRONMENT | EROFS; declarative selection of a trusted built-in profile |
| UTILITY | FDS_UTILITY | EROFS; inspection only, no automatic actions |
| HARDWARE | None required | VID/PID, optional serial, device/interface class and bay |
Storage contains `/FDS/CARTRIDGE.TOML`. For example:
```toml
format = 1
[cartridge]
id = "fds.windowmaker"
name = "WINDOW SYSTEM"
class = "environment"
version = "0.1"
[media]
writable = false
[activation]
profile = "windowmaker"
```
The manifest class must match the GPT partition name. SYSTEM, PROGRAM and
ENVIRONMENT are read-only; DATA declares writable media during the initial read-only inspection. Unknown keys, root commands, unsafe identifiers, control
characters, symlink metadata and nonregular metadata files are rejected.
The parser accepts at most 64 KiB. No `/FDS/autorun.sh` is run.
Inspect a standalone manifest with `fds inspect /path/to/CARTRIDGE.TOML`.
For mounted storage, `fds cartridge N` reports the daemon's validated copy.
Media is inspected in a root-only staging directory, then published at
`/run/fds/media/NN` after validation, with nodev, nosuid and noexec. Device
major/minor and kernel disk sequence are checked before mounting an opened
block-device descriptor, preventing stale enumeration names from selecting a
replacement disk.
## Eject a mounted cartridge
```sh
fds eject 2
```
Leave any shell working directory inside that cartridge and close files first.
SAFE is emitted only after an ordinary unmount succeeds. Busy mounts return an
error; forced or lazy unmount is never used to declare safe removal. Once SAFE,
the daemon keeps that insertion unmounted until removal and reinsertion, including
across a service restart. Its root-owned volatile marker is tied to the kernel
disk sequence, so a replacement disk is not mistaken for ejected media.
The active SYSTEM cannot be ejected. Swap SYSTEM only after shutting down.
Pulling a mounted cartridge without eject is surprise removal. The daemon clears
its inventory and detaches a vanished read-only mount where needed; that cleanup
is not a successful eject. [M7 DATA handling](data.md) extends ejection to writable DATA, consumer
tracking and syncfs. The temporary `/home/fds` remains usable without DATA and is lost at
power-off.
## Hardware recognition
Edit `packages/fds-cartridged/files/hardware-catalog.toml`, then rebuild SYSTEM.
Entries name devices, never executable actions:
```toml
[[device]]
name = "MY SERIAL ADAPTER"
vendor = "1234"
product = "5678"
# Optional exact restrictions:
serial = "UNIT-1"
class = "02"
```
Replace these example IDs with observed values. Device or interface class may
match `class`. Multiple matching catalog entries produce an error; unknown
hardware remains identified by VID/PID without being mistaken for empty media.
Dasung control remains in the independent base monitor service.
## Implementation and diagnostics
The daemon subscribes to kernel USB/block events before taking its first snapshot.
A receive-buffer overflow causes a fresh snapshot. IPC uses a bounded Unix socket
at `/run/fds/control.sock`, mode 0660 and group `fds`, with peer UID checks for
root and the ordinary FDS user. Slow clients have individual deadlines and do not
block other clients. No asynchronous runtime or new external Rust dependency is
introduced: the existing std, libc, serde, serde_json and toml components suffice.
Root diagnostics: `s6-svstat /run/service/cartridged` and
`cat /run/log/cartridged/current`. Logs are bounded and volatile. Restart through
`s6-rc -l /run/s6-rc -d change cartridged`, then the corresponding `-u` command.
Close users of cartridge mounts first; restart cleanup refuses busy leftovers.
On the build host, `make cartridge-test` runs fixtures and full virtual USB tests.
These exercise actual kernel events and mounts but cannot establish physical
wiring, USB power stability, or Pi port behavior. M11 expands to twelve-device
stress, and physical calibration must be recorded separately.
@@ -1,5 +1,8 @@
# Clap command-line migration validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Command usage](tooling.md) · [M12 evidence](m12-validation.md)
Software acceptance completed on 2026-09-21. Every Rust command-line interface
+131
View File
@@ -0,0 +1,131 @@
# Reclaim build space
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
Run these from the repository root after stopping builds and test VMs:
```sh
make clean-preview
make clean
```
The preview lists every selected directory, its combined allocated footprint,
and the generated directories being kept. It does not delete files. `make clean`
recalculates the selection and deletes it without a confirmation prompt. Repeat
it whenever old build/test workspaces accumulate. It needs Python 3, Git and GNU
`du`, already used by the build host; it needs no Rust compilation, downloads,
root privileges or new Python packages.
## What is removed
- Old generated rootfs, kernel, initramfs, SYSTEM, boot, recovery and internal
image workspaces that are no longer referenced by published output links.
- Old generated integration-test directories, including their disk images,
disposable test DATA overlays and local diagnostic logs. The current results
referenced by `*-latest` links or workstation current-pointer files are kept.
- Old generated package staging copies and service compilation directories.
- This checkout's `target/` Rust compilation output. Exported executables in
`out/` and `out/workstation/` remain available. The next Rust build recompiles
as needed using the retained Cargo source cache.
The command recognizes the specific temporary-directory names produced by the
repository's builders and tests. It never treats all of `out/` as disposable.
Unknown names are left alone, including ad hoc diagnostic experiments.
## What is kept
- Current rootfs archives for every profile, kernel, initramfs, boot, recovery,
internal disk and SYSTEM images, through their published symlinks. Links in
`out/manifests/` and links within retained workspaces also retain their targets.
- Latest published test runs, the current workstation image/emulator fixtures,
the current Dasung s6 database, and the newest `m9-images.*` fixture needed by
the media tests. These can still occupy tens of GiB.
- Signed releases such as `out/fds-os-0.1.0/`, input snapshots such as
`out/inputs-m12-v5/`, and independent restored trees such as
`out/rebuild-m12-v5-a/`. These remain available for release reproduction.
- User-created cartridge images, software bundles and personal emulator sessions
such as `out/my-emulator/`, including their persistent DATA overlays.
- `out/logs/`, `out/manifests/`, packages, downloads, `out/cache/`, `.host/`, the
Void checkout/build container, and Cargo/Rustup caches outside this checkout.
- Git-tracked files and any recognized workspace containing a `.fds-keep` entry.
To retain an older generated workspace for investigation, put a marker in it
before cleaning:
```sh
touch out/m8-vm.YOUR_RUN/.fds-keep
make clean-preview
```
Replace `YOUR_RUN` with the actual directory suffix. Remove that marker when the
workspace is disposable again. Reserve the generated names for the build/test
tools; store personal sessions and images under your own names.
Cleanup leaves historical acceptance reports intact. Reports mentioning deleted
older workspaces are historical records; their files cannot be rechecked until
rebuilt, and they never prove that changed source code passed acceptance.
## Why the directory became so large
Image builders and integration tests deliberately retain their working
directories so failed runs can be inspected and a failed build cannot replace a
good published image. Repeated suites therefore accumulate complete rootfs
trees, archives and several disk images per case. Before this target existed,
this checkout's `out/` measured approximately **900 GiB** of allocated file
blocks after repeated M0–M12 and workstation validation runs. That is accumulated
history, not the size of a single OS image.
`make clean` removes superseded workspaces; it does not impose automatic
retention during a build. Use the preview periodically. For complete builds,
allow tens of GiB per independent tree plus space for VM tests and saved inputs;
the small initial smoke test's space requirement does not cover the full suite.
The preview uses allocated blocks, rather than adding the apparent capacities
of sparse VM disks. Hardlinks are counted once within the selection. Reflinks,
filesystem snapshots and hardlinks into retained directories can reduce the
physical space actually recovered. The command also prints the observed change
in filesystem free space; other processes and delayed filesystem accounting can
affect that number.
## Running safely and rebuilding
Use cleanup sequentially, just like the existing shared-container builds. It
holds the existing rootfs/base-package/image-tool locks, excludes another
cleanup, and refuses mounted candidate paths or observable active build/VM
processes. These checks are safeguards, not permission to start a separate build
while cleanup is running. It never follows a candidate symlink into another
directory, and refuses a symlinked `out/` entirely. Only the checkout containing
the script is eligible; there is no arbitrary deletion-path option.
Existing published images and native tools remain usable after cleanup. To
refresh exported binaries, run `make tooling` or `make workstation`. To rebuild
the smoke-test artifacts and rerun build checks:
```sh
make smoke-test
make check
```
`make check` needs the smoke-test binary in `target/`, so run the smoke test first
after cleaning. `make clean-test` independently exercises cleanup selection,
preserved releases/data, repeated runs, symlinks, tracked files, read-only guest
directories, hardlinks, locks, live processes and mount rejection using tiny
temporary repositories. It does not build OS images or boot VMs.
## Recorded cleanup validation
On the development checkout, cleanup removed 576 disposable directories with
a 638.00 GiB allocated footprint. The filesystem reported 605.86 GiB more free
space immediately afterward; `out/` dropped from about 900 GiB to 268 GiB.
The retained space includes independent release rebuilds, input snapshots and
current image/test workspaces. A second preview selected zero directories.
All 52 recorded current image/tool and frozen release file hashes remained
identical, and all 33 published links remained intact. `make bootstrap`,
`make smoke-test`, `make check` and all nine cleanup fixture tests passed.
The smoke test skipped its optional ARM execution because host `qemu-aarch64`
was unavailable; its ELF checks passed. This validates host cleanup and does
not add any physical Pi or monitor acceptance evidence. The local command log
is `out/logs/cleanup.log`, with a summary in
`out/manifests/cleanup-acceptance.json`.
+122
View File
@@ -0,0 +1,122 @@
# Cartridge and desktop revision
This work replaces the custom software bundle workflow and separates the user
manual from development history. The accepted 0.1.0 release stays unchanged.
## Required outcomes
1. Professional user documentation contains installation, usage and reference
material. Milestone history, measurements, test gaps and implementation notes
live separately under `docs/developer/`.
2. `fds-cartridge` builds Void source packages with `xbps-src`, installs their
runtime dependencies on the workstation, and writes installed program trees
directly into EROFS payloads. New cartridges require no guest extraction.
Existing cartridge reading stays available.
3. The grayscale E-Ink WindowMaker appearance is the default desktop theme on
every display, with normal user customization still possible.
4. Validated cartridge commands become available in existing shells through a
stable PATH directory. Direct launches retain terminal behavior, arguments,
exit status, dependency paths and managed eject/unplug cleanup. Command name
collisions have deterministic, documented behavior.
5. A native Rust X11 control panel with a matching retro appearance manages bay
status, cartridge details, program launch and safe eject from WindowMaker.
## Implementation and acceptance
- Replace creation of custom xz bundles with typed Clap source-package options,
installed-tree metadata, deterministic integrity checks and direct mounts.
- Verify real Void builds, dependency installation and resulting image contents;
retain corruption, filesystem and write/readback rejection coverage.
- Add managed PATH launch registration and exercise foreground commands,
collisions, eject, surprise removal and service restart in the ARM guest.
- Package and integrate the X11 panel and defaults. Exercise the actual window,
input actions and rendered appearance in the guest desktop.
- Rebuild CLI and development images. Run workstation/emulator, rootfs/init and
relevant cartridge/DATA/desktop/shutdown regressions against changed sources.
- Rewrite and relocate documentation after the final interfaces are verified.
Keep hardware-specific validation procedures and limits in developer notes.
- Use `make clean-preview` and `make clean` between completed validation batches
to avoid retaining hundreds of GiB of obsolete images again.
Current status: all five changes are implemented and the fresh host/ARM VM
acceptance below has passed. Physical checks remain a separate hardware task.
## Fresh acceptance
The validation logs are retained under `out/logs/cartridge-desktop-revision/`.
`out/manifests/cartridge-desktop-revision.json` records the actual working-tree
source hashes, artifact hashes, fixture identities and log hashes. The source
revision is uncommitted; HEAD alone does not identify these changes.
These checks ran against this revision, independently of the frozen release.
| Check | Evidence and scope |
| --- | --- |
| Build host | `make bootstrap`, `make smoke-test`, `make check`; all passed. The optional host-native `qemu-aarch64` shortcut was skipped; ARM execution uses the prepared Void environment. |
| Root filesystems | `make rootfs PROFILE=cli` and `PROFILE=development`, with `make rootfs-test` for each: passed. Archives `rootfs-build.9f2sNj` and `rootfs-build.PIumlA` contain byte-identical copies of the exported FDS tools and Dasung daemon. |
| Boot / tools | `make init-test`, `out/m2-vm.ATiMG0`: native s6 PID 1, service control and orderly poweroff passed. `make tooling-test` passed shared contracts and real ARM diagnostic execution. Both SYSTEM images and initramfs were rebuilt. |
| Rust | Host tests for common contracts, software, workstation tools, cartridge service, CLI and X11 panel; static-musl ARM compilation passed. |
| Software creation | `make workstation-test`, `out/workstation-images.f6tq1mya`: actual xbps-src builds, installed dependencies, EROFS trees, deterministic image bytes, malformed-content rejection and confirmed file write/readback. |
| Emulator / foreground commands | `out/emu-test.tqxiaglf`: direct PATH and qualified names, literal arguments, pipes, exit status, cwd permissions, interactive input, Ctrl-C, suspend/resume, terminal restoration, collisions, unplug, daemon restart and legacy format-1 archive compatibility passed. |
| Cartridge control | `make cartridge-test`, `out/m6-vm.e5j5n3q5`: active SYSTEM protection, unprivileged IPC, bounded clients, mounts, busy/safe eject, hotplug, removal and restart passed. |
| DATA | `make data-test`, `out/m7-vm.oiam2j2z`: sustained writes, privilege drop, descendant cleanup, sync/unmount, persistence, multiple candidates, interrupted media and surprise removal passed. |
| Desktop | `make desktop-test`, `out/m8-vm.zgk_yiyn`: real ARM Xvfb/WindowMaker, authentication, UID 1000 panel, global grayscale defaults, mouse launch of legacy and Void programs, keyboard rescan, safe eject and session cleanup passed. ENVIRONMENT and isolated DHCP regressions also passed. |
| Shutdown | `make power-test`, `out/m10-vm.rx6kz1zd`: ordered poweroff/reboot, writeback, active preparation, quarantine and refusal on unsafe storage passed. |
| Documentation | User instructions and development history are separated; all 66 Markdown documents had balanced code fences and valid relative file links. The actual panel screenshot was visually reviewed. |
The [desktop screenshot](../images/fds-control.png) comes from the ARM guest,
not a mockup. Additional shell/where commands in its example cartridge belong
to the test fixture; the ordinary example publishes hello and report.
## Validation limits
The host used for acceptance is x86_64 Arch Linux with the prepared Void runner.
The native tool interface supports other Linux workstations with the documented
prerequisites, but this run does not establish coverage of every distribution.
No physical Raspberry Pi boot, USB bay calibration, flash power-loss durability,
Dasung power-cycle recovery, E-Ink refresh quality or hardware latency was tested.
Use the existing hardware procedures for those checks. VM timings are VM
observations only. The independent s6/Dasung base integration remains mandatory.
The saved 0.1.0 release and its frozen inputs were not modified.
## Implementation notes
The X11 panel uses `x11rb` 0.13.2 and its pure-Rust connection, with core X11
requests and no optional extensions or C GUI toolkit. The protocol companion
crate generates requests; `gethostname` and `rustix` support X11 authentication
and OS access. Versions are locked in Cargo.lock, and their licenses are included
by the existing Rust notice generator. This keeps the GUI compatible with the
static-musl target. Reference: https://docs.rs/x11rb/0.13.2/x11rb/.
Foreground commands pass their already-open stdio descriptors to the privileged
service. The service creates the child, joins its root-owned bay cgroup, drops to
UID/GID 1000, and only then resolves the requested working directory. A pidfd and
exit status return to the launcher; interactive sessions use a PTY proxy. No
writable cgroup descriptor or permission to migrate arbitrary PIDs is exposed
to a client. This matters because Linux cgroup writes use open-file credentials.
Reference: Linux 6.12 kernel/cgroup/cgroup.c, cgroup_procs_write_start().
The first ARM compilation exposed musl/glibc differences in msghdr field widths;
portable field casts fixed those. Final acceptance uses rebuilt binaries and
images, rather than the intermediate builds that preceded that correction.
The first emulator run found that Clap's multicall implementation uses
`Path::file_stem()`, truncating dotted aliases such as `b01:demo.report:shell`.
The launcher now lets Clap parse the invocation path as a typed positional and
uses a second typed external-subcommand parser for explicit `fds-program` calls.
Application help/version flags and dotted names have a regression test. This
keeps the no-manual-argument-parser rule while preserving the actual alias.
The initial X11 click test raced the desktop terminal appearing above the panel.
The test now waits for that terminal and raises the panel before each real mouse
click. Actual program output, consumer tracking and SAFE state are still checked;
window existence alone is not treated as successful launch.
The shutdown suite now publishes `out/m10-vm-latest` after successful completion.
That pointer was also installed for the successful run above. Cleanup retains
that evidence using the same published-link rule as the other runtime suites.
After validation, `make clean` removed 52 obsolete workspaces and Rust build
output. The filesystem reported 62.17 GiB of newly available space. Current
images, latest runtime fixtures, caches, personal outputs and frozen release
inputs were retained; the complete cleanup log accompanies this revision.
@@ -1,10 +1,13 @@
# Dasung integration validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Integration and usage](dasung.md)
Validated on 2026-09-20 on the x86_64 Arch development host. This records the FDS
port of the existing local `dasungd`; it is separate from the historical live
monitor validation in [the imported source](../rust/dasungd/VALIDATION.md).
monitor validation in [the imported source](../../rust/dasungd/VALIDATION.md).
This is the original integration record. The subsequent rootfs build, emulated
ARM execution, and integration rerun are recorded in [M1 validation](m1-validation.md).
@@ -54,9 +57,9 @@ Package dependencies are coreutils, execline, s6, s6-rc, and eudev. The static
executable does not add a shared musl, libusb, or libudev runtime dependency.
The packet keepalive/pacing and confirmed 304210 kHz EDID are retained.
Local generated evidence: [build log](../out/logs/dasung-build-final.log),
[test log](../out/logs/dasung-test-final.log), and
[artifact manifest](../out/manifests/dasung-artifacts.sha256). These outputs are
Local generated evidence: [build log](../../out/logs/dasung-build-final.log),
[test log](../../out/logs/dasung-test-final.log), and
[artifact manifest](../../out/manifests/dasung-artifacts.sha256). These outputs are
ignored by Git and must be regenerated in a fresh checkout.
## Not verified
+188
View File
@@ -0,0 +1,188 @@
# Dasung Paperlike 13K base-system integration
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Services](services.md)
FDS includes the Rust **dasungd** controller from the project's earlier
**Fix Dasung monitor black screen** task. It targets the user's **Paperlike 13K
grayscale**, serial `L56051794302`, with the confirmed 3200 × 2400 timing at about
37 Hz. This is core display support: its package is required for the base SYSTEM
image, and its service belongs to the base boot bundle, including console-only use.
It does not require an ENVIRONMENT cartridge, WindowMaker, or a graphical session.
**Implementation boundary:** the daemon, ARM package, and s6 service definitions
are included by the [rootfs assembler](rootfs.md). [M2 native init](init.md) starts
the controller in the base boot graph, including in the ARM VM with no monitor.
The [M4 boot path](boot.md) starts an early copy before SYSTEM exists, then stops
and reaps it before base s6 starts the normal daemon. Software handoff is tested
in a VM; physical Pi display and monitor recovery remain unverified. The final
s6 database is compiled at image build time.
## Build it on the workstation
Complete `make bootstrap` as described in [Your first build](getting-started.md),
then run from the repository root, as your regular user:
```sh
make dasung
make dasung-test
```
The first command prepares the additional C cross compiler inside the existing
Void build container, fetches the crates pinned in `Cargo.lock`, and cross-builds
static-musl `dasungd`. It verifies the ELF, builds the `fds-dasungd` XBPS overlay,
indexes the output, and compiles the actual s6 source into a validation database.
The second runs unit tests and a simulated serial monitor, then inspects the
package and compiles the service definitions extracted from that package. It also
runs that s6 graph with a native test binary, a read-only container root, and no
physical device access to check startup, logging, permissions, restart, and stop.
Python 3 is needed **only on the host for the simulator**. If absent on Arch,
install `python` with pacman. No Python interpreter is shipped for the daemon.
The first additional toolchain download is approximately 243 MB and needs roughly
1 GB of extra container space. Builds still depend on rolling Void binary inputs.
Run these commands sequentially with other FDS builds; they share the container.
| Output | Purpose |
| --- | --- |
| `out/dasungd` | Static AArch64 executable; no target glibc, musl, libusb, or libudev shared library dependency |
| `out/packages/fds-dasungd-0.1.0_1.aarch64.xbps` | Target package with executable, config, exact EDID, s6 source, udev rule, and licenses |
| `out/manifests/dasung-s6-database.txt` | Path to the service database compiled on the host for validation |
| `out/manifests/dasung-artifacts.sha256` | Executable and package hashes |
| `out/manifests/dasung-build-packages.txt` | Installed build-container input versions |
| `out/logs/dasung-build.log`, `dasung-checks.log` | Build and test evidence |
The standalone validation database contains only the Dasung subset. The M2
image assembler merges it with the init package and compiles the full current
graph while building the rootfs. No daemon is started on the workstation by these targets;
the integration test uses its own temporary socket and pseudo-terminal.
## What goes into the base system
`image/base-packages.list` makes `fds-dasungd` a mandatory hardware package.
`packages/fds-dasungd/template` is the active FDS-owned XBPS overlay. The upstream
Void tree remains pinned and unchanged. Source is maintained at `rust/dasungd/`;
the original task directory is not required for future builds.
The package installs:
- `/usr/bin/dasungd` and `/etc/dasungd.toml`.
- `/usr/lib/firmware/edid/dasung-paperlike13k-37hz.bin`.
- `/etc/s6-rc/source/dasungd`, its runtime-directory and log services, and the
`boot/contents.d/dasungd` membership entry.
- `/usr/lib/udev/rules.d/99-dasung-spi.rules`, adapted to eudev syntax.
- Documentation and the controller/libusb licenses.
s6 supervises the foreground daemon. A short oneshot creates private writable
runtime/log directories; it does not wait for the display. The daemon starts with
no monitor attached and retries discovery. Its initial service transition does
not wait for a monitor reply, a desktop, the network, or all cartridge bays.
The log service rotates a small log under `/run/log/dasungd`.
The init package adds a dependency on `runtime-fs` so the controller starts with
its kernel interfaces mounted. The service runs as root because it claims USB interfaces. Its socket
is under a root-owned mode-0750 directory; runtime control is restricted to root.
A future FDS CLI can expose selected operations through an explicit policy.
No world-writable USB rule, systemd unit, or runit service is installed.
## Confirmed profile and Pi-specific choices
| Property | Recorded value |
| --- | --- |
| Monitor | Paperlike 13K grayscale; `L56051794302` |
| Resolution | 3200 × 2400 |
| Pixel clock | **304210 kHz** (304.21 MHz) |
| Horizontal active / sync start / sync end / total | 3200 / 3248 / 3280 / 3360 |
| Vertical active / sync start / sync end / total | 2400 / 2423 / 2427 / 2447 |
| Sync polarities | Positive horizontal and vertical |
| EDID SHA-256 | `b6c1e0a8d315d9cf5c2fb83784a177530bcc085c2d0724dc7478a9c12449e4f4` |
| Control transport | USB UART `1a86:7523`, with companion SPI bridge `1a86:5512` |
| Startup parameters | Raw mode 1 and contrast 4, previously read back on this unit |
| Keepalive | `20 01` every two seconds; minimum 150 ms between command writes |
The video timing was confirmed on the original AMD workstation, **not on a Pi**.
The original USB-C video connection is not a Pi cabling prescription. Pi connector,
cable/adapter, mode acceptance, and picture stability require hardware validation.
Desktop 2× scaling is separate from EDID timing; a console uses its own font size.
FDS configures `display.enabled=false` and `hotplug="none"`: the daemon handles
USB control without applying the source project's AMD debugfs disconnect/reconnect
procedure. The Pi boot/display integration must supply the appropriate KMS/firmware
EDID configuration for its real connector. The Pi boot-volume builder now supplies
an unqualified firmware EDID override for this dedicated monitor; it does not
assume `DP-2` or a numbered HDMI connector. See the [boot guide](boot.md) for
the exact command line and the remaining physical mode test.
The daemon requires the configured monitor's DRM EDID and the matching USB
companion topology before claiming a device. Multiple matching UARTs are rejected;
`usb_path` can disambiguate a verified Pi topology. Do not substitute a blanket
match for all CH340 adapters or copy this unit's identity to a different monitor.
The original monitor's SPI driver interaction caused a dark picture after a power
cycle. `image/kernel/dasung.config` requires `CONFIG_SPI_CH341=n`; the Pi kernel
builder enforces this before boot, including the initramfs environment. The scoped
udev rule is secondary protection: it cannot prevent a built-in or already-loaded
SPI driver from probing. The daemon also reserves the companion interface without
sending SPI data and refuses to take it from an already-bound kernel driver.
## Runtime usage once installed in an FDS image
These are target commands, **not commands to run against the workstation's live
monitor during the build**:
```text
dasungd status
dasungd query
dasungd refresh
dasungd set contrast 4
dasungd set mode 1 --save
dasungd forget mode
```
`status` returns JSON including `connected`, `responsive`, reply age, and cached
parameters. A successful set means a packet was sent; use query/status to check
observed values. It is not proof of physical image quality. Color-model mode names
are not assigned to this grayscale monitor's raw numeric modes.
The default socket is `/run/dasungd/control.sock`. External programs can use its
newline-delimited JSON protocol; the daemon remains the only USB owner:
```json
{"op":"status"}
{"op":"set","parameter":"contrast","value":4,"save":true}
```
In FDS's current payload, saved overrides live in `/run/dasungd/settings.json`.
They survive a daemon restart **within the same boot**, but not power loss.
Mode 1/contrast 4 are persistent image configuration defaults. Once persistent
machine-state mounts are implemented, point `state_file` at that internal writable
storage to retain user overrides across boots. Do not put it on read-only SYSTEM
or make the base display daemon depend on a removable DATA cartridge.
## Dependencies and source provenance
The imported Rust dependencies are retained and pinned by the workspace lockfile:
`rusb` wraps USB access; `libc` supports Linux serial/locking calls; `ctrlc` handles
termination; `clap` provides CLI parsing; `serde`, `serde_json`, and `toml` support
configuration and the local socket; `anyhow` supplies contextual errors.
Transitive versions are recorded in `Cargo.lock`.
The `vendored` feature builds libusb from the pinned `libusb1-sys` crate. The Void
`cross-aarch64-linux-musl` toolchain supplies C headers/compiler for that library;
Rust's bundled musl target alone was enough for the old pure-Rust smoke-test, but
is insufficient for this C dependency. `LIBUSB_NO_PKG_CONFIG` and
`LIBUDEV_NO_PKG_CONFIG` prevent accidental host/shared-library linkage. libusb uses
its Linux netlink backend. No libusb/libudev shared runtime package is needed.
The target package depends on GNU coreutils for directory preparation, execline
for service launch, s6/s6-rc for supervision, and eudev for device rules. These
match the planned base userspace. The container's s6-rc is a build-time compiler,
not a target PID 1 running on Arch.
[Import provenance](../../rust/dasungd/IMPORT.md) records the original source hashes.
[Original validation](../../rust/dasungd/VALIDATION.md) records the live handoff and
remaining physical test. Protocol and reconnect tests use simulated hardware;
Pi boot, image quality, physical disconnect/reconnect, and corrected cold-power
recovery must still be verified on the actual target. No boot benchmark is claimed.
+141
View File
@@ -0,0 +1,141 @@
# Writable DATA and managed programs
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Cartridges](cartridges.md) · [Services](services.md)
M7 implements writable DATA. Its sustained-write, safe-eject and surrounding
image regression checks [passed in the ARM VM](m7-validation.md). A DATA
cartridge uses a GPT partition named `FDS_DATA`, ext4, and a validated
`/FDS/CARTRIDGE.TOML` declaring class `data` and `writable = true`.
Use the [M9 media tools](media-tools.md) to create new DATA media. The virtual
tests construct disposable image files and never format a workstation disk.
## Where your files live
`/home/fds` remains a temporary home, whether DATA is present or absent. Its
contents disappear at power-off. Persistent user files belong under `/data`.
Separating these paths lets you eject DATA without hiding the active shell's
home directory or substituting another cartridge underneath it.
After inserting a DATA cartridge into a configured bay:
```sh
fds bays
fds cartridge 2
ls /data
```
If exactly one valid DATA candidate is available and none is active, the daemon
activates it at `/data`. The filesystem must grant UID/GID 1000 the intended
write permissions; insertion does not recursively change ownership of user files.
M9's DATA formatter supplies that ownership for fresh media.
An active DATA session stays attached to its current cartridge when another is
inserted. If a discovery snapshot contains multiple candidates with none active,
they remain read-only until you select one:
```sh
fds data use 4
```
Eject the existing active DATA before selecting a replacement. The daemon never
changes `/data` underneath an active session. EMPTY, ERROR and SAFE states retain
the meanings in the [cartridge guide](cartridges.md); MOUNTED READ WRITE means
that the displayed DATA filesystem is available for user writes.
## Run a program that participates in eject
Ordinary console commands work as usual. They are not automatically killed when
you request eject: an open file or working directory may make eject fail as busy.
For a background task that should stop when its DATA is ejected, use:
```sh
fds run 2 -- /usr/bin/bash -c 'date > /data/managed-example.txt'
```
This starts a **background** managed program and prints its process ID. It has no
interactive input. Output goes to the bounded, volatile cartridge service log:
```sh
tail /run/log/cartridged/current
fds cartridge 2
```
The executable path must be absolute. The child starts with `/data` as its working
directory, the ordinary FDS UID/GID, no supplementary groups, no effective
capabilities, a small explicit environment, and `no_new_privs`. Programs cannot
gain privileges through setuid executables. The limit is 256 managed processes
per bay. This interface accepts healthy writable DATA; the [desktop guide](desktop.md)
also explains M8's explicit PROGRAM-media launch interface.
A root-owned Linux cgroup tracks the program and its descendants, including
children that outlive their parent. Cgroups are kernel process tracking, not a
new init system or runtime package. The configured kernel already includes the
required cgroup v2 and process-limit support. The daemon sets up its hierarchy
independently of the console. See the [kernel cgroup interface](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html).
## Eject and remove DATA
```sh
cd "$HOME"
fds eject 2
```
Eject checks for additional mounts, stops managed consumers, flushes the DATA
filesystem with `syncfs`, and performs a normal unmount. Only successful completion
produces SAFE. A retained filesystem descriptor observes writeback errors during
the session. M10 additionally makes the filesystem read-only and checks writeback
again before closing that descriptor for unmount, preventing an error-reporting
gap when a busy mount needs a later retry.
Linux's [syncfs interface](https://man7.org/linux/man-pages/man2/syncfs.2.html)
reports filesystem errors that must not be ignored.
Managed programs receive TERM first. The daemon waits on actual cgroup exit
notifications; programs that have not exited after a one-second deadline are
killed as a group, including newly forked descendants. A further bounded exit
check must succeed. These are failure deadlines, not delays imposed on a program
that has already stopped. Requesting eject therefore ends those background jobs;
it does not promise that unfinished application work is completed.
An unmanaged busy shell/file, extra mount, failed flush or failed unmount prevents
SAFE. Close the reported use and retry. A recorded DATA writeback fault requires
investigation or recovery; a later successful call is not used to erase it and
pretend the failed write succeeded. The daemon never uses lazy/forced unmount to
claim successful eject. A successfully ejected insertion remains unmounted across
a daemon restart until removal/reinsertion.
M10 records writable sessions and faults outside the daemon process. If it crashes
before verified unmount, the same insertion stays quarantined and read-only on
restart. A later flush cannot reconstruct the lost error history. See
[Shutdown and recovery records](power.md#data-recovery-records) for the current
implementation and its acceptance status.
Pulling DATA during writes is an error. The daemon stops its managed consumers,
cleans up the vanished mount and logs that writes may have been lost. This path
never produces SAFE and makes no filesystem-cleanliness promise. Use recovery
inspection/repair before trusting media that was removed during I/O.
## Build and verify on the workstation
After changing DATA code or image configuration, build fresh inputs in order:
```sh
make rootfs PROFILE=cli
make initramfs
make system-card
make data-test
```
The test boots disposable virtual media, writes continuously, verifies ordinary
user privileges and descendant shutdown, checks busy and additional mounts,
reopens persisted data, tests multiple candidates and simulates surprise removal.
After safe eject it extracts the ext4 partition into an ordinary file, runs
read-only `e2fsck -fn`, and compares the sustained-write payload byte-for-byte.
No host mount or physical USB device is used.
Run `make cartridge-test`, `make init-test`, `make boot-test` and
`make console-test` for the surrounding regressions. Test logs and VM evidence
are written under `out/`. Real flash-controller caches, battery loss, USB power
and physical Pi eject/shutdown latency remain hardware acceptance work.
+215
View File
@@ -0,0 +1,215 @@
# Desktop, programs, and networking
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Cartridge guide](cartridges.md) · [DATA guide](data.md)
M8 software checks passed in the ARM virtual machine. The [validation report](m8-validation.md)
records desktop, program and networking evidence. Raspberry Pi video output and
Dasung display quality remain deferred physical checks.
## What starts when you boot
FDS opens its ordinary-user console first. Xorg, WindowMaker, and the DHCP client
are installed in SYSTEM but are absent from the boot bundle. Dasung control
remains in the base boot bundle, including when no desktop is running.
The default graphical backend is Xorg on virtual terminal 2, using its built-in
modesetting driver. The physical keyboard and pointer use libinput. Console
access remains on the original terminal. There is no display manager. Physical
VC4 output, monitor resolution, and E-Ink appearance still require Pi testing.
## Start and stop the desktop
At `FDS>`:
```sh
fds profiles
fds profile activate windowmaker
fds profiles
```
Activation is asynchronous. The second status command reports the selected
profile and an activation-to-ready duration once WindowMaker is ready. This
measurement starts at the activation request, so time spent deciding when to
start the desktop is excluded. `fds boot-profile` also records the first desktop
readiness event for the boot. Neither measure includes firmware or power-on.
The desktop opens an FDS terminal. Right-click its white background to open the
menu, launch another terminal, enable/disable Ethernet, or return to the console.
To stop it from either a terminal or the console:
```sh
fds profile deactivate
```
Stopping the desktop closes its terminal and all processes descended from its
session. Save work first. Files in `/home/fds` survive desktop restarts during the
same boot; the home directory is still temporary and disappears at shutdown.
Use [DATA](data.md) for persistent files. Removing an active DATA cartridge also
stops the desktop before flushing DATA, because desktop applications may use it.
`fds-profile status`, `fds-profile activate windowmaker`, and
`fds-profile deactivate` provide the standalone static helper interface.
## ENVIRONMENT cartridges
An ENVIRONMENT cartridge is a read-only EROFS partition named `FDS_ENVIRONMENT`.
Its `FDS/CARTRIDGE.TOML` uses the [documented manifest format](cartridges.md), with:
```toml
[activation]
profile = "windowmaker"
```
When exactly one eligible ENVIRONMENT cartridge is present, the daemon requests
WindowMaker after console readiness. The graphical runtime comes from SYSTEM;
the cartridge provides a declarative request, never a privileged executable.
Unknown profile names do not execute anything. If several eligible cartridges
arrive together, select a profile explicitly instead of relying on bay ordering.
Eject its bay with `fds eject N`. The desktop stops before SAFE is reported.
Surprise removal also stops its associated desktop. Manually deactivating while
the cartridge remains inserted suppresses automatic reactivation until removal
and reinsertion. A manually started desktop is independent of ENVIRONMENT media.
A cartridge-service restart reevaluates mounted media and retires old optional
sessions; this is a maintenance operation, not a way to preserve desktop jobs.
## E-Ink defaults
`fds-eink` installs the WindowMaker policy and Terminus fonts: a white background,
black text and borders, grayscale controls, outline movement/resizing, no
animations, no blinking decorations, no app-icon bounce, no dock, no compositor,
and no periodically updating clock. X11 compositing is disabled. The terminal
uses black on white and a nonblinking cursor.
Defaults are copied into the ordinary user's `~/GNUstep/Defaults` at the first
session. Later sessions preserve edits there. They are temporary unless you
explicitly store a copy on DATA. The authoritative image defaults live in
`/usr/share/fds/eink`; edit their package sources and rebuild SYSTEM to change the
machine-wide defaults.
Font caches and X11 font indexes are generated while building the image.
An active X server's keyboard map is session state, generated only on desktop
activation. That is separate from global boot-time cache generation.
## Launch a software cartridge
Build current software cartridges on a [Linux workstation](workstation.md). They
contain metadata plus xz software bundles in payload partitions. In FDS:
```sh
fds bay 4
fds run 4 -- demo.hello:hello
fds eject 4
```
Use the software id and command listed by `fds bay`. The guest verifies and
extracts the bundle into a read-only temporary cache, then starts it as UID 1000.
There are no guest software build hooks.
### Legacy PROGRAM compatibility
Existing PROGRAM media contains a read-only EROFS partition named `FDS_PROGRAM`, metadata
in `FDS/CARTRIDGE.TOML`, and its self-contained application files:
```text
app/bin/editor
app/lib/
app/share/
```
FDS validates and mounts it at `/run/fds/apps/<cartridge-id>`. Insertion does not
run anything, and the mount initially disallows execution. To launch an executable
named `editor` from bay 4:
```sh
fds cartridge 4
fds run 4 -- editor
```
Use a simple executable name from `app/bin`, followed by that program's arguments.
The daemon enables read-only execution and starts it as UID 1000, with no added
privileges. Executable paths must remain within the cartridge's `app` directory.
Programs receive `FDS_APP`, a PATH including `app/bin`, `LD_LIBRARY_PATH` including
`app/lib`, and `XDG_DATA_DIRS` including `app/share`. Bundled libraries can also use
an executable-relative RPATH. FDS does not resolve dependencies across cartridges.
Programs are background jobs. Graphical programs can use the active authenticated
X display. Standard output/error goes to the bounded, temporary cartridge log:
```sh
tail /run/log/cartridged/current
fds bay 4
fds eject 4
```
The bay status reports managed processes. Eject stops the whole process group,
including forked descendants, before unmounting. The existing DATA form remains
`fds run N -- /absolute/system/command arguments`, using `/data` as its working
directory. DATA itself remains mounted with execution disabled.
## Ethernet on demand
External networking is off until explicitly requested or a mapped USB Ethernet cartridge
produces a real network interface. Interfaces are associated with their USB
ancestors; a descriptive label alone does not start networking. Wi-Fi setup is
not implemented by this Ethernet policy. The local loopback interface is always
up for applications on the same machine; it does not enable external traffic.
```sh
fds network on
fds profiles
ip -brief address
ip route
fds network off
```
Enabling networking starts DHCP without waiting for a lease. An absent server
cannot delay `FDS>`. Explicit activation selects available Ethernet interfaces;
automatic activation selects interfaces under mapped cartridge USB devices.
Stopping networking stops DHCP, brings the managed interfaces down, and clears
temporary DNS settings. This initial profile uses IPv4 DHCP; it disables kernel
IPv6 autoconfiguration on those interfaces. After an explicit stop, an inserted Ethernet
cartridge remains suppressed until it is removed, or you enable networking again.
Leases, DNS settings, and logs live under `/run`; nothing makes SYSTEM writable.
`/etc/resolv.conf` points to `/run/fds/resolv.conf`. Inspect
`/run/log/network/current` when an address is missing.
## Development and troubleshooting
The production console image includes the real desktop stack. The development
image adds Xvfb and X11 diagnostics for a virtual display without a connected GPU:
```sh
make rootfs PROFILE=development
make desktop-test
```
The test creates its own disposable image selecting Xvfb; the normal development
image still selects Xorg. Never mistake an Xvfb pass for Pi graphics verification.
Changing the SYSTEM image profile requires building that rootfs first; the image
builder rejects a profile label that disagrees with its embedded identity.
If desktop activation fails, `fds profiles` reports it. Read
`/run/log/xserver/current` and `/run/log/desktop/current`, then deactivate before
retrying. Xorg's detailed log is `/run/log/xserver/Xorg.0.log`. X access requires
an authority cookie readable only by root and the FDS group; do not use `xhost +`
or disable access control as a workaround.
Dependency rationale: Xorg supplies the display server and modesetting driver;
libinput supplies keyboard/pointer input; WindowMaker supplies window management;
Terminus and fontconfig supply the typography; xterm supplies a terminal; the static FDS helper
observes X11 property events for readiness without polling delays; xset/xsetroot apply the
static session settings. Xvfb, xdotool, xwd, xwininfo, xdpyinfo, xprop, and xauth are
optional development diagnostics. `fds-dhcpcd` builds the pinned upstream DHCP
client with privilege separation and volatile state paths, omitting its runit
service files. All FDS control helpers remain static Rust/musl binaries.
The readiness observer uses the [X11 core protocol](https://xorg.freedesktop.org/archive/X11R7.7/doc/xproto/x11protocol.html)
and establishes its property subscription before launching the window manager.
Font caches are generated with the image's normalized timestamp, following
[fontconfig's reproducible-cache support](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user.html),
so exporting the filesystem does not invalidate their directory timestamps.
@@ -1,10 +1,13 @@
# Working on FDS/OS
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Troubleshooting](troubleshooting.md)
This guide is for someone who has completed the first build and wants to change
or inspect the working code. Run commands from the repository root, as a normal
user. M0–M12 software acceptance is complete; consult [AGENTS.md](../AGENTS.md)
user. M0–M12 software acceptance is complete; consult [AGENTS.md](../../AGENTS.md)
and the [roadmap](roadmap.md) before adding components.
## Choose the smallest useful build
@@ -33,12 +36,15 @@ Run builds sequentially. The scripts share one Void masterdir and package cache;
starting bootstrap and package builds concurrently is not a supported workflow.
`make packages` builds every FDS base package. `make all` builds the complete
artifact set; two independent frozen offline builds passed with matching output. See
[Frozen inputs and offline rebuilds](reproducible-builds.md). There is no
destructive `make clean` or generic `make test` target.
[Frozen inputs and offline rebuilds](reproducible-builds.md). Use `make clean-preview`
and `make clean` to remove obsolete workspaces and Rust compilation output while
keeping published images, latest test fixtures, saved releases, inputs and
caches. See [Cleanup](cleanup.md) for retention rules and rebuilding afterward.
There is no generic `make test` target.
## Edit and rebuild the Rust program
The active program is [rust/fds-smoketest/src/main.rs](../rust/fds-smoketest/src/main.rs).
The active program is [rust/fds-smoketest/src/main.rs](../../rust/fds-smoketest/src/main.rs).
It is deliberately tiny: it establishes that the intended target and static
linking work. `rust/dasungd/`, `rust/fds-common/`, `rust/fds-cli/`, and
`rust/fds-stage0/`, `rust/fds-boottrace/`, and `rust/fds-cartridged/` are also active. Directories absent from the workspace member
@@ -85,9 +91,9 @@ part of the same change. The crate also rejects the wrong architecture, libc,
or missing static CRT at compile time. Do not bypass those checks to make an
x86_64 build appear to pass.
The release profile and target configuration live in [Cargo.toml](../Cargo.toml)
and [.cargo/config.toml](../.cargo/config.toml). Rust and target versions are
selected through [rust-toolchain.toml](../rust-toolchain.toml). This M0 crate has
The release profile and target configuration live in [Cargo.toml](../../Cargo.toml)
and [.cargo/config.toml](../../.cargo/config.toml). Rust and target versions are
selected through [rust-toolchain.toml](../../rust-toolchain.toml). This M0 crate has
no external crate dependencies. `--offline` works after bootstrap has installed
the toolchain; it is not a promise that the complete build pipeline is offline.
@@ -126,7 +132,7 @@ an ARM package into the x86_64 build container.
## Change build configuration deliberately
The source of truth is [config/xbps-src.conf](../config/xbps-src.conf).
The source of truth is [config/xbps-src.conf](../../config/xbps-src.conf).
`XBPS_MAKEJOBS` controls package build parallelism; the current value is 4.
`XBPS_CHROOT_CMD=bwrap` selects the supported container backend.
+152
View File
@@ -0,0 +1,152 @@
# Pi 5 EEPROM configuration
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Boot images](boot.md) · [Recovery](recovery.md) · [Implementation ledger](implementation-status.md)
M12 now provides an offline configuration workflow using the pinned official
Raspberry Pi tool and a real Pi 5 firmware image. The host checks passed;
application to a physical Pi, boot order, PMIC behavior and timing remain deferred.
`tools/configure-pi-eeprom` only creates files. It never reads or writes a hardware
EEPROM, invokes a firmware updater, or reboots a machine.
## Preview a profile on the build workstation
From the repository root:
```sh
./tools/configure-pi-eeprom --profile production
./tools/configure-pi-eeprom --profile development
make eeprom-test
```
The first invocation downloads three checksum-pinned inputs into
`.host/eeprom/`: the official configuration script, the Pi 5 preview firmware and
its upstream license. Later calls verify and reuse them. `FDS_OFFLINE=1` requires
all inputs to exist and refuses a network download.
The command prints its new `out/eeprom-PROFILE.*` directory. It contains:
| File | Purpose |
| --- | --- |
| `base.bin` | Exact input firmware retained unchanged |
| `original.conf` | Configuration to restore |
| `configured.conf` | Reviewed profile merged with the supplied settings |
| `configured.bin` | Real EEPROM image with the new configuration |
| `rollback.bin` | Same base firmware with `original.conf` restored |
| `review.diff` | Human-readable configuration changes |
| `manifest.json` | Input identity, selected settings and output SHA-256 hashes |
| `LICENSE` | Complete upstream tool and firmware notices |
Default inputs are **preview defaults, not a backup of your Pi**. A rollback image
restores the configuration supplied to this command on the supplied base firmware.
It cannot reconstruct a different firmware version or settings that were never
provided. Retain the actual machine's configuration and matching firmware input
before applying anything to that machine.
## Profile behavior
| Setting | Production | Development |
| --- | --- | --- |
| `BOOT_ORDER` | `0xf6`: NVMe, repeat | `0xf16`: NVMe, SD, repeat |
| `BOOT_UART` | `0` | `1` |
| `NET_INSTALL_ENABLED` | `0` | `0` |
| `NET_INSTALL_AT_POWER_ON` | `0` | `0` |
| `POWER_OFF_ON_HALT` | `1` | `1` |
| `WAIT_FOR_POWER_BUTTON` | `0` | `0` |
Boot order is read from the right. Both profiles avoid scanning the twelve USB
cartridges for firmware boot. Development retains an SD rescue path. Both disable
network-install keyboard detection; Raspberry Pi documents that this detection
adds USB initialization and enumeration work. These settings do not establish a
measured FDS boot improvement. [Official bootloader configuration](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#bootloader-configuration).
On Pi 5, the power-off setting requests PMIC standby on halt; the dedicated power
button remains the wake mechanism. The wait setting leaves cold power-on boot
enabled. Whether the assembled computer and attached hardware behave as intended
still needs a physical test. [Official power settings](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#POWER_OFF_ON_HALT).
The source profiles are [production.conf](../../config/eeprom/production.conf) and
[development.conf](../../config/eeprom/development.conf). No GPIO wake option or
unmeasured HDMI tuning is added.
## Preserve the machine's settings
In a Raspberry Pi maintenance environment with the official EEPROM utilities,
collect its configuration and version information before changing firmware:
```sh
rpi-eeprom-config > pi-current.conf
rpi-eeprom-update > pi-eeprom-status.txt
```
Retain the matching original firmware image separately. The status report helps
identify it; it is not a binary backup. Copy the saved inputs to the workstation,
then prepare a new directory:
```sh
./tools/configure-pi-eeprom \
--profile production \
--base-image /absolute/path/to/original-pi5-firmware.bin \
--current-config /absolute/path/to/pi-current.conf \
--output-directory /absolute/path/to/new-eeprom-review
```
The base must be a regular 2 MiB Pi 5 image, not a device node. The output directory
must not already exist. The helper preserves unrelated configuration lines and
conditional sections. It replaces all occurrences of the six managed settings,
including conditional overrides, with the selected profile's final `[all]`
settings. Review that change in `review.diff`; the exact original conditional
configuration remains in `original.conf` and `rollback.bin`.
The upstream parser reads the generated images back before success is reported.
This checks file construction and configuration roundtrip, not hardware
compatibility. No secure-boot key, fuse, customer signature or OTP setting is
modified by this helper.
## Apply and roll back during physical testing
This step is deliberately outside the host/VM acceptance run. Use the Pi's
maintenance environment and the official installed EEPROM utilities. Once the
reviewed files and saved original inputs are available there, the upstream
configuration interface is:
```sh
sudo rpi-eeprom-config --apply ./configured.conf ./base.bin
```
The updater may program EEPROM immediately or schedule an update depending on
its platform and update method. Follow its output and verify the resulting
configuration/version after the required restart. To restore the supplied
configuration on the same supplied firmware:
```sh
sudo rpi-eeprom-config --apply ./original.conf ./base.bin
```
If the firmware version itself changed, use the saved matching original firmware
for the rollback operation. Keep the maintenance SD and an external EEPROM rescue
route available; internal recovery cannot repair an EEPROM that prevents internal
boot. The official [EEPROM update and recovery guide](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-boot-eeprom)
describes that physical workflow. FDS has not yet tested it on your Pi.
## Inputs, dependencies and software evidence
The input lock is [inputs.json](../../config/eeprom/inputs.json), pinned to
`raspberrypi/rpi-eeprom` commit `2fee426f27b6c54d3f5b6f36efd9a2fe1286a45d` and
Pi 5 preview firmware `pieeprom-2026-09-12.bin`. Every download is SHA-256 checked;
a changed cached input is rejected. The
[official source tool](https://github.com/raspberrypi/rpi-eeprom/blob/2fee426f27b6c54d3f5b6f36efd9a2fe1286a45d/rpi-eeprom-config)
is stored unchanged in the cache. Void's tracked source remains unchanged.
The host workflow uses existing Python and curl. It does not add a target daemon,
package or Rust dependency. The upstream parser's optional signing dependencies
are not needed for configuration-only operations.
`make eeprom-test` passed production/development roundtrips, unchanged firmware
payloads, repeatable binary output, refusal to overwrite an existing directory,
preservation of custom settings, exact rollback of conditional settings, and
rejection of malformed images, device nodes and oversized configuration files.
Initial evidence is `out/m12-eeprom.zi144zib/`, with log
`out/logs/m12-eeprom-check.log`. The complete local release acceptance is recorded in [M12 validation](m12-validation.md).
+235
View File
@@ -0,0 +1,235 @@
# Your first build
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Troubleshooting](troubleshooting.md)
This walkthrough takes you from an FDS Git checkout to two verified ARM Linux
artifacts. It explains the output as you go. You do not need a Raspberry Pi.
At the end you will have a small Rust program built with static musl and GNU hello
packaged for aarch64 glibc. This proves that the workstation can build the two
kinds of software FDS/OS needs. It does not produce a bootable OS image.
## 1. Check the machine and checkout
Use an x86_64 Arch Linux workstation as a normal user. Check it with:
```sh
uname -m
cat /etc/os-release
id -u
```
Expect `x86_64`, `ID=arch`, and a user ID other than `0`. The bootstrap script
currently rejects other host platforms and root execution.
Enter your FDS checkout. For the existing development workspace:
```sh
cd /home/felis/source/fds
pwd
git status --short
```
On another workstation, substitute its checkout directory. It must contain
`Makefile`, `Cargo.toml`, `tools/`, `.gitmodules`, and Git metadata. Avoid spaces
in the path. There is no configured public remote in this workspace; obtain a
Git checkout from the project owner rather than using an invented clone URL.
You need HTTPS access to GitHub, Void repositories, Rust distribution servers,
and package source hosts. Initial downloads total hundreds of MB. Plan for
several GB of free disk space; the verified workstation used roughly 1.6 GB for
the Void masterdir and 455 MB for its hostdir cache, before Rustup and other
outputs. These are observations, not fixed requirements or a build-time promise.
```sh
df -h .
```
## 2. Install host prerequisites
On an otherwise maintained Arch installation, install the required packages:
```sh
sudo pacman -S --needed bash coreutils binutils git curl make file tar xz gzip zstd \
bubblewrap rustup ca-certificates findutils diffutils grep sed gawk util-linux
```
This is the host package installation step. Subsequent build commands run as your
normal user. The alternative `./tools/bootstrap-host --install-deps` runs the same
package installation and then continues bootstrap; choose one route.
The [dependency reference](build-host.md#host-dependencies) explains why each
package is needed. This project uses Rustup to select its pinned Rust compiler,
not an arbitrary system Rust version. If pacman reports an existing Rust package
conflict, resolve the host package choice before continuing; do not force file
replacement.
Check that bubblewrap can create a user namespace:
```sh
bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true
```
Success produces no output and exits normally. If it fails, use
[Troubleshooting](troubleshooting.md#bubblewrap-or-user-namespace-failure).
This is a host capability requirement, not something a Pi can fix.
## 3. Prepare the build environment
From the repository root:
```sh
make bootstrap
```
This performs these steps:
1. Initializes `vendor/void-packages` at the recorded Git commit.
2. Downloads the pinned static XBPS archive, verifies its SHA-256, and extracts
the host tools into `.host/xbps/`.
3. Copies the checked-in Void build configuration into the local upstream checkout.
4. Installs Rust 1.98.0, rustfmt, and `aarch64-unknown-linux-musl` through Rustup.
5. Fetches the locked workspace crates needed for Cargo to resolve the Dasung member,
including when building the dependency-free smoketest offline.
6. Creates the x86_64 glibc Void build container and verifies that GCC runs inside it.
The Void build container supplies compilers and package tools; it is not the
future FDS root filesystem. Rustup changes your user toolchain installation.
XBPS host tools stay inside this repository, and no Pi storage is written.
The final success marker is:
```text
PASS: M0 host bootstrap; run make smoke-test to build both aarch64 artifacts
```
The command saves its output in `out/logs/bootstrap.log`. Re-running bootstrap
reuses the checkout and downloads, but still checks their pins and integrity.
If it exits nonzero, stop here and resolve that failure before running smoke-test.
## 4. Cross-build both artifacts
```sh
make smoke-test
```
First Cargo builds `rust/fds-smoketest` for ARM, using the static musl libraries
and linker bundled with the Rust toolchain. Then xbps-src cross-compiles GNU
hello and packages it for aarch64 glibc. The first package build downloads the
cross compiler, target libraries, and build dependencies such as texinfo.
The script inspects actual ELF files and local package metadata. It checks:
| Artifact | Required result |
| --- | --- |
| Rust smoketest | AArch64, static linkage, no dynamic interpreter or shared library requirement |
| GNU hello in the XBPS package | AArch64, glibc loader and `libc.so.6` dependency |
A successful run contains these messages; other compiler output appears between them:
```text
PASS: aarch64 static ELF
PASS: aarch64 glibc ELF
PASS: XBPS package hello-2.12.3_1 architecture=aarch64 (glibc)
PASS: M0 smoke test complete
```
Without QEMU, it also reports:
```text
SKIP: ARM execution (optional qemu-aarch64 not installed); ELF verification passed
```
That skip means the ARM program was compiled and inspected, but not executed.
It does not hide a failed compiler or ELF check. If QEMU is installed, execution
must pass; an emulator failure is a real smoke-test failure.
## 5. Run the protection and formatting checks
```sh
make check
```
Run this after smoke-test: it expects the built Rust executable to exist. It
checks that bad inputs are rejected, that the Void pin and upstream files are
protected, that overlays cannot replace upstream packages, and that Rust source
is formatted. Several `PASS: rejects ...` lines are expected; they mean the
negative tests worked. The final commands include:
```text
PASS: M0 guardrail checks complete
cargo fmt --all -- --check
```
Rustfmt is normally silent on success. `make check` does not rebuild the ARM
program. After a source change, rebuild first, as described in
[Development](development.md#edit-and-rebuild-the-rust-program).
## 6. Inspect what you built
```sh
ls -lh out/fds-smoketest out/packages/
file out/fds-smoketest
./tools/verify-elf out/fds-smoketest aarch64 static
sha256sum -c out/manifests/artifacts.sha256
```
`file` should include `ARM aarch64` and `statically linked` (or `static-pie linked`).
The checksum command should report `OK` for both artifacts. Hashes verify the
files against this run's manifest; they are not release signatures.
For ELF details, use:
```sh
readelf -hW out/fds-smoketest
readelf -lW out/fds-smoketest
readelf -dW out/fds-smoketest
```
The header names AArch64. There should be no `INTERP` segment or `NEEDED` library.
An x86_64 host's `ldd` may say `not a dynamic executable` and return 1; that message
is expected here, but is insufficient by itself to prove a foreign ELF is static.
The [package guide](packages.md#inspect-the-built-package) walks through the
corresponding inspection of GNU hello.
## 7. Optionally execute the ARM Rust program
The [Arch qemu-user package](https://archlinux.org/packages/extra/x86_64/qemu-user/)
provides user-mode emulation. Install it if you want to execute the ARM program
on the x86_64 workstation:
```sh
sudo pacman -S --needed qemu-user
qemu-aarch64 out/fds-smoketest
```
Expected output:
```text
FDS/OS M0: aarch64 static-musl OK
```
Because this executable is static, this command needs no ARM rootfs or glibc
sysroot. Explicitly invoking `qemu-aarch64` also avoids needing automatic binfmt
registration. Re-running `make smoke-test` records its own QEMU check when the
emulator is on PATH.
This executes one Linux userspace program. It does not emulate the Pi's firmware,
USB bays, display, battery, or operating-system boot. QEMU execution was not
performed during the initial M0 validation; see the [recorded results](m0-validation.md).
## 8. Decide what to do next
- To build the configured OS filesystem and try its ARM programs, follow [Rootfs](rootfs.md): `make rootfs PROFILE=cli`, then `make rootfs-test`.
- To boot native s6 and open a development console, follow [Native init and ARM VM](init.md): `make init-test`, then `make vm`.
- To build the included Dasung monitor package, follow [Dasung](dasung.md).
- To edit the existing Rust program, follow [Development](development.md).
- To understand the `.xbps` artifact, follow [Packages](packages.md).
- To understand the intended removable operating system, read [Architecture](architecture.md) and [Cartridges](cartridges.md).
- To see when bootable images become possible, read [Roadmap](roadmap.md).
Bootstrap, smoke-test, and check complete the M0 foundation. Continue with the
M1 guide for a configured rootfs archive. There is still no flashing step.
@@ -1,5 +1,8 @@
# Glossary
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md)
| Term | Meaning in this repository |
@@ -1,5 +1,8 @@
# Implementation through M12
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
This is the software acceptance ledger for the 67 sections of the
[master plan](master-plan.md). The user authorized implementation through M12
and deferred tests requiring the physical Raspberry Pi or attached hardware.
@@ -37,7 +40,7 @@ the commands, artifacts, failures that led to fixes, and measured results.
| Section | Implementation and evidence | Physical or later boundary |
| --- | --- | --- |
| 1. Project objective | Pi 5 images, all cartridge classes, native s6, Dasung base support and 12-device virtual stress; [overview](../README.md), [stress evidence](m11-validation.md) | Assembled FP-85, battery, display and all physical bays deferred |
| 1. Project objective | Pi 5 images, all cartridge classes, native s6, Dasung base support and 12-device virtual stress; [overview](../../README.md), [stress evidence](m11-validation.md) | Assembled FP-85, battery, display and all physical bays deferred |
| 2. ABI strategy | Every profile uses aarch64 glibc packages; all eight production Rust programs are static ARM musl; rootfs and ELF audits pass | Actual Pi execution deferred |
| 3. Limited musl scope | No target musl runtime package or second general-purpose package repository; glibc compiler/desktop acceptance passes | None in software scope |
| 4. Native init | s6-linux-init hands PID 1 to s6-svscan; s6-rc database compiled during image construction; [native init evidence](m2-validation.md) | Pi startup timing deferred |
+3
View File
@@ -1,5 +1,8 @@
# M2: native s6 init and the ARM development VM
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Rootfs build](rootfs.md) · [Services](services.md)
M2 adds the system's real init path. The kernel enters the generated execline
+276
View File
@@ -0,0 +1,276 @@
# Internal storage and machine settings
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Boot images](boot.md) · [Recovery](recovery.md)
The internal NVMe supplies the Pi's firmware boot files, independent recovery,
and machine settings. SYSTEM remains a separate removable cartridge. Ordinary
user files belong on DATA, not internal storage. No physical disk is written by
any build command below; Pi/NVMe boot and power-loss tests remain deferred.
## Build the complete disk image
Build the component images first, in this order:
```sh
make kernel
make initramfs
make boot-volume BOOT_MODE=production
make recovery
make internal-image
```
The result is `out/fds-internal.img`, a **complete GPT disk image**. Its versioned
build directory contains `layout.json`, source payload checksums, and readback
verification. This differs from `out/fds-boot.img` (a raw FAT partition) and
`out/fds-recovery.img` (a raw EROFS partition).
| Partition | Format | Default allocation | Purpose |
| --- | --- | --- | --- |
| 1: FDS_BOOT | FAT32, EFI system type | 512 MiB | Pi firmware, kernel, DTBs, configuration and initramfs |
| 2: FDS_RECOVERY | EROFS | 1 GiB | Complete independent maintenance system |
| 3: FDS_INTERNAL | ext4 | 256 MiB | Machine settings and explicitly saved diagnostics |
The builder pads the recovery partition without changing its EROFS contents,
aligns partitions to 1 MiB, writes both GPT copies, verifies each payload, and
checks the table independently with `sfdisk`. Internal ext4 is fully initialized
before deployment. It contains root-owned `config/` and private `diagnostics/`.
Unused capacity beyond this initial layout is not automatically expanded.
For a 2 GiB recovery allocation or larger settings partition:
```sh
./image/build-internal --recovery-mib 2048 --internal-mib 512
```
Output directories supplied with `--output-directory` must already exist and be
empty. The builder accepts ordinary image files, never a host block-device
output. Final hardware provisioning and identity checks belong to the physical
acceptance procedure; do not confuse a partition payload with the complete disk.
## Install the internal disk when hardware is available
This is a **deferred physical procedure**. The image and virtual NVMe workflow
are software-tested; writing and booting the user's actual NVMe still require
the hardware. Installation erases the selected disk, including any existing
machine settings. Retain backups before replacing an existing installation.
Use an NVMe enclosure or another Linux machine that can access the target drive
while the Pi is off. First verify a downloaded release using a separately trusted
public key as described in [Release signatures](releases.md). Its complete disk
is named `fds-internal-0.1.0.img`; a local build uses `out/fds-internal.img`.
Use the complete disk image, not the separate BOOT or RECOVERY payload.
List disks before and after connecting the intended drive:
```sh
lsblk -d -o NAME,PATH,MODEL,SERIAL,SIZE,TRAN,LOG-SEC
ls -l /dev/disk/by-id/
```
Match the physical model, serial and capacity. Choose a persistent **whole-disk**
`/dev/disk/by-id/` path without a `-partN` suffix; never guess a `/dev/sdX` name.
Unmount every target partition and disable any swap on it. Do not select the
workstation's system disk. The current images require 512-byte logical sectors
and a disk at least as large as the image; 4 KiB logical-sector media is not
supported by this layout.
Open Bash (`bash`), replace both paths below, then run the block. It checks the
selected disk again, requires a typed confirmation, writes and reads back the
complete image, and relocates the backup GPT when the disk is larger than the
image. It does not expand any partition or filesystem.
```bash
(
set -euo pipefail
fds_image="$PWD/out/fds-internal.img"
fds_disk=/dev/disk/by-id/REPLACE_WITH_THE_TARGET_DISK
[[ -f "$fds_image" && -s "$fds_image" && -b "$fds_disk" ]]
[[ $(lsblk -dnro TYPE "$fds_disk") == disk ]]
[[ $(sudo blockdev --getss "$fds_disk") == 512 ]]
fds_bytes=$(stat -Lc %s "$fds_image")
(( $(sudo blockdev --getsize64 "$fds_disk") >= fds_bytes ))
lsblk -p -o NAME,TYPE,MODEL,SERIAL,SIZE,MOUNTPOINTS "$fds_disk"
if lsblk -nrpo MOUNTPOINTS "$fds_disk" | grep '[^[:space:]]' >/dev/null; then
echo 'Target has mounted filesystems or swap; stop and release them first.' >&2
exit 1
fi
if lsblk -nrpo TYPE "$fds_disk" | grep -Ev '^(disk|part)$' >/dev/null; then
echo 'Target has device-mapper or other active descendants; stop.' >&2
exit 1
fi
read -r -p "Type ERASE $fds_disk to erase this disk: " fds_confirmation
[[ "$fds_confirmation" == "ERASE $fds_disk" ]]
sudo dd if="$fds_image" of="$fds_disk" bs=4M conv=fsync status=progress
sudo blockdev --flushbufs "$fds_disk"
sudo cmp -n "$fds_bytes" "$fds_image" "$fds_disk"
sudo sfdisk --lock=yes --relocate gpt-bak-std "$fds_disk"
sudo sfdisk --verify "$fds_disk"
sudo blockdev --flushbufs "$fds_disk"
)
```
Every command must succeed. The byte comparison occurs before relocating GPT,
because relocation intentionally changes disk-table headers. GNU `dd`'s `fsync`
flushes output before it returns; `sfdisk`'s `gpt-bak-std` moves the backup header
to the end of the target. See the [GNU dd manual](https://www.gnu.org/s/coreutils/manual/html_node/dd-invocation.html)
and [sfdisk manual](https://man7.org/linux/man-pages/man8/sfdisk.8.html).
These commands use the already documented coreutils, diffutils and util-linux
host tools; they do not add a target daemon.
Confirm that the disk shows `FDS_BOOT`, `FDS_RECOVERY` and `FDS_INTERNAL` in that
order using `lsblk -o NAME,PARTLABEL,FSTYPE,MOUNTPOINTS`. Unmount anything the
desktop automatically mounted, then safely disconnect the enclosure and install
the NVMe in the powered-off Pi. Follow [EEPROM preparation](eeprom.md) for the
separately reviewed NVMe boot settings.
Prepare the **first SYSTEM cartridge on the workstation** using the same guarded
write block, with its two path assignments changed to the chosen SYSTEM image
and a different, empty USB cartridge disk. For a local build select
`out/fds-system-cli.img` or `out/fds-system-development.img`; release files are
`fds-system-cli-0.1.0.img` and `fds-system-development-0.1.0.img`. They are
alternative complete GPT images, each containing one `FDS_SYSTEM` partition.
Readback and backup-GPT relocation apply to this disk too. Check its partition
label, safely disconnect it, and insert exactly one SYSTEM cartridge in the Pi
before normal boot. Subsequent cartridge creation and updates can use FDS's
confirmed [media workflow](media-tools.md) after bay calibration.
On the first physical boot, check `fds info`, `fds machine status`, and
`fds bays`. The supplied bay map is empty until calibration. Follow
[physical acceptance](stress-testing.md) to measure ports, test the Dasung
display, and record actual boot/shutdown behavior before relying on the machine.
## Configure the machine before building
Copy `config/machine/` to your own directory. It contains three files:
- `machine.toml`: `format = 1` and a short human-readable `name`.
- `bays.toml`: the measured controller/port map described in
[Cartridges and bay calibration](cartridges.md).
- `hardware-catalog.toml`: optional USB identification names using the same schema
as the base catalog. Entries are data and cannot run commands.
The supplied bay map is deliberately empty because the physical wiring has not
been measured. Do not invent Pi USB paths. USB 2 and USB 3 companion ports need
explicit aliases for the same bay.
```sh
cp -a config/machine out/my-machine
# Edit the three files in out/my-machine using your editor.
make internal-image MACHINE_CONFIG=out/my-machine
```
The builder compiles a native host copy of `fds` and uses the same strict parser
as the target system. You can also validate or pack settings yourself:
```sh
cargo build --locked --offline --release --target x86_64-unknown-linux-gnu -p fds-cli
./target/x86_64-unknown-linux-gnu/release/fds machine validate out/my-machine
./target/x86_64-unknown-linux-gnu/release/fds machine pack out/my-machine out/my-machine.json
```
The three source files become one atomic `config/machine.json` document on
FDS_INTERNAL. Names, lengths, bay aliases, catalog fields and unknown keys are
validated before use. Do not put passwords or private keys in this configuration:
its active snapshot is readable by the local FDS user.
## What happens at boot
The `machine-config` native s6 oneshot precedes `cartridged`. It does **not**
precede the console or Dasung controller. It accepts exactly one non-removable
NVMe disk with the three named partitions in the order above. USB lookalikes are
ignored; multiple eligible NVMe disks are rejected instead of choosing by name.
The settings partition must be clean ext4 with the expected label. Loading uses
`ro,noload,nosuid,nodev,noexec` in a private mount namespace, validates the entire
settings bundle, copies it into `/run/fds/machine/`, and unmounts. No filesystem
repair, journal replay, cache compilation or persistent write occurs during
normal boot. The cartridge service uses that snapshot throughout this boot,
including after a service restart.
If internal storage is missing, unclean, invalid or ambiguous, the service records
an explanation and uses the immutable image's `/etc/fds/` defaults. The console
still opens. Check the actual source before treating bays as calibrated:
```sh
fds machine status
fds --json machine status
fds machine export /tmp/current-machine
```
Export creates a new directory with the three editable source files. It never
overwrites an existing directory. The status source is `internal_nvme` or
`image_defaults`. A temporarily unavailable NVMe is not adopted later in the same
boot; resolve the issue and reboot to load its settings. This prevents changing
bay identities underneath active cartridge operations.
## Update settings from recovery
Bring the edited source directory on a DATA cartridge. At the local root
`RECOVERY#` console, identify its bay with `fds bays`; healthy DATA is mounted
read-only under `/run/fds/media/NN`. For example, if it is in BAY 02:
```sh
fds machine validate /run/fds/media/02/my-machine
fds machine install /run/fds/media/02/my-machine
fds reboot
```
Installation is restricted to root in the recovery image. It writes the complete
validated bundle atomically, saves the old bytes as `config/previous.json`,
flushes and unmounts the internal filesystem, and reports success only after
those steps. A reboot activates the new settings. The currently running bay map
is unchanged, so active media does not move to a different bay mid-operation.
Keep a copy of your previous source directory on DATA or the build host to
reinstall it if the new calibration is wrong.
An invalid existing JSON document can be replaced this way. Wrong ownership,
symlinks, an unclean filesystem or a damaged directory require offline filesystem
maintenance first. Recovery does not automatically repair internal NVMe; its
`fds recovery repair` command is deliberately limited to DATA cartridges.
## Save and retrieve diagnostics
Logs and boot records stay in RAM by default. Root may explicitly save a file
of up to 16 MiB. Saved names cannot contain paths, and existing names are refused.
These operations mount internal ext4 only for the operation and then unmount it.
For example, from recovery:
```sh
fds --json boot-profile >/tmp/boot.json
fds machine store boot-first.json /tmp/boot.json
fds --json bays >/tmp/cartridge-inventory.json
fds machine store cartridges-first.json /tmp/cartridge-inventory.json
bash /usr/share/fds/capture-hardware /tmp/hardware-capture
tar -C /tmp -czf /tmp/hardware-capture.tar.gz hardware-capture
fds machine store hardware-first.tar.gz /tmp/hardware-capture.tar.gz
fds machine fetch boot-first.json /tmp/retrieved-boot.json
```
Use distinct names for subsequent sessions. A failed flush or unmount is an
error, not a successful save. These are machine diagnostics; do not use this
facility as ordinary user storage. The saved cartridge inventory is a persistent
diagnostic snapshot, including metadata already inspected during this boot.
Retrieve it with `fds machine fetch cartridges-first.json /tmp/saved-inventory.json`.
The daemon's live metadata cache remains volatile and is rebuilt from currently
attached devices; saved snapshots are never used to authorize media actions.
Together, explicit boot reports, inventory snapshots and hardware captures provide
the boot history, cached metadata and diagnostics assigned to FDS_INTERNAL in
master-plan section 12, without adding internal writes to startup or shutdown.
## Dependencies and validation
No new target package or Rust crate is required. The host image-tool prefix adds
`e2fsprogs` for ext4 creation, inspection and validation; it already supplies FAT
and EROFS tools. A private unprivileged user namespace gives created files root
ownership without requiring a root build session.
`make internal-test` exercises the actual packaged runtime in ARM VMs with
virtual NVMe. Acceptance evidence belongs in [M12 validation](m12-validation.md);
a passing VM does not verify the Pi EEPROM, PCIe path or physical flash durability.
The Linux [ext4 mount documentation](https://www.kernel.org/doc/html/latest/admin-guide/ext4.html)
explains why read-only loading also disables journal replay. Filesystem creation
options follow the upstream [mke2fs manual](https://man7.org/linux/man-pages/man8/mke2fs.8.html).
@@ -1,5 +1,8 @@
# M0 validation report
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Roadmap](roadmap.md)
This is the initial acceptance record, not a setup tutorial. Later documentation
@@ -97,13 +100,13 @@ smoke-test then passed.
Actual logs and inventories:
- [Bootstrap log](../out/logs/bootstrap.log)
- [Smoke-test log](../out/logs/smoke-test.log)
- [Package build log](../out/logs/xbps-hello.log)
- [Guardrail and formatting checks](../out/logs/check.log)
- [Build package inventory](../out/manifests/void-build-packages.txt)
- [Cached package input digests](../out/manifests/void-package-inputs.sha256)
- [Artifact digests](../out/manifests/artifacts.sha256)
- [Bootstrap log](../../out/logs/bootstrap.log)
- [Smoke-test log](../../out/logs/smoke-test.log)
- [Package build log](../../out/logs/xbps-hello.log)
- [Guardrail and formatting checks](../../out/logs/check.log)
- [Build package inventory](../../out/manifests/void-build-packages.txt)
- [Cached package input digests](../../out/manifests/void-package-inputs.sha256)
- [Artifact digests](../../out/manifests/artifacts.sha256)
Generated output is ignored by Git. These links refer to this validated checkout;
rerun the commands to recreate the output in another checkout.
@@ -1,5 +1,8 @@
# M1 root filesystem validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Build and usage](rootfs.md) · [Roadmap](roadmap.md)
Validated on **2026-09-21 (Asia/Shanghai)** on the x86_64 Arch workstation.
@@ -1,5 +1,8 @@
# M10 shutdown validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Shutdown usage](power.md) · [Implementation ledger](implementation-status.md)
M10 software acceptance passed on 2026-09-21 in isolated ARM virtual machines.
@@ -1,5 +1,8 @@
# M11 stress validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Stress-test usage and physical procedure](stress-testing.md) · [Implementation ledger](implementation-status.md)
M11 software acceptance passed on 2026-09-21. The expanded twelve-device suite
@@ -1,5 +1,8 @@
# M12 production preparation: software evidence
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Implementation ledger](implementation-status.md) · [EEPROM](eeprom.md) · [Recovery](recovery.md)
M12 **software acceptance is complete**, including the signed local FDS/OS
@@ -10,7 +13,7 @@ the development history; the final acceptance identifies the released bytes.
## Final local release acceptance
Completed on 2026-09-21. The local release is
[`out/fds-os-0.1.0/README.md`](../out/fds-os-0.1.0/README.md).
[`out/fds-os-0.1.0/README.md`](../../out/fds-os-0.1.0/README.md).
It contains 38 signed manifest entries, versioned images and packages,
profile inventories, source and complete build-input archives, guides and
reproducibility evidence. It has not been published or installed on hardware.
@@ -27,11 +30,11 @@ Both started without FDS build outputs. The complete 27-artifact comparison
passed, covering all five installable images, three rootfs tars, four initramfs
formats, kernel/DTB, four EEPROM preview files and all nine FDS packages.
Logs: `out/logs/m12-offline-v5-{a,b}.log`; comparison:
[`out/m12-reproducibility-v5.json`](../out/m12-reproducibility-v5.json).
[`out/m12-reproducibility-v5.json`](../../out/m12-reproducibility-v5.json).
Every one of those artifacts also matches the normal build used by the complete
[Clap runtime acceptance](clap-validation.md). The identity proof is
[`out/m12-tested-to-release-v5.json`](../out/m12-tested-to-release-v5.json),
[`out/m12-tested-to-release-v5.json`](../../out/m12-tested-to-release-v5.json),
and `out/m12-clap-acceptance-index.json` records the 21 acceptance logs and hashes.
This establishes exact-byte coverage of the released images without repeating
the same VM suite against identical images. Physical tests remain deferred.
@@ -48,7 +51,7 @@ Verification against the separately retained public key checked every manifest
entry. OpenSSL independently verified the actual release signature. Both actual
release archives were extracted with mode-preserving flags; the complete input
lock and standalone source identity passed. Evidence:
[`out/m12-release-acceptance-v5.json`](../out/m12-release-acceptance-v5.json),
[`out/m12-release-acceptance-v5.json`](../../out/m12-release-acceptance-v5.json),
with logs in `out/logs/m12-v5-*.log` and extracted files under
`out/m12-release-acceptance.mzkfsvev`.
@@ -1,5 +1,8 @@
# M2 native init validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Boot and usage](init.md) · [Roadmap](roadmap.md)
Validated on **2026-09-21 (Asia/Shanghai)** on the x86_64 Arch workstation.
@@ -1,5 +1,8 @@
# M3 static Rust tooling validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
Validated on 2026-09-21 (Asia/Shanghai), x86_64 Arch Linux with pinned Rust 1.98.0.
This records the M3 diagnostic foundation before M4 adds the real boot handoff.
See [tooling usage](tooling.md) and the [through-M12 ledger](implementation-status.md).
@@ -1,5 +1,8 @@
# M4 software boot validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
Validated on the x86_64 Arch workstation on 2026-09-21 (Asia/Shanghai).
Physical Pi testing is deferred by the user. This report establishes the
software boot machinery, not the physical NVMe/RP1/display path or performance
@@ -1,5 +1,8 @@
# M4 boot integration development record
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
The user has authorized the remaining software through M12. This page records
M4 development decisions. Software validation is complete; see [M4 evidence](m4-validation.md). Physical Pi validation remains deferred.
@@ -1,5 +1,8 @@
# M5 software validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
M5's ordinary-user console and software timing tools passed on the x86_64 Arch
build host using the actual AArch64 kernel, initramfs and SYSTEM image in QEMU
11.1.1. Physical Pi boot, power-on, display and timing acceptance is deferred.
@@ -1,5 +1,8 @@
# M6 cartridge software validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
Verified on 2026-09-21 on the x86_64 Arch build host. These results use actual
AArch64 binaries, kernel USB events, s6 services and filesystem mounts in QEMU
11.1.1. Physical Pi wiring, bay calibration, simultaneous physical devices and
@@ -1,5 +1,8 @@
# M7 writable DATA validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
Software acceptance passed on 2026-09-21 using the x86_64 Arch build workstation,
QEMU 11.1.1, the actual AArch64 Pi kernel and static FDS tools. Physical Pi,
removable-media power loss and real shutdown/eject latency remain deferred.
@@ -1,5 +1,8 @@
# M8 desktop and network validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Usage guide](desktop.md) · [Implementation ledger](implementation-status.md)
M8 software acceptance passed on 2026-09-21, including corrected CLI and
@@ -1,5 +1,8 @@
# M9 media-tool validation
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Media-tool usage](media-tools.md) · [Implementation ledger](implementation-status.md)
M9 software acceptance passed on 2026-09-21. Both packaged profiles, ARM image
@@ -6,7 +6,7 @@ integration and the signed local 0.1.0 release.**
Tests requiring physical Pi hardware are deferred; software and VM checks remain
in scope. See the [implementation ledger](implementation-status.md). A command appearing
here is not evidence that it exists in the checkout. Start with the
[project overview](../README.md) and [first-build walkthrough](getting-started.md)
[project overview](../../README.md) and [first-build walkthrough](getting-started.md)
for current usage, or the [roadmap](roadmap.md) for implementation status.
The 67 numbered sections below retain the full project requirements.
@@ -1326,6 +1326,10 @@ Activating a GUI profile starts the user's graphical session directly.
# 31. E-Ink profile
Use the grayscale profile as the default WindowMaker appearance on every
display. Install it globally and seed new user preferences without replacing
existing customization. The native FDS Control panel follows the same style.
The monitor controller is base hardware support, independent of the desktop
profile. Include the existing Rust `dasungd` for the user's Paperlike 13K grayscale
in every SYSTEM image and the native s6 boot bundle. Maintain the confirmed
@@ -1374,40 +1378,26 @@ Do not update the clock every second.
# 32. PROGRAM cartridge
Do not design a complex dependency resolver across cartridges in version 1.
Create software cartridges on a generic Linux workstation using `fds-cartridge`.
Each format-2 software recipe identifies a Void source package and its commands.
The tool builds with `xbps-src`, installs the package and runtime dependencies
into a private tree, and creates GPT images with metadata partition 1 plus one
or more EROFS program partitions.
New PROGRAM software cartridges use GPT with exactly `1 + m` read-only EROFS
partitions. Partition 1 (`FDS_METADATA`) contains `FDS/CARTRIDGE.TOML` and
`FDS/SOFTWARE.TOML`. The catalogue describes every software bundle in the next
`m` partitions, named `FDS_PAYLOAD02` onward. Each software bundle is an
xz-compressed tarball; multiple bundles may share a payload partition.
Payloads contain `programs/SOFTWARE-ID/` installed roots, including executable
files and libraries. Verify tree integrity and architecture on insertion; run
new software directly from the read-only EROFS mount without guest extraction.
Retain reading of legacy PROGRAM media and catalogue-format-1 archives.
Software compilation, archive packaging and complete cartridge image creation
run on a generic Linux workstation. The confirmed USB writer writes and verifies
that complete disk image. QEMU workstation tooling boots the real FDS kernel,
initramfs and SYSTEM and inserts/removes these images as virtual USB cartridges.
Publish commands through `/run/fds/bin` using a managed foreground launcher.
Keep `fds run BAY -- SOFTWARE-ID:COMMAND` for managed background execution.
Both paths run as the ordinary user and participate in safe eject and unplug
cleanup. Resolve names by lowest bay, then lexical selector; expose qualified
`bBAY:SOFTWARE-ID:COMMAND` aliases without overriding base system commands.
The guest validates metadata and mounts payloads read-only, then verifies and
extracts a selected bundle to a bounded temporary read-only cache. It executes
only an explicitly requested `SOFTWARE-ID:COMMAND` as the ordinary user. It never
runs software build recipes or installation hooks. Eject, physical removal,
service restart and shutdown release caches and stop managed consumers.
Legacy single-partition `FDS_PROGRAM` media with `app/bin`, `app/lib`, and
`app/share` remain readable. The metadata-first format supersedes that layout
for newly built software cartridges. See [Workstation usage](workstation.md) and
[the format contract](software-format.md).
Where necessary, use:
```text
RPATH
wrapper environment
```
to resolve shared libraries.
Do not modify the SYSTEM root.
Provide `fds-control`, a native static Rust X11 panel styled to match the default
grayscale WindowMaker desktop. It displays bays and software, opens commands in
a terminal, requests rescans and safe eject, and reports service errors.
---
@@ -1466,10 +1456,10 @@ FDS> fds bays
# 34. fds-burn
The target media client remains static-musl Rust. New software creation and
writing use native Linux workstation `fds-cartridge`: build bundles, construct
writing use native Linux workstation `fds-cartridge`: build Void source packages, construct
and verify the complete metadata-first GPT image, preview the USB target, then
write and verify that full image. Never assemble software partitions piecemeal
on the destination drive. See [the workstation workflow](workstation.md).
on the destination drive. See [the workstation workflow](../workstation.md).
Responsible for:
@@ -1512,7 +1502,7 @@ Use XBPS for:
```text
building images
resolving package dependencies
resolving libraries for workstation-built PROGRAM bundles
resolving libraries for installed PROGRAM package trees
development profile
```
@@ -1,5 +1,8 @@
# Cartridge image tools
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Cartridge usage](cartridges.md)
For new software cartridges, use the [Linux workstation workflow](workstation.md):
@@ -1,5 +1,8 @@
# Building and inspecting packages
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Development](development.md) · [Glossary](glossary.md)
FDS uses Void's XBPS package format and xbps-src build machinery for ordinary
@@ -1,5 +1,8 @@
# Performance targets and measurement
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Boot](boot.md) · [Services](services.md)
**Physical Pi performance remains unmeasured.** The target budgets below are
@@ -1,5 +1,8 @@
# Shutdown and reboot
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [DATA usage](data.md) · [Media tools](media-tools.md)
M10 software acceptance passed; see the [validation report](m10-validation.md).
+210
View File
@@ -0,0 +1,210 @@
# Recovery and rollback
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Boot images](boot.md) · [Media tools](media-tools.md) · [EEPROM](eeprom.md)
Recovery is a separate, read-only FDS image. It contains native s6, Bash, GNU
utilities, XBPS, filesystem tools, the static FDS tools, and the base Dasung
controller. It does not need files or programs from a SYSTEM cartridge.
The software workflow is covered by `make recovery-test`; physical Pi recovery remains deferred.
The [complete internal disk](internal-storage.md) includes this recovery image;
its guide separates verified image construction from deferred physical
installation. The build commands below create ordinary files and do not write a
host disk.
## Build the recovery image
Use the prepared x86_64 build host described in [Your first build](getting-started.md).
From the repository root, run these commands sequentially:
```sh
make recovery
make rootfs-test
make recovery-test
```
The full VM test also needs the Pi kernel/initramfs and a known-good CLI SYSTEM
image. On a fresh checkout, build those first with `make rootfs PROFILE=cli`,
`make rootfs-test`, `make initramfs`, and `make system-card PROFILE=cli`, then
run the recovery sequence above. Run builds and VM tests sequentially because
they share the same project-local Void environment.
`make recovery` builds the `recovery` rootfs profile independently, configures all
packages and caches at image construction time, then creates EROFS. Its outputs
are:
| File | Meaning |
| --- | --- |
| `out/rootfs-recovery.tar` | Complete recovery rootfs, including package database and compiled s6 configuration |
| `out/fds-recovery.img` | Raw EROFS payload for the internal GPT partition named `FDS_RECOVERY` |
| `out/recovery-build.*/manifest.json` | Rootfs hash, filesystem hash, profile and size |
| `out/manifests/rootfs-latest/` | Exact selected package archives and build input records |
The latest-rootfs pointers also select this recovery build. Profile-specific
rootfs pointers preserve earlier CLI/development builds. A recovery EROFS payload
is not a whole-disk image and is not a SYSTEM cartridge. The SYSTEM builder
rejects a recovery rootfs to prevent confusing those roles.
## Enter recovery
Stage0 can enter recovery in two ways:
1. At its missing-SYSTEM or ambiguous-SYSTEM console, type `recovery` and Enter.
2. For a deliberate maintenance boot, use `fds.boot=recovery` in the boot
partition's single-line `cmdline.txt`, replacing `fds.boot=normal` if present.
Restore normal mode when maintenance is complete.
Stage0 requires exactly one readable `FDS_RECOVERY` partition and mounts it
read-only. It does not silently choose between duplicate partitions. The ARM VM
suite supplies disposable virtual partitions; physical firmware/NVMe/display
behavior must still be checked on the Pi.
The local prompt is:
```text
FDS RECOVERY — LOCAL MAINTENANCE CONSOLE
RECOVERY#
```
This is an explicit **root maintenance console**. It reads startup files from the
immutable recovery image and uses a temporary home under `/run/fds`. The root
password remains locked; no SSH or network login service is started. Normal CLI
and development images continue to use the ordinary `fds` account and `FDS>`.
Recovery inspects cartridges read-only. It does not automatically mount DATA
writable, activate an ENVIRONMENT desktop, or enable Ethernet. Those actions
require explicit commands. The Dasung controller remains in the base boot bundle.
## Inspect a failed system
```sh
fds info
fds bays
fds bay 2
fds inspect BAY02
fds topology
fds boot-profile
lsusb -t
lsblk -o NAME,MAJ:MIN,MODEL,SERIAL,SIZE,RO,TYPE,FSTYPE,PARTLABEL,MOUNTPOINTS
dmesg
```
Use the calibrated bay number and the displayed model/serial to identify a
cartridge. Do not infer a bay from `/dev/sda` or from discovery order. A valid
inactive SYSTEM mounts read-only under `/run/fds/media/NN`; `fds bay N` reports
the actual path. A bad filesystem or manifest produces an error instead of
running anything from the cartridge.
An empty bay configuration produces `UNCONFIGURED`, not guessed bay numbers.
See [Cartridges](cartridges.md) for calibration. Permanent machine configuration
on `FDS_INTERNAL` is part of the remaining M12 work.
## Check and repair DATA
Start with a read-only check. For example, for DATA in bay 2:
```sh
fds recovery check BAY02
```
This command is available only to root in the recovery image. It identifies one
USB disk and one `FDS_DATA` partition, unmounts its read-only view, reserves the
disk exclusively, verifies GPT and the kernel partition identity, and invokes
`e2fsck -f -n`. It does not repair the filesystem. A clean result leaves DATA
unmounted and reports `SAFE TO REMOVE`.
If DATA was explicitly activated writable, eject that session first. Close any
shell whose current directory is on DATA and any other reader before checking;
an ordinary busy-unmount failure is reported, never bypassed with lazy unmount.
If the check reports problems, review its log and preserve a backup where
possible. Preview the repair with:
```sh
fds recovery repair BAY02
```
The preview displays the model, serial, capacity and a confirmation command.
Copy that exact command only after checking the intended cartridge. Its token
binds the bay, kernel insertion number and current boot. A token from another
insertion or boot is rejected. The preview makes no filesystem changes.
Confirmed repair runs `e2fsck -f -p`, which performs conservative automatic
repairs and stops when manual judgement is required. It then flushes the device,
invalidates its block cache, and runs a second `e2fsck -f -n`. Only a successful
verification produces `DATA REPAIRED AND VERIFIED` and `SAFE TO REMOVE`.
The checker uses a temporary kernel loop device backed by the already verified
partition descriptor. This lets `e2fsck` take its own exclusive device claim
while FDS retains the physical whole-disk reservation. The loop is removed
automatically when its last descriptor closes. This uses the existing kernel
loop driver, `libc` crate and base `e2fsprogs`; no new package or Rust dependency
is introduced.
Failed or interrupted checks/repairs retain a quarantine record across cartridge
daemon restarts for the same insertion. They do not inherit an earlier SAFE
status. The checker holds the disk reservation and is killed if its supervising
daemon dies. The command runs synchronously: bay operations and orderly shutdown
wait for it to finish; the local shell remains usable. A client timeout is not
success and does not authorize removing media. Inspect the log and wait for the
service to finish before retrying. Logs are in `/run/fds/recovery/` and are
limited to 16 MiB per invocation.
This narrow recovery command requires a readable GPT and primary ext filesystem
signature. It deliberately does not guess partition boundaries, recreate a
broken GPT, or force answers to destructive `e2fsck` questions. Such cases require
an offline backup and expert use of the included filesystem tools. It cannot
recover data that was never written to storage.
For structured results, prefix the command with `fds --json`. After a successful
check or repair, remove/reinsert DATA to inspect it again. Recovery still keeps
it read-only until `fds data use N` is explicitly requested.
## Prepare replacement SYSTEM media
A known-good SYSTEM image must be supplied on separate source media. Do not use
a damaged image as an update source. For example, source DATA in bay 2 and an
unmounted replacement cartridge in bay 4:
```sh
fds bay 2
fds inspect image /run/fds/media/02/fds-system-cli.img
fds inspect BAY04
fds burn system /run/fds/media/02/fds-system-cli.img BAY04
```
Use the actual source mount shown by `fds bay 2`. If the destination already
contains a mounted cartridge, run `fds eject 4` first. The burn preview identifies
the source checksum, destination capacity/model/serial and confirmation command.
Follow [Media creation and writing](media-tools.md) for confirmation and status commands.
Completion requires device flush, readback and GPT verification. Recovery uses
the same protected writer as the main system: it refuses mounted destinations,
active root storage and disks containing internal FDS partition names.
Keep the previous known-good SYSTEM cartridge. After a successful replacement
write, use `fds poweroff`, swap SYSTEM while powered off, and boot normally.
Rollback means restoring the previous SYSTEM cartridge; it does not undo DATA
file changes, application migrations, internal boot updates or EEPROM changes.
## Capture diagnostics and shut down
```sh
bash /usr/share/fds/capture-hardware /tmp/recovery-capture
cat /tmp/recovery-capture/status.tsv
fds power status
fds poweroff
```
The collector records identities, USB topology, mounts, kernel messages, packages
and boot events with checksums. Its output is temporary unless copied to a
healthy DATA cartridge after explicitly activating that DATA with `fds data use N`.
Do not activate the damaged cartridge merely to save a report. Eject writable
DATA after copying, or let `fds poweroff` perform the normal verified shutdown.
See [Shutdown](power.md) when shutdown reports a blocking DATA error.
Recovery is independent of SYSTEM, but it still depends on working internal
boot storage, the kernel and firmware. An external rescue medium is required
when that layer fails. EEPROM preparation and rollback are described separately
in [EEPROM configuration](eeprom.md); no EEPROM is changed by these recovery tools.
+170
View File
@@ -0,0 +1,170 @@
# Release signatures and verification
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [M12 evidence](m12-validation.md)
`fds-release` signs an artifact manifest and verifies its signature and every
listed file. The static ARM executable is included in the base CLI package, so
CLI, development and recovery images can all verify downloads. The workstation
build supplies an x86_64 signer with the same format.
The signing tool has passed host, static ARM and independent OpenSSL acceptance.
The complete frozen-input, versioned local 0.1.0 release passed acceptance;
see [M12 validation](m12-validation.md#final-local-release-acceptance) for its identity
and [Offline rebuilds](reproducible-builds.md) for the workflow.
The examples below describe the working signing interface; they do not identify
a published or hardware-qualified FDS release.
## Build and test the tools
After [bootstrap](getting-started.md), run:
```sh
make signing
make signing-test
```
The workstation executable is
`target/x86_64-unknown-linux-gnu/release/fds-release`. The ARM executable is
`target/aarch64-unknown-linux-musl/release/fds-release`; `make tooling` also
exports it to `out/fds-release`. An ARM file cannot execute directly on the
x86_64 workstation. `make signing-test` runs that file through the project-local
QEMU interpreter as well as testing the host executable.
OpenSSL is a **host test dependency** used as an independent Ed25519
implementation. It is not linked into the FDS verifier. The test creates private
disposable keys under its `out/m12-signing.*` directory, checks exact signatures,
then exercises wrong keys, altered manifests, damaged or missing artifacts,
symlinks, FIFOs, malformed fields and unsafe paths. Test keys are not release
keys.
## Create and retain a signing key
For a local signing identity, choose an existing private directory outside the
checkout and its build output. This example creates a new directory; choose a
different name if it already exists:
```sh
mkdir -m 700 "$HOME/fds-signing"
target/x86_64-unknown-linux-gnu/release/fds-release keygen \
"$HOME/fds-signing/release-01"
```
The command creates `release-01.key` (32 secret bytes, mode 0600) and
`release-01.pub` (the hexadecimal public key). Existing files are never
overwritten. It prints SHA-256 of the decoded 32-byte public key, never the
secret. This fingerprint differs from `sha256sum release-01.pub`, which hashes
the hexadecimal text and its newline. Retain the private key separately from release downloads and source
archives. Anyone who has that key can sign a release under that identity.
If public-key export failed after private-key creation, retain the private file
and export it to a new path:
```sh
target/x86_64-unknown-linux-gnu/release/fds-release public-key \
"$HOME/fds-signing/release-01.key" "$HOME/fds-signing/recovered.pub"
```
Private keys must be regular files owned by the invoking user with no group or
other permissions. The tool rejects symlinks and invalid key lengths. Key
generation uses Linux `getrandom`; temporary seed storage and the signing key
are zeroized when dropped.
## Sign an assembled directory
An assembled release directory must already contain its artifacts and a
`manifest.json` matching the format below. Signing verifies every artifact's
length and hash before creating `manifest.sig`:
```sh
target/x86_64-unknown-linux-gnu/release/fds-release sign \
/path/to/release-directory --key "$HOME/fds-signing/release-01.key"
```
If a signature already exists, use a new release directory. The tool deliberately
does not replace signatures or silently rewrite the manifest. A failed operation
may leave its new output file for inspection; it never declares it verified.
## Verify before using images
Obtain the signer's public key through a channel you already trust, or compare
its fingerprint against a separately authenticated value. A public key included
beside an untrusted download does not establish its identity by itself.
On FDS, including recovery:
```sh
fds-release verify /data/downloads/fds-release --key /data/keys/fds-release.pub
```
On the workstation, use the host executable instead:
```sh
target/x86_64-unknown-linux-gnu/release/fds-release verify \
/path/to/release-directory --key /path/to/trusted-fds-release.pub
```
A successful command reports `VERIFIED FDS/OS 0.1.0`, the number of checked
artifacts, and `Hardware validation: deferred`. Failure returns exit status 2
and an explanation. Keep downloaded images unchanged between verification and
use. Verification covers only files listed in the manifest; unrelated extra
files are not endorsed. The [media writer](media-tools.md) performs its own
image/layout validation and readback checks when writing a cartridge.
These are distribution signatures. The Pi firmware and stage0 currently do not
enforce them at boot, so this is not a secure-boot implementation or a claim
that the hardware has been tested.
## Manifest and signature format
The JSON document has exactly these fields:
```json
{
"format": 1,
"version": "0.1.0",
"source_epoch": 1789909701,
"source_sha256": "<64 lowercase hexadecimal characters>",
"void_commit": "02a3cbc132c3c4a3a9d59e9b98f517af5dd11cd1",
"hardware_validation": "deferred",
"files": [
{
"name": "fds-system-cli-0.1.0.img",
"bytes": 123456,
"sha256": "<64 lowercase hexadecimal characters>"
}
]
}
```
This is a schema illustration, not a usable manifest: the size and hash
placeholders must be replaced with values from the actual artifact. Format 1
accepts the current tool version, a positive source epoch, a 40-character Void
commit, and 1–64 nonempty artifacts. Filenames must be flat safe ASCII names;
paths, duplicates, hidden names, reserved manifest filenames and unknown fields
are rejected. The manifest is bounded to 1 MiB. Files must be regular files,
opened without following symlinks; hashing streams their bytes and checks for
changes during the read.
The signature is Ed25519 over the exact bytes
`FDS/OS release manifest v1` followed by a NUL byte and the raw `manifest.json`
bytes. JSON whitespace is therefore authenticated too. `manifest.sig` contains
128 lowercase hexadecimal characters and a newline. The public-key file contains
64 lowercase hexadecimal characters and a newline. Verification rejects weak
public keys and uses strict signature verification.
The implementation uses locked `ed25519-dalek` 3.0.0 for Ed25519,
`zeroize` 1.9.0 for secret buffers, and the existing SHA-256/JSON crates.
Its locked transitive dependencies provide curve arithmetic, digest and
signature types. Default dalek features are disabled; no asynchronous runtime,
OpenSSL binding or shared target library is added. The dependencies and their
license metadata are recorded in `Cargo.lock` and the cached crate sources.
The build collects their license texts, including build dependencies and
vendored libusb notices, into `/usr/share/licenses/fds-cli/RUST-NOTICES.txt`.
The usage guide is installed at `/usr/share/doc/fds/releases.md` in every profile.
Algorithm interoperability is checked against the
[RFC 8032 test vector](https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1)
and OpenSSL; strict verification follows the
[dalek API](https://docs.rs/ed25519-dalek/3.0.0/ed25519_dalek/struct.VerifyingKey.html).
@@ -1,5 +1,8 @@
# Frozen inputs and offline rebuilds
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Build host](build-host.md) · [Release signatures](releases.md)
M12 adds a local input snapshot, fresh restore, network-isolated build, and
@@ -1,5 +1,8 @@
# Roadmap and current status
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Master plan](master-plan.md) · [M0 evidence](m0-validation.md)
**M0–M12 software acceptance is complete. Physical Pi testing is deferred.**
@@ -1,5 +1,8 @@
# Building and using the ARM root filesystem
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Validation](m1-validation.md)
The assembler produces a configured **aarch64 glibc Linux userspace** at
@@ -1,5 +1,8 @@
# Services, readiness, and shutdown
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Native init and ARM VM](init.md) · [Cartridges](cartridges.md)
**Implemented in M2:** native s6 PID 1, a compiled base service graph, console
+120
View File
@@ -0,0 +1,120 @@
# Software cartridge format 1
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Build and use a cartridge](workstation.md) · [Original cartridge classes](cartridges.md)
A software cartridge is a complete GPT disk image with **1 + m** partitions,
where `m` is 1–32. Partition 1 describes every software package in all payload
partitions. All partitions use read-only EROFS. This format is the current
PROGRAM creation format; existing single-partition PROGRAM media remain readable.
SYSTEM, DATA and ENVIRONMENT layouts retain their existing meanings.
## Disk and filesystem layout
```text
GPT protective MBR + primary GPT
1 FDS_METADATA
FDS/CARTRIDGE.TOML
FDS/SOFTWARE.TOML
2 FDS_PAYLOAD02
bundles/demo.hello.tar.xz
bundles/demo.editor.tar.xz
3 FDS_PAYLOAD03
bundles/demo.report.tar.xz
...
backup GPT
```
Partition entries are consecutive, use Linux filesystem type GUIDs, unique
nonzero partition UUIDs, no GPT attributes, and nonoverlapping MiB-aligned
extents. Partition 1 starts at LBA 2048. Both GPT copies, their headers and CRCs
must agree. A full-drive write to a larger USB device relocates the backup table
and header. The existing on-target image parser and the workstation writer
share these checks.
`FDS/CARTRIDGE.TOML` uses the existing format-1 identity with `class = "program"`
and `media.writable = false`. It contains no startup command. A sample catalogue:
```toml
format = 1
[[software]]
id = "demo.hello"
name = "AArch64 hello"
version = "1.0"
architecture = "aarch64"
partition = 2
archive_bytes = 2308
unpacked_bytes = 70504
entries = 2
sha256 = "REPLACE_WITH_THE_64_CHARACTER_LOWERCASE_SHA256"
[software.commands]
hello = "bin/hello"
```
Those lengths and the digest are illustrative. The builder generates actual
values. IDs and command names use the existing restricted FDS identifier syntax.
IDs are unique per cartridge. Every declared payload partition must contain
exactly the catalogue's `bundles/<id>.tar.xz` archive names, and every payload
partition must be represented. Multiple software entries may share a partition.
The catalogue contains 1–128 software entries and at most 64 KiB of TOML.
## Archive contract
Bundles are deterministic USTAR archives compressed with single-threaded xz.
Entries are sorted, timestamps/UID/GID are zero, and permissions normalize to
0755 for directories/executables or 0644 for ordinary data. Host source symlinks
to regular files inside the root are flattened into regular files. Directory or
escaping symlinks are rejected; the archive itself never contains links.
The reader rejects:
- Absolute, parent-traversing, repeated-separator, non-UTF-8 or control-character paths.
- Duplicate entries, file-as-parent conflicts, links, devices, sockets, FIFOs,
privileged permissions, and GNU/PAX extension entries.
- Incorrect SHA-256, compressed length, unpacked length, entry count, or command
paths that do not name executable regular files.
- ELF files that are not little-endian 64-bit AArch64, or any ELF in a bundle
declared `architecture = "any"`.
- Invalid xz data, nonzero material after the tar end marker, and resource-limit
violations. Each archive is at most 512 MiB compressed, 1 GiB unpacked and
65,536 entries. Xz decompression has a 256 MiB memory limit.
A source path must fit USTAR's path fields. Packaging reports paths that cannot
be represented rather than emitting an unsupported extension header.
## Guest lifecycle
On insertion the daemon validates the complete GPT against kernel partition
geometry and the current USB disk identity. It reads metadata without following
symlinks, mounts every payload read-only/noexec/nosuid/nodev, and checks the
archive inventory and lengths. `fds bay BAY` reports the full catalogue;
`fds bays` stays compact. No archive command runs automatically on insertion.
`fds run BAY -- SOFTWARE-ID:COMMAND [ARGUMENT...]` verifies and extracts that
package on first use. It creates a private temporary filesystem, applies the
archive contract, then makes the finished tree read-only and accessible for
execution. The runtime cache has a stricter **256 MiB per software tree** limit,
including reserved inode overhead; a host-valid larger bundle may therefore
need splitting before running. Caches live under `/run/fds/software/<bay>/` and
vanish after eject, unplug, restart cleanup, or shutdown.
Managed consumers run as UID/GID 1000 with no new privileges in the bay's cgroup.
`FDS_APP`, `PATH`, `LD_LIBRARY_PATH` and `XDG_DATA_DIRS` point to that software
root. DISPLAY/XAUTHORITY retain the established optional desktop integration.
The xz utility is already an explicit dependency of the mandatory `fds-base`
package; runtime extraction adds no target package.
Build recipes, compilers, installation hooks and privileged archive scripts are
never part of this runtime path.
Safe eject stops consumers, rejects unexpected mount aliases, unmounts caches,
payloads and metadata, then reports SAFE. Confirmed surprise removal permits
lazy detachment after stopping consumers. Service restart removes stale mounts
before rescanning. SYSTEM stays immutable throughout.
SHA-256 detects corruption and binds an archive to its catalogue; it does not
establish a publisher's identity. Only run software you trust. Release signature
verification remains the separate [release workflow](releases.md).
@@ -1,5 +1,8 @@
# Twelve-bay stress tests
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Cartridge configuration](cartridges.md) · [Shutdown](power.md)
M11 exercises twelve devices together and failures during media writing. The
@@ -86,7 +89,7 @@ stress, use test media whose contents can be replaced. Any `fds burn` or
## Capture evidence on the target
Copy [capture-hardware](../tools/capture-hardware) to the DATA cartridge. At the
Copy [capture-hardware](../../tools/capture-hardware) to the DATA cartridge. At the
FDS console, run it explicitly through Bash; DATA intentionally does not permit
direct execution:
@@ -109,7 +112,7 @@ identifiers before sharing the files.
## Physical acceptance sequence
Copy the [physical session template](../tests/hardware/session-template.json)
Copy the [physical session template](../../tests/hardware/session-template.json)
and fill it with observed values. Null means unknown, not zero.
Record every run, including failures. The following is an initial repeatable
matrix, not evidence that it has already passed:
@@ -1,5 +1,8 @@
# FDS Rust tools
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [Implementation ledger](implementation-status.md)
M3 introduces `fds-common`, the `fds` command, and the `fds-stage0` diagnostic
+246
View File
@@ -0,0 +1,246 @@
# Troubleshooting the build
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
[Documentation index](README.md) · [First build](getting-started.md) · [Build reference](build-host.md)
Find the first failing command or `ERROR:` in the log, fix that problem, then
repeat the same step. Do not continue past a failed bootstrap and interpret later
missing files as independent problems. Run all commands below from the repository
root, as your regular user, unless explicitly shown otherwise.
## Find the right log
| Failing action | Where to look |
| --- | --- |
| `make bootstrap` | `out/logs/bootstrap.log` |
| Void build-container creation | `out/logs/xbps-bootstrap.log` |
| `make smoke-test` | `out/logs/smoke-test.log` |
| GNU hello build | `out/logs/xbps-hello.log` |
| `make check` | Terminal output; this target does not save a log automatically |
For example, after a failed smoke-test:
```sh
tail -n 80 out/logs/smoke-test.log
rg -n 'ERROR:|FAILED|error:|No space left|Permission denied' out/logs/
df -h .
```
A search with no matches exits 1. Read the surrounding log too: upstream tools
may report failures using other wording. Save logs before re-running if you need
the previous output; see [Development](development.md#preserve-logs-and-identify-inputs).
## Unsupported host, root execution, or missing commands
- `M0 requires an x86_64 Linux build host` or `supports Arch Linux`: use the
supported host described in [Your first build](getting-started.md). Changing
the check alone does not port the build system to another platform.
- `Run as a normal user, not root`: leave the root shell and run the build from
your normal account. Only host package installation uses sudo.
- `Missing host command`: install the prerequisites from the first-build guide,
then repeat bootstrap. `readelf` comes from binutils, `bwrap` from bubblewrap,
and `flock` from util-linux.
- `checkout path without whitespace`: move or obtain the checkout at a path
with no spaces, tabs, or other whitespace, then use that directory consistently.
If a previous root-run left files that your account cannot write, inspect their
ownership before correcting it. Do not respond by running all later builds as root.
## Bubblewrap or user namespace failure
Reproduce the same capability check bootstrap uses:
```sh
bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true
```
If it reports `Operation not permitted` or a namespace error, the host kernel,
container policy, or security configuration prevents the unprivileged container.
Use an Arch host that permits this operation, or have the host administrator
resolve the restriction. Repeated downloads, sudo builds, and a different Pi
will not resolve this preflight failure. Success is a zero exit status with no
output; run `make bootstrap` again once that works.
## Void submodule missing, wrong, or modified
Inspect before changing anything:
```sh
cat VOID_PACKAGES_COMMIT
git submodule status vendor/void-packages
git -C vendor/void-packages status --short
git -C vendor/void-packages diff HEAD --
```
If the submodule is missing, `make bootstrap` initializes it. The directory must
be part of a Git checkout; an unpacked source archive is insufficient. If a clone
was interrupted by a network error, restore connectivity and repeat bootstrap.
`Void checkout mismatch` means the checked-out commit differs from the explicit
pin. A `+` in `git submodule status` means it differs from the parent repository's
gitlink. These are two separate checks. Do not update the pin merely to silence
the error; first determine why the commits differ.
`Tracked Void files were changed` means upstream source has local edits. Preserve
and review the diff. Move intentional FDS changes into the overlay design or a
separate patch for review; restore upstream only after saving work you need.
Generated ignored files such as `etc/conf`, `hostdir`, and the masterdir are
normal. Never use a broad reset or clean command without checking what it removes.
## Configuration or overlay conflict
For `etc/conf differs from config/xbps-src.conf`, compare the two:
```sh
diff -u vendor/void-packages/etc/conf config/xbps-src.conf
```
Put the desired settings in `config/xbps-src.conf`, then follow
[the explicit synchronization procedure](development.md#change-build-configuration-deliberately).
A `diff` exit status of 1 means differences were found, not that the comparison failed.
For `Stale overlay`, compare the named `vendor/void-packages/srcpkgs/fds-*`
directory with its source under `packages/`. Preserve any unique edits in the
source first. Move that specific generated copy into a backup directory under
`out/`, outside `srcpkgs/`, then run `./tools/prepare-void` again. Do not move or
remove unrelated upstream packages. The Dasung package is now an active overlay. After changing it, reconcile that
generated copy by this same procedure before rebuilding.
`Overlay would replace an upstream package` is an intentional protection. Choose
an FDS-owned package name and design an overlay; do not disable the check or edit
the tracked upstream template in place.
## Download failure, checksum mismatch, or apparent stall
Bootstrap and package builds need network access. The first run can spend much
of its time downloading compiler packages. Read the latest log and check disk
space before assuming that a quiet download is a deadlock:
```sh
tail -n 40 out/logs/bootstrap.log
tail -n 40 out/logs/xbps-hello.log
du -sh out/downloads vendor/void-packages/hostdir
df -h .
```
A log or directory may not exist if its step has not started. Bootstrap's static
XBPS download has retries and a timeout; an incomplete transfer uses a `.part`
filename. A retry re-downloads that partial file. Completed cached archives are
checksum-checked on every bootstrap.
For a checksum failure, retain the error and compare the named file against
`config/host-tools.conf`. If the cached completed archive is corrupt, move that
specific archive to a backup location and repeat bootstrap so it downloads again.
Do not change the expected hash to match an unexplained download. If a clean
re-download still disagrees, stop and investigate the source.
A Void mirror timeout is separate from the source pin: the templates are pinned,
but bootstrap and missing build dependencies still use rolling binary repositories.
An unavailable historical dependency may need a reviewed input/mirror solution;
blindly changing the source commit does not establish an equivalent build.
## XBPS command not found or package index seems empty
The XBPS tools are intentionally not installed in the host's `/usr/bin`.
`make bootstrap` creates `.host/xbps/usr/bin/`. FDS build helpers set PATH for you.
For manual queries, use the explicit paths in the [package guide](packages.md).
When querying or indexing ARM output, set `XBPS_ARCH=aarch64`. The host's static
musl executable can otherwise default to its own architecture and ignore the ARM
package. `make smoke-test` uses the correct override. `tools/build-package` exports
packages but does not update `out/packages/aarch64-repodata`; use the indexing
command in the package guide after a standalone package build.
## Rust target, linker, or architecture error
Check the selected toolchain and local configuration:
```sh
rustup show active-toolchain
rustup target list --installed
cat rust-toolchain.toml
cat .cargo/config.toml
printenv RUSTFLAGS CARGO_BUILD_TARGET CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER
```
`printenv` exits nonzero when a listed variable is unset; that is normal here.
Bootstrap installs the required target and rustfmt for the pinned toolchain.
Repeat it if those components are missing.
Build from the repository root with the exact Cargo command in
[Development](development.md#edit-and-rebuild-the-rust-program). Environment
variables, command-line `+toolchain` overrides, or outside Cargo configuration
can change the target, flags, or linker. Reconcile such overrides with the
checked-in `rust-lld` and static CRT configuration. Adding an arbitrary system
linker or glibc library is not a repair for a static-musl build.
## Exec format error, ldd output, and QEMU skips
`./out/fds-smoketest` normally gives `Exec format error` on x86_64: it is an ARM
program. Run it explicitly with optional `qemu-aarch64`, as described in
[the first-build guide](getting-started.md#7-optionally-execute-the-arm-rust-program).
Do not change the target to x86_64 merely to make direct execution work.
`ldd: not a dynamic executable` with exit 1 is expected for the static program.
Use `./tools/verify-elf out/fds-smoketest aarch64 static` for the actual check.
Host `ldd` cannot establish that a foreign ARM program will run.
`SKIP: ARM execution` means QEMU is absent. Compilation and ELF checks still run.
If QEMU is present but fails, the smoke-test must fail; inspect its diagnostic.
The dynamic GNU hello program also requires an ARM glibc loader and libraries,
so the simple static-program QEMU command is not a complete hello runtime setup.
## Missing artifacts or check fails before any build
`make check` expects the Cargo release artifact in `target/`. Run the Rust build
or `make smoke-test` first. A bare Cargo build updates `target/`, while
`make smoke-test` also refreshes `out/fds-smoketest` and the output manifests.
If checksums fail after editing or replacing outputs, inspect what changed and
rebuild through smoke-test; do not edit the manifest to conceal a mismatch.
For a Rust formatting failure, run `cargo fmt --all`, inspect the source diff,
then repeat `make check`.
## Busy /tmp message during Void bootstrap
The initial verified bootstrap emitted
`rm: cannot remove '//tmp': Device or resource busy` during upstream cleanup.
The bubblewrap bind mount explained that instance; upstream exited successfully,
and container GCC, the cross build, and the public M0 commands all passed.
Do not treat every later `/tmp` error as harmless. Confirm a zero command exit
status and the final bootstrap PASS, then run smoke-test. If the command exits
nonzero, report it as a failure with the surrounding log.
## Rootfs, images, and runtime commands
M1 implements `make rootfs PROFILE=cli` and `make rootfs-test`. If the target is
missing, check that you are in the current checkout. Read [M1 rootfs](rootfs.md)
for prerequisites, shell access, logs, and retained failed-build directories.
Python 3.14+ is required for XBPS archive inspection. Supported profiles are
`cli`, `development`, and `recovery`; unknown names fail explicitly. Package
configuration must pass before an archive is published.
`tools/in-rootfs` supports ARM child execution through a private user-namespace
binary-format handler. It requires Linux 6.7+ and namespaced `binfmt_misc` support.
The workstation's global registry is unchanged. For a single ELF executable,
`--direct` invokes QEMU without installing even a private child-execution handler.
For example: `./tools/in-rootfs out/rootfs-aarch64 --direct /usr/bin/xbps-pkgdb -a`.
The tar is configured userspace, not a flashable image. M2 adds `make init-test`
for a full ARM boot and `make vm` for a temporary development shell. Follow
[Native init and ARM VM](init.md) for its dependencies, expected output, and
failure logs. For the full kernel/stage0/SYSTEM path, use `make all`,
`make boot-test`, then `make console-vm`. Run `fds bays` and `fds poweroff`
inside the booted FDS console, not on the Arch host. An exported ARM binary will
not execute directly on x86_64. Physical Pi acceptance remains deferred.
## What to include in a bug report
Include the exact failing command, its exit status if available, the relevant log
excerpt, host distribution and architecture, current Void pin, and whether this
is the first build or a previously working checkout. Mention local configuration
changes and toolchain overrides. Preserve the initial error; a later missing-file
message often only describes its consequence.
@@ -0,0 +1,24 @@
# Workstation tooling plan
The current source of requirements is [the active revision](current-revision.md).
The public tools remain native Linux Clap applications: `fds-cartridge` and
`fds-emulator`. Full OS cross builds keep their separate pinned Void environment.
New software creation uses Void source templates and `xbps-src`. The image builder
installs packages and dependencies on the workstation, verifies installed trees,
and writes them directly into EROFS program partitions with format-2 metadata.
Creation no longer generates xz software bundles. Legacy readers stay available.
The emulator still uses actual FDS ARM kernel/userspace, twelve virtual USB bays,
read-only SYSTEM/PROGRAM disks, private writable DATA overlays, the public serial
console, safe eject, forced removal and native shutdown. Hardware timing and
physical behavior require the procedures in the hardware notes.
Acceptance must cover real source-package builds, image integrity and write/readback,
foreground PATH commands and terminal behavior, background `fds run`, eject/unplug,
service restart and the rendered X11 control panel. Keep the frozen 0.1.0 release
unchanged. Build new matching artifacts and record their hashes and source inputs.
See [user instructions](../workstation.md) and [format reference](../software-format.md)
for the supported interface. Historical xz acceptance remains in
[the earlier workstation record](workstation-validation.md).
@@ -1,5 +1,8 @@
# Workstation extension acceptance
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
**Software acceptance passed on 2026-09-21.** This record covers the new native
Linux software/cartridge builder, whole-image writer, public QEMU emulator and
guest software runtime. It is separate from the immutable local 0.1.0 release.
+349
View File
@@ -0,0 +1,349 @@
# Build software cartridges and run FDS in QEMU
Development reference and historical context. For current operating instructions, use the [user manual](../README.md). Acceptance applies only to the source and artifacts identified in each record.
This guide uses a **Linux workstation**, not the Raspberry Pi. It takes you from
source software to a cartridge disk image, runs that image in FDS, and explains
how to write the same complete image to USB. All commands run from the checkout
root unless marked as guest commands.
The two native host tools are `fds-cartridge` and `fds-emulator`. They use typed
Clap interfaces; append `--help` to any command or subcommand. The target's `fds`
command remains a static AArch64 executable. The new host tools are never part
of the Pi base image. Dasung support remains in the base boot bundle.
The accepted local `out/fds-os-0.1.0/` release predates this extension. Leave that
release intact and build current images for the emulator. See
[workstation acceptance](workstation-validation.md) for current evidence.
For accumulated build and test images, use `make clean-preview` and `make clean`.
Current images, native tools, latest test fixtures and personal emulator sessions
are kept; obsolete automated test sessions are disposable. See
[Cleanup](cleanup.md) for the exact scope and how to preserve an older test run.
## Install and build the workstation tools
Use a normal Linux account with Rust/Cargo, a C linker, Bash and Make. The
repository's `rust-toolchain.toml` selects the Rust version. Runtime prerequisites
are:
| Tool | Purpose |
| --- | --- |
| `qemu-system-aarch64` | Runs the FDS ARM kernel and userspace using portable TCG |
| `qemu-img` | Creates a separate writable overlay for DATA; source images stay unchanged |
| `mkfs.erofs`, `fsck.erofs` from erofs-utils | Build and verify cartridge partitions; tested with 1.9.4 |
| `xz` | Compress/decompress software tarballs with a decompression memory limit |
| `bwrap` from bubblewrap | Restricts filesystem inspection to a private writable staging directory |
| A software-specific cross compiler | Builds AArch64 applications; unnecessary for portable scripts |
| `sfdisk`, `tar`, `mke2fs`, Python 3 | Independent integration tests; not needed to boot an existing image |
Install these with your distribution's package manager. Unprivileged user
namespaces must work for bubblewrap. Building **these host tools** does not need
Void, XBPS, an Arch host, a Pi, root, or a running FDS guest:
```sh
make workstation
export PATH="$PWD/out/workstation:$PATH"
fds-cartridge doctor
fds-emulator doctor
```
Current acceptance ran on Arch Linux x86_64. The tools use native Linux
interfaces and distribution-provided utilities; other distributions and ARM
workstations have not yet been exercised by this acceptance run.
The outputs are native binaries in `out/workstation/`. The build explicitly
selects the workstation architecture instead of the workspace's default ARM
target. Rust dependencies are pinned in `Cargo.lock`. `tar` 0.4.46 and its
`filetime` dependency provide archive decoding/creation; FDS additionally checks
entry types, paths, counts, sizes, executable architecture and hashes.
For this repository's **existing Arch/Void setup**, reuse its local tools:
```sh
make workstation
./tools/prepare-image-tools
./tools/in-void xbps-install -y qemu-img
out/workstation/fds-cartridge --image-tool-runner tools/in-image-tools doctor
out/workstation/fds-emulator doctor --qemu-runner tools/in-void
```
The install command above changes only the project-local build container. The
optional runners accept a program name followed by its arguments; omitting them
uses programs in the workstation's `PATH`. Set the image runner on every
cartridge create, inspect or preview invocation that needs it. A QEMU runner is
saved in the session and reused for later DATA insertions.
## Build two software bundles
Each software recipe declares an id, display name, version, target architecture,
root directory, and named executable commands. A trusted `[build]` section may
run a workstation compiler. It is executed only by `software build`, never by
`software pack`, inspection, or a cartridge insertion.
The [hello recipe](../../examples/software/hello/software.toml) builds an AArch64
C executable. Install an AArch64 glibc cross compiler as `aarch64-linux-gnu-gcc`,
then run:
```sh
fds-cartridge software build examples/software/hello/software.toml out/demo-hello
fds-cartridge software pack examples/software/report/software.toml out/demo-report
fds-cartridge software inspect out/demo-hello
```
If your cross compiler has a different name, edit the recipe's `build.command`.
With this checkout's existing Void cross toolchain, the equivalent is:
```sh
mkdir -p examples/software/hello/root/bin
./tools/in-void aarch64-linux-gnu-gcc -O2 examples/software/hello/hello.c \
-o examples/software/hello/root/bin/hello
out/workstation/fds-cartridge software pack \
examples/software/hello/software.toml out/demo-hello
out/workstation/fds-cartridge software pack \
examples/software/report/software.toml out/demo-report
```
Use **one** of those hello workflows. Outputs must not already exist: choose
new output names when rebuilding, then update your cartridge recipe. The example
bundles each contain `software.toml` and `<id>.tar.xz`. The archive contains the
contents of the software root, such as `bin/hello`; it has no leading `root/`.
For your own application, copy a recipe and populate its root with `bin/`,
`lib/` and `share/` as needed. C/C++ glibc builds target AArch64; static Rust
programs should use `aarch64-unknown-linux-musl`. Set `architecture = "any"` only
for scripts/data without ELF files. An x86_64 executable is rejected. Shared
libraries must be compatible with the target SYSTEM's glibc, or bundled as
appropriate. There is no cross-cartridge dependency resolver.
## Assemble the cartridge image
The example cartridge recipe groups the two bundles into two payload partitions:
```sh
fds-cartridge create examples/software/cartridge.toml out/demo-tools.img
fds-cartridge inspect out/demo-tools.img
```
For the project-local EROFS tools, use:
```sh
out/workstation/fds-cartridge --image-tool-runner tools/in-image-tools \
create examples/software/cartridge.toml out/demo-tools.img
```
The resulting complete disk image has **three GPT partitions**:
| Partition | GPT name | Contents |
| --- | --- | --- |
| 1 | `FDS_METADATA` | Cartridge identity and the software catalogue |
| 2 | `FDS_PAYLOAD02` | `bundles/demo.hello.tar.xz` |
| 3 | `FDS_PAYLOAD03` | `bundles/demo.report.tar.xz` |
To put both programs in one payload partition, use one `[[payload]]` entry with
`bundles = ["../../out/demo-hello", "../../out/demo-report"]`. This makes a
two-partition image. Each payload group creates exactly one partition. The first
partition identifies every software bundle and its payload partition.
Creation finishes only after verifying both GPT tables, all EROFS partitions,
every archive and the catalogue. Inspection prints JSON, including offsets,
lengths, SHA-256 and available commands. Repeating an unchanged recipe produces
the same image bytes. See [the format](software-format.md) for limits and rules.
## Boot current FDS in the emulator
You need three matching current artifacts: the FDS Pi kernel, its initramfs, and
a SYSTEM image containing the guest software runtime. Build these on the OS
build workstation using [the build guide](getting-started.md). For an already
bootstrapped checkout:
```sh
make rootfs PROFILE=cli
make system-card PROFILE=cli
make initramfs
make workstation
out/workstation/fds-emulator --session out/my-emulator start \
--qemu-runner tools/in-void
```
If QEMU is installed directly on your Linux workstation, omit `--qemu-runner`.
To use images copied from another build machine, supply their paths:
```sh
fds-emulator --session out/my-emulator start \
--kernel /path/to/kernel_2712.img \
--initramfs /path/to/fds-initramfs.img \
--system /path/to/fds-system-cli.img
```
Create `out/` first if using a fresh directory. The session directory must be
**new**, private, and short enough for Unix sockets (under 90 bytes including its
absolute parent path). Start waits for the FDS prompt and verifies the emulator
bay configuration. It uses two emulated CPUs and 1024 MiB RAM by default;
`--memory-mib 2048` increases RAM. `--timeout` adjusts the boot deadline.
The session records inputs, virtual devices, serial output and QEMU diagnostics.
QEMU stays running after the command exits. SYSTEM and software images are
read-only. No physical disks, host network interface, or Pi monitor are attached.
## Insert, use and remove cartridges
From the workstation:
```sh
fds-emulator --session out/my-emulator insert 01 out/demo-tools.img
fds-emulator --session out/my-emulator guest -- fds bay 01
fds-emulator --session out/my-emulator guest -- fds run 01 -- demo.hello:hello
fds-emulator --session out/my-emulator guest -- fds run 01 -- demo.report:report
fds-emulator --session out/my-emulator guest -- tail -20 /run/log/cartridged/current
fds-emulator --session out/my-emulator eject 01
```
Insertion is asynchronous: `fds bay 01` may briefly show `EMPTY` before USB
storage discovery completes. Retry the status command until it reports
`MOUNTED READ ONLY`, or inspect its error. The catalogue lists exact commands.
`fds run` starts a managed process and prints its PID; its output goes to the
cartridge service log. Packages are verified and extracted into temporary,
read-only RAM filesystems on first run. They are not compiled on the guest.
All twelve bays, numbered `01`–`12`, are available. Simultaneously mounted
PROGRAM cartridges need distinct cartridge IDs; inserting duplicate IDs is
reported as a guest error. `eject` asks FDS to stop
managed consumers, release mounts and declare `SAFE`, then removes the virtual
USB device. If FDS refuses, QEMU leaves the cartridge attached. To deliberately
simulate an accidental pull, use:
```sh
fds-emulator --session out/my-emulator unplug 01
```
A forced unplug can lose DATA writes. The guest must then detect the removal
and clean up its processes and mounts. `status` reports the actual QEMU devices
and block nodes as well as the recorded image paths.
Use the interactive shell if preferred:
```sh
fds-emulator --session out/my-emulator console
```
At the `FDS>` prompt, type `fds bays`, `fds bay 01`, or `fds run 01 --
demo.hello:hello`. **Ctrl-] detaches**; it does not stop the VM. Use a second
workstation terminal for insertion/unplug. Detach the console before `guest`,
`eject`, or ordinary `stop`, because they also need exclusive serial access.
Finish with:
```sh
fds-emulator --session out/my-emulator stop
```
This invokes native FDS shutdown. `stop --force` cuts virtual power without that
sequence. Logs and DATA overlays remain in the session directory. Choose a new
session directory to boot again; stopped sessions are retained for inspection,
not resumed from RAM snapshots.
## Writable DATA images
Insert an existing FDS DATA disk image with the same `insert` command. The
emulator creates a unique `data-<bay>-<id>.qcow2` overlay in the session directory;
all guest writes go there. The original image remains unchanged. Safe eject
retains the overlay, while inserting the original image again starts a fresh
one. Software cartridges remain read-only.
After eject or shutdown, export an overlay if you need the changed DATA contents:
```sh
qemu-img convert -f qcow2 -O raw /path/to/session/data-02-IDENTIFIER.qcow2 out/saved-data.img
```
Keep the original backing image at its recorded path until conversion completes.
Never convert or edit an image while it is attached to a running VM.
## Write the completed image to USB
The burn flow always starts with the complete image created above. It does not
construct partitions directly on a drive. Identify the intended **whole USB
drive**, unmount it, then make a preview as your normal user:
```sh
fds-cartridge preview out/demo-tools.img /dev/sdX out/usb-preview.json
```
Replace `/dev/sdX` with the actual whole USB drive. The JSON records its model,
size, insertion identity, exact image hash and an exact `confirmation` string.
Review these before copying the full phrase into the write command:
```sh
sudo /absolute/path/to/fds-cartridge write out/usb-preview.json \
--confirm 'COPY THE EXACT confirmation VALUE FROM THE PREVIEW'
```
The writer rechecks source and target identity, requires a USB whole disk,
rejects mounted/protected storage, writes the **full image**, flushes and verifies
readback. On larger drives it relocates the backup GPT to the end. A replaced
USB drive or changed image requires a fresh preview. This destroys the selected
drive's existing contents. No physical drive has been written by the automated
acceptance tests.
To rehearse on a disposable file without root or USB hardware:
```sh
truncate -s 64M out/disposable-usb.img
fds-cartridge preview out/demo-tools.img out/disposable-usb.img \
out/file-preview.json --file-target
fds-cartridge write out/file-preview.json --confirm 'COPY THE EXACT confirmation VALUE'
fds-cartridge inspect out/disposable-usb.img
```
The file must be at least as large as the source image. Use `--image-tool-runner`
for preview/inspection if EROFS tools are provided by the local wrapper.
## Troubleshooting
| Symptom | What to do |
| --- | --- |
| Required executable not found | Run the corresponding `doctor`; install the named host utility or select an explicit runner |
| Bubblewrap namespace failure | Enable unprivileged user namespaces according to your workstation policy; do not run creation/inspection as root |
| Output already exists | Use a new bundle/image/preview/session name; creation does not overwrite outputs |
| Unknown `fds.emulator` option or missing emulator settings | Rebuild both the current initramfs and SYSTEM; the frozen 0.1.0 images predate this feature |
| Console in use | Detach with Ctrl-] before guest commands, safe eject, or shutdown |
| Guest command timed out | Inspect `console.log`; the command may still be running, so do not blindly repeat a write |
| Incomplete insertion | Run `unplug BAY` to reconcile the recorded intent with actual QEMU devices, then reinsert |
| Software digest/path/architecture error | Rebuild the bundle and cartridge on the workstation; the guest will not run an invalid archive |
| Cache limit exceeded | Reduce the software bundle; one runtime tree including inode overhead is limited to 256 MiB |
| Safe eject blocked | Close unmanaged processes or extra mounts using the media, then retry; `unplug` is only for deliberate failure simulation |
| VM will not boot | Read session `console.log` and `qemu.log`; verify all three supplied boot artifacts belong to the current build |
QEMU exercises the actual Linux/FDS software path. It does not emulate Pi
firmware, RP1, USB power sequencing, the physical twelve-bay wiring, or Dasung
power recovery. Those acceptance checks remain hardware procedures.
## Physical Pi acceptance procedure (deferred)
After assembling and calibrating the physical bay map, use a disposable USB
cartridge and the current matching SYSTEM/initramfs. Perform these checks on the
Pi; the VM results do not replace them:
1. Build the hello/report cartridge on the workstation, record its image hash,
and use preview/confirmation to write that entire image to the chosen USB drive.
2. Insert it in a calibrated bay. Record `fds --json bay BAY`; confirm both
software entries and their declared payload partitions appear.
3. Run `demo.hello:hello` and `demo.report:report`. Save their output from the
cartridge log and confirm UID 1000. Check `/proc/self/mountinfo` for read-only
payload and software-cache mounts.
4. Run `fds eject BAY`, verify SAFE, then remove and reinsert the cartridge in
another calibrated bay. Repeat both commands. Record any USB enumeration or
I/O errors.
5. With a disposable cartridge and a deliberately long-running test program,
test surprise removal separately. Verify its managed processes terminate and
all of that bay's software mounts disappear. Do not use valuable DATA for
this failure test.
6. Exercise native shutdown with active software, cold boot, and a real USB
power cycle. Save actual observations and kernel/service logs. Follow the
separate [Dasung hardware procedure](dasung.md) for monitor recovery.
Record the Pi, hub, USB drive and kernel versions with the result. Hardware
latency, electrical behavior and data durability remain unverified until these
measurements are performed on the assembled machine.
+11 -39
View File
@@ -1,12 +1,10 @@
# Pi 5 EEPROM configuration
[Boot images](boot.md) · [Recovery](recovery.md) · [Implementation ledger](implementation-status.md)
[Boot images](developer/boot.md) · [Recovery](recovery.md) · [User manual](README.md)
M12 now provides an offline configuration workflow using the pinned official
Raspberry Pi tool and a real Pi 5 firmware image. The host checks passed;
application to a physical Pi, boot order, PMIC behavior and timing remain deferred.
`tools/configure-pi-eeprom` only creates files. It never reads or writes a hardware
EEPROM, invokes a firmware updater, or reboots a machine.
The profile tool creates reviewable firmware/configuration files on the
workstation. Applying them to a Pi is a separate maintenance operation. Preserve
the machine's original firmware and configuration before changing them.
## Preview a profile on the build workstation
@@ -15,7 +13,6 @@ From the repository root:
```sh
./tools/configure-pi-eeprom --profile production
./tools/configure-pi-eeprom --profile development
make eeprom-test
```
The first invocation downloads three checksum-pinned inputs into
@@ -56,17 +53,14 @@ before applying anything to that machine.
Boot order is read from the right. Both profiles avoid scanning the twelve USB
cartridges for firmware boot. Development retains an SD rescue path. Both disable
network-install keyboard detection; Raspberry Pi documents that this detection
adds USB initialization and enumeration work. These settings do not establish a
measured FDS boot improvement. [Official bootloader configuration](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#bootloader-configuration).
adds USB initialization and enumeration work. [Official bootloader configuration](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#bootloader-configuration).
On Pi 5, the power-off setting requests PMIC standby on halt; the dedicated power
button remains the wake mechanism. The wait setting leaves cold power-on boot
enabled. Whether the assembled computer and attached hardware behave as intended
still needs a physical test. [Official power settings](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#POWER_OFF_ON_HALT).
enabled. After shutdown, check that attached power hardware follows the selected policy. [Official power settings](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#POWER_OFF_ON_HALT).
The source profiles are [production.conf](../config/eeprom/production.conf) and
[development.conf](../config/eeprom/development.conf). No GPIO wake option or
unmeasured HDMI tuning is added.
[development.conf](../config/eeprom/development.conf). Review these files before choosing a profile.
## Preserve the machine's settings
@@ -102,12 +96,10 @@ This checks file construction and configuration roundtrip, not hardware
compatibility. No secure-boot key, fuse, customer signature or OTP setting is
modified by this helper.
## Apply and roll back during physical testing
## Apply and roll back
This step is deliberately outside the host/VM acceptance run. Use the Pi's
maintenance environment and the official installed EEPROM utilities. Once the
reviewed files and saved original inputs are available there, the upstream
configuration interface is:
Apply the reviewed configuration from a Raspberry Pi maintenance environment
with the official EEPROM utilities, then follow the updater's restart instructions.
```sh
sudo rpi-eeprom-config --apply ./configured.conf ./base.bin
@@ -126,24 +118,4 @@ If the firmware version itself changed, use the saved matching original firmware
for the rollback operation. Keep the maintenance SD and an external EEPROM rescue
route available; internal recovery cannot repair an EEPROM that prevents internal
boot. The official [EEPROM update and recovery guide](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-boot-eeprom)
describes that physical workflow. FDS has not yet tested it on your Pi.
## Inputs, dependencies and software evidence
The input lock is [inputs.json](../config/eeprom/inputs.json), pinned to
`raspberrypi/rpi-eeprom` commit `2fee426f27b6c54d3f5b6f36efd9a2fe1286a45d` and
Pi 5 preview firmware `pieeprom-2026-09-12.bin`. Every download is SHA-256 checked;
a changed cached input is rejected. The
[official source tool](https://github.com/raspberrypi/rpi-eeprom/blob/2fee426f27b6c54d3f5b6f36efd9a2fe1286a45d/rpi-eeprom-config)
is stored unchanged in the cache. Void's tracked source remains unchanged.
The host workflow uses existing Python and curl. It does not add a target daemon,
package or Rust dependency. The upstream parser's optional signing dependencies
are not needed for configuration-only operations.
`make eeprom-test` passed production/development roundtrips, unchanged firmware
payloads, repeatable binary output, refusal to overwrite an existing directory,
preservation of custom settings, exact rollback of conditional settings, and
rejection of malformed images, device nodes and oversized configuration files.
Initial evidence is `out/m12-eeprom.zi144zib/`, with log
`out/logs/m12-eeprom-check.log`. The complete local release acceptance is recorded in [M12 validation](m12-validation.md).
describes that physical workflow.
+58 -198
View File
@@ -1,232 +1,92 @@
# Your first build
# Build and start FDS
[Documentation index](README.md) · [Troubleshooting](troubleshooting.md)
This guide creates a bootable FDS SYSTEM image and starts it in QEMU. Use an
x86_64 Arch Linux workstation for the complete OS build. To build application
cartridges on another Linux distribution, use the [workstation guide](workstation.md).
A prepared set of kernel, initramfs and SYSTEM images can be run on a Linux
workstation without building the OS locally.
This walkthrough takes you from an FDS Git checkout to two verified ARM Linux
artifacts. It explains the output as you go. You do not need a Raspberry Pi.
## Prepare the workstation
At the end you will have a small Rust program built with static musl and GNU hello
packaged for aarch64 glibc. This proves that the workstation can build the two
kinds of software FDS/OS needs. It does not produce a bootable OS image.
Use a normal user account, a Git checkout with its submodule metadata, and a
checkout path without spaces. Initial preparation needs Internet access. The
host must support unprivileged user namespaces and namespaced binfmt_misc
(Linux 6.7 or newer). Full builds and VM tests use tens of GiB per build tree;
check free space with `df -h .` and use [cleanup](cleanup.md) between build batches.
## 1. Check the machine and checkout
Use an x86_64 Arch Linux workstation as a normal user. Check it with:
```sh
uname -m
cat /etc/os-release
id -u
```
Expect `x86_64`, `ID=arch`, and a user ID other than `0`. The bootstrap script
currently rejects other host platforms and root execution.
Enter your FDS checkout. For the existing development workspace:
```sh
cd /home/felis/source/fds
pwd
git status --short
```
On another workstation, substitute its checkout directory. It must contain
`Makefile`, `Cargo.toml`, `tools/`, `.gitmodules`, and Git metadata. Avoid spaces
in the path. There is no configured public remote in this workspace; obtain a
Git checkout from the project owner rather than using an invented clone URL.
You need HTTPS access to GitHub, Void repositories, Rust distribution servers,
and package source hosts. Initial downloads total hundreds of MB. Plan for
several GB of free disk space; the verified workstation used roughly 1.6 GB for
the Void masterdir and 455 MB for its hostdir cache, before Rustup and other
outputs. These are observations, not fixed requirements or a build-time promise.
```sh
df -h .
```
## 2. Install host prerequisites
On an otherwise maintained Arch installation, install the required packages:
From the checkout directory, install host prerequisites:
```sh
sudo pacman -S --needed bash coreutils binutils git curl make file tar xz gzip zstd \
bubblewrap rustup ca-certificates findutils diffutils grep sed gawk util-linux
```
This is the host package installation step. Subsequent build commands run as your
normal user. The alternative `./tools/bootstrap-host --install-deps` runs the same
package installation and then continues bootstrap; choose one route.
The [dependency reference](build-host.md#host-dependencies) explains why each
package is needed. This project uses Rustup to select its pinned Rust compiler,
not an arbitrary system Rust version. If pacman reports an existing Rust package
conflict, resolve the host package choice before continuing; do not force file
replacement.
Check that bubblewrap can create a user namespace:
```sh
bubblewrap rustup ca-certificates findutils diffutils grep sed gawk util-linux \
python e2fsprogs libarchive lz4
bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true
```
Success produces no output and exits normally. If it fails, use
[Troubleshooting](troubleshooting.md#bubblewrap-or-user-namespace-failure).
This is a host capability requirement, not something a Pi can fix.
## 3. Prepare the build environment
From the repository root:
Python must be 3.14 or newer. The namespace check exits successfully without
output. Build commands below run as your normal user, sequentially; they share
one project-local Void build container.
```sh
make bootstrap
```
This performs these steps:
1. Initializes `vendor/void-packages` at the recorded Git commit.
2. Downloads the pinned static XBPS archive, verifies its SHA-256, and extracts
the host tools into `.host/xbps/`.
3. Copies the checked-in Void build configuration into the local upstream checkout.
4. Installs Rust 1.98.0, rustfmt, and `aarch64-unknown-linux-musl` through Rustup.
5. Fetches the locked workspace crates needed for Cargo to resolve the Dasung member,
including when building the dependency-free smoketest offline.
6. Creates the x86_64 glibc Void build container and verifies that GCC runs inside it.
The Void build container supplies compilers and package tools; it is not the
future FDS root filesystem. Rustup changes your user toolchain installation.
XBPS host tools stay inside this repository, and no Pi storage is written.
The final success marker is:
```text
PASS: M0 host bootstrap; run make smoke-test to build both aarch64 artifacts
```
The command saves its output in `out/logs/bootstrap.log`. Re-running bootstrap
reuses the checkout and downloads, but still checks their pins and integrity.
If it exits nonzero, stop here and resolve that failure before running smoke-test.
## 4. Cross-build both artifacts
```sh
make smoke-test
```
First Cargo builds `rust/fds-smoketest` for ARM, using the static musl libraries
and linker bundled with the Rust toolchain. Then xbps-src cross-compiles GNU
hello and packages it for aarch64 glibc. The first package build downloads the
cross compiler, target libraries, and build dependencies such as texinfo.
The script inspects actual ELF files and local package metadata. It checks:
| Artifact | Required result |
| --- | --- |
| Rust smoketest | AArch64, static linkage, no dynamic interpreter or shared library requirement |
| GNU hello in the XBPS package | AArch64, glibc loader and `libc.so.6` dependency |
A successful run contains these messages; other compiler output appears between them:
```text
PASS: aarch64 static ELF
PASS: aarch64 glibc ELF
PASS: XBPS package hello-2.12.3_1 architecture=aarch64 (glibc)
PASS: M0 smoke test complete
```
Without QEMU, it also reports:
```text
SKIP: ARM execution (optional qemu-aarch64 not installed); ELF verification passed
```
That skip means the ARM program was compiled and inspected, but not executed.
It does not hide a failed compiler or ELF check. If QEMU is installed, execution
must pass; an emulator failure is a real smoke-test failure.
## 5. Run the protection and formatting checks
```sh
make check
```
Run this after smoke-test: it expects the built Rust executable to exist. It
checks that bad inputs are rejected, that the Void pin and upstream files are
protected, that overlays cannot replace upstream packages, and that Rust source
is formatted. Several `PASS: rejects ...` lines are expected; they mean the
negative tests worked. The final commands include:
Bootstrap prepares pinned Rust and XBPS tools and the Void build container.
The smoke test checks the AArch64 build toolchains; `make check` validates the
build guardrails. Stop and resolve any failed command before continuing. Logs
are in `out/logs/`. Repeated preparation reuses verified downloads and caches.
```text
PASS: M0 guardrail checks complete
cargo fmt --all -- --check
```
Rustfmt is normally silent on success. `make check` does not rebuild the ARM
program. After a source change, rebuild first, as described in
[Development](development.md#edit-and-rebuild-the-rust-program).
## 6. Inspect what you built
## Build the images
```sh
ls -lh out/fds-smoketest out/packages/
file out/fds-smoketest
./tools/verify-elf out/fds-smoketest aarch64 static
sha256sum -c out/manifests/artifacts.sha256
make rootfs PROFILE=cli
make system-card PROFILE=cli
make initramfs
make workstation
```
`file` should include `ARM aarch64` and `statically linked` (or `static-pie linked`).
The checksum command should report `OK` for both artifacts. Hashes verify the
files against this run's manifest; they are not release signatures.
The rootfs build includes the kernel and base packages. These are the three
inputs needed by the emulator:
For ELF details, use:
| File | Purpose |
| --- | --- |
| `out/kernel/boot/kernel_2712.img` | ARM kernel |
| `out/fds-initramfs.img` | Early startup and SYSTEM discovery |
| `out/fds-system-cli.img` | Read-only operating system cartridge |
`make rootfs PROFILE=development` followed by `make system-card PROFILE=development`
builds a SYSTEM with compilers, Git, Vim, debuggers and display diagnostics.
Both profiles include WindowMaker and the Dasung daemon. The desktop starts on
request. `make all` also assembles recovery, boot and internal-storage images;
see [internal installation](internal-storage.md) when preparing a physical machine.
## Start the emulator
With the project-local QEMU tools:
```sh
readelf -hW out/fds-smoketest
readelf -lW out/fds-smoketest
readelf -dW out/fds-smoketest
./tools/in-void xbps-install -y qemu-img
out/workstation/fds-emulator --session out/my-emulator start --qemu-runner tools/in-void
out/workstation/fds-emulator --session out/my-emulator console
```
The header names AArch64. There should be no `INTERP` segment or `NEEDED` library.
An x86_64 host's `ldd` may say `not a dynamic executable` and return 1; that message
is expected here, but is insufficient by itself to prove a foreign ELF is static.
The [package guide](packages.md#inspect-the-built-package) walks through the
corresponding inspection of GNU hello.
## 7. Optionally execute the ARM Rust program
The [Arch qemu-user package](https://archlinux.org/packages/extra/x86_64/qemu-user/)
provides user-mode emulation. Install it if you want to execute the ARM program
on the x86_64 workstation:
If QEMU is installed directly on the workstation, omit `--qemu-runner`.
The session directory must be new. At the `FDS>` prompt:
```sh
sudo pacman -S --needed qemu-user
qemu-aarch64 out/fds-smoketest
fds info
fds bays
fds --help
```
Expected output:
The shell runs as the ordinary `fds` user. Press **Ctrl-]** to detach from the
console; the VM remains running. Stop it from the workstation:
```text
FDS/OS M0: aarch64 static-musl OK
```sh
out/workstation/fds-emulator --session out/my-emulator stop
```
Because this executable is static, this command needs no ARM rootfs or glibc
sysroot. Explicitly invoking `qemu-aarch64` also avoids needing automatic binfmt
registration. Re-running `make smoke-test` records its own QEMU check when the
emulator is on PATH.
This executes one Linux userspace program. It does not emulate the Pi's firmware,
USB bays, display, battery, or operating-system boot. QEMU execution was not
performed during the initial M0 validation; see the [recorded results](m0-validation.md).
## 8. Decide what to do next
- To build the configured OS filesystem and try its ARM programs, follow [Rootfs](rootfs.md): `make rootfs PROFILE=cli`, then `make rootfs-test`.
- To boot native s6 and open a development console, follow [Native init and ARM VM](init.md): `make init-test`, then `make vm`.
- To build the included Dasung monitor package, follow [Dasung](dasung.md).
- To edit the existing Rust program, follow [Development](development.md).
- To understand the `.xbps` artifact, follow [Packages](packages.md).
- To understand the intended removable operating system, read [Architecture](architecture.md) and [Cartridges](cartridges.md).
- To see when bootable images become possible, read [Roadmap](roadmap.md).
Bootstrap, smoke-test, and check complete the M0 foundation. Continue with the
M1 guide for a configured rootfs archive. There is still no flashing step.
Continue with [building and inserting software cartridges](workstation.md).
That guide also covers using copied boot images, DATA overlays and USB writing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+10 -43
View File
@@ -1,11 +1,11 @@
# Internal storage and machine settings
[Documentation index](README.md) · [Boot images](boot.md) · [Recovery](recovery.md)
[Documentation index](README.md) · [Boot images](developer/boot.md) · [Recovery](recovery.md)
The internal NVMe supplies the Pi's firmware boot files, independent recovery,
and machine settings. SYSTEM remains a separate removable cartridge. Ordinary
user files belong on DATA, not internal storage. No physical disk is written by
any build command below; Pi/NVMe boot and power-loss tests remain deferred.
Internal storage contains boot files, an independent recovery system and
persistent machine settings. The operating system itself lives on a removable
SYSTEM cartridge. This guide covers preparing the internal disk and updating
its settings from recovery.
## Build the complete disk image
@@ -42,17 +42,7 @@ For a 2 GiB recovery allocation or larger settings partition:
./image/build-internal --recovery-mib 2048 --internal-mib 512
```
Output directories supplied with `--output-directory` must already exist and be
empty. The builder accepts ordinary image files, never a host block-device
output. Final hardware provisioning and identity checks belong to the physical
acceptance procedure; do not confuse a partition payload with the complete disk.
## Install the internal disk when hardware is available
This is a **deferred physical procedure**. The image and virtual NVMe workflow
are software-tested; writing and booting the user's actual NVMe still require
the hardware. Installation erases the selected disk, including any existing
machine settings. Retain backups before replacing an existing installation.
## Install the internal disk
Use an NVMe enclosure or another Linux machine that can access the target drive
while the Pi is off. First verify a downloaded release using a separately trusted
@@ -132,12 +122,7 @@ alternative complete GPT images, each containing one `FDS_SYSTEM` partition.
Readback and backup-GPT relocation apply to this disk too. Check its partition
label, safely disconnect it, and insert exactly one SYSTEM cartridge in the Pi
before normal boot. Subsequent cartridge creation and updates can use FDS's
confirmed [media workflow](media-tools.md) after bay calibration.
On the first physical boot, check `fds info`, `fds machine status`, and
`fds bays`. The supplied bay map is empty until calibration. Follow
[physical acceptance](stress-testing.md) to measure ports, test the Dasung
display, and record actual boot/shutdown behavior before relying on the machine.
confirmed [media workflow](developer/media-tools.md) after bay calibration.
## Configure the machine before building
@@ -149,9 +134,9 @@ Copy `config/machine/` to your own directory. It contains three files:
- `hardware-catalog.toml`: optional USB identification names using the same schema
as the base catalog. Entries are data and cannot run commands.
The supplied bay map is deliberately empty because the physical wiring has not
been measured. Do not invent Pi USB paths. USB 2 and USB 3 companion ports need
explicit aliases for the same bay.
The supplied bay map is empty. Record actual controller/port identities for
each physical slot before installing settings. USB 2 and USB 3 companion ports
need explicit aliases for the same bay.
```sh
cp -a config/machine out/my-machine
@@ -253,21 +238,3 @@ diagnostic snapshot, including metadata already inspected during this boot.
Retrieve it with `fds machine fetch cartridges-first.json /tmp/saved-inventory.json`.
The daemon's live metadata cache remains volatile and is rebuilt from currently
attached devices; saved snapshots are never used to authorize media actions.
Together, explicit boot reports, inventory snapshots and hardware captures provide
the boot history, cached metadata and diagnostics assigned to FDS_INTERNAL in
master-plan section 12, without adding internal writes to startup or shutdown.
## Dependencies and validation
No new target package or Rust crate is required. The host image-tool prefix adds
`e2fsprogs` for ext4 creation, inspection and validation; it already supplies FAT
and EROFS tools. A private unprivileged user namespace gives created files root
ownership without requiring a root build session.
`make internal-test` exercises the actual packaged runtime in ARM VMs with
virtual NVMe. Acceptance evidence belongs in [M12 validation](m12-validation.md);
a passing VM does not verify the Pi EEPROM, PCIe path or physical flash durability.
The Linux [ext4 mount documentation](https://www.kernel.org/doc/html/latest/admin-guide/ext4.html)
explains why read-only loading also disables journal replay. Filesystem creation
options follow the upstream [mke2fs manual](https://man7.org/linux/man-pages/man8/mke2fs.8.html).
+6 -37
View File
@@ -1,15 +1,9 @@
# Recovery and rollback
[Documentation index](README.md) · [Boot images](boot.md) · [Media tools](media-tools.md) · [EEPROM](eeprom.md)
[Documentation index](README.md) · [Boot images](developer/boot.md) · [Write cartridges](workstation.md#write-a-cartridge-to-usb) · [EEPROM](eeprom.md)
Recovery is a separate, read-only FDS image. It contains native s6, Bash, GNU
utilities, XBPS, filesystem tools, the static FDS tools, and the base Dasung
controller. It does not need files or programs from a SYSTEM cartridge.
The software workflow is covered by `make recovery-test`; physical Pi recovery remains deferred.
The [complete internal disk](internal-storage.md) includes this recovery image;
its guide separates verified image construction from deferred physical
installation. The build commands below create ordinary files and do not write a
host disk.
FDS recovery is an independent maintenance system on internal storage. It can
inspect a failed SYSTEM, check DATA and help prepare replacement cartridges.
## Build the recovery image
@@ -18,16 +12,8 @@ From the repository root, run these commands sequentially:
```sh
make recovery
make rootfs-test
make recovery-test
```
The full VM test also needs the Pi kernel/initramfs and a known-good CLI SYSTEM
image. On a fresh checkout, build those first with `make rootfs PROFILE=cli`,
`make rootfs-test`, `make initramfs`, and `make system-card PROFILE=cli`, then
run the recovery sequence above. Run builds and VM tests sequentially because
they share the same project-local Void environment.
`make recovery` builds the `recovery` rootfs profile independently, configures all
packages and caches at image construction time, then creates EROFS. Its outputs
are:
@@ -54,9 +40,7 @@ Stage0 can enter recovery in two ways:
Restore normal mode when maintenance is complete.
Stage0 requires exactly one readable `FDS_RECOVERY` partition and mounts it
read-only. It does not silently choose between duplicate partitions. The ARM VM
suite supplies disposable virtual partitions; physical firmware/NVMe/display
behavior must still be checked on the Pi.
read-only. It does not silently choose between duplicate partitions.
The local prompt is:
@@ -94,10 +78,6 @@ inactive SYSTEM mounts read-only under `/run/fds/media/NN`; `fds bay N` reports
the actual path. A bad filesystem or manifest produces an error instead of
running anything from the cartridge.
An empty bay configuration produces `UNCONFIGURED`, not guessed bay numbers.
See [Cartridges](cartridges.md) for calibration. Permanent machine configuration
on `FDS_INTERNAL` is part of the remaining M12 work.
## Check and repair DATA
Start with a read-only check. For example, for DATA in bay 2:
@@ -112,10 +92,6 @@ disk exclusively, verifies GPT and the kernel partition identity, and invokes
`e2fsck -f -n`. It does not repair the filesystem. A clean result leaves DATA
unmounted and reports `SAFE TO REMOVE`.
If DATA was explicitly activated writable, eject that session first. Close any
shell whose current directory is on DATA and any other reader before checking;
an ordinary busy-unmount failure is reported, never bypassed with lazy unmount.
If the check reports problems, review its log and preserve a backup where
possible. Preview the repair with:
@@ -133,13 +109,6 @@ repairs and stops when manual judgement is required. It then flushes the device,
invalidates its block cache, and runs a second `e2fsck -f -n`. Only a successful
verification produces `DATA REPAIRED AND VERIFIED` and `SAFE TO REMOVE`.
The checker uses a temporary kernel loop device backed by the already verified
partition descriptor. This lets `e2fsck` take its own exclusive device claim
while FDS retains the physical whole-disk reservation. The loop is removed
automatically when its last descriptor closes. This uses the existing kernel
loop driver, `libc` crate and base `e2fsprogs`; no new package or Rust dependency
is introduced.
Failed or interrupted checks/repairs retain a quarantine record across cartridge
daemon restarts for the same insertion. They do not inherit an earlier SAFE
status. The checker holds the disk reservation and is killed if its supervising
@@ -175,7 +144,7 @@ fds burn system /run/fds/media/02/fds-system-cli.img BAY04
Use the actual source mount shown by `fds bay 2`. If the destination already
contains a mounted cartridge, run `fds eject 4` first. The burn preview identifies
the source checksum, destination capacity/model/serial and confirmation command.
Follow [Media creation and writing](media-tools.md) for confirmation and status commands.
Follow [Media creation and writing](developer/media-tools.md) for confirmation and status commands.
Completion requires device flush, readback and GPT verification. Recovery uses
the same protected writer as the main system: it refuses mounted destinations,
active root storage and disks containing internal FDS partition names.
@@ -199,7 +168,7 @@ and boot events with checksums. Its output is temporary unless copied to a
healthy DATA cartridge after explicitly activating that DATA with `fds data use N`.
Do not activate the damaged cartridge merely to save a report. Eject writable
DATA after copying, or let `fds poweroff` perform the normal verified shutdown.
See [Shutdown](power.md) when shutdown reports a blocking DATA error.
See [Shutdown](developer/power.md) when shutdown reports a blocking DATA error.
Recovery is independent of SYSTEM, but it still depends on working internal
boot storage, the kernel and firmware. An external rescue medium is required
+18 -142
View File
@@ -1,88 +1,12 @@
# Release signatures and verification
# Verify a release
[Documentation index](README.md) · [M12 evidence](m12-validation.md)
Verify a downloaded release before writing its images to a cartridge or internal
disk. Verification checks the release manifest signature and the length and
SHA-256 of every listed artifact. Obtain the signer's public key through a
trusted channel independent of the download.
`fds-release` signs an artifact manifest and verifies its signature and every
listed file. The static ARM executable is included in the base CLI package, so
CLI, development and recovery images can all verify downloads. The workstation
build supplies an x86_64 signer with the same format.
The signing tool has passed host, static ARM and independent OpenSSL acceptance.
The complete frozen-input, versioned local 0.1.0 release passed acceptance;
see [M12 validation](m12-validation.md#final-local-release-acceptance) for its identity
and [Offline rebuilds](reproducible-builds.md) for the workflow.
The examples below describe the working signing interface; they do not identify
a published or hardware-qualified FDS release.
## Build and test the tools
After [bootstrap](getting-started.md), run:
```sh
make signing
make signing-test
```
The workstation executable is
`target/x86_64-unknown-linux-gnu/release/fds-release`. The ARM executable is
`target/aarch64-unknown-linux-musl/release/fds-release`; `make tooling` also
exports it to `out/fds-release`. An ARM file cannot execute directly on the
x86_64 workstation. `make signing-test` runs that file through the project-local
QEMU interpreter as well as testing the host executable.
OpenSSL is a **host test dependency** used as an independent Ed25519
implementation. It is not linked into the FDS verifier. The test creates private
disposable keys under its `out/m12-signing.*` directory, checks exact signatures,
then exercises wrong keys, altered manifests, damaged or missing artifacts,
symlinks, FIFOs, malformed fields and unsafe paths. Test keys are not release
keys.
## Create and retain a signing key
For a local signing identity, choose an existing private directory outside the
checkout and its build output. This example creates a new directory; choose a
different name if it already exists:
```sh
mkdir -m 700 "$HOME/fds-signing"
target/x86_64-unknown-linux-gnu/release/fds-release keygen \
"$HOME/fds-signing/release-01"
```
The command creates `release-01.key` (32 secret bytes, mode 0600) and
`release-01.pub` (the hexadecimal public key). Existing files are never
overwritten. It prints SHA-256 of the decoded 32-byte public key, never the
secret. This fingerprint differs from `sha256sum release-01.pub`, which hashes
the hexadecimal text and its newline. Retain the private key separately from release downloads and source
archives. Anyone who has that key can sign a release under that identity.
If public-key export failed after private-key creation, retain the private file
and export it to a new path:
```sh
target/x86_64-unknown-linux-gnu/release/fds-release public-key \
"$HOME/fds-signing/release-01.key" "$HOME/fds-signing/recovered.pub"
```
Private keys must be regular files owned by the invoking user with no group or
other permissions. The tool rejects symlinks and invalid key lengths. Key
generation uses Linux `getrandom`; temporary seed storage and the signing key
are zeroized when dropped.
## Sign an assembled directory
An assembled release directory must already contain its artifacts and a
`manifest.json` matching the format below. Signing verifies every artifact's
length and hash before creating `manifest.sig`:
```sh
target/x86_64-unknown-linux-gnu/release/fds-release sign \
/path/to/release-directory --key "$HOME/fds-signing/release-01.key"
```
If a signature already exists, use a new release directory. The tool deliberately
does not replace signatures or silently rewrite the manifest. A failed operation
may leave its new output file for inspection; it never declares it verified.
On a workstation, build the verifier with `make signing` after bootstrap. On FDS,
`fds-release` is included in the base system and recovery.
## Verify before using images
@@ -103,65 +27,17 @@ target/x86_64-unknown-linux-gnu/release/fds-release verify \
/path/to/release-directory --key /path/to/trusted-fds-release.pub
```
A successful command reports `VERIFIED FDS/OS 0.1.0`, the number of checked
artifacts, and `Hardware validation: deferred`. Failure returns exit status 2
and an explanation. Keep downloaded images unchanged between verification and
use. Verification covers only files listed in the manifest; unrelated extra
files are not endorsed. The [media writer](media-tools.md) performs its own
image/layout validation and readback checks when writing a cartridge.
Signature verification is an explicit pre-installation step. It is not enforced
by Pi firmware or stage0 during boot.
These are distribution signatures. The Pi firmware and stage0 currently do not
enforce them at boot, so this is not a secure-boot implementation or a claim
that the hardware has been tested.
Success reports `VERIFIED` and identifies the release. Failure returns a nonzero
exit code with an explanation. Keep files unchanged between verification and
writing. Files absent from the signed manifest are outside its scope.
## Manifest and signature format
The cartridge writer separately checks image layout, destination identity and
readback. Release verification does not install images; follow the
[USB-writing workflow](workstation.md#write-a-cartridge-to-usb) or
[internal installation](internal-storage.md).
The JSON document has exactly these fields:
```json
{
"format": 1,
"version": "0.1.0",
"source_epoch": 1789909701,
"source_sha256": "<64 lowercase hexadecimal characters>",
"void_commit": "02a3cbc132c3c4a3a9d59e9b98f517af5dd11cd1",
"hardware_validation": "deferred",
"files": [
{
"name": "fds-system-cli-0.1.0.img",
"bytes": 123456,
"sha256": "<64 lowercase hexadecimal characters>"
}
]
}
```
This is a schema illustration, not a usable manifest: the size and hash
placeholders must be replaced with values from the actual artifact. Format 1
accepts the current tool version, a positive source epoch, a 40-character Void
commit, and 1–64 nonempty artifacts. Filenames must be flat safe ASCII names;
paths, duplicates, hidden names, reserved manifest filenames and unknown fields
are rejected. The manifest is bounded to 1 MiB. Files must be regular files,
opened without following symlinks; hashing streams their bytes and checks for
changes during the read.
The signature is Ed25519 over the exact bytes
`FDS/OS release manifest v1` followed by a NUL byte and the raw `manifest.json`
bytes. JSON whitespace is therefore authenticated too. `manifest.sig` contains
128 lowercase hexadecimal characters and a newline. The public-key file contains
64 lowercase hexadecimal characters and a newline. Verification rejects weak
public keys and uses strict signature verification.
The implementation uses locked `ed25519-dalek` 3.0.0 for Ed25519,
`zeroize` 1.9.0 for secret buffers, and the existing SHA-256/JSON crates.
Its locked transitive dependencies provide curve arithmetic, digest and
signature types. Default dalek features are disabled; no asynchronous runtime,
OpenSSL binding or shared target library is added. The dependencies and their
license metadata are recorded in `Cargo.lock` and the cached crate sources.
The build collects their license texts, including build dependencies and
vendored libusb notices, into `/usr/share/licenses/fds-cli/RUST-NOTICES.txt`.
The usage guide is installed at `/usr/share/doc/fds/releases.md` in every profile.
Algorithm interoperability is checked against the
[RFC 8032 test vector](https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1)
and OpenSSL; strict verification follows the
[dalek API](https://docs.rs/ed25519-dalek/3.0.0/ed25519_dalek/struct.VerifyingKey.html).
Release maintainers can find key creation, signing and format details in the
[developer reference](developer/releases.md).
+88 -98
View File
@@ -1,117 +1,107 @@
# Software cartridge format 1
# Software cartridge format
[Build and use a cartridge](workstation.md) · [Original cartridge classes](cartridges.md)
A PROGRAM cartridge is a complete GPT disk image with one EROFS metadata partition
and one or more EROFS payload partitions. New media uses catalogue format 2 and
stores installed Void package trees. Source recipes and build tools remain on
the workstation; FDS executes the installed files directly.
A software cartridge is a complete GPT disk image with **1 + m** partitions,
where `m` is 1–32. Partition 1 describes every software package in all payload
partitions. All partitions use read-only EROFS. This format is the current
PROGRAM creation format; existing single-partition PROGRAM media remain readable.
SYSTEM, DATA and ENVIRONMENT layouts retain their existing meanings.
## Disk and filesystem layout
```text
GPT protective MBR + primary GPT
1 FDS_METADATA
FDS/CARTRIDGE.TOML
FDS/SOFTWARE.TOML
2 FDS_PAYLOAD02
bundles/demo.hello.tar.xz
bundles/demo.editor.tar.xz
3 FDS_PAYLOAD03
bundles/demo.report.tar.xz
...
backup GPT
```
Partition entries are consecutive, use Linux filesystem type GUIDs, unique
nonzero partition UUIDs, no GPT attributes, and nonoverlapping MiB-aligned
extents. Partition 1 starts at LBA 2048. Both GPT copies, their headers and CRCs
must agree. A full-drive write to a larger USB device relocates the backup table
and header. The existing on-target image parser and the workstation writer
share these checks.
`FDS/CARTRIDGE.TOML` uses the existing format-1 identity with `class = "program"`
and `media.writable = false`. It contains no startup command. A sample catalogue:
## Software source recipe
```toml
format = 1
[[software]]
id = "demo.hello"
name = "AArch64 hello"
format = 2
id = "example.hello"
name = "Hello"
version = "1.0"
architecture = "aarch64"
partition = 2
archive_bytes = 2308
unpacked_bytes = 70504
entries = 2
sha256 = "REPLACE_WITH_THE_64_CHARACTER_LOWERCASE_SHA256"
[software.commands]
hello = "bin/hello"
[commands]
hello = "usr/bin/hello"
[source]
package = "hello"
```
Those lengths and the digest are illustrative. The builder generates actual
values. IDs and command names use the existing restricted FDS identifier syntax.
IDs are unique per cartridge. Every declared payload partition must contain
exactly the catalogue's `bundles/<id>.tar.xz` archive names, and every payload
partition must be represented. Multiple software entries may share a partition.
The catalogue contains 1–128 software entries and at most 64 KiB of TOML.
`package` names a source package in the selected `void-packages` checkout. For a
custom package, also set `template = "void"`: that directory must contain a
normal Void `template` and any `files/` or `patches/` directories it uses. Paths
are relative to this recipe. The [included hello package](../examples/software/hello/void/template)
is a complete example.
## Archive contract
Custom sources are copied into an untracked `srcpkgs/PACKAGE` directory. Existing
tracked Void sources are never overwritten. If a generated source copy differs,
inspect it, preserve any independent edits, then remove that generated copy and
retry. An unchanged copy can be reused.
Bundles are deterministic USTAR archives compressed with single-threaded xz.
Entries are sorted, timestamps/UID/GID are zero, and permissions normalize to
0755 for directories/executables or 0644 for ordinary data. Host source symlinks
to regular files inside the root are flattened into regular files. Directory or
escaping symlinks are rejected; the archive itself never contains links.
`fds-cartridge` builds with `xbps-src -a aarch64`, installs the resulting package
and runtime dependencies into a fresh tree, and records installed XBPS versions.
Use package dependency declarations to request runtime libraries and utilities.
Each software tree is self-contained with respect to its installed dependencies;
there is no cross-cartridge package resolver.
The reader rejects:
Software IDs and public command names use lowercase ASCII letters, digits,
periods, hyphens and underscores, with a maximum length of 64. Command paths are
relative to the installed root and must resolve to executable files within it.
- Absolute, parent-traversing, repeated-separator, non-UTF-8 or control-character paths.
- Duplicate entries, file-as-parent conflicts, links, devices, sockets, FIFOs,
privileged permissions, and GNU/PAX extension entries.
- Incorrect SHA-256, compressed length, unpacked length, entry count, or command
paths that do not name executable regular files.
- ELF files that are not little-endian 64-bit AArch64, or any ELF in a bundle
declared `architecture = "any"`.
- Invalid xz data, nonzero material after the tar end marker, and resource-limit
violations. Each archive is at most 512 MiB compressed, 1 GiB unpacked and
65,536 entries. Xz decompression has a 256 MiB memory limit.
## Cartridge recipe
A source path must fit USTAR's path fields. Packaging reports paths that cannot
be represented rather than emitting an unsupported extension header.
```toml
format = 2
id = "example.tools"
name = "Example tools"
version = "1.0"
## Guest lifecycle
[[payload]]
sources = ["hello/software.toml", "report/software.toml"]
```
On insertion the daemon validates the complete GPT against kernel partition
geometry and the current USB disk identity. It reads metadata without following
symlinks, mounts every payload read-only/noexec/nosuid/nodev, and checks the
archive inventory and lengths. `fds bay BAY` reports the full catalogue;
`fds bays` stays compact. No archive command runs automatically on insertion.
Each payload section creates one partition. Sources may be recipe paths or
previously built directories containing `software.toml` and `root/`. The metadata
partition records which software belongs to which partition. Software IDs and
cartridge identity must satisfy the metadata validation rules.
`fds run BAY -- SOFTWARE-ID:COMMAND [ARGUMENT...]` verifies and extracts that
package on first use. It creates a private temporary filesystem, applies the
archive contract, then makes the finished tree read-only and accessible for
execution. The runtime cache has a stricter **256 MiB per software tree** limit,
including reserved inode overhead; a host-valid larger bundle may therefore
need splitting before running. Caches live under `/run/fds/software/<bay>/` and
vanish after eject, unplug, restart cleanup, or shutdown.
## On-disk contents
Managed consumers run as UID/GID 1000 with no new privileges in the bay's cgroup.
`FDS_APP`, `PATH`, `LD_LIBRARY_PATH` and `XDG_DATA_DIRS` point to that software
root. DISPLAY/XAUTHORITY retain the established optional desktop integration.
The xz utility is already an explicit dependency of the mandatory `fds-base`
package; runtime extraction adds no target package.
```text
Partition 1: FDS_METADATA
FDS/CARTRIDGE.TOML
FDS/SOFTWARE.TOML
Partition 2: FDS_PAYLOAD02
programs/example.hello/usr/bin/hello
programs/example.hello/usr/lib/...
programs/example.hello/var/db/xbps/...
programs/example.report/...
```
Build recipes, compilers, installation hooks and privileged archive scripts are
never part of this runtime path.
The software catalogue records identity, architecture, commands, partition,
installed package versions, entry count, byte count and SHA-256. `installed=true`
selects the direct-tree contract. The retained `unpacked_bytes` field records
installed file bytes; it does not imply an archive or guest extraction.
Safe eject stops consumers, rejects unexpected mount aliases, unmounts caches,
payloads and metadata, then reports SAFE. Confirmed surprise removal permits
lazy detachment after stopping consumers. Service restart removes stale mounts
before rescanning. SYSTEM stays immutable throughout.
The tree digest covers sorted paths, entry types, modes, contents and symlink
targets. Root-relative package symlinks are relocated to relative in-tree links
when building. Escaping links, special files, privileged/writeable executable
trees, wrong-architecture ELF files and inconsistent metadata are rejected.
Current per-tree bounds are 1 GiB of file data and 65,536 entries.
SHA-256 detects corruption and binds an archive to its catalogue; it does not
establish a publisher's identity. Only run software you trust. Release signature
verification remains the separate [release workflow](releases.md).
## Runtime behavior
FDS verifies the cartridge's geometry, identity, inventory and tree integrity
on insertion. Payloads are mounted read-only with `nosuid,nodev`. No software
command runs automatically. Published command aliases appear in `/run/fds/bin`.
Programs run as UID/GID 1000 in the bay's managed process group. `FDS_APP` names
the installed tree; PATH, library paths and data search paths include its
`usr/bin`, `usr/lib` and `usr/share`. Dynamically linked ELF entry points use the
package tree's own loader. Programs that hard-code unrelated system paths still
need to be compatible with FDS; the tree is not a container or a writable root.
Safe eject stops consumers and releases every payload before reporting SAFE.
A digest detects corruption, not publisher identity: choose software sources
you trust. Distribution signatures are covered by [release verification](releases.md).
## Older media
The reader retains support for catalogue format 1 with xz payloads and legacy
single-partition PROGRAM cartridges containing `app/`. Old xz payloads retain
their temporary extraction behavior and limits. Creation commands produce only
the installed-tree format. Rebuild old software from its Void source recipe to
use direct execution.
+29 -235
View File
@@ -1,243 +1,37 @@
# Troubleshooting the build
# Troubleshooting
[Documentation index](README.md) · [First build](getting-started.md) · [Build reference](build-host.md)
Read the complete command error and its log before retrying. Build logs are in
`out/logs/`; emulator `console.log` and `qemu.log` are in the selected session.
On FDS, `fds bay N`, `fds profiles` and `fds power status` report the relevant
operation state. Append `--help` to a command for accepted options.
Find the first failing command or `ERROR:` in the log, fix that problem, then
repeat the same step. Do not continue past a failed bootstrap and interpret later
missing files as independent problems. Run all commands below from the repository
root, as your regular user, unless explicitly shown otherwise.
## Find the right log
| Failing action | Where to look |
| Symptom | Resolution |
| --- | --- |
| `make bootstrap` | `out/logs/bootstrap.log` |
| Void build-container creation | `out/logs/xbps-bootstrap.log` |
| `make smoke-test` | `out/logs/smoke-test.log` |
| GNU hello build | `out/logs/xbps-hello.log` |
| `make check` | Terminal output; this target does not save a log automatically |
| Build disk is full | Stop builds and VMs; run `make clean-preview`, then `make clean`. See [retention rules](cleanup.md). |
| Missing host utility | Install the named utility or select the documented image/XBPS/QEMU runner. `doctor` checks image and emulator prerequisites. |
| Bubblewrap/user namespace failure | Check that the host permits unprivileged namespaces. Run the check in [setup](getting-started.md); do not switch software builds to root. |
| Void checkout is unprepared | Bootstrap the checkout using upstream xbps-src instructions, then select it with `--void-packages`. |
| Stale generated overlay/source package | Compare the source and generated copy. Preserve independent edits, remove only that generated copy, and retry. Never overwrite tracked upstream templates. |
| Output already exists | Use a new output directory, cartridge image, preview or emulator session. Creation does not overwrite existing outputs. |
| Executable architecture or tree integrity error | Rebuild the source package and cartridge. FDS cannot run an invalid payload. |
| A command is unavailable | Wait for `MOUNTED READ ONLY`, inspect `fds --json bay N`, and use its qualified command alias. Run `hash -r` after removal if Bash retained an old path. |
| Empty or unconfigured physical bays | Run `fds topology` and calibrate the machine's bay map. Do not infer slots from disk names. |
| Duplicate cartridge ID | Give the second cartridge a distinct identity and rebuild it. |
| Console already in use | Detach the emulator console with Ctrl-] before `guest`, safe eject or shutdown. |
| Guest command timed out | Inspect the console log. It may still be running; do not repeat a write until its state is known. |
| Desktop activation fails | Inspect `fds profiles` and `/run/log/xserver/current`, `/run/log/desktop/current`. Check the display configuration. |
| Control panel cannot connect | Check `/run/log/cartridged/current` and that the cartridge service is running. The panel reconnects automatically. |
| Eject is blocked | Close programs, shells and extra mounts using the cartridge, then retry. Wait for SAFE before removal. |
| Shutdown is blocked | Read `fds power status`; resolve the reported storage problem and retry poweroff. `fds power resume` cancels a recoverable pending shutdown. |
| DATA needs checking | Use the [recovery workflow](recovery.md); preserve a backup before repair where possible. |
For example, after a failed smoke-test:
To inspect cartridge service output on FDS:
```sh
tail -n 80 out/logs/smoke-test.log
rg -n 'ERROR:|FAILED|error:|No space left|Permission denied' out/logs/
df -h .
tail -80 /run/log/cartridged/current
fds --json bay 01
```
A search with no matches exits 1. Read the surrounding log too: upstream tools
may report failures using other wording. Save logs before re-running if you need
the previous output; see [Development](development.md#preserve-logs-and-identify-inputs).
## Unsupported host, root execution, or missing commands
- `M0 requires an x86_64 Linux build host` or `supports Arch Linux`: use the
supported host described in [Your first build](getting-started.md). Changing
the check alone does not port the build system to another platform.
- `Run as a normal user, not root`: leave the root shell and run the build from
your normal account. Only host package installation uses sudo.
- `Missing host command`: install the prerequisites from the first-build guide,
then repeat bootstrap. `readelf` comes from binutils, `bwrap` from bubblewrap,
and `flock` from util-linux.
- `checkout path without whitespace`: move or obtain the checkout at a path
with no spaces, tabs, or other whitespace, then use that directory consistently.
If a previous root-run left files that your account cannot write, inspect their
ownership before correcting it. Do not respond by running all later builds as root.
## Bubblewrap or user namespace failure
Reproduce the same capability check bootstrap uses:
```sh
bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true
```
If it reports `Operation not permitted` or a namespace error, the host kernel,
container policy, or security configuration prevents the unprivileged container.
Use an Arch host that permits this operation, or have the host administrator
resolve the restriction. Repeated downloads, sudo builds, and a different Pi
will not resolve this preflight failure. Success is a zero exit status with no
output; run `make bootstrap` again once that works.
## Void submodule missing, wrong, or modified
Inspect before changing anything:
```sh
cat VOID_PACKAGES_COMMIT
git submodule status vendor/void-packages
git -C vendor/void-packages status --short
git -C vendor/void-packages diff HEAD --
```
If the submodule is missing, `make bootstrap` initializes it. The directory must
be part of a Git checkout; an unpacked source archive is insufficient. If a clone
was interrupted by a network error, restore connectivity and repeat bootstrap.
`Void checkout mismatch` means the checked-out commit differs from the explicit
pin. A `+` in `git submodule status` means it differs from the parent repository's
gitlink. These are two separate checks. Do not update the pin merely to silence
the error; first determine why the commits differ.
`Tracked Void files were changed` means upstream source has local edits. Preserve
and review the diff. Move intentional FDS changes into the overlay design or a
separate patch for review; restore upstream only after saving work you need.
Generated ignored files such as `etc/conf`, `hostdir`, and the masterdir are
normal. Never use a broad reset or clean command without checking what it removes.
## Configuration or overlay conflict
For `etc/conf differs from config/xbps-src.conf`, compare the two:
```sh
diff -u vendor/void-packages/etc/conf config/xbps-src.conf
```
Put the desired settings in `config/xbps-src.conf`, then follow
[the explicit synchronization procedure](development.md#change-build-configuration-deliberately).
A `diff` exit status of 1 means differences were found, not that the comparison failed.
For `Stale overlay`, compare the named `vendor/void-packages/srcpkgs/fds-*`
directory with its source under `packages/`. Preserve any unique edits in the
source first. Move that specific generated copy into a backup directory under
`out/`, outside `srcpkgs/`, then run `./tools/prepare-void` again. Do not move or
remove unrelated upstream packages. The Dasung package is now an active overlay. After changing it, reconcile that
generated copy by this same procedure before rebuilding.
`Overlay would replace an upstream package` is an intentional protection. Choose
an FDS-owned package name and design an overlay; do not disable the check or edit
the tracked upstream template in place.
## Download failure, checksum mismatch, or apparent stall
Bootstrap and package builds need network access. The first run can spend much
of its time downloading compiler packages. Read the latest log and check disk
space before assuming that a quiet download is a deadlock:
```sh
tail -n 40 out/logs/bootstrap.log
tail -n 40 out/logs/xbps-hello.log
du -sh out/downloads vendor/void-packages/hostdir
df -h .
```
A log or directory may not exist if its step has not started. Bootstrap's static
XBPS download has retries and a timeout; an incomplete transfer uses a `.part`
filename. A retry re-downloads that partial file. Completed cached archives are
checksum-checked on every bootstrap.
For a checksum failure, retain the error and compare the named file against
`config/host-tools.conf`. If the cached completed archive is corrupt, move that
specific archive to a backup location and repeat bootstrap so it downloads again.
Do not change the expected hash to match an unexplained download. If a clean
re-download still disagrees, stop and investigate the source.
A Void mirror timeout is separate from the source pin: the templates are pinned,
but bootstrap and missing build dependencies still use rolling binary repositories.
An unavailable historical dependency may need a reviewed input/mirror solution;
blindly changing the source commit does not establish an equivalent build.
## XBPS command not found or package index seems empty
The XBPS tools are intentionally not installed in the host's `/usr/bin`.
`make bootstrap` creates `.host/xbps/usr/bin/`. FDS build helpers set PATH for you.
For manual queries, use the explicit paths in the [package guide](packages.md).
When querying or indexing ARM output, set `XBPS_ARCH=aarch64`. The host's static
musl executable can otherwise default to its own architecture and ignore the ARM
package. `make smoke-test` uses the correct override. `tools/build-package` exports
packages but does not update `out/packages/aarch64-repodata`; use the indexing
command in the package guide after a standalone package build.
## Rust target, linker, or architecture error
Check the selected toolchain and local configuration:
```sh
rustup show active-toolchain
rustup target list --installed
cat rust-toolchain.toml
cat .cargo/config.toml
printenv RUSTFLAGS CARGO_BUILD_TARGET CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER
```
`printenv` exits nonzero when a listed variable is unset; that is normal here.
Bootstrap installs the required target and rustfmt for the pinned toolchain.
Repeat it if those components are missing.
Build from the repository root with the exact Cargo command in
[Development](development.md#edit-and-rebuild-the-rust-program). Environment
variables, command-line `+toolchain` overrides, or outside Cargo configuration
can change the target, flags, or linker. Reconcile such overrides with the
checked-in `rust-lld` and static CRT configuration. Adding an arbitrary system
linker or glibc library is not a repair for a static-musl build.
## Exec format error, ldd output, and QEMU skips
`./out/fds-smoketest` normally gives `Exec format error` on x86_64: it is an ARM
program. Run it explicitly with optional `qemu-aarch64`, as described in
[the first-build guide](getting-started.md#7-optionally-execute-the-arm-rust-program).
Do not change the target to x86_64 merely to make direct execution work.
`ldd: not a dynamic executable` with exit 1 is expected for the static program.
Use `./tools/verify-elf out/fds-smoketest aarch64 static` for the actual check.
Host `ldd` cannot establish that a foreign ARM program will run.
`SKIP: ARM execution` means QEMU is absent. Compilation and ELF checks still run.
If QEMU is present but fails, the smoke-test must fail; inspect its diagnostic.
The dynamic GNU hello program also requires an ARM glibc loader and libraries,
so the simple static-program QEMU command is not a complete hello runtime setup.
## Missing artifacts or check fails before any build
`make check` expects the Cargo release artifact in `target/`. Run the Rust build
or `make smoke-test` first. A bare Cargo build updates `target/`, while
`make smoke-test` also refreshes `out/fds-smoketest` and the output manifests.
If checksums fail after editing or replacing outputs, inspect what changed and
rebuild through smoke-test; do not edit the manifest to conceal a mismatch.
For a Rust formatting failure, run `cargo fmt --all`, inspect the source diff,
then repeat `make check`.
## Busy /tmp message during Void bootstrap
The initial verified bootstrap emitted
`rm: cannot remove '//tmp': Device or resource busy` during upstream cleanup.
The bubblewrap bind mount explained that instance; upstream exited successfully,
and container GCC, the cross build, and the public M0 commands all passed.
Do not treat every later `/tmp` error as harmless. Confirm a zero command exit
status and the final bootstrap PASS, then run smoke-test. If the command exits
nonzero, report it as a failure with the surrounding log.
## Rootfs, images, and runtime commands
M1 implements `make rootfs PROFILE=cli` and `make rootfs-test`. If the target is
missing, check that you are in the current checkout. Read [M1 rootfs](rootfs.md)
for prerequisites, shell access, logs, and retained failed-build directories.
Python 3.14+ is required for XBPS archive inspection. Supported profiles are
`cli`, `development`, and `recovery`; unknown names fail explicitly. Package
configuration must pass before an archive is published.
`tools/in-rootfs` supports ARM child execution through a private user-namespace
binary-format handler. It requires Linux 6.7+ and namespaced `binfmt_misc` support.
The workstation's global registry is unchanged. For a single ELF executable,
`--direct` invokes QEMU without installing even a private child-execution handler.
For example: `./tools/in-rootfs out/rootfs-aarch64 --direct /usr/bin/xbps-pkgdb -a`.
The tar is configured userspace, not a flashable image. M2 adds `make init-test`
for a full ARM boot and `make vm` for a temporary development shell. Follow
[Native init and ARM VM](init.md) for its dependencies, expected output, and
failure logs. For the full kernel/stage0/SYSTEM path, use `make all`,
`make boot-test`, then `make console-vm`. Run `fds bays` and `fds poweroff`
inside the booted FDS console, not on the Arch host. An exported ARM binary will
not execute directly on x86_64. Physical Pi acceptance remains deferred.
## What to include in a bug report
Include the exact failing command, its exit status if available, the relevant log
excerpt, host distribution and architecture, current Void pin, and whether this
is the first build or a previously working checkout. Mention local configuration
changes and toolchain overrides. Preserve the initial error; a later missing-file
message often only describes its consequence.
To inspect a stopped emulator, read its saved logs directly on the workstation.
Rebooting uses a new session directory; existing DATA overlays remain in the old
session and can be [exported](workstation.md#save-emulator-data).
-60
View File
@@ -1,60 +0,0 @@
# Workstation emulator and software cartridges
Software implementation and acceptance are complete; see the
[acceptance record](workstation-validation.md) and [user guide](workstation.md).
This follow-up extends the accepted 0.1.0 checkpoint. That signed release remains
unchanged and does not contain the new format or emulator interface. Physical
hardware acceptance remains deferred.
## Required result
- A Linux workstation CLI boots the real FDS kernel, initramfs and SYSTEM image
with QEMU, exposes its console, and controls twelve virtual USB cartridge bays.
- Cartridge image insertion and removal use QMP USB hotplug. Safe eject asks the
guest to release the cartridge; forced removal explicitly simulates pulling it.
- Software cartridges use GPT with exactly `1 + m` partitions. Partition 1 is
`FDS_METADATA`; partitions 2 through `m + 1` are `FDS_PAYLOAD02`, and so on.
All are read-only EROFS containers. This permits multiple software archives
in one partition without inventing a raw-partition archive container format.
- Metadata includes the cartridge identity and a catalogue locating every
software bundle, its version, architecture, commands, lengths and SHA-256.
Each software payload is an xz-compressed tarball, stored as
`bundles/<software-id>.tar.xz` in its declared payload partition.
- Workstation tools build software from explicit trusted recipes, package the
resulting trees, construct and verify a complete disk image, then separately
preview/confirm a whole-USB write and verify its readback. Building must not
depend on running on the Pi or on an Arch-specific container.
- The guest recognizes this layout, reports its software catalogue, and runs a
selected software command as the ordinary user. Archives never run build hooks
on the guest. Runtime extraction is bounded, verified and temporary; eject
and surprise removal stop consumers and release every associated resource.
- SYSTEM and writable DATA retain their boot/data layouts. Legacy PROGRAM
reading may remain for existing images, but newly built software cartridges
use the metadata-first multi-partition format.
## Acceptance gates
1. Typed Clap host CLIs, actionable help and Linux prerequisite diagnostics.
2. Real workstation builds of at least two software programs, including an
AArch64 executable; archive inspection and independent xz/tar checks.
3. `1 + m` images with multiple software packages sharing a partition as well
as packages in separate partitions; independent GPT/filesystem inspection.
4. Negative checks for overlapping/corrupt GPT, wrong catalogue mappings,
archive corruption, unsupported architecture, unsafe paths/types and size
limits. Existing image/write rejection tests remain passing.
5. Complete-image write/readback using disposable regular files, including a
larger target and confirmation/source/target-change rejection. Actual USB
writes are deferred until the user selects physical hardware.
6. Real FDS boot under the public emulator, insert, catalogue, run, safe eject,
reinsert, forced removal during execution, and cleanup/restart behavior.
Every software partition must be exercised, not just the first payload.
7. Required rootfs/native-init regression and relevant DATA, PROGRAM, media and
shutdown checks; English installation, usage, troubleshooting and format docs.
The QMP implementation follows the [QEMU reference](https://www.qemu.org/docs/master/interop/qemu-qmp-ref.html),
including waiting for completed device deletion before releasing block nodes.
The Rust `tar` library supplies archive entry decoding; FDS imposes stricter
path, entry-type and size rules instead of trusting an archive's ownership or
permissions. The existing `xz` utility supplies streaming compression and
decompression with a memory limit. QEMU is host-only; no emulator or software
build daemon is added to the Pi boot graph.
+146 -255
View File
@@ -1,166 +1,133 @@
# Build software cartridges and run FDS in QEMU
# Build software cartridges and use the emulator
This guide uses a **Linux workstation**, not the Raspberry Pi. It takes you from
source software to a cartridge disk image, runs that image in FDS, and explains
how to write the same complete image to USB. All commands run from the checkout
root unless marked as guest commands.
This walkthrough runs on a Linux workstation. It builds applications from Void
source packages, creates a complete cartridge image, runs it in FDS, and explains
how to write it to USB. Commands run from the FDS checkout root unless marked
as guest commands.
The two native host tools are `fds-cartridge` and `fds-emulator`. They use typed
Clap interfaces; append `--help` to any command or subcommand. The target's `fds`
command remains a static AArch64 executable. The new host tools are never part
of the Pi base image. Dasung support remains in the base boot bundle.
## Prepare the tools
The accepted local `out/fds-os-0.1.0/` release predates this extension. Leave that
release intact and build current images for the emulator. See
[workstation acceptance](workstation-validation.md) for current evidence.
## Install and build the workstation tools
Use a normal Linux account with Rust/Cargo, a C linker, Bash and Make. The
repository's `rust-toolchain.toml` selects the Rust version. Runtime prerequisites
are:
| Tool | Purpose |
| --- | --- |
| `qemu-system-aarch64` | Runs the FDS ARM kernel and userspace using portable TCG |
| `qemu-img` | Creates a separate writable overlay for DATA; source images stay unchanged |
| `mkfs.erofs`, `fsck.erofs` from erofs-utils | Build and verify cartridge partitions; tested with 1.9.4 |
| `xz` | Compress/decompress software tarballs with a decompression memory limit |
| `bwrap` from bubblewrap | Restricts filesystem inspection to a private writable staging directory |
| A software-specific cross compiler | Builds AArch64 applications; unnecessary for portable scripts |
| `sfdisk`, `tar`, `mke2fs`, Python 3 | Independent integration tests; not needed to boot an existing image |
Install these with your distribution's package manager. Unprivileged user
namespaces must work for bubblewrap. Building **these host tools** does not need
Void, XBPS, an Arch host, a Pi, root, or a running FDS guest:
The native tools are `fds-cartridge` and `fds-emulator`. Build them with Rust,
a C linker and Make; the repository selects its pinned Rust toolchain:
```sh
make workstation
mkdir -p out
export PATH="$PWD/out/workstation:$PATH"
fds-cartridge doctor
fds-emulator doctor
```
Current acceptance ran on Arch Linux x86_64. The tools use native Linux
interfaces and distribution-provided utilities; other distributions and ARM
workstations have not yet been exercised by this acceptance run.
| Prerequisite | Used for |
| --- | --- |
| Prepared Void `void-packages` checkout and native XBPS utilities | Build source packages and install their AArch64 runtime dependencies |
| `mkfs.erofs`, `fsck.erofs` | Create and inspect read-only payload filesystems |
| `bwrap` | Inspect filesystems and install package trees in a user namespace |
| `xz` | Read older software cartridges and package/source archives |
| `qemu-system-aarch64`, `qemu-img` | Run the emulator and create writable DATA overlays |
The outputs are native binaries in `out/workstation/`. The build explicitly
selects the workstation architecture instead of the workspace's default ARM
target. Rust dependencies are pinned in `Cargo.lock`. `tar` 0.4.46 and its
`filetime` dependency provide archive decoding/creation; FDS additionally checks
entry types, paths, counts, sizes, executable architecture and hashes.
Install tools through your Linux distribution's package manager. Enable
unprivileged user namespaces for bubblewrap. Software builds run as an ordinary
user and execute trusted Void build templates on the workstation. Run source builds sequentially when they
share the same Void checkout.
For this repository's **existing Arch/Void setup**, reuse its local tools:
Prepare a Void source checkout using its [upstream instructions](https://github.com/void-linux/void-packages/blob/02a3cbc132c3c4a3a9d59e9b98f517af5dd11cd1/README.md).
Complete `xbps-src binary-bootstrap` for the workstation architecture before
using it. The workstation itself need not run Void. Select that checkout with
`--void-packages /path/to/void-packages`; native `xbps-*` tools must be in PATH,
or selected with `--xbps-bin /path/to/bin`. Builds target AArch64/glibc.
For the complete FDS build workstation, [bootstrap](getting-started.md) already
prepares `vendor/void-packages` and `.host/xbps/usr/bin`. Reuse its local tools:
```sh
make workstation
./tools/prepare-image-tools
./tools/in-void xbps-install -y qemu-img
out/workstation/fds-cartridge --image-tool-runner tools/in-image-tools doctor
out/workstation/fds-emulator doctor --qemu-runner tools/in-void
fds-cartridge --image-tool-runner tools/in-image-tools doctor
fds-emulator doctor --qemu-runner tools/in-void
```
The install command above changes only the project-local build container. The
optional runners accept a program name followed by its arguments; omitting them
uses programs in the workstation's `PATH`. Set the image runner on every
cartridge create, inspect or preview invocation that needs it. A QEMU runner is
saved in the session and reused for later DATA insertions.
An optional runner receives a utility name followed by its arguments. Image
runners select EROFS utilities; XBPS runners provide the package-installation
namespace. Without runners, the tools use native utilities and bubblewrap.
## Build two software bundles
## Build the example cartridge
Each software recipe declares an id, display name, version, target architecture,
root directory, and named executable commands. A trusted `[build]` section may
run a workstation compiler. It is executed only by `software build`, never by
`software pack`, inspection, or a cartridge insertion.
The repository includes hello and system-report Void source packages. Each
software recipe names a package, its source-template directory and its public
commands. A cartridge recipe groups those software recipes into payloads.
The [hello recipe](../examples/software/hello/software.toml) builds an AArch64
C executable. Install an AArch64 glibc cross compiler as `aarch64-linux-gnu-gcc`,
then run:
With native tools and a prepared checkout:
```sh
fds-cartridge software build examples/software/hello/software.toml out/demo-hello
fds-cartridge software pack examples/software/report/software.toml out/demo-report
fds-cartridge software inspect out/demo-hello
```
If your cross compiler has a different name, edit the recipe's `build.command`.
With this checkout's existing Void cross toolchain, the equivalent is:
```sh
mkdir -p examples/software/hello/root/bin
./tools/in-void aarch64-linux-gnu-gcc -O2 examples/software/hello/hello.c \
-o examples/software/hello/root/bin/hello
out/workstation/fds-cartridge software pack \
examples/software/hello/software.toml out/demo-hello
out/workstation/fds-cartridge software pack \
examples/software/report/software.toml out/demo-report
```
Use **one** of those hello workflows. Outputs must not already exist: choose
new output names when rebuilding, then update your cartridge recipe. The example
bundles each contain `software.toml` and `<id>.tar.xz`. The archive contains the
contents of the software root, such as `bin/hello`; it has no leading `root/`.
For your own application, copy a recipe and populate its root with `bin/`,
`lib/` and `share/` as needed. C/C++ glibc builds target AArch64; static Rust
programs should use `aarch64-unknown-linux-musl`. Set `architecture = "any"` only
for scripts/data without ELF files. An x86_64 executable is rejected. Shared
libraries must be compatible with the target SYSTEM's glibc, or bundled as
appropriate. There is no cross-cartridge dependency resolver.
## Assemble the cartridge image
The example cartridge recipe groups the two bundles into two payload partitions:
```sh
fds-cartridge create examples/software/cartridge.toml out/demo-tools.img
fds-cartridge --void-packages /path/to/void-packages \
create examples/software/cartridge.toml out/demo-tools.img
fds-cartridge inspect out/demo-tools.img
```
For the project-local EROFS tools, use:
With the project-local Void and image tools, use this equivalent command:
```sh
out/workstation/fds-cartridge --image-tool-runner tools/in-image-tools \
fds-cartridge --image-tool-runner tools/in-image-tools \
--xbps-tool-runner tools/in-void --xbps-bin "$PWD/.host/xbps/usr/bin" \
create examples/software/cartridge.toml out/demo-tools.img
fds-cartridge --image-tool-runner tools/in-image-tools inspect out/demo-tools.img
```
The resulting complete disk image has **three GPT partitions**:
Choose one route. Output paths must be new. The command builds each package
with `xbps-src`, installs the package and runtime dependencies into private
staging trees, verifies their architecture and integrity, and writes the complete
image. Source build logs go to standard error; the final result is JSON.
| Partition | GPT name | Contents |
| --- | --- | --- |
| 1 | `FDS_METADATA` | Cartridge identity and the software catalogue |
| 2 | `FDS_PAYLOAD02` | `bundles/demo.hello.tar.xz` |
| 3 | `FDS_PAYLOAD03` | `bundles/demo.report.tar.xz` |
The example image contains:
To put both programs in one payload partition, use one `[[payload]]` entry with
`bundles = ["../../out/demo-hello", "../../out/demo-report"]`. This makes a
two-partition image. Each payload group creates exactly one partition. The first
partition identifies every software bundle and its payload partition.
| Partition | Contents |
| --- | --- |
| 1: `FDS_METADATA` | Cartridge identity and software catalogue |
| 2: `FDS_PAYLOAD02` | `programs/demo.hello/`, including `usr/bin/hello` and libraries |
| 3: `FDS_PAYLOAD03` | `programs/demo.report/`, including `usr/bin/report` and dependencies |
Creation finishes only after verifying both GPT tables, all EROFS partitions,
every archive and the catalogue. Inspection prints JSON, including offsets,
lengths, SHA-256 and available commands. Repeating an unchanged recipe produces
the same image bytes. See [the format](software-format.md) for limits and rules.
There are no new xz software bundles. Programs are ready to run directly from
EROFS. Inspection validates both GPT tables, filesystem contents and software
hashes, then prints partition and command details as JSON.
## Boot current FDS in the emulator
## Build a reusable installed-software directory
You need three matching current artifacts: the FDS Pi kernel, its initramfs, and
a SYSTEM image containing the guest software runtime. Build these on the OS
build workstation using [the build guide](getting-started.md). For an already
bootstrapped checkout:
To separate package compilation from cartridge assembly:
```sh
make rootfs PROFILE=cli
make system-card PROFILE=cli
make initramfs
make workstation
out/workstation/fds-emulator --session out/my-emulator start \
--qemu-runner tools/in-void
fds-cartridge --xbps-tool-runner tools/in-void \
--xbps-bin "$PWD/.host/xbps/usr/bin" \
software build examples/software/hello/software.toml out/demo-hello
fds-cartridge software inspect out/demo-hello
```
If QEMU is installed directly on your Linux workstation, omit `--qemu-runner`.
To use images copied from another build machine, supply their paths:
The output contains `software.toml` and `root/`. A cartridge's `sources` list can
name this directory instead of a source recipe. For example:
```toml
format = 2
id = "my.tools"
name = "My tools"
version = "1.0"
[[payload]]
sources = ["../out/demo-hello"]
```
Paths are relative to the cartridge recipe. Put several sources in one list to
share a partition, or add `[[payload]]` sections for separate partitions. See
[the format reference](software-format.md) for complete source metadata and limits.
## Start FDS
Build matching boot inputs using [Build and start FDS](getting-started.md), or
copy a kernel, initramfs and SYSTEM image from an FDS build machine.
```sh
fds-emulator --session out/my-emulator start --qemu-runner tools/in-void
```
Omit `--qemu-runner` when QEMU is installed directly on the workstation. To use
copied boot images:
```sh
fds-emulator --session out/my-emulator start \
@@ -169,173 +136,97 @@ fds-emulator --session out/my-emulator start \
--system /path/to/fds-system-cli.img
```
Create `out/` first if using a fresh directory. The session directory must be
**new**, private, and short enough for Unix sockets (under 90 bytes including its
absolute parent path). Start waits for the FDS prompt and verifies the emulator
bay configuration. It uses two emulated CPUs and 1024 MiB RAM by default;
`--memory-mib 2048` increases RAM. `--timeout` adjusts the boot deadline.
The session directory must be new and private. Keep its absolute path short
(under 90 bytes) for Unix sockets. Start waits for the FDS prompt and uses two
emulated CPUs and 1024 MiB RAM; `--memory-mib 2048` increases memory. SYSTEM and
PROGRAM images are read-only. The session holds logs and any DATA overlays.
The session records inputs, virtual devices, serial output and QEMU diagnostics.
QEMU stays running after the command exits. SYSTEM and software images are
read-only. No physical disks, host network interface, or Pi monitor are attached.
## Insert, use and remove cartridges
## Insert and run software
From the workstation:
```sh
fds-emulator --session out/my-emulator insert 01 out/demo-tools.img
fds-emulator --session out/my-emulator guest -- fds bay 01
fds-emulator --session out/my-emulator guest -- fds run 01 -- demo.hello:hello
fds-emulator --session out/my-emulator guest -- hello 'Hello from the emulator'
fds-emulator --session out/my-emulator guest -- fds run 01 -- demo.report:report
```
Discovery is asynchronous. Wait until the bay reports `MOUNTED READ ONLY` before
running commands. Direct commands return their output and exit status to the
caller. `fds run` starts a background job; read its output with:
```sh
fds-emulator --session out/my-emulator guest -- tail -20 /run/log/cartridged/current
```
Open an interactive shell with `fds-emulator --session out/my-emulator console`.
At `FDS>`, use `hello`, `report`, `fds bays` or other guest commands. **Ctrl-]**
detaches without stopping the VM. Detach before using `guest`, `eject` or ordinary
`stop`, since they also need the serial console.
All twelve virtual bays accept images. Simultaneously inserted PROGRAM cartridges
must have distinct cartridge IDs. Duplicate identities produce a guest error.
Command-name collisions are resolved as described in [Using cartridges](cartridges.md).
## Eject and stop
```sh
fds-emulator --session out/my-emulator eject 01
```
Insertion is asynchronous: `fds bay 01` may briefly show `EMPTY` before USB
storage discovery completes. Retry the status command until it reports
`MOUNTED READ ONLY`, or inspect its error. The catalogue lists exact commands.
`fds run` starts a managed process and prints its PID; its output goes to the
cartridge service log. Packages are verified and extracted into temporary,
read-only RAM filesystems on first run. They are not compiled on the guest.
All twelve bays, numbered `01`–`12`, are available. Simultaneously mounted
PROGRAM cartridges need distinct cartridge IDs; inserting duplicate IDs is
reported as a guest error. `eject` asks FDS to stop
managed consumers, release mounts and declare `SAFE`, then removes the virtual
USB device. If FDS refuses, QEMU leaves the cartridge attached. To deliberately
simulate an accidental pull, use:
```sh
fds-emulator --session out/my-emulator unplug 01
```
A forced unplug can lose DATA writes. The guest must then detect the removal
and clean up its processes and mounts. `status` reports the actual QEMU devices
and block nodes as well as the recorded image paths.
Use the interactive shell if preferred:
```sh
fds-emulator --session out/my-emulator console
```
At the `FDS>` prompt, type `fds bays`, `fds bay 01`, or `fds run 01 --
demo.hello:hello`. **Ctrl-] detaches**; it does not stop the VM. Use a second
workstation terminal for insertion/unplug. Detach the console before `guest`,
`eject`, or ordinary `stop`, because they also need exclusive serial access.
Finish with:
```sh
fds-emulator --session out/my-emulator status
fds-emulator --session out/my-emulator stop
```
This invokes native FDS shutdown. `stop --force` cuts virtual power without that
sequence. Logs and DATA overlays remain in the session directory. Choose a new
session directory to boot again; stopped sessions are retained for inspection,
not resumed from RAM snapshots.
Eject asks FDS to stop programs and release storage before removing the virtual
USB device. If FDS refuses, the cartridge stays attached. `stop` performs native
FDS shutdown. Session logs remain available; create a new session to boot again.
## Writable DATA images
`unplug BAY` simulates a physical pull, and `stop --force` cuts virtual power.
Use these only for deliberate failure simulation; DATA writes can be lost.
Insert an existing FDS DATA disk image with the same `insert` command. The
emulator creates a unique `data-<bay>-<id>.qcow2` overlay in the session directory;
all guest writes go there. The original image remains unchanged. Safe eject
retains the overlay, while inserting the original image again starts a fresh
one. Software cartridges remain read-only.
## Save emulator DATA
After eject or shutdown, export an overlay if you need the changed DATA contents:
Inserting a DATA image creates a writable `data-<bay>-<id>.qcow2` overlay in the
session directory. Eject retains it. Reinserting the original image creates a
fresh overlay. To export the changed contents after eject or shutdown:
```sh
qemu-img convert -f qcow2 -O raw /path/to/session/data-02-IDENTIFIER.qcow2 out/saved-data.img
qemu-img convert -f qcow2 -O raw /path/to/session/data-02-ID.qcow2 out/saved-data.img
```
Keep the original backing image at its recorded path until conversion completes.
Never convert or edit an image while it is attached to a running VM.
Keep its backing image at the recorded path until export completes. Never convert
an overlay while a running VM is using it. Personal session directories are
preserved by `make clean`; keep them under your own names, such as `my-emulator`.
## Write the completed image to USB
## Write a cartridge to USB
The burn flow always starts with the complete image created above. It does not
construct partitions directly on a drive. Identify the intended **whole USB
drive**, unmount it, then make a preview as your normal user:
Identify the intended whole USB drive, unmount its partitions, then preview:
```sh
fds-cartridge preview out/demo-tools.img /dev/sdX out/usb-preview.json
```
Replace `/dev/sdX` with the actual whole USB drive. The JSON records its model,
size, insertion identity, exact image hash and an exact `confirmation` string.
Review these before copying the full phrase into the write command:
Replace `/dev/sdX` with the actual destination. The JSON identifies the drive,
capacity, image hash and exact confirmation phrase. Check those values before
writing: this replaces the selected drive's contents.
```sh
sudo /absolute/path/to/fds-cartridge write out/usb-preview.json \
--confirm 'COPY THE EXACT confirmation VALUE FROM THE PREVIEW'
```
The writer rechecks source and target identity, requires a USB whole disk,
rejects mounted/protected storage, writes the **full image**, flushes and verifies
readback. On larger drives it relocates the backup GPT to the end. A replaced
USB drive or changed image requires a fresh preview. This destroys the selected
drive's existing contents. No physical drive has been written by the automated
acceptance tests.
The writer rechecks identity, writes the entire disk image, flushes it and verifies
readback. On a larger drive it moves the backup GPT to the end. A changed image
or reinserted drive requires a fresh preview. Use the image-tool runner for
preview and inspection if your EROFS utilities are in the local container.
To rehearse on a disposable file without root or USB hardware:
To rehearse with a disposable regular file:
```sh
truncate -s 64M out/disposable-usb.img
fds-cartridge preview out/demo-tools.img out/disposable-usb.img \
out/file-preview.json --file-target
truncate -s "$(stat -c %s out/demo-tools.img)" out/disposable-usb.img
fds-cartridge preview out/demo-tools.img out/disposable-usb.img out/file-preview.json --file-target
fds-cartridge write out/file-preview.json --confirm 'COPY THE EXACT confirmation VALUE'
fds-cartridge inspect out/disposable-usb.img
```
The file must be at least as large as the source image. Use `--image-tool-runner`
for preview/inspection if EROFS tools are provided by the local wrapper.
## Troubleshooting
| Symptom | What to do |
| --- | --- |
| Required executable not found | Run the corresponding `doctor`; install the named host utility or select an explicit runner |
| Bubblewrap namespace failure | Enable unprivileged user namespaces according to your workstation policy; do not run creation/inspection as root |
| Output already exists | Use a new bundle/image/preview/session name; creation does not overwrite outputs |
| Unknown `fds.emulator` option or missing emulator settings | Rebuild both the current initramfs and SYSTEM; the frozen 0.1.0 images predate this feature |
| Console in use | Detach with Ctrl-] before guest commands, safe eject, or shutdown |
| Guest command timed out | Inspect `console.log`; the command may still be running, so do not blindly repeat a write |
| Incomplete insertion | Run `unplug BAY` to reconcile the recorded intent with actual QEMU devices, then reinsert |
| Software digest/path/architecture error | Rebuild the bundle and cartridge on the workstation; the guest will not run an invalid archive |
| Cache limit exceeded | Reduce the software bundle; one runtime tree including inode overhead is limited to 256 MiB |
| Safe eject blocked | Close unmanaged processes or extra mounts using the media, then retry; `unplug` is only for deliberate failure simulation |
| VM will not boot | Read session `console.log` and `qemu.log`; verify all three supplied boot artifacts belong to the current build |
QEMU exercises the actual Linux/FDS software path. It does not emulate Pi
firmware, RP1, USB power sequencing, the physical twelve-bay wiring, or Dasung
power recovery. Those acceptance checks remain hardware procedures.
## Physical Pi acceptance procedure (deferred)
After assembling and calibrating the physical bay map, use a disposable USB
cartridge and the current matching SYSTEM/initramfs. Perform these checks on the
Pi; the VM results do not replace them:
1. Build the hello/report cartridge on the workstation, record its image hash,
and use preview/confirmation to write that entire image to the chosen USB drive.
2. Insert it in a calibrated bay. Record `fds --json bay BAY`; confirm both
software entries and their declared payload partitions appear.
3. Run `demo.hello:hello` and `demo.report:report`. Save their output from the
cartridge log and confirm UID 1000. Check `/proc/self/mountinfo` for read-only
payload and software-cache mounts.
4. Run `fds eject BAY`, verify SAFE, then remove and reinsert the cartridge in
another calibrated bay. Repeat both commands. Record any USB enumeration or
I/O errors.
5. With a disposable cartridge and a deliberately long-running test program,
test surprise removal separately. Verify its managed processes terminate and
all of that bay's software mounts disappear. Do not use valuable DATA for
this failure test.
6. Exercise native shutdown with active software, cold boot, and a real USB
power cycle. Save actual observations and kernel/service logs. Follow the
separate [Dasung hardware procedure](dasung.md) for monitor recovery.
Record the Pi, hub, USB drive and kernel versions with the result. Hardware
latency, electrical behavior and data durability remain unverified until these
measurements are performed on the assembled machine.
See [troubleshooting](troubleshooting.md) for failed builds, namespace errors,
busy consoles, invalid media and blocked ejects.
+3 -3
View File
@@ -1,11 +1,11 @@
format = 1
format = 2
id = "demo.tools"
name = "Hello and system report"
version = "1.0"
# Paths are relative to this recipe. Each payload entry creates one partition.
[[payload]]
bundles = ["../../out/demo-hello"]
sources = ["hello/software.toml"]
[[payload]]
bundles = ["../../out/demo-report"]
sources = ["report/software.toml"]
+6 -8
View File
@@ -1,14 +1,12 @@
format = 1
format = 2
id = "demo.hello"
name = "AArch64 hello"
version = "1.0"
architecture = "aarch64"
root = "root"
[commands]
hello = "bin/hello"
hello = "usr/bin/hello"
# This trusted command runs only on the workstation. It is never put on media.
[build]
directory = "."
command = ["sh", "-eu", "-c", "mkdir -p root/bin; aarch64-linux-gnu-gcc -O2 hello.c -o root/bin/hello"]
# Void source templates run only on the workstation.
[source]
package = "fds-demo-hello"
template = "void"
+15
View File
@@ -0,0 +1,15 @@
# Small example of a native Void source package, cross-built by xbps-src.
pkgname=fds-demo-hello
version=1.0
revision=1
archs="aarch64"
short_desc="FDS cartridge hello example"
maintainer="FDS/OS maintainers"
license="MIT"
homepage="https://docs.voidlinux.org/xbps/"
do_build() {
${CC} ${CFLAGS} "${FILESDIR}/hello.c" ${LDFLAGS} -o hello
}
do_install() {
vbin hello
}
+6 -4
View File
@@ -1,9 +1,11 @@
format = 1
format = 2
id = "demo.report"
name = "System report"
version = "1.0"
architecture = "any"
root = "root"
[commands]
report = "bin/report"
report = "usr/bin/report"
[source]
package = "fds-demo-report"
template = "void"
@@ -2,3 +2,4 @@
printf 'FDS cartridge system report\n'
uname -m
id
[ "${1-}" != hold ] || exec tail -f /dev/null
+12
View File
@@ -0,0 +1,12 @@
pkgname=fds-demo-report
version=1.0
revision=1
archs="aarch64"
depends="bash coreutils"
short_desc="FDS cartridge report example"
maintainer="FDS/OS maintainers"
license="MIT"
homepage="https://docs.voidlinux.org/xbps/"
do_install() {
vbin "${FILESDIR}/report"
}
+1 -1
View File
@@ -21,7 +21,7 @@ args = parser.parse_args()
payloads = {'fds-stage0': args.stage0.resolve(strict=True), 'dasungd': project/'out/dasungd'}
for tool in ('lz4', 'zstd'):
if not shutil.which(tool): sys.exit(f'ERROR: missing host {tool}; see docs/m4-work.md')
if not shutil.which(tool): sys.exit(f'ERROR: missing host {tool}; see docs/developer/m4-work.md')
for executable in ('fds-stage0', 'dasungd'):
subprocess.run([str(project/'tools/verify-elf'), str(payloads[executable]), 'aarch64', 'static'], check=True)
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
+1 -1
View File
@@ -1,5 +1,5 @@
# English UTF-8 by default. Interactive shells are userspace, never PID 1.
export PATH=/usr/bin:/bin
export PATH=/usr/bin:/bin:/run/fds/bin
export LANG=en_US.UTF-8
export TZ=UTC
umask 022
+2
View File
@@ -14,6 +14,8 @@ do_install() {
local input="${XBPS_SRCDISTDIR}/${pkgname}-${version}"
(cd "$input" && sha256sum -c SHA256SUMS) || return 1
vbin "${input}/fds"
vbin "${input}/fds-program"
vbin "${input}/fds-control"
vbin "${input}/fds-boottrace"
vbin "${input}/fds-burn"
vbin "${input}/fds-inspect"
+1
View File
@@ -1,4 +1,5 @@
("FDS/OS",
("FDS Control", EXEC, "fds-control"),
("Terminal", EXEC, "/usr/libexec/fds/terminal"),
("Enable Ethernet", EXEC, "fds network on"),
("Disable Ethernet", EXEC, "fds network off"),
+1
View File
@@ -1,5 +1,6 @@
#!/bin/bash
export PS1='FDS> '
export PATH=/usr/bin:/bin:/run/fds/bin
exec /usr/bin/xterm -class FDS -name fds-terminal -title 'FDS Terminal' \
-bg white -fg black -cr black -fn '-xos4-terminus-medium-r-normal--20-200-72-72-c-100-iso10646-1' \
-ut -bc -uc -sb -geometry 100x35+40+40 -e /bin/bash --noprofile --norc -i
@@ -10,4 +10,5 @@ done
xset s off
xset -dpms
xsetroot -solid white
fds-control &
exec wmaker --no-dock --no-clip --no-autolaunch
+2 -2
View File
@@ -7,11 +7,11 @@ if [[ $(cat /usr/share/fds/image-profile) == recovery ]]; then
cd /run/fds/recovery-home
printf '\nFDS RECOVERY — LOCAL MAINTENANCE CONSOLE\nDATA stays read-only until explicitly selected.\nUse fds recovery help for inspection and repair commands.\n'
exec /usr/bin/env -i HOME=/run/fds/recovery-home USER=root LOGNAME=root \
SHELL=/bin/bash PATH=/usr/bin:/bin LANG=en_US.UTF-8 TERM="${TERM:-linux}" \
SHELL=/bin/bash PATH=/usr/bin:/bin:/run/fds/bin LANG=en_US.UTF-8 TERM="${TERM:-linux}" \
/bin/bash --login
fi
cd /home/fds
exec /usr/bin/s6-setuidgid fds /usr/bin/env -i \
HOME=/home/fds USER=fds LOGNAME=fds SHELL=/bin/bash \
PATH=/usr/bin:/bin LANG=en_US.UTF-8 TERM="${TERM:-linux}" \
PATH=/usr/bin:/bin:/run/fds/bin LANG=en_US.UTF-8 TERM="${TERM:-linux}" \
/bin/bash --login
+42 -2
View File
@@ -69,11 +69,36 @@ pub fn start(
) -> Result<Child> {
start_group(&format!("bay{bay}"), arguments, working, environment)
}
pub fn foreground(
bay: Bay,
arguments: &[String],
working: &str,
environment: &[(String, String)],
descriptors: [OwnedFd; 3],
terminal: bool,
) -> Result<Child> {
spawn(
&format!("bay{bay}"),
arguments,
working,
environment,
Some((descriptors, terminal)),
)
}
pub fn start_group(
name: &str,
arguments: &[String],
working: &str,
environment: &[(String, String)],
) -> Result<Child> {
spawn(name, arguments, working, environment, None)
}
fn spawn(
name: &str,
arguments: &[String],
working: &str,
environment: &[(String, String)],
io: Option<([OwnedFd; 3], bool)>,
) -> Result<Child> {
if !fds_common::manifest::identifier(name) {
return Err(Error("Invalid process group".into()));
@@ -95,11 +120,11 @@ pub fn start_group(
.custom_flags(libc::O_CLOEXEC)
.open(path.join("cgroup.procs"))?;
let mut command = Command::new(&arguments[0]);
let working = c(working)?;
command
.args(&arguments[1..])
.current_dir(working)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("PATH", "/usr/bin:/bin:/run/fds/bin")
.env("HOME", "/home/fds")
.env("USER", "fds")
.env("LOGNAME", "fds")
@@ -109,6 +134,14 @@ pub fn start_group(
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
command.envs(environment.iter().cloned());
let mut terminal = false;
if let Some(([input, output, errors], tty)) = io {
command
.stdin(Stdio::from(input))
.stdout(Stdio::from(output))
.stderr(Stdio::from(errors));
terminal = tty;
}
// Only async-signal-safe syscalls are used in the forked child. Writing 0
// moves the child itself, avoiding PID reuse and parent/child migration races.
unsafe {
@@ -119,6 +152,9 @@ pub fn start_group(
if libc::setsid() < 0 {
return Err(io::Error::last_os_error());
}
if terminal && libc::ioctl(0, libc::TIOCSCTTY, 0) < 0 {
return Err(io::Error::last_os_error());
}
if libc::setgroups(0, std::ptr::null()) < 0
|| libc::setgid(1000) < 0
|| libc::setuid(1000) < 0
@@ -128,6 +164,10 @@ pub fn start_group(
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0 {
return Err(io::Error::last_os_error());
}
// Resolve client-selected working directories only as the user.
if libc::chdir(working.as_ptr()) < 0 {
return Err(io::Error::last_os_error());
}
let mut mask: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut mask);
if libc::sigprocmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) < 0 {
+1
View File
@@ -4,6 +4,7 @@ mod data_sessions;
mod media;
mod power;
mod profiles;
mod programs;
mod recovery;
mod server;
mod software;
+1 -1
View File
@@ -228,7 +228,7 @@ impl Mounted {
("FDS_APP".into(), app.display().to_string()),
(
"PATH".into(),
format!("{}/bin:/usr/bin:/bin", app.display()),
format!("{}/bin:/usr/bin:/bin:/run/fds/bin", app.display()),
),
(
"LD_LIBRARY_PATH".into(),
+88
View File
@@ -0,0 +1,88 @@
//! A stable PATH directory is updated as validated cartridges appear/disappear.
use crate::media::Mounted;
use fds_common::{
Bay, Error, Result,
manifest::{Class, identifier},
};
use std::{
collections::BTreeMap,
fs,
os::unix::fs::{PermissionsExt, symlink},
path::Path,
};
pub const BIN: &str = "/run/fds/bin";
pub type Commands = BTreeMap<String, (Bay, String)>;
pub fn collect(mounts: &BTreeMap<Bay, Mounted>) -> Result<Commands> {
let mut result = BTreeMap::new();
for (&bay, mount) in mounts {
if mount.manifest.cartridge.class != Class::Program || mount.fault.is_some() {
continue;
}
let mut commands = Vec::new();
if let Some(software) = &mount.software {
for entry in &software.catalogue.software {
for name in entry.commands.keys() {
commands.push((
name.clone(),
format!("{}:{name}", entry.id),
format!("b{bay}:{}:{name}", entry.id),
));
}
}
} else {
let root = Path::new(&mount.path).join("app/bin");
if root.is_dir() {
for entry in fs::read_dir(root)? {
let entry = entry?;
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if identifier(&name)
&& entry.path().is_file()
&& entry
.path()
.canonicalize()?
.starts_with(Path::new(&mount.path).join("app"))
&& entry.path().metadata()?.permissions().mode() & 0o111 != 0
{
commands.push((name.clone(), name.clone(), format!("b{bay}:{name}")));
}
}
}
}
commands.sort();
for (name, selector, qualified) in commands {
result.entry(name).or_insert((bay, selector.clone()));
// A fully qualified spelling always identifies this cartridge.
result.insert(qualified, (bay, selector));
}
}
Ok(result)
}
pub fn publish(commands: &Commands) -> Result<()> {
fs::create_dir_all(BIN)?;
fs::set_permissions(BIN, fs::Permissions::from_mode(0o755))?;
for entry in fs::read_dir(BIN)? {
let entry = entry?;
if !commands.contains_key(&entry.file_name().to_string_lossy().into_owned()) {
if !entry.file_type()?.is_symlink() {
return Err(Error(
"Unexpected file in cartridge command directory".into(),
));
}
fs::remove_file(entry.path())?;
}
}
for name in commands.keys() {
let path = Path::new(BIN).join(name);
if path.symlink_metadata().is_ok() {
if fs::read_link(&path)? != Path::new("/usr/bin/fds-program") {
return Err(Error("Unexpected cartridge command link".into()));
}
} else {
symlink("/usr/bin/fds-program", path)?;
}
}
Ok(())
}
+166 -9
View File
@@ -32,6 +32,7 @@ struct State {
profiles: profiles::Manager,
burning: burning::Manager,
power: power::Manager,
commands: crate::programs::Commands,
}
impl State {
fn scan(&mut self) -> Result<()> {
@@ -63,6 +64,7 @@ impl State {
software: None,
mount: None,
consumers: consumers::count(bay)?,
commands: Vec::new(),
};
// A hub in a bay may contain several functions, but multiple actual
// devices are ambiguous until an explicit composite policy exists.
@@ -109,8 +111,113 @@ impl State {
if !self.power.frozen() {
self.profiles.reconcile(&self.mounts, &devices)?;
}
self.commands = crate::programs::collect(&self.mounts)?;
crate::programs::publish(&self.commands)?;
for entry in &mut self.bays {
entry.commands = self
.commands
.iter()
.filter_map(|(alias, (bay, selector))| {
(*bay == entry.bay && alias.starts_with(&format!("b{bay}:"))).then(|| {
fds_common::control::PublishedCommand {
selector: selector.clone(),
alias: alias.clone(),
}
})
})
.collect();
}
Ok(())
}
fn program(&mut self, name: &str) -> Result<(Bay, fds_common::launch::Program)> {
if self.recovery || self.power.frozen() {
return Err(Error(
"Program launches are unavailable during recovery or shutdown".into(),
));
}
let (bay, selector) = self
.commands
.get(name)
.cloned()
.ok_or_else(|| Error(format!("Cartridge command {name} is no longer available")))?;
let mount = self
.mounts
.get_mut(&bay)
.ok_or_else(|| Error("Cartridge was removed".into()))?;
let (arguments, environment) = mount.program(&[selector])?;
Ok((
bay,
fds_common::launch::Program {
arguments,
environment,
},
))
}
fn foreground(
&mut self,
name: &str,
arguments: &[String],
terminal: bool,
working: &str,
term: &str,
client: &mut Client,
) -> Result<Response> {
self.program(name)?;
if arguments.len() > 120
|| arguments.iter().any(|a| a.contains('\0'))
|| !working.starts_with('/')
|| working.len() > 4096
|| working.contains('\0')
|| term.len() > 128
|| term.chars().any(char::is_control)
{
return Err(Error(
"Invalid foreground program arguments or environment".into(),
));
}
client.socket.set_nonblocking(false)?;
client
.socket
.set_read_timeout(Some(Duration::from_secs(5)))?;
client
.socket
.set_write_timeout(Some(Duration::from_secs(5)))?;
let result = (|| {
client.socket.write_all(b"R")?;
let descriptors: [OwnedFd; 3] = fds_common::launch::receive_fds(&mut client.socket, 3)?
.try_into()
.map_err(|_| Error("Expected three program I/O descriptors".into()))?;
let (bay, mut program) = self.program(name)?;
program.arguments.extend_from_slice(arguments);
program.environment.push(("TERM".into(), term.into()));
let mut child = consumers::foreground(
bay,
&program.arguments,
working,
&program.environment,
descriptors,
terminal,
)?;
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, child.id(), 0) } as i32;
if fd < 0 {
let _ = child.kill();
self.children.push(child);
return Err(std::io::Error::last_os_error().into());
}
let pidfd = unsafe { OwnedFd::from_raw_fd(fd) };
if let Err(error) = fds_common::launch::send_fds(&client.socket, &[pidfd.as_raw_fd()]) {
let _ = child.kill();
self.children.push(child);
return Err(error);
}
client.foreground = Some(child);
let mut reply = Response::failure("");
reply.error = None;
Ok(reply)
})();
client.socket.set_nonblocking(true)?;
result
}
fn mapped_devices(&self) -> Vec<(Bay, topology::UsbDevice)> {
self.bays
.iter()
@@ -628,6 +735,7 @@ impl State {
},
media_job: None,
recovery: None,
exit_status: None,
disk: None,
power: None,
})
@@ -796,6 +904,7 @@ struct Client {
offset: usize,
deadline: Instant,
uid: u32,
foreground: Option<std::process::Child>,
waiting: Option<(String, u64)>,
}
fn peer_uid(socket: &UnixStream) -> Result<Option<u32>> {
@@ -910,6 +1019,7 @@ pub fn run(notify: bool) -> Result<()> {
profiles: profiles::Manager::new(!recovery),
burning: burning::Manager::load()?,
power: power::Manager::load()?,
commands: BTreeMap::new(),
};
for n in 1..=12 {
let bay = Bay::try_from(n)?;
@@ -965,6 +1075,7 @@ pub fn run(notify: bool) -> Result<()> {
state.profiles.shutdown()?;
}
cleanup_stale_mounts()?;
crate::programs::publish(&BTreeMap::new())?;
state.scan()?;
let mut clients: Vec<Client> = Vec::new();
loop {
@@ -1000,7 +1111,7 @@ pub fn run(notify: bool) -> Result<()> {
fd: client.socket.as_raw_fd(),
events: if client.output.is_some() {
libc::POLLOUT
} else if client.waiting.is_some() {
} else if client.waiting.is_some() || client.foreground.is_some() {
0
} else {
libc::POLLIN
@@ -1010,6 +1121,7 @@ pub fn run(notify: bool) -> Result<()> {
}
let timeout = clients
.iter()
.filter(|c| c.foreground.is_none())
.map(|c| {
c.deadline
.saturating_duration_since(Instant::now())
@@ -1071,9 +1183,25 @@ pub fn run(notify: bool) -> Result<()> {
if (fds[0].revents != 0 && consume_events(&events)?) || console_changed {
state.scan()?;
}
// Reserve connection capacity for eject/status even with many foreground jobs.
let foreground_count = clients.iter().filter(|c| c.foreground.is_some()).count();
// Process existing clients before accepting more; vectors stay aligned.
for index in (0..clients.len()).rev() {
let client = &mut clients[index];
if let Some(child) = &mut client.foreground {
if let Some(status) = child.try_wait()? {
use std::os::unix::process::ExitStatusExt;
let mut reply = Response::failure("");
reply.error = None;
reply.exit_status = Some(status.into_raw());
let mut output =
serde_json::to_vec(&reply).map_err(|e| Error(e.to_string()))?;
output.push(b'\n');
client.output = Some(output);
client.foreground = None;
client.deadline = Instant::now() + Duration::from_secs(5);
}
}
let ready = fds[index + 5].revents;
if let Some((id, sequence)) = &client.waiting {
let job = state.burning.status(id);
@@ -1097,8 +1225,8 @@ pub fn run(notify: bool) -> Result<()> {
client.deadline = Instant::now() + Duration::from_secs(5);
}
}
let mut remove =
Instant::now() >= client.deadline || ready & (libc::POLLERR | libc::POLLNVAL) != 0;
let mut remove = (client.foreground.is_none() && Instant::now() >= client.deadline)
|| ready & (libc::POLLERR | libc::POLLNVAL) != 0;
if !remove && ready & libc::POLLIN != 0 && client.output.is_none() {
let mut chunk = [0u8; 4096];
match client.socket.read(&mut chunk) {
@@ -1124,9 +1252,31 @@ pub fn run(notify: bool) -> Result<()> {
client.waiting = Some((id.clone(), *sequence));
}
}
state
.reply(request, client.uid)
.unwrap_or_else(Response::failure)
if let Request::Program {
name,
arguments,
terminal,
working,
term,
} = request
{
if foreground_count >= 64 {
Response::failure(
"Too many foreground programs; close a program and retry",
)
} else {
state
.foreground(
&name, &arguments, terminal, &working,
&term, client,
)
.unwrap_or_else(Response::failure)
}
} else {
state
.reply(request, client.uid)
.unwrap_or_else(Response::failure)
}
}
Err(_) => Response::failure("Invalid control request"),
}
@@ -1140,7 +1290,9 @@ pub fn run(notify: bool) -> Result<()> {
.unwrap();
}
output.push(b'\n');
if client.waiting.is_some() {
if client.foreground.is_some() {
// SIGCHLD wakes the loop when this foreground command exits.
} else if client.waiting.is_some() {
client.deadline = Instant::now() + Duration::from_secs(90);
} else {
client.output = Some(output);
@@ -1173,7 +1325,11 @@ pub fn run(notify: bool) -> Result<()> {
remove = true;
}
if remove {
clients.swap_remove(index);
let mut removed = clients.swap_remove(index);
if let Some(mut child) = removed.foreground.take() {
let _ = child.kill();
state.children.push(child);
}
}
}
if fds[1].revents & libc::POLLIN != 0 {
@@ -1181,7 +1337,7 @@ pub fn run(notify: bool) -> Result<()> {
match listener.accept() {
Ok((socket, _)) => {
let uid = peer_uid(&socket)?;
if clients.len() >= 16 || uid.is_none() {
if clients.len() >= 128 || uid.is_none() {
continue;
}
socket.set_nonblocking(true)?;
@@ -1192,6 +1348,7 @@ pub fn run(notify: bool) -> Result<()> {
offset: 0,
deadline: Instant::now() + Duration::from_secs(5),
uid: uid.unwrap(),
foreground: None,
waiting: None,
});
}
+53 -6
View File
@@ -2,7 +2,7 @@
use crate::media::{self, c, checked};
use fds_burn::{device::Disk, image};
use fds_common::{Bay, Error, Result, read_text, sysfs::BlockPartition};
use fds_software::{Catalogue, archive};
use fds_software::{Catalogue, archive, tree};
use std::{
collections::{BTreeMap, BTreeSet},
fs::{self, File, OpenOptions},
@@ -136,7 +136,14 @@ impl Mounted {
&format!("/proc/self/fd/{}", source.as_raw_fd()),
&path,
"erofs",
libc::MS_RDONLY | libc::MS_NOEXEC | libc::MS_NOSUID | libc::MS_NODEV,
libc::MS_RDONLY
| libc::MS_NOSUID
| libc::MS_NODEV
| if result.catalogue.format == 1 {
libc::MS_NOEXEC
} else {
0
},
"",
)?;
result.payloads.insert(
@@ -148,7 +155,11 @@ impl Mounted {
key,
},
);
let bundles = Path::new(&path).join("bundles");
let bundles = Path::new(&path).join(if result.catalogue.format == 2 {
"programs"
} else {
"bundles"
});
if !fs::symlink_metadata(&bundles)?.is_dir() {
return Err(Error("Payload bundles must be a real directory".into()));
}
@@ -157,14 +168,20 @@ impl Mounted {
.software
.iter()
.filter(|s| s.partition == spec.number)
.map(|s| format!("{}.tar.xz", s.id))
.map(|s| {
if s.installed {
s.id.clone()
} else {
format!("{}.tar.xz", s.id)
}
})
.collect();
let actual: BTreeSet<_> = fs::read_dir(&bundles)?
.map(|e| Ok(e?.file_name().to_string_lossy().into_owned()))
.collect::<Result<_>>()?;
if actual != expected {
if actual != expected || fs::read_dir(&path)?.count() != 1 {
return Err(Error(
"Payload archive inventory disagrees with catalogue".into(),
"Payload software inventory disagrees with catalogue".into(),
));
}
for software in result
@@ -173,6 +190,10 @@ impl Mounted {
.iter()
.filter(|s| s.partition == spec.number)
{
if software.installed {
tree::verify(&Path::new(&path).join(software.root_path()), software)?;
continue;
}
if archive::open(&Path::new(&path).join(software.archive_path()))?
.metadata()?
.len()
@@ -226,6 +247,32 @@ impl Mounted {
if media::key(&payload.partition)? != payload.key {
return Err(Error("Software payload was removed".into()));
}
if software.installed {
let root = Path::new(&payload.path).join(software.root_path());
let mut args = tree::executable(&root, executable)?;
args.extend_from_slice(&arguments[1..]);
let root = root.display().to_string();
return Ok((
args,
vec![
("FDS_APP".into(), root.clone()),
(
"PATH".into(),
format!("{root}/usr/bin:{root}/bin:/usr/bin:/bin:/run/fds/bin"),
),
(
"LD_LIBRARY_PATH".into(),
format!("{root}/usr/lib:{root}/lib"),
),
(
"XDG_DATA_DIRS".into(),
format!("{root}/usr/share:/usr/share"),
),
("DISPLAY".into(), ":0".into()),
("XAUTHORITY".into(), "/run/fds/x11/authority".into()),
],
));
}
if !self.caches.contains_key(id) {
// Each executable tree receives its own bounded, read-only tmpfs.
// Root owns every path; the consumer only receives ordinary UID 1000.
+4
View File
@@ -9,6 +9,10 @@ description = "FDS system and cartridge command interface"
name = "fds"
path = "src/main.rs"
[[bin]]
name = "fds-program"
path = "src/program.rs"
[dependencies]
clap.workspace = true
fds-burn = { path = "../fds-burn" }
+6
View File
@@ -159,6 +159,7 @@ fn inspect_manifest(path: &Path, json: bool) -> Result<()> {
}
fn cartridge(request: Request, json: bool) -> Result<()> {
let debug = matches!(request, Request::Topology);
let details = matches!(request, Request::Bay { .. });
let response = control::request(&request)?;
if json {
println!(
@@ -237,6 +238,11 @@ fn cartridge(request: Request, json: bool) -> Result<()> {
if let Some(mount) = bay.mount {
println!(" MOUNT {mount}");
}
if details {
for command in bay.commands {
println!(" FOREGROUND {}", command.alias);
}
}
if bay.consumers > 0 {
println!(" MANAGED PROCESSES {}", bay.consumers);
}
+142
View File
@@ -0,0 +1,142 @@
//! Foreground cartridge commands preserve terminal I/O and managed process tracking.
mod program_io;
use clap::{Parser, Subcommand};
use fds_common::{Error, Result, launch};
use std::{ffi::OsString, path::PathBuf, process::ExitCode};
// Parse argv[0] as a typed positional: Clap multicall uses file_stem(), which
// removes dotted software IDs. All options/arguments still go through Clap.
#[derive(Parser)]
#[command(no_binary_name = true, disable_help_flag = true)]
struct Invocation {
executable: PathBuf,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
arguments: Vec<OsString>,
}
#[derive(Parser)]
#[command(
name = "fds-program",
version,
disable_help_subcommand = true,
about = "Run a published cartridge command in the foreground"
)]
struct Explicit {
#[command(subcommand)]
command: Published,
}
#[derive(Subcommand)]
enum Published {
#[command(external_subcommand)]
Command(Vec<OsString>),
}
impl Invocation {
fn command(self) -> std::result::Result<(String, Vec<OsString>), clap::Error> {
let name = self
.executable
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::InvalidUtf8,
"Invalid cartridge command name",
)
})?
.to_owned();
if name == "fds-program" {
let parsed = Explicit::try_parse_from(
std::iter::once(self.executable.into_os_string()).chain(self.arguments),
)?;
let Published::Command(mut arguments) = parsed.command;
let command = arguments.remove(0).into_string().map_err(|_| {
clap::Error::raw(
clap::error::ErrorKind::InvalidUtf8,
"Invalid cartridge command name",
)
})?;
Ok((command, arguments))
} else {
Ok((name, self.arguments))
}
}
}
fn run() -> Result<i32> {
let (name, arguments) = Invocation::parse()
.command()
.unwrap_or_else(|error| error.exit());
if unsafe { libc::geteuid() } == 0 {
if unsafe { libc::setgroups(0, std::ptr::null()) } < 0
|| unsafe { libc::setgid(1000) } < 0
|| unsafe { libc::setuid(1000) } < 0
{
return Err(std::io::Error::last_os_error().into());
}
}
let io = program_io::Io::new()?;
let arguments = arguments
.into_iter()
.map(|value| {
value
.into_string()
.map_err(|_| Error("Program arguments must be UTF-8".into()))
})
.collect::<Result<Vec<_>>>()?;
let (socket, child) = launch::start(
&name,
arguments,
io.terminal(),
std::env::current_dir()?.display().to_string(),
std::env::var("TERM").unwrap_or_else(|_| "linux".into()),
&io.descriptors(),
)?;
io.wait(socket, child)
}
fn main() -> ExitCode {
match run() {
Ok(status) => ExitCode::from(if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status) as u8
} else {
(128 + libc::WTERMSIG(status)) as u8
}),
Err(error) => {
eprintln!("fds-program: {error}");
ExitCode::from(126)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn clap_preserves_dotted_aliases_and_application_options() {
<Invocation as CommandFactory>::command().debug_assert();
Explicit::command().debug_assert();
for name in ["hello", "hello.world", "b01:demo.report:shell"] {
let (command, arguments) = Invocation::try_parse_from([
format!("/run/fds/bin/{name}"),
"--help".into(),
"two words".into(),
])
.unwrap()
.command()
.unwrap();
assert_eq!(command, name);
assert_eq!(arguments, ["--help", "two words"]);
}
let (command, arguments) =
Invocation::try_parse_from(["fds-program", "hello", "--version"])
.unwrap()
.command()
.unwrap();
assert_eq!(command, "hello");
assert_eq!(arguments, ["--version"]);
assert_eq!(
Invocation::try_parse_from(["fds-program", "--help"])
.unwrap()
.command()
.unwrap_err()
.kind(),
clap::error::ErrorKind::DisplayHelp
);
}
}
+345
View File
@@ -0,0 +1,345 @@
//! Terminal proxy for foreground programs whose privileged parent is the service.
use fds_common::{
Error, Result,
control::{LIMIT, Response},
};
use std::{
io::{Read, Write},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::net::UnixStream,
},
};
pub struct Io {
master: Option<OwnedFd>,
slave: Option<OwnedFd>,
terminal: Option<libc::termios>,
signals: OwnedFd,
old_mask: libc::sigset_t,
pending: std::collections::VecDeque<u8>,
}
fn checked(result: i32) -> Result<i32> {
if result < 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(result)
}
}
impl Io {
pub fn new() -> Result<Self> {
let mut mask = unsafe { std::mem::zeroed::<libc::sigset_t>() };
let mut old_mask = unsafe { std::mem::zeroed::<libc::sigset_t>() };
unsafe {
libc::sigemptyset(&mut mask);
for signal in [
libc::SIGINT,
libc::SIGTERM,
libc::SIGHUP,
libc::SIGQUIT,
libc::SIGWINCH,
libc::SIGTSTP,
libc::SIGCONT,
] {
libc::sigaddset(&mut mask, signal);
}
checked(libc::sigprocmask(libc::SIG_BLOCK, &mask, &mut old_mask))?;
}
let fd = unsafe { libc::signalfd(-1, &mask, libc::SFD_CLOEXEC | libc::SFD_NONBLOCK) };
if fd < 0 {
let error = std::io::Error::last_os_error();
unsafe {
libc::sigprocmask(libc::SIG_SETMASK, &old_mask, std::ptr::null_mut());
}
return Err(error.into());
}
let signals = unsafe { OwnedFd::from_raw_fd(fd) };
let mut io = Self {
master: None,
slave: None,
terminal: None,
signals,
old_mask,
pending: Default::default(),
};
if unsafe { libc::isatty(0) == 1 && libc::isatty(1) == 1 } {
let mut term = unsafe { std::mem::zeroed::<libc::termios>() };
let mut size = unsafe { std::mem::zeroed::<libc::winsize>() };
checked(unsafe { libc::tcgetattr(0, &mut term) })?;
checked(unsafe { libc::ioctl(0, libc::TIOCGWINSZ, &mut size) })?;
let (mut master, mut slave) = (-1, -1);
checked(unsafe {
libc::openpty(&mut master, &mut slave, std::ptr::null_mut(), &term, &size)
})?;
io.master = Some(unsafe { OwnedFd::from_raw_fd(master) });
io.slave = Some(unsafe { OwnedFd::from_raw_fd(slave) });
for fd in [master, slave] {
checked(unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) })?;
}
checked(unsafe { libc::fcntl(master, libc::F_SETFL, libc::O_NONBLOCK) })?;
io.terminal = Some(term);
io.raw()?;
}
Ok(io)
}
pub fn terminal(&self) -> bool {
self.terminal.is_some()
}
pub fn descriptors(&self) -> [i32; 3] {
if let Some(slave) = &self.slave {
[
slave.as_raw_fd(),
slave.as_raw_fd(),
if unsafe { libc::isatty(2) } == 1 {
slave.as_raw_fd()
} else {
2
},
]
} else {
[0, 1, 2]
}
}
fn raw(&self) -> Result<()> {
if let Some(term) = self.terminal {
let mut raw = term;
unsafe {
libc::cfmakeraw(&mut raw);
}
raw.c_lflag |= libc::ISIG;
checked(unsafe { libc::tcsetattr(0, libc::TCSANOW, &raw) })?;
}
Ok(())
}
fn restore(&self) {
if let Some(term) = self.terminal {
unsafe {
libc::tcsetattr(0, libc::TCSANOW, &term);
}
}
}
fn write_terminal(&mut self, bytes: &[u8]) -> Result<()> {
if self.pending.len() + bytes.len() > 65536 {
return Err(Error("Terminal input queue exceeded its limit".into()));
}
self.pending.extend(bytes);
Ok(())
}
fn flush_input(&mut self) -> Result<()> {
let Some(master) = &self.master else {
return Ok(());
};
let bytes = self.pending.as_slices().0;
if bytes.is_empty() {
return Ok(());
}
let count = unsafe { libc::write(master.as_raw_fd(), bytes.as_ptr().cast(), bytes.len()) };
if count > 0 {
self.pending.drain(..count as usize);
} else if count < 0 {
let error = std::io::Error::last_os_error();
if !matches!(
error.kind(),
std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
) {
return Err(error.into());
}
}
Ok(())
}
fn output(&self) -> Result<bool> {
let Some(master) = &self.master else {
return Ok(false);
};
loop {
let mut buffer = [0u8; 8192];
let count =
unsafe { libc::read(master.as_raw_fd(), buffer.as_mut_ptr().cast(), buffer.len()) };
if count > 0 {
std::io::stdout().write_all(&buffer[..count as usize])?;
continue;
}
if count == 0 {
return Ok(false);
}
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::Interrupted {
continue;
}
if error.raw_os_error() == Some(libc::EIO) {
return Ok(false);
}
if error.kind() == std::io::ErrorKind::WouldBlock {
return Ok(true);
}
return Err(error.into());
}
}
pub fn wait(mut self, mut socket: UnixStream, child: OwnedFd) -> Result<i32> {
self.slave.take();
let mut bytes = Vec::new();
let mut input = self.master.is_some();
let mut output = self.master.is_some();
loop {
let mut fds = [
libc::pollfd {
fd: socket.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: self.signals.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if input && self.pending.len() < 32768 {
0
} else {
-1
},
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if output {
self.master.as_ref().unwrap().as_raw_fd()
} else {
-1
},
events: libc::POLLIN
| if self.pending.is_empty() {
0
} else {
libc::POLLOUT
},
revents: 0,
},
];
let result = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, -1) };
if result < 0
&& std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted
{
continue;
}
checked(result)?;
if fds[3].revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0 {
output = self.output()?;
}
if output && fds[3].revents & libc::POLLOUT != 0 {
self.flush_input()?;
}
if fds[2].revents != 0 {
let mut buffer = [0u8; 4096];
let count = unsafe { libc::read(0, buffer.as_mut_ptr().cast(), buffer.len()) };
if count > 0 {
self.write_terminal(&buffer[..count as usize])?;
} else if count == 0 {
input = false;
} else if std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted
{
return Err(std::io::Error::last_os_error().into());
}
}
if fds[1].revents != 0 {
let mut info = unsafe { std::mem::zeroed::<libc::signalfd_siginfo>() };
if unsafe {
libc::read(
self.signals.as_raw_fd(),
(&mut info as *mut libc::signalfd_siginfo).cast(),
std::mem::size_of_val(&info),
)
} > 0
{
let signal = info.ssi_signo as i32;
if signal == libc::SIGWINCH {
if let Some(master) = &self.master {
let mut size = unsafe { std::mem::zeroed::<libc::winsize>() };
if unsafe { libc::ioctl(0, libc::TIOCGWINSZ, &mut size) } == 0 {
unsafe {
libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &size);
}
}
}
} else if signal == libc::SIGTSTP {
self.restore();
if let Some(master) = &self.master {
let group = unsafe { libc::tcgetpgrp(master.as_raw_fd()) };
if group > 0 {
unsafe {
libc::kill(-group, libc::SIGSTOP);
}
}
}
unsafe {
libc::kill(libc::getpid(), libc::SIGSTOP);
}
self.raw()?;
if let Some(master) = &self.master {
let group = unsafe { libc::tcgetpgrp(master.as_raw_fd()) };
if group > 0 {
unsafe {
libc::kill(-group, libc::SIGCONT);
}
}
}
} else if self.master.is_some()
&& matches!(signal, libc::SIGINT | libc::SIGQUIT)
{
let index = if signal == libc::SIGINT {
libc::VINTR
} else {
libc::VQUIT
};
self.write_terminal(&[self.terminal.as_ref().unwrap().c_cc[index]])?;
} else if signal != libc::SIGCONT {
unsafe {
libc::syscall(
libc::SYS_pidfd_send_signal,
child.as_raw_fd(),
signal,
std::ptr::null::<libc::siginfo_t>(),
0,
);
}
}
}
}
if fds[0].revents != 0 {
let mut chunk = [0u8; 4096];
loop {
match socket.read(&mut chunk) {
Ok(0) => {
let _ = self.output()?;
let reply: Response = serde_json::from_slice(&bytes)
.map_err(|e| Error(format!("Invalid program completion: {e}")))?;
if let Some(error) = reply.error {
return Err(Error(error));
}
return reply
.exit_status
.ok_or_else(|| Error("Missing program exit status".into()));
}
Ok(n) => {
bytes.extend_from_slice(&chunk[..n]);
if bytes.len() > LIMIT {
return Err(Error("Program response is too large".into()));
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
}
}
}
}
impl Drop for Io {
fn drop(&mut self) {
self.restore();
unsafe {
libc::sigprocmask(libc::SIG_SETMASK, &self.old_mask, std::ptr::null_mut());
}
}
}
+18
View File
@@ -47,6 +47,14 @@ pub enum Request {
bay: Bay,
arguments: Vec<String>,
},
/// Foreground PATH launcher; stdio is passed to an ordinary managed child.
Program {
name: String,
arguments: Vec<String>,
terminal: bool,
working: String,
term: String,
},
MediaPrepare {
bay: Bay,
image: String,
@@ -105,6 +113,11 @@ impl MediaJob {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishedCommand {
pub selector: String,
pub alias: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BayState {
pub bay: Bay,
pub state: String,
@@ -114,6 +127,8 @@ pub struct BayState {
pub manifest: Option<Manifest>,
pub mount: Option<String>,
pub consumers: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<PublishedCommand>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub software: Option<crate::software::Catalogue>,
}
@@ -168,6 +183,8 @@ pub struct Response {
pub power: Option<PowerState>,
#[serde(default)]
pub recovery: Option<RecoveryReport>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_status: Option<i32>,
}
impl Response {
pub fn failure(error: impl ToString) -> Self {
@@ -182,6 +199,7 @@ impl Response {
disk: None,
power: None,
recovery: None,
exit_status: None,
}
}
}
+171
View File
@@ -0,0 +1,171 @@
//! Foreground launch transfers ordinary stdio to a managed child and receives its pidfd.
use crate::{
Error, Result,
control::{LIMIT, Request, Response, SOCKET},
};
use serde::{Deserialize, Serialize};
use std::{
io::{Read, Write},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::net::UnixStream,
},
time::Duration,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Program {
pub arguments: Vec<String>,
pub environment: Vec<(String, String)>,
}
pub fn send_fds(socket: &UnixStream, descriptors: &[i32]) -> Result<()> {
if descriptors.is_empty() || descriptors.len() > 3 {
return Err(Error("Invalid descriptor count".into()));
}
let mut marker = b'F';
let mut vector = libc::iovec {
iov_base: (&mut marker as *mut u8).cast(),
iov_len: 1,
};
let mut control = [0usize; 8];
let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
message.msg_iov = &mut vector;
message.msg_iovlen = 1;
message.msg_control = control.as_mut_ptr().cast();
message.msg_controllen =
unsafe { libc::CMSG_SPACE(std::mem::size_of_val(descriptors) as _) } as _;
unsafe {
let header = libc::CMSG_FIRSTHDR(&message);
(*header).cmsg_level = libc::SOL_SOCKET;
(*header).cmsg_type = libc::SCM_RIGHTS;
(*header).cmsg_len = libc::CMSG_LEN(std::mem::size_of_val(descriptors) as _) as _;
for (index, descriptor) in descriptors.iter().enumerate() {
std::ptr::write_unaligned(
libc::CMSG_DATA(header).cast::<i32>().add(index),
*descriptor,
);
}
if libc::sendmsg(socket.as_raw_fd(), &message, libc::MSG_NOSIGNAL) != 1 {
return Err(std::io::Error::last_os_error().into());
}
}
Ok(())
}
pub fn receive_fds(socket: &mut UnixStream, expected: usize) -> Result<Vec<OwnedFd>> {
let mut marker = 0u8;
let mut vector = libc::iovec {
iov_base: (&mut marker as *mut u8).cast(),
iov_len: 1,
};
let mut control = [0usize; 8];
let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
message.msg_iov = &mut vector;
message.msg_iovlen = 1;
message.msg_control = control.as_mut_ptr().cast();
message.msg_controllen = std::mem::size_of_val(&control) as _;
let n = unsafe { libc::recvmsg(socket.as_raw_fd(), &mut message, libc::MSG_CMSG_CLOEXEC) };
if n < 0 {
return Err(std::io::Error::last_os_error().into());
}
let mut received = Vec::new();
unsafe {
let mut header = libc::CMSG_FIRSTHDR(&message);
while !header.is_null() {
if (*header).cmsg_level == libc::SOL_SOCKET && (*header).cmsg_type == libc::SCM_RIGHTS {
let count = ((*header).cmsg_len as usize - libc::CMSG_LEN(0) as usize)
/ std::mem::size_of::<i32>();
for index in 0..count {
let fd =
std::ptr::read_unaligned(libc::CMSG_DATA(header).cast::<i32>().add(index));
received.push(OwnedFd::from_raw_fd(fd));
}
}
header = libc::CMSG_NXTHDR(&message, header);
}
}
if n == 1
&& marker == b'F'
&& message.msg_flags & libc::MSG_CTRUNC == 0
&& received.len() == expected
{
return Ok(received);
}
if n == 1 && marker == b'{' && received.is_empty() {
let mut bytes = vec![marker];
socket.take(LIMIT as u64).read_to_end(&mut bytes)?;
if bytes.len() <= LIMIT {
if let Ok(reply) = serde_json::from_slice::<Response>(&bytes) {
if let Some(error) = reply.error {
return Err(Error(error));
}
}
}
}
Err(Error("Invalid managed program handshake".into()))
}
pub fn start(
name: &str,
arguments: Vec<String>,
terminal: bool,
working: String,
term: String,
descriptors: &[i32],
) -> Result<(UnixStream, OwnedFd)> {
let mut socket = UnixStream::connect(SOCKET)?;
socket.set_read_timeout(Some(Duration::from_secs(120)))?;
socket.set_write_timeout(Some(Duration::from_secs(5)))?;
let mut request = serde_json::to_vec(&Request::Program {
name: name.into(),
arguments,
terminal,
working,
term,
})
.map_err(|e| Error(e.to_string()))?;
if request.len() >= LIMIT {
return Err(Error("Program request is too long".into()));
}
request.push(b'\n');
socket.write_all(&request)?;
let mut ready = [0u8];
socket.read_exact(&mut ready)?;
if ready != [b'R'] {
let mut bytes = ready.to_vec();
(&mut socket).take(LIMIT as u64).read_to_end(&mut bytes)?;
let reply: Response = serde_json::from_slice(&bytes)
.map_err(|_| Error("Invalid program handshake".into()))?;
return Err(Error(
reply
.error
.unwrap_or_else(|| "Program launch failed".into()),
));
}
send_fds(&socket, descriptors)?;
let mut received = receive_fds(&mut socket, 1)?;
socket.set_read_timeout(None)?;
socket.set_nonblocking(true)?;
Ok((socket, received.pop().unwrap()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn descriptor_transfer_is_cloexec_and_owned() {
let (sender, mut receiver) = UnixStream::pair().unwrap();
let original = File::open("/dev/null").unwrap();
send_fds(&sender, &[original.as_raw_fd()]).unwrap();
let received = receive_fds(&mut receiver, 1).unwrap().pop().unwrap();
assert_ne!(received.as_raw_fd(), original.as_raw_fd());
assert_ne!(
unsafe { libc::fcntl(received.as_raw_fd(), libc::F_GETFD) } & libc::FD_CLOEXEC,
0
);
drop(original);
assert!(File::from(received).metadata().is_ok());
}
}
+1
View File
@@ -1,6 +1,7 @@
//! Shared data contracts. Cartridge contents are data, never startup commands.
pub mod boot;
pub mod control;
pub mod launch;
pub mod machine;
pub mod manifest;
pub mod software;
+60 -4
View File
@@ -1,4 +1,4 @@
//! Software metadata is descriptive. Bundles never contain privileged build hooks.
//! Software metadata describes immutable installed trees or legacy archives.
use crate::{Error, Result, manifest::identifier};
use serde::{Deserialize, Serialize};
@@ -21,6 +21,13 @@ pub struct Software {
pub architecture: String,
/// GPT partition number, starting at 2 after FDS_METADATA.
pub partition: u8,
/// New media executes installed Void package files directly from EROFS.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub installed: bool,
/// Exact XBPS package versions included in this software tree.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub packages: Vec<String>,
#[serde(default, skip_serializing_if = "is_zero")]
pub archive_bytes: u64,
pub unpacked_bytes: u64,
pub entries: u32,
@@ -29,6 +36,9 @@ pub struct Software {
pub commands: BTreeMap<String, String>,
}
impl Software {
pub fn root_path(&self) -> String {
format!("programs/{}", self.id)
}
pub fn archive_path(&self) -> String {
format!("bundles/{}.tar.xz", self.id)
}
@@ -41,7 +51,14 @@ impl Software {
|| !display(&self.version, 32)
|| !matches!(self.architecture.as_str(), "aarch64" | "any")
|| !(2..=33).contains(&self.partition)
|| !(1..=MAX_ARCHIVE).contains(&self.archive_bytes)
|| if self.installed {
self.archive_bytes != 0
|| self.packages.is_empty()
|| self.packages.len() > 512
|| self.packages.iter().any(|p| !xbps_identifier(p))
} else {
!(1..=MAX_ARCHIVE).contains(&self.archive_bytes) || !self.packages.is_empty()
}
|| self.unpacked_bytes > MAX_UNPACKED
|| !(1..=MAX_ENTRIES).contains(&self.entries)
|| self.sha256.len() != 64
@@ -81,15 +98,20 @@ impl Catalogue {
Ok(value)
}
pub fn validate(&self) -> Result<()> {
if self.format != 1 || self.software.is_empty() || self.software.len() > 128 {
if !matches!(self.format, 1 | 2) || self.software.is_empty() || self.software.len() > 128 {
return Err(Error(
"Software catalogue requires format 1 and 1..128 software entries".into(),
"Software catalogue requires format 1 or 2 and 1..128 software entries".into(),
));
}
let mut ids = BTreeSet::new();
let mut partitions = BTreeSet::new();
for software in &self.software {
software.validate()?;
if software.installed != (self.format == 2) {
return Err(Error(
"Catalogue format disagrees with software storage layout".into(),
));
}
if !ids.insert(&software.id) {
return Err(Error("Duplicate software id".into()));
}
@@ -118,6 +140,18 @@ impl Catalogue {
Ok(result)
}
}
fn is_zero(value: &u64) -> bool {
*value == 0
}
pub fn xbps_identifier(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 256
&& value.as_bytes()[0].is_ascii_alphanumeric()
&& value
.bytes()
.all(|c| c.is_ascii_alphanumeric() || b"._-+~".contains(&c))
&& !value.contains("..")
}
pub fn relative(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 1024
@@ -141,6 +175,8 @@ mod tests {
version: "1".into(),
architecture: "aarch64".into(),
partition,
installed: false,
packages: Vec::new(),
archive_bytes: 100,
unpacked_bytes: 200,
entries: 1,
@@ -181,4 +217,24 @@ mod tests {
}
assert!(relative("share/document with spaces.txt"));
}
#[test]
fn installed_trees_use_format_two_and_record_xbps_versions() {
let mut entry = software("one", 2);
entry.installed = true;
entry.archive_bytes = 0;
entry.packages = vec!["WindowMaker-0.96.0_1".into(), "libstdc++-14.2.1_1".into()];
let mut catalogue = Catalogue {
format: 2,
software: vec![entry],
};
assert_eq!(
Catalogue::parse(&catalogue.to_toml().unwrap()).unwrap(),
catalogue
);
catalogue.format = 1;
assert!(catalogue.validate().is_err());
catalogue.format = 2;
catalogue.software[0].packages.clear();
assert!(catalogue.validate().is_err());
}
}

Some files were not shown because too many files have changed in this diff Show More