Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions omarchy-mac-boot/bin/omarchy-mac-boot-update
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@

set -uo pipefail

# Installed next to /usr/lib/omarchy-mac/boot; in the source tree, in ../lib.
grub_keep=$(dirname -- "$(realpath -- "$0")")/../lib/omarchy-mac/boot/grub-keep.sh
[[ -r $grub_keep ]] || grub_keep=$(dirname -- "$(realpath -- "$0")")/../lib/grub-keep.sh
# shellcheck source=../lib/grub-keep.sh
source "$grub_keep" || { echo "omarchy-mac-boot-update: cannot load $grub_keep" >&2; exit 1; }

if ! omarchy-mac-limine-active; then
update-grub || { echo "omarchy-mac-boot-update: update-grub failed" >&2; exit 1; }
exit 0
fi
# A Limine Mac that still carries GRUB keeps its (unused) image current, so a
# rollback to GRUB has something to boot; an image without GRUB has none.
# update-grub ships with asahi-scripts even where GRUB itself does not, and
# fails without grub-probe, so GRUB's own tools are the test. A fresh image's
# first boot (OMARCHY_MAC_BOOT_UPDATE_GRUB=0) never booted GRUB and skips it.
if [[ ${OMARCHY_MAC_BOOT_UPDATE_GRUB:-1} != 0 ]] &&
omarchy-cmd-present grub-probe && omarchy-cmd-present grub-mkconfig; then
# grub_tools_present is that one test, shared with lib/provision.sh. A fresh
# image's first boot (OMARCHY_MAC_BOOT_UPDATE_GRUB=0) never booted GRUB and
# skips it.
if [[ ${OMARCHY_MAC_BOOT_UPDATE_GRUB:-1} != 0 ]] && grub_tools_present; then
update-grub >/dev/null || echo "omarchy-mac-boot-update: update-grub failed; Limine is unaffected" >&2
fi
omarchy-mac-limine-cmdline || { echo "omarchy-mac-boot-update: could not derive the Limine command line" >&2; exit 1; }
Expand Down
8 changes: 8 additions & 0 deletions omarchy-mac-boot/lib/grub-keep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Whether GRUB is kept: omarchy-mac-boot-update regenerates GRUB's image only
# where GRUB's own tools exist, because update-grub fails without grub-probe.
# omarchy-mac-boot-update and provision.sh decide this one thing here, and
# nowhere else. OMARCHY_MAC_BOOT_UPDATE_GRUB is not part of it: it suppresses
# a rebuild, it does not stop GRUB from booting the image already there.
grub_tools_present() {
omarchy-cmd-present grub-probe && omarchy-cmd-present grub-mkconfig
}
57 changes: 48 additions & 9 deletions omarchy-mac-boot/lib/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ BOOT_UUID=4f4d5801-424f-4f54-8000-000000000001

# shellcheck source=boot-image-layout.sh
source "$MAC_BOOT_ROOT/usr/lib/omarchy-mac/boot/boot-image-layout.sh"
# shellcheck source=grub-keep.sh
source "$MAC_BOOT_ROOT/usr/lib/omarchy-mac/boot/grub-keep.sh"

log_step() { printf '%s\n' "$*" >&2; }

Expand Down Expand Up @@ -90,10 +92,16 @@ apple_rekey_boot() {
if grep -q 'rd.luks.key=' "$GRUB_DEFAULT"; then
grub_drop_rd_luks_key "$GRUB_DEFAULT" || return 1
fi
if ! mkinitcpio -P </dev/null >&2 || ! omarchy-mac-boot-update >&2; then
log_step "mkinitcpio or omarchy-mac-boot-update failed while dropping the staged key"
return 1
# A Limine Mac without GRUB boots only the UKI, and omarchy-mac-boot-update
# builds it: that is the one initramfs build the re-key needs, as in
# omarchy-mac-encrypt. /boot's image is rebuilt only where GRUB can boot it.
if limine_mac && ! grub_tools_present; then
omarchy-mac-boot-update >&2 && return
elif "$MAC_BOOT_ROOT/usr/bin/mkinitcpio" -P </dev/null >&2 && omarchy-mac-boot-update >&2; then
return
fi
log_step "mkinitcpio or omarchy-mac-boot-update failed while dropping the staged key"
return 1
}

