Skip to main content

max / alloy

10.1 KB · 224 lines History Blame Raw
1 #!/usr/bin/env bash
2 #
3 # build-image.sh — build the Alloy bootc image and a disk image
4 # (raw by default) with bootc-image-builder.
5 #
6 # Alloy re-brands os-release (ID=alloy, with its own VERSION_ID). Two
7 # consequences are handled in the Containerfile (pinned $releasever, disabled
8 # build-only repos) and one here: bootc-image-builder has no distro definition
9 # named "alloy-<VERSION_ID>", so build/bib-defs.yaml is bind-mounted into its
10 # defs dir under that name. The name is derived from usr/lib/os-release rather
11 # than hardcoded, so a product version bump does not rename a file whose
12 # contents have no version in them.
13 #
14 # Everything runs rootful on purpose: the image build and bib share one
15 # container store, so bib finds the image at /var/lib/containers/storage
16 # without a rootless->rootful copy.
17 #
18 # This script builds disk images only. It does not build ISOs, and asking it
19 # for one is an error rather than a surprise: every ISO type bib offers ends in
20 # Anaconda (verified by generating the manifests; see GO task a1d037f8), and an
21 # install from such an ISO leaves root locked and no account, because the
22 # Anaconda flow has no user-creation spoke. The installer ISO that boots into
23 # `alloy install` is built outside bib by build/build-iso.sh.
24 #
25 # Usage:
26 # build/build-image.sh # build image + raw disk image
27 # build/build-image.sh --type qcow2 # build image + qcow2 disk image
28 # build/build-image.sh --host fw13 # mint with fw13's recipe
29 # build/build-image.sh --no-preflight # mint a knowingly incomplete image
30 # build/build-image.sh --skip-build # reuse the current image, just run bib
31 # build/build-image.sh --write-only --write /dev/sdX # write what is already built
32 # build/build-image.sh --write /dev/sdX # also dd the artifact to a device
33 #
34 # Writing is build/write-device.sh's job, shared with build/build-iso.sh: it
35 # requires an explicit device path and an interactive confirmation, refuses
36 # partitions and anything with a mounted filesystem, and verifies the result
37 # with cmp before claiming success.
38
39 set -euo pipefail
40
41 REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
42
43 # priv / privc. run0 where it exists, sudo where it does not; see the header
44 # of build/privilege.sh for which of the two a call site wants.
45 # shellcheck source=build/privilege.sh
46 . "$REPO_ROOT/build/privilege.sh"
47
48 # stamp_build_args. The build number the Containerfile bakes into os-release;
49 # see the header of build/build-stamp.sh.
50 # shellcheck source=build/build-stamp.sh
51 . "$REPO_ROOT/build/build-stamp.sh"
52
53 # host_recipe_args. The per-machine dials, so a build host is asked for by name
54 # rather than by remembering four flags; see the header of build/host-recipe.sh.
55 # shellcheck source=build/host-recipe.sh
56 . "$REPO_ROOT/build/host-recipe.sh"
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
63 IMAGE="localhost/alloy:local"
64 BIB_IMAGE="quay.io/centos-bootc/bootc-image-builder:latest"
65 DEF="$REPO_ROOT/build/bib-defs.yaml"
66 OUTPUT="$REPO_ROOT/output"
67
68 # The name bib will look the def up under. It resolves a distro def as
69 # <ID>-<VERSION_ID>, both read from the image's os-release, so this has to
70 # track usr/lib/os-release and not a constant here. Read with `sed` rather
71 # than sourced: os-release is shell-shaped but this file is not a place to
72 # execute it.
73 DEF_VERSION="$(sed -n 's/^VERSION_ID="\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' \
74 "$REPO_ROOT/usr/lib/os-release")"
75 [ -n "$DEF_VERSION" ] || { echo "error: no VERSION_ID in usr/lib/os-release" >&2; exit 1; }
76 DEF_NAME="alloy-${DEF_VERSION}.yaml"
77
78 TYPE="raw"
79 WRITE_DEV=""
80 # Empty means no per-machine recipe: the Containerfile's own defaults, which
81 # carry no compiler and no database. See build/host-recipe.sh.
82 HOST=""
83 SKIP_BUILD=0
84 # The preflight runs ahead of every mint that has a recipe. See --no-preflight.
85 NO_PREFLIGHT=0
86 SKIP_BIB=0
87
88 BUILD_ARGS=()
89
90 die() { printf 'error: %s\n' "$*" >&2; exit 1; }
91
92 # The header block is the help text, so it stops where the comments stop.
93 # A hardcoded last line was wrong by seven lines and printed `set -euo
94 # pipefail` and REPO_ROOT= at people; any edit to the header would have
95 # rotted it again.
96 usage() {
97 awk 'NR==1 {next} !/^#/ {exit} {sub(/^# ?/, ""); print}' "${BASH_SOURCE[0]}"
98 exit "${1:-0}"
99 }
100
101 while [ $# -gt 0 ]; do
102 case "$1" in
103 --type) TYPE="${2:?--type needs a value}"; shift 2 ;;
104 --write) WRITE_DEV="${2:?--write needs a device path}"; shift 2 ;;
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 ;;
109 # --skip-bib is the older spelling, kept working. --write-only is the
110 # name both scripts answer to, because "bib" means nothing in the ISO
111 # path and the console emits one flag for both artifacts.
112 --write-only|--skip-bib) SKIP_BIB=1; SKIP_BUILD=1; shift ;;
113 # Passed straight to `podman build`. The Containerfile validates every
114 # one of them against its own curated sets (PROFILE, BROWSER, LANGS), so
115 # the gate lives there rather than here: a bad value has to fail the
116 # build whether it came from `alloy image` or from a hand-typed flag.
117 --build-arg) BUILD_ARGS+=(--build-arg "${2:?--build-arg needs KEY=VALUE}"); shift 2 ;;
118 # The machine this image is for, read from build/hosts/<name>.env. Its
119 # dials go in ahead of anything typed here, so an explicit --build-arg is
120 # the last value podman sees and overrides the recipe.
121 --host) HOST="${2:?--host needs a machine name}"; shift 2 ;;
122 -h|--help) usage 0 ;;
123 *) die "unknown argument: $1 (see --help)" ;;
124 esac
125 done
126
127 [ -z "$HOST" ] || host_recipe_args "$HOST" BUILD_ARGS
128
129 # Refuse the ISO types outright. Both of bib's spellings compose Anaconda, and
130 # an artifact from either one installs a machine nobody can log into, so the
131 # failure has to land here at the argument rather than an hour later at a boot.
132 case "$TYPE" in
133 iso|bootc-installer)
134 die "$TYPE builds an Anaconda ISO, which installs an unloginable machine; use build/build-iso.sh for the Alloy installer ISO" ;;
135 esac
136
137 command -v podman >/dev/null || die "podman not found"
138 [ -f "$DEF" ] || die "missing distro def: $DEF"
139 [ -f "$REPO_ROOT/Containerfile" ] || die "no Containerfile at $REPO_ROOT"
140
141 # --write-only exists to make `--write` usable on its own. Without it the only
142 # way to write an artifact that already exists was to rebuild it first, so
143 # the documented workaround was to bypass this script and run dd by hand,
144 # which is exactly where the guards and the verify live.
145 if [ "$SKIP_BIB" -eq 1 ]; then
146 echo "==> Skipping image build and bib; using the artifact already in $OUTPUT"
147 [ -n "$WRITE_DEV" ] || die "--write-only only makes sense with --write"
148 fi
149
150 # 1. Build the bootc image (rootful, so bib sees it in the same store).
151 if [ "$SKIP_BIB" -eq 1 ]; then
152 :
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"
160 echo "==> Building $IMAGE (rootful)"
161 # --jobs 2 to overlap the two stages; see the same call in build/build-iso.sh
162 # for why two and not more.
163 stamp_build_args BUILD_ARGS
164 priv podman build --jobs 2 "${BUILD_ARGS[@]}" -t "$IMAGE" "$REPO_ROOT"
165 else
166 echo "==> Skipping image build; reusing $IMAGE"
167 privc podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build"
168 fi
169
170 if [ "$SKIP_BIB" -eq 0 ]; then
171 # 2. Make sure the image builder is present.
172 privc podman image exists "$BIB_IMAGE" || {
173 echo "==> Pulling $BIB_IMAGE"
174 priv podman pull "$BIB_IMAGE"
175 }
176
177 # 3. Build the artifact. The def is mounted read-only into bib's defs
178 # directory under the name it looks up ($DEF_NAME); librepo (the default)
179 # resolves repos from the image.
180 echo "==> Building --type $TYPE into $OUTPUT"
181
182 # bib wants a clean output directory, but clearing it up front means a
183 # build that fails half way has already destroyed the artifact that was
184 # working. That happened on 2026-07-19: the previous ISO was gone before
185 # anyone thought to keep it. Rotate one generation aside instead of
186 # deleting, so a failed build leaves something to fall back to.
187 if [ -d "$OUTPUT" ] && [ -n "$(privc ls -A "$OUTPUT" 2>/dev/null)" ]; then
188 echo "==> Rotating previous output to ${OUTPUT}.prev"
189 privc rm -rf "${OUTPUT:?}.prev"
190 privc mv "$OUTPUT" "${OUTPUT}.prev"
191 fi
192 mkdir -p "$OUTPUT"
193
194 priv podman run --rm --privileged \
195 --security-opt label=type:unconfined_t \
196 -v /var/lib/containers/storage:/var/lib/containers/storage \
197 -v "$OUTPUT":/output \
198 -v "$DEF":"/usr/share/bootc-image-builder/defs/$DEF_NAME":ro \
199 "$BIB_IMAGE" \
200 --type "$TYPE" \
201 --log-level info \
202 "$IMAGE"
203 fi
204
205 # 4. Locate the produced artifact.
206 case "$TYPE" in
207 raw) ARTIFACT="$OUTPUT/image/disk.raw" ;;
208 qcow2) ARTIFACT="$OUTPUT/qcow2/disk.qcow2" ;;
209 *) ARTIFACT="$(privc find "$OUTPUT" -type f ! -name '*.json' | head -1)" ;;
210 esac
211 [ -n "$ARTIFACT" ] && privc test -f "$ARTIFACT" || die "expected artifact not found for type $TYPE"
212 echo "==> Built: $ARTIFACT ($(privc du -h "$ARTIFACT" | cut -f1))"
213
214 # 5. Optionally write to a device. The guards, the confirmation and the
215 # verify live in build/write-device.sh, which build/build-iso.sh calls
216 # too — one implementation of the dd path, per wiki alloy-distribution.
217 if [ -n "$WRITE_DEV" ]; then
218 "$REPO_ROOT/build/write-device.sh" "$ARTIFACT" "$WRITE_DEV" "$TYPE"
219 else
220 echo "==> To write it to a USB stick:"
221 echo " $PRIV_NAME dd if=$ARTIFACT of=/dev/sdX bs=4M oflag=direct conv=fsync status=progress"
222 echo " (or re-run with --write /dev/sdX to rebuild and write in one step)"
223 fi
224