diff --git a/etc/mkinitcpio.conf.d/omarchy_hooks.conf b/etc/mkinitcpio.conf.d/omarchy_hooks.conf index a8e39629..68408b00 100644 --- a/etc/mkinitcpio.conf.d/omarchy_hooks.conf +++ b/etc/mkinitcpio.conf.d/omarchy_hooks.conf @@ -1,5 +1,45 @@ HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs) +# The proprietary NVIDIA driver does early KMS itself: nvidia.conf (written by +# install/hardware/nvidia.sh, sourced before this file) early-loads nvidia_drm +# with modeset=1. Keeping the kms hook on such a system makes autodetect pull +# in nouveau — and ~100 MB of its GSP firmware — for a driver that never runs. +# Drop kms only when nvidia_drm is early-loaded and NVIDIA owns every display +# controller. Hybrid systems keep kms: their iGPU still needs it for early +# KMS at the LUKS prompt. So does anything unexpected, like a PCI tree that +# cannot be read. +# +# This reads MODULES midway through mkinitcpio's drop-in sourcing, so a +# later-sorting drop-in that resets MODULES outright — surface_device_modules.conf +# does — would strip nvidia_drm after kms was already dropped. Every machine +# Omarchy writes such a file for carries an Intel iGPU, which keeps kms here +# through the scan below; keep it that way. +if [[ " ${MODULES[*]:-} " == *" nvidia_drm "* ]]; then + _omarchy_nvidia_gpu=0 + _omarchy_other_gpu=0 + for _omarchy_pci in "${OMARCHY_PCI_DEVICES_PATH:-/sys/bus/pci/devices}"/*; do + if [[ ! -r $_omarchy_pci/class || ! -r $_omarchy_pci/vendor ]]; then + # An unreadable device could be another GPU. Inconclusive keeps kms. + _omarchy_other_gpu=1 + continue + fi + [[ $(<"$_omarchy_pci/class") == "0x03"* ]] || continue + if [[ $(<"$_omarchy_pci/vendor") == "0x10de" ]]; then + _omarchy_nvidia_gpu=1 + else + _omarchy_other_gpu=1 + fi + done + if ((_omarchy_nvidia_gpu && !_omarchy_other_gpu)); then + _omarchy_hooks=() + for _omarchy_hook in "${HOOKS[@]}"; do + [[ $_omarchy_hook == "kms" ]] || _omarchy_hooks+=("$_omarchy_hook") + done + HOOKS=("${_omarchy_hooks[@]}") + fi + unset _omarchy_nvidia_gpu _omarchy_other_gpu _omarchy_pci _omarchy_hooks _omarchy_hook +fi + # Bundle vconsole.conf so Plymouth uses the configured keyboard layout at the # LUKS prompt, but only when that layout types Latin letters. Passphrases are # Latin characters, so bundling a Hebrew/Greek/Cyrillic/Arabic layout would diff --git a/migrations/1786605598.sh b/migrations/1786605598.sh new file mode 100644 index 00000000..ce9e9c0a --- /dev/null +++ b/migrations/1786605598.sh @@ -0,0 +1,33 @@ +echo "Rebuild the initramfs so NVIDIA-only systems shed nouveau's unused GSP firmware" + +# omarchy_hooks.conf now filters the kms hook out of HOOKS when the proprietary +# NVIDIA driver handles early KMS and NVIDIA owns every display controller +# (#6790). The settings package deploys that conditional, but nothing rebuilds +# the initramfs for a mkinitcpio drop-in change, so affected machines would +# carry ~100 MB of dead nouveau firmware until their next kernel update. +# Rebuild once, and only where the conditional actually changes the outcome: +# evaluate the installed drop-ins the way mkinitcpio does and check that kms +# dropped out. A user-edited omarchy_hooks.conf (pacman leaves the packaged +# update as a .pacnew) keeps kms and correctly skips the rebuild. + +hooks_conf="${OMARCHY_MKINITCPIO_HOOKS_CONF:-/etc/mkinitcpio.conf.d/omarchy_hooks.conf}" +nvidia_conf="${OMARCHY_MKINITCPIO_NVIDIA_CONF:-/etc/mkinitcpio.conf.d/nvidia.conf}" +rebuild_marker="${OMARCHY_KMS_REBUILD_MARKER:-/var/lib/omarchy/migrations/1786605598}" + +omarchy-cmd-present limine-mkinitcpio || exit 0 +[[ -f $hooks_conf && -f $nvidia_conf ]] || exit 0 + +# The rebuild is machine-wide, but migrations run once per user: a marker +# records completion so another user's run does not repeat it, while a missing +# marker still retries an interrupted rebuild. +[[ ! -e $rebuild_marker ]] || exit 0 + +# Source the drop-ins in mkinitcpio's order (nvidia.conf sorts first) and read +# the HOOKS they produce. Skip conservatively if evaluation fails. +hooks=$(bash -c 'source "$1" && source "$2" && echo " ${HOOKS[*]} "' -- "$nvidia_conf" "$hooks_conf") || exit 0 + +[[ $hooks != *" kms "* ]] || exit 0 + +echo "This machine no longer uses the kms hook; rebuilding the initramfs without nouveau" +sudo limine-mkinitcpio +sudo install -Dm644 /dev/null "$rebuild_marker" diff --git a/test/shell.d/nvidia-kms-hook-test.sh b/test/shell.d/nvidia-kms-hook-test.sh new file mode 100755 index 00000000..a1804e01 --- /dev/null +++ b/test/shell.d/nvidia-kms-hook-test.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +hooks_conf="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf" + +# Each argument is a PCI device as "vendor:class", in sysfs's own format. +write_pci_devices() { + rm -rf "$tmp_dir/devices" + mkdir -p "$tmp_dir/devices" + + local index=0 + local spec + for spec in "$@"; do + local slot + slot=$(printf '0000:%02x:00.0' "$index") + mkdir -p "$tmp_dir/devices/$slot" + printf '%s\n' "${spec%%:*}" >"$tmp_dir/devices/$slot/vendor" + printf '%s\n' "${spec##*:}" >"$tmp_dir/devices/$slot/class" + index=$((index + 1)) + done +} + +# Sources the hook config the way mkinitcpio does — with earlier drop-ins +# already applied — and prints the resulting HOOKS. mkinitcpio does not run +# under set -u, but the config must survive it, so source under it anyway. +# "unset" leaves MODULES undeclared entirely. +resolved_hooks() { + local modules_decl="" + [[ $1 == "unset" ]] || modules_decl="MODULES=($1)" + + # The vconsole block sources the host's /etc/vconsole.conf, which may set + # only KEYMAP; predefine XKBLAYOUT so its expansion survives set -u and the + # test stays independent of the machine it runs on. + OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" bash -uc " + FILES=() + XKBLAYOUT=us + $modules_decl + source '$hooks_conf' + echo \"\${HOOKS[*]}\" + " +} + +nvidia_modules="nvidia nvidia_modeset nvidia_uvm nvidia_drm" + +write_pci_devices +with_kms=$(resolved_hooks "") +without_kms=${with_kms/ kms / } + +[[ $with_kms == *" kms "* ]] || + fail "baseline HOOKS contains the kms hook" "actual: $with_kms" +pass "baseline HOOKS contains the kms hook" + +assert_hooks() { + local description="$1" modules="$2" expected="$3" + local actual + actual=$(resolved_hooks "$modules") + + [[ $actual == "$expected" ]] || + fail "$description" "expected: $expected"$'\n'"actual: $actual" + pass "$description" +} + +# NVIDIA RTX class display controller. +write_pci_devices 0x10de:0x030000 +assert_hooks "nvidia-only system with early nvidia_drm drops only kms" \ + "$nvidia_modules" "$without_kms" +assert_hooks "nvidia-only system without early nvidia_drm keeps kms" \ + "" "$with_kms" +assert_hooks "unset MODULES under set -u keeps kms without erroring" \ + "unset" "$with_kms" + +# AMD integrated graphics next to an NVIDIA 3D controller. +write_pci_devices 0x1002:0x030000 0x10de:0x030200 +assert_hooks "hybrid system keeps kms for the iGPU" \ + "$nvidia_modules" "$with_kms" + +# NVIDIA audio function only: no display controller found. +write_pci_devices 0x10de:0x040300 +assert_hooks "no display controller found keeps kms" \ + "$nvidia_modules" "$with_kms" + +write_pci_devices +assert_hooks "empty PCI tree keeps kms" \ + "$nvidia_modules" "$with_kms" + +# A device directory missing its class/vendor attributes must not error, and +# counts as inconclusive: it could be another GPU, so kms stays. +write_pci_devices +mkdir -p "$tmp_dir/devices/0000:00:00.0" +assert_hooks "unreadable PCI device keeps kms" \ + "$nvidia_modules" "$with_kms" + +# Even next to a readable NVIDIA GPU — the unreadable device may be the iGPU. +write_pci_devices 0x10de:0x030000 +mkdir -p "$tmp_dir/devices/0000:01:00.0" +assert_hooks "unreadable device beside an NVIDIA GPU keeps kms" \ + "$nvidia_modules" "$with_kms" diff --git a/test/shell.d/nvidia-kms-migration-test.sh b/test/shell.d/nvidia-kms-migration-test.sh new file mode 100755 index 00000000..998487ad --- /dev/null +++ b/test/shell.d/nvidia-kms-migration-test.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +migration="$ROOT/migrations/1786605598.sh" +packaged_hooks="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +calls="$test_tmp/calls.log" +mkdir -p "$stub_bin" +: >"$calls" + +cat >"$stub_bin/omarchy-cmd-present" <<'SH' +#!/bin/bash + +(( ${LIMINE_MKINITCPIO_INSTALLED:-1} == 1 )) +SH + +cat >"$stub_bin/sudo" <<'SH' +#!/bin/bash + +printf 'sudo' >>"$TEST_LOG" +printf '\t%s' "$@" >>"$TEST_LOG" +printf '\n' >>"$TEST_LOG" +"$@" +SH + +cat >"$stub_bin/limine-mkinitcpio" <<'SH' +#!/bin/bash + +echo 'limine-mkinitcpio' >>"$TEST_LOG" +SH + +chmod +x "$stub_bin"/* + +hooks_conf="$test_tmp/omarchy_hooks.conf" +nvidia_conf="$test_tmp/nvidia.conf" +rebuild_marker="$test_tmp/rebuild-complete" + +cp "$packaged_hooks" "$hooks_conf" +echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' >"$nvidia_conf" + +# Each argument is a PCI device as "vendor:class", in sysfs's own format. +write_pci_devices() { + rm -rf "$test_tmp/devices" + mkdir -p "$test_tmp/devices" + + local index=0 + local spec + for spec in "$@"; do + local slot + slot=$(printf '0000:%02x:00.0' "$index") + mkdir -p "$test_tmp/devices/$slot" + printf '%s\n' "${spec%%:*}" >"$test_tmp/devices/$slot/vendor" + printf '%s\n' "${spec##*:}" >"$test_tmp/devices/$slot/class" + index=$((index + 1)) + done +} + +run_migration() { + PATH="$stub_bin:$PATH" \ + TEST_LOG="$calls" \ + OMARCHY_MKINITCPIO_HOOKS_CONF="$hooks_conf" \ + OMARCHY_MKINITCPIO_NVIDIA_CONF="$nvidia_conf" \ + OMARCHY_KMS_REBUILD_MARKER="$rebuild_marker" \ + OMARCHY_PCI_DEVICES_PATH="$test_tmp/devices" \ + bash -euo pipefail "$migration" >/dev/null +} + +# NVIDIA-only machine with the proprietary driver: the packaged conditional +# drops kms, so the stale initramfs must be rebuilt once. +write_pci_devices 0x10de:0x030000 +run_migration + +grep -Fxq 'limine-mkinitcpio' "$calls" || + fail "an NVIDIA-only machine rebuilds its initramfs" +[[ -f $rebuild_marker ]] || fail "the rebuild records the machine-wide repair" +pass "migration rebuilds the initramfs on an NVIDIA-only machine" + +: >"$calls" +run_migration + +[[ ! -s $calls ]] || fail "a recorded rebuild is not repeated" "$(cat "$calls")" +pass "migration is machine-idempotent across users" + +rm -f "$rebuild_marker" +: >"$calls" +run_migration + +grep -Fxq 'limine-mkinitcpio' "$calls" || + fail "an interrupted rebuild is retried" +pass "migration retries an interrupted rebuild" + +# Hybrid machine: the conditional keeps kms, so the initramfs already matches. +write_pci_devices 0x1002:0x030000 0x10de:0x030200 +rm -f "$rebuild_marker" +: >"$calls" +run_migration + +[[ ! -s $calls ]] || fail "a hybrid machine is left alone" "$(cat "$calls")" +[[ ! -e $rebuild_marker ]] || fail "an untouched machine is not marked as repaired" +pass "migration skips a hybrid machine that keeps kms" + +# A user-edited hooks conf predating the conditional (the packaged update sits +# in a .pacnew) still carries kms unconditionally: nothing to rebuild for. +write_pci_devices 0x10de:0x030000 +echo 'HOOKS=(base udev autodetect modconf kms block filesystems fsck)' >"$hooks_conf" +: >"$calls" +run_migration + +[[ ! -s $calls ]] || fail "a user-edited hooks conf is left alone" "$(cat "$calls")" +pass "migration skips a hooks conf without the conditional" + +cp "$packaged_hooks" "$hooks_conf" + +: >"$calls" +LIMINE_MKINITCPIO_INSTALLED=0 run_migration + +[[ ! -s $calls ]] || fail "installs without limine-mkinitcpio are skipped" "$(cat "$calls")" + +mv "$nvidia_conf" "$nvidia_conf.away" +run_migration +mv "$nvidia_conf.away" "$nvidia_conf" + +[[ ! -s $calls ]] || fail "machines without the proprietary NVIDIA driver are skipped" "$(cat "$calls")" +pass "migration skips installs it does not apply to"