diff --git a/README.md b/README.md index 261ea04..605b08d 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/modules/user/nvidia.sh b/modules/user/nvidia.sh new file mode 100644 index 0000000..b9c3830 --- /dev/null +++ b/modules/user/nvidia.sh @@ -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: "||" 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." diff --git a/setup.sh b/setup.sh index 4da3e3c..fe41596 100755 --- a/setup.sh +++ b/setup.sh @@ -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}"