diff --git a/nvidia-driver.plg b/nvidia-driver.plg index 4961258..a453a4d 100644 --- a/nvidia-driver.plg +++ b/nvidia-driver.plg @@ -2,7 +2,7 @@ - + @@ -16,6 +16,9 @@ +###2026.08.01 +- Temporarily add the host's 32-bit NVIDIA libraries and aliases when nvidia-ctk omits them + ###2026.06.07 - Fix for missing link @@ -329,10 +332,168 @@ fi } generate_cdi() { - nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml >/dev/null 2>&1 - if ! grep -q '/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so' /etc/cdi/nvidia.yaml ; then - sed -i -E 's#^([[:space:]]*)- \.\./libnvidia-allocator\.so\.1::/usr/lib64/gbm/nvidia-drm_gbm\.so[[:space:]]*$#\1- ../libnvidia-allocator.so.1::/usr/lib64/gbm/nvidia-drm_gbm.so\n\1- --link\n\1- /usr/lib64/libnvidia-allocator.so.1::/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so#' /etc/cdi/nvidia.yaml + local cdi_path="${NVIDIA_CDI_PATH:-/etc/cdi/nvidia.yaml}" + local staging generated transformed compat_output compat_json folders_json + local library canonical candidate header folder scanned target + local -a compat_libraries=("") + local -a compat_folders=("") + local -a compat_targets=("") + local -a scanned_folders=("") + + if ! mkdir -p "$(dirname "${cdi_path}")" ; then + echo "Failed to create NVIDIA CDI directory" >&2 + return 1 + fi + + staging="$(mktemp -d "${cdi_path}.stage.XXXXXX")" || return 1 + generated="${staging}/generated.json" + transformed="${staging}/transformed.json" + + if ! nvidia-ctk cdi generate --format=json --output="${generated}" >/dev/null 2>&1 ; then + echo "Failed to generate NVIDIA CDI specification" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 + fi + + # nvidia-ctk currently omits compat32 libraries from CDI generation. Keep the + # additions idempotent so this becomes a no-op when the upstream fix lands: + # https://github.com/NVIDIA/nvidia-container-toolkit/pull/1968 + if ! compat_output="$(nvidia-container-cli list --libraries --compat32 2>/dev/null)" ; then + echo "Failed to discover NVIDIA compat32 libraries" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 fi + + while IFS= read -r library ; do + [ -n "${library}" ] || continue + + if [[ "${library}" != /* || ! -r "${library}" ]]; then + echo "Invalid NVIDIA compat32 library: ${library}" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 + fi + + header="$(od -An -t x1 -N5 "${library}" 2>/dev/null | tr -d ' \n')" + if [ "${header}" == "7f454c4602" ]; then + continue + fi + if [ "${header}" != "7f454c4601" ]; then + echo "Invalid ELF library reported by NVIDIA compat32 discovery: ${library}" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 + fi + + if ! canonical="$(readlink -f "${library}" 2>/dev/null)" || [ -z "${canonical}" ]; then + echo "Unable to resolve NVIDIA compat32 library: ${library}" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 + fi + + compat_libraries+=("${library}") + compat_folders+=("$(dirname "${library}")") + compat_targets+=("${canonical}") + done <<< "${compat_output}" + + for folder in "${compat_folders[@]}" ; do + [ -n "${folder}" ] || continue + scanned="false" + for target in "${scanned_folders[@]}" ; do + if [ "${folder}" == "${target}" ]; then + scanned="true" + break + fi + done + [ "${scanned}" == "false" ] || continue + scanned_folders+=("${folder}") + + for candidate in "${folder}"/* ; do + [ -L "${candidate}" ] || continue + if ! canonical="$(readlink -f "${candidate}" 2>/dev/null)" || [ -z "${canonical}" ]; then + continue + fi + + for target in "${compat_targets[@]}" ; do + [ -n "${target}" ] || continue + if [ "${canonical}" == "${target}" ]; then + compat_libraries+=("${candidate}") + break + fi + done + done + done + + compat_json="$(printf '%s\n' "${compat_libraries[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | unique | sort')" + folders_json="$(printf '%s\n' "${compat_folders[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | unique | sort')" + + if ! jq -e --argjson compat "${compat_json}" --argjson folders "${folders_json}" ' + def valid_spec: + .kind == "nvidia.com/gpu" and + (.devices | type == "array") and + (.containerEdits | type == "object") and + ((.containerEdits.mounts // []) | type == "array") and + ((.containerEdits.hooks // []) | type == "array") and + all(.containerEdits.hooks[]?; type == "object" and (.args | type == "array")); + def is_ldcache: + .args[0:2] == ["nvidia-cdi-hook", "update-ldcache"]; + def has_folder($args; $folder): + [range(0; $args | length) as $index | + select($args[$index] == "--folder" and $args[$index + 1] == $folder)] | + length > 0; + def add_folders($folders): + reduce $folders[] as $folder (.; + if has_folder(.args; $folder) then . + else .args += ["--folder", $folder] + end); + def gbm_source: + "../libnvidia-allocator.so.1::/usr/lib64/gbm/nvidia-drm_gbm.so"; + def gbm_target: + "/usr/lib64/libnvidia-allocator.so.1::/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so"; + + if (valid_spec | not) then error("invalid NVIDIA CDI specification") else . end | + .containerEdits.mounts = (.containerEdits.mounts // []) | + .containerEdits.hooks = (.containerEdits.hooks // []) | + reduce $compat[] as $path (.; + if any(.containerEdits.mounts[]?; + .hostPath == $path and .containerPath == $path) then . + else .containerEdits.mounts += [{ + hostPath: $path, + containerPath: $path, + options: ["ro", "nosuid", "nodev", "rbind", "rprivate"] + }] + end) | + if ($folders | length) == 0 then . + elif any(.containerEdits.hooks[]; is_ldcache) then + .containerEdits.hooks |= map(if is_ldcache then add_folders($folders) else . end) + else error("NVIDIA CDI update-ldcache hook is missing") + end | + if any(.containerEdits.hooks[].args[]; . == gbm_target) then . + else + .containerEdits.hooks |= map( + if (.args | index(gbm_source)) != null then + .args += ["--link", gbm_target] + else . end) + end + ' "${generated}" >"${transformed}" ; then + echo "Failed to augment NVIDIA CDI specification" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 + fi + + if ! mv -f "${transformed}" "${cdi_path}" ; then + echo "Failed to publish NVIDIA CDI specification" >&2 + rm -f "${generated}" "${transformed}" + rmdir "${staging}" 2>/dev/null || true + return 1 + fi + + rm -f "${generated}" + rmdir "${staging}" 2>/dev/null || true } #Create settings file if not found @@ -390,10 +551,10 @@ if ! modinfo nvidia -0 >/dev/null 2>&1 ; then echo "-----------------Installing Nvidia ${OS}Driver Package v$(echo $LAT_PACKAGE | cut -d '-' -f2)-------------------" install > /dev/null activate - generate_cdi + generate_cdi || exit 1 else activate - generate_cdi + generate_cdi || exit 1 fi #Enable update check diff --git a/packages/nvidia-driver-2026.08.01.txz b/packages/nvidia-driver-2026.08.01.txz new file mode 100644 index 0000000..d82c9d4 Binary files /dev/null and b/packages/nvidia-driver-2026.08.01.txz differ diff --git a/packages/nvidia-driver-2026.08.01.txz.md5 b/packages/nvidia-driver-2026.08.01.txz.md5 new file mode 100644 index 0000000..9ab50ad --- /dev/null +++ b/packages/nvidia-driver-2026.08.01.txz.md5 @@ -0,0 +1 @@ +b78519b667ec93207a04a8ffbf38496c diff --git a/tests/generate-cdi-test.sh b/tests/generate-cdi-test.sh new file mode 100755 index 0000000..398f1bc --- /dev/null +++ b/tests/generate-cdi-test.sh @@ -0,0 +1,248 @@ +#!/bin/bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_ROOT="$(mktemp -d)" +INSTALL_SCRIPT="${TEST_ROOT}/install.sh" +FUNCTION_SCRIPT="${TEST_ROOT}/generate-cdi.sh" + +cleanup() { + rm -rf "${TEST_ROOT}" +} + +trap cleanup EXIT + +python3 - "${REPO_ROOT}/nvidia-driver.plg" "${INSTALL_SCRIPT}" <<'PY' +import sys +import xml.etree.ElementTree as ET + +root = ET.parse(sys.argv[1]).getroot() +for file_element in root.findall("FILE"): + inline = file_element.find("INLINE") + if inline is not None and inline.text and "generate_cdi()" in inline.text: + with open(sys.argv[2], "w", encoding="utf-8") as output: + output.write(inline.text) + break +else: + raise SystemExit("generate_cdi function not found") +PY + +sed -n '/^generate_cdi() {/,/^}$/p' "${INSTALL_SCRIPT}" >"${FUNCTION_SCRIPT}" +# shellcheck source=/dev/null +source "${FUNCTION_SCRIPT}" + +write_fixture() { + local output="$1" + + jq -n \ + --arg compat_path "${MOCK_EXISTING_COMPAT_PATH:-}" \ + --arg compat_alias "${MOCK_EXISTING_COMPAT_ALIAS:-}" \ + --arg compat_folder "${MOCK_EXISTING_COMPAT_FOLDER:-}" ' + def mount($path): { + hostPath: $path, + containerPath: $path, + options: ["ro"] + }; + { + cdiVersion: "0.5.0", + kind: "nvidia.com/gpu", + devices: [{name: "GPU-test", containerEdits: {}}], + containerEdits: { + mounts: [mount("/usr/lib64/existing.so")] + + ([$compat_path, $compat_alias] | map(select(length > 0) | mount(.))), + hooks: [ + { + hookName: "createContainer", + path: "/usr/bin/nvidia-cdi-hook", + args: [ + "nvidia-cdi-hook", + "create-symlinks", + "--link", + "../libnvidia-allocator.so.1::/usr/lib64/gbm/nvidia-drm_gbm.so" + ] + }, + { + hookName: "createContainer", + path: "/usr/bin/nvidia-cdi-hook", + args: ([ + "nvidia-cdi-hook", + "update-ldcache", + "--folder", + "/usr/lib64" + ] + if $compat_folder == "" then [] else ["--folder", $compat_folder] end) + } + ] + } + }' >"${output}" +} + +nvidia-ctk() { + local argument output="" + + if [ "${MOCK_GENERATION_STATUS:-0}" -ne 0 ]; then + return "${MOCK_GENERATION_STATUS}" + fi + + for argument in "$@" ; do + case "${argument}" in + --output=*) output="${argument#--output=}" ;; + esac + done + + if [ "${MOCK_GENERATED_SPEC:-valid}" == "valid" ]; then + write_fixture "${output}" + else + printf '%s\n' "${MOCK_GENERATED_SPEC}" >"${output}" + fi +} + +nvidia-container-cli() { + if [ "${MOCK_DISCOVERY_STATUS:-0}" -ne 0 ]; then + return "${MOCK_DISCOVERY_STATUS}" + fi + + printf '%s' "${MOCK_LIBRARY_OUTPUT:-}" +} + +assert_eq() { + local expected="$1" actual="$2" message="$3" + + if [ "${expected}" != "${actual}" ]; then + echo "FAIL: ${message}: expected '${expected}', got '${actual}'" >&2 + exit 1 + fi +} + +run_positive_case() { + local case_root="${TEST_ROOT}/positive" + local lib32_dir="${case_root}/usr/lib" + local lib64_dir="${case_root}/usr/lib64" + local lib32="${lib32_dir}/libGLX_nvidia.so.595.84" + local alias="${lib32_dir}/libGLX_nvidia.so.0" + local broken_alias="${lib32_dir}/libnvidia-broken.so.0" + local lib64="${lib64_dir}/libGLX_nvidia.so.595.84" + local spec="${case_root}/nvidia.yaml" + + mkdir -p "${lib32_dir}" "${lib64_dir}" + printf '\177ELF\001fixture' >"${lib32}" + printf '\177ELF\002fixture' >"${lib64}" + ln -s "$(basename "${lib32}")" "${alias}" + ln -s "libnvidia-missing.so" "${broken_alias}" + + export NVIDIA_CDI_PATH="${spec}" + MOCK_LIBRARY_OUTPUT="${lib64}"$'\n'"${lib32}"$'\n' + MOCK_DISCOVERY_STATUS=0 + MOCK_GENERATED_SPEC=valid + generate_cdi + + assert_eq "nvidia.com/gpu" "$(jq -r '.kind' "${spec}")" "vendor kind" + assert_eq "true" "$(jq -r --arg path "${lib32}" 'any(.containerEdits.mounts[]; .hostPath == $path and .containerPath == $path)' "${spec}")" "ELF32 mount" + assert_eq "true" "$(jq -r --arg path "${alias}" 'any(.containerEdits.mounts[]; .hostPath == $path and .containerPath == $path)' "${spec}")" "ELF32 alias mount" + assert_eq "false" "$(jq -r --arg path "${broken_alias}" 'any(.containerEdits.mounts[]; .hostPath == $path)' "${spec}")" "broken alias exclusion" + assert_eq "false" "$(jq -r --arg path "${lib64}" 'any(.containerEdits.mounts[]; .hostPath == $path)' "${spec}")" "ELF64 exclusion" + assert_eq "true" "$(jq -r --arg folder "${lib32_dir}" 'any(.containerEdits.hooks[].args[]; . == $folder)' "${spec}")" "compat32 ldconfig folder" + assert_eq "true" "$(jq -r 'any(.containerEdits.hooks[].args[]; . == "/usr/lib64/libnvidia-allocator.so.1::/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so")' "${spec}")" "GBM compatibility link" +} + +run_no_compat_case() { + local case_root="${TEST_ROOT}/no-compat" + local spec="${case_root}/nvidia.yaml" + + mkdir -p "${case_root}" + export NVIDIA_CDI_PATH="${spec}" + MOCK_LIBRARY_OUTPUT="" + MOCK_DISCOVERY_STATUS=0 + MOCK_GENERATED_SPEC=valid + generate_cdi + + assert_eq "1" "$(jq -r '.containerEdits.mounts | length' "${spec}")" "native mount preservation" + assert_eq "true" "$(jq -r 'any(.containerEdits.hooks[].args[]; . == "/usr/lib64/libnvidia-allocator.so.1::/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so")' "${spec}")" "no-compat GBM compatibility link" +} + +run_upstream_complete_case() { + local case_root="${TEST_ROOT}/upstream-complete" + local lib32_dir="${case_root}/usr/lib" + local lib32="${lib32_dir}/libGLX_nvidia.so.595.84" + local alias="${lib32_dir}/libGLX_nvidia.so.0" + local spec="${case_root}/nvidia.yaml" + + mkdir -p "${lib32_dir}" + printf '\177ELF\001fixture' >"${lib32}" + ln -s "$(basename "${lib32}")" "${alias}" + + export NVIDIA_CDI_PATH="${spec}" + MOCK_LIBRARY_OUTPUT="${lib32}"$'\n' + MOCK_DISCOVERY_STATUS=0 + MOCK_GENERATION_STATUS=0 + MOCK_GENERATED_SPEC=valid + MOCK_EXISTING_COMPAT_PATH="${lib32}" + MOCK_EXISTING_COMPAT_ALIAS="${alias}" + MOCK_EXISTING_COMPAT_FOLDER="${lib32_dir}" + generate_cdi + MOCK_EXISTING_COMPAT_PATH="" + MOCK_EXISTING_COMPAT_ALIAS="" + MOCK_EXISTING_COMPAT_FOLDER="" + + assert_eq "1" "$(jq -r --arg path "${lib32}" '[.containerEdits.mounts[] | select(.hostPath == $path)] | length' "${spec}")" "upstream ELF32 mount is not duplicated" + assert_eq "1" "$(jq -r --arg path "${alias}" '[.containerEdits.mounts[] | select(.hostPath == $path)] | length' "${spec}")" "upstream ELF32 alias is not duplicated" + assert_eq "1" "$(jq -r --arg folder "${lib32_dir}" '[.containerEdits.hooks[] | select(.args[0:2] == ["nvidia-cdi-hook", "update-ldcache"]) | .args[] | select(. == $folder)] | length' "${spec}")" "upstream compat32 folder is not duplicated" +} + +run_preservation_case() { + local name="$1" generated_spec="$2" discovery_status="$3" + local generation_status="${4:-0}" library_output="${5:-}" + local case_root="${TEST_ROOT}/${name}" + local spec="${case_root}/nvidia.yaml" + + mkdir -p "${case_root}" + printf '%s\n' 'stable-spec' >"${spec}" + + export NVIDIA_CDI_PATH="${spec}" + MOCK_LIBRARY_OUTPUT="${library_output}" + MOCK_DISCOVERY_STATUS="${discovery_status}" + MOCK_GENERATION_STATUS="${generation_status}" + MOCK_GENERATED_SPEC="${generated_spec}" + + if generate_cdi 2>/dev/null ; then + echo "FAIL: ${name} unexpectedly succeeded" >&2 + exit 1 + fi + + assert_eq "stable-spec" "$(cat "${spec}")" "${name} preserves stable spec" +} + +run_invalid_elf_case() { + local case_root="${TEST_ROOT}/invalid-elf" + local spec="${case_root}/nvidia.yaml" + local library="${case_root}/libnvidia.so" + + mkdir -p "${case_root}" + printf '%s\n' 'stable-spec' >"${spec}" + printf '%s\n' 'not-an-elf-library' >"${library}" + + export NVIDIA_CDI_PATH="${spec}" + MOCK_LIBRARY_OUTPUT="${library}" + MOCK_DISCOVERY_STATUS=0 + MOCK_GENERATION_STATUS=0 + MOCK_GENERATED_SPEC=valid + + if generate_cdi 2>/dev/null ; then + echo "FAIL: invalid-elf unexpectedly succeeded" >&2 + exit 1 + fi + + assert_eq "stable-spec" "$(cat "${spec}")" "invalid-elf preserves stable spec" +} + +run_positive_case +run_no_compat_case +run_upstream_complete_case +run_preservation_case "discovery-failure" valid 17 +run_preservation_case "generation-failure" valid 0 23 +run_preservation_case "malformed-generation" '{not-json' 0 +run_preservation_case "structural-generation" '[]' 0 +run_preservation_case "missing-library" valid 0 0 "${TEST_ROOT}/missing/libnvidia.so" +run_invalid_elf_case + +echo "generate_cdi tests passed" diff --git a/tests/probe-compat32-vulkan.sh b/tests/probe-compat32-vulkan.sh new file mode 100755 index 0000000..80ebcdd --- /dev/null +++ b/tests/probe-compat32-vulkan.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -euo pipefail + +container="${1:-}" +probe_user="${PROBE_USER:-retro}" +display="${DISPLAY:-:0}" +probe="${VULKAN_PROBE:-/home/retro/.steam/steam/steamapps/common/SteamLinuxRuntime/steam-runtime/i386/usr/bin/vkcube}" + +if [ -z "${container}" ]; then + container="$(docker ps --format '{{.Names}}' | grep -m1 '^WolfSteam_' || true)" +fi + +if [ -z "${container}" ]; then + echo "No running WolfSteam container found" >&2 + exit 2 +fi + +probe_type="$(docker exec "${container}" file "${probe}")" +if [[ "${probe_type}" != *"ELF 32-bit"* ]]; then + echo "Vulkan probe is not ELF32: ${probe_type}" >&2 + exit 2 +fi + +set +e +output="$( + timeout 20 docker exec -u "${probe_user}" -e DISPLAY="${display}" "${container}" \ + "${probe}" --c 1 --suppress_popups 2>&1 +)" +status=$? +set -e + +printf '%s\n' "${output}" + +selected="$(printf '%s\n' "${output}" | sed -n 's/^Selected GPU [^:]*: \([^,]*\).*/\1/p' | head -1)" +if [ -z "${selected}" ]; then + echo "The ELF32 Vulkan probe did not report a selected GPU (exit ${status})" >&2 + exit 1 +fi + +if [[ "${selected,,}" != *"nvidia"* ]]; then + echo "Expected the ELF32 Vulkan probe to select NVIDIA, got: ${selected}" >&2 + exit 1 +fi + +echo "ELF32 Vulkan selected NVIDIA"