Building the image#
Wheelhouse OS is VyOS, built from source in VyOS' own build container, with the
Wheelhouse .deb dropped into vyos-build/packages/ and a flavor file that says what
the image should be. packaging/iso/build-iso.sh is
the whole of it: about 200 lines of shell that clone and pin vyos-build, patch three
identity strings in its GPL build tooling, stage the branding overlay, run the build, and
write the corresponding-source record the GPL asks for. It takes 30 to 60 minutes and
needs Docker and a privileged container, because live-build mounts filesystems.
Running it#
DEB=dist/wheelhouse-agent_1.2.3_amd64.deb VERSION=1.2.3 \
bash packaging/iso/build-iso.sh| Variable | Default | Meaning |
|---|---|---|
DEB | required | The agent package to bake in |
VERSION | dev | The image version string |
BUILD_DIR | /srv/wheelhouse-build | Where the vyos-build checkout and the output live. Must be a host path — docker -v needs one even when the script itself runs inside a job container |
VYOS_COMMIT | the pin in the script | head follows the branch instead. Use that to find the next pin, never to ship |
VYOS_BRANCH | rolling | Branch and container tag. Upstream renamed current to rolling in 2026 |
VYOS_BUILD_REPO | https://github.com/vyos/vyos-build | |
VYOS_BUILD_IMAGE | vyos/vyos-build:$VYOS_BRANCH | |
BUILD_BY | releases@rhymelikedi.me | Builder identity written into the image |
Output, in $BUILD_DIR/out/:
| File | What it is |
|---|---|
wheelhouse-<version>-amd64.iso | The image |
wheelhouse-<version>-amd64.iso.sha256 | Its checksum |
vyos-build-wheelhouse-<version>.patch | The changes made to the GPL build tooling, published |
SOURCES-<version>.txt | The corresponding-source record: the vyos-build commit, the container digest, and every installed package |
The base, pinned#
VYOS_COMMIT=${VYOS_COMMIT:-ed1f619f89df0f911918b13a07f6df2cbca0a956}The pin is a commit rather than a tag, and the script's comment explains why: the newest
tag in vyos-build is 1.4.0, which belongs to a branch upstream now marks
public-unmaintained, and the maintained branches are crux, equuleus and rolling.
Rolling has no tags at all. The pin lives in the source tree rather than in CI so that the
base is a property of the source.
To move it: run once with VYOS_COMMIT=head, note the commit the script prints, put it in
the script, rebuild, and boot the result on the bench. The branding hooks assert on
upstream's own strings and fail the build if the new commit moved them, which is the
point of them.
What the script does, in order#
1. Check the splash#
magic=$(od -An -tx1 -N 8 "$splash" | tr -d ' \n')
[ "$magic" = "89504e470d0a1a0a" ] || { echo "$splash is not a PNG" >&2; exit 1; }It reads the PNG signature and the IHDR dimensions with od and awk, and refuses
anything that is not a 640×480 PNG. od rather than an image library because the runner
is a build container with neither Python image tools nor a guaranteed font.
The splash matters legally, not only visually: vyos-build's own artwork is copyright
Sentrium S.L., all rights reserved, and its LICENSE.artwork permits redistributing a
self-built image only if that artwork is replaced. Both copies are replaced —
includes.binary/isolinux/splash.png, which is what both bootloaders draw, and
bootloaders/grub-pc/splash.png, which live-build copies regardless.
packaging/iso/branding/make-splash.py
draws it with zlib and struct, with the letterforms defined as strokes rather than
loaded from a font file.
2. Fetch and pin vyos-build#
A shallow fetch of the pinned commit, with a fallback to a depth-200 fetch of the branch for mirrors that refuse a bare SHA.
3. Patch the GPL build tooling#
build-vyos-image hardcodes several identity strings as literals inside a Jinja2
template, so no flavor key can reach them. The script patches the file with a helper that
fails the build if the expected literal is not there or if the substitution did not
take:
| Changed | Why |
|---|---|
--iso-application "VyOS" → "Wheelhouse" | A Wheelhouse stick otherwise identifies itself as VyOS |
--iso-volume "VyOS" → "WHEELHOUSE" | blkid -L would otherwise see VyOS. Uppercase because ISO 9660 wants it that way |
The live kernel command line: hostname=vyos → hostname=wheelhouse, and quiet added | The host name is set before VyOS loads any configuration, so the first console prompt would otherwise be vyos login:. quiet is there because without it the longest screen of the boot is the base introducing itself — vyos-1x ships fourteen systemd units whose descriptions start with "VyOS", and systemd prints every one between our splash and our banner |
quiet raises the console log level to KERN_ERR and stops systemd printing unit status;
panics, oopses and units that actually fail still reach the screen, so a broken boot still
says so. The escape hatch is the fail-safe menu entry, which boots from
--bootappend-live-failsafe — a line upstream leaves without quiet. The script
fails the build if upstream ever adds quiet there, because the boot menu labels that
entry as the verbose one and the label has to stay true. Either bootloader also lets an
operator edit the line before booting (Tab in isolinux, e in GRUB).
build-vyos-image is GPL-2, so the diff is published:
git diff -- scripts/image-build/build-vyos-image is written to
out/vyos-build-wheelhouse-<version>.patch, and the script fails if that diff is empty —
an empty patch means the branding did not apply.
4. Render the flavor#
packaging/iso/wheelhouse.toml is a template, not
valid flavor input until @VERSION@ and @VYOS_BASE@ are substituted. The script
substitutes them, then greps for any surviving @…@ and fails if it finds one. It also
refuses the literal string <TODO in the rendered flavor and in the three documents that
ship beside it on the image, because /etc/os-release is machine-parsed and a placeholder
there is malformed data rather than a visible blank. What the flavor contains is on
The flavor and the branding hooks.
5. Stage the branding overlay#
includes.binary/isolinux/splash.png replaced
bootloaders/grub-pc/splash.png replaced
hooks/live/50-wheelhouse-brand.binary boot menus
hooks/live/50-wheelhouse-brand.chroot identity and licence files
includes.chroot/usr/share/doc/wheelhouse/ TERMS, THIRD-PARTY, SOURCE-OFFER, SOURCESOld hooks are swept with rm -f hooks/live/50-wheelhouse-* first, because they are
untracked in the vyos-build checkout and a git reset would not remove one that has
since been renamed.
6. Drop in the package#
rm -f vyos-build/packages/wheelhouse-agent_*.deb
cp "$DEB" vyos-build/packages/live-build installs every package it finds there. That is the entire mechanism by which the agent, the UI and the installer get into the image.
7. Write the corresponding-source record#
Twice. Once before the build, into includes.chroot/usr/share/doc/wheelhouse/SOURCES.txt,
because it has to be in the chroot — that copy points at /var/lib/dpkg/status for the
package list. Once after, beside the ISO as SOURCES-<version>.txt, with the package
list vyos-build emitted.
Both name the vyos-build repository and commit, the branch, the container image digest,
the agent package, the three patches to the GPL tooling, and every file added to the base.
The container digest is only recorded if it looks like a digest — a failing docker still
prints something.
8. Build#
docker run --rm --privileged \
-v "$BUILD_DIR/vyos-build:/vyos" -w /vyos \
-e GOSU_UID="$(id -u)" -e GOSU_GID="$(id -g)" \
"$VYOS_BUILD_IMAGE" bash -c "
sudo ./build-vyos-image --architecture amd64 --build-by '$BUILD_BY' \
--build-type release --version 'wheelhouse-$VERSION' \
--build-comment 'Wheelhouse OS $VERSION on VyOS …' wheelhouse"--privileged is required because live-build mounts filesystems. The version passed to
upstream is wheelhouse-<version>, which is what the binary branding hook reads back out
of version.json to title the boot menu.
9. Collect#
iso=$(ls -t vyos-build/build/*.iso | head -1)That line is why the pipeline has an inspection step: in a reused build tree it will
happily select a previous run's image, which then gets copied to a name built from this
run's $VERSION. See below.
The checks that stop the wrong bytes shipping#
.forgejo/workflows/iso.yml runs a standard-library
Python step over the finished file before anything is published. It reads the ISO 9660
primary volume descriptor at sector 16 directly — no loop mount, no privileges, no new
tools:
| Check | Failure or warning |
|---|---|
| The file exists | Failure |
| Larger than 200 MB | Failure — no VyOS image is that small |
CD001 at offset 1 of the descriptor | Failure — is this an image at all? |
Volume label contains wheelhouse | Warning — the --iso-volume patch did not take; a branding defect, not a safety one |
| Creation timestamp within 48 hours | Failure — this is a stale artefact from an earlier run, not what was just built |
.sha256 beside the image matches it | Failure |
SOURCES-<version>.txt exists and names vyos-build | Failure — the corresponding-source record is missing |
The 48-hour window is the one worth understanding. The image's own creation stamp is the
single field that cannot be renamed into agreement, so it is what catches the ls -t
hazard. The window absorbs the builder's timezone offset without letting a month-old image
through.
Building it by hand#
The ISO job runs on the hades runner because that is where /srv/wheelhouse-build
keeps the vyos-build checkout between runs, but the script runs anywhere Docker does.
By hand, from a clean checkout:
# 1. Build the inputs.
(cd ui && npm ci && npm run build)
(cd agent && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
-ldflags="-s -w -X main.version=1.2.3" -o wheelhouse-agent-amd64 .)
python3 packaging/build-deb.py --version 1.2.3 --arch amd64 \
--binary agent/wheelhouse-agent-amd64 --ui ui/dist --out /tmp/wh
# 2. Build the image. 30-60 minutes.
DEB=/tmp/wh/wheelhouse-agent_1.2.3_amd64.deb VERSION=1.2.3 \
BUILD_DIR=/tmp/wh-build bash packaging/iso/build-iso.shWhat has and has not been verified#
See also#
- The flavor and the branding hooks — what the flavor declares and what the two hooks assert.
- Building the package — the input this script consumes.
- Making a release — how the image is attached, checksummed and published.
- What CI does — the ISO job around this script.
- Corresponding source ·
Built on VyOS — what the
SOURCESrecord and the published patch are for. - Download and verify — the customer end of the file this build produces.
- Upgrading the image — what happens to a box when a new one of these arrives.
Checked against#
packaging/iso/build-iso.sh ·
packaging/iso/wheelhouse.toml ·
packaging/iso/branding/make-splash.py ·
packaging/iso/branding/50-wheelhouse-brand.binary ·
packaging/iso/branding/50-wheelhouse-brand.chroot ·
packaging/iso/branding/doc/SOURCE-OFFER.txt ·
packaging/README.md ·
.forgejo/workflows/iso.yml ·
docs/hardware.md