2025-11-30 15:31:19 +01:00
#!/bin/bash
2026-05-09 18:59:59 +02:00
# ==========================================================
# ProxMenux - NVIDIA GPU Driver Installer
# ==========================================================
2025-11-30 15:31:19 +01:00
# Author : MacRimi
2026-03-26 18:18:26 +01:00
# Copyright : (c) 2024 MacRimi
2026-05-09 18:59:59 +02:00
# License : GPL-3.0
2026-08-26 17:23:09 +02:00
# Version : 1.3
# Last Updated: 26/08/2026
2026-05-09 18:59:59 +02:00
# ==========================================================
# Description:
# Installs and manages the NVIDIA proprietary driver on a
2026-08-26 17:23:09 +02:00
# Proxmox VE host. Detects hardware, filters NVIDIA branches by
# the installed GPU PCI IDs and handles the full lifecycle
2026-05-09 18:59:59 +02:00
# (install / update / remove).
#
# Features:
# - GPU detection + VFIO passthrough safety check
2026-08-26 17:23:09 +02:00
# - GPU PCI-ID-aware branch filtering from NVIDIA supportedchips
2026-05-09 18:59:59 +02:00
# - Nouveau blacklist + module unload
# - DKMS-backed install (survives kernel upgrades)
# - udev rules + nvidia-persistenced service
# - Optional keylase/nvidia-patch (NVENC session limit)
# - LXC container driver propagation (Alpine/Arch/Debian)
# - Complete uninstall path
# ==========================================================
2025-11-30 15:31:19 +01:00
2025-11-30 15:41:40 +01:00
SCRIPT_TITLE = "NVIDIA GPU Driver Installer for Proxmox VE"
2025-11-30 15:31:19 +01:00
LOCAL_SCRIPTS = "/usr/local/share/proxmenux/scripts"
BASE_DIR = "/usr/local/share/proxmenux"
UTILS_FILE = " $BASE_DIR /utils.sh"
COMPONENTS_STATUS_FILE = " $BASE_DIR /components_status.json"
LOG_FILE = "/tmp/nvidia_install.log"
screen_capture = "/tmp/proxmenux_nvidia_screen_capture_ $$ .txt"
NVIDIA_BASE_URL = "https://download.nvidia.com/XFree86/Linux-x86_64"
NVIDIA_WORKDIR = "/opt/nvidia"
2026-08-26 17:23:09 +02:00
NVIDIA_NOUVEAU_BLACKLIST = "/etc/modprobe.d/proxmenux-nouveau-blacklist.conf"
NVIDIA_NOUVEAU_STATE = " ${ BASE_DIR } /nvidia-nouveau-blacklist.state"
NVIDIA_NOUVEAU_LEGACY_BLACKLIST = "/etc/modprobe.d/nouveau-blacklist.conf"
NVIDIA_GLOBAL_BLACKLIST = "/etc/modprobe.d/blacklist.conf"
2025-11-30 15:31:19 +01:00
2026-04-17 19:02:46 +02:00
# LXC post-install update constants (used only when NVIDIA LXC passthrough
# containers are detected and the user confirms updating them after the host
# install/reinstall finishes).
NVIDIA_INSTALL_MIN_MB = 2048
CT_ORIG_MEM = ""
2025-11-30 15:31:19 +01:00
export BASE_DIR
export COMPONENTS_STATUS_FILE
if [[ -f " $UTILS_FILE " ]] ; then
source " $UTILS_FILE "
fi
2026-08-17 12:05:40 +02:00
if [[ -f " $LOCAL_SCRIPTS /global/pci_passthrough_helpers.sh" ]] ; then
source " $LOCAL_SCRIPTS /global/pci_passthrough_helpers.sh"
elif [[ -f " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " /.. && pwd ) /global/pci_passthrough_helpers.sh" ]] ; then
source " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " /.. && pwd ) /global/pci_passthrough_helpers.sh"
fi
2025-11-30 15:31:19 +01:00
if [[ ! -f " $COMPONENTS_STATUS_FILE " ]] ; then
echo "{}" > " $COMPONENTS_STATUS_FILE "
fi
load_language
initialize_cache
# ==========================================================
# GPU detection and current status
# ==========================================================
2026-08-15 17:33:05 +02:00
# Populated by detect_nvidia_gpus. Holds every video-controller PCI
# Device ID (lowercase, 4-hex) so the version filter can drop branches
# whose supportedchips.html doesn't list every card on this host.
NVIDIA_HOST_GPU_IDS =()
2025-11-30 15:31:19 +01:00
detect_nvidia_gpus() {
2026-08-15 17:33:05 +02:00
# Video controllers only — the paired HDA audio functions (10de:xxxx
# under class 0403) are not what the display driver ships support for.
2025-11-30 15:31:19 +01:00
local lspci_output
2026-08-15 17:33:05 +02:00
lspci_output = $( lspci -nn | grep -i "NVIDIA" \
2025-11-30 15:31:19 +01:00
| grep -Ei "VGA compatible controller|3D controller|Display controller" || true )
if [[ -z " $lspci_output " ]] ; then
NVIDIA_GPU_PRESENT = false
DETECTED_GPUS_TEXT = " $( translate 'No NVIDIA GPU detected on this system.' ) "
2026-08-15 17:33:05 +02:00
NVIDIA_HOST_GPU_IDS =()
2025-11-30 15:31:19 +01:00
else
NVIDIA_GPU_PRESENT = true
DETECTED_GPUS_TEXT = ""
2026-08-15 17:33:05 +02:00
NVIDIA_HOST_GPU_IDS =()
2025-11-30 15:31:19 +01:00
local i = 1
while IFS = read -r line; do
DETECTED_GPUS_TEXT += " ${ i } . ${ line } \n"
2026-08-15 17:33:05 +02:00
# Extract [10de:XXXX] — Vendor:Device pair. We keep only the
# Device half (4-hex) lowercased, which is what NVIDIA lists in
# each version's README/supportedchips.html.
local dev_id
dev_id = $( echo " $line " | grep -oiE '\[10de:[0-9a-f]{4}\]' | head -1 \
| sed -E 's/^\[10de:([0-9a-f]{4})\]$/\1/i' | tr 'A-F' 'a-f' )
if [[ -n " $dev_id " ]] ; then
NVIDIA_HOST_GPU_IDS +=( " $dev_id " )
fi
2025-11-30 15:31:19 +01:00
(( i++))
done <<< " $lspci_output "
fi
}
2026-04-12 20:32:34 +02:00
check_gpu_not_in_vm_passthrough() {
local dev vendor driver vfio_list = ""
for dev in /sys/bus/pci/devices/*; do
vendor = $( cat " $dev /vendor" 2>/dev/null)
[[ " $vendor " != "0x10de" ]] && continue
if [[ -L " $dev /driver" ]] ; then
driver = $( basename " $( readlink " $dev /driver" ) " )
if [[ " $driver " == "vfio-pci" ]] ; then
vfio_list += " • $( basename " $dev " ) \n"
fi
fi
done
[[ -z " $vfio_list " ]] && return 0
local msg
msg = "\n $( translate "One or more NVIDIA GPUs are currently configured for VM passthrough (vfio-pci):" ) \n\n"
msg += " ${ vfio_list } \n"
msg += " $( translate "Installing host drivers while the GPU is assigned to a VM could break passthrough and destabilize the system." ) \n\n"
msg += " $( translate "To install host drivers, first remove the GPU from VM passthrough configuration and reboot." ) "
dialog --backtitle "ProxMenux" \
--title " $( translate "GPU in VM Passthrough Mode" ) " \
--msgbox " $msg " 16 78
exit 0
}
2026-08-17 12:05:40 +02:00
check_stale_vfio_config_for_nvidia() {
local vfio_conf = "/etc/modprobe.d/vfio.conf"
[[ ! -f " $vfio_conf " ]] && return 0
local ids_line ids_part
ids_line = $( grep "^options vfio-pci ids=" " $vfio_conf " 2>/dev/null | head -1)
[[ -z " $ids_line " ]] && return 0
ids_part = $( echo " $ids_line " | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//' )
[[ -z " $ids_part " ]] && return 0
local dev vendor did vid_did
local -a legacy_ids =()
local legacy_list = ""
for dev in /sys/bus/pci/devices/*; do
vendor = $( cat " $dev /vendor" 2>/dev/null)
[[ " $vendor " != "0x10de" ]] && continue
did = $( cat " $dev /device" 2>/dev/null)
[[ -z " $did " ]] && continue
vid_did = "10de: ${ did #0x } "
if echo ", ${ ids_part } ," | grep -q ", ${ vid_did } ," ; then
legacy_ids +=( " $vid_did " )
legacy_list += " • $( basename " $dev " ) [ ${ vid_did } ]\n"
fi
done
[[ ${# legacy_ids [@] } -eq 0 ]] && return 0
local msg
msg = "\n $( translate 'A previous VFIO passthrough configuration was detected for the following NVIDIA GPU(s):' ) \n\n"
msg += " ${ legacy_list } \n"
msg += " $( translate 'The active kernel driver is not vfio-pci, but the entry in' ) /etc/modprobe.d/vfio.conf $( translate 'will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.' ) \n\n"
msg += "\Z1\Zb $( translate 'Do you want to remove the stale entry from vfio.conf and continue?' ) \Zn"
dialog --colors --backtitle "ProxMenux" \
--title " $( translate 'Stale VFIO Config Detected' ) " \
--yesno " $msg " 18 78 || exit 0
if declare -F _clean_vfio_conf_ids >/dev/null 2>& 1 \
&& _clean_vfio_conf_ids " ${ legacy_ids [@] } " ; then
msg_info " $( translate 'Rebuilding initramfs after vfio.conf cleanup...' ) "
update-initramfs -u >/dev/null 2>& 1 || true
msg_ok " $( translate 'Stale VFIO entries removed and initramfs rebuilt.' ) " | tee -a " $screen_capture "
else
msg_ok " $( translate 'No changes were needed in vfio.conf.' ) " | tee -a " $screen_capture "
fi
}
2025-11-30 15:31:19 +01:00
detect_driver_status() {
CURRENT_DRIVER_INSTALLED = false
CURRENT_DRIVER_VERSION = ""
# First check if nvidia kernel module is actually loaded
2026-04-19 02:01:04 +02:00
if grep -q "^nvidia " /proc/modules 2>/dev/null; then
2025-11-30 15:31:19 +01:00
modprobe nvidia-uvm 2>/dev/null || true
sleep 1
if command -v nvidia-smi >/dev/null 2>& 1; then
CURRENT_DRIVER_VERSION = $( nvidia-smi --query-gpu= driver_version --format= csv,noheader 2>/dev/null | head -n1)
if [[ -n " $CURRENT_DRIVER_VERSION " ]] ; then
CURRENT_DRIVER_INSTALLED = true
# Register the installed driver version in components_status.json
update_component_status "nvidia_driver" "installed" " $CURRENT_DRIVER_VERSION " "gpu" '{"patched":false}'
fi
fi
fi
if $CURRENT_DRIVER_INSTALLED ; then
CURRENT_STATUS_TEXT = " $( printf '%s %s' " $( translate 'NVIDIA driver installed:' ) " " $CURRENT_DRIVER_VERSION " ) "
else
CURRENT_STATUS_TEXT = " $( translate 'No NVIDIA driver installed.' ) "
fi
if $CURRENT_DRIVER_INSTALLED ; then
2025-11-30 18:29:05 +01:00
CURRENT_STATUS_COLORED = " ${ CURRENT_STATUS_TEXT } "
2025-11-30 15:31:19 +01:00
else
2025-11-30 18:29:05 +01:00
CURRENT_STATUS_COLORED = " ${ CURRENT_STATUS_TEXT } "
2025-11-30 15:31:19 +01:00
fi
}
2026-04-17 19:02:46 +02:00
# ==========================================================
# LXC NVIDIA passthrough — discovery & userspace-libs update
# Invoked after the host install/reinstall completes. Aligned with the install
# path used in add_gpu_lxc.sh (distro-aware, memory/disk checks, --no-dkms,
# --no-install-compat32-libs, visible progress via tee).
# ==========================================================
find_nvidia_containers() {
NVIDIA_CONTAINERS =()
for conf in /etc/pve/lxc/*.conf; do
[[ -f " $conf " ]] || continue
if grep -qiE "dev[0-9]+:.*nvidia" " $conf " ; then
NVIDIA_CONTAINERS +=( " $( basename " $conf " .conf) " )
fi
done
}
get_lxc_nvidia_version() {
local ctid = " $1 "
local version = ""
# Prefer nvidia-smi when the container is running (works with .run-installed drivers)
if pct status " $ctid " 2>/dev/null | grep -q "running" ; then
version = $( pct exec " $ctid " -- nvidia-smi \
--query-gpu= driver_version --format= csv,noheader 2>/dev/null \
| head -1 | tr -d '[:space:]' || true )
fi
# Fallback: dpkg status for apt-installed libcuda1 (dir-type storage, no start needed)
if [[ -z " $version " ]] ; then
local rootfs = "/var/lib/lxc/ ${ ctid } /rootfs"
if [[ -f " ${ rootfs } /var/lib/dpkg/status" ]] ; then
version = $( grep -A5 "^Package: libcuda1 $" " ${ rootfs } /var/lib/dpkg/status" \
| grep "^Version:" | head -1 | awk '{print $2}' | cut -d- -f1)
fi
fi
echo " ${ version :-$( translate 'not installed' ) } "
}
_detect_container_distro() {
local distro
distro = $( pct exec " $1 " -- grep "^ID=" /etc/os-release 2>/dev/null \
| cut -d= -f2 | tr -d '[:space:]"' )
echo " ${ distro :- unknown } "
}
_ensure_container_memory() {
local ctid = " $1 "
local cur_mem
cur_mem = $( pct config " $ctid " 2>/dev/null | awk '/^memory:/{print $2}' )
[[ -z " $cur_mem " ]] && cur_mem = 512
if [[ " $cur_mem " -lt " $NVIDIA_INSTALL_MIN_MB " ]] ; then
if whiptail --title " $( translate 'Low Container Memory' ) " --yesno \
" $( translate 'Container' ) ${ ctid } $( translate 'has' ) ${ cur_mem } MB RAM.\n\n $( translate 'The NVIDIA installer needs at least' ) ${ NVIDIA_INSTALL_MIN_MB } MB $( translate 'to run without being killed by the OOM killer.' ) \n\n $( translate 'Increase container RAM temporarily to' ) ${ NVIDIA_INSTALL_MIN_MB } MB?" \
13 72; then
CT_ORIG_MEM = " $cur_mem "
pct set " $ctid " -memory " $NVIDIA_INSTALL_MIN_MB " >>" $LOG_FILE " 2>& 1 || true
else
msg_warn " $( translate 'Insufficient memory. Skipping LXC' ) ${ ctid } ."
return 1
fi
fi
return 0
}
_restore_container_memory() {
local ctid = " $1 "
if [[ -n " $CT_ORIG_MEM " ]] ; then
msg_info " $( translate 'Restoring container memory to' ) ${ CT_ORIG_MEM } MB..."
pct set " $ctid " -memory " $CT_ORIG_MEM " >>" $LOG_FILE " 2>& 1 || true
msg_ok " $( translate 'Memory restored.' ) "
CT_ORIG_MEM = ""
fi
}
_start_container_and_wait() {
local ctid = " $1 "
msg_info " $( translate 'Starting container' ) ${ ctid } ..."
pct start " $ctid " >>" $LOG_FILE " 2>& 1 || true
local ready = false
for _ in { 1..15} ; do
sleep 2
if pct exec " $ctid " -- true >/dev/null 2>& 1; then
ready = true
break
fi
done
if ! $ready ; then
msg_warn " $( translate 'Container' ) ${ ctid } $( translate 'did not become ready. Skipping.' ) "
return 1
fi
msg_ok " $( translate 'Container' ) ${ ctid } $( translate 'started.' ) " | tee -a " $screen_capture "
return 0
}
update_lxc_nvidia() {
local ctid = " $1 "
local version = " $2 "
local started_here = false
local old_version
old_version = $( get_lxc_nvidia_version " $ctid " )
msg_info2 " $( translate 'Container' ) ${ ctid } : $( translate 'updating NVIDIA userspace libs' ) ( ${ old_version } → ${ version } )"
if ! pct status " $ctid " 2>/dev/null | grep -q "running" ; then
started_here = true
_start_container_and_wait " $ctid " || return 1
fi
msg_info " $( translate 'Detecting container OS...' ) "
local distro
distro = $( _detect_container_distro " $ctid " )
msg_ok " $( translate 'Container OS:' ) ${ distro } " | tee -a " $screen_capture "
local install_rc = 0
case " $distro " in
arch| manjaro| endeavouros)
msg_info2 " $( translate 'Upgrading NVIDIA utils (Arch)...' ) "
pct exec " $ctid " -- bash -c \
"pacman -Syu --noconfirm nvidia-utils" \
2>& 1 | tee -a " $LOG_FILE "
install_rc = ${ PIPESTATUS [0] }
;;
*)
local run_file = " ${ NVIDIA_WORKDIR } /NVIDIA-Linux-x86_64- ${ version } .run"
if [[ ! -f " $run_file " ]] ; then
msg_warn " $( translate 'Installer not found:' ) ${ run_file } . $( translate 'Skipping LXC' ) ${ ctid } ."
install_rc = 1
elif ! _ensure_container_memory " $ctid " ; then
install_rc = 1
else
local free_mb
2026-05-09 18:59:59 +02:00
free_mb = $( pct exec " $ctid " -- df -P -m / 2>/dev/null | awk 'END{print $4}' )
free_mb = ${ free_mb :- 0 }
2026-04-17 19:02:46 +02:00
if [[ " $free_mb " -lt 1500 ]] ; then
_restore_container_memory " $ctid "
2026-04-19 02:01:04 +02:00
whiptail --backtitle "ProxMenux" \
2026-04-17 19:02:46 +02:00
--title " $( translate 'Insufficient Disk Space' ) " \
--msgbox "\n $( translate 'Container' ) ${ ctid } $( translate 'has only' ) ${ free_mb } MB $( translate 'of free disk space.' ) \n\n $( translate 'NVIDIA libs require approximately 1.5GB of free space.' ) " \
11 72
msg_warn " $( translate 'Insufficient disk space. Skipping LXC' ) ${ ctid } ."
install_rc = 1
else
local extract_dir = " ${ NVIDIA_WORKDIR } /extracted_ ${ version } "
local archive = "/tmp/nvidia_lxc_ ${ version } .tar.gz"
msg_info2 " $( translate 'Extracting NVIDIA installer on host...' ) "
rm -rf " $extract_dir "
sh " $run_file " --extract-only --target " $extract_dir " 2>& 1 | tee -a " $LOG_FILE "
if [[ ${ PIPESTATUS [0] } -ne 0 ]] ; then
msg_warn " $( translate 'Extraction failed. Check log:' ) ${ LOG_FILE } "
_restore_container_memory " $ctid "
install_rc = 1
else
msg_ok " $( translate 'NVIDIA installer extracted.' ) " | tee -a " $screen_capture "
msg_info2 " $( translate 'Packing installer archive...' ) "
tar --checkpoint= 5000 --checkpoint-action= dot \
-czf " $archive " -C " $extract_dir " . 2>& 1 | tee -a " $LOG_FILE "
echo ""
local archive_size
archive_size = $( du -sh " $archive " 2>/dev/null | cut -f1)
msg_ok " $( translate 'Archive ready' ) ( ${ archive_size } )." | tee -a " $screen_capture "
msg_info " $( translate 'Copying installer to container' ) ${ ctid } ..."
if ! pct push " $ctid " " $archive " /tmp/nvidia_lxc.tar.gz >>" $LOG_FILE " 2>& 1; then
msg_warn " $( translate 'pct push failed. Check log:' ) ${ LOG_FILE } "
rm -f " $archive "
rm -rf " $extract_dir "
_restore_container_memory " $ctid "
install_rc = 1
else
rm -f " $archive "
msg_ok " $( translate 'Installer copied to container.' ) " | tee -a " $screen_capture "
msg_info2 " $( translate 'Running NVIDIA installer in container. This may take several minutes...' ) "
echo "" >>" $LOG_FILE "
2026-05-09 18:59:59 +02:00
if [[ " $distro " == "alpine" ]] ; then
# Alpine uses musl libc and does not ship a glibc dynamic
# loader, so the nvidia-installer binary (glibc) cannot
# execute. We pull `gcompat` to provide the glibc loader
# and a libc shim, then copy the userspace libs and the
# standard NVIDIA binaries by hand. SONAME symlinks are
# built from `readelf` (binutils) instead of trusting a
# hard-coded list — the .run ships ~50 .so files and the
# set varies between branches.
pct exec " $ctid " -- sh -c '
set -e
mkdir -p /tmp/nvidia_lxc_install
tar -xzf /tmp/nvidia_lxc.tar.gz -C /tmp/nvidia_lxc_install
apk add --no-cache gcompat binutils >/dev/null
cd /tmp/nvidia_lxc_install
mkdir -p /usr/lib /usr/bin
cp -P *.so* /usr/lib/ 2>/dev/null || true
for lib in /usr/lib/lib*.so.*; do
[ -f "$lib" ] || continue
soname=$(readelf -d "$lib" 2>/dev/null | grep SONAME | head -n1 | sed -e "s/.*\[//" -e "s/\].*//")
[ -n "$soname" ] && [ "$(basename "$lib")" != "$soname" ] && ln -sf "$(basename "$lib")" "/usr/lib/$soname"
done
for bin in nvidia-smi nvidia-debugdump nvidia-cuda-mps-control nvidia-cuda-mps-server nvidia-persistenced nvidia-modprobe; do
[ -f "$bin" ] && cp -P "$bin" /usr/bin/ && chmod 755 "/usr/bin/$bin"
done
rm -rf /tmp/nvidia_lxc_install /tmp/nvidia_lxc.tar.gz
' 2>& 1 | tee -a " $LOG_FILE "
install_rc = ${ PIPESTATUS [0] }
else
pct exec " $ctid " -- bash -c "
mkdir -p /tmp/nvidia_lxc_install
tar -xzf /tmp/nvidia_lxc.tar.gz -C /tmp/nvidia_lxc_install 2>&1
/tmp/nvidia_lxc_install/nvidia-installer \
--no-kernel-modules \
--no-questions \
--ui=none \
--no-nouveau-check \
--no-dkms \
--no-install-compat32-libs
EXIT=\$?
rm -rf /tmp/nvidia_lxc_install /tmp/nvidia_lxc.tar.gz
exit \$EXIT
" 2>& 1 | tee -a " $LOG_FILE "
install_rc = ${ PIPESTATUS [0] }
fi
2026-04-17 19:02:46 +02:00
rm -rf " $extract_dir "
_restore_container_memory " $ctid "
fi
fi
fi
fi
;;
esac
if [[ $install_rc -ne 0 ]] ; then
msg_warn " $( translate 'NVIDIA update failed for LXC' ) ${ ctid } (rc= ${ install_rc } ). $( translate 'Check log:' ) ${ LOG_FILE } "
if $started_here ; then
pct stop " $ctid " >>" $LOG_FILE " 2>& 1 || true
fi
return 1
fi
if pct exec " $ctid " -- sh -c "which nvidia-smi" >/dev/null 2>& 1; then
local new_ver
new_ver = $( pct exec " $ctid " -- nvidia-smi \
--query-gpu= driver_version --format= csv,noheader 2>/dev/null \
| head -1 | tr -d '[:space:]' || true )
msg_ok " $( translate 'Container' ) ${ ctid } : ${ old_version } → ${ new_ver :- $version } " | tee -a " $screen_capture "
else
msg_warn " $( translate 'nvidia-smi not found in container' ) ${ ctid } $( translate 'after update.' ) "
fi
if $started_here ; then
msg_info " $( translate 'Stopping container' ) ${ ctid } ..."
pct stop " $ctid " >>" $LOG_FILE " 2>& 1 || true
msg_ok " $( translate 'Container stopped.' ) " | tee -a " $screen_capture "
fi
return 0
}
# Post-host-install LXC update offer — scans for NVIDIA LXCs and, if any are
# found, asks the user if they want to propagate the driver update to them.
offer_lxc_updates_if_any() {
local target_version = " $1 "
find_nvidia_containers
[[ ${# NVIDIA_CONTAINERS [@] } -eq 0 ]] && return 0
local info ctid lxc_ver ct_name
info = "\n $( translate 'The following LXC containers have NVIDIA passthrough configured:' ) \n\n"
for ctid in " ${ NVIDIA_CONTAINERS [@] } " ; do
lxc_ver = $( get_lxc_nvidia_version " $ctid " )
ct_name = $( pct config " $ctid " 2>/dev/null | grep "^hostname:" | awk '{print $2}' )
info += " CT ${ ctid } ${ ct_name :+( ${ ct_name } ) } — $( translate 'driver:' ) ${ lxc_ver } \n"
done
info += "\n $( translate 'Do you want to update the NVIDIA userspace libraries inside these containers to match the host?' ) "
2026-04-19 02:01:04 +02:00
if ! hybrid_whiptail_yesno " $( translate 'Update NVIDIA in LXC Containers' ) " " $info " 20 80; then
2026-04-17 19:02:46 +02:00
msg_info2 " $( translate 'LXC update skipped by user.' ) "
return 0
fi
for ctid in " ${ NVIDIA_CONTAINERS [@] } " ; do
update_lxc_nvidia " $ctid " " $target_version " || true
done
}
2025-11-30 15:31:19 +01:00
# ==========================================================
# System preparation (repos, headers, etc.)
# ==========================================================
ensure_repos_and_headers() {
2026-06-21 23:20:00 +02:00
# Bootstrap APT repos FIRST. On a fresh Proxmox install the
# pve-no-subscription / debian repos aren't configured by default
# → `pve-headers-$(uname -r)` and `build-essential` come back as
# "Unable to locate package" and the NVIDIA install bails out with
# "no cc found". We delegate to the shared helper (same one the
# post-install flow uses), which owns its own spinner pair — that's
# why this block has to run BEFORE we open our own msg_info.
2026-06-21 23:08:17 +02:00
if ! declare -F ensure_repositories >/dev/null 2>& 1; then
local _utils_install = " $LOCAL_SCRIPTS /global/utils-install-functions.sh"
[[ ! -f " $_utils_install " ]] && _utils_install = "/usr/local/share/proxmenux/scripts/global/utils-install-functions.sh"
# shellcheck source=/dev/null
[[ -f " $_utils_install " ]] && source " $_utils_install "
fi
if declare -F ensure_repositories >/dev/null 2>& 1; then
ensure_repositories >>" $LOG_FILE " 2>& 1 || true
fi
2026-06-21 23:20:00 +02:00
# Now own the spinner for the headers + build-tools check.
msg_info " $( translate 'Checking kernel headers and build tools...' ) "
2025-11-30 15:31:19 +01:00
local kver
kver = $( uname -r)
apt-get update -qq >>" $LOG_FILE " 2>& 1
if ! dpkg -s "pve-headers- $kver " >/dev/null 2>& 1 && \
! dpkg -s "proxmox-headers- $kver " >/dev/null 2>& 1; then
apt-get install -y "pve-headers- $kver " "proxmox-headers- $kver " build-essential dkms >>" $LOG_FILE " 2>& 1 || true
else
apt-get install -y build-essential dkms >>" $LOG_FILE " 2>& 1 || true
fi
msg_ok " $( translate 'Kernel headers and build tools verified.' ) " | tee -a " $screen_capture "
}
2026-08-26 17:23:09 +02:00
_nouveau_legacy_file_is_proxmenux_shape() {
[[ -f " $NVIDIA_NOUVEAU_LEGACY_BLACKLIST " ]] || return 1
local content
content = $( sed '/^[[:space:]]*$/d' " $NVIDIA_NOUVEAU_LEGACY_BLACKLIST " 2>/dev/null)
[[ " $content " == $'blacklist nouveau\noptions nouveau modeset=0' ]]
}
_nouveau_state_set() {
local key = " $1 "
mkdir -p " $( dirname " $NVIDIA_NOUVEAU_STATE " ) "
touch " $NVIDIA_NOUVEAU_STATE "
grep -qFx " ${ key } =1" " $NVIDIA_NOUVEAU_STATE " 2>/dev/null \
|| echo " ${ key } =1" >> " $NVIDIA_NOUVEAU_STATE "
}
restore_nouveau_after_uninstall() {
local remove_global_line = false
if [[ -f " $NVIDIA_NOUVEAU_STATE " ]] \
&& grep -qFx 'blacklist_conf_line_added=1' " $NVIDIA_NOUVEAU_STATE " 2>/dev/null; then
remove_global_line = true
fi
# Migration for installations made by older ProxMenux versions. That
# version overwrote this exact two-line file and added the matching line
# to blacklist.conf, but had no ownership state yet.
if _nouveau_legacy_file_is_proxmenux_shape; then
rm -f " $NVIDIA_NOUVEAU_LEGACY_BLACKLIST "
remove_global_line = true
fi
rm -f " $NVIDIA_NOUVEAU_BLACKLIST "
if $remove_global_line && [[ -f " $NVIDIA_GLOBAL_BLACKLIST " ]] ; then
sed -i '/^blacklist nouveau$/d' " $NVIDIA_GLOBAL_BLACKLIST "
fi
rm -f " $NVIDIA_NOUVEAU_STATE "
}
2025-11-30 15:31:19 +01:00
blacklist_nouveau() {
msg_info " $( translate 'Blacklisting nouveau driver...' ) "
2026-03-26 18:18:26 +01:00
2026-08-26 17:23:09 +02:00
local legacy_owned = false
if _nouveau_legacy_file_is_proxmenux_shape; then
rm -f " $NVIDIA_NOUVEAU_LEGACY_BLACKLIST "
legacy_owned = true
_nouveau_state_set "legacy_migrated"
2025-11-30 15:31:19 +01:00
fi
2026-03-26 18:18:26 +01:00
2026-08-26 17:23:09 +02:00
if ! grep -q '^blacklist nouveau$' " $NVIDIA_GLOBAL_BLACKLIST " 2>/dev/null; then
echo "blacklist nouveau" >> " $NVIDIA_GLOBAL_BLACKLIST "
_nouveau_state_set "blacklist_conf_line_added"
elif $legacy_owned ; then
# The legacy ProxMenux file proves ownership of the companion line.
_nouveau_state_set "blacklist_conf_line_added"
fi
# ProxMenux-owned file: uninstall can now remove only what we created.
cat > " $NVIDIA_NOUVEAU_BLACKLIST " <<'EOF'
# Managed by ProxMenux NVIDIA installer.
2026-03-26 18:18:26 +01:00
blacklist nouveau
options nouveau modeset=0
EOF
2026-06-12 21:03:01 +02:00
# Attempt to unload nouveau if currently loaded.
# Close the spinner from the opening msg_info before going further —
# otherwise the second msg_info below leaves the first one spinning
# forever (visible on fresh installs where nouveau is loaded; invisible
# on reinstalls where this branch is skipped).
2026-04-19 02:01:04 +02:00
if grep -q "^nouveau " /proc/modules 2>/dev/null; then
2026-06-12 21:03:01 +02:00
msg_ok " $( translate 'nouveau driver has been blacklisted.' ) " | tee -a " $screen_capture "
2026-04-19 02:01:04 +02:00
2026-03-26 18:18:26 +01:00
msg_info " $( translate 'Nouveau module is loaded, attempting to unload...' ) "
modprobe -r nouveau 2>/dev/null || true
2026-04-19 02:01:04 +02:00
sleep 1
2026-03-26 18:18:26 +01:00
# Check if unload succeeded
2026-04-19 02:01:04 +02:00
if grep -q "^nouveau " /proc/modules 2>/dev/null; then
2026-03-26 18:18:26 +01:00
NOUVEAU_STILL_LOADED = true
msg_warn " $( translate 'Could not unload nouveau module (may be in use). The blacklist will take effect after reboot. Installation will continue but a reboot will be required.' ) "
echo "WARNING: nouveau module still loaded after unload attempt" >> " $LOG_FILE "
else
NOUVEAU_STILL_LOADED = false
msg_ok " $( translate 'nouveau module unloaded successfully.' ) " | tee -a " $screen_capture "
fi
else
NOUVEAU_STILL_LOADED = false
msg_ok " $( translate 'nouveau driver has been blacklisted.' ) " | tee -a " $screen_capture "
fi
2026-04-19 02:01:04 +02:00
2025-11-30 15:31:19 +01:00
}
ensure_modules_config() {
2026-08-17 12:05:40 +02:00
msg_info " $( translate 'Configuring NVIDIA modules...' ) "
2025-11-30 15:31:19 +01:00
cat > /etc/modules-load.d/nvidia-vfio.conf <<'EOF'
nvidia
nvidia_uvm
EOF
msg_ok " $( translate 'Modules configuration updated.' ) " | tee -a " $screen_capture "
}
stop_and_disable_nvidia_services() {
local services =(
"nvidia-persistenced.service"
"nvidia-persistenced"
"nvidia-powerd.service"
)
local services_detected = 0
for service in " ${ services [@] } " ; do
if systemctl is-active --quiet " $service " 2>/dev/null || \
systemctl is-enabled --quiet " $service " 2>/dev/null; then
services_detected = 1
break
fi
done
if [ " $services_detected " -eq 1 ] ; then
msg_info " $( translate 'Stopping and disabling NVIDIA services...' ) "
for service in " ${ services [@] } " ; do
if systemctl is-active --quiet " $service " 2>/dev/null; then
systemctl stop " $service " >/dev/null 2>& 1 || true
fi
if systemctl is-enabled --quiet " $service " 2>/dev/null; then
systemctl disable " $service " >/dev/null 2>& 1 || true
fi
done
2026-04-19 02:01:04 +02:00
2025-11-30 15:31:19 +01:00
sleep 2
msg_ok " $( translate 'NVIDIA services stopped and disabled.' ) " | tee -a " $screen_capture "
fi
}
unload_nvidia_modules() {
for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do
modprobe -r " $mod " >/dev/null 2>& 1 || true
done
2026-04-19 02:01:04 +02:00
# Give the kernel a moment to finalize sysfs teardown before re-checking.
# Reading /proc/modules directly (instead of lsmod) avoids the
# "could not open /sys/module/<mod>/holders" race when a module has just
# been removed from /proc/modules but its sysfs dir hasn't been reaped yet.
sleep 1
2025-11-30 15:31:19 +01:00
2026-04-19 02:01:04 +02:00
if grep -q "^nvidia" /proc/modules 2>/dev/null; then
2025-11-30 15:31:19 +01:00
for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do
modprobe -r --force " $mod " >/dev/null 2>& 1 || true
done
2026-04-19 02:01:04 +02:00
sleep 1
2025-11-30 15:31:19 +01:00
fi
2026-04-19 02:01:04 +02:00
if grep -q "^nvidia" /proc/modules 2>/dev/null; then
2025-11-30 15:31:19 +01:00
if command -v lsof >/dev/null 2>& 1; then
echo " $( translate 'Processes using NVIDIA:' ) :" >> " $LOG_FILE "
lsof /dev/nvidia* 2>/dev/null >> " $LOG_FILE " || true
fi
else
2026-04-19 02:01:04 +02:00
2025-11-30 15:31:19 +01:00
msg_ok " $( translate 'NVIDIA kernel modules unloaded successfully.' ) " | tee -a " $screen_capture "
fi
}
complete_nvidia_uninstall() {
stop_and_disable_nvidia_services
unload_nvidia_modules
if command -v nvidia-uninstall >/dev/null 2>& 1; then
2026-04-19 02:01:04 +02:00
msg_info " $( translate 'Running NVIDIA uninstaller...' ) "
2025-11-30 15:31:19 +01:00
nvidia-uninstall --silent >>" $LOG_FILE " 2>& 1 || true
msg_ok " $( translate 'NVIDIA uninstaller completed.' ) "
fi
2026-04-18 08:58:50 +02:00
msg_ok " $( translate 'NVIDIA uninstallation steps completed.' ) " | tee -a " $screen_capture "
2025-11-30 15:31:19 +01:00
cleanup_nvidia_dkms
msg_info " $( translate 'Removing NVIDIA packages...' ) "
apt-get -y purge 'nvidia-*' 'libnvidia-*' 'cuda-*' 'libcudnn*' >>" $LOG_FILE " 2>& 1 || true
apt-get -y autoremove --purge >>" $LOG_FILE " 2>& 1 || true
apt-get -y autoclean >>" $LOG_FILE " 2>& 1 || true
rm -f /etc/modules-load.d/nvidia-vfio.conf
rm -f /etc/udev/rules.d/70-nvidia.rules
rm -rf /usr/lib/modprobe.d/nvidia*.conf
rm -rf /etc/modprobe.d/nvidia*.conf
2026-08-26 17:23:09 +02:00
restore_nouveau_after_uninstall
2025-11-30 15:31:19 +01:00
if [[ -d " $NVIDIA_WORKDIR " ]] ; then
find " $NVIDIA_WORKDIR " -type d -name "nvidia-persistenced" -exec rm -rf {} + 2>/dev/null || true
find " $NVIDIA_WORKDIR " -type d -name "nvidia-patch" -exec rm -rf {} + 2>/dev/null || true
fi
2026-04-19 02:01:04 +02:00
2025-11-30 15:31:19 +01:00
update_component_status "nvidia_driver" "removed" "" "gpu" '{}'
msg_ok " $( translate 'Complete NVIDIA uninstallation finished.' ) " | tee -a " $screen_capture "
2026-04-19 02:01:04 +02:00
2025-11-30 15:31:19 +01:00
}
cleanup_nvidia_dkms() {
local versions
versions = $( dkms status 2>/dev/null | awk -F, '/nvidia/ {gsub(/ /,"",$2); print $2}' || true )
[[ -z " $versions " ]] && return 0
msg_info " $( translate 'Removing NVIDIA DKMS entries...' ) "
while IFS = read -r ver; do
[[ -z " $ver " ]] && continue
dkms remove -m nvidia -v " $ver " --all >/dev/null 2>& 1 || true
done <<< " $versions "
msg_ok " $( translate 'NVIDIA DKMS entries removed.' ) "
}
ensure_workdir() {
mkdir -p " $NVIDIA_WORKDIR "
}
# ==========================================================
2026-08-26 17:23:09 +02:00
# System detection
2025-11-30 15:31:19 +01:00
# ==========================================================
2026-08-26 17:23:09 +02:00
get_system_info() {
2025-11-30 15:31:19 +01:00
if [[ -f /etc/pve/.version ]] ; then
PVE_VERSION = $( cat /etc/pve/.version)
else
PVE_VERSION = "unknown"
fi
}
2025-12-11 22:07:01 +01:00
2026-05-09 18:59:59 +02:00
is_current_nvidia_patched() {
local status_file = "/usr/local/share/proxmenux/components_status.json"
[[ -f " $status_file " ]] || return 1
command -v jq >/dev/null 2>& 1 || return 1
local patched
patched = $( jq -r '.nvidia_driver.patched // false' " $status_file " 2>/dev/null)
[[ " $patched " == "true" ]]
}
KEYLASE_PATCH_CACHE = "/var/cache/proxmenux/keylase_patch_versions.txt"
KEYLASE_PATCH_TTL_SECONDS = $(( 7 * 86400 ))
KEYLASE_PATCH_URL = "https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh"
2026-08-15 17:33:05 +02:00
# NVIDIA branch classification comes from the vendor's own Unix drivers
# page, not the CDN — the CDN publishes every branch (production, new
# feature, vulkan-beta, developer) in the same flat directory, whereas
# the vendor page carries the current heads clearly labelled "Production
# Branch", "New Feature Branch" and "Legacy GPU version". Extracting the
# majors from those three lines gives us the set of branches NVIDIA
# currently endorses for end users, with zero manual maintenance on our
# side — when NVIDIA promotes a new rama the cache picks it up on the
# next 24 h refresh. Cache is fail-open: if the fetch is blocked or the
# page layout changes, we skip the branch filter rather than emptying
# the picker.
NVIDIA_BRANCHES_CACHE = "/var/cache/proxmenux/nvidia_stable_branches.txt"
NVIDIA_PRODUCTION_HEAD_CACHE = "/var/cache/proxmenux/nvidia_production_head.txt"
NVIDIA_BRANCH_HEADS_CACHE = "/var/cache/proxmenux/nvidia_branch_heads.txt"
NVIDIA_GPU_SUPPORT_CACHE_PREFIX = "/var/cache/proxmenux/nvidia_gpu_support_"
NVIDIA_BRANCHES_TTL_SECONDS = $(( 24 * 3600 ))
NVIDIA_BRANCHES_URL = "https://www.nvidia.com/en-us/drivers/unix/"
refresh_nvidia_branches_cache() {
local now ts age
now = $( date +%s)
if [[ -f " $NVIDIA_BRANCHES_CACHE " ]] ; then
ts = $( stat -c '%Y' " $NVIDIA_BRANCHES_CACHE " 2>/dev/null || echo 0)
age = $(( now - ts ))
if (( age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s " $NVIDIA_BRANCHES_CACHE " ]] ; then
return 0
fi
fi
mkdir -p " $( dirname " $NVIDIA_BRANCHES_CACHE " ) " 2>/dev/null || return 1
local html tmp
html = $( curl -fsSL -A "Mozilla/5.0" --max-time 15 " $NVIDIA_BRANCHES_URL " 2>/dev/null) || return 1
[[ -z " $html " ]] && return 1
local clean tmp_full
clean = $( echo " $html " | perl -0777 -pe 's/<!--.*?-->//gs' 2>/dev/null)
tmp = $( mktemp)
tmp_full = $( mktemp)
# Two label shapes on the page:
# • Production / New Feature → "… Branch Version:" then <a>full</a>.
# • Legacy → "Legacy GPU version (NNN.xx series):"
# then <a>full</a> — the word "Version"
# lives inside the parenthesised label
# so the Production/Feature regex misses
# it (separate alternative below).
# HTML comments are stripped first so vestigial `<!-- Beta Version …
# 387.34 -->` blocks in older ia32 rows don't leak stale majors.
2026-08-16 10:33:34 +02:00
# Perl one-liner because we want to keep the label text next to the
# version — shell greps can only give us one or the other, and we
# need the label to tag each head as production / feature / legacy.
2026-08-15 17:33:05 +02:00
echo " $clean " \
2026-08-16 10:33:34 +02:00
| perl -ne '
while (/(Production Branch Version|New Feature Branch Version|Legacy GPU version \(([0-9]+)\.xx series\)):[^<]*(?:<\/span>)?\s*<a[^>]*>([0-9]+\.[0-9]+(?:\.[0-9]+)?)/gi) {
my $label = lc($1);
my $ver = $3;
my ($maj) = split(/\./, $ver);
my $type = "unknown";
if ($label =~ /production branch/) { $type = "production" }
elsif ($label =~ /new feature branch/) { $type = "feature" }
elsif ($label =~ /legacy gpu version/) { $type = "legacy" }
print "$type|$maj|$ver\n";
}' \
| sort -u -t'|' -k1,1 -k2,2n > " $tmp_full "
2026-08-15 17:33:05 +02:00
if [[ ! -s " $tmp_full " ]] ; then
rm -f " $tmp " " $tmp_full "
return 1
fi
2026-08-16 10:33:34 +02:00
# Derive the majors-only file from the same source, but ONLY for
# production + legacy — New Feature Branch heads (610.x today) are
# intentionally NOT in the endorsed whitelist because they carry
# kernel/driver features still under stabilisation. Superseded
# production branches still qualify via the release-count heuristic
# in filter_option_c_branch, so operators on 580 / 570 / 550 / 535
# keep bugfix upgrade options.
awk -F'|' '$1 == "production" || $1 == "legacy" { print $2 }' \
" $tmp_full " | sort -un > " $tmp "
2026-08-15 17:33:05 +02:00
if [[ ! -s " $tmp " ]] ; then
rm -f " $tmp " " $tmp_full "
return 1
fi
mv " $tmp " " $NVIDIA_BRANCHES_CACHE "
mv " $tmp_full " " $NVIDIA_BRANCH_HEADS_CACHE "
# Extra pass: capture the full Production Branch head so the picker
# can default to it instead of the highest numeric available (which
# could be a New Feature Branch head — NVIDIA doesn't recommend those
# as the general-purpose default). Best-effort.
local prod_head
prod_head = $( echo " $clean " \
| grep -oiE 'Production Branch Version:[^<]*(</span>)?\s*<a[^>]*>[0-9]+\.[0-9]+(\.[0-9]+)?' \
| grep -oE '>[0-9]+\.[0-9]+(\.[0-9]+)?' \
| tr -d '>' \
| head -n1)
if [[ -n " $prod_head " ]] ; then
echo " $prod_head " > " $NVIDIA_PRODUCTION_HEAD_CACHE "
else
rm -f " $NVIDIA_PRODUCTION_HEAD_CACHE " 2>/dev/null || true
fi
return 0
}
# Return the full head version associated with a major from the
# branch-heads cache (e.g. `get_nvidia_branch_head 595` → 595.91.07).
2026-08-16 10:33:34 +02:00
# Cache lines are `type|major|version` — filter by major, print
# version. Used to know which release inside a branch to hit for the
# PCI-ID supported-GPUs list.
2026-08-15 17:33:05 +02:00
get_nvidia_branch_head() {
local major = " $1 "
[[ -f " $NVIDIA_BRANCH_HEADS_CACHE " && -s " $NVIDIA_BRANCH_HEADS_CACHE " ]] || return 1
2026-08-16 10:33:34 +02:00
awk -F'|' -v m = " $major " '$2 == m { print $3; exit }' " $NVIDIA_BRANCH_HEADS_CACHE "
2026-08-15 17:33:05 +02:00
}
# Refresh the per-branch supported-GPU cache. Uses the branch head as
# the "sample release" for the whole branch — NVIDIA rarely drops chip
# support inside a live branch, so this is a solid proxy that also
# minimises fetch count (~3 heads total instead of one per release).
# Written to nvidia_gpu_support_MAJOR.txt with one lowercase hex device
# id per line. Same 24h TTL as the branches cache.
refresh_nvidia_gpu_support_for_major() {
local major = " $1 "
[[ -z " $major " ]] && return 1
local cache = " ${ NVIDIA_GPU_SUPPORT_CACHE_PREFIX }${ major } .txt"
local now ts age
now = $( date +%s)
if [[ -f " $cache " ]] ; then
ts = $( stat -c '%Y' " $cache " 2>/dev/null || echo 0)
age = $(( now - ts ))
if (( age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s " $cache " ]] ; then
return 0
fi
fi
local head_ver
head_ver = $( get_nvidia_branch_head " $major " ) || return 1
[[ -z " $head_ver " ]] && return 1
local url = "https://download.nvidia.com/XFree86/Linux-x86_64/ ${ head_ver } /README/supportedchips.html"
local html tmp
html = $( curl -fsSL -A "Mozilla/5.0" --max-time 20 " $url " 2>/dev/null) || return 1
[[ -z " $html " ]] && return 1
mkdir -p " $( dirname " $cache " ) " 2>/dev/null || return 1
tmp = $( mktemp)
# NVIDIA's supportedchips.html lays out each GPU row as a <td> with
# the PCI Device ID in 4-char hex. Anchor on the surrounding tag so
# we don't sweep up unrelated 4-hex strings elsewhere in the page.
echo " $html " \
| grep -oiE '<td>[0-9A-F]{4}</td>' \
| grep -oiE '[0-9A-F]{4}' \
| tr 'A-F' 'a-f' \
| sort -u > " $tmp "
if [[ -s " $tmp " ]] ; then
mv " $tmp " " $cache "
return 0
fi
rm -f " $tmp "
return 1
}
# True if every detected NVIDIA GPU on this host has its device id in
# the branch's supported list. Fail-open when the cache is missing so
# a network hiccup never locks the picker out.
is_branch_compatible_with_host_gpus() {
local major = " $1 "
local cache = " ${ NVIDIA_GPU_SUPPORT_CACHE_PREFIX }${ major } .txt"
[[ -f " $cache " && -s " $cache " ]] || return 0
[[ ${# NVIDIA_HOST_GPU_IDS [@] } -eq 0 ]] && return 0
local id
for id in " ${ NVIDIA_HOST_GPU_IDS [@] } " ; do
grep -qFx " $id " " $cache " || return 1
done
return 0
}
# Reject Vulkan-beta / short-lived / developer branches by counting
# how many releases NVIDIA actually shipped inside that major on the
# CDN. Production and long-lived New Feature branches accumulate many
# release rows (470=17, 535=20, 550=14, 570=12, 580=14 …); Vulkan-beta
# and developer branches only ever get 1-4 releases before being
# superseded (590=2, 565=2, 530=2, 555=4 …). Threshold 5 separates the
# two groups cleanly at time of writing.
# Fail-open: if the release-count map hasn't been built for whatever
# reason, the branch passes (kernel + GPU-compat + curated whitelist
# are still enforced upstream). The map is populated once per
# `filter_option_c_branch` invocation, so no repeated CDN scraping.
NVIDIA_BRANCH_MIN_RELEASES = 5
declare -A NVIDIA_BRANCH_RELEASE_COUNT =()
is_branch_release_count_sufficient() {
local major = " $1 "
[[ -z " $major " ]] && return 1
[[ ${# NVIDIA_BRANCH_RELEASE_COUNT [@] } -eq 0 ]] && return 0
local n = " ${ NVIDIA_BRANCH_RELEASE_COUNT [ $major ] :- 0 } "
(( n >= NVIDIA_BRANCH_MIN_RELEASES ))
}
get_nvidia_production_head() {
[[ -f " $NVIDIA_PRODUCTION_HEAD_CACHE " && -s " $NVIDIA_PRODUCTION_HEAD_CACHE " ]] || return 1
local v
v = $( head -n1 " $NVIDIA_PRODUCTION_HEAD_CACHE " | tr -d '[:space:]' )
[[ -z " $v " ]] && return 1
printf '%s\n' " $v "
}
is_nvidia_stable_branch() {
local major = " $1 "
[[ -z " $major " ]] && return 1
# Fail-open: no cache → don't filter (upstream behaviour preserved).
[[ -f " $NVIDIA_BRANCHES_CACHE " && -s " $NVIDIA_BRANCHES_CACHE " ]] || return 0
grep -qFx " $major " " $NVIDIA_BRANCHES_CACHE "
}
2026-05-09 18:59:59 +02:00
refresh_keylase_patch_cache() {
local now ts age
now = $( date +%s)
if [[ -f " $KEYLASE_PATCH_CACHE " ]] ; then
ts = $( stat -c '%Y' " $KEYLASE_PATCH_CACHE " 2>/dev/null || echo 0)
age = $(( now - ts ))
if (( age < KEYLASE_PATCH_TTL_SECONDS )) && [[ -s " $KEYLASE_PATCH_CACHE " ]] ; then
return 0
fi
fi
mkdir -p " $( dirname " $KEYLASE_PATCH_CACHE " ) " 2>/dev/null || return 1
local tmp
tmp = $( mktemp)
if curl -fsSL --max-time 15 " $KEYLASE_PATCH_URL " 2>/dev/null \
| grep -oE '\["[0-9]+\.[0-9]+(\.[0-9]+)?"\]' \
| sed -E 's/\["([0-9.]+)"\]/\1/' \
| sort -u > " $tmp " && [[ -s " $tmp " ]] ; then
mv " $tmp " " $KEYLASE_PATCH_CACHE "
return 0
fi
rm -f " $tmp "
return 1
}
is_keylase_patch_supported() {
local ver = " $1 "
[[ -z " $ver " ]] && return 1
[[ -f " $KEYLASE_PATCH_CACHE " && -s " $KEYLASE_PATCH_CACHE " ]] || return 1
grep -qFx " $ver " " $KEYLASE_PATCH_CACHE "
}
filter_keylase_supported() {
local versions_in = " $1 "
while IFS = read -r ver; do
[[ -z " $ver " ]] && continue
if is_keylase_patch_supported " $ver " ; then
printf '%s\n' " $ver "
fi
done <<< " $versions_in "
}
filter_option_c_branch() {
local versions_in = " $1 "
local current = " $2 "
2026-08-16 10:33:34 +02:00
local _unused_recommended_branch = " $3 "
2026-05-09 18:59:59 +02:00
2026-08-15 17:33:05 +02:00
refresh_nvidia_branches_cache 2>/dev/null || true
2026-08-16 10:33:34 +02:00
local target_branch = ""
if [[ -f " $NVIDIA_BRANCHES_CACHE " && -s " $NVIDIA_BRANCHES_CACHE " ]] ; then
target_branch = $( head -n1 " $NVIDIA_BRANCHES_CACHE " )
fi
2026-08-15 17:33:05 +02:00
# Build a majors→count map from the incoming version list. This is
# what backs `is_branch_release_count_sufficient` — done once per
# call so the tight loop below stays local-arithmetic only.
NVIDIA_BRANCH_RELEASE_COUNT =()
while IFS = read -r _v; do
[[ -z " $_v " ]] && continue
local _m = " ${ _v %%.* } "
NVIDIA_BRANCH_RELEASE_COUNT[ $_m ]= $(( ${ NVIDIA_BRANCH_RELEASE_COUNT [ $_m ] :- 0 } + 1 ))
done <<< " $versions_in "
# Grab the head (highest version) of every major so we know which
# release to sample for supportedchips.html. We use the CDN listing
# directly for this — the branch-heads cache only carries the
# endorsed heads, not the superseded ones.
declare -A _major_head =()
while IFS = read -r _v; do
[[ -z " $_v " ]] && continue
local _m = " ${ _v %%.* } "
[[ -z " ${ _major_head [ $_m ] :- } " ]] && _major_head[ $_m ]= " $_v "
done < <( printf '%s\n' " $versions_in " )
# Warm the supported-GPU cache for every stable major (whitelist
# heads: head already known → normal path; superseded heads: seed the
# cache-file's head-version by directly writing a lightweight lookup).
# For endorsed majors we can use refresh_nvidia_gpu_support_for_major
# as-is (it looks up NVIDIA_BRANCH_HEADS_CACHE). For non-endorsed
# majors we need to fetch supportedchips.html against the highest
# release we saw in the CDN listing.
local _m _head _cache _now _ts _age _html _tmp
_now = $( date +%s)
for _m in " ${ !_major_head[@] } " ; do
_head = " ${ _major_head [ $_m ] } "
_cache = " ${ NVIDIA_GPU_SUPPORT_CACHE_PREFIX }${ _m } .txt"
if [[ -f " $_cache " ]] ; then
_ts = $( stat -c '%Y' " $_cache " 2>/dev/null || echo 0)
_age = $(( _now - _ts ))
if (( _age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s " $_cache " ]] ; then
continue
fi
fi
_html = $( curl -fsSL -A "Mozilla/5.0" --max-time 20 \
"https://download.nvidia.com/XFree86/Linux-x86_64/ ${ _head } /README/supportedchips.html" \
2>/dev/null) || continue
[[ -z " $_html " ]] && continue
mkdir -p " $( dirname " $_cache " ) " 2>/dev/null || continue
_tmp = $( mktemp)
echo " $_html " \
| grep -oiE '<td>[0-9A-F]{4}</td>' \
| grep -oiE '[0-9A-F]{4}' \
| tr 'A-F' 'a-f' \
| sort -u > " $_tmp "
if [[ -s " $_tmp " ]] ; then
mv " $_tmp " " $_cache "
else
rm -f " $_tmp "
fi
done
2026-05-09 18:59:59 +02:00
while IFS = read -r ver; do
[[ -z " $ver " ]] && continue
local ver_major = " ${ ver %%.* } "
2026-08-16 10:33:34 +02:00
if [[ -n " $target_branch " ]] && (( 10#$ver_major < 10#$target_branch )) ; then
continue
fi
if is_nvidia_stable_branch " $ver_major " || is_branch_release_count_sufficient " $ver_major " ; then
if is_branch_compatible_with_host_gpus " $ver_major " ; then
printf '%s\n' " $ver "
2026-08-15 17:33:05 +02:00
fi
2026-05-09 18:59:59 +02:00
fi
done <<< " $versions_in "
}
2025-12-11 22:07:01 +01:00
version_le() {
local v1 = " $1 "
local v2 = " $2 "
IFS = '.' read -r a1 b1 c1 <<< " $v1 "
IFS = '.' read -r a2 b2 c2 <<< " $v2 "
a1 = ${ a1 :- 0 } ; b1 = ${ b1 :- 0 } ; c1 = ${ c1 :- 0 }
a2 = ${ a2 :- 0 } ; b2 = ${ b2 :- 0 } ; c2 = ${ c2 :- 0 }
a1 = $(( 10# $a1 )) ; b1 = $(( 10# $b1 )) ; c1 = $(( 10# $c1 ))
a2 = $(( 10# $a2 )) ; b2 = $(( 10# $b2 )) ; c2 = $(( 10# $c2 ))
if (( a1 < a2 )) ; then
return 0
elif (( a1 > a2 )) ; then
return 1
fi
if (( b1 < b2 )) ; then
return 0
elif (( b1 > b2 )) ; then
return 1
fi
if (( c1 <= c2 )) ; then
return 0
else
return 1
fi
}
2025-11-30 15:31:19 +01:00
# ==========================================================
# NVIDIA version management - FIXED VERSION
# ==========================================================
download_latest_version() {
local latest_line version
latest_line = $( curl -fsSL " ${ NVIDIA_BASE_URL } /latest.txt" 2>& 1)
if [[ -z " $latest_line " ]] ; then
echo "" >& 2
return 1
fi
version = $( echo " $latest_line " | awk '{print $1}' | tr -d '[:space:]' )
if [[ -z " $version " ]] ; then
echo "" >& 2
return 1
fi
if [[ ! " $version " = ~ ^[ 0-9] +\. [ 0-9] +( \. [ 0-9] +) ?$ ]] ; then
echo "" >& 2
return 1
fi
echo " $version "
return 0
}
list_available_versions() {
local html_content versions
html_content = $( curl -s " $NVIDIA_BASE_URL /" 2>& 1)
if [[ -z " $html_content " ]] ; then
echo "" >& 2
return 1
fi
versions = $( echo " $html_content " \
| grep -o 'href=[^ >]*' \
| awk -F"'" '{print $2}' \
| grep -E '^[0-9]' \
| sed 's/\/$//' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sort -Vr \
| uniq)
if [[ -z " $versions " ]] ; then
echo "" >& 2
return 1
fi
echo " $versions "
return 0
}
verify_version_exists() {
local version = " $1 "
local url = " ${ NVIDIA_BASE_URL } / ${ version } /"
if curl -fsSL --head " $url " >/dev/null 2>& 1; then
return 0
else
return 1
fi
}
download_nvidia_installer() {
ensure_workdir
local version = " $1 "
version = $( echo " $version " | tr -d '[:space:]' | tr -d '\n' | tr -d '\r' )
if [[ ! " $version " = ~ ^[ 0-9] +\. [ 0-9] +( \. [ 0-9] +) ?$ ]] ; then
msg_error "Invalid version format: $version " >& 2
echo "ERROR: Invalid version format: ' $version '" >> " $LOG_FILE "
return 1
fi
local run_file = " $NVIDIA_WORKDIR /NVIDIA-Linux-x86_64- ${ version } .run"
if [[ -f " $run_file " ]] ; then
echo "Found existing file: $run_file " >> " $LOG_FILE "
local existing_size file_type
existing_size = $( stat -c%s " $run_file " 2>/dev/null || stat -f%z " $run_file " 2>/dev/null || echo "0" )
file_type = $( file " $run_file " 2>/dev/null || echo "unknown" )
echo "Existing file size: $existing_size bytes" >> " $LOG_FILE "
echo "Existing file type: $file_type " >> " $LOG_FILE "
if [[ $existing_size -gt 40000000 ]] && echo " $file_type " | grep -q "executable" ; then
if sh " $run_file " --check 2>& 1 | tee -a " $LOG_FILE " | grep -q "OK" ; then
echo "Existing file passed integrity check" >> " $LOG_FILE "
msg_ok " $( translate 'Installer already downloaded and verified.' ) " >& 2
printf '%s\n' " $run_file "
return 0
else
echo "Existing file FAILED integrity check, removing..." >> " $LOG_FILE "
2026-04-19 02:01:04 +02:00
msg_warn " $( translate 'Existing file, re-downloading...' ) " >& 2
2025-11-30 15:31:19 +01:00
rm -f " $run_file "
fi
else
echo "Existing file invalid (size or type), removing..." >> " $LOG_FILE "
msg_warn " $( translate 'Removing invalid existing file...' ) " >& 2
rm -f " $run_file "
fi
fi
if ! verify_version_exists " $version " ; then
msg_error "Version $version does not exist on NVIDIA servers" >& 2
echo "ERROR: Version $version not found on server" >> " $LOG_FILE "
return 1
fi
local urls =(
" ${ NVIDIA_BASE_URL } / ${ version } /NVIDIA-Linux-x86_64- ${ version } .run"
" ${ NVIDIA_BASE_URL } / ${ version } /NVIDIA-Linux-x86_64- ${ version } -no-compat32.run"
)
2026-04-17 18:35:42 +02:00
2026-04-17 19:02:46 +02:00
# Web mode (ProxMenux Monitor) runs scripts without a controlling TTY, so
# /dev/tty is not writable and progress-bar animations using \r don't render
# in the web terminal. Fall back to a quiet wget in that case; interactive
# users (SSH / console) still get the ISO-like progress bar.
local _nv_has_tty = false
if ! is_web_mode 2>/dev/null && [[ -t 2 ]] ; then
_nv_has_tty = true
fi
if $_nv_has_tty ; then
printf '\n %s NVIDIA-Linux-x86_64-%s.run\n' \
" $( translate 'Downloading' ) " " $version " >/dev/tty
else
echo " $( translate 'Downloading' ) NVIDIA-Linux-x86_64- ${ version } .run" >& 2
fi
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
local success = false
local url_index = 0
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
for url in " ${ urls [@] } " ; do
(( url_index++))
echo "Attempting download from: $url " >> " $LOG_FILE "
rm -f " $run_file "
2026-04-17 19:02:46 +02:00
local _dl_ok = false
if $_nv_has_tty ; then
# Interactive: progress bar to /dev/tty (bypasses any caller redirection).
if wget --no-verbose --show-progress \
--connect-timeout= 30 --timeout= 600 --tries= 1 \
-O " $run_file " " $url " 2>/dev/tty; then
_dl_ok = true
fi
else
# Web / no-TTY: silent wget, log errors only.
if wget --quiet \
--connect-timeout= 30 --timeout= 600 --tries= 1 \
-O " $run_file " " $url " 2>>" $LOG_FILE " ; then
_dl_ok = true
fi
fi
if $_dl_ok ; then
2025-11-30 15:31:19 +01:00
echo "Download completed, verifying file..." >> " $LOG_FILE "
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
if [[ ! -f " $run_file " ]] ; then
echo "ERROR: File not created after download" >> " $LOG_FILE "
continue
fi
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
local file_size
file_size = $( stat -c%s " $run_file " 2>/dev/null || stat -f%z " $run_file " 2>/dev/null || echo "0" )
echo "Downloaded file size: $file_size bytes" >> " $LOG_FILE "
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
if [[ $file_size -lt 40000000 ]] ; then
echo "ERROR: File too small ( $file_size bytes, expected >40MB)" >> " $LOG_FILE "
head -c 200 " $run_file " >> " $LOG_FILE " 2>& 1
rm -f " $run_file "
continue
fi
local file_type
file_type = $( file " $run_file " 2>/dev/null)
echo "File type: $file_type " >> " $LOG_FILE "
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
if echo " $file_type " | grep -q "executable" ; then
echo "SUCCESS: Valid executable downloaded" >> " $LOG_FILE "
success = true
break
else
echo "ERROR: Not a valid executable" >> " $LOG_FILE "
head -c 200 " $run_file " | od -c >> " $LOG_FILE " 2>& 1
rm -f " $run_file "
fi
else
2026-04-17 18:35:42 +02:00
echo "ERROR: wget failed for $url (exit code: $? )" >> " $LOG_FILE "
2025-11-30 15:31:19 +01:00
rm -f " $run_file "
fi
done
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
if ! $success ; then
msg_error " $( translate 'Download failed for all attempted URLs' ) " >& 2
msg_error "Version $version may not be available for your architecture" >& 2
echo "ERROR: All download attempts failed" >> " $LOG_FILE "
return 1
fi
chmod +x " $run_file "
echo "Installation file ready: $run_file " >> " $LOG_FILE "
printf '%s\n' " $run_file "
}
# ==========================================================
# Installation / uninstallation
# ==========================================================
run_nvidia_installer() {
local installer = " $1 "
msg_info2 " $( translate 'Starting NVIDIA installer. This may take several minutes...' ) "
echo "" >>" $LOG_FILE "
echo "=== Running NVIDIA installer: $installer ===" >>" $LOG_FILE "
2026-03-26 18:18:26 +01:00
# If nouveau is still loaded, rebuild initramfs first so the blacklist takes
# effect for the installer sanity checks. Without this the .run installer
# detects nouveau as active and aborts even when --disable-nouveau is passed.
if [[ " ${ NOUVEAU_STILL_LOADED :- false } " == "true" ]] ; then
msg_info " $( translate 'Rebuilding initramfs to apply nouveau blacklist before installation...' ) "
update-initramfs -u -k all >>" $LOG_FILE " 2>& 1 || true
2026-06-12 20:05:17 +02:00
proxmox-boot-tool refresh >>" $LOG_FILE " 2>& 1 || true
2026-03-26 18:18:26 +01:00
# Try one more time to unload nouveau after initramfs rebuild
modprobe -r nouveau 2>/dev/null || true
2026-04-19 02:01:04 +02:00
sleep 1
if grep -q "^nouveau " /proc/modules 2>/dev/null; then
2026-03-26 18:18:26 +01:00
echo "WARNING: nouveau still loaded after initramfs rebuild, proceeding with --no-nouveau-check" >> " $LOG_FILE "
msg_warn " $( translate 'nouveau still active. Proceeding with installation. A reboot will be required for the driver to work.' ) "
else
NOUVEAU_STILL_LOADED = false
msg_ok " $( translate 'nouveau module unloaded after initramfs rebuild.' ) " | tee -a " $screen_capture "
fi
fi
2025-11-30 15:31:19 +01:00
local tmp_extract_dir = " $NVIDIA_WORKDIR /tmp_extract"
mkdir -p " $tmp_extract_dir "
2026-03-26 18:18:26 +01:00
# --no-nouveau-check: prevents the installer from aborting when nouveau is
# still loaded. The blacklist files are already in place; nouveau will be
# gone after the reboot that the script offers at the end.
sh " $installer " \
--tmpdir= " $tmp_extract_dir " \
--no-questions \
--ui= none \
--disable-nouveau \
--no-nouveau-check \
--dkms \
2>& 1 | tee -a " $LOG_FILE "
2025-11-30 15:31:19 +01:00
local rc = ${ PIPESTATUS [0] }
echo "" >>" $LOG_FILE "
rm -rf " $tmp_extract_dir "
if [[ $rc -ne 0 ]] ; then
msg_error " $( translate 'NVIDIA installer reported an error. Check /tmp/nvidia_install.log' ) "
update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}'
return 1
fi
msg_ok " $( translate 'NVIDIA driver installed successfully.' ) " | tee -a " $screen_capture "
return 0
}
remove_nvidia_driver() {
complete_nvidia_uninstall
}
install_udev_rules_and_persistenced() {
msg_info " $( translate 'Installing NVIDIA udev rules and persistence service...' ) "
cat >/etc/udev/rules.d/70-nvidia.rules <<'EOF'
# /etc/udev/rules.d/70-nvidia.rules
KERNEL=="nvidia", RUN+="/bin/bash -c '/usr/bin/nvidia-smi -L'"
KERNEL=="nvidia_uvm", RUN+="/bin/bash -c '/usr/bin/nvidia-modprobe -c0 -u'"
EOF
udevadm control --reload-rules
udevadm trigger --subsystem-match= drm --subsystem-match= pci || true
ensure_workdir
cd " $NVIDIA_WORKDIR " || return 1
2026-05-09 18:59:59 +02:00
# Pin to the last release tag so a hostile push to upstream `master`
# can't slip arbitrary code into the install. Bump as needed; the
# `--depth 1` keeps the clone fast. Audit Tier 6 — `nvidia-persistenced`
# git clone sin pinning de versión.
local NVIDIA_PERSISTENCED_TAG = " ${ NVIDIA_PERSISTENCED_TAG :- 575 .64.05 } "
2025-11-30 15:31:19 +01:00
if [[ ! -d nvidia-persistenced ]] ; then
2026-05-09 18:59:59 +02:00
git clone --depth 1 --branch " $NVIDIA_PERSISTENCED_TAG " \
https://github.com/NVIDIA/nvidia-persistenced.git >>" $LOG_FILE " 2>& 1 \
|| git clone --depth 1 https://github.com/NVIDIA/nvidia-persistenced.git >>" $LOG_FILE " 2>& 1 \
|| true
2025-11-30 15:31:19 +01:00
fi
if [[ -d nvidia-persistenced/init ]] ; then
cd nvidia-persistenced/init || return 1
./install.sh >>" $LOG_FILE " 2>& 1 || true
fi
msg_ok " $( translate 'NVIDIA udev rules and persistence service installed.' ) " | tee -a " $screen_capture "
}
apply_nvidia_patch_if_needed() {
2026-06-12 00:05:58 +02:00
# NVIDIA_PATCH_AUTO=yes|no skips the yes/no prompt for non-interactive
# callers; unset preserves the interactive menu behavior.
2026-06-11 23:49:13 +02:00
case " ${ NVIDIA_PATCH_AUTO :- } " in
yes) : ;;
no)
msg_info2 " $( translate 'NVIDIA patch not applied.' ) "
update_component_status "nvidia_driver" "installed" " $CURRENT_DRIVER_VERSION " "gpu" '{"patched":false}'
return 0
;;
*)
if ! hybrid_whiptail_yesno " $( translate 'NVIDIA Patch' ) " \
"\n $( translate 'Do you want to apply the optional NVIDIA patch to remove some GPU limitations?' ) " ; then
msg_info2 " $( translate 'NVIDIA patch not applied.' ) "
update_component_status "nvidia_driver" "installed" " $CURRENT_DRIVER_VERSION " "gpu" '{"patched":false}'
return 0
fi
;;
esac
2025-11-30 15:31:19 +01:00
msg_info " $( translate 'Cloning and applying NVIDIA patch (keylase/nvidia-patch)...' ) "
ensure_workdir
cd " $NVIDIA_WORKDIR " || return 1
2026-05-09 18:59:59 +02:00
# Pin keylase/nvidia-patch to a known-good commit. Override via env var
# for forward-compat as new driver versions land. patch.sh ships a list
# of supported drivers in the repo; if our running driver isn't covered
# the patch silently no-ops, so we surface a warning before running.
# Audit Tier 6 — `keylase/nvidia-patch` sin pinning + sin compat check.
local NVIDIA_PATCH_REF = " ${ NVIDIA_PATCH_REF :- master } "
2025-11-30 15:31:19 +01:00
if [[ ! -d nvidia-patch ]] ; then
2026-05-09 18:59:59 +02:00
git clone --depth 1 --branch " $NVIDIA_PATCH_REF " \
https://github.com/keylase/nvidia-patch.git >>" $LOG_FILE " 2>& 1 \
|| git clone --depth 1 https://github.com/keylase/nvidia-patch.git >>" $LOG_FILE " 2>& 1 \
|| true
fi
# Best-effort compatibility check: peek the supported-driver list in
# patch.sh and warn if our driver isn't on it.
if [[ -n " $CURRENT_DRIVER_VERSION " && -f nvidia-patch/patch.sh ]] ; then
if ! grep -qF " $CURRENT_DRIVER_VERSION " nvidia-patch/patch.sh 2>/dev/null; then
msg_warn " $( translate 'NVIDIA driver' ) $CURRENT_DRIVER_VERSION $( translate 'is not in the patch.sh supported list. The patch may no-op or fail; review keylase/nvidia-patch README before continuing.' ) "
fi
2025-11-30 15:31:19 +01:00
fi
if [[ -x nvidia-patch/patch.sh ]] ; then
cd nvidia-patch || return 1
./patch.sh >>" $LOG_FILE " 2>& 1 || true
msg_ok " $( translate 'NVIDIA patch applied - check README for supported versions.' ) "
update_component_status "nvidia_driver" "installed" " $CURRENT_DRIVER_VERSION " "gpu" '{"patched":true}'
else
msg_warn " $( translate 'Could not run NVIDIA patch script. Please verify repository and driver version.' ) "
update_component_status "nvidia_driver" "installed" " $CURRENT_DRIVER_VERSION " "gpu" '{"patched":false}'
fi
}
restart_prompt() {
2025-11-30 18:29:05 +01:00
if hybrid_whiptail_yesno " $( translate 'NVIDIA Drivers' ) " \
"\n $( translate 'The installation/changes require a server restart to apply correctly. Do you want to reboot now?' ) " ; then
2025-11-30 15:31:19 +01:00
msg_success " $( translate 'Installation completed. Press Enter to continue...' ) "
read -r
msg_warn " $( translate 'Restarting the server...' ) "
rm -f " $screen_capture "
reboot
else
msg_success " $( translate 'Installation completed. Please reboot the server manually as soon as possible.' ) "
msg_success " $( translate 'Completed. Press Enter to return to menu...' ) "
read -r
rm -f " $screen_capture "
fi
}
# ==========================================================
# Dialog menus
# ==========================================================
show_action_menu_if_installed() {
if ! $CURRENT_DRIVER_INSTALLED ; then
ACTION = "install"
return 0
fi
local menu_choices =(
"install" " $( translate 'Reinstall/Update NVIDIA drivers' ) "
"remove" " $( translate 'Uninstall NVIDIA drivers and configuration' ) "
)
2026-08-16 10:33:34 +02:00
ACTION = $( hybrid_menu "ProxMenux" " $( translate 'NVIDIA Actions' ) \n\n $( translate 'Choose an action:' ) " 26 80 16 " ${ menu_choices [@] } " ) || ACTION = "cancel"
2025-11-30 15:31:19 +01:00
}
show_install_overview() {
local overview
overview = "\n $( translate 'This installation will:' ) \n\n"
overview += " • $( translate 'Install NVIDIA proprietary drivers' ) \n"
overview += " • $( translate 'Configure GPU passthrough with VFIO' ) \n"
overview += " • $( translate 'Blacklist nouveau driver' ) \n"
2026-04-17 19:02:46 +02:00
overview += " • $( translate 'Enable IOMMU support if not enabled' ) \n"
overview += " • $( translate 'Optionally update NVIDIA libs in LXC containers with passthrough' ) \n\n"
2025-11-30 15:31:19 +01:00
overview += " $( translate 'Detected GPU(s):' ) \n"
2026-04-17 19:02:46 +02:00
overview += "\Zb\Z4 $DETECTED_GPUS_TEXT \Zn\n"
2025-11-30 15:31:19 +01:00
overview += "\n\Zn $( translate 'Current status: ' ) "
2026-04-17 19:02:46 +02:00
overview += "\Zb ${ CURRENT_STATUS_TEXT } \Zn\n"
2025-11-30 15:31:19 +01:00
2026-04-17 19:02:46 +02:00
# Scan for LXC containers with NVIDIA passthrough and surface them in the
# overview so the user knows upfront they will be offered a driver update.
find_nvidia_containers
if [[ ${# NVIDIA_CONTAINERS [@] } -gt 0 ]] ; then
overview += "\n $( translate 'LXC containers with NVIDIA passthrough:' ) \n"
local ctid lxc_ver ct_name
for ctid in " ${ NVIDIA_CONTAINERS [@] } " ; do
lxc_ver = $( get_lxc_nvidia_version " $ctid " )
ct_name = $( pct config " $ctid " 2>/dev/null | grep "^hostname:" | awk '{print $2}' )
overview += " \Zb\Z4CT ${ ctid } \Zn ${ ct_name :+( ${ ct_name } ) } — $( translate 'driver:' ) ${ lxc_ver } \n"
done
fi
overview += "\n $( translate 'After confirming, you will be asked to choose the NVIDIA driver version to install.' ) \n\n"
2025-11-30 15:31:19 +01:00
overview += " $( translate 'Do you want to continue?' ) "
2026-04-17 19:02:46 +02:00
hybrid_yesno " $( translate 'NVIDIA GPU Driver Installation' ) " " $overview " 24 90
2025-11-30 15:31:19 +01:00
}
show_version_menu() {
local latest versions_list
local kernel_version
kernel_version = $( uname -r)
2026-08-16 10:33:34 +02:00
show_proxmenux_logo
msg_title " $( translate 'NVIDIA GPU Driver Installation' ) "
2026-08-26 17:23:09 +02:00
msg_info " $( translate 'Fetching NVIDIA driver versions supported by your GPU...' ) "
2025-11-30 15:31:19 +01:00
latest = $( download_latest_version 2>/dev/null)
versions_list = $( list_available_versions 2>/dev/null)
if [[ -z " $latest " ]] && [[ -z " $versions_list " ]] ; then
2026-08-16 10:33:34 +02:00
stop_spinner
2025-11-30 18:29:05 +01:00
hybrid_msgbox " $( translate 'Error' ) " \
2025-11-30 15:31:19 +01:00
" $( translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.' ) \n\nURL: ${ NVIDIA_BASE_URL } " 10 80
DRIVER_VERSION = "cancel"
return 1
fi
if [[ -z " $latest " ]] && [[ -n " $versions_list " ]] ; then
latest = $( echo " $versions_list " | head -n1)
fi
if [[ -n " $latest " ]] && [[ -z " $versions_list " ]] ; then
versions_list = " $latest "
fi
# Clean latest version
latest = $( echo " $latest " | tr -d '[:space:]' )
2025-11-30 18:29:05 +01:00
local current_list = " $versions_list "
2025-11-30 15:31:19 +01:00
2026-05-09 18:59:59 +02:00
if [[ -n " $current_list " ]] ; then
2026-08-16 10:33:34 +02:00
current_list = $( filter_option_c_branch " $current_list " " $CURRENT_DRIVER_VERSION " "" )
2026-05-09 18:59:59 +02:00
fi
2026-08-15 17:33:05 +02:00
# Historically the picker capped candidates at `latest` (from the
# CDN's `latest.txt`) so users never saw versions newer than the
# global "latest". But latest.txt lags the Production Branch head
# (595.91.07 today vs 595.84 in latest.txt) and also hides the New
# Feature Branch head (610.x) that is a legitimate option once
# kernel + GPU compat pass. Kernel floor, endorsement whitelist,
# release-count heuristic and GPU-compat filter already narrow the
# list to safe candidates; the Production head still stands out in
# the "Latest available" recommendation, so an artificial ceiling
# only masked valid options.
2025-12-11 22:07:01 +01:00
2026-05-09 18:59:59 +02:00
# If the user has the keylase NVENC patch applied, only offer versions
# that the patch supports — picking an unsupported version reinstalls
# the driver fine but the patch silently no-ops afterwards, so the
# user loses NVENC limit removal without warning.
local patch_filtered = false
local patch_filter_note = ""
if is_current_nvidia_patched && [[ -n " $current_list " ]] ; then
if refresh_keylase_patch_cache; then
local trimmed
trimmed = $( filter_keylase_supported " $current_list " )
if [[ -n " $trimmed " ]] ; then
current_list = " $trimmed "
patch_filtered = true
else
patch_filter_note = " $( translate 'No version in this branch is currently supported by keylase/nvidia-patch — the NVENC patch will not reapply after reinstall.' ) "
fi
else
patch_filter_note = " $( translate 'Could not fetch keylase/nvidia-patch supported list — patch reapply compatibility is not verified.' ) "
fi
fi
2026-08-15 17:33:05 +02:00
# Pick the default "Recommended" version. Three-tier priority so the
# picker stays consistent with what the Monitor's driver-update
# notification promised the user:
# 1. If a driver is already installed AND its branch is still
# offered in the filtered list, recommend the highest release
# of that same branch (bugfix upgrade in place). Matches the
# Monitor's Hardware card, which surfaces "v580.178.04
# available" for a 580.x install — the user hitting Actualizar
# then expects to land on 580.178.04, not a cross-branch jump
# to Production. Cross-branch is still one row away in the
# list.
# 2. Fresh install (no current driver) → Production Branch head
# from NVIDIA's Unix drivers page, when present in the list.
# 3. Fallback → highest numeric in the list (Production may have
2026-08-26 17:23:09 +02:00
# been filtered out by maintained-branch / GPU PCI-ID / patch
2026-08-15 17:33:05 +02:00
# awareness).
latest = ""
if [[ -n " $CURRENT_DRIVER_VERSION " && -n " $current_list " ]] ; then
local _cur_branch = " ${ CURRENT_DRIVER_VERSION %%.* } "
if [[ -n " $_cur_branch " ]] ; then
local _same_branch_head
_same_branch_head = $( printf '%s\n' " $current_list " \
| awk -F. -v b = " $_cur_branch " '$1 == b { print; exit }' \
| tr -d '[:space:]' )
if [[ -n " $_same_branch_head " ]] ; then
latest = " $_same_branch_head "
fi
fi
fi
if [[ -z " $latest " ]] ; then
local prod_head = ""
prod_head = $( get_nvidia_production_head 2>/dev/null) || prod_head = ""
if [[ -n " $prod_head " && -n " $current_list " ]] ; then
if printf '%s\n' " $current_list " | grep -qFx " $prod_head " ; then
latest = " $prod_head "
fi
fi
fi
if [[ -z " $latest " && -n " $current_list " ]] ; then
2026-05-09 18:59:59 +02:00
latest = $( printf '%s\n' " $current_list " | head -n1 | tr -d '[:space:]' )
fi
2025-11-30 18:29:05 +01:00
local menu_text = " $( translate 'Select the NVIDIA driver version to install:' ) \n\n"
2026-08-26 17:23:09 +02:00
menu_text += " $( translate 'Versions shown belong to maintained NVIDIA branches that list your GPU PCI ID. DKMS compilation is the final validation against the running kernel. The recommended version keeps the current branch, or uses the NVIDIA Production Branch on a fresh install.' ) "
2026-05-09 18:59:59 +02:00
if $patch_filtered ; then
menu_text += "\n\n $( translate 'NVENC patch detected — list narrowed to versions supported by keylase/nvidia-patch.' ) "
elif [[ -n " $patch_filter_note " ]] ; then
menu_text += "\n\n ${ patch_filter_note } "
fi
2025-11-30 15:31:19 +01:00
2025-11-30 18:29:05 +01:00
local choices =()
2026-08-16 10:33:34 +02:00
if [[ -n " $latest " ]] ; then
choices +=( " $latest " " $latest — $( translate 'Recommended' ) " )
else
choices +=( "" " $( translate 'Recommended' ) " )
fi
2025-11-30 18:29:05 +01:00
choices +=( "" "" )
2025-11-30 15:31:19 +01:00
2025-11-30 18:29:05 +01:00
if [[ -n " $current_list " ]] ; then
while IFS = read -r ver; do
[[ -z " $ver " ]] && continue
ver = $( echo " $ver " | tr -d '[:space:]' )
[[ -z " $ver " ]] && continue
choices +=( " $ver " " $ver " )
done <<< " $current_list "
else
2026-08-26 17:23:09 +02:00
choices +=( "" " $( translate 'No supported NVIDIA versions found for this GPU' ) " )
2025-11-30 18:29:05 +01:00
fi
2025-11-30 15:31:19 +01:00
2026-08-16 10:33:34 +02:00
stop_spinner
2025-11-30 18:29:05 +01:00
local selection = $( hybrid_menu " $( translate 'NVIDIA Driver Version' ) " " $menu_text " 26 90 16 " ${ choices [@] } " ) || { DRIVER_VERSION = "cancel" ; return 1; }
2025-11-30 15:31:19 +01:00
2025-11-30 18:29:05 +01:00
case " $selection " in
"" )
DRIVER_VERSION = "cancel"
return 1
;;
*)
2026-08-16 10:33:34 +02:00
DRIVER_VERSION = $( echo " $selection " | tr -d '[:space:]' )
2025-11-30 18:29:05 +01:00
return 0
;;
esac
2025-11-30 15:31:19 +01:00
}
# ==========================================================
# Main flow
# ==========================================================
main() {
2026-05-09 18:59:59 +02:00
# Rotate the previous run's log instead of truncating — when the
# current install fails, the user can compare against the previous
# attempt to see what changed. Audit Tier 7 — log truncation.
if [[ -f " $LOG_FILE " && -s " $LOG_FILE " ]] ; then
cp -p " $LOG_FILE " " ${ LOG_FILE } .prev" 2>/dev/null || true
fi
2025-11-30 15:31:19 +01:00
: >" $LOG_FILE "
: >" $screen_capture "
2026-03-26 18:18:26 +01:00
NOUVEAU_STILL_LOADED = false
2025-11-30 15:31:19 +01:00
detect_nvidia_gpus
detect_driver_status
2026-04-12 20:32:34 +02:00
check_gpu_not_in_vm_passthrough
2026-08-17 12:05:40 +02:00
check_stale_vfio_config_for_nvidia
2025-11-30 15:31:19 +01:00
if ! $NVIDIA_GPU_PRESENT ; then
dialog --backtitle "ProxMenux" --title " $( translate 'NVIDIA GPU Driver Installation' ) " --msgbox \
"\n $( translate 'No NVIDIA GPU has been detected on this system. The installer will now exit.' ) " 20 70
exit 1
fi
show_action_menu_if_installed
case " $ACTION " in
install)
if ! show_install_overview; then
exit 0
fi
2026-08-26 17:23:09 +02:00
get_system_info
2025-11-30 15:31:19 +01:00
show_version_menu
if [[ " $DRIVER_VERSION " == "cancel" || -z " $DRIVER_VERSION " ]] ; then
exit 0
fi
if $CURRENT_DRIVER_INSTALLED ; then
if [[ " $CURRENT_DRIVER_VERSION " == " $DRIVER_VERSION " ]] ; then
2025-11-30 18:29:05 +01:00
local confirm_text
confirm_text = "\n\n\n $( translate 'Version' ) \Zb\Z4 $DRIVER_VERSION \Zn\n\n $( translate 'is already installed. Do you want to reinstall it? This will perform a clean uninstall first.' ) "
if ! hybrid_yesno " $( translate 'Same Version Detected' ) " " $confirm_text " 14 70; then
2025-11-30 15:31:19 +01:00
exit 0
fi
else
2025-11-30 18:29:05 +01:00
local confirm_text
confirm_text = "\n\n $( translate 'Current version:' ) \Zb $CURRENT_DRIVER_VERSION \Zn\n"
confirm_text += " $( translate 'New version:' ) \Zb\Z4 $DRIVER_VERSION \Zn\n\n"
confirm_text += " $( translate 'The current driver will be completely uninstalled before installing the new version. Continue?' ) "
if ! hybrid_yesno " $( translate 'Version Change Detected' ) " " $confirm_text " 20 70; then
2025-11-30 15:31:19 +01:00
exit 0
fi
fi
show_proxmenux_logo
msg_title " $( translate " $SCRIPT_TITLE " ) "
2026-04-19 02:01:04 +02:00
msg_info2 " $( translate 'Uninstalling current NVIDIA driver before installing new version' ) "
2025-11-30 15:31:19 +01:00
complete_nvidia_uninstall
sleep 2
CURRENT_DRIVER_INSTALLED = false
CURRENT_DRIVER_VERSION = ""
fi
show_proxmenux_logo
msg_title " $( translate " $SCRIPT_TITLE " ) "
ensure_repos_and_headers
blacklist_nouveau
ensure_modules_config
stop_and_disable_nvidia_services
unload_nvidia_modules
local installer
2026-04-17 18:35:42 +02:00
installer = $( download_nvidia_installer " $DRIVER_VERSION " )
2025-11-30 15:31:19 +01:00
local download_result = $?
2026-04-17 18:35:42 +02:00
2025-11-30 15:31:19 +01:00
if [[ $download_result -ne 0 ]] ; then
msg_error " $( translate 'Failed to download NVIDIA installer' ) "
exit 1
fi
2026-04-17 18:35:42 +02:00
msg_ok " $( translate 'NVIDIA installer downloaded successfully' ) " | tee -a " $screen_capture "
2025-11-30 15:31:19 +01:00
if [[ -z " $installer " || ! -f " $installer " ]] ; then
msg_error " $( translate 'Internal error: NVIDIA installer path is empty or file not found.' ) "
rm -f " $screen_capture "
exit 1
fi
if ! run_nvidia_installer " $installer " ; then
rm -f " $screen_capture "
exit 1
fi
sleep 2
show_proxmenux_logo
msg_title " $( translate " $SCRIPT_TITLE " ) "
cat " $screen_capture "
echo -e " ${ TAB }${ GN } 📄 $( translate "Log file" ) ${ CL } : ${ BL } $LOG_FILE ${ CL } "
install_udev_rules_and_persistenced
msg_info " $( translate 'Updating initramfs for all kernels...' ) "
update-initramfs -u -k all >>" $LOG_FILE " 2>& 1 || true
2026-06-12 20:05:17 +02:00
proxmox-boot-tool refresh >>" $LOG_FILE " 2>& 1 || true
2025-11-30 15:31:19 +01:00
msg_ok " $( translate 'initramfs updated.' ) "
msg_info2 " $( translate 'Checking NVIDIA driver status with nvidia-smi' ) "
if command -v nvidia-smi >/dev/null 2>& 1; then
nvidia-smi || true
CURRENT_DRIVER_VERSION = $( nvidia-smi --query-gpu= driver_version --format= csv,noheader 2>/dev/null | head -n1)
CURRENT_DRIVER_INSTALLED = true
else
msg_warn " $( translate 'nvidia-smi not found in PATH. Please verify the driver installation.' ) "
fi
if [[ -n " $CURRENT_DRIVER_VERSION " ]] ; then
msg_ok " $( translate 'NVIDIA driver' ) $CURRENT_DRIVER_VERSION $( translate 'installed successfully.' ) "
update_component_status "nvidia_driver" "installed" " $CURRENT_DRIVER_VERSION " "gpu" '{"patched":false}'
msg_success " $( translate 'Driver installed successfully. Press Enter to continue...' ) "
read -r
else
msg_error " $( translate 'Failed to detect installed NVIDIA driver version.' ) "
update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}'
fi
2026-04-17 19:02:46 +02:00
# Propagate the new driver to LXC containers with NVIDIA passthrough, if any.
# Uses the same .run installer cached in $NVIDIA_WORKDIR — runs only if the
# host install succeeded and the user confirms.
if [[ -n " $CURRENT_DRIVER_VERSION " ]] ; then
offer_lxc_updates_if_any " $CURRENT_DRIVER_VERSION "
fi
2025-11-30 15:31:19 +01:00
apply_nvidia_patch_if_needed
restart_prompt
;;
remove)
2025-11-30 18:29:05 +01:00
if hybrid_yesno " $( translate 'NVIDIA Driver Uninstall' ) " \
2025-11-30 15:31:19 +01:00
"\n\n\n $( translate 'This will remove NVIDIA drivers and related configuration. Do you want to continue?' ) " 14 70; then
show_proxmenux_logo
msg_title " $( translate " $SCRIPT_TITLE " ) "
remove_nvidia_driver
msg_info " $( translate 'Updating initramfs for all kernels...' ) "
update-initramfs -u -k all >>" $LOG_FILE " 2>& 1 || true
2026-06-12 20:05:17 +02:00
proxmox-boot-tool refresh >>" $LOG_FILE " 2>& 1 || true
2025-11-30 15:31:19 +01:00
msg_ok " $( translate 'initramfs updated.' ) "
restart_prompt
fi
;;
cancel| *)
exit 0
;;
esac
}
2026-06-09 17:42:51 +02:00
# ==========================================================
# Non-interactive auto-reinstall entry point
# ==========================================================
# Invoked after a host-config restore by apply_cluster_postboot.sh
# when components_status.json reports nvidia_driver as installed
# but the kernel module isn't loaded on the live system (i.e. the
# restore brought back the configs but not the binary driver from
# /lib/modules/<kernel>/). Replays the install path the user
# originally ran via `menu → 2`, using the recorded version, with
# no dialogs.
#
# Exit codes:
# 0 installed (or no-op — GPU absent / driver already present)
# 1 state file unreadable or no nvidia_driver entry
# 2 install failed
auto_reinstall_from_state() {
: >" $LOG_FILE "
echo "=== auto_reinstall_from_state started $( date -Iseconds) ===" >>" $LOG_FILE "
if ! command -v jq >/dev/null 2>& 1; then
echo "jq not available — cannot read components_status.json" | tee -a " $LOG_FILE "
return 1
fi
if [[ ! -f " $COMPONENTS_STATUS_FILE " ]] ; then
echo "No components_status.json at $COMPONENTS_STATUS_FILE " | tee -a " $LOG_FILE "
return 1
fi
2026-06-11 23:49:13 +02:00
local recorded_status recorded_version recorded_patched
recorded_status = $( jq -r '.nvidia_driver.status // ""' " $COMPONENTS_STATUS_FILE " 2>/dev/null)
recorded_version = $( jq -r '.nvidia_driver.version // ""' " $COMPONENTS_STATUS_FILE " 2>/dev/null)
recorded_patched = $( jq -r '.nvidia_driver.patched // false' " $COMPONENTS_STATUS_FILE " 2>/dev/null)
2026-06-09 17:42:51 +02:00
if [[ " $recorded_status " != "installed" ]] ; then
echo "nvidia_driver not marked installed in state ( $recorded_status ) — nothing to do" | tee -a " $LOG_FILE "
return 0
fi
if [[ -z " $recorded_version " || " $recorded_version " == "null" ]] ; then
echo "nvidia_driver marked installed but no version recorded — aborting" | tee -a " $LOG_FILE "
return 1
fi
2026-06-11 23:49:13 +02:00
echo "Recorded driver: $recorded_version (patched= $recorded_patched )" >>" $LOG_FILE "
2026-06-09 17:42:51 +02:00
detect_nvidia_gpus
if ! $NVIDIA_GPU_PRESENT ; then
echo "No NVIDIA GPU detected on this host — skipping reinstall" | tee -a " $LOG_FILE "
return 0
fi
2026-06-21 22:41:54 +02:00
# Skip when the host is configured for VFIO passthrough. Two
# signals — config wins over runtime:
#
# 1. modprobe.d declares the nvidia* modules blacklisted (the
# file ProxMenux drops when switching the GPU to VM mode is
# `proxmenux-nvidia-vfio-blacklist.conf`, but any blacklist
# file counts).
# 2. The PCI device is bound to vfio-pci.
#
# The config check has to come FIRST because right after a host
# restore + reboot, the binding may not yet be effective (the
# GPU temporarily shows "no driver" while udev/initramfs settle).
# Reinstalling NVIDIA in that window forces the module onto a
# PCI device vfio-pci has already reserved → "NVRM: Try unloading
# the conflicting kernel module" and exit 1.
if [[ -f /etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf ]] \
|| grep -qrhE '^[[:space:]]*blacklist[[:space:]]+nvidia([[:space:]_]|$)' \
/etc/modprobe.d/ 2>/dev/null; then
echo "NVIDIA blacklisted (host is VFIO-configured) — skipping host driver reinstall" \
| tee -a " $LOG_FILE "
return 0
fi
2026-06-12 17:19:20 +02:00
local _vfio_dev
for _vfio_dev in /sys/bus/pci/devices/*; do
[[ " $( cat " $_vfio_dev /vendor" 2>/dev/null) " == "0x10de" ]] || continue
if [[ -L " $_vfio_dev /driver" ]] \
&& [[ " $( basename " $( readlink " $_vfio_dev /driver" ) " ) " == "vfio-pci" ]] ; then
echo "NVIDIA GPU bound to vfio-pci — skipping host driver reinstall" | tee -a " $LOG_FILE "
return 0
fi
done
2026-06-09 17:42:51 +02:00
detect_driver_status
if $CURRENT_DRIVER_INSTALLED && [[ " $CURRENT_DRIVER_VERSION " == " $recorded_version " ]] ; then
echo "Driver $recorded_version already installed and matches state — no-op" | tee -a " $LOG_FILE "
return 0
fi
DRIVER_VERSION = " $recorded_version "
# Same install path as the interactive main() flow, minus all
# dialogs and confirmations.
echo "Reinstalling NVIDIA driver $DRIVER_VERSION non-interactively..." | tee -a " $LOG_FILE "
ensure_workdir
ensure_repos_and_headers >>" $LOG_FILE " 2>& 1
blacklist_nouveau >>" $LOG_FILE " 2>& 1
ensure_modules_config >>" $LOG_FILE " 2>& 1
if $CURRENT_DRIVER_INSTALLED ; then
echo "Different version currently installed; cleaning up first..." | tee -a " $LOG_FILE "
complete_nvidia_uninstall >>" $LOG_FILE " 2>& 1
fi
2026-06-09 19:14:27 +02:00
local installer
installer = $( download_nvidia_installer " $DRIVER_VERSION " 2>>" $LOG_FILE " )
if [[ -z " $installer " || ! -f " $installer " ]] ; then
2026-06-09 17:42:51 +02:00
echo "Download failed — see $LOG_FILE " | tee -a " $LOG_FILE "
return 2
fi
2026-06-09 19:14:27 +02:00
echo "Installer ready: $installer " >>" $LOG_FILE "
if ! run_nvidia_installer " $installer " >>" $LOG_FILE " 2>& 1; then
2026-06-09 17:42:51 +02:00
echo "Install failed — see $LOG_FILE " | tee -a " $LOG_FILE "
return 2
fi
install_udev_rules_and_persistenced >>" $LOG_FILE " 2>& 1
2026-06-12 00:05:58 +02:00
# Preserve the recorded patched state across the reinstall. The patch
# helper writes its own update_component_status on success.
2026-06-11 23:49:13 +02:00
CURRENT_DRIVER_VERSION = " $DRIVER_VERSION "
if [[ " $recorded_patched " == "true" ]] ; then
echo "Recorded state had patched=true — re-applying NVIDIA patch..." | tee -a " $LOG_FILE "
NVIDIA_PATCH_AUTO = yes apply_nvidia_patch_if_needed >>" $LOG_FILE " 2>& 1 || true
else
if declare -F update_component_status >/dev/null 2>& 1; then
update_component_status "nvidia_driver" "installed" " $DRIVER_VERSION " "gpu" '{"patched":false}' >>" $LOG_FILE " 2>& 1
fi
2026-06-09 17:42:51 +02:00
fi
echo "✓ NVIDIA driver $DRIVER_VERSION reinstalled" | tee -a " $LOG_FILE "
return 0
}
2025-11-30 15:31:19 +01:00
if [[ " ${ BASH_SOURCE [0] } " == " $0 " ]] ; then
2026-06-09 17:42:51 +02:00
if [[ " ${ 1 :- } " == "--auto-reinstall" ]] ; then
auto_reinstall_from_state
exit $?
fi
2025-11-30 15:31:19 +01:00
main
2026-08-26 17:23:09 +02:00
fi