fix(installer): harden CUDA diagnostics with auto-remediation and behavioral tests (#451)

* fix(installer): surface pywhispercpp CUDA fallback diagnostics

* fix(installer): add patchelf auto-remediation and behavioral tests

- Add patchelf auto-remediation for bundled libcuda linkage issue
- Add ARM64 CUDA toolkit path support (aarch64-linux, sbsa-linux)
- Fix nvcc symlink resolution with readlink -f
- Add --verbose flag to CPU pywhispercpp pip install
- Add behavioral tests for CUDA diagnostic functions
- Add string-presence test for patchelf remediation

Fixes #450

* fix(installer): replace ★ with * to fix box border alignment

The ★ character (U+2605) renders as 2 columns wide in most terminals,
but the box padding assumes 1 column. This pushes the right border │
one column past the intended box edge, making it appear cut off.
Replacing with ASCII * (always 1 column) fixes the alignment.
This commit is contained in:
Jatin K Malik
2026-06-06 19:18:57 -07:00
committed by GitHub
parent cb90e849ee
commit 0c3c32f584
3 changed files with 678 additions and 12 deletions
+304 -12
View File
@@ -4,6 +4,10 @@
set -e # Exit on error
# Keep the venv isolated from ~/.local site packages while still allowing
# --system-site-packages to expose distro-provided GTK/PyGObject bindings.
export PYTHONNOUSERSITE=1
# Function to display colored output
print_info() {
echo -e "\e[1;34m[INFO]\e[0m $1"
@@ -516,6 +520,141 @@ detect_nvidia_gpu() {
fi
}
cuda_toolkit_root_has_runtime_library() {
local CUDA_ROOT="$1"
compgen -G "$CUDA_ROOT/lib64/libcudart.so*" >/dev/null && return 0
compgen -G "$CUDA_ROOT/lib/libcudart.so*" >/dev/null && return 0
compgen -G "$CUDA_ROOT/lib/x86_64-linux-gnu/libcudart.so*" >/dev/null && return 0
compgen -G "$CUDA_ROOT/targets/x86_64-linux/lib/libcudart.so*" >/dev/null && return 0
compgen -G "$CUDA_ROOT/targets/aarch64-linux/lib/libcudart.so*" >/dev/null && return 0
compgen -G "$CUDA_ROOT/targets/sbsa-linux/lib/libcudart.so*" >/dev/null && return 0
return 1
}
validate_cuda_toolkit_root() {
local CUDA_ROOT="$1"
[ -n "$CUDA_ROOT" ] || return 1
[ -x "$CUDA_ROOT/bin/nvcc" ] || return 1
[ -f "$CUDA_ROOT/include/cuda_runtime.h" ] || return 1
cuda_toolkit_root_has_runtime_library "$CUDA_ROOT" || return 1
}
candidate_cuda_toolkit_roots() {
local CUDA_VAR
for CUDA_VAR in CUDAToolkit_ROOT CUDA_HOME CUDA_PATH; do
local CUDA_ROOT="${!CUDA_VAR:-}"
[ -n "$CUDA_ROOT" ] && printf '%s\n' "$CUDA_ROOT"
done
if command -v nvcc >/dev/null 2>&1; then
local NVCC_PATH
local NVCC_ROOT
NVCC_PATH=$(readlink -f "$(command -v nvcc)" 2>/dev/null || command -v nvcc)
NVCC_ROOT=$(cd "$(dirname "$NVCC_PATH")/.." 2>/dev/null && pwd -P)
[ -n "$NVCC_ROOT" ] && printf '%s\n' "$NVCC_ROOT"
fi
local CUDA_ROOT
for CUDA_ROOT in /usr/local/cuda /usr/local/cuda-* /opt/cuda; do
[ -d "$CUDA_ROOT" ] && printf '%s\n' "$CUDA_ROOT"
done
}
find_valid_cuda_toolkit_root() {
local QUIET="${1:-no}"
local SEEN_ROOTS=""
local CUDA_ROOT
while IFS= read -r CUDA_ROOT; do
[ -n "$CUDA_ROOT" ] || continue
local NORMALIZED_ROOT
NORMALIZED_ROOT=$(cd "$CUDA_ROOT" 2>/dev/null && pwd -P) || NORMALIZED_ROOT="$CUDA_ROOT"
if [[ ":$SEEN_ROOTS:" == *":$NORMALIZED_ROOT:"* ]]; then
continue
fi
SEEN_ROOTS="${SEEN_ROOTS:+$SEEN_ROOTS:}$NORMALIZED_ROOT"
if validate_cuda_toolkit_root "$NORMALIZED_ROOT"; then
printf '%s\n' "$NORMALIZED_ROOT"
return 0
fi
if [[ "$QUIET" != "quiet" ]]; then
print_warning "Ignoring incomplete CUDA toolkit root: $NORMALIZED_ROOT" >&2
print_warning " Required: bin/nvcc, include/cuda_runtime.h, and libcudart.so*" >&2
fi
done < <(candidate_cuda_toolkit_roots)
return 1
}
detect_nvidia_compute_architectures() {
command -v nvidia-smi >/dev/null 2>&1 || return 1
local COMPUTE_CAPS
COMPUTE_CAPS=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true)
[ -n "$COMPUTE_CAPS" ] || return 1
printf '%s\n' "$COMPUTE_CAPS" | awk '
{
gsub(/[[:space:]]/, "", $1)
if ($1 ~ /^[0-9]+(\.[0-9]+)?$/) {
split($1, parts, ".")
minor = parts[2]
if (minor == "") {
minor = "0"
}
print parts[1] minor "-real"
}
}
' | sort -u | paste -sd ';' -
}
cuda_toolkit_supports_architectures() {
local CUDA_ROOT="$1"
local CUDA_ARCHS="$2"
[ -n "$CUDA_ARCHS" ] || return 0
local NVCC_ARCHS
NVCC_ARCHS=$("$CUDA_ROOT/bin/nvcc" --list-gpu-arch 2>/dev/null || true)
if [ -z "$NVCC_ARCHS" ]; then
print_warning "Could not query supported CUDA architectures from $CUDA_ROOT/bin/nvcc" >&2
print_warning "Continuing with detected CMAKE_CUDA_ARCHITECTURES=$CUDA_ARCHS" >&2
return 0
fi
local IFS=';'
local CUDA_ARCH
for CUDA_ARCH in $CUDA_ARCHS; do
local ARCH_DIGITS="${CUDA_ARCH%%-*}"
if ! printf '%s\n' "$NVCC_ARCHS" | grep -q "compute_$ARCH_DIGITS"; then
print_warning "CUDA toolkit at $CUDA_ROOT cannot target compute capability $ARCH_DIGITS." >&2
print_warning "Install a newer CUDA toolkit, such as CUDA 11.8+ for RTX 40/Ada GPUs." >&2
return 1
fi
done
return 0
}
get_cuda_cmake_args() {
local CUDA_ROOT="$1"
local CUDA_ARGS="-DCUDAToolkit_ROOT=$CUDA_ROOT -DCMAKE_CUDA_COMPILER=$CUDA_ROOT/bin/nvcc"
local CUDA_ARCHS
CUDA_ARCHS=$(detect_nvidia_compute_architectures || true)
if [ -n "$CUDA_ARCHS" ]; then
cuda_toolkit_supports_architectures "$CUDA_ROOT" "$CUDA_ARCHS" || return 1
CUDA_ARGS="$CUDA_ARGS -DCMAKE_CUDA_ARCHITECTURES=$CUDA_ARCHS"
fi
printf '%s\n' "$CUDA_ARGS"
}
# Detect Vulkan support for whisper.cpp
detect_vulkan() {
# Check for vulkaninfo command
@@ -696,7 +835,7 @@ detect_whispercpp_backends() {
# Check for CUDA
local HAS_CUDA_DEV=false
if command -v nvcc >/dev/null 2>&1; then
if find_valid_cuda_toolkit_root quiet >/dev/null 2>&1; then
HAS_CUDA_DEV=true
fi
@@ -866,7 +1005,7 @@ EOF
print_header "Step 2: Choose Speech Recognition Engine"
echo ""
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 1. WHISPER.CPP ★ RECOMMENDED │"
echo " │ 1. WHISPER.CPP * RECOMMENDED │"
echo " │ • Fastest, most accurate, works with any GPU │"
echo " │ • Supports NVIDIA (CUDA), AMD, Intel (Vulkan) │"
echo " │ • CPU-only mode available for older systems │"
@@ -983,7 +1122,7 @@ EOF
if [[ "$CAN_BUILD_GPU" == "true" ]]; then
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 1. GPU (Vulkan/CUDA) ★ RECOMMENDED │"
echo " │ 1. GPU (Vulkan/CUDA) * RECOMMENDED │"
echo " │ • Fastest performance with GPU acceleration │"
echo " │ • $RECOMMENDED_REASON │"
echo " │ • Requires building from source (takes ~2-5 min) │"
@@ -1007,7 +1146,7 @@ EOF
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 2. CPU (Pre-built) ★ RECOMMENDED │"
echo " │ 2. CPU (Pre-built) * RECOMMENDED │"
echo " │ • Works on all systems │"
echo " │ • Fast installation (no compilation) │"
echo " │ • Good performance on modern CPUs │"
@@ -2108,6 +2247,7 @@ ACTIVATION_SCRIPT="$ACTIVATION_SCRIPT_DIR/activate-vocalinux.sh"
cat > "$ACTIVATION_SCRIPT" << EOF
#!/bin/bash
# This script activates the Vocalinux virtual environment
export PYTHONNOUSERSITE=1
source "$VENV_DIR/bin/activate"
echo "Vocalinux virtual environment activated."
echo "To start the application, run: vocalinux"
@@ -2134,7 +2274,7 @@ for attr in ("getsitepackages",):
pass
user_site = getattr(site, "getusersitepackages", lambda: None)()
if user_site:
if user_site and getattr(site, "ENABLE_USER_SITE", False):
roots.append(user_site)
for key in ("platlib", "purelib"):
@@ -2183,6 +2323,124 @@ is_pywhispercpp_gpu_capable() {
return 1
}
is_pywhispercpp_backend_capable() {
local BACKEND
BACKEND=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
local LIB_DIRS
LIB_DIRS=$(get_pywhispercpp_library_path || true)
[ -n "$LIB_DIRS" ] || return 1
local EXPECTED_LIB=""
case "$BACKEND" in
cuda)
EXPECTED_LIB="libggml-cuda.so"
;;
vulkan)
EXPECTED_LIB="libggml-vulkan.so"
;;
*)
return 1
;;
esac
local IFS=:
local LIB_DIR
for LIB_DIR in $LIB_DIRS; do
if compgen -G "$LIB_DIR/$EXPECTED_LIB*" >/dev/null; then
return 0
fi
done
return 1
}
is_pywhispercpp_cuda_linkage_usable() {
if ! command -v readelf >/dev/null 2>&1; then
print_warning "readelf not found; skipping CUDA linkage verification." >&2
return 0
fi
local LIB_DIRS
LIB_DIRS=$(get_pywhispercpp_library_path || true)
[ -n "$LIB_DIRS" ] || return 1
local HAS_PATCHELF=false
if command -v patchelf >/dev/null 2>&1; then
HAS_PATCHELF=true
fi
local IFS=:
local LIB_DIR
for LIB_DIR in $LIB_DIRS; do
local CUDA_LIB
for CUDA_LIB in "$LIB_DIR"/libggml-cuda.so*; do
[ -f "$CUDA_LIB" ] || continue
local BUNDLED_LIBS
BUNDLED_LIBS=$(readelf -d "$CUDA_LIB" 2>/dev/null | sed -En 's/.*Shared library: \[(libcuda-[^]]+\.so).*/\1/p')
if [ -z "$BUNDLED_LIBS" ]; then
continue
fi
local BUNDLED_LIB
while IFS= read -r BUNDLED_LIB; do
[ -n "$BUNDLED_LIB" ] || continue
print_warning "CUDA backend links against bundled $BUNDLED_LIB instead of libcuda.so.1:"
print_warning " $CUDA_LIB"
if [[ "$HAS_PATCHELF" == "true" ]]; then
print_info "Attempting to relink with patchelf ($BUNDLED_LIB → libcuda.so.1)..."
if patchelf --replace-needed "$BUNDLED_LIB" libcuda.so.1 "$CUDA_LIB" 2>/dev/null; then
print_success "Successfully relinked $CUDA_LIB to libcuda.so.1"
else
print_warning "patchelf relink failed for $CUDA_LIB"
print_warning "Treating CUDA verification as failed so the installer does not report broken GPU support."
return 1
fi
else
print_warning "Install patchelf to attempt automatic relinking: sudo apt install patchelf"
print_warning "Treating CUDA verification as failed so the installer does not report broken GPU support."
return 1
fi
done <<< "$BUNDLED_LIBS"
done
done
return 0
}
verify_pywhispercpp_backend_install() {
local BACKEND="$1"
if ! is_pywhispercpp_installed; then
print_warning "pywhispercpp installed but import verification failed for $BACKEND backend."
return 1
fi
if ! is_pywhispercpp_backend_capable "$BACKEND"; then
print_warning "pywhispercpp installed but $BACKEND backend libraries were not found."
return 1
fi
if [[ "$BACKEND" == "CUDA" ]] && ! is_pywhispercpp_cuda_linkage_usable; then
return 1
fi
return 0
}
print_pip_log_tail() {
local PIP_LOG_FILE="$1"
local LINE_COUNT="${2:-80}"
if [ -s "$PIP_LOG_FILE" ]; then
print_warning "Last $LINE_COUNT lines from pip build log ($PIP_LOG_FILE):"
tail -n "$LINE_COUNT" "$PIP_LOG_FILE" | sed 's/^/ /'
else
print_warning "Pip build log is empty or missing: $PIP_LOG_FILE"
fi
}
with_pywhispercpp_library_path() {
local PYWHISPERCPP_LIBRARY_PATH
PYWHISPERCPP_LIBRARY_PATH=$(get_pywhispercpp_library_path || true)
@@ -2204,7 +2462,7 @@ install_cpu_pywhispercpp() {
PYWHISPERCPP_CMAKE_ARGS=$(get_pywhispercpp_cmake_args)
CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" \
pip install --force-reinstall --no-cache-dir pywhispercpp --log "$PIP_LOG_FILE"
pip install --verbose --force-reinstall --no-cache-dir pywhispercpp --log "$PIP_LOG_FILE"
}
is_pywhispercpp_installed() {
@@ -2312,10 +2570,17 @@ install_whispercpp_with_gpu_support() {
print_info " Installing pywhispercpp with Vulkan support..."
GPU_BACKEND="Vulkan"
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
if CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" GGML_VULKAN=1 pip install --force-reinstall --no-cache-dir git+https://github.com/absadiki/pywhispercpp --log "$PIP_LOG_FILE" 2>&1; then
GPU_INSTALL_SUCCESS=true
if CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" \
GGML_VULKAN=1 \
pip install --verbose --force-reinstall --no-cache-dir git+https://github.com/absadiki/pywhispercpp --log "$PIP_LOG_FILE" 2>&1; then
if verify_pywhispercpp_backend_install "$GPU_BACKEND"; then
GPU_INSTALL_SUCCESS=true
else
print_pip_log_tail "$PIP_LOG_FILE"
fi
else
print_warning "Vulkan build failed - checking for NVIDIA GPU to try CUDA..."
print_warning "Vulkan build failed; checking for NVIDIA GPU to try CUDA..."
print_pip_log_tail "$PIP_LOG_FILE"
fi
fi
@@ -2324,9 +2589,31 @@ install_whispercpp_with_gpu_support() {
print_info "✓ NVIDIA GPU detected: $GPU_NAME"
print_info " Installing pywhispercpp with CUDA support..."
GPU_BACKEND="CUDA"
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
if CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" GGML_CUDA=1 pip install --force-reinstall --no-cache-dir git+https://github.com/absadiki/pywhispercpp --log "$PIP_LOG_FILE" 2>&1; then
GPU_INSTALL_SUCCESS=true
local CUDA_TOOLKIT_ROOT=""
local CUDA_CMAKE_ARGS=""
if CUDA_TOOLKIT_ROOT=$(find_valid_cuda_toolkit_root); then
if CUDA_CMAKE_ARGS=$(get_cuda_cmake_args "$CUDA_TOOLKIT_ROOT"); then
print_info "Using CUDA toolkit: $CUDA_TOOLKIT_ROOT"
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
if CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS $CUDA_CMAKE_ARGS" \
GGML_CUDA=1 \
pip install --verbose --force-reinstall --no-cache-dir git+https://github.com/absadiki/pywhispercpp --log "$PIP_LOG_FILE" 2>&1; then
if verify_pywhispercpp_backend_install "$GPU_BACKEND"; then
GPU_INSTALL_SUCCESS=true
else
print_pip_log_tail "$PIP_LOG_FILE"
fi
else
print_warning "CUDA build failed."
print_pip_log_tail "$PIP_LOG_FILE"
fi
else
print_warning "Skipping CUDA build because the detected toolkit cannot target this NVIDIA GPU."
fi
else
print_warning "No complete CUDA toolkit root found; skipping CUDA build."
print_info " Required CUDA files: bin/nvcc, include/cuda_runtime.h, and libcudart.so*"
fi
fi
fi
@@ -2351,6 +2638,9 @@ install_whispercpp_with_gpu_support() {
print_info "ℹ No GPU detected - installing CPU-only version"
print_info " CPU mode is still very fast!"
fi
if [[ "$GPU_BACKEND" != "CPU" ]]; then
print_warning "Continuing with CPU-only pywhispercpp; GPU acceleration is not active."
fi
GPU_BACKEND="CPU"
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
install_cpu_pywhispercpp "$PIP_LOG_FILE" || {
@@ -2774,6 +3064,7 @@ REMOTE_CONFIG
#!/bin/bash
# Wrapper script for Vocalinux that sets required environment variables
# and applies the 'input' group for keyboard shortcuts on Wayland
export PYTHONNOUSERSITE=1
export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED
PYWHISPERCPP_LIBRARY_PATH=""
PY_SITE_PATHS=\$("$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
@@ -2818,6 +3109,7 @@ WRAPPER_EOF
#!/bin/bash
# Wrapper script for Vocalinux GUI that sets required environment variables
# and applies the 'input' group for keyboard shortcuts on Wayland
export PYTHONNOUSERSITE=1
export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED
PYWHISPERCPP_LIBRARY_PATH=""
PY_SITE_PATHS=\$("$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
+73
View File
@@ -0,0 +1,73 @@
"""Regression checks for installer CUDA diagnostics."""
import unittest
from pathlib import Path
INSTALLER = Path(__file__).resolve().parents[1] / "install.sh"
def _installer_source() -> str:
return INSTALLER.read_text(encoding="utf-8")
class InstallerCudaDiagnosticsTests(unittest.TestCase):
def test_installer_disables_user_site_packages(self) -> None:
"""Keep user-site packages out of the venv, activation script, and wrappers."""
source = _installer_source()
self.assertGreaterEqual(source.count("export PYTHONNOUSERSITE=1"), 4)
self.assertIn('getattr(site, "ENABLE_USER_SITE", False)', source)
def test_cuda_toolkit_validation_requires_complete_root(self) -> None:
"""CUDA roots must not be accepted when only a stale /usr/local/cuda exists."""
source = _installer_source()
self.assertIn("validate_cuda_toolkit_root()", source)
self.assertIn("$CUDA_ROOT/bin/nvcc", source)
self.assertIn("$CUDA_ROOT/include/cuda_runtime.h", source)
self.assertIn("$CUDA_ROOT/lib64/libcudart.so*", source)
self.assertIn("Ignoring incomplete CUDA toolkit root", source)
def test_cuda_build_passes_explicit_cmake_toolkit_and_architecture(self) -> None:
"""CUDA builds should steer CMake away from stale defaults."""
source = _installer_source()
self.assertIn("-DCUDAToolkit_ROOT=$CUDA_ROOT", source)
self.assertIn("-DCMAKE_CUDA_COMPILER=$CUDA_ROOT/bin/nvcc", source)
self.assertIn("-DCMAKE_CUDA_ARCHITECTURES=$CUDA_ARCHS", source)
self.assertIn("CUDA_CMAKE_ARGS=$(get_cuda_cmake_args", source)
self.assertIn("GGML_CUDA=1", source)
def test_gpu_build_failures_print_pip_log_tail_before_cpu_fallback(self) -> None:
"""Backend failures should expose the real pip/CMake log."""
source = _installer_source()
self.assertIn("print_pip_log_tail()", source)
self.assertIn("Vulkan build failed; checking for NVIDIA GPU to try CUDA", source)
self.assertIn("CUDA build failed.", source)
self.assertIn(
"Continuing with CPU-only pywhispercpp; GPU acceleration is not active.",
source,
)
def test_backend_verification_checks_gpu_libraries_and_cuda_linkage(self) -> None:
"""Do not report GPU support unless native backend libraries exist."""
source = _installer_source()
self.assertIn("verify_pywhispercpp_backend_install()", source)
self.assertIn("libggml-vulkan.so", source)
self.assertIn("libggml-cuda.so", source)
self.assertIn("libcuda.so.1", source)
self.assertIn("libcuda-[^]]+\\.so", source)
def test_patchelf_remediation_is_attempted_for_bundled_libcuda(self) -> None:
"""Bundled libcuda should trigger patchelf relinking when available."""
source = _installer_source()
self.assertIn("patchelf", source)
self.assertIn("--replace-needed", source)
self.assertIn("readelf not found", source)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,301 @@
"""Behavioral tests for installer CUDA diagnostic functions."""
import os
import subprocess
import tempfile
from pathlib import Path
import pytest
INSTALL_SH = Path(__file__).resolve().parents[1] / "install.sh"
def source_install_sh_functions(tmpdir: Path) -> str:
"""Extract and source only the function definitions from install.sh."""
content = INSTALL_SH.read_text()
setup_script = f"""
set -e
cd "{tmpdir}"
# Mock output functions
print_warning() {{ echo "WARNING: $*" >&2; }}
print_info() {{ echo "INFO: $*" >&2; }}
print_success() {{ echo "SUCCESS: $*" >&2; }}
print_error() {{ echo "ERROR: $*" >&2; }}
# Mock get_pywhispercpp_library_path (will be overridden per test)
get_pywhispercpp_library_path() {{ echo ""; }}
# Source the actual functions from install.sh
# Extract function definitions using sed
"""
return setup_script
def run_bash_test(script: str) -> subprocess.CompletedProcess:
"""Run a bash script and return the result."""
result = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=10)
return result
class TestCudaToolkitRootHasRuntimeLibrary:
"""Tests for cuda_toolkit_root_has_runtime_library function."""
def test_accepts_lib64_layout(self, tmp_path):
"""Standard x86_64 layout with lib64/libcudart.so."""
cuda_root = tmp_path / "cuda"
(cuda_root / "lib64").mkdir(parents=True)
(cuda_root / "lib64" / "libcudart.so.12.2").touch()
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
cuda_toolkit_root_has_runtime_library "{cuda_root}" && echo "PASS" || echo "FAIL"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
def test_accepts_lib_layout(self, tmp_path):
"""Layout with lib/libcudart.so."""
cuda_root = tmp_path / "cuda"
(cuda_root / "lib").mkdir(parents=True)
(cuda_root / "lib" / "libcudart.so").touch()
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
cuda_toolkit_root_has_runtime_library "{cuda_root}" && echo "PASS" || echo "FAIL"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
def test_accepts_debian_layout(self, tmp_path):
"""Debian multiarch layout with lib/x86_64-linux-gnu/libcudart.so."""
cuda_root = tmp_path / "cuda"
(cuda_root / "lib" / "x86_64-linux-gnu").mkdir(parents=True)
(cuda_root / "lib" / "x86_64-linux-gnu" / "libcudart.so.11.8").touch()
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
cuda_toolkit_root_has_runtime_library "{cuda_root}" && echo "PASS" || echo "FAIL"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
def test_accepts_aarch64_layout(self, tmp_path):
"""ARM64 layout with targets/aarch64-linux/lib/libcudart.so."""
cuda_root = tmp_path / "cuda"
(cuda_root / "targets" / "aarch64-linux" / "lib").mkdir(parents=True)
(cuda_root / "targets" / "aarch64-linux" / "lib" / "libcudart.so.12.2").touch()
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
cuda_toolkit_root_has_runtime_library "{cuda_root}" && echo "PASS" || echo "FAIL"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
def test_rejects_missing_runtime(self, tmp_path):
"""Reject when libcudart.so* is missing."""
cuda_root = tmp_path / "cuda"
cuda_root.mkdir(parents=True)
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
cuda_toolkit_root_has_runtime_library "{cuda_root}" && echo "FAIL" || echo "PASS"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
class TestValidateCudaToolkitRoot:
"""Tests for validate_cuda_toolkit_root function."""
def test_accepts_complete_root(self, tmp_path):
"""Accept when nvcc, cuda_runtime.h, and libcudart.so all present."""
cuda_root = tmp_path / "cuda"
(cuda_root / "bin").mkdir(parents=True)
(cuda_root / "include").mkdir(parents=True)
(cuda_root / "lib64").mkdir(parents=True)
nvcc = cuda_root / "bin" / "nvcc"
nvcc.touch()
nvcc.chmod(0o755)
(cuda_root / "include" / "cuda_runtime.h").touch()
(cuda_root / "lib64" / "libcudart.so.12.2").touch()
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
source <(sed -n '/^validate_cuda_toolkit_root/,/^}}/p' "{INSTALL_SH}")
validate_cuda_toolkit_root "{cuda_root}" && echo "PASS" || echo "FAIL"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
def test_rejects_stale_root(self, tmp_path):
"""Reject when only bin/nvcc exists (stale /usr/local/cuda)."""
cuda_root = tmp_path / "cuda"
(cuda_root / "bin").mkdir(parents=True)
nvcc = cuda_root / "bin" / "nvcc"
nvcc.touch()
nvcc.chmod(0o755)
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
source <(sed -n '/^validate_cuda_toolkit_root/,/^}}/p' "{INSTALL_SH}")
validate_cuda_toolkit_root "{cuda_root}" && echo "FAIL" || echo "PASS"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
def test_rejects_nvcc_not_executable(self, tmp_path):
"""Reject when nvcc exists but is not executable."""
cuda_root = tmp_path / "cuda"
(cuda_root / "bin").mkdir(parents=True)
(cuda_root / "include").mkdir(parents=True)
(cuda_root / "lib64").mkdir(parents=True)
(cuda_root / "bin" / "nvcc").touch()
(cuda_root / "include" / "cuda_runtime.h").touch()
(cuda_root / "lib64" / "libcudart.so.12.2").touch()
script = f"""
{source_install_sh_functions(tmp_path)}
source <(sed -n '/^cuda_toolkit_root_has_runtime_library/,/^}}/p' "{INSTALL_SH}")
source <(sed -n '/^validate_cuda_toolkit_root/,/^}}/p' "{INSTALL_SH}")
validate_cuda_toolkit_root "{cuda_root}" && echo "FAIL" || echo "PASS"
"""
result = run_bash_test(script)
assert "PASS" in result.stdout
class TestDetectNvidiaComputeArchitecturesAwkParser:
"""Tests for the awk parser in detect_nvidia_compute_architectures."""
def test_parses_single_gpu(self):
"""Parse single GPU compute capability 8.9 (RTX 4090)."""
script = """
echo "8.9" | awk '
{
gsub(/[[:space:]]/, "", $1)
if ($1 ~ /^[0-9]+(\\.[0-9]+)?$/) {
split($1, parts, ".")
minor = parts[2]
if (minor == "") {
minor = "0"
}
print parts[1] minor "-real"
}
}
' | sort -u | paste -sd ';' -
"""
result = run_bash_test(script)
assert result.stdout.strip() == "89-real"
def test_parses_multi_gpu(self):
"""Parse multi-GPU with different architectures."""
script = """
echo -e "8.9\\n8.6" | awk '
{
gsub(/[[:space:]]/, "", $1)
if ($1 ~ /^[0-9]+(\\.[0-9]+)?$/) {
split($1, parts, ".")
minor = parts[2]
if (minor == "") {
minor = "0"
}
print parts[1] minor "-real"
}
}
' | sort -u | paste -sd ';' -
"""
result = run_bash_test(script)
assert result.stdout.strip() == "86-real;89-real"
def test_filters_garbage(self):
"""Filter out non-numeric values like [Not Supported]."""
script = """
echo -e "[Not Supported]\\n8.9" | awk '
{
gsub(/[[:space:]]/, "", $1)
if ($1 ~ /^[0-9]+(\\.[0-9]+)?$/) {
split($1, parts, ".")
minor = parts[2]
if (minor == "") {
minor = "0"
}
print parts[1] minor "-real"
}
}
' | sort -u | paste -sd ';' -
"""
result = run_bash_test(script)
assert result.stdout.strip() == "89-real"
def test_deduplicates(self):
"""Deduplicate identical compute capabilities."""
script = """
echo -e "8.9\\n8.9\\n8.9" | awk '
{
gsub(/[[:space:]]/, "", $1)
if ($1 ~ /^[0-9]+(\\.[0-9]+)?$/) {
split($1, parts, ".")
minor = parts[2]
if (minor == "") {
minor = "0"
}
print parts[1] minor "-real"
}
}
' | sort -u | paste -sd ';' -
"""
result = run_bash_test(script)
assert result.stdout.strip() == "89-real"
class TestReadelfBundledLibcudaRegex:
"""Tests for the readelf regex that detects bundled libcuda."""
def test_detects_bundled_libcuda(self):
"""Detect auditwheel-bundled libcuda with hashed name."""
script = """
echo " 0x0000000000000001 (NEEDED) Shared library: [libcuda-535.129.03.so]" | \
sed -En 's/.*Shared library: \\[(libcuda-[^]]+\\.so).*/\\1/p'
"""
result = run_bash_test(script)
assert result.stdout.strip() == "libcuda-535.129.03.so"
def test_ignores_system_libcuda(self):
"""Do not match standard libcuda.so.1."""
script = """
echo " 0x0000000000000001 (NEEDED) Shared library: [libcuda.so.1]" | \
sed -En 's/.*Shared library: \\[(libcuda-[^]]+\\.so).*/\\1/p'
"""
result = run_bash_test(script)
assert result.stdout.strip() == ""
def test_ignores_bundled_cudart(self):
"""Do not match bundled libcudart (different library)."""
script = """
echo " 0x0000000000000001 (NEEDED) Shared library: [libcudart-12.2.140.so]" | \
sed -En 's/.*Shared library: \\[(libcuda-[^]]+\\.so).*/\\1/p'
"""
result = run_bash_test(script)
assert result.stdout.strip() == ""
def test_detects_hashed_libcuda(self):
"""Detect auditwheel hash-style bundled libcuda."""
script = """
echo " 0x0000000000000001 (NEEDED) Shared library: [libcuda-abc12345.so]" | \
sed -En 's/.*Shared library: \\[(libcuda-[^]]+\\.so).*/\\1/p'
"""
result = run_bash_test(script)
assert result.stdout.strip() == "libcuda-abc12345.so"