From 63a120d66c12bc3acf1f998f91a7747b9dc49552 Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 21:11:41 +0200 Subject: [PATCH 01/15] feat: UI redesign, GitHub Actions CI, security hardening, beta branch - Redesign settings page with card-based layout and full Unraid theme support (dark/light), responsive 2-column grid, GPU arch & CUDA info - Add GitHub Actions CI with JSON-driven build matrix, auto-detection of new driver versions/kernels, and skip-check for existing builds - Add beta branch support to download.sh, update-check.sh, exec.sh - Security: input validation in update_version(), change_update_check() to prevent command injection; function whitelist replacing $@ dispatcher; safe file cleanup preventing mass deletion when variables are empty - Fix: extract FETCH_VERSIONS() to eliminate redundant API call - Fix: $DRIVERS variable scope bug in legacy 580 driver check - Integrate upstream card-support.php for GPU architecture detection --- .github/workflows/auto-update-matrix.yml | 243 +++++++ .github/workflows/build-nvidia.yml | 219 ++++++ build-matrix.json | 60 ++ .../plugins/nvidia-driver/include/download.sh | 19 +- .../plugins/nvidia-driver/include/exec.sh | 73 +- .../nvidia-driver/include/update-check.sh | 19 +- .../plugins/nvidia-driver/nvidia-driver.page | 669 ++++++++---------- 7 files changed, 901 insertions(+), 401 deletions(-) create mode 100644 .github/workflows/auto-update-matrix.yml create mode 100644 .github/workflows/build-nvidia.yml create mode 100644 build-matrix.json diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml new file mode 100644 index 0000000..bf23adf --- /dev/null +++ b/.github/workflows/auto-update-matrix.yml @@ -0,0 +1,243 @@ +name: Auto-detect and update build matrix + +on: + schedule: + - cron: '0 4 * * *' # Daily at 04:00 UTC + workflow_dispatch: + +permissions: + contents: write + +jobs: + detect-and-update: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install dependencies + run: sudo apt-get -y install jq tidy wget + + - name: Detect NVIDIA driver versions + id: nvidia + run: | + RAW_DATA="$(wget -qO- https://www.nvidia.com/en-us/drivers/unix/ | tidy -quiet -wrap 4096 2>/dev/null | grep -A8 "Linux x86_64/AMD64/EM64T")" + PRB="$(echo "${RAW_DATA}" | grep -i "Latest Production Branch" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + NFB="$(echo "${RAW_DATA}" | grep -i "Latest New Feature Branch" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + BETA="$(echo "${RAW_DATA}" | grep -i "Latest Beta" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + LEGACY="$(echo "${RAW_DATA}" | grep -i "Latest Legacy" | grep "(4" | head -1 | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + + # Fallback to NVIDIA developer forum + if [ -z "${RAW_DATA}" ]; then + RAW_DATA="$(wget -qO- https://forums.developer.nvidia.com/t/current-graphics-driver-releases/28500 | tidy -quiet -wrap 4096 2>/dev/null || true)" + PRB="$(echo "${RAW_DATA}" | grep -i "^Current production branch" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + NFB="$(echo "${RAW_DATA}" | grep -i "^Current new feature branch" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + BETA="$(echo "${RAW_DATA}" | grep -i "^Current beta" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + LEGACY="$(echo "${RAW_DATA}" | grep -i -A1 '>Legacy releases' | tail -1 | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" + fi + + if [ -z "${RAW_DATA}" ]; then + echo "::error::Failed to fetch NVIDIA driver data from both sources" + exit 1 + fi + + echo "prb=${PRB}" >> "$GITHUB_OUTPUT" + echo "nfb=${NFB}" >> "$GITHUB_OUTPUT" + echo "beta=${BETA}" >> "$GITHUB_OUTPUT" + echo "legacy=${LEGACY}" >> "$GITHUB_OUTPUT" + echo "Detected: PRB=${PRB} NFB=${NFB} BETA=${BETA} LEGACY=${LEGACY}" + + - name: Detect latest Unraid kernel versions + id: kernels + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get kernel tags from upstream releases (format: 6.x.y-Unraid) + KERNEL_TAGS=$(gh api repos/unraid/unraid-nvidia-driver/releases \ + --paginate --jq '.[].tag_name' 2>/dev/null \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-Unraid$' \ + | sort -t. -k1,1nr -k2,2nr -k3,3nr \ + | head -2) + + if [ -z "$KERNEL_TAGS" ]; then + echo "::warning::No kernel tags found, keeping current kernel_versions" + echo "kernels=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + KERNEL_JSON=$(echo "$KERNEL_TAGS" | jq -R -c -s 'split("\n") | map(select(length > 0))') + echo "kernels=${KERNEL_JSON}" >> "$GITHUB_OUTPUT" + echo "Detected kernels: ${KERNEL_JSON}" + + - name: Detect container runtime versions + id: runtimes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CONTAINER_TOOLKIT=$(gh api repos/unraid/nvidia-container-toolkit/releases/latest --jq '.tag_name' 2>/dev/null || echo "") + LIBNVIDIA=$(gh api repos/unraid/libnvidia-container/releases/latest --jq '.tag_name' 2>/dev/null || echo "") + + # Ensure libnvidia-container matches toolkit version to prevent mismatches. + # Both repos release in sync; if versions differ, use the toolkit version + # and verify the matching libnvidia-container release exists. + if [ -n "${CONTAINER_TOOLKIT}" ] && [ -n "${LIBNVIDIA}" ] && [ "${CONTAINER_TOOLKIT}" != "${LIBNVIDIA}" ]; then + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/unraid/libnvidia-container/releases/tags/${CONTAINER_TOOLKIT}") + if [ "${HTTP_CODE}" == "200" ]; then + LIBNVIDIA="${CONTAINER_TOOLKIT}" + fi + fi + + echo "toolkit=${CONTAINER_TOOLKIT}" >> "$GITHUB_OUTPUT" + echo "libnvidia=${LIBNVIDIA}" >> "$GITHUB_OUTPUT" + echo "Detected: toolkit=${CONTAINER_TOOLKIT} libnvidia=${LIBNVIDIA}" + + - name: Detect latest GCC tag + id: gcc + run: | + TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:ich777/unraid_kernel:pull" | jq -r '.token') + GCC_TAG=$(curl -s -H "Authorization: Bearer $TOKEN" "https://ghcr.io/v2/ich777/unraid_kernel/tags/list" \ + | jq -r '.tags[]' | grep '^gcc_' | sort -V | tail -1) + + if [ -z "${GCC_TAG}" ]; then + echo "::warning::Could not detect GCC tag, keeping current" + GCC_TAG=$(jq -r '.gcc_tag' build-matrix.json) + fi + + echo "gcc_tag=${GCC_TAG}" >> "$GITHUB_OUTPUT" + echo "Detected GCC tag: ${GCC_TAG}" + + - name: Update build-matrix.json + env: + NEW_PRB: ${{ steps.nvidia.outputs.prb }} + NEW_NFB: ${{ steps.nvidia.outputs.nfb }} + NEW_BETA: ${{ steps.nvidia.outputs.beta }} + NEW_KERNELS: ${{ steps.kernels.outputs.kernels }} + NEW_GCC: ${{ steps.gcc.outputs.gcc_tag }} + run: | + CUR_PRB=$(jq -r '.branches.production.driver_version' build-matrix.json) + CUR_NFB=$(jq -r '.branches.newfeature.driver_version' build-matrix.json) + CUR_BETA=$(jq -r '.branches.beta.driver_version' build-matrix.json) + + jq --arg new_prb "${NEW_PRB}" \ + --arg new_nfb "${NEW_NFB}" \ + --arg new_beta "${NEW_BETA}" \ + --arg new_gcc "${NEW_GCC}" \ + --arg cur_prb "${CUR_PRB}" \ + --arg cur_nfb "${CUR_NFB}" \ + --arg cur_beta "${CUR_BETA}" \ + --argjson new_kernels "${NEW_KERNELS:-null}" \ + ' + # Update gcc_tag + (if ($new_gcc != "") then .gcc_tag = $new_gcc else . end) + + # Update kernel versions if detected + | (if $new_kernels != null and ($new_kernels | length > 0) then + .kernel_versions = $new_kernels + else . end) + + # Rotate production: old version → extra_builds, set new version + | (if ($new_prb != "") and ($cur_prb != $new_prb) then + (if (.extra_builds | map(.driver_version) | index($cur_prb) | not) then + .extra_builds += [{"driver_version": $cur_prb, "module_types": ["proprietary", "opensource"]}] + else . end) + | .branches.production.driver_version = $new_prb + else . end) + + # Rotate newfeature: old version → extra_builds, set new version + | (if ($new_nfb != "") and ($cur_nfb != $new_nfb) then + (if (.extra_builds | map(.driver_version) | index($cur_nfb) | not) then + .extra_builds += [{"driver_version": $cur_nfb, "module_types": ["proprietary", "opensource"]}] + else . end) + | .branches.newfeature.driver_version = $new_nfb + else . end) + + # Rotate beta: old version → extra_builds, set new version + | (if ($new_beta != "") and ($cur_beta != $new_beta) then + (if (.extra_builds | map(.driver_version) | index($cur_beta) | not) then + .extra_builds += [{"driver_version": $cur_beta, "module_types": ["proprietary", "opensource"]}] + else . end) + | .branches.beta.driver_version = $new_beta + else . end) + + # Remove from extra_builds any version that is now a branch version + | .extra_builds = [ + .extra_builds[] | + select( + .driver_version != (.branches.production.driver_version // "") and + .driver_version != (.branches.newfeature.driver_version // "") and + .driver_version != (.branches.beta.driver_version // "") + ) + ] + + # Deduplicate extra_builds + | .extra_builds = (.extra_builds | unique_by(.driver_version)) + ' build-matrix.json > build-matrix.json.tmp && mv build-matrix.json.tmp build-matrix.json + + echo "Updated build-matrix.json:" + cat build-matrix.json + + - name: Update versions.json + env: + NEW_PRB: ${{ steps.nvidia.outputs.prb }} + NEW_NFB: ${{ steps.nvidia.outputs.nfb }} + NEW_BETA: ${{ steps.nvidia.outputs.beta }} + NEW_LEGACY: ${{ steps.nvidia.outputs.legacy }} + TOOLKIT: ${{ steps.runtimes.outputs.toolkit }} + LIBNVIDIA: ${{ steps.runtimes.outputs.libnvidia }} + run: | + CUR_PRB=$(jq -r '.branches.production.current' versions.json) + CUR_NFB=$(jq -r '.branches.newfeature.current' versions.json) + + jq --arg prb "${NEW_PRB}" \ + --arg nfb "${NEW_NFB}" \ + --arg beta "${NEW_BETA}" \ + --arg legacy "${NEW_LEGACY}" \ + --arg last_prb "${CUR_PRB}" \ + --arg last_nfb "${CUR_NFB}" \ + --arg toolkit "${TOOLKIT}" \ + --arg libnvidia "${LIBNVIDIA}" \ + ' + (if ($prb != "") then + .branches.production.current = $prb + | (if ($prb != $last_prb) then .branches.production.last_prb = $last_prb else . end) + else . end) + | (if ($nfb != "") then + .branches.newfeature.current = $nfb + | (if ($nfb != $last_nfb) then .branches.newfeature.last_nfb = $last_nfb else . end) + else . end) + | (if ($beta != "") then .branches.beta.current = $beta else . end) + | (if ($legacy != "") then .branches.legacy.current = $legacy else . end) + | (if ($toolkit != "" and ($toolkit | test("^[0-9]"))) then .runtimes.containertoolkit.current = $toolkit else . end) + | (if ($libnvidia != "" and ($libnvidia | test("^[0-9]"))) then .runtimes.libnvidia.current = $libnvidia else . end) + ' versions.json > versions.json.tmp && mv versions.json.tmp versions.json + + - name: Commit and push if changed + id: commit + run: | + if git diff --quiet; then + echo "No changes detected" + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add build-matrix.json versions.json + git diff --cached --quiet && { echo "No staged changes"; echo "changed=false" >> "$GITHUB_OUTPUT"; exit 0; } + git commit -m "auto-update: detect new driver/kernel versions" + git push + echo "changed=true" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Trigger build workflow + if: steps.commit.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Changes committed, triggering build workflow..." + gh workflow run "Build NVIDIA Driver Packages" --repo ${{ github.repository }} --ref master diff --git a/.github/workflows/build-nvidia.yml b/.github/workflows/build-nvidia.yml new file mode 100644 index 0000000..9f06a34 --- /dev/null +++ b/.github/workflows/build-nvidia.yml @@ -0,0 +1,219 @@ +name: Build NVIDIA Driver Packages + +on: + workflow_dispatch: + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + gcc_tag: ${{ steps.set-matrix.outputs.gcc_tag }} + steps: + - uses: actions/checkout@v4 + - id: set-matrix + run: | + MATRIX=$(jq -c ' + # Builds from branch definitions + [ + .kernel_versions[] as $k | + .branches | to_entries[] | + .value.module_types[] as $m | + { + driver_version: .value.driver_version, + module_type: $m, + kernel_version: $k, + branch: .key + } + ] + + + # Extra builds for pin-to-specific-version (outside branch structure) + [ + .kernel_versions[] as $k | + (.extra_builds // [])[] | + .module_types[] as $m | + { + driver_version: .driver_version, + module_type: $m, + kernel_version: $k, + branch: "extra" + } + ] + ' build-matrix.json) + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + echo "gcc_tag=$(jq -r '.gcc_tag' build-matrix.json)" >> "$GITHUB_OUTPUT" + + build: + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.prepare.outputs.matrix) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check if package already exists + id: check-existing + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "${{ matrix.module_type }}" == "opensource" ]; then + PKG_PREFIX="nvos" + else + PKG_PREFIX="nvidia" + fi + PKG_NAME="${PKG_PREFIX}-${{ matrix.driver_version }}-${{ matrix.kernel_version }}-1.txz" + TAG="${{ matrix.kernel_version }}" + + if gh release view "${TAG}" --repo ${{ github.repository }} --json assets --jq ".assets[].name" 2>/dev/null | grep -qx "${PKG_NAME}"; then + echo "Package ${PKG_NAME} already exists in release ${TAG}, skipping build" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "Package ${PKG_NAME} not found, proceeding with build" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Free up disk space + if: steps.check-existing.outputs.skip != 'true' + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + df -h / + + - name: Create output directory + if: steps.check-existing.outputs.skip != 'true' + run: mkdir -p output + + - name: Build package in container + if: steps.check-existing.outputs.skip != 'true' + run: | + docker pull ghcr.io/ich777/unraid_kernel:${{ needs.prepare.outputs.gcc_tag }} + docker run --rm \ + --entrypoint bash \ + -v "$GITHUB_WORKSPACE":/workspace \ + -v "$GITHUB_WORKSPACE/output":/output \ + ghcr.io/ich777/unraid_kernel:${{ needs.prepare.outputs.gcc_tag }} \ + -c ' + set -euo pipefail + + DRIVER_VERSION="${{ matrix.driver_version }}" + KERNEL_VERSION="${{ matrix.kernel_version }}" + MODULE_TYPE="${{ matrix.module_type }}" + + if [ "${MODULE_TYPE}" == "opensource" ]; then + PKG_PREFIX="nvos" + BUILD_ARG="opensource" + else + PKG_PREFIX="nvidia" + BUILD_ARG="" + fi + + echo "=== Building ${PKG_PREFIX}-${DRIVER_VERSION} (${MODULE_TYPE}) for kernel ${KERNEL_VERSION} ===" + + export UNAME="${KERNEL_VERSION}" + export CPU_COUNT="$(nproc)" + export DATA_DIR="/tmp/nvidia-build" + mkdir -p "${DATA_DIR}" + + # --------------------------------------------------------------- + # Step 1: Prepare kernel sources + # --------------------------------------------------------------- + echo "=== Downloading kernel sources ===" + KERNEL_TAR="linux-${KERNEL_VERSION}.tar.xz" + KERNEL_URL="https://github.com/ich777/unraid_kernel/releases/download/${KERNEL_VERSION}/${KERNEL_TAR}" + wget -q -O "/tmp/${KERNEL_TAR}" "${KERNEL_URL}" + mkdir -p "/usr/src/linux-${KERNEL_VERSION}" + tar xf "/tmp/${KERNEL_TAR}" -C "/usr/src/linux-${KERNEL_VERSION}" + cd "/usr/src/linux-${KERNEL_VERSION}" + + echo "=== Preparing kernel headers ===" + make oldconfig &1 | tail -5 + make modules_prepare -j${CPU_COUNT} 2>&1 | tail -5 + + mkdir -p "/lib/modules/${UNAME}" + ln -sf "/usr/src/linux-${KERNEL_VERSION}" "/lib/modules/${UNAME}/build" + ln -sf "/usr/src/linux-${KERNEL_VERSION}" "/lib/modules/${UNAME}/source" + cd / + + # --------------------------------------------------------------- + # Step 2: Create makepkg shim + # --------------------------------------------------------------- + mkdir -p "${DATA_DIR}/bzroot-extracted-${UNAME}/sbin" + printf "#!/bin/bash\n# Grab last argument as output file\nfor PKG; do true; done\ntar cJf \"\${PKG}\" . 2>/dev/null; exit 0\n" > "${DATA_DIR}/bzroot-extracted-${UNAME}/sbin/makepkg" + chmod +x "${DATA_DIR}/bzroot-extracted-${UNAME}/sbin/makepkg" + + # --------------------------------------------------------------- + # Step 3: Build using compile.sh + # --------------------------------------------------------------- + echo "=== Loading compile.sh and building ===" + cd /workspace + + real_wget="$(which wget)" + mkdir -p /usr/local/bin + printf "#!/bin/bash\nexec ${real_wget} \"\${@/--show-progress/}\"" > /usr/local/bin/wget + chmod +x /usr/local/bin/wget + + set +u + source source/compile.sh "${DRIVER_VERSION}" ${BUILD_ARG} + nvidia_driver "${DRIVER_VERSION}" ${BUILD_ARG} + set -u + + # --------------------------------------------------------------- + # Step 4: Copy output artifacts + # --------------------------------------------------------------- + echo "=== Copying build artifacts ===" + find /tmp -name "${PKG_PREFIX}-*.txz" -exec cp {} /output/ \; + find /tmp -name "${PKG_PREFIX}-*.txz.md5" -exec cp {} /output/ \; + + echo "=== Build artifacts ===" + ls -la /output/ + ' + + - name: Verify build output + if: steps.check-existing.outputs.skip != 'true' + run: | + ls -la output/ + if [ -z "$(ls output/*.txz 2>/dev/null)" ]; then + echo "ERROR: No .txz package found in output!" + exit 1 + fi + cd output + for f in *.txz; do + if [ -f "${f}.md5" ]; then + EXPECTED="$(cat "${f}.md5")" + ACTUAL="$(md5sum "${f}" | awk '{print $1}')" + echo "${f}: expected=${EXPECTED} actual=${ACTUAL}" + if [ "${EXPECTED}" != "${ACTUAL}" ]; then + echo "CHECKSUM MISMATCH!" + exit 1 + fi + echo "Checksum OK" + fi + done + + - name: Upload artifact + if: steps.check-existing.outputs.skip != 'true' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.module_type == 'opensource' && 'nvos' || 'nvidia' }}-${{ matrix.driver_version }}-${{ matrix.kernel_version }} + path: output/* + retention-days: 90 + + - name: Create or update GitHub Release + if: steps.check-existing.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${{ matrix.kernel_version }}" + gh release view "${TAG}" --repo ${{ github.repository }} >/dev/null 2>&1 || \ + gh release create "${TAG}" --repo ${{ github.repository }} \ + --title "Drivers for kernel ${TAG}" \ + --notes "NVIDIA driver packages for Unraid kernel ${TAG}" + for f in output/*; do + gh release upload "${TAG}" "${f}" --repo ${{ github.repository }} --clobber + done + echo "=== Release ${TAG} updated ===" + gh release view "${TAG}" --repo ${{ github.repository }} diff --git a/build-matrix.json b/build-matrix.json new file mode 100644 index 0000000..ba9b613 --- /dev/null +++ b/build-matrix.json @@ -0,0 +1,60 @@ +{ + "gcc_tag": "gcc_14.2.0", + "kernel_versions": [ + "6.18.20-Unraid", + "6.18.19-Unraid" + ], + "branches": { + "production": { + "driver_version": "595.58.03", + "module_types": [ + "proprietary", + "opensource" + ] + }, + "newfeature": { + "driver_version": "590.48.01", + "module_types": [ + "proprietary", + "opensource" + ] + }, + "beta": { + "driver_version": "595.45.04", + "module_types": [ + "proprietary", + "opensource" + ] + } + }, + "extra_builds": [ + { + "driver_version": "575.64.05", + "module_types": [ + "proprietary", + "opensource" + ] + }, + { + "driver_version": "580.126.18", + "module_types": [ + "proprietary", + "opensource" + ] + }, + { + "driver_version": "580.142", + "module_types": [ + "proprietary", + "opensource" + ] + }, + { + "driver_version": "590.44.01", + "module_types": [ + "proprietary", + "opensource" + ] + } + ] +} diff --git a/source/usr/local/emhttp/plugins/nvidia-driver/include/download.sh b/source/usr/local/emhttp/plugins/nvidia-driver/include/download.sh index 9d46829..970d509 100644 --- a/source/usr/local/emhttp/plugins/nvidia-driver/include/download.sh +++ b/source/usr/local/emhttp/plugins/nvidia-driver/include/download.sh @@ -235,9 +235,22 @@ fi #Begin Check check -#Check for old packages that are not suitable for this Kernel and not suitable for the current Nvidia driver version -rm -rf $(ls -d /boot/config/plugins/nvidia-driver/packages/* 2>/dev/null | grep -v "${KERNEL_V%%-*}") -rm -f $(ls /boot/config/plugins/nvidia-driver/packages/${KERNEL_V%%-*}/* 2>/dev/null | grep -v "$LAT_PACKAGE") +# SEC: Safe cleanup of old kernel directories. +# Original used unquoted command substitution with rm -rf, which could +# delete everything if variables were empty (grep -v "" matches all lines). +# Using a loop with explicit checks prevents accidental mass deletion. +while IFS= read -r dir; do + [[ "$(basename "$dir")" != "${KERNEL_V%%-*}" ]] && rm -rf "$dir" +done < <(find /boot/config/plugins/nvidia-driver/packages -mindepth 1 -maxdepth 1 -type d 2>/dev/null) + +# SEC: Only clean up old packages if we know the current package name. +# Without this guard, an empty $LAT_PACKAGE would cause grep -v "" to +# match nothing, and all files would be deleted from the packages dir. +if [ -n "$LAT_PACKAGE" ]; then + while IFS= read -r file; do + [[ "$(basename "$file")" != "$LAT_PACKAGE" && "$(basename "$file")" != "${LAT_PACKAGE}.md5" ]] && rm -f "$file" + done < <(find "/boot/config/plugins/nvidia-driver/packages/${KERNEL_V%%-*}" -mindepth 1 -maxdepth 1 -type f 2>/dev/null) +fi #Display message to reboot server both in Plugin and WebUI echo diff --git a/source/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh b/source/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh index a20bc17..9986f5a 100644 --- a/source/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh +++ b/source/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh @@ -5,30 +5,30 @@ KERNEL_V="$(uname -r)" PACKAGE="nvidia" CURENTTIME=$(date +%s) CHK_TIMEOUT=300 +FETCH_VERSIONS() { + DRIVERS="$(wget -qO- https://api.github.com/repos/unraid/unraid-nvidia-driver/releases/tags/${KERNEL_V} | jq -r '.assets[].name' | grep -E -v '\.md5$' | sort -V)" + echo -n "$(grep ${PACKAGE} <<< "$DRIVERS" | awk -F "-" '{print $2}' | sort -V | uniq)" > /tmp/nvidia_driver + echo -n "$(grep nvos <<< "$DRIVERS" | awk -F "-" '{print $2}' | sort -V | uniq)" > /tmp/nvos_driver + if [ ! -s /tmp/nvidia_driver ]; then + echo -n "$(modinfo nvidia | grep "version:" | awk '{print $2}' | head -1)" > /tmp/nvidia_driver + fi +} if [ -f /tmp/nvidia_driver ]; then FILETIME=$(stat /tmp/nvidia_driver -c %Y) DIFF=$(expr $CURENTTIME - $FILETIME) if [ $DIFF -gt $CHK_TIMEOUT ]; then - DRIVERS="$(wget -qO- https://api.github.com/repos/unraid/unraid-nvidia-driver/releases/tags/${KERNEL_V} | jq -r '.assets[].name' | grep -E -v '\.md5$' | sort -V)" - echo -n "$(grep ${PACKAGE} <<< "$DRIVERS" | awk -F "-" '{print $2}' | sort -V | tail -10)" > /tmp/nvidia_driver - echo -n "$(grep nvos <<< "$DRIVERS" | awk -F "-" '{print $2}' | sort -V | tail -1)" > /tmp/nvos_driver - echo -n "$(wget -qO- https://api.github.com/repos/unraid/unraid-nvidia-driver/releases/tags/${KERNEL_V} | jq -r '.assets[].name' | grep "${PACKAGE}" | grep -E -v '\.md5$' | awk -F "-" '{print $2}' | sort -V | tail -10)" > /tmp/nvidia_driver - if [ ! -s /tmp/nvidia_driver ]; then - echo -n "$(modinfo nvidia | grep "version:" | awk '{print $2}' | head -1)" > /tmp/nvidia_driver - fi + FETCH_VERSIONS fi else - DRIVERS="$(wget -qO- https://api.github.com/repos/unraid/unraid-nvidia-driver/releases/tags/${KERNEL_V} | jq -r '.assets[].name' | grep -E -v '\.md5$' | sort -V)" - echo -n "$(grep ${PACKAGE} <<< "$DRIVERS" | awk -F "-" '{print $2}' | sort -V | tail -10)" > /tmp/nvidia_driver - echo -n "$(grep nvos <<< "$DRIVERS" | awk -F "-" '{print $2}' | sort -V | tail -10)" > /tmp/nvos_driver - if [ ! -s /tmp/nvidia_driver ]; then - echo -n "$(modinfo nvidia | grep "version:" | awk '{print $2}' | head -1)" > /tmp/nvidia_driver - fi + FETCH_VERSIONS fi -# Check if driver version 580 is in /tmp/nvos_driver -if [ ! $(grep "580" /tmp/nvidia_driver) ]; then - LEGACY_DRIVER="$(echo "$DRIVERS" | awk -F "-" '{print $2}' | grep "580")" - if [ ! -z "${LEGACY_DRIVER}" ]; then +# FIX: Read available versions from cache file instead of $DRIVERS variable. +# $DRIVERS is only populated inside FETCH_VERSIONS() which may not run +# if the cache is still fresh. Reading from the file ensures the 580 +# legacy driver check works regardless of whether the cache was refreshed. +if ! grep -q "580" /tmp/nvidia_driver 2>/dev/null; then + LEGACY_DRIVER="$(grep "580" /tmp/nvos_driver 2>/dev/null | head -1)" + if [ -n "${LEGACY_DRIVER}" ]; then sed -i "1s/^/${LEGACY_DRIVER}\n/" /tmp/nvidia_driver fi fi @@ -47,8 +47,16 @@ fi } function update_version(){ +# SEC: Validate input to prevent command injection via sed. +# exec.sh is called with $@ dispatcher, so any string passed from the web UI +# ends up as ${1} here. Without validation, a crafted version string like +# "1.0; rm -rf /" would execute arbitrary commands through sed. +if [[ ! "${1}" =~ ^[a-zA-Z0-9._]+$ ]]; then + echo "ERROR: Invalid version string" + exit 1 +fi sed -i "/driver_version=/c\driver_version=${1}" "/boot/config/plugins/nvidia-driver/settings.cfg" -if [[ "${1}" != "latest" && "${1}" != "latest_prb" && "${1}" != "latest_nfb" ]]; then +if [[ "${1}" != "latest" && "${1}" != "latest_prb" && "${1}" != "latest_nfb" && "${1}" != "latest_beta" ]]; then sed -i "/update_check=/c\update_check=false" "/boot/config/plugins/nvidia-driver/settings.cfg" echo -n "$(crontab -l | grep -v '/usr/local/emhttp/plugins/nvidia-driver/include/update-check.sh &>/dev/null 2>&1' | crontab -)" fi @@ -68,10 +76,22 @@ function get_nfb(){ echo -n "$(comm -12 <(cat /tmp/nvidia_driver | awk -F '.' '{printf "%d.%03d.%d\n", $1,$2,$3}' | awk -F '.' '{printf "%d.%03d.%02d\n", $1,$2,$3}') <(echo "$(cat /tmp/nvidia_branches | jq -r '.branches.newfeature[]' | sort -V | awk -F '.' '{printf "%d.%03d.%d\n", $1,$2,$3}' | awk -F '.' '{printf "%d.%03d.%02d\n", $1,$2,$3}')") | tail -1 | awk -F '.' '{printf "%d.%02d.%02d\n", $1,$2,$3}' | awk '{sub(/\.0+$/,"")}1')" } +function get_beta(){ +echo -n "$(comm -12 <(cat /tmp/nvidia_driver | awk -F '.' '{printf "%d.%03d.%d\n", $1,$2,$3}' | awk -F '.' '{printf "%d.%03d.%02d\n", $1,$2,$3}') <(echo "$(cat /tmp/nvidia_branches | jq -r '.branches.beta.current' | sort -V | awk -F '.' '{printf "%d.%03d.%d\n", $1,$2,$3}' | awk -F '.' '{printf "%d.%03d.%02d\n", $1,$2,$3}')") | tail -1 | awk -F '.' '{printf "%d.%02d.%02d\n", $1,$2,$3}' | awk '{sub(/\.0+$/,"")}1')" +} + function get_nos(){ echo -n "$(cat /tmp/nvos_driver | sort -V | tail -1)" } +function get_gpu_arch(){ +echo -n "$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1)" +} + +function get_cuda_version(){ +echo -n "$(nvidia-smi 2>/dev/null | grep 'CUDA Version' | grep -oE '[0-9]+\.[0-9]+' | tail -1)" +} + function get_selected_version(){ echo -n "$(cat /boot/config/plugins/nvidia-driver/settings.cfg | grep "driver_version" | cut -d '=' -f2)" } @@ -96,6 +116,12 @@ echo -n "$(cat /boot/config/plugins/nvidia-driver/settings.cfg | grep "update_ch } function change_update_check(){ +# SEC: Whitelist boolean values to prevent command injection. +# Only "true" or "false" are valid — anything else is rejected. +if [[ "${1}" != "true" && "${1}" != "false" ]]; then + echo "ERROR: Invalid value for update_check" + exit 1 +fi sed -i "/update_check=/c\update_check=${1}" "/boot/config/plugins/nvidia-driver/settings.cfg" if [ "${1}" == "true" ]; then if [ ! "$(crontab -l | grep "/usr/local/emhttp/plugins/nvidia-driver/include/update-check.sh")" ]; then @@ -107,4 +133,13 @@ fi } -$@ +# SEC: Restrict callable functions to prevent arbitrary code execution. +# The web UI calls this script via shell_exec("... exec.sh function_name args"). +# Without a whitelist, any bash function (or command) could be invoked. +ALLOWED_FUNCTIONS="update update_version get_latest_version get_prb get_nfb get_beta get_nos get_gpu_arch get_cuda_version get_selected_version get_installed_version get_license update_check change_update_check" +if [[ " ${ALLOWED_FUNCTIONS} " == *" ${1} "* ]]; then + "$@" +else + echo "ERROR: Unknown function '${1}'" + exit 1 +fi diff --git a/source/usr/local/emhttp/plugins/nvidia-driver/include/update-check.sh b/source/usr/local/emhttp/plugins/nvidia-driver/include/update-check.sh index f503b68..08815b0 100644 --- a/source/usr/local/emhttp/plugins/nvidia-driver/include/update-check.sh +++ b/source/usr/local/emhttp/plugins/nvidia-driver/include/update-check.sh @@ -77,6 +77,19 @@ elif [ "${SET_DRV_V}" == "latest_nos" ]; then fi fi -#Check for old packages that are not suitable for this Kernel and not suitable for the current Nvidia driver version -rm -rf $(ls -d /boot/config/plugins/nvidia-driver/packages/* 2>/dev/null | grep -v "${KERNEL_V%%-*}") -rm -f $(ls /boot/config/plugins/nvidia-driver/packages/${KERNEL_V%%-*}/* 2>/dev/null | grep -v "$LAT_PACKAGE") +# SEC: Safe cleanup of old kernel directories. +# Original used unquoted command substitution with rm -rf, which could +# delete everything if variables were empty (grep -v "" matches all lines). +# Using a loop with explicit checks prevents accidental mass deletion. +while IFS= read -r dir; do + [[ "$(basename "$dir")" != "${KERNEL_V%%-*}" ]] && rm -rf "$dir" +done < <(find /boot/config/plugins/nvidia-driver/packages -mindepth 1 -maxdepth 1 -type d 2>/dev/null) + +# SEC: Only clean up old packages if we know the current package name. +# Without this guard, an empty $LAT_PACKAGE would cause grep -v "" to +# match nothing, and all files would be deleted from the packages dir. +if [ -n "$LAT_PACKAGE" ]; then + while IFS= read -r file; do + [[ "$(basename "$file")" != "$LAT_PACKAGE" && "$(basename "$file")" != "${LAT_PACKAGE}.md5" ]] && rm -f "$file" + done < <(find "/boot/config/plugins/nvidia-driver/packages/${KERNEL_V%%-*}" -mindepth 1 -maxdepth 1 -type f 2>/dev/null) +fi diff --git a/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page b/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page index 0f1c890..03c33bb 100644 --- a/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page +++ b/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page @@ -5,425 +5,342 @@ Icon="nvidia-driver.png" = 10.0); + if ($arch_num >= 10.0) $arch_name = 'Blackwell'; + elseif ($arch_num >= 8.9) $arch_name = 'Ada Lovelace'; + elseif ($arch_num >= 8.0) $arch_name = 'Ampere'; + elseif ($arch_num >= 7.5) $arch_name = 'Turing'; + elseif ($arch_num >= 7.0) $arch_name = 'Volta'; + elseif ($arch_num >= 6.0) $arch_name = 'Pascal'; + else $arch_name = 'Legacy'; } -$candidate_versions = array_values(array_unique($candidate_versions)); -$latest_v_norm = nvidia_extract_version(trim((string)$latest_v)); -$latest_prb_v_norm = nvidia_extract_version(trim((string)$latest_prb_v)); -$latest_nfb_v_norm = nvidia_extract_version(trim((string)$latest_nfb_v)); -$latest_nos_v_norm = nvidia_extract_version(trim((string)$latest_nos_v)); +$module_type_display = 'Unknown'; +if (trim($module_license) === 'OPENSOURCE') $module_type_display = 'Open Source'; +elseif (trim($module_license) === 'PROPRIETARY') $module_type_display = 'Proprietary'; -//Get installed driver version -$cur_drv_v = shell_exec("nvidia-smi | grep \"Driver Version\" | cut -d ' ' -f3"); -if (empty($cur_drv_v)) { - $cur_drv_v = shell_exec("nvidia-settings --help | grep -oP '(?<=version )[\d\.]+'"); - if (empty($cur_drv_v)) { - $cur_drv_v = "ERROR: No driver found!"; - } +if (empty($installed_v)) { + $installed_v = trim(shell_exec("nvidia-smi 2>/dev/null | grep 'Driver Version' | grep -oE '[0-9]+\\.[0-9]+\\.?[0-9]*' | head -1")); } +if (empty($installed_v)) $installed_v = 'Not installed'; -//Get Unraid driver version -$unraid_version = parse_ini_file('/etc/unraid-version'); -?> +$eachlines = array(); +if (file_exists("/tmp/nvidia_driver")) { + $eachlines = file("/tmp/nvidia_driver", FILE_IGNORE_NEW_LINES); + if ($eachlines === false) $eachlines = array(); +} +$nvos_versions = array(); +if (file_exists("/tmp/nvos_driver")) { + $nvos_lines = file("/tmp/nvos_driver", FILE_IGNORE_NEW_LINES); + if ($nvos_lines !== false) $nvos_versions = $nvos_lines; +} - +$gpu_list = trim(shell_exec("nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null")); +$gpu_entries = !empty($gpu_list) ? explode("\n", $gpu_list) : array(); - - - -

