Skip to main content

max / alloy

Mint a machine by name, and refuse a recipe key the Containerfile never declared
Author: Max Johnson <me@maxj.phd> · 2026-09-03 17:18 UTC
Signed with PGP, not checked
Commit: 50bff9b481f13c521d3cba28e5c9cd61e6dedeea
Parent: 19b0960
7 files changed, +186 insertions, -0 deletions
@@ -25,6 +25,7 @@
25 25 # Usage:
26 26 # build/build-image.sh # build image + raw disk image
27 27 # build/build-image.sh --type qcow2 # build image + qcow2 disk image
28 + # build/build-image.sh --host fw13 # mint with fw13's recipe
28 29 # build/build-image.sh --skip-build # reuse the current image, just run bib
29 30 # build/build-image.sh --write-only --write /dev/sdX # write what is already built
30 31 # build/build-image.sh --write /dev/sdX # also dd the artifact to a device
@@ -48,6 +49,11 @@
48 49 # shellcheck source=build/build-stamp.sh
49 50 . "$REPO_ROOT/build/build-stamp.sh"
50 51
52 + # host_recipe_args. The per-machine dials, so a build host is asked for by name
53 + # rather than by remembering four flags; see the header of build/host-recipe.sh.
54 + # shellcheck source=build/host-recipe.sh
55 + . "$REPO_ROOT/build/host-recipe.sh"
56 +
51 57 IMAGE="localhost/alloy:local"
52 58 BIB_IMAGE="quay.io/centos-bootc/bootc-image-builder:latest"
53 59 DEF="$REPO_ROOT/build/bib-defs.yaml"
@@ -65,6 +71,9 @@
65 71
66 72 TYPE="raw"
67 73 WRITE_DEV=""
74 + # Empty means no per-machine recipe: the Containerfile's own defaults, which
75 + # carry no compiler and no database. See build/host-recipe.sh.
76 + HOST=""
68 77 SKIP_BUILD=0
69 78 SKIP_BIB=0
70 79
@@ -95,11 +104,17 @@
95 104 # the gate lives there rather than here: a bad value has to fail the
96 105 # build whether it came from `alloy image` or from a hand-typed flag.
97 106 --build-arg) BUILD_ARGS+=(--build-arg "${2:?--build-arg needs KEY=VALUE}"); shift 2 ;;
107 + # The machine this image is for, read from build/hosts/<name>.env. Its
108 + # dials go in ahead of anything typed here, so an explicit --build-arg is
109 + # the last value podman sees and overrides the recipe.
110 + --host) HOST="${2:?--host needs a machine name}"; shift 2 ;;
98 111 -h|--help) usage 0 ;;
99 112 *) die "unknown argument: $1 (see --help)" ;;
100 113 esac
101 114 done
102 115
116 + [ -z "$HOST" ] || host_recipe_args "$HOST" BUILD_ARGS
117 +
103 118 # Refuse the ISO types outright. Both of bib's spellings compose Anaconda, and
104 119 # an artifact from either one installs a machine nobody can log into, so the
105 120 # failure has to land here at the argument rather than an hour later at a boot.
@@ -26,6 +26,7 @@
26 26 # build/build-iso.sh --fast # iteration: cheap compression, keeps source
27 27 # build/build-iso.sh --fast --skip-source # boot chain only, cannot install
28 28 # build/build-iso.sh --update-target host:5000/alloy:43 # updates come from there
29 + # build/build-iso.sh --host astra # mint with astra's recipe, arch included
29 30 # build/build-iso.sh --arch aarch64 # override the detected architecture
30 31 # build/build-iso.sh --write /dev/sdX # build, then write to a device
31 32 # build/build-iso.sh --write-only --write /dev/sdX # write the ISO already built
@@ -47,6 +48,11 @@
47 48 # shellcheck source=build/build-stamp.sh
48 49 . "$REPO_ROOT/build/build-stamp.sh"
49 50
51 + # host_recipe_args. The per-machine dials, so a build host is asked for by name
52 + # rather than by remembering four flags; see the header of build/host-recipe.sh.
53 + # shellcheck source=build/host-recipe.sh
54 + . "$REPO_ROOT/build/host-recipe.sh"
55 +
50 56 IMAGE="localhost/alloy:local"
51 57 BUILDER="localhost/alloy-iso-builder:local"
52 58 OUTPUT="$REPO_ROOT/output"
@@ -74,6 +80,9 @@
74 80 # the medium and its contents are the same architecture by construction.
75 81 # See --arch below.
76 82 ARCH=""
83 + # Empty means no per-machine recipe: the Containerfile's own defaults, which
84 + # carry no compiler and no database. See build/host-recipe.sh.
85 + HOST=""
77 86
78 87 BUILD_ARGS=()
79 88
@@ -88,6 +97,10 @@
88 97 # the gate lives there rather than here: a bad value has to fail the
89 98 # build whether it came from `alloy image` or from a hand-typed flag.
90 99 --build-arg) BUILD_ARGS+=(--build-arg "${2:?--build-arg needs KEY=VALUE}"); shift 2 ;;
100 + # The machine this medium is for, read from build/hosts/<name>.env. Its
101 + # dials go in ahead of anything typed here, and an ARCH line in the recipe
102 + # sets the medium's architecture unless --arch says otherwise.
103 + --host) HOST="${2:?--host needs a machine name}"; shift 2 ;;
91 104 --skip-source) SKIP_SOURCE=1; shift ;;
92 105 # Iteration mode: reuse the image and compress cheaply, but still carry
93 106 # the install source. Measured on fw13, 2026-08-15: level 19 costs 192s
@@ -124,6 +137,14 @@
124 137 esac
125 138 done
126 139
140 + if [ -n "$HOST" ]; then
141 + host_recipe_args "$HOST" BUILD_ARGS
142 + # --arch wins over the recipe: the flag is how a one-off test of the boot
143 + # chain asks for the other architecture, and the recipe is what the machine
144 + # normally takes.
145 + [ -n "$ARCH" ] || ARCH="$HOST_ARCH"
146 + fi
147 +
127 148 ARTIFACT="$OUTPUT/install.iso"
128 149
129 150 # --write-only skips every build step, including the output rotation, and
@@ -255,6 +255,35 @@
255 255 browser and both are the product; the point is that every large thing in this image is
256 256 either explained above or gone.
257 257
258 + ## Per-machine mint recipes
259 +
260 + The dials that decide what a machine can do all default to the smallest thing
261 + that boots. `LANGS` defaults to empty, so a default mint carries no language
262 + toolchain and no C compiler; `DB` defaults to `none`; `PROFILE` defaults to
263 + `client`. That is right for an image whose job is to run software, and it is a
264 + trap for the three machines that build it: a forgotten flag produces a build
265 + host that boots, looks healthy, and cannot compile.
266 +
267 + So each machine's dials live in `build/hosts/<name>.env`, and both builders take
268 + `--host <name>`:
269 +
270 + build/build-image.sh --host fw13
271 + build/build-iso.sh --host astra
272 +
273 + Format is `KEY=VALUE`, one per line, with `#` comments. Every key must name an
274 + `ARG` the Containerfile declares, checked against the Containerfile itself when
275 + the recipe is read, because podman drops an unknown build arg silently and a
276 + typo would mint exactly the image the file exists to prevent. `ARCH` is the one
277 + reserved key: `build-iso.sh` uses it to pick the medium's architecture and
278 + `build-image.sh` ignores it, since nothing here cross-compiles.
279 +
280 + Recipe values go in ahead of anything typed on the command line, so an explicit
281 + `--build-arg` overrides the recipe rather than racing it.
282 +
283 + What the three recipes say today: fw12 is a stock client mint and sets nothing;
284 + fw13 takes `LANGS=rust,js` and `DB=postgres16` for the build-host role; astra
285 + takes those plus `PROFILE=server` and `ARCH=aarch64`.
286 +
258 287 ## Version fields
259 288
260 289 Three numbers describe an Alloy machine, and they move on three different clocks. That is why os-release carries three fields rather than one, and why `alloy --version` prints all of them:
@@ -1,0 +1,76 @@
1 + #!/usr/bin/env bash
2 + #
3 + # host-recipe.sh — the per-machine mint recipe both image builders read.
4 + #
5 + # Sourced by build/build-image.sh and build/build-iso.sh alongside
6 + # build/build-stamp.sh and build/privilege.sh.
7 + #
8 + # The dials that decide what a machine can do (PROFILE, LANGS, DB) all default
9 + # to the smallest thing that boots. LANGS defaults to empty, so a default mint
10 + # carries no compiler at all, and the C toolchain leaves with it. That is the
11 + # right default for an image whose job is to run software, and it is a trap for
12 + # the three machines in this tree that build it: a forgotten flag produces a
13 + # build host that boots, looks healthy, and cannot compile.
14 + #
15 + # So the dials live in a file per machine. The difference between two machines
16 + # is then a diff rather than a memory, and `--host fw13` is what a mint is
17 + # asked for rather than four flags somebody has to remember in the right order.
18 + #
19 + # Format: KEY=VALUE, one per line. Blank lines and # comments are ignored, and
20 + # so is trailing whitespace. Quotes are not stripped, because no dial takes a
21 + # value that needs them.
22 + #
23 + # Every key must name an ARG the Containerfile declares. That is checked here,
24 + # against the Containerfile itself rather than a list kept in step by hand,
25 + # because a typo'd key is silently dropped by podman and would mint exactly the
26 + # image this file exists to prevent.
27 + #
28 + # ARCH is the one reserved key, and it is not a build arg. build/build-iso.sh
29 + # consumes it to pick the medium's architecture; build/build-image.sh ignores
30 + # it and builds natively, per the standing rule that nothing here
31 + # cross-compiles.
32 +
33 + # Read a recipe into a BUILD_ARGS array, and set HOST_ARCH from a recipe that
34 + # names one. Recipe args go in before anything the caller typed, so an explicit
35 + # `--build-arg` on the command line is the last value podman sees and wins.
36 + #
37 + # Usage: host_recipe_args NAME BUILD_ARGS (the array's NAME, not its contents)
38 + host_recipe_args() {
39 + local host="$1"
40 + local -n out="$2"
41 + local file="$REPO_ROOT/build/hosts/$host.env"
42 + local containerfile="$REPO_ROOT/Containerfile"
43 +
44 + [ -f "$file" ] || {
45 + printf 'error: no recipe for host %s at %s\n' "$host" "$file" >&2
46 + printf 'known hosts: %s\n' "$(cd "$REPO_ROOT/build/hosts" 2>/dev/null \
47 + && ls -1 ./*.env 2>/dev/null | sed 's|^\./||; s|\.env$||' | tr '\n' ' ')" >&2
48 + return 1
49 + }
50 +
51 + HOST_ARCH=""
52 + local recipe=()
53 + local line key value
54 + while IFS= read -r line || [ -n "$line" ]; do
55 + line="${line%%#*}"
56 + line="${line%"${line##*[![:space:]]}"}"
57 + [ -n "$line" ] || continue
58 + case "$line" in
59 + *=*) ;;
60 + *) printf 'error: %s: not KEY=VALUE: %s\n' "$file" "$line" >&2; return 1 ;;
61 + esac
62 + key="${line%%=*}"
63 + value="${line#*=}"
64 + if [ "$key" = "ARCH" ]; then
65 + HOST_ARCH="$value"
66 + continue
67 + fi
68 + grep -q "^ARG $key=" "$containerfile" || {
69 + printf 'error: %s: %s is not an ARG the Containerfile declares\n' "$file" "$key" >&2
70 + return 1
71 + }
72 + recipe+=(--build-arg "$key=$value")
73 + done < "$file"
74 +
75 + out=("${recipe[@]}" "${out[@]}")
76 + }
@@ -1,0 +1,19 @@
1 + # astra — ASRock Rack Ampere Altra, aarch64, 96 cores. Service host, the only
2 + # aarch64 builder, and the machine the nightly evidence tier runs on.
3 +
4 + # The only server-profile machine in the tree. It has no session, tty1 belongs
5 + # to the panel, and the profile's absence assertions are what keep the desktop
6 + # estate off it.
7 + PROFILE=server
8 +
9 + # A build host is a mint-time choice rather than a property of the profile:
10 + # astra builds the aarch64 images, runs the sweep, and hosts PoM's runner.
11 + LANGS=rust,js
12 +
13 + # Same reasoning as fw13: PoM's runner and the CI scratch databases want the
14 + # major production runs, not the one Fedora ships.
15 + DB=postgres16
16 +
17 + # Reserved key, read by build/build-iso.sh and ignored by build/build-image.sh.
18 + # Not a cross-build: astra builds its own medium, on aarch64 hardware.
19 + ARCH=aarch64
@@ -1,0 +1,7 @@
1 + # fw12 — Framework 12, Intel, second daily driver.
2 + #
3 + # Deliberately a stock client mint. fw12 builds nothing and carries no service,
4 + # so it is the machine that proves the image an ordinary person would install,
5 + # and every dial it sets is a way that proof stops being about that image.
6 + #
7 + # Nothing here overrides a default. The file exists so that stays a decision.
@@ -1,0 +1,19 @@
1 + # fw13 — Framework 13, AMD Strix Point, primary dev box and the tree's only
2 + # x86_64 build host.
3 + #
4 + # Four roles ride on this machine: Bento's local x86_64 builder, Bento's
5 + # controller for astra, mbp and windows-x86, the crates.io publish host, and
6 + # Sando's controller with the local PostgreSQL its gates restore into.
7 +
8 + # rust for the tree; js because Sando's code_smoke gate builds two frontends
9 + # with `npm run build` before it creates a database or boots anything, and that
10 + # is the one place a frontend failure is fatal. The C toolchain arrives with
11 + # rust: a mint without it has no gcc, no binutils and no make.
12 + LANGS=rust,js
13 +
14 + # Sando's migration_dry_run and cargo_test gates restore production dumps into
15 + # a local scratch cluster over a unix socket. Production is PostgreSQL 16, so a
16 + # scratch cluster on Fedora's own postgresql-server (18.4) would test a
17 + # database nobody deploys. Binaries only: creating the cluster is the machine's
18 + # business, not the image's.
19 + DB=postgres16