diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9fc270f3..106fe6aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,9 +25,16 @@ jobs: -w /workspace \ archlinux:base-devel bash -lc ' set -euo pipefail - pacman -Syu --noconfirm git jq + pacman -Syu --noconfirm git jq python-yaml xz zstd ./bin/sync-upstream self-test ./bin/sync-rebuilds --self-test ./bin/omarchy-pkgs self-test ./bin/omarchy-release self-test + shopt -s nullglob + for test in pkgbuilds/*/test.sh; do + bash "$test" + done + if [[ -d test ]]; then + runuser -u nobody -- python -m unittest discover -s test -v + fi ' diff --git a/pkgbuilds/linux-aarch64-pkgbase-shim/.omarchy/package.json b/pkgbuilds/linux-aarch64-pkgbase-shim/.omarchy/package.json new file mode 100644 index 00000000..2a9719d3 --- /dev/null +++ b/pkgbuilds/linux-aarch64-pkgbase-shim/.omarchy/package.json @@ -0,0 +1,3 @@ +{ + "source": "local" +} diff --git a/pkgbuilds/linux-aarch64-pkgbase-shim/85-linux-aarch64-pkgbase-shim.hook b/pkgbuilds/linux-aarch64-pkgbase-shim/85-linux-aarch64-pkgbase-shim.hook new file mode 100644 index 00000000..a0e7eaab --- /dev/null +++ b/pkgbuilds/linux-aarch64-pkgbase-shim/85-linux-aarch64-pkgbase-shim.hook @@ -0,0 +1,19 @@ +# Run before mkinitcpio and Limine discover installed kernels. +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/modules/*/ + +[Trigger] +Type = Package +Operation = Install +Operation = Upgrade +Target = linux-aarch64-pkgbase-shim + +[Action] +Description = Writing pkgbase and vmlinuz for Arch Linux ARM kernels... +When = PostTransaction +Exec = /usr/share/libalpm/scripts/linux-aarch64-pkgbase-shim +NeedsTargets diff --git a/pkgbuilds/linux-aarch64-pkgbase-shim/PKGBUILD b/pkgbuilds/linux-aarch64-pkgbase-shim/PKGBUILD new file mode 100644 index 00000000..633ddd71 --- /dev/null +++ b/pkgbuilds/linux-aarch64-pkgbase-shim/PKGBUILD @@ -0,0 +1,26 @@ +# Maintainer: Jimmy Van Veen + +# Add kernel metadata required by mkinitcpio and Limine on Arch Linux ARM. + +pkgname=linux-aarch64-pkgbase-shim +pkgver=1 +pkgrel=3 +pkgdesc="Writes usr/lib/modules//{pkgbase,vmlinuz} for Arch Linux ARM kernels so mkinitcpio and Limine hooks find them" +arch=('aarch64') +url="https://github.com/omacom/omarchy-pkgs" +license=('MIT') +depends=('bash' 'diffutils' 'pacman') +optdepends=('limine-mkinitcpio-hook: rebuild the UKI and Limine entry when the shim is installed after the kernel') +options=('!debug') +source=( + '85-linux-aarch64-pkgbase-shim.hook' + 'linux-aarch64-pkgbase-shim' + 'README.package.md' +) +sha256sums=('SKIP' 'SKIP' 'SKIP') + +package() { + install -Dm644 85-linux-aarch64-pkgbase-shim.hook "${pkgdir}/usr/share/libalpm/hooks/85-linux-aarch64-pkgbase-shim.hook" + install -Dm755 linux-aarch64-pkgbase-shim "${pkgdir}/usr/share/libalpm/scripts/linux-aarch64-pkgbase-shim" + install -Dm644 README.package.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" +} diff --git a/pkgbuilds/linux-aarch64-pkgbase-shim/README.package.md b/pkgbuilds/linux-aarch64-pkgbase-shim/README.package.md new file mode 100644 index 00000000..ee4f387a --- /dev/null +++ b/pkgbuilds/linux-aarch64-pkgbase-shim/README.package.md @@ -0,0 +1,28 @@ +# linux-aarch64-pkgbase-shim + +Arch Linux ARM installs its kernel as `/boot/Image` instead of +`usr/lib/modules//vmlinuz`. Limine discovers the kernel through +`modules.builtin`, but needs `vmlinuz` beside it to build a boot entry. + +This package is one pacman hook. After a kernel package is installed or +upgraded (`usr/lib/modules/*/`), and when the shim itself is installed, it +writes into each package-owned modules directory without native kernel metadata: + +- `pkgbase`: compatibility metadata naming the package that owns the directory +- `vmlinuz`: a copy of `/boot/Image` (the owner must own that too) + +Reinstalling a kernel refreshes the shim's copy when the image changes, even +if the module-directory version is unchanged. Package-owned files are preserved. + +It runs as `85-`, before `90-mkinitcpio-install`, so the usual hook then builds +the initramfs/UKI and the Limine entry. If that hook would not run in the same +transaction (the shim installed after the kernel, or a kernel package that +does not touch `usr/lib/initcpio/`), the script runs +`limine-mkinitcpio-install` itself. Leftover directories of removed kernels +that hold nothing but these two files are cleaned up. + +## Retirement + +Delete this package when `linux-aarch64` ships `vmlinuz` in its kernel module +directory. Package-owned metadata is never modified, so both approaches can coexist +during the transition. diff --git a/pkgbuilds/linux-aarch64-pkgbase-shim/linux-aarch64-pkgbase-shim b/pkgbuilds/linux-aarch64-pkgbase-shim/linux-aarch64-pkgbase-shim new file mode 100755 index 00000000..ae21522e --- /dev/null +++ b/pkgbuilds/linux-aarch64-pkgbase-shim/linux-aarch64-pkgbase-shim @@ -0,0 +1,103 @@ +#!/bin/bash +# Supply pkgbase and vmlinuz metadata missing from Arch Linux ARM kernels. +set -uo pipefail + +readonly SELF=linux-aarch64-pkgbase-shim +readonly ROOT=${OMARCHY_KERNEL_SHIM_ROOT:-} +readonly IMAGE=$ROOT/boot/Image +readonly LIMINE_INSTALL_SCRIPT=$ROOT/usr/share/libalpm/scripts/limine-mkinitcpio-install + +declare -A DIRS=() +modules_in_transaction=0 +self_in_transaction=0 +rebuild=0 +failed=0 + +owner_of() { + pacman -Qqo "${1#"$ROOT"}" 2>/dev/null +} + +# NeedsTargets requires consuming every line from stdin. +while IFS= read -r line; do + case $line in + usr/lib/modules/*) + v=${line#usr/lib/modules/} + v=${v%%/*} + [[ -n $v ]] && DIRS["$ROOT/usr/lib/modules/$v"]=1 + modules_in_transaction=1 + ;; + "$SELF") + self_in_transaction=1 + ;; + esac +done + +# Cover kernels already present when the shim itself is installed. +if ((self_in_transaction)); then + for d in "$ROOT"/usr/lib/modules/*/; do + [[ -d $d ]] && DIRS["${d%/}"]=1 + done +fi + +for dir in "${!DIRS[@]}"; do + [[ -d $dir ]] || continue + # Not a packaged kernel (a leftover or DKMS-only directory). + owner=$(owner_of "$dir") || continue + if [[ $(owner_of "$IMAGE") != "$owner" ]]; then + echo "$SELF: $owner owns $dir but not $IMAGE, leaving it alone" >&2 + continue + fi + # Preserve package-owned files; refresh metadata previously written by the shim. + owner_of "$dir/pkgbase" >/dev/null && continue + owner_of "$dir/vmlinuz" >/dev/null && continue + if [[ -e $dir/pkgbase && $(<"$dir/pkgbase") != "$owner" ]]; then + continue + fi + if [[ -f $dir/pkgbase ]] && cmp -s "$IMAGE" "$dir/vmlinuz"; then + continue + fi + image_tmp=$(mktemp "$dir/.vmlinuz.XXXXXX") || { failed=1; continue; } + if ! cp --preserve=mode "$IMAGE" "$image_tmp" || ! mv -f "$image_tmp" "$dir/vmlinuz"; then + echo "$SELF: failed to copy $IMAGE to $dir/vmlinuz" >&2 + rm -f "$image_tmp" + failed=1 + continue + fi + if ! printf '%s\n' "$owner" >"$dir/pkgbase"; then + failed=1 + continue + fi + echo "$SELF: wrote pkgbase ($owner) and vmlinuz in $dir" + # Rebuild when the transaction will not trigger mkinitcpio itself. + if ! ((modules_in_transaction)) || ! pacman -Qlq "$owner" 2>/dev/null | grep '^/usr/lib/initcpio/' >/dev/null; then + rebuild=1 + fi +done + +# Remove directories containing only stale shim metadata. +for dir in "$ROOT"/usr/lib/modules/*/; do + dir=${dir%/} + [[ -f $dir/pkgbase && -f $dir/vmlinuz ]] || continue + owner_of "$dir" >/dev/null && continue + owner_of "$dir/pkgbase" >/dev/null && continue + owner_of "$dir/vmlinuz" >/dev/null && continue + [[ -z $(find "$dir" -mindepth 1 ! -name pkgbase ! -name vmlinuz -print -quit) ]] || continue + rm -f "$dir/pkgbase" "$dir/vmlinuz" && rmdir "$dir" && echo "$SELF: removed leftover $dir" +done + +# Respect the installer's deferred boot-image build. +deferred_hook=$ROOT/etc/pacman.d/hooks/90-mkinitcpio-install.hook +if ((rebuild)) && [[ -L $deferred_hook && $(readlink "$deferred_hook") == /dev/null ]]; then + echo "$SELF: $deferred_hook is masked, leaving the rebuild to limine-update" + rebuild=0 +fi + +if ((rebuild)); then + if [[ -x $LIMINE_INSTALL_SCRIPT ]]; then + echo rebuild | "$LIMINE_INSTALL_SCRIPT" || failed=1 + else + echo "$SELF: $LIMINE_INSTALL_SCRIPT not found; run mkinitcpio for the new kernel yourself" >&2 + fi +fi + +exit "$failed" diff --git a/pkgbuilds/linux-aarch64-pkgbase-shim/test.sh b/pkgbuilds/linux-aarch64-pkgbase-shim/test.sh new file mode 100644 index 00000000..f0fca47f --- /dev/null +++ b/pkgbuilds/linux-aarch64-pkgbase-shim/test.sh @@ -0,0 +1,107 @@ +#!/bin/bash +set -euo pipefail + +package_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +export OMARCHY_KERNEL_SHIM_ROOT=$scratch +export SHIM_NATIVE=0 SHIM_KERNEL_PRESENT=1 +export SHIM_IMAGE_OWNER=linux-aarch64 + +pacman() { + if [[ $1 == -Qlq ]]; then + printf '/usr/lib/initcpio/install/test\n' + return 0 + fi + case $2 in + /boot/Image) + printf '%s\n' "$SHIM_IMAGE_OWNER" + ;; + /usr/lib/modules/test) + ((SHIM_KERNEL_PRESENT)) || return 1 + printf 'linux-aarch64\n' + ;; + /usr/lib/modules/test/pkgbase | /usr/lib/modules/test/vmlinuz) + [[ $SHIM_NATIVE == 1 || $2 == /usr/lib/modules/test/"$SHIM_NATIVE" ]] || return 1 + printf 'linux-aarch64\n' + ;; + *) return 1 ;; + esac +} +export -f pacman + +modules=$scratch/usr/lib/modules/test +mkdir -p "$modules" "$scratch/boot" "$scratch/etc/pacman.d/hooks" \ + "$scratch/usr/share/libalpm/scripts" +printf 'first-kernel' >"$scratch/boot/Image" +cat >"$scratch/usr/share/libalpm/scripts/limine-mkinitcpio-install" <<'SCRIPT' +#!/bin/bash +cat >>"$OMARCHY_KERNEL_SHIM_ROOT/rebuilds" +SCRIPT +chmod +x "$scratch/usr/share/libalpm/scripts/limine-mkinitcpio-install" + +run_shim() { + printf '%s\n' "$1" | bash "$package_dir/linux-aarch64-pkgbase-shim" +} + +run_shim usr/lib/modules/test/ +[[ $(<"$modules/pkgbase") == linux-aarch64 ]] +cmp "$scratch/boot/Image" "$modules/vmlinuz" +[[ $(stat -c %a "$modules/vmlinuz") == $(stat -c %a "$scratch/boot/Image") ]] +[[ ! -e $scratch/rebuilds ]] +echo 'ok - kernel installation supplies metadata before the normal rebuild hook' + +printf 'replacement-kernel' >"$scratch/boot/Image" +run_shim usr/lib/modules/test/ +cmp "$scratch/boot/Image" "$modules/vmlinuz" +echo 'ok - reinstalling the same kernel version refreshes its image' + +printf 'shim-upgrade' >"$scratch/boot/Image" +run_shim linux-aarch64-pkgbase-shim +[[ $(<"$scratch/rebuilds") == rebuild ]] +run_shim linux-aarch64-pkgbase-shim +[[ $(wc -l <"$scratch/rebuilds") == 1 ]] +echo 'ok - unchanged images do not trigger redundant rebuilds' + +ln -s /dev/null "$scratch/etc/pacman.d/hooks/90-mkinitcpio-install.hook" +printf 'deferred-kernel' >"$scratch/boot/Image" +run_shim linux-aarch64-pkgbase-shim +cmp "$scratch/boot/Image" "$modules/vmlinuz" +[[ $(wc -l <"$scratch/rebuilds") == 1 ]] +echo 'ok - the installer can defer boot-image regeneration' + +for SHIM_NATIVE in pkgbase vmlinuz; do + printf 'package-owned' >"$modules/vmlinuz" + run_shim usr/lib/modules/test/ + [[ $(<"$modules/vmlinuz") == package-owned ]] +done +echo 'ok - either package-owned metadata file prevents replacement' + +SHIM_NATIVE=0 +SHIM_IMAGE_OWNER=another-kernel +run_shim usr/lib/modules/test/ +[[ $(<"$modules/vmlinuz") == package-owned ]] +SHIM_IMAGE_OWNER=linux-aarch64 +echo 'ok - a different kernel image owner prevents replacement' + +( + # shellcheck disable=SC2329 # Invoked by the shim in a child shell. + cp() { return 1; } + export -f cp + if run_shim usr/lib/modules/test/; then + echo 'not ok - a failed image copy was accepted' >&2 + exit 1 + fi + [[ $(<"$modules/vmlinuz") == package-owned ]] +) +echo 'ok - failed copies preserve the previous kernel image' + +SHIM_KERNEL_PRESENT=0 +mkdir -p "$modules/updates/dkms" +run_shim usr/lib/modules/test/ +[[ -d $modules/updates/dkms && -f $modules/vmlinuz ]] +rmdir "$modules/updates/dkms" "$modules/updates" +echo 'ok - kernel removal preserves DKMS leftovers' +run_shim usr/lib/modules/test/ +[[ ! -d $modules ]] +echo 'ok - kernel removal cleans up leftover shim metadata'