Nvidia Driver Package

-
- -
- -
-

Nvidia Info:

-

Nvidia Driver Version: -

- -

Open Source Kernel Module: - Yes

- -

Open Source Kernel Module: - No

- - -

Installed GPU(s):
-


-

GPU Driver Support:

-
-

Loading supported drivers...

-
-

- - GPU-based driver filtering is enabled. -

-
- - - - - Driver Update Notification - - -
- - -
- -

The Driver Update Notification, checks once a day (between 8 am and 10 am) whether a new version of the selected branch is available and, if necessary, downloads the new driver and sends a notification.

-

The server needs to be restarted in order to install the new driver.


- - - -
- -
-

Select preferred driver version:

-
- - - - - - - - - - - - - -
Latest Versions: -

/>latest:

- -

/>Production Branch:

- -

Production Branch: Not found for this Unraid version!

- - -

/>New Feature Branch:

- -

New Feature Branch: Not found for this Unraid version!

- - -

/>Open Source Driver: - Please see readme below!' : ''; ?>

- -

Open Source Driver: Not found for this Unraid version!

- -
Available Versions: - -

- /> - -

- -
- - -
- -

ATTENTION: If you set the version to 'latest', 'Production Branch' or 'New Feature Branch' you either have to enable the Driver Update Notification (this will download the driver automatically) or if you disabled the Driver Update Notification you manually have to click the button "Update & Download" to download a newer driver.
Please keep in mind, to install a newer driver you have to reboot your server.

