ProxmoxVED/misc/tools.func
CanbiZ e03359fc80
Some checks failed
Bump build.func Revision / bump-revision (push) Has been cancelled
fixes
2025-10-16 22:11:15 +02:00

3044 lines
101 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# ==============================================================================
# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# Cache installed version to avoid repeated checks
# ------------------------------------------------------------------------------
cache_installed_version() {
local app="$1"
local version="$2"
mkdir -p /var/cache/app-versions
echo "$version" >"/var/cache/app-versions/${app}_version.txt"
}
get_cached_version() {
local app="$1"
mkdir -p /var/cache/app-versions
[[ -f "/var/cache/app-versions/${app}_version.txt" ]] && cat "/var/cache/app-versions/${app}_version.txt"
}
# ------------------------------------------------------------------------------
# Unified package upgrade function (with apt update caching)
# ------------------------------------------------------------------------------
upgrade_package() {
local package="$1"
msg_info "Upgrading $package"
# Use same caching logic as ensure_dependencies
local apt_cache_file="/var/cache/apt-update-timestamp"
local current_time=$(date +%s)
local last_update=0
if [[ -f "$apt_cache_file" ]]; then
last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
fi
if ((current_time - last_update > 300)); then
$STD apt update
echo "$current_time" >"$apt_cache_file"
fi
$STD apt install --only-upgrade -y "$package"
msg_ok "Upgraded $package"
}
# ------------------------------------------------------------------------------
# Repository availability check
# ------------------------------------------------------------------------------
verify_repo_available() {
local repo_url="$1"
local suite="$2"
if curl -fsSL --max-time 10 "${repo_url}/dists/${suite}/Release" &>/dev/null; then
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# Ensure dependencies are installed (with apt update caching)
# ------------------------------------------------------------------------------
ensure_dependencies() {
local deps=("$@")
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" &>/dev/null && ! is_package_installed "$dep"; then
missing+=("$dep")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
msg_info "Installing dependencies: ${missing[*]}"
# 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
$STD apt update
echo "$current_time" >"$apt_cache_file"
fi
$STD apt install -y "${missing[@]}"
msg_ok "Installed dependencies"
fi
}
# ------------------------------------------------------------------------------
# Smart version comparison
# ------------------------------------------------------------------------------
version_gt() {
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
}
# ------------------------------------------------------------------------------
# Get system architecture (normalized)
# ------------------------------------------------------------------------------
get_system_arch() {
local arch_type="${1:-dpkg}" # dpkg, uname, or both
local arch
case "$arch_type" in
dpkg)
arch=$(dpkg --print-architecture 2>/dev/null)
;;
uname)
arch=$(uname -m)
[[ "$arch" == "x86_64" ]] && arch="amd64"
[[ "$arch" == "aarch64" ]] && arch="arm64"
;;
both | *)
arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
[[ "$arch" == "x86_64" ]] && arch="amd64"
[[ "$arch" == "aarch64" ]] && arch="arm64"
;;
esac
echo "$arch"
}
# ------------------------------------------------------------------------------
# Create temporary directory with automatic cleanup
# ------------------------------------------------------------------------------
create_temp_dir() {
local tmp_dir=$(mktemp -d)
# Set trap to cleanup on EXIT, ERR, INT, TERM
trap "rm -rf '$tmp_dir'" EXIT ERR INT TERM
echo "$tmp_dir"
}
# ------------------------------------------------------------------------------
# Check if package is installed (faster than dpkg -l | grep)
# ------------------------------------------------------------------------------
is_package_installed() {
local package="$1"
dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "^install ok installed$"
}
# ------------------------------------------------------------------------------
# GitHub API call with authentication and rate limit handling
# ------------------------------------------------------------------------------
github_api_call() {
local url="$1"
local output_file="${2:-/dev/stdout}"
local max_retries=3
local retry_delay=2
local header_args=()
[[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
for attempt in $(seq 1 $max_retries); do
local http_code
http_code=$(curl -fsSL -w "%{http_code}" -o "$output_file" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${header_args[@]}" \
"$url" 2>/dev/null || echo "000")
case "$http_code" in
200)
return 0
;;
403)
# Rate limit - check if we can retry
if [[ $attempt -lt $max_retries ]]; then
msg_warn "GitHub API rate limit, waiting ${retry_delay}s... (attempt $attempt/$max_retries)"
sleep "$retry_delay"
retry_delay=$((retry_delay * 2))
continue
fi
msg_error "GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits."
return 1
;;
404)
msg_error "GitHub API endpoint not found: $url"
return 1
;;
*)
if [[ $attempt -lt $max_retries ]]; then
sleep "$retry_delay"
continue
fi
msg_error "GitHub API call failed with HTTP $http_code"
return 1
;;
esac
done
return 1
}
should_upgrade() {
local current="$1"
local target="$2"
[[ -z "$current" ]] && return 0
version_gt "$target" "$current" && return 0
return 1
}
# ------------------------------------------------------------------------------
# Get OS information (cached for performance)
# ------------------------------------------------------------------------------
get_os_info() {
local field="${1:-all}" # id, codename, version, version_id, all
# Cache OS info to avoid repeated file reads
if [[ -z "${_OS_ID:-}" ]]; then
export _OS_ID=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
export _OS_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
export _OS_VERSION=$(awk -F= '/^VERSION_ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
export _OS_VERSION_FULL=$(awk -F= '/^VERSION=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
fi
case "$field" in
id) echo "$_OS_ID" ;;
codename) echo "$_OS_CODENAME" ;;
version) echo "$_OS_VERSION" ;;
version_id) echo "$_OS_VERSION" ;;
version_full) echo "$_OS_VERSION_FULL" ;;
all) echo "ID=$_OS_ID CODENAME=$_OS_CODENAME VERSION=$_OS_VERSION" ;;
*) echo "$_OS_ID" ;;
esac
}
# ------------------------------------------------------------------------------
# Check if running on specific OS
# ------------------------------------------------------------------------------
is_debian() {
[[ "$(get_os_info id)" == "debian" ]]
}
is_ubuntu() {
[[ "$(get_os_info id)" == "ubuntu" ]]
}
is_alpine() {
[[ "$(get_os_info id)" == "alpine" ]]
}
# ------------------------------------------------------------------------------
# Get Debian/Ubuntu major version
# ------------------------------------------------------------------------------
get_os_version_major() {
local version=$(get_os_info version)
echo "${version%%.*}"
}
# ------------------------------------------------------------------------------
# Download file with retry logic and progress
# ------------------------------------------------------------------------------
download_file() {
local url="$1"
local output="$2"
local max_retries="${3:-3}"
local show_progress="${4:-false}"
local curl_opts=(-fsSL)
[[ "$show_progress" == "true" ]] && curl_opts=(-fL#)
for attempt in $(seq 1 $max_retries); do
if curl "${curl_opts[@]}" -o "$output" "$url"; then
return 0
fi
if [[ $attempt -lt $max_retries ]]; then
msg_warn "Download failed, retrying... (attempt $attempt/$max_retries)"
sleep 2
fi
done
msg_error "Failed to download: $url"
return 1
}
# ------------------------------------------------------------------------------
# Get fallback suite for repository (comprehensive mapping)
# ------------------------------------------------------------------------------
get_fallback_suite() {
local distro_id="$1"
local distro_codename="$2"
local repo_base_url="$3"
# Check if current codename works
if verify_repo_available "$repo_base_url" "$distro_codename"; then
echo "$distro_codename"
return 0
fi
# Comprehensive fallback mappings
case "$distro_id" in
debian)
case "$distro_codename" in
# Debian 13 (Trixie) → Debian 12 (Bookworm)
trixie | forky | sid)
echo "bookworm"
;;
# Debian 12 (Bookworm) stays
bookworm)
echo "bookworm"
;;
# Debian 11 (Bullseye) stays
bullseye)
echo "bullseye"
;;
# Unknown → latest stable
*)
echo "bookworm"
;;
esac
;;
ubuntu)
case "$distro_codename" in
# Ubuntu 24.10 (Oracular) → 24.04 LTS (Noble)
oracular | plucky)
echo "noble"
;;
# Ubuntu 24.04 LTS (Noble) stays
noble)
echo "noble"
;;
# Ubuntu 23.10 (Mantic) → 22.04 LTS (Jammy)
mantic | lunar)
echo "jammy"
;;
# Ubuntu 22.04 LTS (Jammy) stays
jammy)
echo "jammy"
;;
# Ubuntu 20.04 LTS (Focal) stays
focal)
echo "focal"
;;
# Unknown → latest LTS
*)
echo "jammy"
;;
esac
;;
*)
echo "$distro_codename"
;;
esac
}
# ------------------------------------------------------------------------------
# Verify package source and version
# ------------------------------------------------------------------------------
verify_package_source() {
local package="$1"
local expected_version="$2"
if apt-cache policy "$package" 2>/dev/null | grep -q "$expected_version"; then
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# Check if running on LTS version
# ------------------------------------------------------------------------------
is_lts_version() {
local os_id=$(get_os_info id)
local codename=$(get_os_info codename)
if [[ "$os_id" == "ubuntu" ]]; then
case "$codename" in
focal | jammy | noble) return 0 ;; # 20.04, 22.04, 24.04
*) return 1 ;;
esac
elif [[ "$os_id" == "debian" ]]; then
# Debian releases are all "stable"
case "$codename" in
bullseye | bookworm | trixie) return 0 ;;
*) return 1 ;;
esac
fi
return 1
}
# ------------------------------------------------------------------------------
# Get optimal number of parallel jobs (cached)
# ------------------------------------------------------------------------------
get_parallel_jobs() {
if [[ -z "${_PARALLEL_JOBS:-}" ]]; then
local cpu_count=$(nproc 2>/dev/null || echo 1)
local mem_gb=$(free -g | awk '/^Mem:/{print $2}')
# Limit by available memory (assume 1GB per job for compilation)
local max_by_mem=$((mem_gb > 0 ? mem_gb : 1))
local max_jobs=$((cpu_count < max_by_mem ? cpu_count : max_by_mem))
# At least 1, at most cpu_count
export _PARALLEL_JOBS=$((max_jobs > 0 ? max_jobs : 1))
fi
echo "$_PARALLEL_JOBS"
}
# ------------------------------------------------------------------------------
# Get default PHP version for OS
# ------------------------------------------------------------------------------
get_default_php_version() {
local os_id=$(get_os_info id)
local os_version=$(get_os_version_major)
case "$os_id" in
debian)
case "$os_version" in
13) echo "8.3" ;; # Debian 13 (Trixie)
12) echo "8.2" ;; # Debian 12 (Bookworm)
11) echo "7.4" ;; # Debian 11 (Bullseye)
*) echo "8.2" ;;
esac
;;
ubuntu)
case "$os_version" in
24) echo "8.3" ;; # Ubuntu 24.04 LTS (Noble)
22) echo "8.1" ;; # Ubuntu 22.04 LTS (Jammy)
20) echo "7.4" ;; # Ubuntu 20.04 LTS (Focal)
*) echo "8.1" ;;
esac
;;
*)
echo "8.2"
;;
esac
}
# ------------------------------------------------------------------------------
# Get default Python version for OS
# ------------------------------------------------------------------------------
get_default_python_version() {
local os_id=$(get_os_info id)
local os_version=$(get_os_version_major)
case "$os_id" in
debian)
case "$os_version" in
13) echo "3.12" ;; # Debian 13 (Trixie)
12) echo "3.11" ;; # Debian 12 (Bookworm)
11) echo "3.9" ;; # Debian 11 (Bullseye)
*) echo "3.11" ;;
esac
;;
ubuntu)
case "$os_version" in
24) echo "3.12" ;; # Ubuntu 24.04 LTS
22) echo "3.10" ;; # Ubuntu 22.04 LTS
20) echo "3.8" ;; # Ubuntu 20.04 LTS
*) echo "3.10" ;;
esac
;;
*)
echo "3.11"
;;
esac
}
# ------------------------------------------------------------------------------
# Get default Node.js LTS version
# ------------------------------------------------------------------------------
get_default_nodejs_version() {
# Always return current LTS (as of 2025)
echo "20"
}
# ------------------------------------------------------------------------------
# Check if package manager is locked
# ------------------------------------------------------------------------------
is_apt_locked() {
if fuser /var/lib/dpkg/lock-frontend &>/dev/null ||
fuser /var/lib/apt/lists/lock &>/dev/null ||
fuser /var/cache/apt/archives/lock &>/dev/null; then
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# Wait for apt to be available
# ------------------------------------------------------------------------------
wait_for_apt() {
local max_wait="${1:-300}" # 5 minutes default
local waited=0
while is_apt_locked; do
if [[ $waited -ge $max_wait ]]; then
msg_error "Timeout waiting for apt to be available"
return 1
fi
debug_log "Waiting for apt to be available... (${waited}s)"
sleep 5
waited=$((waited + 5))
done
return 0
}
# ------------------------------------------------------------------------------
# Cleanup old repository files (migration helper)
# ------------------------------------------------------------------------------
cleanup_old_repo_files() {
local app="$1"
# Remove old-style .list files (including backups)
rm -f /etc/apt/sources.list.d/"${app}"*.list
rm -f /etc/apt/sources.list.d/"${app}"*.list.save
rm -f /etc/apt/sources.list.d/"${app}"*.list.distUpgrade
rm -f /etc/apt/sources.list.d/"${app}"*.list.dpkg-*
# Remove old GPG keys from trusted.gpg.d
rm -f /etc/apt/trusted.gpg.d/"${app}"*.gpg
# Remove duplicate .sources files (keep only the main one)
local sources_file="/etc/apt/sources.list.d/${app}.sources"
if [[ -f "$sources_file" ]]; then
find /etc/apt/sources.list.d/ -name "${app}*.sources" ! -name "${app}.sources" -delete 2>/dev/null || true
fi
}
# ------------------------------------------------------------------------------
# Standardized deb822 repository setup
# ------------------------------------------------------------------------------
setup_deb822_repo() {
local name="$1"
local gpg_url="$2"
local repo_url="$3"
local suite="$4"
local component="${5:-main}"
local architectures="${6:-amd64 arm64}"
msg_info "Setting up $name repository"
# Cleanup old configs
cleanup_old_repo_files "$name"
# Ensure keyring directory exists
mkdir -p /etc/apt/keyrings
# Download GPG key
curl -fsSL "$gpg_url" | gpg --dearmor -o "/etc/apt/keyrings/${name}.gpg"
# Create deb822 sources file
cat <<EOF >/etc/apt/sources.list.d/${name}.sources
Types: deb
URIs: $repo_url
Suites: $suite
Components: $component
Architectures: $architectures
Signed-By: /etc/apt/keyrings/${name}.gpg
EOF
# Use cached apt update
local apt_cache_file="/var/cache/apt-update-timestamp"
local current_time=$(date +%s)
local last_update=0
if [[ -f "$apt_cache_file" ]]; then
last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
fi
# For repo changes, always update but respect short-term cache (30s)
if ((current_time - last_update > 30)); then
$STD apt update
echo "$current_time" >"$apt_cache_file"
fi
msg_ok "Set up $name repository"
}
# ------------------------------------------------------------------------------
# Package version hold/unhold helpers
# ------------------------------------------------------------------------------
hold_package_version() {
local package="$1"
$STD apt-mark hold "$package"
}
unhold_package_version() {
local package="$1"
$STD apt-mark unhold "$package"
}
# ------------------------------------------------------------------------------
# Safe service restart with verification
# ------------------------------------------------------------------------------
safe_service_restart() {
local service="$1"
if systemctl is-active --quiet "$service"; then
$STD systemctl restart "$service"
else
$STD systemctl start "$service"
fi
if ! systemctl is-active --quiet "$service"; then
msg_error "Failed to start $service"
systemctl status "$service" --no-pager
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# Enable and start service (with error handling)
# ------------------------------------------------------------------------------
enable_and_start_service() {
local service="$1"
if ! systemctl enable "$service" &>/dev/null; then
msg_warn "Could not enable $service (may not be installed yet)"
fi
if ! systemctl start "$service" &>/dev/null; then
msg_error "Failed to start $service"
systemctl status "$service" --no-pager
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# Check if service is enabled
# ------------------------------------------------------------------------------
is_service_enabled() {
local service="$1"
systemctl is-enabled --quiet "$service" 2>/dev/null
}
# ------------------------------------------------------------------------------
# Check if service is running
# ------------------------------------------------------------------------------
is_service_running() {
local service="$1"
systemctl is-active --quiet "$service" 2>/dev/null
}
# ------------------------------------------------------------------------------
# Extract version from JSON (GitHub releases)
# ------------------------------------------------------------------------------
extract_version_from_json() {
local json="$1"
local field="${2:-tag_name}"
local strip_v="${3:-true}"
ensure_dependencies jq
local version
version=$(echo "$json" | jq -r ".${field} // empty")
if [[ -z "$version" ]]; then
return 1
fi
if [[ "$strip_v" == "true" ]]; then
echo "${version#v}"
else
echo "$version"
fi
}
# ------------------------------------------------------------------------------
# Get latest GitHub release version
# ------------------------------------------------------------------------------
get_latest_github_release() {
local repo="$1"
local strip_v="${2:-true}"
local temp_file=$(mktemp)
if ! github_api_call "https://api.github.com/repos/${repo}/releases/latest" "$temp_file"; then
rm -f "$temp_file"
return 1
fi
local version
version=$(extract_version_from_json "$(cat "$temp_file")" "tag_name" "$strip_v")
rm -f "$temp_file"
if [[ -z "$version" ]]; then
return 1
fi
echo "$version"
}
# ------------------------------------------------------------------------------
# Debug logging (only if DEBUG=1)
# ------------------------------------------------------------------------------
debug_log() {
[[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2
}
# ------------------------------------------------------------------------------
# Performance timing helper
# ------------------------------------------------------------------------------
start_timer() {
echo $(date +%s)
}
end_timer() {
local start_time="$1"
local label="${2:-Operation}"
local end_time=$(date +%s)
local duration=$((end_time - start_time))
debug_log "$label took ${duration}s"
}
# ------------------------------------------------------------------------------
# GPG key fingerprint verification
# ------------------------------------------------------------------------------
verify_gpg_fingerprint() {
local key_file="$1"
local expected_fingerprint="$2"
local actual_fingerprint
actual_fingerprint=$(gpg --show-keys --with-fingerprint --with-colons "$key_file" 2>&1 | grep -m1 '^fpr:' | cut -d: -f10)
if [[ "$actual_fingerprint" == "$expected_fingerprint" ]]; then
return 0
fi
msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint"
return 1
}
# ==============================================================================
# EXISTING FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# Checks for new GitHub release (latest tag).
#
# Description:
# - Queries the GitHub API for the latest release tag
# - Compares it to a local cached version (~/.<app>)
# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
#
# Usage:
# if check_for_gh_release "flaresolverr" "FlareSolverr/FlareSolverr" [optional] "v1.1.1"; then
# # trigger update...
# fi
# exit 0
# } (end of update_script not from the function)
#
# Notes:
# - Requires `jq` (auto-installed if missing)
# - Does not modify anything, only checks version state
# - 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}"
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]}"
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
msg_info "${app} pinned to ${pinned_version_in} (installed ${current:-none}) → update required"
CHECK_UPDATE_RELEASE="$match_raw"
return 0
fi
if [[ "$pin_clean" == "$latest_clean" ]]; then
msg_ok "${app} pinned to ${pinned_version_in} (up to date)"
else
msg_ok "${app} pinned to ${pinned_version_in} (already installed, upstream ${latest_raw})"
fi
return 1
fi
# No pinning → use latest
if [[ -z "$current" || "$current" != "$latest_clean" ]]; then
CHECK_UPDATE_RELEASE="$latest_raw"
msg_info "New release available: ${latest_raw} (current: v${current:-none})"
return 0
fi
msg_ok "${app} is up to date (${latest_raw})"
return 1
}
# ------------------------------------------------------------------------------
# Creates and installs self-signed certificates.
#
# Description:
# - Create a self-signed certificate with option to override application name
#
# Variables:
# 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"
if [[ -f "$CERT_CRT" && -f "$CERT_KEY" ]]; then
return 0
fi
$STD apt update
$STD apt install -y openssl
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"
chmod 600 "$CERT_KEY"
chmod 644 "$CERT_CRT"
}
# ------------------------------------------------------------------------------
# Downloads file with optional progress indicator using pv.
#
# Arguments:
# $1 - URL
# $2 - Destination path
# ------------------------------------------------------------------------------
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
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)
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
}
# ------------------------------------------------------------------------------
# Ensures /usr/local/bin is permanently in system PATH.
#
# Description:
# - Adds to /etc/profile.d if not present
# ------------------------------------------------------------------------------
function ensure_usr_local_bin_persist() {
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
}
# ------------------------------------------------------------------------------
# Downloads and deploys latest GitHub release (source, binary, tarball, asset).
#
# Description:
# - Fetches latest release metadata from GitHub API
# - Supports the following modes:
# - tarball: Source code tarball (default if omitted)
# - source: Alias for tarball (same behavior)
# - binary: .deb package install (arch-dependent)
# - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries)
# - singlefile: Standalone binary (no archive, direct chmod +x install)
# - Handles download, extraction/installation and version tracking in ~/.<app>
#
# Parameters:
# $1 APP - Application name (used for install path and version file)
# $2 REPO - GitHub repository in form user/repo
# $3 MODE - Release type:
# tarball → source tarball (.tar.gz)
# binary → .deb file (auto-arch matched)
# prebuild → prebuilt archive (e.g. tar.gz)
# singlefile→ standalone binary (chmod +x)
# $4 VERSION - Optional release tag (default: latest)
# $5 TARGET_DIR - Optional install path (default: /opt/<app>)
# $6 ASSET_FILENAME - Required for:
# - prebuild → archive filename or pattern
# - singlefile→ binary filename or pattern
#
# Optional:
# - Set GITHUB_TOKEN env var to increase API rate limit (recommended for CI/CD).
#
# Examples:
# # 1. Minimal: Fetch and deploy source tarball
# fetch_and_deploy_gh_release "myapp" "myuser/myapp"
#
# # 2. Binary install via .deb asset (architecture auto-detected)
# fetch_and_deploy_gh_release "myapp" "myuser/myapp" "binary"
#
# # 3. Prebuilt archive (.tar.gz) with asset filename match
# fetch_and_deploy_gh_release "hanko" "teamhanko/hanko" "prebuild" "latest" "/opt/hanko" "hanko_Linux_x86_64.tar.gz"
#
# # 4. Single binary (chmod +x) like Argus, Promtail etc.
# fetch_and_deploy_gh_release "argus" "release-argus/Argus" "singlefile" "0.26.3" "/opt/argus" "Argus-.*linux-amd64"
# ------------------------------------------------------------------------------
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_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 current_version=""
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
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")
# 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=$(</tmp/gh_rel.json)
tag_name=$(echo "$json" | jq -r '.tag_name // .name // empty')
[[ "$tag_name" =~ ^v ]] && version="${tag_name:1}" || version="$tag_name"
if [[ "$current_version" == "$version" ]]; then
$STD msg_ok "$app is already up-to-date (v$version)"
return 0
fi
local tmpdir
tmpdir=$(mktemp -d) || return 1
local filename="" url=""
msg_info "Fetching GitHub release: $app ($version)"
local clean_install=false
[[ -n "${CLEAN_INSTALL:-}" && "$CLEAN_INSTALL" == "1" ]] && clean_install=true
### Tarball Mode ###
if [[ "$mode" == "tarball" || "$mode" == "source" ]]; then
url=$(echo "$json" | jq -r '.tarball_url // empty')
[[ -z "$url" ]] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
filename="${app_lc}-${version}.tar.gz"
curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url" || {
msg_error "Download failed: $url"
rm -rf "$tmpdir"
return 1
}
mkdir -p "$target"
if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
rm -rf "${target:?}/"*
fi
tar -xzf "$tmpdir/$filename" -C "$tmpdir"
local unpack_dir
unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1)
shopt -s dotglob nullglob
cp -r "$unpack_dir"/* "$target/"
shopt -u dotglob nullglob
### 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
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"
elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then
tar -xf "$tmpdir/$filename" -C "$unpack_tmp"
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
fi
echo "$version" >"$version_file"
msg_ok "Deployed: $app ($version)"
rm -rf "$tmpdir"
}
# ------------------------------------------------------------------------------
# Loads LOCAL_IP from persistent store or detects if missing.
#
# Description:
# - Loads from /run/local-ip.env or performs runtime lookup
# ------------------------------------------------------------------------------
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
fi
fi
export LOCAL_IP
}
# ------------------------------------------------------------------------------
# Installs Adminer (Debian/Ubuntu via APT, Alpine via direct download).
#
# Description:
# - Adds Adminer to Apache or web root
# - Supports Alpine and Debian-based systems
# ------------------------------------------------------------------------------
function setup_adminer() {
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "adminer")
if grep -qi alpine /etc/os-release; then
msg_info "Installing 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 "Installed Adminer (Alpine)"
else
msg_info "Installing Adminer (Debian/Ubuntu)"
ensure_dependencies adminer
$STD a2enconf adminer
$STD systemctl reload apache2
local VERSION
VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}')
cache_installed_version "adminer" "${VERSION:-unknown}"
msg_ok "Installed Adminer (Debian/Ubuntu)"
fi
}
# ------------------------------------------------------------------------------
# Installs or updates Composer globally (robust, idempotent).
#
# - 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)
# ------------------------------------------------------------------------------
function setup_composer() {
local COMPOSER_BIN="/usr/local/bin/composer"
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "composer")
export COMPOSER_ALLOW_SUPERUSER=1
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"
if [[ -x "$COMPOSER_BIN" ]]; then
local CURRENT_VERSION
CURRENT_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
msg_info "Updating Composer from $CURRENT_VERSION to latest"
else
msg_info "Installing Composer"
fi
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
msg_error "Failed to download Composer installer"
return 1
}
$STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm -f /tmp/composer-setup.php
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
local FINAL_VERSION
FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
cache_installed_version "composer" "$FINAL_VERSION"
msg_ok "Installed Composer $FINAL_VERSION"
}
# ------------------------------------------------------------------------------
# Installs FFmpeg from source or prebuilt binary (Debian/Ubuntu only).
#
# Description:
# - Downloads and builds FFmpeg from GitHub (https://github.com/FFmpeg/FFmpeg)
# - Supports specific version override via FFMPEG_VERSION (e.g. n7.1.1)
# - Supports build profile via FFMPEG_TYPE:
# - minimal : x264, vpx, mp3 only
# - medium : adds subtitles, fonts, opus, vorbis
# - full : adds dav1d, svt-av1, zlib, numa
# - binary : downloads static build (johnvansickle.com)
# - Defaults to latest stable version and full feature set
#
# Notes:
# - Requires: curl, jq, build-essential, and matching codec libraries
# - Result is installed to /usr/local/bin/ffmpeg
# ------------------------------------------------------------------------------
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 CACHED_VERSION
CACHED_VERSION=$(get_cached_version "ffmpeg")
# Binary fallback mode
if [[ "$TYPE" == "binary" ]]; then
msg_info "Installing FFmpeg (static binary)"
curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" || {
msg_error "Failed to download FFmpeg binary"
rm -rf "$TMP_DIR"
return 1
}
tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR"
local EXTRACTED_DIR
EXTRACTED_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*")
cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH"
cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe
chmod +x "$BIN_PATH" /usr/local/bin/ffprobe
local FINAL_VERSION=$($BIN_PATH -version | head -n1 | awk '{print $3}')
rm -rf "$TMP_DIR"
cache_installed_version "ffmpeg" "$FINAL_VERSION"
ensure_usr_local_bin_persist
msg_ok "Installed FFmpeg binary $FINAL_VERSION"
return 0
fi
ensure_dependencies jq
# 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" |
jq -r '.[].name' |
grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' |
sort -V | tail -n1)
fi
if [[ -z "$VERSION" ]]; then
msg_error "Could not determine FFmpeg version"
rm -rf "$TMP_DIR"
return 1
fi
msg_info "Installing FFmpeg ${VERSION} ($TYPE)"
# 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[@]}"
curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || {
msg_error "Failed to download FFmpeg source"
rm -rf "$TMP_DIR"
return 1
}
tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || {
msg_error "Failed to extract FFmpeg"
rm -rf "$TMP_DIR"
return 1
}
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[@]}"
$STD make -j"$(nproc)"
$STD make install
echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf
$STD ldconfig
ldconfig -p | 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 | head -n1 | awk '{print $3}')
rm -rf "$TMP_DIR"
cache_installed_version "ffmpeg" "$FINAL_VERSION"
ensure_usr_local_bin_persist
msg_ok "Installed FFmpeg $FINAL_VERSION"
}
# ------------------------------------------------------------------------------
# Installs Go (Golang) from official tarball.
#
# Description:
# - Determines system architecture
# - Downloads latest version if GO_VERSION not set
#
# Variables:
# GO_VERSION - Version to install (e.g. 1.22.2 or latest)
# ------------------------------------------------------------------------------
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
# 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
msg_error "Could not determine latest Go version"
return 1
fi
fi
local GO_BIN="/usr/local/bin/go"
local GO_INSTALL_DIR="/usr/local/go"
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "go")
if [[ -x "$GO_BIN" ]]; then
local CURRENT_VERSION
CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//')
if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then
if [[ "$CACHED_VERSION" == "$GO_VERSION" ]]; then
return 0
fi
cache_installed_version "go" "$GO_VERSION"
return 0
else
msg_info "Upgrading Go from $CURRENT_VERSION to $GO_VERSION"
rm -rf "$GO_INSTALL_DIR"
fi
else
msg_info "Installing 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"
rm -f "$TMP_TAR"
return 1
}
$STD tar -C /usr/local -xzf "$TMP_TAR"
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 "Installed Go $GO_VERSION"
}
# ------------------------------------------------------------------------------
# Installs or updates Ghostscript (gs) from source.
#
# Description:
# - Fetches latest release
# - Builds and installs system-wide
# ------------------------------------------------------------------------------
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)
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_error "Could not determine latest Ghostscript version"
rm -rf "$TMP_DIR"
return 1
fi
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 "Installing Ghostscript $LATEST_VERSION_DOTTED"
curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" || {
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
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
$STD make -j"$(nproc)"
$STD make install
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 "Installed Ghostscript $LATEST_VERSION_DOTTED"
}
# ------------------------------------------------------------------------------
# Installs ImageMagick 7 from source (Debian/Ubuntu only).
#
# Description:
# - Downloads the latest ImageMagick source tarball
# - Builds and installs ImageMagick to /usr/local
# - Configures dynamic linker (ldconfig)
#
# Notes:
# - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc.
# ------------------------------------------------------------------------------
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")
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
fi
msg_info "Installing ImageMagick (Patience)"
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"
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
$STD make -j"$(nproc)"
$STD make install
$STD ldconfig /usr/local/lib
if [[ ! -x "$BINARY_PATH" ]]; then
msg_error "ImageMagick installation failed"
rm -rf "$TMP_DIR"
return 1
fi
VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}')
rm -rf "$TMP_DIR"
cache_installed_version "imagemagick" "$VERSION"
ensure_usr_local_bin_persist
msg_ok "Installed ImageMagick $VERSION"
}
# ------------------------------------------------------------------------------
# Installs Temurin JDK via Adoptium APT repository.
#
# Description:
# - Removes previous JDK if version mismatch
# - Installs or upgrades to specified JAVA_VERSION
#
# Variables:
# JAVA_VERSION - Temurin JDK version to install (e.g. 17, 21)
# ------------------------------------------------------------------------------
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"
# Check cached version
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "temurin-jdk")
# Add repo nur wenn nötig
if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then
# Cleanup old repository files
cleanup_old_repo_files "adoptium"
# Use helper function to get fallback suite
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb")
# Use standardized repo setup
setup_deb822_repo \
"adoptium" \
"https://packages.adoptium.net/artifactory/api/gpg/key/public" \
"https://packages.adoptium.net/artifactory/deb" \
"$SUITE" \
"main" \
"amd64 arm64"
fi
local INSTALLED_VERSION=""
if dpkg -l | grep -q "temurin-.*-jdk"; then
INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+')
fi
if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then
if [[ "$CACHED_VERSION" == "$JAVA_VERSION" ]]; then
# Already at correct version, just upgrade if available
upgrade_package "$DESIRED_PACKAGE"
else
msg_info "Upgrading Temurin JDK $JAVA_VERSION"
$STD apt update
$STD apt install --only-upgrade -y "$DESIRED_PACKAGE"
cache_installed_version "temurin-jdk" "$JAVA_VERSION"
msg_ok "Upgraded Temurin JDK $JAVA_VERSION"
fi
else
if [[ -n "$INSTALLED_VERSION" ]]; then
msg_info "Removing old Temurin JDK $INSTALLED_VERSION"
$STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk"
msg_ok "Removed old Temurin JDK"
fi
msg_info "Installing Temurin JDK $JAVA_VERSION"
$STD apt install -y "$DESIRED_PACKAGE"
cache_installed_version "temurin-jdk" "$JAVA_VERSION"
msg_ok "Installed Temurin JDK $JAVA_VERSION"
fi
}
# ------------------------------------------------------------------------------
# Installs a local IP updater script using networkd-dispatcher.
#
# Description:
# - Stores current IP in /run/local-ip.env
# - Automatically runs on network changes
# ------------------------------------------------------------------------------
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"
mkdir -p "$BASE_DIR"
# Install networkd-dispatcher if not present
if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then
$STD apt update
$STD apt install -y networkd-dispatcher
fi
# Write update_local_ip.sh
cat <<'EOF' >"$SCRIPT_PATH"
#!/bin/bash
set -euo pipefail
IP_FILE="/run/local-ip.env"
mkdir -p "$(dirname "$IP_FILE")"
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
}
current_ip="$(get_current_ip)"
if [[ -z "$current_ip" ]]; then
echo "[ERROR] Could not detect local IP" >&2
exit 1
fi
if [[ -f "$IP_FILE" ]]; then
source "$IP_FILE"
[[ "$LOCAL_IP" == "$current_ip" ]] && exit 0
fi
echo "LOCAL_IP=$current_ip" > "$IP_FILE"
echo "[INFO] LOCAL_IP updated to $current_ip"
EOF
chmod +x "$SCRIPT_PATH"
# Install dispatcher hook
mkdir -p "$(dirname "$DISPATCHER_SCRIPT")"
cat <<EOF >"$DISPATCHER_SCRIPT"
#!/bin/bash
$SCRIPT_PATH
EOF
chmod +x "$DISPATCHER_SCRIPT"
systemctl enable -q --now networkd-dispatcher.service
}
# ------------------------------------------------------------------------------
# Installs or updates MariaDB from official repo.
#
# Description:
# - Detects current MariaDB version and replaces it if necessary
# - Preserves existing database data
# - Dynamically determines latest GA version if "latest" is given
#
# Variables:
# MARIADB_VERSION - MariaDB version to install (e.g. 10.11, latest) (default: latest)
# ------------------------------------------------------------------------------
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
if [[ "$MARIADB_VERSION" == "latest" ]]; then
MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ |
grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' |
grep -vE 'rc/|rolling/' |
sed 's|/||' |
sort -Vr |
head -n1)
if [[ -z "$MARIADB_VERSION" ]]; then
msg_error "Could not determine latest GA MariaDB version"
return 1
fi
fi
local CURRENT_VERSION=""
if command -v mariadb >/dev/null; then
CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
fi
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "mariadb")
if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then
if [[ "$CACHED_VERSION" == "$MARIADB_VERSION" ]]; then
upgrade_package mariadb-server
upgrade_package mariadb-client
else
msg_info "Upgrading MariaDB $MARIADB_VERSION"
$STD apt update
$STD apt install --only-upgrade -y mariadb-server mariadb-client
cache_installed_version "mariadb" "$MARIADB_VERSION"
msg_ok "Upgraded MariaDB $MARIADB_VERSION"
fi
return 0
fi
msg_info "Installing MariaDB $MARIADB_VERSION"
if [[ -n "$CURRENT_VERSION" ]]; then
$STD systemctl stop mariadb >/dev/null 2>&1 || true
$STD apt purge -y 'mariadb*' || true
fi
# Cleanup old repository files
cleanup_old_repo_files "mariadb"
# Use helper function to get fallback suite
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}")
# Use standardized repo setup
setup_deb822_repo \
"mariadb" \
"https://mariadb.org/mariadb_release_signing_key.asc" \
"http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \
"$SUITE" \
"main" \
"amd64 arm64"
local MARIADB_MAJOR_MINOR
MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}')
if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then
echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections
fi
DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || {
cleanup_old_repo_files "mariadb"
$STD apt update
DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client
}
cache_installed_version "mariadb" "$MARIADB_VERSION"
msg_ok "Installed MariaDB $MARIADB_VERSION"
}
# ------------------------------------------------------------------------------
# Installs or updates MongoDB to specified major version.
#
# Description:
# - Preserves data across installations
# - Adds official MongoDB repo
#
# Variables:
# MONGO_VERSION - MongoDB major version to install (e.g. 7.0, 8.0)
# ------------------------------------------------------------------------------
function setup_mongodb() {
local MONGO_VERSION="${MONGO_VERSION:-8.0}"
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID=$(awk -F= '/^ID=/{ gsub(/"/,"",$2); print $2 }' /etc/os-release)
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release)
# 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
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"
return 1
;;
esac
local INSTALLED_VERSION=""
if command -v mongod >/dev/null; then
INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2)
fi
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "mongodb")
if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then
if [[ "$CACHED_VERSION" == "$MONGO_VERSION" ]]; then
upgrade_package mongodb-org
else
msg_info "Upgrading MongoDB $MONGO_VERSION"
$STD apt update
$STD apt install --only-upgrade -y mongodb-org
cache_installed_version "mongodb" "$MONGO_VERSION"
msg_ok "Upgraded MongoDB $MONGO_VERSION"
fi
return 0
fi
msg_info "Installing MongoDB $MONGO_VERSION"
if [[ -n "$INSTALLED_VERSION" ]]; then
$STD systemctl stop mongod || true
$STD apt purge -y mongodb-org || true
fi
# Cleanup old repository files
cleanup_old_repo_files "mongodb-org-${MONGO_VERSION}"
# Use helper function to get fallback suite
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "$MONGO_BASE_URL")
# Use standardized repo setup
mkdir -p /etc/apt/keyrings
curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg"
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.sources
Types: deb
URIs: ${MONGO_BASE_URL}
Suites: ${SUITE}/mongodb-org/${MONGO_VERSION}
Components: ${REPO_COMPONENT}
Architectures: amd64 arm64
Signed-By: /etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg
EOF
$STD apt update || {
msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?"
return 1
}
$STD apt install -y mongodb-org
mkdir -p /var/lib/mongodb
chown -R mongodb:mongodb /var/lib/mongodb
$STD systemctl enable mongod
safe_service_restart mongod
cache_installed_version "mongodb" "$MONGO_VERSION"
msg_ok "Installed MongoDB $MONGO_VERSION"
}
# ------------------------------------------------------------------------------
# Installs or upgrades MySQL and configures APT repo.
#
# Description:
# - Detects existing MySQL installation
# - Purges conflicting packages before installation
# - Supports clean upgrade
#
# Variables:
# MYSQL_VERSION - MySQL version to install (e.g. 5.7, 8.0) (default: 8.0)
# ------------------------------------------------------------------------------
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
msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION"
NEED_INSTALL=true
else
if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then
msg_info "MySQL $CURRENT_VERSION available for upgrade"
$STD apt update
$STD apt install --only-upgrade -y mysql-server
msg_ok "MySQL upgraded"
fi
return
fi
else
msg_info "Setup MySQL $MYSQL_VERSION"
NEED_INSTALL=true
fi
if [[ "$NEED_INSTALL" == true ]]; then
$STD systemctl stop mysql || true
$STD apt purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true
# Cleanup old repository files
cleanup_old_repo_files "mysql"
# Use helper function to get fallback suite
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}")
# Use standardized repo setup
setup_deb822_repo \
"mysql" \
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" \
"https://repo.mysql.com/apt/${DISTRO_ID}" \
"$SUITE" \
"mysql-${MYSQL_VERSION}" \
"amd64 arm64"
export DEBIAN_FRONTEND=noninteractive
$STD apt install -y mysql-server
cache_installed_version "mysql" "$MYSQL_VERSION"
msg_ok "Installed MySQL $MYSQL_VERSION"
fi
}
# ------------------------------------------------------------------------------
# Installs Node.js and optional global modules.
#
# Description:
# - Installs specified Node.js version using NodeSource APT repo
# - Optionally installs or updates global npm modules
#
# Variables:
# NODE_VERSION - Node.js version to install (default: 22)
# NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0")
# ------------------------------------------------------------------------------
function setup_nodejs() {
local NODE_VERSION="${NODE_VERSION:-22}"
local NODE_MODULE="${NODE_MODULE:-}"
local CURRENT_NODE_VERSION=""
local NEED_NODE_INSTALL=false
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
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
ensure_dependencies jq
if [[ "$NEED_NODE_INSTALL" == true ]]; then
$STD apt purge -y nodejs
# Cleanup old repository files
cleanup_old_repo_files "nodesource"
# NodeSource uses 'nodistro' for all distributions - no fallback needed
# Use standardized repo setup
setup_deb822_repo \
"nodesource" \
"https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \
"https://deb.nodesource.com/node_${NODE_VERSION}.x" \
"nodistro" \
"main" \
"amd64 arm64"
if ! $STD apt install -y nodejs; then
msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource"
return 1
fi
$STD npm install -g npm@latest || {
msg_warn "Failed to update npm to latest version"
}
cache_installed_version "nodejs" "$NODE_VERSION"
msg_ok "Installed Node.js ${NODE_VERSION}"
fi
export NODE_OPTIONS="--max-old-space-size=4096"
if [[ ! -d /opt ]]; then
mkdir -p /opt
fi
cd /opt || {
msg_error "Failed to set safe working directory before npm install"
return 1
}
if [[ -n "$NODE_MODULE" ]]; then
IFS=',' read -ra MODULES <<<"$NODE_MODULE"
for mod in "${MODULES[@]}"; do
local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION
if [[ "$mod" == @*/*@* ]]; then
MODULE_NAME="${mod%@*}"
MODULE_REQ_VERSION="${mod##*@}"
elif [[ "$mod" == *"@"* ]]; then
MODULE_NAME="${mod%@*}"
MODULE_REQ_VERSION="${mod##*@}"
else
MODULE_NAME="$mod"
MODULE_REQ_VERSION="latest"
fi
if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then
MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')"
if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then
msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION"
if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then
msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION"
return 1
fi
elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then
msg_info "Updating $MODULE_NAME to latest version"
if ! $STD npm install -g "${MODULE_NAME}@latest"; then
msg_error "Failed to update $MODULE_NAME to latest version"
return 1
fi
fi
else
msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION"
if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then
msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION"
return 1
fi
fi
done
msg_ok "Installed Node.js modules: $NODE_MODULE"
fi
}
# ------------------------------------------------------------------------------
# Installs PHP with selected modules and configures Apache/FPM support.
#
# Description:
# - Adds Sury PHP repo if needed
# - Installs default and user-defined modules
# - Patches php.ini for CLI, Apache, and FPM as needed
#
# Variables:
# PHP_VERSION - PHP version to install (default: 8.4)
# PHP_MODULE - Additional comma-separated modules
# PHP_APACHE - Set YES to enable PHP with Apache
# PHP_FPM - Set YES to enable PHP-FPM
# PHP_MEMORY_LIMIT - (default: 512M)
# PHP_UPLOAD_MAX_FILESIZE - (default: 128M)
# PHP_POST_MAX_SIZE - (default: 128M)
# PHP_MAX_EXECUTION_TIME - (default: 300)
# ------------------------------------------------------------------------------
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 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}"
# 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=""
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
if [[ -z "$CURRENT_PHP" ]]; then
msg_info "Setup PHP $PHP_VERSION"
elif [[ "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
msg_info "Old PHP $CURRENT_PHP detected, Setup new PHP $PHP_VERSION"
$STD apt purge -y "php${CURRENT_PHP//./}"* || true
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"
$STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
$STD dpkg -i /tmp/debsuryorg-archive-keyring.deb
# Use helper function to get fallback suite
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.sury.org/php")
cat <<EOF >/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
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}"
else
msg_warn "PHP-Module ${mod} for PHP ${PHP_VERSION} not found skipping"
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 | grep -q "libapache2-mod-php${PHP_VERSION}"; then
msg_info "Installing Apache with PHP${PHP_VERSION} support"
$STD apt install -y apache2 libapache2-mod-php${PHP_VERSION}
else
msg_info "Apache with PHP${PHP_VERSION} already installed skipping install"
fi
fi
# setup / update PHP modules
$STD apt install -y $MODULE_LIST
cache_installed_version "php" "$PHP_VERSION"
msg_ok "Installed 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)
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
else
msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found"
fi
fi
}
# ------------------------------------------------------------------------------
# Installs or upgrades PostgreSQL and optional extensions/modules.
#
# Description:
# - Detects existing PostgreSQL version
# - Dumps all databases before upgrade
# - Adds PGDG repo and installs specified version
# - Installs optional PG_MODULES (e.g. postgis, contrib)
# - Restores dumped data post-upgrade
#
# 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)
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
fi
if [[ "$NEED_PG_INSTALL" == true ]]; then
msg_info "Installing PostgreSQL $PG_VERSION"
if [[ -n "$CURRENT_PG_VERSION" ]]; then
$STD su - postgres -c "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"
# Use helper function to get fallback suite
local SUITE
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"
# 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"
$STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}"
if [[ -n "$CURRENT_PG_VERSION" ]]; then
$STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true
$STD su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql"
fi
$STD systemctl enable --now postgresql
cache_installed_version "postgresql" "$PG_VERSION"
msg_ok "Installed PostgreSQL $PG_VERSION"
fi
if [[ -n "$PG_MODULES" ]]; then
msg_info "Installing PostgreSQL modules"
IFS=',' read -ra MODULES <<<"$PG_MODULES"
for module in "${MODULES[@]}"; do
$STD apt install -y "postgresql-${PG_VERSION}-${module}" || {
msg_warn "Failed to install postgresql-${PG_VERSION}-${module}"
continue
}
done
msg_ok "Installed PostgreSQL modules"
fi
}
# ------------------------------------------------------------------------------
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
#
# Description:
# - Downloads rbenv and ruby-build from GitHub
# - Compiles and installs target Ruby version
# - Optionally installs Rails via gem
#
# Variables:
# RUBY_VERSION - Ruby version to install (default: 3.4.4)
# RUBY_INSTALL_RAILS - true/false to install Rails (default: true)
# ------------------------------------------------------------------------------
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 CACHED_VERSION
CACHED_VERSION=$(get_cached_version "ruby")
msg_info "Installing Ruby $RUBY_VERSION"
ensure_dependencies jq
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
}
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
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
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/"
echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt"
if ! grep -q 'rbenv init' "$PROFILE_FILE"; then
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE"
echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE"
fi
export PATH="$RBENV_DIR/bin:$PATH"
eval "$("$RBENV_BIN" init - bash)"
if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then
$STD "$RBENV_BIN" install "$RUBY_VERSION"
fi
"$RBENV_BIN" global "$RUBY_VERSION"
hash -r
if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then
$STD gem install rails
local RAILS_VERSION=$(rails -v 2>/dev/null | awk '{print $2}')
msg_ok "Installed Rails $RAILS_VERSION"
fi
rm -rf "$TMP_DIR"
cache_installed_version "ruby" "$RUBY_VERSION"
msg_ok "Installed Ruby $RUBY_VERSION"
}
# ------------------------------------------------------------------------------
# Installs or upgrades ClickHouse database server.
#
# Description:
# - Adds ClickHouse official repository
# - Installs specified version
# - Configures systemd service
# - Supports Debian/Ubuntu with fallback mechanism
#
# Variables:
# CLICKHOUSE_VERSION - ClickHouse version to install (default: latest)
# ------------------------------------------------------------------------------
function setup_clickhouse() {
local CLICKHOUSE_VERSION="${CLICKHOUSE_VERSION:-latest}"
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
# Determine latest version if needed
if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then
CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ |
grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
sort -V |
tail -n1)
if [[ -z "$CLICKHOUSE_VERSION" ]]; then
msg_error "Could not determine latest ClickHouse version"
return 1
fi
fi
local CURRENT_VERSION=""
if command -v clickhouse-server >/dev/null 2>&1; then
CURRENT_VERSION=$(clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
fi
local CACHED_VERSION
CACHED_VERSION=$(get_cached_version "clickhouse")
# Check if already at target version
if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
upgrade_package clickhouse-server
upgrade_package clickhouse-client
else
msg_info "Upgrading ClickHouse $CLICKHOUSE_VERSION"
$STD apt update
$STD apt install --only-upgrade -y clickhouse-server clickhouse-client
cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
msg_ok "Upgraded ClickHouse $CLICKHOUSE_VERSION"
fi
return 0
fi
msg_info "Installing ClickHouse $CLICKHOUSE_VERSION"
# Stop existing service if upgrading
if [[ -n "$CURRENT_VERSION" ]]; then
$STD systemctl stop clickhouse-server || true
fi
# Cleanup old repository files
cleanup_old_repo_files "clickhouse"
# Ensure dependencies
ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg
# Use helper function to get fallback suite
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.clickhouse.com/deb")
# Use standardized repo setup
setup_deb822_repo \
"clickhouse" \
"https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
"https://packages.clickhouse.com/deb" \
"$SUITE" \
"main" \
"amd64 arm64"
# Install ClickHouse packages
export DEBIAN_FRONTEND=noninteractive
$STD apt install -y clickhouse-server clickhouse-client
# Create data directory if it doesn't exist
mkdir -p /var/lib/clickhouse
chown -R clickhouse:clickhouse /var/lib/clickhouse
# Enable and start service
$STD systemctl enable clickhouse-server
safe_service_restart clickhouse-server
cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
msg_ok "Installed ClickHouse $CLICKHOUSE_VERSION"
}
# ------------------------------------------------------------------------------
# Installs Rust toolchain and optional global crates via cargo.
#
# Description:
# - Installs rustup (if missing)
# - Installs or updates desired Rust toolchain (stable, nightly, or versioned)
# - Installs or updates specified global crates using `cargo install`
#
# Notes:
# - Skips crate install if exact version is already present
# - Updates crate if newer version or different version is requested
#
# Variables:
# RUST_TOOLCHAIN - Rust toolchain to install (default: stable)
# RUST_CRATES - Comma-separated list of crates (e.g. "cargo-edit,wasm-pack@0.12.1")
# ------------------------------------------------------------------------------
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")
if ! command -v rustup &>/dev/null; then
msg_info "Installing Rust"
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 "Installed Rust $RUST_VERSION"
else
$STD rustup install "$RUST_TOOLCHAIN"
$STD rustup default "$RUST_TOOLCHAIN"
$STD rustup update "$RUST_TOOLCHAIN"
local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}')
cache_installed_version "rust" "$RUST_VERSION"
msg_ok "Updated Rust toolchain to $RUST_TOOLCHAIN ($RUST_VERSION)"
fi
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')
if [[ -n "$INSTALLED_VER" ]]; then
if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then
msg_info "Updating $NAME: $INSTALLED_VER$VER"
$STD cargo install "$NAME" --version "$VER" --force
msg_ok "Updated $NAME to $VER"
elif [[ -z "$VER" ]]; then
msg_info "Updating $NAME: $INSTALLED_VER → latest"
$STD cargo install "$NAME" --force
local NEW_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v')
msg_ok "Updated $NAME to $NEW_VER"
fi
else
msg_info "Installing $NAME ${VER:+$VER}"
$STD cargo install "$NAME" ${VER:+--version "$VER"}
msg_ok "Installed $NAME ${VER:-latest}"
fi
done
fi
}
# ------------------------------------------------------------------------------
# Installs or upgrades uv (Python package manager) from GitHub releases.
# - Downloads platform-specific tarball (no install.sh!)
# - Extracts uv binary
# - Places it in /usr/local/bin
# - Optionally installs a specific Python version via uv
# ------------------------------------------------------------------------------
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 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
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
msg_error "Could not fetch latest uv version"
rm -rf "$TMP_DIR"
return 1
fi
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
else
msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION"
fi
else
msg_info "Installing 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 "Installed uv $LATEST_VERSION"
if [[ -n "${PYTHON_VERSION:-}" ]]; then
local VERSION_MATCH
VERSION_MATCH=$(uv python list --only-downloads |
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 | 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
}
msg_ok "Installed Python $VERSION_MATCH"
fi
fi
}
# ------------------------------------------------------------------------------
# Installs or updates yq (mikefarah/yq - Go version).
#
# Description:
# - Checks if yq is installed and from correct source
# - Compares with latest release on GitHub
# - Updates if outdated or wrong implementation
# ------------------------------------------------------------------------------
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
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
msg_error "Could not determine latest yq version"
rm -rf "$TMP_DIR"
return 1
fi
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then
if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then
rm -rf "$TMP_DIR"
return 0
fi
cache_installed_version "yq" "$LATEST_VERSION"
rm -rf "$TMP_DIR"
return 0
fi
msg_info "Installing yq $LATEST_VERSION"
curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" || {
msg_error "Failed to download yq"
rm -rf "$TMP_DIR"
return 1
}
chmod +x "$TMP_DIR/yq"
mv "$TMP_DIR/yq" "$BINARY_PATH"
if [[ ! -x "$BINARY_PATH" ]]; then
msg_error "Failed to install yq"
rm -rf "$TMP_DIR"
return 1
fi
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 "Installed yq $FINAL_VERSION"
}