From 5ae38e84c81bde6712c2f78d4debed7e6190ab84 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:13:08 +0100 Subject: [PATCH] Fix LibreNMS release name and improve Node.js install logs Corrects the case of the release name in the LibreNMS installer to 'librenms' and updates Node.js installation commands to use the $STD variable for consistent output and logging. --- install/librenms-install.sh | 2 +- misc/tools.func | 5990 +++++++++++++++++------------------ 2 files changed, 2996 insertions(+), 2996 deletions(-) diff --git a/install/librenms-install.sh b/install/librenms-install.sh index 8b60b65de..2b9ff3e44 100644 --- a/install/librenms-install.sh +++ b/install/librenms-install.sh @@ -58,7 +58,7 @@ $STD mariadb -u root -e "GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUS } >>~/librenms.creds msg_ok "Configured Database" -fetch_and_deploy_gh_release "LibreNMS" "librenms/librenms" +fetch_and_deploy_gh_release "librenms" "librenms/librenms" msg_info "Configuring LibreNMS" $STD useradd librenms -d /opt/librenms -M -r -s "$(which bash)" diff --git a/misc/tools.func b/misc/tools.func index fb96ed0ee..dc3b00b45 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -8,20 +8,20 @@ # 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" + 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" + 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 - fi - return 0 } # ------------------------------------------------------------------------------ @@ -30,74 +30,74 @@ get_cached_version() { # Usage: is_tool_installed "mariadb" "11.4" || echo "Not installed" # ------------------------------------------------------------------------------ is_tool_installed() { - local tool_name="$1" - local required_version="${2:-}" - local installed_version="" + local tool_name="$1" + local required_version="${2:-}" + local installed_version="" - case "$tool_name" in - mariadb) - if command -v mariadb >/dev/null 2>&1; then - installed_version=$(mariadb --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - fi - ;; - mysql) - if command -v mysql >/dev/null 2>&1; then - installed_version=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - fi - ;; - mongodb | mongod) - if command -v mongod >/dev/null 2>&1; then - installed_version=$(mongod --version 2>/dev/null | awk '/db version/{print $3}' | cut -d. -f1,2) - fi - ;; - node | nodejs) - if command -v node >/dev/null 2>&1; then - installed_version=$(node -v 2>/dev/null | grep -oP '^v\K[0-9]+') - fi - ;; - php) - if command -v php >/dev/null 2>&1; then - installed_version=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) - fi - ;; - postgres | postgresql) - if command -v psql >/dev/null 2>&1; then - installed_version=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1) - fi - ;; - ruby) - if command -v ruby >/dev/null 2>&1; then - installed_version=$(ruby --version 2>/dev/null | awk '{print $2}' | cut -d. -f1,2) - fi - ;; - rust | rustc) - if command -v rustc >/dev/null 2>&1; then - installed_version=$(rustc --version 2>/dev/null | awk '{print $2}') - fi - ;; - go | golang) - if command -v go >/dev/null 2>&1; then - installed_version=$(go version 2>/dev/null | awk '{print $3}' | sed 's/go//') - fi - ;; - clickhouse) - if command -v clickhouse >/dev/null 2>&1; then - installed_version=$(clickhouse --version 2>/dev/null | awk '{print $2}') - fi - ;; - esac + case "$tool_name" in + mariadb) + if command -v mariadb >/dev/null 2>&1; then + installed_version=$(mariadb --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + fi + ;; + mysql) + if command -v mysql >/dev/null 2>&1; then + installed_version=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + fi + ;; + mongodb | mongod) + if command -v mongod >/dev/null 2>&1; then + installed_version=$(mongod --version 2>/dev/null | awk '/db version/{print $3}' | cut -d. -f1,2) + fi + ;; + node | nodejs) + if command -v node >/dev/null 2>&1; then + installed_version=$(node -v 2>/dev/null | grep -oP '^v\K[0-9]+') + fi + ;; + php) + if command -v php >/dev/null 2>&1; then + installed_version=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) + fi + ;; + postgres | postgresql) + if command -v psql >/dev/null 2>&1; then + installed_version=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1) + fi + ;; + ruby) + if command -v ruby >/dev/null 2>&1; then + installed_version=$(ruby --version 2>/dev/null | awk '{print $2}' | cut -d. -f1,2) + fi + ;; + rust | rustc) + if command -v rustc >/dev/null 2>&1; then + installed_version=$(rustc --version 2>/dev/null | awk '{print $2}') + fi + ;; + go | golang) + if command -v go >/dev/null 2>&1; then + installed_version=$(go version 2>/dev/null | awk '{print $3}' | sed 's/go//') + fi + ;; + clickhouse) + if command -v clickhouse >/dev/null 2>&1; then + installed_version=$(clickhouse --version 2>/dev/null | awk '{print $2}') + fi + ;; + esac - if [[ -z "$installed_version" ]]; then - return 1 # Not installed - fi + if [[ -z "$installed_version" ]]; then + return 1 # Not installed + fi + + if [[ -n "$required_version" && "$installed_version" != "$required_version" ]]; then + echo "$installed_version" + return 1 # Version mismatch + fi - if [[ -n "$required_version" && "$installed_version" != "$required_version" ]]; then echo "$installed_version" - return 1 # Version mismatch - fi - - echo "$installed_version" - return 0 # Installed and version matches (if specified) + return 0 # Installed and version matches (if specified) } # ------------------------------------------------------------------------------ @@ -105,65 +105,65 @@ is_tool_installed() { # Usage: remove_old_tool_version "mariadb" "repository-name" # ------------------------------------------------------------------------------ remove_old_tool_version() { - local tool_name="$1" - local repo_name="${2:-$tool_name}" + local tool_name="$1" + local repo_name="${2:-$tool_name}" - case "$tool_name" in - mariadb) - $STD systemctl stop mariadb >/dev/null 2>&1 || true - $STD apt purge -y 'mariadb*' >/dev/null 2>&1 || true - ;; - mysql) - $STD systemctl stop mysql >/dev/null 2>&1 || true - $STD apt purge -y 'mysql*' >/dev/null 2>&1 || true - rm -rf /var/lib/mysql >/dev/null 2>&1 || true - ;; - mongodb) - $STD systemctl stop mongod >/dev/null 2>&1 || true - $STD apt purge -y 'mongodb*' >/dev/null 2>&1 || true - rm -rf /var/lib/mongodb >/dev/null 2>&1 || true - ;; - node | nodejs) - $STD apt purge -y nodejs npm >/dev/null 2>&1 || true - npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' | while read -r module; do - npm uninstall -g "$module" >/dev/null 2>&1 || true - done - ;; - php) - # Disable PHP-FPM if running - $STD systemctl disable php*-fpm >/dev/null 2>&1 || true - $STD systemctl stop php*-fpm >/dev/null 2>&1 || true - $STD apt purge -y 'php*' >/dev/null 2>&1 || true - rm -rf /etc/php >/dev/null 2>&1 || true - ;; - postgresql) - $STD systemctl stop postgresql >/dev/null 2>&1 || true - $STD apt purge -y 'postgresql*' >/dev/null 2>&1 || true - rm -rf /var/lib/postgresql >/dev/null 2>&1 || true - ;; - ruby) - if [[ -d "$HOME/.rbenv" ]]; then - rm -rf "$HOME/.rbenv" - fi - $STD apt purge -y 'ruby*' >/dev/null 2>&1 || true - ;; - rust) - rm -rf "$HOME/.cargo" "$HOME/.rustup" >/dev/null 2>&1 || true - ;; - go | golang) - rm -rf /usr/local/go >/dev/null 2>&1 || true - ;; - clickhouse) - $STD systemctl stop clickhouse-server >/dev/null 2>&1 || true - $STD apt purge -y 'clickhouse*' >/dev/null 2>&1 || true - rm -rf /var/lib/clickhouse >/dev/null 2>&1 || true - ;; - esac + case "$tool_name" in + mariadb) + $STD systemctl stop mariadb >/dev/null 2>&1 || true + $STD apt purge -y 'mariadb*' >/dev/null 2>&1 || true + ;; + mysql) + $STD systemctl stop mysql >/dev/null 2>&1 || true + $STD apt purge -y 'mysql*' >/dev/null 2>&1 || true + rm -rf /var/lib/mysql >/dev/null 2>&1 || true + ;; + mongodb) + $STD systemctl stop mongod >/dev/null 2>&1 || true + $STD apt purge -y 'mongodb*' >/dev/null 2>&1 || true + rm -rf /var/lib/mongodb >/dev/null 2>&1 || true + ;; + node | nodejs) + $STD apt purge -y nodejs npm >/dev/null 2>&1 || true + npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' | while read -r module; do + npm uninstall -g "$module" >/dev/null 2>&1 || true + done + ;; + php) + # Disable PHP-FPM if running + $STD systemctl disable php*-fpm >/dev/null 2>&1 || true + $STD systemctl stop php*-fpm >/dev/null 2>&1 || true + $STD apt purge -y 'php*' >/dev/null 2>&1 || true + rm -rf /etc/php >/dev/null 2>&1 || true + ;; + postgresql) + $STD systemctl stop postgresql >/dev/null 2>&1 || true + $STD apt purge -y 'postgresql*' >/dev/null 2>&1 || true + rm -rf /var/lib/postgresql >/dev/null 2>&1 || true + ;; + ruby) + if [[ -d "$HOME/.rbenv" ]]; then + rm -rf "$HOME/.rbenv" + fi + $STD apt purge -y 'ruby*' >/dev/null 2>&1 || true + ;; + rust) + rm -rf "$HOME/.cargo" "$HOME/.rustup" >/dev/null 2>&1 || true + ;; + go | golang) + rm -rf /usr/local/go >/dev/null 2>&1 || true + ;; + clickhouse) + $STD systemctl stop clickhouse-server >/dev/null 2>&1 || true + $STD apt purge -y 'clickhouse*' >/dev/null 2>&1 || true + rm -rf /var/lib/clickhouse >/dev/null 2>&1 || true + ;; + esac - # Clean up old repositories - cleanup_old_repo_files "$repo_name" + # Clean up old repositories + cleanup_old_repo_files "$repo_name" - return 0 + return 0 } # ------------------------------------------------------------------------------ @@ -172,19 +172,19 @@ remove_old_tool_version() { # Usage: if should_update_tool "mariadb" "11.4"; then ... fi # ------------------------------------------------------------------------------ should_update_tool() { - local tool_name="$1" - local target_version="$2" - local current_version="" + local tool_name="$1" + local target_version="$2" + local current_version="" - # Get currently installed version - current_version=$(is_tool_installed "$tool_name" 2>/dev/null) || return 0 # Not installed = needs install + # Get currently installed version + current_version=$(is_tool_installed "$tool_name" 2>/dev/null) || return 0 # Not installed = needs install - # If versions are identical, no update needed - if [[ "$current_version" == "$target_version" ]]; then - return 1 # No update needed - fi + # If versions are identical, no update needed + if [[ "$current_version" == "$target_version" ]]; then + return 1 # No update needed + fi - return 0 # Update needed + return 0 # Update needed } # ---------------------–---------------------------------------------------------- @@ -194,59 +194,59 @@ should_update_tool() { # Supports: mariadb, mongodb, nodejs, postgresql, php, mysql # ------------------------------------------------------------------------------ manage_tool_repository() { - local tool_name="$1" - local version="$2" - local repo_url="$3" - local gpg_key_url="${4:-}" - local distro_id repo_component suite + local tool_name="$1" + local version="$2" + local repo_url="$3" + local gpg_key_url="${4:-}" + local distro_id repo_component suite - distro_id=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') + distro_id=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') - case "$tool_name" in - mariadb) - if [[ -z "$repo_url" || -z "$gpg_key_url" ]]; then - msg_error "MariaDB repository requires repo_url and gpg_key_url" - return 1 - fi + case "$tool_name" in + mariadb) + if [[ -z "$repo_url" || -z "$gpg_key_url" ]]; then + msg_error "MariaDB repository requires repo_url and gpg_key_url" + return 1 + fi - # Clean old repos first - cleanup_old_repo_files "mariadb" + # Clean old repos first + cleanup_old_repo_files "mariadb" - # Get suite for fallback handling - local distro_codename - distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url/$distro_id") + # Get suite for fallback handling + local distro_codename + distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url/$distro_id") - # Setup new repository using deb822 format - setup_deb822_repo "mariadb" "$gpg_key_url" "$repo_url/$distro_id" "$suite" "main" "amd64 arm64" || return 1 - return 0 - ;; + # Setup new repository using deb822 format + setup_deb822_repo "mariadb" "$gpg_key_url" "$repo_url/$distro_id" "$suite" "main" "amd64 arm64" || return 1 + return 0 + ;; - mongodb) - if [[ -z "$repo_url" || -z "$gpg_key_url" ]]; then - msg_error "MongoDB repository requires repo_url and gpg_key_url" - return 1 - fi + mongodb) + if [[ -z "$repo_url" || -z "$gpg_key_url" ]]; then + msg_error "MongoDB repository requires repo_url and gpg_key_url" + return 1 + fi - # Clean old repos first - cleanup_old_repo_files "mongodb" + # Clean old repos first + cleanup_old_repo_files "mongodb" - # Import GPG key - mkdir -p /etc/apt/keyrings - if ! curl -fsSL "$gpg_key_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-server-${version}.gpg" 2>/dev/null; then - msg_error "Failed to download MongoDB GPG key" - return 1 - fi + # Import GPG key + mkdir -p /etc/apt/keyrings + if ! curl -fsSL "$gpg_key_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-server-${version}.gpg" 2>/dev/null; then + msg_error "Failed to download MongoDB GPG key" + return 1 + fi - # Setup repository - local distro_codename - distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url") + # Setup repository + local distro_codename + distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url") - repo_component="main" - [[ "$distro_id" == "ubuntu" ]] && repo_component="multiverse" + repo_component="main" + [[ "$distro_id" == "ubuntu" ]] && repo_component="multiverse" - cat </etc/apt/sources.list.d/mongodb-org-${version}.sources + cat </etc/apt/sources.list.d/mongodb-org-${version}.sources Types: deb URIs: ${repo_url} Suites: ${suite}/mongodb-org/${version} @@ -254,31 +254,31 @@ Components: ${repo_component} Architectures: amd64 arm64 Signed-By: /etc/apt/keyrings/mongodb-server-${version}.gpg EOF - return 0 - ;; + return 0 + ;; - nodejs) - if [[ -z "$repo_url" || -z "$gpg_key_url" ]]; then - msg_error "Node.js repository requires repo_url and gpg_key_url" - return 1 - fi + nodejs) + if [[ -z "$repo_url" || -z "$gpg_key_url" ]]; then + msg_error "Node.js repository requires repo_url and gpg_key_url" + return 1 + fi - cleanup_old_repo_files "nodesource" + cleanup_old_repo_files "nodesource" - # NodeSource uses deb822 format with GPG from repo - local distro_codename - distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + # NodeSource uses deb822 format with GPG from repo + local distro_codename + distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - # Create keyring directory first - mkdir -p /etc/apt/keyrings + # Create keyring directory first + mkdir -p /etc/apt/keyrings - # Download GPG key from NodeSource - curl -fsSL "$gpg_key_url" | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg || { - msg_error "Failed to import NodeSource GPG key" - return 1 - } + # Download GPG key from NodeSource + curl -fsSL "$gpg_key_url" | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg || { + msg_error "Failed to import NodeSource GPG key" + return 1 + } - cat </etc/apt/sources.list.d/nodesource.sources + cat </etc/apt/sources.list.d/nodesource.sources Types: deb URIs: $repo_url Suites: nodistro @@ -286,33 +286,33 @@ Components: main Architectures: amd64 arm64 Signed-By: /etc/apt/keyrings/nodesource.gpg EOF - return 0 - ;; + return 0 + ;; - php) - if [[ -z "$gpg_key_url" ]]; then - msg_error "PHP repository requires gpg_key_url" - return 1 - fi + php) + if [[ -z "$gpg_key_url" ]]; then + msg_error "PHP repository requires gpg_key_url" + return 1 + fi - cleanup_old_repo_files "php" + cleanup_old_repo_files "php" - # Download and install keyring - curl -fsSLo /tmp/debsuryorg-archive-keyring.deb "$gpg_key_url" || { - msg_error "Failed to download PHP keyring" - return 1 - } - dpkg -i /tmp/debsuryorg-archive-keyring.deb >/dev/null 2>&1 || { - msg_error "Failed to install PHP keyring" - rm -f /tmp/debsuryorg-archive-keyring.deb - return 1 - } - rm -f /tmp/debsuryorg-archive-keyring.deb + # Download and install keyring + curl -fsSLo /tmp/debsuryorg-archive-keyring.deb "$gpg_key_url" || { + msg_error "Failed to download PHP keyring" + return 1 + } + dpkg -i /tmp/debsuryorg-archive-keyring.deb >/dev/null 2>&1 || { + msg_error "Failed to install PHP keyring" + rm -f /tmp/debsuryorg-archive-keyring.deb + return 1 + } + rm -f /tmp/debsuryorg-archive-keyring.deb - # Setup repository - local distro_codename - distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - cat </etc/apt/sources.list.d/php.sources + # Setup repository + local distro_codename + distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + cat </etc/apt/sources.list.d/php.sources Types: deb URIs: https://packages.sury.org/php Suites: $distro_codename @@ -320,30 +320,30 @@ Components: main Architectures: amd64 arm64 Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg EOF - return 0 - ;; + return 0 + ;; - postgresql) - if [[ -z "$gpg_key_url" ]]; then - msg_error "PostgreSQL repository requires gpg_key_url" - return 1 - fi + postgresql) + if [[ -z "$gpg_key_url" ]]; then + msg_error "PostgreSQL repository requires gpg_key_url" + return 1 + fi - cleanup_old_repo_files "postgresql" + cleanup_old_repo_files "postgresql" - # Create keyring directory first - mkdir -p /etc/apt/keyrings + # Create keyring directory first + mkdir -p /etc/apt/keyrings - # Import PostgreSQL key - curl -fsSL "$gpg_key_url" | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg || { - msg_error "Failed to import PostgreSQL GPG key" - return 1 - } + # Import PostgreSQL key + curl -fsSL "$gpg_key_url" | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg || { + msg_error "Failed to import PostgreSQL GPG key" + return 1 + } - # Setup repository - local distro_codename - distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - cat </etc/apt/sources.list.d/postgresql.sources + # Setup repository + local distro_codename + distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + cat </etc/apt/sources.list.d/postgresql.sources Types: deb URIs: http://apt.postgresql.org/pub/repos/apt Suites: $distro_codename-pgdg @@ -351,532 +351,532 @@ Components: main Architectures: amd64 arm64 Signed-By: /etc/apt/keyrings/postgresql.gpg EOF + return 0 + ;; + + *) + msg_error "Unknown tool repository: $tool_name" + return 1 + ;; + esac + return 0 - ;; - - *) - msg_error "Unknown tool repository: $tool_name" - return 1 - ;; - esac - - return 0 } # ------–---------------------------------------------------------------------- # Unified package upgrade function (with apt update caching) # ------------------------------------------------------------------------------ upgrade_package() { - local package="$1" + 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 + # 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 [[ -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 || { - msg_warn "APT update failed in upgrade_package - continuing with cached packages" + if ((current_time - last_update > 300)); then + $STD apt update || { + msg_warn "APT update failed in upgrade_package - continuing with cached packages" + } + echo "$current_time" >"$apt_cache_file" + fi + + $STD apt install --only-upgrade -y "$package" || { + msg_warn "Failed to upgrade $package" + return 1 } - echo "$current_time" >"$apt_cache_file" - fi - - $STD apt install --only-upgrade -y "$package" || { - msg_warn "Failed to upgrade $package" - return 1 - } } # ------------------------------------------------------------------------------ # Repository availability check # ------------------------------------------------------------------------------ verify_repo_available() { - local repo_url="$1" - local suite="$2" + 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 + 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=() + local deps=("$@") + local missing=() - for dep in "${deps[@]}"; do - if ! command -v "$dep" &>/dev/null && ! is_package_installed "$dep"; then - missing+=("$dep") + 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 - 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" + 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 + 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 + 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" + 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" + 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$" + 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 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") + 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") + 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 + 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 + return 1 } should_upgrade() { - local current="$1" - local target="$2" + local current="$1" + local target="$2" - [[ -z "$current" ]] && return 0 - version_gt "$target" "$current" && return 0 - return 1 + [[ -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 + 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 + # 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 + 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" ]] + [[ "$(get_os_info id)" == "debian" ]] } is_ubuntu() { - [[ "$(get_os_info id)" == "ubuntu" ]] + [[ "$(get_os_info id)" == "ubuntu" ]] } is_alpine() { - [[ "$(get_os_info id)" == "alpine" ]] + [[ "$(get_os_info id)" == "alpine" ]] } # ------------------------------------------------------------------------------ # Get Debian/Ubuntu major version # ------------------------------------------------------------------------------ get_os_version_major() { - local version=$(get_os_info version) - echo "${version%%.*}" + 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 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#) + 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 + 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 + 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 + 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" + 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 + # 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 + # 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 "bookworm" - ;; + echo "$distro_codename" + ;; 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" + 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 + 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) + 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 + 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 + 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}') + 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)) + # 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" + # 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) + 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" ;; + 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 - ;; - 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) + 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" ;; + 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 - ;; - 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 "22" + # Always return current LTS (as of 2025) + echo "22" } # ------------------------------------------------------------------------------ # 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 + 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 + 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 + 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 + sleep 5 + waited=$((waited + 5)) + done - return 0 + return 0 } # ------------------------------------------------------------------------------ # Cleanup old repository files (migration helper) # ------------------------------------------------------------------------------ cleanup_old_repo_files() { - local app="$1" + 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-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 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 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 + # 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 } # ------------------------------------------------------------------------------ @@ -885,34 +885,34 @@ cleanup_old_repo_files() { # 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" + local sources_dir="/etc/apt/sources.list.d" + local keyrings_dir="/etc/apt/keyrings" - [[ ! -d "$sources_dir" ]] && return 0 + [[ ! -d "$sources_dir" ]] && return 0 - while IFS= read -r -d '' sources_file; do - local basename_file - basename_file=$(basename "$sources_file") + 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 + # 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 - - # 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 } # ------------------------------------------------------------------------------ @@ -920,23 +920,23 @@ cleanup_orphaned_sources() { # 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 update -qq 2>/dev/null; then - # More aggressive cleanup - rm -f /etc/apt/sources.list.d/*.sources 2>/dev/null || true + # Clean up orphaned sources first cleanup_orphaned_sources - # Try again + # Try to update package lists if ! apt update -qq 2>/dev/null; then - msg_error "Cannot update package lists - APT is critically broken" - return 1 - fi - fi + # More aggressive cleanup + rm -f /etc/apt/sources.list.d/*.sources 2>/dev/null || true + cleanup_orphaned_sources - return 0 + # Try again + if ! apt update -qq 2>/dev/null; then + msg_error "Cannot update package lists - APT is critically broken" + return 1 + fi + fi + + return 0 } # ------------------------------------------------------------------------------ @@ -944,39 +944,39 @@ ensure_apt_working() { # Validates all parameters and fails safely if any are empty # ------------------------------------------------------------------------------ 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}" + local name="$1" + local gpg_url="$2" + local repo_url="$3" + local suite="$4" + local component="${5:-main}" + local architectures="${6:-amd64 arm64}" - # Validate required parameters - if [[ -z "$name" || -z "$gpg_url" || -z "$repo_url" || -z "$suite" ]]; then - msg_error "setup_deb822_repo: missing required parameters (name=$name, gpg=$gpg_url, repo=$repo_url, suite=$suite)" - return 1 - fi + # Validate required parameters + if [[ -z "$name" || -z "$gpg_url" || -z "$repo_url" || -z "$suite" ]]; then + msg_error "setup_deb822_repo: missing required parameters (name=$name, gpg=$gpg_url, repo=$repo_url, suite=$suite)" + return 1 + fi - # Cleanup old configs for this app - cleanup_old_repo_files "$name" + # Cleanup old configs for this app + cleanup_old_repo_files "$name" - # Cleanup any orphaned .sources files from other apps - cleanup_orphaned_sources + # Cleanup any orphaned .sources files from other apps + cleanup_orphaned_sources - # Ensure keyring directory exists - mkdir -p /etc/apt/keyrings || { - msg_error "Failed to create /etc/apt/keyrings directory" - return 1 - } + # Ensure keyring directory exists + mkdir -p /etc/apt/keyrings || { + msg_error "Failed to create /etc/apt/keyrings directory" + return 1 + } - # Download GPG key (with --yes to avoid interactive prompts) - curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" 2>/dev/null || { - msg_error "Failed to download or import GPG key for ${name} from $gpg_url" - return 1 - } + # Download GPG key (with --yes to avoid interactive prompts) + curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" 2>/dev/null || { + msg_error "Failed to download or import GPG key for ${name} from $gpg_url" + return 1 + } - # Create deb822 sources file - cat </etc/apt/sources.list.d/${name}.sources + # Create deb822 sources file + cat </etc/apt/sources.list.d/${name}.sources Types: deb URIs: $repo_url Suites: $suite @@ -985,175 +985,175 @@ 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 + # 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 + 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 + # 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" + local package="$1" + $STD apt-mark hold "$package" } unhold_package_version() { - local package="$1" - $STD apt-mark unhold "$package" + local package="$1" + $STD apt-mark unhold "$package" } # ------------------------------------------------------------------------------ # Safe service restart with verification # ------------------------------------------------------------------------------ safe_service_restart() { - local service="$1" + 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 + $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 + 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" + local service="$1" - if ! systemctl enable "$service" &>/dev/null; then - return 1 - fi + 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 + if ! systemctl start "$service" &>/dev/null; then + msg_error "Failed to start $service" + systemctl status "$service" --no-pager + return 1 + fi - return 0 + return 0 } # ------------------------------------------------------------------------------ # Check if service is enabled # ------------------------------------------------------------------------------ is_service_enabled() { - local service="$1" - systemctl is-enabled --quiet "$service" 2>/dev/null + 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 + 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}" + local json="$1" + local field="${2:-tag_name}" + local strip_v="${3:-true}" - ensure_dependencies jq + ensure_dependencies jq - local version - version=$(echo "$json" | jq -r ".${field} // empty") + local version + version=$(echo "$json" | jq -r ".${field} // empty") - if [[ -z "$version" ]]; then - return 1 - fi + if [[ -z "$version" ]]; then + return 1 + fi - if [[ "$strip_v" == "true" ]]; then - echo "${version#v}" - else - echo "$version" - 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) + 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 + 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" - 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 - if [[ -z "$version" ]]; then - return 1 - fi - - echo "$version" + echo "$version" } # ------------------------------------------------------------------------------ # Debug logging (only if DEBUG=1) # ------------------------------------------------------------------------------ debug_log() { - [[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2 + [[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2 } # ------------------------------------------------------------------------------ # Performance timing helper # ------------------------------------------------------------------------------ start_timer() { - echo $(date +%s) + echo $(date +%s) } end_timer() { - local start_time="$1" - local label="${2:-Operation}" - local end_time=$(date +%s) - local duration=$((end_time - start_time)) + 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 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) + 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 + if [[ "$actual_fingerprint" == "$expected_fingerprint" ]]; then + return 0 + fi - msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint" - return 1 + msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint" + return 1 } # ============================================================================== @@ -1181,97 +1181,97 @@ verify_gpg_fingerprint() { # - Does not support pre-releases # ------------------------------------------------------------------------------ check_for_gh_release() { - local app="$1" - local source="$2" - local pinned_version_in="${3:-}" # optional - local app_lc="${app,,}" - local current_file="$HOME/.${app_lc}" + local app="$1" + local source="$2" + local pinned_version_in="${3:-}" # optional + local app_lc="${app,,}" + local current_file="$HOME/.${app_lc}" - msg_info "Checking for update: ${app}" + msg_info "Checking for update: ${app}" - # DNS check - if ! getent hosts api.github.com >/dev/null 2>&1; then - msg_error "Network error: cannot resolve api.github.com" - return 1 - fi - - ensure_dependencies jq - - # Fetch releases and exclude drafts/prereleases - local releases_json - releases_json=$(curl -fsSL --max-time 20 \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "https://api.github.com/repos/${source}/releases") || { - msg_error "Unable to fetch releases for ${app}" - return 1 - } - - mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json") - if ((${#raw_tags[@]} == 0)); then - msg_error "No stable releases found for ${app}" - return 1 - fi - - local clean_tags=() - for t in "${raw_tags[@]}"; do - clean_tags+=("${t#v}") - done - - local latest_raw="${raw_tags[0]}" - local latest_clean="${clean_tags[0]}" - - # current installed (stored without v) - local current="" - if [[ -f "$current_file" ]]; then - current="$(<"$current_file")" - else - # Migration: search for any /opt/*_version.txt - local legacy_files - mapfile -t legacy_files < <(find /opt -maxdepth 1 -type f -name "*_version.txt" 2>/dev/null) - if ((${#legacy_files[@]} == 1)); then - current="$(<"${legacy_files[0]}")" - echo "${current#v}" >"$current_file" - rm -f "${legacy_files[0]}" + # DNS check + if ! getent hosts api.github.com >/dev/null 2>&1; then + msg_error "Network error: cannot resolve api.github.com" + return 1 fi - fi - current="${current#v}" - # Pinned version handling - if [[ -n "$pinned_version_in" ]]; then - local pin_clean="${pinned_version_in#v}" - local match_raw="" - for i in "${!clean_tags[@]}"; do - if [[ "${clean_tags[$i]}" == "$pin_clean" ]]; then - match_raw="${raw_tags[$i]}" - break - fi + ensure_dependencies jq + + # Fetch releases and exclude drafts/prereleases + local releases_json + releases_json=$(curl -fsSL --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${source}/releases") || { + msg_error "Unable to fetch releases for ${app}" + return 1 + } + + mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json") + if ((${#raw_tags[@]} == 0)); then + msg_error "No stable releases found for ${app}" + return 1 + fi + + local clean_tags=() + for t in "${raw_tags[@]}"; do + clean_tags+=("${t#v}") done - if [[ -z "$match_raw" ]]; then - msg_error "Pinned version ${pinned_version_in} not found upstream" - return 1 + local latest_raw="${raw_tags[0]}" + local latest_clean="${clean_tags[0]}" + + # current installed (stored without v) + local current="" + if [[ -f "$current_file" ]]; then + current="$(<"$current_file")" + else + # Migration: search for any /opt/*_version.txt + local legacy_files + mapfile -t legacy_files < <(find /opt -maxdepth 1 -type f -name "*_version.txt" 2>/dev/null) + if ((${#legacy_files[@]} == 1)); then + current="$(<"${legacy_files[0]}")" + echo "${current#v}" >"$current_file" + rm -f "${legacy_files[0]}" + fi + fi + current="${current#v}" + + # Pinned version handling + if [[ -n "$pinned_version_in" ]]; then + local pin_clean="${pinned_version_in#v}" + local match_raw="" + for i in "${!clean_tags[@]}"; do + if [[ "${clean_tags[$i]}" == "$pin_clean" ]]; then + match_raw="${raw_tags[$i]}" + break + fi + done + + if [[ -z "$match_raw" ]]; then + msg_error "Pinned version ${pinned_version_in} not found upstream" + return 1 + fi + + if [[ "$current" != "$pin_clean" ]]; then + CHECK_UPDATE_RELEASE="$match_raw" + msg_ok "Update available: ${app} ${current:-not installed} → ${pin_clean}" + return 0 + fi + + msg_error "No update available: ${app} is not installed!" + return 1 fi - if [[ "$current" != "$pin_clean" ]]; then - CHECK_UPDATE_RELEASE="$match_raw" - msg_ok "Update available: ${app} ${current:-not installed} → ${pin_clean}" - return 0 + # No pinning → use latest + if [[ -z "$current" || "$current" != "$latest_clean" ]]; then + CHECK_UPDATE_RELEASE="$latest_raw" + msg_ok "Update available: ${app} ${current:-not installed} → ${latest_clean}" + return 0 fi - msg_error "No update available: ${app} is not installed!" + msg_ok "No update available: ${app} (${latest_clean})" return 1 - fi - - # No pinning → use latest - if [[ -z "$current" || "$current" != "$latest_clean" ]]; then - CHECK_UPDATE_RELEASE="$latest_raw" - msg_ok "Update available: ${app} ${current:-not installed} → ${latest_clean}" - return 0 - fi - - msg_ok "No update available: ${app} (${latest_clean})" - return 1 } # ------------------------------------------------------------------------------ @@ -1284,35 +1284,35 @@ check_for_gh_release() { # APP - Application name (default: $APPLICATION variable) # ------------------------------------------------------------------------------ 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" + 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 + 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 - } + $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 - } + 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" + chmod 600 "$CERT_KEY" + chmod 644 "$CERT_CRT" } # ------------------------------------------------------------------------------ @@ -1324,28 +1324,28 @@ create_self_signed_cert() { # ------------------------------------------------------------------------------ function download_with_progress() { - local url="$1" - local output="$2" - if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi + local url="$1" + local output="$2" + if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi - ensure_dependencies pv - set -o pipefail + ensure_dependencies pv + set -o pipefail - # Content-Length aus HTTP-Header holen - local content_length - content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true) + # Content-Length aus HTTP-Header holen + local content_length + content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true) - if [[ -z "$content_length" ]]; then - if ! curl -fL# -o "$output" "$url"; then - msg_error "Download failed" - return 1 + if [[ -z "$content_length" ]]; then + if ! curl -fL# -o "$output" "$url"; then + msg_error "Download failed" + return 1 + fi + else + if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then + msg_error "Download failed" + return 1 + fi fi - else - if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then - msg_error "Download failed" - return 1 - fi - fi } # ------------------------------------------------------------------------------ @@ -1356,12 +1356,12 @@ function download_with_progress() { # ------------------------------------------------------------------------------ function ensure_usr_local_bin_persist() { - local PROFILE_FILE="/etc/profile.d/custom_path.sh" + local PROFILE_FILE="/etc/profile.d/custom_path.sh" - if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then - echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE" - chmod +x "$PROFILE_FILE" - fi + if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then + echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE" + chmod +x "$PROFILE_FILE" + fi } # ------------------------------------------------------------------------------ @@ -1409,315 +1409,315 @@ function ensure_usr_local_bin_persist() { # ------------------------------------------------------------------------------ function fetch_and_deploy_gh_release() { - local app="$1" - local repo="$2" - local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile - local version="${4:-latest}" - local target="${5:-/opt/$app}" - local asset_pattern="${6:-}" + local app="$1" + local repo="$2" + local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile + local version="${4:-latest}" + local target="${5:-/opt/$app}" + local asset_pattern="${6:-}" - local app_lc=$(echo "${app,,}" | tr -d ' ') - local version_file="$HOME/.${app_lc}" + local app_lc=$(echo "${app,,}" | tr -d ' ') + local version_file="$HOME/.${app_lc}" - local api_timeout="--connect-timeout 10 --max-time 60" - local download_timeout="--connect-timeout 15 --max-time 900" + local api_timeout="--connect-timeout 10 --max-time 60" + local download_timeout="--connect-timeout 15 --max-time 900" - local current_version="" - [[ -f "$version_file" ]] && current_version=$(<"$version_file") + local current_version="" + [[ -f "$version_file" ]] && current_version=$(<"$version_file") - ensure_dependencies jq + 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" - local header=() - [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN") + local api_url="https://api.github.com/repos/$repo/releases" + [[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest" + local header=() + [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN") - # dns pre check - local gh_host - gh_host=$(awk -F/ '{print $3}' <<<"$api_url") - if ! getent hosts "$gh_host" &>/dev/null; then - msg_error "DNS resolution failed for $gh_host – check /etc/resolv.conf or networking" - return 1 - fi - - local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code - - while ((attempt <= max_retries)); do - resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url") && success=true && break - sleep "$retry_delay" - ((attempt++)) - done - - if ! $success; then - msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts" - return 1 - fi - - http_code="${resp:(-3)}" - [[ "$http_code" != "200" ]] && { - msg_error "GitHub API returned HTTP $http_code" - return 1 - } - - local json tag_name - json=$(/dev/null; then + msg_error "DNS resolution failed for $gh_host – check /etc/resolv.conf or networking" + return 1 fi - tar --no-same-owner -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) + local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code - shopt -s dotglob nullglob - cp -r "$unpack_dir"/* "$target/" - shopt -u dotglob nullglob + while ((attempt <= max_retries)); do + resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url") && success=true && break + sleep "$retry_delay" + ((attempt++)) + done - ### Binary Mode ### - elif [[ "$mode" == "binary" ]]; then - local arch - arch=$(dpkg --print-architecture 2>/dev/null || uname -m) - [[ "$arch" == "x86_64" ]] && arch="amd64" - [[ "$arch" == "aarch64" ]] && arch="arm64" - - local assets url_match="" - assets=$(echo "$json" | jq -r '.assets[].browser_download_url') - - # If explicit filename pattern is provided (param $6), match that first - if [[ -n "$asset_pattern" ]]; then - for u in $assets; do - case "${u##*/}" in - $asset_pattern) - url_match="$u" - break - ;; - esac - done + if ! $success; then + msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts" + return 1 fi - # If no match via explicit pattern, fall back to architecture heuristic - if [[ -z "$url_match" ]]; then - for u in $assets; do - if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then - url_match="$u" - break + http_code="${resp:(-3)}" + [[ "$http_code" != "200" ]] && { + msg_error "GitHub API returned HTTP $http_code" + return 1 + } + + local json tag_name + json=$(/dev/null || uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" + [[ "$arch" == "aarch64" ]] && arch="arm64" - chmod 644 "$tmpdir/$filename" - $STD apt install -y "$tmpdir/$filename" || { - $STD dpkg -i "$tmpdir/$filename" || { - msg_error "Both apt and dpkg installation failed" + local assets url_match="" + assets=$(echo "$json" | jq -r '.assets[].browser_download_url') + + # If explicit filename pattern is provided (param $6), match that first + if [[ -n "$asset_pattern" ]]; then + for u in $assets; do + case "${u##*/}" in + $asset_pattern) + url_match="$u" + break + ;; + esac + done + fi + + # If no match via explicit pattern, fall back to architecture heuristic + if [[ -z "$url_match" ]]; then + for u in $assets; do + if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then + url_match="$u" + break + fi + done + fi + + # Fallback: any .deb file + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ \.deb$ ]] && url_match="$u" && break + done + fi + + if [[ -z "$url_match" ]]; then + msg_error "No suitable .deb asset found for $app" + rm -rf "$tmpdir" + return 1 + fi + + filename="${url_match##*/}" + curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url_match" || { + msg_error "Download failed: $url_match" + rm -rf "$tmpdir" + return 1 + } + + chmod 644 "$tmpdir/$filename" + $STD apt install -y "$tmpdir/$filename" || { + $STD dpkg -i "$tmpdir/$filename" || { + msg_error "Both apt and dpkg installation failed" + rm -rf "$tmpdir" + return 1 + } + } + + ### Prebuild Mode ### + elif [[ "$mode" == "prebuild" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + local unpack_tmp + unpack_tmp=$(mktemp -d) + mkdir -p "$target" + if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then + rm -rf "${target:?}/"* + fi + + if [[ "$filename" == *.zip ]]; then + 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 --no-same-owner -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" + return 1 + fi + + local top_dirs + top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l) + local top_entries inner_dir + top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) + if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then + # Strip leading folder + inner_dir="$top_entries" + shopt -s dotglob nullglob + if compgen -G "$inner_dir/*" >/dev/null; then + cp -r "$inner_dir"/* "$target/" || { + msg_error "Failed to copy contents from $inner_dir to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Inner directory is empty: $inner_dir" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + else + # Copy all contents + shopt -s dotglob nullglob + if compgen -G "$unpack_tmp/*" >/dev/null; then + cp -r "$unpack_tmp"/* "$target/" || { + msg_error "Failed to copy contents to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unpacked archive is empty" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + fi + + ### Singlefile Mode ### + elif [[ "$mode" == "singlefile" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + mkdir -p "$target" + + local use_filename="${USE_ORIGINAL_FILENAME:-false}" + local target_file="$app" + [[ "$use_filename" == "true" ]] && target_file="$filename" + + curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then + chmod +x "$target/$target_file" + fi + + else + msg_error "Unknown mode: $mode" rm -rf "$tmpdir" return 1 - } - } - - ### Prebuild Mode ### - elif [[ "$mode" == "prebuild" ]]; then - local pattern="${6%\"}" - pattern="${pattern#\"}" - [[ -z "$pattern" ]] && { - msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" - rm -rf "$tmpdir" - return 1 - } - - local asset_url="" - for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do - filename_candidate="${u##*/}" - case "$filename_candidate" in - $pattern) - asset_url="$u" - break - ;; - esac - done - - [[ -z "$asset_url" ]] && { - msg_error "No asset matching '$pattern' found" - rm -rf "$tmpdir" - return 1 - } - - filename="${asset_url##*/}" - curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || { - msg_error "Download failed: $asset_url" - rm -rf "$tmpdir" - return 1 - } - - local unpack_tmp - unpack_tmp=$(mktemp -d) - mkdir -p "$target" - if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then - rm -rf "${target:?}/"* fi - if [[ "$filename" == *.zip ]]; then - 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 --no-same-owner -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" - return 1 - fi - - local top_dirs - top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l) - local top_entries inner_dir - top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) - if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then - # Strip leading folder - inner_dir="$top_entries" - shopt -s dotglob nullglob - if compgen -G "$inner_dir/*" >/dev/null; then - cp -r "$inner_dir"/* "$target/" || { - msg_error "Failed to copy contents from $inner_dir to $target" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Inner directory is empty: $inner_dir" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - shopt -u dotglob nullglob - else - # Copy all contents - shopt -s dotglob nullglob - if compgen -G "$unpack_tmp/*" >/dev/null; then - cp -r "$unpack_tmp"/* "$target/" || { - msg_error "Failed to copy contents to $target" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Unpacked archive is empty" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - shopt -u dotglob nullglob - fi - - ### Singlefile Mode ### - elif [[ "$mode" == "singlefile" ]]; then - local pattern="${6%\"}" - pattern="${pattern#\"}" - [[ -z "$pattern" ]] && { - msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" - rm -rf "$tmpdir" - return 1 - } - - local asset_url="" - for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do - filename_candidate="${u##*/}" - case "$filename_candidate" in - $pattern) - asset_url="$u" - break - ;; - esac - done - - [[ -z "$asset_url" ]] && { - msg_error "No asset matching '$pattern' found" - rm -rf "$tmpdir" - return 1 - } - - filename="${asset_url##*/}" - mkdir -p "$target" - - local use_filename="${USE_ORIGINAL_FILENAME:-false}" - local target_file="$app" - [[ "$use_filename" == "true" ]] && target_file="$filename" - - curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || { - msg_error "Download failed: $asset_url" - rm -rf "$tmpdir" - return 1 - } - - if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then - chmod +x "$target/$target_file" - fi - - else - msg_error "Unknown mode: $mode" + echo "$version" >"$version_file" + msg_ok "Deployed: $app ($version)" rm -rf "$tmpdir" - return 1 - fi - - echo "$version" >"$version_file" - msg_ok "Deployed: $app ($version)" - rm -rf "$tmpdir" } # ------------------------------------------------------------------------------ @@ -1728,40 +1728,40 @@ function fetch_and_deploy_gh_release() { # ------------------------------------------------------------------------------ function import_local_ip() { - local IP_FILE="/run/local-ip.env" - if [[ -f "$IP_FILE" ]]; then - # shellcheck disable=SC1090 - source "$IP_FILE" - fi - - if [[ -z "${LOCAL_IP:-}" ]]; then - get_current_ip() { - local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") - local ip - - for target in "${targets[@]}"; do - if [[ "$target" == "default" ]]; then - ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - else - ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - fi - if [[ -n "$ip" ]]; then - echo "$ip" - return 0 - fi - done - - return 1 - } - - LOCAL_IP="$(get_current_ip || true)" - if [[ -z "$LOCAL_IP" ]]; then - msg_error "Could not determine LOCAL_IP" - return 1 + local IP_FILE="/run/local-ip.env" + if [[ -f "$IP_FILE" ]]; then + # shellcheck disable=SC1090 + source "$IP_FILE" fi - fi - export LOCAL_IP + if [[ -z "${LOCAL_IP:-}" ]]; then + get_current_ip() { + local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") + local ip + + for target in "${targets[@]}"; do + if [[ "$target" == "default" ]]; then + ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + else + ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + fi + if [[ -n "$ip" ]]; then + echo "$ip" + return 0 + fi + done + + return 1 + } + + LOCAL_IP="$(get_current_ip || true)" + if [[ -z "$LOCAL_IP" ]]; then + msg_error "Could not determine LOCAL_IP" + return 1 + fi + fi + + export LOCAL_IP } # ------------------------------------------------------------------------------ @@ -1773,32 +1773,32 @@ function import_local_ip() { # ------------------------------------------------------------------------------ function setup_adminer() { - if grep -qi alpine /etc/os-release; then - msg_info "Setup Adminer (Alpine)" - mkdir -p /var/www/localhost/htdocs/adminer - 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 - } - cache_installed_version "adminer" "latest-alpine" - msg_ok "Setup Adminer (Alpine)" - else - msg_info "Setup 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 + if grep -qi alpine /etc/os-release; then + msg_info "Setup Adminer (Alpine)" + mkdir -p /var/www/localhost/htdocs/adminer + 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 + } + cache_installed_version "adminer" "latest-alpine" + msg_ok "Setup Adminer (Alpine)" + else + msg_info "Setup 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 } # ------------------------------------------------------------------------------ @@ -1811,60 +1811,60 @@ function setup_adminer() { # ------------------------------------------------------------------------------ function setup_composer() { - local COMPOSER_BIN="/usr/local/bin/composer" - export COMPOSER_ALLOW_SUPERUSER=1 + local COMPOSER_BIN="/usr/local/bin/composer" + export COMPOSER_ALLOW_SUPERUSER=1 - # Get currently installed version - local INSTALLED_VERSION="" - if [[ -x "$COMPOSER_BIN" ]]; then - INSTALLED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') - fi + # Get currently installed version + local INSTALLED_VERSION="" + if [[ -x "$COMPOSER_BIN" ]]; then + INSTALLED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') + fi - # Scenario 1: Already installed - just self-update - if [[ -n "$INSTALLED_VERSION" ]]; then - msg_info "Update Composer $INSTALLED_VERSION" - $STD "$COMPOSER_BIN" self-update --no-interaction || true - local UPDATED_VERSION - UPDATED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') - cache_installed_version "composer" "$UPDATED_VERSION" - msg_ok "Update Composer $UPDATED_VERSION" - return 0 - fi + # Scenario 1: Already installed - just self-update + if [[ -n "$INSTALLED_VERSION" ]]; then + msg_info "Update Composer $INSTALLED_VERSION" + $STD "$COMPOSER_BIN" self-update --no-interaction || true + local UPDATED_VERSION + UPDATED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') + cache_installed_version "composer" "$UPDATED_VERSION" + msg_ok "Update Composer $UPDATED_VERSION" + return 0 + fi - # Scenario 2: Fresh install - msg_info "Setup Composer" + # Scenario 2: Fresh install + 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 + 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_persist - export PATH="/usr/local/bin:$PATH" + ensure_usr_local_bin_persist + export PATH="/usr/local/bin:$PATH" - curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || { - msg_error "Failed to download Composer installer" - return 1 - } + curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || { + msg_error "Failed to download Composer installer" + return 1 + } - $STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer || { - msg_error "Failed to install Composer" + $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 - return 1 - } - rm -f /tmp/composer-setup.php - if [[ ! -x "$COMPOSER_BIN" ]]; then - msg_error "Composer installation failed" - return 1 - fi + if [[ ! -x "$COMPOSER_BIN" ]]; then + msg_error "Composer installation failed" + return 1 + fi - chmod +x "$COMPOSER_BIN" - $STD "$COMPOSER_BIN" self-update --no-interaction || true + chmod +x "$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" + local FINAL_VERSION + FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') + cache_installed_version "composer" "$FINAL_VERSION" + msg_ok "Setup Composer" } # ------------------------------------------------------------------------------ @@ -1886,201 +1886,201 @@ function setup_composer() { # ------------------------------------------------------------------------------ function setup_ffmpeg() { - 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 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" - # Get currently installed version - local INSTALLED_VERSION="" - if command -v ffmpeg &>/dev/null; then - INSTALLED_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}') - fi + # Get currently installed version + local INSTALLED_VERSION="" + if command -v ffmpeg &>/dev/null; then + INSTALLED_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}') + fi - msg_info "Setup FFmpeg ${VERSION} ($TYPE)" + msg_info "Setup FFmpeg ${VERSION} ($TYPE)" - # Binary fallback mode - if [[ "$TYPE" == "binary" ]]; then - 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 + # Binary fallback mode + if [[ "$TYPE" == "binary" ]]; then + 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 2>/dev/null | head -n1 | awk '{print $3}') + rm -rf "$TMP_DIR" + cache_installed_version "ffmpeg" "$FINAL_VERSION" + ensure_usr_local_bin_persist + [[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION" + return 0 + fi + + ensure_dependencies jq + + # Auto-detect latest stable version if none specified + if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then + local ffmpeg_tags + ffmpeg_tags=$(curl -fsSL --max-time 15 "https://api.github.com/repos/${GITHUB_REPO}/tags" 2>/dev/null || echo "") + + if [[ -z "$ffmpeg_tags" ]]; then + msg_warn "Could not fetch FFmpeg versions from GitHub, trying binary fallback" + VERSION="" # Will trigger binary fallback below + else + VERSION=$(echo "$ffmpeg_tags" | jq -r '.[].name' 2>/dev/null | + grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' | + sort -V | tail -n1 || echo "") + fi + fi + + if [[ -z "$VERSION" ]]; then + msg_info "Could not determine FFmpeg source version, using pre-built binary" + VERSION="" # Will use binary fallback + fi + + # Dependency selection + local DEPS=(build-essential yasm nasm pkg-config) + case "$TYPE" in + minimal) + DEPS+=(libx264-dev libvpx-dev libmp3lame-dev) + ;; + medium) + DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev) + ;; + full) + DEPS+=( + libx264-dev libx265-dev libvpx-dev libmp3lame-dev + libfreetype6-dev libass-dev libopus-dev libvorbis-dev + libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev + libva-dev libdrm-dev + ) + ;; + *) + msg_error "Invalid FFMPEG_TYPE: $TYPE" + rm -rf "$TMP_DIR" + return 1 + ;; + esac + + ensure_dependencies "${DEPS[@]}" + + # Try to download source if VERSION is set + if [[ -n "$VERSION" ]]; then + curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || { + msg_warn "Failed to download FFmpeg source ${VERSION}, falling back to pre-built binary" + VERSION="" + } + fi + + # If no source download (either VERSION empty or download failed), use binary + if [[ -z "$VERSION" ]]; then + msg_info "Setup FFmpeg from pre-built binary" + 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 pre-built binary" + rm -rf "$TMP_DIR" + return 1 + } + + tar -xJf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" || { + msg_error "Failed to extract FFmpeg binary archive" + rm -rf "$TMP_DIR" + return 1 + } + + if ! cp "$TMP_DIR/ffmpeg-"*/ffmpeg /usr/local/bin/ffmpeg 2>/dev/null; then + msg_error "Failed to install FFmpeg binary" + rm -rf "$TMP_DIR" + return 1 + fi + + cache_installed_version "ffmpeg" "static" + rm -rf "$TMP_DIR" + msg_ok "Setup FFmpeg from pre-built binary" + return 0 + fi + + tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract FFmpeg source" + 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 + + cd "$TMP_DIR/FFmpeg-"* || { + msg_error "Source extraction failed" + 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 2>/dev/null | head -n1 | awk '{print $3}') + + local args=( + --enable-gpl + --enable-shared + --enable-nonfree + --disable-static + --enable-libx264 + --enable-libvpx + --enable-libmp3lame + ) + + if [[ "$TYPE" != "minimal" ]]; then + args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis) + fi + + if [[ "$TYPE" == "full" ]]; then + args+=(--enable-libx265 --enable-libdav1d --enable-zlib) + args+=(--enable-vaapi --enable-libdrm) + fi + + if [[ ${#args[@]} -eq 0 ]]; then + msg_error "FFmpeg configure args array is empty" + rm -rf "$TMP_DIR" + return 1 + fi + + $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 + } + echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf + $STD ldconfig + + ldconfig -p 2>/dev/null | grep libavdevice >/dev/null || { + msg_error "libavdevice not registered with dynamic linker" + rm -rf "$TMP_DIR" + return 1 + } + + if ! command -v ffmpeg &>/dev/null; then + msg_error "FFmpeg installation failed" + rm -rf "$TMP_DIR" + return 1 + fi + + local FINAL_VERSION + FINAL_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}') rm -rf "$TMP_DIR" cache_installed_version "ffmpeg" "$FINAL_VERSION" ensure_usr_local_bin_persist [[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION" - return 0 - fi - - ensure_dependencies jq - - # Auto-detect latest stable version if none specified - if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then - local ffmpeg_tags - ffmpeg_tags=$(curl -fsSL --max-time 15 "https://api.github.com/repos/${GITHUB_REPO}/tags" 2>/dev/null || echo "") - - if [[ -z "$ffmpeg_tags" ]]; then - msg_warn "Could not fetch FFmpeg versions from GitHub, trying binary fallback" - VERSION="" # Will trigger binary fallback below - else - VERSION=$(echo "$ffmpeg_tags" | jq -r '.[].name' 2>/dev/null | - grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' | - sort -V | tail -n1 || echo "") - fi - fi - - if [[ -z "$VERSION" ]]; then - msg_info "Could not determine FFmpeg source version, using pre-built binary" - VERSION="" # Will use binary fallback - fi - - # Dependency selection - local DEPS=(build-essential yasm nasm pkg-config) - case "$TYPE" in - minimal) - DEPS+=(libx264-dev libvpx-dev libmp3lame-dev) - ;; - medium) - DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev) - ;; - full) - DEPS+=( - libx264-dev libx265-dev libvpx-dev libmp3lame-dev - libfreetype6-dev libass-dev libopus-dev libvorbis-dev - libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev - libva-dev libdrm-dev - ) - ;; - *) - msg_error "Invalid FFMPEG_TYPE: $TYPE" - rm -rf "$TMP_DIR" - return 1 - ;; - esac - - ensure_dependencies "${DEPS[@]}" - - # Try to download source if VERSION is set - if [[ -n "$VERSION" ]]; then - curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || { - msg_warn "Failed to download FFmpeg source ${VERSION}, falling back to pre-built binary" - VERSION="" - } - fi - - # If no source download (either VERSION empty or download failed), use binary - if [[ -z "$VERSION" ]]; then - msg_info "Setup FFmpeg from pre-built binary" - 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 pre-built binary" - rm -rf "$TMP_DIR" - return 1 - } - - tar -xJf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" || { - msg_error "Failed to extract FFmpeg binary archive" - rm -rf "$TMP_DIR" - return 1 - } - - if ! cp "$TMP_DIR/ffmpeg-"*/ffmpeg /usr/local/bin/ffmpeg 2>/dev/null; then - msg_error "Failed to install FFmpeg binary" - rm -rf "$TMP_DIR" - return 1 - fi - - cache_installed_version "ffmpeg" "static" - rm -rf "$TMP_DIR" - msg_ok "Setup FFmpeg from pre-built binary" - return 0 - fi - - tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || { - msg_error "Failed to extract FFmpeg source" - rm -rf "$TMP_DIR" - return 1 - } - - cd "$TMP_DIR/FFmpeg-"* || { - msg_error "Source extraction failed" - rm -rf "$TMP_DIR" - return 1 - } - - local args=( - --enable-gpl - --enable-shared - --enable-nonfree - --disable-static - --enable-libx264 - --enable-libvpx - --enable-libmp3lame - ) - - if [[ "$TYPE" != "minimal" ]]; then - args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis) - fi - - if [[ "$TYPE" == "full" ]]; then - args+=(--enable-libx265 --enable-libdav1d --enable-zlib) - args+=(--enable-vaapi --enable-libdrm) - fi - - if [[ ${#args[@]} -eq 0 ]]; then - msg_error "FFmpeg configure args array is empty" - rm -rf "$TMP_DIR" - return 1 - fi - - $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 - } - echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf - $STD ldconfig - - ldconfig -p 2>/dev/null | grep libavdevice >/dev/null || { - msg_error "libavdevice not registered with dynamic linker" - rm -rf "$TMP_DIR" - return 1 - } - - if ! command -v ffmpeg &>/dev/null; then - msg_error "FFmpeg installation failed" - rm -rf "$TMP_DIR" - return 1 - fi - - local FINAL_VERSION - FINAL_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}') - rm -rf "$TMP_DIR" - cache_installed_version "ffmpeg" "$FINAL_VERSION" - ensure_usr_local_bin_persist - [[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION" } # ------------------------------------------------------------------------------ @@ -2095,75 +2095,75 @@ function setup_ffmpeg() { # ------------------------------------------------------------------------------ function setup_go() { - local ARCH - case "$(uname -m)" in - x86_64) ARCH="amd64" ;; - aarch64) ARCH="arm64" ;; - *) - msg_error "Unsupported architecture: $(uname -m)" - return 1 - ;; - esac + local ARCH + case "$(uname -m)" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) + msg_error "Unsupported architecture: $(uname -m)" + return 1 + ;; + esac - # Resolve "latest" version - local GO_VERSION="${GO_VERSION:-latest}" - if [[ "$GO_VERSION" == "latest" ]]; then - GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text 2>/dev/null | head -n1 | sed 's/^go//') || { - msg_error "Could not determine latest Go version" - return 1 + # Resolve "latest" version + local GO_VERSION="${GO_VERSION:-latest}" + if [[ "$GO_VERSION" == "latest" ]]; then + GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text 2>/dev/null | head -n1 | sed 's/^go//') || { + msg_error "Could not determine latest Go version" + return 1 + } + [[ -z "$GO_VERSION" ]] && { + msg_error "Latest Go version is empty" + return 1 + } + fi + + local GO_BIN="/usr/local/bin/go" + local GO_INSTALL_DIR="/usr/local/go" + + # Get currently installed version + local CURRENT_VERSION="" + if [[ -x "$GO_BIN" ]]; then + CURRENT_VERSION=$("$GO_BIN" version 2>/dev/null | awk '{print $3}' | sed 's/go//') + fi + + # Scenario 1: Already at target version + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$GO_VERSION" ]]; then + cache_installed_version "go" "$GO_VERSION" + return 0 + fi + + # Scenario 2: Different version or not installed + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$GO_VERSION" ]]; then + msg_info "Upgrade Go from $CURRENT_VERSION to $GO_VERSION" + remove_old_tool_version "go" + else + msg_info "Setup Go $GO_VERSION" + fi + + 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 Go $GO_VERSION" + rm -f "$TMP_TAR" + return 1 } - [[ -z "$GO_VERSION" ]] && { - msg_error "Latest Go version is empty" - return 1 + + $STD tar -C /usr/local -xzf "$TMP_TAR" || { + msg_error "Failed to extract Go tarball" + rm -f "$TMP_TAR" + return 1 } - fi - local GO_BIN="/usr/local/bin/go" - local GO_INSTALL_DIR="/usr/local/go" + 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" - # Get currently installed version - local CURRENT_VERSION="" - if [[ -x "$GO_BIN" ]]; then - CURRENT_VERSION=$("$GO_BIN" version 2>/dev/null | awk '{print $3}' | sed 's/go//') - fi - - # Scenario 1: Already at target version - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$GO_VERSION" ]]; then cache_installed_version "go" "$GO_VERSION" - return 0 - fi - - # Scenario 2: Different version or not installed - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$GO_VERSION" ]]; then - msg_info "Upgrade Go from $CURRENT_VERSION to $GO_VERSION" - remove_old_tool_version "go" - else - msg_info "Setup Go $GO_VERSION" - fi - - 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 Go $GO_VERSION" - rm -f "$TMP_TAR" - return 1 - } - - $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" + ensure_usr_local_bin_persist + msg_ok "Setup Go $GO_VERSION" } # ------------------------------------------------------------------------------ @@ -2175,110 +2175,110 @@ function setup_go() { # ------------------------------------------------------------------------------ function setup_gs() { - local TMP_DIR=$(mktemp -d) - local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") + local TMP_DIR=$(mktemp -d) + local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") - ensure_dependencies jq + ensure_dependencies jq - local RELEASE_JSON - RELEASE_JSON=$(curl -fsSL --max-time 15 https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest 2>/dev/null || echo "") + local RELEASE_JSON + RELEASE_JSON=$(curl -fsSL --max-time 15 https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest 2>/dev/null || echo "") - if [[ -z "$RELEASE_JSON" ]]; then - msg_warn "Cannot fetch latest Ghostscript version from GitHub API" - # Try to get from current version - if command -v gs &>/dev/null; then - gs --version | head -n1 - cache_installed_version "ghostscript" "$CURRENT_VERSION" - return 0 + if [[ -z "$RELEASE_JSON" ]]; then + msg_warn "Cannot fetch latest Ghostscript version from GitHub API" + # Try to get from current version + if command -v gs &>/dev/null; then + gs --version | head -n1 + cache_installed_version "ghostscript" "$CURRENT_VERSION" + return 0 + fi + msg_error "Cannot determine Ghostscript version and no existing installation found" + return 1 fi - msg_error "Cannot determine Ghostscript version and no existing installation found" - return 1 - fi - 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]\+') + 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" || -z "$LATEST_VERSION_DOTTED" ]]; then - msg_warn "Could not determine latest Ghostscript version from GitHub - checking system" - # Fallback: try to use system version or return error - if [[ "$CURRENT_VERSION" == "0" ]]; then - msg_error "Ghostscript not installed and cannot determine latest version" - rm -rf "$TMP_DIR" - return 1 + if [[ -z "$LATEST_VERSION" || -z "$LATEST_VERSION_DOTTED" ]]; then + msg_warn "Could not determine latest Ghostscript version from GitHub - checking system" + # Fallback: try to use system version or return error + if [[ "$CURRENT_VERSION" == "0" ]]; then + msg_error "Ghostscript not installed and cannot determine latest version" + rm -rf "$TMP_DIR" + return 1 + fi + rm -rf "$TMP_DIR" + return 0 fi + + # Scenario 1: Already at latest version + if [[ -n "$LATEST_VERSION_DOTTED" ]] && dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then + cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" + rm -rf "$TMP_DIR" + return 0 + fi + + # Scenario 2: New install or upgrade + if [[ "$CURRENT_VERSION" != "0" && "$CURRENT_VERSION" != "$LATEST_VERSION_DOTTED" ]]; then + msg_info "Upgrade Ghostscript from $CURRENT_VERSION to $LATEST_VERSION_DOTTED" + else + msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED" + fi + + 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" + rm -rf "$TMP_DIR" + return 1 + fi + + # Verify directory exists before cd + if [[ ! -d "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" ]]; then + msg_error "Ghostscript source directory not found: $TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" + rm -rf "$TMP_DIR" + return 1 + fi + + cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { + msg_error "Failed to enter Ghostscript source directory" + rm -rf "$TMP_DIR" + return 1 + } + + 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 + ln -sf /usr/local/bin/gs /usr/bin/gs + fi + fi + rm -rf "$TMP_DIR" - return 0 - fi - - # Scenario 1: Already at latest version - if [[ -n "$LATEST_VERSION_DOTTED" ]] && dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" - rm -rf "$TMP_DIR" - return 0 - fi - - # Scenario 2: New install or upgrade - if [[ "$CURRENT_VERSION" != "0" && "$CURRENT_VERSION" != "$LATEST_VERSION_DOTTED" ]]; then - msg_info "Upgrade Ghostscript from $CURRENT_VERSION to $LATEST_VERSION_DOTTED" - else - msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED" - fi - - 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" - rm -rf "$TMP_DIR" - return 1 - fi - - # Verify directory exists before cd - if [[ ! -d "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" ]]; then - msg_error "Ghostscript source directory not found: $TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" - rm -rf "$TMP_DIR" - return 1 - fi - - cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { - msg_error "Failed to enter Ghostscript source directory" - rm -rf "$TMP_DIR" - return 1 - } - - 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 - ln -sf /usr/local/bin/gs /usr/bin/gs - fi - fi - - rm -rf "$TMP_DIR" - cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" - ensure_usr_local_bin_persist - msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" + ensure_usr_local_bin_persist + msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" } # ------------------------------------------------------------------------------ @@ -2293,111 +2293,111 @@ function setup_gs() { # - Some things are fetched from intel repositories due to not being in debian repositories. # ------------------------------------------------------------------------------ function setup_hwaccel() { - msg_info "Setup Hardware Acceleration" + 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 + 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 2>/dev/null | grep -Ei 'vga|3d|display' | grep -Eo 'Intel|AMD|NVIDIA' | head -n1 || echo "") + # Detect GPU vendor (Intel, AMD, NVIDIA) + local gpu_vendor + gpu_vendor=$(lspci 2>/dev/null | grep -Ei 'vga|3d|display' | grep -Eo 'Intel|AMD|NVIDIA' | head -n1 || echo "") - # Detect CPU vendor (relevant for AMD APUs) - local cpu_vendor - cpu_vendor=$(lscpu 2>/dev/null | grep -i 'Vendor ID' | awk '{print $3}' || echo "") + # Detect CPU vendor (relevant for AMD APUs) + local cpu_vendor + cpu_vendor=$(lscpu 2>/dev/null | grep -i 'Vendor ID' | awk '{print $3}' || echo "") - 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 with fallbacks - local os_id os_codename - os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "debian") - os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^VERSION_CODENAME=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "unknown") - - # Validate os_id - if [[ -z "$os_id" ]]; then - os_id="debian" - fi - - # 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" + if [[ -z "$gpu_vendor" && -z "$cpu_vendor" ]]; then + msg_error "No GPU or CPU vendor detected (missing lspci/lscpu output)" return 1 - } - else - # For Debian: fetch Intel GPU drivers from GitHub - fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || { - msg_warn "Failed to deploy Intel IGC core 2" - } - fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || { - msg_warn "Failed to deploy Intel IGC OpenCL 2" - } - fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || { - msg_warn "Failed to deploy Intel GDGMM12" - } - fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || { - msg_warn "Failed to deploy Intel OpenCL ICD" - } fi - $STD apt -y install va-driver-all ocl-icd-libopencl1 vainfo intel-gpu-tools || { - msg_error "Failed to install Intel GPU dependencies" - return 1 - } - ;; - AMD) - $STD apt -y install mesa-va-drivers mesa-vdpau-drivers mesa-opencl-icd vainfo clinfo || { - msg_error "Failed to install AMD GPU dependencies" - return 1 - } + # Detect OS with fallbacks + local os_id os_codename + os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "debian") + os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^VERSION_CODENAME=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "unknown") - # For AMD CPUs without discrete GPU (APUs) - if [[ "$cpu_vendor" == "AuthenticAMD" && -n "$gpu_vendor" ]]; then - $STD apt -y install libdrm-amdgpu1 firmware-amd-graphics || true + # Validate os_id + if [[ -z "$os_id" ]]; then + os_id="debian" fi - ;; - NVIDIA) - # NVIDIA needs manual driver setup - skip for now - msg_info "NVIDIA GPU detected - manual driver setup required" - ;; - *) - # 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" - return 1 - } - else - msg_warn "No supported GPU vendor detected - skipping GPU acceleration" + + # 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" + return 1 + } + else + # For Debian: fetch Intel GPU drivers from GitHub + fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || { + msg_warn "Failed to deploy Intel IGC core 2" + } + fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || { + msg_warn "Failed to deploy Intel IGC OpenCL 2" + } + fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || { + msg_warn "Failed to deploy Intel GDGMM12" + } + fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || { + msg_warn "Failed to deploy Intel OpenCL ICD" + } + fi + + $STD apt -y install va-driver-all ocl-icd-libopencl1 vainfo intel-gpu-tools || { + msg_error "Failed to install Intel GPU dependencies" + return 1 + } + ;; + AMD) + $STD apt -y install mesa-va-drivers mesa-vdpau-drivers mesa-opencl-icd vainfo clinfo || { + msg_error "Failed to install AMD GPU dependencies" + return 1 + } + + # For AMD CPUs without discrete GPU (APUs) + if [[ "$cpu_vendor" == "AuthenticAMD" && -n "$gpu_vendor" ]]; then + $STD apt -y install libdrm-amdgpu1 firmware-amd-graphics || true + fi + ;; + NVIDIA) + # NVIDIA needs manual driver setup - skip for now + msg_info "NVIDIA GPU detected - manual driver setup required" + ;; + *) + # 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" + return 1 + } + else + msg_warn "No supported GPU vendor detected - skipping GPU acceleration" + 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 - ;; - 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 - - cache_installed_version "hwaccel" "1.0" - msg_ok "Setup Hardware Acceleration" + cache_installed_version "hwaccel" "1.0" + msg_ok "Setup Hardware Acceleration" } # ------------------------------------------------------------------------------ @@ -2412,89 +2412,89 @@ function setup_hwaccel() { # - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc. # ------------------------------------------------------------------------------ function setup_imagemagick() { - local TMP_DIR=$(mktemp -d) - local BINARY_PATH="/usr/local/bin/magick" + local TMP_DIR=$(mktemp -d) + local BINARY_PATH="/usr/local/bin/magick" - # Get currently installed version - local INSTALLED_VERSION="" - if command -v magick &>/dev/null; then - INSTALLED_VERSION=$(magick -version | awk '/^Version/ {print $3}') - fi + # Get currently installed version + local INSTALLED_VERSION="" + if command -v magick &>/dev/null; then + INSTALLED_VERSION=$(magick -version | awk '/^Version/ {print $3}') + fi - msg_info "Setup ImageMagick" + msg_info "Setup ImageMagick" - ensure_dependencies \ - build-essential \ - libtool \ - libjpeg-dev \ - libpng-dev \ - libtiff-dev \ - libwebp-dev \ - libheif-dev \ - libde265-dev \ - libopenjp2-7-dev \ - libxml2-dev \ - liblcms2-dev \ - libfreetype6-dev \ - libraw-dev \ - libfftw3-dev \ - liblqr-1-0-dev \ - libgsl-dev \ - pkg-config \ - ghostscript + ensure_dependencies \ + build-essential \ + libtool \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libwebp-dev \ + libheif-dev \ + libde265-dev \ + libopenjp2-7-dev \ + libxml2-dev \ + liblcms2-dev \ + libfreetype6-dev \ + libraw-dev \ + libfftw3-dev \ + liblqr-1-0-dev \ + libgsl-dev \ + pkg-config \ + ghostscript - curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" || { - msg_error "Failed to download ImageMagick" + 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 + } + + $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 + msg_error "ImageMagick installation failed" + rm -rf "$TMP_DIR" + return 1 + fi + + local FINAL_VERSION + FINAL_VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') rm -rf "$TMP_DIR" - return 1 - } + cache_installed_version "imagemagick" "$FINAL_VERSION" + ensure_usr_local_bin_persist - 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 - } - - $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 - msg_error "ImageMagick installation failed" - rm -rf "$TMP_DIR" - return 1 - fi - - local FINAL_VERSION - FINAL_VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') - rm -rf "$TMP_DIR" - cache_installed_version "imagemagick" "$FINAL_VERSION" - ensure_usr_local_bin_persist - - if [[ -n "$INSTALLED_VERSION" ]]; then - msg_ok "Upgrade ImageMagick $INSTALLED_VERSION → $FINAL_VERSION" - else - msg_ok "Setup ImageMagick $FINAL_VERSION" - fi + if [[ -n "$INSTALLED_VERSION" ]]; then + msg_ok "Upgrade ImageMagick $INSTALLED_VERSION → $FINAL_VERSION" + else + msg_ok "Setup ImageMagick $FINAL_VERSION" + fi } # ------------------------------------------------------------------------------ @@ -2509,74 +2509,74 @@ function setup_imagemagick() { # ------------------------------------------------------------------------------ function setup_java() { - local JAVA_VERSION="${JAVA_VERSION:-21}" - 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" + local JAVA_VERSION="${JAVA_VERSION:-21}" + 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 repo if needed - if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then - cleanup_old_repo_files "adoptium" - local SUITE - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb") - setup_deb822_repo \ - "adoptium" \ - "https://packages.adoptium.net/artifactory/api/gpg/key/public" \ - "https://packages.adoptium.net/artifactory/deb" \ - "$SUITE" \ - "main" \ - "amd64 arm64" - fi + # Add repo if needed + if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then + cleanup_old_repo_files "adoptium" + local SUITE + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb") + setup_deb822_repo \ + "adoptium" \ + "https://packages.adoptium.net/artifactory/api/gpg/key/public" \ + "https://packages.adoptium.net/artifactory/deb" \ + "$SUITE" \ + "main" \ + "amd64 arm64" + fi - # Get currently installed version - local INSTALLED_VERSION="" - if dpkg -l | grep -q "temurin-.*-jdk" 2>/dev/null; then - INSTALLED_VERSION=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+' | head -n1 || echo "") - fi + # Get currently installed version + local INSTALLED_VERSION="" + if dpkg -l | grep -q "temurin-.*-jdk" 2>/dev/null; then + INSTALLED_VERSION=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+' | head -n1 || echo "") + fi - # Validate INSTALLED_VERSION is not empty if matched - local JDK_COUNT=$(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || echo "0") - if [[ -z "$INSTALLED_VERSION" && "$JDK_COUNT" -gt 0 ]]; then - msg_warn "Found Temurin JDK but cannot determine version" - INSTALLED_VERSION="0" - fi + # Validate INSTALLED_VERSION is not empty if matched + local JDK_COUNT=$(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || echo "0") + if [[ -z "$INSTALLED_VERSION" && "$JDK_COUNT" -gt 0 ]]; then + msg_warn "Found Temurin JDK but cannot determine version" + INSTALLED_VERSION="0" + fi + + # Scenario 1: Already at correct version + if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then + msg_info "Update Temurin JDK $JAVA_VERSION" + $STD apt update || { + msg_error "APT update failed" + return 1 + } + $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" || { + msg_error "Failed to update Temurin JDK" + return 1 + } + cache_installed_version "temurin-jdk" "$JAVA_VERSION" + msg_ok "Update Temurin JDK $JAVA_VERSION" + return 0 + fi + + # Scenario 2: Different version - remove old and install new + if [[ -n "$INSTALLED_VERSION" ]]; then + msg_info "Upgrade Temurin JDK from $INSTALLED_VERSION to $JAVA_VERSION" + $STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk" || true + else + msg_info "Setup Temurin JDK $JAVA_VERSION" + fi - # Scenario 1: Already at correct version - if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then - msg_info "Update Temurin JDK $JAVA_VERSION" $STD apt update || { - msg_error "APT update failed" - return 1 + msg_error "APT update failed" + return 1 } - $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" || { - msg_error "Failed to update Temurin JDK" - return 1 + $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 "Update Temurin JDK $JAVA_VERSION" - return 0 - fi - - # Scenario 2: Different version - remove old and install new - if [[ -n "$INSTALLED_VERSION" ]]; then - msg_info "Upgrade Temurin JDK from $INSTALLED_VERSION to $JAVA_VERSION" - $STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk" || true - else - msg_info "Setup Temurin JDK $JAVA_VERSION" - fi - - $STD apt update || { - msg_error "APT update failed" - return 1 - } - $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" + msg_ok "Setup Temurin JDK $JAVA_VERSION" } # ------------------------------------------------------------------------------ @@ -2588,36 +2588,36 @@ function setup_java() { # ------------------------------------------------------------------------------ function setup_local_ip_helper() { - local BASE_DIR="/usr/local/community-scripts/ip-management" - local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh" - local IP_FILE="/run/local-ip.env" - local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh" + local BASE_DIR="/usr/local/community-scripts/ip-management" + local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh" + local IP_FILE="/run/local-ip.env" + local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh" - # Check if already set up - if [[ -f "$SCRIPT_PATH" && -f "$DISPATCHER_SCRIPT" ]]; then - msg_info "Update Local IP Helper" - cache_installed_version "local-ip-helper" "1.0" - msg_ok "Update Local IP Helper" - else - msg_info "Setup Local IP Helper" - fi + # Check if already set up + if [[ -f "$SCRIPT_PATH" && -f "$DISPATCHER_SCRIPT" ]]; then + msg_info "Update Local IP Helper" + cache_installed_version "local-ip-helper" "1.0" + msg_ok "Update Local IP Helper" + else + msg_info "Setup Local IP Helper" + fi - mkdir -p "$BASE_DIR" + mkdir -p "$BASE_DIR" - # Install networkd-dispatcher if not present - if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then - $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 + # Install networkd-dispatcher if not present + if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then + $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 - cat <<'EOF' >"$SCRIPT_PATH" + # Write update_local_ip.sh + cat <<'EOF' >"$SCRIPT_PATH" #!/bin/bash set -euo pipefail @@ -2659,22 +2659,22 @@ echo "LOCAL_IP=$current_ip" > "$IP_FILE" echo "[INFO] LOCAL_IP updated to $current_ip" EOF - chmod +x "$SCRIPT_PATH" + chmod +x "$SCRIPT_PATH" - # Install dispatcher hook - mkdir -p "$(dirname "$DISPATCHER_SCRIPT")" - cat <"$DISPATCHER_SCRIPT" + # Install dispatcher hook + mkdir -p "$(dirname "$DISPATCHER_SCRIPT")" + cat <"$DISPATCHER_SCRIPT" #!/bin/bash $SCRIPT_PATH EOF - chmod +x "$DISPATCHER_SCRIPT" - systemctl enable -q --now networkd-dispatcher.service || { - msg_warn "Failed to enable networkd-dispatcher service" - } + chmod +x "$DISPATCHER_SCRIPT" + systemctl enable -q --now networkd-dispatcher.service || { + msg_warn "Failed to enable networkd-dispatcher service" + } - cache_installed_version "local-ip-helper" "1.0" - msg_ok "Setup Local IP Helper" + cache_installed_version "local-ip-helper" "1.0" + msg_ok "Setup Local IP Helper" } # ------------------------------------------------------------------------------ @@ -2690,122 +2690,122 @@ EOF # ------------------------------------------------------------------------------ setup_mariadb() { - local MARIADB_VERSION="${MARIADB_VERSION:-latest}" + local MARIADB_VERSION="${MARIADB_VERSION:-latest}" - # Resolve "latest" to actual version - if [[ "$MARIADB_VERSION" == "latest" ]]; then - if ! curl -fsI --max-time 10 http://mirror.mariadb.org/repo/ >/dev/null 2>&1; then - msg_warn "MariaDB mirror not reachable - trying cached package list fallback" - # Fallback: try to use a known stable version - MARIADB_VERSION="12.0" - else - MARIADB_VERSION=$(curl -fsSL --max-time 15 http://mirror.mariadb.org/repo/ 2>/dev/null | - grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | - grep -vE 'rc/|rolling/' | - sed 's|/||' | - sort -Vr | - head -n1 || echo "") + # Resolve "latest" to actual version + if [[ "$MARIADB_VERSION" == "latest" ]]; then + if ! curl -fsI --max-time 10 http://mirror.mariadb.org/repo/ >/dev/null 2>&1; then + msg_warn "MariaDB mirror not reachable - trying cached package list fallback" + # Fallback: try to use a known stable version + MARIADB_VERSION="12.0" + else + MARIADB_VERSION=$(curl -fsSL --max-time 15 http://mirror.mariadb.org/repo/ 2>/dev/null | + grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | + grep -vE 'rc/|rolling/' | + sed 's|/||' | + sort -Vr | + head -n1 || echo "") - if [[ -z "$MARIADB_VERSION" ]]; then - msg_warn "Could not parse latest GA MariaDB version from mirror - using fallback" - MARIADB_VERSION="12.0" - fi + if [[ -z "$MARIADB_VERSION" ]]; then + msg_warn "Could not parse latest GA MariaDB version from mirror - using fallback" + MARIADB_VERSION="12.0" + fi + fi fi - fi - # Get currently installed version - local CURRENT_VERSION="" - CURRENT_VERSION=$(is_tool_installed "mariadb" 2>/dev/null) || true + # Get currently installed version + local CURRENT_VERSION="" + CURRENT_VERSION=$(is_tool_installed "mariadb" 2>/dev/null) || true - # Scenario 1: Already installed at target version - just update packages - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then - msg_info "Update MariaDB $MARIADB_VERSION" + # Scenario 1: Already installed at target version - just update packages + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then + msg_info "Update MariaDB $MARIADB_VERSION" - # Ensure APT is working + # Ensure APT is working + ensure_apt_working || return 1 + + # Check if repository needs to be refreshed + if [[ -f /etc/apt/sources.list.d/mariadb.sources ]]; then + local REPO_VERSION="" + REPO_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.sources 2>/dev/null || echo "") + if [[ -n "$REPO_VERSION" && "$REPO_VERSION" != "${MARIADB_VERSION%.*}" ]]; then + msg_warn "Repository version mismatch, updating..." + manage_tool_repository "mariadb" "$MARIADB_VERSION" "http://mirror.mariadb.org/repo/$MARIADB_VERSION" \ + "https://mariadb.org/mariadb_release_signing_key.asc" || { + msg_error "Failed to update MariaDB repository" + return 1 + } + fi + fi + + # Perform upgrade + $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 packages" + return 1 + } + cache_installed_version "mariadb" "$MARIADB_VERSION" + msg_ok "Update MariaDB $MARIADB_VERSION" + return 0 + fi + + # Scenario 2: Different version installed - clean upgrade + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$MARIADB_VERSION" ]]; then + msg_info "Upgrade MariaDB from $CURRENT_VERSION to $MARIADB_VERSION" + remove_old_tool_version "mariadb" + fi + + # Scenario 3: Fresh install or version change + msg_info "Setup MariaDB $MARIADB_VERSION" + + # Ensure APT is working before proceeding ensure_apt_working || return 1 - # Check if repository needs to be refreshed - if [[ -f /etc/apt/sources.list.d/mariadb.sources ]]; then - local REPO_VERSION="" - REPO_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.sources 2>/dev/null || echo "") - if [[ -n "$REPO_VERSION" && "$REPO_VERSION" != "${MARIADB_VERSION%.*}" ]]; then - msg_warn "Repository version mismatch, updating..." - manage_tool_repository "mariadb" "$MARIADB_VERSION" "http://mirror.mariadb.org/repo/$MARIADB_VERSION" \ - "https://mariadb.org/mariadb_release_signing_key.asc" || { - msg_error "Failed to update MariaDB repository" - return 1 - } - fi + # Install required dependencies first + 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 - # Perform upgrade - $STD apt update || { - msg_error "Failed to update package list" - return 1 + # Setup repository + manage_tool_repository "mariadb" "$MARIADB_VERSION" "http://mirror.mariadb.org/repo/$MARIADB_VERSION" \ + "https://mariadb.org/mariadb_release_signing_key.asc" || { + msg_error "Failed to setup MariaDB repository" + return 1 } - $STD apt install --only-upgrade -y mariadb-server mariadb-client || { - msg_error "Failed to upgrade MariaDB packages" - return 1 - } - cache_installed_version "mariadb" "$MARIADB_VERSION" - msg_ok "Update MariaDB $MARIADB_VERSION" - return 0 - fi - # Scenario 2: Different version installed - clean upgrade - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$MARIADB_VERSION" ]]; then - msg_info "Upgrade MariaDB from $CURRENT_VERSION to $MARIADB_VERSION" - remove_old_tool_version "mariadb" - fi - - # Scenario 3: Fresh install or version change - msg_info "Setup MariaDB $MARIADB_VERSION" - - # Ensure APT is working before proceeding - ensure_apt_working || return 1 - - # Install required dependencies first - 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") + # Set debconf selections for all potential versions + 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 fi - done - if [[ ${#mariadb_deps[@]} -gt 0 ]]; then - $STD apt install -y "${mariadb_deps[@]}" 2>/dev/null || true - fi - - # Setup repository - manage_tool_repository "mariadb" "$MARIADB_VERSION" "http://mirror.mariadb.org/repo/$MARIADB_VERSION" \ - "https://mariadb.org/mariadb_release_signing_key.asc" || { - msg_error "Failed to setup MariaDB repository" - return 1 - } - - # Set debconf selections for all potential versions - 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 - fi - - # Install packages - DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { - # Fallback: try without specific version - msg_warn "Failed to install MariaDB packages from upstream repo, trying distro fallback..." - cleanup_old_repo_files "mariadb" - $STD apt update || { - msg_warn "APT update also failed, continuing with cache" - } + # Install packages DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { - msg_error "Failed to install MariaDB packages (both upstream and distro)" - return 1 + # Fallback: try without specific version + msg_warn "Failed to install MariaDB packages from upstream repo, trying distro fallback..." + cleanup_old_repo_files "mariadb" + $STD apt update || { + msg_warn "APT update also failed, continuing with cache" + } + DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { + msg_error "Failed to install MariaDB packages (both upstream and distro)" + return 1 + } } - } - cache_installed_version "mariadb" "$MARIADB_VERSION" - msg_ok "Setup MariaDB $MARIADB_VERSION" + cache_installed_version "mariadb" "$MARIADB_VERSION" + msg_ok "Setup MariaDB $MARIADB_VERSION" } # ------------------------------------------------------------------------------ @@ -2820,92 +2820,92 @@ setup_mariadb() { # ------------------------------------------------------------------------------ function setup_mongodb() { - local MONGO_VERSION="${MONGO_VERSION:-8.0}" - local DISTRO_ID DISTRO_CODENAME - DISTRO_ID=$(get_os_info id) - DISTRO_CODENAME=$(get_os_info codename) + local MONGO_VERSION="${MONGO_VERSION:-8.0}" + local DISTRO_ID DISTRO_CODENAME + DISTRO_ID=$(get_os_info id) + DISTRO_CODENAME=$(get_os_info codename) - # Check AVX support - if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then - local major="${MONGO_VERSION%%.*}" - if ((major > 5)); then - msg_error "MongoDB ${MONGO_VERSION} requires AVX support, which is not available on this system." - return 1 + # Check AVX support + if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then + local major="${MONGO_VERSION%%.*}" + if ((major > 5)); then + msg_error "MongoDB ${MONGO_VERSION} requires AVX support, which is not available on this system." + return 1 + fi fi - fi - case "$DISTRO_ID" in - ubuntu) - MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu" - ;; - debian) - MONGO_BASE_URL="https://repo.mongodb.org/apt/debian" - ;; - *) - msg_error "Unsupported distribution: $DISTRO_ID" - return 1 - ;; - esac + case "$DISTRO_ID" in + ubuntu) + MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu" + ;; + debian) + MONGO_BASE_URL="https://repo.mongodb.org/apt/debian" + ;; + *) + msg_error "Unsupported distribution: $DISTRO_ID" + return 1 + ;; + esac - # Get currently installed version - local INSTALLED_VERSION="" - INSTALLED_VERSION=$(is_tool_installed "mongodb" 2>/dev/null) || true + # Get currently installed version + local INSTALLED_VERSION="" + INSTALLED_VERSION=$(is_tool_installed "mongodb" 2>/dev/null) || true - # Scenario 1: Already at target version - just update packages - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then - msg_info "Update MongoDB $MONGO_VERSION" + # Scenario 1: Already at target version - just update packages + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then + msg_info "Update MongoDB $MONGO_VERSION" - ensure_apt_working || return 1 + ensure_apt_working || return 1 - # Perform upgrade - $STD apt install --only-upgrade -y mongodb-org || { - msg_error "Failed to upgrade MongoDB" - return 1 + # Perform upgrade + $STD apt install --only-upgrade -y mongodb-org || { + msg_error "Failed to upgrade MongoDB" + return 1 + } + cache_installed_version "mongodb" "$MONGO_VERSION" + msg_ok "Update MongoDB $MONGO_VERSION" + return 0 + fi + + # Scenario 2: Different version installed - clean upgrade + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$MONGO_VERSION" ]]; then + msg_info "Upgrade MongoDB from $INSTALLED_VERSION to $MONGO_VERSION" + remove_old_tool_version "mongodb" + else + msg_info "Setup MongoDB $MONGO_VERSION" + fi + + cleanup_orphaned_sources + + # Setup repository + manage_tool_repository "mongodb" "$MONGO_VERSION" "$MONGO_BASE_URL" \ + "https://www.mongodb.org/static/pgp/server-${MONGO_VERSION}.asc" || { + msg_error "Failed to setup MongoDB repository" + return 1 } + + # Wait for repo to settle + $STD apt update || { + msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" + return 1 + } + + # Install MongoDB + $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 || { + msg_warn "Failed to enable mongod service" + } + safe_service_restart mongod cache_installed_version "mongodb" "$MONGO_VERSION" - msg_ok "Update MongoDB $MONGO_VERSION" - return 0 - fi - # Scenario 2: Different version installed - clean upgrade - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$MONGO_VERSION" ]]; then - msg_info "Upgrade MongoDB from $INSTALLED_VERSION to $MONGO_VERSION" - remove_old_tool_version "mongodb" - else - msg_info "Setup MongoDB $MONGO_VERSION" - fi - - cleanup_orphaned_sources - - # Setup repository - manage_tool_repository "mongodb" "$MONGO_VERSION" "$MONGO_BASE_URL" \ - "https://www.mongodb.org/static/pgp/server-${MONGO_VERSION}.asc" || { - msg_error "Failed to setup MongoDB repository" - return 1 - } - - # Wait for repo to settle - $STD apt update || { - msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" - return 1 - } - - # Install MongoDB - $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 || { - msg_warn "Failed to enable mongod service" - } - safe_service_restart mongod - cache_installed_version "mongodb" "$MONGO_VERSION" - - msg_ok "Setup MongoDB $MONGO_VERSION" + msg_ok "Setup MongoDB $MONGO_VERSION" } # ------------------------------------------------------------------------------ @@ -2922,48 +2922,48 @@ function setup_mongodb() { # ------------------------------------------------------------------------------ function setup_mysql() { - local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" - 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 MYSQL_VERSION="${MYSQL_VERSION:-8.0}" + 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) - # Get currently installed version - local CURRENT_VERSION="" - CURRENT_VERSION=$(is_tool_installed "mysql" 2>/dev/null) || true + # Get currently installed version + local CURRENT_VERSION="" + CURRENT_VERSION=$(is_tool_installed "mysql" 2>/dev/null) || true - # Scenario 1: Already at target version - just update packages - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$MYSQL_VERSION" ]]; then - msg_info "Update MySQL $MYSQL_VERSION" + # Scenario 1: Already at target version - just update packages + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$MYSQL_VERSION" ]]; then + msg_info "Update MySQL $MYSQL_VERSION" - ensure_apt_working || return 1 + ensure_apt_working || return 1 - $STD apt install --only-upgrade -y mysql-server mysql-client || true + $STD apt install --only-upgrade -y mysql-server mysql-client || true - cache_installed_version "mysql" "$MYSQL_VERSION" - msg_ok "Update MySQL $MYSQL_VERSION" - return 0 - fi - - # Scenario 2: Different version installed - clean upgrade - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then - msg_info "Upgrade MySQL from $CURRENT_VERSION to $MYSQL_VERSION" - remove_old_tool_version "mysql" - else - msg_info "Setup MySQL $MYSQL_VERSION" - fi - - # Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64, use 8.4 LTS - if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then - msg_info "Debian ${DISTRO_CODENAME} detected → using MySQL 8.4 LTS (libaio1t64 compatible)" - - cleanup_old_repo_files "mysql" - - if ! curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/keyrings/mysql.gpg 2>/dev/null; then - msg_error "Failed to import MySQL GPG key" - return 1 + cache_installed_version "mysql" "$MYSQL_VERSION" + msg_ok "Update MySQL $MYSQL_VERSION" + return 0 fi - cat >/etc/apt/sources.list.d/mysql.sources <<'EOF' + # Scenario 2: Different version installed - clean upgrade + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then + msg_info "Upgrade MySQL from $CURRENT_VERSION to $MYSQL_VERSION" + remove_old_tool_version "mysql" + else + msg_info "Setup MySQL $MYSQL_VERSION" + fi + + # Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64, use 8.4 LTS + if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then + msg_info "Debian ${DISTRO_CODENAME} detected → using MySQL 8.4 LTS (libaio1t64 compatible)" + + cleanup_old_repo_files "mysql" + + if ! curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/keyrings/mysql.gpg 2>/dev/null; then + msg_error "Failed to import MySQL GPG key" + return 1 + fi + + cat >/etc/apt/sources.list.d/mysql.sources <<'EOF' Types: deb URIs: https://repo.mysql.com/apt/debian/ Suites: bookworm @@ -2972,79 +2972,79 @@ Architectures: amd64 arm64 Signed-By: /etc/apt/keyrings/mysql.gpg EOF - $STD apt update || { - msg_error "Failed to update APT for MySQL 8.4 LTS" - return 1 + $STD apt update || { + msg_error "Failed to update APT for MySQL 8.4 LTS" + return 1 + } + + if ! $STD apt install -y mysql-community-server mysql-community-client; then + msg_warn "MySQL 8.4 LTS installation failed – falling back to MariaDB" + cleanup_old_repo_files "mysql" + $STD apt update + $STD apt install -y mariadb-server mariadb-client || { + msg_error "Failed to install database engine (MySQL/MariaDB fallback)" + return 1 + } + msg_ok "Setup Database Engine (MariaDB fallback on Debian ${DISTRO_CODENAME})" + return 0 + fi + + cache_installed_version "mysql" "8.4" + msg_ok "Setup MySQL 8.4 LTS (Debian ${DISTRO_CODENAME})" + return 0 + fi + + # Standard setup for other distributions + local SUITE + if [[ "$DISTRO_ID" == "debian" ]]; then + case "$DISTRO_CODENAME" in + bookworm | bullseye) SUITE="$DISTRO_CODENAME" ;; + *) SUITE="bookworm" ;; + esac + else + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}") + fi + + # Setup repository + manage_tool_repository "mysql" "$MYSQL_VERSION" "https://repo.mysql.com/apt/${DISTRO_ID}" \ + "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" || { + msg_error "Failed to setup MySQL repository" + return 1 } - if ! $STD apt install -y mysql-community-server mysql-community-client; then - msg_warn "MySQL 8.4 LTS installation failed – falling back to MariaDB" - cleanup_old_repo_files "mysql" - $STD apt update - $STD apt install -y mariadb-server mariadb-client || { - msg_error "Failed to install database engine (MySQL/MariaDB fallback)" + ensure_apt_working || return 1 + + # Try multiple package names (mysql-server, mysql-community-server, mysql) + export DEBIAN_FRONTEND=noninteractive + local mysql_install_success=false + + 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 + elif 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 + elif 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 - } - msg_ok "Setup Database Engine (MariaDB fallback on Debian ${DISTRO_CODENAME})" - return 0 fi - cache_installed_version "mysql" "8.4" - msg_ok "Setup MySQL 8.4 LTS (Debian ${DISTRO_CODENAME})" - return 0 - fi - - # Standard setup for other distributions - local SUITE - if [[ "$DISTRO_ID" == "debian" ]]; then - case "$DISTRO_CODENAME" in - bookworm | bullseye) SUITE="$DISTRO_CODENAME" ;; - *) SUITE="bookworm" ;; - esac - else - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}") - fi - - # Setup repository - manage_tool_repository "mysql" "$MYSQL_VERSION" "https://repo.mysql.com/apt/${DISTRO_ID}" \ - "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" || { - msg_error "Failed to setup MySQL repository" - return 1 - } - - ensure_apt_working || return 1 - - # Try multiple package names (mysql-server, mysql-community-server, mysql) - export DEBIAN_FRONTEND=noninteractive - local mysql_install_success=false - - 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 - elif 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 - elif 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 mysql command is accessible - if ! command -v mysql >/dev/null 2>&1; then - hash -r + # Verify mysql command is accessible if ! command -v mysql >/dev/null 2>&1; then - msg_error "MySQL installed but mysql command still not found" - return 1 + 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 - fi - cache_installed_version "mysql" "$MYSQL_VERSION" - msg_ok "Setup MySQL $MYSQL_VERSION" + cache_installed_version "mysql" "$MYSQL_VERSION" + msg_ok "Setup MySQL $MYSQL_VERSION" } # ------------------------------------------------------------------------------ @@ -3060,142 +3060,142 @@ EOF # ------------------------------------------------------------------------------ function setup_nodejs() { - local NODE_VERSION="${NODE_VERSION:-22}" - local NODE_MODULE="${NODE_MODULE:-}" + local NODE_VERSION="${NODE_VERSION:-22}" + local NODE_MODULE="${NODE_MODULE:-}" - # Get currently installed version - local CURRENT_NODE_VERSION="" - CURRENT_NODE_VERSION=$(is_tool_installed "nodejs" 2>/dev/null) || true + # Get currently installed version + local CURRENT_NODE_VERSION="" + CURRENT_NODE_VERSION=$(is_tool_installed "nodejs" 2>/dev/null) || true - # Ensure jq is available for JSON parsing - if ! command -v jq &>/dev/null; then - $STD apt update - $STD apt install -y jq || { - msg_error "Failed to install jq" - return 1 - } - fi - - # Scenario 1: Already installed at target version - just update packages/modules - if [[ -n "$CURRENT_NODE_VERSION" && "$CURRENT_NODE_VERSION" == "$NODE_VERSION" ]]; then - msg_info "Update Node.js $NODE_VERSION" - - ensure_apt_working || return 1 - - # Just update npm to latest - $STD npm install -g npm@latest 2>/dev/null || true - - cache_installed_version "nodejs" "$NODE_VERSION" - msg_ok "Update Node.js $NODE_VERSION" - else - # Scenario 2: Different version installed - clean upgrade - if [[ -n "$CURRENT_NODE_VERSION" && "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then - msg_info "Upgrade Node.js from $CURRENT_NODE_VERSION to $NODE_VERSION" - remove_old_tool_version "nodejs" - else - msg_info "Setup Node.js $NODE_VERSION" + # Ensure jq is available for JSON parsing + if ! command -v jq &>/dev/null; then + $STD apt update + $STD apt install -y jq || { + msg_error "Failed to install jq" + return 1 + } fi - ensure_dependencies curl ca-certificates gnupg + # Scenario 1: Already installed at target version - just update packages/modules + if [[ -n "$CURRENT_NODE_VERSION" && "$CURRENT_NODE_VERSION" == "$NODE_VERSION" ]]; then + msg_info "Update Node.js $NODE_VERSION" - # Setup repository - manage_tool_repository "nodejs" "$NODE_VERSION" "https://deb.nodesource.com/node_${NODE_VERSION}.x" "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" || { - msg_error "Failed to setup Node.js repository" - return 1 - } + ensure_apt_working || return 1 - # Wait for repo to settle - sleep 2 + # Just update npm to latest + $STD npm install -g npm@latest 2>/dev/null || true - # Install Node.js - if ! apt update >/dev/null 2>&1; then - msg_warn "APT update failed – retrying in 5s" - sleep 5 - if ! apt update >/dev/null 2>&1; then - msg_error "Failed to update APT repositories after adding NodeSource" + cache_installed_version "nodejs" "$NODE_VERSION" + msg_ok "Update Node.js $NODE_VERSION" + else + # Scenario 2: Different version installed - clean upgrade + if [[ -n "$CURRENT_NODE_VERSION" && "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then + msg_info "Upgrade Node.js from $CURRENT_NODE_VERSION to $NODE_VERSION" + remove_old_tool_version "nodejs" + else + msg_info "Setup Node.js $NODE_VERSION" + fi + + ensure_dependencies curl ca-certificates gnupg + + # Setup repository + manage_tool_repository "nodejs" "$NODE_VERSION" "https://deb.nodesource.com/node_${NODE_VERSION}.x" "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" || { + msg_error "Failed to setup Node.js repository" + return 1 + } + + # Wait for repo to settle + sleep 2 + + # Install Node.js + if ! $STD apt update; then + msg_warn "APT update failed – retrying in 5s" + sleep 5 + if ! $STD apt update; then + msg_error "Failed to update APT repositories after adding NodeSource" + return 1 + fi + fi + + if ! $STD apt install -y nodejs; then + msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" + return 1 + fi + + # Update to latest npm + $STD npm install -g npm@latest || { + msg_error "Failed to update npm to latest version" + return 1 + } + + 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" return 1 - fi - fi - - if ! apt install -y nodejs >/dev/null 2>&1; then - msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" - return 1 - fi - - # Update to latest npm - $STD npm install -g npm@latest || { - msg_error "Failed to update npm to latest version" - return 1 } - cache_installed_version "nodejs" "$NODE_VERSION" - msg_ok "Setup Node.js $NODE_VERSION" - fi + # Install global Node modules + if [[ -n "$NODE_MODULE" ]]; then + IFS=',' read -ra MODULES <<<"$NODE_MODULE" + local failed_modules=0 + 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 - 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" - return 1 - } - - # Install global Node modules - if [[ -n "$NODE_MODULE" ]]; then - IFS=',' read -ra MODULES <<<"$NODE_MODULE" - local failed_modules=0 - 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}" 2>/dev/null; then - msg_warn "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" - ((failed_modules++)) - continue - fi - elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then - msg_info "Updating $MODULE_NAME to latest version" - if ! $STD npm install -g "${MODULE_NAME}@latest" 2>/dev/null; then - msg_warn "Failed to update $MODULE_NAME to latest version" - ((failed_modules++)) - continue - 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}" 2>/dev/null; then + msg_warn "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" + ((failed_modules++)) + continue + fi + elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then + msg_info "Updating $MODULE_NAME to latest version" + if ! $STD npm install -g "${MODULE_NAME}@latest" 2>/dev/null; then + msg_warn "Failed to update $MODULE_NAME to latest version" + ((failed_modules++)) + continue + fi + fi + else + msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" + if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" 2>/dev/null; then + msg_warn "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" + ((failed_modules++)) + continue + fi + fi + done + if [[ $failed_modules -eq 0 ]]; then + msg_ok "Installed Node.js modules: $NODE_MODULE" + else + msg_warn "Installed Node.js modules with $failed_modules failure(s): $NODE_MODULE" fi - else - msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" - if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" 2>/dev/null; then - msg_warn "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" - ((failed_modules++)) - continue - fi - fi - done - if [[ $failed_modules -eq 0 ]]; then - msg_ok "Installed Node.js modules: $NODE_MODULE" - else - msg_warn "Installed Node.js modules with $failed_modules failure(s): $NODE_MODULE" fi - fi } # ------------------------------------------------------------------------------ @@ -3218,139 +3218,139 @@ function setup_nodejs() { # ------------------------------------------------------------------------------ function setup_php() { - local PHP_VERSION="${PHP_VERSION:-8.4}" - local PHP_MODULE="${PHP_MODULE:-}" - local PHP_APACHE="${PHP_APACHE:-NO}" - local PHP_FPM="${PHP_FPM:-NO}" - 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 PHP_VERSION="${PHP_VERSION:-8.4}" + local PHP_MODULE="${PHP_MODULE:-}" + local PHP_APACHE="${PHP_APACHE:-NO}" + local PHP_FPM="${PHP_FPM:-NO}" + 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 + local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip" + local COMBINED_MODULES - local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}" - local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}" - local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}" - local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}" + local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}" + local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}" + local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}" + local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}" - # Merge default + user-defined modules - if [[ -n "$PHP_MODULE" ]]; then - COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}" - else - COMBINED_MODULES="${DEFAULT_MODULES}" - fi - - # Deduplicate - COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -) - - # Get current PHP-CLI version - local CURRENT_PHP="" - CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true - - # Scenario 1: Already at target version - just update packages - if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" == "$PHP_VERSION" ]]; then - msg_info "Update PHP $PHP_VERSION" - - # Ensure Sury repo is available - if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then - manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || { - msg_error "Failed to setup PHP repository" - return 1 - } - fi - - ensure_apt_working || return 1 - - # Just update PHP packages - $STD apt install --only-upgrade -y "php${PHP_VERSION}" || true - - cache_installed_version "php" "$PHP_VERSION" - msg_ok "Update PHP $PHP_VERSION" - else - # Scenario 2: Different version installed - clean upgrade - if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then - msg_info "Upgrade PHP from $CURRENT_PHP to $PHP_VERSION" - # Stop old PHP-FPM if running - $STD systemctl stop "php${CURRENT_PHP}-fpm" >/dev/null 2>&1 || true - $STD systemctl disable "php${CURRENT_PHP}-fpm" >/dev/null 2>&1 || true - remove_old_tool_version "php" + # Merge default + user-defined modules + if [[ -n "$PHP_MODULE" ]]; then + COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}" else - msg_info "Setup PHP $PHP_VERSION" + COMBINED_MODULES="${DEFAULT_MODULES}" fi - # Setup Sury repository - manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || { - msg_error "Failed to setup PHP repository" - return 1 - } + # Deduplicate + COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -) - ensure_apt_working || return 1 - fi + # Get current PHP-CLI version + local CURRENT_PHP="" + CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true - # Build module list - local MODULE_LIST="php${PHP_VERSION}" - IFS=',' read -ra MODULES <<<"$COMBINED_MODULES" - for mod in "${MODULES[@]}"; do - if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then - MODULE_LIST+=" php${PHP_VERSION}-${mod}" + # Scenario 1: Already at target version - just update packages + if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" == "$PHP_VERSION" ]]; then + msg_info "Update PHP $PHP_VERSION" + + # Ensure Sury repo is available + if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then + manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || { + msg_error "Failed to setup PHP repository" + return 1 + } + fi + + ensure_apt_working || return 1 + + # Just update PHP packages + $STD apt install --only-upgrade -y "php${PHP_VERSION}" || true + + cache_installed_version "php" "$PHP_VERSION" + msg_ok "Update PHP $PHP_VERSION" + else + # Scenario 2: Different version installed - clean upgrade + if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then + msg_info "Upgrade PHP from $CURRENT_PHP to $PHP_VERSION" + # Stop old PHP-FPM if running + $STD systemctl stop "php${CURRENT_PHP}-fpm" >/dev/null 2>&1 || true + $STD systemctl disable "php${CURRENT_PHP}-fpm" >/dev/null 2>&1 || true + remove_old_tool_version "php" + else + msg_info "Setup PHP $PHP_VERSION" + fi + + # Setup Sury repository + manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || { + msg_error "Failed to setup PHP repository" + return 1 + } + + ensure_apt_working || return 1 fi - done - if [[ "$PHP_FPM" == "YES" ]]; then - MODULE_LIST+=" php${PHP_VERSION}-fpm" - fi - # install apache2 with PHP support if requested - if [[ "$PHP_APACHE" == "YES" ]]; then - if ! dpkg -l 2>/dev/null | grep -q "libapache2-mod-php${PHP_VERSION}"; then - $STD apt install -y apache2 libapache2-mod-php${PHP_VERSION} || { - msg_error "Failed to install Apache with PHP module" - return 1 - } - fi - fi - - # Install PHP packages - $STD apt install -y $MODULE_LIST || { - msg_error "Failed to install PHP packages" - return 1 - } - cache_installed_version "php" "$PHP_VERSION" - - # Patch all relevant php.ini files - 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 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 - - # Patch Apache configuration if needed - if [[ "$PHP_APACHE" == "YES" ]]; then - for mod in $(ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//'); do - if [[ "$mod" != "php${PHP_VERSION}" ]]; then - $STD a2dismod "$mod" || true - fi + # Build module list + local MODULE_LIST="php${PHP_VERSION}" + IFS=',' read -ra MODULES <<<"$COMBINED_MODULES" + for mod in "${MODULES[@]}"; do + if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then + MODULE_LIST+=" php${PHP_VERSION}-${mod}" + fi done - $STD a2enmod mpm_prefork - $STD a2enmod "php${PHP_VERSION}" - 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 - safe_service_restart php${PHP_VERSION}-fpm + if [[ "$PHP_FPM" == "YES" ]]; then + MODULE_LIST+=" php${PHP_VERSION}-fpm" fi - fi - msg_ok "Setup PHP $PHP_VERSION" + # install apache2 with PHP support if requested + if [[ "$PHP_APACHE" == "YES" ]]; then + if ! dpkg -l 2>/dev/null | grep -q "libapache2-mod-php${PHP_VERSION}"; then + $STD apt install -y apache2 libapache2-mod-php${PHP_VERSION} || { + msg_error "Failed to install Apache with PHP module" + return 1 + } + fi + fi + + # Install PHP packages + $STD apt install -y $MODULE_LIST || { + msg_error "Failed to install PHP packages" + return 1 + } + cache_installed_version "php" "$PHP_VERSION" + + # Patch all relevant php.ini files + 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 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 + + # Patch Apache configuration if needed + if [[ "$PHP_APACHE" == "YES" ]]; then + for mod in $(ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//'); do + if [[ "$mod" != "php${PHP_VERSION}" ]]; then + $STD a2dismod "$mod" || true + fi + done + $STD a2enmod mpm_prefork + $STD a2enmod "php${PHP_VERSION}" + 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 + safe_service_restart php${PHP_VERSION}-fpm + fi + fi + + msg_ok "Setup PHP $PHP_VERSION" } # ------------------------------------------------------------------------------ @@ -3366,141 +3366,141 @@ function setup_php() { # Variables: # PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16) function setup_postgresql() { - local PG_VERSION="${PG_VERSION:-16}" - local PG_MODULES="${PG_MODULES:-}" - 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 PG_VERSION="${PG_VERSION:-16}" + local PG_MODULES="${PG_MODULES:-}" + 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) - # Get currently installed version - local CURRENT_PG_VERSION="" - if command -v psql >/dev/null; then - CURRENT_PG_VERSION="$(psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1)" - fi - - # Scenario 1: Already at correct version - if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then - msg_info "Update PostgreSQL $PG_VERSION" - $STD apt update - $STD apt install --only-upgrade -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null || true - cache_installed_version "postgresql" "$PG_VERSION" - msg_ok "Update PostgreSQL $PG_VERSION" - - # Still install modules if specified - if [[ -n "$PG_MODULES" ]]; then - IFS=',' read -ra MODULES <<<"$PG_MODULES" - for module in "${MODULES[@]}"; do - $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true - done + # Get currently installed version + local CURRENT_PG_VERSION="" + if command -v psql >/dev/null; then + CURRENT_PG_VERSION="$(psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1)" fi - return 0 - fi - # Scenario 2: Different version - backup, remove old, install new - if [[ -n "$CURRENT_PG_VERSION" ]]; then - msg_info "Upgrade PostgreSQL from $CURRENT_PG_VERSION to $PG_VERSION" - msg_info "Creating backup of PostgreSQL $CURRENT_PG_VERSION databases..." - $STD runuser -u postgres -- pg_dumpall >/var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql || { - msg_error "Failed to backup PostgreSQL databases" - return 1 - } - $STD systemctl stop postgresql || true - $STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" 2>/dev/null || true - else - msg_info "Setup PostgreSQL $PG_VERSION" - fi + # Scenario 1: Already at correct version + if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then + msg_info "Update PostgreSQL $PG_VERSION" + $STD apt update + $STD apt install --only-upgrade -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null || true + cache_installed_version "postgresql" "$PG_VERSION" + msg_ok "Update PostgreSQL $PG_VERSION" - # Scenario 3: Fresh install or after removal - setup repo and install - cleanup_old_repo_files "pgdg" + # Still install modules if specified + if [[ -n "$PG_MODULES" ]]; then + IFS=',' read -ra MODULES <<<"$PG_MODULES" + for module in "${MODULES[@]}"; do + $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true + done + fi + return 0 + fi - local SUITE - case "$DISTRO_CODENAME" in - trixie | forky | sid) - if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then - SUITE="trixie-pgdg" + # Scenario 2: Different version - backup, remove old, install new + if [[ -n "$CURRENT_PG_VERSION" ]]; then + msg_info "Upgrade PostgreSQL from $CURRENT_PG_VERSION to $PG_VERSION" + msg_info "Creating backup of PostgreSQL $CURRENT_PG_VERSION databases..." + $STD runuser -u postgres -- pg_dumpall >/var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql || { + msg_error "Failed to backup PostgreSQL databases" + return 1 + } + $STD systemctl stop postgresql || true + $STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" 2>/dev/null || true else - SUITE="bookworm-pgdg" + msg_info "Setup PostgreSQL $PG_VERSION" fi - ;; - *) - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") - SUITE="${SUITE}-pgdg" - ;; - esac - setup_deb822_repo \ - "pgdg" \ - "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ - "https://apt.postgresql.org/pub/repos/apt" \ - "$SUITE" \ - "main" \ - "amd64 arm64" + # Scenario 3: Fresh install or after removal - setup repo and install + cleanup_old_repo_files "pgdg" - if ! $STD apt update; then - msg_error "APT update failed for PostgreSQL repository" - return 1 - fi + local SUITE + case "$DISTRO_CODENAME" in + trixie | forky | sid) + if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then + SUITE="trixie-pgdg" + else + SUITE="bookworm-pgdg" + fi + ;; + *) + SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") + SUITE="${SUITE}-pgdg" + ;; + esac - # Install ssl-cert dependency if available - if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then - $STD apt install -y ssl-cert 2>/dev/null || true - fi + setup_deb822_repo \ + "pgdg" \ + "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ + "https://apt.postgresql.org/pub/repos/apt" \ + "$SUITE" \ + "main" \ + "amd64 arm64" - # Try multiple PostgreSQL package patterns - local pg_install_success=false + if ! $STD apt update; then + msg_error "APT update failed for PostgreSQL repository" + return 1 + fi - 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 + # Install ssl-cert dependency if available + if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then + $STD apt install -y ssl-cert 2>/dev/null || true + fi - 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 + # Try multiple PostgreSQL package patterns + local pg_install_success=false - 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 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 - if [[ "$pg_install_success" == false ]]; then - msg_error "PostgreSQL package not available for suite ${SUITE}" - return 1 - fi + 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 - if ! command -v psql >/dev/null 2>&1; then - msg_error "PostgreSQL installed but psql command not found" - return 1 - fi + 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 - # Restore database backup if we upgraded from previous version - if [[ -n "$CURRENT_PG_VERSION" ]]; then - msg_info "Restoring PostgreSQL databases from backup..." - $STD runuser -u postgres -- psql /dev/null || { - msg_warn "Failed to restore database backup - this may be expected for major version upgrades" - } - fi + if [[ "$pg_install_success" == false ]]; then + msg_error "PostgreSQL package not available for suite ${SUITE}" + return 1 + fi - $STD systemctl enable --now postgresql 2>/dev/null || true + if ! command -v psql >/dev/null 2>&1; then + msg_error "PostgreSQL installed but psql command not found" + return 1 + fi - # Add PostgreSQL binaries to PATH - 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 + # Restore database backup if we upgraded from previous version + if [[ -n "$CURRENT_PG_VERSION" ]]; then + msg_info "Restoring PostgreSQL databases from backup..." + $STD runuser -u postgres -- psql /dev/null || { + msg_warn "Failed to restore database backup - this may be expected for major version upgrades" + } + fi - cache_installed_version "postgresql" "$PG_VERSION" - msg_ok "Setup PostgreSQL $PG_VERSION" + $STD systemctl enable --now postgresql 2>/dev/null || true - # Install optional modules - if [[ -n "$PG_MODULES" ]]; then - IFS=',' read -ra MODULES <<<"$PG_MODULES" - for module in "${MODULES[@]}"; do - $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true - done - fi + # Add PostgreSQL binaries to PATH + 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" + + # Install optional modules + if [[ -n "$PG_MODULES" ]]; then + IFS=',' read -ra MODULES <<<"$PG_MODULES" + for module in "${MODULES[@]}"; do + $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true + done + fi } # ------------------------------------------------------------------------------ @@ -3517,192 +3517,192 @@ 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=$(mktemp -d) + 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=$(mktemp -d) - # Get currently installed Ruby version - local CURRENT_RUBY_VERSION="" - if [[ -x "$RBENV_BIN" ]]; then - CURRENT_RUBY_VERSION=$("$RBENV_BIN" global 2>/dev/null || echo "") - fi + # Get currently installed Ruby version + local CURRENT_RUBY_VERSION="" + if [[ -x "$RBENV_BIN" ]]; then + CURRENT_RUBY_VERSION=$("$RBENV_BIN" global 2>/dev/null || echo "") + fi - # Scenario 1: Already at correct Ruby version - if [[ "$CURRENT_RUBY_VERSION" == "$RUBY_VERSION" ]]; then - msg_info "Update Ruby $RUBY_VERSION" - cache_installed_version "ruby" "$RUBY_VERSION" - msg_ok "Update Ruby $RUBY_VERSION" - return 0 - fi + # Scenario 1: Already at correct Ruby version + if [[ "$CURRENT_RUBY_VERSION" == "$RUBY_VERSION" ]]; then + msg_info "Update Ruby $RUBY_VERSION" + cache_installed_version "ruby" "$RUBY_VERSION" + msg_ok "Update Ruby $RUBY_VERSION" + return 0 + fi - # Scenario 2: Different version - reinstall - if [[ -n "$CURRENT_RUBY_VERSION" ]]; then - msg_info "Upgrade Ruby from $CURRENT_RUBY_VERSION to $RUBY_VERSION" - else - msg_info "Setup Ruby $RUBY_VERSION" - fi - - ensure_apt_working || return 1 - - # Install build dependencies with fallbacks - 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 - IFS='|' read -ra variations <<<"$dep_pattern" - for var in "${variations[@]}"; do - if apt-cache search "^${var}$" 2>/dev/null | grep -q .; then - ruby_deps+=("$var") - break - fi - done + # Scenario 2: Different version - reinstall + if [[ -n "$CURRENT_RUBY_VERSION" ]]; then + msg_info "Upgrade Ruby from $CURRENT_RUBY_VERSION to $RUBY_VERSION" else - if apt-cache search "^${dep_pattern}$" 2>/dev/null | grep -q .; then - ruby_deps+=("$dep_pattern") - fi + msg_info "Setup Ruby $RUBY_VERSION" + fi + + ensure_apt_working || return 1 + + # Install build dependencies with fallbacks + 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 + IFS='|' read -ra variations <<<"$dep_pattern" + for var in "${variations[@]}"; do + if apt-cache search "^${var}$" 2>/dev/null | grep -q .; then + ruby_deps+=("$var") + 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" + rm -rf "$TMP_DIR" + return 1 + fi + + # Download and build rbenv if needed + if [[ ! -x "$RBENV_BIN" ]]; then + local RBENV_RELEASE + local rbenv_json + rbenv_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/rbenv/rbenv/releases/latest 2>/dev/null || echo "") + + if [[ -z "$rbenv_json" ]]; then + msg_error "Failed to fetch latest rbenv version from GitHub" + rm -rf "$TMP_DIR" + return 1 + fi + + RBENV_RELEASE=$(echo "$rbenv_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") + + if [[ -z "$RBENV_RELEASE" ]]; then + msg_error "Could not parse rbenv version from GitHub response" + 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" || { + 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) || { + msg_error "Failed to build rbenv" + rm -rf "$TMP_DIR" + return 1 + } + + # Setup profile + if ! grep -q 'rbenv init' "$PROFILE_FILE" 2>/dev/null; then + echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE" + echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE" + fi + fi + + # Install ruby-build plugin + if [[ ! -d "$RBENV_DIR/plugins/ruby-build" ]]; then + local RUBY_BUILD_RELEASE + local ruby_build_json + ruby_build_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/rbenv/ruby-build/releases/latest 2>/dev/null || echo "") + + if [[ -z "$ruby_build_json" ]]; then + msg_error "Failed to fetch latest ruby-build version from GitHub" + rm -rf "$TMP_DIR" + return 1 + fi + + RUBY_BUILD_RELEASE=$(echo "$ruby_build_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") + + if [[ -z "$RUBY_BUILD_RELEASE" ]]; then + msg_error "Could not parse ruby-build version from GitHub response" + 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" || { + 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/" + fi + + # Setup PATH and install Ruby version + export PATH="$RBENV_DIR/bin:$PATH" + eval "$("$RBENV_BIN" init - bash)" 2>/dev/null || true + + if ! "$RBENV_BIN" versions --bare 2>/dev/null | grep -qx "$RUBY_VERSION"; then + $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" || { + msg_error "Failed to set Ruby $RUBY_VERSION as global version" + rm -rf "$TMP_DIR" + return 1 + } + + hash -r + + # Install Rails if requested + if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then + $STD gem install rails || { + msg_warn "Failed to install Rails - Ruby installation successful" + } 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" rm -rf "$TMP_DIR" - return 1 - fi - - # Download and build rbenv if needed - if [[ ! -x "$RBENV_BIN" ]]; then - local RBENV_RELEASE - local rbenv_json - rbenv_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/rbenv/rbenv/releases/latest 2>/dev/null || echo "") - - if [[ -z "$rbenv_json" ]]; then - msg_error "Failed to fetch latest rbenv version from GitHub" - rm -rf "$TMP_DIR" - return 1 - fi - - RBENV_RELEASE=$(echo "$rbenv_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") - - if [[ -z "$RBENV_RELEASE" ]]; then - msg_error "Could not parse rbenv version from GitHub response" - 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" || { - 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) || { - msg_error "Failed to build rbenv" - rm -rf "$TMP_DIR" - return 1 - } - - # Setup profile - if ! grep -q 'rbenv init' "$PROFILE_FILE" 2>/dev/null; then - echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE" - echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE" - fi - fi - - # Install ruby-build plugin - if [[ ! -d "$RBENV_DIR/plugins/ruby-build" ]]; then - local RUBY_BUILD_RELEASE - local ruby_build_json - ruby_build_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/rbenv/ruby-build/releases/latest 2>/dev/null || echo "") - - if [[ -z "$ruby_build_json" ]]; then - msg_error "Failed to fetch latest ruby-build version from GitHub" - rm -rf "$TMP_DIR" - return 1 - fi - - RUBY_BUILD_RELEASE=$(echo "$ruby_build_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") - - if [[ -z "$RUBY_BUILD_RELEASE" ]]; then - msg_error "Could not parse ruby-build version from GitHub response" - 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" || { - 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/" - fi - - # Setup PATH and install Ruby version - export PATH="$RBENV_DIR/bin:$PATH" - eval "$("$RBENV_BIN" init - bash)" 2>/dev/null || true - - if ! "$RBENV_BIN" versions --bare 2>/dev/null | grep -qx "$RUBY_VERSION"; then - $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" || { - msg_error "Failed to set Ruby $RUBY_VERSION as global version" - rm -rf "$TMP_DIR" - return 1 - } - - hash -r - - # Install Rails if requested - if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then - $STD gem install rails || { - msg_warn "Failed to install Rails - Ruby installation successful" - } - fi - - rm -rf "$TMP_DIR" - cache_installed_version "ruby" "$RUBY_VERSION" - msg_ok "Setup Ruby $RUBY_VERSION" + cache_installed_version "ruby" "$RUBY_VERSION" + msg_ok "Setup Ruby $RUBY_VERSION" } # ------------------------------------------------------------------------------ @@ -3719,97 +3719,97 @@ function setup_ruby() { # ------------------------------------------------------------------------------ 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) + 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) - # Resolve "latest" version - if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then - CLICKHOUSE_VERSION=$(curl -fsSL --max-time 15 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 || echo "") + # Resolve "latest" version + if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then + CLICKHOUSE_VERSION=$(curl -fsSL --max-time 15 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 || echo "") - # Fallback to GitHub API if package server failed - if [[ -z "$CLICKHOUSE_VERSION" ]]; then - CLICKHOUSE_VERSION=$(curl -fsSL --max-time 15 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 || echo "") + # Fallback to GitHub API if package server failed + if [[ -z "$CLICKHOUSE_VERSION" ]]; then + CLICKHOUSE_VERSION=$(curl -fsSL --max-time 15 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 || echo "") + fi + + [[ -z "$CLICKHOUSE_VERSION" ]] && { + msg_error "Could not determine latest ClickHouse version from any source" + return 1 + } fi - [[ -z "$CLICKHOUSE_VERSION" ]] && { - msg_error "Could not determine latest ClickHouse version from any source" - return 1 + # Get currently installed version + 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 + + # Scenario 1: Already at target version - just update packages + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then + msg_info "Update ClickHouse $CLICKHOUSE_VERSION" + ensure_apt_working || return 1 + $STD apt install --only-upgrade -y clickhouse-server clickhouse-client || true + cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" + msg_ok "Update ClickHouse $CLICKHOUSE_VERSION" + return 0 + fi + + # Scenario 2: Different version - clean upgrade + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$CLICKHOUSE_VERSION" ]]; then + msg_info "Upgrade ClickHouse from $CURRENT_VERSION to $CLICKHOUSE_VERSION" + $STD systemctl stop clickhouse-server >/dev/null 2>&1 || true + remove_old_tool_version "clickhouse" + else + msg_info "Setup ClickHouse $CLICKHOUSE_VERSION" + fi + + ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg + + # Setup repository (ClickHouse uses 'stable' suite) + setup_deb822_repo \ + "clickhouse" \ + "https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \ + "https://packages.clickhouse.com/deb" \ + "stable" \ + "main" \ + "amd64 arm64" + + # Install packages + export DEBIAN_FRONTEND=noninteractive + $STD apt update || { + msg_error "APT update failed for ClickHouse repository" + return 1 } - fi - # Get currently installed version - 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 + $STD apt install -y clickhouse-server clickhouse-client || { + msg_error "Failed to install ClickHouse packages" + return 1 + } + + # 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 + + # Setup data directory + mkdir -p /var/lib/clickhouse + 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 || { + msg_warn "Failed to enable clickhouse-server service" + } + safe_service_restart clickhouse-server || true - # Scenario 1: Already at target version - just update packages - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then - msg_info "Update ClickHouse $CLICKHOUSE_VERSION" - ensure_apt_working || return 1 - $STD apt install --only-upgrade -y clickhouse-server clickhouse-client || true cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" - msg_ok "Update ClickHouse $CLICKHOUSE_VERSION" - return 0 - fi - - # Scenario 2: Different version - clean upgrade - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$CLICKHOUSE_VERSION" ]]; then - msg_info "Upgrade ClickHouse from $CURRENT_VERSION to $CLICKHOUSE_VERSION" - $STD systemctl stop clickhouse-server >/dev/null 2>&1 || true - remove_old_tool_version "clickhouse" - else - msg_info "Setup ClickHouse $CLICKHOUSE_VERSION" - fi - - ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg - - # Setup repository (ClickHouse uses 'stable' suite) - setup_deb822_repo \ - "clickhouse" \ - "https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \ - "https://packages.clickhouse.com/deb" \ - "stable" \ - "main" \ - "amd64 arm64" - - # Install packages - export DEBIAN_FRONTEND=noninteractive - $STD apt update || { - msg_error "APT update failed for ClickHouse repository" - return 1 - } - - $STD apt install -y clickhouse-server clickhouse-client || { - msg_error "Failed to install ClickHouse packages" - return 1 - } - - # 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 - - # Setup data directory - mkdir -p /var/lib/clickhouse - 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 || { - msg_warn "Failed to enable clickhouse-server service" - } - safe_service_restart clickhouse-server || true - - cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" - msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION" + msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION" } # ------------------------------------------------------------------------------ @@ -3830,71 +3830,71 @@ function setup_clickhouse() { # ------------------------------------------------------------------------------ function setup_rust() { - local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}" - local RUST_CRATES="${RUST_CRATES:-}" - local CARGO_BIN="${HOME}/.cargo/bin" + local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}" + local RUST_CRATES="${RUST_CRATES:-}" + local CARGO_BIN="${HOME}/.cargo/bin" - # Get currently installed version - local CURRENT_VERSION="" - if command -v rustc &>/dev/null; then - CURRENT_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') - fi + # Get currently installed version + local CURRENT_VERSION="" + if command -v rustc &>/dev/null; then + CURRENT_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + fi - # Scenario 1: Rustup not installed - fresh install - if ! command -v rustup &>/dev/null; then - msg_info "Setup Rust ($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" - local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') - cache_installed_version "rust" "$RUST_VERSION" - msg_ok "Setup Rust $RUST_VERSION" - else - # Scenario 2: Rustup already installed - update/maintain - msg_info "Update Rust ($RUST_TOOLCHAIN)" - $STD rustup install "$RUST_TOOLCHAIN" || { - msg_error "Failed to install Rust toolchain $RUST_TOOLCHAIN" - return 1 - } - $STD rustup default "$RUST_TOOLCHAIN" || { - msg_error "Failed to set default Rust toolchain" - return 1 - } - $STD rustup update "$RUST_TOOLCHAIN" || true - local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') - cache_installed_version "rust" "$RUST_VERSION" - msg_ok "Update Rust $RUST_VERSION" - fi + # Scenario 1: Rustup not installed - fresh install + if ! command -v rustup &>/dev/null; then + msg_info "Setup Rust ($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" + local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + cache_installed_version "rust" "$RUST_VERSION" + msg_ok "Setup Rust $RUST_VERSION" + else + # Scenario 2: Rustup already installed - update/maintain + msg_info "Update Rust ($RUST_TOOLCHAIN)" + $STD rustup install "$RUST_TOOLCHAIN" || { + msg_error "Failed to install Rust toolchain $RUST_TOOLCHAIN" + return 1 + } + $STD rustup default "$RUST_TOOLCHAIN" || { + msg_error "Failed to set default Rust toolchain" + return 1 + } + $STD rustup update "$RUST_TOOLCHAIN" || true + local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + cache_installed_version "rust" "$RUST_VERSION" + msg_ok "Update Rust $RUST_VERSION" + fi - # Install global crates - if [[ -n "$RUST_CRATES" ]]; then - IFS=',' read -ra CRATES <<<"$RUST_CRATES" - for crate in "${CRATES[@]}"; do - local NAME VER INSTALLED_VER - if [[ "$crate" == *"@"* ]]; then - NAME="${crate%@*}" - VER="${crate##*@}" - else - NAME="$crate" - VER="" - fi + # Install global crates + if [[ -n "$RUST_CRATES" ]]; then + IFS=',' read -ra CRATES <<<"$RUST_CRATES" + for crate in "${CRATES[@]}"; do + local NAME VER INSTALLED_VER + if [[ "$crate" == *"@"* ]]; then + NAME="${crate%@*}" + VER="${crate##*@}" + else + NAME="$crate" + VER="" + fi - INSTALLED_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v') + INSTALLED_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v') - if [[ -n "$INSTALLED_VER" ]]; then - if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then - $STD cargo install "$NAME" --version "$VER" --force - elif [[ -z "$VER" ]]; then - $STD cargo install "$NAME" --force - fi - else - $STD cargo install "$NAME" ${VER:+--version "$VER"} - fi - done - fi + if [[ -n "$INSTALLED_VER" ]]; then + if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then + $STD cargo install "$NAME" --version "$VER" --force + elif [[ -z "$VER" ]]; then + $STD cargo install "$NAME" --force + fi + else + $STD cargo install "$NAME" ${VER:+--version "$VER"} + fi + done + fi } # ------------------------------------------------------------------------------ @@ -3906,122 +3906,122 @@ function setup_rust() { # ------------------------------------------------------------------------------ function setup_uv() { - local UV_BIN="/usr/local/bin/uv" - local TMP_DIR=$(mktemp -d) - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "uv") + local UV_BIN="/usr/local/bin/uv" + local TMP_DIR=$(mktemp -d) + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "uv") - local ARCH=$(uname -m) - local UV_TAR + local ARCH=$(uname -m) + local UV_TAR - case "$ARCH" in - x86_64) - if grep -qi "alpine" /etc/os-release; then - UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz" - else - UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz" - fi - ;; - aarch64) - if grep -qi "alpine" /etc/os-release; then - UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz" - else - UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz" - fi - ;; - *) - msg_error "Unsupported architecture: $ARCH" - rm -rf "$TMP_DIR" - return 1 - ;; - esac - - ensure_dependencies jq - - local LATEST_VERSION - local releases_json - releases_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/astral-sh/uv/releases/latest 2>/dev/null || echo "") - - if [[ -z "$releases_json" ]]; then - msg_error "Could not fetch latest uv version from GitHub API" - rm -rf "$TMP_DIR" - return 1 - fi - - LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") - - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not parse uv version from GitHub API response" - rm -rf "$TMP_DIR" - return 1 - fi - - # Get currently installed version - local INSTALLED_VERSION="" - if [[ -x "$UV_BIN" ]]; then - INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}') - fi - - # Scenario 1: Already at latest version - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then - cache_installed_version "uv" "$LATEST_VERSION" - rm -rf "$TMP_DIR" - return 0 - fi - - # Scenario 2: New install or upgrade - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$LATEST_VERSION" ]]; then - msg_info "Upgrade uv from $INSTALLED_VERSION to $LATEST_VERSION" - else - msg_info "Setup uv $LATEST_VERSION" - fi - - local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" - curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || { - msg_error "Failed to download uv" - rm -rf "$TMP_DIR" - return 1 - } - - tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || { - msg_error "Failed to extract uv" - rm -rf "$TMP_DIR" - return 1 - } - - install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || { - msg_error "Failed to install uv binary" - rm -rf "$TMP_DIR" - return 1 - } - - rm -rf "$TMP_DIR" - ensure_usr_local_bin_persist - export PATH="/usr/local/bin:$PATH" - - $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 2>/dev/null | - grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" | - cut -d'-' -f2 | sort -V | tail -n1) - - if [[ -z "$VERSION_MATCH" ]]; then - msg_error "No matching Python $PYTHON_VERSION.x version found" - return 1 - fi - - if ! uv python list 2>/dev/null | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then - $STD uv python install "$VERSION_MATCH" || { - msg_error "Failed to install Python $VERSION_MATCH" + case "$ARCH" in + x86_64) + if grep -qi "alpine" /etc/os-release; then + UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz" + else + UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz" + fi + ;; + aarch64) + if grep -qi "alpine" /etc/os-release; then + UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz" + else + UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz" + fi + ;; + *) + msg_error "Unsupported architecture: $ARCH" + rm -rf "$TMP_DIR" + return 1 + ;; + esac + + ensure_dependencies jq + + local LATEST_VERSION + local releases_json + releases_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/astral-sh/uv/releases/latest 2>/dev/null || echo "") + + if [[ -z "$releases_json" ]]; then + msg_error "Could not fetch latest uv version from GitHub API" + rm -rf "$TMP_DIR" return 1 - } fi - fi + + LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") + + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not parse uv version from GitHub API response" + rm -rf "$TMP_DIR" + return 1 + fi + + # Get currently installed version + local INSTALLED_VERSION="" + if [[ -x "$UV_BIN" ]]; then + INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}') + fi + + # Scenario 1: Already at latest version + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then + cache_installed_version "uv" "$LATEST_VERSION" + rm -rf "$TMP_DIR" + return 0 + fi + + # Scenario 2: New install or upgrade + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$LATEST_VERSION" ]]; then + msg_info "Upgrade uv from $INSTALLED_VERSION to $LATEST_VERSION" + else + msg_info "Setup uv $LATEST_VERSION" + fi + + local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" + curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || { + msg_error "Failed to download uv" + rm -rf "$TMP_DIR" + return 1 + } + + tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract uv" + rm -rf "$TMP_DIR" + return 1 + } + + install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || { + msg_error "Failed to install uv binary" + rm -rf "$TMP_DIR" + return 1 + } + + rm -rf "$TMP_DIR" + ensure_usr_local_bin_persist + export PATH="/usr/local/bin:$PATH" + + $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 2>/dev/null | + grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" | + cut -d'-' -f2 | sort -V | tail -n1) + + if [[ -z "$VERSION_MATCH" ]]; then + msg_error "No matching Python $PYTHON_VERSION.x version found" + return 1 + fi + + if ! uv python list 2>/dev/null | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then + $STD uv python install "$VERSION_MATCH" || { + msg_error "Failed to install Python $VERSION_MATCH" + return 1 + } + fi + fi } # ------------------------------------------------------------------------------ @@ -4034,76 +4034,76 @@ function setup_uv() { # ------------------------------------------------------------------------------ function setup_yq() { - local TMP_DIR=$(mktemp -d) - local BINARY_PATH="/usr/local/bin/yq" - local GITHUB_REPO="mikefarah/yq" + local TMP_DIR=$(mktemp -d) + local BINARY_PATH="/usr/local/bin/yq" + local GITHUB_REPO="mikefarah/yq" - ensure_dependencies jq - ensure_usr_local_bin_persist + ensure_dependencies jq + ensure_usr_local_bin_persist - # Remove non-mikefarah implementations - if command -v yq &>/dev/null; then - if ! yq --version 2>&1 | grep -q 'mikefarah'; then - rm -f "$(command -v yq)" + # Remove non-mikefarah implementations + if command -v yq &>/dev/null; then + if ! yq --version 2>&1 | grep -q 'mikefarah'; then + rm -f "$(command -v yq)" + fi fi - fi - local LATEST_VERSION - local releases_json - releases_json=$(curl -fsSL --max-time 15 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null || echo "") + local LATEST_VERSION + local releases_json + releases_json=$(curl -fsSL --max-time 15 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null || echo "") + + if [[ -z "$releases_json" ]]; then + msg_error "Could not fetch latest yq version from GitHub API" + rm -rf "$TMP_DIR" + return 1 + fi + + LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") + + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not parse yq version from GitHub API response" + rm -rf "$TMP_DIR" + return 1 + fi + + # Get currently installed version + local INSTALLED_VERSION="" + if command -v yq &>/dev/null && yq --version 2>&1 | grep -q 'mikefarah'; then + INSTALLED_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') + fi + + # Scenario 1: Already at latest version + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then + cache_installed_version "yq" "$LATEST_VERSION" + rm -rf "$TMP_DIR" + return 0 + fi + + # Scenario 2: New install or upgrade + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$LATEST_VERSION" ]]; then + msg_info "Upgrade yq from $INSTALLED_VERSION to $LATEST_VERSION" + else + msg_info "Setup yq $LATEST_VERSION" + fi + + 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" || { + msg_error "Failed to install yq" + rm -rf "$TMP_DIR" + return 1 + } - if [[ -z "$releases_json" ]]; then - msg_error "Could not fetch latest yq version from GitHub API" rm -rf "$TMP_DIR" - return 1 - fi + hash -r - LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "") - - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not parse yq version from GitHub API response" - rm -rf "$TMP_DIR" - return 1 - fi - - # Get currently installed version - local INSTALLED_VERSION="" - if command -v yq &>/dev/null && yq --version 2>&1 | grep -q 'mikefarah'; then - INSTALLED_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') - fi - - # Scenario 1: Already at latest version - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then - cache_installed_version "yq" "$LATEST_VERSION" - rm -rf "$TMP_DIR" - return 0 - fi - - # Scenario 2: New install or upgrade - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$LATEST_VERSION" ]]; then - msg_info "Upgrade yq from $INSTALLED_VERSION to $LATEST_VERSION" - else - msg_info "Setup yq $LATEST_VERSION" - fi - - 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" || { - msg_error "Failed to install yq" - rm -rf "$TMP_DIR" - return 1 - } - - rm -rf "$TMP_DIR" - hash -r - - local FINAL_VERSION - 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" + local FINAL_VERSION + 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" }