diff --git a/.github/workflows/auto-update-matrix.yml b/.github/workflows/auto-update-matrix.yml new file mode 100644 index 0000000..7a38905 --- /dev/null +++ b/.github/workflows/auto-update-matrix.yml @@ -0,0 +1,253 @@ +name: Auto-detect and update build matrix + +on: + schedule: + - cron: '0 4 * * *' # Daily at 04:00 UTC + workflow_dispatch: + +permissions: + contents: write + actions: write + +concurrency: + group: auto-update-matrix-${{ github.ref }} + cancel-in-progress: true + +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 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')" + 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}" ] || { [ -z "${PRB}" ] && [ -z "${NFB}" ]; }; 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) + # 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) + + 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, 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}" + + - 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 != "") and ($cur_prb != "null") 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 != "") and ($cur_nfb != "null") 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 != "") and ($cur_beta != "null") 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 + | . as $root | .extra_builds = [ + .extra_builds[] | + select( + .driver_version != ($root.branches.production.driver_version // "") and + .driver_version != ($root.branches.newfeature.driver_version // "") and + .driver_version != ($root.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 | test("^[0-9]+(\\\\.[0-9]+)+$")) then .runtimes.containertoolkit.current = $toolkit else . end) + | (if ($libnvidia | test("^[0-9]+(\\\\.[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 ${{ github.ref_name }} diff --git a/.github/workflows/build-nvidia.yml b/.github/workflows/build-nvidia.yml new file mode 100644 index 0000000..6064465 --- /dev/null +++ b/.github/workflows/build-nvidia.yml @@ -0,0 +1,234 @@ +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 + 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 \ + -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/ + ' || 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 + + - name: Verify build output + 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 + 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' && steps.build.outputs.incompatible != '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' && steps.build.outputs.incompatible != '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/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/build-matrix.json b/build-matrix.json new file mode 100644 index 0000000..993fdb3 --- /dev/null +++ b/build-matrix.json @@ -0,0 +1,61 @@ +{ + "gcc_tag": "gcc_14.2.0", + "kernel_versions": [ + "6.18.20-Unraid", + "6.18.19-Unraid", + "6.12.54-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..242fba7 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,418 @@ Icon="nvidia-driver.png" /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(); -//Get value from update check -$update_check = shell_exec("/usr/local/emhttp/plugins/nvidia-driver/include/exec.sh update_check"); +$has_legacy_only = false; +$has_open_only = false; +foreach ($gpu_rows as $row) { + $support = $row['kernel_module_support'] ?? 'unknown'; + if ($support === 'proprietary-only') $has_legacy_only = true; + if ($support === 'open-only') $has_open_only = true; +} +$is_conflict = $has_legacy_only && $has_open_only; +$force_opensource = $has_open_only && !$has_legacy_only; +$force_legacy = $has_legacy_only && !$has_open_only; -$available_versions = $version_data['available_versions']; -$candidate_versions = $version_data['candidate_versions']; -$eachlines = $available_versions; +$module_type_display = 'Unknown'; +if ($module_license === 'OPENSOURCE') $module_type_display = 'Open Source'; +elseif ($module_license === 'PROPRIETARY') $module_type_display = 'Legacy (Proprietary)'; -foreach (array(470, 390, 340) as $legacy_major) { - $legacy_best = nvidia_best_version_for_branch($available_versions, $legacy_major); - if ($legacy_best !== null) { - $candidate_versions[] = $legacy_best; - } +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'; -$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)); - -//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!"; - } +$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; } -//Get Unraid driver version -$unraid_version = parse_ini_file('/etc/unraid-version'); +$is_opensource_selected = ($force_opensource || $module_license === 'OPENSOURCE' || $selected_v === 'latest_nos'); ?> - - - - - -
Nvidia Info:
-Nvidia Driver Version: -
- if (isset($module_license ) === true && trim($module_license ) === 'OPENSOURCE'): ?> -Open Source Kernel Module: - Yes
- elseif (isset($module_license ) === true && trim($module_license ) === 'PROPRIETARY'): ?> -Open Source Kernel Module: - No
- endif; ?> - -Installed GPU(s):
-
GPU Driver Support:
-Loading supported drivers...
-- - GPU-based driver filtering is enabled. -
-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:
-/boot/config/modprobe.d/nvidia.conf with options nvidia NVreg_OpenRmEnableUnsupportedGpus=1
+
+ Checks once daily (8-10am) for new versions on the selected branch. Downloads automatically and sends a notification. Reboot required to install.
+