Package workstation tools, reuse emulator sessions, and derive build versions
This commit is contained in:
Generated
+13
-13
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+45
-5
@@ -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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -12,6 +13,7 @@ import uuid
|
||||
|
||||
project = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(project/'tools'))
|
||||
from fds_version import validate
|
||||
from image_formats import digest, gpt, LINUX_FILESYSTEM, NAMESPACE
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
@@ -21,6 +23,8 @@ parser.add_argument('--output-directory', type=Path)
|
||||
args = parser.parse_args()
|
||||
rootfs = args.rootfs.resolve(strict=True)
|
||||
with tarfile.open(rootfs) as archive:
|
||||
identity = dict(line.split('=', 1) for line in shlex.split(archive.extractfile('usr/lib/os-release').read().decode(), comments=True))
|
||||
image_version = validate(identity['VERSION_ID'])
|
||||
embedded = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
|
||||
try:
|
||||
root = archive.getmember('.')
|
||||
@@ -34,7 +38,7 @@ args.profile = embedded
|
||||
work = args.output_directory or Path(tempfile.mkdtemp(prefix='system-build.', dir=project/'out'))
|
||||
if not work.is_dir() or any(work.iterdir()): parser.error('output directory must exist and be empty')
|
||||
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
||||
manifest = f'''format = 1\n[cartridge]\nid = "fds.system.{args.profile}"\nname = "FDS/OS {args.profile.upper()}"\nclass = "system"\nversion = "0.1.0"\n[media]\nwritable = false\n'''.encode()
|
||||
manifest = f'''format = 1\n[cartridge]\nid = "fds.system.{args.profile}"\nname = "FDS/OS {args.profile.upper()}"\nclass = "system"\nversion = "{image_version}"\n[media]\nwritable = false\n'''.encode()
|
||||
with tarfile.open(rootfs) as source, tarfile.open(work/'system.tar', 'w', format=tarfile.PAX_FORMAT) as output:
|
||||
for member in source:
|
||||
if member.name in ('FDS', 'FDS/CARTRIDGE.TOML'): continue
|
||||
@@ -57,6 +61,7 @@ layout = gpt(work/'system.img', [('FDS_SYSTEM', LINUX_FILESYSTEM, work/'system.e
|
||||
layout['rootfs_sha256'] = digest(rootfs)
|
||||
layout['image_sha256'] = digest(work/'system.img')
|
||||
layout['profile'] = args.profile
|
||||
layout['version'] = image_version
|
||||
(work/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
|
||||
# An independent implementation checks the actual on-disk partition table.
|
||||
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(work/'system.img')]))['partitiontable']
|
||||
|
||||
@@ -1 +1 @@
|
||||
FDS/OS 0.1
|
||||
FDS/OS @FDS_VERSION@
|
||||
|
||||
@@ -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@"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=fds-base-files
|
||||
version=0.1.0
|
||||
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||
revision=1
|
||||
archs="aarch64"
|
||||
depends="xbps-triggers"
|
||||
@@ -22,6 +22,7 @@ do_install() {
|
||||
vinstall "${FILESDIR}/${f}" 600 etc
|
||||
done
|
||||
vinstall "${FILESDIR}/os-release" 644 usr/lib
|
||||
sed -i "s/@FDS_VERSION@/${version}/g" "${DESTDIR}/usr/lib/os-release" "${DESTDIR}/etc/issue"
|
||||
ln -s ../usr/lib/os-release "${DESTDIR}/etc/os-release"
|
||||
ln -s /run/fds/resolv.conf "${DESTDIR}/etc/resolv.conf"
|
||||
ln -s /proc/self/mounts "${DESTDIR}/etc/mtab"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Explicit FDS userspace; never depend on Void's base-system metapackage.
|
||||
pkgname=fds-base
|
||||
version=0.1.0
|
||||
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||
revision=1
|
||||
archs="aarch64"
|
||||
metapackage=yes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=fds-cartridged
|
||||
version=0.1.0
|
||||
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||
revision=1
|
||||
archs="aarch64"
|
||||
nostrip=yes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Static payloads are built and checked on the host before packaging.
|
||||
pkgname=fds-cli
|
||||
version=0.1.0
|
||||
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||
revision=1
|
||||
archs="aarch64"
|
||||
nostrip=yes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# FDS-owned overlay. The build helper supplies the verified static ARM payload.
|
||||
pkgname=fds-dasungd
|
||||
version=0.1.0
|
||||
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||
revision=1
|
||||
archs="aarch64"
|
||||
nostrip=yes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=fds-eink
|
||||
version=0.1.0
|
||||
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||
revision=1
|
||||
archs="aarch64"
|
||||
depends="xorg-server xf86-input-libinput WindowMaker terminus-font xterm xset xsetroot fontconfig"
|
||||
|
||||
@@ -1,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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -11,7 +11,7 @@ pub mod trace;
|
||||
|
||||
use std::{fmt, fs::File, io::Read, path::Path, str::FromStr};
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const VERSION: &str = env!("FDS_BUILD_VERSION");
|
||||
pub const MAX_CONFIG_BYTES: u64 = 64 * 1024;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -24,7 +24,7 @@ type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "FDS cartridge control panel for X11",
|
||||
after_help = "Select a bay to inspect its cartridge. Run opens a terminal; Eject stops its programs and releases the cartridge. Remove media only after SAFE appears."
|
||||
)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "fds-smoketest"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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."
|
||||
)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -12,8 +12,11 @@ use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::Write,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
|
||||
fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
unix::{
|
||||
fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
|
||||
net::UnixStream,
|
||||
},
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
@@ -90,6 +93,43 @@ fn lock(root: &Path) -> Result<File> {
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
// A pidfd remains attached to the original process even if its PID is reused.
|
||||
// QMP EOF alone does not establish that QEMU finished releasing its files.
|
||||
fn process(root: &Path) -> Result<Option<OwnedFd>> {
|
||||
let text = match fs::read_to_string(root.join("qemu.pid")) {
|
||||
Ok(text) => text,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let pid: i32 = text
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| Error("Invalid QEMU pid file".into()))?;
|
||||
if pid <= 0 {
|
||||
return Err(Error("Invalid QEMU pid file".into()));
|
||||
}
|
||||
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as i32;
|
||||
if fd < 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.raw_os_error() == Some(libc::ESRCH) {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(error.into());
|
||||
}
|
||||
Ok(Some(unsafe { OwnedFd::from_raw_fd(fd) }))
|
||||
}
|
||||
fn exited(process: &OwnedFd, timeout_ms: i32) -> Result<bool> {
|
||||
let mut event = libc::pollfd {
|
||||
fd: process.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
let result = unsafe { libc::poll(&mut event, 1, timeout_ms) };
|
||||
if result < 0 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(result > 0 && event.revents & libc::POLLIN != 0)
|
||||
}
|
||||
impl Session {
|
||||
pub fn load(root: &Path) -> Result<Self> {
|
||||
ordinary()?;
|
||||
@@ -159,17 +199,70 @@ impl Session {
|
||||
"Use a shorter session path (under 90 bytes), without commas or line breaks".into(),
|
||||
));
|
||||
}
|
||||
fs::DirBuilder::new()
|
||||
.mode(0o700)
|
||||
.create(&root)
|
||||
.map_err(|e| {
|
||||
Error(format!(
|
||||
"Create a new session directory {}: {e}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
let runner = runner.map(regular).transpose()?;
|
||||
let guard = match fs::DirBuilder::new().mode(0o700).create(&root) {
|
||||
Ok(()) => lock(&root)?,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
let previous = Self::load(&root)?;
|
||||
if process(&root)?
|
||||
.as_ref()
|
||||
.map(|pid| exited(pid, 0))
|
||||
.transpose()?
|
||||
== Some(false)
|
||||
{
|
||||
return Err(Error(
|
||||
"Session still has a live process; stop it before starting again".into(),
|
||||
));
|
||||
}
|
||||
for name in ["qmp.sock", "console.sock"] {
|
||||
match UnixStream::connect(root.join(name)) {
|
||||
Ok(_) => {
|
||||
return Err(Error(
|
||||
"Session still has an active socket; stop it before starting again"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
std::io::ErrorKind::NotFound
|
||||
| std::io::ErrorKind::ConnectionRefused
|
||||
) =>
|
||||
{
|
||||
()
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
// Keep the previous cartridge-to-overlay mapping as well as the
|
||||
// overlay files. A fresh boot starts with empty cartridge bays.
|
||||
let archive = root.join(format!(
|
||||
"previous-{}.json",
|
||||
image::hex(&image::random_id()?)
|
||||
));
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(0o600)
|
||||
.open(archive)?;
|
||||
file.write_all(
|
||||
&serde_json::to_vec_pretty(&previous.state)
|
||||
.map_err(|e| Error(e.to_string()))?,
|
||||
)?;
|
||||
file.sync_all()?;
|
||||
for name in ["qmp.sock", "console.sock", "qemu.pid"] {
|
||||
match fs::remove_file(root.join(name)) {
|
||||
Ok(()) => (),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
previous._lock
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let session = Self {
|
||||
_lock: lock(&root)?,
|
||||
_lock: guard,
|
||||
root,
|
||||
state: State {
|
||||
format: 1,
|
||||
@@ -177,7 +270,7 @@ impl Session {
|
||||
kernel,
|
||||
initramfs,
|
||||
system,
|
||||
qemu_runner: runner.map(regular).transpose()?,
|
||||
qemu_runner: runner,
|
||||
cartridges: BTreeMap::new(),
|
||||
},
|
||||
};
|
||||
@@ -193,7 +286,7 @@ impl Session {
|
||||
.arg("-initrd").arg(&session.state.initramfs)
|
||||
.args(["-append","console=ttyAMA0 rdinit=/init ro quiet loglevel=3 fds.emulator=1","-blockdev"]).arg(block.to_string())
|
||||
.args(["-device","virtio-blk-pci,drive=system","-device","qemu-xhci,id=xhci,addr=05.0,p2=12,p3=12"])
|
||||
.stdin(Stdio::null()).stdout(Stdio::null()).stderr(File::create(session.root.join("qemu.log"))?);
|
||||
.stdin(Stdio::null()).stdout(Stdio::null()).stderr(OpenOptions::new().create(true).append(true).mode(0o600).custom_flags(libc::O_NOFOLLOW).open(session.root.join("qemu.log"))?);
|
||||
crate::success(&mut command)
|
||||
.map_err(|e| Error(format!("{e}; inspect {}/qemu.log", session.root.display())))?;
|
||||
let boot = (|| -> Result<()> {
|
||||
@@ -344,16 +437,26 @@ impl Session {
|
||||
}
|
||||
pub fn stop(&self, force: bool) -> Result<Value> {
|
||||
let mut qmp = self.qmp()?;
|
||||
if force {
|
||||
qmp.execute("quit", json!({}))?;
|
||||
let process = process(&self.root)?.ok_or_else(|| {
|
||||
Error("QEMU process record is unavailable; cannot confirm shutdown".into())
|
||||
})?;
|
||||
let response = if force {
|
||||
// QEMU may reset QMP before acknowledging quit. The pidfd,
|
||||
// rather than transport EOF or a reply, establishes actual exit.
|
||||
qmp.execute("quit", json!({})).map(|_| ())
|
||||
} else {
|
||||
let output =
|
||||
Serial::connect(&self.root, 120)?.command(&["fds".into(), "poweroff".into()])?;
|
||||
if output.status != 0 {
|
||||
return Err(Error(format!("Guest refused shutdown: {}", output.output)));
|
||||
}
|
||||
qmp.closed()
|
||||
};
|
||||
if !exited(&process, 30_000)? {
|
||||
return Err(response
|
||||
.err()
|
||||
.unwrap_or_else(|| Error("QEMU has not exited; session remains in use".into())));
|
||||
}
|
||||
qmp.closed()?;
|
||||
Ok(json!({"stopped":self.root,"forced":force,"logs_and_data_overlays_retained":true}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
@@ -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}')
|
||||
@@ -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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||
[[ $# == 0 ]] || die 'Usage: tools/build-base-packages'
|
||||
cd "$FDS_ROOT"
|
||||
fds_version=$(tools/version)
|
||||
use_xbps
|
||||
check_void_pin
|
||||
mkdir -p out/logs out/packages out/manifests
|
||||
@@ -12,7 +13,7 @@ flock -n 8 || die 'Another base package build is already running'
|
||||
tools/build-fds-package
|
||||
tools/build-kernel
|
||||
tools/build-dasungd
|
||||
input="$FDS_VOID/hostdir/sources/fds-init-0.1.0"
|
||||
input="$FDS_VOID/hostdir/sources/fds-init-${fds_version}"
|
||||
if [[ -d $input ]]; then
|
||||
previous=$(mktemp -d "$FDS_ROOT/out/init-source.XXXXXX")
|
||||
mv "$input" "$previous/"
|
||||
@@ -35,7 +36,9 @@ for package in fds-dhcpcd fds-eink fds-base-files fds-init fds-base; do
|
||||
xbps_src -a aarch64 clean "$package"
|
||||
xbps_src -f -a aarch64 pkg "$package"
|
||||
) 2>&1 | tee "out/logs/xbps-$package.log"
|
||||
cp "$FDS_VOID/hostdir/binpkgs/$package-"*.aarch64.xbps out/packages/
|
||||
package_version=$fds_version
|
||||
[[ $package != fds-dhcpcd ]] || package_version=10.3.2
|
||||
tools/export-package "$FDS_VOID/hostdir/binpkgs/${package}-${package_version}_1.aarch64.xbps"
|
||||
done
|
||||
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-*.aarch64.xbps
|
||||
printf 'PASS: all FDS base packages exported to out/packages/\n'
|
||||
|
||||
+6
-6
@@ -2,12 +2,13 @@
|
||||
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||
[[ $# == 0 ]] || die 'Usage: tools/build-dasungd'
|
||||
cd "$FDS_ROOT"
|
||||
fds_version=$(tools/version)
|
||||
use_xbps
|
||||
check_void_pin
|
||||
mkdir -p out/logs out/manifests out/packages
|
||||
# These are build-container tools; no Arch packages or host services are changed.
|
||||
missing=()
|
||||
for package in cross-aarch64-linux-musl pkg-config s6-rc; do
|
||||
for package in cross-aarch64-linux-musl pkg-config s6-rc python3 git; do
|
||||
if ! XBPS_ARCH=x86_64 xbps-query -r "$FDS_VOID/masterdir-x86_64" "$package" >/dev/null 2>&1; then
|
||||
missing+=("$package")
|
||||
fi
|
||||
@@ -26,7 +27,7 @@ tools/in-void env LIBUSB_NO_PKG_CONFIG=1 LIBUDEV_NO_PKG_CONFIG=1 \
|
||||
binary=target/aarch64-unknown-linux-musl/release/dasungd
|
||||
tools/verify-elf "$binary" aarch64 static
|
||||
cp -- "$binary" out/dasungd
|
||||
input="$FDS_VOID/hostdir/sources/fds-dasungd-0.1.0"
|
||||
input="$FDS_VOID/hostdir/sources/fds-dasungd-${fds_version}"
|
||||
mkdir -p "$input"
|
||||
cp -- "$binary" "$input/dasungd"
|
||||
cp rust/dasungd/LICENSE rust/dasungd/profiles/paperlike13k-37hz.edid "$input/"
|
||||
@@ -62,10 +63,9 @@ tools/prepare-void
|
||||
cd "$FDS_VOID"
|
||||
xbps_src -f -a aarch64 pkg fds-dasungd
|
||||
) 2>&1 | tee out/logs/xbps-fds-dasungd.log
|
||||
package=fds-dasungd-0.1.0_1.aarch64.xbps
|
||||
cp "$FDS_VOID/hostdir/binpkgs/$package" out/packages/
|
||||
XBPS_ARCH=aarch64 xbps-rindex -fa "out/packages/$package"
|
||||
package="fds-dasungd-${fds_version}_1.aarch64.xbps"
|
||||
tools/export-package "$FDS_VOID/hostdir/binpkgs/$package"
|
||||
sha256sum out/dasungd "out/packages/$package" >out/manifests/dasung-artifacts.sha256
|
||||
XBPS_ARCH=x86_64 xbps-query -r "$FDS_VOID/masterdir-x86_64" -l >out/manifests/dasung-build-packages.txt
|
||||
sha256sum Cargo.toml Cargo.lock rust-toolchain.toml .cargo/config.toml tools/cargo-build tools/lib.sh tools/build-dasungd >out/manifests/dasung-rust-inputs.sha256
|
||||
sha256sum tools/version tools/fds_version.py tools/version-build.rs Cargo.toml Cargo.lock rust-toolchain.toml .cargo/config.toml tools/cargo-build tools/lib.sh tools/build-dasungd >out/manifests/dasung-rust-inputs.sha256
|
||||
printf 'PASS: Dasung static ARM executable, base package, and s6 database built\n'
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -69,6 +69,7 @@ if [[ ${FDS_OFFLINE:-0} == 1 ]]; then
|
||||
fi
|
||||
python3 tools/rootfs-archive "$root" "$work" "${archive_inputs[@]}"
|
||||
{
|
||||
printf 'version=%s\n' "$(tools/version)"
|
||||
printf 'profile=%s\nvoid_commit=%s\n' "$profile" "$(cat VOID_PACKAGES_COMMIT)"
|
||||
printf 'emulation=private-user-namespace-binfmt\n'
|
||||
tools/in-void xbps-query -p pkgver qemu-user-aarch64
|
||||
|
||||
+18
-10
@@ -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]}"
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Native Linux host binaries; independent of the Pi and Void build container.
|
||||
set -euo pipefail
|
||||
[[ $# == 0 ]] || { printf 'Usage: tools/build-workstation-binaries\n' >&2; exit 2; }
|
||||
[[ $(uname -s) == Linux ]] || { printf 'A Linux workstation is required\n' >&2; exit 2; }
|
||||
(( EUID != 0 )) || { printf 'Build as an ordinary user\n' >&2; exit 2; }
|
||||
cd "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
command -v cargo >/dev/null || { printf 'Install Rust with Cargo before building workstation tools\n' >&2; exit 2; }
|
||||
fds_host_target=$(rustc -vV | sed -n 's/^host: //p')
|
||||
[[ $fds_host_target == *-linux-* ]] || { printf 'Rust must target the native Linux host\n' >&2; exit 2; }
|
||||
cargo build --locked --release --target "$fds_host_target" -p fds-workstation
|
||||
mkdir -p out/workstation
|
||||
install -m755 "target/$fds_host_target/release/fds-cartridge" out/workstation/fds-cartridge
|
||||
install -m755 "target/$fds_host_target/release/fds-emulator" out/workstation/fds-emulator
|
||||
printf 'Built native Linux tools in out/workstation/\n'
|
||||
+4
-2
@@ -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):
|
||||
|
||||
Executable
+33
@@ -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')
|
||||
@@ -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}')
|
||||
+5
-3
@@ -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'}
|
||||
|
||||
Executable
+12
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+18
@@ -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')
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user