spin-machine

module
v0.0.0-...-bdc3ad0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: Apache-2.0

README

spin-machine

CI QEMU Kernel Base image

A virtual machine: QEMU, the guest kernel, the base image, and the definition of the machine they make.

The four are one thing, and the reason is not tidiness. A VM resumed from a checkpoint loads device and CPU state into a machine that has to be the same hardware as the one the checkpoint was saved on, and nothing checks that at run time. So the machine's identity is its contract — machine.Spec.Fingerprint hashes the versioned machine type (machine.MachineType, pc-q35-11.1), the shape, every device as the command line gives it, and the BIOS's size. Two machines with the same fingerprint can take each other's checkpoints, and two with different fingerprints cannot. A new QEMU of the same machine type, a new kernel or new firmware of the same size is the same machine: QEMU promises a newer release loads what an older one of the same machine type saved, and task boot:compat holds every release to it.

One repository, one version per release, and checkpoints that resume across releases until the machine itself changes.

What is deliberately not in a release: software that owns a guest. This repository builds a machine, and a release is not bootable on its own by design — whoever runs guests brings the initrd. Tests and performance probes may use caller-supplied diagnostic initrds or disposable guest helpers, but those are not release artifacts and must stay isolated from the published machine.

What it publishes

One tarball:

path under usr/share/spin-stack/
bin/qemu-system-x86_64 static; KVM only — it refuses to emulate, on purpose
bin/qemu-system-x86_64-tcg for CI, which has no /dev/kvm
bin/qemu-img
bin/mkfs.ext4 static e2fsprogs; read e2fsprogs/mke2fs.conf through MKE2FS_CONFIG
bin/debugfs, bin/e2fsck, bin/dumpe2fs the same e2fsprogs build; the boot/ probes edit a copy of the base image with this debugfs
e2fsprogs/mke2fs.conf the defaults an ext4 made for this kernel is made with
qemu/{qboot.bin,bios.bin,bios-256k.bin,pvh.bin,kvmvapic.bin,efi-virtio.rom} qboot.bin is the BIOS a machine boots with
qemu/patches/, qemu/qboot-*.patch, qemu/qboot-COPYING the changes this tree makes to QEMU and qboot, shipped with the binaries built from them
kernel/vmlinux plus kernel-config
image/rootfs.qcow2 read-only, 0444
machine.env the version, the commit it was built from, each file's checksum, and fingerprint_sha256, the contract that decides whether a checkpoint resumes
SOURCES every upstream source by version, URL and SHA-256, and the written offer
packages.txt every package and exact version in the base image

LICENSE and NOTICE sit at the root of the tarball.

task build writes that same tree into _output/, and machine.OpenRelease reads either. What a release has beyond a build directory is what hack/release creates or copies when it names one: machine.env and SOURCES it writes, and the QEMU and qboot patches under qemu/ it takes from this repository, not from the build. A tree without a manifest is still a release for every purpose except being named — the fingerprint decides whether checkpoints resume, and it is computed from the machine's definition and its BIOS, not from machine.env. _output/bin/ holds one thing more: spin-machine, which task tools builds for working in this repository and hack/release does not ship — a release is the machine, not the tool that boots it. There is one layout: nothing rearranges the files on the way out of a build, into a tarball or into a consumer. Let the three differ and what falls out of the translation between them is a path that exists and holds the previous release's kernel.

Using it

rel, err := machine.OpenRelease("/usr/share/spin-stack")  // says which file is missing, if one is
spec := rel.Spec()                                        // QEMU, Kernel, Firmware
// … the caller's initrd, memory, CPUs, disks, monitors
args, err := spec.Args()          // the QEMU command line
fp, err := spec.Fingerprint()     // which checkpoints this machine may resume

By hand, spin-machine (task tools) boots one, prints its command line or its fingerprint, attaches, detaches or saves on a running one, and restores a saved one; spin-machine <command> -h lists each command's flags.

To run a command inside a throwaway guest and read what it printed, boot it non-interactively. The guest's console is on stdin/stdout, so a script on stdin is what the guest's shell runs; poweroff -f ends the boot.

task build                                   # once; a boot reads _output/
printf '\n\n\n\n\n\n\n\n\n\nmount -t proc proc /proc\necho hello from $(uname -r)\npoweroff -f\n' |
  _output/bin/spin-machine boot --release _output \
    --init /bin/sh --console stdio --memory 1024 --cpus 2

--init /bin/sh skips systemd and runs the commands as PID 1 with nothing mounted, so the script mounts /proc itself — poweroff refuses without it. The leading blank lines are load-bearing: the guest's shell is not reading its console when QEMU starts feeding it, so the first bytes of the first line are lost, and the blanks absorb that instead of the first real command. How far that window reaches is host timing, not a line count — a quiet host may lose nothing — so treat the number of blanks as insurance, not a figure to trim: a blank costs one prompt. Nothing touches the base image — without --disk, QEMU boots it under a throwaway overlay (-snapshot).

Read the output by grepping a marker, since the guest's prompt and the kernel's boot chatter share stdout: | grep -a MARKER. The exit status is QEMU's, so a script that ends in poweroff -f returns 0.

