max / alloy
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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:" |