diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbb1ab7..aec0b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,10 @@ permissions: # --------------------------------------------------------------------------------- # Two jobs. It was five, because `task ci:full` could not run on one hosted runner: the -# guest lanes needed the pinned QEMU, only executable inside the published runtime image -# (ADR-0025), and a container job gets no Docker daemon for the TestContainers lanes. -# Nothing in the merge gate needs QEMU now. The `gate` job below stays regardless — its -# reason never depended on the split. +# guest lanes needed a QEMU that was dynamically linked and so only executable inside the +# image that built it, and a container job gets no Docker daemon for the TestContainers +# lanes. QEMU is static now and nothing in the merge gate needs it anyway. The `gate` job +# below stays regardless — its reason never depended on the split. # --------------------------------------------------------------------------------- jobs: diff --git a/CLAUDE.md b/CLAUDE.md index 1803546..b4776ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,14 +147,14 @@ task db:dev:up|down # pinned Postgres 18 task db:plan -- # DDL for the current schema.sql change → migrations/ task db:apply PLAN= # apply a *saved* plan, never a recomputed one task db:verify # schema.sql → empty DB; assert the plan is empty -task qemu:build / qemu:verify / qemu:version / qemu:tools -task guest:kernel:fetch / guest:build / guest:verify +task machine # fetch the pinned spin-machine release into _output/ +task guest:build / guest:verify task demo:stage1 # Stage 1 end to end: a Linux guest boots off our qcow2 task backend:conformance # §6.1 object-store conformance (blocking per backend) ``` -QEMU is built by `.github/workflows/qemu.yml` (not the per-push gate), which calls the -same Taskfile targets a developer runs and publishes `ghcr.io///qemu:`. +QEMU, the guest kernel and the firmware are not built here: `task machine` fetches one +`spin-stack/spin-machine` release, pinned by version and SHA-256 in `Taskfile.yml`. Test object store: RustFS, pinned by digest, via TestContainers (`internal/testinfra`); the S3 SDK is confined to `internal/simio/real/s3*.go` behind `objectstore.Store`, and nothing else in the tree imports it. diff --git a/docs/plan/DECISIONS/ADR-0021-integration-with-spin.md b/docs/plan/DECISIONS/ADR-0021-integration-with-spin.md index 4aa59a7..94a665b 100644 --- a/docs/plan/DECISIONS/ADR-0021-integration-with-spin.md +++ b/docs/plan/DECISIONS/ADR-0021-integration-with-spin.md @@ -54,13 +54,6 @@ Everything else stays internal and stays free to change. A narrow surface is wha format work (a human-review zone) from becoming a compatibility obligation to another repository. -### 5. The guest kernel is consumed as an artefact, not as code - -storage's test lane needs a Linux guest that can issue FLUSH. spinbox already builds one. -storage consumes **the built kernel image**, pinned the way `RUSTFS_IMAGE` is pinned — a -build-time dependency on a binary, reversible by changing a path, with no Go import in -either direction. storage never builds a kernel. - ## What this does not decide - **When.** Integration starts when storage serves one volume end to end, not before. diff --git a/docs/plan/DECISIONS/ADR-0022-guest-kernel-pinned-and-mirrored.md b/docs/plan/DECISIONS/ADR-0022-guest-kernel-pinned-and-mirrored.md deleted file mode 100644 index 9c7a069..0000000 --- a/docs/plan/DECISIONS/ADR-0022-guest-kernel-pinned-and-mirrored.md +++ /dev/null @@ -1,58 +0,0 @@ -# ADR-0022 — the guest kernel is pinned by content and mirrored, not resolved by path - -- **Status:** Accepted — 2026-07-28 -- **Relates to:** ADR-0021 (storage consumes spinbox's artefacts, never its code), - the same pin-by-digest rule the object-store image follows - -## Decision - -storage does not build a kernel (ADR-0021). It names spinbox's artefact **by content**, -obtains it from whichever source has it, and verifies it before booting anything with it. - -1. **One canonical path:** `_output/guest/vmlinux`. `task guest:kernel:fetch` puts it there; - every other task and test reads it there. `SPINBOX_KERNEL` survives only as one *source - to copy from*, not as the place the lane looks. -2. **Pinned by sha256 in `Taskfile.yml`** (`GUEST_KERNEL_SHA256`), next to - `GUEST_KERNEL_VERSION`. Any source offering a kernel with a different hash is rejected, - loudly, with both hashes printed. Bumping the pin is a deliberate edit whose commit says - why the kernel moved — the same contract as `RUSTFS_IMAGE` and the pinned QEMU version. - The mirror therefore lags spinbox, deliberately: the lane wants the kernel it has - evaluated, not the newest one. -3. **Sources are tried in order:** a sibling spinbox checkout (no network, and what exists - today), then the mirrored image (what makes the lane runnable anywhere else). Failure - names every source it tried and what each one lacked. -4. **Mirroring is not building.** `Dockerfile.guest-kernel` packages the kernel as a single - file on `scratch`, so the image digest is a hash of the kernel and of nothing else. No - kernel source, `.config`, cross-toolchain or build flags enter this repository; the - mirror cannot produce a kernel spinbox did not produce first. -5. **What is verified is verified from the artefact itself.** `task guest:verify` reads the - config the kernel carries (`CONFIG_IKCONFIG=y` embeds it gzipped after the `IKCFG_ST` - marker) and asserts `CONFIG_VIRTIO_BLK`, `CONFIG_PVH`, `CONFIG_BLK_DEV_INITRD` and - `CONFIG_SERIAL_8250_CONSOLE`. A `kernel-config` file sitting next to the binary would be - easier to read and worthless: it does not travel with the artefact and can describe a - different kernel. Each check turns a boot-time symptom into a named failure — without - `VIRTIO_BLK` there is no `/dev/vda` and the guest's complaint reads exactly like a bug in - our backend; without `PVH` QEMU fails opening a ROM; without `BLK_DEV_INITRD` the - initramfs is ignored and PID 1 never runs; without the 8250 console the verdict the host - greps for is never printed. All four were proven to fail by planting them. - -**The external constraint that forces the mirror:** spinbox publishes no kernel. Its -workflows do not mention one; the artefact exists only as an untracked file under -`_output/` on whoever ran `task build:kernel` last, so there is nothing to point a URL at. -`GUEST_KERNEL_IMAGE` therefore defaults to empty — a machine with no checkout fails saying -the kernel is missing rather than pointing at a registry path that answers 404 — and -`task guest:kernel:push` still needs a decision about *where*: this repository's packages, -or spinbox's. - -## Rejected - -- **Build our own kernel.** ADR-0021 already rejected it: two kernels drift, and the one - spin boots is the one that matters. -- **Wait for spinbox to publish it.** Correct long-term and another repository's decision; - it would leave this lane undefendable meanwhile. If it lands, `GUEST_KERNEL_IMAGE` points - at spinbox's package and the mirror becomes redundant — nothing here is undone, which is - why the pin lives here rather than in the mirror. -- **Commit the kernel to this repository.** 37 MB of binary in git per bump, for an - artefact that is not ours and that a digest names just as precisely. -- **Trust the path and check nothing.** The status quo this replaced: it let "a real guest - issues FLUSH" become a claim about a file whose identity nobody had recorded. diff --git a/docs/plan/DECISIONS/ADR-0025-the-guest-lane-runs-in-the-qemu-runtime-image.md b/docs/plan/DECISIONS/ADR-0025-the-guest-lane-runs-in-the-qemu-runtime-image.md deleted file mode 100644 index 16b8f21..0000000 --- a/docs/plan/DECISIONS/ADR-0025-the-guest-lane-runs-in-the-qemu-runtime-image.md +++ /dev/null @@ -1,48 +0,0 @@ -# ADR-0025 — the guest lane's QEMU comes from the published runtime image, not from the runner's packages - -Accepted 2026-08-02; the mechanism was replaced on 2026-08-27, the principle is unchanged. -Relates to ADR-0022 (the guest kernel), `Dockerfile.qemu`, `.github/workflows/qemu.yml`. - -## The constraint - -The guest lane is the only test that proves a **real Linux kernel** issues -`VIRTIO_BLK_T_FLUSH` and that our backend's answer satisfies `fsync(2)`. `qemu.yml` -publishes a runtime image and the extracted binaries, so obtaining them is easy; the -difficulty is that they are dynamically linked against the `runtime` stage of -`Dockerfile.qemu` — `libglib2.0-0`, `libpixman-1-0`, `libcap-ng0`, `libseccomp2`, `libaio1`, -`liburing2`, `zlib1g`. Copying them onto a bare runner is not enough: the first CI run this -repository ever had died on `liburing.so.2: cannot open shared object file`. - -## Decision - -**One definition of the dependency set, and it is `Dockerfile.qemu`'s.** The runtime image is -the artefact of record — pinned by the same QEMU version `task qemu:verify` asserts — and the -lane's QEMU comes out of that image together with the libraries it was built against. `task -qemu:tools` computes the closure with `ldd` *inside* the image and copies the loader with it, -so the list is still the Dockerfile's and still moves when the image does. - -## Alternatives rejected - -- **Install the runtime libraries on the runner.** A second, hand-written list; two lists - drift silently, and the drift presents as *a QEMU that will not start*, inside the one lane - whose purpose is to tell us something else. Still refused. -- **Skip the lane when the image is missing, rather than fail the gate.** Decided twice, on - the grounds that "a red build that means *you did not build QEMU* trains people to ignore - red builds". Reversed 2026-08-05: `ci:full` exited 0 on every machine without QEMU while - running none of the proofs a real kernel carries, and the sentence people exchange is - "ci:full was green" — a skip notice nobody reads does not survive that sentence, and a - skipped job leaves its dependents free to run. Trained-to-ignore-red is a real cost; - believed-to-be-proven is a larger one, and the one this repository has actually paid. - **A missing lane input is a failure, never a skip.** -- **Run the lane as a container job inside the image** — this ADR's original mechanism, - withdrawn 2026-08-27. It cannot be had: the demos start the development Postgres through - Docker, and a container job gets no Docker daemon of its own. The `qemu:tools` wrapper - answers the same problem on an ordinary runner and was not invented for it. - -## Outside constraints - -- **No `/dev/kvm` on GitHub runners**, so the guest boots under TCG — slower than the ~1.1 s - measured on a developer machine, which is what the lane's `-timeout 15m` allows for. -- **The image exists only if `qemu.yml` has run.** That workflow is not part of the per-push - gate: it fires on `Dockerfile.qemu`, `Taskfile.yml` and itself, because the build takes tens - of minutes. A fresh clone has no image until it runs once. diff --git a/hack/demo-lib.sh b/hack/demo-lib.sh index 574e01f..5922553 100755 --- a/hack/demo-lib.sh +++ b/hack/demo-lib.sh @@ -53,7 +53,7 @@ SIZE=${SIZE:-268435456} # reaching a qcow2 through virtio, and that is true at either speed; refusing to run # without KVM would make the one command a human runs unrunnable on a developer outside # the `kvm` group and on every hosted CI runner. A host serving tenants is the opposite -# case, which is why the binary it runs has no TCG in it at all (Dockerfile.qemu). +# case, which is why the production binary in a release has no TCG in it at all. # # Chosen here rather than left to QEMU's `kvm:tcg` fallback list, which would pick the # same thing and say nothing. The silence is the problem: a machine that should have KVM diff --git a/hack/spin-machine b/hack/spin-machine index a866d87..00b548c 100755 --- a/hack/spin-machine +++ b/hack/spin-machine @@ -2,12 +2,8 @@ # # Put a spin-machine release under _output/, from wherever one is available. # -# This repository builds no QEMU and no kernel. It used to do both — a Dockerfile.qemu -# modelled on another project's, a device list that was byte-for-byte that project's, and a -# Dockerfile.guest-kernel that built nothing at all and existed only to move somebody -# else's kernel through a registry and back out. All three are gone: QEMU, the guest kernel -# and the firmware are one versioned artefact, and this is the only path by which one -# arrives. +# This repository builds no QEMU and no kernel. They are one versioned artefact, pinned by +# version and by SHA-256 in Taskfile.yml, and this is the only path by which one arrives. # # Two sources, in order: # diff --git a/taskfiles/machine.yml b/taskfiles/machine.yml index f93ad0e..55515b8 100644 --- a/taskfiles/machine.yml +++ b/taskfiles/machine.yml @@ -2,11 +2,9 @@ version: "3" # The machine this repository runs guests on: QEMU, the guest kernel, the firmware. # -# None of it is built here. It used to be — a Dockerfile.qemu modelled on another -# project's, a device list that was byte-for-byte that project's, and a -# Dockerfile.guest-kernel that built nothing and existed only to move somebody else's -# kernel through a registry and back out (ADR-0022). All of that has one home now, and -# `task machine` is the only path by which a machine arrives. +# None of it is built here, and none of it is mirrored here. QEMU, the guest kernel and +# the firmware are one versioned artefact from spin-machine, pinned by version and by +# SHA-256 below; `task machine` is the only path by which a machine arrives. tasks: