Skip to main content

max / alloy

build: stop the script losing artifacts and mis-guarding writes Four things, all found while using it. The output directory was cleared before bib ran, so a build that failed half way had already destroyed the artifact that worked. That happened on 2026-07-19. Rotate one generation to output.prev instead of deleting. The write guard only refused mountpoints matching / or /boot, so a disk holding /home, /var or an active swap passed it. Refuse anything mounted, which is the rule the installer already applies to its own disk picker (install.rs, Disk::blocker). Also refuse a partition: an ISO written to /dev/sda1 boots nothing and eats a filesystem getting there. Verified against this box's nvme, which the old regex caught only by luck of having / on it. There was no way to write an artifact that already existed without rebuilding it, so the documented workaround was to bypass this script and run dd by hand, which is where the guards live. --skip-bib fixes that. dd reporting success is not evidence the bytes landed, so verify with cmp over the artifact's length, behind a negative control. A check that cannot fail is not a check. Also records in the header that the ISO boots Anaconda rather than `alloy install`, which the file did not say anywhere.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-20 18:41 UTC
Signed with PGP, not checked
Commit: 3eb749c2f194093210daa07aaf498cf373a9e39a
Parent: 56cd79b
1 file changed, +103 insertions, -26 deletions
@@ -12,13 +12,25 @@
12 12 # container store, so bib finds the image at /var/lib/containers/storage
13 13 # without a rootless->rootful copy.
14 14 #
15 + # NOTE: the ISO this produces boots Anaconda, not `alloy install`. Every ISO
16 + # type bib offers ends in Anaconda (verified by generating the manifests; see
17 + # GO task a1d037f8), and the installer binary lives in the bootc image rather
18 + # than in the ISO's live root, which bib composes separately from the
19 + # anaconda-iso package set in build/alloy-0.0.yaml. An install from this ISO
20 + # leaves root locked and no account, because the Anaconda flow has no
21 + # user-creation spoke. Booting straight into the Alloy installer needs an ISO
22 + # built outside bib, and that retires build/alloy-0.0.yaml with it.
23 + #
15 24 # Usage:
16 25 # build/build-image.sh # build image + installer ISO
17 26 # build/build-image.sh --type raw # build image + raw disk image
18 27 # build/build-image.sh --skip-build # reuse the current image, just run bib
28 + # build/build-image.sh --skip-bib --write /dev/sdX # write what is already built
19 29 # build/build-image.sh --write /dev/sdX # also dd the artifact to a device
20 30 #
21 - # Writing requires an explicit device path and an interactive confirmation.
31 + # Writing requires an explicit device path and an interactive confirmation,
32 + # refuses partitions and anything with a mounted filesystem, and verifies the
33 + # result with cmp before claiming success.
22 34
23 35 set -euo pipefail
24 36
@@ -31,11 +43,12 @@
31 43 TYPE="iso"
32 44 WRITE_DEV=""
33 45 SKIP_BUILD=0
46 + SKIP_BIB=0
34 47
35 48 die() { printf 'error: %s\n' "$*" >&2; exit 1; }
36 49
37 50 usage() {
38 - sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
51 + sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
39 52 exit "${1:-0}"
40 53 }
41 54
@@ -44,6 +57,7 @@
44 57 --type) TYPE="${2:?--type needs a value}"; shift 2 ;;
45 58 --write) WRITE_DEV="${2:?--write needs a device path}"; shift 2 ;;
46 59 --skip-build) SKIP_BUILD=1; shift ;;
60 + --skip-bib) SKIP_BIB=1; SKIP_BUILD=1; shift ;;
47 61 -h|--help) usage 0 ;;
48 62 *) die "unknown argument: $1 (see --help)" ;;
49 63 esac
@@ -53,8 +67,19 @@
53 67 [ -f "$DEF" ] || die "missing distro def: $DEF"
54 68 [ -f "$REPO_ROOT/Containerfile" ] || die "no Containerfile at $REPO_ROOT"
55 69
70 + # --skip-bib exists to make `--write` usable on its own. Without it the only
71 + # way to write an artifact that already exists was to rebuild it first, so
72 + # the documented workaround was to bypass this script and run dd by hand,
73 + # which is exactly where the guards and the verify live.
74 + if [ "$SKIP_BIB" -eq 1 ]; then
75 + echo "==> Skipping image build and bib; using the artifact already in $OUTPUT"
76 + [ -n "$WRITE_DEV" ] || die "--skip-bib only makes sense with --write"
77 + fi
78 +
56 79 # 1. Build the bootc image (rootful, so bib sees it in the same store).
57 - if [ "$SKIP_BUILD" -eq 0 ]; then
80 + if [ "$SKIP_BIB" -eq 1 ]; then
81 + :
82 + elif [ "$SKIP_BUILD" -eq 0 ]; then
58 83 echo "==> Building $IMAGE (rootful)"
59 84 sudo podman build -t "$IMAGE" "$REPO_ROOT"
60 85 else
@@ -62,26 +87,39 @@
62 87 sudo podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build"
63 88 fi
64 89
65 - # 2. Make sure the image builder is present.
66 - sudo podman image exists "$BIB_IMAGE" || {
67 - echo "==> Pulling $BIB_IMAGE"
68 - sudo podman pull "$BIB_IMAGE"
69 - }
90 + if [ "$SKIP_BIB" -eq 0 ]; then
91 + # 2. Make sure the image builder is present.
92 + sudo podman image exists "$BIB_IMAGE" || {
93 + echo "==> Pulling $BIB_IMAGE"
94 + sudo podman pull "$BIB_IMAGE"
95 + }
70 96
71 - # 3. Build the artifact. The alloy-0.0 def is mounted read-only into bib's
72 - # defs directory; librepo (the default) resolves repos from the image.
73 - echo "==> Building --type $TYPE into $OUTPUT"
74 - mkdir -p "$OUTPUT"
75 - sudo rm -rf "${OUTPUT:?}/"* 2>/dev/null || true
76 - sudo podman run --rm --privileged \
77 - --security-opt label=type:unconfined_t \
78 - -v /var/lib/containers/storage:/var/lib/containers/storage \
79 - -v "$OUTPUT":/output \
80 - -v "$DEF":/usr/share/bootc-image-builder/defs/alloy-0.0.yaml:ro \
81 - "$BIB_IMAGE" \
82 - --type "$TYPE" \
83 - --log-level info \
84 - "$IMAGE"
97 + # 3. Build the artifact. The alloy-0.0 def is mounted read-only into bib's
98 + # defs directory; librepo (the default) resolves repos from the image.
99 + echo "==> Building --type $TYPE into $OUTPUT"
100 +
101 + # bib wants a clean output directory, but clearing it up front means a
102 + # build that fails half way has already destroyed the artifact that was
103 + # working. That happened on 2026-07-19: the previous ISO was gone before
104 + # anyone thought to keep it. Rotate one generation aside instead of
105 + # deleting, so a failed build leaves something to fall back to.
106 + if [ -d "$OUTPUT" ] && [ -n "$(sudo ls -A "$OUTPUT" 2>/dev/null)" ]; then
107 + echo "==> Rotating previous output to ${OUTPUT}.prev"
108 + sudo rm -rf "${OUTPUT:?}.prev"
109 + sudo mv "$OUTPUT" "${OUTPUT}.prev"
110 + fi
111 + mkdir -p "$OUTPUT"
112 +
113 + sudo podman run --rm --privileged \
114 + --security-opt label=type:unconfined_t \
115 + -v /var/lib/containers/storage:/var/lib/containers/storage \
116 + -v "$OUTPUT":/output \
117 + -v "$DEF":/usr/share/bootc-image-builder/defs/alloy-0.0.yaml:ro \
118 + "$BIB_IMAGE" \
119 + --type "$TYPE" \
120 + --log-level info \
121 + "$IMAGE"
122 + fi
85 123
86 124 # 4. Locate the produced artifact.
87 125 case "$TYPE" in
@@ -96,19 +134,58 @@
96 134 # 5. Optionally write to a device.
97 135 if [ -n "$WRITE_DEV" ]; then
98 136 [ -b "$WRITE_DEV" ] || die "$WRITE_DEV is not a block device"
99 - # Refuse to write to a disk that carries a mounted filesystem (root disk guard).
100 - if lsblk -nro MOUNTPOINT "$WRITE_DEV" | grep -qE '^/$|^/boot'; then
101 - die "$WRITE_DEV has a system mountpoint; refusing to write"
137 +
138 + # A whole disk, not a partition. An ISO written to /dev/sda1 produces
139 + # nothing bootable and quietly eats a filesystem on the way.
140 + [ "$(lsblk -dnro TYPE "$WRITE_DEV")" = "disk" ] \
141 + || die "$WRITE_DEV is not a whole disk; pass the disk, not a partition"
142 +
143 + # Any mountpoint at or below the device, not just / and /boot. The old
144 + # guard matched those two patterns only, so a disk holding /home, /var or
145 + # an active swap passed it and got written. This is the rule the installer
146 + # itself applies (install.rs, Disk::blocker): anything mounted blocks.
147 + # `lsblk -r` escapes a newline as \x0a, so a device with two mountpoints
148 + # arrives as one run-together line. Unescape before printing: this list is
149 + # read by someone deciding whether to erase a disk.
150 + mounts="$(lsblk -nro MOUNTPOINTS "$WRITE_DEV" 2>/dev/null \
151 + | sed 's/\\x0a/\n/g' | grep -v '^$' || true)"
152 + if [ -n "$mounts" ]; then
153 + printf 'error: %s has mounted filesystems; refusing to write:\n' "$WRITE_DEV" >&2
154 + printf '%s\n' "$mounts" | sed 's/^/ /' >&2
155 + exit 1
102 156 fi
157 +
103 158 echo
104 - lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,TRAN "$WRITE_DEV"
159 + lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL,SERIAL,TRAN,RM,RO "$WRITE_DEV"
105 160 echo
161 + # Removable is worth saying out loud: on this box the system disk is nvme
162 + # and a USB stick reports usb/RM=1, so a non-removable target is the shape
163 + # of a mistake even when nothing is mounted on it.
164 + if [ "$(lsblk -dnro RM "$WRITE_DEV")" != "1" ]; then
165 + echo "WARNING: $WRITE_DEV is not removable. This is the shape of an internal disk."
166 + echo
167 + fi
106 168 printf 'This ERASES all data on %s. Type the device path to confirm: ' "$WRITE_DEV"
107 169 read -r reply
108 170 [ "$reply" = "$WRITE_DEV" ] || die "confirmation did not match; not writing"
109 171 echo "==> Writing $ARTIFACT to $WRITE_DEV"
110 172 sudo dd if="$ARTIFACT" of="$WRITE_DEV" bs=4M oflag=direct conv=fsync status=progress
111 173 sync
174 +
175 + # Verify, because dd reporting success is not evidence the bytes landed.
176 + # cmp over exactly the artifact's length is the authoritative check; a
177 + # `dd | head -c N | sha256sum` pipeline reported phantom corruption on a
178 + # write that cmp proved perfect (wiki alloy-build-notes).
179 + echo "==> Verifying the write"
180 + size="$(sudo stat -c %s "$ARTIFACT")"
181 + # A check that cannot fail is not a check: prove cmp can still disagree
182 + # before trusting it to agree.
183 + if sudo cmp -s -n 4096 "$WRITE_DEV" /dev/zero; then
184 + die "negative control passed, which means cmp is not comparing anything"
185 + fi
186 + sudo cmp -n "$size" "$WRITE_DEV" "$ARTIFACT" \
187 + || die "$WRITE_DEV does not match $ARTIFACT; the write is bad"
188 + echo "==> Verified $size bytes."
112 189 echo "==> Done. $WRITE_DEV is now a bootable Alloy $TYPE."
113 190 else
114 191 echo "==> To write it to a USB stick:"