max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01WFBzMprSmNCfvdj2cGZyka
5 files changed,
+187 insertions,
-10 deletions
| @@ -1525,6 +1525,12 @@ | |||
| 1525 | 1525 | # signing is a build-host act and has nothing to do with a desktop | |
| 1526 | 1526 | # session. | |
| 1527 | 1527 | minisign \ | |
| 1528 | + | # efibootmgr, for usr/bin/alloy-boot-entry, which names the firmware boot | |
| 1529 | + | # entry after `bootc install` has made one. It arrives anyway as something | |
| 1530 | + | # else's dependency; named here because the install step that needs it | |
| 1531 | + | # exits 0 when it is missing, so losing it would show up as machines | |
| 1532 | + | # quietly going back to saying Fedora rather than as a build failure. | |
| 1533 | + | efibootmgr \ | |
| 1528 | 1534 | # podman, the runtime behind two of `alloy pkg box`'s three isolation | |
| 1529 | 1535 | # levels: `workspace` calls it directly and distrobox wraps it for | |
| 1530 | 1536 | # `host`. flatpak is the third and is client-only, since `sandboxed` | |
| @@ -4157,12 +4163,24 @@ | |||
| 4157 | 4163 | # is one layer earlier than either, in the firmware's boot menu. | |
| 4158 | 4164 | # | |
| 4159 | 4165 | # HOW THE NAME GETS THERE. shim ships BOOTX64.CSV beside itself. When the | |
| 4160 | - | # machine has no NVRAM entry pointing at shim -- which is every freshly | |
| 4161 | - | # installed machine, because bootupd writes the ESP and calls no efibootmgr -- | |
| 4162 | - | # the firmware runs \EFI\BOOT\BOOTX64.EFI, which is shim, which chain-loads | |
| 4163 | - | # fbx64.efi, which reads this CSV and creates the entry. Field 1 is the binary | |
| 4164 | - | # to point at and field 2 is the description the boot menu shows. Stock, it | |
| 4165 | - | # reads `shimx64.efi,Fedora,,This is the boot entry for Fedora`. | |
| 4166 | + | # machine has no NVRAM entry pointing at shim, the firmware runs | |
| 4167 | + | # \EFI\BOOT\BOOTX64.EFI, which is shim, which chain-loads fbx64.efi, which reads | |
| 4168 | + | # this CSV and creates the entry. Field 1 is the binary to point at and field 2 | |
| 4169 | + | # is the description the boot menu shows. Stock, it reads | |
| 4170 | + | # `shimx64.efi,Fedora,,This is the boot entry for Fedora`. | |
| 4171 | + | # | |
| 4172 | + | # THIS IS NOT SUFFICIENT ON ITS OWN, and this comment said it was until | |
| 4173 | + | # 2026-09-07. It claimed the no-entry case was "every freshly installed machine, | |
| 4174 | + | # because bootupd writes the ESP and calls no efibootmgr". Measured on a fresh | |
| 4175 | + | # fw12 install that day: the installed ESP's CSV reads `shimx64.efi,Alloy,,...` | |
| 4176 | + | # and `efibootmgr` reports two entries named Fedora. Something on the install | |
| 4177 | + | # path makes them, so fbx64 never runs and this file is never read. | |
| 4178 | + | # | |
| 4179 | + | # What this RUN is still for: the machine whose NVRAM is cleared, which then | |
| 4180 | + | # rebuilds its entry from the right CSV. The ordinary install is named by | |
| 4181 | + | # usr/bin/alloy-boot-entry, which crates/alloy/src/install/plan.rs runs after | |
| 4182 | + | # the deploy. The two are not redundant -- they cover different moments -- and | |
| 4183 | + | # neither covers the other's. | |
| 4166 | 4184 | # | |
| 4167 | 4185 | # WHAT IS NOT TOUCHED, deliberately. The vendor directory stays EFI/fedora: the | |
| 4168 | 4186 | # path is baked into the signed shim, and renaming it is a Secure Boot question |
| @@ -109,9 +109,12 @@ | |||
| 109 | 109 | continue; | |
| 110 | 110 | } | |
| 111 | 111 | let key = line.split('=').next().unwrap_or("").trim(); | |
| 112 | - | // ARCH is build/host-recipe.sh's own reserved key and never a build | |
| 113 | - | // argument; see that file's header. | |
| 114 | - | if key == "ARCH" { | |
| 112 | + | // The reserved keys are build/host-recipe.sh's own and are never build | |
| 113 | + | // arguments; see that file's header. Read from it rather than listed | |
| 114 | + | // here, because listing them here is what broke this test: UPDATE_TARGET | |
| 115 | + | // became reserved and this skipped ARCH alone, so the suite went red on | |
| 116 | + | // a recipe that was correct. | |
| 117 | + | if reserved_keys().iter().any(|k| k == key) { | |
| 115 | 118 | continue; | |
| 116 | 119 | } | |
| 117 | 120 | assert!( | |
| @@ -120,3 +123,18 @@ | |||
| 120 | 123 | ); | |
| 121 | 124 | } | |
| 122 | 125 | } | |
| 126 | + | ||
| 127 | + | /// The keys `build/host-recipe.sh` consumes itself instead of passing to podman. | |
| 128 | + | /// | |
| 129 | + | /// Derived from the script rather than restated, on the same reasoning as the | |
| 130 | + | /// language whitelist: a value that lives in two files agrees in both only | |
| 131 | + | /// until someone edits one. | |
| 132 | + | fn reserved_keys() -> Vec<String> { | |
| 133 | + | tree("build/host-recipe.sh") | |
| 134 | + | .lines() | |
| 135 | + | .filter_map(|line| { | |
| 136 | + | let rest = line.trim().strip_prefix(r#"if [ "$key" = ""#)?; | |
| 137 | + | rest.split('"').next().map(str::to_string) | |
| 138 | + | }) | |
| 139 | + | .collect() | |
| 140 | + | } |
| @@ -503,6 +503,28 @@ | |||
| 503 | 503 | ) | |
| 504 | 504 | }), | |
| 505 | 505 | )); | |
| 506 | + | // Name the firmware boot entry, last. | |
| 507 | + | // | |
| 508 | + | // The image ships a patched BOOTX64.CSV and it reaches the ESP intact, but | |
| 509 | + | // that file is read by fbx64.efi, and fbx64 runs only when no entry points | |
| 510 | + | // at shim. `bootc install` leaves entries of its own, so the fallback never | |
| 511 | + | // runs and a machine comes up saying Fedora. Measured on a fresh fw12 | |
| 512 | + | // install 2026-09-07: the installed ESP's CSV says Alloy and `efibootmgr` | |
| 513 | + | // says Fedora, twice. | |
| 514 | + | // | |
| 515 | + | // LAST, AND NOT MERELY AFTER THE DEPLOY. The helper maps the entry's | |
| 516 | + | // PARTUUID to a device with `lsblk`, which is the same reading `udevadm | |
| 517 | + | // settle` above exists to make trustworthy: for a moment after bootc | |
| 518 | + | // returns, lsblk reports null for every partition. Queued here it is behind | |
| 519 | + | // that settle and behind everything a resolver pushes in front of it, so | |
| 520 | + | // the disk it reads is one that has already been mounted, configured and | |
| 521 | + | // checked. | |
| 522 | + | // | |
| 523 | + | // `Run` rather than `Attempt`: the helper exits 0 for every reason to do | |
| 524 | + | // nothing at all, so a non-zero status means it left NVRAM holding | |
| 525 | + | // something it could not account for. That is worth stopping for; a name in | |
| 526 | + | // a firmware menu is not. | |
| 527 | + | stages.push(Stage::Run(Invocation::new("alloy-boot-entry"))); | |
| 506 | 528 | stages | |
| 507 | 529 | } | |
| 508 | 530 |
| @@ -1398,7 +1398,7 @@ | |||
| 1398 | 1398 | let (view, _log) = at_summary(); | |
| 1399 | 1399 | let shown = view.plan_display(); | |
| 1400 | 1400 | ||
| 1401 | - | assert_eq!(shown.len(), 4, "{shown:#?}"); | |
| 1401 | + | assert_eq!(shown.len(), 5, "{shown:#?}"); | |
| 1402 | 1402 | // The update stream is on the line the confirm screen shows, which is | |
| 1403 | 1403 | // where a flag that decides how the machine behaves for its whole life | |
| 1404 | 1404 | // belongs. | |
| @@ -1420,6 +1420,10 @@ | |||
| 1420 | 1420 | assert_eq!(shown[1], "udevadm settle"); | |
| 1421 | 1421 | assert_eq!(shown[2], "mkdir -p /run/alloy-target"); | |
| 1422 | 1422 | assert!(shown[3].starts_with("lsblk"), "{}", shown[3]); | |
| 1423 | + | // Naming the firmware boot entry is the last line the summary can show. It | |
| 1424 | + | // is queued behind everything the discovery resolves, because it reads the | |
| 1425 | + | // disk with lsblk and wants the settle above to have happened. | |
| 1426 | + | assert_eq!(shown[4], "alloy-boot-entry"); | |
| 1423 | 1427 | } | |
| 1424 | 1428 | ||
| 1425 | 1429 | // The summary can only show what is known before anything runs. Everything |
| @@ -1,0 +1,115 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # alloy-boot-entry — make the firmware boot menu say Alloy, after the install. | |
| 3 | + | # | |
| 4 | + | # WHY THIS EXISTS AND WHY THE CONTAINERFILE'S CSV IS NOT ENOUGH. The image ships | |
| 5 | + | # a patched BOOTX64.CSV in the bootupd payload, and it reaches the ESP intact: | |
| 6 | + | # measured on a fresh fw12 install 2026-09-07, the installed ESP's | |
| 7 | + | # EFI/fedora/BOOTX64.CSV reads `shimx64.efi,Alloy,,...`. The machine still came | |
| 8 | + | # up with two entries named Fedora. | |
| 9 | + | # | |
| 10 | + | # The reason is that the CSV is read by fbx64.efi, and fbx64 runs only when the | |
| 11 | + | # firmware has no entry pointing at shim. Something on the install path creates | |
| 12 | + | # those entries itself, so fbx64's fallback never runs and the CSV is never | |
| 13 | + | # read. The Containerfile comment beside that RUN says the opposite -- "bootupd | |
| 14 | + | # writes the ESP and calls no efibootmgr" -- and that is measured to be false. | |
| 15 | + | # | |
| 16 | + | # So the CSV is necessary and not sufficient. It is what a machine whose NVRAM | |
| 17 | + | # is cleared will use; this is what every ordinary install needs. | |
| 18 | + | # | |
| 19 | + | # WHAT IT DOES NOT DO, matching the Containerfile: the vendor directory stays | |
| 20 | + | # EFI/fedora and the loader stays shimx64.efi. Both are baked into the signed | |
| 21 | + | # shim, so changing either is a Secure Boot question rather than a branding one. | |
| 22 | + | # Only the description changes. | |
| 23 | + | # | |
| 24 | + | # ORDERING IS THE SAFETY PROPERTY. efibootmgr has no rename verb, so each entry | |
| 25 | + | # is recreated and verified BEFORE the old one is deleted. There is no moment at | |
| 26 | + | # which the machine has no entry pointing at shim, and a failure anywhere leaves | |
| 27 | + | # either the old entry or both. Both boot. | |
| 28 | + | # | |
| 29 | + | # EXIT STATUS IS A CONTRACT. This runs as the last thing in an install that has | |
| 30 | + | # otherwise succeeded, and the name in a firmware menu is not worth failing a | |
| 31 | + | # good install over. So every reason to do nothing -- not an EFI boot, no | |
| 32 | + | # efibootmgr, nothing left to rename -- exits 0. A non-zero exit means NVRAM was | |
| 33 | + | # left worse than it was found, which is the only outcome worth stopping for. | |
| 34 | + | ||
| 35 | + | set -eu | |
| 36 | + | ||
| 37 | + | NAME="Alloy" | |
| 38 | + | ||
| 39 | + | [ -d /sys/firmware/efi ] || { echo "not an EFI boot; no entry to name"; exit 0; } | |
| 40 | + | command -v efibootmgr >/dev/null 2>&1 || { echo "no efibootmgr; leaving the boot entry alone"; exit 0; } | |
| 41 | + | ||
| 42 | + | dump="$(efibootmgr -v)" || { echo "efibootmgr could not read NVRAM; leaving it alone"; exit 0; } | |
| 43 | + | ||
| 44 | + | # Entries pointing at a shim on an ESP whose description is not already ours. | |
| 45 | + | stale="$(printf '%s\n' "$dump" \ | |
| 46 | + | | grep -E '^Boot[0-9A-Fa-f]{4}\*?[[:space:]]' \ | |
| 47 | + | | grep -iE '\\EFI\\[^\\]+\\shim' \ | |
| 48 | + | | grep -viE "[[:space:]]${NAME}[[:space:]]" || true)" | |
| 49 | + | ||
| 50 | + | if [ -z "$stale" ]; then | |
| 51 | + | echo "the firmware boot entry already says $NAME" | |
| 52 | + | exit 0 | |
| 53 | + | fi | |
| 54 | + | ||
| 55 | + | order="$(printf '%s\n' "$dump" | sed -nE 's/^BootOrder: (.*)$/\1/p')" | |
| 56 | + | new_order="$order" | |
| 57 | + | ||
| 58 | + | # Looped from a file rather than from a pipe. A `while read` on the right of a | |
| 59 | + | # pipe runs in a subshell, where the order this builds would be discarded at the | |
| 60 | + | # end of the loop and an `exit 1` would end the subshell rather than the script. | |
| 61 | + | # Both halves of this loop's job are exactly those two things. | |
| 62 | + | entries="$(mktemp)" | |
| 63 | + | trap 'rm -f "$entries"' EXIT | |
| 64 | + | printf '%s\n' "$stale" > "$entries" | |
| 65 | + | ||
| 66 | + | while read -r entry; do | |
| 67 | + | [ -n "$entry" ] || continue | |
| 68 | + | num="$(printf '%s\n' "$entry" | sed -nE 's/^Boot([0-9A-Fa-f]{4})\*?[[:space:]].*/\1/p')" | |
| 69 | + | loader="$(printf '%s\n' "$entry" | sed -nE 's/.*(\\EFI\\[^\\]+\\[^ \t]*\.efi).*/\1/p' | head -1)" | |
| 70 | + | uuid="$(printf '%s\n' "$entry" | sed -nE 's/.*HD\([0-9]+,GPT,([0-9a-fA-F-]+),.*/\1/p')" | |
| 71 | + | [ -n "$num" ] && [ -n "$loader" ] && [ -n "$uuid" ] || { | |
| 72 | + | echo "could not read an entry off: $entry" >&2 | |
| 73 | + | continue | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | # The disk and partition the entry already names, so the replacement points | |
| 77 | + | # at the same place the firmware boots from rather than at a device this | |
| 78 | + | # script guessed. It also keeps it off the installer medium, which carries | |
| 79 | + | # an EFI System partition of its own and is still plugged in. | |
| 80 | + | dev="$(lsblk -rno PATH,PARTUUID 2>/dev/null | awk -v u="$uuid" 'tolower($2)==tolower(u){print $1}' | head -1)" | |
| 81 | + | [ -n "$dev" ] || { echo "no device carries PARTUUID $uuid; leaving Boot$num alone" >&2; continue; } | |
| 82 | + | disk="/dev/$(lsblk -no PKNAME "$dev" | head -1)" | |
| 83 | + | partn="$(cat "/sys/class/block/$(basename "$dev")/partition" 2>/dev/null || true)" | |
| 84 | + | [ -b "$disk" ] && [ -n "$partn" ] || { echo "could not place $dev; leaving Boot$num alone" >&2; continue; } | |
| 85 | + | ||
| 86 | + | before="$(efibootmgr | sed -nE 's/^Boot([0-9A-Fa-f]{4})\*?.*/\1/p' | sort -u)" | |
| 87 | + | efibootmgr -q -c -d "$disk" -p "$partn" -L "$NAME" -l "$loader" | |
| 88 | + | after="$(efibootmgr | sed -nE 's/^Boot([0-9A-Fa-f]{4})\*?.*/\1/p' | sort -u)" | |
| 89 | + | created="$(printf '%s\n' "$before" "$after" | sort | uniq -u | head -1)" | |
| 90 | + | ||
| 91 | + | # From here the machine has two entries pointing at shim and boots either | |
| 92 | + | # way, so every exit below is safe. Non-zero because NVRAM now holds | |
| 93 | + | # something this script put there and could not finish accounting for. | |
| 94 | + | [ -n "$created" ] || { | |
| 95 | + | echo "no new boot entry appeared; Boot$num is unchanged" >&2 | |
| 96 | + | exit 1 | |
| 97 | + | } | |
| 98 | + | efibootmgr | grep -q "^Boot${created}\*\?[[:space:]]*${NAME}\b" || { | |
| 99 | + | echo "Boot$created is not the $NAME entry expected; Boot$num is unchanged" >&2 | |
| 100 | + | exit 1 | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | efibootmgr -q -b "$num" -B | |
| 104 | + | # printf, not echo: the loader path is full of backslashes and `echo` in | |
| 105 | + | # dash interprets them, so `\EFI\fedora\shimx64.efi` logs as | |
| 106 | + | # `\EFIedora\shimx64.efi` and the line quietly misreports what was done. | |
| 107 | + | printf 'boot entry: Boot%s -> Boot%s "%s" (%s)\n' "$num" "$created" "$NAME" "$loader" | |
| 108 | + | ||
| 109 | + | new_order="$(printf '%s' "$new_order" | sed "s/\b$num\b/$created/I")" | |
| 110 | + | done < "$entries" | |
| 111 | + | ||
| 112 | + | if [ -n "$new_order" ] && [ "$new_order" != "$order" ]; then | |
| 113 | + | efibootmgr -q -o "$new_order" | |
| 114 | + | echo "boot order: $order -> $new_order" | |
| 115 | + | fi |