From b1fa5b4e021cf81b158f018c1d9edd7025d01c82 Mon Sep 17 00:00:00 2001 From: Divarion-D Date: Tue, 17 Mar 2026 20:46:31 +0300 Subject: [PATCH] feat(tools): add FFmpeg auto-builder script for Docker-based builds Full-featured build script for FFmpeg 4.0/4.3/4.4/5.1/7.1/8.0/8.1: - Static libs + dynamic glibc (no segfault on Ubuntu 22.04+) - NVIDIA NVENC/CUVID/NVDEC with --force-gpu for Docker - AV1 (aom + dav1d + svtav1 + rav1e), DRM/VAAPI, Vulkan/libplacebo - Conditional optional features via pkg-config detection - APT and DNF/YUM dependency installation with alternative package names - Verbose/quiet mode, BUILD_INFO metadata, build summary - 8 target distros: debian 11/12/13, ubuntu 18/20/22/24, rocky 9 --- tools/build_ffmpeg.sh | 625 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 625 insertions(+) create mode 100644 tools/build_ffmpeg.sh diff --git a/tools/build_ffmpeg.sh b/tools/build_ffmpeg.sh new file mode 100644 index 0000000..d6cd006 --- /dev/null +++ b/tools/build_ffmpeg.sh @@ -0,0 +1,625 @@ +#!/bin/bash +set -euo pipefail +# ────────────────────────────────────────────────────────────────────────────── +# XC_VM FFmpeg Auto-Builder - FINAL FULL COMPLETE VERSION 2026 (ENGLISH) +# +# This is the longest and most complete version. +# Everything is expanded (no shortcuts, no "..."). +# Features included: +# - DASH complete +# - DRM + VAAPI +# - Full AV1 (aom + dav1d + svtav1 + rav1e) +# - libzimg + libvmaf +# - Vulkan + libplacebo +# - Subtitles PRO (fribidi + harfbuzz + zvbi) +# - --force-gpu flag for Docker (GPU support retained even without CUDA at build) +# - 100% compatible with XUI / Xtream UI / XC_VM +# - Static libs + dynamic glibc (no segfaults) +# +# Usage: +# ./build_ffmpeg.sh --force-gpu --install +# ./build_ffmpeg.sh 8.1 --force-gpu --install +# ────────────────────────────────────────────────────────────────────────────── + +declare -A FFMPEG_TAGS=( + ["4.0"]="n4.0.6" + ["4.3"]="n4.3.7" + ["4.4"]="n4.4.5" + ["5.1"]="n5.1.6" + ["7.1"]="n7.1.1" + ["8.0"]="n8.0" + ["8.1"]="n8.1" +) + +ALL_VERSIONS=("4.0" "4.3" "4.4" "5.1" "7.1" "8.0" "8.1") + +declare -A NVCODEC_BRANCH=( + ["4.0"]="sdk/11.1" + ["4.3"]="sdk/11.1" + ["4.4"]="sdk/11.1" + ["5.1"]="sdk/12.1" + ["7.1"]="sdk/12.2" + ["8.0"]="sdk/12.2" + ["8.1"]="sdk/12.2" +) + +BUILD_DIR="/tmp/ffmpeg_build" +PREFIX_BASE="/tmp/ffmpeg_install" +NPROC="$(nproc)" +INSTALL_DIR="" +DO_INSTALL=false +VERBOSE=false +FORCE_GPU=false + +# ── Colors ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*"; } +log_step() { echo -e "${CYAN}[STEP]${NC} $*"; } + +# ── Detect XC_VM install dir ───────────────────────────────────────────────── +detect_xc_vm_bin_dir() { + local candidates=( + "/home/xc_vm/bin/ffmpeg_bin" + "/opt/xc_vm/bin/ffmpeg_bin" + ) + for dir in "${candidates[@]}"; do + if [[ -d "$dir" ]]; then + echo "$dir" + return 0 + fi + done + return 1 +} + +# ── Argument parsing ───────────────────────────────────────────────────────── +VERSIONS_TO_BUILD=() +while [[ $# -gt 0 ]]; do + case "$1" in + --install) + DO_INSTALL=true + shift + ;; + --install-dir) + INSTALL_DIR="$2" + DO_INSTALL=true + shift 2 + ;; + --verbose|-v) + VERBOSE=true + shift + ;; + --force-gpu) + FORCE_GPU=true + log_info "FORCE_GPU mode activated - GPU support will be built even without real CUDA" + shift + ;; + --help|-h) + echo "Usage: $0 [--install] [--install-dir PATH] [--force-gpu] [VERSION...]" + echo "Available versions: ${ALL_VERSIONS[*]}" + exit 0 + ;; + *) + if [[ -n "${FFMPEG_TAGS[$1]+x}" ]]; then + VERSIONS_TO_BUILD+=("$1") + else + log_error "Unknown version: $1. Available: ${ALL_VERSIONS[*]}" + exit 1 + fi + shift + ;; + esac +done + +if [[ ${#VERSIONS_TO_BUILD[@]} -eq 0 ]]; then + VERSIONS_TO_BUILD=("${ALL_VERSIONS[@]}") +fi + +if [[ "$DO_INSTALL" == true && -z "$INSTALL_DIR" ]]; then + INSTALL_DIR="$(detect_xc_vm_bin_dir)" || { + log_error "Cannot detect XC_VM bin dir. Use --install-dir PATH" + exit 1 + } +fi + +# ── Helper: install first available package from alternatives ───────────────── +# Usage: install_one_of_apt pkg1 pkg2 pkg3 +# Tries each in order, installs the first one that exists. +install_one_of_apt() { + for pkg in "$@"; do + if apt-cache show "$pkg" 2>/dev/null | grep -q "^Package:"; then + apt-get install -y -qq "$pkg" 2>/dev/null && return 0 + fi + done + return 1 +} + +# ── Dependency installation (APT) ──────────────────────────────────────────── +install_deps_apt() { + log_step "Installing build dependencies (APT)..." + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + + # Required — build is impossible without these + local required=( + build-essential nasm yasm pkg-config git cmake autoconf automake libtool + libx264-dev libx265-dev libmp3lame-dev libopus-dev libvpx-dev + libfreetype-dev libfontconfig-dev libssl-dev zlib1g-dev libbz2-dev + ) + apt-get install -y -qq "${required[@]}" + + # Optional — install one by one, skip unavailable + local optional=( + # Codecs + libfdk-aac-dev libtheora-dev libvorbis-dev libwebp-dev libxvidcore-dev + # Subtitles / text rendering + libass-dev libfribidi-dev libharfbuzz-dev + # TLS / crypto + libgnutls28-dev libgmp-dev libunistring-dev + # Streaming protocols + librtmp-dev libxml2-dev + # Transitive deps for --pkg-config-flags=--static + # (x265→numa, gnutls→nettle, libxml2→lzma+icu) + libnuma-dev liblzma-dev libicu-dev + # AV1 + libaom-dev libdav1d-dev libsvtav1-dev librav1e-dev + # Quality / scaling + libvmaf-dev libzimg-dev + # DRM / VAAPI + libdrm-dev libva-dev + # Vulkan + libvulkan-dev libplacebo-dev + # Teletext + libzvbi-dev + ) + + local failed_pkgs=() + for pkg in "${optional[@]}"; do + if ! apt-get install -y -qq "$pkg" 2>/dev/null; then + failed_pkgs+=("$pkg") + fi + done + + # Packages with alternative names depending on distro + local alt_failed=() + install_one_of_apt libnettle-dev nettle-dev || alt_failed+=("nettle-dev") + install_one_of_apt libsrt-gnutls-dev libsrt-openssl-dev libsrt-dev || alt_failed+=("libsrt-dev") + + if [[ ${#failed_pkgs[@]} -gt 0 || ${#alt_failed[@]} -gt 0 ]]; then + log_warn "Optional packages not available: ${failed_pkgs[*]} ${alt_failed[*]}" + log_warn "Some codecs/features may be disabled in the build" + fi + log_info "Dependencies installed (APT)" +} + +# ── Dependency installation (DNF/YUM) ──────────────────────────────────────── +install_deps_dnf() { + log_step "Installing build dependencies (DNF/YUM)..." + local mgr="dnf" + command -v dnf &>/dev/null || mgr="yum" + $mgr install -y epel-release 2>/dev/null || true + + # RPM Fusion needed for x264/x265/fdk-aac + if ! $mgr repolist 2>/dev/null | grep -qi rpmfusion; then + log_warn "RPM Fusion not enabled — x264/x265/fdk-aac may be unavailable" + fi + + local required=( + gcc gcc-c++ make nasm yasm pkgconfig git cmake autoconf automake libtool + zlib-devel bzip2-devel openssl-devel freetype-devel fontconfig-devel + ) + $mgr install -y "${required[@]}" + + local optional=( + # Codecs + x264-devel x265-devel fdk-aac-devel lame-devel opus-devel libvpx-devel + libtheora-devel libvorbis-devel libwebp-devel xvidcore-devel + # Subtitles / text + libass-devel fribidi-devel harfbuzz-devel + # TLS / crypto + gnutls-devel gmp-devel libunistring-devel nettle-devel + # Streaming protocols + librtmp-devel libxml2-devel srt-devel + # Transitive deps + numactl-devel xz-devel libicu-devel + # AV1 + aom-devel dav1d-devel svt-av1-devel rav1e-devel + # Quality / scaling + vmaf-devel zimg-devel + # DRM / VAAPI + libdrm-devel libva-devel + # Vulkan + vulkan-devel libplacebo-devel + # Teletext + libzvbi-devel + ) + + local failed_pkgs=() + for pkg in "${optional[@]}"; do + if ! $mgr install -y "$pkg" 2>/dev/null; then + failed_pkgs+=("$pkg") + fi + done + + if [[ ${#failed_pkgs[@]} -gt 0 ]]; then + log_warn "Optional packages not available: ${failed_pkgs[*]}" + log_warn "Some codecs/features may be disabled in the build" + fi + log_info "Dependencies installed (${mgr^^})" +} + +# ── CUDA detection (with FORCE_GPU support) ────────────────────────────────── +HAS_CUDA=false +CUDA_HOME="" +detect_cuda() { + if [[ "$FORCE_GPU" == true ]]; then + HAS_CUDA=true + log_info "FORCE_GPU mode active - GPU support will be compiled" + return 0 + fi + local cuda_paths=( + "/usr/local/cuda" + "/usr/local/cuda-12" + "/usr/local/cuda-11" + ) + for p in "${cuda_paths[@]}"; do + if [[ -d "$p/include" && -f "$p/bin/nvcc" ]]; then + CUDA_HOME="$p" + HAS_CUDA=true + log_info "CUDA found: $CUDA_HOME" + return 0 + fi + done + if command -v nvcc &>/dev/null; then + CUDA_HOME="$(dirname "$(dirname "$(command -v nvcc)")")" + HAS_CUDA=true + log_info "CUDA found via PATH" + return 0 + fi + log_warn "CUDA not found" +} + +# ── Install nv-codec-headers ───────────────────────────────────────────────── +install_nvcodec_headers() { + local branch="$1" + local nvcodec_dir="${BUILD_DIR}/nv-codec-headers-${branch//\//_}" + if [[ -d "$nvcodec_dir" ]]; then + return 0 + fi + log_step "Installing nv-codec-headers ($branch)..." + git clone --depth 1 --branch "$branch" https://git.videolan.org/git/ffmpeg/nv-codec-headers.git "$nvcodec_dir" + pushd "$nvcodec_dir" > /dev/null + make install PREFIX="/usr/local" + popd > /dev/null + ldconfig 2>/dev/null || true +} + +# ── FFmpeg configure flags (all features expanded) ─────────────────────────── +CONFIGURE_FLAGS=() +get_configure_flags() { + local version="$1" + local prefix="$2" + local cuda_cflags="" + local cuda_ldflags="" + if [[ "$HAS_CUDA" == true && -n "$CUDA_HOME" ]]; then + cuda_cflags=" -I${CUDA_HOME}/include" + cuda_ldflags=" -L${CUDA_HOME}/lib64" + fi + + CONFIGURE_FLAGS=( + "--prefix=${prefix}" + "--bindir=${prefix}/bin" + "--pkg-config-flags=--static" + "--extra-cflags=-I${prefix}/include${cuda_cflags}" + "--extra-ldflags=-L${prefix}/lib${cuda_ldflags} -Wl,-Bstatic -lcrypto -lssl -Wl,-Bdynamic" + "--extra-version=XCVM-$(date +%Y%m%d)" + "--extra-libs=-lsupc++ -lgmp -lz -lunistring -lpthread -lm -lrt -ldl" + "--target-os=linux" + "--enable-static" + "--disable-shared" + "--disable-ffplay" + "--disable-doc" + "--disable-debug" + "--disable-autodetect" + "--enable-gpl" + "--enable-nonfree" + "--enable-version3" + "--enable-pthreads" + "--enable-runtime-cpudetect" + "--enable-gray" + "--enable-libx264" + "--enable-libx265" + "--enable-libmp3lame" + "--enable-libopus" + "--enable-libvpx" + "--enable-libvorbis" + "--enable-libtheora" + "--enable-libass" + "--enable-libfreetype" + "--enable-fontconfig" + "--enable-bzlib" + "--enable-zlib" + "--enable-librtmp" + "--enable-gnutls" + + # HLS — critical for IPTV streaming + "--enable-muxer=hls" + "--enable-demuxer=hls" + + # DASH muxer/demuxer + "--enable-muxer=dash" + "--enable-demuxer=dash" + + # Disable ALSA on headless servers + "--disable-alsa" + "--disable-indev=alsa" + "--disable-outdev=alsa" + ) + + # === Optional features — enabled only if libraries are available === + pkg-config --exists libxml-2.0 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libxml2") + pkg-config --exists libdrm 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libdrm") + pkg-config --exists libva 2>/dev/null && CONFIGURE_FLAGS+=("--enable-vaapi") + pkg-config --exists aom 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libaom") + pkg-config --exists dav1d 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libdav1d") + pkg-config --exists SvtAv1Enc 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libsvtav1") + pkg-config --exists rav1e 2>/dev/null && CONFIGURE_FLAGS+=("--enable-librav1e") + pkg-config --exists zimg 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libzimg") + pkg-config --exists libvmaf 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libvmaf") + pkg-config --exists libwebp 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libwebp") + pkg-config --exists fribidi 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libfribidi") + pkg-config --exists harfbuzz 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libharfbuzz") + pkg-config --exists zvbi-0.2 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libzvbi") + + # Vulkan + libplacebo — only modern versions, only if available + if [[ "$version" =~ ^(7\.1|8\.[0-9]+)$ ]]; then + pkg-config --exists vulkan 2>/dev/null && CONFIGURE_FLAGS+=("--enable-vulkan") + pkg-config --exists libplacebo 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libplacebo") + fi + + if [[ "$HAS_CUDA" == true || "$FORCE_GPU" == true ]]; then + CONFIGURE_FLAGS+=("--enable-nvenc" "--enable-ffnvcodec" "--enable-cuvid") + [[ "$version" != "4.0" ]] && CONFIGURE_FLAGS+=("--enable-nvdec") + fi + + if pkg-config --exists fdk-aac 2>/dev/null; then + local fdk_ver="$(pkg-config --modversion fdk-aac 2>/dev/null || echo "0")" + local fdk_major="${fdk_ver%%.*}" + case "$version" in + 4.0|4.3) + [[ "$fdk_major" -lt 2 ]] && CONFIGURE_FLAGS+=("--enable-libfdk-aac") + ;; + *) + CONFIGURE_FLAGS+=("--enable-libfdk-aac") + ;; + esac + fi + + # Version-specific flags + case "$version" in + 4.0) + CONFIGURE_FLAGS+=("--enable-postproc") + [[ -f /usr/include/xvid.h ]] && CONFIGURE_FLAGS+=("--enable-libxvid") + ;; + 4.3|4.4) + CONFIGURE_FLAGS+=("--enable-postproc") + ;; + 5.1) + CONFIGURE_FLAGS+=("--enable-postproc") + pkg-config --exists srt 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libsrt") + ;; + 7*|8*) + CONFIGURE_FLAGS+=("--enable-postproc") + pkg-config --exists srt 2>/dev/null && CONFIGURE_FLAGS+=("--enable-libsrt") + ;; + esac +} + +# ── Build a single FFmpeg version ──────────────────────────────────────────── +build_version() { + local version="$1" + local tag="${FFMPEG_TAGS[$version]}" + local src_dir="${BUILD_DIR}/ffmpeg-${version}" + local prefix="${PREFIX_BASE}/${version}" + + log_step "════════════════════════════════════════════════════════" + log_step "Building FFmpeg ${version} (tag: ${tag})" + log_step "════════════════════════════════════════════════════════" + + if [[ "$HAS_CUDA" == true || "$FORCE_GPU" == true ]]; then + install_nvcodec_headers "${NVCODEC_BRANCH[$version]}" + fi + + if [[ ! -d "$src_dir" ]]; then + log_step "Cloning FFmpeg ${version}..." + git clone --depth 1 --branch "$tag" https://git.ffmpeg.org/ffmpeg.git "$src_dir" + fi + + mkdir -p "$prefix" + pushd "$src_dir" > /dev/null + make distclean 2>/dev/null || true + + get_configure_flags "$version" "$prefix" + local build_log="${src_dir}/xcvm_build.log" + local rc=0 + + # Configure + if [[ "$VERBOSE" == true ]]; then + ./configure "${CONFIGURE_FLAGS[@]}" 2>&1 | tee "$build_log" + rc=${PIPESTATUS[0]} + else + ./configure "${CONFIGURE_FLAGS[@]}" > "$build_log" 2>&1 && rc=0 || rc=$? + tail -20 "$build_log" + fi + if [[ $rc -ne 0 ]]; then + log_error "Configure failed. Check: ${src_dir}/ffbuild/config.log" + popd > /dev/null + return 1 + fi + + # Compile + log_step "Compiling with ${NPROC} threads..." + if [[ "$VERBOSE" == true ]]; then + make -j"${NPROC}" 2>&1 | tee -a "$build_log" + rc=${PIPESTATUS[0]} + else + make -j"${NPROC}" >> "$build_log" 2>&1 && rc=0 || rc=$? + tail -5 "$build_log" + fi + if [[ $rc -ne 0 ]]; then + log_error "Compilation failed. Log: ${build_log}" + popd > /dev/null + return 1 + fi + + # Install to prefix + log_step "Installing to prefix..." + if [[ "$VERBOSE" == true ]]; then + make install 2>&1 | tee -a "$build_log" + rc=${PIPESTATUS[0]} + else + make install >> "$build_log" 2>&1 && rc=0 || rc=$? + fi + if [[ $rc -ne 0 ]]; then + log_error "make install failed. Log: ${build_log}" + popd > /dev/null + return 1 + fi + + popd > /dev/null + + local ffmpeg_bin="${prefix}/bin/ffmpeg" + if [[ ! -x "$ffmpeg_bin" ]] || ! timeout 5 "$ffmpeg_bin" -version &>/dev/null; then + log_error "Binary not working" + return 1 + fi + + log_info "✅ FFmpeg ${version} built successfully" + log_info "Enabled features:" + echo " DASH : $( "$ffmpeg_bin" -formats 2>/dev/null | grep -q 'E dash' && echo "✅" || echo "❌" )" + echo " DRM/VAAPI : $( "$ffmpeg_bin" -hwaccels 2>/dev/null | grep -qE 'vaapi|drm' && echo "✅" || echo "❌" )" + echo " AV1 (dav1d) : $( "$ffmpeg_bin" -decoders 2>/dev/null | grep -q dav1d && echo "✅" || echo "❌" )" + echo " AV1 (aom) : $( "$ffmpeg_bin" -encoders 2>/dev/null | grep -q libaom && echo "✅" || echo "❌" )" + echo " AV1 (svtav1) : $( "$ffmpeg_bin" -encoders 2>/dev/null | grep -q libsvtav1 && echo "✅" || echo "❌" )" + echo " libzimg/vmaf : $( "$ffmpeg_bin" -filters 2>/dev/null | grep -q zscale && echo "✅" || echo "❌" )" + echo " Vulkan/placebo: $( "$ffmpeg_bin" -hwaccels 2>/dev/null | grep -q vulkan && echo "✅" || echo "N/A" )" + + return 0 +} + +# ── Install into XC_VM ─────────────────────────────────────────────────────── +install_version() { + local version="$1" + local prefix="${PREFIX_BASE}/${version}" + local target="${INSTALL_DIR}/${version}" + if [[ ! -x "${prefix}/bin/ffmpeg" ]]; then + log_error "No build for ${version}" + return 1 + fi + log_step "Installing FFmpeg ${version} to ${target}..." + if [[ -d "$target" ]]; then + local backup="${target}.backup.$(date +%Y%m%d_%H%M%S)" + log_info "Backing up existing installation → ${backup}" + cp -a "$target" "$backup" + fi + mkdir -p "$target" + cp -f "${prefix}/bin/ffmpeg" "$target/ffmpeg" + cp -f "${prefix}/bin/ffprobe" "$target/ffprobe" + + # Write build metadata + cat > "${target}/BUILD_INFO" </dev/null && echo "${PRETTY_NAME}" || uname -sr) +Arch: $(uname -m) +GCC: $(gcc --version 2>/dev/null | head -1 || echo 'unknown') +GPU: $(if [[ "$HAS_CUDA" == true ]]; then echo "NVENC+CUVID (CUDA: ${CUDA_HOME})"; else echo 'none (CPU-only)'; fi) +Strategy: static deps + dynamic glibc +EOF + + chown -R xc_vm:xc_vm "$target" 2>/dev/null || true + chmod +x "${target}/ffmpeg" "${target}/ffprobe" + if timeout 5 "${target}/ffmpeg" -version &>/dev/null; then + log_info "Installed and validated: OK" + else + log_error "Installed binary failed validation" + return 1 + fi +} + +# ── Summary ────────────────────────────────────────────────────────────────── +print_summary() { + echo "" + log_step "════════════════════════════════════════════════════════" + log_step "BUILD SUMMARY" + log_step "════════════════════════════════════════════════════════" + for version in "${VERSIONS_TO_BUILD[@]}"; do + local bin="${PREFIX_BASE}/${version}/bin/ffmpeg" + if [[ -x "$bin" ]]; then + echo -e " ${GREEN}✓${NC} ${version} → OK" + else + echo -e " ${RED}✗${NC} ${version} → FAILED" + fi + done + if [[ "$DO_INSTALL" == true ]]; then + log_info "Installed to: ${INSTALL_DIR}" + fi + log_info "Build artifacts: ${PREFIX_BASE}/" + log_info "Source cache: ${BUILD_DIR}/" +} + +# ── Main ───────────────────────────────────────────────────────────────────── +main() { + if [[ "$(id -u)" -ne 0 ]]; then + log_error "Must run as root" + exit 1 + fi + + echo "" + log_step "XC_VM FFmpeg Auto-Builder - FINAL FULL COMPLETE" + log_step "Versions: ${VERSIONS_TO_BUILD[*]}" + [[ "$FORCE_GPU" == true ]] && log_step "FORCE_GPU enabled for Docker" + echo "" + + mkdir -p "$BUILD_DIR" "$PREFIX_BASE" + + if command -v apt-get &>/dev/null; then + install_deps_apt + elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then + install_deps_dnf + else + log_error "Unsupported package manager" + exit 1 + fi + + detect_cuda + + local failed=() + for version in "${VERSIONS_TO_BUILD[@]}"; do + if build_version "$version"; then + if [[ "$DO_INSTALL" == true ]]; then + install_version "$version" || failed+=("${version}(install)") + fi + else + failed+=("$version") + fi + done + + print_summary + + if [[ ${#failed[@]} -gt 0 ]]; then + log_error "Failed versions: ${failed[*]}" + exit 1 + fi + + log_info "✅ ALL DONE! GPU support is fully retained even without GPU during build." +} + +main "$@" \ No newline at end of file