NVIDIA driver module: detect the GPU, install the right branch #16

Merged
yuri merged 1 commits from feat/nvidia-driver-module into main 2026-11-13 12:39:29 +01:00
3 changed files with 421 additions and 0 deletions
+37
View File
@@ -34,6 +34,8 @@ lib/common.sh colours, logging, dnf helpers, rpmfusion/terra, nerd f
lib/session.sh display-manager detection/switching, session entries
modules/de/{gnome,plasma,xfce}.sh
modules/twm/{niri,hyprland,mango,chadwm}.sh
modules/user/dotfiles.sh bare-repo dotfiles checkout
modules/user/nvidia.sh GPU-detected NVIDIA driver install
```
Modules are **separate processes**, not sourced into `setup.sh`. That means a
@@ -75,6 +77,41 @@ Each module pulls in `lib/` by relative path, so it works both ways.
`chadwm` is the only X11 entry in the menu. The other three are Wayland
compositors — do not expect the same config story from it.
## GPU drivers (NVIDIA)
`modules/user/nvidia.sh` runs on **detected hardware**, no question asked: the
card in the machine decides what is installed. Opt out with `--nvidia=no`
(e.g. to stay on nouveau).
Branch selection comes from the chip family in the name `lspci` prints, because
the numeric device-id ranges overlap between families and a wrong guess installs
a module the card cannot use:
| Chip family in the name | Driver branch | Packages |
|---|---|---|
| `GB`/`AD`/`GA`/`TU` (Turing and newer) | current | `akmod-nvidia` + `xorg-x11-drv-nvidia-cuda` |
| `GV`/`GP`/`GM` (Volta, Pascal, Maxwell) | 580xx | `akmod-nvidia-580xx` + `xorg-x11-drv-nvidia-580xx{,-cuda}` |
| `GK` (Kepler) | 470xx | `akmod-nvidia-470xx` + `xorg-x11-drv-nvidia-470xx{,-cuda}` |
| `GF` (Fermi) | 390xx | `akmod-nvidia-390xx` + `xorg-x11-drv-nvidia-390xx{,-cuda}` |
| pre-Fermi (`G92`, `GT200`, ...) | none | refused: 340xx is EOL and needs a longterm kernel |
If the name cannot be parsed, the module asks for the branch instead of guessing.
**Secure Boot.** With enforcement on, the akmods key is generated and imported,
and the module then *stops before installing the driver* — installing an
unsigned module while Secure Boot enforces is the documented way to get a black
screen, because the driver package blacklists nouveau as it lands. Enrol the key
in MokManager at the next boot, then run the setup again. With Secure Boot off
or in **Setup mode**, no signing happens at all: shim skips MokManager when
enforcement is off, so an import request would just sit in the efivars.
After the transaction the module waits (bounded, 5 min) for the akmod to build
so you cannot reboot into a driverless boot, runs `dnf mark user` on the akmod
(so `dnf autoremove` cannot take it), and offers VA-API decode + vulkan as an
opt-in extra. Nothing here sets `nvidia-drm.modeset=1`: RPM Fusion's kernel
package already enables KMS, and the flag now conflicts with Fedora's simpledrm
early-boot patch.
## Display manager policy
* A **DE module** leaves the DE's own greeter alone (GNOME→gdm, Plasma→sddm,
+354
View File
@@ -0,0 +1,354 @@
#!/bin/bash
#
# modules/user/nvidia.sh — install the NVIDIA driver branch that matches the
# GPU that is actually in the machine.
#
# Runs on its own:
# bash modules/user/nvidia.sh
# LSPCI_BIN=/tmp/fake-lspci bash modules/user/nvidia.sh
#
# Behaviour:
# * no NVIDIA GPU -> prints one line and exits 0
# * GPU found, SB off/setup -> installs, no signing, no MOK enrolment
# * GPU found, SB enabled -> generates + imports the akmods key, then STOPS
# and asks for a reboot. The driver is installed
# on the next run, once the key is enrolled.
# (Installing before enrolment is the documented
# way to get a black screen: RPM Fusion blacklists
# nouveau the moment the driver package lands.)
# * already installed -> skips the transaction, keeps `dnf mark user`
#
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../../lib/common.sh
source "$HERE/../../lib/common.sh"
# shellcheck source=../../lib/ui.sh
source "$HERE/../../lib/ui.sh"
if is_debug; then set +e; else set -e; fi
# Overridable so the detection logic can be tested on a host with no NVIDIA
# hardware (same pattern as OS_RELEASE_FILE / FSTAB_FILE in lib/common.sh).
LSPCI_BIN="${LSPCI_BIN:-lspci}"
MOKUTIL_BIN="${MOKUTIL_BIN:-mokutil}"
EFIVARS_DIR="${EFIVARS_DIR:-/sys/firmware/efi/efivars}"
AKMODS_CERT="${AKMODS_CERT:-/etc/pki/akmods/certs/public_key.der}"
AKMOD_COMPILE_TIMEOUT="${AKMOD_COMPILE_TIMEOUT:-300}"
step "NVIDIA driver"
rule
#--------------------------------------------------
# Detection
#--------------------------------------------------
# Prints: "<branch>|<model>|<pciid>" or nothing at all when there is no card.
#
# Branch is derived from the chip family in the name lspci prints (hwdata always
# has it): GB/AD/GA/TU -> current, GV/GP/GM -> 580xx, GK -> 470xx, GF -> 390xx.
# The numeric device id is NOT used for the decision: the id ranges overlap
# between families, and a wrong guess installs a module the card cannot use.
detect_nvidia_gpu() {
local line model arch id
line=$("$LSPCI_BIN" -nn 2>/dev/null \
| grep -iE '(VGA compatible|3D|Display) controller' \
| grep -iE 'nvidia|\[10de:' \
| head -n1) || true
[[ -z "$line" ]] && return 1
id=$(printf '%s' "$line" \
| grep -oE '\[10de:[0-9a-fA-F]{4}\]' | head -n1 | tr -d '[]') || true
# "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA102
# [GeForce RTX 3080 Ti] [10de:2208] (rev a1)" -> "GA102 [GeForce RTX 3080 Ti]"
model=$(printf '%s' "$line" \
| sed -n 's/.*NVIDIA Corporation \(.*\)\[10de:[0-9a-fA-F]\{4\}\].*/\1/p' \
| sed -e 's/[[:space:]]*$//') || true
[[ -z "$model" ]] && model="unknown NVIDIA device"
arch=$(printf '%s' "$model" | grep -oE '(GB|AD|GA|TU|GV|GP|GM|GK|GF)[0-9]{3}[A-Z]?' \
| head -n1 | grep -oE '^(GB|AD|GA|TU|GV|GP|GM|GK|GF)') || true
local branch="unknown"
case "$arch" in
GB|AD|GA|TU) branch="current" ;; # Turing and newer: open kernel module
GV|GP|GM) branch="580xx" ;; # Volta / Pascal / Maxwell (last branch)
GK) branch="470xx" ;; # Kepler, Xorg only
GF) branch="390xx" ;; # Fermi, Xorg only, EOL upstream
"")
# Pre-Fermi naming (G80/G92/GT200/GT215...) has no architecture
# prefix; those cards need the EOL 340xx branch plus a longterm
# kernel, which this script deliberately does not automate.
if printf '%s' "$model" | grep -qE '(^|[^A-Za-z])(G[0-9]{2,3}|GT[0-9]{3}|NV[0-9]{2})'; then
branch="340xx"
else
branch="unknown"
fi
;;
esac
printf '%s|%s|%s\n' "$branch" "$model" "${id:-10de:unknown}"
}
gpu_info=""
if ! command -v "$LSPCI_BIN" >/dev/null 2>&1; then
info "pciutils missing (no lspci). Installing..."
install_optional pciutils
fi
if command -v "$LSPCI_BIN" >/dev/null 2>&1; then
gpu_info=$(detect_nvidia_gpu) || true
fi
if [[ -z "$gpu_info" ]]; then
info "No NVIDIA GPU detected. Nothing to do."
exit 0
fi
BRANCH="${gpu_info%%|*}"
rest="${gpu_info#*|}"
MODEL="${rest%%|*}"
PCIID="${rest#*|}"
ok "GPU: $MODEL ($PCIID)"
# Multiple cards of different generations are not something one dnf transaction
# can serve correctly: the packages are mutually exclusive.
nv_count=$("$LSPCI_BIN" -nn 2>/dev/null \
| grep -icE '(VGA compatible|3D|Display) controller.*(nvidia|\[10de:)') || true
if [[ "${nv_count:-0}" -gt 1 ]]; then
warn "Found $nv_count NVIDIA display devices. Only the first one determines"
warn "the driver branch; mixed generations on one machine need manual work."
fi
# A 3D controller (rather than a VGA/Display controller) is the Optimus layout.
if printf '%s' "$("$LSPCI_BIN" -nn 2>/dev/null | grep -i '10de:' | head -n1)" \
| grep -q '3D controller'; then
info "Hybrid (Optimus) layout: Fedora handles the iGPU/dGPU split automatically."
fi
#--------------------------------------------------
# Branch -> packages
#--------------------------------------------------
if [[ "$BRANCH" == "unknown" ]]; then
warn "Could not identify the GPU generation from '$MODEL'."
choice=$(ui_choose "NVIDIA driver branch" "Which driver branch does this card need?" \
"current (Turing / Ampere / Ada / Blackwell)" \
"current (Turing / Ampere / Ada / Blackwell)" \
"580xx (Maxwell / Pascal / Volta)" \
"470xx (Kepler)" \
"390xx (Fermi)")
case "$choice" in
580xx*) BRANCH="580xx" ;;
470xx*) BRANCH="470xx" ;;
390xx*) BRANCH="390xx" ;;
*) BRANCH="current" ;;
esac
fi
case "$BRANCH" in
current)
PKGS=(akmod-nvidia xorg-x11-drv-nvidia-cuda)
MARK="akmod-nvidia"
BRANCH_LABEL="current (Turing/Ampere/Ada/Blackwell, open kernel module)"
;;
580xx)
PKGS=(akmod-nvidia-580xx xorg-x11-drv-nvidia-580xx xorg-x11-drv-nvidia-580xx-cuda)
MARK="akmod-nvidia-580xx"
BRANCH_LABEL="580xx (Maxwell/Pascal/Volta, last branch for those cards)"
;;
470xx)
PKGS=(akmod-nvidia-470xx xorg-x11-drv-nvidia-470xx xorg-x11-drv-nvidia-470xx-cuda)
MARK="akmod-nvidia-470xx"
BRANCH_LABEL="470xx (Kepler, Xorg only)"
;;
390xx)
PKGS=(akmod-nvidia-390xx xorg-x11-drv-nvidia-390xx xorg-x11-drv-nvidia-390xx-cuda)
MARK="akmod-nvidia-390xx"
BRANCH_LABEL="390xx (Fermi, EOL upstream, Xorg only)"
;;
340xx)
warn "This card predates Fermi. Driver 340xx is end-of-life and needs a"
warn "longterm kernel from a COPR, which this script does not automate."
warn "See https://rpmfusion.org/Howto/NVIDIA and install it by hand."
exit 0
;;
esac
info "Driver branch: $BRANCH_LABEL"
if [[ "$BRANCH" != "current" ]]; then
warn "This branch has no Wayland support. Plan on an X11 session."
fi
#--------------------------------------------------
# Secure Boot gate
#--------------------------------------------------
# enabled -> sign and enrol, then stop for a reboot
# disabled / setup mode -> no signing at all (an import request would never be
# processed: shim skips MokManager when enforcement is
# off, so the request just sits in the efivars)
sb_state() {
local out sb_var pk_var
if command -v "$MOKUTIL_BIN" >/dev/null 2>&1; then
out=$("$MOKUTIL_BIN" --sb-state 2>/dev/null) || true
case "$out" in
*"SecureBoot enabled"*) echo "enabled"; return 0 ;;
*"Setup mode"*) echo "setup"; return 0 ;;
*"SecureBoot disabled"*) echo "disabled"; return 0 ;;
esac
fi
# Fallback: read the efivars directly (byte 5 of the variable data).
if [[ -d "$EFIVARS_DIR" ]]; then
sb_var=$(ls "$EFIVARS_DIR"/SecureBoot-* 2>/dev/null | head -n1) || true
pk_var=$(ls "$EFIVARS_DIR"/PK-* 2>/dev/null | head -n1) || true
if [[ -n "$sb_var" ]]; then
if [[ "$(od -An -t u1 -j 4 -N 1 "$sb_var" 2>/dev/null | tr -d '[:space:]')" == "1" ]]; then
echo "enabled"; return 0
fi
# No Platform Key enrolled = Setup mode: nothing can be enforced.
[[ -z "$pk_var" ]] && { echo "setup"; return 0; }
echo "disabled"; return 0
fi
fi
echo "unknown"
}
SB_STATE=$(sb_state)
case "$SB_STATE" in
enabled)
info "Secure Boot is enabled: the module has to be signed and the key enrolled."
install_packages akmods
if [[ ! -f "$AKMODS_CERT" ]]; then
info "Generating the akmods signing key..."
sudo kmodgenca -a
else
ok "akmods signing key present."
fi
if sudo "$MOKUTIL_BIN" --test-key "$AKMODS_CERT" 2>&1 | grep -qi 'not enrolled'; then
warn "The akmods key is not enrolled yet. Importing it now."
sudo "$MOKUTIL_BIN" --import "$AKMODS_CERT"
printf "\n"
color_yellow "------------------------------------------------------------"
color_yellow " ACTION REQUIRED: enrolment is pending"
color_yellow "------------------------------------------------------------"
info "The driver is deliberately NOT installed yet: with Secure Boot"
info "enforcing, an unsigned module leaves you with a black screen."
printf "\n"
info "1. Reboot now."
info "2. At the boot screen press a key during the countdown."
info "3. Enroll MOK -> View key 0 (check the fingerprint) -> Continue"
info " -> Yes -> type the password you just set. The keyboard there"
info " is QWERTY, so use a short, symbol-free password."
info "4. Boot Fedora and run this setup again: it will install the"
info " driver now that the key is trusted."
printf "\n"
warn "If you miss the countdown the request is dropped silently and"
warn "you have to import again."
exit 0
fi
ok "akmods key already enrolled."
;;
setup)
ok "Secure Boot is off (platform is in Setup mode): no signing needed."
warn "Setup mode is a temporary state: a firmware update or 'Restore"
warn "Factory Keys' turns enforcement on, and this unsigned module then"
warn "stops loading at the next boot. Enrol the key before you need it:"
warn " sudo dnf install akmods && sudo kmodgenca -a"
warn " sudo mokutil --import /etc/pki/akmods/certs/public_key.der"
;;
disabled)
ok "Secure Boot is disabled: no signing needed."
warn "Keep it that way deliberately: a firmware update or 'Restore Factory"
warn "Keys' can enable enforcement, and the unsigned module then fails to"
warn "load at the next boot."
;;
*)
warn "Could not determine the Secure Boot state. Assuming it is off."
warn "If Secure Boot actually is on, the driver will not load: check"
warn "'mokutil --sb-state' and enrol the akmods key first."
;;
esac
#--------------------------------------------------
# Install
#--------------------------------------------------
if pkg_installed_any "${PKGS[@]}"; then
ok "NVIDIA driver packages already installed."
else
info "Installing: ${PKGS[*]}"
if ! install_packages "${PKGS[@]}"; then
err "Driver installation failed for branch $BRANCH."
err "Check that RPM Fusion nonfree is enabled: dnf repolist | grep rpmfusion"
exit 1
fi
fi
# An akmod is removable as an "unused dependency" unless it is marked user
# installed; `dnf autoremove` has taken the driver (and its compiler) away on
# people before.
if ! sudo dnf mark user "$MARK" >/dev/null 2>&1; then
warn "Could not mark $MARK as user-installed (dnf autoremove may remove it)."
fi
#--------------------------------------------------
# Wait for the module build
#--------------------------------------------------
# dnf returns before the kmod exists. Rebooting in that window gives a boot with
# no driver at all, because nouveau is already blacklisted by the driver package.
NEWEST_KERNEL=$(rpm -q --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' kernel-core 2>/dev/null \
| sort -V | tail -n1) || true
info "Waiting for the akmod build (up to ${AKMOD_COMPILE_TIMEOUT}s)..."
built=false
deadline=$(( SECONDS + AKMOD_COMPILE_TIMEOUT ))
while (( SECONDS < deadline )); do
if [[ -n "$NEWEST_KERNEL" ]] && modinfo -k "$NEWEST_KERNEL" -F version nvidia >/dev/null 2>&1; then
built=true; break
fi
if [[ -z "$NEWEST_KERNEL" ]] && modinfo -F version nvidia >/dev/null 2>&1; then
built=true; break
fi
sleep 5
done
if [[ "$built" == "true" ]]; then
if [[ -n "$NEWEST_KERNEL" ]]; then
VER=$(modinfo -k "$NEWEST_KERNEL" -F version nvidia 2>/dev/null) || true
else
VER=$(modinfo -F version nvidia 2>/dev/null) || true
fi
ok "Kernel module built: nvidia ${VER:-unknown}"
if [[ -n "$NEWEST_KERNEL" ]] && ! [[ "$NEWEST_KERNEL" == "$(uname -r)" ]]; then
warn "Built for $NEWEST_KERNEL but you are running $(uname -r): reboot."
fi
else
warn "The module is not built yet after ${AKMOD_COMPILE_TIMEOUT}s."
warn "Do NOT reboot until it is: the next boot would have no driver."
warn "Check the build log, then retry:"
warn " ls -l /var/cache/akmods/nvidia/"
warn " sudo akmods --force"
fi
#--------------------------------------------------
# Optional extras
#--------------------------------------------------
printf "\n"
extras=$(ui_confirm "Install the optional graphics extras (VA-API decode, vulkan)?" "no")
if [[ "$extras" == "yes" ]]; then
install_optional libva-nvidia-driver libva-utils vulkan
fi
#--------------------------------------------------
# What to check after the reboot
#--------------------------------------------------
printf "\n"
info "After the reboot, verify with:"
info " nvidia-smi"
info " cat /sys/module/nvidia_drm/parameters/modeset # expect Y"
info " lsmod | grep -Ei 'nouveau|nova_core' # expect nothing"
warn "A reboot is required before the driver is active."
+30
View File
@@ -32,6 +32,7 @@ TWM=""
SELINUX_OPT=""
SNAPPER_OPT=""
DOTFILES_OPT=""
NVIDIA_OPT=""
usage() {
cat <<'EOF'
@@ -48,6 +49,8 @@ Options:
--selinux=X keep | disabled (asked, default keep)
--snapper=X yes | no (automatic on btrfs; use 'no' to opt out)
--dotfiles=X yes | no (clone personal dotfiles, default no)
--nvidia=X auto | no (auto: install the matching NVIDIA driver
when an NVIDIA GPU is detected; default auto)
-y, --yes never prompt (requires --de and --twm)
-d, --debug pause after each section instead of aborting on error
-h, --help show this help
@@ -72,6 +75,7 @@ for arg in "$@"; do
--selinux=*) SELINUX_OPT="${arg#*=}" ;;
--snapper=*) SNAPPER_OPT="${arg#*=}" ;;
--dotfiles=*) DOTFILES_OPT="${arg#*=}" ;;
--nvidia=*) NVIDIA_OPT="${arg#*=}" ;;
--twm=*) TWM="${arg#*=}" ;;
-h|--help) usage; exit 0 ;;
*)
@@ -258,6 +262,15 @@ else
*) err "Unknown --dotfiles value: '$DOTFILES_OPT' (yes|no)"; exit 2 ;;
esac
fi
# GPU drivers are driven by DETECTED hardware, not by a prompt: an NVIDIA card
# in the machine means install the branch that fits it. 'yes' is accepted as an
# alias of 'auto' so muscle memory does not hit an error.
case "${NVIDIA_OPT:-}" in
auto|yes|"") NVIDIA_OPT="auto" ;;
no) NVIDIA_OPT="no" ;;
*) err "Unknown --nvidia value: '$NVIDIA_OPT' (auto|no)"; exit 2 ;;
esac
pause_if_debug
#------------------------------------------
@@ -365,6 +378,14 @@ if [[ "$TWM" != "none" ]]; then
pause_if_debug
fi
# Hardware drivers. The module re-detects the card and exits quietly when there
# is none, so running it unconditionally is safe; --nvidia=no is the opt-out
# (nouveau or a hand-built driver).
if [[ "$NVIDIA_OPT" != "no" ]]; then
run_module "$SCRIPT_DIR/modules/user/nvidia.sh"
pause_if_debug
fi
#------------------------------------------
# 6 Done
#------------------------------------------
@@ -814,6 +835,15 @@ ok "TWM: $TWM"
ok "SELinux: $([[ "$SELINUX_DISABLED" == "true" ]] && echo disabled || echo keeping)"
ok "Btrfs snapshots: $([[ "$BTRFS_SNAPPER" == "true" ]] && echo yes || echo skipped)"
ok "Dotfiles: $([[ "$DOTFILES" == "true" ]] && echo clone || echo no)"
NV_VER=$(modinfo -F version nvidia 2>/dev/null) || true
if [[ -n "${NV_VER:-}" ]]; then
ok "NVIDIA: module nvidia ${NV_VER} built (reboot if it was installed in this run)"
elif pkg_installed_any akmod-nvidia akmod-nvidia-580xx akmod-nvidia-470xx akmod-nvidia-390xx; then
ok "NVIDIA: driver packages installed; the kmod builds at the next boot"
else
ok "NVIDIA: none (no supported card, --nvidia=no, or no card present)"
fi
DM=$(detect_display_manager)
ok "Display manager: ${DM:-none}"