3044 lines
101 KiB
Bash
3044 lines
101 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
|
||
[[ -f "/var/cache/app-versions/${app}_version.txt" ]] && cat "/var/cache/app-versions/${app}_version.txt"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Unified package upgrade function (with apt update caching)
|
||
# ------------------------------------------------------------------------------
|
||
upgrade_package() {
|
||
local package="$1"
|
||
msg_info "Upgrading $package"
|
||
|
||
# Use same caching logic as ensure_dependencies
|
||
local apt_cache_file="/var/cache/apt-update-timestamp"
|
||
local current_time=$(date +%s)
|
||
local last_update=0
|
||
|
||
if [[ -f "$apt_cache_file" ]]; then
|
||
last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
|
||
fi
|
||
|
||
if ((current_time - last_update > 300)); then
|
||
$STD apt update
|
||
echo "$current_time" >"$apt_cache_file"
|
||
fi
|
||
|
||
$STD apt install --only-upgrade -y "$package"
|
||
msg_ok "Upgraded $package"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Repository availability check
|
||
# ------------------------------------------------------------------------------
|
||
verify_repo_available() {
|
||
local repo_url="$1"
|
||
local suite="$2"
|
||
|
||
if curl -fsSL --max-time 10 "${repo_url}/dists/${suite}/Release" &>/dev/null; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Ensure dependencies are installed (with apt update caching)
|
||
# ------------------------------------------------------------------------------
|
||
ensure_dependencies() {
|
||
local deps=("$@")
|
||
local missing=()
|
||
|
||
for dep in "${deps[@]}"; do
|
||
if ! command -v "$dep" &>/dev/null && ! is_package_installed "$dep"; then
|
||
missing+=("$dep")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
msg_info "Installing dependencies: ${missing[*]}"
|
||
|
||
# Only run apt update if not done recently (within last 5 minutes)
|
||
local apt_cache_file="/var/cache/apt-update-timestamp"
|
||
local current_time=$(date +%s)
|
||
local last_update=0
|
||
|
||
if [[ -f "$apt_cache_file" ]]; then
|
||
last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
|
||
fi
|
||
|
||
if ((current_time - last_update > 300)); then
|
||
$STD apt update
|
||
echo "$current_time" >"$apt_cache_file"
|
||
fi
|
||
|
||
$STD apt install -y "${missing[@]}"
|
||
msg_ok "Installed dependencies"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Smart version comparison
|
||
# ------------------------------------------------------------------------------
|
||
version_gt() {
|
||
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get system architecture (normalized)
|
||
# ------------------------------------------------------------------------------
|
||
get_system_arch() {
|
||
local arch_type="${1:-dpkg}" # dpkg, uname, or both
|
||
local arch
|
||
|
||
case "$arch_type" in
|
||
dpkg)
|
||
arch=$(dpkg --print-architecture 2>/dev/null)
|
||
;;
|
||
uname)
|
||
arch=$(uname -m)
|
||
[[ "$arch" == "x86_64" ]] && arch="amd64"
|
||
[[ "$arch" == "aarch64" ]] && arch="arm64"
|
||
;;
|
||
both | *)
|
||
arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
|
||
[[ "$arch" == "x86_64" ]] && arch="amd64"
|
||
[[ "$arch" == "aarch64" ]] && arch="arm64"
|
||
;;
|
||
esac
|
||
|
||
echo "$arch"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Create temporary directory with automatic cleanup
|
||
# ------------------------------------------------------------------------------
|
||
create_temp_dir() {
|
||
local tmp_dir=$(mktemp -d)
|
||
# Set trap to cleanup on EXIT, ERR, INT, TERM
|
||
trap "rm -rf '$tmp_dir'" EXIT ERR INT TERM
|
||
echo "$tmp_dir"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Check if package is installed (faster than dpkg -l | grep)
|
||
# ------------------------------------------------------------------------------
|
||
is_package_installed() {
|
||
local package="$1"
|
||
dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "^install ok installed$"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# GitHub API call with authentication and rate limit handling
|
||
# ------------------------------------------------------------------------------
|
||
github_api_call() {
|
||
local url="$1"
|
||
local output_file="${2:-/dev/stdout}"
|
||
local max_retries=3
|
||
local retry_delay=2
|
||
|
||
local header_args=()
|
||
[[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
|
||
|
||
for attempt in $(seq 1 $max_retries); do
|
||
local http_code
|
||
http_code=$(curl -fsSL -w "%{http_code}" -o "$output_file" \
|
||
-H "Accept: application/vnd.github+json" \
|
||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||
"${header_args[@]}" \
|
||
"$url" 2>/dev/null || echo "000")
|
||
|
||
case "$http_code" in
|
||
200)
|
||
return 0
|
||
;;
|
||
403)
|
||
# Rate limit - check if we can retry
|
||
if [[ $attempt -lt $max_retries ]]; then
|
||
msg_warn "GitHub API rate limit, waiting ${retry_delay}s... (attempt $attempt/$max_retries)"
|
||
sleep "$retry_delay"
|
||
retry_delay=$((retry_delay * 2))
|
||
continue
|
||
fi
|
||
msg_error "GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits."
|
||
return 1
|
||
;;
|
||
404)
|
||
msg_error "GitHub API endpoint not found: $url"
|
||
return 1
|
||
;;
|
||
*)
|
||
if [[ $attempt -lt $max_retries ]]; then
|
||
sleep "$retry_delay"
|
||
continue
|
||
fi
|
||
msg_error "GitHub API call failed with HTTP $http_code"
|
||
return 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
should_upgrade() {
|
||
local current="$1"
|
||
local target="$2"
|
||
|
||
[[ -z "$current" ]] && return 0
|
||
version_gt "$target" "$current" && return 0
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get OS information (cached for performance)
|
||
# ------------------------------------------------------------------------------
|
||
get_os_info() {
|
||
local field="${1:-all}" # id, codename, version, version_id, all
|
||
|
||
# Cache OS info to avoid repeated file reads
|
||
if [[ -z "${_OS_ID:-}" ]]; then
|
||
export _OS_ID=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
|
||
export _OS_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
|
||
export _OS_VERSION=$(awk -F= '/^VERSION_ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
|
||
export _OS_VERSION_FULL=$(awk -F= '/^VERSION=/{gsub(/"/,"",$2); print $2}' /etc/os-release)
|
||
fi
|
||
|
||
case "$field" in
|
||
id) echo "$_OS_ID" ;;
|
||
codename) echo "$_OS_CODENAME" ;;
|
||
version) echo "$_OS_VERSION" ;;
|
||
version_id) echo "$_OS_VERSION" ;;
|
||
version_full) echo "$_OS_VERSION_FULL" ;;
|
||
all) echo "ID=$_OS_ID CODENAME=$_OS_CODENAME VERSION=$_OS_VERSION" ;;
|
||
*) echo "$_OS_ID" ;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Check if running on specific OS
|
||
# ------------------------------------------------------------------------------
|
||
is_debian() {
|
||
[[ "$(get_os_info id)" == "debian" ]]
|
||
}
|
||
|
||
is_ubuntu() {
|
||
[[ "$(get_os_info id)" == "ubuntu" ]]
|
||
}
|
||
|
||
is_alpine() {
|
||
[[ "$(get_os_info id)" == "alpine" ]]
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get Debian/Ubuntu major version
|
||
# ------------------------------------------------------------------------------
|
||
get_os_version_major() {
|
||
local version=$(get_os_info version)
|
||
echo "${version%%.*}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Download file with retry logic and progress
|
||
# ------------------------------------------------------------------------------
|
||
download_file() {
|
||
local url="$1"
|
||
local output="$2"
|
||
local max_retries="${3:-3}"
|
||
local show_progress="${4:-false}"
|
||
|
||
local curl_opts=(-fsSL)
|
||
[[ "$show_progress" == "true" ]] && curl_opts=(-fL#)
|
||
|
||
for attempt in $(seq 1 $max_retries); do
|
||
if curl "${curl_opts[@]}" -o "$output" "$url"; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ $attempt -lt $max_retries ]]; then
|
||
msg_warn "Download failed, retrying... (attempt $attempt/$max_retries)"
|
||
sleep 2
|
||
fi
|
||
done
|
||
|
||
msg_error "Failed to download: $url"
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get fallback suite for repository (comprehensive mapping)
|
||
# ------------------------------------------------------------------------------
|
||
get_fallback_suite() {
|
||
local distro_id="$1"
|
||
local distro_codename="$2"
|
||
local repo_base_url="$3"
|
||
|
||
# Check if current codename works
|
||
if verify_repo_available "$repo_base_url" "$distro_codename"; then
|
||
echo "$distro_codename"
|
||
return 0
|
||
fi
|
||
|
||
# Comprehensive fallback mappings
|
||
case "$distro_id" in
|
||
debian)
|
||
case "$distro_codename" in
|
||
# Debian 13 (Trixie) → Debian 12 (Bookworm)
|
||
trixie | forky | sid)
|
||
echo "bookworm"
|
||
;;
|
||
# Debian 12 (Bookworm) stays
|
||
bookworm)
|
||
echo "bookworm"
|
||
;;
|
||
# Debian 11 (Bullseye) stays
|
||
bullseye)
|
||
echo "bullseye"
|
||
;;
|
||
# Unknown → latest stable
|
||
*)
|
||
echo "bookworm"
|
||
;;
|
||
esac
|
||
;;
|
||
ubuntu)
|
||
case "$distro_codename" in
|
||
# Ubuntu 24.10 (Oracular) → 24.04 LTS (Noble)
|
||
oracular | plucky)
|
||
echo "noble"
|
||
;;
|
||
# Ubuntu 24.04 LTS (Noble) stays
|
||
noble)
|
||
echo "noble"
|
||
;;
|
||
# Ubuntu 23.10 (Mantic) → 22.04 LTS (Jammy)
|
||
mantic | lunar)
|
||
echo "jammy"
|
||
;;
|
||
# Ubuntu 22.04 LTS (Jammy) stays
|
||
jammy)
|
||
echo "jammy"
|
||
;;
|
||
# Ubuntu 20.04 LTS (Focal) stays
|
||
focal)
|
||
echo "focal"
|
||
;;
|
||
# Unknown → latest LTS
|
||
*)
|
||
echo "jammy"
|
||
;;
|
||
esac
|
||
;;
|
||
*)
|
||
echo "$distro_codename"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Verify package source and version
|
||
# ------------------------------------------------------------------------------
|
||
verify_package_source() {
|
||
local package="$1"
|
||
local expected_version="$2"
|
||
|
||
if apt-cache policy "$package" 2>/dev/null | grep -q "$expected_version"; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Check if running on LTS version
|
||
# ------------------------------------------------------------------------------
|
||
is_lts_version() {
|
||
local os_id=$(get_os_info id)
|
||
local codename=$(get_os_info codename)
|
||
|
||
if [[ "$os_id" == "ubuntu" ]]; then
|
||
case "$codename" in
|
||
focal | jammy | noble) return 0 ;; # 20.04, 22.04, 24.04
|
||
*) return 1 ;;
|
||
esac
|
||
elif [[ "$os_id" == "debian" ]]; then
|
||
# Debian releases are all "stable"
|
||
case "$codename" in
|
||
bullseye | bookworm | trixie) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get optimal number of parallel jobs (cached)
|
||
# ------------------------------------------------------------------------------
|
||
get_parallel_jobs() {
|
||
if [[ -z "${_PARALLEL_JOBS:-}" ]]; then
|
||
local cpu_count=$(nproc 2>/dev/null || echo 1)
|
||
local mem_gb=$(free -g | awk '/^Mem:/{print $2}')
|
||
|
||
# Limit by available memory (assume 1GB per job for compilation)
|
||
local max_by_mem=$((mem_gb > 0 ? mem_gb : 1))
|
||
local max_jobs=$((cpu_count < max_by_mem ? cpu_count : max_by_mem))
|
||
|
||
# At least 1, at most cpu_count
|
||
export _PARALLEL_JOBS=$((max_jobs > 0 ? max_jobs : 1))
|
||
fi
|
||
echo "$_PARALLEL_JOBS"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get default PHP version for OS
|
||
# ------------------------------------------------------------------------------
|
||
get_default_php_version() {
|
||
local os_id=$(get_os_info id)
|
||
local os_version=$(get_os_version_major)
|
||
|
||
case "$os_id" in
|
||
debian)
|
||
case "$os_version" in
|
||
13) echo "8.3" ;; # Debian 13 (Trixie)
|
||
12) echo "8.2" ;; # Debian 12 (Bookworm)
|
||
11) echo "7.4" ;; # Debian 11 (Bullseye)
|
||
*) echo "8.2" ;;
|
||
esac
|
||
;;
|
||
ubuntu)
|
||
case "$os_version" in
|
||
24) echo "8.3" ;; # Ubuntu 24.04 LTS (Noble)
|
||
22) echo "8.1" ;; # Ubuntu 22.04 LTS (Jammy)
|
||
20) echo "7.4" ;; # Ubuntu 20.04 LTS (Focal)
|
||
*) echo "8.1" ;;
|
||
esac
|
||
;;
|
||
*)
|
||
echo "8.2"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get default Python version for OS
|
||
# ------------------------------------------------------------------------------
|
||
get_default_python_version() {
|
||
local os_id=$(get_os_info id)
|
||
local os_version=$(get_os_version_major)
|
||
|
||
case "$os_id" in
|
||
debian)
|
||
case "$os_version" in
|
||
13) echo "3.12" ;; # Debian 13 (Trixie)
|
||
12) echo "3.11" ;; # Debian 12 (Bookworm)
|
||
11) echo "3.9" ;; # Debian 11 (Bullseye)
|
||
*) echo "3.11" ;;
|
||
esac
|
||
;;
|
||
ubuntu)
|
||
case "$os_version" in
|
||
24) echo "3.12" ;; # Ubuntu 24.04 LTS
|
||
22) echo "3.10" ;; # Ubuntu 22.04 LTS
|
||
20) echo "3.8" ;; # Ubuntu 20.04 LTS
|
||
*) echo "3.10" ;;
|
||
esac
|
||
;;
|
||
*)
|
||
echo "3.11"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get default Node.js LTS version
|
||
# ------------------------------------------------------------------------------
|
||
get_default_nodejs_version() {
|
||
# Always return current LTS (as of 2025)
|
||
echo "20"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Check if package manager is locked
|
||
# ------------------------------------------------------------------------------
|
||
is_apt_locked() {
|
||
if fuser /var/lib/dpkg/lock-frontend &>/dev/null ||
|
||
fuser /var/lib/apt/lists/lock &>/dev/null ||
|
||
fuser /var/cache/apt/archives/lock &>/dev/null; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Wait for apt to be available
|
||
# ------------------------------------------------------------------------------
|
||
wait_for_apt() {
|
||
local max_wait="${1:-300}" # 5 minutes default
|
||
local waited=0
|
||
|
||
while is_apt_locked; do
|
||
if [[ $waited -ge $max_wait ]]; then
|
||
msg_error "Timeout waiting for apt to be available"
|
||
return 1
|
||
fi
|
||
|
||
debug_log "Waiting for apt to be available... (${waited}s)"
|
||
sleep 5
|
||
waited=$((waited + 5))
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Cleanup old repository files (migration helper)
|
||
# ------------------------------------------------------------------------------
|
||
cleanup_old_repo_files() {
|
||
local app="$1"
|
||
|
||
# Remove old-style .list files (including backups)
|
||
rm -f /etc/apt/sources.list.d/"${app}"*.list
|
||
rm -f /etc/apt/sources.list.d/"${app}"*.list.save
|
||
rm -f /etc/apt/sources.list.d/"${app}"*.list.distUpgrade
|
||
rm -f /etc/apt/sources.list.d/"${app}"*.list.dpkg-*
|
||
|
||
# Remove old GPG keys from trusted.gpg.d
|
||
rm -f /etc/apt/trusted.gpg.d/"${app}"*.gpg
|
||
|
||
# Remove duplicate .sources files (keep only the main one)
|
||
local sources_file="/etc/apt/sources.list.d/${app}.sources"
|
||
if [[ -f "$sources_file" ]]; then
|
||
find /etc/apt/sources.list.d/ -name "${app}*.sources" ! -name "${app}.sources" -delete 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Standardized deb822 repository setup
|
||
# ------------------------------------------------------------------------------
|
||
setup_deb822_repo() {
|
||
local name="$1"
|
||
local gpg_url="$2"
|
||
local repo_url="$3"
|
||
local suite="$4"
|
||
local component="${5:-main}"
|
||
local architectures="${6:-amd64 arm64}"
|
||
|
||
msg_info "Setting up $name repository"
|
||
|
||
# Cleanup old configs
|
||
cleanup_old_repo_files "$name"
|
||
|
||
# Ensure keyring directory exists
|
||
mkdir -p /etc/apt/keyrings
|
||
|
||
# Download GPG key
|
||
curl -fsSL "$gpg_url" | gpg --dearmor -o "/etc/apt/keyrings/${name}.gpg"
|
||
|
||
# Create deb822 sources file
|
||
cat <<EOF >/etc/apt/sources.list.d/${name}.sources
|
||
Types: deb
|
||
URIs: $repo_url
|
||
Suites: $suite
|
||
Components: $component
|
||
Architectures: $architectures
|
||
Signed-By: /etc/apt/keyrings/${name}.gpg
|
||
EOF
|
||
|
||
# Use cached apt update
|
||
local apt_cache_file="/var/cache/apt-update-timestamp"
|
||
local current_time=$(date +%s)
|
||
local last_update=0
|
||
|
||
if [[ -f "$apt_cache_file" ]]; then
|
||
last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
|
||
fi
|
||
|
||
# For repo changes, always update but respect short-term cache (30s)
|
||
if ((current_time - last_update > 30)); then
|
||
$STD apt update
|
||
echo "$current_time" >"$apt_cache_file"
|
||
fi
|
||
|
||
msg_ok "Set up $name repository"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Package version hold/unhold helpers
|
||
# ------------------------------------------------------------------------------
|
||
hold_package_version() {
|
||
local package="$1"
|
||
$STD apt-mark hold "$package"
|
||
}
|
||
|
||
unhold_package_version() {
|
||
local package="$1"
|
||
$STD apt-mark unhold "$package"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Safe service restart with verification
|
||
# ------------------------------------------------------------------------------
|
||
safe_service_restart() {
|
||
local service="$1"
|
||
|
||
if systemctl is-active --quiet "$service"; then
|
||
$STD systemctl restart "$service"
|
||
else
|
||
$STD systemctl start "$service"
|
||
fi
|
||
|
||
if ! systemctl is-active --quiet "$service"; then
|
||
msg_error "Failed to start $service"
|
||
systemctl status "$service" --no-pager
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Enable and start service (with error handling)
|
||
# ------------------------------------------------------------------------------
|
||
enable_and_start_service() {
|
||
local service="$1"
|
||
|
||
if ! systemctl enable "$service" &>/dev/null; then
|
||
msg_warn "Could not enable $service (may not be installed yet)"
|
||
fi
|
||
|
||
if ! systemctl start "$service" &>/dev/null; then
|
||
msg_error "Failed to start $service"
|
||
systemctl status "$service" --no-pager
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Check if service is enabled
|
||
# ------------------------------------------------------------------------------
|
||
is_service_enabled() {
|
||
local service="$1"
|
||
systemctl is-enabled --quiet "$service" 2>/dev/null
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Check if service is running
|
||
# ------------------------------------------------------------------------------
|
||
is_service_running() {
|
||
local service="$1"
|
||
systemctl is-active --quiet "$service" 2>/dev/null
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Extract version from JSON (GitHub releases)
|
||
# ------------------------------------------------------------------------------
|
||
extract_version_from_json() {
|
||
local json="$1"
|
||
local field="${2:-tag_name}"
|
||
local strip_v="${3:-true}"
|
||
|
||
ensure_dependencies jq
|
||
|
||
local version
|
||
version=$(echo "$json" | jq -r ".${field} // empty")
|
||
|
||
if [[ -z "$version" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$strip_v" == "true" ]]; then
|
||
echo "${version#v}"
|
||
else
|
||
echo "$version"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Get latest GitHub release version
|
||
# ------------------------------------------------------------------------------
|
||
get_latest_github_release() {
|
||
local repo="$1"
|
||
local strip_v="${2:-true}"
|
||
local temp_file=$(mktemp)
|
||
|
||
if ! github_api_call "https://api.github.com/repos/${repo}/releases/latest" "$temp_file"; then
|
||
rm -f "$temp_file"
|
||
return 1
|
||
fi
|
||
|
||
local version
|
||
version=$(extract_version_from_json "$(cat "$temp_file")" "tag_name" "$strip_v")
|
||
rm -f "$temp_file"
|
||
|
||
if [[ -z "$version" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
echo "$version"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Debug logging (only if DEBUG=1)
|
||
# ------------------------------------------------------------------------------
|
||
debug_log() {
|
||
[[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Performance timing helper
|
||
# ------------------------------------------------------------------------------
|
||
start_timer() {
|
||
echo $(date +%s)
|
||
}
|
||
|
||
end_timer() {
|
||
local start_time="$1"
|
||
local label="${2:-Operation}"
|
||
local end_time=$(date +%s)
|
||
local duration=$((end_time - start_time))
|
||
debug_log "$label took ${duration}s"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# GPG key fingerprint verification
|
||
# ------------------------------------------------------------------------------
|
||
verify_gpg_fingerprint() {
|
||
local key_file="$1"
|
||
local expected_fingerprint="$2"
|
||
|
||
local actual_fingerprint
|
||
actual_fingerprint=$(gpg --show-keys --with-fingerprint --with-colons "$key_file" 2>&1 | grep -m1 '^fpr:' | cut -d: -f10)
|
||
|
||
if [[ "$actual_fingerprint" == "$expected_fingerprint" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint"
|
||
return 1
|
||
}
|
||
|
||
# ==============================================================================
|
||
# EXISTING FUNCTIONS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Checks for new GitHub release (latest tag).
|
||
#
|
||
# Description:
|
||
# - Queries the GitHub API for the latest release tag
|
||
# - Compares it to a local cached version (~/.<app>)
|
||
# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
|
||
#
|
||
# Usage:
|
||
# if check_for_gh_release "flaresolverr" "FlareSolverr/FlareSolverr" [optional] "v1.1.1"; then
|
||
# # trigger update...
|
||
# fi
|
||
# exit 0
|
||
# } (end of update_script not from the function)
|
||
#
|
||
# Notes:
|
||
# - Requires `jq` (auto-installed if missing)
|
||
# - Does not modify anything, only checks version state
|
||
# - Does not support pre-releases
|
||
# ------------------------------------------------------------------------------
|
||
check_for_gh_release() {
|
||
local app="$1"
|
||
local source="$2"
|
||
local pinned_version_in="${3:-}" # optional
|
||
local app_lc="${app,,}"
|
||
local current_file="$HOME/.${app_lc}"
|
||
|
||
msg_info "Checking for update: ${app}"
|
||
|
||
# DNS check
|
||
if ! getent hosts api.github.com >/dev/null 2>&1; then
|
||
msg_error "Network error: cannot resolve api.github.com"
|
||
return 1
|
||
fi
|
||
|
||
ensure_dependencies jq
|
||
|
||
# Fetch releases and exclude drafts/prereleases
|
||
local releases_json
|
||
releases_json=$(curl -fsSL --max-time 20 \
|
||
-H 'Accept: application/vnd.github+json' \
|
||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||
"https://api.github.com/repos/${source}/releases") || {
|
||
msg_error "Unable to fetch releases for ${app}"
|
||
return 1
|
||
}
|
||
|
||
mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json")
|
||
if ((${#raw_tags[@]} == 0)); then
|
||
msg_error "No stable releases found for ${app}"
|
||
return 1
|
||
fi
|
||
|
||
local clean_tags=()
|
||
for t in "${raw_tags[@]}"; do
|
||
clean_tags+=("${t#v}")
|
||
done
|
||
|
||
local latest_raw="${raw_tags[0]}"
|
||
local latest_clean="${clean_tags[0]}"
|
||
|
||
# current installed (stored without v)
|
||
local current=""
|
||
if [[ -f "$current_file" ]]; then
|
||
current="$(<"$current_file")"
|
||
else
|
||
# Migration: search for any /opt/*_version.txt
|
||
local legacy_files
|
||
mapfile -t legacy_files < <(find /opt -maxdepth 1 -type f -name "*_version.txt" 2>/dev/null)
|
||
if ((${#legacy_files[@]} == 1)); then
|
||
current="$(<"${legacy_files[0]}")"
|
||
echo "${current#v}" >"$current_file"
|
||
rm -f "${legacy_files[0]}"
|
||
fi
|
||
fi
|
||
current="${current#v}"
|
||
|
||
# Pinned version handling
|
||
if [[ -n "$pinned_version_in" ]]; then
|
||
local pin_clean="${pinned_version_in#v}"
|
||
local match_raw=""
|
||
for i in "${!clean_tags[@]}"; do
|
||
if [[ "${clean_tags[$i]}" == "$pin_clean" ]]; then
|
||
match_raw="${raw_tags[$i]}"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [[ -z "$match_raw" ]]; then
|
||
msg_error "Pinned version ${pinned_version_in} not found upstream"
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$current" != "$pin_clean" ]]; then
|
||
msg_info "${app} pinned to ${pinned_version_in} (installed ${current:-none}) → update required"
|
||
CHECK_UPDATE_RELEASE="$match_raw"
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$pin_clean" == "$latest_clean" ]]; then
|
||
msg_ok "${app} pinned to ${pinned_version_in} (up to date)"
|
||
else
|
||
msg_ok "${app} pinned to ${pinned_version_in} (already installed, upstream ${latest_raw})"
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
# No pinning → use latest
|
||
if [[ -z "$current" || "$current" != "$latest_clean" ]]; then
|
||
CHECK_UPDATE_RELEASE="$latest_raw"
|
||
msg_info "New release available: ${latest_raw} (current: v${current:-none})"
|
||
return 0
|
||
fi
|
||
|
||
msg_ok "${app} is up to date (${latest_raw})"
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Creates and installs self-signed certificates.
|
||
#
|
||
# Description:
|
||
# - Create a self-signed certificate with option to override application name
|
||
#
|
||
# Variables:
|
||
# APP - Application name (default: $APPLICATION variable)
|
||
# ------------------------------------------------------------------------------
|
||
create_self_signed_cert() {
|
||
local APP_NAME="${1:-${APPLICATION}}"
|
||
local CERT_DIR="/etc/ssl/${APP_NAME}"
|
||
local CERT_KEY="${CERT_DIR}/${APP_NAME}.key"
|
||
local CERT_CRT="${CERT_DIR}/${APP_NAME}.crt"
|
||
|
||
if [[ -f "$CERT_CRT" && -f "$CERT_KEY" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
$STD apt update
|
||
$STD apt install -y openssl
|
||
|
||
mkdir -p "$CERT_DIR"
|
||
$STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
|
||
-subj "/C=US/ST=State/L=City/O=Organization/CN=${APP_NAME}" \
|
||
-keyout "$CERT_KEY" \
|
||
-out "$CERT_CRT"
|
||
|
||
chmod 600 "$CERT_KEY"
|
||
chmod 644 "$CERT_CRT"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Downloads file with optional progress indicator using pv.
|
||
#
|
||
# Arguments:
|
||
# $1 - URL
|
||
# $2 - Destination path
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function download_with_progress() {
|
||
local url="$1"
|
||
local output="$2"
|
||
if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi
|
||
|
||
ensure_dependencies pv
|
||
set -o pipefail
|
||
|
||
# Content-Length aus HTTP-Header holen
|
||
local content_length
|
||
content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true)
|
||
|
||
if [[ -z "$content_length" ]]; then
|
||
if ! curl -fL# -o "$output" "$url"; then
|
||
msg_error "Download failed"
|
||
return 1
|
||
fi
|
||
else
|
||
if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then
|
||
msg_error "Download failed"
|
||
return 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Ensures /usr/local/bin is permanently in system PATH.
|
||
#
|
||
# Description:
|
||
# - Adds to /etc/profile.d if not present
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function ensure_usr_local_bin_persist() {
|
||
local PROFILE_FILE="/etc/profile.d/custom_path.sh"
|
||
|
||
if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then
|
||
echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE"
|
||
chmod +x "$PROFILE_FILE"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Downloads and deploys latest GitHub release (source, binary, tarball, asset).
|
||
#
|
||
# Description:
|
||
# - Fetches latest release metadata from GitHub API
|
||
# - Supports the following modes:
|
||
# - tarball: Source code tarball (default if omitted)
|
||
# - source: Alias for tarball (same behavior)
|
||
# - binary: .deb package install (arch-dependent)
|
||
# - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries)
|
||
# - singlefile: Standalone binary (no archive, direct chmod +x install)
|
||
# - Handles download, extraction/installation and version tracking in ~/.<app>
|
||
#
|
||
# Parameters:
|
||
# $1 APP - Application name (used for install path and version file)
|
||
# $2 REPO - GitHub repository in form user/repo
|
||
# $3 MODE - Release type:
|
||
# tarball → source tarball (.tar.gz)
|
||
# binary → .deb file (auto-arch matched)
|
||
# prebuild → prebuilt archive (e.g. tar.gz)
|
||
# singlefile→ standalone binary (chmod +x)
|
||
# $4 VERSION - Optional release tag (default: latest)
|
||
# $5 TARGET_DIR - Optional install path (default: /opt/<app>)
|
||
# $6 ASSET_FILENAME - Required for:
|
||
# - prebuild → archive filename or pattern
|
||
# - singlefile→ binary filename or pattern
|
||
#
|
||
# Optional:
|
||
# - Set GITHUB_TOKEN env var to increase API rate limit (recommended for CI/CD).
|
||
#
|
||
# Examples:
|
||
# # 1. Minimal: Fetch and deploy source tarball
|
||
# fetch_and_deploy_gh_release "myapp" "myuser/myapp"
|
||
#
|
||
# # 2. Binary install via .deb asset (architecture auto-detected)
|
||
# fetch_and_deploy_gh_release "myapp" "myuser/myapp" "binary"
|
||
#
|
||
# # 3. Prebuilt archive (.tar.gz) with asset filename match
|
||
# fetch_and_deploy_gh_release "hanko" "teamhanko/hanko" "prebuild" "latest" "/opt/hanko" "hanko_Linux_x86_64.tar.gz"
|
||
#
|
||
# # 4. Single binary (chmod +x) like Argus, Promtail etc.
|
||
# fetch_and_deploy_gh_release "argus" "release-argus/Argus" "singlefile" "0.26.3" "/opt/argus" "Argus-.*linux-amd64"
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function fetch_and_deploy_gh_release() {
|
||
local app="$1"
|
||
local repo="$2"
|
||
local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile
|
||
local version="${4:-latest}"
|
||
local target="${5:-/opt/$app}"
|
||
local asset_pattern="${6:-}"
|
||
|
||
local app_lc=$(echo "${app,,}" | tr -d ' ')
|
||
local version_file="$HOME/.${app_lc}"
|
||
|
||
local api_timeout="--connect-timeout 10 --max-time 60"
|
||
local download_timeout="--connect-timeout 15 --max-time 900"
|
||
|
||
local current_version=""
|
||
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
|
||
|
||
ensure_dependencies jq
|
||
|
||
local api_url="https://api.github.com/repos/$repo/releases"
|
||
[[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest"
|
||
local header=()
|
||
[[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN")
|
||
|
||
# dns pre check
|
||
local gh_host
|
||
gh_host=$(awk -F/ '{print $3}' <<<"$api_url")
|
||
if ! getent hosts "$gh_host" &>/dev/null; then
|
||
msg_error "DNS resolution failed for $gh_host – check /etc/resolv.conf or networking"
|
||
return 1
|
||
fi
|
||
|
||
local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code
|
||
|
||
while ((attempt <= max_retries)); do
|
||
resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url") && success=true && break
|
||
sleep "$retry_delay"
|
||
((attempt++))
|
||
done
|
||
|
||
if ! $success; then
|
||
msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts"
|
||
return 1
|
||
fi
|
||
|
||
http_code="${resp:(-3)}"
|
||
[[ "$http_code" != "200" ]] && {
|
||
msg_error "GitHub API returned HTTP $http_code"
|
||
return 1
|
||
}
|
||
|
||
local json tag_name
|
||
json=$(</tmp/gh_rel.json)
|
||
tag_name=$(echo "$json" | jq -r '.tag_name // .name // empty')
|
||
[[ "$tag_name" =~ ^v ]] && version="${tag_name:1}" || version="$tag_name"
|
||
|
||
if [[ "$current_version" == "$version" ]]; then
|
||
$STD msg_ok "$app is already up-to-date (v$version)"
|
||
return 0
|
||
fi
|
||
|
||
local tmpdir
|
||
tmpdir=$(mktemp -d) || return 1
|
||
local filename="" url=""
|
||
|
||
msg_info "Fetching GitHub release: $app ($version)"
|
||
|
||
local clean_install=false
|
||
[[ -n "${CLEAN_INSTALL:-}" && "$CLEAN_INSTALL" == "1" ]] && clean_install=true
|
||
|
||
### Tarball Mode ###
|
||
if [[ "$mode" == "tarball" || "$mode" == "source" ]]; then
|
||
url=$(echo "$json" | jq -r '.tarball_url // empty')
|
||
[[ -z "$url" ]] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
|
||
filename="${app_lc}-${version}.tar.gz"
|
||
|
||
curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url" || {
|
||
msg_error "Download failed: $url"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
mkdir -p "$target"
|
||
if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
|
||
rm -rf "${target:?}/"*
|
||
fi
|
||
|
||
tar -xzf "$tmpdir/$filename" -C "$tmpdir"
|
||
local unpack_dir
|
||
unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1)
|
||
|
||
shopt -s dotglob nullglob
|
||
cp -r "$unpack_dir"/* "$target/"
|
||
shopt -u dotglob nullglob
|
||
|
||
### Binary Mode ###
|
||
elif [[ "$mode" == "binary" ]]; then
|
||
local arch
|
||
arch=$(dpkg --print-architecture 2>/dev/null || uname -m)
|
||
[[ "$arch" == "x86_64" ]] && arch="amd64"
|
||
[[ "$arch" == "aarch64" ]] && arch="arm64"
|
||
|
||
local assets url_match=""
|
||
assets=$(echo "$json" | jq -r '.assets[].browser_download_url')
|
||
|
||
# If explicit filename pattern is provided (param $6), match that first
|
||
if [[ -n "$asset_pattern" ]]; then
|
||
for u in $assets; do
|
||
case "${u##*/}" in
|
||
$asset_pattern)
|
||
url_match="$u"
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
fi
|
||
|
||
# If no match via explicit pattern, fall back to architecture heuristic
|
||
if [[ -z "$url_match" ]]; then
|
||
for u in $assets; do
|
||
if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then
|
||
url_match="$u"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Fallback: any .deb file
|
||
if [[ -z "$url_match" ]]; then
|
||
for u in $assets; do
|
||
[[ "$u" =~ \.deb$ ]] && url_match="$u" && break
|
||
done
|
||
fi
|
||
|
||
if [[ -z "$url_match" ]]; then
|
||
msg_error "No suitable .deb asset found for $app"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
fi
|
||
|
||
filename="${url_match##*/}"
|
||
curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url_match" || {
|
||
msg_error "Download failed: $url_match"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
chmod 644 "$tmpdir/$filename"
|
||
$STD apt install -y "$tmpdir/$filename" || {
|
||
$STD dpkg -i "$tmpdir/$filename" || {
|
||
msg_error "Both apt and dpkg installation failed"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
}
|
||
|
||
### Prebuild Mode ###
|
||
elif [[ "$mode" == "prebuild" ]]; then
|
||
local pattern="${6%\"}"
|
||
pattern="${pattern#\"}"
|
||
[[ -z "$pattern" ]] && {
|
||
msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
local asset_url=""
|
||
for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do
|
||
filename_candidate="${u##*/}"
|
||
case "$filename_candidate" in
|
||
$pattern)
|
||
asset_url="$u"
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
[[ -z "$asset_url" ]] && {
|
||
msg_error "No asset matching '$pattern' found"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
filename="${asset_url##*/}"
|
||
curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || {
|
||
msg_error "Download failed: $asset_url"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
local unpack_tmp
|
||
unpack_tmp=$(mktemp -d)
|
||
mkdir -p "$target"
|
||
if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
|
||
rm -rf "${target:?}/"*
|
||
fi
|
||
|
||
if [[ "$filename" == *.zip ]]; then
|
||
ensure_dependencies unzip
|
||
unzip -q "$tmpdir/$filename" -d "$unpack_tmp"
|
||
elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then
|
||
tar -xf "$tmpdir/$filename" -C "$unpack_tmp"
|
||
else
|
||
msg_error "Unsupported archive format: $filename"
|
||
rm -rf "$tmpdir" "$unpack_tmp"
|
||
return 1
|
||
fi
|
||
|
||
local top_dirs
|
||
top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||
local top_entries inner_dir
|
||
top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1)
|
||
if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then
|
||
# Strip leading folder
|
||
inner_dir="$top_entries"
|
||
shopt -s dotglob nullglob
|
||
if compgen -G "$inner_dir/*" >/dev/null; then
|
||
cp -r "$inner_dir"/* "$target/" || {
|
||
msg_error "Failed to copy contents from $inner_dir to $target"
|
||
rm -rf "$tmpdir" "$unpack_tmp"
|
||
return 1
|
||
}
|
||
else
|
||
msg_error "Inner directory is empty: $inner_dir"
|
||
rm -rf "$tmpdir" "$unpack_tmp"
|
||
return 1
|
||
fi
|
||
shopt -u dotglob nullglob
|
||
else
|
||
# Copy all contents
|
||
shopt -s dotglob nullglob
|
||
if compgen -G "$unpack_tmp/*" >/dev/null; then
|
||
cp -r "$unpack_tmp"/* "$target/" || {
|
||
msg_error "Failed to copy contents to $target"
|
||
rm -rf "$tmpdir" "$unpack_tmp"
|
||
return 1
|
||
}
|
||
else
|
||
msg_error "Unpacked archive is empty"
|
||
rm -rf "$tmpdir" "$unpack_tmp"
|
||
return 1
|
||
fi
|
||
shopt -u dotglob nullglob
|
||
fi
|
||
|
||
### Singlefile Mode ###
|
||
elif [[ "$mode" == "singlefile" ]]; then
|
||
local pattern="${6%\"}"
|
||
pattern="${pattern#\"}"
|
||
[[ -z "$pattern" ]] && {
|
||
msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
local asset_url=""
|
||
for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do
|
||
filename_candidate="${u##*/}"
|
||
case "$filename_candidate" in
|
||
$pattern)
|
||
asset_url="$u"
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
[[ -z "$asset_url" ]] && {
|
||
msg_error "No asset matching '$pattern' found"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
filename="${asset_url##*/}"
|
||
mkdir -p "$target"
|
||
|
||
local use_filename="${USE_ORIGINAL_FILENAME:-false}"
|
||
local target_file="$app"
|
||
[[ "$use_filename" == "true" ]] && target_file="$filename"
|
||
|
||
curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || {
|
||
msg_error "Download failed: $asset_url"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
}
|
||
|
||
if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then
|
||
chmod +x "$target/$target_file"
|
||
fi
|
||
|
||
else
|
||
msg_error "Unknown mode: $mode"
|
||
rm -rf "$tmpdir"
|
||
return 1
|
||
fi
|
||
|
||
echo "$version" >"$version_file"
|
||
msg_ok "Deployed: $app ($version)"
|
||
rm -rf "$tmpdir"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Loads LOCAL_IP from persistent store or detects if missing.
|
||
#
|
||
# Description:
|
||
# - Loads from /run/local-ip.env or performs runtime lookup
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function import_local_ip() {
|
||
local IP_FILE="/run/local-ip.env"
|
||
if [[ -f "$IP_FILE" ]]; then
|
||
# shellcheck disable=SC1090
|
||
source "$IP_FILE"
|
||
fi
|
||
|
||
if [[ -z "${LOCAL_IP:-}" ]]; then
|
||
get_current_ip() {
|
||
local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default")
|
||
local ip
|
||
|
||
for target in "${targets[@]}"; do
|
||
if [[ "$target" == "default" ]]; then
|
||
ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
else
|
||
ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
fi
|
||
if [[ -n "$ip" ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
LOCAL_IP="$(get_current_ip || true)"
|
||
if [[ -z "$LOCAL_IP" ]]; then
|
||
msg_error "Could not determine LOCAL_IP"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
export LOCAL_IP
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs Adminer (Debian/Ubuntu via APT, Alpine via direct download).
|
||
#
|
||
# Description:
|
||
# - Adds Adminer to Apache or web root
|
||
# - Supports Alpine and Debian-based systems
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_adminer() {
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "adminer")
|
||
|
||
if grep -qi alpine /etc/os-release; then
|
||
msg_info "Installing Adminer (Alpine)"
|
||
mkdir -p /var/www/localhost/htdocs/adminer
|
||
curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
|
||
-o /var/www/localhost/htdocs/adminer/index.php || {
|
||
msg_error "Failed to download Adminer"
|
||
return 1
|
||
}
|
||
cache_installed_version "adminer" "latest-alpine"
|
||
msg_ok "Installed Adminer (Alpine)"
|
||
else
|
||
msg_info "Installing Adminer (Debian/Ubuntu)"
|
||
ensure_dependencies adminer
|
||
$STD a2enconf adminer
|
||
$STD systemctl reload apache2
|
||
local VERSION
|
||
VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}')
|
||
cache_installed_version "adminer" "${VERSION:-unknown}"
|
||
msg_ok "Installed Adminer (Debian/Ubuntu)"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or updates Composer globally (robust, idempotent).
|
||
#
|
||
# - Installs to /usr/local/bin/composer
|
||
# - Removes old binaries/symlinks in /usr/bin, /bin, /root/.composer, etc.
|
||
# - Ensures /usr/local/bin is in PATH (permanent)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_composer() {
|
||
local COMPOSER_BIN="/usr/local/bin/composer"
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "composer")
|
||
export COMPOSER_ALLOW_SUPERUSER=1
|
||
|
||
for old in /usr/bin/composer /bin/composer /root/.composer/vendor/bin/composer; do
|
||
[[ -e "$old" && "$old" != "$COMPOSER_BIN" ]] && rm -f "$old"
|
||
done
|
||
|
||
ensure_usr_local_bin_persist
|
||
export PATH="/usr/local/bin:$PATH"
|
||
|
||
if [[ -x "$COMPOSER_BIN" ]]; then
|
||
local CURRENT_VERSION
|
||
CURRENT_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
|
||
msg_info "Updating Composer from $CURRENT_VERSION to latest"
|
||
else
|
||
msg_info "Installing Composer"
|
||
fi
|
||
|
||
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
|
||
msg_error "Failed to download Composer installer"
|
||
return 1
|
||
}
|
||
|
||
$STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
|
||
rm -f /tmp/composer-setup.php
|
||
|
||
if [[ ! -x "$COMPOSER_BIN" ]]; then
|
||
msg_error "Composer installation failed"
|
||
return 1
|
||
fi
|
||
|
||
chmod +x "$COMPOSER_BIN"
|
||
$STD "$COMPOSER_BIN" self-update --no-interaction || true
|
||
|
||
local FINAL_VERSION
|
||
FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
|
||
cache_installed_version "composer" "$FINAL_VERSION"
|
||
msg_ok "Installed Composer $FINAL_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs FFmpeg from source or prebuilt binary (Debian/Ubuntu only).
|
||
#
|
||
# Description:
|
||
# - Downloads and builds FFmpeg from GitHub (https://github.com/FFmpeg/FFmpeg)
|
||
# - Supports specific version override via FFMPEG_VERSION (e.g. n7.1.1)
|
||
# - Supports build profile via FFMPEG_TYPE:
|
||
# - minimal : x264, vpx, mp3 only
|
||
# - medium : adds subtitles, fonts, opus, vorbis
|
||
# - full : adds dav1d, svt-av1, zlib, numa
|
||
# - binary : downloads static build (johnvansickle.com)
|
||
# - Defaults to latest stable version and full feature set
|
||
#
|
||
# Notes:
|
||
# - Requires: curl, jq, build-essential, and matching codec libraries
|
||
# - Result is installed to /usr/local/bin/ffmpeg
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_ffmpeg() {
|
||
local TMP_DIR=$(mktemp -d)
|
||
local GITHUB_REPO="FFmpeg/FFmpeg"
|
||
local VERSION="${FFMPEG_VERSION:-latest}"
|
||
local TYPE="${FFMPEG_TYPE:-full}"
|
||
local BIN_PATH="/usr/local/bin/ffmpeg"
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "ffmpeg")
|
||
|
||
# Binary fallback mode
|
||
if [[ "$TYPE" == "binary" ]]; then
|
||
msg_info "Installing FFmpeg (static binary)"
|
||
curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" || {
|
||
msg_error "Failed to download FFmpeg binary"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR"
|
||
local EXTRACTED_DIR
|
||
EXTRACTED_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*")
|
||
cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH"
|
||
cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe
|
||
chmod +x "$BIN_PATH" /usr/local/bin/ffprobe
|
||
local FINAL_VERSION=$($BIN_PATH -version | head -n1 | awk '{print $3}')
|
||
rm -rf "$TMP_DIR"
|
||
cache_installed_version "ffmpeg" "$FINAL_VERSION"
|
||
ensure_usr_local_bin_persist
|
||
msg_ok "Installed FFmpeg binary $FINAL_VERSION"
|
||
return 0
|
||
fi
|
||
|
||
ensure_dependencies jq
|
||
|
||
# Auto-detect latest stable version if none specified
|
||
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
|
||
VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" |
|
||
jq -r '.[].name' |
|
||
grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' |
|
||
sort -V | tail -n1)
|
||
fi
|
||
|
||
if [[ -z "$VERSION" ]]; then
|
||
msg_error "Could not determine FFmpeg version"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
msg_info "Installing FFmpeg ${VERSION} ($TYPE)"
|
||
|
||
# Dependency selection
|
||
local DEPS=(build-essential yasm nasm pkg-config)
|
||
case "$TYPE" in
|
||
minimal)
|
||
DEPS+=(libx264-dev libvpx-dev libmp3lame-dev)
|
||
;;
|
||
medium)
|
||
DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev)
|
||
;;
|
||
full)
|
||
DEPS+=(
|
||
libx264-dev libx265-dev libvpx-dev libmp3lame-dev
|
||
libfreetype6-dev libass-dev libopus-dev libvorbis-dev
|
||
libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev
|
||
libva-dev libdrm-dev
|
||
)
|
||
;;
|
||
*)
|
||
msg_error "Invalid FFMPEG_TYPE: $TYPE"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
ensure_dependencies "${DEPS[@]}"
|
||
|
||
curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || {
|
||
msg_error "Failed to download FFmpeg source"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || {
|
||
msg_error "Failed to extract FFmpeg"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
cd "$TMP_DIR/FFmpeg-"* || {
|
||
msg_error "Source extraction failed"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
local args=(
|
||
--enable-gpl
|
||
--enable-shared
|
||
--enable-nonfree
|
||
--disable-static
|
||
--enable-libx264
|
||
--enable-libvpx
|
||
--enable-libmp3lame
|
||
)
|
||
|
||
if [[ "$TYPE" != "minimal" ]]; then
|
||
args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis)
|
||
fi
|
||
|
||
if [[ "$TYPE" == "full" ]]; then
|
||
args+=(--enable-libx265 --enable-libdav1d --enable-zlib)
|
||
args+=(--enable-vaapi --enable-libdrm)
|
||
fi
|
||
|
||
if [[ ${#args[@]} -eq 0 ]]; then
|
||
msg_error "FFmpeg configure args array is empty"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
$STD ./configure "${args[@]}"
|
||
$STD make -j"$(nproc)"
|
||
$STD make install
|
||
echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf
|
||
$STD ldconfig
|
||
|
||
ldconfig -p | grep libavdevice >/dev/null || {
|
||
msg_error "libavdevice not registered with dynamic linker"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
if ! command -v ffmpeg &>/dev/null; then
|
||
msg_error "FFmpeg installation failed"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
local FINAL_VERSION
|
||
FINAL_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}')
|
||
rm -rf "$TMP_DIR"
|
||
cache_installed_version "ffmpeg" "$FINAL_VERSION"
|
||
ensure_usr_local_bin_persist
|
||
msg_ok "Installed FFmpeg $FINAL_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs Go (Golang) from official tarball.
|
||
#
|
||
# Description:
|
||
# - Determines system architecture
|
||
# - Downloads latest version if GO_VERSION not set
|
||
#
|
||
# Variables:
|
||
# GO_VERSION - Version to install (e.g. 1.22.2 or latest)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_go() {
|
||
local ARCH
|
||
case "$(uname -m)" in
|
||
x86_64) ARCH="amd64" ;;
|
||
aarch64) ARCH="arm64" ;;
|
||
*)
|
||
msg_error "Unsupported architecture: $(uname -m)"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
# Determine version
|
||
if [[ -z "${GO_VERSION:-}" || "${GO_VERSION}" == "latest" ]]; then
|
||
GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//')
|
||
if [[ -z "$GO_VERSION" ]]; then
|
||
msg_error "Could not determine latest Go version"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local GO_BIN="/usr/local/bin/go"
|
||
local GO_INSTALL_DIR="/usr/local/go"
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "go")
|
||
|
||
if [[ -x "$GO_BIN" ]]; then
|
||
local CURRENT_VERSION
|
||
CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//')
|
||
if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$GO_VERSION" ]]; then
|
||
return 0
|
||
fi
|
||
cache_installed_version "go" "$GO_VERSION"
|
||
return 0
|
||
else
|
||
msg_info "Upgrading Go from $CURRENT_VERSION to $GO_VERSION"
|
||
rm -rf "$GO_INSTALL_DIR"
|
||
fi
|
||
else
|
||
msg_info "Installing Go $GO_VERSION"
|
||
fi
|
||
|
||
local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
|
||
local URL="https://go.dev/dl/${TARBALL}"
|
||
local TMP_TAR=$(mktemp)
|
||
|
||
curl -fsSL "$URL" -o "$TMP_TAR" || {
|
||
msg_error "Failed to download $TARBALL"
|
||
rm -f "$TMP_TAR"
|
||
return 1
|
||
}
|
||
|
||
$STD tar -C /usr/local -xzf "$TMP_TAR"
|
||
ln -sf /usr/local/go/bin/go /usr/local/bin/go
|
||
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
||
rm -f "$TMP_TAR"
|
||
|
||
cache_installed_version "go" "$GO_VERSION"
|
||
ensure_usr_local_bin_persist
|
||
msg_ok "Installed Go $GO_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or updates Ghostscript (gs) from source.
|
||
#
|
||
# Description:
|
||
# - Fetches latest release
|
||
# - Builds and installs system-wide
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_gs() {
|
||
local TMP_DIR=$(mktemp -d)
|
||
local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0")
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "ghostscript")
|
||
|
||
ensure_dependencies jq
|
||
|
||
local RELEASE_JSON
|
||
RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest)
|
||
local LATEST_VERSION
|
||
LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^gs//')
|
||
local LATEST_VERSION_DOTTED
|
||
LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | jq -r '.name' | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+')
|
||
|
||
if [[ -z "$LATEST_VERSION" || -z "$LATEST_VERSION_DOTTED" ]]; then
|
||
msg_error "Could not determine latest Ghostscript version"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then
|
||
if [[ "$CACHED_VERSION" == "$LATEST_VERSION_DOTTED" ]]; then
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED"
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Installing Ghostscript $LATEST_VERSION_DOTTED"
|
||
|
||
curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" || {
|
||
msg_error "Failed to download Ghostscript"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then
|
||
msg_error "Failed to extract Ghostscript archive"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || {
|
||
msg_error "Failed to enter Ghostscript source directory"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
ensure_dependencies build-essential libpng-dev zlib1g-dev
|
||
|
||
$STD ./configure
|
||
$STD make -j"$(nproc)"
|
||
$STD make install
|
||
|
||
hash -r
|
||
if [[ ! -x "$(command -v gs)" ]]; then
|
||
if [[ -x /usr/local/bin/gs ]]; then
|
||
ln -sf /usr/local/bin/gs /usr/bin/gs
|
||
fi
|
||
fi
|
||
|
||
rm -rf "$TMP_DIR"
|
||
cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED"
|
||
ensure_usr_local_bin_persist
|
||
msg_ok "Installed Ghostscript $LATEST_VERSION_DOTTED"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs ImageMagick 7 from source (Debian/Ubuntu only).
|
||
#
|
||
# Description:
|
||
# - Downloads the latest ImageMagick source tarball
|
||
# - Builds and installs ImageMagick to /usr/local
|
||
# - Configures dynamic linker (ldconfig)
|
||
#
|
||
# Notes:
|
||
# - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc.
|
||
# ------------------------------------------------------------------------------
|
||
function setup_imagemagick() {
|
||
local TMP_DIR=$(mktemp -d)
|
||
local VERSION=""
|
||
local BINARY_PATH="/usr/local/bin/magick"
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "imagemagick")
|
||
|
||
if command -v magick &>/dev/null; then
|
||
VERSION=$(magick -version | awk '/^Version/ {print $3}')
|
||
if [[ "$CACHED_VERSION" == "$VERSION" ]]; then
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
cache_installed_version "imagemagick" "$VERSION"
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Installing ImageMagick (Patience)"
|
||
|
||
ensure_dependencies \
|
||
build-essential \
|
||
libtool \
|
||
libjpeg-dev \
|
||
libpng-dev \
|
||
libtiff-dev \
|
||
libwebp-dev \
|
||
libheif-dev \
|
||
libde265-dev \
|
||
libopenjp2-7-dev \
|
||
libxml2-dev \
|
||
liblcms2-dev \
|
||
libfreetype6-dev \
|
||
libraw-dev \
|
||
libfftw3-dev \
|
||
liblqr-1-0-dev \
|
||
libgsl-dev \
|
||
pkg-config \
|
||
ghostscript
|
||
|
||
curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" || {
|
||
msg_error "Failed to download ImageMagick"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" || {
|
||
msg_error "Failed to extract ImageMagick"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
cd "$TMP_DIR"/ImageMagick-* || {
|
||
msg_error "Source extraction failed"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
$STD ./configure --disable-static
|
||
$STD make -j"$(nproc)"
|
||
$STD make install
|
||
$STD ldconfig /usr/local/lib
|
||
|
||
if [[ ! -x "$BINARY_PATH" ]]; then
|
||
msg_error "ImageMagick installation failed"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}')
|
||
rm -rf "$TMP_DIR"
|
||
cache_installed_version "imagemagick" "$VERSION"
|
||
ensure_usr_local_bin_persist
|
||
msg_ok "Installed ImageMagick $VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs Temurin JDK via Adoptium APT repository.
|
||
#
|
||
# Description:
|
||
# - Removes previous JDK if version mismatch
|
||
# - Installs or upgrades to specified JAVA_VERSION
|
||
#
|
||
# Variables:
|
||
# JAVA_VERSION - Temurin JDK version to install (e.g. 17, 21)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_java() {
|
||
local JAVA_VERSION="${JAVA_VERSION:-21}"
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)
|
||
local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk"
|
||
|
||
# Check cached version
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "temurin-jdk")
|
||
|
||
# Add repo nur wenn nötig
|
||
if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "adoptium"
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb")
|
||
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"adoptium" \
|
||
"https://packages.adoptium.net/artifactory/api/gpg/key/public" \
|
||
"https://packages.adoptium.net/artifactory/deb" \
|
||
"$SUITE" \
|
||
"main" \
|
||
"amd64 arm64"
|
||
fi
|
||
|
||
local INSTALLED_VERSION=""
|
||
if dpkg -l | grep -q "temurin-.*-jdk"; then
|
||
INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+')
|
||
fi
|
||
|
||
if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$JAVA_VERSION" ]]; then
|
||
# Already at correct version, just upgrade if available
|
||
upgrade_package "$DESIRED_PACKAGE"
|
||
else
|
||
msg_info "Upgrading Temurin JDK $JAVA_VERSION"
|
||
$STD apt update
|
||
$STD apt install --only-upgrade -y "$DESIRED_PACKAGE"
|
||
cache_installed_version "temurin-jdk" "$JAVA_VERSION"
|
||
msg_ok "Upgraded Temurin JDK $JAVA_VERSION"
|
||
fi
|
||
else
|
||
if [[ -n "$INSTALLED_VERSION" ]]; then
|
||
msg_info "Removing old Temurin JDK $INSTALLED_VERSION"
|
||
$STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk"
|
||
msg_ok "Removed old Temurin JDK"
|
||
fi
|
||
|
||
msg_info "Installing Temurin JDK $JAVA_VERSION"
|
||
$STD apt install -y "$DESIRED_PACKAGE"
|
||
cache_installed_version "temurin-jdk" "$JAVA_VERSION"
|
||
msg_ok "Installed Temurin JDK $JAVA_VERSION"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs a local IP updater script using networkd-dispatcher.
|
||
#
|
||
# Description:
|
||
# - Stores current IP in /run/local-ip.env
|
||
# - Automatically runs on network changes
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_local_ip_helper() {
|
||
local BASE_DIR="/usr/local/community-scripts/ip-management"
|
||
local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh"
|
||
local IP_FILE="/run/local-ip.env"
|
||
local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh"
|
||
|
||
mkdir -p "$BASE_DIR"
|
||
|
||
# Install networkd-dispatcher if not present
|
||
if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then
|
||
$STD apt update
|
||
$STD apt install -y networkd-dispatcher
|
||
fi
|
||
|
||
# Write update_local_ip.sh
|
||
cat <<'EOF' >"$SCRIPT_PATH"
|
||
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
IP_FILE="/run/local-ip.env"
|
||
mkdir -p "$(dirname "$IP_FILE")"
|
||
|
||
get_current_ip() {
|
||
local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default")
|
||
local ip
|
||
|
||
for target in "${targets[@]}"; do
|
||
if [[ "$target" == "default" ]]; then
|
||
ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
else
|
||
ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
fi
|
||
if [[ -n "$ip" ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
current_ip="$(get_current_ip)"
|
||
|
||
if [[ -z "$current_ip" ]]; then
|
||
echo "[ERROR] Could not detect local IP" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -f "$IP_FILE" ]]; then
|
||
source "$IP_FILE"
|
||
[[ "$LOCAL_IP" == "$current_ip" ]] && exit 0
|
||
fi
|
||
|
||
echo "LOCAL_IP=$current_ip" > "$IP_FILE"
|
||
echo "[INFO] LOCAL_IP updated to $current_ip"
|
||
EOF
|
||
|
||
chmod +x "$SCRIPT_PATH"
|
||
|
||
# Install dispatcher hook
|
||
mkdir -p "$(dirname "$DISPATCHER_SCRIPT")"
|
||
cat <<EOF >"$DISPATCHER_SCRIPT"
|
||
#!/bin/bash
|
||
$SCRIPT_PATH
|
||
EOF
|
||
|
||
chmod +x "$DISPATCHER_SCRIPT"
|
||
systemctl enable -q --now networkd-dispatcher.service
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or updates MariaDB from official repo.
|
||
#
|
||
# Description:
|
||
# - Detects current MariaDB version and replaces it if necessary
|
||
# - Preserves existing database data
|
||
# - Dynamically determines latest GA version if "latest" is given
|
||
#
|
||
# Variables:
|
||
# MARIADB_VERSION - MariaDB version to install (e.g. 10.11, latest) (default: latest)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
setup_mariadb() {
|
||
local MARIADB_VERSION="${MARIADB_VERSION:-latest}"
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
|
||
|
||
if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then
|
||
msg_error "MariaDB mirror not reachable"
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$MARIADB_VERSION" == "latest" ]]; then
|
||
MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ |
|
||
grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' |
|
||
grep -vE 'rc/|rolling/' |
|
||
sed 's|/||' |
|
||
sort -Vr |
|
||
head -n1)
|
||
if [[ -z "$MARIADB_VERSION" ]]; then
|
||
msg_error "Could not determine latest GA MariaDB version"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local CURRENT_VERSION=""
|
||
if command -v mariadb >/dev/null; then
|
||
CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||
fi
|
||
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "mariadb")
|
||
|
||
if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$MARIADB_VERSION" ]]; then
|
||
upgrade_package mariadb-server
|
||
upgrade_package mariadb-client
|
||
else
|
||
msg_info "Upgrading MariaDB $MARIADB_VERSION"
|
||
$STD apt update
|
||
$STD apt install --only-upgrade -y mariadb-server mariadb-client
|
||
cache_installed_version "mariadb" "$MARIADB_VERSION"
|
||
msg_ok "Upgraded MariaDB $MARIADB_VERSION"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Installing MariaDB $MARIADB_VERSION"
|
||
|
||
if [[ -n "$CURRENT_VERSION" ]]; then
|
||
$STD systemctl stop mariadb >/dev/null 2>&1 || true
|
||
$STD apt purge -y 'mariadb*' || true
|
||
fi
|
||
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "mariadb"
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}")
|
||
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"mariadb" \
|
||
"https://mariadb.org/mariadb_release_signing_key.asc" \
|
||
"http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \
|
||
"$SUITE" \
|
||
"main" \
|
||
"amd64 arm64"
|
||
|
||
local MARIADB_MAJOR_MINOR
|
||
MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}')
|
||
if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then
|
||
echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections
|
||
fi
|
||
|
||
DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || {
|
||
cleanup_old_repo_files "mariadb"
|
||
$STD apt update
|
||
DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client
|
||
}
|
||
|
||
cache_installed_version "mariadb" "$MARIADB_VERSION"
|
||
msg_ok "Installed MariaDB $MARIADB_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or updates MongoDB to specified major version.
|
||
#
|
||
# Description:
|
||
# - Preserves data across installations
|
||
# - Adds official MongoDB repo
|
||
#
|
||
# Variables:
|
||
# MONGO_VERSION - MongoDB major version to install (e.g. 7.0, 8.0)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_mongodb() {
|
||
local MONGO_VERSION="${MONGO_VERSION:-8.0}"
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{ gsub(/"/,"",$2); print $2 }' /etc/os-release)
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release)
|
||
|
||
# Check AVX support
|
||
if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then
|
||
local major="${MONGO_VERSION%%.*}"
|
||
if ((major > 5)); then
|
||
msg_error "MongoDB ${MONGO_VERSION} requires AVX support, which is not available on this system."
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
case "$DISTRO_ID" in
|
||
ubuntu)
|
||
MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu"
|
||
REPO_COMPONENT="multiverse"
|
||
;;
|
||
debian)
|
||
MONGO_BASE_URL="https://repo.mongodb.org/apt/debian"
|
||
REPO_COMPONENT="main"
|
||
;;
|
||
*)
|
||
msg_error "Unsupported distribution: $DISTRO_ID"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
local INSTALLED_VERSION=""
|
||
if command -v mongod >/dev/null; then
|
||
INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2)
|
||
fi
|
||
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "mongodb")
|
||
|
||
if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$MONGO_VERSION" ]]; then
|
||
upgrade_package mongodb-org
|
||
else
|
||
msg_info "Upgrading MongoDB $MONGO_VERSION"
|
||
$STD apt update
|
||
$STD apt install --only-upgrade -y mongodb-org
|
||
cache_installed_version "mongodb" "$MONGO_VERSION"
|
||
msg_ok "Upgraded MongoDB $MONGO_VERSION"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Installing MongoDB $MONGO_VERSION"
|
||
|
||
if [[ -n "$INSTALLED_VERSION" ]]; then
|
||
$STD systemctl stop mongod || true
|
||
$STD apt purge -y mongodb-org || true
|
||
fi
|
||
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "mongodb-org-${MONGO_VERSION}"
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "$MONGO_BASE_URL")
|
||
|
||
# Use standardized repo setup
|
||
mkdir -p /etc/apt/keyrings
|
||
curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg"
|
||
|
||
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.sources
|
||
Types: deb
|
||
URIs: ${MONGO_BASE_URL}
|
||
Suites: ${SUITE}/mongodb-org/${MONGO_VERSION}
|
||
Components: ${REPO_COMPONENT}
|
||
Architectures: amd64 arm64
|
||
Signed-By: /etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg
|
||
EOF
|
||
|
||
$STD apt update || {
|
||
msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?"
|
||
return 1
|
||
}
|
||
|
||
$STD apt install -y mongodb-org
|
||
|
||
mkdir -p /var/lib/mongodb
|
||
chown -R mongodb:mongodb /var/lib/mongodb
|
||
|
||
$STD systemctl enable mongod
|
||
safe_service_restart mongod
|
||
cache_installed_version "mongodb" "$MONGO_VERSION"
|
||
msg_ok "Installed MongoDB $MONGO_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or upgrades MySQL and configures APT repo.
|
||
#
|
||
# Description:
|
||
# - Detects existing MySQL installation
|
||
# - Purges conflicting packages before installation
|
||
# - Supports clean upgrade
|
||
#
|
||
# Variables:
|
||
# MYSQL_VERSION - MySQL version to install (e.g. 5.7, 8.0) (default: 8.0)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_mysql() {
|
||
local MYSQL_VERSION="${MYSQL_VERSION:-8.0}"
|
||
local CURRENT_VERSION=""
|
||
local NEED_INSTALL=false
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
|
||
|
||
if command -v mysql >/dev/null; then
|
||
CURRENT_VERSION="$(mysql --version | grep -oP '[0-9]+\.[0-9]+' | head -n1)"
|
||
if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then
|
||
msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION"
|
||
NEED_INSTALL=true
|
||
else
|
||
if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then
|
||
msg_info "MySQL $CURRENT_VERSION available for upgrade"
|
||
$STD apt update
|
||
$STD apt install --only-upgrade -y mysql-server
|
||
msg_ok "MySQL upgraded"
|
||
fi
|
||
return
|
||
fi
|
||
else
|
||
msg_info "Setup MySQL $MYSQL_VERSION"
|
||
NEED_INSTALL=true
|
||
fi
|
||
|
||
if [[ "$NEED_INSTALL" == true ]]; then
|
||
$STD systemctl stop mysql || true
|
||
$STD apt purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true
|
||
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "mysql"
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}")
|
||
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"mysql" \
|
||
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" \
|
||
"https://repo.mysql.com/apt/${DISTRO_ID}" \
|
||
"$SUITE" \
|
||
"mysql-${MYSQL_VERSION}" \
|
||
"amd64 arm64"
|
||
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
$STD apt install -y mysql-server
|
||
cache_installed_version "mysql" "$MYSQL_VERSION"
|
||
msg_ok "Installed MySQL $MYSQL_VERSION"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs Node.js and optional global modules.
|
||
#
|
||
# Description:
|
||
# - Installs specified Node.js version using NodeSource APT repo
|
||
# - Optionally installs or updates global npm modules
|
||
#
|
||
# Variables:
|
||
# NODE_VERSION - Node.js version to install (default: 22)
|
||
# NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0")
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_nodejs() {
|
||
local NODE_VERSION="${NODE_VERSION:-22}"
|
||
local NODE_MODULE="${NODE_MODULE:-}"
|
||
local CURRENT_NODE_VERSION=""
|
||
local NEED_NODE_INSTALL=false
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
|
||
|
||
if command -v node >/dev/null; then
|
||
CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')"
|
||
if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then
|
||
msg_info "Old Node.js $CURRENT_NODE_VERSION found, replacing with $NODE_VERSION"
|
||
NEED_NODE_INSTALL=true
|
||
fi
|
||
else
|
||
msg_info "Setup Node.js $NODE_VERSION"
|
||
NEED_NODE_INSTALL=true
|
||
fi
|
||
|
||
ensure_dependencies jq
|
||
|
||
if [[ "$NEED_NODE_INSTALL" == true ]]; then
|
||
$STD apt purge -y nodejs
|
||
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "nodesource"
|
||
|
||
# NodeSource uses 'nodistro' for all distributions - no fallback needed
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"nodesource" \
|
||
"https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \
|
||
"https://deb.nodesource.com/node_${NODE_VERSION}.x" \
|
||
"nodistro" \
|
||
"main" \
|
||
"amd64 arm64"
|
||
|
||
if ! $STD apt install -y nodejs; then
|
||
msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource"
|
||
return 1
|
||
fi
|
||
|
||
$STD npm install -g npm@latest || {
|
||
msg_warn "Failed to update npm to latest version"
|
||
}
|
||
|
||
cache_installed_version "nodejs" "$NODE_VERSION"
|
||
msg_ok "Installed Node.js ${NODE_VERSION}"
|
||
fi
|
||
|
||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||
|
||
if [[ ! -d /opt ]]; then
|
||
mkdir -p /opt
|
||
fi
|
||
cd /opt || {
|
||
msg_error "Failed to set safe working directory before npm install"
|
||
return 1
|
||
}
|
||
|
||
if [[ -n "$NODE_MODULE" ]]; then
|
||
IFS=',' read -ra MODULES <<<"$NODE_MODULE"
|
||
for mod in "${MODULES[@]}"; do
|
||
local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION
|
||
if [[ "$mod" == @*/*@* ]]; then
|
||
MODULE_NAME="${mod%@*}"
|
||
MODULE_REQ_VERSION="${mod##*@}"
|
||
elif [[ "$mod" == *"@"* ]]; then
|
||
MODULE_NAME="${mod%@*}"
|
||
MODULE_REQ_VERSION="${mod##*@}"
|
||
else
|
||
MODULE_NAME="$mod"
|
||
MODULE_REQ_VERSION="latest"
|
||
fi
|
||
|
||
if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then
|
||
MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')"
|
||
if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then
|
||
msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION"
|
||
if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then
|
||
msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION"
|
||
return 1
|
||
fi
|
||
elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then
|
||
msg_info "Updating $MODULE_NAME to latest version"
|
||
if ! $STD npm install -g "${MODULE_NAME}@latest"; then
|
||
msg_error "Failed to update $MODULE_NAME to latest version"
|
||
return 1
|
||
fi
|
||
fi
|
||
else
|
||
msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION"
|
||
if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then
|
||
msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION"
|
||
return 1
|
||
fi
|
||
fi
|
||
done
|
||
msg_ok "Installed Node.js modules: $NODE_MODULE"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs PHP with selected modules and configures Apache/FPM support.
|
||
#
|
||
# Description:
|
||
# - Adds Sury PHP repo if needed
|
||
# - Installs default and user-defined modules
|
||
# - Patches php.ini for CLI, Apache, and FPM as needed
|
||
#
|
||
# Variables:
|
||
# PHP_VERSION - PHP version to install (default: 8.4)
|
||
# PHP_MODULE - Additional comma-separated modules
|
||
# PHP_APACHE - Set YES to enable PHP with Apache
|
||
# PHP_FPM - Set YES to enable PHP-FPM
|
||
# PHP_MEMORY_LIMIT - (default: 512M)
|
||
# PHP_UPLOAD_MAX_FILESIZE - (default: 128M)
|
||
# PHP_POST_MAX_SIZE - (default: 128M)
|
||
# PHP_MAX_EXECUTION_TIME - (default: 300)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_php() {
|
||
local PHP_VERSION="${PHP_VERSION:-8.4}"
|
||
local PHP_MODULE="${PHP_MODULE:-}"
|
||
local PHP_APACHE="${PHP_APACHE:-NO}"
|
||
local PHP_FPM="${PHP_FPM:-NO}"
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
|
||
|
||
local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip"
|
||
local COMBINED_MODULES
|
||
|
||
local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}"
|
||
local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}"
|
||
local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}"
|
||
local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}"
|
||
|
||
# Merge default + user-defined modules
|
||
if [[ -n "$PHP_MODULE" ]]; then
|
||
COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}"
|
||
else
|
||
COMBINED_MODULES="${DEFAULT_MODULES}"
|
||
fi
|
||
|
||
# Deduplicate
|
||
COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -)
|
||
|
||
# Get current PHP-CLI version
|
||
local CURRENT_PHP=""
|
||
if command -v php >/dev/null 2>&1; then
|
||
CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2)
|
||
fi
|
||
|
||
if [[ -z "$CURRENT_PHP" ]]; then
|
||
msg_info "Setup PHP $PHP_VERSION"
|
||
elif [[ "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
|
||
msg_info "Old PHP $CURRENT_PHP detected, Setup new PHP $PHP_VERSION"
|
||
$STD apt purge -y "php${CURRENT_PHP//./}"* || true
|
||
fi
|
||
|
||
# Ensure Sury repo is available
|
||
if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "php"
|
||
|
||
$STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
|
||
$STD dpkg -i /tmp/debsuryorg-archive-keyring.deb
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.sury.org/php")
|
||
|
||
cat <<EOF >/etc/apt/sources.list.d/php.sources
|
||
Types: deb
|
||
URIs: https://packages.sury.org/php/
|
||
Suites: $SUITE
|
||
Components: main
|
||
Architectures: amd64 arm64
|
||
Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg
|
||
EOF
|
||
$STD apt update
|
||
fi
|
||
|
||
# Build module list
|
||
local MODULE_LIST="php${PHP_VERSION}"
|
||
IFS=',' read -ra MODULES <<<"$COMBINED_MODULES"
|
||
for mod in "${MODULES[@]}"; do
|
||
if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then
|
||
MODULE_LIST+=" php${PHP_VERSION}-${mod}"
|
||
else
|
||
msg_warn "PHP-Module ${mod} for PHP ${PHP_VERSION} not found – skipping"
|
||
fi
|
||
done
|
||
if [[ "$PHP_FPM" == "YES" ]]; then
|
||
MODULE_LIST+=" php${PHP_VERSION}-fpm"
|
||
fi
|
||
|
||
# install apache2 with PHP support if requested
|
||
if [[ "$PHP_APACHE" == "YES" ]]; then
|
||
if ! dpkg -l | grep -q "libapache2-mod-php${PHP_VERSION}"; then
|
||
msg_info "Installing Apache with PHP${PHP_VERSION} support"
|
||
$STD apt install -y apache2 libapache2-mod-php${PHP_VERSION}
|
||
else
|
||
msg_info "Apache with PHP${PHP_VERSION} already installed – skipping install"
|
||
fi
|
||
fi
|
||
|
||
# setup / update PHP modules
|
||
$STD apt install -y $MODULE_LIST
|
||
cache_installed_version "php" "$PHP_VERSION"
|
||
msg_ok "Installed PHP $PHP_VERSION"
|
||
|
||
# optional stop old PHP-FPM service
|
||
if [[ "$PHP_FPM" == "YES" && -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
|
||
$STD systemctl stop php"${CURRENT_PHP}"-fpm || true
|
||
$STD systemctl disable php"${CURRENT_PHP}"-fpm || true
|
||
fi
|
||
|
||
# Patch all relevant php.ini files (silent)
|
||
local PHP_INI_PATHS=("/etc/php/${PHP_VERSION}/cli/php.ini")
|
||
[[ "$PHP_FPM" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/fpm/php.ini")
|
||
[[ "$PHP_APACHE" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/apache2/php.ini")
|
||
for ini in "${PHP_INI_PATHS[@]}"; do
|
||
if [[ -f "$ini" ]]; then
|
||
$STD sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini"
|
||
$STD sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini"
|
||
$STD sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini"
|
||
$STD sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini"
|
||
fi
|
||
done
|
||
|
||
# patch Apache configuration if needed
|
||
if [[ "$PHP_APACHE" == "YES" ]]; then
|
||
for mod in $(ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//'); do
|
||
if [[ "$mod" != "php${PHP_VERSION}" ]]; then
|
||
$STD a2dismod "$mod" || true
|
||
fi
|
||
done
|
||
$STD a2enmod mpm_prefork
|
||
$STD a2enmod "php${PHP_VERSION}"
|
||
safe_service_restart apache2 || true
|
||
fi
|
||
|
||
# enable and restart PHP-FPM if requested
|
||
if [[ "$PHP_FPM" == "YES" ]]; then
|
||
if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then
|
||
$STD systemctl enable php${PHP_VERSION}-fpm
|
||
safe_service_restart php${PHP_VERSION}-fpm
|
||
else
|
||
msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or upgrades PostgreSQL and optional extensions/modules.
|
||
#
|
||
# Description:
|
||
# - Detects existing PostgreSQL version
|
||
# - Dumps all databases before upgrade
|
||
# - Adds PGDG repo and installs specified version
|
||
# - Installs optional PG_MODULES (e.g. postgis, contrib)
|
||
# - Restores dumped data post-upgrade
|
||
#
|
||
# Variables:
|
||
# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16)
|
||
# PG_MODULES - Comma-separated list of extensions (e.g. "postgis,contrib")
|
||
# ------------------------------------------------------------------------------
|
||
function setup_postgresql() {
|
||
local PG_VERSION="${PG_VERSION:-16}"
|
||
local PG_MODULES="${PG_MODULES:-}"
|
||
local CURRENT_PG_VERSION=""
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
local NEED_PG_INSTALL=false
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
|
||
|
||
if command -v psql >/dev/null; then
|
||
CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)"
|
||
[[ "$CURRENT_PG_VERSION" != "$PG_VERSION" ]] && NEED_PG_INSTALL=true
|
||
else
|
||
NEED_PG_INSTALL=true
|
||
fi
|
||
|
||
if [[ "$NEED_PG_INSTALL" == true ]]; then
|
||
msg_info "Installing PostgreSQL $PG_VERSION"
|
||
|
||
if [[ -n "$CURRENT_PG_VERSION" ]]; then
|
||
$STD su - postgres -c "pg_dumpall > /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql"
|
||
$STD systemctl stop postgresql
|
||
fi
|
||
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "pgdg"
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
|
||
|
||
# PGDG uses special suite naming: ${SUITE}-pgdg
|
||
SUITE="${SUITE}-pgdg"
|
||
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"pgdg" \
|
||
"https://www.postgresql.org/media/keys/ACCC4CF8.asc" \
|
||
"https://apt.postgresql.org/pub/repos/apt" \
|
||
"$SUITE" \
|
||
"main" \
|
||
"amd64 arm64"
|
||
|
||
$STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}"
|
||
|
||
if [[ -n "$CURRENT_PG_VERSION" ]]; then
|
||
$STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true
|
||
$STD su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql"
|
||
fi
|
||
|
||
$STD systemctl enable --now postgresql
|
||
cache_installed_version "postgresql" "$PG_VERSION"
|
||
msg_ok "Installed PostgreSQL $PG_VERSION"
|
||
fi
|
||
|
||
if [[ -n "$PG_MODULES" ]]; then
|
||
msg_info "Installing PostgreSQL modules"
|
||
IFS=',' read -ra MODULES <<<"$PG_MODULES"
|
||
for module in "${MODULES[@]}"; do
|
||
$STD apt install -y "postgresql-${PG_VERSION}-${module}" || {
|
||
msg_warn "Failed to install postgresql-${PG_VERSION}-${module}"
|
||
continue
|
||
}
|
||
done
|
||
msg_ok "Installed PostgreSQL modules"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
|
||
#
|
||
# Description:
|
||
# - Downloads rbenv and ruby-build from GitHub
|
||
# - Compiles and installs target Ruby version
|
||
# - Optionally installs Rails via gem
|
||
#
|
||
# Variables:
|
||
# RUBY_VERSION - Ruby version to install (default: 3.4.4)
|
||
# RUBY_INSTALL_RAILS - true/false to install Rails (default: true)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_ruby() {
|
||
local RUBY_VERSION="${RUBY_VERSION:-3.4.4}"
|
||
local RUBY_INSTALL_RAILS="${RUBY_INSTALL_RAILS:-true}"
|
||
local RBENV_DIR="$HOME/.rbenv"
|
||
local RBENV_BIN="$RBENV_DIR/bin/rbenv"
|
||
local PROFILE_FILE="$HOME/.profile"
|
||
local TMP_DIR=$(mktemp -d)
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "ruby")
|
||
|
||
msg_info "Installing Ruby $RUBY_VERSION"
|
||
|
||
ensure_dependencies jq
|
||
|
||
local RBENV_RELEASE
|
||
RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||
if [[ -z "$RBENV_RELEASE" ]]; then
|
||
msg_error "Failed to fetch latest rbenv version"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" || {
|
||
msg_error "Failed to download rbenv"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" || {
|
||
msg_error "Failed to extract rbenv"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
mkdir -p "$RBENV_DIR"
|
||
cp -r "$TMP_DIR/rbenv-${RBENV_RELEASE}/." "$RBENV_DIR/"
|
||
cd "$RBENV_DIR" && src/configure && $STD make -C src
|
||
|
||
local RUBY_BUILD_RELEASE
|
||
RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||
if [[ -z "$RUBY_BUILD_RELEASE" ]]; then
|
||
msg_error "Failed to fetch latest ruby-build version"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/v${RUBY_BUILD_RELEASE}.tar.gz" -o "$TMP_DIR/ruby-build.tar.gz" || {
|
||
msg_error "Failed to download ruby-build"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" || {
|
||
msg_error "Failed to extract ruby-build"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
mkdir -p "$RBENV_DIR/plugins/ruby-build"
|
||
cp -r "$TMP_DIR/ruby-build-${RUBY_BUILD_RELEASE}/." "$RBENV_DIR/plugins/ruby-build/"
|
||
echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt"
|
||
|
||
if ! grep -q 'rbenv init' "$PROFILE_FILE"; then
|
||
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE"
|
||
echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE"
|
||
fi
|
||
|
||
export PATH="$RBENV_DIR/bin:$PATH"
|
||
eval "$("$RBENV_BIN" init - bash)"
|
||
|
||
if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then
|
||
$STD "$RBENV_BIN" install "$RUBY_VERSION"
|
||
fi
|
||
|
||
"$RBENV_BIN" global "$RUBY_VERSION"
|
||
hash -r
|
||
|
||
if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then
|
||
$STD gem install rails
|
||
local RAILS_VERSION=$(rails -v 2>/dev/null | awk '{print $2}')
|
||
msg_ok "Installed Rails $RAILS_VERSION"
|
||
fi
|
||
|
||
rm -rf "$TMP_DIR"
|
||
cache_installed_version "ruby" "$RUBY_VERSION"
|
||
msg_ok "Installed Ruby $RUBY_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or upgrades ClickHouse database server.
|
||
#
|
||
# Description:
|
||
# - Adds ClickHouse official repository
|
||
# - Installs specified version
|
||
# - Configures systemd service
|
||
# - Supports Debian/Ubuntu with fallback mechanism
|
||
#
|
||
# Variables:
|
||
# CLICKHOUSE_VERSION - ClickHouse version to install (default: latest)
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_clickhouse() {
|
||
local CLICKHOUSE_VERSION="${CLICKHOUSE_VERSION:-latest}"
|
||
local DISTRO_ID DISTRO_CODENAME
|
||
DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
|
||
DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
|
||
|
||
# Determine latest version if needed
|
||
if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then
|
||
CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ |
|
||
grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
|
||
sort -V |
|
||
tail -n1)
|
||
if [[ -z "$CLICKHOUSE_VERSION" ]]; then
|
||
msg_error "Could not determine latest ClickHouse version"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local CURRENT_VERSION=""
|
||
if command -v clickhouse-server >/dev/null 2>&1; then
|
||
CURRENT_VERSION=$(clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
|
||
fi
|
||
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "clickhouse")
|
||
|
||
# Check if already at target version
|
||
if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
|
||
upgrade_package clickhouse-server
|
||
upgrade_package clickhouse-client
|
||
else
|
||
msg_info "Upgrading ClickHouse $CLICKHOUSE_VERSION"
|
||
$STD apt update
|
||
$STD apt install --only-upgrade -y clickhouse-server clickhouse-client
|
||
cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
|
||
msg_ok "Upgraded ClickHouse $CLICKHOUSE_VERSION"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Installing ClickHouse $CLICKHOUSE_VERSION"
|
||
|
||
# Stop existing service if upgrading
|
||
if [[ -n "$CURRENT_VERSION" ]]; then
|
||
$STD systemctl stop clickhouse-server || true
|
||
fi
|
||
|
||
# Cleanup old repository files
|
||
cleanup_old_repo_files "clickhouse"
|
||
|
||
# Ensure dependencies
|
||
ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg
|
||
|
||
# Use helper function to get fallback suite
|
||
local SUITE
|
||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.clickhouse.com/deb")
|
||
|
||
# Use standardized repo setup
|
||
setup_deb822_repo \
|
||
"clickhouse" \
|
||
"https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
|
||
"https://packages.clickhouse.com/deb" \
|
||
"$SUITE" \
|
||
"main" \
|
||
"amd64 arm64"
|
||
|
||
# Install ClickHouse packages
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
$STD apt install -y clickhouse-server clickhouse-client
|
||
|
||
# Create data directory if it doesn't exist
|
||
mkdir -p /var/lib/clickhouse
|
||
chown -R clickhouse:clickhouse /var/lib/clickhouse
|
||
|
||
# Enable and start service
|
||
$STD systemctl enable clickhouse-server
|
||
safe_service_restart clickhouse-server
|
||
|
||
cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
|
||
msg_ok "Installed ClickHouse $CLICKHOUSE_VERSION"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs Rust toolchain and optional global crates via cargo.
|
||
#
|
||
# Description:
|
||
# - Installs rustup (if missing)
|
||
# - Installs or updates desired Rust toolchain (stable, nightly, or versioned)
|
||
# - Installs or updates specified global crates using `cargo install`
|
||
#
|
||
# Notes:
|
||
# - Skips crate install if exact version is already present
|
||
# - Updates crate if newer version or different version is requested
|
||
#
|
||
# Variables:
|
||
# RUST_TOOLCHAIN - Rust toolchain to install (default: stable)
|
||
# RUST_CRATES - Comma-separated list of crates (e.g. "cargo-edit,wasm-pack@0.12.1")
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_rust() {
|
||
local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}"
|
||
local RUST_CRATES="${RUST_CRATES:-}"
|
||
local CARGO_BIN="${HOME}/.cargo/bin"
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "rust")
|
||
|
||
if ! command -v rustup &>/dev/null; then
|
||
msg_info "Installing Rust"
|
||
curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" || {
|
||
msg_error "Failed to install Rust"
|
||
return 1
|
||
}
|
||
export PATH="$CARGO_BIN:$PATH"
|
||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile"
|
||
local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}')
|
||
cache_installed_version "rust" "$RUST_VERSION"
|
||
msg_ok "Installed Rust $RUST_VERSION"
|
||
else
|
||
$STD rustup install "$RUST_TOOLCHAIN"
|
||
$STD rustup default "$RUST_TOOLCHAIN"
|
||
$STD rustup update "$RUST_TOOLCHAIN"
|
||
local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}')
|
||
cache_installed_version "rust" "$RUST_VERSION"
|
||
msg_ok "Updated Rust toolchain to $RUST_TOOLCHAIN ($RUST_VERSION)"
|
||
fi
|
||
|
||
if [[ -n "$RUST_CRATES" ]]; then
|
||
IFS=',' read -ra CRATES <<<"$RUST_CRATES"
|
||
for crate in "${CRATES[@]}"; do
|
||
local NAME VER INSTALLED_VER
|
||
if [[ "$crate" == *"@"* ]]; then
|
||
NAME="${crate%@*}"
|
||
VER="${crate##*@}"
|
||
else
|
||
NAME="$crate"
|
||
VER=""
|
||
fi
|
||
|
||
INSTALLED_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v')
|
||
|
||
if [[ -n "$INSTALLED_VER" ]]; then
|
||
if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then
|
||
msg_info "Updating $NAME: $INSTALLED_VER → $VER"
|
||
$STD cargo install "$NAME" --version "$VER" --force
|
||
msg_ok "Updated $NAME to $VER"
|
||
elif [[ -z "$VER" ]]; then
|
||
msg_info "Updating $NAME: $INSTALLED_VER → latest"
|
||
$STD cargo install "$NAME" --force
|
||
local NEW_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v')
|
||
msg_ok "Updated $NAME to $NEW_VER"
|
||
fi
|
||
else
|
||
msg_info "Installing $NAME ${VER:+$VER}"
|
||
$STD cargo install "$NAME" ${VER:+--version "$VER"}
|
||
msg_ok "Installed $NAME ${VER:-latest}"
|
||
fi
|
||
done
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or upgrades uv (Python package manager) from GitHub releases.
|
||
# - Downloads platform-specific tarball (no install.sh!)
|
||
# - Extracts uv binary
|
||
# - Places it in /usr/local/bin
|
||
# - Optionally installs a specific Python version via uv
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_uv() {
|
||
local UV_BIN="/usr/local/bin/uv"
|
||
local TMP_DIR=$(mktemp -d)
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "uv")
|
||
|
||
local ARCH=$(uname -m)
|
||
local UV_TAR
|
||
|
||
case "$ARCH" in
|
||
x86_64)
|
||
if grep -qi "alpine" /etc/os-release; then
|
||
UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz"
|
||
else
|
||
UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz"
|
||
fi
|
||
;;
|
||
aarch64)
|
||
if grep -qi "alpine" /etc/os-release; then
|
||
UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz"
|
||
else
|
||
UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz"
|
||
fi
|
||
;;
|
||
*)
|
||
msg_error "Unsupported architecture: $ARCH"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
ensure_dependencies jq
|
||
|
||
local LATEST_VERSION
|
||
LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest |
|
||
jq -r '.tag_name' | sed 's/^v//')
|
||
|
||
if [[ -z "$LATEST_VERSION" ]]; then
|
||
msg_error "Could not fetch latest uv version"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
if [[ -x "$UV_BIN" ]]; then
|
||
local INSTALLED_VERSION
|
||
INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}')
|
||
if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
cache_installed_version "uv" "$LATEST_VERSION"
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
else
|
||
msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION"
|
||
fi
|
||
else
|
||
msg_info "Installing uv $LATEST_VERSION"
|
||
fi
|
||
|
||
local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}"
|
||
curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || {
|
||
msg_error "Failed to download uv"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || {
|
||
msg_error "Failed to extract uv"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || {
|
||
msg_error "Failed to install uv binary"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
rm -rf "$TMP_DIR"
|
||
ensure_usr_local_bin_persist
|
||
export PATH="/usr/local/bin:$PATH"
|
||
|
||
$STD uv python update-shell || true
|
||
cache_installed_version "uv" "$LATEST_VERSION"
|
||
msg_ok "Installed uv $LATEST_VERSION"
|
||
|
||
if [[ -n "${PYTHON_VERSION:-}" ]]; then
|
||
local VERSION_MATCH
|
||
VERSION_MATCH=$(uv python list --only-downloads |
|
||
grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" |
|
||
cut -d'-' -f2 | sort -V | tail -n1)
|
||
|
||
if [[ -z "$VERSION_MATCH" ]]; then
|
||
msg_error "No matching Python $PYTHON_VERSION.x version found"
|
||
return 1
|
||
fi
|
||
|
||
if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then
|
||
$STD uv python install "$VERSION_MATCH" || {
|
||
msg_error "Failed to install Python $VERSION_MATCH"
|
||
return 1
|
||
}
|
||
msg_ok "Installed Python $VERSION_MATCH"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Installs or updates yq (mikefarah/yq - Go version).
|
||
#
|
||
# Description:
|
||
# - Checks if yq is installed and from correct source
|
||
# - Compares with latest release on GitHub
|
||
# - Updates if outdated or wrong implementation
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function setup_yq() {
|
||
local TMP_DIR=$(mktemp -d)
|
||
local BINARY_PATH="/usr/local/bin/yq"
|
||
local GITHUB_REPO="mikefarah/yq"
|
||
local CURRENT_VERSION=""
|
||
local CACHED_VERSION
|
||
CACHED_VERSION=$(get_cached_version "yq")
|
||
|
||
ensure_dependencies jq
|
||
ensure_usr_local_bin_persist
|
||
|
||
if command -v yq &>/dev/null; then
|
||
if ! yq --version 2>&1 | grep -q 'mikefarah'; then
|
||
rm -f "$(command -v yq)"
|
||
else
|
||
CURRENT_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
|
||
fi
|
||
fi
|
||
|
||
local LATEST_VERSION
|
||
LATEST_VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" |
|
||
jq -r '.tag_name' | sed 's/^v//')
|
||
|
||
if [[ -z "$LATEST_VERSION" ]]; then
|
||
msg_error "Could not determine latest yq version"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then
|
||
if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
cache_installed_version "yq" "$LATEST_VERSION"
|
||
rm -rf "$TMP_DIR"
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Installing yq $LATEST_VERSION"
|
||
curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" || {
|
||
msg_error "Failed to download yq"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
}
|
||
|
||
chmod +x "$TMP_DIR/yq"
|
||
mv "$TMP_DIR/yq" "$BINARY_PATH"
|
||
|
||
if [[ ! -x "$BINARY_PATH" ]]; then
|
||
msg_error "Failed to install yq"
|
||
rm -rf "$TMP_DIR"
|
||
return 1
|
||
fi
|
||
|
||
rm -rf "$TMP_DIR"
|
||
hash -r
|
||
|
||
local FINAL_VERSION
|
||
FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
|
||
cache_installed_version "yq" "$FINAL_VERSION"
|
||
msg_ok "Installed yq $FINAL_VERSION"
|
||
}
|