
Some checks failed
Bump build.func Revision / bump-revision (push) Has been cancelled
Adds explicit installation of MariaDB dependencies before setup. Updates MySQL setup to handle unsupported Debian testing/unstable suites by falling back to bookworm if available, with improved error messaging. Ensures PostgreSQL binaries are added to PATH during installation. Expands Ruby setup to install all required build dependencies.
3205 lines
99 KiB
Bash
3205 lines
99 KiB
Bash
#!/bin/bash
|
||
|
||
# ==============================================================================
|
||
# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Cache installed version to avoid repeated checks
|
||
# ------------------------------------------------------------------------------
|
||
cache_installed_version() {
|
||
local app="$1"
|
||
local version="$2"
|
||
mkdir -p /var/cache/app-versions
|
||
echo "$version" >"/var/cache/app-versions/${app}_version.txt"
|
||
}
|
||
|
||
get_cached_version() {
|
||
local app="$1"
|
||
mkdir -p /var/cache/app-versions
|
||
if [[ -f "/var/cache/app-versions/${app}_version.txt" ]]; then
|
||
cat "/var/cache/app-versions/${app}_version.txt"
|
||
return 0
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Unified package upgrade function (with apt update caching)
|
||
# ------------------------------------------------------------------------------
|
||
upgrade_package() {
|
||
local package="$1"
|
||
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 keyrings from /etc/apt/keyrings
|
||
rm -f /etc/apt/keyrings/"${app}"*.gpg
|
||
|
||
# Remove ALL .sources files for this app (including the main one)
|
||
# This ensures no orphaned .sources files reference deleted keyrings
|
||
rm -f /etc/apt/sources.list.d/"${app}"*.sources
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Cleanup orphaned .sources files that reference missing keyrings
|
||
# This prevents APT signature verification errors
|
||
# ------------------------------------------------------------------------------
|
||
cleanup_orphaned_sources() {
|
||
local sources_dir="/etc/apt/sources.list.d"
|
||
local keyrings_dir="/etc/apt/keyrings"
|
||
|
||
[[ ! -d "$sources_dir" ]] && return 0
|
||
|
||
while IFS= read -r -d '' sources_file; do
|
||
# Extract Signed-By path from .sources file
|
||
local keyring_path
|
||
keyring_path=$(grep -E '^Signed-By:' "$sources_file" 2>/dev/null | awk '{print $2}')
|
||
|
||
# If keyring doesn't exist, remove the .sources file
|
||
if [[ -n "$keyring_path" ]] && [[ ! -f "$keyring_path" ]]; then
|
||
msg_warn "Removing orphaned sources file: $(basename "$sources_file") (missing keyring: $(basename "$keyring_path"))"
|
||
rm -f "$sources_file"
|
||
fi
|
||
done < <(find "$sources_dir" -name "*.sources" -print0 2>/dev/null)
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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 for this app
|
||
cleanup_old_repo_files "$name"
|
||
|
||
# Cleanup any orphaned .sources files from other apps
|
||
cleanup_orphaned_sources
|
||
|
||
# Ensure keyring directory exists
|
||
mkdir -p /etc/apt/keyrings
|
||
|
||
# Download GPG key (with --yes to avoid interactive prompts)
|
||
curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg"
|
||
|
||
# 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
|
||
|
||
# Install required dependencies first (MariaDB needs these from main repos)
|
||
msg_info "Installing MariaDB dependencies"
|
||
$STD apt update
|
||
$STD apt install -y gawk rsync socat libdbi-perl pv || {
|
||
msg_error "Failed to install MariaDB dependencies"
|
||
return 1
|
||
}
|
||
|
||
# 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}"
|
||
|
||
# Cleanup any orphaned .sources files from other apps
|
||
cleanup_orphaned_sources
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "$MONGO_BASE_URL")
|
||
|
||
# Use standardized repo setup
|
||
mkdir -p /etc/apt/keyrings
|
||
curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg"
|
||
|
||
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"
|
||
|
||
# MySQL doesn't support Debian testing/unstable - try bookworm as fallback
|
||
local SUITE
|
||
case "$DISTRO_CODENAME" in
|
||
trixie | forky | sid)
|
||
# MySQL doesn't publish packages for Debian testing/unstable
|
||
# Try bookworm as fallback
|
||
if verify_repo_available "https://repo.mysql.com/apt/${DISTRO_ID}" "bookworm"; then
|
||
SUITE="bookworm"
|
||
msg_warn "MySQL ${MYSQL_VERSION} not available for ${DISTRO_CODENAME}, using bookworm packages"
|
||
else
|
||
msg_error "MySQL ${MYSQL_VERSION} package not available for ${DISTRO_ID}-${DISTRO_CODENAME}"
|
||
msg_info "MySQL typically doesn't support Debian testing/unstable. Consider using MariaDB instead."
|
||
return 1
|
||
fi
|
||
;;
|
||
*)
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}")
|
||
;;
|
||
esac
|
||
|
||
# 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
|
||
|
||
# Update apt and check if package is available
|
||
if ! $STD apt update; then
|
||
msg_error "APT update failed for MySQL repository"
|
||
return 1
|
||
fi
|
||
|
||
if ! apt-cache policy mysql-server | grep -q 'Candidate:'; then
|
||
msg_error "MySQL ${MYSQL_VERSION} package not available for suite ${SUITE}"
|
||
msg_info "Available MySQL components:"
|
||
apt-cache search "^mysql-" | grep "^mysql-" | head -20 | sed 's/^/ /'
|
||
return 1
|
||
fi
|
||
|
||
if ! $STD apt install -y mysql-server; then
|
||
msg_error "Failed to install MySQL ${MYSQL_VERSION}"
|
||
return 1
|
||
fi
|
||
|
||
# Verify installation
|
||
if ! command -v mysql >/dev/null 2>&1; then
|
||
msg_error "MySQL installation completed but mysql command not found"
|
||
return 1
|
||
fi
|
||
|
||
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"
|
||
|
||
# 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 and verify package availability
|
||
if ! $STD apt update; then
|
||
msg_error "APT update failed for PostgreSQL repository"
|
||
return 1
|
||
fi
|
||
|
||
if ! apt-cache policy "postgresql-${PG_VERSION}" | grep -q 'Candidate:'; then
|
||
msg_error "PostgreSQL ${PG_VERSION} package not available for suite ${SUITE}"
|
||
msg_info "Available PostgreSQL versions:"
|
||
apt-cache search "^postgresql-[0-9]" | grep "^postgresql-" | sed 's/^/ /'
|
||
return 1
|
||
fi
|
||
|
||
if ! $STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}"; then
|
||
msg_error "Failed to install PostgreSQL ${PG_VERSION}"
|
||
return 1
|
||
fi
|
||
|
||
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
|
||
|
||
# Add PostgreSQL binaries to PATH for the install script
|
||
if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then
|
||
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/'"${PG_VERSION}"'/bin"' >/etc/environment
|
||
fi
|
||
|
||
cache_installed_version "postgresql" "$PG_VERSION"
|
||
msg_ok "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"
|
||
|
||
# Install all required build dependencies
|
||
ensure_dependencies jq autoconf patch build-essential rustc libssl-dev libyaml-dev \
|
||
libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 \
|
||
libgdbm-dev libdb-dev uuid-dev
|
||
|
||
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
|
||
# Try multiple methods to get the latest version
|
||
CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null |
|
||
grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
|
||
sort -V |
|
||
tail -n1)
|
||
|
||
# Fallback: Try GitHub releases API
|
||
if [[ -z "$CLICKHOUSE_VERSION" ]]; then
|
||
CLICKHOUSE_VERSION=$(curl -fsSL https://api.github.com/repos/ClickHouse/ClickHouse/releases/latest 2>/dev/null |
|
||
grep -oP '"tag_name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
|
||
head -n1)
|
||
fi
|
||
|
||
# Final fallback: Parse HTML more liberally
|
||
if [[ -z "$CLICKHOUSE_VERSION" ]]; then
|
||
CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ 2>/dev/null |
|
||
grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(?=/clickhouse)' |
|
||
sort -V |
|
||
tail -n1)
|
||
fi
|
||
|
||
if [[ -z "$CLICKHOUSE_VERSION" ]]; then
|
||
msg_error "Could not determine latest ClickHouse version"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local CURRENT_VERSION=""
|
||
if command -v clickhouse-server >/dev/null 2>&1; then
|
||
CURRENT_VERSION=$(clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
|
||
fi
|
||
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "clickhouse")
|
||
|
||
# Check if already at target version
|
||
if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
|
||
upgrade_package clickhouse-server
|
||
upgrade_package clickhouse-client
|
||
else
|
||
msg_info "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
|
||
|
||
# ClickHouse uses 'stable' instead of distro codenames
|
||
local SUITE="stable"
|
||
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"clickhouse" \
|
||
"https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
|
||
"https://packages.clickhouse.com/deb" \
|
||
"$SUITE" \
|
||
"main" \
|
||
"amd64 arm64"
|
||
|
||
# Update and install ClickHouse packages
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
if ! $STD apt update; then
|
||
msg_error "APT update failed for ClickHouse repository"
|
||
return 1
|
||
fi
|
||
|
||
if ! $STD apt install -y clickhouse-server clickhouse-client; then
|
||
msg_error "Failed to install ClickHouse packages"
|
||
return 1
|
||
fi
|
||
|
||
# Verify installation
|
||
if ! command -v clickhouse-server >/dev/null 2>&1; then
|
||
msg_error "ClickHouse installation completed but clickhouse-server command not found"
|
||
return 1
|
||
fi
|
||
|
||
# Create data directory if it doesn't exist
|
||
mkdir -p /var/lib/clickhouse
|
||
|
||
# Check if clickhouse user exists before chown
|
||
if id clickhouse >/dev/null 2>&1; then
|
||
chown -R clickhouse:clickhouse /var/lib/clickhouse
|
||
else
|
||
msg_warn "ClickHouse user not found, skipping chown"
|
||
fi
|
||
|
||
# Enable and start service
|
||
if ! $STD systemctl enable clickhouse-server; then
|
||
msg_warn "Failed to enable clickhouse-server service"
|
||
fi
|
||
|
||
if ! safe_service_restart clickhouse-server; then
|
||
msg_warn "Failed to start clickhouse-server service (this may be normal on first install)"
|
||
fi
|
||
|
||
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"
|
||
}
|