Compare commits

...
3 Commits
Author SHA1 Message Date
felis 0d42dc5acc update docs 2026-09-24 13:03:52 +08:00
felis f5d9f96409 Package workstation tools, reuse emulator sessions, and derive build versions 2026-09-23 03:45:48 +08:00
felis 6bfcce8070 FDS/OS 1.0 fixes 2026-09-23 03:12:45 +08:00
102 changed files with 1850 additions and 181 deletions
+2 -1
View File
@@ -23,7 +23,8 @@ claim Pi boot or physical power-cycle recovery is verified.
commands and options with typed parsers; do not hand-parse argument vectors.
- No systemd, runit, BusyBox, shell PID1, boot-time cache compilation, or sleeps
to hide races in the target system.
- Keep upstream Void files unchanged; pin its submodule and use `packages/` overlays.
- Keep `vendor/void-packages` clean and pinned. Use `.host/void-packages` for
generated build state; keep authoritative FDS overlays in `packages/`.
- Explain every new dependency and keep documentation accurate.
- Run `make bootstrap`, `make smoke-test`, and `make check` for M0 build changes.
Report skips and missing hardware honestly. Do not invent boot benchmarks.
Generated
+13 -13
View File
@@ -236,7 +236,7 @@ dependencies = [
[[package]]
name = "dasungd"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"anyhow",
"clap",
@@ -320,7 +320,7 @@ dependencies = [
[[package]]
name = "fds-boottrace"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-common",
@@ -330,7 +330,7 @@ dependencies = [
[[package]]
name = "fds-burn"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-common",
@@ -342,7 +342,7 @@ dependencies = [
[[package]]
name = "fds-cartridged"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-burn",
@@ -355,7 +355,7 @@ dependencies = [
[[package]]
name = "fds-cli"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-burn",
@@ -366,7 +366,7 @@ dependencies = [
[[package]]
name = "fds-common"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"libc",
"serde",
@@ -376,7 +376,7 @@ dependencies = [
[[package]]
name = "fds-control"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-common",
@@ -387,7 +387,7 @@ dependencies = [
[[package]]
name = "fds-release"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"ed25519-dalek",
@@ -401,11 +401,11 @@ dependencies = [
[[package]]
name = "fds-smoketest"
version = "0.1.0"
version = "1.0.0"
[[package]]
name = "fds-software"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"fds-common",
"libc",
@@ -417,7 +417,7 @@ dependencies = [
[[package]]
name = "fds-stage0"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-common",
@@ -427,7 +427,7 @@ dependencies = [
[[package]]
name = "fds-test-helpers"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"libc",
@@ -435,7 +435,7 @@ dependencies = [
[[package]]
name = "fds-workstation"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"clap",
"fds-burn",
+4
View File
@@ -11,6 +11,10 @@ members = [
"tests/helpers",
]
[workspace.package]
# Cargo requires three numeric components; public build versions come from Git.
version = "1.0.0"
[workspace.dependencies]
clap = { version = "4", features = ["derive"] }
+16 -4
View File
@@ -10,7 +10,9 @@ help:
'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 Build the Arch fds-tools package' \
'make workstation-install Install the Arch fds-tools package with pacman' \
'make workstation-binaries Build native binaries on generic Linux' \
'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' \
'make bootstrap Prepare Arch x86_64 host and Void build container' \
@@ -91,6 +93,7 @@ smoke-test:
./tools/smoke-test 2>&1 | tee out/logs/smoke-test.log
check:
python3 tests/integration/version-checks.py
./tests/integration/m0-checks
cargo fmt --all -- --check
$(MAKE) clean-test
@@ -241,14 +244,23 @@ release-contract-test:
@mkdir -p out/logs
python3 tests/integration/m12-release-build.py 2>&1 | tee out/logs/release-contract-checks.log
.PHONY: workstation workstation-test emulator-test
.PHONY: workstation workstation-install workstation-binaries workstation-package-test workstation-test emulator-test
workstation:
@mkdir -p out/logs
./tools/build-workstation 2>&1 | tee out/logs/workstation-build.log
workstation-test: workstation
workstation-package-test: workstation
python3 tests/integration/arch-package.py
workstation-install: workstation
./tools/install-workstation
workstation-binaries:
./tools/build-workstation-binaries
workstation-test: workstation-binaries
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 --xbps-tool-runner tools/in-void --xbps-bin "$(CURDIR)/.host/xbps/usr/bin" 2>&1 | tee out/logs/workstation-images.log
emulator-test: workstation
emulator-test: workstation-binaries
python3 tests/integration/workstation-emulator.py --cli out/workstation/fds-emulator --qemu-runner tools/in-void 2>&1 | tee out/logs/workstation-emulator.log
+7 -2
View File
@@ -23,7 +23,8 @@ the desktop or any ENVIRONMENT cartridge.
## Choose a starting point
- **Build and try FDS:** follow [Build and start FDS](docs/getting-started.md).
- **Run on your Raspberry Pi 5:** follow [the real-hardware setup guide](docs/hardware-setup.md), including HDMI-plus-USB Dasung wiring, disk installation and first boot.
- **Build and try the emulator:** 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).
@@ -55,7 +56,9 @@ Run these from the repository root:
| Command | Result |
| --- | --- |
| `make workstation` | Native Linux `fds-cartridge` and `fds-emulator` executables |
| `make workstation` | Arch `fds-tools` package containing both workstation commands |
| `make workstation-install` | Install that package using pacman |
| `make workstation-binaries` | Native commands for other Linux distributions |
| `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 |
@@ -71,3 +74,5 @@ 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.
Build identities come from Git release tags. See [versioning](docs/developer/versioning.md) for release, development and source-export behavior.
+1
View File
@@ -8,6 +8,7 @@ operating guides run at the FDS console unless stated otherwise.
| Guide | What you will learn |
| --- | --- |
| [Overview](../README.md) | How the computer, internal storage and cartridges fit together |
| [Run on a Raspberry Pi 5](hardware-setup.md) | Wire HDMI/USB/power, prepare NVMe and SYSTEM, reach the first console, calibrate bays and diagnose hardware |
| [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 |
+2 -1
View File
@@ -44,7 +44,8 @@ Unknown names are left alone, including ad hoc diagnostic experiments.
- 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.
writable Void checkout/build container under `.host/void-packages/`, 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
+150 -2
View File
@@ -4,14 +4,42 @@ 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.
For installation, disk preparation and first power-on, start with
[Run FDS on a Raspberry Pi 5](hardware-setup.md).
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.
## HDMI plus USB-C on the Pi
Use the Pi's **micro-HDMI output** for video to the monitor's HDMI input. Use a
separate **USB-A-to-USB-C data cable** from a Pi host port to the monitor for
controller access, and power the monitor with its supplied DC adapter. The
Pi's own USB-C connector is its power input; it does not output USB-C video.
See the [wiring table](hardware-setup.md#2-wire-hdmi-video-usb-control-and-power-separately),
[official Pi display guidance](https://www.raspberrypi.com/documentation/computers/getting-started.html#display),
and [DASUNG's power guidance](https://shop.dasung.com/pages/q-a).
HDMI video and USB controller access are separate checks. The daemon talks USB
regardless of the video cable, but the HDMI-plus-USB arrangement still needs to
expose the expected UART and companion bridge on this monitor. HDMI alone does
not carry the daemon's control protocol. A charge-only cable or a USB-C lead
connected only to a charger cannot provide host USB control.
The image currently supplies a firmware EDID override for 3200 × 2400 at about
37 Hz, recorded on the original workstation. DASUNG says some HDMI hosts need
27 Hz instead; that does not establish which mode the Pi and this cable will
accept. The forced profile can prevent using the monitor's native HDMI mode
list. Use the diagnostic procedure below if needed; neither Pi HDMI compatibility
nor physical power-cycle recovery is recorded as passed yet.
[Manufacturer HDMI note](https://shop.dasung.com/pages/dasung-paperlike-13k-the-worlds-first-13-3-inch-37hz-3k-e-ink-monitor).
## Inspect the controller
The controller's socket is restricted to root. From a root maintenance console
on FDS:
The controller's socket is restricted to root. Use the local `RECOVERY#` console
described in [Enter recovery](recovery.md#enter-recovery); the ordinary `FDS>`
account cannot access it. On FDS recovery:
```sh
dasungd status
@@ -40,6 +68,126 @@ 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.
At `RECOVERY#`, start with:
```sh
lsusb
lsusb -t
dasungd status
dasungd query
tail -80 /run/log/dasungd/current
```
The existing profile expects UART `1a86:7523` alongside SPI bridge `1a86:5512`
under the same hub. If neither appears, check monitor power, its USB data port,
the cable and the Pi host connection. If only one appears, preserve the USB tree
and log: the cable/input combination has not yet satisfied the current profile.
Do not disable companion checks or match every CH340 device to hide the failure.
`status` separates `connected` from `responsive` and includes `usb_error`,
`display_error`, `displays`, `rx_frames` and `last_rx_ms_ago`. `query` queues
requests; run `status` again after replies arrive. Keepalive packets being sent
are not themselves evidence that the monitor answered.
Capture the actual DRM connector names and EDID at `RECOVERY#`:
```sh
mkdir /tmp/dasung-hdmi
cat /proc/cmdline > /tmp/dasung-hdmi/cmdline.txt
for fds_connector in /sys/class/drm/card*-*; do
[ -f "$fds_connector/status" ] || continue
fds_connector_name=${fds_connector##*/}
printf '%s: ' "$fds_connector_name"
cat "$fds_connector/status"
cat "$fds_connector/modes"
cp "$fds_connector/status" "/tmp/dasung-hdmi/$fds_connector_name.status"
cp "$fds_connector/modes" "/tmp/dasung-hdmi/$fds_connector_name.modes"
cp "$fds_connector/edid" "/tmp/dasung-hdmi/$fds_connector_name.edid"
done
```
Use a new directory for each capture and save it before shutdown using
[machine diagnostics](internal-storage.md#save-and-retrieve-diagnostics).
An empty EDID on a disconnected connector is expected. The `modes` file lists
available resolution names, not proof of the active refresh rate or picture
quality. With `drm.edid_firmware` active, the EDID you read may be the supplied
override; capture again without it to learn what HDMI actually advertises.
The configured DRM serial is `L56051794302`. Native HDMI EDID might identify the
display differently from the recorded USB-C profile. With `require_display=true`,
an identity mismatch can leave USB control disconnected even when HDMI video
works. Retain the native EDID, USB topology and errors for a deliberate profile
update; do not silently remove identity checks. Config changes must ultimately
be rebuilt into both early boot and base/recovery images. The read-only
`/etc/dasungd.toml` is not a persistent settings editor.
## Try the monitor's own HDMI EDID
Use this when the stock forced timing gives a blank or unstable picture, or when
temporarily connecting a different HDMI display. The override currently applies
to connected DRM outputs without a connector qualifier, so attaching another
screen while retaining it is not an independent native-mode test.
Follow the offline edit procedure below. In the disk's **single-line**
`cmdline.txt`, remove only this token for the EDID experiment:
```text
drm.edid_firmware=edid/dasung-paperlike13k-37hz.bin
```
For verbose diagnosis with a USB keyboard and display console, a complete
command line based on the current production template is:
```text
console=tty1 rdinit=/init ro loglevel=7 vt.global_cursor_default=0 fds.boot=normal fds.debug=1
```
For the serial boot mode, retain `console=ttyAMA10,115200` after `console=tty1`
instead. Use `fds.boot=recovery` in place of `fds.boot=normal` when intentionally
booting recovery; never append a second conflicting `fds.boot` option. Do not add
`fds.emulator=1` on real hardware or replace stage0 with a Raspberry Pi OS
`root=…` command line.
Reboot and repeat the DRM/USB capture. Removing the override allows the monitor's
HDMI EDID to supply modes; it does **not** guarantee 27 Hz or fix missing USB
control. Record any change in picture and controller status. Restore the saved
command line to return to the shipped profile. Keep both captures so a future
HDMI profile change can be based on observed timings and identity.
### Edit the BOOT command line offline
Shut FDS down and disconnect Pi power. Connect NVMe to the Linux workstation, or
boot the separate maintenance SD and access the inactive NVMe there. Identify
the same disk by model/serial used when flashing it. Mount only its first
partition, labeled `FDS_BOOT`; FDS does not normally mount it at
`/boot/firmware` as Raspberry Pi OS does.
On that **workstation or maintenance OS**, replacing the partition path below:
```sh
lsblk -p -o NAME,MODEL,SERIAL,SIZE,TYPE,PARTLABEL,MOUNTPOINTS
sudo mkdir -p /mnt/fds-boot
sudo mount -o nosuid,nodev,noexec \
/dev/disk/by-id/REPLACE_WITH_VERIFIED_NVME-part1 /mnt/fds-boot
sudo cp -n /mnt/fds-boot/cmdline.txt /mnt/fds-boot/cmdline.before-hardware.txt
sudoedit /mnt/fds-boot/cmdline.txt
cat /mnt/fds-boot/cmdline.txt
sudo umount /mnt/fds-boot
```
Check the identity before the mount and stop on any failed step. If the partition
was automounted, unmount that instance first. Keep `/mnt/fds-boot` dedicated to
this operation. The no-clobber copy retains an existing backup; if repeating the
experiment, inspect that backup and skip the copy step if it already exists.
Do not assume an older backup contains your latest settings. Keep `cmdline.txt`
on one line and record its final contents with the
test session. Safely disconnect the enclosure before moving NVMe back to the Pi.
To undo the edit, mount the same verified partition again, restore
`cmdline.before-hardware.txt` over `cmdline.txt`, and unmount before rebooting.
These edits change the flashed copy only; they do not change the source images
or their recorded hashes. Reflashing restores the image's original command line.
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
+4
View File
@@ -10,6 +10,8 @@ the inputs named in those reports; a changed source tree needs fresh checks.
## Design and build references
- [Clean upstream and writable Void workspace](void-workspace.md)
- [Master plan](master-plan.md)
- [Build host and dependencies](build-host.md)
- [Development workflow](development.md)
@@ -78,3 +80,5 @@ cartridge creation: new payloads contain installed Void package trees.
- [troubleshooting](troubleshooting.md)
- [workstation-tooling-plan](workstation-tooling-plan.md)
- [workstation](workstation.md)
- [Git-derived build versions and Arch packaging](versioning.md)
+3 -2
View File
@@ -13,8 +13,9 @@ are never used as cartridge identities.
The software boot path has passed on an ARM virtual machine using the actual
FDS Pi kernel. That does not verify Pi firmware, NVMe, RP1, display output, USB
power or physical boot speed. Those checks are deferred until the machine is
assembled. M5 now provides an ordinary-user `FDS>` console with a temporary home
power or physical boot speed. Those checks still require physical testing; follow
the [real-hardware setup guide](../hardware-setup.md). M5 now provides an
ordinary-user `FDS>` console with a temporary home
and measured boot events. Password login remains locked; this is an intentional
local single-user session. The M2 test VM separately offers a temporary root
development shell. See [native init](init.md) and [performance reports](performance.md).
+20 -7
View File
@@ -46,7 +46,10 @@ indexing also explicitly sets `XBPS_ARCH=aarch64`.
## Generic Linux workstation tools and desktop additions
`make workstation` builds native `fds-cartridge` and `fds-emulator`. Runtime
`make workstation` builds the Arch `fds-tools` package; `make workstation-install`
installs it with pacman. `make workstation-binaries` builds native `fds-cartridge`
and `fds-emulator` on generic Linux. Python 3 and Git resolve build versions;
Dasung builds install these tools in the project-local Void container as well. 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
@@ -112,7 +115,7 @@ equivalent of:
```sh
export PATH="$PWD/.host/xbps/usr/bin:$PATH"
export XBPS_ARCH=x86_64
cd vendor/void-packages
cd .host/void-packages
./xbps-src -A x86_64 binary-bootstrap
```
@@ -131,7 +134,7 @@ To reproduce the two builds individually from the repository root:
```
The latter command sets the local XBPS PATH, checks the Void pin, and executes
this command within the submodule:
this command within the writable build checkout:
```sh
./xbps-src -a aarch64 pkg hello
@@ -186,9 +189,11 @@ 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.
- `vendor/void-packages/`: clean, pinned upstream submodule; no generated overlays.
- `.host/void-packages/`: independent local build checkout at the same pin.
- `.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.
- `.host/void-packages/masterdir-x86_64/`: glibc build container.
- `.host/void-packages/hostdir/`: download caches, cross packages, and build output.
- `out/packages/`: exported aarch64 test package and its repository index.
- `out/manifests/void-build-packages.txt`: packages remaining installed after the
build. Cross toolchains and temporary dependencies removed by xbps-src remain
@@ -197,12 +202,20 @@ GiB per working tree, beyond the initial M0 smoke test's requirements.
- `out/manifests/artifacts.sha256`: validation artifact digests.
- `out/logs/`: actual command logs, including logs from failed attempts.
Place FDS package overlays at `packages/fds-*/template`. `tools/prepare-void`
copies only directories containing a template and refuses to replace tracked
Place FDS package overlays at `packages/fds-*/template`. `tools/prepare-void-workspace`
clones the pinned upstream locally without shared Git objects or network access.
`tools/prepare-void` copies directories containing a template into that writable checkout and refuses to replace tracked
upstream packages. These copies are visible inside the build container. Base,
base-files, init, CLI, kernel and Dasung overlays are active; future directories
without templates remain inactive.
Bootstrap migrates matching old generated copies, configuration, `hostdir` and
`masterdir-*` out of the submodule using renames, preserving cached downloads
and container files. Unknown files, staged edits, changed copies or destination
conflicts stop migration for explicit reconciliation. Python 3 is used for
checked migration and locking; it is already a documented FDS host prerequisite.
The workstation builder also rejects submodules as writable build targets.
If an overlay changes and preparation reports a stale copy, inspect and preserve
any unique edits before moving the specific generated copy out of `srcpkgs/` and
retrying. Modified upstream tracked files cause immediate failure. Conflicting
+2
View File
@@ -105,6 +105,8 @@ No world-writable USB rule, systemd unit, or runit service is installed.
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.
For the current HDMI video plus USB control procedure, including separate monitor
power and a reversible native-EDID experiment, use the [display guide](../dasung.md).
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
+4 -1
View File
@@ -453,7 +453,10 @@ Record:
VOID_PACKAGES_COMMIT
```
Inject FDS packages through an overlay.
Keep this submodule clean. Prepare a writable checkout at `.host/void-packages`
from the same pinned commit. Inject FDS packages from `packages/` into that
build checkout; keep generated sources, caches and build containers outside
the submodule.
Required relationship:
+1 -1
View File
@@ -141,7 +141,7 @@ The JSON document has exactly these fields:
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
accepts a bounded numeric-leading release/development 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,
+7
View File
@@ -16,6 +16,13 @@ Normal builds still use Void's signed rolling repositories. A frozen build uses
the selected package files and build environment from one explicit snapshot.
It does not silently consult a newer repository if an input is missing.
Current builds keep the upstream reference in `vendor/void-packages/` and use
`.host/void-packages/` for overlays and cached build state. New snapshots and
restores use this separate layout. Restoring an older
snapshot retains its historical layout and saved build scripts; its files and
acceptance are not rewritten. See [the workspace change](void-workspace.md) for
fresh validation of this migration.
## What is frozen
`tools/frozen-inputs create NEW_DIRECTORY` records:
+2 -1
View File
@@ -10,7 +10,8 @@ entry point. Both executables cross-build as static-musl AArch64 programs. The
normal OS continues to use glibc; no target musl runtime package is added.
For native Linux software packaging and QEMU hotplug, use `make workstation`
and the [workstation guide](workstation.md). Those Clap tools are host binaries;
on Arch, or `make workstation-binaries` on other Linux distributions, and follow
the [current workstation guide](../workstation.md). Those Clap tools are host binaries;
`make tooling` below builds the static ARM runtime tools.
## Build and test
+3 -3
View File
@@ -94,14 +94,14 @@ normal. Never use a broad reset or clean command without checking what it remove
For `etc/conf differs from config/xbps-src.conf`, compare the two:
```sh
diff -u vendor/void-packages/etc/conf config/xbps-src.conf
diff -u .host/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-*`
For `Stale overlay`, compare the named `.host/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
@@ -121,7 +121,7 @@ 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
du -sh out/downloads .host/void-packages/hostdir
df -h .
```
+78
View File
@@ -0,0 +1,78 @@
# Build versions and the workstation package
`tools/version` is the authoritative FDS build-version resolver. It reads this
repository's Git history, never the Void submodule's history. All FDS Rust
commands, base XBPS packages, login/boot banners and `/etc/os-release` use it.
SYSTEM cartridge metadata copies the version already embedded in its rootfs,
so wrapping an older rootfs does not mislabel it as the current source version.
Release tags are numeric `MAJOR.MINOR` or `MAJOR.MINOR.PATCH`, optionally prefixed
with `v`. Unrelated tags are ignored. The nearest reachable release tag is used:
| Source state | Example build version |
| --- | --- |
| Exactly release tag `1.0`, clean checkout | `1.0` |
| Two commits after `1.0` | `1.0.r2.g123456789abc` |
| No reachable release tag | `0.dev.g123456789abc` |
| Uncommitted changes to tracked files | Append `.dirty` |
Untracked files do not affect the version, matching Git’s usual dirty-checkout
convention. Add new release sources to Git before tagging.
Versions are bounded to 32 letters, digits and periods, starting with a digit,
for the cartridge metadata, XBPS and Arch package formats. A shallow clone
without a reachable release tag gets a development version; fetch the intended
release tag/history before building a release. No value is silently invented
when both Git identity and an exported identity are missing.
Each Rust crate uses the shared build script, including direct Cargo builds.
Cargo notices tag/ref changes, index changes and source edits, avoiding an old
version string in a cached binary. Cargo's workspace metadata uses the SemVer
form `1.0.0`; the public build version comes from Git. Protocol/schema versions,
application recipe versions, the Linux kernel version and third-party package
versions are independent and remain unchanged.
`tools/prepare-void` writes the resolved value to the writable build checkout's
`fds-version` file. FDS-owned XBPS templates read it; base-files substitutes it
into `/usr/lib/os-release` and `/etc/issue`. `/etc/os-release` links to that
installed identity file. Runtime checks compare installed command versions
against `VERSION_ID`. Superseded FDS packages are archived under
`out/package-history/` so the active repository cannot accidentally select an
older development package whose version sorts above a release.
Frozen source exports save a validated `FDS_VERSION` file before inventorying
and hashing their contents. Their builds use that saved value without Git or
network access. Legacy 0.1.0 snapshots retain their original scripts and layout;
the historical release stays unchanged. Release verification accepts supported
version strings independently of the verifier's own version, so newer tools can
still authenticate an older signed release using the supplied trust key.
## Build and install on Arch
`packaging/arch/PKGBUILD` packages the local checkout. `make workstation` invokes
makepkg, building both binaries in the existing Cargo cache and recording the
result in `out/workstation/package-path.txt`. It builds without implicit package
installation; required build commands are checked directly. Runtime feature
packages are declared as optional because project-local runners are supported.
`make workstation-install` explicitly invokes pacman to install the exact
recorded package. Generic Linux users use `make workstation-binaries`.
The package includes binaries, the user/developer manuals, source examples,
MIT licensing and dependency notices. It has no service, hook or installation
script. The workflow follows the official [PKGBUILD specification](https://man.archlinux.org/man/PKGBUILD.5.en)
and [makepkg interface](https://man.archlinux.org/man/makepkg.8.en).
## Verification
`make check` includes release/no-tag/dirty/export and real Cargo cache-invalidation
checks. `make workstation-test` exercises the installed-tree cartridge creator;
`make emulator-test` covers stopping and starting the same directory, preserved
logs/overlays/mappings, and rejecting a restart of a live VM.
`make workstation-package-test` installs the real Arch package with pacman in a
disposable root and executes both installed commands. After building matching
images, `python3 tests/integration/version-images.py` reads the identity and
metadata directly from the GPT/EROFS image and executes its twelve FDS commands
under ARM emulation to compare their versions. Image acceptance
requires a new CLI rootfs and SYSTEM image: inspect their identity, execute the
ARM tools and run the VM checks. A VM result does not establish physical Pi or
Dasung power-cycle behavior.
+77
View File
@@ -0,0 +1,77 @@
# Clean upstream and writable Void workspace
FDS keeps upstream source separate from its build state:
| Path | Ownership and purpose |
| --- | --- |
| `vendor/void-packages/` | Clean upstream Git submodule at `VOID_PACKAGES_COMMIT` |
| `packages/` | Authoritative FDS base-package sources, including Dasung |
| `examples/software/*/void/` | Authoritative example PROGRAM package sources |
| `.host/void-packages/` | Generated writable checkout, package copies, local configuration, caches and build container |
`make bootstrap` creates the build checkout locally at the pinned commit. It
copies Git objects independently; there is no network fetch for this step, no
Git alternates dependency and no hardlinks into the upstream source. Normal
package bootstrap still uses its configured repositories.
`tools/prepare-void` copies FDS package overlays into the writable checkout and
retains the existing checks against replacing upstream packages or overwriting
changed generated copies. Workstation builds default to the same checkout.
Explicitly selecting a Git submodule with `--void-packages` is rejected before
source files or build state can be written there.
The workspace helper migrates old, matching generated files and build caches by
renaming them. It compares every file, mode and symlink in each known source
copy first. Unknown upstream files, staged changes, independent edits and
conflicting destinations stop migration. Run builds and migration sequentially.
The workspace lock prevents two preparations from changing the layout together.
Python is already a build-host prerequisite; this introduces no new dependency.
For this checkout, the 58 staged additions matched the main-repository sources.
Their local commit `47580322e801ef195c9bf279fd1fe4d8a1f723e9` and both staged
patches were saved under `.host/backups/void-overlays-20260922-134611/` before
unstaging those copies. The parent gitlink was restored to upstream commit
`02a3cbc132c3c4a3a9d59e9b98f517af5dd11cd1`, matching the declared pin. All eleven
package directories, `hostdir`, `masterdir-x86_64` and `etc/conf` were moved into
the build checkout. No upstream tracked source was changed.
An explicit future pin update advances the writable checkout without replacing
its caches. Git refuses updates that would overwrite local files. Continue to
record the parent gitlink and `VOID_PACKAGES_COMMIT` together.
Frozen-input creation uses the separate build checkout. New restores recreate
that layout. Older saved projects retain their
original layout and scripts; the frozen 0.1.0 release is untouched. Restore
compatibility tests do not imply a new reproducibility or physical-hardware claim.
## Validation
Fresh checks for this change passed:
- `make bootstrap`, `make smoke-test`, and `make check`: migrated container use,
ARM/glibc package compilation, static-musl compilation, clean upstream state,
exact overlay/cache preservation, shallow pin updates, rejection of independent
edits and source conflicts, and cleanup protections.
- `make workstation-test`: `out/workstation-images.ckkodex6`; real Void source
builds from `.host/void-packages`, image validation and confirmed file write/readback.
Selecting the upstream submodule as a build target is rejected without writes.
- `make emulator-test`: `out/emu-test.op5azhlm`; full twelve-bay PROGRAM/DATA,
foreground terminal, eject/unplug, legacy reading and service-restart checks.
- `make release-contract-test`: `out/m12-release-contracts.wmsfrcl0`; both separate
and legacy frozen-layout restores, retained source caches, local offline
repositories and existing tamper/release-comparison checks.
- Documentation links, formatting, Python syntax and a final empty upstream Git
status, including ignored files, were checked.
Logs are in `out/logs/void-workspace/`; source and log hashes, pins, native binary
hashes and fixture results are recorded in `out/manifests/void-workspace-acceptance.json`.
The optional host-native ARM execution shortcut was skipped in smoke-test. ARM
execution was exercised by the full emulator suite. Physical hardware was not used.
The first emulator attempt (`out/emu-test.463emq8u`) failed on an empty cartridge
IPC response during initial discovery in its admin VM. The complete rerun passed
without target-system changes. SYSTEM, kernel and initramfs hashes matched the
previous accepted inputs. The cause of that first response failure was not
established; its command/serial logs are retained. Future failures now collect
live service diagnostics before VM shutdown, without retrying failed commands
or relaxing their acceptance conditions.
@@ -22,3 +22,8 @@ unchanged. Build new matching artifacts and record their hashes and source input
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).
The Arch distribution package is `fds-tools`, built by `make workstation` and
installed by `make workstation-install`. `make workstation-binaries` preserves
the generic Linux build. Stopped emulator directories support a fresh boot while
retaining logs, DATA overlays and archived cartridge mappings.
+5 -1
View File
@@ -3,7 +3,11 @@
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
This page records the earlier archive-based software prototype. For installed
Void software trees, the Arch `fds-tools` package, and reusable emulator sessions,
follow the [current walkthrough](../workstation.md).
This historical 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.
+10 -2
View File
@@ -6,6 +6,9 @@ cartridges on another Linux distribution, use the [workstation guide](workstatio
A prepared set of kernel, initramfs and SYSTEM images can be run on a Linux
workstation without building the OS locally.
For the physical computer, follow [Run FDS on a Raspberry Pi 5](hardware-setup.md).
It covers HDMI-plus-USB monitor wiring, both disks, EEPROM, first boot and recovery.
## Prepare the workstation
Use a normal user account, a Git checkout with its submodule metadata, and a
@@ -34,6 +37,9 @@ make check
```
Bootstrap prepares pinned Rust and XBPS tools and the Void build container.
The upstream reference stays clean in `vendor/void-packages/`. Generated package
copies, build configuration and caches live in `.host/void-packages/`; edit
FDS package sources in `packages/`, not in either generated checkout.
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.
@@ -60,7 +66,7 @@ inputs needed by the emulator:
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.
see [real-hardware setup](hardware-setup.md) when preparing a physical machine.
## Start the emulator
@@ -73,7 +79,9 @@ out/workstation/fds-emulator --session out/my-emulator console
```
If QEMU is installed directly on the workstation, omit `--qemu-runner`.
The session directory must be new. At the `FDS>` prompt:
Use a new session directory for the first run; after `stop`, you can `start` the
same session again. Logs and DATA overlays are retained, and bays start empty.
At the `FDS>` prompt:
```sh
fds info
+392
View File
@@ -0,0 +1,392 @@
# Run FDS on a Raspberry Pi 5
[User manual](README.md) · [Dasung wiring and diagnostics](dasung.md) · [Recovery](recovery.md)
This guide takes a partly assembled Pi from prepared disks to its first FDS
console, then adds monitor control and physical bay numbering. Start with one
SYSTEM cartridge; the twelve-bay assembly and a desktop are not prerequisites.
These are instructions for physical testing, not a record of a successful Pi
boot. Existing QEMU results do not establish HDMI compatibility, USB power
stability, monitor recovery or physical boot times.
Follow steps 1–6 to reach the first console. Steps 7–9 check the monitor, install
bay mappings and save results. If the screen stays blank, jump to
[first-boot troubleshooting](#when-the-first-boot-fails).
Commands marked **workstation** run on the Linux build computer, **maintenance
OS** means a separate Raspberry Pi OS rescue installation, and **FDS** means the
console on the Pi. FDS does not start SSH or networking automatically.
## 1. Prepare the minimum hardware
You need:
- A Raspberry Pi 5 with cooling and a suitable USB-C power supply.
- A PCIe NVMe adapter/HAT and NVMe drive for internal boot and recovery storage.
- One USB storage device for SYSTEM, a USB keyboard, and the Paperlike 13K.
- HDMI video cabling, a USB-A-to-USB-C **data** cable for the monitor controller,
and the monitor's supplied power adapter.
- A Linux workstation and an NVMe enclosure/adapter for writing the disks.
A separate maintenance microSD card is useful for EEPROM and display diagnosis.
The current FDS installation uses **two disks**:
| Disk | Contents | When it is needed |
| --- | --- | --- |
| Internal PCIe NVMe | `FDS_BOOT`, `FDS_RECOVERY`, `FDS_INTERNAL` | Every FDS boot; also recovery without SYSTEM |
| USB SYSTEM cartridge | `FDS_SYSTEM` | Normal FDS operation |
PROGRAM, DATA and ENVIRONMENT cartridges can wait until the console works.
If only microSD is connected today, use it for Raspberry Pi OS maintenance and
cable checks. This guide's complete FDS installation requires NVMe: persistent
machine settings deliberately accept PCIe NVMe only. Writing the internal image
to microSD or leaving NVMe in a USB enclosure does not provide that supported
layout. A SYSTEM image alone contains no Pi firmware boot partition.
Connect or reseat the PCIe ribbon and HAT with power disconnected, following the
adapter's instructions. For the first boot, attach SYSTEM directly to a Pi USB
port if the cartridge hubs are not ready. Changing the hub wiring later requires
a fresh bay calibration.
## 2. Wire HDMI video, USB control and power separately
The Pi's micro-HDMI ports are **video outputs**; the monitor receives that video.
The Pi 5's USB-C socket supplies power to the Pi and does not provide DisplayPort
Alt Mode video. A USB-C display cable alone cannot replace HDMI on this board.
See [Raspberry Pi's display connection guide](https://www.raspberrypi.com/documentation/computers/getting-started.html#display).
| Connection | Purpose |
| --- | --- |
| Pi micro-HDMI output → Paperlike HDMI video input | Picture; use the correct HDMI connector sizes for both ends |
| Pi USB-A host port → Paperlike USB-C data port | Candidate USB control path for `dasungd`; verify enumeration in step 7 |
| Supplied Dasung power adapter → monitor power input | Monitor power, separate from USB control |
| Pi power supply → Pi USB-C power input | Pi power |
Use the monitor's HDMI input selection, if its controls offer one. A charging-only
USB cable will not expose the controller. This HDMI-plus-USB arrangement is the
route to test; whether this unit exposes both controller interfaces while HDMI
is active still needs observation. HDMI can show a picture even when USB control
is missing, and USB replies do not prove the picture is correct.
Use the supplied Dasung DC power connection: the manufacturer specifically warns
that ordinary USB power is insufficient for reliable 13K operation.
[DASUNG power guidance](https://shop.dasung.com/pages/q-a).
For Pi 5, a supply supporting 5 V/5 A permits a larger USB power budget; a 3 A
supply limits downstream peripherals to 600 mA. Use powered hubs for the cartridge
assembly rather than budgeting the monitor and twelve cartridges from the Pi.
[Raspberry Pi power guidance](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#power-supply).
The shipped FDS boot configuration forces this monitor's recorded 3200 × 2400
timing at about 37 Hz. That timing was established on the original workstation.
DASUNG documents that some HDMI hosts instead use 27 Hz. FDS's forced EDID means
you should not assume automatic fallback. If HDMI is blank or unstable, use the
[native-EDID diagnostic boot](dasung.md#try-the-monitors-own-hdmi-edid) before
changing monitor identity or USB matching rules.
[DASUNG HDMI refresh-rate guidance](https://shop.dasung.com/pages/dasung-paperlike-13k-the-worlds-first-13-3-inch-37hz-3k-e-ink-monitor).
## 3. Build a matching image set on the workstation
Complete [Prepare the workstation](getting-started.md#prepare-the-workstation),
including bootstrap, smoke test and build checks. From the repository root, run
the following commands **sequentially**, stopping if any fails:
```sh
make rootfs PROFILE=cli
make system-card PROFILE=cli
make recovery
make initramfs
make boot-volume BOOT_MODE=production
make internal-image
```
This builds one normal SYSTEM and independent recovery, then combines the current
kernel, initramfs and recovery into the internal disk. The CLI rootfs includes
the matching kernel modules, native s6 and the base Dasung controller. Rebuild
the complete sequence after a source change; existing convenience links in
`out/` can otherwise point to components from different builds.
`BOOT_MODE=production` puts the interactive console on the HDMI display with
USB keyboard input. It is suitable for first bring-up without a serial cable.
`PROFILE=development` is a separate choice that adds compilers and debugging
tools to SYSTEM; it does not select the console transport. The smaller CLI
profile is sufficient here, and includes the optional desktop too.
| Output | Write it to |
| --- | --- |
| `out/fds-internal.img` | Whole NVMe disk |
| `out/fds-system-cli.img` | Whole USB SYSTEM disk |
| `out/fds-boot.img` | Raw FAT partition payload; already included in the internal image |
| `out/fds-recovery.img` | Raw EROFS partition payload; already included in the internal image |
Do not write the last two files as whole disks. `make all` builds additional
profiles and release tooling; it is unnecessary for this first-boot image set.
Use [cleanup](cleanup.md) to review retained build trees when space is tight.
Record the inputs before flashing. On the **workstation**, choose a new session
directory and retain the printed build logs/manifests with it:
```sh
mkdir out/hardware-first
cp tests/hardware/session-template.json out/hardware-first/session.json
git rev-parse HEAD > out/hardware-first/source-commit.txt
git diff --binary HEAD > out/hardware-first/source-changes.patch
./tools/version > out/hardware-first/build-version.txt
sha256sum out/fds-internal.img out/fds-system-cli.img \
out/fds-boot.img out/fds-recovery.img out/fds-initramfs.img \
out/kernel/boot/kernel_2712.img > out/hardware-first/images.sha256
cp out/boot-volume/files/cmdline.txt out/hardware-first/cmdline-built.txt
```
Keep any untracked build inputs separately too. These checksums identify your
local build; they are not a release signature. For a downloaded release, follow
[signature verification](releases.md) and use its matching complete image set.
The historical `0.1.0` release and its acceptance records remain separate from
new local builds.
## 4. Check NVMe boot configuration
If the Pi already boots from this NVMe adapter, first record its EEPROM settings;
changing firmware is not a prerequisite just to try FDS. Otherwise, use a
separate Raspberry Pi OS maintenance microSD with the official EEPROM utilities.
Keep that rescue medium available throughout bring-up.
In the **maintenance OS**, before editing:
```sh
rpi-eeprom-config > pi-current.conf
rpi-eeprom-update > pi-eeprom-status.txt
```
Copy these files to the workstation and preserve the matching original firmware
as described in [EEPROM preparation and rollback](eeprom.md). To review the
installed boot configuration, the official editor is:
```sh
sudo rpi-eeprom-config --edit
```
For initial FDS bring-up, `BOOT_ORDER=0xf16` means NVMe first, SD fallback, then
repeat (read from the right). A non-HAT+ NVMe adapter may also require
`PCIE_PROBE=1`; follow the adapter's instructions and the
[official PCIe boot procedure](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#boot-from-pcie).
Preserve unrelated board settings, follow the updater's restart instructions,
then read the configuration back. FDS already supplies `dtparam=pciex1` in
`config.txt`; EEPROM discovery and the kernel's PCIe enablement are separate.
The [FDS EEPROM development profile](eeprom.md#profile-behavior) also keeps SD
fallback and enables firmware UART logging. Its offline preview is not a backup
of your board and does not apply itself. Do not switch to NVMe-only production
boot order until the rescue and normal boot paths have been checked.
## 5. Write the two disks
On the **workstation**, follow the complete
[guarded disk-writing procedure](internal-storage.md#install-the-internal-disk).
It includes disk identification by model/serial, an explicit erase confirmation,
readback, and backup-GPT relocation. Run it separately for these assignments:
| Pass | `fds_image` | `fds_disk` |
| --- | --- | --- |
| Internal disk | `$PWD/out/fds-internal.img` | The verified NVMe enclosure's whole-disk `/dev/disk/by-id/…` path |
| SYSTEM | `$PWD/out/fds-system-cli.img` | A different, verified USB cartridge's whole-disk `/dev/disk/by-id/…` path |
Both writes erase the selected disk. Follow all checks in that procedure; a
`-part1` path is not a whole-disk destination. The disks must use 512-byte logical
sectors and be at least as large as their respective images.
Confirm the three internal partition labels and the USB's single `FDS_SYSTEM`
label after writing. Safely disconnect the disks, install NVMe on the powered-off
Pi, and connect exactly one SYSTEM. Do not leave the NVMe enclosure connected as
a duplicate source of FDS partitions.
## 6. Power on and reach the console
Power the monitor and any required hub, attach video, USB control, keyboard and
SYSTEM, then power the Pi. Remove the maintenance SD for this attempt so an SD
fallback cannot be mistaken for an FDS boot.
The expected sequence is Pi firmware → kernel and stage0 from NVMe → read-only
SYSTEM from USB → native s6 → `FDS>`. The production image is quiet, so a missing
splash or scrolling kernel log is not by itself a failure. At **FDS**, run:
```sh
fds info
cat /usr/share/fds/image-profile
cat /proc/cmdline
findmnt -no SOURCE,FSTYPE,OPTIONS /
ps -p 1 -o pid,comm,args
fds machine status
fds bays
fds topology
fds boot-profile
```
Expect the intended version/profile, an EROFS root mounted read-only, and native
s6 as PID 1. `fds machine status` should report `internal_nvme` for valid internal
settings; `image_defaults` means settings loading fell back and needs diagnosis.
An empty initial map still legitimately reports `UNCONFIGURED` bays. Stage0
discovers SYSTEM by its GPT partition label before bay mapping, so that does not
prevent reaching the console. Do not unplug the running SYSTEM even if it has
not yet acquired a bay number.
If no SYSTEM is found, stage0 waits for media. Its prompt accepts `list`,
`rescan`, `recovery`, `reboot` and `poweroff`; inserting one valid SYSTEM should
resume boot. Two matching SYSTEMs are ambiguous: remove the unintended one.
This prompt is an early-boot command interface, not Bash.
The normal console is the ordinary `fds` user (UID 1000), with a temporary home
and locked account passwords. For root diagnostics, shut down with `fds poweroff`,
remove SYSTEM after shutdown, boot again and type `recovery` at stage0's
missing-SYSTEM prompt. The independent recovery image opens `RECOVERY#`.
Use that root console for monitor diagnostics and initial machine configuration;
there is no default root password to enter at `FDS>`.
## 7. Verify HDMI and the Dasung controller
At **`RECOVERY#`**, with both HDMI and USB data connected:
```sh
lsusb
lsusb -t
dasungd status
dasungd query
tail -80 /run/log/dasungd/current
```
The existing profile expects UART `1a86:7523` and companion bridge `1a86:5512`
under the same USB hub, plus a connected DRM output matching the configured
monitor serial. Query again and check status after replies arrive: `connected`
alone is not the same as `responsive`. See [Dasung diagnostics](dasung.md) for
DRM capture, blank HDMI, missing USB interfaces and the 37 Hz override.
Confirm visible text entry, a refresh, and parameter readback separately. Once
basic operation works, test a **monitor-only** off/on cycle while retaining Pi
power, SYSTEM and hub power; record the picture, USB re-enumeration and daemon
reconnection. This is not permission to unplug writable storage. A passing VM
controller test cannot establish physical monitor recovery.
## 8. Install the first bay map without a DATA cartridge
Stay at **`RECOVERY#`**. Keep the hubs in their intended final ports. Insert one
identifiable test USB device at a time and record `fds topology` for each physical
slot. Test USB 2 and USB 3 connections separately where supported. Map only the
slots currently assembled; unmapped slots can remain unconfigured.
Create editable settings in RAM:
```sh
fds machine export /tmp/my-machine
cat /tmp/my-machine/machine.toml
fds topology
```
Recovery includes Bash but no full-screen editor. Use a here-document to enter
the measured map. **The path below is a syntax example, not a Pi port identity**:
replace it and the port number with your actual observation before installing.
If the observed device path ends in `:usb2/1/3`, the parent hub ends in
`:usb2/1` and downstream port `3` maps to your chosen bay number:
```sh
cat > /tmp/my-machine/bays.toml <<'EOF'
[first_usb2]
hub = "platform/REPLACE_WITH_OBSERVED_CONTROLLER:usb2/1"
[first_usb2.ports]
3 = 1
EOF
```
Add separate groups for the measured USB 3 aliases and other hubs following
[the mapping reference](developer/cartridges.md#calibrate-the-physical-bay-map).
Do not map keyboard/monitor connections as cartridge bays or copy the emulator's
map. Validation checks syntax and conflicts; only your insertion tests establish
that a mapping matches the physical labels.
```sh
cat /tmp/my-machine/bays.toml
fds machine validate /tmp/my-machine
fds machine install /tmp/my-machine
fds reboot
```
Installation writes the complete settings bundle to NVMe and retains the previous
one; it does not change the active mapping during this boot. With SYSTEM still
absent, select `recovery` again after reboot. Verify `fds machine status`, then
`fds bay 01` as you move the test device through the configured slots. Use
`fds eject N` and wait for SAFE before removing storage managed in a mapped bay.
Later, [export a backup and update settings from DATA](internal-storage.md#update-settings-from-recovery).
Do not reflash the whole NVMe merely to change bay numbering: that would replace
saved settings and diagnostics. For normal boot, shut down, insert SYSTEM,
and power on again.
## 9. Save evidence and shut down
At **`RECOVERY#`**, collect a fresh capture and explicitly save it to NVMe before
losing the RAM filesystem:
```sh
bash /usr/share/fds/capture-hardware /tmp/hardware-first
cat /tmp/hardware-first/status.tsv
dasungd status > /tmp/hardware-first/dasung-status.json
cp /run/log/dasungd/current /tmp/hardware-first/dasung.log
tar -C /tmp -czf /tmp/hardware-first.tar.gz hardware-first
fds machine store hardware-first.tar.gz /tmp/hardware-first.tar.gz
fds power status
fds poweroff
```
Stored names must be new and files must be at most 16 MiB. The collector's
`SHA256SUMS` covers its own original outputs; the two manually added Dasung files
are additional evidence. Retrieve the archive later with
`fds machine fetch hardware-first.tar.gz /tmp/retrieved-hardware-first.tar.gz`
from recovery, then copy it to healthy, explicitly activated DATA and safely eject.
This capture describes the recovery boot; collect another capture in normal FDS
on DATA when that path is ready. `/tmp`, `/run`, logs and the normal home are
otherwise lost at shutdown.
Fill the workstation session record with observed results, wiring, EEPROM version,
image hashes and remaining failures. Leave unperformed cases `not_run`. Log
events measure software stages; physical power-on-to-picture and power-off timing
need an external observation. Follow [physical acceptance testing](developer/stress-testing.md#physical-acceptance-sequence)
when the complete assembly is ready.
Use `fds poweroff` and wait for shutdown before disconnecting power or swapping
SYSTEM. If shutdown is blocked, inspect `fds power status` and resolve the
reported storage problem. An E-Ink picture can remain visible after power is
removed, so the retained picture is not proof that the Pi is still running.
After basic console/display checks, try the [optional desktop](desktop.md) and
[cartridge workflow](cartridges.md).
## When the first boot fails
| Observation | Next check |
| --- | --- |
| Firmware cannot find NVMe | Adapter/ribbon seating, supply, EEPROM boot order, and conditional `PCIE_PROBE=1`; return to the maintenance SD |
| Stage0 says SYSTEM is missing | Whole GPT SYSTEM image, exactly one `FDS_SYSTEM`, USB power and `list`; bay calibration is not required at this stage |
| HDMI is blank or unstable | Monitor input/power, cable connector sizes, then [native HDMI EDID](dasung.md#try-the-monitors-own-hdmi-edid) or the serial console below |
| Kernel text appears but keyboard cannot reach a prompt | Check whether `BOOT_MODE=development` made UART the primary console |
| Picture works, `dasungd` does not | USB data cable, both USB IDs, DRM serial, permissions and daemon log; see [Dasung diagnostics](dasung.md) |
| `fds bays` is unconfigured | Complete step 8 using physical topology observations |
| `fds machine status` uses image defaults | Check the reported reason, NVMe transport, partition layout and clean internal ext4; do not run DATA repair against internal NVMe |
| USB resets or disappears under load | Reduce attached devices and check the Pi, monitor and hub supplies separately |
### Use a serial console when HDMI is unavailable
Use the Pi 5's dedicated debug UART connector and a compatible 3.3 V UART/debug
probe, following the [official UART documentation](https://www.raspberrypi.com/documentation/computers/configuration.html#configuring-uarts).
Connect the probe's host USB to the workstation, then open its serial terminal at
**115200 baud, 8 data bits, no parity, 1 stop bit, no flow control**. This is a
separate connection from the monitor's USB cable. Save the terminal log.
For an initial, still-disposable disk build, replace step 3's boot mode with
`make boot-volume BOOT_MODE=development` before `make internal-image`. This
enables verbose kernel output and selects `ttyAMA10` as the primary userspace
console. The development EEPROM profile separately enables firmware UART output.
Boot mode does not grant root; select recovery for a root shell.
For an already provisioned NVMe, use the
[offline BOOT edit procedure](dasung.md#edit-the-boot-command-line-offline) and
copy the single line from [cmdline-development.txt](../image/pi5/cmdline-development.txt)
into the disk's `cmdline.txt`; preserve any deliberate native-EDID diagnostic
change. This avoids overwriting settings to switch consoles. Restore the saved
command line to return to display/keyboard operation after debugging.
+3
View File
@@ -7,6 +7,9 @@ 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.
For the complete first-install sequence, including cabling, EEPROM and a first
bay map without DATA, use [the real-hardware setup guide](hardware-setup.md).
## Build the complete disk image
Build the component images first, in this order:
+4
View File
@@ -5,6 +5,10 @@
FDS recovery is an independent maintenance system on internal storage. It can
inspect a failed SYSTEM, check DATA and help prepare replacement cartridges.
For a machine whose bays have never been configured, follow
[initial bay calibration](hardware-setup.md#8-install-the-first-bay-map-without-a-data-cartridge)
at the root recovery console before relying on a DATA cartridge for settings.
## Build the recovery image
Use the prepared x86_64 build host described in [Your first build](getting-started.md).
+4 -1
View File
@@ -26,7 +26,10 @@ 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.
Custom sources are copied into an untracked `srcpkgs/PACKAGE` directory. Existing
Custom sources are copied into `srcpkgs/PACKAGE` inside the selected writable
build checkout. FDS uses `.host/void-packages/`; its `vendor/void-packages/`
submodule remains unchanged. The builder rejects a Git submodule as its build
checkout. 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.
+8 -3
View File
@@ -5,6 +5,9 @@ Read the complete command error and its log before retrying. Build logs are in
On FDS, `fds bay N`, `fds profiles` and `fds power status` report the relevant
operation state. Append `--help` to a command for accepted options.
For physical Pi startup, use [first-boot troubleshooting](hardware-setup.md#when-the-first-boot-fails).
For the Paperlike on HDMI plus USB, use [Dasung diagnostics](dasung.md#diagnose-a-connection-problem).
| Symptom | Resolution |
| --- | --- |
| Build disk is full | Stop builds and VMs; run `make clean-preview`, then `make clean`. See [retention rules](cleanup.md). |
@@ -12,7 +15,7 @@ operation state. Append `--help` to a command for accepted options.
| 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. |
| Output already exists | Use a new output directory, cartridge image or preview. A stopped emulator session can be restarted; see below. |
| 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. |
@@ -33,5 +36,7 @@ fds --json bay 01
```
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).
Run `start` again with the same session directory and boot options after `stop`.
Logs and DATA overlays are retained; bays start empty. Saved DATA can be
[exported](workstation.md#save-emulator-data). If restart reports a live process,
check `status` and stop the existing VM before retrying.
+50 -8
View File
@@ -7,18 +7,45 @@ as guest commands.
## Prepare the tools
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:
The native tools are `fds-cartridge` and `fds-emulator`. On Arch Linux, build
and install their `fds-tools` package from the checkout:
```sh
sudo pacman -S --needed base-devel rustup python git
make workstation
mkdir -p out
make workstation-install
```
`make workstation` produces `out/workstation/fds-tools-VERSION-1-ARCH.pkg.tar.zst`
with both commands, documentation, examples and license notices. It does not
install packages. `make workstation-install` builds that package and invokes
`sudo pacman -U` to install it into `/usr/bin`. Pacman tracks upgrades and removal;
remove it later with `sudo pacman -R fds-tools`. The exact latest package path is
recorded in `out/workstation/package-path.txt`.
The package requires glibc, GCC runtime libraries and xz. EROFS, bubblewrap and
QEMU are optional package dependencies because the tools can use the project-local
Void runners instead. For all features using native Arch utilities, install:
```sh
sudo pacman -S --needed bubblewrap erofs-utils qemu-system-aarch64 qemu-img
```
On other Linux distributions, install Rust, a C linker, Make, Python 3 and Git,
then build the same native commands without Arch packaging:
```sh
make workstation-binaries
export PATH="$PWD/out/workstation:$PATH"
```
Python and Git resolve the build version; neither is needed to run the installed
Rust commands. The repository selects its pinned Rust toolchain. XBPS and a
prepared Void checkout are still separate source-build prerequisites.
| Prerequisite | Used for |
| --- | --- |
| Prepared Void `void-packages` checkout and native XBPS utilities | Build source packages and install their AArch64 runtime dependencies |
| Prepared writable 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 |
@@ -31,12 +58,14 @@ share the same Void checkout.
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
using it. Keep this writable checkout outside Git submodules. 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:
keeps `vendor/void-packages` as a clean upstream reference and prepares the
writable `.host/void-packages` checkout plus `.host/xbps/usr/bin`. The native
builder defaults to `.host/void-packages`. Reuse its local tools:
```sh
./tools/prepare-image-tools
@@ -136,7 +165,9 @@ fds-emulator --session out/my-emulator start \
--system /path/to/fds-system-cli.img
```
The session directory must be new and private. Keep its absolute path short
The session directory is created private (0700). After stopping, reuse the same
directory with the same `start` command; an existing directory must contain a
valid stopped FDS session owned by you. 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.
@@ -179,7 +210,18 @@ fds-emulator --session out/my-emulator stop
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.
FDS shutdown and waits for QEMU to exit. Start the same session again:
```sh
fds-emulator --session out/my-emulator start --qemu-runner tools/in-void
```
Pass any custom boot-image or memory options again. This is a fresh boot with
empty cartridge bays. Console and QEMU logs are appended, DATA overlay files
remain in place, and `previous-*.json` preserves the previous session's image
and overlay mapping. Insert cartridges again as needed. A running session,
unknown directory, insecure permissions, or an outstanding control operation
prevents restart; the tool never clears a live session to make room.
`unplug BAY` simulates a physical pull, and `stop --force` cuts virtual power.
Use these only for deliberate failure simulation; DATA writes can be lost.
+6 -1
View File
@@ -3,6 +3,7 @@
import argparse
import io
import json
import shlex
from pathlib import Path
import subprocess
import sys
@@ -12,6 +13,7 @@ import uuid
project = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project/'tools'))
from fds_version import validate
from image_formats import digest, gpt, LINUX_FILESYSTEM, NAMESPACE
parser = argparse.ArgumentParser(description=__doc__)
@@ -21,6 +23,8 @@ parser.add_argument('--output-directory', type=Path)
args = parser.parse_args()
rootfs = args.rootfs.resolve(strict=True)
with tarfile.open(rootfs) as archive:
identity = dict(line.split('=', 1) for line in shlex.split(archive.extractfile('usr/lib/os-release').read().decode(), comments=True))
image_version = validate(identity['VERSION_ID'])
embedded = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
try:
root = archive.getmember('.')
@@ -34,7 +38,7 @@ args.profile = embedded
work = args.output_directory or Path(tempfile.mkdtemp(prefix='system-build.', dir=project/'out'))
if not work.is_dir() or any(work.iterdir()): parser.error('output directory must exist and be empty')
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
manifest = f'''format = 1\n[cartridge]\nid = "fds.system.{args.profile}"\nname = "FDS/OS {args.profile.upper()}"\nclass = "system"\nversion = "0.1.0"\n[media]\nwritable = false\n'''.encode()
manifest = f'''format = 1\n[cartridge]\nid = "fds.system.{args.profile}"\nname = "FDS/OS {args.profile.upper()}"\nclass = "system"\nversion = "{image_version}"\n[media]\nwritable = false\n'''.encode()
with tarfile.open(rootfs) as source, tarfile.open(work/'system.tar', 'w', format=tarfile.PAX_FORMAT) as output:
for member in source:
if member.name in ('FDS', 'FDS/CARTRIDGE.TOML'): continue
@@ -57,6 +61,7 @@ layout = gpt(work/'system.img', [('FDS_SYSTEM', LINUX_FILESYSTEM, work/'system.e
layout['rootfs_sha256'] = digest(rootfs)
layout['image_sha256'] = digest(work/'system.img')
layout['profile'] = args.profile
layout['version'] = image_version
(work/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
# An independent implementation checks the actual on-disk partition table.
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(work/'system.img')]))['partitiontable']
+1 -1
View File
@@ -1 +1 @@
FDS/OS 0.1
FDS/OS @FDS_VERSION@
+3 -3
View File
@@ -1,6 +1,6 @@
NAME="FDS/OS"
PRETTY_NAME="FDS/OS 0.1 (M2 development rootfs)"
PRETTY_NAME="FDS/OS @FDS_VERSION@"
ID=fds
ID_LIKE=void
VERSION_ID=0.1
VERSION="0.1 (M2)"
VERSION_ID="@FDS_VERSION@"
VERSION="@FDS_VERSION@"
+2 -1
View File
@@ -1,5 +1,5 @@
pkgname=fds-base-files
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
depends="xbps-triggers"
@@ -22,6 +22,7 @@ do_install() {
vinstall "${FILESDIR}/${f}" 600 etc
done
vinstall "${FILESDIR}/os-release" 644 usr/lib
sed -i "s/@FDS_VERSION@/${version}/g" "${DESTDIR}/usr/lib/os-release" "${DESTDIR}/etc/issue"
ln -s ../usr/lib/os-release "${DESTDIR}/etc/os-release"
ln -s /run/fds/resolv.conf "${DESTDIR}/etc/resolv.conf"
ln -s /proc/self/mounts "${DESTDIR}/etc/mtab"
+1 -1
View File
@@ -1,6 +1,6 @@
# Explicit FDS userspace; never depend on Void's base-system metapackage.
pkgname=fds-base
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
metapackage=yes
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=fds-cartridged
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
nostrip=yes
+1 -1
View File
@@ -1,6 +1,6 @@
# Static payloads are built and checked on the host before packaging.
pkgname=fds-cli
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
nostrip=yes
+1 -1
View File
@@ -1,6 +1,6 @@
# FDS-owned overlay. The build helper supplies the verified static ARM payload.
pkgname=fds-dasungd
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
nostrip=yes
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=fds-eink
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
depends="xorg-server xf86-input-libinput WindowMaker terminus-font xterm xset xsetroot fontconfig"
+1 -1
View File
@@ -1,6 +1,6 @@
# Image-time s6 configuration. The native upstream binaries own PID 1.
pkgname=fds-init
version=0.1.0
version=$(cat "${XBPS_DISTDIR}/fds-version")
revision=1
archs="aarch64"
depends="iproute2 s6-linux-init s6-rc s6 execline coreutils util-linux bash eudev fds-dasungd fds-cli fds-cartridged"
+52
View File
@@ -0,0 +1,52 @@
# Local source build: invoked by tools/build-workstation with an explicit checkout.
pkgname=fds-tools
_fds_source=${FDS_SOURCE_DIR:?Run make workstation from the FDS checkout}
pkgver=$("$_fds_source/tools/version")
pkgrel=1
pkgdesc='FDS/OS workstation cartridge builder and twelve-bay ARM emulator'
arch=('x86_64' 'aarch64')
license=('MIT')
depends=('glibc' 'gcc-libs' 'xz')
makedepends=('rust' 'python' 'git')
optdepends=(
'bubblewrap: cartridge creation and inspection in a private namespace'
'erofs-utils: create and inspect cartridge payload filesystems'
'qemu-system-aarch64: run FDS in the emulator'
'qemu-img: writable emulator DATA overlays'
)
options=('!debug' '!strip')
build() {
cd "$_fds_source"
./tools/build-workstation-binaries
# Includes the notices of locked dependencies; this is an intentionally
# complete workspace inventory rather than a claim they all link into tools.
if [[ -f "$srcdir/RUST-NOTICES.txt" ]]; then rm "$srcdir/RUST-NOTICES.txt"; fi
python3 tools/rust-notices "$srcdir/RUST-NOTICES.txt"
}
check() {
local tool
for tool in fds-cartridge fds-emulator; do
[[ $("$_fds_source/out/workstation/$tool" --version) == "$tool $pkgver" ]]
"$_fds_source/out/workstation/$tool" --help >/dev/null
done
}
package() {
local tool
for tool in fds-cartridge fds-emulator; do
install -Dm755 "$_fds_source/out/workstation/$tool" "$pkgdir/usr/bin/$tool"
done
install -Dm644 "$_fds_source/LICENSE" "$pkgdir/usr/share/licenses/fds-tools/LICENSE"
install -Dm644 "$srcdir/RUST-NOTICES.txt" "$pkgdir/usr/share/licenses/fds-tools/RUST-NOTICES.txt"
install -Dm644 "$_fds_source/README.md" "$pkgdir/usr/share/doc/fds-tools/README.md"
cp -a "$_fds_source/docs" "$pkgdir/usr/share/doc/fds-tools/"
# Ship source recipes, excluding any locally compiled example output.
local example
for example in hello report; do
install -Dm644 "$_fds_source/examples/software/$example/software.toml" "$pkgdir/usr/share/doc/fds-tools/examples/software/$example/software.toml"
cp -a "$_fds_source/examples/software/$example/void" "$pkgdir/usr/share/doc/fds-tools/examples/software/$example/"
done
install -Dm644 "$_fds_source/examples/software/cartridge.toml" "$pkgdir/usr/share/doc/fds-tools/examples/software/cartridge.toml"
}
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "dasungd"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
description = "DASUNG Paperlike USB keepalive, control socket and Linux display profile daemon"
license = "MIT"
+2 -2
View File
@@ -31,7 +31,7 @@ use std::{
};
#[derive(Parser)]
#[command(version, about)]
#[command(version = env!("FDS_BUILD_VERSION"), about)]
struct Cli {
#[arg(long, global = true, default_value = "/etc/dasungd.toml")]
config: PathBuf,
@@ -286,7 +286,7 @@ fn daemon(config: Config) -> Result<()> {
let mut last_error = String::new();
eprintln!(
"dasungd {} ready; waiting for {}",
env!("CARGO_PKG_VERSION"),
env!("FDS_BUILD_VERSION"),
config.monitor_serial
);
while running.load(Ordering::Relaxed) {
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-boottrace"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Measured FDS boot events and regression reports"
+1 -1
View File
@@ -80,7 +80,7 @@ fn adopt() -> Result<()> {
}
#[derive(Parser)]
#[command(
version,
version = env!("FDS_BUILD_VERSION"),
about = "Record and compare measured boot events",
after_help = "Measurements use Linux CLOCK_BOOTTIME, excluding firmware and power-on."
)]
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-burn"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Verified cartridge images and protected FDS media writes"
+1 -1
View File
@@ -5,7 +5,7 @@ use std::{fs::OpenOptions, os::unix::fs::OpenOptionsExt, path::PathBuf, process:
#[derive(Parser)]
#[command(
version,
version = env!("FDS_BUILD_VERSION"),
about = "Create cartridge images and preview confirmed media writes",
after_help = "Creation writes a new regular file. Writes require confirmation tied to the selected insertion and image hash."
)]
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-cartridged"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "FDS event-driven cartridge and physical bay manager"
+1 -1
View File
@@ -14,7 +14,7 @@ use std::{path::PathBuf, process::ExitCode};
#[derive(Parser)]
#[command(
version,
version = env!("FDS_BUILD_VERSION"),
about = "Run the root cartridge service or inspect USB topology"
)]
struct Cli {
+2 -1
View File
@@ -226,7 +226,8 @@ fn session() -> Result<()> {
}
#[derive(clap::Parser)]
#[command(
version,
name = "fds-profile",
version = env!("FDS_BUILD_VERSION"),
about = "Control the desktop and network profile",
after_help = "Examples: fds-profile activate windowmaker; fds-profile deactivate"
)]
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-cli"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "FDS system and cartridge command interface"
+4 -4
View File
@@ -12,10 +12,10 @@ pub struct Cli {
#[derive(Debug, Subcommand)]
pub enum Applet {
#[command(version, about = "FDS/OS system and cartridge control")]
#[command(version = env!("FDS_BUILD_VERSION"), about = "FDS/OS system and cartridge control")]
Fds(FdsArgs),
/// Inspect a bay, image, or cartridge manifest.
#[command(version)]
#[command(version = env!("FDS_BUILD_VERSION"))]
FdsInspect {
#[arg(long, global = true)]
json: bool,
@@ -23,7 +23,7 @@ pub enum Applet {
args: InspectArgs,
},
/// Unmount a cartridge before removal.
#[command(version)]
#[command(version = env!("FDS_BUILD_VERSION"))]
FdsEject {
#[arg(long, global = true)]
json: bool,
@@ -31,7 +31,7 @@ pub enum Applet {
args: BayArgs,
},
/// Prepare native shutdown or inspect its status.
#[command(version)]
#[command(version = env!("FDS_BUILD_VERSION"))]
FdsPower {
#[arg(long, global = true)]
json: bool,
+1 -1
View File
@@ -16,7 +16,7 @@ struct Invocation {
#[derive(Parser)]
#[command(
name = "fds-program",
version,
version = env!("FDS_BUILD_VERSION"),
disable_help_subcommand = true,
about = "Run a published cartridge command in the foreground"
)]
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-common"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Shared bounded configuration and Linux discovery for FDS tools"
+1 -1
View File
@@ -11,7 +11,7 @@ pub mod trace;
use std::{fmt, fs::File, io::Read, path::Path, str::FromStr};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const VERSION: &str = env!("FDS_BUILD_VERSION");
pub const MAX_CONFIG_BYTES: u64 = 64 * 1024;
pub type Result<T> = std::result::Result<T, Error>;
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-control"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Native grayscale X11 cartridge control panel"
+1 -1
View File
@@ -24,7 +24,7 @@ type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Parser)]
#[command(
version,
version = env!("FDS_BUILD_VERSION"),
about = "FDS cartridge control panel for X11",
after_help = "Select a bay to inspect its cartridge. Run opens a terminal; Eject stops its programs and releases the cartridge. Remove media only after SAFE appears."
)]
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-release"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "FDS release manifest signing and explicit-key verification"
+7 -1
View File
@@ -63,7 +63,13 @@ pub fn filename(value: &str) -> Result<()> {
impl Manifest {
pub fn validate(&self) -> Result<()> {
if self.format != 1
|| self.version != fds_common::VERSION
|| self.version.is_empty()
|| self.version.len() > 32
|| !self.version.starts_with(|c: char| c.is_ascii_digit())
|| !self
.version
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'.')
|| self.source_epoch == 0
|| !hex_valid(&self.source_sha256, 64)
|| !hex_valid(&self.void_commit, 40)
+1 -1
View File
@@ -4,7 +4,7 @@ use std::{path::PathBuf, process::ExitCode};
#[derive(Parser)]
#[command(
version,
version = env!("FDS_BUILD_VERSION"),
about = "Sign and verify FDS release artifacts",
after_help = "Keys and signatures are never overwritten. Verification checks every listed artifact.
An independently trusted public key is required; a bundled key is not automatically trusted."
+2 -1
View File
@@ -1,5 +1,6 @@
[package]
name = "fds-smoketest"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2021"
publish = false
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-software"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Verified installed software trees and legacy archive reading"
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-stage0"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Static FDS early boot discovery and root handoff"
+8 -2
View File
@@ -364,7 +364,10 @@ pub fn boot() -> Result<()> {
let events = netlink()?; // Bind before scanning so insertion cannot fall into a gap.
let signals = child_events()?;
let mut monitor = start_monitor(options.debug)?;
println!("FELIS DATA SYSTEMS\nPORTABLE COMPUTER FP-85\n\nFDS BOOT ROM 0.1");
println!(
"FELIS DATA SYSTEMS\nPORTABLE COMPUTER FP-85\n\nFDS BOOT ROM {}",
fds_common::VERSION
);
println!("Commands while waiting: list, rescan, recovery, reboot, poweroff");
let mut previous = String::new();
let mut pending = ConsoleInput::default();
@@ -397,7 +400,10 @@ pub fn boot() -> Result<()> {
if options.debug {
println!("FDS_STAGE0_ROOT: {}", options.root_label());
}
println!("MEMORY ........ READY\nSYSTEM ........ FDS/OS 0.1");
println!(
"MEMORY ........ READY\nSYSTEM ........ FDS/OS {}",
fds_common::VERSION
);
return handoff(monitor, options.debug);
}
Err(error) => {
+1 -1
View File
@@ -5,7 +5,7 @@ use std::{path::PathBuf, process::ExitCode};
#[derive(Parser)]
#[command(
version,
version = env!("FDS_BUILD_VERSION"),
about = "Early boot discovery and native s6 handoff",
after_help = "With no arguments, root PID 1 boots FDS. Diagnostic options only read their supplied inputs."
)]
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-workstation"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
description = "Linux workstation software and cartridge tools"
+3 -2
View File
@@ -4,12 +4,13 @@ use fds_workstation::emulator::Session;
use std::{path::PathBuf, process::ExitCode};
#[derive(Parser)]
#[command(
version,
name = "fds-emulator",
version = env!("FDS_BUILD_VERSION"),
about = "Boot FDS/OS and hotplug virtual USB cartridges on a Linux workstation",
after_help = "QEMU virt tests FDS software, not Raspberry Pi firmware or physical hardware. DATA uses retained temporary overlays; source images remain unchanged."
)]
struct Cli {
/// Private session directory. Choose a new directory for each boot.
/// Private session directory. A stopped session can be started again.
#[arg(long, global = true, default_value = "out/emulator")]
session: PathBuf,
#[command(subcommand)]
+120 -17
View File
@@ -12,8 +12,11 @@ use std::{
fs::{self, File, OpenOptions},
io::Write,
os::{
fd::AsRawFd,
unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::{
fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
net::UnixStream,
},
},
path::{Path, PathBuf},
process::Stdio,
@@ -90,6 +93,43 @@ fn lock(root: &Path) -> Result<File> {
}
Ok(file)
}
// A pidfd remains attached to the original process even if its PID is reused.
// QMP EOF alone does not establish that QEMU finished releasing its files.
fn process(root: &Path) -> Result<Option<OwnedFd>> {
let text = match fs::read_to_string(root.join("qemu.pid")) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
};
let pid: i32 = text
.trim()
.parse()
.map_err(|_| Error("Invalid QEMU pid file".into()))?;
if pid <= 0 {
return Err(Error("Invalid QEMU pid file".into()));
}
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as i32;
if fd < 0 {
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
return Ok(None);
}
return Err(error.into());
}
Ok(Some(unsafe { OwnedFd::from_raw_fd(fd) }))
}
fn exited(process: &OwnedFd, timeout_ms: i32) -> Result<bool> {
let mut event = libc::pollfd {
fd: process.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let result = unsafe { libc::poll(&mut event, 1, timeout_ms) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(result > 0 && event.revents & libc::POLLIN != 0)
}
impl Session {
pub fn load(root: &Path) -> Result<Self> {
ordinary()?;
@@ -159,17 +199,70 @@ impl Session {
"Use a shorter session path (under 90 bytes), without commas or line breaks".into(),
));
}
fs::DirBuilder::new()
.mode(0o700)
.create(&root)
.map_err(|e| {
Error(format!(
"Create a new session directory {}: {e}",
root.display()
))
})?;
let runner = runner.map(regular).transpose()?;
let guard = match fs::DirBuilder::new().mode(0o700).create(&root) {
Ok(()) => lock(&root)?,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let previous = Self::load(&root)?;
if process(&root)?
.as_ref()
.map(|pid| exited(pid, 0))
.transpose()?
== Some(false)
{
return Err(Error(
"Session still has a live process; stop it before starting again".into(),
));
}
for name in ["qmp.sock", "console.sock"] {
match UnixStream::connect(root.join(name)) {
Ok(_) => {
return Err(Error(
"Session still has an active socket; stop it before starting again"
.into(),
));
}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::ConnectionRefused
) =>
{
()
}
Err(error) => return Err(error.into()),
}
}
// Keep the previous cartridge-to-overlay mapping as well as the
// overlay files. A fresh boot starts with empty cartridge bays.
let archive = root.join(format!(
"previous-{}.json",
image::hex(&image::random_id()?)
));
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(archive)?;
file.write_all(
&serde_json::to_vec_pretty(&previous.state)
.map_err(|e| Error(e.to_string()))?,
)?;
file.sync_all()?;
for name in ["qmp.sock", "console.sock", "qemu.pid"] {
match fs::remove_file(root.join(name)) {
Ok(()) => (),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
Err(error) => return Err(error.into()),
}
}
previous._lock
}
Err(error) => return Err(error.into()),
};
let session = Self {
_lock: lock(&root)?,
_lock: guard,
root,
state: State {
format: 1,
@@ -177,7 +270,7 @@ impl Session {
kernel,
initramfs,
system,
qemu_runner: runner.map(regular).transpose()?,
qemu_runner: runner,
cartridges: BTreeMap::new(),
},
};
@@ -193,7 +286,7 @@ impl Session {
.arg("-initrd").arg(&session.state.initramfs)
.args(["-append","console=ttyAMA0 rdinit=/init ro quiet loglevel=3 fds.emulator=1","-blockdev"]).arg(block.to_string())
.args(["-device","virtio-blk-pci,drive=system","-device","qemu-xhci,id=xhci,addr=05.0,p2=12,p3=12"])
.stdin(Stdio::null()).stdout(Stdio::null()).stderr(File::create(session.root.join("qemu.log"))?);
.stdin(Stdio::null()).stdout(Stdio::null()).stderr(OpenOptions::new().create(true).append(true).mode(0o600).custom_flags(libc::O_NOFOLLOW).open(session.root.join("qemu.log"))?);
crate::success(&mut command)
.map_err(|e| Error(format!("{e}; inspect {}/qemu.log", session.root.display())))?;
let boot = (|| -> Result<()> {
@@ -344,16 +437,26 @@ impl Session {
}
pub fn stop(&self, force: bool) -> Result<Value> {
let mut qmp = self.qmp()?;
if force {
qmp.execute("quit", json!({}))?;
let process = process(&self.root)?.ok_or_else(|| {
Error("QEMU process record is unavailable; cannot confirm shutdown".into())
})?;
let response = if force {
// QEMU may reset QMP before acknowledging quit. The pidfd,
// rather than transport EOF or a reply, establishes actual exit.
qmp.execute("quit", json!({})).map(|_| ())
} else {
let output =
Serial::connect(&self.root, 120)?.command(&["fds".into(), "poweroff".into()])?;
if output.status != 0 {
return Err(Error(format!("Guest refused shutdown: {}", output.output)));
}
qmp.closed()
};
if !exited(&process, 30_000)? {
return Err(response
.err()
.unwrap_or_else(|| Error("QEMU has not exited; session remains in use".into())));
}
qmp.closed()?;
Ok(json!({"stopped":self.root,"forced":force,"logs_and_data_overlays_retained":true}))
}
}
+2 -1
View File
@@ -4,7 +4,8 @@ use std::{path::PathBuf, process::ExitCode};
#[derive(Parser)]
#[command(
version,
name = "fds-cartridge",
version = env!("FDS_BUILD_VERSION"),
about = "Build Void source packages and ready-to-run cartridge images on Linux"
)]
struct Cli {
+12 -2
View File
@@ -13,8 +13,8 @@ use std::{
#[derive(clap::Args, Debug, Clone)]
pub struct BuildOptions {
/// Prepared Void source checkout used by xbps-src (not the workstation OS).
#[arg(long, global = true, default_value = "vendor/void-packages")]
/// Writable Void build checkout; keep it outside Git submodules.
#[arg(long, global = true, default_value = ".host/void-packages")]
pub void_packages: PathBuf,
/// Optional rootless XBPS wrapper, for example this checkout's tools/in-void.
#[arg(long, global = true)]
@@ -81,6 +81,16 @@ pub fn build(recipe: &Path, output: &Path, options: &BuildOptions) -> Result<Sof
"--void-packages must name a prepared Void source checkout".into(),
));
}
let superproject = Command::new("git")
.arg("-C")
.arg(&checkout)
.args(["rev-parse", "--show-superproject-working-tree"])
.output()?;
if !superproject.status.success() || !superproject.stdout.is_empty() {
return Err(Error(
"Use a writable Void build checkout outside Git submodules; FDS prepares .host/void-packages with make bootstrap".into(),
));
}
let destination = checkout.join("srcpkgs").join(&input.source.package);
if let Some(template) = input.source.template {
let source = parent(recipe)?.join(template).canonicalize()?;
+2 -2
View File
@@ -1,7 +1,7 @@
# Physical acceptance records
Physical tests are deferred until the Pi and its attached hardware are available.
Follow [the complete procedure](../../docs/developer/stress-testing.md), including bay
Start with [real-hardware setup](../../docs/hardware-setup.md) on the assembled Pi.
Then follow [the complete procedure](../../docs/developer/stress-testing.md), including bay
calibration, repeatable device populations, explicit SAFE handling and separate
external timing measurements.
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "fds-test-helpers"
version = "0.1.0"
version.workspace = true
build = "../../tools/version-build.rs"
edition = "2024"
license = "MIT"
publish = false
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""Install the actual Arch package into a disposable pacman root, without host writes."""
from pathlib import Path
import json
import os
import subprocess
import tempfile
project=Path(__file__).resolve().parents[2]
package=Path((project/'out/workstation/package-path.txt').read_text().strip())
work=Path(tempfile.mkdtemp(prefix='arch-package-test.',dir=project/'out'))
root=work/'root';root.mkdir()
(root/'var/lib/pacman').mkdir(parents=True)
(root/'var/cache/pacman/pkg').mkdir(parents=True)
config=work/'pacman.conf'
config.write_text('[options]\nArchitecture = auto\nSigLevel = Never\nDownloadUser = root\n')
wrapper=work/'bin';wrapper.mkdir()
# Run the actual installer. In this private namespace it is already uid 0;
# redirect only pacman's installation root and disable dependency resolution.
(wrapper/'pacman').write_text('#!/bin/bash\nexec /usr/bin/pacman --config '+str(config)+' --root '+str(root)+' --dbpath '+str(root/'var/lib/pacman')+' --cachedir '+str(root/'var/cache/pacman/pkg')+' --logfile '+str(work/'pacman.log')+' --nodeps --nodeps --noconfirm "$@"\n')
(wrapper/'pacman').chmod(0o755)
command=['bwrap','--unshare-user','--uid','0','--gid','0','--ro-bind','/','/','--bind',str(work),str(work),'--dev','/dev','--proc','/proc','--setenv','PATH',str(wrapper)+':'+os.environ['PATH'],str(project/'tools/install-workstation')]
result=subprocess.run(command,capture_output=True,text=True)
(work/'install.log').write_text(result.stdout+result.stderr)
assert result.returncode==0,(result.stdout,result.stderr)
version=subprocess.check_output([str(project/'tools/version')],text=True).strip()
for tool in ['fds-cartridge','fds-emulator']:
output=subprocess.check_output([str(root/'usr/bin'/tool),'--version'],text=True).strip()
assert output==f'{tool} {version}',output
subprocess.run([str(root/'usr/bin'/tool),'--help'],check=True,stdout=subprocess.DEVNULL)
assert (root/'usr/share/doc/fds-tools/docs/workstation.md').is_file()
assert (root/'usr/share/licenses/fds-tools/RUST-NOTICES.txt').stat().st_size>0
installed=subprocess.check_output(['pacman','--config',str(config),'--root',str(root),'--dbpath',str(root/'var/lib/pacman'),'-Q','fds-tools'],text=True).strip()
assert installed==f'fds-tools {version}-1',installed
(work/'acceptance.json').write_text(json.dumps(dict(status='passed',package=str(package),installed=installed,installer='tools/install-workstation',host_install=False),indent=2)+'\n')
(project/'out/arch-package-current.txt').write_text(str(work)+'\n')
print('PASS: real pacman installation and both installed commands:',work)
+2 -1
View File
@@ -61,7 +61,8 @@ class CleanupTests(unittest.TestCase):
self.file('out/manifests/acceptance.json'),
self.file('out/.gitkeep'),
self.file('.host/xbps/tool'),
self.file('vendor/void-packages/hostdir/download'),
self.file('.host/void-packages/hostdir/download'),
self.file('vendor/void-packages/xbps-src'),
]
(self.root / 'out/rootfs-cli.tar').symlink_to('rootfs-build.NEW123/rootfs.tar')
preview = self.run_cli('--dry-run')
+42 -7
View File
@@ -30,31 +30,66 @@ expect_failure 'unsupported bootstrap option' tools/bootstrap-host --unknown
# Use a disposable minimal checkout to exercise pin and dirty-tree detection.
mkdir -p "$scratch/repo/tools" "$scratch/repo/vendor/void-packages"
cp tools/lib.sh tools/prepare-void "$scratch/repo/tools/"
cp tools/lib.sh tools/prepare-void tools/prepare-void-workspace tools/version tools/fds_version.py "$scratch/repo/tools/"
printf "1.0\n" >"$scratch/repo/FDS_VERSION"
fake="$scratch/repo/vendor/void-packages"
git -C "$fake" init -q
printf '# fixture\n' >"$fake/xbps-src"
mkdir -p "$fake/srcpkgs/fds-upstream" "$fake/etc" "$scratch/repo/config" "$scratch/repo/packages/fds-fixture"
printf '# upstream\n' >"$fake/srcpkgs/fds-upstream/template"
printf 'etc/conf\n' >"$fake/.gitignore"
printf 'etc/conf\nhostdir/\nmasterdir-*/\n' >"$fake/.gitignore"
cp config/xbps-src.conf "$scratch/repo/config/"
printf '# inert overlay fixture\n' >"$scratch/repo/packages/fds-fixture/template"
git -C "$fake" add .
git -C "$fake" -c user.name=FDS -c user.email=test@example.invalid commit -qm fixture
git -C "$fake" rev-parse HEAD >"$scratch/repo/VOID_PACKAGES_COMMIT"
bash -c 'source "$1"; check_void_pin' _ "$scratch/repo/tools/lib.sh"
bash -c 'source "$1"; check_void_source' _ "$scratch/repo/tools/lib.sh"
printf 'PASS: accepts exact clean Void commit\n'
# Existing generated files and caches move without copying their payloads.
cp -a "$scratch/repo/packages/fds-fixture" "$fake/srcpkgs/"
cp config/xbps-src.conf "$fake/etc/conf"
mkdir -p "$fake/hostdir" "$fake/masterdir-x86_64"
printf 'cached input\n' >"$fake/hostdir/cache"
cache_inode=$(stat -c %i "$fake/hostdir/cache")
"$scratch/repo/tools/prepare-void"
cmp "$scratch/repo/packages/fds-fixture/template" "$fake/srcpkgs/fds-fixture/template"
build="$scratch/repo/.host/void-packages"
[[ -z $(git -C "$fake" status --porcelain --untracked-files=all) ]] || die 'Migration dirtied upstream'
[[ ! -e $fake/hostdir && ! -e $fake/etc/conf && ! -e $fake/srcpkgs/fds-fixture ]] || die 'Migration left build state in upstream'
[[ $(stat -c %i "$build/hostdir/cache") == "$cache_inode" ]] || die 'Migration copied cache data'
[[ ! -f $build/.git/objects/info/alternates ]] || die 'Build checkout depends on upstream object storage'
bash -c 'source "$1"; check_void_pin' _ "$scratch/repo/tools/lib.sh"
cmp "$scratch/repo/packages/fds-fixture/template" "$build/srcpkgs/fds-fixture/template"
"$scratch/repo/tools/prepare-void"
printf 'PASS: overlay copied and repeat preparation is idempotent\n'
printf 'PASS: overlays and caches moved outside clean upstream; preparation is idempotent\n'
printf '# changed\n' >>"$scratch/repo/packages/fds-fixture/template"
expect_failure 'stale generated overlay' "$scratch/repo/tools/prepare-void"
cp "$fake/srcpkgs/fds-fixture/template" "$scratch/repo/packages/fds-fixture/template"
cp "$build/srcpkgs/fds-fixture/template" "$scratch/repo/packages/fds-fixture/template"
# Preserve unexpected upstream additions and independently edited legacy copies.
printf 'personal source\n' >"$fake/personal-file"
expect_failure 'unknown upstream addition' "$scratch/repo/tools/prepare-void"
[[ -f $fake/personal-file ]] || die 'Unknown file was removed'
rm "$fake/personal-file"
cp -a "$scratch/repo/packages/fds-fixture" "$fake/srcpkgs/"
printf '# independent edit\n' >>"$fake/srcpkgs/fds-fixture/template"
expect_failure 'edited legacy overlay' "$scratch/repo/tools/prepare-void"
grep -q 'independent edit' "$fake/srcpkgs/fds-fixture/template"
rm -r "$fake/srcpkgs/fds-fixture"
# An explicit upstream pin update advances the build checkout and keeps caches.
printf 'new upstream revision\n' >"$fake/README"
git -C "$fake" add README
git -C "$fake" -c user.name=FDS -c user.email=test@example.invalid commit -qm update
git -C "$fake" rev-parse HEAD >"$scratch/repo/VOID_PACKAGES_COMMIT"
# Production initializes upstream with depth 1; updating that boundary must work.
git -C "$fake" rev-parse HEAD >"$fake/.git/shallow"
expect_failure 'outdated build checkout' bash -c 'source "$1"; check_void_pin' _ "$scratch/repo/tools/lib.sh"
"$scratch/repo/tools/prepare-void"
[[ $(stat -c %i "$build/hostdir/cache") == "$cache_inode" ]] || die 'Pin update replaced cache'
[[ $(git -C "$build" rev-parse HEAD) == "$(cat "$scratch/repo/VOID_PACKAGES_COMMIT")" ]] || die 'Build pin not advanced'
mkdir -p "$scratch/repo/packages/fds-upstream"
printf '# collision\n' >"$scratch/repo/packages/fds-upstream/template"
expect_failure 'overlay replacing upstream package' "$scratch/repo/tools/prepare-void"
printf '# changed config\n' >>"$fake/etc/conf"
rm -r "$scratch/repo/packages/fds-upstream"
printf '# changed config\n' >>"$build/etc/conf"
expect_failure 'overwriting local xbps configuration' "$scratch/repo/tools/prepare-void"
printf '\n# dirty\n' >>"$fake/xbps-src"
expect_failure 'modified Void source' bash -c 'source "$1"; check_void_pin' _ "$scratch/repo/tools/lib.sh"
+43
View File
@@ -11,6 +11,7 @@ import sys
import tempfile
project = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project / 'tools'))
loader = importlib.machinery.SourceFileLoader('fds_frozen_fixture', str(project / 'tools/frozen-inputs'))
spec = importlib.util.spec_from_loader(loader.name, loader)
frozen = importlib.util.module_from_spec(spec)
@@ -131,4 +132,46 @@ command[-1] = linked
result = invoke(*command, ok=False)
assert 'not a symlink' in result.stderr
print(f'PASS: release output preservation and private-key symlink rejection: {work}')
# Restore small real Git bundles through both workspace layouts. These fixtures
# exercise path selection and cache preservation, not an operating-system build.
upstream = work / 'upstream'
upstream.mkdir()
invoke('git', 'init', '-q', upstream)
(upstream / 'xbps-src').write_text('# inert source fixture\n')
(upstream / '.gitignore').write_text('hostdir/\nmasterdir-*/\netc/conf\netc/xbps.d/custom/\n')
invoke('git', '-C', upstream, 'add', '.')
invoke('git', '-C', upstream, '-c', 'user.name=FDS', '-c', 'user.email=test@example.invalid', 'commit', '-qm', 'fixture')
pin = invoke('git', '-C', upstream, 'rev-parse', 'HEAD').stdout.strip()
epoch = int(invoke('git', '-C', upstream, 'show', '-s', '--format=%ct', 'HEAD').stdout)
for layout in ('separate', 'legacy'):
fixture = work / ('restore-input-' + layout)
(fixture / 'project/tools').mkdir(parents=True)
(fixture / 'project/config').mkdir()
(fixture / 'project/VOID_PACKAGES_COMMIT').write_text(pin + '\n')
(fixture / 'project/config/xbps-src.conf').write_text('# fixture\n')
if layout == 'separate':
shutil.copy2(project / 'tools/prepare-void-workspace', fixture / 'project/tools/prepare-void-workspace')
invoke('git', '-C', upstream, 'bundle', 'create', fixture / 'void.bundle', 'HEAD')
for name in ('masterdir/etc/xbps.d', 'sources', 'repositories/build',
'repositories/cli', 'repositories/development', 'repositories/recovery',
'xbps', 'image-tools', 'eeprom', 'cache', 'downloads', 'cargo-vendor', 'rust-toolchain'):
(fixture / name).mkdir(parents=True, exist_ok=True)
(fixture / 'sources/retained.tar.gz').write_bytes(b'cached source fixture')
restore_lock = dict(lock, void_commit=pin, source_epoch=epoch)
restore_lock['files'] = frozen.inventory(fixture)
restore_lock['source_sha256'] = frozen.source_digest(restore_lock['files'])
(fixture / 'lock.json').write_text(json.dumps(restore_lock))
destination = work / ('restored-' + layout)
invoke(verify, 'restore', fixture, destination)
build = destination / ('.host/void-packages' if layout == 'separate' else 'vendor/void-packages')
assert (build / 'hostdir/sources/retained.tar.gz').read_bytes() == b'cached source fixture'
assert (build / 'masterdir-x86_64/etc/xbps.d/05-fds-frozen-local.conf').read_text() == 'repository=/host/frozen-repository\n'
assert invoke('git', '-C', build, 'rev-parse', 'HEAD').stdout.strip() == pin
if layout == 'separate':
reference = destination / 'vendor/void-packages'
assert not (reference / 'hostdir').exists()
assert not invoke('git', '-C', reference, 'status', '--porcelain', '--untracked-files=all').stdout.strip()
invoke(verify, 'verify', fixture)
print('PASS: separate-workspace and legacy frozen restores retain source caches and offline repositories', flush=True)
print('NOTE: these are small contract fixtures; actual source-to-image offline reproduction remains a separate gate')
+11 -2
View File
@@ -33,7 +33,9 @@ def invoke(prefix, arguments, ok=True):
def check_all(directory, key, ok=True):
for label, command in commands:
result = invoke(command, ['verify', directory, '--key', key], ok)
if ok: assert 'VERIFIED FDS/OS 0.1.0' in result
if ok:
expected=json.loads((directory/'manifest.json').read_text())['version']
assert f'VERIFIED FDS/OS {expected}' in result
key_prefix = work / 'test-key'
invoke([str(host)], ['keygen', key_prefix])
@@ -55,6 +57,13 @@ manifest = {'format': 1, 'version': '0.1.0', 'source_epoch': 1,
invoke([str(host)], ['sign', release, '--key', private])
check_all(release, public)
invoke([str(host)], ['sign', release, '--key', private], False)
# Keep the historical release fixture, and also sign the current Git-derived identity.
current = work / 'current-version'; current.mkdir()
shutil.copy2(release / 'image.img', current / 'image.img')
current_manifest = dict(manifest, version=subprocess.check_output([str(project/'tools/version')], text=True).strip())
(current / 'manifest.json').write_text(json.dumps(current_manifest))
invoke([str(host)], ['sign', current, '--key', private])
check_all(current, public)
# Independent standard Ed25519 verification/signing over the exact domain prefix
# and raw manifest bytes. Test-only DER files are private and never printed.
@@ -111,7 +120,7 @@ for name in ['traversal', 'duplicate', 'unknown', 'wrong-version', 'unbounded']:
if name == 'traversal': value['files'][0]['name'] = '../image.img'
elif name == 'duplicate': value['files'].append(copy.deepcopy(value['files'][0]))
elif name == 'unknown': value['execute'] = 'sh'
elif name == 'wrong-version': value['version'] = '999'
elif name == 'wrong-version': value['version'] = '../invalid'
(bad / 'manifest.json').write_text(json.dumps(value) + (' ' * (1024 * 1024) if name == 'unbounded' else ''))
(bad / 'manifest.sig').write_bytes((release / 'manifest.sig').read_bytes())
check_all(bad, public, False)
+4 -3
View File
@@ -6,6 +6,7 @@ import subprocess
import tempfile
project = Path(__file__).resolve().parents[2]
version = subprocess.check_output([str(project/'tools/version')], text=True).strip()
def run(binary, *args, ok=True):
result = subprocess.run([str(project/'tools/in-void'), 'qemu-aarch64',
@@ -22,11 +23,11 @@ with tempfile.TemporaryDirectory(prefix='m3-', dir=project/'out') as directory:
for helper in ['fds', 'fds-burn', 'fds-inspect', 'fds-eject', 'fds-power',
'fds-stage0', 'fds-boottrace', 'fds-cartridged', 'fds-profile', 'fds-release']:
assert f'Usage: {helper}' in run(helper, '--help')
assert '0.1.0' in run(helper, '--version')
assert version in run(helper, '--version')
assert 'activate windowmaker' in run('fds-profile', '--help')
run('fds-profile', 'activate', 'windowmaker', ok=False)
assert '0.1.0' in run('fds', '--version')
assert '0.1.0' in run('fds-stage0', '--version')
assert version in run('fds', '--version')
assert version in run('fds-stage0', '--version')
run('fds-stage0', ok=False) # Must refuse root-switch operations outside PID 1.
assert json.loads(run('fds', '--json', 'info'))['target'] == 'aarch64 static-musl'
fixture = project/'tests/fixtures/manifests/windowmaker.toml'
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Check release, development, dirty, exported and cached Cargo version behavior."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
project = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project/'tools'))
from fds_version import version
class Versions(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix='fds-version-')
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.git('init', '-q')
self.git('config', 'user.name', 'FDS test')
self.git('config', 'user.email', 'test@example.invalid')
(self.root/'source').write_text('first\n')
self.commit()
def git(self, *args):
return subprocess.check_output(['git', '-C', str(self.root), *args], text=True).strip()
def commit(self):
self.git('add', '.')
self.git('commit', '-qm', 'fixture')
def test_git_versions(self):
revision=self.git('rev-parse','--short=12','HEAD')
self.assertEqual(version(self.root), f'0.dev.g{revision}')
self.git('tag','unrelated')
self.assertEqual(version(self.root), f'0.dev.g{revision}')
self.git('tag','-a','1.0','-m','Release 1.0')
self.assertEqual(version(self.root),'1.0')
(self.root/'source').write_text('changed\n')
self.assertEqual(version(self.root),'1.0.dirty')
self.commit()
self.assertEqual(version(self.root),f'1.0.r1.g{self.git("rev-parse","--short=12","HEAD")}')
self.git('tag','v1.1')
self.assertEqual(version(self.root),'1.1')
(self.root/'new').write_text('new\n')
self.assertEqual(version(self.root),'1.1')
self.git('add', 'new')
self.assertEqual(version(self.root),'1.1.dirty')
def test_export_is_independent_of_containing_git(self):
self.git('tag','1.0')
exported=self.root/'export';exported.mkdir()
(exported/'FDS_VERSION').write_text('0.dev.gabcdef123456\n')
self.assertEqual(version(exported),'0.dev.gabcdef123456')
(exported/'FDS_VERSION').write_text('../bad\n')
with self.assertRaises(ValueError):version(exported)
(exported/'FDS_VERSION').unlink()
with self.assertRaises(FileNotFoundError):version(exported)
def test_cargo_rebuilds_on_new_tags_and_edits(self):
(self.root/'tools').mkdir()
for name in ['version','fds_version.py','version-build.rs']:
shutil.copy2(project/'tools'/name,self.root/'tools'/name)
crate=self.root/'rust/probe';(crate/'src').mkdir(parents=True)
(self.root/'Cargo.toml').write_text('[workspace]\nmembers=["rust/probe"]\nresolver="2"\n')
(self.root/'.gitignore').write_text('/target/\n__pycache__/\n')
(crate/'Cargo.toml').write_text('[package]\nname="probe"\nversion="0.0.0"\nedition="2024"\nbuild="../../tools/version-build.rs"\n')
(crate/'src/main.rs').write_text('fn main() { println!("{}", env!("FDS_BUILD_VERSION")); }\n')
self.commit()
def run():
return subprocess.check_output(['cargo','run','--quiet','--offline','-p','probe'],cwd=self.root,text=True).strip()
self.assertEqual(run(),version(self.root))
self.commit() # Cargo generated its lockfile on the first run.
self.git('tag','1.0')
self.assertEqual(run(),'1.0')
(self.root/'source').write_text('edited\n')
self.assertEqual(run(),'1.0.dirty')
self.git('checkout','--','source')
self.assertEqual(run(),'1.0')
self.git('tag','-d','1.0')
self.assertEqual(run(),version(self.root))
# A frozen source tree builds with its saved identity without Git.
with tempfile.TemporaryDirectory(prefix='fds-export-') as directory:
exported=Path(directory)/'source'
shutil.copytree(self.root,exported,ignore=shutil.ignore_patterns('.git','target','__pycache__'))
(exported/'FDS_VERSION').write_text('1.0\n')
output=subprocess.check_output(['cargo','run','--quiet','--offline','-p','probe'],cwd=exported,text=True).strip()
self.assertEqual(output,'1.0')
if __name__=='__main__': unittest.main()
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Verify the current version in actual GPT/EROFS bytes and execute its ARM commands."""
from pathlib import Path
import hashlib
import json
import shlex
import subprocess
import sys
import tarfile
import tempfile
import tomllib
project=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(project/'tools'))
from fds_version import version
expected=version(project)
image=(project/'out/fds-system-cli.img').resolve(strict=True)
work=Path(tempfile.mkdtemp(prefix='version-image-test.',dir=project/'out'))
table=json.loads(subprocess.check_output(['sfdisk','--json',str(image)]))['partitiontable']
assert len(table['partitions'])==1 and table['partitions'][0]['name']=='FDS_SYSTEM'
offset=table['partitions'][0]['start']*table['sectorsize']
def extract(path,name):
target=work/name
subprocess.run([str(project/'tools/in-image-tools'),'fsck.erofs',f'--offset={offset}',f'--path=/{path}',f'--extract={target}',str(image)],check=True)
return target
identity=extract('usr/lib/os-release','os-release').read_text()
values=dict(line.split('=',1) for line in shlex.split(identity,comments=True))
assert values['VERSION_ID']==values['VERSION']==expected,values
assert values['PRETTY_NAME']==f'FDS/OS {expected}',values
assert extract('etc/issue','issue').read_text()==f'FDS/OS {expected}\n'
metadata=tomllib.loads(extract('FDS/CARTRIDGE.TOML','CARTRIDGE.TOML').read_text())
assert metadata['cartridge']['version']==expected,metadata
with tarfile.open(project/'out/rootfs-cli.tar') as archive:
assert archive.getmember('etc/os-release').linkname=='../usr/lib/os-release'
assert archive.extractfile('usr/lib/os-release').read().decode()==identity
commands={}
for name in ['fds','fds-control','fds-program','fds-boottrace','fds-cartridged','fds-profile','fds-burn','fds-release','fds-inspect','fds-eject','fds-power','dasungd']:
binary=extract(f'usr/bin/{name}',name)
output=subprocess.check_output([str(project/'tools/in-void'),'qemu-aarch64',str(binary),'--version'],text=True).strip()
assert output==f'{name} {expected}',output
commands[name]=output
with image.open('rb') as stream:
checksum=hashlib.file_digest(stream,'sha256').hexdigest()
record=dict(status='passed',version=expected,image=str(image),image_sha256=checksum,identity=values,cartridge_version=metadata['cartridge']['version'],arm_commands=commands,physical_hardware=False)
(work/'acceptance.json').write_text(json.dumps(record,indent=2)+'\n')
(project/'out/version-image-current.txt').write_text(str(work)+'\n')
print(f'PASS: {expected} in GPT/EROFS identity, cartridge metadata and all installed FDS commands: {work}')
+39 -2
View File
@@ -39,6 +39,16 @@ def invoke(*command, ok=True):
result = subprocess.run([cli, '--session', str(session), *map(str, command)], capture_output=True, text=True, timeout=240)
log.write(repr(command) + '\n' + result.stdout + result.stderr)
log.flush()
if (result.returncode == 0) != ok and command and command[0] == 'guest':
# Preserve live service evidence before the finally block stops the VM.
# Do not retry or relax the failed command's acceptance condition.
diagnostics = subprocess.run([
cli, '--session', str(session), 'guest', '--', 'sh', '-c',
'cat /run/log/cartridged/current; s6-svstat /run/service/cartridged; '
'dmesg | tail -40',
], capture_output=True, text=True, timeout=60)
log.write('Failure diagnostics:\n' + diagnostics.stdout + diagnostics.stderr)
log.flush()
assert (result.returncode == 0) == ok, (command, result.returncode, result.stdout, result.stderr)
return result.stdout
@@ -94,13 +104,17 @@ def interactive_program_checks():
os.write(master, b'\x03')
until(b'RETURN:130\r\n')
until(b'FDS> ')
os.write(master, b"b01:demo.report:shell -c 'printf \"STOP:%s\\n\" ready; exec tail -f /dev/null'\n")
os.write(master, b"b01:demo.report:shell -c 'printf \"STOP:%s\\n\" ready; read -r resumed; printf \"RESUMED:%s\\n\" \"$resumed\"; exec tail -f /dev/null'\n")
until(b'STOP:ready\r\n')
os.write(master, b'\x1a')
until(b'Stopped')
until(b'FDS> ')
os.write(master, b'fg\n')
until(b"exec tail -f /dev/null'\r\n")
# The fg command echo precedes terminal handoff and SIGCONT. Require
# actual input/output from the resumed foreground program before Ctrl-C.
os.write(master, b'continue\n')
until(b'RESUMED:continue\r\n')
os.write(master, b'\x03')
until(b'FDS> ')
os.write(master, b"test \"$(stty -g)\" = \"$(cat /tmp/program-tty-before)\" && printf 'RESTORE:%s\\n' passed\n")
@@ -146,6 +160,9 @@ original_software = digest(software)
try:
report = json.loads(invoke('start', *start_options))
assert report['qemu']['running']
initial_state = (session / 'session.json').read_bytes()
invoke('start', *start_options, ok=False)
assert (session / 'session.json').read_bytes() == initial_state
assert guest('id', '-u').strip() == '1000'
master, slave = pty.openpty()
original_termios = termios.tcgetattr(slave)
@@ -270,6 +287,24 @@ try:
guest('fds', 'run', 12, '--', 'demo.report:report', 'hold')
invoke('stop')
assert 'FDS_SHUTDOWN_FINAL' in (session / 'console.log').read_text()
retained_log = (session / 'console.log').read_bytes()
retained_data = {p: digest(p) for p in session.glob('data-*.qcow2')}
assert retained_data
previous_state = json.loads((session / 'session.json').read_text())
restarted = json.loads(invoke('start', *start_options))
assert restarted['qemu']['running'] and restarted['state']['cartridges'] == {}
assert restarted['state']['name'] != previous_state['name']
assert any(json.loads(p.read_text()) == previous_state for p in session.glob('previous-*.json'))
assert (session / 'console.log').read_bytes().startswith(retained_log)
assert all(digest(p) == value for p, value in retained_data.items())
assert guest('id', '-u').strip() == '1000'
assert all(b['state'] == 'empty' for b in query('bays')['bays'])
invoke('insert', 1, software); wait_bay(1, 'mounted_read_only')
assert 'Hello from' in guest('hello', 'restarted')
invoke('stop', '--force')
assert json.loads(invoke('start', *start_options))['qemu']['running']
assert guest('id', '-u').strip() == '1000'
invoke('stop')
finally:
subprocess.run([cli, '--session', str(session), 'stop', '--force'], capture_output=True, timeout=40)
@@ -330,7 +365,9 @@ record = dict(status='passed', ordinary_guest_uid=1000, public_emulator_cli=True
safe_eject=True, forced_removal_stops_consumer=True,
corrupt_program_and_catalogue_rejected=True, data_overlay_preserves_source=True,
restart_cleans_mounts_commands_and_consumers=True, extra_mount_blocks_safe=True,
native_shutdown_with_active_software=True, physical_pi='not tested')
native_shutdown_with_active_software=True, same_directory_restart_after_stop=True,
same_directory_restart_after_force=True, restart_retains_logs_overlays_and_mapping=True,
live_session_restart_rejected=True, physical_pi='not tested')
(work / 'acceptance.json').write_text(json.dumps(record, indent=2) + '\n')
(project / 'out/workstation-emulator-current.txt').write_text(str(work) + '\n')
print('PASS: public emulator, guest software and lifecycle acceptance:', work)
+12 -2
View File
@@ -14,7 +14,7 @@ from image_formats import gpt, LINUX_FILESYSTEM
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cli', type=Path, required=True)
parser.add_argument('--image-tool-runner', type=Path)
parser.add_argument('--void-packages', type=Path, default=project / 'vendor/void-packages')
parser.add_argument('--void-packages', type=Path, default=project / '.host/void-packages')
parser.add_argument('--xbps-tool-runner', type=Path)
parser.add_argument('--xbps-bin', type=Path)
args = parser.parse_args()
@@ -43,6 +43,15 @@ def digest(path):
return hashlib.file_digest(stream, 'sha256').hexdigest()
# A user-provided submodule path must fail before copying any package sources.
upstream = project / 'vendor/void-packages'
assert not subprocess.check_output(['git', '-C', str(upstream), 'status', '--porcelain', '--untracked-files=all'])
rejected = subprocess.run([cli[0], '--void-packages', str(upstream), 'software', 'build',
str(project / 'examples/software/hello/software.toml'), str(work / 'forbidden-build')],
capture_output=True, text=True)
assert rejected.returncode != 0 and 'outside Git submodules' in rejected.stderr, rejected.stderr
assert not (work / 'forbidden-build').exists()
for name in ['hello', 'report']:
recipe = project / f'examples/software/{name}/software.toml'
if name == 'report':
@@ -149,7 +158,8 @@ for case in ['program-corrupt', 'catalogue-mapping']:
malformed = work / (case + '.img')
gpt(malformed, [(part['name'], LINUX_FILESYSTEM, filesystem) for part, filesystem in zip(parts, selected)])
invoke(['inspect', malformed], False)
record = dict(status='passed', work=str(work), cli_sha256=digest(args.cli.resolve()), compiled_aarch64_software=True,
assert not subprocess.check_output(['git', '-C', str(upstream), 'status', '--porcelain', '--untracked-files=all'])
record = dict(source_checkout_clean=True, submodule_build_rejected=True, status='passed', work=str(work), cli_sha256=digest(args.cli.resolve()), compiled_aarch64_software=True,
void_source_packages=True, installed_runtime_dependencies=True, wrong_elf_and_escaping_symlink_rejected=True, shared_and_separate_payload_partitions=True,
repeat_image_identical=True, direct_installed_erofs_programs=True,
exact_and_larger_target_readback=True, wrong_confirmation_unchanged=True,
+3 -3
View File
@@ -8,10 +8,10 @@ source /etc/os-release
[[ ! $FDS_ROOT =~ [[:space:]] ]] || die 'xbps-src requires a checkout path without whitespace'
if [[ ${1:-} == --install-deps ]]; then
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
bubblewrap rustup ca-certificates findutils diffutils grep sed gawk util-linux python
fi
for cmd in bash git curl make file readelf ldd tar xz gzip zstd sha256sum bwrap rustup \
find diff grep sed awk flock install; do
find diff grep sed awk flock install python3; do
need "$cmd"
done
# Fail before downloading if unprivileged build containers cannot run.
@@ -21,7 +21,7 @@ mkdir -p out/downloads out/logs .host
if [[ ${FDS_OFFLINE:-0} != 1 ]]; then
git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=60 submodule update --init --depth 1 vendor/void-packages
fi
check_void_pin
check_void_source
source config/host-tools.conf
archive="out/downloads/${XBPS_STATIC_URL##*/}"
if [[ ! -f $archive ]]; then
+5 -2
View File
@@ -3,6 +3,7 @@
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
[[ $# == 0 ]] || die 'Usage: tools/build-base-packages'
cd "$FDS_ROOT"
fds_version=$(tools/version)
use_xbps
check_void_pin
mkdir -p out/logs out/packages out/manifests
@@ -12,7 +13,7 @@ flock -n 8 || die 'Another base package build is already running'
tools/build-fds-package
tools/build-kernel
tools/build-dasungd
input="$FDS_VOID/hostdir/sources/fds-init-0.1.0"
input="$FDS_VOID/hostdir/sources/fds-init-${fds_version}"
if [[ -d $input ]]; then
previous=$(mktemp -d "$FDS_ROOT/out/init-source.XXXXXX")
mv "$input" "$previous/"
@@ -35,7 +36,9 @@ for package in fds-dhcpcd fds-eink fds-base-files fds-init fds-base; do
xbps_src -a aarch64 clean "$package"
xbps_src -f -a aarch64 pkg "$package"
) 2>&1 | tee "out/logs/xbps-$package.log"
cp "$FDS_VOID/hostdir/binpkgs/$package-"*.aarch64.xbps out/packages/
package_version=$fds_version
[[ $package != fds-dhcpcd ]] || package_version=10.3.2
tools/export-package "$FDS_VOID/hostdir/binpkgs/${package}-${package_version}_1.aarch64.xbps"
done
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-*.aarch64.xbps
printf 'PASS: all FDS base packages exported to out/packages/\n'
+6 -6
View File
@@ -2,12 +2,13 @@
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
[[ $# == 0 ]] || die 'Usage: tools/build-dasungd'
cd "$FDS_ROOT"
fds_version=$(tools/version)
use_xbps
check_void_pin
mkdir -p out/logs out/manifests out/packages
# These are build-container tools; no Arch packages or host services are changed.
missing=()
for package in cross-aarch64-linux-musl pkg-config s6-rc; do
for package in cross-aarch64-linux-musl pkg-config s6-rc python3 git; do
if ! XBPS_ARCH=x86_64 xbps-query -r "$FDS_VOID/masterdir-x86_64" "$package" >/dev/null 2>&1; then
missing+=("$package")
fi
@@ -26,7 +27,7 @@ tools/in-void env LIBUSB_NO_PKG_CONFIG=1 LIBUDEV_NO_PKG_CONFIG=1 \
binary=target/aarch64-unknown-linux-musl/release/dasungd
tools/verify-elf "$binary" aarch64 static
cp -- "$binary" out/dasungd
input="$FDS_VOID/hostdir/sources/fds-dasungd-0.1.0"
input="$FDS_VOID/hostdir/sources/fds-dasungd-${fds_version}"
mkdir -p "$input"
cp -- "$binary" "$input/dasungd"
cp rust/dasungd/LICENSE rust/dasungd/profiles/paperlike13k-37hz.edid "$input/"
@@ -62,10 +63,9 @@ tools/prepare-void
cd "$FDS_VOID"
xbps_src -f -a aarch64 pkg fds-dasungd
) 2>&1 | tee out/logs/xbps-fds-dasungd.log
package=fds-dasungd-0.1.0_1.aarch64.xbps
cp "$FDS_VOID/hostdir/binpkgs/$package" out/packages/
XBPS_ARCH=aarch64 xbps-rindex -fa "out/packages/$package"
package="fds-dasungd-${fds_version}_1.aarch64.xbps"
tools/export-package "$FDS_VOID/hostdir/binpkgs/$package"
sha256sum out/dasungd "out/packages/$package" >out/manifests/dasung-artifacts.sha256
XBPS_ARCH=x86_64 xbps-query -r "$FDS_VOID/masterdir-x86_64" -l >out/manifests/dasung-build-packages.txt
sha256sum Cargo.toml Cargo.lock rust-toolchain.toml .cargo/config.toml tools/cargo-build tools/lib.sh tools/build-dasungd >out/manifests/dasung-rust-inputs.sha256
sha256sum tools/version tools/fds_version.py tools/version-build.rs Cargo.toml Cargo.lock rust-toolchain.toml .cargo/config.toml tools/cargo-build tools/lib.sh tools/build-dasungd >out/manifests/dasung-rust-inputs.sha256
printf 'PASS: Dasung static ARM executable, base package, and s6 database built\n'
+1 -1
View File
@@ -15,5 +15,5 @@ cp out/fds out/fds-power
sha256sum out/fds-control out/fds-program out/fds-release out/fds-burn out/fds-inspect out/fds-eject out/fds-power out/fds out/fds-stage0 out/fds-boottrace out/fds-cartridged out/fds-profile >out/manifests/fds-tools.sha256
find rust/fds-control rust/fds-software rust/fds-common rust/fds-cli rust/fds-stage0 rust/fds-boottrace rust/fds-cartridged rust/fds-burn rust/fds-release -type f -print0 | sort -z | \
xargs -0 sha256sum >out/manifests/fds-tools-inputs.sha256
sha256sum Cargo.toml Cargo.lock rust-toolchain.toml .cargo/config.toml tools/cargo-build tools/lib.sh tools/build-fds >>out/manifests/fds-tools-inputs.sha256
sha256sum tools/version tools/fds_version.py tools/version-build.rs Cargo.toml Cargo.lock rust-toolchain.toml .cargo/config.toml tools/cargo-build tools/lib.sh tools/build-fds >>out/manifests/fds-tools-inputs.sha256
printf 'PASS: static ARM FDS CLI and stage0 tools built\n'
+5 -6
View File
@@ -2,10 +2,11 @@
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
[[ $# == 0 ]] || die 'Usage: tools/build-fds-package'
cd "$FDS_ROOT"
fds_version=$(tools/version)
use_xbps
tools/build-fds
tools/prepare-void
input="$FDS_VOID/hostdir/sources/fds-cli-0.1.0"
input="$FDS_VOID/hostdir/sources/fds-cli-${fds_version}"
mkdir -p "$input" out/packages
cp out/fds out/fds-control out/fds-program out/fds-boottrace out/fds-burn out/fds-inspect out/fds-eject out/fds-power out/fds-release LICENSE "$input/"
notices=$(mktemp -d "$FDS_ROOT/out/rust-notices.XXXXXX")
@@ -17,9 +18,8 @@ cp "$notices/RUST-NOTICES.txt" "$input/"
xbps_src -a aarch64 clean fds-cli
xbps_src -f -a aarch64 pkg fds-cli
) 2>&1 | tee out/logs/xbps-fds-cli.log
cp "$FDS_VOID/hostdir/binpkgs/fds-cli-0.1.0_1.aarch64.xbps" out/packages/
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-cli-0.1.0_1.aarch64.xbps
input="$FDS_VOID/hostdir/sources/fds-cartridged-0.1.0"
tools/export-package "$FDS_VOID/hostdir/binpkgs/fds-cli-${fds_version}_1.aarch64.xbps"
input="$FDS_VOID/hostdir/sources/fds-cartridged-${fds_version}"
if [[ -d $input ]]; then
previous=$(mktemp -d "$FDS_ROOT/out/cartridged-source.XXXXXX")
mv "$input" "$previous/"
@@ -37,6 +37,5 @@ cp s6/source/boot/contents.d/cartridged "$input/s6-source/boot/contents.d/"
xbps_src -a aarch64 clean fds-cartridged
xbps_src -f -a aarch64 pkg fds-cartridged
) 2>&1 | tee out/logs/xbps-fds-cartridged.log
cp "$FDS_VOID/hostdir/binpkgs/fds-cartridged-0.1.0_1.aarch64.xbps" out/packages/
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-cartridged-0.1.0_1.aarch64.xbps
tools/export-package "$FDS_VOID/hostdir/binpkgs/fds-cartridged-${fds_version}_1.aarch64.xbps"
printf 'PASS: FDS CLI package built without a dynamic libc dependency\n'
+1
View File
@@ -69,6 +69,7 @@ if [[ ${FDS_OFFLINE:-0} == 1 ]]; then
fi
python3 tools/rootfs-archive "$root" "$work" "${archive_inputs[@]}"
{
printf 'version=%s\n' "$(tools/version)"
printf 'profile=%s\nvoid_commit=%s\n' "$profile" "$(cat VOID_PACKAGES_COMMIT)"
printf 'emulation=private-user-namespace-binfmt\n'
tools/in-void xbps-query -p pkgver qemu-user-aarch64
+18 -10
View File
@@ -1,15 +1,23 @@
#!/usr/bin/env bash
# Native Linux host binaries; independent of the Pi and Void build container.
# Build an installable Arch package without installing host packages implicitly.
set -euo pipefail
[[ $# == 0 ]] || { printf 'Usage: tools/build-workstation\n' >&2; exit 2; }
[[ $(uname -s) == Linux ]] || { printf 'A Linux workstation is required\n' >&2; exit 2; }
(( EUID != 0 )) || { printf 'Build as an ordinary user\n' >&2; exit 2; }
cd "$(dirname -- "${BASH_SOURCE[0]}")/.."
command -v cargo >/dev/null || { printf 'Install Rust with Cargo before building workstation tools\n' >&2; exit 2; }
fds_host_target=$(rustc -vV | sed -n 's/^host: //p')
[[ $fds_host_target == *-linux-* ]] || { printf 'Rust must target the native Linux host\n' >&2; exit 2; }
cargo build --locked --release --target "$fds_host_target" -p fds-workstation
mkdir -p out/workstation
install -m755 "target/$fds_host_target/release/fds-cartridge" out/workstation/fds-cartridge
install -m755 "target/$fds_host_target/release/fds-emulator" out/workstation/fds-emulator
printf 'Built native Linux tools in out/workstation/\n'
for tool in makepkg pacman cargo rustc python3 git; do
command -v "$tool" >/dev/null || { printf 'Missing %s; see docs/workstation.md\n' "$tool" >&2; exit 1; }
done
export FDS_SOURCE_DIR="$PWD"
mkdir -p out/workstation out/workstation-package
exec 9>out/.workstation-package.lock
flock -n 9 || { printf 'Another workstation package build is running\n' >&2; exit 1; }
cp packaging/arch/PKGBUILD out/workstation-package/PKGBUILD
export PKGDEST="$PWD/out/workstation"
cd out/workstation-package
# Dependencies are checked by their actual build commands. Runtime features
# are optional, and package installation is a separate explicit Make target.
makepkg --nodeps --force
mapfile -t packages < <(makepkg --packagelist)
(( ${#packages[@]} == 1 )) && [[ -s ${packages[0]} ]] || { printf 'Expected one fds-tools package\n' >&2; exit 1; }
printf '%s\n' "${packages[0]}" >../workstation/package-path.txt
printf 'Built Arch package: %s\nInstall with make workstation-install\n' "${packages[0]}"
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Native Linux host binaries; independent of the Pi and Void build container.
set -euo pipefail
[[ $# == 0 ]] || { printf 'Usage: tools/build-workstation-binaries\n' >&2; exit 2; }
[[ $(uname -s) == Linux ]] || { printf 'A Linux workstation is required\n' >&2; exit 2; }
(( EUID != 0 )) || { printf 'Build as an ordinary user\n' >&2; exit 2; }
cd "$(dirname -- "${BASH_SOURCE[0]}")/.."
command -v cargo >/dev/null || { printf 'Install Rust with Cargo before building workstation tools\n' >&2; exit 2; }
fds_host_target=$(rustc -vV | sed -n 's/^host: //p')
[[ $fds_host_target == *-linux-* ]] || { printf 'Rust must target the native Linux host\n' >&2; exit 2; }
cargo build --locked --release --target "$fds_host_target" -p fds-workstation
mkdir -p out/workstation
install -m755 "target/$fds_host_target/release/fds-cartridge" out/workstation/fds-cartridge
install -m755 "target/$fds_host_target/release/fds-emulator" out/workstation/fds-emulator
printf 'Built native Linux tools in out/workstation/\n'
+4 -2
View File
@@ -24,15 +24,17 @@ PREFIXES = (
'm9-images', 'm9-vm', 'm9-system', 'm10-vm', 'm11-vm', 'm11-faults',
'm12-clock', 'm12-development', 'm12-eeprom', 'm12-internal',
'm12-recovery', 'm12-release-contracts', 'm12-signing',
'rust-profiles', 'workstation-images', 'emu-test',
'rust-profiles', 'workstation-images', 'emu-test', 'arch-package-test', 'version-image-test',
)
GENERATED = re.compile(r'(?:' + '|'.join(PREFIXES) + r')\.[A-Za-z0-9_-]{6,8}')
POINTERS = (
'out/arch-package-current.txt',
'out/version-image-current.txt',
'out/workstation-images-current.txt',
'out/workstation-emulator-current.txt',
'out/manifests/dasung-s6-database.txt',
)
LOCKS = ('.clean.lock', '.rootfs.lock', '.base-packages.lock', '.image-tools.lock')
LOCKS = ('.clean.lock', '.rootfs.lock', '.base-packages.lock', '.image-tools.lock', '.workstation-package.lock')
def within(path, parent):
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Export one FDS XBPS package, retaining superseded builds outside the active index."""
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
project = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('package', type=Path)
args = parser.parse_args()
source = args.package.resolve(strict=True)
name, release = source.name.rsplit('-', 1)
if not name.startswith('fds-') or not release.endswith('.aarch64.xbps'):
parser.error('Expected an FDS AArch64 package')
out = project / 'out/packages'
out.mkdir(exist_ok=True)
old = [p for p in out.glob(f'{name}-*.aarch64.xbps') if p.name.rsplit('-', 1)[0] == name]
if old:
history = project / 'out/package-history'
history.mkdir(exist_ok=True)
saved = Path(tempfile.mkdtemp(prefix=f'{name}.', dir=history))
for path in old:
path.rename(saved / path.name)
# Prune entries for archived versions before indexing the selected package.
env = dict(os.environ, XBPS_ARCH='aarch64')
index = project / '.host/xbps/usr/bin/xbps-rindex'
subprocess.run([str(index), '-c', str(out)], env=env, check=True)
shutil.copy2(source, out / source.name)
subprocess.run([str(index), '-fa', str(out / source.name)], env=env, check=True)
print(f'Exported {source.name}; previous builds retained under out/package-history')
+59
View File
@@ -0,0 +1,59 @@
"""Canonical FDS build version, shared by Cargo, images and package builders."""
from pathlib import Path
import re
import subprocess
ROOT = Path(__file__).resolve().parents[1]
RELEASE = re.compile(r'v?([0-9]+(?:\.[0-9]+){1,2})\Z')
VALID = re.compile(r'[0-9][A-Za-z0-9.]{0,31}\Z')
def validate(value):
if not isinstance(value, str) or not VALID.fullmatch(value):
raise ValueError(f'Invalid FDS version: {value!r}')
return value
def git(root, *args):
return subprocess.check_output(['git', '-C', str(root), *args], text=True, stderr=subprocess.DEVNULL).strip()
def version(root=ROOT):
root = Path(root).resolve()
# Source exports carry the value resolved when the export was created.
# Never accidentally describe a containing repository during offline builds.
if not (root / '.git').exists():
return validate((root / 'FDS_VERSION').read_text().strip())
revision = git(root, 'rev-parse', '--short=12', 'HEAD')
tags = [tag for tag in git(root, 'tag', '--merged', 'HEAD').splitlines() if RELEASE.fullmatch(tag)]
if tags:
description = git(root, 'describe', '--tags', '--long', '--abbrev=12', *[f'--match={tag}' for tag in tags], 'HEAD')
tag, distance, commit = description.rsplit('-', 2)
value = RELEASE.fullmatch(tag)[1]
if int(distance):
value += f'.r{distance}.{commit}'
else:
value = f'0.dev.g{revision}'
if git(root, 'status', '--porcelain', '--untracked-files=no'):
value += '.dirty'
return validate(value)
def cargo(root=ROOT):
print(f'cargo:rustc-env=FDS_BUILD_VERSION={version(root)}')
for path in [root / 'tools/fds_version.py', root / 'tools/version', root / 'tools/version-build.rs']:
print(f'cargo:rerun-if-changed={path}')
if not (root / '.git').exists():
print(f'cargo:rerun-if-changed={root / "FDS_VERSION"}')
return
# Tag creation, ref changes, staging and edits must invalidate cached binaries.
for name in ['HEAD', 'index', 'packed-refs', 'refs']:
path = git(root, 'rev-parse', '--path-format=absolute', '--git-path', name)
print(f'cargo:rerun-if-changed={path}')
head = subprocess.run(['git', '-C', str(root), 'symbolic-ref', '-q', 'HEAD'], capture_output=True, text=True)
if head.returncode == 0:
print('cargo:rerun-if-changed=' + git(root, 'rev-parse', '--path-format=absolute', '--git-path', head.stdout.strip()))
paths = git(root, 'ls-files', '--cached', '--others', '--exclude-standard').splitlines()
for name in paths:
if name != 'vendor/void-packages':
print(f'cargo:rerun-if-changed={root / name}')
+18 -4
View File
@@ -12,11 +12,12 @@ import stat
import subprocess
import sys
import tomllib
from fds_version import version, validate
project = Path(__file__).resolve().parents[1]
OWNED = ('.cargo', '.gitignore', '.gitmodules', 'AGENTS.md', 'Cargo.lock',
'Cargo.toml', 'LICENSE', 'Makefile', 'README.md', 'VOID_PACKAGES_COMMIT',
'config', 'docs', 'image', 'packages', 'profiles', 'rust-toolchain.toml',
'config', 'docs', 'examples', 'image', 'packaging', 'packages', 'profiles', 'rust-toolchain.toml',
'rust', 's6', 'tests', 'tools')
@@ -79,7 +80,13 @@ def check_pin():
if pin != actual:
raise ValueError('Void checkout differs from its pin')
run('git', '-C', void, 'diff', '--exit-code', 'HEAD', '--')
return void, pin
build = project / '.host/void-packages'
if subprocess.check_output(['git', '-C', str(build), 'rev-parse', 'HEAD'], text=True).strip() != pin:
raise ValueError('Void build checkout differs from its pin')
run('git', '-C', build, 'diff', '--exit-code', 'HEAD', '--')
if subprocess.check_output(['git', '-C', str(void), 'status', '--porcelain', '--untracked-files=all'], text=True).strip():
raise ValueError('Void upstream submodule is dirty')
return build, pin
def add_package(source, destination):
@@ -113,6 +120,7 @@ def create(destination):
new_directory(destination)
for name in OWNED:
copy(project / name, destination / 'project' / name)
(destination / 'project/FDS_VERSION').write_text(version(project) + '\n')
run('git', '-C', void, 'bundle', 'create', destination / 'void.bundle', 'HEAD')
# Bundles carry commit objects, but do not retain a shallow checkout's
# boundary file. Without it, history-aware commands follow missing parents.
@@ -199,7 +207,7 @@ def create(destination):
('tar', ['tar', '--version']), ('rustup', ['rustup', '--version'])]:
host[name] = subprocess.check_output(command, text=True, stderr=subprocess.DEVNULL).splitlines()[0]
files = inventory(destination)
lock = {'format': 1, 'version': '0.1.0', 'void_commit': pin, 'source_epoch': epoch,
lock = {'format': 1, 'version': version(project), 'void_commit': pin, 'source_epoch': epoch,
'source_sha256': source_digest(files), 'rust_toolchain': toolchain_version,
'host_prerequisites': host, 'files': files}
(destination / 'lock.json').write_text(json.dumps(lock, indent=2) + '\n')
@@ -213,7 +221,7 @@ def verify(directory):
if lockfile.is_symlink() or not lockfile.is_file() or lockfile.stat().st_size > 64 * 1024 * 1024:
raise ValueError('Missing or invalid frozen input lock')
lock = json.loads(lockfile.read_text())
if lock.get('format') != 1 or lock.get('version') != '0.1.0':
if lock.get('format') != 1 or not validate(lock.get('version')):
raise ValueError('Unsupported frozen input format')
if (set(lock) != {'format', 'version', 'void_commit', 'source_epoch', 'source_sha256',
'rust_toolchain', 'host_prerequisites', 'files'}
@@ -245,6 +253,12 @@ def restore(source, destination):
epoch = int(subprocess.check_output(['git', '-C', str(void), 'show', '-s', '--format=%ct', 'HEAD']))
if epoch != lock['source_epoch']:
raise ValueError('Restored Void commit timestamp differs from the snapshot')
# Older frozen projects expect caches under vendor; their saved sources and
# release inputs remain unchanged. New projects use the separate workspace.
prepare = destination / 'tools/prepare-void-workspace'
if prepare.is_file():
run(sys.executable, prepare)
void = destination / '.host/void-packages'
copy(source / 'masterdir', void / 'masterdir-x86_64')
copy(source / 'sources', void / 'hostdir/sources')
copy(source / 'repositories/build', void / 'hostdir/frozen-repository')
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Only this explicit target changes the host package database.
set -euo pipefail
[[ $# == 0 ]] || { printf 'Usage: tools/install-workstation\n' >&2; exit 2; }
cd "$(dirname -- "${BASH_SOURCE[0]}")/.."
package=$(cat out/workstation/package-path.txt)
[[ -f $package ]] || { printf 'Build fds-tools with make workstation first\n' >&2; exit 1; }
if (( EUID == 0 )); then
exec pacman -U -- "$package"
else
exec sudo pacman -U -- "$package"
fi
+15 -6
View File
@@ -3,22 +3,31 @@
set -euo pipefail
export LC_ALL=C
FDS_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
FDS_VOID="$FDS_ROOT/vendor/void-packages"
FDS_VOID_SOURCE="$FDS_ROOT/vendor/void-packages"
FDS_VOID="$FDS_ROOT/.host/void-packages"
FDS_XBPS="$FDS_ROOT/.host/xbps/usr/bin"
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
need() { command -v "$1" >/dev/null || die "Missing host command: $1 (see docs/developer/build-host.md)"; }
check_void_pin() {
check_void_source() {
local pin actual
pin=$(cat "$FDS_ROOT/VOID_PACKAGES_COMMIT")
[[ $pin =~ ^[0-9a-f]{40}$ ]] || die 'Invalid VOID_PACKAGES_COMMIT'
[[ -f $FDS_VOID/xbps-src ]] || die 'Void submodule is missing; run make bootstrap'
actual=$(git -C "$FDS_VOID" rev-parse HEAD)
[[ -f $FDS_VOID_SOURCE/xbps-src ]] || die 'Void submodule is missing; run make bootstrap'
actual=$(git -C "$FDS_VOID_SOURCE" rev-parse HEAD)
[[ $actual == "$pin" ]] || die "Void checkout mismatch: expected $pin, found $actual"
git -C "$FDS_VOID" diff --quiet HEAD -- || die 'Tracked Void files were changed; keep FDS changes in packages/'
git -C "$FDS_VOID_SOURCE" diff --quiet HEAD -- || die 'Tracked Void files were changed; keep FDS changes in packages/'
export SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH=$(git -C "$FDS_VOID" show -s --format=%ct HEAD)
SOURCE_DATE_EPOCH=$(git -C "$FDS_VOID_SOURCE" show -s --format=%ct HEAD)
}
check_void_pin() {
check_void_source
[[ -z $(git -C "$FDS_VOID_SOURCE" status --porcelain --untracked-files=all) ]] || die 'Upstream Void submodule is dirty; run make bootstrap to migrate known build copies'
[[ -f $FDS_VOID/xbps-src ]] || die 'Void build checkout is missing; run make bootstrap'
[[ $(git -C "$FDS_VOID" rev-parse HEAD) == "$(cat "$FDS_ROOT/VOID_PACKAGES_COMMIT")" ]] || die 'Void build checkout pin changed; run make bootstrap'
git -C "$FDS_VOID" diff --quiet HEAD -- || die 'Tracked build-checkout files changed; keep FDS sources in packages/'
}
# Keep the upstream entry point unchanged. Offline builds retain normal
+4 -1
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env bash
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
[[ $# == 0 ]] || die 'Usage: tools/prepare-void'
"$FDS_ROOT/tools/prepare-void-workspace"
check_void_pin
"$FDS_ROOT/tools/version" >"$FDS_VOID/fds-version.next"
mv "$FDS_VOID/fds-version.next" "$FDS_VOID/fds-version"
# xbps-src reads this ignored local configuration instead of user-global settings.
if [[ -f $FDS_VOID/etc/conf ]] && ! cmp -s "$FDS_ROOT/config/xbps-src.conf" "$FDS_VOID/etc/conf"; then
die 'vendor/void-packages/etc/conf differs from config/xbps-src.conf; reconcile it explicitly'
die '.host/void-packages/etc/conf differs from config/xbps-src.conf; reconcile it explicitly'
fi
cp -- "$FDS_ROOT/config/xbps-src.conf" "$FDS_VOID/etc/conf"
# Only directories containing a template are active overlays.
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Prepare a writable Void build checkout outside the upstream submodule."""
import fcntl
import hashlib
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
def git(directory, *arguments):
return subprocess.check_output(['git', '-C', str(directory), *arguments], text=True).strip()
def inventory(root):
result = {}
for path in sorted(root.rglob('*')):
name = path.relative_to(root).as_posix()
if path.is_symlink():
result[name] = ('link', os.readlink(path))
elif path.is_file():
with path.open('rb') as stream:
result[name] = ('file', path.stat().st_mode & 0o777, hashlib.file_digest(stream, 'sha256').hexdigest())
elif path.is_dir():
result[name] = ('directory',)
else:
raise ValueError(f'Unexpected special source file: {path}')
return result
def prepare(project):
source = project / 'vendor/void-packages'
workspace = project / '.host/void-packages'
pin = (project / 'VOID_PACKAGES_COMMIT').read_text().strip()
if not re.fullmatch('[0-9a-f]{40}', pin) or git(source, 'rev-parse', 'HEAD') != pin:
raise ValueError('Upstream Void checkout differs from VOID_PACKAGES_COMMIT')
if git(source, 'diff', 'HEAD', '--'):
raise ValueError('Upstream Void has staged or tracked changes; preserve them outside the submodule first')
if source.is_symlink() or workspace.is_symlink():
raise ValueError('Void source and build checkout must be real directories')
# Migrate only known generated copies after comparing every entry. Unknown
# or independently edited source files are never deleted or overwritten.
overlays = {path.parent.name: path.parent for path in (project / 'packages').glob('*/template')}
for name in ('hello', 'report'):
example = project / f'examples/software/{name}/void'
if (example / 'template').is_file():
overlays[f'fds-demo-{name}'] = example
moves = []
duplicates = []
known = set()
for package, original in overlays.items():
old = source / 'srcpkgs' / package
if not old.exists() and not old.is_symlink():
continue
if git(source, 'ls-tree', 'HEAD', f'srcpkgs/{package}'):
raise ValueError(f'FDS overlay collides with upstream package: {package}')
if old.is_symlink() or not old.is_dir() or inventory(old) != inventory(original):
raise ValueError(f'Preserve independent edits in {old} before migrating it')
known.add(f'srcpkgs/{package}/')
new = workspace / 'srcpkgs' / package
if new.exists() or new.is_symlink():
if new.is_symlink() or not new.is_dir() or inventory(new) != inventory(original):
raise ValueError(f'Conflicting generated overlay: {new}')
duplicates.append(old)
else:
moves.append((old, new))
for name in git(source, 'ls-files', '--others', '--exclude-standard').splitlines():
if not any(name.startswith(prefix) for prefix in known):
raise ValueError(f'Untracked upstream file must be moved outside the submodule: {name}')
for old in [source / 'hostdir', *sorted(source.glob('masterdir-*')), source / 'etc/conf']:
if not old.exists() and not old.is_symlink():
continue
new = workspace / old.relative_to(source)
if old.is_symlink() or new.exists() or new.is_symlink():
raise ValueError(f'Cannot migrate {old}: symlink or existing destination {new}; reconcile explicitly')
if old == source / 'etc/conf' and old.read_bytes() != (project / 'config/xbps-src.conf').read_bytes():
raise ValueError('Old Void configuration differs from config/xbps-src.conf; reconcile explicitly')
moves.append((old, new))
if not workspace.exists():
# Independent objects keep this cache usable without Git alternates or
# hardlinks back into the submodule. No network access is needed.
with tempfile.TemporaryDirectory(prefix='void-checkout.', dir=workspace.parent) as temporary:
checkout = Path(temporary) / 'checkout'
subprocess.run(['git', 'clone', '--quiet', '--no-hardlinks', '--no-checkout', str(source), str(checkout)], check=True)
subprocess.run(['git', '-C', str(checkout), 'checkout', '--quiet', '--detach', pin], check=True)
subprocess.run(['git', '-C', str(checkout), 'remote', 'remove', 'origin'], check=True)
checkout.rename(workspace)
else:
if not (workspace / '.git').is_dir() or git(workspace, 'diff', 'HEAD', '--'):
raise ValueError('Build checkout has tracked changes; preserve them before preparing it')
if git(workspace, 'rev-parse', 'HEAD') != pin:
subprocess.run(['git', '-C', str(workspace), 'fetch', '--quiet', '--update-shallow', str(source), pin], check=True)
# Git refuses an update that would overwrite local files.
subprocess.run(['git', '-C', str(workspace), 'checkout', '--quiet', '--detach', pin], check=True)
for old, new in moves:
new.parent.mkdir(parents=True, exist_ok=True)
old.rename(new)
print(f'Moved {old.relative_to(project)} to {new.relative_to(project)}', flush=True)
for old in duplicates:
shutil.rmtree(old)
if git(source, 'status', '--porcelain', '--untracked-files=all'):
raise ValueError('Upstream submodule is not clean after preparation')
print('PASS: clean upstream reference; writable build checkout in .host/void-packages', flush=True)
def main():
project = Path(__file__).resolve().parents[1]
if len(sys.argv) != 1:
sys.exit('Usage: tools/prepare-void-workspace')
try:
(project / '.host').mkdir(exist_ok=True)
with (project / '.host/void-workspace.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
prepare(project)
except (OSError, ValueError, subprocess.CalledProcessError) as error:
sys.exit(f'ERROR: {error}')
if __name__ == '__main__':
main()
+2 -1
View File
@@ -2,6 +2,7 @@
import hashlib
import json
from pathlib import Path
from fds_version import validate
def digest(path):
@@ -42,7 +43,7 @@ def artifacts(project):
def read_lock(project):
path = Path(project) / '.host/frozen/lock.json'
lock = json.loads(path.read_text())
if lock.get('format') != 1 or lock.get('version') != '0.1.0':
if lock.get('format') != 1 or not validate(lock.get('version')):
raise ValueError('Build does not identify a supported frozen input snapshot')
return lock, digest(path)
+8
View File
@@ -6,6 +6,8 @@ import pathlib
import stat
import struct
import sys
import shlex
from fds_version import validate
from rootfs_lib import package_db, rooted
@@ -29,6 +31,12 @@ def audit(root, configured=True):
assert path.is_dir() and not path.is_symlink(), f"Missing directory: {name}"
assert stat.S_IMODE(path.stat().st_mode) == int(mode, 8), f"Wrong directory mode: {name}"
assert 'ID=fds\n' in (root / "usr/lib/os-release").read_text(), "Wrong OS identity"
identity = dict(line.split('=', 1) for line in shlex.split((root / 'usr/lib/os-release').read_text(), comments=True))
version = validate(identity['VERSION_ID'])
assert identity['VERSION'] == version and identity['PRETTY_NAME'] == f'FDS/OS {version}', 'Inconsistent OS version'
assert (root / 'etc/issue').read_text() == f'FDS/OS {version}\n', 'Inconsistent login version'
for package in ('fds-base', 'fds-base-files', 'fds-init', 'fds-cli', 'fds-cartridged', 'fds-dasungd', 'fds-eink'):
assert db[package]['pkgver'] == f'{package}-{version}_1', f'OS/package version mismatch: {package}'
release = (root / "usr/share/fds/kernel-release").read_text().strip()
assert (root / "usr/lib/modules" / release / "modules.dep").is_file(), "Missing matching kernel module index"
assert (root / "etc/shadow").stat().st_mode & 0o777 == 0o600, "Shadow file is exposed"
+4 -1
View File
@@ -26,7 +26,10 @@ trap 'rm -f "$log"' EXIT
dasungd --config /etc/dasungd.toml check
fds --json info | grep -q aarch64
fds-cartridged --help | grep -q SYSFS_ROOT
fds-release --version | grep -qx "fds-release 0.1.0"
. /etc/os-release
fds-release --version | grep -Fx "fds-release $VERSION_ID"
fds --version | grep -Fx "fds $VERSION_ID"
dasungd --version | grep -Fx "dasungd $VERSION_ID"
s6-rc-db -c /etc/s6-rc/compiled list all | grep -qx cartridged
s6-rc-db -c /etc/s6-rc/compiled list all | grep -qx dasungd
s6-rc-db -c /etc/s6-rc/compiled list all | grep -qx test-echo
+1 -1
View File
@@ -42,7 +42,7 @@ else
printf 'SKIP: ARM execution (optional qemu-aarch64 not installed); ELF verification passed\n'
fi
XBPS_ARCH=x86_64 xbps-query -r "$FDS_VOID/masterdir-x86_64" -l >out/manifests/void-build-packages.txt
find vendor/void-packages/hostdir -type f -name '*.xbps' -print0 \
find "$FDS_VOID/hostdir" -type f -name '*.xbps' -print0 \
| sort -z | xargs -0 -r sha256sum >out/manifests/void-package-inputs.sha256
sha256sum "$package" out/fds-smoketest >out/manifests/artifacts.sha256
printf 'PASS: M0 smoke test complete\n'

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