diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index de4835ec6b6..0273248cedb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: "$ARCH_IMAGE" sleep infinity docker exec arch pacman -Syu --noconfirm --needed \ git jq lua python python-yaml nodejs npm ripgrep shellcheck imagemagick ffmpeg kitty \ - gum plocate openssh unzip bc libarchive libxkbcommon desktop-file-utils glib2 sqlite + gum plocate openssh unzip bc libarchive libxkbcommon desktop-file-utils glib2 sqlite dtc docker exec arch bash -c 'echo "en_US.UTF-8 UTF-8" >/etc/locale.gen && locale-gen' docker exec arch useradd --create-home --uid "$(id -u)" tester diff --git a/packages/omarchy-mac/boot/README.md b/packages/omarchy-mac/boot/README.md index 004ef0b4534..32f6e41c437 100644 --- a/packages/omarchy-mac/boot/README.md +++ b/packages/omarchy-mac/boot/README.md @@ -8,6 +8,12 @@ While first boot keeps the splash up, a laptop with its lid open shows it on the The implementation was moved from runtime integration `e82fe3c8850b184dfa22eefd571646ad85446122`, preserving Marcelo's provenance recorded in the runtime source-port ledger. The payload in `files/` comes from the boot recipe paired with #503 (omarchy-mac/omarchy-pkgs-aarch64#65 at `617a907f99a1`) with maralcbr/omarchy-pkgs#202 applied (omarchy-mac-boot 20260921-10: the owner's keyboard layout and dock keyboards at the disk prompt). The owner wizard, recovery-slot operations, journal/retry handling and shared Limine menu/hash operations remain core code. Hardware setup reaches its setup leaves through `omarchy-lifecycle-dispatch setup-boot [image-first-boot]` (`entrypoints/setup-boot`: `setup/grub-console.sh`, then `setup/limine-boot.sh`, which on an image's first boot asks for the deferred rebuild instead of building the UKI); the image builder and the migration engine source them directly. `entrypoints/update-takeover ...` answers an update that wants to move aside files no package owns (the step 6 seam asks before it moves anything): read-only, it refuses the whole takeover when any path is the Mac's boot chain, `/usr/lib/initcpio` or pacman configuration, which its image and this package write, or an mkinitcpio file whose own `HOOKS` carry the `asahi` or busybox `encrypt` hook (a legacy Mac's unlock), and lets anything else go, including the memory, USB and mkinitcpio drop-ins omarchy-settings ships on aarch64 too. Owner provisioning, factory reset and `omarchy-drive-password` reach this package only through `omarchy-lifecycle-dispatch`, which runs the `entrypoints/` staged in `/usr/lib/omarchy/mac-boot` and fails on Apple Silicon when they are missing. +## Device tree overlays + +A package can add a device tree node that the kernel's device trees do not have yet. Any package may ship one: a new SoC or a new device needs only a new `.dtbo` in that package's file list, never a change to omarchy-mac-boot. The package ships a compiled overlay as `/usr/share/omarchy-platform/dtb-overlays/PREFIX/NAME.dtbo`, the platform root the rest of quattro uses (`/usr/share/omarchy-platform`). `PREFIX` selects the device trees: `t8103` selects every `t8103-*.dtb`, and `t6001-j316c` selects `t6001-j316c.dtb` only. `/etc/default/update-m1n1` sources `lib/dtb-overlays.sh`. When an overlay applies to one of the newest kernel's device trees, it sets `DTBS` to that kernel's device trees, with overlaid copies in `/run/omarchy-dtb-overlays` in place of the originals. The kernel files do not change, so pacman's mtree check still holds. The overlays apply in C order of `PREFIX/NAME`. An overlay whose root node has the string list `omarchy,skip-if-compatible` does not apply to a device tree that already has an available node with one of those compatibles (Linux's `of_device_is_available()`: no `status`, `okay` or `ok`). So a kernel that adds the node takes over from the overlay. An overlay whose root node has the string `omarchy,opt-in` applies only when that string is a line of `/etc/omarchy-platform/dtb-overlays.opt-in`. Use it for hardware whose driver must not start until the owner chooses it. If an overlay fails to apply, or dtc cannot read the result, that device tree stays as the kernel shipped it. With no overlays, `DTBS` keeps update-m1n1's default, and `boot.bin` does not change. Overlays need `dtc` (`dtc`, `fdtoverlay` and `fdtget`). The package that ships an overlay depends on it. + +`omarchy-apple-silicon-boot-check` applies the same overlays when it rebuilds `boot.bin` for its comparison. Each overlay must belong to a package, or the check fails. It reads `/etc/default/update-m1n1` with `OMARCHY_DTB_OVERLAYS=0`, so reading the configuration builds nothing. `95-omarchy-mac-dtb-overlays.hook` runs `update-m1n1` when a package adds, changes or removes an overlay, as `95-m1n1-install.hook` does after a kernel update. So the next boot and the boot check read the same device trees. + ## Temporary adaptations - Deploy ARM64 Limine to U-Boot's `EFI/BOOT/BOOTAA64.EFI` slot until the shared Limine installer supports that target. diff --git a/packages/omarchy-mac/boot/bin/omarchy-apple-silicon-boot-check b/packages/omarchy-mac/boot/bin/omarchy-apple-silicon-boot-check index 983bca072da..f10370bf776 100755 --- a/packages/omarchy-mac/boot/bin/omarchy-apple-silicon-boot-check +++ b/packages/omarchy-mac/boot/bin/omarchy-apple-silicon-boot-check @@ -114,18 +114,47 @@ for package in "${rivals[@]}"; do [[ $package == "$kernel" || $package == "$bootloader" ]] || ! is_installed "$package" || fail "$kernel boots with $bootloader, but $package is installed too" done +# True when every size or checksum mismatch pacman reports on a modules.* file +# of this kernel is what depmod itself writes: a fresh depmod run over the +# installed modules reproduces the file byte for byte. depmod regenerates +# modules.dep and its siblings whenever a module is installed or removed, so +# after any DKMS module they no longer match the package mtree; what it does +# not regenerate (modules.builtin, modules.builtin.modinfo) never passes. +depmod_maps_match() { + local fresh=$workdir/depmod kver line rel + local flagged + flagged=$(grep -E '^warning: [^:]+: /usr/lib/modules/[^/]+/modules\.[A-Za-z.]+ \((Size|SHA256 checksum) mismatch\)$' <<<"$1") || return 0 + kver=${flagged#*/usr/lib/modules/} + kver=${kver%%/*} + mkdir -p "$fresh" + if [[ -n $root ]]; then + depmod -b "$root" -o "$fresh" "$kver" 2>/dev/null || return 1 + else + depmod -o "$fresh" "$kver" 2>/dev/null || return 1 + fi + while IFS= read -r line; do + rel=${line#warning: *: } + rel=${rel%% (*} + cmp -s "$fresh/lib/modules/${rel#/usr/lib/modules/}" "$(path "$rel")" || return 1 + done <<<"$flagged" +} + # The mtree check, minus the depmod outputs: the kernel package ships -# modules.* and the depmod hook rewrites them on every install, so their -# modification times never match. Anything else altered or missing fails. +# modules.* and the depmod hook rewrites them on every install and after any +# DKMS module, so their modification times never match and their size and +# checksum match only while they are depmod's own (depmod_maps_match). +# Anything else altered or missing fails. for package in "$kernel" "$bootloader"; do set +e mtree_output=$(LC_ALL=C pacman -Qkk "$package" 2>&1) mtree_status=$? set -e + modules_drop='\(Modification time mismatch\)$' + depmod_maps_match "$mtree_output" && modules_drop='\((Modification time|Size|SHA256 checksum) mismatch\)$' # A fresh image ships no sync databases; pacman warns about each on stderr. altered=$(grep -Ev ' altered files$' <<<"$mtree_output" | grep -Ev "^warning: database file for '[^']+' does not exist" | - grep -Ev '^warning: [^:]+: /usr/lib/modules/[^/]+/modules\.[A-Za-z.]+ \(Modification time mismatch\)$' || true) + grep -Ev "^warning: [^:]+: /usr/lib/modules/[^/]+/modules\.[A-Za-z.]+ $modules_drop" || true) # --boot-chain holds only the files the boot files are built from against # the mtree: the kernel image, its device trees and m1n1. Drift in any other # file of the package is left out; everything else pacman reports counts. @@ -513,6 +542,13 @@ if (( has_crypttab_root || busybox_luks )) && [[ -f $host_vconsole ]]; then fi fi +# Package-owned device tree overlays, applied the way /etc/default/update-m1n1 +# applies them (dtb-overlays.sh). +overlay_lib=$(dirname -- "$(realpath -- "$0")")/../lib/omarchy-mac/boot/dtb-overlays.sh +[[ -r $overlay_lib ]] || overlay_lib=$(dirname -- "$(realpath -- "$0")")/../lib/dtb-overlays.sh +# shellcheck source=../lib/dtb-overlays.sh +source "$overlay_lib" + # m1n1 stage 2, built the way this update-m1n1 builds it. script=$(path /usr/bin/update-m1n1) [[ -r $script ]] || fail "/usr/bin/update-m1n1 is not installed" @@ -550,7 +586,7 @@ done SOURCE="" M1N1="" U_BOOT="" CONFIG="" DTBS="" disabled="" config_file=$(path /etc/default/update-m1n1) if [[ -e $config_file ]]; then - env -i PATH=/usr/bin:/bin bash --noprofile --norc -c ' + env -i PATH=/usr/bin:/bin OMARCHY_DTB_OVERLAYS=0 bash --noprofile --norc -c ' unset DTBS SOURCE M1N1 U_BOOT CONFIG M1N1_UPDATE_DISABLED . "$1" >/dev/null 2>&1 || exit 1 for name in DTBS SOURCE M1N1 U_BOOT CONFIG M1N1_UPDATE_DISABLED; do @@ -650,6 +686,33 @@ for dtb in "${dtbs[@]}"; do grep -Fxq -- "$owned" "$workdir/kernel-files" || fail "device tree $dtb is not owned by $kernel" done +# The file each device tree is read from: the kernel's, or the copy that +# carries the package-owned overlays update-m1n1 applies to it. +paths=() +for dtb in "${dtbs[@]}"; do + paths+=("$root$dtb") +done +export OMARCHY_DTB_OVERLAYS_ROOT=$root +mapfile -t overlays < <(dtb_overlays_list) +if (( ${#overlays[@]} )); then + for overlay in "${overlays[@]}"; do + pacman -Qoq "${overlay#"$root"}" >/dev/null 2>&1 || + fail "device tree overlay ${overlay#"$root"} is not owned by a package" + done + if ! dtb_overlays_supported "$script"; then + echo "Apple Silicon boot check: warning: /usr/bin/update-m1n1 has a DTBS default the device tree overlays do not follow, so it leaves them out" >&2 + elif ! dtb_overlays_tools; then + echo "Apple Silicon boot check: warning: device tree overlays are installed, but update-m1n1 leaves them out without dtc 1.7.1 or newer (dtc, fdtoverlay and fdtget); install or update dtc" >&2 + else + mkdir "$workdir/dtbs" + mapfile -t paths < <(dtb_overlays_apply "$workdir/dtbs" "${paths[@]}") + fi +fi +declare -A dtb_file=() +for i in "${!dtbs[@]}"; do + dtb_file[${dtbs[$i]}]=${paths[$i]} +done + m1n1_options() { local line [[ -e $root$CONFIG ]] || return 0 @@ -667,7 +730,7 @@ build_image() { local image=$1 dtb paths=() shift for dtb; do - paths+=("$root$dtb") + paths+=("${dtb_file[$dtb]}") done { cat "$root$M1N1" "${paths[@]}" @@ -686,7 +749,7 @@ dtb_order() { local image=$1 offset=$2 end magic size hash dtb local -A by_hash=() left=() for dtb in "${dtbs[@]}"; do - hash=$(sha256sum <"$root$dtb") + hash=$(sha256sum <"${dtb_file[$dtb]}") hash=${hash%% *} by_hash[$hash]=$dtb left[$hash]=$(( ${left[$hash]:-0} + 1 )) diff --git a/packages/omarchy-mac/boot/files/etc/default/update-m1n1 b/packages/omarchy-mac/boot/files/etc/default/update-m1n1 index b199ce028b2..5b024fecca7 100644 --- a/packages/omarchy-mac/boot/files/etc/default/update-m1n1 +++ b/packages/omarchy-mac/boot/files/etc/default/update-m1n1 @@ -5,3 +5,10 @@ # hook inherits the caller's. The C locale gives the same boot.bin however it # is rebuilt, which the boot check can then reproduce byte for byte. export LC_ALL=C + +# Package-owned device tree overlays under /usr/share/omarchy-platform/dtb-overlays +# (dtb-overlays.sh). With none installed, DTBS keeps update-m1n1's default. +if [ -r /usr/lib/omarchy-mac/boot/dtb-overlays.sh ]; then + . /usr/lib/omarchy-mac/boot/dtb-overlays.sh + dtb_overlays_update_m1n1 || : +fi diff --git a/packages/omarchy-mac/boot/files/usr/share/libalpm/hooks/95-omarchy-mac-dtb-overlays.hook b/packages/omarchy-mac/boot/files/usr/share/libalpm/hooks/95-omarchy-mac-dtb-overlays.hook new file mode 100644 index 00000000000..be9245731c4 --- /dev/null +++ b/packages/omarchy-mac/boot/files/usr/share/libalpm/hooks/95-omarchy-mac-dtb-overlays.hook @@ -0,0 +1,14 @@ +# A package added, changed or removed a device tree overlay: rebuild m1n1 +# stage 2 the way 95-m1n1-install.hook does after a kernel update, so the +# next boot and the boot check read the same device trees. +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/share/omarchy-platform/dtb-overlays/* + +[Action] +Description = Updating m1n1 image for device tree overlays... +When = PostTransaction +Exec = /usr/bin/update-m1n1 diff --git a/packages/omarchy-mac/boot/lib/dtb-overlays.sh b/packages/omarchy-mac/boot/lib/dtb-overlays.sh new file mode 100644 index 00000000000..64cf7863316 --- /dev/null +++ b/packages/omarchy-mac/boot/lib/dtb-overlays.sh @@ -0,0 +1,174 @@ +# shellcheck shell=dash +# Package-owned device tree overlays for m1n1 stage 2 (sh with local). +# +# A package that adds hardware support the kernel's device trees lack ships a +# compiled overlay as /usr/share/omarchy-platform/dtb-overlays/PREFIX/NAME.dtbo. +# It applies to every device tree whose file name is PREFIX.dtb or starts with +# PREFIX- ("t8103" covers every M1 board, "t6001-j316c" one board). A device +# tree takes its overlays in C order of PREFIX/NAME. An overlay whose root node +# has the string list "omarchy,skip-if-compatible" is left out of a device tree +# that already has an available node with one of those compatibles (Linux's +# of_device_is_available(): no status, "okay" or "ok"), so a kernel that gains +# the node wins. An overlay whose root node has the string "omarchy,opt-in" +# applies only when that string is a line of +# /etc/omarchy-platform/dtb-overlays.opt-in: the owner's choice for hardware +# whose driver must not start by default. When an overlay does not apply, or +# dtc cannot read the result, that device tree stays as the kernel shipped it. +# With no overlays, nothing changes. +# +# /etc/default/update-m1n1 calls dtb_overlays_update_m1n1 to set DTBS, and +# omarchy-apple-silicon-boot-check calls dtb_overlays_apply to rebuild the same +# image. OMARCHY_DTB_OVERLAYS=0 turns the update-m1n1 side off, so the boot +# check can read the configuration without building anything. +# OMARCHY_DTB_OVERLAYS_ROOT prefixes every path read or written (tests, and the +# boot check's root). + +dtb_overlays_dir() { + printf '%s\n' "${OMARCHY_DTB_OVERLAYS_ROOT:-}/usr/share/omarchy-platform/dtb-overlays" +} + +# The overlays, one path per line, in the order they apply. +dtb_overlays_list() { + ( + LC_ALL=C + for overlay in "$(dtb_overlays_dir)"/*/*.dtbo; do + if [ -f "$overlay" ]; then + printf '%s\n' "$overlay" + fi + done + ) +} + +# dtc, fdtoverlay and fdtget, from dtc 1.7.1 or newer: older fdtoverlay gives +# an existing node a new phandle when an overlay labels it, and every +# reference to the old one then points nowhere. +dtb_overlays_tools() { + local version + command -v dtc >/dev/null 2>&1 && command -v fdtoverlay >/dev/null 2>&1 && + command -v fdtget >/dev/null 2>&1 || return 1 + # "Version: DTC v1.8.1" on Arch, "Version: DTC 1.6.1" on Debian. + version=$(dtc --version 2>/dev/null | sed -n 's/^Version: DTC v\{0,1\}\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1 \2 \3/p') + # shellcheck disable=SC2086 # split into major minor patch + set -- $version + [ $# = 3 ] || return 1 + [ "$1" -gt 1 ] || { [ "$1" = 1 ] && { [ "$2" -gt 7 ] || { [ "$2" = 7 ] && [ "$3" -ge 1 ]; }; }; } +} + +# True when UPDATE_M1N1 has the DTBS default dtb_overlays_update_m1n1 +# reproduces: asahi-scripts with the default Arch Linux ARM adds. +dtb_overlays_supported() { + # shellcheck disable=SC2016 # the literal default line + grep -Fqx -- ': ${DTBS:=$(/bin/ls -d /lib/modules/*-ARCH | sort -rV | head -1)/dtbs/*.dtb}' "$1" +} + +# True when DTB has an available node whose compatible list holds COMPATIBLE. +# Available is Linux's of_device_is_available(): the node has no status, or +# its status is "okay" or "ok". A disabled node does not count. +dtb_overlays_has_compatible() { + dtc -q -I dtb -O dts -o - "$1" 2>/dev/null | awk -v compatible="\"$2\"" ' + { + if ($0 ~ /^[[:space:]]*\}[;]?[[:space:]]*$/) { + if (matched[depth] && (status[depth] == "" || status[depth] == "okay" || + status[depth] == "ok")) { + found = 1 + exit + } + delete matched[depth] + delete status[depth] + depth-- + } else if ($0 ~ /\{[[:space:]]*$/) { + depth++ + } else if ($0 ~ /^[[:space:]]*compatible[[:space:]]*=/ && index($0, compatible)) { + matched[depth] = 1 + } else if ($0 ~ /^[[:space:]]*status[[:space:]]*=/ && match($0, /"[^"]*"/)) { + status[depth] = substr($0, RSTART + 1, RLENGTH - 2) + } + } + END { exit found ? 0 : 1 } + ' +} + +# Writes OUT: DTB with every overlay in OVERLAYS (newline-separated) that +# applies to it. Returns 1, and writes nothing, when none applies or one fails. +dtb_overlays_build() { + local dtb="$1" out="$2" overlays="$3" name="${1##*/}" applied=0 overlay prefix skip compatible key + local opt_in="${OMARCHY_DTB_OVERLAYS_ROOT:-}/etc/omarchy-platform/dtb-overlays.opt-in" + cp -- "$dtb" "$out.base" || return 1 + for overlay in $overlays; do + prefix=${overlay%/*} + prefix=${prefix##*/} + case "$name" in + "$prefix.dtb" | "$prefix"-*) ;; + *) continue ;; + esac + skip=0 + for key in $(fdtget -t s "$overlay" / omarchy,opt-in 2>/dev/null); do + grep -Fqx -- "$key" "$opt_in" 2>/dev/null || skip=1 + done + for compatible in $(fdtget -t s "$overlay" / omarchy,skip-if-compatible 2>/dev/null); do + if dtb_overlays_has_compatible "$out.base" "$compatible"; then + skip=1 + fi + done + [ "$skip" = 0 ] || continue + if fdtoverlay -i "$out.base" -o "$out.next" "$overlay" 2>/dev/null && + dtc -q -I dtb -O dtb -o /dev/null "$out.next" 2>/dev/null; then + mv -f -- "$out.next" "$out.base" + applied=1 + else + echo "dtb-overlays: $overlay does not apply to $name; $name stays as the kernel shipped it" >&2 + rm -f -- "$out.next" "$out.base" + return 1 + fi + done + if [ "$applied" = 0 ]; then + rm -f -- "$out.base" + return 1 + fi + mv -f -- "$out.base" "$out" +} + +# Prints each DTB, or the copy of it in OUTDIR that carries its overlays, one +# per line and in the same order. +dtb_overlays_apply() { + local outdir="$1" overlays dtb + shift + overlays=$(dtb_overlays_list) + if [ -z "$overlays" ] || ! dtb_overlays_tools; then + overlays="" + fi + for dtb in "$@"; do + if [ -n "$overlays" ] && dtb_overlays_build "$dtb" "$outdir/${dtb##*/}" "$overlays"; then + printf '%s\n' "$outdir/${dtb##*/}" + else + printf '%s\n' "$dtb" + fi + done +} + +# Sets DTBS for update-m1n1 when an overlay applies to one of the newest +# kernel's device trees; otherwise leaves DTBS as it was. +dtb_overlays_update_m1n1() { + local root="${OMARCHY_DTB_OVERLAYS_ROOT:-}" modules outdir list="" path + outdir=$root/run/omarchy-dtb-overlays + [ "${OMARCHY_DTB_OVERLAYS:-1}" != 0 ] || return 0 + [ -z "${DTBS:-}" ] || return 0 + [ -n "$(dtb_overlays_list)" ] || return 0 + if ! dtb_overlays_supported "$root/usr/bin/update-m1n1"; then + echo "dtb-overlays: /usr/bin/update-m1n1 has a DTBS default this does not reproduce; device tree overlays are not applied" >&2 + return 0 + fi + if ! dtb_overlays_tools; then + echo "dtb-overlays: device tree overlays need dtc 1.7.1 or newer (dtc, fdtoverlay and fdtget); install or update dtc" >&2 + return 0 + fi + modules=$(/bin/ls -d "$root"/lib/modules/*-ARCH | sort -rV | head -1) + rm -rf -- "$outdir" + mkdir -p -- "$outdir" || return 0 + for path in $(dtb_overlays_apply "$outdir" "$modules"/dtbs/*.dtb); do + list="$list $path" + done + case "$list" in + *" $outdir/"*) DTBS=${list# } ;; + esac +} diff --git a/packages/omarchy-mac/boot/test/apple-silicon-boot-check-test.sh b/packages/omarchy-mac/boot/test/apple-silicon-boot-check-test.sh index 570c737cb3a..5edcd7b59af 100644 --- a/packages/omarchy-mac/boot/test/apple-silicon-boot-check-test.sh +++ b/packages/omarchy-mac/boot/test/apple-silicon-boot-check-test.sh @@ -58,6 +58,16 @@ case "$*" in printf '%s: 2 total files, 1 altered files\n' "$2" exit 1 ;; + "$2:depmod-size") + printf 'warning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.dep (Modification time mismatch)\nwarning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.dep (Size mismatch)\nwarning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.dep (SHA256 checksum mismatch)\nwarning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.alias (Modification time mismatch)\nwarning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.alias (Size mismatch)\nwarning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.alias (SHA256 checksum mismatch)\n' "$2" "$2" "$2" "$2" "$2" "$2" >&2 + printf '%s: 2353 total files, 2 altered files\n' "$2" + exit 1 + ;; + "$2:builtin-size") + printf 'warning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.builtin (Size mismatch)\n' "$2" >&2 + printf '%s: 2 total files, 1 altered files\n' "$2" + exit 1 + ;; "$2:missing") printf 'warning: %s: /usr/lib/modules/6.17.0-aurora1-ARCH/modules.dep (No such file or directory)\n' "$2" >&2 printf '%s: 2 total files, 1 altered files\n' "$2" @@ -72,6 +82,11 @@ case "$*" in fi printf '%s: 2 total files, 0 altered files\n' "$2" ;; + "-Qoq "*) + owner=$(awk -v path="$2" '$1 == path { print $2 }' "$TEST_FILES/owners" 2>/dev/null) + [[ -n $owner ]] || exit 1 + echo "$owner" + ;; "-Q "*) [[ -f $TEST_FILES/version-$2 ]] || exit 1 echo "$2 $(cat "$TEST_FILES/version-$2")" @@ -154,6 +169,31 @@ case "$*" in esac exit 0 SH +# The dtc tools on the fixture's device trees (magic, size, then a text body): +# fdtoverlay appends "overlay NAME" to the body, dtc prints or accepts it, and +# no fixture overlay names skip-if-compatible. +cat >"$stub_bin/fdtoverlay" <<'SH' +#!/bin/bash +[[ $1 == -i && $3 == -o ]] || exit 1 +body="$(tail -c +9 "$2")"$'\n'"overlay ${5##*/}"$'\n' +size=$(( 8 + ${#body} )) +{ + printf '\xd0\x0d\xfe\xed' + printf "$(printf '\\x%02x' $(( size >> 24 & 255 )) $(( size >> 16 & 255 )) $(( size >> 8 & 255 )) $(( size & 255 )))" + printf '%s' "$body" +} >"$4" +SH +cat >"$stub_bin/dtc" <<'SH' +#!/bin/bash +[[ $1 != --version ]] || { echo "Version: DTC v1.8.1"; exit 0; } +file=${!#} +[[ $(od -An -tx1 -N4 "$file" | tr -d ' \n') == d00dfeed ]] || exit 1 +[[ " $* " != *" -O dts "* ]] || tail -c +9 "$file" +SH +cat >"$stub_bin/fdtget" <<'SH' +#!/bin/bash +exit 1 +SH chmod +x "$stub_bin"/* # update-m1n1 as asahi-scripts 20260127.1 ships it, with the DTBS default ALARM adds. @@ -509,6 +549,37 @@ run_check expect_fail "a device tree the kernel does not own" "device tree /lib/modules/$kver/dtbs/zz-stray.dtb is not owned by linux-aurora" pass "a stale kernel, initramfs, GRUB entry or m1n1 image, disabled m1n1 updates and a stray device tree all fail" +# A package-owned overlay: update-m1n1 applies it to the kernel's device trees +# (dtb-overlays.sh), and the rebuild applies it the same way. +system linux-aurora +mkdir -p "$root/usr/share/omarchy-platform/dtb-overlays/t8103" +printf 'overlay\n' >"$root/usr/share/omarchy-platform/dtb-overlays/t8103/omarchy-ane.dtbo" +printf '/usr/share/omarchy-platform/dtb-overlays/t8103/omarchy-ane.dtbo omarchy-ane\n' >"$test_tmp/files/owners" +run_check --boot-chain +expect_fail "an overlay update-m1n1 has not applied yet" "m1n1/boot.bin on the system ESP (/boot/efi) is not m1n1" +overlaid="$test_tmp/t8103-j274.dtb" +body="device tree t8103-j274.dtb"$'\n'"overlay omarchy-ane.dtbo"$'\n' +size=$(( 8 + ${#body} )) +{ + printf '\xd0\x0d\xfe\xed' + printf "$(printf '\\x%02x' $(( size >> 24 & 255 )) $(( size >> 16 & 255 )) $(( size >> 8 & 255 )) $(( size & 255 )))" + printf '%s' "$body" +} >"$overlaid" +{ + cat "$root/usr/lib/asahi-boot/m1n1.bin" "$root${dtbs[0]}" "$root${dtbs[1]}" "$overlaid" + gzip -c "$root/usr/lib/asahi-boot/u-boot-nodtb.bin" + printf 'chosen.asahi,efi-system-partition=1234\ndisplay=2560x1600\nmitigations=off\n' +} >"$esp/m1n1/boot.bin" +run_check --boot-chain +expect_pass "an m1n1 image update-m1n1 built with the overlaid device tree" +run_check +expect_pass "an m1n1 image with the overlaid device tree, in the full check" +[[ -z $(ls -A "$root/run") ]] || fail "the rebuild writes the overlaid device tree only to its own work directory" +: >"$test_tmp/files/owners" +run_check --boot-chain +expect_fail "an overlay no package owns" "device tree overlay /usr/share/omarchy-platform/dtb-overlays/t8103/omarchy-ane.dtbo is not owned by a package" +pass "package-owned device tree overlays are rebuilt into m1n1 stage 2, and an unowned one fails" + # update-m1n1's defaults: := fills unset and empty settings alike. for config in '' 'DTBS=\nSOURCE=""\n' 'CONFIG=\nM1N1=\nU_BOOT=\n' 'M1N1_UPDATE_DISABLED=\n'; do system linux-aurora @@ -665,6 +736,21 @@ TEST_QKK_FAIL=linux-aurora:missing run_check expect_fail "a missing modules.* file" "linux-aurora files do not match the package mtree" TEST_QKK_FAIL=linux-aurora:silent run_check expect_fail "a pacman -Qkk that fails without a word" "pacman -Qkk linux-aurora failed" +# A DKMS module leaves depmod's maps rewritten: the mtree's size and checksum +# no longer match, and only depmod itself may vouch for them. +mkdir -p "$modules/kernel/drivers/net/xone" +printf 'fake module\n' >"$modules/kernel/drivers/net/xone/xone_dongle.ko" +TEST_QKK_FAIL=linux-aurora:depmod-size run_check +expect_fail "depmod maps that have not been regenerated after a DKMS module" "linux-aurora files do not match the package mtree" +depmod -b "$root" -o "$test_tmp/depmod" "$kver" 2>/dev/null +cp -a "$test_tmp/depmod/lib/modules/$kver/." "$modules/" +TEST_QKK_FAIL=linux-aurora:depmod-size run_check +expect_pass "depmod maps regenerated after a DKMS module" +printf 'corrupt\n' >>"$modules/modules.dep" +TEST_QKK_FAIL=linux-aurora:depmod-size run_check +expect_fail "a corrupted modules.dep beside a DKMS module" "linux-aurora files do not match the package mtree" +TEST_QKK_FAIL=linux-aurora:builtin-size run_check +expect_fail "modules.builtin, which depmod reads but does not write" "linux-aurora files do not match the package mtree" unset TEST_QKK_FAIL TEST_QKK_NODB=1 run_check diff --git a/packages/omarchy-mac/boot/test/dtb-overlays-test.sh b/packages/omarchy-mac/boot/test/dtb-overlays-test.sh new file mode 100755 index 00000000000..5ce9fa5d674 --- /dev/null +++ b/packages/omarchy-mac/boot/test/dtb-overlays-test.sh @@ -0,0 +1,209 @@ +#!/bin/bash + +set -euo pipefail +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +require_command dtc +require_command fdtoverlay +require_command fdtget + +# shellcheck source=../lib/dtb-overlays.sh +source "$ROOT/lib/dtb-overlays.sh" +dtb_overlays_tools || fail "dtc is 1.7.1 or newer: $(dtc --version)" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +root=$tmp/root +export OMARCHY_DTB_OVERLAYS_ROOT=$root +kver=7.1.12-2-11-ARCH +dtbs=$root/lib/modules/$kver/dtbs +overlays=$root/usr/share/omarchy-platform/dtb-overlays +mkdir -p "$dtbs" "$overlays" "$root/usr/bin" "$root/run" "$tmp/out" + +# A board device tree: its board and SoC compatibles and one /soc node. +board_dtb() { + local name=$1 board=$2 soc=$3 extra=${4:-} + dtc -q -I dts -O dtb -o "$dtbs/$name" - <; + #size-cells = <2>; + soc { + compatible = "simple-bus"; + #address-cells = <2>; + #size-cells = <2>; + ranges; + serial@1000 { compatible = "apple,s5l-uart"; reg = <0 0x1000 0 0x100>; }; + $extra + }; +}; +DTS +} + +# An overlay PREFIX/NAME.dtbo that adds /soc/ane@2000 with COMPATIBLE. With +# SKIP, it says to stay out of a tree that already has COMPATIBLE. TARGET is +# the node it adds to. With OPT_IN, it applies only when opted in. +overlay() { + local prefix=$1 name=$2 compatible=$3 skip=${4:-} target=${5:-/soc} opt_in=${6:-} extra="" + [[ -z $skip ]] || extra="omarchy,skip-if-compatible = \"$compatible\";" + [[ -z $opt_in ]] || extra="$extra omarchy,opt-in = \"$opt_in\";" + mkdir -p "$overlays/$prefix" + dtc -q -@ -I dts -O dtb -o "$overlays/$prefix/$name.dtbo" - <; status = "okay"; }; + }; + }; +}; +DTS +} + +has_ane() { + [[ $(fdtget "$1" /soc/ane@2000 compatible 2>/dev/null) == "$2" ]] +} + +board_dtb t6001-j316c.dtb j316c t6001 +board_dtb t8103-j274.dtb j274 t8103 +board_dtb t8103-j293.dtb j293 t8103 +board_dtb t8103-j293-kernel-ane.dtb j293 t8103 'ane@2000 { compatible = "apple,t8103-ane"; reg = <0 0x2000 0 0x100>; };' +stock=("$dtbs/t6001-j316c.dtb" "$dtbs/t8103-j274.dtb" "$dtbs/t8103-j293.dtb") +cp "$dtbs"/*.dtb "$tmp/" + +# update-m1n1 as Arch Linux ARM's asahi-scripts ships its DTBS default. +cat >"$root/usr/bin/update-m1n1" <<'SH' +#!/bin/sh +: ${DTBS:=$(/bin/ls -d /lib/modules/*-ARCH | sort -rV | head -1)/dtbs/*.dtb} +cat "$M1N1" $DTBS >"${TARGET}.new" +SH + +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}") +[[ ${result[*]} == "${stock[*]}" && -z $(ls -A "$tmp/out") ]] || fail "with no overlays, every device tree stays the kernel's" +unset DTBS +dtb_overlays_update_m1n1 +[[ -z ${DTBS:-} ]] || fail "with no overlays, update-m1n1 keeps its DTBS default" +pass "no overlays change nothing" + +overlay t8103 omarchy-ane apple,t8103-ane skip +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}") +[[ ${result[0]} == "$dtbs/t6001-j316c.dtb" ]] || fail "a t8103 overlay leaves a t6001 device tree alone" +[[ ${result[1]} == "$tmp/out/t8103-j274.dtb" && ${result[2]} == "$tmp/out/t8103-j293.dtb" ]] || + fail "a t8103 overlay applies to every t8103 board: ${result[*]}" +for path in "${result[1]}" "${result[2]}"; do + has_ane "$path" apple,t8103-ane || fail "the overlaid device trees carry the overlay's node" +done +[[ $(fdtget "${result[2]}" / compatible) == "apple,j293 apple,t8103" ]] || fail "the overlaid device tree keeps its board compatibles" +dtc -q -I dtb -O dts -o /dev/null "${result[2]}" || fail "dtc reads the overlaid device tree" +cmp -s "$dtbs/t8103-j293.dtb" "$tmp/t8103-j293.dtb" || fail "the kernel's device tree is not written" +pass "an overlay applies to the device trees its prefix names and nowhere else" + +cp "${result[2]}" "$tmp/first.dtb" +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}") +cmp -s "${result[2]}" "$tmp/first.dtb" || fail "applying the overlays again gives the same bytes" +pass "the overlaid device tree is the same bytes every time" + +# dtc before 1.7.1 renumbers a labelled existing node, so it gets no overlays. +mkdir -p "$tmp/old-dtc" +printf '#!/bin/bash\n[[ $1 == --version ]] && { echo "Version: DTC 1.6.1"; exit 0; }\nexec %q "$@"\n' "$(command -v dtc)" >"$tmp/old-dtc/dtc" +chmod +x "$tmp/old-dtc/dtc" +mapfile -t result < <(PATH="$tmp/old-dtc:$PATH" dtb_overlays_apply "$tmp/out" "${stock[@]}") +[[ ${result[*]} == "${stock[*]}" ]] || fail "dtc 1.6.1 gets no overlays: ${result[*]}" +pass "dtc older than 1.7.1 leaves every device tree the kernel's" + +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "$dtbs/t8103-j293-kernel-ane.dtb") +[[ ${result[0]} == "$dtbs/t8103-j293-kernel-ane.dtb" ]] || + fail "an overlay stays out of a device tree that already has its skip-if-compatible node" +pass "a kernel device tree that has the node wins over the overlay" + +# Only an available node has the compatible: Linux's of_device_is_available() +# rule is no status, "okay" or "ok"; a disabled node does not count. +dtc -q -I dts -O dtb -o "$tmp/available.dtb" - <<'DTS' +/dts-v1/; +/ { + compatible = "apple,j293", "apple,t8103"; + none@1000 { compatible = "apple,t8103-ane"; }; + okay@1000 { compatible = "apple,t8103-ane"; status = "okay"; }; + ok@1000 { compatible = "apple,t8103-ane"; status = "ok"; }; +}; +DTS +dtb_overlays_has_compatible "$tmp/available.dtb" apple,t8103-ane || + fail "a node without status, with \"okay\" or with \"ok\" has the compatible" +for unavailable in disabled reserved; do + dtc -q -I dts -O dtb -o "$tmp/$unavailable.dtb" - <; status = "disabled"; };' +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "$dtbs/t8103-j293-kernel-ane-disabled.dtb") +[[ ${result[0]} == "$tmp/out/t8103-j293-kernel-ane-disabled.dtb" ]] || + fail "a disabled kernel node does not keep the overlay out: ${result[*]}" +has_ane "${result[0]}" apple,t8103-ane || fail "the overlaid tree carries the overlay's node" +[[ $(fdtget "${result[0]}" /soc/ane@2000 status) == okay ]] || + fail "the overlay enables the disabled node in place" +pass "a disabled kernel node does not keep the overlay out, and the overlay enables it" + +rm -rf "$overlays/t8103" +overlay t8103-j293 board apple,t8103-ane +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}") +[[ ${result[1]} == "$dtbs/t8103-j274.dtb" && ${result[2]} == "$tmp/out/t8103-j293.dtb" ]] || + fail "a board prefix applies to that board only: ${result[*]}" +pass "a board prefix names one board" + +overlay t6001 opt apple,t6000-ane "" /soc ane-t6001 +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}") +[[ ${result[0]} == "$dtbs/t6001-j316c.dtb" ]] || fail "an opt-in overlay stays out until the owner opts in" +mkdir -p "$root/etc/omarchy-platform" +printf 'other\nane-t6001\n' >"$root/etc/omarchy-platform/dtb-overlays.opt-in" +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}") +[[ ${result[0]} == "$tmp/out/t6001-j316c.dtb" ]] || + fail "an opt-in overlay applies once its name is a line of /etc/omarchy-platform/dtb-overlays.opt-in" +has_ane "${result[0]}" apple,t6000-ane || fail "the opted-in overlay's node is in the device tree" +rm -f "$overlays/t6001/opt.dtbo" "$root/etc/omarchy-platform/dtb-overlays.opt-in" "$tmp/out"/* +pass "an opt-in overlay applies only after the owner opts in" + +overlay t8103-j293 zz-broken apple,t8103-extra "" /soc/missing@0 +rm -f "$tmp/out"/* +mapfile -t result < <(dtb_overlays_apply "$tmp/out" "${stock[@]}" 2>"$tmp/err") +[[ ${result[2]} == "$dtbs/t8103-j293.dtb" ]] || fail "a device tree an overlay does not apply to stays the kernel's" +[[ -z $(ls -A "$tmp/out") ]] || fail "a failed overlay leaves no file behind: $(ls -A "$tmp/out")" +grep -Fq "zz-broken.dtbo does not apply to t8103-j293.dtb" "$tmp/err" || fail "a failed overlay is reported: $(cat "$tmp/err")" +pass "an overlay that does not apply leaves the kernel's device tree in place" +rm -f "$overlays/t8103-j293/zz-broken.dtbo" + +# update-m1n1's side: the newest kernel's device trees, in C order, with the +# overlaid copy in place of the one it replaces. +mkdir -p "$root/lib/modules/6.1.0-1-ARCH/dtbs" +rm -f "$dtbs/t8103-j293-kernel-ane.dtb" "$dtbs/t8103-j293-kernel-ane-disabled.dtb" +unset DTBS +dtb_overlays_update_m1n1 +out=$root/run/omarchy-dtb-overlays +[[ $DTBS == "$dtbs/t6001-j316c.dtb $dtbs/t8103-j274.dtb $out/t8103-j293.dtb" ]] || + fail "update-m1n1 takes the newest kernel's device trees with the overlaid one in its place: $DTBS" +has_ane "$out/t8103-j293.dtb" apple,t8103-ane || fail "update-m1n1's copy carries the overlay" +DTBS=/custom.dtb +dtb_overlays_update_m1n1 +[[ $DTBS == /custom.dtb ]] || fail "a DTBS the administrator set is kept" +unset DTBS +OMARCHY_DTB_OVERLAYS=0 dtb_overlays_update_m1n1 +[[ -z ${DTBS:-} ]] || fail "OMARCHY_DTB_OVERLAYS=0 leaves DTBS alone" +sed -i 's|sort -rV|sort -r|' "$root/usr/bin/update-m1n1" +dtb_overlays_update_m1n1 2>"$tmp/err" +[[ -z ${DTBS:-} ]] || fail "an update-m1n1 with another DTBS default gets no overlays" +grep -Fq "does not reproduce" "$tmp/err" || fail "an unknown update-m1n1 is reported" +pass "update-m1n1 gets the overlaid device trees only where it reproduces the default"