Skip to main content

max / alloy

Give the installer ISO an aarch64 path make-iso.sh hardcoded x86_64: the grub2-mkimage format, the EFI binary name, and the assertion that read it back. On aarch64 there was no installer medium at all, so every question `alloy install` exists to ask -- disk, layout, LUKS, the recovery phrase, the ssh key, hostname -- was unavailable on arm, and the only path was dd'ing a pre-installed image. Take the architecture from `uname -m` with an ALLOY_ISO_ARCH override, map it to a GRUB target and the removable-media name UEFI specifies for it, and refuse anything that is neither. The module list is unchanged: every module named is in both targets. The builder picks its grub2-efi-*-modules package the same way, and make-iso.sh checks the module directory before the squashfs rather than after, naming the package that is missing. The serial console is the fifth architecture-specific thing, and was not in the plan: ttyS0 is an 8250 and an SBSA board presents a PL011, so a headless arm boot would have logged nowhere. Both are listed on arm and the kernel ignores the one that is absent; tty0 stays last, so /dev/console is still the screen. Unverified on hardware: no arm ISO has been built or booted yet. That is the astra work in GO 35bc1716, and it is gated on validating the aarch64 image first.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 19:47 UTC
Signed with PGP, not checked
Commit: 7e26c2915ae0296a830192e66a38673d61698469
Parent: eeeb768
4 files changed, +100 insertions, -14 deletions
@@ -14,12 +14,21 @@
14 14 # dnf is only usable here because this stage is thrown away; the shipped
15 15 # image disables the third-party repos it layered from at build time, so
16 16 # nothing below reaches for them.
17 - RUN dnf install -y \
17 + #
18 + # The GRUB packages are the one architecture-specific part: Fedora names them
19 + # for the EFI target, and the x64 pair does not exist on an aarch64 host. The
20 + # builder is always native (it is derived FROM the Alloy image, so it carries
21 + # that image's kernel), so `uname -m` is the right question to ask.
22 + RUN case "$(uname -m)" in \
23 + x86_64) GRUB_EFI="grub2-efi-x64 grub2-efi-x64-modules" ;; \
24 + aarch64) GRUB_EFI="grub2-efi-aa64 grub2-efi-aa64-modules" ;; \
25 + *) echo "no GRUB packages known for $(uname -m)" >&2; exit 1 ;; \
26 + esac \
27 + && dnf install -y \
18 28 dracut-live \
19 29 xorriso \
20 30 squashfs-tools \
21 - grub2-efi-x64 \
22 - grub2-efi-x64-modules \
31 + $GRUB_EFI \
23 32 grub2-tools \
24 33 grub2-tools-extra \
25 34 dosfstools \
@@ -26,6 +26,7 @@
26 26 # build/build-iso.sh --fast # iteration: cheap compression, keeps source
27 27 # build/build-iso.sh --fast --skip-source # boot chain only, cannot install
28 28 # build/build-iso.sh --update-target host:5000/alloy:43 # updates come from there
29 + # build/build-iso.sh --arch aarch64 # override the detected architecture
29 30 # build/build-iso.sh --write /dev/sdX # build, then write to a device
30 31 # build/build-iso.sh --write-only --write /dev/sdX # write the ISO already built
31 32 #
@@ -48,6 +49,11 @@
48 49 # Empty means the installer keeps its compiled-in default, the public registry.
49 50 # See --update-target below.
50 51 UPDATE_TARGET=""
52 + # Empty means make-iso.sh takes the builder's own `uname -m`, which is what
53 + # every real build wants: the builder carries the Alloy image's kernel, so
54 + # the medium and its contents are the same architecture by construction.
55 + # See --arch below.
56 + ARCH=""
51 57
52 58 BUILD_ARGS=()
53 59
@@ -78,11 +84,19 @@
78 84 --update-target)
79 85 [ $# -ge 2 ] || die "--update-target needs a registry reference"
80 86 UPDATE_TARGET="$2"; shift 2 ;;
87 + # Overrides the architecture make-iso.sh would detect. For exercising the
88 + # arm64-efi GRUB path from an x86_64 box: the module packages are noarch,
89 + # so the EFI binary builds anywhere, while the kernel and the squashfs
90 + # around it stay whatever the image is. That makes this a test of the boot
91 + # chain and not a cross-build, and the ISO it produces boots nothing.
92 + --arch)
93 + [ $# -ge 2 ] || die "--arch needs x86_64 or aarch64"
94 + ARCH="$2"; shift 2 ;;
81 95 # The device write. Same two flags build-image.sh takes, because the
82 96 # console emits one spelling for both artifacts.
83 97 --write) WRITE_DEV="${2:?--write needs a device path}"; shift 2 ;;
84 98 --write-only) WRITE_ONLY=1; shift ;;
85 - -h|--help) sed -n '2,33p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
99 + -h|--help) sed -n '2,34p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
86 100 *) die "unknown argument: $1 (see --help)" ;;
87 101 esac
88 102 done
@@ -171,6 +185,7 @@
171 185 sudo podman run --rm --privileged \
172 186 -e "ALLOY_ISO_FAST=$FAST" \
173 187 -e "ALLOY_UPDATE_TARGET=$UPDATE_TARGET" \
188 + -e "ALLOY_ISO_ARCH=$ARCH" \
174 189 --security-opt label=type:unconfined_t \
175 190 -v "$ROOTFS":/rootfs:ro \
176 191 -v "$OUTPUT":/output \
M build/make-iso.sh +68 -10
@@ -27,6 +27,54 @@
27 27 [ -d "$ROOTFS" ] || { echo "no $ROOTFS" >&2; exit 1; }
28 28 [ -d "$OUTPUT" ] || { echo "no $OUTPUT" >&2; exit 1; }
29 29
30 + # ---------------------------------------------------------------------
31 + # 0. Which architecture this medium is for.
32 + #
33 + # From `uname -m`, because the builder image is derived from the Alloy image
34 + # and carries its kernel: the squashfs, the initramfs and the OCI layout are
35 + # all already the architecture this is running on, and an ISO whose boot
36 + # chain disagreed with them would boot nothing. ALLOY_ISO_ARCH overrides it
37 + # for testing the GRUB half alone; it does not make this a cross-build.
38 + #
39 + # Anything but the two mappings is an explicit refusal. Falling through to a
40 + # default would produce an ISO with the wrong EFI binary name, which fails
41 + # as "the stick is not bootable" with no diagnosis available on the machine
42 + # that cannot boot it.
43 + # ---------------------------------------------------------------------
44 + ARCH="${ALLOY_ISO_ARCH:-$(uname -m)}"
45 + case "$ARCH" in
46 + x86_64)
47 + GRUB_FORMAT="x86_64-efi"
48 + # The removable-media path from the UEFI spec. Firmware looks for this
49 + # exact name on a medium with no boot entry of its own and nothing else,
50 + # so it is per-architecture and is not ours to choose.
51 + EFI_NAME="BOOTX64.EFI"
52 + GRUB_MODULE_PKG="grub2-efi-x64-modules"
53 + SERIAL_CONSOLES="console=ttyS0,115200" ;;
54 + aarch64)
55 + GRUB_FORMAT="arm64-efi"
56 + EFI_NAME="BOOTAA64.EFI"
57 + GRUB_MODULE_PKG="grub2-efi-aa64-modules"
58 + # ttyAMA0 first because SBSA mandates a PL011 and that is what an Ampere
59 + # board actually presents; ttyS0 stays listed for boards that present an
60 + # 8250 instead. The kernel writes to every console that exists and
61 + # ignores the ones that do not, so listing both costs nothing and losing
62 + # the serial log on a headless arm box costs the only view of a failed
63 + # boot. tty0 stays last, so /dev/console is still the screen.
64 + SERIAL_CONSOLES="console=ttyAMA0,115200 console=ttyS0,115200" ;;
65 + *)
66 + echo "unsupported architecture: $ARCH (this builds x86_64 and aarch64)" >&2
67 + exit 1 ;;
68 + esac
69 +
70 + # grub2-mkimage reads the modules for the target format out of this
71 + # directory, and says only "cannot open directory" when they are absent.
72 + # Name the package instead, before the several minutes of squashfs that
73 + # would otherwise run first.
74 + [ -d "/usr/lib/grub/$GRUB_FORMAT" ] \
75 + || { echo "no GRUB modules for $GRUB_FORMAT; the builder needs $GRUB_MODULE_PKG" >&2; exit 1; }
76 + say "architecture $ARCH, $GRUB_FORMAT, $EFI_NAME"
77 +
30 78 # Version-sorted, not `head -1`: `ls` collates 7.1.10 before 7.1.3, so the
31 79 # alphabetical pick is arbitrary rather than newest. A bootc image should carry
32 80 # exactly one kernel, so more than one means something layered a second and the
@@ -163,13 +211,15 @@
163 211 # reads to find the squashfs.
164 212 # ---------------------------------------------------------------------
165 213 say "grub"
166 - # console= twice on purpose. The kernel writes to every console listed, so
167 - # serial gets a full log for headless debugging, while /dev/console resolves
168 - # to the last one named and so stays on the screen the user is looking at.
214 + # console= more than once on purpose. The kernel writes to every console
215 + # listed, so serial gets a full log for headless debugging, while
216 + # /dev/console resolves to the last one named and so stays on the screen the
217 + # user is looking at. Which serial lines are named is per-architecture; see
218 + # the arch block at the top.
169 219 # Reversed, the installer's own output would go down the serial line and the
170 220 # monitor would sit black. The installer renders on tty1 explicitly
171 221 # (alloy-installer.service, TTYPath) and does not depend on this ordering.
172 - CONSOLES="console=ttyS0,115200 console=tty0"
222 + CONSOLES="$SERIAL_CONSOLES console=tty0"
173 223 # SELinux on the live medium: permissive by default, off as a fallback.
174 224 #
175 225 # The live rootfs.img is built by mkfs.ext4 from a container rootfs, and it
@@ -285,7 +335,7 @@
285 335 configfile /boot/grub/grub.cfg
286 336 EOF
287 337
288 - # Our own GRUB *is* BOOTX64.EFI, with no shim in front of it.
338 + # Our own GRUB *is* the removable-media binary, with no shim in front of it.
289 339 #
290 340 # The signed shim and signed grubx64.efi that shim expects to chain to both
291 341 # ship in RPMs whose files a bootc image strips out of /boot, and the
@@ -294,10 +344,14 @@
294 344 # either way, so this ISO needs Secure Boot off regardless of whether shim
295 345 # is in front of it. Signing is a distribution problem and distribution is
296 346 # not set up (GO task 0d7505b5); when it is, this is where shim goes.
347 + #
348 + # Every module named below is present in both the x86_64-efi and the
349 + # arm64-efi target, so the list is architecture-independent and only the
350 + # format and the output name change.
297 351 grub2-mkimage \
298 - --format=x86_64-efi \
352 + --format="$GRUB_FORMAT" \
299 353 --prefix="/EFI/BOOT" \
300 - --output="$WORK/iso/EFI/BOOT/BOOTX64.EFI" \
354 + --output="$WORK/iso/EFI/BOOT/$EFI_NAME" \
301 355 part_gpt part_msdos fat iso9660 udf normal linux echo all_video test \
302 356 search search_label search_fs_uuid search_fs_file gfxterm gfxterm_background \
303 357 configfile loadenv chain efi_gop efi_uga ls cat halt reboot minicmd \
@@ -336,9 +390,13 @@
336 390 # Read it back out of the filesystem rather than trusting the exit codes: a
337 391 # bootloader that is not in here is the one defect this whole file exists to
338 392 # avoid, and it is invisible everywhere else.
339 - mdir -i "$WORK/efiboot.img" ::/EFI/BOOT | grep -qi 'BOOTX64' \
340 - || { echo "BOOTX64.EFI is not in the ESP; the medium cannot boot" >&2; exit 1; }
341 - say "ESP $(( EFI_KB / 1024 ))M FAT16, BOOTX64.EFI present"
393 + #
394 + # Checked against the name this build wrote, not a fixed one: an arm ISO
395 + # that failed a check naming BOOTX64 would report an architecture it was
396 + # never building for.
397 + mdir -i "$WORK/efiboot.img" ::/EFI/BOOT | grep -qi "${EFI_NAME%.EFI}" \
398 + || { echo "$EFI_NAME is not in the ESP; the medium cannot boot" >&2; exit 1; }
399 + say "ESP $(( EFI_KB / 1024 ))M FAT16, $EFI_NAME present"
342 400 # Deliberately not copied into $WORK/iso. It is appended to the medium as a
343 401 # real partition below, and El Torito is pointed at that partition rather
344 402 # than at a file in the tree, so a copy inside the ISO filesystem would be
@@ -50,6 +50,10 @@
50 50
51 51 The image is built by `build/build-image.sh` (disk images, via bootc-image-builder) and `build/build-iso.sh` (the installer ISO) on fw13, against rootful podman. That is the whole build story today, and it is a deliberate consequence of two standing rules rather than an accident: builds are native per architecture and nothing is cross-compiled, and app binaries are never built on the production host.
52 52
53 + **Both architectures build an installer, not just x86_64.** `build/make-iso.sh` takes its architecture from `uname -m` and maps it to a GRUB target and a removable-media binary name: `x86_64-efi`/`BOOTX64.EFI`, `arm64-efi`/`BOOTAA64.EFI`. Anything else is refused by name rather than defaulted. The builder installs the matching `grub2-efi-*-modules` package the same way, and the script fails early naming that package if the modules are not there, rather than several minutes into a squashfs. `--arch` on `build/build-iso.sh` overrides the detection; it exercises the GRUB half from the other architecture and does not produce a bootable medium, because everything around the boot chain is still the native image.
54 +
55 + Arm is an installer target and not a dd-the-image special case (decided 2026-08-06). The installer is the only place LUKS, the recovery phrase, the ssh key and the hostname are collected, and a written disk image collects none of them, so an arm machine installed by that route would be installed by a path no user of Alloy takes. `alloy install` has never been run on aarch64, so a medium existing is necessary and not sufficient.
56 +
53 57 An always-on x86_64 builder is still wanted for development. It is no longer between Alloy and other people running it: nothing has to be built centrally for a stranger to install Alloy, because the stranger does the building. astra is always-on but aarch64, fw13 is x86_64 and sleeps aggressively, and the Fedora build host meant to replace Pop!_OS on fw13 is where that resolves whenever it does.
54 58
55 59 What deleting the sourcehut job actually cost is worth naming rather than glossing: it ran a build-and-lint smoke test on every push, and now nothing validates the Containerfile except someone remembering to build. That gap is real and is independent of distribution.