aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-02 17:14:22 -0400
committerCraig Jennings <c@cjennings.net>2026-07-02 17:14:22 -0400
commit72c5fdf1f05a4232c6afda1371d00d2b89b65cd7 (patch)
treee6ce97bb27932870d0b3da514f790556aa159b71
parent24a2098dd2adf7bcfe4272c66cf105cb0234703f (diff)
downloadarchsetup-72c5fdf1f05a4232c6afda1371d00d2b89b65cd7.tar.gz
archsetup-72c5fdf1f05a4232c6afda1371d00d2b89b65cd7.zip
feat(preflight): add NVIDIA/Wayland check with driver-version gate
Installing Hyprland on an NVIDIA box silently produced a rough Wayland session. nvidia_preflight_report detects the card via modalias (DRM, then PCI display-class only), prints the required env vars and the pre-Turing AUR caveat, and checks the repo's nvidia-utils major against 535. preflight_checks aborts when the requirement can't be met and asks before continuing on a detected card. 9 unit tests over fake modalias trees and a fake pacman.
-rwxr-xr-xarchsetup79
-rw-r--r--tests/nvidia-preflight/test_nvidia_preflight.py162
-rw-r--r--todo.org8
3 files changed, 242 insertions, 7 deletions
diff --git a/archsetup b/archsetup
index b4b6f69..6b80bca 100755
--- a/archsetup
+++ b/archsetup
@@ -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()
diff --git a/todo.org b/todo.org
index 644ce56..1776486 100644
--- a/todo.org
+++ b/todo.org
@@ -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]