max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01WFBzMprSmNCfvdj2cGZyka
5 files changed,
+218 insertions,
-3 deletions
| @@ -26,6 +26,7 @@ | |||
| 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 | 28 | # build/build-image.sh --host fw13 # mint with fw13's recipe | |
| 29 | + | # build/build-image.sh --no-preflight # mint a knowingly incomplete image | |
| 29 | 30 | # build/build-image.sh --skip-build # reuse the current image, just run bib | |
| 30 | 31 | # build/build-image.sh --write-only --write /dev/sdX # write what is already built | |
| 31 | 32 | # build/build-image.sh --write /dev/sdX # also dd the artifact to a device | |
| @@ -54,6 +55,11 @@ | |||
| 54 | 55 | # shellcheck source=build/host-recipe.sh | |
| 55 | 56 | . "$REPO_ROOT/build/host-recipe.sh" | |
| 56 | 57 | ||
| 58 | + | # preflight_gate. Runs build/preflight.sh ahead of the mint and decides what a | |
| 59 | + | # finding means; see the header of build/preflight-gate.sh. | |
| 60 | + | # shellcheck source=build/preflight-gate.sh | |
| 61 | + | . "$REPO_ROOT/build/preflight-gate.sh" | |
| 62 | + | ||
| 57 | 63 | IMAGE="localhost/alloy:local" | |
| 58 | 64 | BIB_IMAGE="quay.io/centos-bootc/bootc-image-builder:latest" | |
| 59 | 65 | DEF="$REPO_ROOT/build/bib-defs.yaml" | |
| @@ -75,6 +81,8 @@ | |||
| 75 | 81 | # carry no compiler and no database. See build/host-recipe.sh. | |
| 76 | 82 | HOST="" | |
| 77 | 83 | SKIP_BUILD=0 | |
| 84 | + | # The preflight runs ahead of every mint that has a recipe. See --no-preflight. | |
| 85 | + | NO_PREFLIGHT=0 | |
| 78 | 86 | SKIP_BIB=0 | |
| 79 | 87 | ||
| 80 | 88 | BUILD_ARGS=() | |
| @@ -95,6 +103,9 @@ | |||
| 95 | 103 | --type) TYPE="${2:?--type needs a value}"; shift 2 ;; | |
| 96 | 104 | --write) WRITE_DEV="${2:?--write needs a device path}"; shift 2 ;; | |
| 97 | 105 | --skip-build) SKIP_BUILD=1; shift ;; | |
| 106 | + | # Mint without running build/preflight.sh first, for deliberately building a | |
| 107 | + | # known-incomplete image. Not a way past a finding you would rather not read. | |
| 108 | + | --no-preflight) NO_PREFLIGHT=1; shift ;; | |
| 98 | 109 | # --skip-bib is the older spelling, kept working. --write-only is the | |
| 99 | 110 | # name both scripts answer to, because "bib" means nothing in the ISO | |
| 100 | 111 | # path and the console emits one flag for both artifacts. | |
| @@ -140,6 +151,12 @@ | |||
| 140 | 151 | if [ "$SKIP_BIB" -eq 1 ]; then | |
| 141 | 152 | : | |
| 142 | 153 | elif [ "$SKIP_BUILD" -eq 0 ]; then | |
| 154 | + | # Here rather than at the top: --write-only and --skip-build both reach this | |
| 155 | + | # point without minting anything, and a gate that refused to write a disk | |
| 156 | + | # because a recipe had a finding would be gating the wrong act. build-image.sh | |
| 157 | + | # builds disk images for this machine and reads no ARCH, so the gate always | |
| 158 | + | # sees the host's own architecture and runs in full. | |
| 159 | + | preflight_gate "$HOST" "$HOST_ARCH" "$NO_PREFLIGHT" | |
| 143 | 160 | echo "==> Building $IMAGE (rootful)" | |
| 144 | 161 | # --jobs 2 to overlap the two stages; see the same call in build/build-iso.sh | |
| 145 | 162 | # for why two and not more. |
| @@ -24,6 +24,7 @@ | |||
| 24 | 24 | # build/build-iso.sh --skip-source # omit the OCI layout (faster; the | |
| 25 | 25 | # # ISO boots but cannot install) | |
| 26 | 26 | # build/build-iso.sh --fast # iteration: cheap compression, keeps source | |
| 27 | + | # build/build-iso.sh --no-preflight # mint a knowingly incomplete image | |
| 27 | 28 | # build/build-iso.sh --fast --skip-source # boot chain only, cannot install | |
| 28 | 29 | # build/build-iso.sh --update-target host:5000/alloy:43 # updates come from there | |
| 29 | 30 | # # (or UPDATE_TARGET in the host recipe) | |
| @@ -56,6 +57,12 @@ | |||
| 56 | 57 | # shellcheck source=build/host-recipe.sh | |
| 57 | 58 | . "$REPO_ROOT/build/host-recipe.sh" | |
| 58 | 59 | ||
| 60 | + | # preflight_gate. Runs build/preflight.sh ahead of the mint and decides what a | |
| 61 | + | # finding means; see the header of build/preflight-gate.sh for which findings | |
| 62 | + | # refuse a build and which only warn. | |
| 63 | + | # shellcheck source=build/preflight-gate.sh | |
| 64 | + | . "$REPO_ROOT/build/preflight-gate.sh" | |
| 65 | + | ||
| 59 | 66 | IMAGE="localhost/alloy:local" | |
| 60 | 67 | BUILDER="localhost/alloy-iso-builder:local" | |
| 61 | 68 | OUTPUT="$REPO_ROOT/output" | |
| @@ -89,6 +96,8 @@ | |||
| 89 | 96 | # Empty means the recipe decides, and a recipe that says nothing means no. See | |
| 90 | 97 | # --nvidia below and the NVIDIA key in build/host-recipe.sh. | |
| 91 | 98 | NVIDIA="" | |
| 99 | + | # The preflight runs ahead of every mint that has a recipe. See --no-preflight. | |
| 100 | + | NO_PREFLIGHT=0 | |
| 92 | 101 | ||
| 93 | 102 | BUILD_ARGS=() | |
| 94 | 103 | ||
| @@ -108,6 +117,11 @@ | |||
| 108 | 117 | # sets the medium's architecture unless --arch says otherwise. | |
| 109 | 118 | --host) HOST="${2:?--host needs a machine name}"; shift 2 ;; | |
| 110 | 119 | --skip-source) SKIP_SOURCE=1; shift ;; | |
| 120 | + | # Mint without running build/preflight.sh first. The case it exists for is | |
| 121 | + | # deliberately building a known-incomplete image -- bisecting a Containerfile | |
| 122 | + | # change, or reproducing a defect the preflight would refuse. It is not a way | |
| 123 | + | # past a finding you would rather not read. | |
| 124 | + | --no-preflight) NO_PREFLIGHT=1; shift ;; | |
| 111 | 125 | # Iteration mode: reuse the image and compress cheaply, but still carry | |
| 112 | 126 | # the install source. Measured on fw13, 2026-08-15: level 19 costs 192s | |
| 113 | 127 | # against level 3's 8s, and the export costs 22s and is now skipped | |
| @@ -178,7 +192,12 @@ | |||
| 178 | 192 | command -v podman >/dev/null || die "podman not found" | |
| 179 | 193 | ||
| 180 | 194 | # 1. The Alloy image. | |
| 195 | + | # | |
| 196 | + | # The preflight goes here rather than at the top: --write-only and --skip-build | |
| 197 | + | # both reach this point without minting anything, and a gate that refused to | |
| 198 | + | # write a stick because a recipe had a finding would be gating the wrong act. | |
| 181 | 199 | if [ "$SKIP_BUILD" -eq 0 ]; then | |
| 200 | + | preflight_gate "$HOST" "$ARCH" "$NO_PREFLIGHT" | |
| 182 | 201 | say "building $IMAGE" | |
| 183 | 202 | # --jobs 2 because the Containerfile is two stages that meet only at a COPY. | |
| 184 | 203 | # The rust-build stage compiles the console and the terminal (about four |
| @@ -3,6 +3,16 @@ | |||
| 3 | 3 | # preflight.sh — answer, in seconds, the questions a mint would answer in minutes. | |
| 4 | 4 | # | |
| 5 | 5 | # build/preflight.sh fw13 # check fw13's recipe before building it | |
| 6 | + | # build/preflight.sh fw13 --fast # the instant checks only, no probe | |
| 7 | + | # | |
| 8 | + | # Exit codes, and the split matters because both builders gate on it: | |
| 9 | + | # | |
| 10 | + | # 0 clean | |
| 11 | + | # 1 findings about the image; do not build | |
| 12 | + | # 2 usage: no host named, or no recipe by that name | |
| 13 | + | # 4 findings about this host alone; the medium is fine | |
| 14 | + | # | |
| 15 | + | # See the comment above the exit at the bottom for why those are two codes. | |
| 6 | 16 | # | |
| 7 | 17 | # WHY THIS EXISTS. Every assertion Alloy has lives inside the Containerfile, so | |
| 8 | 18 | # running one costs a build up to that step, and `bootc container lint` is step | |
| @@ -55,7 +65,14 @@ | |||
| 55 | 65 | # run0 without --pipe puts its child on a pty, which staples a carriage return to | |
| 56 | 66 | # every line. Not `exec`, because privc is a shell function. | |
| 57 | 67 | out="$(mktemp)"; probe="" | |
| 58 | - | cleanup() { rm -f "$out"; [ -n "$probe" ] && rm -rf "$probe"; } | |
| 68 | + | # The `|| :` is load-bearing, and its absence was a bug this script carried from | |
| 69 | + | # the start. bash lets an EXIT trap set the script's exit status, and | |
| 70 | + | # `[ -n "$probe" ] && rm -rf "$probe"` ends on a false test whenever the probe | |
| 71 | + | # was never built -- which is every --fast run. So a clean --fast run printed | |
| 72 | + | # "preflight: clean" and exited 1. Invisible while nothing read the code, and | |
| 73 | + | # fatal the moment the builders started gating on it: --fast is exactly what a | |
| 74 | + | # cross-architecture mint runs, so astra's every mint would have been refused. | |
| 75 | + | cleanup() { rm -f "$out"; [ -n "$probe" ] && rm -rf "$probe"; :; } | |
| 59 | 76 | trap cleanup EXIT | |
| 60 | 77 | ||
| 61 | 78 | # `set -e` with `pipefail` would end the script the moment the resolver reports a | |
| @@ -90,7 +107,15 @@ | |||
| 90 | 107 | # copies etc/ and usr/ and runs its own steps, and any of those could leave a | |
| 91 | 108 | # file the probe never carries. The class it does catch is the one that has | |
| 92 | 109 | # actually bitten -- a package's %post leaving content behind. | |
| 93 | - | if [ "${2:-}" = "--fast" ]; then | |
| 110 | + | if [ "$rc" -ne 0 ]; then | |
| 111 | + | # The dials failed, so the recipe describes an image the Containerfile's own | |
| 112 | + | # validator refuses. Everything the probe would measure is a property of a | |
| 113 | + | # mint that cannot happen, so building it costs a dnf transaction to answer | |
| 114 | + | # questions about nothing. It also makes the gate slow precisely when it is | |
| 115 | + | # about to refuse, and a gate people wait on is a gate people route around. | |
| 116 | + | echo | |
| 117 | + | echo "== probe: skipped; the dials above are already a refusal" | |
| 118 | + | elif [ "${2:-}" = "--fast" ]; then | |
| 94 | 119 | echo | |
| 95 | 120 | echo "== probe: skipped (--fast); dials and guards only" | |
| 96 | 121 | else | |
| @@ -237,9 +262,25 @@ | |||
| 237 | 262 | fi | |
| 238 | 263 | done | |
| 239 | 264 | ||
| 265 | + | # Two kinds of finding, and they are not the same kind of problem, so they no | |
| 266 | + | # longer share an exit code. | |
| 267 | + | # | |
| 268 | + | # dials, requires and var-payload are about the medium: an image built with one | |
| 269 | + | # of them outstanding is wrong wherever it is built, and a mint should not | |
| 270 | + | # proceed. guards is about the machine you are standing on -- it reads sibling | |
| 271 | + | # repos under ~/Code that no image contains -- so a stale goingson clone predicts | |
| 272 | + | # a failed AppImage on this host and says nothing about the medium being minted. | |
| 273 | + | # | |
| 274 | + | # Collapsed into one code, the script could not be used as a gate at all: it | |
| 275 | + | # would refuse to build a perfectly good ISO because a repo next door was | |
| 276 | + | # behind. Separated, a caller can act on each. build/preflight-gate.sh does. | |
| 240 | 277 | if [ "$rc" -eq 0 ] && [ "$guards_bad" -eq 0 ]; then | |
| 241 | 278 | echo; echo "preflight: clean" | |
| 242 | 279 | exit 0 | |
| 243 | 280 | fi | |
| 281 | + | if [ "$rc" -eq 0 ]; then | |
| 282 | + | echo; echo "preflight: this host has findings; the medium itself is clean" | |
| 283 | + | exit 4 | |
| 284 | + | fi | |
| 244 | 285 | echo; echo "preflight: findings above; fix them before building" | |
| 245 | 286 | exit 1 |
| @@ -312,7 +312,8 @@ | |||
| 312 | 312 | ## Preflight | |
| 313 | 313 | ||
| 314 | 314 | `build/preflight.sh <host>` answers, before any mint, what a mint would otherwise | |
| 315 | - | answer slowly. Run it whenever a recipe or the package set changes: | |
| 315 | + | answer slowly. **Both builders run it themselves**, so this is the explicit form | |
| 316 | + | rather than the usual one: | |
| 316 | 317 | ||
| 317 | 318 | ```sh | |
| 318 | 319 | build/preflight.sh fw13 # everything | |
| @@ -344,6 +345,47 @@ | |||
| 344 | 345 | what the script exists to avoid, but it is one dnf transaction rather than 103 | |
| 345 | 346 | steps, and it is right rather than close. | |
| 346 | 347 | ||
| 348 | + | ### The gate | |
| 349 | + | ||
| 350 | + | `build/build-iso.sh` and `build/build-image.sh` both run the preflight before | |
| 351 | + | they mint anything, and refuse on a finding. `build/preflight-gate.sh` holds that | |
| 352 | + | policy once, so the two builders cannot drift apart on it. | |
| 353 | + | ||
| 354 | + | It was deliberately not a gate on day one: a gate that fires wrongly on its first | |
| 355 | + | day is worse than no gate, and the preflight had never been run in anger. It has | |
| 356 | + | since caught four things nobody predicted, including a recipe combination the | |
| 357 | + | Containerfile's validator refuses on a machine whose medium had never been built. | |
| 358 | + | ||
| 359 | + | **Only findings about the medium refuse a build.** `dials`, `requires` and | |
| 360 | + | `var-payload` describe the image, and an image with one of them outstanding is | |
| 361 | + | wrong wherever it is built. `guards` describes the machine you are standing on -- | |
| 362 | + | it reads sibling repos under `~/Code` that no image contains -- so a stale | |
| 363 | + | goingson clone predicts a failed AppImage here and says nothing about the medium. | |
| 364 | + | That warns loudly and the build goes on. Refusing a mint because a repo next door | |
| 365 | + | is behind would make the gate a thing people route around, and routing around it | |
| 366 | + | is how the four findings above would have been missed. | |
| 367 | + | ||
| 368 | + | The two kinds are two exit codes, which is what makes the split possible: | |
| 369 | + | ||
| 370 | + | | code | means | | |
| 371 | + | |---|---| | |
| 372 | + | | 0 | clean | | |
| 373 | + | | 1 | findings about the image; do not build | | |
| 374 | + | | 2 | usage: no host named, or no recipe by that name | | |
| 375 | + | | 4 | findings about this host alone; the medium is fine | | |
| 376 | + | ||
| 377 | + | **How much of the preflight a mint runs** is decided the same way. The probe is | |
| 378 | + | one dnf transaction: seconds on the host's own architecture, minutes under | |
| 379 | + | emulation (measured 2026-09-04, the astra probe took minutes on fw13 and seconds | |
| 380 | + | on astra). So the gate runs the full preflight when the medium's architecture is | |
| 381 | + | this machine's and `--fast` when it is not, and says which it chose. A mint with | |
| 382 | + | no `--host` is not gated, because there is no recipe to check; that is printed | |
| 383 | + | rather than silent. | |
| 384 | + | ||
| 385 | + | `--no-preflight` skips it, for deliberately building a known-incomplete image -- | |
| 386 | + | bisecting a Containerfile change, or reproducing a defect the preflight would | |
| 387 | + | refuse. It is not a way past a finding you would rather not read. | |
| 388 | + | ||
| 347 | 389 | **What the preflight cannot see**, stated so a clean run is not read as more than | |
| 348 | 390 | it is: | |
| 349 | 391 |
| @@ -1,0 +1,96 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # | |
| 3 | + | # preflight-gate.sh — run build/preflight.sh ahead of a mint, and decide what a | |
| 4 | + | # finding means. Sourced by build/build-image.sh and build/build-iso.sh, so the | |
| 5 | + | # policy exists once rather than in two places that drift. | |
| 6 | + | # | |
| 7 | + | # WHY THIS IS A GATE AND WAS NOT ONE ON DAY ONE. A gate that fires wrongly on | |
| 8 | + | # its first day is worse than no gate, and the preflight had never been run in | |
| 9 | + | # anger. It has since caught four things nobody predicted, one of them a recipe | |
| 10 | + | # combination the Containerfile's own validator refuses, so the condition is met. | |
| 11 | + | # | |
| 12 | + | # WHAT REFUSES A BUILD, and this is the decision the gate exists to hold. | |
| 13 | + | # preflight answers two different kinds of question, and only one of them is | |
| 14 | + | # about the thing being built: | |
| 15 | + | # | |
| 16 | + | # dials, requires, var-payload about the medium. An image with one of these | |
| 17 | + | # outstanding is wrong wherever it is built. | |
| 18 | + | # These refuse. | |
| 19 | + | # guards about this machine. It reads sibling repos | |
| 20 | + | # under ~/Code that no image contains, so it | |
| 21 | + | # predicts a failed AppImage on this host and | |
| 22 | + | # says nothing about the medium. This warns. | |
| 23 | + | # | |
| 24 | + | # Refusing a mint because a repo next door is stale would make the gate a thing | |
| 25 | + | # people route around, and routing around it is how the four findings above | |
| 26 | + | # would have been missed. So the warning is loud and the build proceeds; the | |
| 27 | + | # finding is still on the screen and still worth fixing before a release. | |
| 28 | + | # | |
| 29 | + | # HOW MUCH PREFLIGHT RUNS, which is the other half of the decision. The probe is | |
| 30 | + | # one dnf transaction: seconds on the host's own architecture, minutes under | |
| 31 | + | # emulation. Measured 2026-09-04, the astra probe took minutes on fw13 and | |
| 32 | + | # seconds on astra. A gate that always builds it makes every cross-architecture | |
| 33 | + | # mint slow to start, for an answer emulation is the worst way to get. So the | |
| 34 | + | # gate runs the full preflight when the mint's architecture is this machine's, | |
| 35 | + | # and --fast when it is not, saying which it chose and why. | |
| 36 | + | # | |
| 37 | + | # Usage: preflight_gate HOST ARCH SKIP | |
| 38 | + | # HOST the recipe name, empty for a mint with no recipe | |
| 39 | + | # ARCH the medium's architecture, empty for this machine's | |
| 40 | + | # SKIP 1 to skip the gate entirely (--no-preflight) | |
| 41 | + | # | |
| 42 | + | # Returns 0 to proceed. On a finding about the image it does not return: it | |
| 43 | + | # exits the calling script, because a caller that had to remember to check would | |
| 44 | + | # eventually forget. | |
| 45 | + | ||
| 46 | + | preflight_gate() { | |
| 47 | + | local host="$1" arch="${2:-}" skip="${3:-0}" | |
| 48 | + | local rc=0 mode=() | |
| 49 | + | ||
| 50 | + | if [ "$skip" = "1" ]; then | |
| 51 | + | printf '==> preflight: skipped (--no-preflight)\n' | |
| 52 | + | return 0 | |
| 53 | + | fi | |
| 54 | + | ||
| 55 | + | # No recipe, nothing to check. preflight reads build/hosts/<name>.env and has | |
| 56 | + | # nothing to say about a mint that takes the Containerfile's own defaults. | |
| 57 | + | # Stated rather than silent, so a hostless mint does not read as a gated one. | |
| 58 | + | if [ -z "$host" ]; then | |
| 59 | + | printf '==> preflight: skipped (no --host, so there is no recipe to check)\n' | |
| 60 | + | return 0 | |
| 61 | + | fi | |
| 62 | + | ||
| 63 | + | if [ -n "$arch" ] && [ "$arch" != "$(uname -m)" ]; then | |
| 64 | + | mode=(--fast) | |
| 65 | + | printf '==> preflight %s --fast (minting %s on %s; the probe would be emulated)\n' \ | |
| 66 | + | "$host" "$arch" "$(uname -m)" | |
| 67 | + | else | |
| 68 | + | printf '==> preflight %s\n' "$host" | |
| 69 | + | fi | |
| 70 | + | ||
| 71 | + | "$REPO_ROOT/build/preflight.sh" "$host" "${mode[@]}" || rc=$? | |
| 72 | + | ||
| 73 | + | case "$rc" in | |
| 74 | + | 0) return 0 ;; | |
| 75 | + | 4) | |
| 76 | + | printf '\n' | |
| 77 | + | printf '==> preflight: this HOST has findings; the medium is clean, so the build goes on.\n' | |
| 78 | + | printf ' Fix them before you cut a release from this machine.\n\n' | |
| 79 | + | return 0 | |
| 80 | + | ;; | |
| 81 | + | 2) | |
| 82 | + | # Usage, not a finding: --host named a recipe preflight could not read. | |
| 83 | + | # Saying "findings" here would send someone looking for a defect in an | |
| 84 | + | # image that was never described. | |
| 85 | + | printf '\n' | |
| 86 | + | printf 'error: preflight could not read a recipe for %s. Not building.\n' "$host" >&2 | |
| 87 | + | exit 2 | |
| 88 | + | ;; | |
| 89 | + | *) | |
| 90 | + | printf '\n' | |
| 91 | + | printf 'error: preflight found something the mint would carry. Not building.\n' >&2 | |
| 92 | + | printf ' Pass --no-preflight to mint anyway, for a deliberately incomplete image.\n' >&2 | |
| 93 | + | exit "$rc" | |
| 94 | + | ;; | |
| 95 | + | esac | |
| 96 | + | } |