-
-

Latest versions:

-
    -

    -

  • latest - will always download the latest driver version available regardless of the branch

  • -
  • Production Branch - This is the long lifecycle (i.e., multiple releases in a branch over a single year period) driver branch providing ISV Certification, optimal stability and performance, and regular security updates for NVIDIA RTX/Quadro professional customers. This driver was previously named, Optimal Driver for Enterprise (ODE), and is most commonly deployed in production systems at enterprises.

  • -
  • New Feature Branch - This is a short lived branch driver (i.e., 1-2 releases per branch only) where new NVIDIA RTX/Quadro features are introduced and made available between Production Branch releases. New Feature Branch drivers, previously named, Quadro New Feature (QNF), typically provide support for new operating systems, third party hardware, and game engines.

  • -
  • Open Source Driver - This package includes the Open Source Kernel modules and the libraries/execuatables from the proprietary Nvidia driver package. -
    Supported GPUs: This driver package only supports Turing based cards and newer! - ATTENTION: If you want to use the Open Source driver with GeForce and/or Workstation cards you have to create the file "/boot/config/modprobe.d/nvidia.conf" on your USB boot device with the contents, before you reboot your server:
    options nvidia NVreg_OpenRmEnableUnsupportedGpus=1

    For more information see here.

    '; +
    + + +
    +

    System Information

    +
    +
    Driver Version
    +
    Kernel Module
    + +
    CUDA Version
    + + +
    GPU Architecture
    ()
    + +
    Kernel
    + +
    Installed GPU(s)
    + +
    + +
    + +
    + +
    Blackwell GPU detected (sm_): This GPU architecture requires Open Source kernel modules. Proprietary modules will not load.
    + +
    + + +
    +

    Driver Selection

    +
    +
    + + +
    + +
    + +