state_get() {
Expand All @@ -112,16 +120,47 @@ limine_mac() {
# The initramfs that asks for the owner's password must load the vendor
# firmware first, or an M2 or later laptop's keyboard cannot type it.
initramfs_orders_firmware() {
local kernel listing
local kernel
kernel=$(omarchy-mac-kernel) || return 1
image_orders_firmware "$MAC_BOOT_ROOT/boot/initramfs-$kernel.img" "/boot/initramfs-$kernel.img"
}

# The images this Mac boots: on a Limine Mac the initramfs inside its UKI,
# and /boot's image besides wherever GRUB is kept, because GRUB's retained
# entries boot it -- also where OMARCHY_MAC_BOOT_UPDATE_GRUB only suppressed
# the rebuild. Elsewhere GRUB boots /boot's image.
boot_image_orders_firmware() {
local kernel uki image status=0
limine_mac || {
initramfs_orders_firmware
return
}
kernel=$(omarchy-mac-kernel) || return 1
if ! listing=$(lsinitcpio -l "$MAC_BOOT_ROOT/boot/initramfs-$kernel.img" 2>/dev/null); then
log_step "cannot list /boot/initramfs-$kernel.img"
uki=$(limine_esp_path)/EFI/Linux/omarchy_$kernel.efi
image=$(mktemp) || return 1
if objcopy -O binary --only-section=.initrd "$MAC_BOOT_ROOT$uki" "$image" 2>/dev/null && [[ -s $image ]]; then
image_orders_firmware "$image" "the initramfs inside $uki" || status=1
else
log_step "cannot read the initramfs inside $uki"
status=1
fi
rm -f "$image"
if grub_tools_present; then
image_orders_firmware "$MAC_BOOT_ROOT/boot/initramfs-$kernel.img" "/boot/initramfs-$kernel.img" || status=1
fi
return "$status"
}

image_orders_firmware() {
local listing
if ! listing=$(lsinitcpio -l "$1" 2>/dev/null); then
log_step "cannot list $2"
return 1
fi
grep -Eq '(^|/)usr/lib/systemd/system-generators/systemd-cryptsetup-generator$' <<<"$listing" &&
grep -Eq '(^|/)usr/lib/systemd/system/omarchy-vendorfw-initrd\.service$' <<<"$listing" &&
grep -Eq '(^|/)usr/lib/systemd/system/systemd-cryptsetup@\.service\.d/omarchy-vendorfw-initrd\.conf$' <<<"$listing" || {
log_step "/boot/initramfs-$kernel.img does not load the vendor firmware before the disk password prompt"
log_step "$2 does not load the vendor firmware before the disk password prompt"
return 1
}
}
Expand Down Expand Up @@ -297,7 +336,7 @@ provision_prepare() {

luks_device_found ||
refuse "Could not find the encrypted disk that /etc/crypttab names."
initramfs_orders_firmware ||
boot_image_orders_firmware ||
refuse "The boot image would ask for the disk password before the keyboard firmware loads."
esp_selected ||
refuse "The EFI partition this Mac boots from is not where its boot files are written."
Expand All @@ -317,7 +356,7 @@ provision_commit() {
# The boot-partition key goes last: until then the initramfs still unlocks
# with it, so a failure only has to put rd.luks.key= back, whichever attempt
# dropped it.
if ! apple_rekey_boot || ! initramfs_orders_firmware || ! boot_image_types_layout; then
if ! apple_rekey_boot || ! boot_image_orders_firmware || ! boot_image_types_layout; then
if [[ -f $BOOT_LUKS_KEY ]] && ! grep -q 'rd.luks.key=' "$GRUB_DEFAULT" 2>/dev/null; then
log_step "restoring rd.luks.key= for the retry"
grub_restore_rd_luks_key && omarchy-mac-boot-update >&2 || true
Expand Down
13 changes: 13 additions & 0 deletions omarchy-mac-boot/test/mac-boot-update-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,16 @@ pass "a Mac with no GRUB needs none"
FAIL_limine_update=1 run && fail "a failed limine-update fails the boot update"
! grep -q omarchy-mac-limine-deploy "$calls" || fail "Limine is not deployed after a failed limine-update"
pass "a failed limine-update stops the boot update"

# A missing GRUB helper stops the update: the script has no set -e, so an
# unloaded helper would make grub_tools_present return 127 and the update
# would carry on as if GRUB were absent.
helper_less=$test_tmp/helper-less
mkdir -p "$helper_less"
cp "$update" "$helper_less/omarchy-mac-boot-update"
: >"$calls"
if PATH="$stub_bin:$PATH" bash "$helper_less/omarchy-mac-boot-update" 2>"$test_tmp/err"; then
fail "a missing GRUB helper fails the boot update"
fi
[[ ! -s $calls ]] || fail "a missing GRUB helper changes no boot files" "$(cat "$calls")"
pass "a missing GRUB helper stops the boot update"
112 changes: 102 additions & 10 deletions omarchy-mac-boot/test/mac-provision-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ SH
# initramfs it carries.
cat >"$stub_bin/lsinitcpio" <<'SH'
#!/bin/bash
[[ $1 == -l && -f $2 ]] && exec cat "$2"
[[ $1 == -l && -f $2 ]] && exec cat "$(f=$(head -n 1 "$2"); [[ $f == /* && -f $f ]] && echo "$f" || echo "$2")"
[[ $1 == -x && -f $2 ]] || exit 1
tree=$2.tree
[[ -d $tree ]] || tree=$(head -n 1 "$2").tree
Expand Down Expand Up @@ -63,13 +63,17 @@ done
for layout in ${layouts//,/ }; do [[ -f $include/symbols/$layout ]] || exit 1; done
SH
# mkinitcpio builds from /etc/vconsole.conf as sd-vconsole and the plymouth
# hook do; keep-layout leaves the previous build's layout in the image.
cat >"$stub_bin/mkinitcpio" <<SH
# hook do; keep-layout leaves the previous build's layout in the image. A UKI
# build (omarchy-mac-boot-update on a Limine Mac) runs the same build into its
# own image; uki-without-firmware leaves the firmware out of that one only.
cat >"$stub_bin/build-image" <<SH
#!/bin/bash
echo "mkinitcpio \$*" >>"$calls"
[[ ! -e $test_tmp/fail-mkinitcpio ]] || exit 1
image=$root/boot/initramfs-linux-aurora.img
if [[ -e $test_tmp/build-without-firmware ]]; then
image=\$1
# grub-without-firmware leaves the firmware out of /boot's GRUB image only,
# the way a build can leave one image behind.
if [[ -e $test_tmp/build-without-firmware ]] \\
|| { [[ \${2:-} == uki && -e $test_tmp/uki-without-firmware ]]; } \\
|| { [[ \${2:-} != uki && -e $test_tmp/grub-without-firmware ]]; }; then
echo ./usr/lib/systemd/system-generators/systemd-cryptsetup-generator >"\$image"
else
printf '%s\n' "$firmware_listing" >"\$image"
Expand All @@ -88,6 +92,16 @@ layout=\$(. "$root/etc/vconsole.conf"; echo "\$XKBLAYOUT")
[[ -z \$keymap || -e $test_tmp/build-without-keymap ]] || : >"\$tree/usr/share/kbd/keymaps/i386/qwerty/\$keymap.map.gz"
[[ -z \$layout ]] || : >"\$tree/usr/share/X11/xkb/symbols/\$layout"
SH
# The re-key builds with the image root's own mkinitcpio; the stub sits where
# the fixed path below MAC_BOOT_ROOT resolves in tests.
mkdir -p "$root/usr/bin"
cat >"$root/usr/bin/mkinitcpio" <<SH
#!/bin/bash
echo "mkinitcpio \$*" >>"$calls"
[[ ! -e $test_tmp/fail-mkinitcpio ]] || exit 1
exec "$stub_bin/build-image" "$root/boot/initramfs-linux-aurora.img"
SH
chmod +x "$root/usr/bin/mkinitcpio"
cat >"$stub_bin/omarchy-mac-boot-update" <<SH
#!/bin/bash
cmdline=\$(sed -n 's/^GRUB_CMDLINE_LINUX="\(.*\)"/\1/p' "$root/etc/default/grub")
Expand All @@ -96,10 +110,12 @@ echo "omarchy-mac-boot-update \$cmdline" >>"$calls"
if [[ -e $root/var/lib/omarchy/limine.enabled ]]; then
printf 'ESP_PATH="/boot/efi"\nKERNEL_CMDLINE[default]="%s"\n' "\$cmdline" >"$root/etc/default/limine"
mkdir -p "$root/boot/efi/EFI/Linux"
echo "$root/boot/initramfs-linux-aurora.img" >"$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi"
"$stub_bin/build-image" "$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi.img" uki
echo "$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi.img" >"$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi"
else
printf 'linux /vmlinuz-linux-aurora %s\n' "\$cmdline" >"$root/boot/grub/grub.cfg"
fi
[[ ! -e $test_tmp/fail-boot-update-after-write ]] || exit 1
SH
cat >"$stub_bin/findmnt" <<'SH'
#!/bin/bash
Expand All @@ -115,6 +131,12 @@ cat >"$stub_bin/omarchy-mac-esp" <<'SH'
[[ -n ${TEST_ESP-/boot/efi} ]] || exit 1
echo "${TEST_ESP-/boot/efi}"
SH
# omarchy-cmd-present, as the entrypoints' PATH sees it on a live system: the
# command is the fixture root's /usr/bin.
cat >"$stub_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
[[ -x $OMARCHY_MAC_BOOT_ROOT/usr/bin/$1 ]]
SH
# The root's LUKS header: keyslots, then tokens, which luksDump lists alike.
cat >"$stub_bin/cryptsetup" <<'SH'
#!/bin/bash
Expand Down Expand Up @@ -149,7 +171,9 @@ fixture() {
printf 'format=1\nencrypt=1\n' >"$root/var/lib/omarchy/mac-first-boot/install.conf"
printf 'staged_slot=0\nowner_slot=2\nrecovery_slot=3\nrecovery_shown=1\nphase=owner\n' >"$root/var/lib/omarchy/provisioning/luks-rekey.state"
printf '%s\n' "$firmware_listing" >"$root/boot/initramfs-linux-aurora.img"
rm -f "$test_tmp"/fail-* "$test_tmp/build-without-firmware" "$test_tmp/build-without-keymap" "$test_tmp/keep-layout"
rm -f "$test_tmp"/fail-* "$test_tmp/build-without-firmware" "$test_tmp/uki-without-firmware" "$test_tmp/grub-without-firmware" \
"$test_tmp/build-without-keymap" "$test_tmp/keep-layout"
rm -rf "$root/usr/bin/grub-probe" "$root/usr/bin/grub-mkconfig"
: >"$calls"
}

Expand All @@ -158,6 +182,10 @@ limine_fixture() {
: >"$root/var/lib/omarchy/limine.enabled"
printf 'ESP_PATH="/boot/efi"\nKERNEL_CMDLINE[default]="root=UUID=x rw quiet rd.luks.name=%s=root %s"\n' "$luks_uuid" "$key_line" \
>"$root/etc/default/limine"
# The UKI limine-update last built, which this Mac boots.
mkdir -p "$root/boot/efi/EFI/Linux"
"$stub_bin/build-image" "$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi.img" uki
echo "$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi.img" >"$root/boot/efi/EFI/Linux/omarchy_linux-aurora.efi"
}

run() {
Expand Down Expand Up @@ -316,7 +344,53 @@ limine_fixture
run provision-commit || fail "commit succeeds on a Limine Mac" "$(cat "$test_tmp/err")"
! grep -q 'rd.luks.key=' "$root/etc/default/limine" || fail "the Limine command line drops rd.luks.key="
run provision-verify || fail "a Limine Mac verifies after commit" "$(cat "$test_tmp/err")"
pass "provision-commit rebuilds a Limine Mac's command line without the staged key"
[[ $(cat "$calls") == "omarchy-mac-boot-update quiet rd.luks.name=$luks_uuid=root" ]] ||
fail "a Limine Mac without GRUB builds only the UKI, not /boot's GRUB image" "$(cat "$calls")"
pass "provision-commit rebuilds a Limine Mac's command line without the staged key, with one initramfs build"

limine_fixture
install -Dm755 /dev/null "$root/usr/bin/grub-probe"
install -Dm755 /dev/null "$root/usr/bin/grub-mkconfig"
run provision-commit || fail "commit succeeds on a Limine Mac that keeps GRUB" "$(cat "$test_tmp/err")"
[[ $(cat "$calls") == $'mkinitcpio -P\n'"omarchy-mac-boot-update quiet rd.luks.name=$luks_uuid=root" ]] ||
fail "a Limine Mac that keeps GRUB also rebuilds GRUB's image" "$(cat "$calls")"
pass "a Limine Mac that keeps GRUB keeps its GRUB image current"

# The firmware ordering is proven in the image the Mac boots, not in /boot's.
limine_fixture
key_before=$(sha256sum <"$root/boot/omarchy/luks-key")
touch "$test_tmp/uki-without-firmware"
if run provision-commit; then fail "a UKI without the firmware ordering fails commit"; fi
error_says "the initramfs inside /boot/efi/EFI/Linux/omarchy_linux-aurora.efi does not load the vendor firmware"
[[ $(sha256sum <"$root/boot/omarchy/luks-key") == "$key_before" ]] || fail "the key stays for the retry"
rm "$test_tmp/uki-without-firmware"
run provision-commit || fail "the retry with a good UKI commits" "$(cat "$test_tmp/err")"
pass "on a Limine Mac the firmware ordering is proven in the UKI that boots"

# A Limine Mac that keeps GRUB boots /boot's image through GRUB's retained
# entries, so a good UKI does not save a /boot image without the firmware.
limine_fixture
install -Dm755 /dev/null "$root/usr/bin/grub-probe"
install -Dm755 /dev/null "$root/usr/bin/grub-mkconfig"
touch "$test_tmp/grub-without-firmware"
key_before=$(sha256sum <"$root/boot/omarchy/luks-key")
if run provision-commit; then fail "a good UKI does not save a GRUB image without the firmware ordering"; fi
error_says "/boot/initramfs-linux-aurora.img does not load the vendor firmware"
[[ $(sha256sum <"$root/boot/omarchy/luks-key") == "$key_before" ]] || fail "the key stays for the retry"
rm "$test_tmp/grub-without-firmware"
run provision-commit || fail "the retry with both images good commits" "$(cat "$test_tmp/err")"
pass "on a Limine Mac that keeps GRUB, the firmware ordering is proven in /boot's image too"

# Prepare applies the same rule: a stale /boot image nothing boots does not
# refuse a UKI-only Mac, and a Limine Mac that keeps GRUB is held to both.
limine_fixture
echo ./usr/lib/systemd/system-generators/systemd-cryptsetup-generator >"$root/boot/initramfs-linux-aurora.img"
run provision-prepare || fail "a UKI-only Mac is ready however stale its unbooted GRUB image is" "$(cat "$test_tmp/err")"
install -Dm755 /dev/null "$root/usr/bin/grub-probe"
install -Dm755 /dev/null "$root/usr/bin/grub-mkconfig"
if run provision-prepare; then fail "a Limine Mac that keeps GRUB is held to its GRUB image too"; fi
error_says "before the keyboard firmware loads"
pass "provision-prepare proves the firmware ordering in the images that boot"

fixture
sed -i '/^recovery_shown=/d' "$root/var/lib/omarchy/provisioning/luks-rekey.state"
Expand Down Expand Up @@ -349,6 +423,24 @@ for failure in fail-mkinitcpio fail-boot-update build-without-firmware; do
done
pass "a failed rebuild, or one without the firmware ordering, keeps the unattended unlock for the retry"

# An updater that fails after rewriting the command line, and whose restore
# fails too: the key stays and the boot files carry the restored command line
# for the retry.
limine_fixture
key_before=$(sha256sum <"$root/boot/omarchy/luks-key")
touch "$test_tmp/fail-boot-update-after-write"
if run provision-commit; then fail "commit fails when the updater fails after its rewrite"; fi
[[ $(sha256sum <"$root/boot/omarchy/luks-key") == "$key_before" ]] || fail "the boot-partition key stays"
grep -Fq 'rd.luks.key=' "$root/etc/default/limine" || fail "the restore names the key again" "$(cat "$root/etc/default/limine")"
grep -Fxq 'phase=configured' "$root/boot/omarchy/encrypt.state" || fail "encrypt.state stays configured"
[[ $(tail -n 1 "$calls") == "omarchy-mac-boot-update quiet rd.luks.name=$luks_uuid=root $key_line" ]] ||
fail "the restore rewrites the boot files with the key" "$(cat "$calls")"
if run provision-verify; then fail "the staged unlock remains"; fi
rm -f "$test_tmp/fail-boot-update-after-write"
run provision-commit || fail "the retry commits" "$(cat "$test_tmp/err")"
run provision-verify || fail "the retry leaves nothing behind"
pass "an updater that fails after its rewrite still leaves the restored command line"

# An attempt killed after it rewrote GRUB's defaults, then a retry whose
# rebuild fails: the key is still on the boot partition, so the command line
# names it again.
Expand Down
8 changes: 6 additions & 2 deletions test/integration/provision-owner-luks-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ done
chmod 755 "$lifecycle/usr/lib/omarchy/mac-boot"/*
chmod -R go-w "$lifecycle"

cat >"$stub_bin/mkinitcpio" <<SH
# The re-key builds with the image root's own mkinitcpio; the stub sits where
# the fixed path below MAC_BOOT_ROOT resolves in tests.
mkdir -p "$root/usr/bin"
cat >"$root/usr/bin/mkinitcpio" <<SH
#!/bin/bash
echo "mkinitcpio \$*" >>"$calls"
[[ ! -e $tmp/fail-mkinitcpio ]] || exit 1
printf '%s\n' ./usr/lib/systemd/system-generators/systemd-cryptsetup-generator \\
./usr/lib/systemd/system/omarchy-vendorfw-initrd.service \\
./usr/lib/systemd/system/systemd-cryptsetup@.service.d/omarchy-vendorfw-initrd.conf >"$root/boot/initramfs-linux-aurora.img"
SH
chmod +x "$root/usr/bin/mkinitcpio"
# The image holds no vconsole.conf, like one built for a Mac left on the US map.
cat >"$stub_bin/lsinitcpio" <<'SH'
#!/bin/bash
Expand Down Expand Up @@ -255,7 +259,7 @@ fixture() {
printf 'root UUID=%s none luks\n' "$luks_uuid" >"$root/etc/crypttab"
: >"$root/dev/disk/by-uuid/$luks_uuid"
printf 'format=1\nencrypt=1\n' >"$root/var/lib/omarchy/mac-first-boot/install.conf"
"$stub_bin/mkinitcpio" && : >"$calls"
"$root/usr/bin/mkinitcpio" && : >"$calls"
printf '0 throwaway-install-key\n' >"$slots"
rm -f "$tmp"/fail-* "$tmp/token-slot" "$tmp/token-id" "$screen" "$tmp/gum-stdin"
rm -rf "$units"
Expand Down
Loading