From f5d9f9640992027a0902ebb82465ab7e58a8b5fd Mon Sep 17 00:00:00 2001 From: Felis Date: Wed, 23 Sep 2026 03:45:48 +0800 Subject: [PATCH] Package workstation tools, reuse emulator sessions, and derive build versions --- Cargo.lock | 26 ++-- Cargo.toml | 4 + Makefile | 20 ++- README.md | 6 +- docs/developer/README.md | 2 + docs/developer/build-host.md | 5 +- docs/developer/releases.md | 2 +- docs/developer/tooling.md | 3 +- docs/developer/versioning.md | 78 ++++++++++++ docs/developer/workstation-tooling-plan.md | 5 + docs/developer/workstation.md | 6 +- docs/troubleshooting.md | 6 +- docs/workstation.md | 50 +++++++- image/build-system-cartridge | 7 +- packages/fds-base-files/files/issue | 2 +- packages/fds-base-files/files/os-release | 6 +- packages/fds-base-files/template | 3 +- packages/fds-base/template | 2 +- packages/fds-cartridged/template | 2 +- packages/fds-cli/template | 2 +- packages/fds-dasungd/template | 2 +- packages/fds-eink/template | 2 +- packages/fds-init/template | 2 +- packaging/arch/PKGBUILD | 52 ++++++++ rust/dasungd/Cargo.toml | 3 +- rust/dasungd/src/main.rs | 4 +- rust/fds-boottrace/Cargo.toml | 3 +- rust/fds-boottrace/src/main.rs | 2 +- rust/fds-burn/Cargo.toml | 3 +- rust/fds-burn/src/main.rs | 2 +- rust/fds-cartridged/Cargo.toml | 3 +- rust/fds-cartridged/src/main.rs | 2 +- rust/fds-cartridged/src/profile-main.rs | 3 +- rust/fds-cli/Cargo.toml | 3 +- rust/fds-cli/src/cli.rs | 8 +- rust/fds-cli/src/program.rs | 2 +- rust/fds-common/Cargo.toml | 3 +- rust/fds-common/src/lib.rs | 2 +- rust/fds-control/Cargo.toml | 3 +- rust/fds-control/src/main.rs | 2 +- rust/fds-release/Cargo.toml | 3 +- rust/fds-release/src/lib.rs | 8 +- rust/fds-release/src/main.rs | 2 +- rust/fds-smoketest/Cargo.toml | 3 +- rust/fds-software/Cargo.toml | 3 +- rust/fds-stage0/Cargo.toml | 3 +- rust/fds-stage0/src/linux.rs | 10 +- rust/fds-stage0/src/main.rs | 2 +- rust/fds-workstation/Cargo.toml | 3 +- rust/fds-workstation/src/emulator-main.rs | 5 +- rust/fds-workstation/src/emulator.rs | 137 ++++++++++++++++++--- rust/fds-workstation/src/main.rs | 3 +- tests/helpers/Cargo.toml | 3 +- tests/integration/arch-package.py | 37 ++++++ tests/integration/m0-checks | 3 +- tests/integration/m12-release-build.py | 1 + tests/integration/m12-signing.py | 13 +- tests/integration/m3-runtime.py | 7 +- tests/integration/version-checks.py | 91 ++++++++++++++ tests/integration/version-images.py | 49 ++++++++ tests/integration/workstation-emulator.py | 31 ++++- tools/build-base-packages | 7 +- tools/build-dasungd | 12 +- tools/build-fds | 2 +- tools/build-fds-package | 11 +- tools/build-rootfs | 1 + tools/build-workstation | 28 +++-- tools/build-workstation-binaries | 15 +++ tools/clean-builds | 6 +- tools/export-package | 33 +++++ tools/fds_version.py | 59 +++++++++ tools/frozen-inputs | 8 +- tools/install-workstation | 12 ++ tools/prepare-void | 2 + tools/release_artifacts.py | 3 +- tools/rootfs-audit | 8 ++ tools/rootfs-runtime-check | 5 +- tools/version | 18 +++ tools/version-build.rs | 20 +++ 79 files changed, 878 insertions(+), 132 deletions(-) create mode 100644 docs/developer/versioning.md create mode 100644 packaging/arch/PKGBUILD create mode 100644 tests/integration/arch-package.py create mode 100644 tests/integration/version-checks.py create mode 100644 tests/integration/version-images.py create mode 100755 tools/build-workstation-binaries create mode 100755 tools/export-package create mode 100644 tools/fds_version.py create mode 100755 tools/install-workstation create mode 100755 tools/version create mode 100644 tools/version-build.rs diff --git a/Cargo.lock b/Cargo.lock index f75937f..983b8e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 0e24c79..a60d8d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/Makefile b/Makefile index e6f367e..81a2cca 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 1d7700e..bf3aa60 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,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 +73,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. diff --git a/docs/developer/README.md b/docs/developer/README.md index 198bd44..2f026b1 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -80,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) diff --git a/docs/developer/build-host.md b/docs/developer/build-host.md index 18fbaa0..0c8c18e 100644 --- a/docs/developer/build-host.md +++ b/docs/developer/build-host.md @@ -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 diff --git a/docs/developer/releases.md b/docs/developer/releases.md index ed17e57..6aa11fa 100644 --- a/docs/developer/releases.md +++ b/docs/developer/releases.md @@ -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, diff --git a/docs/developer/tooling.md b/docs/developer/tooling.md index af48b21..9cd6e18 100644 --- a/docs/developer/tooling.md +++ b/docs/developer/tooling.md @@ -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 diff --git a/docs/developer/versioning.md b/docs/developer/versioning.md new file mode 100644 index 0000000..cb28ff2 --- /dev/null +++ b/docs/developer/versioning.md @@ -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. diff --git a/docs/developer/workstation-tooling-plan.md b/docs/developer/workstation-tooling-plan.md index cd0102d..ad0ae33 100644 --- a/docs/developer/workstation-tooling-plan.md +++ b/docs/developer/workstation-tooling-plan.md @@ -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. diff --git a/docs/developer/workstation.md b/docs/developer/workstation.md index 7b0bb23..bd883a5 100644 --- a/docs/developer/workstation.md +++ b/docs/developer/workstation.md @@ -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. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3bdca8a..90fc268 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -33,5 +33,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. diff --git a/docs/workstation.md b/docs/workstation.md index 64fdae2..b6f80e9 100644 --- a/docs/workstation.md +++ b/docs/workstation.md @@ -7,15 +7,42 @@ 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 writable Void `void-packages` checkout and native XBPS utilities | Build source packages and install their AArch64 runtime dependencies | @@ -138,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. @@ -181,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. diff --git a/image/build-system-cartridge b/image/build-system-cartridge index 1441eea..6c28a58 100755 --- a/image/build-system-cartridge +++ b/image/build-system-cartridge @@ -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'] diff --git a/packages/fds-base-files/files/issue b/packages/fds-base-files/files/issue index 5c53345..faf4900 100644 --- a/packages/fds-base-files/files/issue +++ b/packages/fds-base-files/files/issue @@ -1 +1 @@ -FDS/OS 0.1 +FDS/OS @FDS_VERSION@ diff --git a/packages/fds-base-files/files/os-release b/packages/fds-base-files/files/os-release index 2b13e8f..3a818f6 100644 --- a/packages/fds-base-files/files/os-release +++ b/packages/fds-base-files/files/os-release @@ -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@" diff --git a/packages/fds-base-files/template b/packages/fds-base-files/template index 10d0931..0528983 100644 --- a/packages/fds-base-files/template +++ b/packages/fds-base-files/template @@ -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" diff --git a/packages/fds-base/template b/packages/fds-base/template index f3108a1..9d1978c 100644 --- a/packages/fds-base/template +++ b/packages/fds-base/template @@ -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 diff --git a/packages/fds-cartridged/template b/packages/fds-cartridged/template index 2d4e761..99f550b 100644 --- a/packages/fds-cartridged/template +++ b/packages/fds-cartridged/template @@ -1,5 +1,5 @@ pkgname=fds-cartridged -version=0.1.0 +version=$(cat "${XBPS_DISTDIR}/fds-version") revision=1 archs="aarch64" nostrip=yes diff --git a/packages/fds-cli/template b/packages/fds-cli/template index 748c565..f5a22bc 100644 --- a/packages/fds-cli/template +++ b/packages/fds-cli/template @@ -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 diff --git a/packages/fds-dasungd/template b/packages/fds-dasungd/template index bb38f12..d361c05 100644 --- a/packages/fds-dasungd/template +++ b/packages/fds-dasungd/template @@ -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 diff --git a/packages/fds-eink/template b/packages/fds-eink/template index db008af..0f0b195 100644 --- a/packages/fds-eink/template +++ b/packages/fds-eink/template @@ -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" diff --git a/packages/fds-init/template b/packages/fds-init/template index 923d713..592f451 100644 --- a/packages/fds-init/template +++ b/packages/fds-init/template @@ -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" diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..f38fcaf --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -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" +} diff --git a/rust/dasungd/Cargo.toml b/rust/dasungd/Cargo.toml index 1457850..b63e2db 100644 --- a/rust/dasungd/Cargo.toml +++ b/rust/dasungd/Cargo.toml @@ -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" diff --git a/rust/dasungd/src/main.rs b/rust/dasungd/src/main.rs index e217e92..c5be6e6 100644 --- a/rust/dasungd/src/main.rs +++ b/rust/dasungd/src/main.rs @@ -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) { diff --git a/rust/fds-boottrace/Cargo.toml b/rust/fds-boottrace/Cargo.toml index 827d7d1..7ec66b0 100644 --- a/rust/fds-boottrace/Cargo.toml +++ b/rust/fds-boottrace/Cargo.toml @@ -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" diff --git a/rust/fds-boottrace/src/main.rs b/rust/fds-boottrace/src/main.rs index 83d641b..73da5de 100644 --- a/rust/fds-boottrace/src/main.rs +++ b/rust/fds-boottrace/src/main.rs @@ -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." )] diff --git a/rust/fds-burn/Cargo.toml b/rust/fds-burn/Cargo.toml index a045146..4dc027b 100644 --- a/rust/fds-burn/Cargo.toml +++ b/rust/fds-burn/Cargo.toml @@ -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" diff --git a/rust/fds-burn/src/main.rs b/rust/fds-burn/src/main.rs index b4c42b8..c07d061 100644 --- a/rust/fds-burn/src/main.rs +++ b/rust/fds-burn/src/main.rs @@ -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." )] diff --git a/rust/fds-cartridged/Cargo.toml b/rust/fds-cartridged/Cargo.toml index 87b62b6..690929d 100644 --- a/rust/fds-cartridged/Cargo.toml +++ b/rust/fds-cartridged/Cargo.toml @@ -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" diff --git a/rust/fds-cartridged/src/main.rs b/rust/fds-cartridged/src/main.rs index 06b91c9..40500ba 100644 --- a/rust/fds-cartridged/src/main.rs +++ b/rust/fds-cartridged/src/main.rs @@ -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 { diff --git a/rust/fds-cartridged/src/profile-main.rs b/rust/fds-cartridged/src/profile-main.rs index f655f5a..d405d02 100644 --- a/rust/fds-cartridged/src/profile-main.rs +++ b/rust/fds-cartridged/src/profile-main.rs @@ -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" )] diff --git a/rust/fds-cli/Cargo.toml b/rust/fds-cli/Cargo.toml index c66dd5b..f9bc0b4 100644 --- a/rust/fds-cli/Cargo.toml +++ b/rust/fds-cli/Cargo.toml @@ -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" diff --git a/rust/fds-cli/src/cli.rs b/rust/fds-cli/src/cli.rs index 90748a8..5958627 100644 --- a/rust/fds-cli/src/cli.rs +++ b/rust/fds-cli/src/cli.rs @@ -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, diff --git a/rust/fds-cli/src/program.rs b/rust/fds-cli/src/program.rs index eddbc19..979f8dc 100644 --- a/rust/fds-cli/src/program.rs +++ b/rust/fds-cli/src/program.rs @@ -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" )] diff --git a/rust/fds-common/Cargo.toml b/rust/fds-common/Cargo.toml index 22f99bc..494b4fc 100644 --- a/rust/fds-common/Cargo.toml +++ b/rust/fds-common/Cargo.toml @@ -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" diff --git a/rust/fds-common/src/lib.rs b/rust/fds-common/src/lib.rs index b7a8ab0..c5ef42c 100644 --- a/rust/fds-common/src/lib.rs +++ b/rust/fds-common/src/lib.rs @@ -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 = std::result::Result; diff --git a/rust/fds-control/Cargo.toml b/rust/fds-control/Cargo.toml index ccc9121..55d2f47 100644 --- a/rust/fds-control/Cargo.toml +++ b/rust/fds-control/Cargo.toml @@ -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" diff --git a/rust/fds-control/src/main.rs b/rust/fds-control/src/main.rs index 28a4111..2aa52e3 100644 --- a/rust/fds-control/src/main.rs +++ b/rust/fds-control/src/main.rs @@ -24,7 +24,7 @@ type Result = std::result::Result>; #[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." )] diff --git a/rust/fds-release/Cargo.toml b/rust/fds-release/Cargo.toml index 6872034..81e3471 100644 --- a/rust/fds-release/Cargo.toml +++ b/rust/fds-release/Cargo.toml @@ -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" diff --git a/rust/fds-release/src/lib.rs b/rust/fds-release/src/lib.rs index 4536027..242b139 100644 --- a/rust/fds-release/src/lib.rs +++ b/rust/fds-release/src/lib.rs @@ -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) diff --git a/rust/fds-release/src/main.rs b/rust/fds-release/src/main.rs index f647ca7..2dd4be3 100644 --- a/rust/fds-release/src/main.rs +++ b/rust/fds-release/src/main.rs @@ -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." diff --git a/rust/fds-smoketest/Cargo.toml b/rust/fds-smoketest/Cargo.toml index 921cb26..14ffa13 100644 --- a/rust/fds-smoketest/Cargo.toml +++ b/rust/fds-smoketest/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "fds-smoketest" -version = "0.1.0" +version.workspace = true +build = "../../tools/version-build.rs" edition = "2021" publish = false diff --git a/rust/fds-software/Cargo.toml b/rust/fds-software/Cargo.toml index 0242acf..b04a2f6 100644 --- a/rust/fds-software/Cargo.toml +++ b/rust/fds-software/Cargo.toml @@ -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" diff --git a/rust/fds-stage0/Cargo.toml b/rust/fds-stage0/Cargo.toml index 641202f..d6a4d8c 100644 --- a/rust/fds-stage0/Cargo.toml +++ b/rust/fds-stage0/Cargo.toml @@ -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" diff --git a/rust/fds-stage0/src/linux.rs b/rust/fds-stage0/src/linux.rs index a13e21c..b7d2410 100644 --- a/rust/fds-stage0/src/linux.rs +++ b/rust/fds-stage0/src/linux.rs @@ -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) => { diff --git a/rust/fds-stage0/src/main.rs b/rust/fds-stage0/src/main.rs index d2f7e2e..52bdffc 100644 --- a/rust/fds-stage0/src/main.rs +++ b/rust/fds-stage0/src/main.rs @@ -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." )] diff --git a/rust/fds-workstation/Cargo.toml b/rust/fds-workstation/Cargo.toml index fc6de14..70d25da 100644 --- a/rust/fds-workstation/Cargo.toml +++ b/rust/fds-workstation/Cargo.toml @@ -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" diff --git a/rust/fds-workstation/src/emulator-main.rs b/rust/fds-workstation/src/emulator-main.rs index bebae39..8bd7f6d 100644 --- a/rust/fds-workstation/src/emulator-main.rs +++ b/rust/fds-workstation/src/emulator-main.rs @@ -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)] diff --git a/rust/fds-workstation/src/emulator.rs b/rust/fds-workstation/src/emulator.rs index 42e89db..10d6a3b 100644 --- a/rust/fds-workstation/src/emulator.rs +++ b/rust/fds-workstation/src/emulator.rs @@ -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 { } 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> { + 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 { + 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 { 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 { 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})) } } diff --git a/rust/fds-workstation/src/main.rs b/rust/fds-workstation/src/main.rs index eb8adc5..dce8a16 100644 --- a/rust/fds-workstation/src/main.rs +++ b/rust/fds-workstation/src/main.rs @@ -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 { diff --git a/tests/helpers/Cargo.toml b/tests/helpers/Cargo.toml index 3a2e71b..b1f6004 100644 --- a/tests/helpers/Cargo.toml +++ b/tests/helpers/Cargo.toml @@ -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 diff --git a/tests/integration/arch-package.py b/tests/integration/arch-package.py new file mode 100644 index 0000000..db61a40 --- /dev/null +++ b/tests/integration/arch-package.py @@ -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) diff --git a/tests/integration/m0-checks b/tests/integration/m0-checks index 0df6a0b..17cd647 100755 --- a/tests/integration/m0-checks +++ b/tests/integration/m0-checks @@ -30,7 +30,8 @@ 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 tools/prepare-void-workspace "$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" diff --git a/tests/integration/m12-release-build.py b/tests/integration/m12-release-build.py index e281364..95ba85a 100644 --- a/tests/integration/m12-release-build.py +++ b/tests/integration/m12-release-build.py @@ -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) diff --git a/tests/integration/m12-signing.py b/tests/integration/m12-signing.py index 390b966..fb9bcdd 100644 --- a/tests/integration/m12-signing.py +++ b/tests/integration/m12-signing.py @@ -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) diff --git a/tests/integration/m3-runtime.py b/tests/integration/m3-runtime.py index f3e24f3..776832a 100755 --- a/tests/integration/m3-runtime.py +++ b/tests/integration/m3-runtime.py @@ -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' diff --git a/tests/integration/version-checks.py b/tests/integration/version-checks.py new file mode 100644 index 0000000..898c4a7 --- /dev/null +++ b/tests/integration/version-checks.py @@ -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() diff --git a/tests/integration/version-images.py b/tests/integration/version-images.py new file mode 100644 index 0000000..9396734 --- /dev/null +++ b/tests/integration/version-images.py @@ -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}') diff --git a/tests/integration/workstation-emulator.py b/tests/integration/workstation-emulator.py index fc67ea5..a58d693 100644 --- a/tests/integration/workstation-emulator.py +++ b/tests/integration/workstation-emulator.py @@ -104,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") @@ -156,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) @@ -280,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) @@ -340,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) diff --git a/tools/build-base-packages b/tools/build-base-packages index 3e3420b..9e7199c 100755 --- a/tools/build-base-packages +++ b/tools/build-base-packages @@ -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' diff --git a/tools/build-dasungd b/tools/build-dasungd index 9f80139..b20077e 100755 --- a/tools/build-dasungd +++ b/tools/build-dasungd @@ -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' diff --git a/tools/build-fds b/tools/build-fds index 9951e32..ca4c651 100755 --- a/tools/build-fds +++ b/tools/build-fds @@ -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' diff --git a/tools/build-fds-package b/tools/build-fds-package index 1f0db8b..90b4ca9 100755 --- a/tools/build-fds-package +++ b/tools/build-fds-package @@ -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' diff --git a/tools/build-rootfs b/tools/build-rootfs index c7bf27e..880de3e 100755 --- a/tools/build-rootfs +++ b/tools/build-rootfs @@ -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 diff --git a/tools/build-workstation b/tools/build-workstation index cc178e0..f90ea4c 100755 --- a/tools/build-workstation +++ b/tools/build-workstation @@ -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]}" diff --git a/tools/build-workstation-binaries b/tools/build-workstation-binaries new file mode 100755 index 0000000..5d5202d --- /dev/null +++ b/tools/build-workstation-binaries @@ -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' diff --git a/tools/clean-builds b/tools/clean-builds index bfbe65f..8c4611b 100755 --- a/tools/clean-builds +++ b/tools/clean-builds @@ -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): diff --git a/tools/export-package b/tools/export-package new file mode 100755 index 0000000..f874a21 --- /dev/null +++ b/tools/export-package @@ -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') diff --git a/tools/fds_version.py b/tools/fds_version.py new file mode 100644 index 0000000..259b4cb --- /dev/null +++ b/tools/fds_version.py @@ -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}') diff --git a/tools/frozen-inputs b/tools/frozen-inputs index 3354432..3bd5417 100755 --- a/tools/frozen-inputs +++ b/tools/frozen-inputs @@ -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') @@ -119,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. @@ -205,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') @@ -219,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'} diff --git a/tools/install-workstation b/tools/install-workstation new file mode 100755 index 0000000..bcedf60 --- /dev/null +++ b/tools/install-workstation @@ -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 diff --git a/tools/prepare-void b/tools/prepare-void index b7d97d8..31b9276 100755 --- a/tools/prepare-void +++ b/tools/prepare-void @@ -3,6 +3,8 @@ 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 '.host/void-packages/etc/conf differs from config/xbps-src.conf; reconcile it explicitly' diff --git a/tools/release_artifacts.py b/tools/release_artifacts.py index f80be1e..da93708 100644 --- a/tools/release_artifacts.py +++ b/tools/release_artifacts.py @@ -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) diff --git a/tools/rootfs-audit b/tools/rootfs-audit index d1dde68..8147dfb 100755 --- a/tools/rootfs-audit +++ b/tools/rootfs-audit @@ -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" diff --git a/tools/rootfs-runtime-check b/tools/rootfs-runtime-check index 9a81de4..7afa13f 100755 --- a/tools/rootfs-runtime-check +++ b/tools/rootfs-runtime-check @@ -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 diff --git a/tools/version b/tools/version new file mode 100755 index 0000000..28327d0 --- /dev/null +++ b/tools/version @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Print the FDS version derived from release tags or a Git source export.""" +import argparse +import subprocess +from pathlib import Path +from fds_version import ROOT, cargo, version + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--root', type=Path, default=ROOT) +parser.add_argument('--cargo', action='store_true', help='emit Cargo build metadata and change tracking') +args = parser.parse_args() +try: + if args.cargo: + cargo(args.root.resolve()) + else: + print(version(args.root)) +except (OSError, ValueError, subprocess.CalledProcessError) as error: + parser.exit(1, f'Cannot determine FDS version: {error}\n') diff --git a/tools/version-build.rs b/tools/version-build.rs new file mode 100644 index 0000000..a090987 --- /dev/null +++ b/tools/version-build.rs @@ -0,0 +1,20 @@ +// Every workspace crate uses the same resolver, including direct Cargo builds. +use std::{env, path::PathBuf, process::Command}; +fn main() { + let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let root = manifest.parent().unwrap().parent().unwrap(); + let output = Command::new("python3") + .arg(root.join("tools/version")) + .arg("--cargo") + .output() + .expect("Python 3 is required to resolve the FDS build version"); + assert!( + output.status.success(), + "FDS version discovery failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + print!( + "{}", + String::from_utf8(output.stdout).expect("Version metadata must be UTF-8") + ); +}