diff options
| -rwxr-xr-x | archsetup | 79 | ||||
| -rw-r--r-- | tests/nvidia-preflight/test_nvidia_preflight.py | 162 | ||||
| -rw-r--r-- | todo.org | 8 |
3 files changed, 242 insertions, 7 deletions
@@ -419,6 +419,67 @@ if [ "$fresh_install" = "true" ]; then fi ### Pre-flight Checks +# NVIDIA/Wayland preflight core — pure report + status; the caller prompts. +# Scans DRM (then PCI display-class) modalias files for the NVIDIA vendor id +# (10DE); on a match prints the Wayland guidance + required env vars and +# checks the repo's candidate nvidia-utils major version. Globs overridable +# for tests: NVIDIA_DRM_GLOB, NVIDIA_PCI_GLOB. +# Returns: 0 = no NVIDIA GPU; 10 = NVIDIA, driver requirement met (>= 535); +# 11 = NVIDIA, requirement not met (repo driver too old or unknown). +nvidia_preflight_report() { + local drm_glob="${NVIDIA_DRM_GLOB:-/sys/class/drm/card*/device/modalias}" + local pci_glob="${NVIDIA_PCI_GLOB:-/sys/bus/pci/devices/*/modalias}" + local found=false modalias modalias_file + + # shellcheck disable=SC2086 # the unquoted vars ARE the globs + for modalias_file in $drm_glob; do + [[ -r "$modalias_file" ]] || continue + modalias=$(cat "$modalias_file" 2>/dev/null) + case "$modalias" in + *v000010DE*|*v000010de*) found=true ;; + esac + done + if [ "$found" != "true" ]; then + # shellcheck disable=SC2086 + for modalias_file in $pci_glob; do + [[ -r "$modalias_file" ]] || continue + modalias=$(cat "$modalias_file" 2>/dev/null) + # Only display-class devices (bc03), so an NVIDIA audio/usb + # function on the same card doesn't trigger the check. + if [[ "$modalias" == *bc03* ]]; then + case "$modalias" in + *v000010DE*|*v000010de*) found=true ;; + esac + fi + done + fi + [ "$found" = "true" ] || return 0 + + echo " [!!] NVIDIA GPU detected." + echo " Wayland/Hyprland on NVIDIA needs driver 535+ and explicit" + echo " environment variables; expect rougher edges than AMD/Intel:" + echo " LIBVA_DRIVER_NAME=nvidia" + echo " GBM_BACKEND=nvidia-drm" + echo " __GLX_VENDOR_LIBRARY_NAME=nvidia" + echo " ELECTRON_OZONE_PLATFORM_HINT=auto" + echo " archsetup installs nvidia-open-dkms (Turing+); pre-Turing" + echo " cards need an AUR legacy driver instead." + + local ver major + ver=$(pacman -Si nvidia-utils 2>/dev/null | awk '/^Version/ {print $3; exit}') + major="${ver%%.*}" + if [[ "$major" =~ ^[0-9]+$ ]] && [ "$major" -ge 535 ]; then + echo " [OK] NVIDIA driver candidate: ${ver} (>= 535)" + return 10 + fi + echo "ERROR: NVIDIA driver requirement not met" + echo " Required: driver 535 or newer for usable Wayland" + echo " Repo offers: ${ver:-unknown (pacman -Si nvidia-utils failed)}" + echo " Fix: refresh the package database (pacman -Syy) and retry, or" + echo " install with DESKTOP_ENV=dwm (X11) instead." + return 11 +} + preflight_checks() { echo "Running pre-flight checks..." @@ -460,6 +521,20 @@ preflight_checks() { fi echo " [OK] System: Arch Linux detected" + # NVIDIA + Wayland preflight: abort on a too-old driver, confirm on a + # detected card (the report prints the guidance + env vars). + local nvidia_rc=0 + nvidia_preflight_report || nvidia_rc=$? + if [ "$nvidia_rc" = "11" ]; then + exit 1 + elif [ "$nvidia_rc" = "10" ]; then + read -r -p "Continue installing for Wayland on NVIDIA? [Y/n]: " nvidia_go + case "$nvidia_go" in + [nN]*) echo "Aborted at NVIDIA preflight."; exit 1 ;; + *) echo " [OK] Continuing on NVIDIA." ;; + esac + fi + # Check locale configuration if grep -q "^LANG=" /etc/locale.conf 2>/dev/null; then current_locale=$(grep "^LANG=" /etc/locale.conf | cut -d= -f2) @@ -861,8 +936,8 @@ install_gpu_drivers() { if [ "$detected_nvidia" = "true" ]; then display "task" "NVIDIA GPU detected (via modalias) - installing drivers" # nvidia-dkms left the repos; nvidia-open-dkms is the packaged driver - # (Turing and newer — pre-Turing cards need an AUR legacy variant, - # see the NVIDIA preflight task). + # (Turing and newer — pre-Turing cards need an AUR legacy variant; + # nvidia_preflight_report warns about this at preflight). pacman_install nvidia-open-dkms pacman_install nvidia-utils pacman_install nvidia-settings diff --git a/tests/nvidia-preflight/test_nvidia_preflight.py b/tests/nvidia-preflight/test_nvidia_preflight.py new file mode 100644 index 0000000..bdacfd5 --- /dev/null +++ b/tests/nvidia-preflight/test_nvidia_preflight.py @@ -0,0 +1,162 @@ +"""Tests for the nvidia_preflight_report helper in the archsetup installer. + +nvidia_preflight_report is the pure core of the NVIDIA/Wayland preflight +check: it scans DRM (then PCI display-class) modalias files for the NVIDIA +vendor id, and when one matches it prints the Wayland warning + required +environment variables and checks the repo's candidate nvidia-utils major +version. Return codes: 0 = no NVIDIA GPU, 10 = NVIDIA and the driver +requirement (535+) is met, 11 = NVIDIA and the requirement is not met +(driver too old or unknown). The interactive continue/abort prompt lives in +preflight_checks, not here, so this core is unit testable. + +These tests exercise the REAL function body, extracted from the `archsetup` +script at run time (not a copy), against temp modalias trees and a fake +pacman on PATH. + +Run from repo root: + python3 -m unittest tests.nvidia-preflight.test_nvidia_preflight +""" + +import os +import shutil +import subprocess +import tempfile +import unittest + + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") + +NVIDIA_MODALIAS = "pci:v000010DEd00002684sv00001043sd000088E2bc03sc00i00" +NVIDIA_MODALIAS_LOWER = "pci:v000010ded00002684sv00001043sd000088e2bc03sc00i00" +AMD_MODALIAS = "pci:v00001002d0000164Esv00001462sd00007D78bc03sc80i00" +NON_DISPLAY_NVIDIA = "pci:v000010DEd00002684sv00001043sd000088E2bc0Csc03i30" + + +class NvidiaPreflightHarness(unittest.TestCase): + """Source nvidia_preflight_report out of the real archsetup script.""" + + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="nvidia-preflight-test-") + self.drm = os.path.join(self.tmp, "drm") + self.pci = os.path.join(self.tmp, "pci") + os.makedirs(self.drm) + os.makedirs(self.pci) + self.fakebin = os.path.join(self.tmp, "bin") + os.makedirs(self.fakebin) + self.wrapper = os.path.join(self.tmp, "run.sh") + with open(self.wrapper, "w") as f: + f.write( + "#!/bin/bash\n" + 'ARCHSETUP="$1"; shift\n' + "source <(sed -n " + "'/^nvidia_preflight_report() {/,/^}/p' \"$ARCHSETUP\")\n" + "nvidia_preflight_report\n" + ) + os.chmod(self.wrapper, 0o755) + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def fake_pacman(self, version=None, fail=False): + """A pacman stub answering `pacman -Si nvidia-utils`.""" + path = os.path.join(self.fakebin, "pacman") + with open(path, "w") as f: + if fail: + f.write("#!/bin/sh\nexit 1\n") + else: + f.write( + "#!/bin/sh\n" + "printf 'Repository : extra\\n'\n" + "printf 'Name : nvidia-utils\\n'\n" + "printf 'Version : %s\\n'\n" % version + ) + os.chmod(path, 0o755) + + def add_modalias(self, root, subdir, content): + d = os.path.join(root, subdir) + os.makedirs(d, exist_ok=True) + with open(os.path.join(d, "modalias"), "w") as f: + f.write(content + "\n") + + def run_check(self): + env = dict(os.environ) + env["PATH"] = self.fakebin + os.pathsep + env["PATH"] + env["NVIDIA_DRM_GLOB"] = os.path.join(self.drm, "card*", "modalias") + env["NVIDIA_PCI_GLOB"] = os.path.join(self.pci, "*", "modalias") + return subprocess.run( + ["bash", self.wrapper, ARCHSETUP], + capture_output=True, text=True, env=env, + ) + + # ---------------------------------------------------------- normal ---- + def test_no_gpu_files_returns_zero_and_silent(self): + self.fake_pacman(version="575.51.02-1") + r = self.run_check() + self.assertEqual(r.returncode, 0) + self.assertNotIn("NVIDIA", r.stdout) + + def test_amd_only_returns_zero(self): + self.fake_pacman(version="575.51.02-1") + self.add_modalias(self.drm, "card0", AMD_MODALIAS) + r = self.run_check() + self.assertEqual(r.returncode, 0) + self.assertNotIn("NVIDIA", r.stdout) + + def test_nvidia_with_modern_driver_returns_ten_with_guidance(self): + self.fake_pacman(version="575.51.02-1") + self.add_modalias(self.drm, "card0", NVIDIA_MODALIAS) + r = self.run_check() + self.assertEqual(r.returncode, 10) + self.assertIn("NVIDIA GPU detected", r.stdout) + self.assertIn("LIBVA_DRIVER_NAME=nvidia", r.stdout) + self.assertIn("GBM_BACKEND=nvidia-drm", r.stdout) + self.assertIn("__GLX_VENDOR_LIBRARY_NAME=nvidia", r.stdout) + self.assertIn("575.51.02-1", r.stdout) + + # -------------------------------------------------------- boundary ---- + def test_lowercase_vendor_id_detected(self): + self.fake_pacman(version="575.51.02-1") + self.add_modalias(self.drm, "card0", NVIDIA_MODALIAS_LOWER) + r = self.run_check() + self.assertEqual(r.returncode, 10) + + def test_exactly_535_meets_requirement(self): + self.fake_pacman(version="535.216.01-1") + self.add_modalias(self.drm, "card0", NVIDIA_MODALIAS) + r = self.run_check() + self.assertEqual(r.returncode, 10) + + def test_pci_fallback_display_class_only(self): + # No DRM entries; PCI holds a display-class NVIDIA device -> detected. + self.fake_pacman(version="575.51.02-1") + self.add_modalias(self.pci, "0000:01:00.0", NVIDIA_MODALIAS) + r = self.run_check() + self.assertEqual(r.returncode, 10) + + def test_pci_non_display_nvidia_ignored(self): + # An NVIDIA audio/usb function (bc0C) must not trigger the check. + self.fake_pacman(version="575.51.02-1") + self.add_modalias(self.pci, "0000:01:00.1", NON_DISPLAY_NVIDIA) + r = self.run_check() + self.assertEqual(r.returncode, 0) + + # ----------------------------------------------------------- error ---- + def test_old_driver_returns_eleven_with_error(self): + self.fake_pacman(version="470.256.02-1") + self.add_modalias(self.drm, "card0", NVIDIA_MODALIAS) + r = self.run_check() + self.assertEqual(r.returncode, 11) + self.assertIn("535", r.stdout) + self.assertIn("470.256.02-1", r.stdout) + + def test_pacman_failure_returns_eleven_unknown(self): + self.fake_pacman(fail=True) + self.add_modalias(self.drm, "card0", NVIDIA_MODALIAS) + r = self.run_check() + self.assertEqual(r.returncode, 11) + self.assertIn("unknown", r.stdout) + + +if __name__ == "__main__": + unittest.main() @@ -883,14 +883,12 @@ Practical guidelines for working in public spaces :END: Ensure new tools integrate with the Hyprland environment and don't break workflow (the fleet is all Hyprland now; archsetup still supports DWM/X11 but no current machine uses it) -** TODO [#B] Add NVIDIA preflight check for Hyprland +** DONE [#B] Add NVIDIA preflight check for Hyprland +CLOSED: [2026-07-02 Thu] :PROPERTIES: :LAST_REVIEWED: 2026-05-21 :END: -Detect NVIDIA GPU and warn user about potential Wayland issues: -- Require driver version 535+ or abort -- Document required env vars (LIBVA_DRIVER_NAME, GBM_BACKEND, etc.) -- Prompt to continue or abort if NVIDIA detected +Shipped 2026-07-02 (speedrun), TDD. =nvidia_preflight_report= is a pure sed-extractable core (same harness pattern as zig-pin): modalias scan for vendor 10DE — DRM first, PCI display-class (bc03) fallback so an NVIDIA audio function can't false-trigger — then the repo's =nvidia-utils= candidate major checked against 535. Prints the Wayland guidance + env vars (LIBVA_DRIVER_NAME, GBM_BACKEND, __GLX_VENDOR_LIBRARY_NAME, ELECTRON_OZONE_PLATFORM_HINT) and the pre-Turing/AUR-legacy note. preflight_checks aborts on <535/unknown (rc 11), prompts continue/abort on a healthy NVIDIA box (rc 10), silent on non-NVIDIA (rc 0). 9 Normal/Boundary/Error tests over fake modalias trees + a fake pacman (=tests/nvidia-preflight/=, glob-discovered by test-unit — 10 suites green). ** DONE [#C] Wlogout exit-menu buttons are rectangular, not square CLOSED: [2026-07-02 Thu] |
