From 1c9d325ae8b2dd90807c2c21e272242716f75268 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 22 Oct 2025 06:25:26 -0700 Subject: [PATCH] Refactor: Full Change & Feature-Bump of tools.func (#8409) --- misc/tools.func | 2356 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 1924 insertions(+), 432 deletions(-) diff --git a/misc/tools.func b/misc/tools.func index d389d9a5d..ee35767cf 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -1,5 +1,811 @@ #!/bin/bash +# ============================================================================== +# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Cache installed version to avoid repeated checks +# ------------------------------------------------------------------------------ +cache_installed_version() { + local app="$1" + local version="$2" + mkdir -p /var/cache/app-versions + echo "$version" >"/var/cache/app-versions/${app}_version.txt" +} + +get_cached_version() { + local app="$1" + mkdir -p /var/cache/app-versions + if [[ -f "/var/cache/app-versions/${app}_version.txt" ]]; then + cat "/var/cache/app-versions/${app}_version.txt" + return 0 + fi + return 0 +} + +# ------------------------------------------------------------------------------ +# Unified package upgrade function (with apt update caching) +# ------------------------------------------------------------------------------ +upgrade_package() { + local package="$1" + + # Use same caching logic as ensure_dependencies + local apt_cache_file="/var/cache/apt-update-timestamp" + local current_time=$(date +%s) + local last_update=0 + + if [[ -f "$apt_cache_file" ]]; then + last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) + fi + + if ((current_time - last_update > 300)); then + $STD apt update + echo "$current_time" >"$apt_cache_file" + fi + + $STD apt install --only-upgrade -y "$package" +} + +# ------------------------------------------------------------------------------ +# Repository availability check +# ------------------------------------------------------------------------------ +verify_repo_available() { + local repo_url="$1" + local suite="$2" + + if curl -fsSL --max-time 10 "${repo_url}/dists/${suite}/Release" &>/dev/null; then + return 0 + fi + return 1 +} + +# ------------------------------------------------------------------------------ +# Ensure dependencies are installed (with apt update caching) +# ------------------------------------------------------------------------------ +ensure_dependencies() { + local deps=("$@") + local missing=() + + for dep in "${deps[@]}"; do + if ! command -v "$dep" &>/dev/null && ! is_package_installed "$dep"; then + missing+=("$dep") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + # Only run apt update if not done recently (within last 5 minutes) + local apt_cache_file="/var/cache/apt-update-timestamp" + local current_time=$(date +%s) + local last_update=0 + + if [[ -f "$apt_cache_file" ]]; then + last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) + fi + + if ((current_time - last_update > 300)); then + # Ensure orphaned sources are cleaned before updating + cleanup_orphaned_sources 2>/dev/null || true + + if ! $STD apt update; then + ensure_apt_working || return 1 + fi + echo "$current_time" >"$apt_cache_file" + fi + + $STD apt install -y "${missing[@]}" || { + msg_error "Failed to install dependencies: ${missing[*]}" + return 1 + } + fi +} + +# ------------------------------------------------------------------------------ +# Smart version comparison +# ------------------------------------------------------------------------------ +version_gt() { + test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1" +} + +# ------------------------------------------------------------------------------ +# Get system architecture (normalized) +# ------------------------------------------------------------------------------ +get_system_arch() { + local arch_type="${1:-dpkg}" # dpkg, uname, or both + local arch + + case "$arch_type" in + dpkg) + arch=$(dpkg --print-architecture 2>/dev/null) + ;; + uname) + arch=$(uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" + [[ "$arch" == "aarch64" ]] && arch="arm64" + ;; + both | *) + arch=$(dpkg --print-architecture 2>/dev/null || uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" + [[ "$arch" == "aarch64" ]] && arch="arm64" + ;; + esac + + echo "$arch" +} + +# ------------------------------------------------------------------------------ +# Create temporary directory with automatic cleanup +# ------------------------------------------------------------------------------ +create_temp_dir() { + local tmp_dir=$(mktemp -d) + # Set trap to cleanup on EXIT, ERR, INT, TERM + trap "rm -rf '$tmp_dir'" EXIT ERR INT TERM + echo "$tmp_dir" +} + +# ------------------------------------------------------------------------------ +# Check if package is installed (faster than dpkg -l | grep) +# ------------------------------------------------------------------------------ +is_package_installed() { + local package="$1" + dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "^install ok installed$" +} + +# ------------------------------------------------------------------------------ +# GitHub API call with authentication and rate limit handling +# ------------------------------------------------------------------------------ +github_api_call() { + local url="$1" + local output_file="${2:-/dev/stdout}" + local max_retries=3 + local retry_delay=2 + + local header_args=() + [[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN") + + for attempt in $(seq 1 $max_retries); do + local http_code + http_code=$(curl -fsSL -w "%{http_code}" -o "$output_file" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${header_args[@]}" \ + "$url" 2>/dev/null || echo "000") + + case "$http_code" in + 200) + return 0 + ;; + 403) + # Rate limit - check if we can retry + if [[ $attempt -lt $max_retries ]]; then + msg_warn "GitHub API rate limit, waiting ${retry_delay}s... (attempt $attempt/$max_retries)" + sleep "$retry_delay" + retry_delay=$((retry_delay * 2)) + continue + fi + msg_error "GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits." + return 1 + ;; + 404) + msg_error "GitHub API endpoint not found: $url" + return 1 + ;; + *) + if [[ $attempt -lt $max_retries ]]; then + sleep "$retry_delay" + continue + fi + msg_error "GitHub API call failed with HTTP $http_code" + return 1 + ;; + esac + done + + return 1 +} + +should_upgrade() { + local current="$1" + local target="$2" + + [[ -z "$current" ]] && return 0 + version_gt "$target" "$current" && return 0 + return 1 +} + +# ------------------------------------------------------------------------------ +# Get OS information (cached for performance) +# ------------------------------------------------------------------------------ +get_os_info() { + local field="${1:-all}" # id, codename, version, version_id, all + + # Cache OS info to avoid repeated file reads + if [[ -z "${_OS_ID:-}" ]]; then + export _OS_ID=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release) + export _OS_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{gsub(/"/,"",$2); print $2}' /etc/os-release) + export _OS_VERSION=$(awk -F= '/^VERSION_ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release) + export _OS_VERSION_FULL=$(awk -F= '/^VERSION=/{gsub(/"/,"",$2); print $2}' /etc/os-release) + fi + + case "$field" in + id) echo "$_OS_ID" ;; + codename) echo "$_OS_CODENAME" ;; + version) echo "$_OS_VERSION" ;; + version_id) echo "$_OS_VERSION" ;; + version_full) echo "$_OS_VERSION_FULL" ;; + all) echo "ID=$_OS_ID CODENAME=$_OS_CODENAME VERSION=$_OS_VERSION" ;; + *) echo "$_OS_ID" ;; + esac +} + +# ------------------------------------------------------------------------------ +# Check if running on specific OS +# ------------------------------------------------------------------------------ +is_debian() { + [[ "$(get_os_info id)" == "debian" ]] +} + +is_ubuntu() { + [[ "$(get_os_info id)" == "ubuntu" ]] +} + +is_alpine() { + [[ "$(get_os_info id)" == "alpine" ]] +} + +# ------------------------------------------------------------------------------ +# Get Debian/Ubuntu major version +# ------------------------------------------------------------------------------ +get_os_version_major() { + local version=$(get_os_info version) + echo "${version%%.*}" +} + +# ------------------------------------------------------------------------------ +# Download file with retry logic and progress +# ------------------------------------------------------------------------------ +download_file() { + local url="$1" + local output="$2" + local max_retries="${3:-3}" + local show_progress="${4:-false}" + + local curl_opts=(-fsSL) + [[ "$show_progress" == "true" ]] && curl_opts=(-fL#) + + for attempt in $(seq 1 $max_retries); do + if curl "${curl_opts[@]}" -o "$output" "$url"; then + return 0 + fi + + if [[ $attempt -lt $max_retries ]]; then + msg_warn "Download failed, retrying... (attempt $attempt/$max_retries)" + sleep 2 + fi + done + + msg_error "Failed to download: $url" + return 1 +} + +# ------------------------------------------------------------------------------ +# Get fallback suite for repository (comprehensive mapping) +# ------------------------------------------------------------------------------ +get_fallback_suite() { + local distro_id="$1" + local distro_codename="$2" + local repo_base_url="$3" + + # Check if current codename works + if verify_repo_available "$repo_base_url" "$distro_codename"; then + echo "$distro_codename" + return 0 + fi + + # Comprehensive fallback mappings + case "$distro_id" in + debian) + case "$distro_codename" in + # Debian 13 (Trixie) → Debian 12 (Bookworm) + trixie | forky | sid) + echo "bookworm" + ;; + # Debian 12 (Bookworm) stays + bookworm) + echo "bookworm" + ;; + # Debian 11 (Bullseye) stays + bullseye) + echo "bullseye" + ;; + # Unknown → latest stable + *) + echo "bookworm" + ;; + esac + ;; + ubuntu) + case "$distro_codename" in + # Ubuntu 24.10 (Oracular) → 24.04 LTS (Noble) + oracular | plucky) + echo "noble" + ;; + # Ubuntu 24.04 LTS (Noble) stays + noble) + echo "noble" + ;; + # Ubuntu 23.10 (Mantic) → 22.04 LTS (Jammy) + mantic | lunar) + echo "jammy" + ;; + # Ubuntu 22.04 LTS (Jammy) stays + jammy) + echo "jammy" + ;; + # Ubuntu 20.04 LTS (Focal) stays + focal) + echo "focal" + ;; + # Unknown → latest LTS + *) + echo "jammy" + ;; + esac + ;; + *) + echo "$distro_codename" + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Verify package source and version +# ------------------------------------------------------------------------------ +verify_package_source() { + local package="$1" + local expected_version="$2" + + if apt-cache policy "$package" 2>/dev/null | grep -q "$expected_version"; then + return 0 + fi + return 1 +} + +# ------------------------------------------------------------------------------ +# Check if running on LTS version +# ------------------------------------------------------------------------------ +is_lts_version() { + local os_id=$(get_os_info id) + local codename=$(get_os_info codename) + + if [[ "$os_id" == "ubuntu" ]]; then + case "$codename" in + focal | jammy | noble) return 0 ;; # 20.04, 22.04, 24.04 + *) return 1 ;; + esac + elif [[ "$os_id" == "debian" ]]; then + # Debian releases are all "stable" + case "$codename" in + bullseye | bookworm | trixie) return 0 ;; + *) return 1 ;; + esac + fi + + return 1 +} + +# ------------------------------------------------------------------------------ +# Get optimal number of parallel jobs (cached) +# ------------------------------------------------------------------------------ +get_parallel_jobs() { + if [[ -z "${_PARALLEL_JOBS:-}" ]]; then + local cpu_count=$(nproc 2>/dev/null || echo 1) + local mem_gb=$(free -g | awk '/^Mem:/{print $2}') + + # Limit by available memory (assume 1GB per job for compilation) + local max_by_mem=$((mem_gb > 0 ? mem_gb : 1)) + local max_jobs=$((cpu_count < max_by_mem ? cpu_count : max_by_mem)) + + # At least 1, at most cpu_count + export _PARALLEL_JOBS=$((max_jobs > 0 ? max_jobs : 1)) + fi + echo "$_PARALLEL_JOBS" +} + +# ------------------------------------------------------------------------------ +# Get default PHP version for OS +# ------------------------------------------------------------------------------ +get_default_php_version() { + local os_id=$(get_os_info id) + local os_version=$(get_os_version_major) + + case "$os_id" in + debian) + case "$os_version" in + 13) echo "8.3" ;; # Debian 13 (Trixie) + 12) echo "8.2" ;; # Debian 12 (Bookworm) + 11) echo "7.4" ;; # Debian 11 (Bullseye) + *) echo "8.2" ;; + esac + ;; + ubuntu) + case "$os_version" in + 24) echo "8.3" ;; # Ubuntu 24.04 LTS (Noble) + 22) echo "8.1" ;; # Ubuntu 22.04 LTS (Jammy) + 20) echo "7.4" ;; # Ubuntu 20.04 LTS (Focal) + *) echo "8.1" ;; + esac + ;; + *) + echo "8.2" + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Get default Python version for OS +# ------------------------------------------------------------------------------ +get_default_python_version() { + local os_id=$(get_os_info id) + local os_version=$(get_os_version_major) + + case "$os_id" in + debian) + case "$os_version" in + 13) echo "3.12" ;; # Debian 13 (Trixie) + 12) echo "3.11" ;; # Debian 12 (Bookworm) + 11) echo "3.9" ;; # Debian 11 (Bullseye) + *) echo "3.11" ;; + esac + ;; + ubuntu) + case "$os_version" in + 24) echo "3.12" ;; # Ubuntu 24.04 LTS + 22) echo "3.10" ;; # Ubuntu 22.04 LTS + 20) echo "3.8" ;; # Ubuntu 20.04 LTS + *) echo "3.10" ;; + esac + ;; + *) + echo "3.11" + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Get default Node.js LTS version +# ------------------------------------------------------------------------------ +get_default_nodejs_version() { + # Always return current LTS (as of 2025) + echo "20" +} + +# ------------------------------------------------------------------------------ +# Check if package manager is locked +# ------------------------------------------------------------------------------ +is_apt_locked() { + if fuser /var/lib/dpkg/lock-frontend &>/dev/null || + fuser /var/lib/apt/lists/lock &>/dev/null || + fuser /var/cache/apt/archives/lock &>/dev/null; then + return 0 + fi + return 1 +} + +# ------------------------------------------------------------------------------ +# Wait for apt to be available +# ------------------------------------------------------------------------------ +wait_for_apt() { + local max_wait="${1:-300}" # 5 minutes default + local waited=0 + + while is_apt_locked; do + if [[ $waited -ge $max_wait ]]; then + msg_error "Timeout waiting for apt to be available" + return 1 + fi + + sleep 5 + waited=$((waited + 5)) + done + + return 0 +} + +# ------------------------------------------------------------------------------ +# Cleanup old repository files (migration helper) +# ------------------------------------------------------------------------------ +cleanup_old_repo_files() { + local app="$1" + + # Remove old-style .list files (including backups) + rm -f /etc/apt/sources.list.d/"${app}"*.list + rm -f /etc/apt/sources.list.d/"${app}"*.list.save + rm -f /etc/apt/sources.list.d/"${app}"*.list.distUpgrade + rm -f /etc/apt/sources.list.d/"${app}"*.list.dpkg-* + + # Remove old GPG keys from trusted.gpg.d + rm -f /etc/apt/trusted.gpg.d/"${app}"*.gpg + + # Remove keyrings from /etc/apt/keyrings + rm -f /etc/apt/keyrings/"${app}"*.gpg + + # Remove ALL .sources files for this app (including the main one) + # This ensures no orphaned .sources files reference deleted keyrings + rm -f /etc/apt/sources.list.d/"${app}"*.sources +} + +# ------------------------------------------------------------------------------ +# Cleanup orphaned .sources files that reference missing keyrings +# This prevents APT signature verification errors +# Call this at the start of any setup function to ensure APT is in a clean state +# ------------------------------------------------------------------------------ +cleanup_orphaned_sources() { + local sources_dir="/etc/apt/sources.list.d" + local keyrings_dir="/etc/apt/keyrings" + + [[ ! -d "$sources_dir" ]] && return 0 + + while IFS= read -r -d '' sources_file; do + local basename_file + basename_file=$(basename "$sources_file") + + # NEVER remove debian.sources - this is the standard Debian repository + if [[ "$basename_file" == "debian.sources" ]]; then + continue + fi + + # Extract Signed-By path from .sources file + local keyring_path + keyring_path=$(grep -E '^Signed-By:' "$sources_file" 2>/dev/null | awk '{print $2}') + + # If keyring doesn't exist, remove the .sources file + if [[ -n "$keyring_path" ]] && [[ ! -f "$keyring_path" ]]; then + rm -f "$sources_file" + fi + done < <(find "$sources_dir" -name "*.sources" -print0 2>/dev/null) + + # Also check for broken symlinks in keyrings directory + if [[ -d "$keyrings_dir" ]]; then + find "$keyrings_dir" -type l ! -exec test -e {} \; -delete 2>/dev/null || true + fi +} + +# ------------------------------------------------------------------------------ +# Ensure APT is in a working state before installing packages +# This should be called at the start of any setup function +# ------------------------------------------------------------------------------ +ensure_apt_working() { + # Clean up orphaned sources first + cleanup_orphaned_sources + + # Try to update package lists + if ! apt-get update -qq 2>/dev/null; then + # More aggressive cleanup + rm -f /etc/apt/sources.list.d/*.sources 2>/dev/null || true + cleanup_orphaned_sources + + # Try again + if ! apt-get update -qq 2>/dev/null; then + msg_error "Cannot update package lists - APT is critically broken" + return 1 + fi + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# Standardized deb822 repository setup +# ------------------------------------------------------------------------------ +setup_deb822_repo() { + local name="$1" + local gpg_url="$2" + local repo_url="$3" + local suite="$4" + local component="${5:-main}" + local architectures="${6:-amd64 arm64}" + + # Cleanup old configs for this app + cleanup_old_repo_files "$name" + + # Cleanup any orphaned .sources files from other apps + cleanup_orphaned_sources + + # Ensure keyring directory exists + mkdir -p /etc/apt/keyrings + + # Download GPG key (with --yes to avoid interactive prompts) + curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" || { + msg_error "Failed to download or import GPG key for ${name}" + return 1 + } + + # Create deb822 sources file + cat </etc/apt/sources.list.d/${name}.sources +Types: deb +URIs: $repo_url +Suites: $suite +Components: $component +Architectures: $architectures +Signed-By: /etc/apt/keyrings/${name}.gpg +EOF + + # Use cached apt update + local apt_cache_file="/var/cache/apt-update-timestamp" + local current_time=$(date +%s) + local last_update=0 + + if [[ -f "$apt_cache_file" ]]; then + last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) + fi + + # For repo changes, always update but respect short-term cache (30s) + if ((current_time - last_update > 30)); then + $STD apt update + echo "$current_time" >"$apt_cache_file" + fi +} + +# ------------------------------------------------------------------------------ +# Package version hold/unhold helpers +# ------------------------------------------------------------------------------ +hold_package_version() { + local package="$1" + $STD apt-mark hold "$package" +} + +unhold_package_version() { + local package="$1" + $STD apt-mark unhold "$package" +} + +# ------------------------------------------------------------------------------ +# Safe service restart with verification +# ------------------------------------------------------------------------------ +safe_service_restart() { + local service="$1" + + if systemctl is-active --quiet "$service"; then + $STD systemctl restart "$service" + else + $STD systemctl start "$service" + fi + + if ! systemctl is-active --quiet "$service"; then + msg_error "Failed to start $service" + systemctl status "$service" --no-pager + return 1 + fi + return 0 +} + +# ------------------------------------------------------------------------------ +# Enable and start service (with error handling) +# ------------------------------------------------------------------------------ +enable_and_start_service() { + local service="$1" + + if ! systemctl enable "$service" &>/dev/null; then + return 1 + fi + + if ! systemctl start "$service" &>/dev/null; then + msg_error "Failed to start $service" + systemctl status "$service" --no-pager + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# Check if service is enabled +# ------------------------------------------------------------------------------ +is_service_enabled() { + local service="$1" + systemctl is-enabled --quiet "$service" 2>/dev/null +} + +# ------------------------------------------------------------------------------ +# Check if service is running +# ------------------------------------------------------------------------------ +is_service_running() { + local service="$1" + systemctl is-active --quiet "$service" 2>/dev/null +} + +# ------------------------------------------------------------------------------ +# Extract version from JSON (GitHub releases) +# ------------------------------------------------------------------------------ +extract_version_from_json() { + local json="$1" + local field="${2:-tag_name}" + local strip_v="${3:-true}" + + ensure_dependencies jq + + local version + version=$(echo "$json" | jq -r ".${field} // empty") + + if [[ -z "$version" ]]; then + return 1 + fi + + if [[ "$strip_v" == "true" ]]; then + echo "${version#v}" + else + echo "$version" + fi +} + +# ------------------------------------------------------------------------------ +# Get latest GitHub release version +# ------------------------------------------------------------------------------ +get_latest_github_release() { + local repo="$1" + local strip_v="${2:-true}" + local temp_file=$(mktemp) + + if ! github_api_call "https://api.github.com/repos/${repo}/releases/latest" "$temp_file"; then + rm -f "$temp_file" + return 1 + fi + + local version + version=$(extract_version_from_json "$(cat "$temp_file")" "tag_name" "$strip_v") + rm -f "$temp_file" + + if [[ -z "$version" ]]; then + return 1 + fi + + echo "$version" +} + +# ------------------------------------------------------------------------------ +# Debug logging (only if DEBUG=1) +# ------------------------------------------------------------------------------ +debug_log() { + [[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2 +} + +# ------------------------------------------------------------------------------ +# Performance timing helper +# ------------------------------------------------------------------------------ +start_timer() { + echo $(date +%s) +} + +end_timer() { + local start_time="$1" + local label="${2:-Operation}" + local end_time=$(date +%s) + local duration=$((end_time - start_time)) +} + +# ------------------------------------------------------------------------------ +# GPG key fingerprint verification +# ------------------------------------------------------------------------------ +verify_gpg_fingerprint() { + local key_file="$1" + local expected_fingerprint="$2" + + local actual_fingerprint + actual_fingerprint=$(gpg --show-keys --with-fingerprint --with-colons "$key_file" 2>&1 | grep -m1 '^fpr:' | cut -d: -f10) + + if [[ "$actual_fingerprint" == "$expected_fingerprint" ]]; then + return 0 + fi + + msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint" + return 1 +} + +# ============================================================================== +# EXISTING FUNCTIONS +# ============================================================================== + # ------------------------------------------------------------------------------ # Checks for new GitHub release (latest tag). # @@ -35,14 +841,7 @@ check_for_gh_release() { return 1 fi - # jq check - if ! command -v jq &>/dev/null; then - $STD apt-get update -qq - $STD apt-get install -y jq || { - msg_error "Failed to install jq" - return 1 - } - fi + ensure_dependencies jq # Fetch releases and exclude drafts/prereleases local releases_json @@ -101,27 +900,23 @@ check_for_gh_release() { fi if [[ "$current" != "$pin_clean" ]]; then - msg_info "${app} pinned to ${pinned_version_in} (installed ${current:-none}) → update required" CHECK_UPDATE_RELEASE="$match_raw" + msg_ok "Checking for update: ${app}" return 0 fi - if [[ "$pin_clean" == "$latest_clean" ]]; then - msg_ok "${app} pinned to ${pinned_version_in} (up to date)" - else - msg_ok "${app} pinned to ${pinned_version_in} (already installed, upstream ${latest_raw})" - fi + msg_ok "Checking for update: ${app}" return 1 fi # No pinning → use latest if [[ -z "$current" || "$current" != "$latest_clean" ]]; then CHECK_UPDATE_RELEASE="$latest_raw" - msg_info "New release available: ${latest_raw} (current: v${current:-none})" + msg_ok "Checking for update: ${app}" return 0 fi - msg_ok "${app} is up to date (${latest_raw})" + msg_ok "Checking for update: ${app}" return 1 } @@ -134,12 +929,36 @@ check_for_gh_release() { # Variables: # APP - Application name (default: $APPLICATION variable) # ------------------------------------------------------------------------------ -function create_selfsigned_certs() { - local app=${APP:-$(echo "${APPLICATION,,}" | tr -d ' ')} - $STD openssl req -x509 -nodes -days 365 -newkey rsa:4096 \ - -keyout /etc/ssl/private/"$app"-selfsigned.key \ - -out /etc/ssl/certs/"$app"-selfsigned.crt \ - -subj "/C=US/O=$app/OU=Domain Control Validated/CN=localhost" +create_self_signed_cert() { + local APP_NAME="${1:-${APPLICATION}}" + local CERT_DIR="/etc/ssl/${APP_NAME}" + local CERT_KEY="${CERT_DIR}/${APP_NAME}.key" + local CERT_CRT="${CERT_DIR}/${APP_NAME}.crt" + + if [[ -f "$CERT_CRT" && -f "$CERT_KEY" ]]; then + return 0 + fi + + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install -y openssl || { + msg_error "Failed to install OpenSSL" + return 1 + } + + mkdir -p "$CERT_DIR" + $STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=${APP_NAME}" \ + -keyout "$CERT_KEY" \ + -out "$CERT_CRT" || { + msg_error "Failed to create self-signed certificate" + return 1 + } + + chmod 600 "$CERT_KEY" + chmod 644 "$CERT_CRT" } # ------------------------------------------------------------------------------ @@ -155,9 +974,7 @@ function download_with_progress() { local output="$2" if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi - if ! command -v pv &>/dev/null; then - $STD apt-get install -y pv - fi + ensure_dependencies pv set -o pipefail # Content-Length aus HTTP-Header holen @@ -254,9 +1071,7 @@ function fetch_and_deploy_gh_release() { local current_version="" [[ -f "$version_file" ]] && current_version=$(<"$version_file") - if ! command -v jq &>/dev/null; then - $STD apt-get install -y jq &>/dev/null - fi + ensure_dependencies jq local api_url="https://api.github.com/repos/$repo/releases" [[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest" @@ -326,7 +1141,11 @@ function fetch_and_deploy_gh_release() { rm -rf "${target:?}/"* fi - tar -xzf "$tmpdir/$filename" -C "$tmpdir" + tar -xzf "$tmpdir/$filename" -C "$tmpdir" || { + msg_error "Failed to extract tarball" + rm -rf "$tmpdir" + return 1 + } local unpack_dir unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1) @@ -387,7 +1206,7 @@ function fetch_and_deploy_gh_release() { } chmod 644 "$tmpdir/$filename" - $STD apt-get install -y "$tmpdir/$filename" || { + $STD apt install -y "$tmpdir/$filename" || { $STD dpkg -i "$tmpdir/$filename" || { msg_error "Both apt and dpkg installation failed" rm -rf "$tmpdir" @@ -437,12 +1256,18 @@ function fetch_and_deploy_gh_release() { fi if [[ "$filename" == *.zip ]]; then - if ! command -v unzip &>/dev/null; then - $STD apt-get install -y unzip - fi - unzip -q "$tmpdir/$filename" -d "$unpack_tmp" + ensure_dependencies unzip + unzip -q "$tmpdir/$filename" -d "$unpack_tmp" || { + msg_error "Failed to extract ZIP archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then - tar -xf "$tmpdir/$filename" -C "$unpack_tmp" + tar -xf "$tmpdir/$filename" -C "$unpack_tmp" || { + msg_error "Failed to extract TAR archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } else msg_error "Unsupported archive format: $filename" rm -rf "$tmpdir" "$unpack_tmp" @@ -594,21 +1419,34 @@ function import_local_ip() { # ------------------------------------------------------------------------------ function setup_adminer() { + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "adminer") + if grep -qi alpine /etc/os-release; then msg_info "Setup Adminer (Alpine)" mkdir -p /var/www/localhost/htdocs/adminer - if ! curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ - -o /var/www/localhost/htdocs/adminer/index.php; then + curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ + -o /var/www/localhost/htdocs/adminer/index.php || { msg_error "Failed to download Adminer" return 1 - fi - msg_ok "Adminer available at /adminer (Alpine)" + } + cache_installed_version "adminer" "latest-alpine" + msg_ok "Setup Adminer (Alpine)" else msg_info "Setup Adminer (Debian/Ubuntu)" - $STD apt-get install -y adminer - $STD a2enconf adminer - $STD systemctl reload apache2 - msg_ok "Adminer available at /adminer (Debian/Ubuntu)" + ensure_dependencies adminer + $STD a2enconf adminer || { + msg_error "Failed to enable Adminer Apache config" + return 1 + } + $STD systemctl reload apache2 || { + msg_error "Failed to reload Apache" + return 1 + } + local VERSION + VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}') + cache_installed_version "adminer" "${VERSION:-unknown}" + msg_ok "Setup Adminer (Debian/Ubuntu)" fi } @@ -622,39 +1460,43 @@ function setup_adminer() { function setup_composer() { local COMPOSER_BIN="/usr/local/bin/composer" + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "composer") export COMPOSER_ALLOW_SUPERUSER=1 - # Clean up old Composer binaries/symlinks (if any) + msg_info "Setup Composer" + for old in /usr/bin/composer /bin/composer /root/.composer/vendor/bin/composer; do [[ -e "$old" && "$old" != "$COMPOSER_BIN" ]] && rm -f "$old" done - # Ensure /usr/local/bin is in PATH for future logins (and current shell) ensure_usr_local_bin_persist export PATH="/usr/local/bin:$PATH" - # Check if composer is already installed - if [[ -x "$COMPOSER_BIN" ]]; then - local CURRENT_VERSION - CURRENT_VERSION=$("$COMPOSER_BIN" --version | awk '{print $3}') - $STD msg_info "Old Composer $CURRENT_VERSION found, updating to latest" - else - msg_info "Installing Composer" - fi + curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || { + msg_error "Failed to download Composer installer" + return 1 + } - # Download and install latest Composer - curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 + $STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer || { + msg_error "Failed to install Composer" + rm -f /tmp/composer-setup.php + return 1 + } + rm -f /tmp/composer-setup.php if [[ ! -x "$COMPOSER_BIN" ]]; then - msg_error "Composer was not successfully installed (no binary at $COMPOSER_BIN)" + msg_error "Composer installation failed" return 1 fi chmod +x "$COMPOSER_BIN" - $STD "$COMPOSER_BIN" self-update --no-interaction || true # safe if already latest - $STD "$COMPOSER_BIN" diagnose - msg_ok "Composer is ready at $COMPOSER_BIN" + $STD "$COMPOSER_BIN" self-update --no-interaction || true + + local FINAL_VERSION + FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') + cache_installed_version "composer" "$FINAL_VERSION" + msg_ok "Setup Composer" } # ------------------------------------------------------------------------------ @@ -676,36 +1518,45 @@ function setup_composer() { # ------------------------------------------------------------------------------ function setup_ffmpeg() { - local TMP_DIR - TMP_DIR=$(mktemp -d) + local TMP_DIR=$(mktemp -d) local GITHUB_REPO="FFmpeg/FFmpeg" local VERSION="${FFMPEG_VERSION:-latest}" local TYPE="${FFMPEG_TYPE:-full}" local BIN_PATH="/usr/local/bin/ffmpeg" + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "ffmpeg") + + msg_info "Setup FFmpeg ${VERSION} ($TYPE)" # Binary fallback mode if [[ "$TYPE" == "binary" ]]; then - msg_info "Installing FFmpeg (static binary)" - curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" - tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" + curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" || { + msg_error "Failed to download FFmpeg binary" + rm -rf "$TMP_DIR" + return 1 + } + tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" || { + msg_error "Failed to extract FFmpeg binary" + rm -rf "$TMP_DIR" + return 1 + } local EXTRACTED_DIR EXTRACTED_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*") cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH" cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe chmod +x "$BIN_PATH" /usr/local/bin/ffprobe + local FINAL_VERSION=$($BIN_PATH -version | head -n1 | awk '{print $3}') rm -rf "$TMP_DIR" - msg_ok "Installed FFmpeg binary ($($BIN_PATH -version | head -n1))" - return + cache_installed_version "ffmpeg" "$FINAL_VERSION" + ensure_usr_local_bin_persist + msg_ok "Setup FFmpeg" + return 0 fi - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq - fi + ensure_dependencies jq # Auto-detect latest stable version if none specified if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then - msg_info "Resolving latest FFmpeg tag" VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" | jq -r '.[].name' | grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' | @@ -718,8 +1569,6 @@ function setup_ffmpeg() { return 1 fi - msg_info "Installing FFmpeg ${VERSION} ($TYPE)" - # Dependency selection local DEPS=(build-essential yasm nasm pkg-config) case "$TYPE" in @@ -744,11 +1593,20 @@ function setup_ffmpeg() { ;; esac - $STD apt-get update - $STD apt-get install -y "${DEPS[@]}" + ensure_dependencies "${DEPS[@]}" + + curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || { + msg_error "Failed to download FFmpeg source" + rm -rf "$TMP_DIR" + return 1 + } + + tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract FFmpeg" + rm -rf "$TMP_DIR" + return 1 + } - curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" - tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" cd "$TMP_DIR/FFmpeg-"* || { msg_error "Source extraction failed" rm -rf "$TMP_DIR" @@ -775,25 +1633,32 @@ function setup_ffmpeg() { fi if [[ ${#args[@]} -eq 0 ]]; then - msg_error "FFmpeg configure args array is empty – aborting." + msg_error "FFmpeg configure args array is empty" rm -rf "$TMP_DIR" return 1 fi - ./configure "${args[@]}" >"$TMP_DIR/configure.log" 2>&1 || { - msg_error "FFmpeg ./configure failed (see $TMP_DIR/configure.log)" - cat "$TMP_DIR/configure.log" | tail -n 20 + $STD ./configure "${args[@]}" || { + msg_error "FFmpeg configure failed" + rm -rf "$TMP_DIR" + return 1 + } + $STD make -j"$(nproc)" || { + msg_error "FFmpeg compilation failed" + rm -rf "$TMP_DIR" + return 1 + } + $STD make install || { + msg_error "FFmpeg installation failed" rm -rf "$TMP_DIR" return 1 } - - $STD make -j"$(nproc)" - $STD make install echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf - ldconfig + $STD ldconfig ldconfig -p | grep libavdevice >/dev/null || { msg_error "libavdevice not registered with dynamic linker" + rm -rf "$TMP_DIR" return 1 } @@ -806,8 +1671,9 @@ function setup_ffmpeg() { local FINAL_VERSION FINAL_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}') rm -rf "$TMP_DIR" + cache_installed_version "ffmpeg" "$FINAL_VERSION" ensure_usr_local_bin_persist - msg_ok "Setup FFmpeg $FINAL_VERSION" + msg_ok "Setup FFmpeg" } # ------------------------------------------------------------------------------ @@ -843,34 +1709,46 @@ function setup_go() { local GO_BIN="/usr/local/bin/go" local GO_INSTALL_DIR="/usr/local/go" + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "go") if [[ -x "$GO_BIN" ]]; then local CURRENT_VERSION CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//') if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then + if [[ "$CACHED_VERSION" == "$GO_VERSION" ]]; then + return 0 + fi + cache_installed_version "go" "$GO_VERSION" return 0 else - $STD msg_info "Old Go Installation ($CURRENT_VERSION) found, upgrading to $GO_VERSION" rm -rf "$GO_INSTALL_DIR" fi - else - msg_info "Setup Go $GO_VERSION" fi + msg_info "Setup Go $GO_VERSION" + local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" local URL="https://go.dev/dl/${TARBALL}" local TMP_TAR=$(mktemp) curl -fsSL "$URL" -o "$TMP_TAR" || { msg_error "Failed to download $TARBALL" + rm -f "$TMP_TAR" return 1 } - tar -C /usr/local -xzf "$TMP_TAR" + $STD tar -C /usr/local -xzf "$TMP_TAR" || { + msg_error "Failed to extract Go tarball" + rm -f "$TMP_TAR" + return 1 + } ln -sf /usr/local/go/bin/go /usr/local/bin/go ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt rm -f "$TMP_TAR" + cache_installed_version "go" "$GO_VERSION" + ensure_usr_local_bin_persist msg_ok "Setup Go $GO_VERSION" } @@ -883,43 +1761,74 @@ function setup_go() { # ------------------------------------------------------------------------------ function setup_gs() { - mkdir -p /tmp - TMP_DIR=$(mktemp -d) - CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") + local TMP_DIR=$(mktemp -d) + local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "ghostscript") + ensure_dependencies jq + + local RELEASE_JSON RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) - LATEST_VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name":' | head -n1 | cut -d '"' -f4 | sed 's/^gs//') - LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | grep '"name":' | head -n1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') + local LATEST_VERSION + LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^gs//') + local LATEST_VERSION_DOTTED + LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | jq -r '.name' | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not determine latest Ghostscript version from GitHub." + if [[ -z "$LATEST_VERSION" || -z "$LATEST_VERSION_DOTTED" ]]; then + msg_error "Could not determine latest Ghostscript version" rm -rf "$TMP_DIR" - return + return 1 fi - if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED"; then + if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then + if [[ "$CACHED_VERSION" == "$LATEST_VERSION_DOTTED" ]]; then + rm -rf "$TMP_DIR" + return 0 + fi + cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" rm -rf "$TMP_DIR" - return + return 0 fi msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED" - curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" + + curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" || { + msg_error "Failed to download Ghostscript" + rm -rf "$TMP_DIR" + return 1 + } if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then - msg_error "Failed to extract Ghostscript archive." + msg_error "Failed to extract Ghostscript archive" rm -rf "$TMP_DIR" - return + return 1 fi cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { - msg_error "Failed to enter Ghostscript source directory." + msg_error "Failed to enter Ghostscript source directory" rm -rf "$TMP_DIR" + return 1 } - $STD apt-get install -y build-essential libpng-dev zlib1g-dev - $STD ./configure >/dev/null - $STD make - $STD sudo make install - local EXIT_CODE=$? + + ensure_dependencies build-essential libpng-dev zlib1g-dev + + $STD ./configure || { + msg_error "Ghostscript configure failed" + rm -rf "$TMP_DIR" + return 1 + } + $STD make -j"$(nproc)" || { + msg_error "Ghostscript compilation failed" + rm -rf "$TMP_DIR" + return 1 + } + $STD make install || { + msg_error "Ghostscript installation failed" + rm -rf "$TMP_DIR" + return 1 + } + hash -r if [[ ! -x "$(command -v gs)" ]]; then if [[ -x /usr/local/bin/gs ]]; then @@ -928,12 +1837,101 @@ function setup_gs() { fi rm -rf "$TMP_DIR" + cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" + ensure_usr_local_bin_persist + msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" +} - if [[ $EXIT_CODE -eq 0 ]]; then - msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" - else - msg_error "Ghostscript installation failed" +# ------------------------------------------------------------------------------ +# Sets up Hardware Acceleration on debian or ubuntu. +# +# Description: +# - Determites CPU/GPU/APU Vendor +# - Installs the correct libraries and packages +# - Sets up Hardware Acceleration +# +# Notes: +# - Some things are fetched from intel repositories due to not being in debian repositories. +# ------------------------------------------------------------------------------ +function setup_hwaccel() { + msg_info "Setup Hardware Acceleration" + + if ! command -v lspci &>/dev/null; then + $STD apt -y update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt -y install pciutils || { + msg_error "Failed to install pciutils" + return 1 + } fi + + # Detect GPU vendor (Intel, AMD, NVIDIA) + local gpu_vendor + gpu_vendor=$(lspci | grep -Ei 'vga|3d|display' | grep -Eo 'Intel|AMD|NVIDIA' | head -n1) + + # Detect CPU vendor (relevant for AMD APUs) + local cpu_vendor + cpu_vendor=$(lscpu | grep -i 'Vendor ID' | awk '{print $3}') + + if [[ -z "$gpu_vendor" && -z "$cpu_vendor" ]]; then + msg_error "No GPU or CPU vendor detected (missing lspci/lscpu output)" + return 1 + fi + + # Detect OS + local os_id os_codename + os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release | tr -d '"') + os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release | tr -d '"') + + # Determine if we are on a VM or LXC + local in_ct="${CTTYPE:-0}" + + case "$gpu_vendor" in + Intel) + if [[ "$os_id" == "ubuntu" ]]; then + $STD apt -y install intel-opencl-icd || msg_error "Failed to install intel-opencl-icd" + else + fetch_and_deploy_gh_release "intel-igc-core-2" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" + fetch_and_deploy_gh_release "intel-igc-opencl-2" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" + fetch_and_deploy_gh_release "intel-libgdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" + fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" + fi + + $STD apt -y install va-driver-all ocl-icd-libopencl1 vainfo intel-gpu-tools || msg_error "Failed to install GPU dependencies" + ;; + AMD) + $STD apt -y install mesa-va-drivers mesa-vdpau-drivers mesa-opencl-icd vainfo clinfo || msg_error "Failed to install AMD GPU dependencies" + + # For AMD CPUs without discrete GPU (APUs) + if [[ "$cpu_vendor" == "AuthenticAMD" && "$gpu_vendor" != "AMD" ]]; then + $STD apt -y install libdrm-amdgpu1 firmware-amd-graphics || true + fi + ;; + NVIDIA) + # NVIDIA needs manual driver setup + ;; + *) + # If no discrete GPU, but AMD CPU (e.g., Ryzen APU) + if [[ "$cpu_vendor" == "AuthenticAMD" ]]; then + $STD apt -y install mesa-opencl-icd ocl-icd-libopencl1 clinfo || msg_error "Failed to install Mesa OpenCL stack" + else + msg_error "No supported GPU vendor detected" + return 1 + fi + ;; + esac + + if [[ "$in_ct" == "0" ]]; then + chgrp video /dev/dri 2>/dev/null || true + chmod 755 /dev/dri 2>/dev/null || true + chmod 660 /dev/dri/* 2>/dev/null || true + $STD adduser "$(id -u -n)" video + $STD adduser "$(id -u -n)" render + fi + + msg_ok "Setup Hardware Acceleration" } # ------------------------------------------------------------------------------ @@ -948,20 +1946,26 @@ function setup_gs() { # - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc. # ------------------------------------------------------------------------------ function setup_imagemagick() { - local TMP_DIR - TMP_DIR=$(mktemp -d) + local TMP_DIR=$(mktemp -d) local VERSION="" local BINARY_PATH="/usr/local/bin/magick" + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "imagemagick") if command -v magick &>/dev/null; then VERSION=$(magick -version | awk '/^Version/ {print $3}') - msg_ok "ImageMagick already installed ($VERSION)" + if [[ "$CACHED_VERSION" == "$VERSION" ]]; then + rm -rf "$TMP_DIR" + return 0 + fi + cache_installed_version "imagemagick" "$VERSION" + rm -rf "$TMP_DIR" return 0 fi - msg_info "Setup ImageMagick (Patience)" - $STD apt-get update - $STD apt-get install -y \ + msg_info "Setup ImageMagick" + + ensure_dependencies \ build-essential \ libtool \ libjpeg-dev \ @@ -981,17 +1985,39 @@ function setup_imagemagick() { pkg-config \ ghostscript - curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" - tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" + curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" || { + msg_error "Failed to download ImageMagick" + rm -rf "$TMP_DIR" + return 1 + } + + tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract ImageMagick" + rm -rf "$TMP_DIR" + return 1 + } + cd "$TMP_DIR"/ImageMagick-* || { msg_error "Source extraction failed" rm -rf "$TMP_DIR" return 1 } - ./configure --disable-static >/dev/null - $STD make - $STD make install + $STD ./configure --disable-static || { + msg_error "ImageMagick configure failed" + rm -rf "$TMP_DIR" + return 1 + } + $STD make -j"$(nproc)" || { + msg_error "ImageMagick compilation failed" + rm -rf "$TMP_DIR" + return 1 + } + $STD make install || { + msg_error "ImageMagick installation failed" + rm -rf "$TMP_DIR" + return 1 + } $STD ldconfig /usr/local/lib if [[ ! -x "$BINARY_PATH" ]]; then @@ -1002,8 +2028,9 @@ function setup_imagemagick() { VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') rm -rf "$TMP_DIR" + cache_installed_version "imagemagick" "$VERSION" ensure_usr_local_bin_persist - msg_ok "Setup ImageMagick $VERSION" + msg_ok "Setup ImageMagick" } # ------------------------------------------------------------------------------ @@ -1019,40 +2046,59 @@ function setup_imagemagick() { function setup_java() { local JAVA_VERSION="${JAVA_VERSION:-21}" - local DISTRO_CODENAME + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" - # Add Adoptium repo if missing - if [[ ! -f /etc/apt/sources.list.d/adoptium.list ]]; then - $STD msg_info "Setting up Adoptium Repository" - mkdir -p /etc/apt/keyrings - curl -fsSL "https://packages.adoptium.net/artifactory/api/gpg/key/public" | gpg --dearmor -o /etc/apt/trusted.gpg.d/adoptium.gpg - echo "deb [signed-by=/etc/apt/trusted.gpg.d/adoptium.gpg] https://packages.adoptium.net/artifactory/deb ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/adoptium.list - $STD apt-get update - $STD msg_ok "Set up Adoptium Repository" + # Check cached version + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "temurin-jdk") + + # Add repo nur wenn nötig + if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then + # Cleanup old repository files + cleanup_old_repo_files "adoptium" + + # Use helper function to get fallback suite + local SUITE + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb") + + # Use standardized repo setup + setup_deb822_repo \ + "adoptium" \ + "https://packages.adoptium.net/artifactory/api/gpg/key/public" \ + "https://packages.adoptium.net/artifactory/deb" \ + "$SUITE" \ + "main" \ + "amd64 arm64" fi - # Detect currently installed temurin version local INSTALLED_VERSION="" if dpkg -l | grep -q "temurin-.*-jdk"; then INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+') fi if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then - $STD msg_info "Upgrading Temurin JDK $JAVA_VERSION" - $STD apt-get update - $STD apt-get install --only-upgrade -y "$DESIRED_PACKAGE" - $STD msg_ok "Upgraded Temurin JDK $JAVA_VERSION" + if [[ "$CACHED_VERSION" == "$JAVA_VERSION" ]]; then + # Already at correct version, just upgrade if available + upgrade_package "$DESIRED_PACKAGE" + else + $STD apt update + $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" + cache_installed_version "temurin-jdk" "$JAVA_VERSION" + fi else + msg_info "Setup Temurin JDK $JAVA_VERSION" if [[ -n "$INSTALLED_VERSION" ]]; then - $STD msg_info "Removing Temurin JDK $INSTALLED_VERSION" - $STD apt-get purge -y "temurin-${INSTALLED_VERSION}-jdk" + $STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk" fi - msg_info "Setup Temurin JDK $JAVA_VERSION" - $STD apt-get install -y "$DESIRED_PACKAGE" + $STD apt install -y "$DESIRED_PACKAGE" || { + msg_error "Failed to install Temurin JDK $JAVA_VERSION" + return 1 + } + cache_installed_version "temurin-jdk" "$JAVA_VERSION" msg_ok "Setup Temurin JDK $JAVA_VERSION" fi } @@ -1075,8 +2121,14 @@ function setup_local_ip_helper() { # Install networkd-dispatcher if not present if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then - $STD apt-get update - $STD apt-get install -y networkd-dispatcher + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install -y networkd-dispatcher || { + msg_error "Failed to install networkd-dispatcher" + return 1 + } fi # Write update_local_ip.sh @@ -1149,17 +2201,15 @@ EOF setup_mariadb() { local MARIADB_VERSION="${MARIADB_VERSION:-latest}" - local DISTRO_CODENAME - DISTRO_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)" - CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then msg_error "MariaDB mirror not reachable" return 1 fi - msg_info "Setting up MariaDB $MARIADB_VERSION" - # Grab dynamic latest LTS version if [[ "$MARIADB_VERSION" == "latest" ]]; then MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | @@ -1178,54 +2228,81 @@ setup_mariadb() { CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') fi + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "mariadb") + if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then - $STD msg_info "MariaDB $MARIADB_VERSION, upgrading" - $STD apt-get update - $STD apt-get install --only-upgrade -y mariadb-server mariadb-client - $STD msg_ok "MariaDB upgraded to $MARIADB_VERSION" + if [[ "$CACHED_VERSION" == "$MARIADB_VERSION" ]]; then + upgrade_package mariadb-server + upgrade_package mariadb-client + else + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install --only-upgrade -y mariadb-server mariadb-client || { + msg_error "Failed to upgrade MariaDB" + return 1 + } + cache_installed_version "mariadb" "$MARIADB_VERSION" + fi return 0 fi + msg_info "Setup MariaDB $MARIADB_VERSION" + if [[ -n "$CURRENT_VERSION" ]]; then - $STD msg_info "Upgrading MariaDB $CURRENT_VERSION to $MARIADB_VERSION" $STD systemctl stop mariadb >/dev/null 2>&1 || true - $STD apt-get purge -y 'mariadb*' || true - rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg - else - $STD msg_info "Setup MariaDB $MARIADB_VERSION" + $STD apt purge -y 'mariadb*' || true fi - curl -fsSL "https://mariadb.org/mariadb_release_signing_key.asc" | - gpg --dearmor -o /etc/apt/trusted.gpg.d/mariadb.gpg + # Ensure APT is working before proceeding + ensure_apt_working || return 1 - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mariadb.gpg] http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${CURRENT_OS} ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/mariadb.list + # Cleanup old repository files + cleanup_old_repo_files "mariadb" - $STD apt-get update + # Install required dependencies first (MariaDB needs these from main repos) + local mariadb_deps=() + for dep in gawk rsync socat libdbi-perl pv; do + if apt-cache search "^${dep}$" 2>/dev/null | grep -q .; then + mariadb_deps+=("$dep") + fi + done + + if [[ ${#mariadb_deps[@]} -gt 0 ]]; then + $STD apt install -y "${mariadb_deps[@]}" 2>/dev/null || true + fi + + # Use helper function to get fallback suite + local SUITE + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}") + + # Use standardized repo setup + setup_deb822_repo \ + "mariadb" \ + "https://mariadb.org/mariadb_release_signing_key.asc" \ + "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \ + "$SUITE" \ + "main" \ + "amd64 arm64" local MARIADB_MAJOR_MINOR MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}') if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections - else - for ver in 12.1 12.0 11.4 11.3 11.2 11.1 11.0 10.11 10.6 10.5 10.4 10.3; do - echo "mariadb-server-$ver mariadb-server/feedback boolean false" | debconf-set-selections - done fi - DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { - msg_warn "Failed to install MariaDB ${MARIADB_VERSION} from upstream repo – trying distro package as fallback..." - # Cleanup, remove upstream repo to avoid conflicts - rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg - $STD apt-get update - # Final fallback: distro package - DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { - msg_error "MariaDB installation failed even with distro fallback!" + + DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { + cleanup_old_repo_files "mariadb" + $STD apt update + DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { + msg_error "Failed to install MariaDB packages" return 1 } - msg_ok "Setup MariaDB (distro fallback)" - return 0 } + cache_installed_version "mariadb" "$MARIADB_VERSION" msg_ok "Setup MariaDB $MARIADB_VERSION" } @@ -1242,7 +2319,7 @@ setup_mariadb() { function setup_mongodb() { local MONGO_VERSION="${MONGO_VERSION:-8.0}" - local DISTRO_ID DISTRO_CODENAME MONGO_BASE_URL + local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{ gsub(/"/,"",$2); print $2 }' /etc/os-release) DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release) @@ -1270,46 +2347,80 @@ function setup_mongodb() { ;; esac - local REPO_LIST="/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.list" - local INSTALLED_VERSION="" if command -v mongod >/dev/null; then INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) fi + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "mongodb") + if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then - $STD msg_info "Upgrading MongoDB $MONGO_VERSION" - $STD apt-get update - $STD apt-get install --only-upgrade -y mongodb-org - $STD msg_ok "Upgraded MongoDB $MONGO_VERSION" + if [[ "$CACHED_VERSION" == "$MONGO_VERSION" ]]; then + upgrade_package mongodb-org + else + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install --only-upgrade -y mongodb-org || { + msg_error "Failed to upgrade MongoDB" + return 1 + } + cache_installed_version "mongodb" "$MONGO_VERSION" + fi return 0 fi + msg_info "Setup MongoDB $MONGO_VERSION" + if [[ -n "$INSTALLED_VERSION" ]]; then $STD systemctl stop mongod || true - $STD apt-get purge -y mongodb-org || true - rm -f /etc/apt/sources.list.d/mongodb-org-*.list - rm -f /etc/apt/trusted.gpg.d/mongodb-*.gpg - else - msg_info "Setup MongoDB $MONGO_VERSION" + $STD apt purge -y mongodb-org || true fi - curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg" - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg] ${MONGO_BASE_URL} ${DISTRO_CODENAME}/mongodb-org/${MONGO_VERSION} ${REPO_COMPONENT}" \ - >"$REPO_LIST" + # Cleanup old repository files + cleanup_old_repo_files "mongodb-org-${MONGO_VERSION}" - $STD apt-get update || { + # Cleanup any orphaned .sources files from other apps + cleanup_orphaned_sources + + # Use helper function to get fallback suite + local SUITE + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "$MONGO_BASE_URL") + + # Use standardized repo setup + mkdir -p /etc/apt/keyrings + curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg" || { + msg_error "Failed to download or import MongoDB GPG key" + return 1 + } + + cat </etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.sources +Types: deb +URIs: ${MONGO_BASE_URL} +Suites: ${SUITE}/mongodb-org/${MONGO_VERSION} +Components: ${REPO_COMPONENT} +Architectures: amd64 arm64 +Signed-By: /etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg +EOF + + $STD apt update || { msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" return 1 } - $STD apt-get install -y mongodb-org + $STD apt install -y mongodb-org || { + msg_error "Failed to install MongoDB packages" + return 1 + } mkdir -p /var/lib/mongodb chown -R mongodb:mongodb /var/lib/mongodb $STD systemctl enable mongod - $STD systemctl start mongod + safe_service_restart mongod + cache_installed_version "mongodb" "$MONGO_VERSION" msg_ok "Setup MongoDB $MONGO_VERSION" } @@ -1320,6 +2431,7 @@ function setup_mongodb() { # - Detects existing MySQL installation # - Purges conflicting packages before installation # - Supports clean upgrade +# - Handles Debian Trixie libaio1t64 transition # # Variables: # MYSQL_VERSION - MySQL version to install (e.g. 5.7, 8.0) (default: 8.0) @@ -1329,42 +2441,158 @@ function setup_mysql() { local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" local CURRENT_VERSION="" local NEED_INSTALL=false - CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if command -v mysql >/dev/null; then CURRENT_VERSION="$(mysql --version | grep -oP '[0-9]+\.[0-9]+' | head -n1)" if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then - $STD msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION" NEED_INSTALL=true else - # Check for patch-level updates if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then - $STD msg_info "MySQL $CURRENT_VERSION available for upgrade" - $STD apt-get update - $STD apt-get install --only-upgrade -y mysql-server - $STD msg_ok "MySQL upgraded" + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install --only-upgrade -y mysql-server || { + msg_error "Failed to upgrade MySQL" + return 1 + } fi return fi else - msg_info "Setup MySQL $MYSQL_VERSION" NEED_INSTALL=true fi if [[ "$NEED_INSTALL" == true ]]; then - $STD systemctl stop mysql || true - $STD apt-get purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true - rm -f /etc/apt/sources.list.d/mysql.list /etc/apt/trusted.gpg.d/mysql.gpg + msg_info "Setup MySQL $MYSQL_VERSION" - local DISTRO_CODENAME - DISTRO_CODENAME="$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)" - curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/${CURRENT_OS}/ ${DISTRO_CODENAME} mysql-${MYSQL_VERSION}" \ - >/etc/apt/sources.list.d/mysql.list + # Cleanup old repository files + cleanup_old_repo_files "mysql" + + # Determine suite - use bookworm for Debian testing/unstable + local SUITE + if [[ "$DISTRO_ID" == "debian" ]]; then + case "$DISTRO_CODENAME" in + bookworm | bullseye) + SUITE="$DISTRO_CODENAME" + ;; + trixie | forky | sid) + SUITE="bookworm" + ;; + *) + SUITE="bookworm" # Fallback to bookworm for unknown Debian versions + ;; + esac + else + # For Ubuntu - use fallback detection + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}") + fi + + # Stop existing MySQL if running + $STD systemctl stop mysql 2>/dev/null || true + + # Only purge if MySQL is actually installed + if dpkg -l 2>/dev/null | grep -q "^ii.*mysql-server"; then + $STD apt purge -y mysql-server* mysql-client* mysql-common 2>/dev/null || true + fi + + # Handle libaio dependency for Debian Trixie+ (time64 transition) + if [[ "$DISTRO_ID" == "debian" ]] && [[ "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then + # Install libaio1t64 if not present + if ! dpkg -l libaio1t64 2>/dev/null | grep -q "^ii"; then + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install -y libaio1t64 || { + msg_error "Failed to install libaio1t64" + return 1 + } + fi + + # Create dummy libaio1 package for dependency satisfaction + local TEMP_DIR="/tmp/libaio1-compat-$$" + mkdir -p "$TEMP_DIR" + cd "$TEMP_DIR" + + # Create control file + mkdir -p DEBIAN + cat >DEBIAN/control < libaio1t64 transition + This is a transitional dummy package to satisfy dependencies on libaio1 + while actually using libaio1t64 (time64 transition). +EOF + + # Build the dummy package + cd /tmp + dpkg-deb -b "$TEMP_DIR" libaio1-compat.deb >/dev/null 2>&1 + + # Install it + $STD dpkg -i libaio1-compat.deb + + # Cleanup + rm -rf "$TEMP_DIR" libaio1-compat.deb + fi + + # Use standardized repo setup + setup_deb822_repo \ + "mysql" \ + "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" \ + "https://repo.mysql.com/apt/${DISTRO_ID}" \ + "$SUITE" \ + "mysql-${MYSQL_VERSION}" \ + "amd64 arm64" export DEBIAN_FRONTEND=noninteractive - $STD apt-get update - $STD apt-get install -y mysql-server + + # Update apt + if ! $STD apt update; then + msg_error "APT update failed for MySQL repository" + return 1 + fi + + # Try multiple MySQL package patterns + local mysql_install_success=false + + # First try: mysql-server (most common) + if apt-cache search "^mysql-server$" 2>/dev/null | grep -q . && $STD apt install -y mysql-server mysql-client 2>/dev/null; then + mysql_install_success=true + fi + + # Second try: mysql-community-server (when official repo) + if [[ "$mysql_install_success" == false ]] && apt-cache search "^mysql-community-server$" 2>/dev/null | grep -q . && $STD apt install -y mysql-community-server mysql-community-client 2>/dev/null; then + mysql_install_success=true + fi + + # Third try: just mysql meta package + if [[ "$mysql_install_success" == false ]] && apt-cache search "^mysql$" 2>/dev/null | grep -q . && $STD apt install -y mysql 2>/dev/null; then + mysql_install_success=true + fi + + if [[ "$mysql_install_success" == false ]]; then + msg_error "MySQL ${MYSQL_VERSION} package not available for suite ${SUITE}" + return 1 + fi + + # Verify installation + if ! command -v mysql >/dev/null 2>&1; then + hash -r + if ! command -v mysql >/dev/null 2>&1; then + msg_error "MySQL installed but mysql command still not found" + return 1 + fi + fi + + cache_installed_version "mysql" "$MYSQL_VERSION" msg_ok "Setup MySQL $MYSQL_VERSION" fi } @@ -1386,120 +2614,95 @@ function setup_nodejs() { local NODE_MODULE="${NODE_MODULE:-}" local CURRENT_NODE_VERSION="" local NEED_NODE_INSTALL=false + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - # Check if Node.js is already installed if command -v node >/dev/null; then CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')" if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then - msg_info "Old Node.js $CURRENT_NODE_VERSION found, replacing with $NODE_VERSION" NEED_NODE_INSTALL=true fi else - msg_info "Setup Node.js $NODE_VERSION" NEED_NODE_INSTALL=true fi - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq || { - msg_error "Failed to install jq" - return 1 - } - fi - - # Install Node.js if required if [[ "$NEED_NODE_INSTALL" == true ]]; then - $STD apt-get purge -y nodejs - rm -f /etc/apt/sources.list.d/nodesource.list /etc/apt/keyrings/nodesource.gpg + msg_info "Setup Node.js $NODE_VERSION" - mkdir -p /etc/apt/keyrings + ensure_dependencies jq - if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | - gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; then - msg_error "Failed to download or import NodeSource GPG key" - exit 1 - fi + $STD apt purge -y nodejs - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" \ - >/etc/apt/sources.list.d/nodesource.list + # Cleanup old repository files + cleanup_old_repo_files "nodesource" - sleep 2 - if ! apt-get update >/dev/null 2>&1; then - msg_warn "APT update failed – retrying in 5s" - sleep 5 - if ! apt-get update >/dev/null 2>&1; then - msg_error "Failed to update APT repositories after adding NodeSource" - exit 1 - fi - fi + # NodeSource uses 'nodistro' for all distributions - no fallback needed + # Use standardized repo setup + setup_deb822_repo \ + "nodesource" \ + "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \ + "https://deb.nodesource.com/node_${NODE_VERSION}.x" \ + "nodistro" \ + "main" \ + "amd64 arm64" - if ! apt-get install -y nodejs >/dev/null 2>&1; then + if ! $STD apt install -y nodejs; then msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" - exit 1 + return 1 fi - # Update to latest npm - $STD npm install -g npm@latest || { - msg_error "Failed to update npm to latest version" - } - msg_ok "Setup Node.js ${NODE_VERSION}" + $STD npm install -g npm@latest 2>/dev/null || true + + cache_installed_version "nodejs" "$NODE_VERSION" + msg_ok "Setup Node.js $NODE_VERSION" fi export NODE_OPTIONS="--max-old-space-size=4096" - # Ensure valid working directory for npm (avoids uv_cwd error) if [[ ! -d /opt ]]; then mkdir -p /opt fi cd /opt || { msg_error "Failed to set safe working directory before npm install" - exit 1 + return 1 } - # Install global Node modules if [[ -n "$NODE_MODULE" ]]; then IFS=',' read -ra MODULES <<<"$NODE_MODULE" for mod in "${MODULES[@]}"; do local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION if [[ "$mod" == @*/*@* ]]; then - # Scoped package with version, e.g. @vue/cli-service@latest MODULE_NAME="${mod%@*}" MODULE_REQ_VERSION="${mod##*@}" elif [[ "$mod" == *"@"* ]]; then - # Unscoped package with version, e.g. yarn@latest MODULE_NAME="${mod%@*}" MODULE_REQ_VERSION="${mod##*@}" else - # No version specified MODULE_NAME="$mod" MODULE_REQ_VERSION="latest" fi - # Check if the module is already installed if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')" if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then - msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION" if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" - exit 1 + return 1 fi elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then - msg_info "Updating $MODULE_NAME to latest version" if ! $STD npm install -g "${MODULE_NAME}@latest"; then msg_error "Failed to update $MODULE_NAME to latest version" - exit 1 + return 1 fi fi else - msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" - exit 1 + return 1 fi fi done - msg_ok "Installed Node.js modules: $NODE_MODULE" fi } @@ -1527,8 +2730,9 @@ function setup_php() { local PHP_MODULE="${PHP_MODULE:-}" local PHP_APACHE="${PHP_APACHE:-NO}" local PHP_FPM="${PHP_FPM:-NO}" - local DISTRO_CODENAME - DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip" local COMBINED_MODULES @@ -1554,20 +2758,43 @@ function setup_php() { CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) fi - if [[ -z "$CURRENT_PHP" ]]; then - msg_info "Setup PHP $PHP_VERSION" - elif [[ "$CURRENT_PHP" != "$PHP_VERSION" ]]; then - msg_info "Old PHP $CURRENT_PHP detected, Setup new PHP $PHP_VERSION" - $STD apt-get purge -y "php${CURRENT_PHP//./}"* || true + msg_info "Setup PHP $PHP_VERSION" + + if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then + $STD apt purge -y "php${CURRENT_PHP//./}"* || true fi # Ensure Sury repo is available - if [[ ! -f /etc/apt/sources.list.d/php.list ]]; then - $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb - $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb - echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/php.list - $STD apt-get update + if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then + # Cleanup old repository files + cleanup_old_repo_files "php" + + $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb || { + msg_error "Failed to download PHP repository keyring" + return 1 + } + $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb || { + msg_error "Failed to install PHP repository keyring" + rm -f /tmp/debsuryorg-archive-keyring.deb + return 1 + } + + # Use helper function to get fallback suite + local SUITE + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.sury.org/php") + + cat </etc/apt/sources.list.d/php.sources +Types: deb +URIs: https://packages.sury.org/php/ +Suites: $SUITE +Components: main +Architectures: amd64 arm64 +Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg +EOF + $STD apt update || { + msg_error "APT update failed for PHP repository" + return 1 + } fi # Build module list @@ -1576,8 +2803,6 @@ function setup_php() { for mod in "${MODULES[@]}"; do if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then MODULE_LIST+=" php${PHP_VERSION}-${mod}" - else - msg_warn "PHP-Module ${mod} for PHP ${PHP_VERSION} not found – skipping" fi done if [[ "$PHP_FPM" == "YES" ]]; then @@ -1587,16 +2812,19 @@ function setup_php() { # install apache2 with PHP support if requested if [[ "$PHP_APACHE" == "YES" ]]; then if ! dpkg -l | grep -q "libapache2-mod-php${PHP_VERSION}"; then - msg_info "Installing Apache with PHP${PHP_VERSION} support" - $STD apt-get install -y apache2 libapache2-mod-php${PHP_VERSION} - else - msg_info "Apache with PHP${PHP_VERSION} already installed – skipping install" + $STD apt install -y apache2 libapache2-mod-php${PHP_VERSION} || { + msg_error "Failed to install Apache with PHP module" + return 1 + } fi fi # setup / update PHP modules - $STD apt-get install -y $MODULE_LIST - msg_ok "Setup PHP $PHP_VERSION" + $STD apt install -y $MODULE_LIST || { + msg_error "Failed to install PHP packages" + return 1 + } + cache_installed_version "php" "$PHP_VERSION" # optional stop old PHP-FPM service if [[ "$PHP_FPM" == "YES" && -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then @@ -1604,18 +2832,16 @@ function setup_php() { $STD systemctl disable php"${CURRENT_PHP}"-fpm || true fi - # Patch all relevant php.ini files + # Patch all relevant php.ini files (silent) local PHP_INI_PATHS=("/etc/php/${PHP_VERSION}/cli/php.ini") [[ "$PHP_FPM" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/fpm/php.ini") [[ "$PHP_APACHE" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/apache2/php.ini") for ini in "${PHP_INI_PATHS[@]}"; do if [[ -f "$ini" ]]; then - $STD msg_info "Patching $ini" - sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" - sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" - sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" - sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" - $STD msg_ok "Patched $ini" + $STD sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" + $STD sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" + $STD sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" + $STD sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" fi done @@ -1628,18 +2854,18 @@ function setup_php() { done $STD a2enmod mpm_prefork $STD a2enmod "php${PHP_VERSION}" - $STD systemctl restart apache2 || true + safe_service_restart apache2 || true fi # enable and restart PHP-FPM if requested if [[ "$PHP_FPM" == "YES" ]]; then if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then $STD systemctl enable php${PHP_VERSION}-fpm - $STD systemctl restart php${PHP_VERSION}-fpm - else - msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found" + safe_service_restart php${PHP_VERSION}-fpm fi fi + + msg_ok "Setup PHP $PHP_VERSION" } # ------------------------------------------------------------------------------ @@ -1660,74 +2886,119 @@ function setup_postgresql() { local PG_VERSION="${PG_VERSION:-16}" local PG_MODULES="${PG_MODULES:-}" local CURRENT_PG_VERSION="" - local DISTRO + local DISTRO_ID DISTRO_CODENAME local NEED_PG_INSTALL=false - DISTRO="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)" + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if command -v psql >/dev/null; then CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)" - if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then - : # PostgreSQL is already at the desired version – no action needed - else - $STD msg_info "Detected PostgreSQL $CURRENT_PG_VERSION, preparing upgrade to $PG_VERSION" - NEED_PG_INSTALL=true - fi + [[ "$CURRENT_PG_VERSION" != "$PG_VERSION" ]] && NEED_PG_INSTALL=true else - NEED_PG_INSTALL=true fi if [[ "$NEED_PG_INSTALL" == true ]]; then - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD msg_info "Dumping PostgreSQL $CURRENT_PG_VERSION data" - su - postgres -c "pg_dumpall > /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" - $STD msg_ok "Data dump completed" - - systemctl stop postgresql - fi - - rm -f /etc/apt/sources.list.d/pgdg.list /etc/apt/trusted.gpg.d/postgresql.gpg - - $STD msg_info "Adding PostgreSQL PGDG repository" - curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | - gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg - - echo "deb https://apt.postgresql.org/pub/repos/apt ${DISTRO}-pgdg main" \ - >/etc/apt/sources.list.d/pgdg.list - - $STD apt-get update - $STD msg_ok "Repository added" - msg_info "Setup PostgreSQL $PG_VERSION" - $STD apt-get install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD apt-get purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true + $STD runuser -u postgres -- pg_dumpall >/var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql + $STD systemctl stop postgresql fi - systemctl enable -q --now postgresql + # Cleanup old repository files + cleanup_old_repo_files "pgdg" + + # PostgreSQL PGDG repository uses special suite naming + # For unstable/testing Debian, we need to check what's actually available + local SUITE + case "$DISTRO_CODENAME" in + trixie | forky | sid) + # Try trixie-pgdg first, fallback to bookworm-pgdg if not available + if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then + SUITE="trixie-pgdg" + else + SUITE="bookworm-pgdg" + fi + ;; + *) + # Use helper function for stable releases + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") + # PGDG uses special suite naming: ${SUITE}-pgdg + SUITE="${SUITE}-pgdg" + ;; + esac + + # Use standardized repo setup + setup_deb822_repo \ + "pgdg" \ + "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ + "https://apt.postgresql.org/pub/repos/apt" \ + "$SUITE" \ + "main" \ + "amd64 arm64" + + # Update apt + if ! $STD apt update; then + msg_error "APT update failed for PostgreSQL repository" + return 1 + fi + + # Try multiple PostgreSQL package patterns + local pg_install_success=false + + # First, ensure ssl-cert is available (PostgreSQL dependency) + if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then + $STD apt install -y ssl-cert 2>/dev/null || true + fi + + # First try: postgresql-X (common in most repos) + if apt-cache search "^postgresql-${PG_VERSION}$" 2>/dev/null | grep -q . && $STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then + pg_install_success=true + fi + + # Second try: postgresql-server-X (some repos use this) + if [[ "$pg_install_success" == false ]] && apt-cache search "^postgresql-server-${PG_VERSION}$" 2>/dev/null | grep -q . && $STD apt install -y "postgresql-server-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then + pg_install_success=true + fi + + # Third try: just postgresql (any version) + if [[ "$pg_install_success" == false ]] && apt-cache search "^postgresql$" 2>/dev/null | grep -q . && $STD apt install -y postgresql postgresql-client 2>/dev/null; then + pg_install_success=true + fi + + if [[ "$pg_install_success" == false ]]; then + msg_error "PostgreSQL package not available for suite ${SUITE}" + return 1 + fi + + # Verify installation + if ! command -v psql >/dev/null 2>&1; then + msg_error "PostgreSQL installed but psql command not found" + return 1 + fi if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD msg_info "Restoring dumped data" - su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" - $STD msg_ok "Data restored" + $STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" 2>/dev/null || true + $STD runuser -u postgres -- psql /dev/null || true fi - $STD msg_ok "PostgreSQL $PG_VERSION installed" + $STD systemctl enable --now postgresql 2>/dev/null || true + + # Add PostgreSQL binaries to PATH for the install script + if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then + echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/'"${PG_VERSION}"'/bin"' >/etc/environment + fi + + cache_installed_version "postgresql" "$PG_VERSION" + msg_ok "Setup PostgreSQL $PG_VERSION" fi - # Install optional PostgreSQL modules if [[ -n "$PG_MODULES" ]]; then IFS=',' read -ra MODULES <<<"$PG_MODULES" for module in "${MODULES[@]}"; do - local pkg="postgresql-${PG_VERSION}-${module}" - $STD msg_info "Setup PostgreSQL module/s: $pkg" - $STD apt-get install -y "$pkg" || { - msg_error "Failed to install $pkg" - continue - } + $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true done - $STD msg_ok "Setup PostgreSQL modules" fi } @@ -1747,39 +3018,112 @@ function setup_postgresql() { function setup_ruby() { local RUBY_VERSION="${RUBY_VERSION:-3.4.4}" local RUBY_INSTALL_RAILS="${RUBY_INSTALL_RAILS:-true}" - local RBENV_DIR="$HOME/.rbenv" local RBENV_BIN="$RBENV_DIR/bin/rbenv" local PROFILE_FILE="$HOME/.profile" - local TMP_DIR - TMP_DIR=$(mktemp -d) + local TMP_DIR=$(mktemp -d) + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "ruby") msg_info "Setup Ruby $RUBY_VERSION" + # Ensure APT is working before installing dependencies + ensure_apt_working || return 1 + + # Install build dependencies - with fallback for different Debian versions + # In Trixie: libreadline6-dev -> libreadline-dev, libncurses5-dev -> libncurses-dev + local ruby_deps=() + local dep_variations=( + "jq" + "autoconf" + "patch" + "build-essential" + "libssl-dev" + "libyaml-dev" + "libreadline-dev|libreadline6-dev" + "zlib1g-dev" + "libgmp-dev" + "libncurses-dev|libncurses5-dev" + "libffi-dev" + "libgdbm-dev" + "libdb-dev" + "uuid-dev" + ) + + for dep_pattern in "${dep_variations[@]}"; do + if [[ "$dep_pattern" == *"|"* ]]; then + # Try multiple variations + IFS='|' read -ra variations <<<"$dep_pattern" + local found=false + for var in "${variations[@]}"; do + if apt-cache search "^${var}$" 2>/dev/null | grep -q .; then + ruby_deps+=("$var") + found=true + break + fi + done + else + if apt-cache search "^${dep_pattern}$" 2>/dev/null | grep -q .; then + ruby_deps+=("$dep_pattern") + fi + fi + done + + if [[ ${#ruby_deps[@]} -gt 0 ]]; then + $STD apt install -y "${ruby_deps[@]}" 2>/dev/null || true + else + msg_error "No Ruby build dependencies available" + return 1 + fi + local RBENV_RELEASE - RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') + RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | jq -r '.tag_name' | sed 's/^v//') if [[ -z "$RBENV_RELEASE" ]]; then msg_error "Failed to fetch latest rbenv version" rm -rf "$TMP_DIR" return 1 fi - curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" - tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" + curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" || { + msg_error "Failed to download rbenv" + rm -rf "$TMP_DIR" + return 1 + } + + tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract rbenv" + rm -rf "$TMP_DIR" + return 1 + } + mkdir -p "$RBENV_DIR" cp -r "$TMP_DIR/rbenv-${RBENV_RELEASE}/." "$RBENV_DIR/" - cd "$RBENV_DIR" && src/configure && $STD make -C src + cd "$RBENV_DIR" && src/configure && $STD make -C src || { + msg_error "Failed to build rbenv" + rm -rf "$TMP_DIR" + return 1 + } local RUBY_BUILD_RELEASE - RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') + RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | jq -r '.tag_name' | sed 's/^v//') if [[ -z "$RUBY_BUILD_RELEASE" ]]; then msg_error "Failed to fetch latest ruby-build version" rm -rf "$TMP_DIR" return 1 fi - curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/v${RUBY_BUILD_RELEASE}.tar.gz" -o "$TMP_DIR/ruby-build.tar.gz" - tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" + curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/v${RUBY_BUILD_RELEASE}.tar.gz" -o "$TMP_DIR/ruby-build.tar.gz" || { + msg_error "Failed to download ruby-build" + rm -rf "$TMP_DIR" + return 1 + } + + tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract ruby-build" + rm -rf "$TMP_DIR" + return 1 + } + mkdir -p "$RBENV_DIR/plugins/ruby-build" cp -r "$TMP_DIR/ruby-build-${RUBY_BUILD_RELEASE}/." "$RBENV_DIR/plugins/ruby-build/" echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt" @@ -1793,22 +3137,165 @@ function setup_ruby() { eval "$("$RBENV_BIN" init - bash)" if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then - $STD "$RBENV_BIN" install "$RUBY_VERSION" + $STD "$RBENV_BIN" install "$RUBY_VERSION" || { + msg_error "Failed to install Ruby $RUBY_VERSION" + rm -rf "$TMP_DIR" + return 1 + } fi "$RBENV_BIN" global "$RUBY_VERSION" hash -r if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then - msg_info "Setup Rails via gem" - gem install rails - msg_ok "Setup Rails $(rails -v)" + $STD gem install rails || { + msg_error "Failed to install Rails" + rm -rf "$TMP_DIR" + return 1 + } fi rm -rf "$TMP_DIR" + cache_installed_version "ruby" "$RUBY_VERSION" msg_ok "Setup Ruby $RUBY_VERSION" } +# ------------------------------------------------------------------------------ +# Installs or upgrades ClickHouse database server. +# +# Description: +# - Adds ClickHouse official repository +# - Installs specified version +# - Configures systemd service +# - Supports Debian/Ubuntu with fallback mechanism +# +# Variables: +# CLICKHOUSE_VERSION - ClickHouse version to install (default: latest) +# ------------------------------------------------------------------------------ + +function setup_clickhouse() { + local CLICKHOUSE_VERSION="${CLICKHOUSE_VERSION:-latest}" + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + + # Determine latest version if needed + if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then + # Try multiple methods to get the latest version + CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null | + grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | + sort -V | + tail -n1) + + # Fallback: Try GitHub releases API + if [[ -z "$CLICKHOUSE_VERSION" ]]; then + CLICKHOUSE_VERSION=$(curl -fsSL https://api.github.com/repos/ClickHouse/ClickHouse/releases/latest 2>/dev/null | + grep -oP '"tag_name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | + head -n1) + fi + + # Final fallback: Parse HTML more liberally + if [[ -z "$CLICKHOUSE_VERSION" ]]; then + CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null | + grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(?=/clickhouse)' | + sort -V | + tail -n1) + fi + + if [[ -z "$CLICKHOUSE_VERSION" ]]; then + msg_error "Could not determine latest ClickHouse version" + return 1 + fi + fi + + local CURRENT_VERSION="" + if command -v clickhouse-server >/dev/null 2>&1; then + CURRENT_VERSION=$(clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1) + fi + + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "clickhouse") + + # Check if already at target version + if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then + if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then + upgrade_package clickhouse-server + upgrade_package clickhouse-client + else + msg_info "Setup ClickHouse $CLICKHOUSE_VERSION" + $STD apt update || { + msg_error "Failed to update package list" + return 1 + } + $STD apt install --only-upgrade -y clickhouse-server clickhouse-client || { + msg_error "Failed to upgrade ClickHouse" + return 1 + } + cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" + msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION" + fi + return 0 + fi + + msg_info "Setup ClickHouse $CLICKHOUSE_VERSION" + + # Stop existing service if upgrading + if [[ -n "$CURRENT_VERSION" ]]; then + $STD systemctl stop clickhouse-server || true + fi + + # Cleanup old repository files + cleanup_old_repo_files "clickhouse" + + # Ensure dependencies + ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg + + # ClickHouse uses 'stable' instead of distro codenames + local SUITE="stable" + + # Use standardized repo setup + setup_deb822_repo \ + "clickhouse" \ + "https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \ + "https://packages.clickhouse.com/deb" \ + "$SUITE" \ + "main" \ + "amd64 arm64" + + # Update and install ClickHouse packages + export DEBIAN_FRONTEND=noninteractive + if ! $STD apt update; then + msg_error "APT update failed for ClickHouse repository" + return 1 + fi + + if ! $STD apt install -y clickhouse-server clickhouse-client; then + msg_error "Failed to install ClickHouse packages" + return 1 + fi + + # Verify installation + if ! command -v clickhouse-server >/dev/null 2>&1; then + msg_error "ClickHouse installation completed but clickhouse-server command not found" + return 1 + fi + + # Create data directory if it doesn't exist + mkdir -p /var/lib/clickhouse + + # Check if clickhouse user exists before chown + if id clickhouse >/dev/null 2>&1; then + chown -R clickhouse:clickhouse /var/lib/clickhouse + fi + + # Enable and start service + $STD systemctl enable clickhouse-server + safe_service_restart clickhouse-server || true + + cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" + msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION" +} + # ------------------------------------------------------------------------------ # Installs Rust toolchain and optional global crates via cargo. # @@ -1830,22 +3317,36 @@ function setup_rust() { local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}" local RUST_CRATES="${RUST_CRATES:-}" local CARGO_BIN="${HOME}/.cargo/bin" + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "rust") - # rustup & toolchain if ! command -v rustup &>/dev/null; then msg_info "Setup Rust" - curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" + curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" || { + msg_error "Failed to install Rust" + return 1 + } export PATH="$CARGO_BIN:$PATH" echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile" - msg_ok "Setup Rust" + local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + cache_installed_version "rust" "$RUST_VERSION" + msg_ok "Setup Rust $RUST_VERSION" else - $STD rustup install "$RUST_TOOLCHAIN" - $STD rustup default "$RUST_TOOLCHAIN" + msg_info "Setup Rust" + $STD rustup install "$RUST_TOOLCHAIN" || { + msg_error "Failed to install Rust toolchain" + return 1 + } + $STD rustup default "$RUST_TOOLCHAIN" || { + msg_error "Failed to set default Rust toolchain" + return 1 + } $STD rustup update "$RUST_TOOLCHAIN" - msg_ok "Rust toolchain set to $RUST_TOOLCHAIN" + local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + cache_installed_version "rust" "$RUST_VERSION" + msg_ok "Setup Rust $RUST_VERSION" fi - # install/update crates if [[ -n "$RUST_CRATES" ]]; then IFS=',' read -ra CRATES <<<"$RUST_CRATES" for crate in "${CRATES[@]}"; do @@ -1862,21 +3363,14 @@ function setup_rust() { if [[ -n "$INSTALLED_VER" ]]; then if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then - msg_info "Update $NAME: $INSTALLED_VER → $VER" $STD cargo install "$NAME" --version "$VER" --force - msg_ok "Updated $NAME to $VER" elif [[ -z "$VER" ]]; then - msg_info "Update $NAME: $INSTALLED_VER → latest" $STD cargo install "$NAME" --force - msg_ok "Updated $NAME to latest" fi else - msg_info "Setup $NAME ${VER:+($VER)}" $STD cargo install "$NAME" ${VER:+--version "$VER"} - msg_ok "Setup $NAME ${VER:-latest}" fi done - msg_ok "Setup Rust" fi } @@ -1890,12 +3384,11 @@ function setup_rust() { function setup_uv() { local UV_BIN="/usr/local/bin/uv" - local TMP_DIR - TMP_DIR=$(mktemp -d) + local TMP_DIR=$(mktemp -d) + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "uv") - # Determine system architecture - local ARCH - ARCH=$(uname -m) + local ARCH=$(uname -m) local UV_TAR case "$ARCH" in @@ -1920,45 +3413,46 @@ function setup_uv() { ;; esac - # Get latest version from GitHub + ensure_dependencies jq + local LATEST_VERSION LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | - grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') + jq -r '.tag_name' | sed 's/^v//') if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not fetch latest uv version from GitHub." + msg_error "Could not fetch latest uv version" rm -rf "$TMP_DIR" return 1 fi - # Check if uv is already up to date if [[ -x "$UV_BIN" ]]; then local INSTALLED_VERSION - INSTALLED_VERSION=$($UV_BIN -V | awk '{print $2}') + INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}') if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then + if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then + rm -rf "$TMP_DIR" + return 0 + fi + cache_installed_version "uv" "$LATEST_VERSION" rm -rf "$TMP_DIR" - [[ ":$PATH:" != *":/usr/local/bin:"* ]] && export PATH="/usr/local/bin:$PATH" return 0 - else - msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION" fi - else - msg_info "Setup uv $LATEST_VERSION" fi - # Download and install manually + msg_info "Setup uv $LATEST_VERSION" + local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" - if ! curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz"; then - msg_error "Failed to download $UV_URL" + curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || { + msg_error "Failed to download uv" rm -rf "$TMP_DIR" return 1 - fi + } - if ! tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR"; then - msg_error "Failed to extract uv archive" + tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract uv" rm -rf "$TMP_DIR" return 1 - fi + } install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || { msg_error "Failed to install uv binary" @@ -1966,15 +3460,14 @@ function setup_uv() { return 1 } - if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then - export PATH="/usr/local/bin:$PATH" - fi + rm -rf "$TMP_DIR" ensure_usr_local_bin_persist + export PATH="/usr/local/bin:$PATH" - $STD uv python update-shell >/dev/null 2>&1 || true + $STD uv python update-shell || true + cache_installed_version "uv" "$LATEST_VERSION" msg_ok "Setup uv $LATEST_VERSION" - # Optional: install specific Python version if [[ -n "${PYTHON_VERSION:-}" ]]; then local VERSION_MATCH VERSION_MATCH=$(uv python list --only-downloads | @@ -1982,16 +3475,15 @@ function setup_uv() { cut -d'-' -f2 | sort -V | tail -n1) if [[ -z "$VERSION_MATCH" ]]; then - msg_error "No matching Python $PYTHON_VERSION.x version found via uv" + msg_error "No matching Python $PYTHON_VERSION.x version found" return 1 fi if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then - if ! $STD uv python install "$VERSION_MATCH"; then - msg_error "Failed to install Python $VERSION_MATCH via uv" + $STD uv python install "$VERSION_MATCH" || { + msg_error "Failed to install Python $VERSION_MATCH" return 1 - fi - msg_ok "Setup Python $VERSION_MATCH via uv" + } fi fi } @@ -2006,53 +3498,56 @@ function setup_uv() { # ------------------------------------------------------------------------------ function setup_yq() { - local TMP_DIR - TMP_DIR=$(mktemp -d) - local CURRENT_VERSION="" + local TMP_DIR=$(mktemp -d) local BINARY_PATH="/usr/local/bin/yq" local GITHUB_REPO="mikefarah/yq" + local CURRENT_VERSION="" + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "yq") - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq || { - msg_error "Failed to install jq" - rm -rf "$TMP_DIR" - return 1 - } - fi - - [[ ":$PATH:" != *":/usr/local/bin:"* ]] && echo 'export PATH="/usr/local/bin:$PATH"' >>~/.bashrc && source ~/.bashrc + ensure_dependencies jq + ensure_usr_local_bin_persist if command -v yq &>/dev/null; then if ! yq --version 2>&1 | grep -q 'mikefarah'; then rm -f "$(command -v yq)" else - CURRENT_VERSION=$(yq --version | awk '{print $NF}' | sed 's/^v//') + CURRENT_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') fi fi - local RELEASE_JSON - RELEASE_JSON=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") local LATEST_VERSION - LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^v//') + LATEST_VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | + jq -r '.tag_name' | sed 's/^v//') if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not determine latest yq version from GitHub." + msg_error "Could not determine latest yq version" rm -rf "$TMP_DIR" return 1 fi if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then - return + if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then + rm -rf "$TMP_DIR" + return 0 + fi + cache_installed_version "yq" "$LATEST_VERSION" + rm -rf "$TMP_DIR" + return 0 fi - msg_info "Setup yq ($LATEST_VERSION)" - curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" + msg_info "Setup yq $LATEST_VERSION" + curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" || { + msg_error "Failed to download yq" + rm -rf "$TMP_DIR" + return 1 + } + chmod +x "$TMP_DIR/yq" mv "$TMP_DIR/yq" "$BINARY_PATH" if [[ ! -x "$BINARY_PATH" ]]; then - msg_error "Failed to install yq to $BINARY_PATH" + msg_error "Failed to install yq" rm -rf "$TMP_DIR" return 1 fi @@ -2061,10 +3556,7 @@ function setup_yq() { hash -r local FINAL_VERSION - FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}') - if [[ "$FINAL_VERSION" == "v$LATEST_VERSION" ]]; then - msg_ok "Setup yq ($LATEST_VERSION)" - else - msg_error "yq installation incomplete or version mismatch" - fi + FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') + cache_installed_version "yq" "$FINAL_VERSION" + msg_ok "Setup yq $FINAL_VERSION" }