From baf6df3e8048037331ee41b6f1931a5dcc1f678c Mon Sep 17 00:00:00 2001 From: Marcelo Alcantara Date: Fri, 25 Sep 2026 18:20:07 +1000 Subject: [PATCH] limine-mkinitcpio-hook 1.39.0-2: Apple Silicon activation gate on aarch64 On aarch64 the Limine hooks and the mkinitcpio wrapper run through limine-apple-gate. Only an Apple Silicon Mac (device tree "apple,") behaves differently: Limine's kernel and removal hooks wait until Omarchy activates Limine (/var/lib/omarchy/limine.enabled and /etc/default/limine), mkinitcpio's own kernel hook keeps /boot current before and after activation, Limine's EFI deploy hook is left to omarchy-mac-boot, and the wrapper is plain mkinitcpio. Skipped hooks drain the socket pacman streams targets over. x86_64 packages exactly what 1.39.0-1 did; generic aarch64 and Snapdragon keep upstream behaviour. --- .github/workflows/test.yml | 1 + pkgbuilds/limine-mkinitcpio-hook/PKGBUILD | 64 ++- .../limine-mkinitcpio-hook/limine-apple-gate | 69 +++ .../mkinitcpio-install.hook | 38 ++ tests/limine-mkinitcpio-hook.sh | 532 ++++++++++++++++++ 5 files changed, 701 insertions(+), 3 deletions(-) create mode 100644 pkgbuilds/limine-mkinitcpio-hook/limine-apple-gate create mode 100644 pkgbuilds/limine-mkinitcpio-hook/mkinitcpio-install.hook create mode 100755 tests/limine-mkinitcpio-hook.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29d18b0..302e693 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,7 @@ jobs: ./tests/published-build-plan.sh ./tests/controller.sh ./tests/artifact-helpers.sh + ./tests/limine-mkinitcpio-hook.sh pacman -S --noconfirm --quiet rclone >/dev/null ./tests/publish-artifact.sh ' diff --git a/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD b/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD index 76e4d9a..caa9255 100644 --- a/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD +++ b/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD @@ -3,14 +3,16 @@ _pkgname="limine-entry-tool" pkgname="limine-mkinitcpio-hook" _gradle_version=9.7.1 pkgver=1.39.0 -pkgrel=1 +pkgrel=2 pkgdesc="Install kernels for the Limine bootloader." arch=('x86_64' 'aarch64') url="https://gitlab.com/Zesko/limine-entry-tool" source=("${_pkgname}::git+${url}.git#tag=${pkgver}") source_x86_64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-x64_bin.tar.gz") source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-aarch64_bin.tar.gz" - "https://services.gradle.org/distributions/gradle-${_gradle_version}-bin.zip") + "https://services.gradle.org/distributions/gradle-${_gradle_version}-bin.zip" + "limine-apple-gate" + "mkinitcpio-install.hook") license=("GPL3") provides=('limine-entry-tool') options=(!debug !strip) @@ -34,7 +36,9 @@ conflicts=('limine-entry-tool') sha256sums=('6c4affb6fb6367a1222f7d0c54957a3142781d7894bbf61b1a274bd153e5d869') sha256sums_x86_64=('3f4a89de8eaa96f2ed677f09957c7e872cd8467aad3537f8b5394c1b8c4b942e') sha256sums_aarch64=('22286f7ecd21b9aedb3226b9bf797469e1bd3eefc491e12ef3dd49b452d230b7' - 'acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a') + 'acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a' + '199fa2122e4a20ed80e6cf5df235b4e50739593803522bfa65700b1ef242b9ce' + '9fc607e81d2f09aa0dfd313805277af92977535bd222f1e86caf5fbb28138cb4') prepare() { [[ -d "${_graalvm_version}" ]] && rm -rf "${_graalvm_version}" @@ -77,4 +81,58 @@ package() { # limine hook symlinks ln -sf /usr/bin/limine-reset-enroll "$pkgdir/etc/boot/hooks/pre.d/10-limine-reset-enroll" ln -sf /usr/bin/limine-enroll-config "$pkgdir/etc/boot/hooks/post.d/90-limine-enroll-config" + + if [[ $CARCH == aarch64 ]]; then + _apple_silicon_hooks + fi +} + +# An Apple Silicon Mac boots GRUB until Omarchy activates Limine, and keeps +# the kernel and initramfs that mkinitcpio's presets install under /boot +# after that too (GRUB, the boot check and the snapshot tools read them). +# The aarch64 hooks and mkinitcpio wrapper run through limine-apple-gate, +# which leaves every other machine as upstream ships it. On a Mac: +# - Limine's kernel and removal hooks wait until Limine is active; +# - mkinitcpio's own kernel hook, which Limine's takes over by name, runs from +# a copy of mkinitcpio 42's (mkinitcpio-install.hook: keep its triggers in +# step with mkinitcpio's), just before Limine's; +# - Limine's EFI deploy hook stays out: omarchy-mac-boot puts Limine in +# U-Boot's EFI/BOOT/BOOTAA64.EFI slot; +# - the wrapper is plain mkinitcpio: it would run limine-mkinitcpio after +# every preset build, mkinitcpio's kernel hook included. +_apple_silicon_hooks() { + local scripts=/usr/share/libalpm/scripts + local gate=$scripts/limine-apple-gate + local hooks="$pkgdir/usr/share/libalpm/hooks" + local kernel_hook="$pkgdir/etc/pacman.d/hooks/90-mkinitcpio-install.hook" + local mac_hook="$hooks/90-mkinitcpio-apple-install.hook" + local wrapper="$pkgdir/usr/local/bin/mkinitcpio" + + install -Dm755 "$srcdir/limine-apple-gate" "$pkgdir$gate" + install -Dm644 "$srcdir/mkinitcpio-install.hook" "$mac_hook" + sed -i -e 's|^Description = .*|Description = Updating linux initcpios under /boot (Apple Silicon)...|' \ + -e "s|^Exec = $scripts/mkinitcpio install\$|Exec = $gate --mac $scripts/mkinitcpio install|" "$mac_hook" + sed -i "s|^Exec = $scripts/limine-mkinitcpio-|Exec = $gate $scripts/limine-mkinitcpio-|" \ + "$kernel_hook" "$hooks/60-limine-mkinitcpio-remove-pre.hook" "$hooks/90-limine-mkinitcpio-remove-post.hook" + sed -i "s|^Exec = /usr/bin/limine-install\$|Exec = $gate --not-mac /usr/bin/limine-install|" "$hooks/80-limine-efi-deploy.hook" + sed -i "1a $gate --mac \\&\\& exec /usr/bin/mkinitcpio \"\$@\"" "$wrapper" + + local expected=( + "$mac_hook|Exec = $gate --mac $scripts/mkinitcpio install" + "$kernel_hook|Exec = $gate $scripts/limine-mkinitcpio-install" + "$hooks/60-limine-mkinitcpio-remove-pre.hook|Exec = $gate $scripts/limine-mkinitcpio-remove pre" + "$hooks/90-limine-mkinitcpio-remove-post.hook|Exec = $gate $scripts/limine-mkinitcpio-remove post" + "$hooks/80-limine-efi-deploy.hook|Exec = $gate --not-mac /usr/bin/limine-install" + ) + local entry + for entry in "${expected[@]}"; do + [[ $(grep '^Exec = ' "${entry%%|*}") == "${entry#*|}" ]] || { + echo "${entry%%|*}: expected '${entry#*|}'" >&2 + return 1 + } + done + [[ $(sed -n 1,2p "$wrapper") == $'#!/usr/bin/env bash\n'"$gate --mac && exec /usr/bin/mkinitcpio \"\$@\"" ]] || { + echo "the mkinitcpio wrapper does not start with the Apple Silicon gate" >&2 + return 1 + } } diff --git a/pkgbuilds/limine-mkinitcpio-hook/limine-apple-gate b/pkgbuilds/limine-mkinitcpio-hook/limine-apple-gate new file mode 100644 index 0000000..380e45e --- /dev/null +++ b/pkgbuilds/limine-mkinitcpio-hook/limine-apple-gate @@ -0,0 +1,69 @@ +#!/bin/bash +# The aarch64 package runs its pacman hooks and its mkinitcpio wrapper through +# this, so that only an Apple Silicon Mac behaves differently from upstream: +# +# limine-apple-gate COMMAND... COMMAND, except on a Mac that has not activated Limine +# limine-apple-gate --mac COMMAND... COMMAND on a Mac only +# limine-apple-gate --not-mac COMMAND... COMMAND everywhere but a Mac +# limine-apple-gate --mac succeeds on a Mac only +# +# A Mac boots GRUB until Omarchy activates Limine: the gate +# /var/lib/omarchy/limine.enabled and the /etc/default/limine the activation +# writes, the same test as omarchy-mac-limine-active. A command that does not +# run reads the targets pacman streams to it and succeeds quietly. + +gate=/var/lib/omarchy/limine.enabled +limine_default=/etc/default/limine +compatible=(/proc/device-tree/compatible /sys/firmware/devicetree/base/compatible) + +# Fixtures fake the machine for an unprivileged caller only; as root, which +# pacman hooks always are, the live system decides. +if ((EUID != 0)); then + gate=${OMARCHY_LIMINE_GATE:-$gate} + limine_default=${OMARCHY_LIMINE_DEFAULT:-$limine_default} + if [[ -n ${OMARCHY_PROC_ROOT:-}${OMARCHY_SYS_ROOT:-} ]]; then + compatible=() + [[ -z ${OMARCHY_PROC_ROOT:-} ]] || compatible+=("$OMARCHY_PROC_ROOT/device-tree/compatible") + [[ -z ${OMARCHY_SYS_ROOT:-} ]] || compatible+=("$OMARCHY_SYS_ROOT/firmware/devicetree/base/compatible") + fi +fi + +# m1n1 names every Apple Silicon Mac "apple," in the root node's +# NUL-separated compatible list, as omarchy-hw-platform reads it. +is_mac() { + local source + for source in "${compatible[@]}"; do + [[ -r $source ]] && tr '\0' '\n' <"$source" | grep -q '^apple,' && return 0 + done + return 1 +} + +# pacman writes NeedsTargets to a socket and fails the write if the hook has +# gone; hooks without NeedsTargets get no stdin at all. +pass() { + if [[ -S /dev/stdin || -p /dev/stdin ]]; then + cat >/dev/null + fi + exit 0 +} + +mode=limine +case ${1:-} in +--mac | --not-mac) + mode=${1#--} + shift + ;; +esac + +if (($# == 0)); then + [[ $mode == mac ]] && { is_mac; exit; } + echo "usage: limine-apple-gate [--mac|--not-mac] COMMAND [ARG]..." >&2 + exit 2 +fi + +case $mode in +mac) is_mac || pass ;; +not-mac) ! is_mac || pass ;; +limine) ! is_mac || [[ -e $gate && -f $limine_default ]] || pass ;; +esac +exec "$@" diff --git a/pkgbuilds/limine-mkinitcpio-hook/mkinitcpio-install.hook b/pkgbuilds/limine-mkinitcpio-hook/mkinitcpio-install.hook new file mode 100644 index 0000000..3607cd5 --- /dev/null +++ b/pkgbuilds/limine-mkinitcpio-hook/mkinitcpio-install.hook @@ -0,0 +1,38 @@ +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/initcpio/* +Target = usr/lib/firmware/* +Target = usr/lib/modules/*/extramodules/ +Target = usr/src/*/dkms.conf +Target = usr/lib/systemd/systemd +Target = usr/lib/systemd/systemd-udevd +Target = usr/bin/cryptsetup +Target = usr/bin/lvm +Target = usr/bin/mdadm +Target = usr/bin/pdata_tools +Target = usr/lib/libcryptsetup.so +Target = usr/lib/libp11-kit.so +Target = usr/lib/libpcsclite.so +Target = usr/lib/modprobe.d/ + +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/modules/*/vmlinuz + +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = mkinitcpio +Target = mkinitcpio-git + +[Action] +Description = Updating linux initcpios... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/mkinitcpio install +NeedsTargets diff --git a/tests/limine-mkinitcpio-hook.sh b/tests/limine-mkinitcpio-hook.sh new file mode 100755 index 0000000..f91866b --- /dev/null +++ b/tests/limine-mkinitcpio-hook.sh @@ -0,0 +1,532 @@ +#!/bin/bash +# limine-mkinitcpio-hook: the x86_64 package is upstream's as before. On +# aarch64, limine-apple-gate leaves every machine but an Apple Silicon Mac as +# the x86_64 package behaves; a Mac keeps /boot current through mkinitcpio's +# own kernel hook, and Limine waits until Omarchy activates it. +# +# The package() of the PKGBUILD runs over an upstream-shaped source tree +# (LIMINE_ENTRY_TOOL_SRC may name a real limine-entry-tool checkout instead), +# and pacman transactions replay through the packaged hooks on fixture machines. +set -euo pipefail + +REPO_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") +RECIPE=$REPO_ROOT/pkgbuilds/limine-mkinitcpio-hook +GATE=$RECIPE/limine-apple-gate +TEST_ROOT=$(mktemp -d) +trap 'rm -rf "$TEST_ROOT"' EXIT + +fail() { + echo "not ok - $1" >&2 + [[ $# -lt 2 ]] || printf '%s\n' "$2" >&2 + exit 1 +} +pass() { + echo "ok - $1" +} + +# pacman hands NeedsTargets to a hook over a socket, and a hook that stops +# reading early fails the write ("unable to write to pipe"). +SOCKET_FEED=' +import socket, subprocess, sys +parent, child = socket.socketpair() +hook = subprocess.Popen(sys.argv[1:], stdin=child) +child.close() +try: + parent.sendall(sys.stdin.buffer.read()) + parent.shutdown(socket.SHUT_WR) +except OSError as error: + print("unable to write to pipe (%s)" % error.strerror, file=sys.stderr) + hook.wait() + sys.exit(141) +sys.exit(hook.wait()) +' +socket_feed() { python3 -c "$SOCKET_FEED" "$@"; } + +apple_tree() { printf 'apple,j316s\0apple,t6000\0apple,arm-platform\0'; } +live_is_mac() { + local source + for source in /proc/device-tree/compatible /sys/firmware/devicetree/base/compatible; do + [[ -r $source ]] && tr '\0' '\n' <"$source" | grep -q '^apple,' && return 0 + done + return 1 +} + +# The gate trusts fixtures only from an unprivileged caller: as root it reads +# the live machine. Check that, then carry on as nobody. +if ((EUID == 0)); then + if ! live_is_mac; then + mkdir -p "$TEST_ROOT/root-proc/device-tree" + apple_tree >"$TEST_ROOT/root-proc/device-tree/compatible" + out=$(OMARCHY_PROC_ROOT=$TEST_ROOT/root-proc bash "$GATE" echo ran "$upstream/README.md" + echo '# CHANGELOG' >"$upstream/CHANGELOG.md" + echo 'ESP_PATH=""' >"$tool/etc/limine-entry-tool.conf" + echo 'ESP_PATH=""' >"$hook/etc/limine-entry-tool.conf" + for name in limine-install limine-entry-tool limine-reset-enroll limine-enroll-config; do + printf '#!/usr/bin/env bash\n' >"$tool/usr/bin/$name" + done + printf '#!/usr/bin/env bash\n' >"$tool/usr/lib/limine/limine-common-functions" + for name in limine-mkinitcpio limine-update; do + printf '#!/usr/bin/env bash\n' >"$hook/usr/bin/$name" + done + for name in limine-mkinitcpio-install limine-mkinitcpio-remove; do + printf '#!/usr/bin/env bash\n' >"$hook/usr/share/libalpm/scripts/$name" + done + cat >"$tool/usr/share/libalpm/hooks/80-limine-efi-deploy.hook" <<'HOOK' +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = limine +Target = limine-git +Target = limine-dev + +[Action] +Description = Deploying Limine after upgrade... +When = PostTransaction +Exec = /usr/bin/limine-install --no-efi-register +HOOK + cat >"$hook/usr/share/libalpm/hooks/80-limine-efi-deploy.hook" <<'HOOK' +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = limine +Target = limine-git +Target = limine-dev +Target = limine-mkinitcpio-hook +Target = limine-mkinitcpio-hook-git + +[Action] +Description = Deploying Limine after upgrade... +When = PostTransaction +Exec = /usr/bin/limine-install +HOOK + cat >"$hook/usr/share/libalpm/hooks/60-limine-mkinitcpio-remove-pre.hook" <<'HOOK' +[Trigger] +Type = Path +Operation = Remove +Target = usr/lib/modules/*/modules.builtin + +[Action] +Description = Record kernels marked for removal in Limine +When = PreTransaction +Exec = /usr/share/libalpm/scripts/limine-mkinitcpio-remove pre +NeedsTargets +HOOK + cat >"$hook/usr/share/libalpm/hooks/90-limine-mkinitcpio-remove-post.hook" <<'HOOK' +[Trigger] +Type = Path +Operation = Remove +Target = usr/lib/modules/*/modules.builtin + +[Action] +Description = Clean Limine boot entries of removed kernels +When = PostTransaction +Exec = /usr/share/libalpm/scripts/limine-mkinitcpio-remove post +HOOK + cat >"$hook/etc/pacman.d/hooks/90-mkinitcpio-install.hook" <<'HOOK' +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/initcpio/* +Target = usr/lib/firmware/* +Target = usr/lib/modules/*/extramodules/ +Target = usr/src/*/dkms.conf +Target = usr/lib/systemd/systemd +Target = usr/bin/cryptsetup +Target = usr/bin/lvm + +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Target = usr/lib/modules/*/modules.builtin + +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = mkinitcpio +Target = mkinitcpio-git + +[Action] +Description = Updating linux initcpios... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/limine-mkinitcpio-install +NeedsTargets +HOOK + cat >"$hook/usr/local/bin/mkinitcpio" <<'WRAPPER' +#!/usr/bin/env bash + +/usr/bin/mkinitcpio "$@" + +_color_reset="" +_color_yellow="" +colors="$(tput colors 2>/dev/null || echo 0)" +if ((colors >= 8)); then + _color_reset="\033[0m" + _color_yellow="\033[1;33m" +fi + +if [[ " $* " == *" -P "* || " $* " == *" --allpresets "* || " $* " == *" -p "* || " $* " == *" --preset "* ]]; then + printf '%b==> WARNING: This does not update Limine boot entries.%b\n' "${_color_yellow}" "${_color_reset}" >&2 + printf "%b Use 'limine-mkinitcpio' or 'limine-update' instead.%b\n" "${_color_yellow}" "${_color_reset}" >&2 + + read -rp "==> Would you like to run 'limine-mkinitcpio' now? [Y/n]: " answer + case "${answer,,}" in + "" | "y" | "yes") + /usr/bin/limine-mkinitcpio + ;; + *) + # nothing + ;; + esac +fi +WRAPPER +fi +printf 'native image\n' >"$upstream/build/native/nativeCompile/limine-entry-tool" +chmod -R u+w,go+rX "$upstream" +find "$upstream/install" -path '*/bin/*' -type f -exec chmod 755 {} + -o -path '*/scripts/*' -type f -exec chmod 755 {} + +ln -s "$RECIPE/limine-apple-gate" "$RECIPE/mkinitcpio-install.hook" "$SRC/" + +# makepkg runs package() with errexit. +build_package() { + local arch=$1 out=$2 + mkdir -p "$out" + CARCH=$arch srcdir=$SRC pkgdir=$out bash -euo pipefail -c 'source "$1"; package' _ "$RECIPE/PKGBUILD" || + fail "package() succeeds for $arch" +} +PKG_X86=$TEST_ROOT/pkg-x86_64 +PKG_ARM=$TEST_ROOT/pkg-aarch64 +build_package x86_64 "$PKG_X86" +build_package aarch64 "$PKG_ARM" + +manifest() { + (cd "$1" && find . \( -type f -o -type l \) -printf '%p %m %l\n' | LC_ALL=C sort) | + while read -r path mode target; do + if [[ -n $target ]]; then + echo "$path $mode -> $target" + else + echo "$path $mode $(sha256sum "$1/$path" | cut -d' ' -f1)" + fi + done +} + +scripts=/usr/share/libalpm/scripts +gate=$scripts/limine-apple-gate +upstream_hook=$upstream/install/arch-linux/limine-mkinitcpio-hook +for file in etc/pacman.d/hooks/90-mkinitcpio-install.hook usr/local/bin/mkinitcpio \ + usr/share/libalpm/hooks/{60-limine-mkinitcpio-remove-pre,80-limine-efi-deploy,90-limine-mkinitcpio-remove-post}.hook; do + cmp -s "$upstream_hook/$file" "$PKG_X86/$file" || fail "x86_64 ships upstream's $file unchanged" +done +[[ ! -e $PKG_X86$gate && ! -e $PKG_X86/usr/share/libalpm/hooks/90-mkinitcpio-apple-install.hook ]] || + fail "x86_64 ships no Apple Silicon file" +pass "x86_64 packages upstream's hooks and wrapper unchanged" + +differences=$(diff <(manifest "$PKG_X86") <(manifest "$PKG_ARM") || true) +changed=$(sed -n 's/^> \(\S*\) .*/\1/p' <<<"$differences") +expected=$(printf '%s\n' ./etc/pacman.d/hooks/90-mkinitcpio-install.hook \ + ./usr/local/bin/mkinitcpio \ + ./usr/share/libalpm/hooks/60-limine-mkinitcpio-remove-pre.hook \ + ./usr/share/libalpm/hooks/80-limine-efi-deploy.hook \ + ./usr/share/libalpm/hooks/90-limine-mkinitcpio-remove-post.hook \ + ./usr/share/libalpm/hooks/90-mkinitcpio-apple-install.hook \ + ./usr/share/libalpm/scripts/limine-apple-gate) +[[ $changed == "$expected" ]] || fail "aarch64 differs from x86_64 only in the gated hooks, the wrapper and the gate" "$changed" +[[ -z $(sed -n 's/^< \(\S*\) .*/\1/p' <<<"$differences" | grep -vxF "$expected") ]] || + fail "aarch64 keeps every x86_64 file" +[[ $(stat -c %a "$PKG_ARM$gate") == 755 ]] || fail "the gate is executable" +diff <(grep -v '^Exec = \|^Description = ' "$RECIPE/mkinitcpio-install.hook") \ + <(grep -v '^Exec = \|^Description = ' "$PKG_ARM/usr/share/libalpm/hooks/90-mkinitcpio-apple-install.hook") >/dev/null || + fail "the Mac's kernel hook keeps mkinitcpio's triggers" +[[ $(sed -n 2p "$PKG_ARM/usr/local/bin/mkinitcpio") == "$gate --mac && exec /usr/bin/mkinitcpio \"\$@\"" ]] || + fail "the aarch64 wrapper is plain mkinitcpio on a Mac" +pass "aarch64 routes Limine's hooks and the wrapper through the gate and adds the Mac's kernel hook" + +# --------------------------------------------------------------------------- +# Fixture machines, read through OMARCHY_PROC_ROOT, OMARCHY_LIMINE_GATE and +# OMARCHY_LIMINE_DEFAULT. +machine() { + local name=$1 compatible=$2 dir=$TEST_ROOT/machines/$1 + mkdir -p "$dir/proc" "$dir/state" + if [[ -n $compatible ]]; then + mkdir -p "$dir/proc/device-tree" + printf "$compatible" >"$dir/proc/device-tree/compatible" + fi +} +machine x86 '' +machine generic-aarch64 'linux,dummy-virt\0' +machine qualcomm 'lenovo,thinkpad-t14s\0qcom,x1e78100\0qcom,x1e80100\0' +machine mac-dormant 'apple,j316s\0apple,t6000\0apple,arm-platform\0' +machine mac-marker-only 'apple,j316s\0apple,t6000\0apple,arm-platform\0' +machine mac-active 'apple,j316s\0apple,t6000\0apple,arm-platform\0' +: >"$TEST_ROOT/machines/mac-marker-only/state/limine.enabled" +: >"$TEST_ROOT/machines/mac-active/state/limine.enabled" +echo 'ESP_PATH="/boot/efi"' >"$TEST_ROOT/machines/mac-active/state/limine" + +on() { + local dir=$TEST_ROOT/machines/$1 + shift + ( + export OMARCHY_PROC_ROOT=$dir/proc OMARCHY_LIMINE_GATE=$dir/state/limine.enabled OMARCHY_LIMINE_DEFAULT=$dir/state/limine + "$@" + ) +} +status() { + local rc=0 + "$@" || rc=$? + echo "$rc" +} + +for m in x86 generic-aarch64 qualcomm; do + [[ $(on "$m" status bash "$GATE" --mac) == 1 ]] || fail "$m is not a Mac" + [[ $(printf 'a\nb\n' | on "$m" bash "$GATE" cat) == $'a\nb' ]] || fail "$m runs Limine's hooks with their targets" + [[ $(on "$m" status bash "$GATE" bash -c 'exit 7' &1) ]] || fail "$m skips the Mac's hooks quietly" + [[ $(on "$m" bash "$GATE" --not-mac echo ran &1) ]] || fail "$m leaves Limine's EFI binary to omarchy-mac-boot" +done +for m in mac-dormant mac-marker-only; do + out=$(printf 'usr/lib/modules/7.1.0/modules.builtin\n' | on "$m" bash "$GATE" bash -c 'echo ran; exit 100' 2>&1) || + fail "$m passes over Limine's hooks successfully" + [[ -z $out ]] || fail "$m passes over Limine's hooks quietly" "$out" + [[ -z $(on "$m" bash "$GATE" echo ran <&- 2>&1) ]] || fail "$m passes over a hook without targets" +done +# A linux-firmware upgrade streams thousands of targets, more than a socket holds. +many_targets() { printf 'usr/lib/firmware/fixture/%s.bin\n' $(seq 60000); } +many_targets | on mac-dormant socket_feed bash "$GATE" true || fail "a dormant Mac reads every target of a hook it passes over" +many_targets | on x86 socket_feed bash "$GATE" --mac true || fail "other machines read every target of the Mac's hooks" +many_targets | on mac-active socket_feed bash "$GATE" --not-mac true || fail "a Mac reads every target of the deploy hook" +many_targets | on mac-dormant bash "$GATE" true || fail "a dormant Mac reads every target piped to a hook it passes over" +[[ $(printf 'a\n' | on mac-active bash "$GATE" bash -c 'echo "ran $1"; cat' _ post) == $'ran post\na' ]] || + fail "an active Mac runs Limine's hooks with their arguments and targets" +[[ $(on mac-active status bash "$GATE" bash -c 'exit 7' /dev/null) == 2 ]] || fail "the gate needs a command" +pass "the gate runs Limine's hooks everywhere but a Mac that has not activated Limine" + +# --------------------------------------------------------------------------- +# pacman: hooks from /usr/share/libalpm/hooks, overridden by name from +# /etc/pacman.d/hooks, run in name order, each with the targets of the +# transaction its triggers match (sorted, over a socket for NeedsTargets, +# else no stdin). +# Transactions are lines of " ". +fake_root() { + local pkg=$1 root=$2 name + cp -a "$pkg" "$root" + cp "$RECIPE/mkinitcpio-install.hook" "$root/usr/share/libalpm/hooks/90-mkinitcpio-install.hook" + for name in usr/share/libalpm/scripts/limine-mkinitcpio-install usr/share/libalpm/scripts/limine-mkinitcpio-remove \ + usr/bin/limine-install usr/share/libalpm/scripts/mkinitcpio usr/bin/mkinitcpio usr/bin/limine-mkinitcpio; do + rm -f "$root/$name" + printf '#!/bin/bash\n{ printf %%s %q; if (($#)); then printf " %%s" "$@"; fi; echo; if [[ -S /dev/stdin || -p /dev/stdin ]]; then sed "s/^/ /"; fi; } >>"$SIM_LOG"\n' \ + "${name##*/}" >"$root/$name" + chmod 755 "$root/$name" + done +} + +run_hook() { + local root=$1 hook=$2 when=$3 transaction=$4 + local line section='' hook_when='' exec_line='' needs=0 type='' ops='' targets='' + local -a triggers=() + while IFS= read -r line || [[ -n $line ]]; do + case $line in + '[Trigger]' | '[Action]') + [[ $section != Trigger ]] || triggers+=("$type|$ops|$targets") + section=${line//[][]/} type='' ops=' ' targets='' + ;; + NeedsTargets) needs=1 ;; + *' = '*) + case $section/${line%% = *} in + Trigger/Type) type=${line#* = } ;; + Trigger/Operation) ops+="${line#* = } " ;; + Trigger/Target) targets+="${line#* = } " ;; + Action/When) hook_when=${line#* = } ;; + Action/Exec) exec_line=${line#* = } ;; + esac + ;; + esac + done <"$hook" + [[ $hook_when == "$when" ]] || return 0 + + local op kind target trigger t_type t_ops t_targets pattern + local -a matched=() patterns=() + while read -r op kind target; do + for trigger in "${triggers[@]}"; do + IFS='|' read -r t_type t_ops t_targets <<<"$trigger" + [[ $t_type == "$kind" && $t_ops == *" $op "* ]] || continue + read -ra patterns <<<"$t_targets" + for pattern in "${patterns[@]}"; do + # shellcheck disable=SC2053 + if [[ $target == $pattern ]]; then + matched+=("$target") + break 2 + fi + done + done + done <"$transaction" + ((${#matched[@]})) || return 0 + + local word + local -a argv=() command=() + read -ra argv <<<"$exec_line" + for word in "${argv[@]}"; do + if [[ $word == /* && -e $root$word ]]; then command+=("$root$word"); else command+=("$word"); fi + done + if ((needs)); then + printf '%s\n' "${matched[@]}" | LC_ALL=C sort -u | socket_feed "${command[@]}" + else + "${command[@]}" <&- + fi +} + +run_transaction() { + local root=$1 transaction=$2 when name + local -A hooks=() + for name in "$root"/usr/share/libalpm/hooks/*.hook "$root"/etc/pacman.d/hooks/*.hook; do + [[ -e $name ]] && hooks[${name##*/}]=$name + done + for when in PreTransaction PostTransaction; do + while read -r name; do + run_hook "$root" "${hooks[$name.hook]}" "$when" "$transaction" + done < <(printf '%s\n' "${!hooks[@]}" | sed 's/\.hook$//' | LC_ALL=C sort) + done +} + +fake_root "$PKG_X86" "$TEST_ROOT/root-x86_64" +fake_root "$PKG_ARM" "$TEST_ROOT/root-aarch64" + +transaction() { + local name=$1 + shift + printf '%s\n' "$@" >"$TEST_ROOT/tx-$name" +} +transaction kernel \ + 'Install Path usr/lib/modules/7.1.0-asahi/vmlinuz' \ + 'Install Path usr/lib/modules/7.1.0-asahi/modules.builtin' \ + 'Install Path usr/lib/modules/7.1.0-asahi/pkgbase' \ + 'Remove Path usr/lib/modules/7.0.0-asahi/vmlinuz' \ + 'Remove Path usr/lib/modules/7.0.0-asahi/modules.builtin' \ + 'Remove Path usr/lib/modules/7.0.0-asahi/pkgbase' \ + 'Upgrade Package linux-asahi' +transaction firmware 'Upgrade Path usr/lib/firmware/brcm/fixture.bin' 'Upgrade Package linux-firmware' +transaction modprobe 'Upgrade Path usr/lib/modprobe.d/' 'Install Path usr/lib/modprobe.d/omarchy-mac.conf' 'Upgrade Package omarchy-mac' +transaction limine 'Upgrade Path usr/share/limine/BOOTAA64.EFI' 'Upgrade Package limine' + +replay() { + local arch=$1 m=$2 name=$3 + export SIM_LOG=$TEST_ROOT/log + : >"$SIM_LOG" + local out + out=$(on "$m" run_transaction "$TEST_ROOT/root-$arch" "$TEST_ROOT/tx-$name" 2>&1) || fail "$name on $arch/$m replays" + [[ -z $out ]] || fail "$name on $arch/$m prints nothing of the gate's" "$out" + cat "$SIM_LOG" +} + +limine_kernel=$'limine-mkinitcpio-remove pre\n usr/lib/modules/7.0.0-asahi/modules.builtin\nlimine-mkinitcpio-remove post\nlimine-mkinitcpio-install\n usr/lib/modules/7.1.0-asahi/modules.builtin' +stock_kernel=$'mkinitcpio install\n usr/lib/modules/7.1.0-asahi/vmlinuz' +limine_firmware=$'limine-mkinitcpio-install\n usr/lib/firmware/brcm/fixture.bin' +stock_firmware=$'mkinitcpio install\n usr/lib/firmware/brcm/fixture.bin' +stock_modprobe=$'mkinitcpio install\n usr/lib/modprobe.d/' + +declare -A want=( + [kernel/other]=$limine_kernel + [firmware/other]=$limine_firmware + [modprobe/other]='' + [limine/other]='limine-install' + [kernel/mac-dormant]=$stock_kernel + [firmware/mac-dormant]=$stock_firmware + [modprobe/mac-dormant]=$stock_modprobe + [limine/mac-dormant]='' + [kernel/mac-active]=$'limine-mkinitcpio-remove pre\n usr/lib/modules/7.0.0-asahi/modules.builtin\nlimine-mkinitcpio-remove post\n'"$stock_kernel"$'\nlimine-mkinitcpio-install\n usr/lib/modules/7.1.0-asahi/modules.builtin' + [firmware/mac-active]=$stock_firmware$'\n'$limine_firmware + [modprobe/mac-active]=$stock_modprobe + [limine/mac-active]='' +) +want[kernel/mac-marker-only]=${want[kernel/mac-dormant]} + +for name in kernel firmware modprobe limine; do + got=$(replay x86_64 x86 "$name") + [[ $got == "${want[$name/other]}" ]] || fail "x86_64: $name transaction runs upstream's hooks" "$got" + for m in generic-aarch64 qualcomm; do + got=$(replay aarch64 "$m" "$name") + [[ $got == "${want[$name/other]}" ]] || fail "aarch64 $m: $name transaction runs as on x86_64" "$got" + done +done +pass "x86_64, generic aarch64 and Snapdragon transactions run Limine's hooks as upstream ships them" + +for m in mac-dormant mac-marker-only mac-active; do + for name in kernel firmware modprobe limine; do + [[ -v want[$name/$m] ]] || continue + got=$(replay aarch64 "$m" "$name") + [[ $got == "${want[$name/$m]}" ]] || fail "$m: $name transaction" "$got" + done +done +pass "a dormant Mac defers to mkinitcpio's own hook; an active Mac also writes Limine's entries after it" + +# --------------------------------------------------------------------------- +# The wrapper, as sudo mkinitcpio -P and mkinitcpio's hook script call it +# (/usr/local/bin first in PATH, stdin at its end). +wrapper() { + local root=$1 m=$2 + sed "s|/usr/|$root/usr/|g" "$root/usr/local/bin/mkinitcpio" >"$TEST_ROOT/wrapper" + export SIM_LOG=$TEST_ROOT/log + : >"$SIM_LOG" + on "$m" bash "$TEST_ROOT/wrapper" -P /dev/null 2>&1 || true + cat "$SIM_LOG" +} +upstream_wrapper=$'mkinitcpio -P\nlimine-mkinitcpio' +[[ $(wrapper "$TEST_ROOT/root-x86_64" x86) == "$upstream_wrapper" ]] || fail "x86_64 keeps upstream's wrapper" +for m in generic-aarch64 qualcomm; do + [[ $(wrapper "$TEST_ROOT/root-aarch64" "$m") == "$upstream_wrapper" ]] || fail "$m keeps upstream's wrapper" +done +for m in mac-dormant mac-active; do + [[ $(wrapper "$TEST_ROOT/root-aarch64" "$m") == 'mkinitcpio -P' ]] || fail "$m runs plain mkinitcpio" +done +pass "the mkinitcpio wrapper is upstream's everywhere but a Mac, where it is plain mkinitcpio"