-
+ if (!empty($nvos_versions)) { + $nvos_sorted = $nvos_versions; rsort($nvos_sorted, SORT_NATURAL); + foreach ($nvos_sorted as $nv) { + $ver = trim($nv); + if (!empty($ver)) echo mk_option(trim($selected_v), $ver, 'v'.$ver, 'data-type="opensource"'); + } + } + ?> + + +
+
+ + +
+ +
+ Branch descriptions +
+
Latest
Always downloads the latest driver version regardless of branch.
+
Production
Long lifecycle, ISV certified, optimal stability. Recommended for production.
+
New Feature
Short lived branch (1-2 releases) with new features between Production releases.
+
Beta
Cutting-edge features. May contain issues. Not for production use.
+
Open Source
Open Source kernel modules (MIT/GPL) + proprietary libraries. Turing and newer only. Required for Blackwell GPUs. + +
For GeForce/Workstation: create /boot/config/modprobe.d/nvidia.conf with options nvidia NVreg_OpenRmEnableUnsupportedGpus=1 + +
+
+
+ + + +
+

Auto-Update Notification

+
+
+ + + +
+
+ +

Checks once daily (8-10am) for new versions on the selected branch. Downloads automatically and sends a notification. Reboot required to install.

+
+ + + parent.window.location.reload();'; +if (isset($_POST['changeUPDcheck'])) { + shell_exec("/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh change_update_check " . escapeshellarg($_POST["updata_check_selected"])); + echo ''; } ?> From ec4698a12d4d2374214ece8f1e918b583b310ab9 Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 21:21:42 +0200 Subject: [PATCH 02/15] fix: treat NVIDIA installer exit code 8 as incompatible, not failure When a driver version doesn't support a kernel version, the NVIDIA installer exits with code 8. Instead of failing the entire job, detect this and skip gracefully with a warning annotation. --- .github/workflows/build-nvidia.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-nvidia.yml b/.github/workflows/build-nvidia.yml index 9f06a34..98b6d89 100644 --- a/.github/workflows/build-nvidia.yml +++ b/.github/workflows/build-nvidia.yml @@ -88,9 +88,11 @@ jobs: run: mkdir -p output - name: Build package in container + id: build if: steps.check-existing.outputs.skip != 'true' run: | docker pull ghcr.io/ich777/unraid_kernel:${{ needs.prepare.outputs.gcc_tag }} + EXIT_CODE=0 docker run --rm \ --entrypoint bash \ -v "$GITHUB_WORKSPACE":/workspace \ @@ -170,10 +172,20 @@ jobs: echo "=== Build artifacts ===" ls -la /output/ - ' + ' || EXIT_CODE=$? + + if [ "$EXIT_CODE" -eq 8 ]; then + echo "::warning::Driver ${{ matrix.driver_version }} is not compatible with kernel ${{ matrix.kernel_version }}, skipping" + echo "incompatible=true" >> "$GITHUB_OUTPUT" + elif [ "$EXIT_CODE" -ne 0 ]; then + echo "::error::Build failed with exit code $EXIT_CODE" + exit $EXIT_CODE + else + echo "incompatible=false" >> "$GITHUB_OUTPUT" + fi - name: Verify build output - if: steps.check-existing.outputs.skip != 'true' + if: steps.check-existing.outputs.skip != 'true' && steps.build.outputs.incompatible != 'true' run: | ls -la output/ if [ -z "$(ls output/*.txz 2>/dev/null)" ]; then @@ -195,7 +207,7 @@ jobs: done - name: Upload artifact - if: steps.check-existing.outputs.skip != 'true' + if: steps.check-existing.outputs.skip != 'true' && steps.build.outputs.incompatible != 'true' uses: actions/upload-artifact@v4 with: name: ${{ matrix.module_type == 'opensource' && 'nvos' || 'nvidia' }}-${{ matrix.driver_version }}-${{ matrix.kernel_version }} @@ -203,7 +215,7 @@ jobs: retention-days: 90 - name: Create or update GitHub Release - if: steps.check-existing.outputs.skip != 'true' + if: steps.check-existing.outputs.skip != 'true' && steps.build.outputs.incompatible != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | From 44a87fe994803e71a9e485f782e365acac1bf253 Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 21:35:17 +0200 Subject: [PATCH 03/15] fix: detect incompatible builds by checking for output artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NVIDIA installer returns exit code 1 (not 8) when kernel modules fail to build due to kernel incompatibility, because set -euo pipefail overrides the original exit code. Instead of checking exit codes, check if any .txz package was produced — no output + non-zero exit means the driver is incompatible with the kernel. --- .github/workflows/build-nvidia.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-nvidia.yml b/.github/workflows/build-nvidia.yml index 98b6d89..6064465 100644 --- a/.github/workflows/build-nvidia.yml +++ b/.github/workflows/build-nvidia.yml @@ -174,12 +174,15 @@ jobs: ls -la /output/ ' || EXIT_CODE=$? - if [ "$EXIT_CODE" -eq 8 ]; then - echo "::warning::Driver ${{ matrix.driver_version }} is not compatible with kernel ${{ matrix.kernel_version }}, skipping" - echo "incompatible=true" >> "$GITHUB_OUTPUT" - elif [ "$EXIT_CODE" -ne 0 ]; then - echo "::error::Build failed with exit code $EXIT_CODE" - exit $EXIT_CODE + # Check if build produced output artifacts + if [ -z "$(ls output/*.txz 2>/dev/null)" ]; then + if [ "${EXIT_CODE:-0}" -ne 0 ]; then + echo "::warning::Driver ${{ matrix.driver_version }} failed to build for kernel ${{ matrix.kernel_version }} (exit code ${EXIT_CODE}), skipping" + echo "incompatible=true" >> "$GITHUB_OUTPUT" + else + echo "::error::Build succeeded but no .txz package produced" + exit 1 + fi else echo "incompatible=false" >> "$GITHUB_OUTPUT" fi From c1c1c74c071912deead55204d5246efb3b8e547a Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 21:50:32 +0200 Subject: [PATCH 04/15] fix: detect latest kernel per major.minor branch, not just top 2 Different Unraid versions use different kernel branches (e.g. 7.2.x uses 6.12.x, newer versions use 6.17.x or 6.18.x). Now picks the latest patch version per major.minor branch so all active Unraid versions are covered. --- .github/workflows/auto-update-matrix.yml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml index bf23adf..17c0e2b 100644 --- a/.github/workflows/auto-update-matrix.yml +++ b/.github/workflows/auto-update-matrix.yml @@ -55,18 +55,33 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Get kernel tags from upstream releases (format: 6.x.y-Unraid) - KERNEL_TAGS=$(gh api repos/unraid/unraid-nvidia-driver/releases \ + # Keep the latest kernel per major.minor branch (e.g. 6.12.x, 6.17.x, 6.18.x) + # so all active Unraid versions are covered + ALL_TAGS=$(gh api repos/unraid/unraid-nvidia-driver/releases \ --paginate --jq '.[].tag_name' 2>/dev/null \ | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-Unraid$' \ - | sort -t. -k1,1nr -k2,2nr -k3,3nr \ - | head -2) + | sort -t. -k1,1nr -k2,2nr -k3,3nr) - if [ -z "$KERNEL_TAGS" ]; then + if [ -z "$ALL_TAGS" ]; then echo "::warning::No kernel tags found, keeping current kernel_versions" echo "kernels=" >> "$GITHUB_OUTPUT" exit 0 fi + # Pick the latest patch version per major.minor branch + KERNEL_TAGS="" + SEEN_BRANCHES="" + while IFS= read -r tag; do + BRANCH=$(echo "$tag" | grep -oE '^[0-9]+\.[0-9]+') + if ! echo "$SEEN_BRANCHES" | grep -qx "$BRANCH"; then + SEEN_BRANCHES="${SEEN_BRANCHES}${BRANCH} +" + KERNEL_TAGS="${KERNEL_TAGS}${tag} +" + fi + done <<< "$ALL_TAGS" + + KERNEL_TAGS=$(echo "$KERNEL_TAGS" | sed '/^$/d') KERNEL_JSON=$(echo "$KERNEL_TAGS" | jq -R -c -s 'split("\n") | map(select(length > 0))') echo "kernels=${KERNEL_JSON}" >> "$GITHUB_OUTPUT" echo "Detected kernels: ${KERNEL_JSON}" From b7c6c25abf1cf34e7a862062d98a84d0b2117e1a Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 21:55:04 +0200 Subject: [PATCH 05/15] fix: use awk for kernel branch dedup, avoid YAML-breaking newlines --- .github/workflows/auto-update-matrix.yml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml index 17c0e2b..c4daa94 100644 --- a/.github/workflows/auto-update-matrix.yml +++ b/.github/workflows/auto-update-matrix.yml @@ -69,19 +69,7 @@ jobs: fi # Pick the latest patch version per major.minor branch - KERNEL_TAGS="" - SEEN_BRANCHES="" - while IFS= read -r tag; do - BRANCH=$(echo "$tag" | grep -oE '^[0-9]+\.[0-9]+') - if ! echo "$SEEN_BRANCHES" | grep -qx "$BRANCH"; then - SEEN_BRANCHES="${SEEN_BRANCHES}${BRANCH} -" - KERNEL_TAGS="${KERNEL_TAGS}${tag} -" - fi - done <<< "$ALL_TAGS" - - KERNEL_TAGS=$(echo "$KERNEL_TAGS" | sed '/^$/d') + KERNEL_TAGS=$(echo "$ALL_TAGS" | awk -F'[.-]' '!seen[$1"."$2]++') KERNEL_JSON=$(echo "$KERNEL_TAGS" | jq -R -c -s 'split("\n") | map(select(length > 0))') echo "kernels=${KERNEL_JSON}" >> "$GITHUB_OUTPUT" echo "Detected kernels: ${KERNEL_JSON}" From 4a811df84af3d229bff6201e1266a73bc83f8c25 Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 21:57:34 +0200 Subject: [PATCH 06/15] fix: add actions:write permission for workflow_dispatch trigger --- .github/workflows/auto-update-matrix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml index c4daa94..2fe6dcc 100644 --- a/.github/workflows/auto-update-matrix.yml +++ b/.github/workflows/auto-update-matrix.yml @@ -7,6 +7,7 @@ on: permissions: contents: write + actions: write jobs: detect-and-update: From 75abebd363ca8e5eca5f134dd152e3ba6b8a566d Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 22:02:18 +0200 Subject: [PATCH 07/15] fix: limit kernel detection to top 3 major.minor branches --- .github/workflows/auto-update-matrix.yml | 4 ++-- build-matrix.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml index 2fe6dcc..38b28f5 100644 --- a/.github/workflows/auto-update-matrix.yml +++ b/.github/workflows/auto-update-matrix.yml @@ -69,8 +69,8 @@ jobs: exit 0 fi - # Pick the latest patch version per major.minor branch - KERNEL_TAGS=$(echo "$ALL_TAGS" | awk -F'[.-]' '!seen[$1"."$2]++') + # Pick the latest patch version per major.minor branch, keep top 3 + KERNEL_TAGS=$(echo "$ALL_TAGS" | awk -F'[.-]' '!seen[$1"."$2]++' | head -3) KERNEL_JSON=$(echo "$KERNEL_TAGS" | jq -R -c -s 'split("\n") | map(select(length > 0))') echo "kernels=${KERNEL_JSON}" >> "$GITHUB_OUTPUT" echo "Detected kernels: ${KERNEL_JSON}" diff --git a/build-matrix.json b/build-matrix.json index ba9b613..993fdb3 100644 --- a/build-matrix.json +++ b/build-matrix.json @@ -2,7 +2,8 @@ "gcc_tag": "gcc_14.2.0", "kernel_versions": [ "6.18.20-Unraid", - "6.18.19-Unraid" + "6.18.19-Unraid", + "6.12.54-Unraid" ], "branches": { "production": { From 0912bd444b61b95942a73a9958886a8574371a16 Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 22:10:28 +0200 Subject: [PATCH 08/15] fix: jq scoping bug in extra_builds cleanup, improve fallback logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix critical bug: select() inside extra_builds iteration had wrong scope — .branches referenced the current item, not root. Use '. as $root' to capture root object before iterating. - Improve fallback: trigger forum fallback also when nvidia.com returns data but parsing fails (all version vars empty). --- .github/workflows/auto-update-matrix.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml index 38b28f5..da90ee7 100644 --- a/.github/workflows/auto-update-matrix.yml +++ b/.github/workflows/auto-update-matrix.yml @@ -30,8 +30,8 @@ jobs: BETA="$(echo "${RAW_DATA}" | grep -i "Latest Beta" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" LEGACY="$(echo "${RAW_DATA}" | grep -i "Latest Legacy" | grep "(4" | head -1 | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" - # Fallback to NVIDIA developer forum - if [ -z "${RAW_DATA}" ]; then + # Fallback to NVIDIA developer forum if fetch or parse failed + if [ -z "${RAW_DATA}" ] || { [ -z "${PRB}" ] && [ -z "${NFB}" ] && [ -z "${BETA}" ]; }; then RAW_DATA="$(wget -qO- https://forums.developer.nvidia.com/t/current-graphics-driver-releases/28500 | tidy -quiet -wrap 4096 2>/dev/null || true)" PRB="$(echo "${RAW_DATA}" | grep -i "^Current production branch" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" NFB="$(echo "${RAW_DATA}" | grep -i "^Current new feature branch" | grep -oE '\b[0-9]+\.[0-9]+(\.[0-9]+)?\b')" @@ -168,12 +168,12 @@ jobs: else . end) # Remove from extra_builds any version that is now a branch version - | .extra_builds = [ + | . as $root | .extra_builds = [ .extra_builds[] | select( - .driver_version != (.branches.production.driver_version // "") and - .driver_version != (.branches.newfeature.driver_version // "") and - .driver_version != (.branches.beta.driver_version // "") + .driver_version != ($root.branches.production.driver_version // "") and + .driver_version != ($root.branches.newfeature.driver_version // "") and + .driver_version != ($root.branches.beta.driver_version // "") ) ] From cf781254f8ce2e30c2143ce977f4b63075ddf77a Mon Sep 17 00:00:00 2001 From: "manuel.richarz" Date: Sun, 5 Apr 2026 23:16:59 +0200 Subject: [PATCH 09/15] feat: multi-GPU support with expandable details, rename labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - List all GPUs with expandable detail panels showing chip codename, architecture, module support, device ID, and driver recommendations - Detect module support intersection across all GPUs: if all support open-source → auto-select, if conflict (e.g. Volta+Blackwell) → error - Rename labels: Proprietary → Legacy (Proprietary), Open Source is default - Auto-hide module type dropdown when only one option is valid - Use upstream gpu-support.php for architecture detection instead of manual compute_cap parsing --- .../plugins/nvidia-driver/nvidia-driver.page | 126 +++++++++++++----- 1 file changed, 96 insertions(+), 30 deletions(-) diff --git a/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page b/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page index 03c33bb..b48191f 100644 --- a/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page +++ b/source/usr/local/emhttp/plugins/nvidia-driver/nvidia-driver.page @@ -16,29 +16,32 @@ $latest_nos_v = shell_exec('/usr/local/emhttp/plugins/nvidia-driver/include/exec $module_license = shell_exec('/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh get_license'); $installed_v = trim(shell_exec('/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh get_installed_version')); -$gpu_arch = trim(shell_exec('/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh get_gpu_arch')); $cuda_v = trim(shell_exec('/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh get_cuda_version')); $update_check = shell_exec("/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh update_check"); $kernel_v = php_uname('r'); $unraid_version = parse_ini_file('/etc/unraid-version'); +// Query all GPUs via gpu-support.php for multi-GPU detection +$gpu_support_json = shell_exec('php /usr/local/emhttp/plugins/nvidia-driver/include/gpu-support.php 2>/dev/null'); +$gpu_support = json_decode(trim($gpu_support_json), true); +$gpu_rows = ($gpu_support && isset($gpu_support['ok']) && $gpu_support['ok']) ? $gpu_support['rows'] : array(); + +// Compute module support intersection across all GPUs +$has_proprietary_only = false; +$has_open_only = false; $is_blackwell = false; -$arch_name = ''; -if (!empty($gpu_arch)) { - $arch_num = floatval($gpu_arch); - $is_blackwell = ($arch_num >= 10.0); - if ($arch_num >= 10.0) $arch_name = 'Blackwell'; - elseif ($arch_num >= 8.9) $arch_name = 'Ada Lovelace'; - elseif ($arch_num >= 8.0) $arch_name = 'Ampere'; - elseif ($arch_num >= 7.5) $arch_name = 'Turing'; - elseif ($arch_num >= 7.0) $arch_name = 'Volta'; - elseif ($arch_num >= 6.0) $arch_name = 'Pascal'; - else $arch_name = 'Legacy'; +foreach ($gpu_rows as $row) { + $support = $row['kernel_module_support'] ?? 'unknown'; + if ($support === 'proprietary-only') $has_proprietary_only = true; + if ($support === 'open-only') { $has_open_only = true; $is_blackwell = true; } } +$is_conflict = $has_proprietary_only && $has_open_only; +$force_opensource = $has_open_only && !$has_proprietary_only; +$force_legacy = $has_proprietary_only && !$has_open_only; $module_type_display = 'Unknown'; if (trim($module_license) === 'OPENSOURCE') $module_type_display = 'Open Source'; -elseif (trim($module_license) === 'PROPRIETARY') $module_type_display = 'Proprietary'; +elseif (trim($module_license) === 'PROPRIETARY') $module_type_display = 'Legacy (Proprietary)'; if (empty($installed_v)) { $installed_v = trim(shell_exec("nvidia-smi 2>/dev/null | grep 'Driver Version' | grep -oE '[0-9]+\\.[0-9]+\\.?[0-9]*' | head -1")); @@ -56,10 +59,7 @@ if (file_exists("/tmp/nvos_driver")) { if ($nvos_lines !== false) $nvos_versions = $nvos_lines; } -$gpu_list = trim(shell_exec("nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null")); -$gpu_entries = !empty($gpu_list) ? explode("\n", $gpu_list) : array(); - -$is_opensource_selected = ($is_blackwell || trim($module_license) === 'OPENSOURCE' || trim($selected_v) === 'latest_nos'); +$is_opensource_selected = ($force_opensource || trim($module_license) === 'OPENSOURCE' || trim($selected_v) === 'latest_nos'); ?>