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:
+304
-12
@@ -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
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user