Package workstation tools, reuse emulator sessions, and derive build versions
This commit is contained in:
Generated
+13
-13
@@ -236,7 +236,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dasungd"
|
name = "dasungd"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -320,7 +320,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-boottrace"
|
name = "fds-boottrace"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-common",
|
"fds-common",
|
||||||
@@ -330,7 +330,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-burn"
|
name = "fds-burn"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-common",
|
"fds-common",
|
||||||
@@ -342,7 +342,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-cartridged"
|
name = "fds-cartridged"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-burn",
|
"fds-burn",
|
||||||
@@ -355,7 +355,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-cli"
|
name = "fds-cli"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-burn",
|
"fds-burn",
|
||||||
@@ -366,7 +366,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-common"
|
name = "fds-common"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -376,7 +376,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-control"
|
name = "fds-control"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-common",
|
"fds-common",
|
||||||
@@ -387,7 +387,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-release"
|
name = "fds-release"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
@@ -401,11 +401,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-smoketest"
|
name = "fds-smoketest"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-software"
|
name = "fds-software"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fds-common",
|
"fds-common",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -417,7 +417,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-stage0"
|
name = "fds-stage0"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-common",
|
"fds-common",
|
||||||
@@ -427,7 +427,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-test-helpers"
|
name = "fds-test-helpers"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -435,7 +435,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fds-workstation"
|
name = "fds-workstation"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fds-burn",
|
"fds-burn",
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ members = [
|
|||||||
"tests/helpers",
|
"tests/helpers",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
# Cargo requires three numeric components; public build versions come from Git.
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = { version = "4", features = ["derive"] }
|
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-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 Remove that output; keep current images, latest tests, caches and releases' \
|
||||||
'make clean-test Test cleanup protections using small disposable fixtures' \
|
'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 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 emulator-test Boot FDS and test cartridge hotplug with the public emulator' \
|
||||||
'make bootstrap Prepare Arch x86_64 host and Void build container' \
|
'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
|
./tools/smoke-test 2>&1 | tee out/logs/smoke-test.log
|
||||||
|
|
||||||
check:
|
check:
|
||||||
|
python3 tests/integration/version-checks.py
|
||||||
./tests/integration/m0-checks
|
./tests/integration/m0-checks
|
||||||
cargo fmt --all -- --check
|
cargo fmt --all -- --check
|
||||||
$(MAKE) clean-test
|
$(MAKE) clean-test
|
||||||
@@ -241,14 +244,23 @@ release-contract-test:
|
|||||||
@mkdir -p out/logs
|
@mkdir -p out/logs
|
||||||
python3 tests/integration/m12-release-build.py 2>&1 | tee out/logs/release-contract-checks.log
|
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:
|
workstation:
|
||||||
@mkdir -p out/logs
|
@mkdir -p out/logs
|
||||||
./tools/build-workstation 2>&1 | tee out/logs/workstation-build.log
|
./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
|
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
|
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
|
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 |
|
| 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 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 rootfs PROFILE=cli` | Builds the ARM base filesystem and its packages |
|
||||||
| `make system-card PROFILE=cli` | Creates a whole-disk SYSTEM cartridge image |
|
| `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.
|
expected outputs, emulator lifecycle and the confirmed USB-writing workflow.
|
||||||
Append `--help` to either native tool for its command reference; run `make help`
|
Append `--help` to either native tool for its command reference; run `make help`
|
||||||
for the complete build target list.
|
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)
|
- [troubleshooting](troubleshooting.md)
|
||||||
- [workstation-tooling-plan](workstation-tooling-plan.md)
|
- [workstation-tooling-plan](workstation-tooling-plan.md)
|
||||||
- [workstation](workstation.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
|
## 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
|
requirements are QEMU, erofs-utils, bubblewrap and native XBPS with a prepared
|
||||||
Void source checkout for software creation. `fds-cartridge` invokes xbps-src,
|
Void source checkout for software creation. `fds-cartridge` invokes xbps-src,
|
||||||
installs runtime dependencies and creates direct EROFS trees. The Rust tar reader
|
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
|
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
|
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;
|
commit, and 1–64 nonempty artifacts. Filenames must be flat safe ASCII names;
|
||||||
paths, duplicates, hidden names, reserved manifest filenames and unknown fields
|
paths, duplicates, hidden names, reserved manifest filenames and unknown fields
|
||||||
are rejected. The manifest is bounded to 1 MiB. Files must be regular files,
|
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.
|
normal OS continues to use glibc; no target musl runtime package is added.
|
||||||
|
|
||||||
For native Linux software packaging and QEMU hotplug, use `make workstation`
|
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.
|
`make tooling` below builds the static ARM runtime tools.
|
||||||
|
|
||||||
## Build and test
|
## 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)
|
See [user instructions](../workstation.md) and [format reference](../software-format.md)
|
||||||
for the supported interface. Historical xz acceptance remains in
|
for the supported interface. Historical xz acceptance remains in
|
||||||
[the earlier workstation record](workstation-validation.md).
|
[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.
|
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
|
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
|
how to write the same complete image to USB. All commands run from the checkout
|
||||||
root unless marked as guest commands.
|
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.
|
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
|
Run `start` again with the same session directory and boot options after `stop`.
|
||||||
session and can be [exported](workstation.md#save-emulator-data).
|
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
|
## Prepare the tools
|
||||||
|
|
||||||
The native tools are `fds-cartridge` and `fds-emulator`. Build them with Rust,
|
The native tools are `fds-cartridge` and `fds-emulator`. On Arch Linux, build
|
||||||
a C linker and Make; the repository selects its pinned Rust toolchain:
|
and install their `fds-tools` package from the checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
sudo pacman -S --needed base-devel rustup python git
|
||||||
make workstation
|
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"
|
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 |
|
| Prerequisite | Used for |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Prepared writable Void `void-packages` checkout and native XBPS utilities | Build source packages and install their AArch64 runtime dependencies |
|
| Prepared writable Void `void-packages` checkout and native XBPS utilities | Build source packages and install their AArch64 runtime dependencies |
|
||||||
@@ -138,7 +165,9 @@ fds-emulator --session out/my-emulator start \
|
|||||||
--system /path/to/fds-system-cli.img
|
--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
|
(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
|
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.
|
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
|
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
|
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.
|
`unplug BAY` simulates a physical pull, and `stop --force` cuts virtual power.
|
||||||
Use these only for deliberate failure simulation; DATA writes can be lost.
|
Use these only for deliberate failure simulation; DATA writes can be lost.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import shlex
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -12,6 +13,7 @@ import uuid
|
|||||||
|
|
||||||
project = Path(__file__).resolve().parents[1]
|
project = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(project/'tools'))
|
sys.path.insert(0, str(project/'tools'))
|
||||||
|
from fds_version import validate
|
||||||
from image_formats import digest, gpt, LINUX_FILESYSTEM, NAMESPACE
|
from image_formats import digest, gpt, LINUX_FILESYSTEM, NAMESPACE
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
@@ -21,6 +23,8 @@ parser.add_argument('--output-directory', type=Path)
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
rootfs = args.rootfs.resolve(strict=True)
|
rootfs = args.rootfs.resolve(strict=True)
|
||||||
with tarfile.open(rootfs) as archive:
|
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()
|
embedded = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
|
||||||
try:
|
try:
|
||||||
root = archive.getmember('.')
|
root = archive.getmember('.')
|
||||||
@@ -34,7 +38,7 @@ args.profile = embedded
|
|||||||
work = args.output_directory or Path(tempfile.mkdtemp(prefix='system-build.', dir=project/'out'))
|
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')
|
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']))
|
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:
|
with tarfile.open(rootfs) as source, tarfile.open(work/'system.tar', 'w', format=tarfile.PAX_FORMAT) as output:
|
||||||
for member in source:
|
for member in source:
|
||||||
if member.name in ('FDS', 'FDS/CARTRIDGE.TOML'): continue
|
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['rootfs_sha256'] = digest(rootfs)
|
||||||
layout['image_sha256'] = digest(work/'system.img')
|
layout['image_sha256'] = digest(work/'system.img')
|
||||||
layout['profile'] = args.profile
|
layout['profile'] = args.profile
|
||||||
|
layout['version'] = image_version
|
||||||
(work/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
|
(work/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
|
||||||
# An independent implementation checks the actual on-disk partition table.
|
# An independent implementation checks the actual on-disk partition table.
|
||||||
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(work/'system.img')]))['partitiontable']
|
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"
|
NAME="FDS/OS"
|
||||||
PRETTY_NAME="FDS/OS 0.1 (M2 development rootfs)"
|
PRETTY_NAME="FDS/OS @FDS_VERSION@"
|
||||||
ID=fds
|
ID=fds
|
||||||
ID_LIKE=void
|
ID_LIKE=void
|
||||||
VERSION_ID=0.1
|
VERSION_ID="@FDS_VERSION@"
|
||||||
VERSION="0.1 (M2)"
|
VERSION="@FDS_VERSION@"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=fds-base-files
|
pkgname=fds-base-files
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
depends="xbps-triggers"
|
depends="xbps-triggers"
|
||||||
@@ -22,6 +22,7 @@ do_install() {
|
|||||||
vinstall "${FILESDIR}/${f}" 600 etc
|
vinstall "${FILESDIR}/${f}" 600 etc
|
||||||
done
|
done
|
||||||
vinstall "${FILESDIR}/os-release" 644 usr/lib
|
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 ../usr/lib/os-release "${DESTDIR}/etc/os-release"
|
||||||
ln -s /run/fds/resolv.conf "${DESTDIR}/etc/resolv.conf"
|
ln -s /run/fds/resolv.conf "${DESTDIR}/etc/resolv.conf"
|
||||||
ln -s /proc/self/mounts "${DESTDIR}/etc/mtab"
|
ln -s /proc/self/mounts "${DESTDIR}/etc/mtab"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Explicit FDS userspace; never depend on Void's base-system metapackage.
|
# Explicit FDS userspace; never depend on Void's base-system metapackage.
|
||||||
pkgname=fds-base
|
pkgname=fds-base
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
metapackage=yes
|
metapackage=yes
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=fds-cartridged
|
pkgname=fds-cartridged
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
nostrip=yes
|
nostrip=yes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Static payloads are built and checked on the host before packaging.
|
# Static payloads are built and checked on the host before packaging.
|
||||||
pkgname=fds-cli
|
pkgname=fds-cli
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
nostrip=yes
|
nostrip=yes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# FDS-owned overlay. The build helper supplies the verified static ARM payload.
|
# FDS-owned overlay. The build helper supplies the verified static ARM payload.
|
||||||
pkgname=fds-dasungd
|
pkgname=fds-dasungd
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
nostrip=yes
|
nostrip=yes
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=fds-eink
|
pkgname=fds-eink
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
depends="xorg-server xf86-input-libinput WindowMaker terminus-font xterm xset xsetroot fontconfig"
|
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.
|
# Image-time s6 configuration. The native upstream binaries own PID 1.
|
||||||
pkgname=fds-init
|
pkgname=fds-init
|
||||||
version=0.1.0
|
version=$(cat "${XBPS_DISTDIR}/fds-version")
|
||||||
revision=1
|
revision=1
|
||||||
archs="aarch64"
|
archs="aarch64"
|
||||||
depends="iproute2 s6-linux-init s6-rc s6 execline coreutils util-linux bash eudev fds-dasungd fds-cli fds-cartridged"
|
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]
|
[package]
|
||||||
name = "dasungd"
|
name = "dasungd"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "DASUNG Paperlike USB keepalive, control socket and Linux display profile daemon"
|
description = "DASUNG Paperlike USB keepalive, control socket and Linux display profile daemon"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about)]
|
#[command(version = env!("FDS_BUILD_VERSION"), about)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(long, global = true, default_value = "/etc/dasungd.toml")]
|
#[arg(long, global = true, default_value = "/etc/dasungd.toml")]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
@@ -286,7 +286,7 @@ fn daemon(config: Config) -> Result<()> {
|
|||||||
let mut last_error = String::new();
|
let mut last_error = String::new();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"dasungd {} ready; waiting for {}",
|
"dasungd {} ready; waiting for {}",
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("FDS_BUILD_VERSION"),
|
||||||
config.monitor_serial
|
config.monitor_serial
|
||||||
);
|
);
|
||||||
while running.load(Ordering::Relaxed) {
|
while running.load(Ordering::Relaxed) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-boottrace"
|
name = "fds-boottrace"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Measured FDS boot events and regression reports"
|
description = "Measured FDS boot events and regression reports"
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ fn adopt() -> Result<()> {
|
|||||||
}
|
}
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Record and compare measured boot events",
|
about = "Record and compare measured boot events",
|
||||||
after_help = "Measurements use Linux CLOCK_BOOTTIME, excluding firmware and power-on."
|
after_help = "Measurements use Linux CLOCK_BOOTTIME, excluding firmware and power-on."
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-burn"
|
name = "fds-burn"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Verified cartridge images and protected FDS media writes"
|
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)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Create cartridge images and preview confirmed media writes",
|
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."
|
after_help = "Creation writes a new regular file. Writes require confirmation tied to the selected insertion and image hash."
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-cartridged"
|
name = "fds-cartridged"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "FDS event-driven cartridge and physical bay manager"
|
description = "FDS event-driven cartridge and physical bay manager"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::{path::PathBuf, process::ExitCode};
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Run the root cartridge service or inspect USB topology"
|
about = "Run the root cartridge service or inspect USB topology"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ fn session() -> Result<()> {
|
|||||||
}
|
}
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
name = "fds-profile",
|
||||||
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Control the desktop and network profile",
|
about = "Control the desktop and network profile",
|
||||||
after_help = "Examples: fds-profile activate windowmaker; fds-profile deactivate"
|
after_help = "Examples: fds-profile activate windowmaker; fds-profile deactivate"
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-cli"
|
name = "fds-cli"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "FDS system and cartridge command interface"
|
description = "FDS system and cartridge command interface"
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ pub struct Cli {
|
|||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Applet {
|
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),
|
Fds(FdsArgs),
|
||||||
/// Inspect a bay, image, or cartridge manifest.
|
/// Inspect a bay, image, or cartridge manifest.
|
||||||
#[command(version)]
|
#[command(version = env!("FDS_BUILD_VERSION"))]
|
||||||
FdsInspect {
|
FdsInspect {
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
json: bool,
|
json: bool,
|
||||||
@@ -23,7 +23,7 @@ pub enum Applet {
|
|||||||
args: InspectArgs,
|
args: InspectArgs,
|
||||||
},
|
},
|
||||||
/// Unmount a cartridge before removal.
|
/// Unmount a cartridge before removal.
|
||||||
#[command(version)]
|
#[command(version = env!("FDS_BUILD_VERSION"))]
|
||||||
FdsEject {
|
FdsEject {
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
json: bool,
|
json: bool,
|
||||||
@@ -31,7 +31,7 @@ pub enum Applet {
|
|||||||
args: BayArgs,
|
args: BayArgs,
|
||||||
},
|
},
|
||||||
/// Prepare native shutdown or inspect its status.
|
/// Prepare native shutdown or inspect its status.
|
||||||
#[command(version)]
|
#[command(version = env!("FDS_BUILD_VERSION"))]
|
||||||
FdsPower {
|
FdsPower {
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct Invocation {
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "fds-program",
|
name = "fds-program",
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
disable_help_subcommand = true,
|
disable_help_subcommand = true,
|
||||||
about = "Run a published cartridge command in the foreground"
|
about = "Run a published cartridge command in the foreground"
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-common"
|
name = "fds-common"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Shared bounded configuration and Linux discovery for FDS tools"
|
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};
|
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 const MAX_CONFIG_BYTES: u64 = 64 * 1024;
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-control"
|
name = "fds-control"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Native grayscale X11 cartridge control panel"
|
description = "Native grayscale X11 cartridge control panel"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "FDS cartridge control panel for X11",
|
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."
|
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]
|
[package]
|
||||||
name = "fds-release"
|
name = "fds-release"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "FDS release manifest signing and explicit-key verification"
|
description = "FDS release manifest signing and explicit-key verification"
|
||||||
|
|||||||
@@ -63,7 +63,13 @@ pub fn filename(value: &str) -> Result<()> {
|
|||||||
impl Manifest {
|
impl Manifest {
|
||||||
pub fn validate(&self) -> Result<()> {
|
pub fn validate(&self) -> Result<()> {
|
||||||
if self.format != 1
|
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
|
|| self.source_epoch == 0
|
||||||
|| !hex_valid(&self.source_sha256, 64)
|
|| !hex_valid(&self.source_sha256, 64)
|
||||||
|| !hex_valid(&self.void_commit, 40)
|
|| !hex_valid(&self.void_commit, 40)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::{path::PathBuf, process::ExitCode};
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Sign and verify FDS release artifacts",
|
about = "Sign and verify FDS release artifacts",
|
||||||
after_help = "Keys and signatures are never overwritten. Verification checks every listed artifact.
|
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."
|
An independently trusted public key is required; a bundled key is not automatically trusted."
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-smoketest"
|
name = "fds-smoketest"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-software"
|
name = "fds-software"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Verified installed software trees and legacy archive reading"
|
description = "Verified installed software trees and legacy archive reading"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-stage0"
|
name = "fds-stage0"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Static FDS early boot discovery and root handoff"
|
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 events = netlink()?; // Bind before scanning so insertion cannot fall into a gap.
|
||||||
let signals = child_events()?;
|
let signals = child_events()?;
|
||||||
let mut monitor = start_monitor(options.debug)?;
|
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");
|
println!("Commands while waiting: list, rescan, recovery, reboot, poweroff");
|
||||||
let mut previous = String::new();
|
let mut previous = String::new();
|
||||||
let mut pending = ConsoleInput::default();
|
let mut pending = ConsoleInput::default();
|
||||||
@@ -397,7 +400,10 @@ pub fn boot() -> Result<()> {
|
|||||||
if options.debug {
|
if options.debug {
|
||||||
println!("FDS_STAGE0_ROOT: {}", options.root_label());
|
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);
|
return handoff(monitor, options.debug);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{path::PathBuf, process::ExitCode};
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Early boot discovery and native s6 handoff",
|
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."
|
after_help = "With no arguments, root PID 1 boots FDS. Diagnostic options only read their supplied inputs."
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-workstation"
|
name = "fds-workstation"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Linux workstation software and cartridge tools"
|
description = "Linux workstation software and cartridge tools"
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ use fds_workstation::emulator::Session;
|
|||||||
use std::{path::PathBuf, process::ExitCode};
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
name = "fds-emulator",
|
||||||
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Boot FDS/OS and hotplug virtual USB cartridges on a Linux workstation",
|
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."
|
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 {
|
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")]
|
#[arg(long, global = true, default_value = "out/emulator")]
|
||||||
session: PathBuf,
|
session: PathBuf,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ use std::{
|
|||||||
fs::{self, File, OpenOptions},
|
fs::{self, File, OpenOptions},
|
||||||
io::Write,
|
io::Write,
|
||||||
os::{
|
os::{
|
||||||
fd::AsRawFd,
|
fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||||
unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
|
unix::{
|
||||||
|
fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
|
||||||
|
net::UnixStream,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
@@ -90,6 +93,43 @@ fn lock(root: &Path) -> Result<File> {
|
|||||||
}
|
}
|
||||||
Ok(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 {
|
impl Session {
|
||||||
pub fn load(root: &Path) -> Result<Self> {
|
pub fn load(root: &Path) -> Result<Self> {
|
||||||
ordinary()?;
|
ordinary()?;
|
||||||
@@ -159,17 +199,70 @@ impl Session {
|
|||||||
"Use a shorter session path (under 90 bytes), without commas or line breaks".into(),
|
"Use a shorter session path (under 90 bytes), without commas or line breaks".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
fs::DirBuilder::new()
|
let runner = runner.map(regular).transpose()?;
|
||||||
.mode(0o700)
|
let guard = match fs::DirBuilder::new().mode(0o700).create(&root) {
|
||||||
.create(&root)
|
Ok(()) => lock(&root)?,
|
||||||
.map_err(|e| {
|
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||||
Error(format!(
|
let previous = Self::load(&root)?;
|
||||||
"Create a new session directory {}: {e}",
|
if process(&root)?
|
||||||
root.display()
|
.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 {
|
let session = Self {
|
||||||
_lock: lock(&root)?,
|
_lock: guard,
|
||||||
root,
|
root,
|
||||||
state: State {
|
state: State {
|
||||||
format: 1,
|
format: 1,
|
||||||
@@ -177,7 +270,7 @@ impl Session {
|
|||||||
kernel,
|
kernel,
|
||||||
initramfs,
|
initramfs,
|
||||||
system,
|
system,
|
||||||
qemu_runner: runner.map(regular).transpose()?,
|
qemu_runner: runner,
|
||||||
cartridges: BTreeMap::new(),
|
cartridges: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -193,7 +286,7 @@ impl Session {
|
|||||||
.arg("-initrd").arg(&session.state.initramfs)
|
.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(["-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"])
|
.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)
|
crate::success(&mut command)
|
||||||
.map_err(|e| Error(format!("{e}; inspect {}/qemu.log", session.root.display())))?;
|
.map_err(|e| Error(format!("{e}; inspect {}/qemu.log", session.root.display())))?;
|
||||||
let boot = (|| -> Result<()> {
|
let boot = (|| -> Result<()> {
|
||||||
@@ -344,16 +437,26 @@ impl Session {
|
|||||||
}
|
}
|
||||||
pub fn stop(&self, force: bool) -> Result<Value> {
|
pub fn stop(&self, force: bool) -> Result<Value> {
|
||||||
let mut qmp = self.qmp()?;
|
let mut qmp = self.qmp()?;
|
||||||
if force {
|
let process = process(&self.root)?.ok_or_else(|| {
|
||||||
qmp.execute("quit", json!({}))?;
|
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 {
|
} else {
|
||||||
let output =
|
let output =
|
||||||
Serial::connect(&self.root, 120)?.command(&["fds".into(), "poweroff".into()])?;
|
Serial::connect(&self.root, 120)?.command(&["fds".into(), "poweroff".into()])?;
|
||||||
if output.status != 0 {
|
if output.status != 0 {
|
||||||
return Err(Error(format!("Guest refused shutdown: {}", output.output)));
|
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}))
|
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)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
version,
|
name = "fds-cartridge",
|
||||||
|
version = env!("FDS_BUILD_VERSION"),
|
||||||
about = "Build Void source packages and ready-to-run cartridge images on Linux"
|
about = "Build Void source packages and ready-to-run cartridge images on Linux"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fds-test-helpers"
|
name = "fds-test-helpers"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
|
build = "../../tools/version-build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
publish = false
|
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.
|
# Use a disposable minimal checkout to exercise pin and dirty-tree detection.
|
||||||
mkdir -p "$scratch/repo/tools" "$scratch/repo/vendor/void-packages"
|
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"
|
fake="$scratch/repo/vendor/void-packages"
|
||||||
git -C "$fake" init -q
|
git -C "$fake" init -q
|
||||||
printf '# fixture\n' >"$fake/xbps-src"
|
printf '# fixture\n' >"$fake/xbps-src"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
project = Path(__file__).resolve().parents[2]
|
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'))
|
loader = importlib.machinery.SourceFileLoader('fds_frozen_fixture', str(project / 'tools/frozen-inputs'))
|
||||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||||
frozen = importlib.util.module_from_spec(spec)
|
frozen = importlib.util.module_from_spec(spec)
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ def invoke(prefix, arguments, ok=True):
|
|||||||
def check_all(directory, key, ok=True):
|
def check_all(directory, key, ok=True):
|
||||||
for label, command in commands:
|
for label, command in commands:
|
||||||
result = invoke(command, ['verify', directory, '--key', key], ok)
|
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'
|
key_prefix = work / 'test-key'
|
||||||
invoke([str(host)], ['keygen', key_prefix])
|
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])
|
invoke([str(host)], ['sign', release, '--key', private])
|
||||||
check_all(release, public)
|
check_all(release, public)
|
||||||
invoke([str(host)], ['sign', release, '--key', private], False)
|
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
|
# Independent standard Ed25519 verification/signing over the exact domain prefix
|
||||||
# and raw manifest bytes. Test-only DER files are private and never printed.
|
# 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'
|
if name == 'traversal': value['files'][0]['name'] = '../image.img'
|
||||||
elif name == 'duplicate': value['files'].append(copy.deepcopy(value['files'][0]))
|
elif name == 'duplicate': value['files'].append(copy.deepcopy(value['files'][0]))
|
||||||
elif name == 'unknown': value['execute'] = 'sh'
|
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.json').write_text(json.dumps(value) + (' ' * (1024 * 1024) if name == 'unbounded' else ''))
|
||||||
(bad / 'manifest.sig').write_bytes((release / 'manifest.sig').read_bytes())
|
(bad / 'manifest.sig').write_bytes((release / 'manifest.sig').read_bytes())
|
||||||
check_all(bad, public, False)
|
check_all(bad, public, False)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
project = Path(__file__).resolve().parents[2]
|
project = Path(__file__).resolve().parents[2]
|
||||||
|
version = subprocess.check_output([str(project/'tools/version')], text=True).strip()
|
||||||
|
|
||||||
def run(binary, *args, ok=True):
|
def run(binary, *args, ok=True):
|
||||||
result = subprocess.run([str(project/'tools/in-void'), 'qemu-aarch64',
|
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',
|
for helper in ['fds', 'fds-burn', 'fds-inspect', 'fds-eject', 'fds-power',
|
||||||
'fds-stage0', 'fds-boottrace', 'fds-cartridged', 'fds-profile', 'fds-release']:
|
'fds-stage0', 'fds-boottrace', 'fds-cartridged', 'fds-profile', 'fds-release']:
|
||||||
assert f'Usage: {helper}' in run(helper, '--help')
|
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')
|
assert 'activate windowmaker' in run('fds-profile', '--help')
|
||||||
run('fds-profile', 'activate', 'windowmaker', ok=False)
|
run('fds-profile', 'activate', 'windowmaker', ok=False)
|
||||||
assert '0.1.0' in run('fds', '--version')
|
assert version in run('fds', '--version')
|
||||||
assert '0.1.0' in run('fds-stage0', '--version')
|
assert version in run('fds-stage0', '--version')
|
||||||
run('fds-stage0', ok=False) # Must refuse root-switch operations outside PID 1.
|
run('fds-stage0', ok=False) # Must refuse root-switch operations outside PID 1.
|
||||||
assert json.loads(run('fds', '--json', 'info'))['target'] == 'aarch64 static-musl'
|
assert json.loads(run('fds', '--json', 'info'))['target'] == 'aarch64 static-musl'
|
||||||
fixture = project/'tests/fixtures/manifests/windowmaker.toml'
|
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')
|
os.write(master, b'\x03')
|
||||||
until(b'RETURN:130\r\n')
|
until(b'RETURN:130\r\n')
|
||||||
until(b'FDS> ')
|
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')
|
until(b'STOP:ready\r\n')
|
||||||
os.write(master, b'\x1a')
|
os.write(master, b'\x1a')
|
||||||
until(b'Stopped')
|
until(b'Stopped')
|
||||||
until(b'FDS> ')
|
until(b'FDS> ')
|
||||||
os.write(master, b'fg\n')
|
os.write(master, b'fg\n')
|
||||||
until(b"exec tail -f /dev/null'\r\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')
|
os.write(master, b'\x03')
|
||||||
until(b'FDS> ')
|
until(b'FDS> ')
|
||||||
os.write(master, b"test \"$(stty -g)\" = \"$(cat /tmp/program-tty-before)\" && printf 'RESTORE:%s\\n' passed\n")
|
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:
|
try:
|
||||||
report = json.loads(invoke('start', *start_options))
|
report = json.loads(invoke('start', *start_options))
|
||||||
assert report['qemu']['running']
|
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'
|
assert guest('id', '-u').strip() == '1000'
|
||||||
master, slave = pty.openpty()
|
master, slave = pty.openpty()
|
||||||
original_termios = termios.tcgetattr(slave)
|
original_termios = termios.tcgetattr(slave)
|
||||||
@@ -280,6 +287,24 @@ try:
|
|||||||
guest('fds', 'run', 12, '--', 'demo.report:report', 'hold')
|
guest('fds', 'run', 12, '--', 'demo.report:report', 'hold')
|
||||||
invoke('stop')
|
invoke('stop')
|
||||||
assert 'FDS_SHUTDOWN_FINAL' in (session / 'console.log').read_text()
|
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:
|
finally:
|
||||||
subprocess.run([cli, '--session', str(session), 'stop', '--force'], capture_output=True, timeout=40)
|
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,
|
safe_eject=True, forced_removal_stops_consumer=True,
|
||||||
corrupt_program_and_catalogue_rejected=True, data_overlay_preserves_source=True,
|
corrupt_program_and_catalogue_rejected=True, data_overlay_preserves_source=True,
|
||||||
restart_cleans_mounts_commands_and_consumers=True, extra_mount_blocks_safe=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')
|
(work / 'acceptance.json').write_text(json.dumps(record, indent=2) + '\n')
|
||||||
(project / 'out/workstation-emulator-current.txt').write_text(str(work) + '\n')
|
(project / 'out/workstation-emulator-current.txt').write_text(str(work) + '\n')
|
||||||
print('PASS: public emulator, guest software and lifecycle acceptance:', work)
|
print('PASS: public emulator, guest software and lifecycle acceptance:', work)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
[[ $# == 0 ]] || die 'Usage: tools/build-base-packages'
|
[[ $# == 0 ]] || die 'Usage: tools/build-base-packages'
|
||||||
cd "$FDS_ROOT"
|
cd "$FDS_ROOT"
|
||||||
|
fds_version=$(tools/version)
|
||||||
use_xbps
|
use_xbps
|
||||||
check_void_pin
|
check_void_pin
|
||||||
mkdir -p out/logs out/packages out/manifests
|
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-fds-package
|
||||||
tools/build-kernel
|
tools/build-kernel
|
||||||
tools/build-dasungd
|
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
|
if [[ -d $input ]]; then
|
||||||
previous=$(mktemp -d "$FDS_ROOT/out/init-source.XXXXXX")
|
previous=$(mktemp -d "$FDS_ROOT/out/init-source.XXXXXX")
|
||||||
mv "$input" "$previous/"
|
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 -a aarch64 clean "$package"
|
||||||
xbps_src -f -a aarch64 pkg "$package"
|
xbps_src -f -a aarch64 pkg "$package"
|
||||||
) 2>&1 | tee "out/logs/xbps-$package.log"
|
) 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
|
done
|
||||||
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-*.aarch64.xbps
|
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-*.aarch64.xbps
|
||||||
printf 'PASS: all FDS base packages exported to out/packages/\n'
|
printf 'PASS: all FDS base packages exported to out/packages/\n'
|
||||||
|
|||||||
+6
-6
@@ -2,12 +2,13 @@
|
|||||||
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
[[ $# == 0 ]] || die 'Usage: tools/build-dasungd'
|
[[ $# == 0 ]] || die 'Usage: tools/build-dasungd'
|
||||||
cd "$FDS_ROOT"
|
cd "$FDS_ROOT"
|
||||||
|
fds_version=$(tools/version)
|
||||||
use_xbps
|
use_xbps
|
||||||
check_void_pin
|
check_void_pin
|
||||||
mkdir -p out/logs out/manifests out/packages
|
mkdir -p out/logs out/manifests out/packages
|
||||||
# These are build-container tools; no Arch packages or host services are changed.
|
# These are build-container tools; no Arch packages or host services are changed.
|
||||||
missing=()
|
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
|
if ! XBPS_ARCH=x86_64 xbps-query -r "$FDS_VOID/masterdir-x86_64" "$package" >/dev/null 2>&1; then
|
||||||
missing+=("$package")
|
missing+=("$package")
|
||||||
fi
|
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
|
binary=target/aarch64-unknown-linux-musl/release/dasungd
|
||||||
tools/verify-elf "$binary" aarch64 static
|
tools/verify-elf "$binary" aarch64 static
|
||||||
cp -- "$binary" out/dasungd
|
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"
|
mkdir -p "$input"
|
||||||
cp -- "$binary" "$input/dasungd"
|
cp -- "$binary" "$input/dasungd"
|
||||||
cp rust/dasungd/LICENSE rust/dasungd/profiles/paperlike13k-37hz.edid "$input/"
|
cp rust/dasungd/LICENSE rust/dasungd/profiles/paperlike13k-37hz.edid "$input/"
|
||||||
@@ -62,10 +63,9 @@ tools/prepare-void
|
|||||||
cd "$FDS_VOID"
|
cd "$FDS_VOID"
|
||||||
xbps_src -f -a aarch64 pkg fds-dasungd
|
xbps_src -f -a aarch64 pkg fds-dasungd
|
||||||
) 2>&1 | tee out/logs/xbps-fds-dasungd.log
|
) 2>&1 | tee out/logs/xbps-fds-dasungd.log
|
||||||
package=fds-dasungd-0.1.0_1.aarch64.xbps
|
package="fds-dasungd-${fds_version}_1.aarch64.xbps"
|
||||||
cp "$FDS_VOID/hostdir/binpkgs/$package" out/packages/
|
tools/export-package "$FDS_VOID/hostdir/binpkgs/$package"
|
||||||
XBPS_ARCH=aarch64 xbps-rindex -fa "out/packages/$package"
|
|
||||||
sha256sum out/dasungd "out/packages/$package" >out/manifests/dasung-artifacts.sha256
|
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
|
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'
|
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
|
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 | \
|
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
|
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'
|
printf 'PASS: static ARM FDS CLI and stage0 tools built\n'
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
source "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
|
||||||
[[ $# == 0 ]] || die 'Usage: tools/build-fds-package'
|
[[ $# == 0 ]] || die 'Usage: tools/build-fds-package'
|
||||||
cd "$FDS_ROOT"
|
cd "$FDS_ROOT"
|
||||||
|
fds_version=$(tools/version)
|
||||||
use_xbps
|
use_xbps
|
||||||
tools/build-fds
|
tools/build-fds
|
||||||
tools/prepare-void
|
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
|
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/"
|
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")
|
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 -a aarch64 clean fds-cli
|
||||||
xbps_src -f -a aarch64 pkg fds-cli
|
xbps_src -f -a aarch64 pkg fds-cli
|
||||||
) 2>&1 | tee out/logs/xbps-fds-cli.log
|
) 2>&1 | tee out/logs/xbps-fds-cli.log
|
||||||
cp "$FDS_VOID/hostdir/binpkgs/fds-cli-0.1.0_1.aarch64.xbps" out/packages/
|
tools/export-package "$FDS_VOID/hostdir/binpkgs/fds-cli-${fds_version}_1.aarch64.xbps"
|
||||||
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-cli-0.1.0_1.aarch64.xbps
|
input="$FDS_VOID/hostdir/sources/fds-cartridged-${fds_version}"
|
||||||
input="$FDS_VOID/hostdir/sources/fds-cartridged-0.1.0"
|
|
||||||
if [[ -d $input ]]; then
|
if [[ -d $input ]]; then
|
||||||
previous=$(mktemp -d "$FDS_ROOT/out/cartridged-source.XXXXXX")
|
previous=$(mktemp -d "$FDS_ROOT/out/cartridged-source.XXXXXX")
|
||||||
mv "$input" "$previous/"
|
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 -a aarch64 clean fds-cartridged
|
||||||
xbps_src -f -a aarch64 pkg fds-cartridged
|
xbps_src -f -a aarch64 pkg fds-cartridged
|
||||||
) 2>&1 | tee out/logs/xbps-fds-cartridged.log
|
) 2>&1 | tee out/logs/xbps-fds-cartridged.log
|
||||||
cp "$FDS_VOID/hostdir/binpkgs/fds-cartridged-0.1.0_1.aarch64.xbps" out/packages/
|
tools/export-package "$FDS_VOID/hostdir/binpkgs/fds-cartridged-${fds_version}_1.aarch64.xbps"
|
||||||
XBPS_ARCH=aarch64 xbps-rindex -fa out/packages/fds-cartridged-0.1.0_1.aarch64.xbps
|
|
||||||
printf 'PASS: FDS CLI package built without a dynamic libc dependency\n'
|
printf 'PASS: FDS CLI package built without a dynamic libc dependency\n'
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ if [[ ${FDS_OFFLINE:-0} == 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
python3 tools/rootfs-archive "$root" "$work" "${archive_inputs[@]}"
|
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 'profile=%s\nvoid_commit=%s\n' "$profile" "$(cat VOID_PACKAGES_COMMIT)"
|
||||||
printf 'emulation=private-user-namespace-binfmt\n'
|
printf 'emulation=private-user-namespace-binfmt\n'
|
||||||
tools/in-void xbps-query -p pkgver qemu-user-aarch64
|
tools/in-void xbps-query -p pkgver qemu-user-aarch64
|
||||||
|
|||||||
+18
-10
@@ -1,15 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
set -euo pipefail
|
||||||
[[ $# == 0 ]] || { printf 'Usage: tools/build-workstation\n' >&2; exit 2; }
|
[[ $# == 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; }
|
(( EUID != 0 )) || { printf 'Build as an ordinary user\n' >&2; exit 2; }
|
||||||
cd "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
cd "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||||
command -v cargo >/dev/null || { printf 'Install Rust with Cargo before building workstation tools\n' >&2; exit 2; }
|
for tool in makepkg pacman cargo rustc python3 git; do
|
||||||
fds_host_target=$(rustc -vV | sed -n 's/^host: //p')
|
command -v "$tool" >/dev/null || { printf 'Missing %s; see docs/workstation.md\n' "$tool" >&2; exit 1; }
|
||||||
[[ $fds_host_target == *-linux-* ]] || { printf 'Rust must target the native Linux host\n' >&2; exit 2; }
|
done
|
||||||
cargo build --locked --release --target "$fds_host_target" -p fds-workstation
|
export FDS_SOURCE_DIR="$PWD"
|
||||||
mkdir -p out/workstation
|
mkdir -p out/workstation out/workstation-package
|
||||||
install -m755 "target/$fds_host_target/release/fds-cartridge" out/workstation/fds-cartridge
|
exec 9>out/.workstation-package.lock
|
||||||
install -m755 "target/$fds_host_target/release/fds-emulator" out/workstation/fds-emulator
|
flock -n 9 || { printf 'Another workstation package build is running\n' >&2; exit 1; }
|
||||||
printf 'Built native Linux tools in out/workstation/\n'
|
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',
|
'm9-images', 'm9-vm', 'm9-system', 'm10-vm', 'm11-vm', 'm11-faults',
|
||||||
'm12-clock', 'm12-development', 'm12-eeprom', 'm12-internal',
|
'm12-clock', 'm12-development', 'm12-eeprom', 'm12-internal',
|
||||||
'm12-recovery', 'm12-release-contracts', 'm12-signing',
|
'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}')
|
GENERATED = re.compile(r'(?:' + '|'.join(PREFIXES) + r')\.[A-Za-z0-9_-]{6,8}')
|
||||||
POINTERS = (
|
POINTERS = (
|
||||||
|
'out/arch-package-current.txt',
|
||||||
|
'out/version-image-current.txt',
|
||||||
'out/workstation-images-current.txt',
|
'out/workstation-images-current.txt',
|
||||||
'out/workstation-emulator-current.txt',
|
'out/workstation-emulator-current.txt',
|
||||||
'out/manifests/dasung-s6-database.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):
|
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 subprocess
|
||||||
import sys
|
import sys
|
||||||
import tomllib
|
import tomllib
|
||||||
|
from fds_version import version, validate
|
||||||
|
|
||||||
project = Path(__file__).resolve().parents[1]
|
project = Path(__file__).resolve().parents[1]
|
||||||
OWNED = ('.cargo', '.gitignore', '.gitmodules', 'AGENTS.md', 'Cargo.lock',
|
OWNED = ('.cargo', '.gitignore', '.gitmodules', 'AGENTS.md', 'Cargo.lock',
|
||||||
'Cargo.toml', 'LICENSE', 'Makefile', 'README.md', 'VOID_PACKAGES_COMMIT',
|
'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')
|
'rust', 's6', 'tests', 'tools')
|
||||||
|
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ def create(destination):
|
|||||||
new_directory(destination)
|
new_directory(destination)
|
||||||
for name in OWNED:
|
for name in OWNED:
|
||||||
copy(project / name, destination / 'project' / name)
|
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')
|
run('git', '-C', void, 'bundle', 'create', destination / 'void.bundle', 'HEAD')
|
||||||
# Bundles carry commit objects, but do not retain a shallow checkout's
|
# Bundles carry commit objects, but do not retain a shallow checkout's
|
||||||
# boundary file. Without it, history-aware commands follow missing parents.
|
# boundary file. Without it, history-aware commands follow missing parents.
|
||||||
@@ -205,7 +207,7 @@ def create(destination):
|
|||||||
('tar', ['tar', '--version']), ('rustup', ['rustup', '--version'])]:
|
('tar', ['tar', '--version']), ('rustup', ['rustup', '--version'])]:
|
||||||
host[name] = subprocess.check_output(command, text=True, stderr=subprocess.DEVNULL).splitlines()[0]
|
host[name] = subprocess.check_output(command, text=True, stderr=subprocess.DEVNULL).splitlines()[0]
|
||||||
files = inventory(destination)
|
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,
|
'source_sha256': source_digest(files), 'rust_toolchain': toolchain_version,
|
||||||
'host_prerequisites': host, 'files': files}
|
'host_prerequisites': host, 'files': files}
|
||||||
(destination / 'lock.json').write_text(json.dumps(lock, indent=2) + '\n')
|
(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:
|
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')
|
raise ValueError('Missing or invalid frozen input lock')
|
||||||
lock = json.loads(lockfile.read_text())
|
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')
|
raise ValueError('Unsupported frozen input format')
|
||||||
if (set(lock) != {'format', 'version', 'void_commit', 'source_epoch', 'source_sha256',
|
if (set(lock) != {'format', 'version', 'void_commit', 'source_epoch', 'source_sha256',
|
||||||
'rust_toolchain', 'host_prerequisites', 'files'}
|
'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'
|
[[ $# == 0 ]] || die 'Usage: tools/prepare-void'
|
||||||
"$FDS_ROOT/tools/prepare-void-workspace"
|
"$FDS_ROOT/tools/prepare-void-workspace"
|
||||||
check_void_pin
|
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.
|
# 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
|
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'
|
die '.host/void-packages/etc/conf differs from config/xbps-src.conf; reconcile it explicitly'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from fds_version import validate
|
||||||
|
|
||||||
|
|
||||||
def digest(path):
|
def digest(path):
|
||||||
@@ -42,7 +43,7 @@ def artifacts(project):
|
|||||||
def read_lock(project):
|
def read_lock(project):
|
||||||
path = Path(project) / '.host/frozen/lock.json'
|
path = Path(project) / '.host/frozen/lock.json'
|
||||||
lock = json.loads(path.read_text())
|
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')
|
raise ValueError('Build does not identify a supported frozen input snapshot')
|
||||||
return lock, digest(path)
|
return lock, digest(path)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import pathlib
|
|||||||
import stat
|
import stat
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
import shlex
|
||||||
|
from fds_version import validate
|
||||||
|
|
||||||
from rootfs_lib import package_db, rooted
|
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 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 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"
|
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()
|
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 / "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"
|
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
|
dasungd --config /etc/dasungd.toml check
|
||||||
fds --json info | grep -q aarch64
|
fds --json info | grep -q aarch64
|
||||||
fds-cartridged --help | grep -q SYSFS_ROOT
|
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 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 dasungd
|
||||||
s6-rc-db -c /etc/s6-rc/compiled list all | grep -qx test-echo
|
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