From 0f624dfba8574637c286992e94867da47859b83e Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:03:59 +0200 Subject: [PATCH] Add unified tool install, upgrade, and repo management Introduces functions for checking tool installation and version, removing old versions, determining if upgrades are needed, and managing repositories for major tools (MariaDB, MongoDB, Node.js, PHP, PostgreSQL, MySQL). Refactors setup functions for Composer, FFmpeg, Go, Ghostscript, ImageMagick, Java, local IP helper, MariaDB, MongoDB, MySQL, and Node.js to use these new utilities, improving idempotency, upgrade handling, and error reporting. Enhances repository setup with parameter validation and error handling. --- misc/tools.func | 1750 ++++++++++++++++++++++++++++------------------- 1 file changed, 1033 insertions(+), 717 deletions(-) diff --git a/misc/tools.func b/misc/tools.func index cc27410b..ab3e1130 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -25,6 +25,345 @@ get_cached_version() { } # ------------------------------------------------------------------------------ +# Check if tool is already installed and optionally verify exact version +# Returns: 0 if installed (with optional version match), 1 if not installed +# Usage: is_tool_installed "mariadb" "11.4" || echo "Not installed" +# ------------------------------------------------------------------------------ +is_tool_installed() { + 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 + + 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 + + echo "$installed_version" + return 0 # Installed and version matches (if specified) +} + +# ------------------------------------------------------------------------------ +# Remove old tool version completely (purge + cleanup repos) +# Usage: remove_old_tool_version "mariadb" "repository-name" +# ------------------------------------------------------------------------------ +remove_old_tool_version() { + 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 + + # Clean up old repositories + cleanup_old_repo_files "$repo_name" + + return 0 +} + +# ------------------------------------------------------------------------------ +# Determine if tool update/upgrade is needed +# Returns: 0 (update needed), 1 (already up-to-date) +# Usage: if should_update_tool "mariadb" "11.4"; then ... fi +# ------------------------------------------------------------------------------ +should_update_tool() { + 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 + + # If versions are identical, no update needed + if [[ "$current_version" == "$target_version" ]]; then + return 1 # No update needed + fi + + return 0 # Update needed +} + +# ---------------------–---------------------------------------------------------- +# Unified repository management for tools +# Handles adding, updating, and verifying tool repositories +# Usage: manage_tool_repository "mariadb" "11.4" "https://repo..." "GPG_key_url" +# 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 + + 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 + + # 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") + + # 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 + + # 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 + + # 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" + + cat </etc/apt/sources.list.d/mongodb-org-${version}.sources +Types: deb +URIs: ${repo_url} +Suites: ${suite}/mongodb-org/${version} +Components: ${repo_component} +Architectures: amd64 arm64 +Signed-By: /etc/apt/keyrings/mongodb-server-${version}.gpg +EOF + return 0 + ;; + + nodejs) + if [[ -z "$repo_url" ]]; then + msg_error "Node.js repository requires repo_url" + return 1 + fi + + 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) + + # Create keyring directory first + mkdir -p /etc/apt/keyrings + + # Download GPG key from NodeSource + curl -fsSL "$repo_url/gpg.key" | 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 +Types: deb +URIs: $repo_url +Suites: $distro_codename +Components: main +Architectures: amd64 arm64 +Signed-By: /etc/apt/keyrings/nodesource.gpg +EOF + return 0 + ;; + + php) + if [[ -z "$gpg_key_url" ]]; then + msg_error "PHP repository requires gpg_key_url" + return 1 + fi + + 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 + + # 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 +Components: main +Architectures: amd64 arm64 +Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg +EOF + return 0 + ;; + + postgresql) + if [[ -z "$gpg_key_url" ]]; then + msg_error "PostgreSQL repository requires gpg_key_url" + return 1 + fi + + cleanup_old_repo_files "postgresql" + + # 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 + } + + # 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 +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 +} + +# ------–---------------------------------------------------------------------- # Unified package upgrade function (with apt update caching) # ------------------------------------------------------------------------------ upgrade_package() { @@ -597,6 +936,7 @@ ensure_apt_working() { # ------------------------------------------------------------------------------ # Standardized deb822 repository setup +# Validates all parameters and fails safely if any are empty # ------------------------------------------------------------------------------ setup_deb822_repo() { local name="$1" @@ -606,6 +946,12 @@ setup_deb822_repo() { 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 + # Cleanup old configs for this app cleanup_old_repo_files "$name" @@ -613,11 +959,14 @@ setup_deb822_repo() { cleanup_orphaned_sources # Ensure keyring directory exists - mkdir -p /etc/apt/keyrings + 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" || { - msg_error "Failed to download or import GPG key for ${name}" + 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 } @@ -1419,9 +1768,6 @@ function import_local_ip() { # ------------------------------------------------------------------------------ function setup_adminer() { - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "adminer") - if grep -qi alpine /etc/os-release; then msg_info "Setup Adminer (Alpine)" mkdir -p /var/www/localhost/htdocs/adminer @@ -1456,14 +1802,31 @@ function setup_adminer() { # - Installs to /usr/local/bin/composer # - Removes old binaries/symlinks in /usr/bin, /bin, /root/.composer, etc. # - Ensures /usr/local/bin is in PATH (permanent) +# - Auto-updates to latest version # ------------------------------------------------------------------------------ function setup_composer() { local COMPOSER_BIN="/usr/local/bin/composer" - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "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 + + # 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" for old in /usr/bin/composer /bin/composer /root/.composer/vendor/bin/composer; do @@ -1488,7 +1851,7 @@ function setup_composer() { 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 @@ -1523,8 +1886,12 @@ function setup_ffmpeg() { local VERSION="${FFMPEG_VERSION:-latest}" local TYPE="${FFMPEG_TYPE:-full}" local BIN_PATH="/usr/local/bin/ffmpeg" - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "ffmpeg") + + # 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)" @@ -1545,11 +1912,11 @@ function setup_ffmpeg() { cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH" cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe chmod +x "$BIN_PATH" /usr/local/bin/ffprobe - local FINAL_VERSION=$($BIN_PATH -version | head -n1 | awk '{print $3}') + 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 - msg_ok "Setup FFmpeg" + [[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION" return 0 fi @@ -1557,7 +1924,7 @@ function setup_ffmpeg() { # Auto-detect latest stable version if none specified if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then - VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" | + VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" 2>/dev/null | jq -r '.[].name' | grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1) @@ -1656,7 +2023,7 @@ function setup_ffmpeg() { echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf $STD ldconfig - ldconfig -p | grep libavdevice >/dev/null || { + ldconfig -p 2>/dev/null | grep libavdevice >/dev/null || { msg_error "libavdevice not registered with dynamic linker" rm -rf "$TMP_DIR" return 1 @@ -1669,11 +2036,11 @@ function setup_ffmpeg() { fi local FINAL_VERSION - FINAL_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}') + 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 - msg_ok "Setup FFmpeg" + [[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION" } # ------------------------------------------------------------------------------ @@ -1698,42 +2065,48 @@ function setup_go() { ;; esac - # Determine version - if [[ -z "${GO_VERSION:-}" || "${GO_VERSION}" == "latest" ]]; then - GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//') - if [[ -z "$GO_VERSION" ]]; then + # 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 - fi + } + [[ -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" - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "go") + # Get currently installed version + local CURRENT_VERSION="" if [[ -x "$GO_BIN" ]]; then - local CURRENT_VERSION - CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//') - if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$GO_VERSION" ]]; then - return 0 - fi - cache_installed_version "go" "$GO_VERSION" - return 0 - else - rm -rf "$GO_INSTALL_DIR" - fi + CURRENT_VERSION=$("$GO_BIN" version 2>/dev/null | awk '{print $3}' | sed 's/go//') fi - msg_info "Setup Go $GO_VERSION" + # 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 $TARBALL" + msg_error "Failed to download Go $GO_VERSION" rm -f "$TMP_TAR" return 1 } @@ -1743,6 +2116,7 @@ function setup_go() { 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" @@ -1763,13 +2137,11 @@ function setup_go() { function setup_gs() { local TMP_DIR=$(mktemp -d) local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "ghostscript") ensure_dependencies jq local RELEASE_JSON - RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) + RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest 2>/dev/null) local LATEST_VERSION LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^gs//') local LATEST_VERSION_DOTTED @@ -1781,17 +2153,19 @@ function setup_gs() { return 1 fi + # Scenario 1: Already at latest version if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then - if [[ "$CACHED_VERSION" == "$LATEST_VERSION_DOTTED" ]]; then - rm -rf "$TMP_DIR" - return 0 - fi cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" rm -rf "$TMP_DIR" return 0 fi - msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED" + # 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" @@ -1931,6 +2305,7 @@ function setup_hwaccel() { $STD adduser "$(id -u -n)" render fi + cache_installed_version "hwaccel" "1.0" msg_ok "Setup Hardware Acceleration" } @@ -1947,20 +2322,12 @@ function setup_hwaccel() { # ------------------------------------------------------------------------------ function setup_imagemagick() { local TMP_DIR=$(mktemp -d) - local VERSION="" local BINARY_PATH="/usr/local/bin/magick" - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "imagemagick") + # Get currently installed version + local INSTALLED_VERSION="" if command -v magick &>/dev/null; then - VERSION=$(magick -version | awk '/^Version/ {print $3}') - if [[ "$CACHED_VERSION" == "$VERSION" ]]; then - rm -rf "$TMP_DIR" - return 0 - fi - cache_installed_version "imagemagick" "$VERSION" - rm -rf "$TMP_DIR" - return 0 + INSTALLED_VERSION=$(magick -version | awk '/^Version/ {print $3}') fi msg_info "Setup ImageMagick" @@ -2026,11 +2393,17 @@ function setup_imagemagick() { return 1 fi - VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') + local FINAL_VERSION + FINAL_VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') rm -rf "$TMP_DIR" - cache_installed_version "imagemagick" "$VERSION" + cache_installed_version "imagemagick" "$FINAL_VERSION" ensure_usr_local_bin_persist - msg_ok "Setup ImageMagick" + + if [[ -n "$INSTALLED_VERSION" ]]; then + msg_ok "Upgrade ImageMagick $INSTALLED_VERSION → $FINAL_VERSION" + else + msg_ok "Setup ImageMagick $FINAL_VERSION" + fi } # ------------------------------------------------------------------------------ @@ -2051,20 +2424,11 @@ function setup_java() { DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" - # Check cached version - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "temurin-jdk") - - # Add repo nur wenn nötig + # Add repo if needed if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then - # Cleanup old repository files cleanup_old_repo_files "adoptium" - - # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb") - - # Use standardized repo setup setup_deb822_repo \ "adoptium" \ "https://packages.adoptium.net/artifactory/api/gpg/key/public" \ @@ -2074,33 +2438,38 @@ function setup_java() { "amd64 arm64" fi + # Get currently installed version local INSTALLED_VERSION="" if dpkg -l | grep -q "temurin-.*-jdk"; then INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+') fi + # Scenario 1: Already at correct version if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$JAVA_VERSION" ]]; then - # Already at correct version, just upgrade if available - upgrade_package "$DESIRED_PACKAGE" - else - $STD apt update - $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" - cache_installed_version "temurin-jdk" "$JAVA_VERSION" - fi + msg_info "Update Temurin JDK $JAVA_VERSION" + $STD apt update + $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" + 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" - if [[ -n "$INSTALLED_VERSION" ]]; then - $STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk" - fi - - $STD apt install -y "$DESIRED_PACKAGE" || { - msg_error "Failed to install Temurin JDK $JAVA_VERSION" - return 1 - } - cache_installed_version "temurin-jdk" "$JAVA_VERSION" - msg_ok "Setup Temurin JDK $JAVA_VERSION" fi + + $STD apt update + $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" } # ------------------------------------------------------------------------------ @@ -2117,6 +2486,15 @@ function setup_local_ip_helper() { 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 + mkdir -p "$BASE_DIR" # Install networkd-dispatcher if not present @@ -2185,6 +2563,9 @@ EOF chmod +x "$DISPATCHER_SCRIPT" systemctl enable -q --now networkd-dispatcher.service + + cache_installed_version "local-ip-helper" "1.0" + msg_ok "Setup Local IP Helper" } # ------------------------------------------------------------------------------ @@ -2201,108 +2582,80 @@ EOF setup_mariadb() { local MARIADB_VERSION="${MARIADB_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) - - if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then - msg_error "MariaDB mirror not reachable" - return 1 - fi + # Resolve "latest" to actual version if [[ "$MARIADB_VERSION" == "latest" ]]; then - MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ | + if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null 2>&1; then + msg_error "MariaDB mirror not reachable" + return 1 + fi + MARIADB_VERSION=$(curl -fsSL 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) - if [[ -z "$MARIADB_VERSION" ]]; then + head -n1) || { msg_error "Could not determine latest GA MariaDB version" return 1 - fi + } + [[ -z "$MARIADB_VERSION" ]] && { + msg_error "Latest MariaDB version is empty" + return 1 + } fi + # Get currently installed version local CURRENT_VERSION="" - if command -v mariadb >/dev/null; then - CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') - fi + CURRENT_VERSION=$(is_tool_installed "mariadb" 2>/dev/null) || true - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "mariadb") + # 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" - # Check if MariaDB is already installed and needs update - if [[ -n "$CURRENT_VERSION" ]]; then - # Check if repository exists and needs update - local REPO_VERSION="" + # 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 - REPO_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.sources 2>/dev/null || echo "") - fi - - if [[ -n "$REPO_VERSION" && "$REPO_VERSION" != "$MARIADB_VERSION" ]]; then - msg_info "Update MariaDB repository from $REPO_VERSION to $MARIADB_VERSION" - - # Remove old repository - cleanup_old_repo_files "mariadb" - - # Use helper function to get fallback suite - local SUITE - DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') - DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}") - - # Setup new repository - setup_deb822_repo \ - "mariadb" \ - "https://mariadb.org/mariadb_release_signing_key.asc" \ - "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \ - "$SUITE" \ - "main" \ - "amd64 arm64" || { - msg_error "Failed to update MariaDB repository" - return 1 - } - - msg_ok "Update MariaDB repository from $REPO_VERSION to $MARIADB_VERSION" - fi - - # Now perform the update - if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$MARIADB_VERSION" ]]; then - msg_info "Update MariaDB $MARIADB_VERSION" - upgrade_package mariadb-server - upgrade_package mariadb-client - msg_ok "Update MariaDB $MARIADB_VERSION" - else - msg_info "Update MariaDB $MARIADB_VERSION" - $STD apt update || { - msg_error "Failed to update package list" + 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 } - $STD apt install --only-upgrade -y mariadb-server mariadb-client || { - msg_error "Failed to upgrade MariaDB" - return 1 - } - cache_installed_version "mariadb" "$MARIADB_VERSION" - msg_ok "Update MariaDB $MARIADB_VERSION" fi - return 0 - else - # Version mismatch - need to upgrade to new version - msg_info "Upgrade MariaDB from $CURRENT_VERSION to $MARIADB_VERSION" - $STD systemctl stop mariadb >/dev/null 2>&1 || true 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 - # Cleanup old repository files - cleanup_old_repo_files "mariadb" - - # Install required dependencies first (MariaDB needs these from main repos) + # 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 @@ -2314,30 +2667,28 @@ setup_mariadb() { $STD apt install -y "${mariadb_deps[@]}" 2>/dev/null || true fi - # Use helper function to get fallback suite - local SUITE - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}") - - # Use standardized repo setup - setup_deb822_repo \ - "mariadb" \ - "https://mariadb.org/mariadb_release_signing_key.asc" \ - "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \ - "$SUITE" \ - "main" \ - "amd64 arm64" + # 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 DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { - msg_error "Failed to install MariaDB packages" + msg_error "Failed to install MariaDB packages (both upstream and distro)" return 1 } } @@ -2375,11 +2726,9 @@ function setup_mongodb() { case "$DISTRO_ID" in ubuntu) MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu" - REPO_COMPONENT="multiverse" ;; debian) MONGO_BASE_URL="https://repo.mongodb.org/apt/debian" - REPO_COMPONENT="main" ;; *) msg_error "Unsupported distribution: $DISTRO_ID" @@ -2387,115 +2736,50 @@ function setup_mongodb() { ;; esac + # Get currently installed version local INSTALLED_VERSION="" - if command -v mongod >/dev/null 2>&1; then - INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) - fi + INSTALLED_VERSION=$(is_tool_installed "mongodb" 2>/dev/null) || true - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "mongodb") + # Scenario 1: Already at target version - just update packages + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then + msg_info "Update MongoDB $MONGO_VERSION" - if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$MONGO_VERSION" ]]; then - upgrade_package mongodb-org - else - $STD apt update || { - msg_error "Failed to update package list" - return 1 - } - $STD apt install --only-upgrade -y mongodb-org || { - msg_error "Failed to upgrade MongoDB" - return 1 - } - cache_installed_version "mongodb" "$MONGO_VERSION" - fi + ensure_apt_working || 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 - msg_info "Setup MongoDB $MONGO_VERSION" - - if [[ -n "$INSTALLED_VERSION" ]]; then - $STD systemctl stop mongod 2>/dev/null || true - $STD apt purge -y mongodb-org 2>/dev/null || true + # 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_old_repo_files "mongodb-org-${MONGO_VERSION}" cleanup_orphaned_sources - # Map distro codenames to MongoDB-supported suites - local SUITE - if [[ "$DISTRO_ID" == "debian" ]]; then - case "$DISTRO_CODENAME" in - trixie | forky | sid) - # Debian 13+ and testing/unstable use Bookworm (MongoDB doesn't support newer) - SUITE="bookworm" - ;; - bookworm) - SUITE="bookworm" - ;; - bullseye) - SUITE="bullseye" - ;; - buster) - SUITE="buster" - ;; - *) - # Fallback: try bookworm for unknown Debian releases - msg_warn "Unknown Debian release '${DISTRO_CODENAME}', using bookworm" - SUITE="bookworm" - ;; - esac - elif [[ "$DISTRO_ID" == "ubuntu" ]]; then - case "$DISTRO_CODENAME" in - oracular | plucky) - # Ubuntu 24.10+ uses noble - SUITE="noble" - ;; - noble) - SUITE="noble" - ;; - jammy) - SUITE="jammy" - ;; - focal) - SUITE="focal" - ;; - *) - # Fallback: try noble for unknown Ubuntu releases - msg_warn "Unknown Ubuntu release '${DISTRO_CODENAME}', using noble" - SUITE="noble" - ;; - esac - fi - - # Verify MongoDB repository is available (MongoDB has nested structure) - if ! curl -fsSL --max-time 10 "${MONGO_BASE_URL}/dists/${SUITE}/mongodb-org/${MONGO_VERSION}/Release" &>/dev/null; then - msg_error "MongoDB ${MONGO_VERSION} repository not available for ${DISTRO_ID}-${SUITE}" - msg_error "Please check: ${MONGO_BASE_URL}/dists/${SUITE}/mongodb-org/${MONGO_VERSION}/" - return 1 - fi - - mkdir -p /etc/apt/keyrings - if ! curl -fsSL "https://www.mongodb.org/static/pgp/server-${MONGO_VERSION}.asc" | - gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-server-${MONGO_VERSION}.gpg"; then - msg_error "Failed to download or import MongoDB GPG key" - return 1 - fi - - cat </etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.sources -Types: deb -URIs: ${MONGO_BASE_URL} -Suites: ${SUITE}/mongodb-org/${MONGO_VERSION} -Components: ${REPO_COMPONENT} -Architectures: amd64 arm64 -Signed-By: /etc/apt/keyrings/mongodb-server-${MONGO_VERSION}.gpg -EOF - - $STD apt update || { - msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}? (Suite: ${SUITE})" + # 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 @@ -2526,59 +2810,47 @@ EOF function setup_mysql() { local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" - local CURRENT_VERSION="" - local NEED_INSTALL=false local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - if command -v mysql >/dev/null; then - CURRENT_VERSION="$(mysql --version | grep -oP '[0-9]+\.[0-9]+' | head -n1)" - if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then - NEED_INSTALL=true - else - if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then - $STD apt update || { - msg_error "Failed to update package list" - return 1 - } - $STD apt install --only-upgrade -y mysql-server || { - msg_error "Failed to upgrade MySQL" - return 1 - } - fi - return - fi - else - NEED_INSTALL=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" + + ensure_apt_working || return 1 + + $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 - if [[ "$NEED_INSTALL" == true ]]; then + # 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" - local SUITE - if [[ "$DISTRO_ID" == "debian" ]]; then - case "$DISTRO_CODENAME" in - bookworm | bullseye) SUITE="$DISTRO_CODENAME" ;; - trixie | forky | sid) SUITE="bookworm" ;; - *) SUITE="bookworm" ;; - esac - else - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}") + 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 - $STD systemctl stop mysql 2>/dev/null || true - if dpkg -l 2>/dev/null | grep -q "^ii.*mysql-server"; then - $STD apt purge -y mysql-server* mysql-client* mysql-common mysql-community-* 2>/dev/null || true - fi - - # --- Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64 --- - if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then - msg_info "Debian ${DISTRO_CODENAME} detected → using MySQL 8.4 LTS (libaio1t64)" - curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/keyrings/mysql.gpg - cat >/etc/apt/sources.list.d/mysql.sources <<'EOF' + cat >/etc/apt/sources.list.d/mysql.sources <<'EOF' Types: deb URIs: https://repo.mysql.com/apt/debian/ Suites: bookworm @@ -2586,56 +2858,80 @@ Components: mysql-8.4-lts 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 + } + + 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 - if ! $STD apt install -y mysql-community-server mysql-community-client; then - msg_error "MySQL 8.4 LTS installation failed – falling back to MariaDB" - $STD apt install -y mariadb-server mariadb-client - fi - msg_ok "Setup Database Engine (MySQL 8.4 LTS / MariaDB)" - return - fi - # ------------------------------------------------------------- - - setup_deb822_repo \ - "mysql" \ - "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" \ - "https://repo.mysql.com/apt/${DISTRO_ID}" \ - "$SUITE" \ - "mysql-${MYSQL_VERSION}" \ - "amd64 arm64" - - export DEBIAN_FRONTEND=noninteractive - if ! $STD apt update; then - msg_error "APT update failed for MySQL repository" - return 1 - fi - - 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 - fi - if [[ "$mysql_install_success" == false ]] && apt-cache search "^mysql-community-server$" 2>/dev/null | grep -q . && $STD apt install -y mysql-community-server mysql-community-client 2>/dev/null; then - mysql_install_success=true - fi - if [[ "$mysql_install_success" == false ]] && apt-cache search "^mysql$" 2>/dev/null | grep -q . && $STD apt install -y mysql 2>/dev/null; then - mysql_install_success=true - fi - if [[ "$mysql_install_success" == false ]]; then - msg_error "MySQL ${MYSQL_VERSION} package not available for suite ${SUITE}" - return 1 - fi - - if ! command -v mysql >/dev/null 2>&1; then - hash -r - if ! command -v mysql >/dev/null 2>&1; then - msg_error "MySQL installed but mysql command still not found" + $STD apt install -y mariadb-server mariadb-client || { + msg_error "Failed to install database engine (MySQL/MariaDB fallback)" return 1 - fi + } + msg_ok "Setup Database Engine (MariaDB fallback on Debian ${DISTRO_CODENAME})" + return 0 fi - cache_installed_version "mysql" "$MYSQL_VERSION" - msg_ok "Setup MySQL $MYSQL_VERSION" + 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 + if ! command -v mysql >/dev/null 2>&1; then + msg_error "MySQL installed but mysql command still not found" + return 1 + fi + fi + + cache_installed_version "mysql" "$MYSQL_VERSION" + msg_ok "Setup MySQL $MYSQL_VERSION" } # ------------------------------------------------------------------------------ @@ -2653,59 +2949,62 @@ EOF function setup_nodejs() { local NODE_VERSION="${NODE_VERSION:-22}" local NODE_MODULE="${NODE_MODULE:-}" + + # Get currently installed version local CURRENT_NODE_VERSION="" - local NEED_NODE_INSTALL=false - - # Check if Node.js is already installed - if command -v node >/dev/null; then - CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')" - if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then - msg_info "Old Node.js $CURRENT_NODE_VERSION found, replacing with $NODE_VERSION" - NEED_NODE_INSTALL=true - fi - else - msg_info "Setup Node.js $NODE_VERSION" - NEED_NODE_INSTALL=true - fi + 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-get update - $STD apt-get install -y jq || { + $STD apt update + $STD apt install -y jq || { msg_error "Failed to install jq" return 1 } fi - # Install Node.js if required - if [[ "$NEED_NODE_INSTALL" == true ]]; then - 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" - if [[ -n "$CURRENT_NODE_VERSION" ]]; then - $STD apt-get purge -y nodejs npm || true + 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" fi - cleanup_old_repo_files "nodesource" + ensure_dependencies curl ca-certificates gnupg - # NodeSource uses deb822 format with "nodistro" - setup_deb822_repo \ - "nodesource" \ - "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \ - "https://deb.nodesource.com/node_${NODE_VERSION}.x" \ - "nodistro" \ - "main" \ - "amd64 arm64" + # Setup repository + manage_tool_repository "nodejs" "$NODE_VERSION" "https://deb.nodesource.com/node_${NODE_VERSION}.x" "" || { + msg_error "Failed to setup Node.js repository" + return 1 + } + # Wait for repo to settle sleep 2 - if ! apt-get update >/dev/null 2>&1; then + + # Install Node.js + if ! apt update >/dev/null 2>&1; then msg_warn "APT update failed – retrying in 5s" sleep 5 - if ! apt-get update >/dev/null 2>&1; then + if ! apt update >/dev/null 2>&1; then msg_error "Failed to update APT repositories after adding NodeSource" return 1 fi fi - if ! apt-get install -y nodejs >/dev/null 2>&1; then + if ! apt install -y nodejs >/dev/null 2>&1; then msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" return 1 fi @@ -2713,10 +3012,11 @@ function setup_nodejs() { # 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}" + msg_ok "Setup Node.js $NODE_VERSION" fi export NODE_OPTIONS="--max-old-space-size=4096" @@ -2733,6 +3033,7 @@ function setup_nodejs() { # 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 @@ -2754,26 +3055,33 @@ function setup_nodejs() { MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')" if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION" - if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then - msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" - return 1 + 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"; then - msg_error "Failed to update $MODULE_NAME to latest version" - return 1 + 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}"; then - msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" - return 1 + 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 - msg_ok "Installed Node.js modules: $NODE_MODULE" + 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 } @@ -2825,47 +3133,46 @@ function setup_php() { # Get current PHP-CLI version local CURRENT_PHP="" - if command -v php >/dev/null 2>&1; then - CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) - fi + CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true - msg_info "Setup PHP $PHP_VERSION" + # Scenario 1: Already at target version - just update packages + if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" == "$PHP_VERSION" ]]; then + msg_info "Update PHP $PHP_VERSION" - if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then - $STD apt purge -y "php${CURRENT_PHP//./}"* || true - fi + # Ensure Sury repo is available + if [[ ! -f /etc/apt/sources.list.d/php.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 Sury repo is available - if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then - # Cleanup old repository files - cleanup_old_repo_files "php" + ensure_apt_working || return 1 - $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb || { - msg_error "Failed to download PHP repository keyring" - return 1 - } - $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb || { - msg_error "Failed to install PHP repository keyring" - rm -f /tmp/debsuryorg-archive-keyring.deb + # 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 } - # Use helper function to get fallback suite - local SUITE - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.sury.org/php") - - cat </etc/apt/sources.list.d/php.sources -Types: deb -URIs: https://packages.sury.org/php/ -Suites: $SUITE -Components: main -Architectures: amd64 arm64 -Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg -EOF - $STD apt update || { - msg_error "APT update failed for PHP repository" - return 1 - } + ensure_apt_working || return 1 fi # Build module list @@ -2890,20 +3197,14 @@ EOF fi fi - # setup / update PHP modules + # Install PHP packages $STD apt install -y $MODULE_LIST || { msg_error "Failed to install PHP packages" return 1 } cache_installed_version "php" "$PHP_VERSION" - # optional stop old PHP-FPM service - if [[ "$PHP_FPM" == "YES" && -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then - $STD systemctl stop php"${CURRENT_PHP}"-fpm || true - $STD systemctl disable php"${CURRENT_PHP}"-fpm || true - fi - - # Patch all relevant php.ini files (silent) + # 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") @@ -2916,7 +3217,7 @@ EOF fi done - # patch Apache configuration if needed + # 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 @@ -2928,7 +3229,7 @@ EOF safe_service_restart apache2 || true fi - # enable and restart PHP-FPM if requested + # 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 @@ -2951,120 +3252,136 @@ EOF # # Variables: # PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16) -# PG_MODULES - Comma-separated list of extensions (e.g. "postgis,contrib") -# ------------------------------------------------------------------------------ function setup_postgresql() { local PG_VERSION="${PG_VERSION:-16}" local PG_MODULES="${PG_MODULES:-}" - local CURRENT_PG_VERSION="" local DISTRO_ID DISTRO_CODENAME - local NEED_PG_INSTALL=false 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 | awk '{print $3}' | cut -d. -f1)" - [[ "$CURRENT_PG_VERSION" != "$PG_VERSION" ]] && NEED_PG_INSTALL=true - else - NEED_PG_INSTALL=true + CURRENT_PG_VERSION="$(psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1)" fi - if [[ "$NEED_PG_INSTALL" == true ]]; then - msg_info "Setup PostgreSQL $PG_VERSION" - - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD runuser -u postgres -- pg_dumpall >/var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql - $STD systemctl stop postgresql - fi - - # Cleanup old repository files - cleanup_old_repo_files "pgdg" - - # PostgreSQL PGDG repository uses special suite naming - # For unstable/testing Debian, we need to check what's actually available - local SUITE - case "$DISTRO_CODENAME" in - trixie | forky | sid) - # Try trixie-pgdg first, fallback to bookworm-pgdg if not available - if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then - SUITE="trixie-pgdg" - else - SUITE="bookworm-pgdg" - fi - ;; - *) - # Use helper function for stable releases - SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") - # PGDG uses special suite naming: ${SUITE}-pgdg - SUITE="${SUITE}-pgdg" - ;; - esac - - # Use standardized repo setup - setup_deb822_repo \ - "pgdg" \ - "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ - "https://apt.postgresql.org/pub/repos/apt" \ - "$SUITE" \ - "main" \ - "amd64 arm64" - - # Update apt - if ! $STD apt update; then - msg_error "APT update failed for PostgreSQL repository" - return 1 - fi - - # Try multiple PostgreSQL package patterns - local pg_install_success=false - - # First, ensure ssl-cert is available (PostgreSQL dependency) - if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then - $STD apt install -y ssl-cert 2>/dev/null || true - fi - - # First try: postgresql-X (common in most repos) - if apt-cache search "^postgresql-${PG_VERSION}$" 2>/dev/null | grep -q . && $STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then - pg_install_success=true - fi - - # Second try: postgresql-server-X (some repos use this) - if [[ "$pg_install_success" == false ]] && apt-cache search "^postgresql-server-${PG_VERSION}$" 2>/dev/null | grep -q . && $STD apt install -y "postgresql-server-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then - pg_install_success=true - fi - - # Third try: just postgresql (any version) - if [[ "$pg_install_success" == false ]] && apt-cache search "^postgresql$" 2>/dev/null | grep -q . && $STD apt install -y postgresql postgresql-client 2>/dev/null; then - pg_install_success=true - fi - - if [[ "$pg_install_success" == false ]]; then - msg_error "PostgreSQL package not available for suite ${SUITE}" - return 1 - fi - - # Verify installation - if ! command -v psql >/dev/null 2>&1; then - msg_error "PostgreSQL installed but psql command not found" - return 1 - fi - - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" 2>/dev/null || true - $STD runuser -u postgres -- psql /dev/null || true - fi - - $STD systemctl enable --now postgresql 2>/dev/null || true - - # Add PostgreSQL binaries to PATH for the install script - if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then - echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/'"${PG_VERSION}"'/bin"' >/etc/environment - fi - + # 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 "Setup 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 + 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 3: Fresh install or after removal - setup repo and install + cleanup_old_repo_files "pgdg" + + 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 + + setup_deb822_repo \ + "pgdg" \ + "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ + "https://apt.postgresql.org/pub/repos/apt" \ + "$SUITE" \ + "main" \ + "amd64 arm64" + + if ! $STD apt update; then + msg_error "APT update failed for PostgreSQL repository" + return 1 + 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 + + # Try multiple PostgreSQL package patterns + local pg_install_success=false + + 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 ]] && \ + 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 [[ "$pg_install_success" == false ]] && \ + apt-cache search "^postgresql$" 2>/dev/null | grep -q . && \ + $STD apt install -y postgresql postgresql-client 2>/dev/null; then + pg_install_success=true + fi + + if [[ "$pg_install_success" == false ]]; then + msg_error "PostgreSQL package not available for suite ${SUITE}" + return 1 + fi + + if ! command -v psql >/dev/null 2>&1; then + msg_error "PostgreSQL installed but psql command not found" + return 1 + 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 + + $STD systemctl enable --now postgresql 2>/dev/null || true + + # 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 @@ -3093,16 +3410,31 @@ function setup_ruby() { local RBENV_BIN="$RBENV_DIR/bin/rbenv" local PROFILE_FILE="$HOME/.profile" local TMP_DIR=$(mktemp -d) - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "ruby") - msg_info "Setup Ruby $RUBY_VERSION" + # 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 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 is working before installing dependencies ensure_apt_working || return 1 - # Install build dependencies - with fallback for different Debian versions - # In Trixie: libreadline6-dev -> libreadline-dev, libncurses5-dev -> libncurses-dev + # Install build dependencies with fallbacks local ruby_deps=() local dep_variations=( "jq" @@ -3123,13 +3455,10 @@ function setup_ruby() { for dep_pattern in "${dep_variations[@]}"; do if [[ "$dep_pattern" == *"|"* ]]; then - # Try multiple variations IFS='|' read -ra variations <<<"$dep_pattern" - local found=false for var in "${variations[@]}"; do if apt-cache search "^${var}$" 2>/dev/null | grep -q .; then ruby_deps+=("$var") - found=true break fi done @@ -3144,70 +3473,76 @@ function setup_ruby() { $STD apt install -y "${ruby_deps[@]}" 2>/dev/null || true else msg_error "No Ruby build dependencies available" - return 1 - fi - - local RBENV_RELEASE - RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | jq -r '.tag_name' | sed 's/^v//') - if [[ -z "$RBENV_RELEASE" ]]; then - msg_error "Failed to fetch latest rbenv version" rm -rf "$TMP_DIR" return 1 fi - curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" || { - msg_error "Failed to download rbenv" - rm -rf "$TMP_DIR" - return 1 - } + # Download and build rbenv if needed + if [[ ! -x "$RBENV_BIN" ]]; then + local RBENV_RELEASE + RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest 2>/dev/null | jq -r '.tag_name' | sed 's/^v//') || { + msg_error "Failed to fetch latest rbenv version" + 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 - } + 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 + } - 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 - } + tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract rbenv" + rm -rf "$TMP_DIR" + return 1 + } - local RUBY_BUILD_RELEASE - RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | jq -r '.tag_name' | sed 's/^v//') - if [[ -z "$RUBY_BUILD_RELEASE" ]]; then - msg_error "Failed to fetch latest ruby-build version" - rm -rf "$TMP_DIR" - return 1 + 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 - 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 - } + # Install ruby-build plugin + if [[ ! -d "$RBENV_DIR/plugins/ruby-build" ]]; then + local RUBY_BUILD_RELEASE + RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest 2>/dev/null | jq -r '.tag_name' | sed 's/^v//') || { + msg_error "Failed to fetch latest ruby-build version" + 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 - } + 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 + } - mkdir -p "$RBENV_DIR/plugins/ruby-build" - cp -r "$TMP_DIR/ruby-build-${RUBY_BUILD_RELEASE}/." "$RBENV_DIR/plugins/ruby-build/" - echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt" + tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract ruby-build" + rm -rf "$TMP_DIR" + return 1 + } - if ! grep -q 'rbenv init' "$PROFILE_FILE"; then - echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE" - echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE" + 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)" + eval "$("$RBENV_BIN" init - bash)" 2>/dev/null || true - if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then + 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" @@ -3215,14 +3550,18 @@ function setup_ruby() { } fi - "$RBENV_BIN" global "$RUBY_VERSION" + "$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_error "Failed to install Rails" - rm -rf "$TMP_DIR" - return 1 + msg_warn "Failed to install Rails - Ruby installation successful" } fi @@ -3250,100 +3589,68 @@ function setup_clickhouse() { DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) - # Determine latest version if needed + # Resolve "latest" version if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then - # Try multiple methods to get the latest version CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null | grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | - sort -V | - tail -n1) - - # Fallback: Try GitHub releases API - if [[ -z "$CLICKHOUSE_VERSION" ]]; then + sort -V | tail -n1) || { CLICKHOUSE_VERSION=$(curl -fsSL https://api.github.com/repos/ClickHouse/ClickHouse/releases/latest 2>/dev/null | - grep -oP '"tag_name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | - head -n1) - fi + grep -oP '"tag_name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1) + } - # Final fallback: Parse HTML more liberally - if [[ -z "$CLICKHOUSE_VERSION" ]]; then - CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null | - grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(?=/clickhouse)' | - sort -V | - tail -n1) - fi - - if [[ -z "$CLICKHOUSE_VERSION" ]]; then + [[ -z "$CLICKHOUSE_VERSION" ]] && { msg_error "Could not determine latest ClickHouse version" return 1 - fi + } 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 - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "clickhouse") - - # Check if already at target version - if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then - upgrade_package clickhouse-server - upgrade_package clickhouse-client - else - msg_info "Setup ClickHouse $CLICKHOUSE_VERSION" - $STD apt update || { - msg_error "Failed to update package list" - return 1 - } - $STD apt install --only-upgrade -y clickhouse-server clickhouse-client || { - msg_error "Failed to upgrade ClickHouse" - return 1 - } - cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" - msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION" - fi + # 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 - msg_info "Setup ClickHouse $CLICKHOUSE_VERSION" - - # Stop existing service if upgrading - if [[ -n "$CURRENT_VERSION" ]]; then - $STD systemctl stop clickhouse-server || true + # 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 - # Cleanup old repository files - cleanup_old_repo_files "clickhouse" - - # Ensure dependencies ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg - # ClickHouse uses 'stable' instead of distro codenames - local SUITE="stable" - - # Use standardized repo setup + # Setup repository (ClickHouse uses 'stable' suite) setup_deb822_repo \ "clickhouse" \ "https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \ "https://packages.clickhouse.com/deb" \ - "$SUITE" \ + "stable" \ "main" \ "amd64 arm64" - # Update and install ClickHouse packages + # Install packages export DEBIAN_FRONTEND=noninteractive - if ! $STD apt update; then + $STD apt update || { msg_error "APT update failed for ClickHouse repository" return 1 - fi + } - if ! $STD apt install -y clickhouse-server clickhouse-client; then + $STD apt install -y clickhouse-server clickhouse-client || { msg_error "Failed to install ClickHouse packages" return 1 - fi + } # Verify installation if ! command -v clickhouse-server >/dev/null 2>&1; then @@ -3351,10 +3658,8 @@ function setup_clickhouse() { return 1 fi - # Create data directory if it doesn't exist + # Setup data directory mkdir -p /var/lib/clickhouse - - # Check if clickhouse user exists before chown if id clickhouse >/dev/null 2>&1; then chown -R clickhouse:clickhouse /var/lib/clickhouse fi @@ -3388,11 +3693,16 @@ function setup_rust() { local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}" local RUST_CRATES="${RUST_CRATES:-}" local CARGO_BIN="${HOME}/.cargo/bin" - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "rust") + # 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" + 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 @@ -3403,21 +3713,23 @@ function setup_rust() { cache_installed_version "rust" "$RUST_VERSION" msg_ok "Setup Rust $RUST_VERSION" else - msg_info "Setup Rust" + # 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" + 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" + $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 "Setup 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 @@ -3487,30 +3799,32 @@ function setup_uv() { ensure_dependencies jq local LATEST_VERSION - LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | - jq -r '.tag_name' | sed 's/^v//') - - if [[ -z "$LATEST_VERSION" ]]; then + LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest 2>/dev/null | + jq -r '.tag_name' | sed 's/^v//') || { msg_error "Could not fetch latest uv version" rm -rf "$TMP_DIR" return 1 - fi + } + # Get currently installed version + local INSTALLED_VERSION="" if [[ -x "$UV_BIN" ]]; then - local INSTALLED_VERSION INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}') - if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then - rm -rf "$TMP_DIR" - return 0 - fi - cache_installed_version "uv" "$LATEST_VERSION" - rm -rf "$TMP_DIR" - return 0 - fi fi - msg_info "Setup uv $LATEST_VERSION" + # 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" || { @@ -3539,9 +3853,10 @@ function setup_uv() { 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 | + 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) @@ -3550,7 +3865,7 @@ function setup_uv() { return 1 fi - if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then + 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 @@ -3572,42 +3887,45 @@ function setup_yq() { local TMP_DIR=$(mktemp -d) local BINARY_PATH="/usr/local/bin/yq" local GITHUB_REPO="mikefarah/yq" - local CURRENT_VERSION="" - local CACHED_VERSION - CACHED_VERSION=$(get_cached_version "yq") 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)" - else - CURRENT_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') fi fi local LATEST_VERSION - LATEST_VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | - jq -r '.tag_name' | sed 's/^v//') - - if [[ -z "$LATEST_VERSION" ]]; then + LATEST_VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | + jq -r '.tag_name' | sed 's/^v//') || { msg_error "Could not determine latest yq version" rm -rf "$TMP_DIR" return 1 + } + + # 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 - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then - if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then - rm -rf "$TMP_DIR" - return 0 - 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 - msg_info "Setup yq $LATEST_VERSION" + # 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" @@ -3615,13 +3933,11 @@ function setup_yq() { } chmod +x "$TMP_DIR/yq" - mv "$TMP_DIR/yq" "$BINARY_PATH" - - if [[ ! -x "$BINARY_PATH" ]]; then + mv "$TMP_DIR/yq" "$BINARY_PATH" || { msg_error "Failed to install yq" rm -rf "$TMP_DIR" return 1 - fi + } rm -rf "$TMP_DIR" hash -r