What the guest answers is a property of the machine, not of the host that ran it. uname -r reports kernel/vmlinux's version, not the host kernel's. grep -c -E 'vmx|svm' /proc/cpuinfo is 0 because the machine removes both from the CPU it shows (-cpu host,migratable=on,-vmx,-svm: nothing inside a guest may nest, and exposing the flags would hand a guest's root the host's nested-virtualisation code), and there is no /dev/kvm to open because this kernel is built without KVM and without loadable modules. So a check run inside a guest can prove things about the guest and can never establish anything about its host's KVM — read those on the host.

Building

task build         # everything, into _output/
task shell         # boot the machine and look around inside it
task lint          # gofmt, vet, and whether the scripts and Taskfiles parse
task test          # the machine definition
task verify:args   # and whether the QEMU in _output/ accepts what it produces
task fingerprint   # this machine's identity
task release       # one tarball, one version

task is taskfile.dev, not GNU task, at the version go.mod pins: go tool task build runs it with nothing installed, and is what CI runs. The part builds run in containers (qemu/Dockerfile, kernel/Dockerfile, e2fsprogs/Dockerfile, image/Dockerfile), so a container engine is needed to build; a boot is not, and neither is task build — a consumer points --release at an unpacked release tree instead. A boot needs /dev/kvm readable and writable by whoever runs it, the same requirement docs/releasing.md states for the lab runner; a host without it boots the release's TCG build with --accel tcg, at perhaps fifty times the cost, which is why it is stated and never silently fallen back to.

Each part's targets live beside what they build — task qemu:build is next to qemu/Dockerfile — and the root Taskfile.yml holds the vars every part reads and the targets that cross all of them. Every version, digest and commit a build takes from outside is in versions.yaml: task versions says what is behind, task bump NAME=... moves one.

docs/machine.md the definition: fixed slots, vmgenid, and what the fingerprint hashes
docs/migration.md one VM moved to another host, and why the CPU model decides it
docs/memory-and-disk.md what a guest's page cache, freed memory and disk cost its host: the defaults, what was measured to choose them, and what was turned down
docs/releasing.md CI, versions, and what a release owes its upstreams
qemu/, kernel/, image/, e2fsprogs/ why each part is built the way it is
boot/ what a boot costs, measured from the host
CLAUDE.md how to work in here: the release invariants, and the rules for changing them

Licence

Apache-2.0, matching the rest of this stack — and not only for consistency: three scripts under image/mkosi.extra/ came from another Apache-2.0 project here, so a different licence would make this a mixed-licence tree for nothing. NOTICE names them and states which was modified, as section 4(b) requires.

That licence covers the recipes, not what they build: a release is mostly GPL and LGPL binaries, and docs/releasing.md says how a release answers for them.

Directories

Path Synopsis
Package boot measures what a boot of this machine costs, from the host.
Package boot measures what a boot of this machine costs, from the host.
probeinit command
probeinit is PID 1 of the diagnostic initrd the firmware and floor probes boot when no SPIN_PROBE_INITRD is given (boot/probe_initrd_test.go builds it).
probeinit is PID 1 of the diagnostic initrd the firmware and floor probes boot when no SPIN_PROBE_INITRD is given (boot/probe_initrd_test.go builds it).
cmd
spin-machine command
Command spin-machine starts a VM of the machine this repository defines, and prints what that machine is.
Command spin-machine starts a VM of the machine this repository defines, and prints what that machine is.
hack
apklock command
Command apklock writes the lock of an Alpine builder: every package an apk database says is installed, as name=version, sorted - what the builder's `apk add` is then held to, so the static libraries QEMU and e2fsprogs link are the versions a commit names, not whatever the mirror held that day.
Command apklock writes the lock of an Alpine builder: every package an apk database says is installed, as name=version, sorted - what the builder's `apk add` is then held to, so the static libraries QEMU and e2fsprogs link are the versions a commit names, not whatever the mirror held that day.
machinetype command
Command machinetype watches the versioned q35 the machine is pinned to (machine.MachineType) against the QEMU it runs on.
Command machinetype watches the versioned q35 the machine is pinned to (machine.MachineType) against the QEMU it runs on.
osvissues command
Command osvissues keeps one issue per package of the base image for what OSV knows against the newest release (task osv): it opens one for a package that has findings, rewrites its list as the findings change, and closes it once the release has none in that package.
Command osvissues keeps one issue per package of the base image for what OSV knows against the newest release (task osv): it opens one for a package that has findings, rewrites its list as the findings change, and closes it once the release has none in that package.
releasenotes command
Command releasenotes says what a release changed against the one before it, in Markdown for the release's notes: the commits that reached the machine, grouped by the part they changed; the pins that moved; the kernel options that were turned on, off or changed; and the patches added, removed or changed.
Command releasenotes says what a release changed against the one before it, in Markdown for the release's notes: the commits that reached the machine, grouped by the part they changed; the pins that moved; the kernel options that were turned on, off or changed; and the patches added, removed or changed.
Package machine defines the virtual machine this repository builds: which chipset, which devices at which PCI slots, which kernel command line, and how much memory.
Package machine defines the virtual machine this repository builds: which chipset, which devices at which PCI slots, which kernel command line, and how much memory.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL