From 698c89fd3b51e35b5e8e824afa968d1054f62e9e Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sun, 16 Aug 2026 10:33:34 +0200 Subject: [PATCH] NVIDIA installer: dynamic version picker + Pattern A UX --- AppImage/scripts/managed_installs.py | 98 ++---------- scripts/gpu_tpu/nvidia_installer.sh | 216 ++++++++------------------- 2 files changed, 71 insertions(+), 243 deletions(-) diff --git a/AppImage/scripts/managed_installs.py b/AppImage/scripts/managed_installs.py index 52ecd84a..b7d8f56a 100644 --- a/AppImage/scripts/managed_installs.py +++ b/AppImage/scripts/managed_installs.py @@ -1045,47 +1045,13 @@ _NVIDIA_CACHE_TTL = 7 * 86400 _nvidia_cache: dict[str, Any] = {"versions": [], "fetched_at": 0} -def _nvidia_kernel_compat() -> dict: - """Python port of `get_kernel_compatibility_info` in the bash - installer. Returns ``{kernel, min_version, recommended_branch, - note}``. Kept identical to the bash matrix so the recommendation - here matches what the installer would do.""" +def _kernel_string() -> str: try: - kernel = subprocess.run( + return subprocess.run( ["uname", "-r"], capture_output=True, text=True, timeout=2, ).stdout.strip() except (OSError, subprocess.TimeoutExpired): - kernel = "" - parts = kernel.split(".") if kernel else [] - try: - major = int(parts[0]) if len(parts) >= 1 else 0 - minor = int(parts[1]) if len(parts) >= 2 else 0 - except (ValueError, TypeError): - major, minor = 0, 0 - - if major >= 7 or (major == 6 and minor >= 17): - return { - "kernel": kernel, - "min_version": "580.105.08", - "recommended_branch": "580", - "note": (f"Kernel {kernel} requires NVIDIA driver 580.105.08 or " - f"newer (older 580.x builds fail to compile)"), - } - if major >= 6 and minor >= 8: - return {"kernel": kernel, "min_version": "550", - "recommended_branch": "580", - "note": f"Kernel {kernel} works with NVIDIA driver 550.x or newer"} - if major >= 6: - return {"kernel": kernel, "min_version": "535", - "recommended_branch": "550", - "note": f"Kernel {kernel} works with NVIDIA driver 535.x or newer"} - if major == 5 and minor >= 15: - return {"kernel": kernel, "min_version": "470", - "recommended_branch": "535", - "note": f"Kernel {kernel} works with NVIDIA driver 470.x or newer"} - return {"kernel": kernel, "min_version": "450", - "recommended_branch": "470", - "note": "For older kernels, compatibility may vary"} + return "" def _version_tuple(v: str) -> tuple: @@ -1130,30 +1096,8 @@ def _fetch_nvidia_versions(force: bool = False) -> list[str]: return versions -def _is_compat_with_kernel(version: str, kernel_compat: dict) -> bool: - """Compare ``version`` (e.g. ``580.105.08``) against the kernel - compatibility floor. Mirrors the bash ``is_version_compatible`` - helper (full-triple compare when min is dotted, major-only otherwise).""" - min_str = kernel_compat.get("min_version", "0") - if "." in min_str and re.match(r"^\d+\.\d+\.\d+$", min_str): - return _version_tuple(version) >= _version_tuple(min_str) - # Single-major threshold like "535" or "550" - try: - ver_major = int(version.split(".")[0]) - min_major = int(min_str) - except (ValueError, TypeError): - return True - return ver_major >= min_major - - def _check_nvidia_xfree86(entry: dict) -> dict: - """Compute the update state for a host NVIDIA driver entry. - - Policy (Option C from the design discussion): - 1. Same-branch newer version available → notify. - 2. Current branch no longer compatible with current kernel → - notify a branch upgrade with explicit messaging. - """ + """Report same-branch bugfix upgrades for the installed driver.""" current = entry.get("current_version") if not current or not re.match(r"^\d+\.\d+(\.\d+)?$", current): return {"available": False, "latest": None, @@ -1165,48 +1109,24 @@ def _check_nvidia_xfree86(entry: dict) -> dict: "last_check": _now_iso(), "error": "could not parse upstream version listing"} - kernel_compat = _nvidia_kernel_compat() current_branch = current.split(".")[0] - - same_branch = [v for v in versions if v.split(".")[0] == current_branch - and _is_compat_with_kernel(v, kernel_compat)] + same_branch = [v for v in versions if v.split(".")[0] == current_branch] same_branch_latest = same_branch[0] if same_branch else None - notify_branch_upgrade = False - branch_upgrade_target: Optional[str] = None - if not _is_compat_with_kernel(current, kernel_compat): - # Current branch / version no longer works with current kernel. - # Recommend the kernel-recommended branch's latest. - rec_branch = kernel_compat["recommended_branch"] - rec_branch_versions = [v for v in versions - if v.split(".")[0] == rec_branch - and _is_compat_with_kernel(v, kernel_compat)] - if rec_branch_versions: - branch_upgrade_target = rec_branch_versions[0] - notify_branch_upgrade = True - available = False latest: Optional[str] = None - upgrade_kind = None # "patch" | "branch_upgrade" | None - - if notify_branch_upgrade and branch_upgrade_target: - latest = branch_upgrade_target - available = True - upgrade_kind = "branch_upgrade" - elif same_branch_latest and \ - _version_tuple(same_branch_latest) > _version_tuple(current): + if same_branch_latest and \ + _version_tuple(same_branch_latest) > _version_tuple(current): latest = same_branch_latest available = True - upgrade_kind = "patch" return { "available": available, "latest": latest, "last_check": _now_iso(), "error": None, - "_upgrade_kind": upgrade_kind, - "_kernel": kernel_compat.get("kernel"), - "_kernel_note": kernel_compat.get("note"), + "_upgrade_kind": "patch" if available else None, + "_kernel": _kernel_string(), } diff --git a/scripts/gpu_tpu/nvidia_installer.sh b/scripts/gpu_tpu/nvidia_installer.sh index 02114978..03df5593 100644 --- a/scripts/gpu_tpu/nvidia_installer.sh +++ b/scripts/gpu_tpu/nvidia_installer.sh @@ -660,106 +660,28 @@ ensure_workdir() { } # ========================================================== -# Kernel compatibility detection +# Kernel + system detection # ========================================================== get_kernel_compatibility_info() { local kernel_version kernel_version=$(uname -r) - - # Determine Proxmox and kernel version + if [[ -f /etc/pve/.version ]]; then PVE_VERSION=$(cat /etc/pve/.version) else PVE_VERSION="unknown" fi - - # Extract kernel major version (6.x, 5.x, etc) + KERNEL_MAJOR=$(echo "$kernel_version" | cut -d. -f1) KERNEL_MINOR=$(echo "$kernel_version" | cut -d. -f2) - - # Define minimum compatible versions based on kernel. - # Floor bumped from 580.82.07 → 580.105.08 for kernel 6.17+ after a - # user report (issue tracked as Sprint 11.4) that 580.82-580.95 builds - # fail on kernel 6.17.13 (DKMS module compile errors with the newer - # toolchain shipped with PVE 9.1). 580.105.08 is verified working on - # the test host. Future kernel 7.x falls into the same bucket — the - # `KERNEL_MAJOR -ge 7` branch was previously missing and routed 7.x - # kernels to MIN=535 incorrectly. - if { [[ "$KERNEL_MAJOR" -ge 7 ]]; } || \ - { [[ "$KERNEL_MAJOR" -eq 6 ]] && [[ "$KERNEL_MINOR" -ge 17 ]]; }; then - # Kernel 6.17+ / 7.x (Proxmox 9.x +) - Requires 580.105.08 or higher - MIN_DRIVER_VERSION="580.105.08" - RECOMMENDED_BRANCH="580" - COMPATIBILITY_NOTE="Kernel $kernel_version requires NVIDIA driver 580.105.08 or newer (older 580.x builds fail to compile)" - elif [[ "$KERNEL_MAJOR" -ge 6 ]] && [[ "$KERNEL_MINOR" -ge 8 ]]; then - # Kernel 6.8-6.16 (Proxmox 8.2+) - Works with 550.x or higher - MIN_DRIVER_VERSION="550" - RECOMMENDED_BRANCH="580" - COMPATIBILITY_NOTE="Kernel $kernel_version works best with NVIDIA driver 550.x or newer" - elif [[ "$KERNEL_MAJOR" -ge 6 ]]; then - # Kernel 6.2-6.7 (Proxmox 8.x initial) - Works with 535.x or higher - MIN_DRIVER_VERSION="535" - RECOMMENDED_BRANCH="550" - COMPATIBILITY_NOTE="Kernel $kernel_version works with NVIDIA driver 535.x or newer" - elif [[ "$KERNEL_MAJOR" -eq 5 ]] && [[ "$KERNEL_MINOR" -ge 15 ]]; then - # Kernel 5.15+ (Proxmox 7.x, 8.x legacy) - Works with 470.x or higher - MIN_DRIVER_VERSION="470" - RECOMMENDED_BRANCH="535" - COMPATIBILITY_NOTE="Kernel $kernel_version works with NVIDIA driver 470.x or newer" - else - # Old kernels - MIN_DRIVER_VERSION="450" - RECOMMENDED_BRANCH="470" - COMPATIBILITY_NOTE="For older kernels, compatibility may vary" - fi + + MIN_DRIVER_VERSION="" + RECOMMENDED_BRANCH="" + COMPATIBILITY_NOTE="" } is_version_compatible() { - local version="$1" - local ver_major ver_minor ver_patch - - # Extract version components (major.minor.patch) - ver_major=$(echo "$version" | cut -d. -f1) - ver_minor=$(echo "$version" | cut -d. -f2) - ver_patch=$(echo "$version" | cut -d. -f3) - - # Full-version comparison when MIN is dotted (e.g. "580.105.08"). - # Strips the dotted threshold from MIN_DRIVER_VERSION and reuses the - # existing `version_le` helper. The previous code had a hardcoded - # branch only for "580.82.07" — bumping the floor required editing two - # places. Sprint 11.4. - case "$MIN_DRIVER_VERSION" in - *.*.*) - # Dotted threshold: compare full triple. - local _min_major _min_minor _min_patch - IFS='.' read -r _min_major _min_minor _min_patch <<<"$MIN_DRIVER_VERSION" - _min_major=${_min_major:-0} - _min_minor=${_min_minor:-0} - _min_patch=${_min_patch:-0} - ver_minor=${ver_minor:-0} - ver_patch=${ver_patch:-0} - if (( 10#$ver_major > 10#$_min_major )); then - return 0 - elif (( 10#$ver_major == 10#$_min_major )); then - if (( 10#$ver_minor > 10#$_min_minor )); then - return 0 - elif (( 10#$ver_minor == 10#$_min_minor )); then - if (( 10#${ver_patch:-0} >= 10#$_min_patch )); then - return 0 - fi - fi - fi - return 1 - ;; - *) - # Single-major threshold (e.g. "550", "535"): compare major only. - if [[ ${ver_major} -ge ${MIN_DRIVER_VERSION} ]]; then - return 0 - else - return 1 - fi - ;; - esac + return 0 } @@ -821,19 +743,35 @@ refresh_nvidia_branches_cache() { # it (separate alternative below). # HTML comments are stripped first so vestigial `` blocks in older ia32 rows don't leak stale majors. + # 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. echo "$clean" \ - | grep -oiE '(Production Branch Version|New Feature Branch Version|Legacy GPU version \([0-9]+\.xx series\)):[^<]*()?\s*]*>[0-9]+\.[0-9]+(\.[0-9]+)?' \ - | grep -oE '>[0-9]+\.[0-9]+(\.[0-9]+)?' \ - | tr -d '>' \ - | awk -F. '{ printf "%s|%s\n", $1, $0 }' \ - | sort -u -t'|' -k1,1 > "$tmp_full" + | perl -ne ' + while (/(Production Branch Version|New Feature Branch Version|Legacy GPU version \(([0-9]+)\.xx series\)):[^<]*(?:<\/span>)?\s*]*>([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" if [[ ! -s "$tmp_full" ]]; then rm -f "$tmp" "$tmp_full" return 1 fi - # Derive the majors-only file from the same source so both caches - # never disagree. - cut -d'|' -f1 "$tmp_full" | sort -un > "$tmp" + # 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" if [[ ! -s "$tmp" ]]; then rm -f "$tmp" "$tmp_full" return 1 @@ -861,12 +799,13 @@ refresh_nvidia_branches_cache() { # Return the full head version associated with a major from the # branch-heads cache (e.g. `get_nvidia_branch_head 595` → 595.91.07). -# Used to know which release inside a branch to hit for the PCI-ID -# supported-GPUs list. +# 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. get_nvidia_branch_head() { local major="$1" [[ -f "$NVIDIA_BRANCH_HEADS_CACHE" && -s "$NVIDIA_BRANCH_HEADS_CACHE" ]] || return 1 - awk -F'|' -v m="$major" '$1 == m { print $2; exit }' "$NVIDIA_BRANCH_HEADS_CACHE" + awk -F'|' -v m="$major" '$2 == m { print $3; exit }' "$NVIDIA_BRANCH_HEADS_CACHE" } # Refresh the per-branch supported-GPU cache. Uses the branch head as @@ -1010,41 +949,14 @@ filter_keylase_supported() { filter_option_c_branch() { local versions_in="$1" local current="$2" - local recommended_branch="$3" - local target_branch="" + local _unused_recommended_branch="$3" - if [[ -n "$current" && "$current" =~ ^([0-9]+)\. ]]; then - local current_branch="${BASH_REMATCH[1]}" - if is_version_compatible "$current"; then - target_branch="$current_branch" - fi - fi - - if [[ -z "$target_branch" ]]; then - target_branch="$recommended_branch" - fi - - if [[ -z "$target_branch" ]]; then - printf '%s\n' "$versions_in" - return 0 - fi - - # Four-way gate for every candidate version: - # 1. `major >= target_branch` — kernel floor. - # 2. Branch is currently endorsed on NVIDIA's Unix drivers page - # (Production / New Feature / Legacy heads) OR was substantial - # enough to accumulate ≥ NVIDIA_BRANCH_MIN_RELEASES releases on - # the CDN. The endorsement path always passes; the release-count - # path lets superseded production branches (580, 570, 550, 535 … - # still maintained via bugfix releases) stay selectable while - # Vulkan-beta / developer branches with 1-4 releases (590, 565, - # 530 …) get dropped. - # 3. `is_branch_compatible_with_host_gpus` — every detected NVIDIA - # GPU on this host must appear in that branch's supportedchips - # list. A host with a Kepler card ends up with 470.x only. - # All three fail open when their caches / lookups miss, so the picker - # never empties on a network glitch. refresh_nvidia_branches_cache 2>/dev/null || true + + local target_branch="" + if [[ -f "$NVIDIA_BRANCHES_CACHE" && -s "$NVIDIA_BRANCHES_CACHE" ]]; then + target_branch=$(head -n1 "$NVIDIA_BRANCHES_CACHE") + fi # 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. @@ -1103,11 +1015,12 @@ filter_option_c_branch() { while IFS= read -r ver; do [[ -z "$ver" ]] && continue local ver_major="${ver%%.*}" - if (( 10#$ver_major >= 10#$target_branch )); then - 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" - fi + 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" fi fi done <<< "$versions_in" @@ -1540,7 +1453,7 @@ show_action_menu_if_installed() { "remove" "$(translate 'Uninstall NVIDIA drivers and configuration')" ) - ACTION=$(hybrid_menu "ProxMenux" "$(translate 'NVIDIA Actions')\n\n$(translate 'Choose an action:')" 14 80 8 "${menu_choices[@]}") || ACTION="cancel" + ACTION=$(hybrid_menu "ProxMenux" "$(translate 'NVIDIA Actions')\n\n$(translate 'Choose an action:')" 26 80 16 "${menu_choices[@]}") || ACTION="cancel" } show_install_overview() { @@ -1581,15 +1494,16 @@ show_version_menu() { local latest versions_list local kernel_version kernel_version=$(uname -r) - + + show_proxmenux_logo + msg_title "$(translate 'NVIDIA GPU Driver Installation')" + msg_info "$(translate 'Fetching compatible driver versions for your kernel and GPU...')" latest=$(download_latest_version 2>/dev/null) - - versions_list=$(list_available_versions 2>/dev/null) - if [[ -z "$latest" ]] && [[ -z "$versions_list" ]]; then + stop_spinner hybrid_msgbox "$(translate 'Error')" \ "$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80 DRIVER_VERSION="cancel" @@ -1623,13 +1537,8 @@ show_version_menu() { current_list="$filtered_list" fi - # Option C: kernel-compat alone is too permissive (e.g. kernel 6.14 - # accepts ≥ 550 so 595.x shows up — but 595.x has historically broken - # builds on this kernel). Restrict the offered list to the user's - # current branch when their installed driver still works, otherwise - # fall back to the recommended branch for the kernel. if [[ -n "$current_list" ]]; then - current_list=$(filter_option_c_branch "$current_list" "$CURRENT_DRIVER_VERSION" "$RECOMMENDED_BRANCH") + current_list=$(filter_option_c_branch "$current_list" "$CURRENT_DRIVER_VERSION" "") fi # Historically the picker capped candidates at `latest` (from the @@ -1715,7 +1624,11 @@ show_version_menu() { fi local choices=() - choices+=("latest" "$(translate 'Recommended') (${latest:-unknown})") + if [[ -n "$latest" ]]; then + choices+=("$latest" "$latest — $(translate 'Recommended')") + else + choices+=("" "$(translate 'Recommended')") + fi choices+=("" "") if [[ -n "$current_list" ]]; then @@ -1730,6 +1643,7 @@ show_version_menu() { choices+=("" "$(translate 'No compatible versions found for your kernel')") fi + stop_spinner local selection=$(hybrid_menu "$(translate 'NVIDIA Driver Version')" "$menu_text" 26 90 16 "${choices[@]}") || { DRIVER_VERSION="cancel"; return 1; } case "$selection" in @@ -1737,14 +1651,8 @@ show_version_menu() { DRIVER_VERSION="cancel" return 1 ;; - latest) - DRIVER_VERSION="$latest" - DRIVER_VERSION=$(echo "$DRIVER_VERSION" | tr -d '[:space:]') - return 0 - ;; *) - DRIVER_VERSION="$selection" - DRIVER_VERSION=$(echo "$DRIVER_VERSION" | tr -d '[:space:]') + DRIVER_VERSION=$(echo "$selection" | tr -d '[:space:]') return 0 ;; esac