max / alloy
5 files changed,
+103 insertions,
-37 deletions
| @@ -34,6 +34,12 @@ | |||
| 34 | 34 | set -euo pipefail | |
| 35 | 35 | ||
| 36 | 36 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| 37 | + | ||
| 38 | + | # priv / privc. run0 where it exists, sudo where it does not; see the header | |
| 39 | + | # of build/privilege.sh for which of the two a call site wants. | |
| 40 | + | # shellcheck source=build/privilege.sh | |
| 41 | + | . "$REPO_ROOT/build/privilege.sh" | |
| 42 | + | ||
| 37 | 43 | IMAGE="localhost/alloy:local" | |
| 38 | 44 | BIB_IMAGE="quay.io/centos-bootc/bootc-image-builder:latest" | |
| 39 | 45 | DEF="$REPO_ROOT/build/alloy-0.0.yaml" | |
| @@ -48,8 +54,12 @@ | |||
| 48 | 54 | ||
| 49 | 55 | die() { printf 'error: %s\n' "$*" >&2; exit 1; } | |
| 50 | 56 | ||
| 57 | + | # The header block is the help text, so it stops where the comments stop. | |
| 58 | + | # A hardcoded last line was wrong by seven lines and printed `set -euo | |
| 59 | + | # pipefail` and REPO_ROOT= at people; any edit to the header would have | |
| 60 | + | # rotted it again. | |
| 51 | 61 | usage() { | |
| 52 | - | sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' | |
| 62 | + | awk 'NR==1 {next} !/^#/ {exit} {sub(/^# ?/, ""); print}' "${BASH_SOURCE[0]}" | |
| 53 | 63 | exit "${1:-0}" | |
| 54 | 64 | } | |
| 55 | 65 | ||
| @@ -98,17 +108,17 @@ | |||
| 98 | 108 | : | |
| 99 | 109 | elif [ "$SKIP_BUILD" -eq 0 ]; then | |
| 100 | 110 | echo "==> Building $IMAGE (rootful)" | |
| 101 | - | sudo podman build "${BUILD_ARGS[@]}" -t "$IMAGE" "$REPO_ROOT" | |
| 111 | + | priv podman build "${BUILD_ARGS[@]}" -t "$IMAGE" "$REPO_ROOT" | |
| 102 | 112 | else | |
| 103 | 113 | echo "==> Skipping image build; reusing $IMAGE" | |
| 104 | - | sudo podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build" | |
| 114 | + | privc podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build" | |
| 105 | 115 | fi | |
| 106 | 116 | ||
| 107 | 117 | if [ "$SKIP_BIB" -eq 0 ]; then | |
| 108 | 118 | # 2. Make sure the image builder is present. | |
| 109 | - | sudo podman image exists "$BIB_IMAGE" || { | |
| 119 | + | privc podman image exists "$BIB_IMAGE" || { | |
| 110 | 120 | echo "==> Pulling $BIB_IMAGE" | |
| 111 | - | sudo podman pull "$BIB_IMAGE" | |
| 121 | + | priv podman pull "$BIB_IMAGE" | |
| 112 | 122 | } | |
| 113 | 123 | ||
| 114 | 124 | # 3. Build the artifact. The alloy-0.0 def is mounted read-only into bib's | |
| @@ -120,14 +130,14 @@ | |||
| 120 | 130 | # working. That happened on 2026-07-19: the previous ISO was gone before | |
| 121 | 131 | # anyone thought to keep it. Rotate one generation aside instead of | |
| 122 | 132 | # deleting, so a failed build leaves something to fall back to. | |
| 123 | - | if [ -d "$OUTPUT" ] && [ -n "$(sudo ls -A "$OUTPUT" 2>/dev/null)" ]; then | |
| 133 | + | if [ -d "$OUTPUT" ] && [ -n "$(privc ls -A "$OUTPUT" 2>/dev/null)" ]; then | |
| 124 | 134 | echo "==> Rotating previous output to ${OUTPUT}.prev" | |
| 125 | - | sudo rm -rf "${OUTPUT:?}.prev" | |
| 126 | - | sudo mv "$OUTPUT" "${OUTPUT}.prev" | |
| 135 | + | privc rm -rf "${OUTPUT:?}.prev" | |
| 136 | + | privc mv "$OUTPUT" "${OUTPUT}.prev" | |
| 127 | 137 | fi | |
| 128 | 138 | mkdir -p "$OUTPUT" | |
| 129 | 139 | ||
| 130 | - | sudo podman run --rm --privileged \ | |
| 140 | + | priv podman run --rm --privileged \ | |
| 131 | 141 | --security-opt label=type:unconfined_t \ | |
| 132 | 142 | -v /var/lib/containers/storage:/var/lib/containers/storage \ | |
| 133 | 143 | -v "$OUTPUT":/output \ | |
| @@ -142,10 +152,10 @@ | |||
| 142 | 152 | case "$TYPE" in | |
| 143 | 153 | raw) ARTIFACT="$OUTPUT/image/disk.raw" ;; | |
| 144 | 154 | qcow2) ARTIFACT="$OUTPUT/qcow2/disk.qcow2" ;; | |
| 145 | - | *) ARTIFACT="$(sudo find "$OUTPUT" -type f ! -name '*.json' | head -1)" ;; | |
| 155 | + | *) ARTIFACT="$(privc find "$OUTPUT" -type f ! -name '*.json' | head -1)" ;; | |
| 146 | 156 | esac | |
| 147 | - | [ -n "$ARTIFACT" ] && sudo test -f "$ARTIFACT" || die "expected artifact not found for type $TYPE" | |
| 148 | - | echo "==> Built: $ARTIFACT ($(sudo du -h "$ARTIFACT" | cut -f1))" | |
| 157 | + | [ -n "$ARTIFACT" ] && privc test -f "$ARTIFACT" || die "expected artifact not found for type $TYPE" | |
| 158 | + | echo "==> Built: $ARTIFACT ($(privc du -h "$ARTIFACT" | cut -f1))" | |
| 149 | 159 | ||
| 150 | 160 | # 5. Optionally write to a device. The guards, the confirmation and the | |
| 151 | 161 | # verify live in build/write-device.sh, which build/build-iso.sh calls | |
| @@ -154,6 +164,6 @@ | |||
| 154 | 164 | "$REPO_ROOT/build/write-device.sh" "$ARTIFACT" "$WRITE_DEV" "$TYPE" | |
| 155 | 165 | else | |
| 156 | 166 | echo "==> To write it to a USB stick:" | |
| 157 | - | echo " sudo dd if=$ARTIFACT of=/dev/sdX bs=4M oflag=direct conv=fsync status=progress" | |
| 167 | + | echo " $PRIV_NAME dd if=$ARTIFACT of=/dev/sdX bs=4M oflag=direct conv=fsync status=progress" | |
| 158 | 168 | echo " (or re-run with --write /dev/sdX to rebuild and write in one step)" | |
| 159 | 169 | fi |
| @@ -36,6 +36,12 @@ | |||
| 36 | 36 | set -euo pipefail | |
| 37 | 37 | ||
| 38 | 38 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| 39 | + | ||
| 40 | + | # priv / privc. run0 where it exists, sudo where it does not; see the header | |
| 41 | + | # of build/privilege.sh for which of the two a call site wants. | |
| 42 | + | # shellcheck source=build/privilege.sh | |
| 43 | + | . "$REPO_ROOT/build/privilege.sh" | |
| 44 | + | ||
| 39 | 45 | IMAGE="localhost/alloy:local" | |
| 40 | 46 | BUILDER="localhost/alloy-iso-builder:local" | |
| 41 | 47 | OUTPUT="$REPO_ROOT/output" | |
| @@ -96,7 +102,9 @@ | |||
| 96 | 102 | # console emits one spelling for both artifacts. | |
| 97 | 103 | --write) WRITE_DEV="${2:?--write needs a device path}"; shift 2 ;; | |
| 98 | 104 | --write-only) WRITE_ONLY=1; shift ;; | |
| 99 | - | -h|--help) sed -n '2,34p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; | |
| 105 | + | # The header block is the help text, so it stops where the comments stop | |
| 106 | + | # rather than at a line number that rots the next time the header grows. | |
| 107 | + | -h|--help) awk 'NR==1 {next} !/^#/ {exit} {sub(/^# ?/, ""); print}' "${BASH_SOURCE[0]}"; exit 0 ;; | |
| 100 | 108 | *) die "unknown argument: $1 (see --help)" ;; | |
| 101 | 109 | esac | |
| 102 | 110 | done | |
| @@ -108,7 +116,7 @@ | |||
| 108 | 116 | # before that check: writing a stick should not depend on being able to build. | |
| 109 | 117 | if [ "$WRITE_ONLY" -eq 1 ]; then | |
| 110 | 118 | [ -n "$WRITE_DEV" ] || die "--write-only only makes sense with --write" | |
| 111 | - | sudo test -f "$ARTIFACT" || die "no ISO at $ARTIFACT; build one first" | |
| 119 | + | privc test -f "$ARTIFACT" || die "no ISO at $ARTIFACT; build one first" | |
| 112 | 120 | say "writing the ISO already in $OUTPUT" | |
| 113 | 121 | exec "$REPO_ROOT/build/write-device.sh" "$ARTIFACT" "$WRITE_DEV" "installer ISO" | |
| 114 | 122 | fi | |
| @@ -118,27 +126,27 @@ | |||
| 118 | 126 | # 1. The Alloy image. | |
| 119 | 127 | if [ "$SKIP_BUILD" -eq 0 ]; then | |
| 120 | 128 | say "building $IMAGE" | |
| 121 | - | sudo podman build "${BUILD_ARGS[@]}" -t "$IMAGE" "$REPO_ROOT" | |
| 129 | + | priv podman build "${BUILD_ARGS[@]}" -t "$IMAGE" "$REPO_ROOT" | |
| 122 | 130 | else | |
| 123 | - | sudo podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build" | |
| 131 | + | privc podman image exists "$IMAGE" || die "$IMAGE not in the root store; drop --skip-build" | |
| 124 | 132 | say "reusing $IMAGE" | |
| 125 | 133 | fi | |
| 126 | 134 | ||
| 127 | 135 | # 2. The builder, derived from it so the initramfs matches the shipped kernel. | |
| 128 | 136 | say "building $BUILDER" | |
| 129 | - | sudo podman build -t "$BUILDER" \ | |
| 137 | + | priv podman build -t "$BUILDER" \ | |
| 130 | 138 | --build-arg "BASE=$IMAGE" \ | |
| 131 | 139 | -f "$REPO_ROOT/build/Containerfile.iso" \ | |
| 132 | 140 | "$REPO_ROOT" | |
| 133 | 141 | ||
| 134 | 142 | # 3. Somewhere to work. Rotated rather than deleted, same as build-image.sh: | |
| 135 | 143 | # a failed run should not take the last good ISO with it. | |
| 136 | - | if [ -d "$OUTPUT" ] && [ -n "$(sudo ls -A "$OUTPUT" 2>/dev/null)" ]; then | |
| 144 | + | if [ -d "$OUTPUT" ] && [ -n "$(privc ls -A "$OUTPUT" 2>/dev/null)" ]; then | |
| 137 | 145 | say "rotating previous output to ${OUTPUT}.prev" | |
| 138 | - | sudo rm -rf "${OUTPUT:?}.prev" | |
| 139 | - | sudo mv "$OUTPUT" "${OUTPUT}.prev" | |
| 146 | + | privc rm -rf "${OUTPUT:?}.prev" | |
| 147 | + | privc mv "$OUTPUT" "${OUTPUT}.prev" | |
| 140 | 148 | fi | |
| 141 | - | sudo mkdir -p "$OUTPUT" "$WORKDIR/source" | |
| 149 | + | privc mkdir -p "$OUTPUT" "$WORKDIR/source" | |
| 142 | 150 | ||
| 143 | 151 | # 4. The image to install, as an OCI layout. skopeo rather than `podman | |
| 144 | 152 | # save` because bootc reads skopeo transports, and this is the exact | |
| @@ -159,7 +167,7 @@ | |||
| 159 | 167 | # has no skopeo, and requiring one would make the build depend on which | |
| 160 | 168 | # distro happens to be running it. The host's container store is bind | |
| 161 | 169 | # mounted in so skopeo can read the image out of it. | |
| 162 | - | sudo podman run --rm --privileged \ | |
| 170 | + | priv podman run --rm --privileged \ | |
| 163 | 171 | --security-opt label=type:unconfined_t \ | |
| 164 | 172 | -v /var/lib/containers/storage:/var/lib/containers/storage \ | |
| 165 | 173 | -v "$WORKDIR/source":/source \ | |
| @@ -176,13 +184,13 @@ | |||
| 176 | 184 | # extracting it, which saves writing several GB to disk only to read it | |
| 177 | 185 | # straight back. Rootful, so it lands in the same store the image is in. | |
| 178 | 186 | say "mounting $IMAGE" | |
| 179 | - | ROOTFS="$(sudo podman image mount "$IMAGE")" | |
| 187 | + | ROOTFS="$(privc podman image mount "$IMAGE")" | |
| 180 | 188 | [ -n "$ROOTFS" ] || die "could not mount $IMAGE" | |
| 181 | - | trap 'sudo podman image umount "$IMAGE" >/dev/null 2>&1 || true' EXIT | |
| 189 | + | trap 'privc podman image umount "$IMAGE" >/dev/null 2>&1 || true' EXIT | |
| 182 | 190 | say "rootfs at $ROOTFS" | |
| 183 | 191 | ||
| 184 | 192 | say "assembling the ISO" | |
| 185 | - | sudo podman run --rm --privileged \ | |
| 193 | + | priv podman run --rm --privileged \ | |
| 186 | 194 | -e "ALLOY_ISO_FAST=$FAST" \ | |
| 187 | 195 | -e "ALLOY_UPDATE_TARGET=$UPDATE_TARGET" \ | |
| 188 | 196 | -e "ALLOY_ISO_ARCH=$ARCH" \ | |
| @@ -192,8 +200,8 @@ | |||
| 192 | 200 | -v "$WORKDIR/source":/source:ro \ | |
| 193 | 201 | "$BUILDER" | |
| 194 | 202 | ||
| 195 | - | sudo test -f "$ARTIFACT" || die "no ISO produced" | |
| 196 | - | say "built: $ARTIFACT ($(sudo du -h "$ARTIFACT" | cut -f1))" | |
| 203 | + | privc test -f "$ARTIFACT" || die "no ISO produced" | |
| 204 | + | say "built: $ARTIFACT ($(privc du -h "$ARTIFACT" | cut -f1))" | |
| 197 | 205 | echo | |
| 198 | 206 | ||
| 199 | 207 | if [ -n "$WRITE_DEV" ]; then |
| @@ -42,9 +42,10 @@ | |||
| 42 | 42 | # registry or a git remote nobody could reach. Prints `error:`, which the | |
| 43 | 43 | # sweep turns into an error cell rather than a finding. | |
| 44 | 44 | # | |
| 45 | - | # Rootless on purpose, unlike build-image.sh, which needs `sudo podman` for the | |
| 46 | - | # bootc conversion. Nothing here leaves the builder stage, so the root store is | |
| 47 | - | # not involved and a check should not ask for privilege it does not need. The | |
| 45 | + | # Rootless on purpose, unlike build-image.sh, which needs rootful podman for | |
| 46 | + | # the bootc conversion and escalates through build/privilege.sh to get it. | |
| 47 | + | # Nothing here leaves the builder stage, so the root store is not involved and | |
| 48 | + | # a check should not ask for privilege it does not need. The | |
| 48 | 49 | # consequence worth knowing: this populates the *rootless* layer cache, so it | |
| 49 | 50 | # does not warm a subsequent `build/build-image.sh`. | |
| 50 | 51 |
| @@ -22,11 +22,16 @@ | |||
| 22 | 22 | ||
| 23 | 23 | die() { printf 'error: %s\n' "$*" >&2; exit 1; } | |
| 24 | 24 | ||
| 25 | + | # priv / privc. run0 where it exists, sudo where it does not; see the header | |
| 26 | + | # of build/privilege.sh for which of the two a call site wants. | |
| 27 | + | # shellcheck source=build/privilege.sh | |
| 28 | + | . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/privilege.sh" | |
| 29 | + | ||
| 25 | 30 | ARTIFACT="${1:?usage: write-device.sh <artifact> <device> [label]}" | |
| 26 | 31 | DEVICE="${2:?usage: write-device.sh <artifact> <device> [label]}" | |
| 27 | 32 | LABEL="${3:-image}" | |
| 28 | 33 | ||
| 29 | - | sudo test -f "$ARTIFACT" || die "$ARTIFACT is not a file" | |
| 34 | + | privc test -f "$ARTIFACT" || die "$ARTIFACT is not a file" | |
| 30 | 35 | [ -b "$DEVICE" ] || die "$DEVICE is not a block device" | |
| 31 | 36 | ||
| 32 | 37 | # A whole disk, not a partition. An ISO written to /dev/sda1 produces | |
| @@ -74,9 +79,9 @@ | |||
| 74 | 79 | # still prints the transfer summary once, at the end, which is the whole of what | |
| 75 | 80 | # a log wants from it. Checked on stderr because that is where dd writes both. | |
| 76 | 81 | if [ -t 2 ]; then | |
| 77 | - | sudo dd if="$ARTIFACT" of="$DEVICE" bs=4M oflag=direct conv=fsync status=progress | |
| 82 | + | priv dd if="$ARTIFACT" of="$DEVICE" bs=4M oflag=direct conv=fsync status=progress | |
| 78 | 83 | else | |
| 79 | - | sudo dd if="$ARTIFACT" of="$DEVICE" bs=4M oflag=direct conv=fsync | |
| 84 | + | privc dd if="$ARTIFACT" of="$DEVICE" bs=4M oflag=direct conv=fsync | |
| 80 | 85 | fi | |
| 81 | 86 | sync | |
| 82 | 87 | ||
| @@ -85,13 +90,13 @@ | |||
| 85 | 90 | # `dd | head -c N | sha256sum` pipeline reported phantom corruption on a | |
| 86 | 91 | # write that cmp proved perfect (wiki alloy-build-notes). | |
| 87 | 92 | echo "==> Verifying the write" | |
| 88 | - | size="$(sudo stat -c %s "$ARTIFACT")" | |
| 93 | + | size="$(privc stat -c %s "$ARTIFACT")" | |
| 89 | 94 | # A check that cannot fail is not a check: prove cmp can still disagree | |
| 90 | 95 | # before trusting it to agree. | |
| 91 | - | if sudo cmp -s -n 4096 "$DEVICE" /dev/zero; then | |
| 96 | + | if privc cmp -s -n 4096 "$DEVICE" /dev/zero; then | |
| 92 | 97 | die "negative control passed, which means cmp is not comparing anything" | |
| 93 | 98 | fi | |
| 94 | - | sudo cmp -n "$size" "$DEVICE" "$ARTIFACT" \ | |
| 99 | + | privc cmp -n "$size" "$DEVICE" "$ARTIFACT" \ | |
| 95 | 100 | || die "$DEVICE does not match $ARTIFACT; the write is bad" | |
| 96 | 101 | echo "==> Verified $size bytes." | |
| 97 | 102 | echo "==> Done. $DEVICE is now a bootable Alloy $LABEL." |
| @@ -1,0 +1,42 @@ | |||
| 1 | + | # privilege.sh — how the build scripts become root. Sourced, never executed. | |
| 2 | + | # | |
| 3 | + | # Alloy names run0 as the way a person becomes root (wiki alloy-privilege, | |
| 4 | + | # "run0-first, not sudo"). These scripts are the one place that could not | |
| 5 | + | # follow, because they run on the *dev host* rather than on the shipped OS, | |
| 6 | + | # and the x86_64 build host is fw13: Pop!_OS, systemd 255, no run0 at all. | |
| 7 | + | # So the mechanism resolves here, once, instead of being hardcoded to either | |
| 8 | + | # one. When every build host is on systemd 256 or newer this file loses its | |
| 9 | + | # else-branch and becomes two lines. | |
| 10 | + | # | |
| 11 | + | # Two functions, because run0 runs its child on a pty by default and sudo | |
| 12 | + | # does not: | |
| 13 | + | # | |
| 14 | + | # priv output is for the person watching. The pty is right: podman keeps | |
| 15 | + | # its progress rendering and dd redraws its status line in place. | |
| 16 | + | # privc output is captured, piped, or consumed as an exit status. --pipe | |
| 17 | + | # suppresses the pty, without which every captured line arrives | |
| 18 | + | # carrying a carriage return and `$(privc du -h X | cut -f1)` | |
| 19 | + | # returns "1.2G" with a \r stuck to the end of it. | |
| 20 | + | # | |
| 21 | + | # The rule for a new call site: if a human reads it as it scrolls past, priv. | |
| 22 | + | # Anything else, privc. | |
| 23 | + | # | |
| 24 | + | # Working directory is the other thing run0 does not inherit the way sudo | |
| 25 | + | # does. Every privileged call site under build/ passes absolute paths, built | |
| 26 | + | # from $REPO_ROOT, so it does not matter — verified 2026-08-10 across | |
| 27 | + | # build-iso.sh, build-image.sh and write-device.sh. A new call site taking a | |
| 28 | + | # relative path is the thing that would break it, so check rather than assume. | |
| 29 | + | # | |
| 30 | + | # PRIV_NAME is the resolved command, for scripts that print a root command | |
| 31 | + | # for the operator to type. A message naming sudo on a machine where the | |
| 32 | + | # script itself used run0 teaches the wrong one. | |
| 33 | + | ||
| 34 | + | if command -v run0 >/dev/null 2>&1; then | |
| 35 | + | PRIV_NAME="run0" | |
| 36 | + | priv() { run0 "$@"; } | |
| 37 | + | privc() { run0 --pipe "$@"; } | |
| 38 | + | else | |
| 39 | + | PRIV_NAME="sudo" | |
| 40 | + | priv() { sudo "$@"; } | |
| 41 | + | privc() { sudo "$@"; } | |
| 42 | + | fi |