2025-05-05 15:47:43 +02:00
#!/bin/bash
2025-05-15 15:18:41 +02:00
2025-10-22 06:25:26 -07:00
# ==============================================================================
# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT
# ==============================================================================
2025-11-05 05:19:35 -08:00
#
# This file provides unified helper functions for robust package installation
# and repository management across Debian/Ubuntu OS upgrades.
#
# Key Features:
# - Automatic retry logic for transient APT/network failures
# - Unified keyring cleanup from all 3 locations
# - Legacy installation cleanup (nvm, rbenv, rustup)
# - OS-upgrade-safe repository preparation
# - Service pattern matching for multi-version tools
2026-03-02 08:44:59 +01:00
# - Debug mode for troubleshooting (TOOLS_DEBUG=true)
2025-11-05 05:19:35 -08:00
#
# Usage in install scripts:
# source /dev/stdin <<< "$FUNCTIONS" # Load from build.func
# prepare_repository_setup "mysql"
# install_packages_with_retry "mysql-server" "mysql-client"
#
# Quick Reference (Core Helpers):
# cleanup_tool_keyrings() - Remove keyrings from all 3 locations
# stop_all_services() - Stop services by pattern (e.g. "php*-fpm")
# verify_tool_version() - Validate installed version matches expected
# cleanup_legacy_install() - Remove nvm, rbenv, rustup, etc.
# prepare_repository_setup() - Cleanup repos + keyrings + validate APT
# install_packages_with_retry() - Install with 3 retries and APT refresh
# upgrade_packages_with_retry() - Upgrade with 3 retries and APT refresh
2026-03-02 08:44:59 +01:00
# curl_with_retry() - Curl with retry logic and timeouts
#
# Debug Mode:
# TOOLS_DEBUG=true ./script.sh - Enable verbose output for troubleshooting
2025-11-05 05:19:35 -08:00
#
# ==============================================================================
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
# ------------------------------------------------------------------------------
# Debug helper - outputs to stderr when TOOLS_DEBUG is enabled
# Usage: debug_log "message"
# ------------------------------------------------------------------------------
debug_log() {
if [[ " ${ TOOLS_DEBUG :- false } " == "true" || " ${ TOOLS_DEBUG :- 0 } " == "1" || " ${ DEBUG :- 0 } " == "1" ]] ; then
echo "[DEBUG] $* " >& 2
fi
}
# ------------------------------------------------------------------------------
# Robust curl wrapper with retry logic, timeouts, and error handling
#
# Usage:
# curl_with_retry "https://example.com/file" "/tmp/output"
# curl_with_retry "https://api.github.com/..." "-" | jq .
# CURL_RETRIES=5 curl_with_retry "https://slow.server/file" "/tmp/out"
#
# Parameters:
# $1 - URL to download
# $2 - Output file path (use "-" for stdout)
# $3 - (optional) Additional curl options as string
#
# Variables:
# CURL_RETRIES - Number of retries (default: 3)
# CURL_TIMEOUT - Max time per attempt in seconds (default: 60)
# CURL_CONNECT_TO - Connection timeout in seconds (default: 10)
#
# Returns: 0 on success, 1 on failure after all retries
# ------------------------------------------------------------------------------
curl_with_retry() {
local url = " $1 "
local output = " ${ 2 :- - } "
local extra_opts = " ${ 3 :- } "
local retries = " ${ CURL_RETRIES :- 3 } "
local timeout = " ${ CURL_TIMEOUT :- 60 } "
local connect_timeout = " ${ CURL_CONNECT_TO :- 10 } "
local attempt = 1
local success = false
local backoff = 1
# Extract hostname for DNS pre-check
local host
host = $( echo " $url " | sed -E 's|^https?://([^/:]+).*|\1|' )
# DNS pre-check - fail fast if host is unresolvable
if ! getent hosts " $host " & >/dev/null; then
debug_log "DNS resolution failed for $host "
2026-03-26 16:07:38 +01:00
return 6
2026-03-02 08:44:59 +01:00
fi
while [[ $attempt -le $retries ]] ; do
debug_log "curl attempt $attempt / $retries : $url "
local curl_cmd = "curl -fsSL --connect-timeout $connect_timeout --max-time $timeout "
[[ -n " $extra_opts " ]] && curl_cmd = " $curl_cmd $extra_opts "
if [[ " $output " == "-" ]] ; then
if $curl_cmd " $url " ; then
success = true
break
fi
else
if $curl_cmd -o " $output " " $url " ; then
success = true
break
fi
fi
2026-03-15 10:00:37 +01:00
debug_log "curl attempt $attempt failed (timeout= ${ timeout } s), waiting ${ backoff } s before retry..."
2026-03-02 08:44:59 +01:00
sleep " $backoff "
# Exponential backoff: 1, 2, 4, 8... capped at 30s
backoff = $(( backoff * 2 ))
(( backoff > 30)) && backoff = 30
2026-03-15 10:00:37 +01:00
# Double --max-time on each retry so slow connections can finish
timeout = $(( timeout * 2 ))
2026-03-02 08:44:59 +01:00
(( attempt++))
done
if [[ " $success " == "true" ]] ; then
debug_log "curl successful: $url "
return 0
else
debug_log "curl FAILED after $retries attempts: $url "
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
}
# ------------------------------------------------------------------------------
# Robust curl wrapper for API calls (returns HTTP code + body)
#
# Usage:
# response=$(curl_api_with_retry "https://api.github.com/repos/owner/repo/releases/latest")
# http_code=$(curl_api_with_retry "https://api.github.com/..." "/tmp/body.json")
#
# Parameters:
# $1 - URL to call
# $2 - (optional) Output file for body (default: stdout)
# $3 - (optional) Additional curl options as string
#
# Returns: HTTP status code, body in file or stdout
# ------------------------------------------------------------------------------
curl_api_with_retry() {
local url = " $1 "
local body_file = " ${ 2 :- } "
local extra_opts = " ${ 3 :- } "
local retries = " ${ CURL_RETRIES :- 3 } "
local timeout = " ${ CURL_TIMEOUT :- 60 } "
local connect_timeout = " ${ CURL_CONNECT_TO :- 10 } "
local attempt = 1
local http_code = ""
while [[ $attempt -le $retries ]] ; do
debug_log "curl API attempt $attempt / $retries : $url "
local curl_cmd = "curl -fsSL --connect-timeout $connect_timeout --max-time $timeout -w '%{http_code}'"
[[ -n " $extra_opts " ]] && curl_cmd = " $curl_cmd $extra_opts "
if [[ -n " $body_file " ]] ; then
http_code = $( $curl_cmd -o " $body_file " " $url " 2>/dev/null) || true
else
# Capture body and http_code separately
local tmp_body = "/tmp/curl_api_body_ $$ "
http_code = $( $curl_cmd -o " $tmp_body " " $url " 2>/dev/null) || true
if [[ -f " $tmp_body " ]] ; then
cat " $tmp_body "
rm -f " $tmp_body "
fi
fi
# Success on 2xx codes
if [[ " $http_code " = ~ ^2[ 0-9]{ 2} $ ]] ; then
debug_log "curl API successful: $url (HTTP $http_code )"
echo " $http_code "
return 0
fi
2026-03-15 10:00:37 +01:00
debug_log "curl API attempt $attempt failed (HTTP $http_code , timeout= ${ timeout } s), waiting ${ attempt } s..."
2026-03-02 08:44:59 +01:00
sleep " $attempt "
2026-03-15 10:00:37 +01:00
# Double --max-time on each retry so slow connections can finish
timeout = $(( timeout * 2 ))
2026-03-02 08:44:59 +01:00
(( attempt++))
done
debug_log "curl API FAILED after $retries attempts: $url "
echo " $http_code "
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
}
# ------------------------------------------------------------------------------
# Download and install GPG key with retry logic and validation
#
# Usage:
# download_gpg_key "https://example.com/key.gpg" "/etc/apt/keyrings/example.gpg"
# download_gpg_key "https://example.com/key.asc" "/etc/apt/keyrings/example.gpg" "dearmor"
#
# Parameters:
# $1 - URL to GPG key
# $2 - Output path for keyring file
# $3 - (optional) "dearmor" to convert ASCII-armored key to binary
#
# Features:
# - Auto-detects key format (binary vs armored)
# - Validates downloaded key
# - Multiple mirror fallback support
#
# Returns: 0 on success, 1 on failure
# ------------------------------------------------------------------------------
download_gpg_key() {
local url = " $1 "
local output = " $2 "
local mode = " ${ 3 :- auto } " # auto, dearmor, or binary
local retries = " ${ CURL_RETRIES :- 3 } "
local timeout = " ${ CURL_TIMEOUT :- 30 } "
local temp_key
temp_key = $( mktemp)
mkdir -p " $( dirname " $output " ) "
local attempt = 1
while [[ $attempt -le $retries ]] ; do
debug_log "GPG key download attempt $attempt / $retries : $url "
# Download to temp file first
if ! curl -fsSL --connect-timeout 10 --max-time " $timeout " -o " $temp_key " " $url " 2>/dev/null; then
debug_log "GPG key download attempt $attempt failed, waiting ${ attempt } s..."
sleep " $attempt "
(( attempt++))
continue
fi
# Auto-detect key format if mode is auto
if [[ " $mode " == "auto" ]] ; then
if file " $temp_key " 2>/dev/null | grep -qi "pgp\\|gpg\\|public key" ; then
mode = "binary"
elif grep -q "BEGIN PGP" " $temp_key " 2>/dev/null; then
mode = "dearmor"
else
# Try to detect by extension
[[ " $url " == *.asc || " $url " == *.txt ]] && mode = "dearmor" || mode = "binary"
fi
fi
# Process based on mode
if [[ " $mode " == "dearmor" ]] ; then
2026-03-23 22:22:23 +01:00
if gpg --dearmor --yes -o " $output " <" $temp_key " 2>/dev/null && [[ -s " $output " ]] ; then
2026-03-02 08:44:59 +01:00
rm -f " $temp_key "
debug_log "GPG key installed (dearmored): $output "
return 0
fi
else
if mv " $temp_key " " $output " 2>/dev/null; then
chmod 644 " $output "
debug_log "GPG key installed: $output "
return 0
fi
fi
debug_log "GPG key processing attempt $attempt failed"
sleep " $attempt "
(( attempt++))
done
rm -f " $temp_key "
debug_log "GPG key download FAILED after $retries attempts: $url "
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
}
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
# Cache installed version to avoid repeated checks
# ------------------------------------------------------------------------------
cache_installed_version() {
local app = " $1 "
local version = " $2 "
mkdir -p /var/cache/app-versions
echo " $version " >"/var/cache/app-versions/ ${ app } _version.txt"
}
get_cached_version() {
local app = " $1 "
mkdir -p /var/cache/app-versions
if [[ -f "/var/cache/app-versions/ ${ app } _version.txt" ]] ; then
cat "/var/cache/app-versions/ ${ app } _version.txt"
return 0
fi
return 0
}
# ------------------------------------------------------------------------------
2025-11-05 05:19:35 -08:00
# Clean up ALL keyring locations for a tool (unified helper)
# Usage: cleanup_tool_keyrings "mariadb" "mysql" "postgresql"
# ------------------------------------------------------------------------------
cleanup_tool_keyrings() {
local tool_patterns =( " $@ " )
for pattern in " ${ tool_patterns [@] } " ; do
rm -f /usr/share/keyrings/${ pattern } *.gpg \
/etc/apt/keyrings/${ pattern } *.gpg \
/etc/apt/trusted.gpg.d/${ pattern } *.gpg 2>/dev/null || true
done
}
# ------------------------------------------------------------------------------
# Stop and disable all service instances matching a pattern
# Usage: stop_all_services "php*-fpm" "mysql" "mariadb"
# ------------------------------------------------------------------------------
stop_all_services() {
local service_patterns =( " $@ " )
for pattern in " ${ service_patterns [@] } " ; do
2025-12-07 21:30:23 +01:00
# Find all matching services (grep || true to handle no matches)
local services
services = $( systemctl list-units --type= service --all 2>/dev/null |
grep -oE " ${ pattern } [^ ]*\.service" 2>/dev/null | sort -u) || true
2025-12-04 07:52:18 +01:00
2025-12-07 21:30:23 +01:00
if [[ -n " $services " ]] ; then
2025-12-04 07:52:18 +01:00
while read -r service; do
2025-11-05 05:19:35 -08:00
$STD systemctl stop " $service " 2>/dev/null || true
$STD systemctl disable " $service " 2>/dev/null || true
2025-12-07 21:30:23 +01:00
done <<< " $services "
fi
2025-11-05 05:19:35 -08:00
done
2025-11-10 13:58:04 +01:00
2025-11-05 05:19:35 -08:00
}
# ------------------------------------------------------------------------------
# Verify installed tool version matches expected version
# Returns: 0 if match, 1 if mismatch (with warning)
# Usage: verify_tool_version "nodejs" "22" "$(node -v | grep -oP '^v\K[0-9]+')"
# ------------------------------------------------------------------------------
verify_tool_version() {
local tool_name = " $1 "
local expected_version = " $2 "
local installed_version = " $3 "
# Extract major version for comparison
local expected_major = " ${ expected_version %%.* } "
local installed_major = " ${ installed_version %%.* } "
if [[ " $installed_major " != " $expected_major " ]] ; then
msg_warn " $tool_name version mismatch: expected $expected_version , got $installed_version "
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# Clean up legacy installation methods (nvm, rbenv, rustup, etc.)
# Usage: cleanup_legacy_install "nodejs" -> removes nvm
# ------------------------------------------------------------------------------
cleanup_legacy_install() {
local tool_name = " $1 "
case " $tool_name " in
nodejs | node)
if [[ -d " $HOME /.nvm" ]] ; then
msg_info "Removing legacy nvm installation"
rm -rf " $HOME /.nvm" " $HOME /.npm" " $HOME /.bower" " $HOME /.config/yarn" 2>/dev/null || true
sed -i '/NVM_DIR/d' " $HOME /.bashrc" " $HOME /.profile" 2>/dev/null || true
msg_ok "Legacy nvm installation removed"
fi
;;
ruby)
if [[ -d " $HOME /.rbenv" ]] ; then
msg_info "Removing legacy rbenv installation"
rm -rf " $HOME /.rbenv" 2>/dev/null || true
sed -i '/rbenv/d' " $HOME /.bashrc" " $HOME /.profile" 2>/dev/null || true
msg_ok "Legacy rbenv installation removed"
fi
;;
rust)
if [[ -d " $HOME /.cargo" ]] || [[ -d " $HOME /.rustup" ]] ; then
msg_info "Removing legacy rustup installation"
rm -rf " $HOME /.cargo" " $HOME /.rustup" 2>/dev/null || true
sed -i '/cargo/d' " $HOME /.bashrc" " $HOME /.profile" 2>/dev/null || true
msg_ok "Legacy rustup installation removed"
fi
;;
go | golang)
if [[ -d " $HOME /go" ]] ; then
msg_info "Removing legacy Go workspace"
# Keep user code, just remove GOPATH env
sed -i '/GOPATH/d' " $HOME /.bashrc" " $HOME /.profile" 2>/dev/null || true
msg_ok "Legacy Go workspace cleaned"
fi
;;
esac
}
# ------------------------------------------------------------------------------
# Unified repository preparation before setup
# Cleans up old repos, keyrings, and ensures APT is working
# Usage: prepare_repository_setup "mariadb" "mysql"
# ------------------------------------------------------------------------------
prepare_repository_setup() {
local repo_names =( " $@ " )
# Clean up all old repository files
for repo in " ${ repo_names [@] } " ; do
cleanup_old_repo_files " $repo "
done
# Clean up all keyrings
cleanup_tool_keyrings " ${ repo_names [@] } "
# Ensure APT is in working state
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
return 0
}
# ------------------------------------------------------------------------------
# Install packages with retry logic
# Usage: install_packages_with_retry "mysql-server" "mysql-client"
2026-03-02 08:44:59 +01:00
# Features:
# - Automatic dpkg recovery on failure
# - Individual package fallback if batch fails
# - Dependency resolution with apt-get -f install
2025-11-05 05:19:35 -08:00
# ------------------------------------------------------------------------------
install_packages_with_retry() {
local packages =( " $@ " )
2026-03-02 08:44:59 +01:00
local max_retries = 3
2025-11-05 05:19:35 -08:00
local retry = 0
2026-03-02 08:44:59 +01:00
# Pre-check: ensure dpkg is not in a broken state
if dpkg --audit 2>& 1 | grep -q .; then
$STD dpkg --configure -a 2>/dev/null || true
fi
2025-11-05 05:19:35 -08:00
while [[ $retry -le $max_retries ]] ; do
2026-01-09 21:19:47 +01:00
if DEBIAN_FRONTEND = noninteractive $STD apt install -y \
-o Dpkg::Options::= "--force-confdef" \
-o Dpkg::Options::= "--force-confold" \
" ${ packages [@] } " 2>/dev/null; then
2025-11-05 05:19:35 -08:00
return 0
fi
retry = $(( retry + 1 ))
if [[ $retry -le $max_retries ]] ; then
msg_warn "Package installation failed, retrying ( $retry / $max_retries )..."
2026-03-02 08:44:59 +01:00
# Progressive recovery steps based on retry count
case $retry in
1)
# First retry: just fix dpkg and update
$STD dpkg --configure -a 2>/dev/null || true
$STD apt update 2>/dev/null || true
;;
2)
# Second retry: fix broken dependencies
$STD apt --fix-broken install -y 2>/dev/null || true
$STD apt update 2>/dev/null || true
;;
3)
# Third retry: try installing packages one by one
local failed =()
for pkg in " ${ packages [@] } " ; do
if ! $STD apt install -y " $pkg " 2>/dev/null; then
# Try with --fix-missing
if ! $STD apt install -y --fix-missing " $pkg " 2>/dev/null; then
failed +=( " $pkg " )
fi
fi
done
# If some packages installed, consider partial success
if [[ ${# failed [@] } -lt ${# packages [@] } ]] ; then
if [[ ${# failed [@] } -gt 0 ]] ; then
msg_warn "Partially installed. Failed packages: ${ failed [*] } "
fi
return 0
fi
;;
esac
sleep $(( retry * 2 ))
2025-11-05 05:19:35 -08:00
fi
done
2026-02-27 13:59:02 +01:00
msg_error "Failed to install packages after $(( max_retries + 1 )) attempts: ${ packages [*] } "
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
# ------------------------------------------------------------------------------
# Upgrade specific packages with retry logic
# Usage: upgrade_packages_with_retry "mariadb-server" "mariadb-client"
# ------------------------------------------------------------------------------
upgrade_packages_with_retry() {
local packages =( " $@ " )
local max_retries = 2
local retry = 0
while [[ $retry -le $max_retries ]] ; do
2026-01-09 21:19:47 +01:00
if DEBIAN_FRONTEND = noninteractive $STD apt install --only-upgrade -y \
-o Dpkg::Options::= "--force-confdef" \
-o Dpkg::Options::= "--force-confold" \
" ${ packages [@] } " 2>/dev/null; then
2025-11-05 05:19:35 -08:00
return 0
fi
retry = $(( retry + 1 ))
if [[ $retry -le $max_retries ]] ; then
msg_warn "Package upgrade failed, retrying ( $retry / $max_retries )..."
sleep 2
2025-12-05 09:17:19 +01:00
# Fix any interrupted dpkg operations before retry
$STD dpkg --configure -a 2>/dev/null || true
2025-11-05 05:19:35 -08:00
$STD apt update 2>/dev/null || true
fi
done
2026-02-27 13:59:02 +01:00
msg_error "Failed to upgrade packages after $(( max_retries + 1 )) attempts: ${ packages [*] } "
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
# ------------------------------------------------------------------------------
# Check if tool is already installed and optionally verify exact version
# Returns: 0 if installed (with optional version match), 1 if not installed
# Usage: is_tool_installed "mariadb" "11.4" || echo "Not installed"
# ------------------------------------------------------------------------------
is_tool_installed() {
local tool_name = " $1 "
local required_version = " ${ 2 :- } "
local installed_version = ""
case " $tool_name " in
mariadb)
if command -v mariadb >/dev/null 2>& 1; then
2026-03-25 13:06:21 +01:00
installed_version = $( mariadb --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true )
2025-11-05 05:19:35 -08:00
fi
;;
mysql)
if command -v mysql >/dev/null 2>& 1; then
2026-03-25 13:06:21 +01:00
installed_version = $( mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true )
2025-11-05 05:19:35 -08:00
fi
;;
mongodb | mongod)
if command -v mongod >/dev/null 2>& 1; then
installed_version = $( mongod --version 2>/dev/null | awk '/db version/{print $3}' | cut -d. -f1,2)
fi
;;
node | nodejs)
if command -v node >/dev/null 2>& 1; then
2026-03-25 13:06:21 +01:00
installed_version = $( node -v 2>/dev/null | grep -oP '^v\K[0-9]+' || true )
2025-11-05 05:19:35 -08:00
fi
;;
php)
if command -v php >/dev/null 2>& 1; then
installed_version = $( php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2)
fi
;;
postgres | postgresql)
if command -v psql >/dev/null 2>& 1; then
installed_version = $( psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1)
fi
;;
ruby)
if command -v ruby >/dev/null 2>& 1; then
installed_version = $( ruby --version 2>/dev/null | awk '{print $2}' | cut -d. -f1,2)
fi
;;
rust | rustc)
if command -v rustc >/dev/null 2>& 1; then
installed_version = $( rustc --version 2>/dev/null | awk '{print $2}' )
fi
;;
go | golang)
if command -v go >/dev/null 2>& 1; then
installed_version = $( go version 2>/dev/null | awk '{print $3}' | sed 's/go//' )
fi
;;
clickhouse)
if command -v clickhouse >/dev/null 2>& 1; then
installed_version = $( clickhouse --version 2>/dev/null | awk '{print $2}' )
fi
;;
esac
if [[ -z " $installed_version " ]] ; then
return 1 # Not installed
fi
if [[ -n " $required_version " && " $installed_version " != " $required_version " ]] ; then
echo " $installed_version "
return 1 # Version mismatch
fi
echo " $installed_version "
return 0 # Installed and version matches (if specified)
}
# ------------------------------------------------------------------------------
# Remove old tool version completely (purge + cleanup repos)
# Usage: remove_old_tool_version "mariadb" "repository-name"
# ------------------------------------------------------------------------------
remove_old_tool_version() {
local tool_name = " $1 "
local repo_name = " ${ 2 :- $tool_name } "
case " $tool_name " in
mariadb)
stop_all_services "mariadb"
$STD apt purge -y 'mariadb*' >/dev/null 2>& 1 || true
cleanup_tool_keyrings "mariadb"
;;
mysql)
stop_all_services "mysql"
$STD apt purge -y 'mysql*' >/dev/null 2>& 1 || true
rm -rf /var/lib/mysql 2>/dev/null || true
cleanup_tool_keyrings "mysql"
;;
mongodb)
stop_all_services "mongod"
$STD apt purge -y 'mongodb*' >/dev/null 2>& 1 || true
rm -rf /var/lib/mongodb 2>/dev/null || true
cleanup_tool_keyrings "mongodb"
;;
node | nodejs)
$STD apt purge -y nodejs npm >/dev/null 2>& 1 || true
# Clean up npm global modules
if command -v npm >/dev/null 2>& 1; then
2025-12-18 18:38:08 +01:00
npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' 2>/dev/null | while read -r module; do
2025-11-05 05:19:35 -08:00
npm uninstall -g " $module " >/dev/null 2>& 1 || true
2025-12-18 18:38:08 +01:00
done || true
2025-11-05 05:19:35 -08:00
fi
cleanup_legacy_install "nodejs"
cleanup_tool_keyrings "nodesource"
;;
php)
stop_all_services "php.*-fpm"
$STD apt purge -y 'php*' >/dev/null 2>& 1 || true
rm -rf /etc/php 2>/dev/null || true
cleanup_tool_keyrings "deb.sury.org-php" "php"
;;
postgresql)
stop_all_services "postgresql"
$STD apt purge -y 'postgresql*' >/dev/null 2>& 1 || true
# Keep data directory for safety (can be removed manually if needed)
# rm -rf /var/lib/postgresql 2>/dev/null || true
cleanup_tool_keyrings "postgresql" "pgdg"
;;
java)
$STD apt purge -y 'temurin*' 'adoptium*' 'openjdk*' >/dev/null 2>& 1 || true
cleanup_tool_keyrings "adoptium"
;;
ruby)
cleanup_legacy_install "ruby"
$STD apt purge -y 'ruby*' >/dev/null 2>& 1 || true
;;
rust)
cleanup_legacy_install "rust"
;;
go | golang)
rm -rf /usr/local/go 2>/dev/null || true
cleanup_legacy_install "golang"
;;
clickhouse)
stop_all_services "clickhouse-server"
$STD apt purge -y 'clickhouse*' >/dev/null 2>& 1 || true
rm -rf /var/lib/clickhouse 2>/dev/null || true
cleanup_tool_keyrings "clickhouse"
;;
esac
# Clean up old repository files (both .list and .sources)
cleanup_old_repo_files " $repo_name "
return 0
}
# ------------------------------------------------------------------------------
# Determine if tool update/upgrade is needed
# Returns: 0 (update needed), 1 (already up-to-date)
# Usage: if should_update_tool "mariadb" "11.4"; then ... fi
# ------------------------------------------------------------------------------
should_update_tool() {
local tool_name = " $1 "
local target_version = " $2 "
local current_version = ""
# Get currently installed version
current_version = $( is_tool_installed " $tool_name " 2>/dev/null) || return 0 # Not installed = needs install
# If versions are identical, no update needed
if [[ " $current_version " == " $target_version " ]] ; then
return 1 # No update needed
fi
return 0 # Update needed
}
2026-03-02 08:44:59 +01:00
# ------------------------------------------------------------------------------
2025-11-05 05:19:35 -08:00
# Unified repository management for tools
# Handles adding, updating, and verifying tool repositories
# Usage: manage_tool_repository "mariadb" "11.4" "https://repo..." "GPG_key_url"
# Supports: mariadb, mongodb, nodejs, postgresql, php, mysql
# ------------------------------------------------------------------------------
manage_tool_repository() {
local tool_name = " $1 "
local version = " $2 "
local repo_url = " $3 "
local gpg_key_url = " ${ 4 :- } "
local distro_id repo_component suite
distro_id = $( awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"' )
case " $tool_name " in
mariadb)
if [[ -z " $repo_url " || -z " $gpg_key_url " ]] ; then
msg_error "MariaDB repository requires repo_url and gpg_key_url"
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
fi
# Clean old repos first
cleanup_old_repo_files "mariadb"
# Get suite for fallback handling
local distro_codename
distro_codename = $( awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
suite = $( get_fallback_suite " $distro_id " " $distro_codename " " $repo_url / $distro_id " )
# Setup new repository using deb822 format
2025-11-07 02:49:16 -08:00
setup_deb822_repo \
"mariadb" \
" $gpg_key_url " \
" $repo_url / $distro_id " \
" $suite " \
"main"
2025-11-05 05:19:35 -08:00
return 0
;;
mongodb)
if [[ -z " $repo_url " || -z " $gpg_key_url " ]] ; then
msg_error "MongoDB repository requires repo_url and gpg_key_url"
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
fi
# Clean old repos first
cleanup_old_repo_files "mongodb"
2026-03-02 08:44:59 +01:00
# Import GPG key with retry logic
if ! download_gpg_key " $gpg_key_url " "/etc/apt/keyrings/mongodb-server- ${ version } .gpg" "dearmor" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download MongoDB GPG key"
2026-03-26 16:07:38 +01:00
return 7
2025-11-05 05:19:35 -08:00
fi
2026-02-12 22:24:24 +01:00
chmod 644 "/etc/apt/keyrings/mongodb-server- ${ version } .gpg"
2025-11-05 05:19:35 -08:00
# Setup repository
local distro_codename
distro_codename = $( awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
2025-11-06 07:57:30 +01:00
# Suite mapping with fallback for newer releases not yet supported by upstream
if [[ " $distro_id " == "debian" ]] ; then
case " $distro_codename " in
trixie | forky | sid)
# Testing/unstable releases fallback to latest stable suite
suite = "bookworm"
;;
bookworm)
suite = "bookworm"
;;
bullseye)
suite = "bullseye"
;;
*)
# Unknown release: fallback to latest stable suite
msg_warn "Unknown Debian release ' ${ distro_codename } ', using bookworm"
suite = "bookworm"
;;
esac
elif [[ " $distro_id " == "ubuntu" ]] ; then
case " $distro_codename " in
oracular | plucky)
# Newer releases fallback to latest LTS
suite = "noble"
;;
noble)
suite = "noble"
;;
jammy)
suite = "jammy"
;;
focal)
suite = "focal"
;;
*)
# Unknown release: fallback to latest LTS
msg_warn "Unknown Ubuntu release ' ${ distro_codename } ', using noble"
suite = "noble"
;;
esac
else
# For other distros, try generic fallback
suite = $( get_fallback_suite " $distro_id " " $distro_codename " " $repo_url " )
fi
2025-11-05 05:19:35 -08:00
repo_component = "main"
[[ " $distro_id " == "ubuntu" ]] && repo_component = "multiverse"
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-${version}.sources
Types: deb
URIs: ${repo_url}
Suites: ${suite}/mongodb-org/${version}
Components: ${repo_component}
2025-11-07 02:49:16 -08:00
Architectures: $(dpkg --print-architecture)
2025-11-05 05:19:35 -08:00
Signed-By: /etc/apt/keyrings/mongodb-server-${version}.gpg
EOF
return 0
;;
nodejs)
if [[ -z " $repo_url " || -z " $gpg_key_url " ]] ; then
msg_error "Node.js repository requires repo_url and gpg_key_url"
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
fi
cleanup_old_repo_files "nodesource"
# NodeSource uses deb822 format with GPG from repo
local distro_codename
distro_codename = $( awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
2026-03-02 08:44:59 +01:00
# Download GPG key from NodeSource with retry logic
if ! download_gpg_key " $gpg_key_url " "/etc/apt/keyrings/nodesource.gpg" "dearmor" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to import NodeSource GPG key"
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
cat <<EOF >/etc/apt/sources.list.d/nodesource.sources
Types: deb
URIs: $repo_url
Suites: nodistro
Components: main
2025-11-07 02:49:16 -08:00
Architectures: $(dpkg --print-architecture)
2025-11-05 05:19:35 -08:00
Signed-By: /etc/apt/keyrings/nodesource.gpg
EOF
return 0
;;
php)
if [[ -z " $gpg_key_url " ]] ; then
msg_error "PHP repository requires gpg_key_url"
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
fi
cleanup_old_repo_files "php"
2026-03-02 08:44:59 +01:00
# Download and install keyring with retry logic
if ! curl_with_retry " $gpg_key_url " "/tmp/debsuryorg-archive-keyring.deb" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download PHP keyring"
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
2026-01-28 11:09:30 +01:00
# Don't use /dev/null redirection for dpkg as it may use background processes
dpkg -i /tmp/debsuryorg-archive-keyring.deb >>" $( get_active_logfile) " 2>& 1 || {
2025-11-05 05:19:35 -08:00
msg_error "Failed to install PHP keyring"
rm -f /tmp/debsuryorg-archive-keyring.deb
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
rm -f /tmp/debsuryorg-archive-keyring.deb
# Setup repository
local distro_codename
distro_codename = $( awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
cat <<EOF >/etc/apt/sources.list.d/php.sources
Types: deb
URIs: https://packages.sury.org/php
Suites: $distro_codename
Components: main
2025-11-07 02:49:16 -08:00
Architectures: $(dpkg --print-architecture)
2025-11-05 05:19:35 -08:00
Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg
EOF
return 0
;;
postgresql)
if [[ -z " $gpg_key_url " ]] ; then
msg_error "PostgreSQL repository requires gpg_key_url"
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
fi
cleanup_old_repo_files "postgresql"
2026-03-02 08:44:59 +01:00
# Import PostgreSQL key with retry logic
if ! download_gpg_key " $gpg_key_url " "/etc/apt/keyrings/postgresql.gpg" "dearmor" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to import PostgreSQL GPG key"
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
# Setup repository
local distro_codename
distro_codename = $( awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
cat <<EOF >/etc/apt/sources.list.d/postgresql.sources
Types: deb
URIs: http://apt.postgresql.org/pub/repos/apt
Suites: $distro_codename-pgdg
Components: main
2025-11-07 02:49:16 -08:00
Architectures: $(dpkg --print-architecture)
2025-11-05 05:19:35 -08:00
Signed-By: /etc/apt/keyrings/postgresql.gpg
EOF
return 0
;;
*)
msg_error "Unknown tool repository: $tool_name "
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
;;
esac
return 0
}
2026-03-02 08:44:59 +01:00
# ------------------------------------------------------------------------------
2025-10-22 06:25:26 -07:00
# Unified package upgrade function (with apt update caching)
# ------------------------------------------------------------------------------
upgrade_package() {
local package = " $1 "
# Use same caching logic as ensure_dependencies
local apt_cache_file = "/var/cache/apt-update-timestamp"
local current_time = $( date +%s)
local last_update = 0
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
2025-11-05 05:19:35 -08:00
$STD apt update || {
msg_warn "APT update failed in upgrade_package - continuing with cached packages"
}
2025-10-22 06:25:26 -07:00
echo " $current_time " >" $apt_cache_file "
fi
2025-11-05 05:19:35 -08:00
$STD apt install --only-upgrade -y " $package " || {
msg_warn "Failed to upgrade $package "
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
2026-03-02 08:44:59 +01:00
# Repository availability check with caching
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-03-02 10:40:17 +01:00
# Note: Must use -gA (global) because tools.func is sourced inside update_os()
# function scope. Plain 'declare -A' would create a local variable that gets
# destroyed when update_os() returns, causing "unbound variable" errors later
# when setup_postgresql/verify_repo_available tries to access the cache key.
declare -gA _REPO_CACHE 2>/dev/null || declare -A _REPO_CACHE 2>/dev/null || true
2026-03-02 08:44:59 +01:00
2025-10-22 06:25:26 -07:00
verify_repo_available() {
local repo_url = " $1 "
local suite = " $2 "
2026-03-02 08:44:59 +01:00
local cache_key = " ${ repo_url } | ${ suite } "
local cache_ttl = 300 # 5 minutes
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
# Check cache first (avoid repeated HTTP requests)
if [[ -n " ${ _REPO_CACHE [ $cache_key ] :- } " ]] ; then
local cached_time cached_result
cached_time = $( echo " ${ _REPO_CACHE [ $cache_key ] } " | cut -d'|' -f1)
cached_result = $( echo " ${ _REPO_CACHE [ $cache_key ] } " | cut -d'|' -f2)
if (( $( date +%s) - cached_time < cache_ttl)) ; then
[[ " $cached_result " == "1" ]] && return 0 || return 1
fi
2025-10-22 06:25:26 -07:00
fi
2026-03-02 08:44:59 +01:00
# Perform actual check with short timeout
local result = 1
if curl -fsSL --max-time 5 --connect-timeout 3 " ${ repo_url } /dists/ ${ suite } /Release" & >/dev/null; then
result = 0
fi
# Cache the result
_REPO_CACHE[ $cache_key ]= " $( date +%s) | $result "
return $result
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
2026-03-09 10:03:04 +01:00
# Ensure dependencies are installed (with apt/apk update caching)
# Supports both Debian (apt/dpkg) and Alpine (apk) systems
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
ensure_dependencies() {
local deps =( " $@ " )
local missing =()
2026-03-09 10:03:04 +01:00
# Detect Alpine Linux
if [[ -f /etc/alpine-release ]] ; then
for dep in " ${ deps [@] } " ; do
if command -v " $dep " & >/dev/null; then
continue
fi
if apk info -e " $dep " & >/dev/null; then
continue
fi
missing +=( " $dep " )
done
if [[ ${# missing [@] } -gt 0 ]] ; then
$STD apk add --no-cache " ${ missing [@] } " || {
local failed =()
for pkg in " ${ missing [@] } " ; do
if ! $STD apk add --no-cache " $pkg " 2>/dev/null; then
failed +=( " $pkg " )
fi
done
if [[ ${# failed [@] } -gt 0 ]] ; then
msg_error "Failed to install dependencies: ${ failed [*] } "
return 1
fi
}
fi
return 0
fi
# Debian/Ubuntu: Fast batch check using dpkg-query
2026-03-02 08:44:59 +01:00
local installed_pkgs
installed_pkgs = $( dpkg-query -W -f= '${Package}\n' 2>/dev/null | sort -u)
2025-10-22 06:25:26 -07:00
for dep in " ${ deps [@] } " ; do
2026-03-02 08:44:59 +01:00
# First check if command exists (for binaries like jq, curl)
if command -v " $dep " & >/dev/null; then
continue
2025-10-22 06:25:26 -07:00
fi
2026-03-02 08:44:59 +01:00
# Then check if package is installed
if echo " $installed_pkgs " | grep -qx " $dep " ; then
continue
fi
missing +=( " $dep " )
2025-10-22 06:25:26 -07:00
done
if [[ ${# missing [@] } -gt 0 ]] ; then
# Only run apt update if not done recently (within last 5 minutes)
local apt_cache_file = "/var/cache/apt-update-timestamp"
2026-03-02 08:44:59 +01:00
local current_time
current_time = $( date +%s)
2025-10-22 06:25:26 -07:00
local last_update = 0
if [[ -f " $apt_cache_file " ]] ; then
last_update = $( cat " $apt_cache_file " 2>/dev/null || echo 0)
fi
if (( current_time - last_update > 300)) ; then
# Ensure orphaned sources are cleaned before updating
cleanup_orphaned_sources 2>/dev/null || true
if ! $STD apt update; then
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-10-22 06:25:26 -07:00
fi
echo " $current_time " >" $apt_cache_file "
fi
$STD apt install -y " ${ missing [@] } " || {
2026-03-02 08:44:59 +01:00
# Fallback: try installing one by one to identify problematic package
local failed =()
for pkg in " ${ missing [@] } " ; do
if ! $STD apt install -y " $pkg " 2>/dev/null; then
failed +=( " $pkg " )
fi
done
if [[ ${# failed [@] } -gt 0 ]] ; then
msg_error "Failed to install dependencies: ${ failed [*] } "
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
fi
2025-10-22 06:25:26 -07:00
}
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 "
}
# ------------------------------------------------------------------------------
2026-03-09 10:03:04 +01:00
# Check if package is installed (supports both Debian and Alpine)
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
is_package_installed() {
local package = " $1 "
2026-03-09 10:03:04 +01:00
if [[ -f /etc/alpine-release ]] ; then
apk info -e " $package " & >/dev/null
else
dpkg-query -W -f= '${Status}' " $package " 2>/dev/null | grep -q "^install ok installed $"
fi
2025-10-22 06:25:26 -07:00
}
2026-04-10 11:28:52 +02:00
# ------------------------------------------------------------------------------
# validate_github_token()
# Checks a GitHub token via the /user endpoint.
# Prints a status message and returns:
# 0 - token is valid
# 1 - token is invalid / expired (HTTP 401)
# 2 - token has no public repo scope (HTTP 200 but missing scope)
# 3 - network/API error
# Also reports expiry date if the token carries an x-oauth-expiry header.
# ------------------------------------------------------------------------------
validate_github_token() {
local token = " ${ 1 :- ${ GITHUB_TOKEN :- }} "
[[ -z " $token " ]] && return 3
local response headers http_code expiry_date scopes
headers = $( mktemp)
response = $( curl -sSL -w "%{http_code}" \
-D " $headers " \
-o /dev/null \
-H "Authorization: Bearer $token " \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
2026-04-17 21:58:31 +02:00
"https://api.github.com/user" 2>/dev/null) || {
rm -f " $headers "
return 3
}
2026-04-10 11:28:52 +02:00
http_code = " $response "
# Read expiry header (fine-grained PATs carry this)
2026-04-17 21:58:31 +02:00
expiry_date = $( grep -i '^github-authentication-token-expiration:' " $headers " |
sed 's/.*: *//' | tr -d '\r\n' || true )
2026-04-10 11:28:52 +02:00
# Read token scopes (classic PATs)
2026-04-17 21:58:31 +02:00
scopes = $( grep -i '^x-oauth-scopes:' " $headers " |
sed 's/.*: *//' | tr -d '\r\n' || true )
2026-04-10 11:28:52 +02:00
rm -f " $headers "
case " $http_code " in
2026-04-17 21:58:31 +02:00
200)
if [[ -n " $expiry_date " ]] ; then
msg_ok "GitHub token is valid (expires: $expiry_date )."
else
msg_ok "GitHub token is valid (no expiry / fine-grained PAT)."
fi
# Warn if classic PAT has no public_repo scope
if [[ -n " $scopes " && " $scopes " != *"public_repo" * && " $scopes " != *"repo" * ]] ; then
msg_warn "Token has no 'public_repo' scope - private repos and some release APIs may fail."
return 2
fi
return 0
;;
401)
msg_error "GitHub token is invalid or expired (HTTP 401)."
return 1
;;
*)
msg_warn "GitHub token validation returned HTTP $http_code - treating as valid."
return 0
;;
2026-04-10 11:28:52 +02:00
esac
}
2026-03-07 22:14:18 +01:00
# ------------------------------------------------------------------------------
# Prompt user to enter a GitHub Personal Access Token (PAT) interactively
# Returns 0 if a valid token was provided, 1 otherwise
# ------------------------------------------------------------------------------
prompt_for_github_token() {
if [[ ! -t 0 ]] ; then
2026-04-10 11:28:52 +02:00
# Non-interactive: pick up var_github_token if set (from default.vars / app.vars / env)
if [[ -z " ${ GITHUB_TOKEN :- } " && -n " ${ var_github_token :- } " ]] ; then
export GITHUB_TOKEN = " ${ var_github_token } "
msg_ok "GitHub token loaded from var_github_token."
return 0
fi
2026-03-07 22:14:18 +01:00
return 1
fi
2026-04-10 11:28:52 +02:00
# Prefer var_github_token when already set and no interactive override needed
if [[ -z " ${ GITHUB_TOKEN :- } " && -n " ${ var_github_token :- } " ]] ; then
export GITHUB_TOKEN = " ${ var_github_token } "
msg_ok "GitHub token loaded from var_github_token."
validate_github_token || true
return 0
fi
2026-03-07 22:14:18 +01:00
local reply
read -rp " ${ TAB } Would you like to enter a GitHub Personal Access Token (PAT)? [y/N]: " reply
reply = " ${ reply :- n } "
if [[ ! " ${ reply ,, } " = ~ ^( y| yes) $ ]] ; then
return 1
fi
local token
while true; do
read -rp " ${ TAB } Enter your GitHub PAT: " token
# Trim leading/trailing whitespace
token = " $( echo " $token " | xargs) "
if [[ -z " $token " ]] ; then
msg_warn "Token cannot be empty. Please try again."
continue
fi
if [[ " $token " = ~ [[ :space:]] ]] ; then
msg_warn "Token must not contain spaces. Please try again."
continue
fi
2026-04-10 11:28:52 +02:00
# Validate before accepting
export GITHUB_TOKEN = " $token "
if validate_github_token " $token " ; then
break
else
msg_warn "Please enter a valid token, or press Ctrl+C to abort."
unset GITHUB_TOKEN
fi
2026-03-07 22:14:18 +01:00
done
msg_ok "GitHub token has been set."
return 0
}
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
# 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 " )
2026-03-07 22:14:18 +01:00
local attempt = 1
while (( attempt <= max_retries)) ; do
2025-10-22 06:25:26 -07:00
local http_code
2026-02-25 21:01:45 +01:00
http_code = $( curl -sSL -w "%{http_code}" -o " $output_file " \
2025-10-22 06:25:26 -07:00
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
" ${ header_args [@] } " \
2026-02-25 21:01:45 +01:00
" $url " 2>/dev/null) || true
2025-10-22 06:25:26 -07:00
case " $http_code " in
200)
return 0
;;
2026-02-25 21:01:45 +01:00
401)
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n " ${ GITHUB_TOKEN :- } " ]] ; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
2026-03-07 22:14:18 +01:00
msg_error "The repository may require authentication."
fi
if prompt_for_github_token; then
header_args =( -H "Authorization: Bearer $GITHUB_TOKEN " )
continue
2026-02-25 21:01:45 +01:00
fi
2026-03-26 16:07:38 +01:00
return 22
2026-02-25 21:01:45 +01:00
;;
2025-10-22 06:25:26 -07:00
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 ))
2026-03-07 22:14:18 +01:00
(( attempt++))
2025-10-22 06:25:26 -07:00
continue
fi
2026-02-25 21:01:45 +01:00
msg_error "GitHub API rate limit exceeded (HTTP 403)."
2026-03-07 22:14:18 +01:00
if prompt_for_github_token; then
header_args =( -H "Authorization: Bearer $GITHUB_TOKEN " )
retry_delay = 2
attempt = 1
continue
fi
2026-02-25 21:01:45 +01:00
msg_error "To increase the limit, export a GitHub token before running the script:"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
2026-03-26 16:07:38 +01:00
return 22
2025-10-22 06:25:26 -07:00
;;
404)
2026-02-25 21:01:45 +01:00
msg_error "GitHub repository or release not found (HTTP 404): $url "
2026-03-26 16:07:38 +01:00
return 22
2026-02-25 21:01:45 +01:00
;;
000 | "" )
if [[ $attempt -lt $max_retries ]] ; then
sleep " $retry_delay "
2026-03-07 22:14:18 +01:00
(( attempt++))
2026-02-25 21:01:45 +01:00
continue
fi
msg_error "GitHub API connection failed (no response)."
msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit"
2026-03-26 16:07:38 +01:00
return 22
2025-10-22 06:25:26 -07:00
;;
*)
if [[ $attempt -lt $max_retries ]] ; then
sleep " $retry_delay "
2026-03-07 22:14:18 +01:00
(( attempt++))
2025-10-22 06:25:26 -07:00
continue
fi
2026-02-25 21:01:45 +01:00
msg_error "GitHub API call failed (HTTP $http_code )."
2026-03-26 16:07:38 +01:00
return 22
2025-10-22 06:25:26 -07:00
;;
esac
2026-03-07 22:14:18 +01:00
(( attempt++))
2025-10-22 06:25:26 -07:00
done
2026-02-27 13:59:02 +01:00
msg_error "GitHub API call failed after ${ max_retries } attempts: ${ url } "
2026-03-26 16:07:38 +01:00
return 22
2025-10-22 06:25:26 -07:00
}
2026-02-01 22:27:31 +01:00
# ------------------------------------------------------------------------------
# Codeberg API call with retry logic
# ------------------------------------------------------------------------------
codeberg_api_call() {
local url = " $1 "
local output_file = " ${ 2 :- /dev/stdout } "
local max_retries = 3
local retry_delay = 2
for attempt in $( seq 1 $max_retries ) ; do
local http_code
2026-02-25 21:01:45 +01:00
http_code = $( curl -sSL -w "%{http_code}" -o " $output_file " \
2026-02-01 22:27:31 +01:00
-H "Accept: application/json" \
2026-02-25 21:01:45 +01:00
" $url " 2>/dev/null) || true
2026-02-01 22:27:31 +01:00
case " $http_code " in
200)
return 0
;;
2026-02-25 21:01:45 +01:00
401)
msg_error "Codeberg API authentication failed (HTTP 401)."
2026-03-26 16:07:38 +01:00
return 22
2026-02-25 21:01:45 +01:00
;;
2026-02-01 22:27:31 +01:00
403)
# Rate limit - retry
if [[ $attempt -lt $max_retries ]] ; then
msg_warn "Codeberg API rate limit, waiting ${ retry_delay } s... (attempt $attempt / $max_retries )"
sleep " $retry_delay "
retry_delay = $(( retry_delay * 2 ))
continue
fi
2026-02-25 21:01:45 +01:00
msg_error "Codeberg API rate limit exceeded (HTTP 403)."
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
;;
404)
2026-02-25 21:01:45 +01:00
msg_error "Codeberg repository or release not found (HTTP 404): $url "
2026-03-26 16:07:38 +01:00
return 22
2026-02-25 21:01:45 +01:00
;;
000 | "" )
if [[ $attempt -lt $max_retries ]] ; then
sleep " $retry_delay "
continue
fi
msg_error "Codeberg API connection failed (no response)."
msg_error "Check your network/DNS: curl -sSL https://codeberg.org"
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
;;
*)
if [[ $attempt -lt $max_retries ]] ; then
sleep " $retry_delay "
continue
fi
2026-02-25 21:01:45 +01:00
msg_error "Codeberg API call failed (HTTP $http_code )."
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
;;
esac
done
2026-02-27 13:59:02 +01:00
msg_error "Codeberg API call failed after ${ max_retries } attempts: ${ url } "
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
}
2025-10-22 06:25:26 -07:00
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 "
2026-03-26 16:07:38 +01:00
return 250
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# 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
2026-03-02 08:44:59 +01:00
# Build fallback chain based on distro
local fallback_chain =()
2025-10-22 06:25:26 -07:00
case " $distro_id " in
debian)
case " $distro_codename " in
trixie | forky | sid)
2026-03-02 08:44:59 +01:00
fallback_chain =( "bookworm" "bullseye" )
2025-10-22 06:25:26 -07:00
;;
bookworm)
2026-03-02 08:44:59 +01:00
fallback_chain =( "bookworm" "bullseye" )
2025-10-22 06:25:26 -07:00
;;
bullseye)
2026-03-02 08:44:59 +01:00
fallback_chain =( "bullseye" "buster" )
2025-10-22 06:25:26 -07:00
;;
*)
2026-03-02 08:44:59 +01:00
fallback_chain =( "bookworm" "bullseye" )
2025-10-22 06:25:26 -07:00
;;
esac
;;
ubuntu)
case " $distro_codename " in
oracular | plucky)
2026-03-02 08:44:59 +01:00
fallback_chain =( "noble" "jammy" "focal" )
2025-10-22 06:25:26 -07:00
;;
noble)
2026-03-02 08:44:59 +01:00
fallback_chain =( "noble" "jammy" )
2025-10-22 06:25:26 -07:00
;;
mantic | lunar)
2026-03-02 08:44:59 +01:00
fallback_chain =( "jammy" "focal" )
2025-10-22 06:25:26 -07:00
;;
jammy)
2026-03-02 08:44:59 +01:00
fallback_chain =( "jammy" "focal" )
2025-10-22 06:25:26 -07:00
;;
focal)
2026-03-02 08:44:59 +01:00
fallback_chain =( "focal" "bionic" )
2025-10-22 06:25:26 -07:00
;;
*)
2026-03-02 08:44:59 +01:00
fallback_chain =( "jammy" "focal" )
2025-10-22 06:25:26 -07:00
;;
esac
;;
*)
echo " $distro_codename "
2026-03-02 08:44:59 +01:00
return 0
2025-10-22 06:25:26 -07:00
;;
esac
2026-03-02 08:44:59 +01:00
# Try each fallback suite with actual HTTP check
for suite in " ${ fallback_chain [@] } " ; do
if verify_repo_available " $repo_base_url " " $suite " ; then
debug_log "Fallback suite found: $suite for $distro_codename "
echo " $suite "
return 0
fi
done
# Last resort: return first fallback without verification
echo " ${ fallback_chain [0] :- $distro_codename } "
return 0
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# 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)
2026-03-02 08:44:59 +01:00
# Features:
# - CPU count detection
# - Memory-based limiting (1.5GB per job for safety)
# - Current load awareness
# - Container/VM detection for conservative limits
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
get_parallel_jobs() {
if [[ -z " ${ _PARALLEL_JOBS :- } " ]] ; then
2026-03-02 08:44:59 +01:00
local cpu_count
cpu_count = $( nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo 2>/dev/null || echo 1)
local mem_mb
mem_mb = $( free -m 2>/dev/null | awk '/^Mem:/{print $2}' || echo 1024)
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
# Assume 1.5GB per compilation job for safety margin
local max_by_mem = $(( mem_mb / 1536 ))
(( max_by_mem < 1)) && max_by_mem = 1
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
# Check current system load - reduce jobs if already loaded
local load_1m
load_1m = $( awk '{print int($1)}' /proc/loadavg 2>/dev/null || echo 0)
local available_cpus = $(( cpu_count - load_1m))
(( available_cpus < 1)) && available_cpus = 1
# Take minimum of: available CPUs, memory-limited, and total CPUs
local max_jobs = $cpu_count
(( max_by_mem < max_jobs)) && max_jobs = $max_by_mem
(( available_cpus < max_jobs)) && max_jobs = $available_cpus
# Container detection - be more conservative in containers
if [[ -f /.dockerenv ]] || grep -q 'lxc\|docker\|container' /proc/1/cgroup 2>/dev/null; then
# Reduce by 25% in containers to leave headroom
max_jobs = $(( max_jobs * 3 / 4 ))
(( max_jobs < 1)) && max_jobs = 1
fi
# Final bounds check
(( max_jobs < 1)) && max_jobs = 1
(( max_jobs > cpu_count)) && max_jobs = $cpu_count
export _PARALLEL_JOBS = $max_jobs
debug_log "Parallel jobs: $_PARALLEL_JOBS (CPUs: $cpu_count , mem-limit: $max_by_mem , load: $load_1m )"
2025-10-22 06:25:26 -07:00
fi
echo " $_PARALLEL_JOBS "
}
# ------------------------------------------------------------------------------
# Get default PHP version for OS
2026-03-02 08:44:59 +01:00
# Updated for latest distro releases
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
get_default_php_version() {
2026-03-02 08:44:59 +01:00
local os_id
os_id = $( get_os_info id)
local os_version
os_version = $( get_os_version_major)
2025-10-22 06:25:26 -07:00
case " $os_id " in
debian)
case " $os_version " in
2026-03-02 08:44:59 +01:00
14) echo "8.4" ;; # Debian 14 (Forky) - future
2025-10-22 06:25:26 -07:00
13) echo "8.3" ;; # Debian 13 (Trixie)
12) echo "8.2" ;; # Debian 12 (Bookworm)
11) echo "7.4" ;; # Debian 11 (Bullseye)
2026-03-02 08:44:59 +01:00
*) echo "8.3" ;; # Default to latest stable
2025-10-22 06:25:26 -07:00
esac
;;
ubuntu)
case " $os_version " in
2026-03-02 08:44:59 +01:00
26) echo "8.4" ;; # Ubuntu 26.04 - future
2025-10-22 06:25:26 -07:00
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)
2026-03-02 08:44:59 +01:00
*) echo "8.3" ;; # Default to latest stable
2025-10-22 06:25:26 -07:00
esac
;;
*)
2026-03-02 08:44:59 +01:00
echo "8.3"
2025-10-22 06:25:26 -07:00
;;
esac
}
# ------------------------------------------------------------------------------
# Get default Python version for OS
2026-03-02 08:44:59 +01:00
# Updated for latest distro releases
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
get_default_python_version() {
2026-03-02 08:44:59 +01:00
local os_id
os_id = $( get_os_info id)
local os_version
os_version = $( get_os_version_major)
2025-10-22 06:25:26 -07:00
case " $os_id " in
debian)
case " $os_version " in
2026-03-02 08:44:59 +01:00
14) echo "3.13" ;; # Debian 14 (Forky) - future
2025-10-22 06:25:26 -07:00
13) echo "3.12" ;; # Debian 13 (Trixie)
12) echo "3.11" ;; # Debian 12 (Bookworm)
11) echo "3.9" ;; # Debian 11 (Bullseye)
2026-03-02 08:44:59 +01:00
*) echo "3.12" ;; # Default to latest stable
2025-10-22 06:25:26 -07:00
esac
;;
ubuntu)
case " $os_version " in
2026-03-02 08:44:59 +01:00
26) echo "3.13" ;; # Ubuntu 26.04 - future
2025-10-22 06:25:26 -07:00
24) echo "3.12" ;; # Ubuntu 24.04 LTS
22) echo "3.10" ;; # Ubuntu 22.04 LTS
20) echo "3.8" ;; # Ubuntu 20.04 LTS
2026-03-02 08:44:59 +01:00
*) echo "3.12" ;; # Default to latest stable
2025-10-22 06:25:26 -07:00
esac
;;
*)
2026-03-02 08:44:59 +01:00
echo "3.12"
2025-10-22 06:25:26 -07:00
;;
esac
}
# ------------------------------------------------------------------------------
# Get default Node.js LTS version
# ------------------------------------------------------------------------------
get_default_nodejs_version() {
2026-03-02 08:44:59 +01:00
# Current LTS as of January 2026 (Node.js 24 LTS)
echo "24"
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# 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"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 06:25:26 -07:00
fi
sleep 5
waited = $(( waited + 5 ))
done
return 0
}
# ------------------------------------------------------------------------------
# Cleanup old repository files (migration helper)
# ------------------------------------------------------------------------------
cleanup_old_repo_files() {
local app = " $1 "
# Remove old-style .list files (including backups)
rm -f /etc/apt/sources.list.d/" ${ app } " *.list
rm -f /etc/apt/sources.list.d/" ${ app } " *.list.save
rm -f /etc/apt/sources.list.d/" ${ app } " *.list.distUpgrade
rm -f /etc/apt/sources.list.d/" ${ app } " *.list.dpkg-*
# Remove old GPG keys from trusted.gpg.d
rm -f /etc/apt/trusted.gpg.d/" ${ app } " *.gpg
# Remove keyrings from /etc/apt/keyrings
rm -f /etc/apt/keyrings/" ${ app } " *.gpg
# Remove ALL .sources files for this app (including the main one)
# This ensures no orphaned .sources files reference deleted keyrings
rm -f /etc/apt/sources.list.d/" ${ app } " *.sources
}
# ------------------------------------------------------------------------------
# Cleanup orphaned .sources files that reference missing keyrings
# This prevents APT signature verification errors
# Call this at the start of any setup function to ensure APT is in a clean state
# ------------------------------------------------------------------------------
cleanup_orphaned_sources() {
local sources_dir = "/etc/apt/sources.list.d"
local keyrings_dir = "/etc/apt/keyrings"
[[ ! -d " $sources_dir " ]] && return 0
while IFS = read -r -d '' sources_file; do
local basename_file
basename_file = $( basename " $sources_file " )
# NEVER remove debian.sources - this is the standard Debian repository
if [[ " $basename_file " == "debian.sources" ]] ; then
continue
fi
# Extract Signed-By path from .sources file
local keyring_path
2025-12-18 18:38:08 +01:00
keyring_path = $( grep -E '^Signed-By:' " $sources_file " 2>/dev/null | awk '{print $2}' 2>/dev/null || true )
2025-10-22 06:25:26 -07:00
# If keyring doesn't exist, remove the .sources file
if [[ -n " $keyring_path " ]] && [[ ! -f " $keyring_path " ]] ; then
rm -f " $sources_file "
fi
done < <( find " $sources_dir " -name "*.sources" -print0 2>/dev/null)
# Also check for broken symlinks in keyrings directory
if [[ -d " $keyrings_dir " ]] ; then
find " $keyrings_dir " -type l ! -exec test -e {} \; -delete 2>/dev/null || true
fi
}
# ------------------------------------------------------------------------------
# Ensure APT is in a working state before installing packages
# This should be called at the start of any setup function
2026-03-02 08:44:59 +01:00
# Features:
# - Fixes interrupted dpkg operations
# - Removes orphaned sources
# - Handles lock file contention
# - Progressive recovery with fallbacks
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
ensure_apt_working() {
2026-03-02 08:44:59 +01:00
local max_wait = 60 # Maximum seconds to wait for apt lock
# Wait for any existing apt/dpkg processes to finish
local waited = 0
while fuser /var/lib/dpkg/lock-frontend & >/dev/null ||
fuser /var/lib/apt/lists/lock & >/dev/null ||
fuser /var/cache/apt/archives/lock & >/dev/null; do
if (( waited >= max_wait)) ; then
msg_warn "APT lock held for ${ max_wait } s, attempting to continue anyway"
break
fi
debug_log "Waiting for APT lock ( ${ waited } s)..."
sleep 2
(( waited += 2))
done
2025-12-05 09:17:19 +01:00
# Fix interrupted dpkg operations first
# This can happen if a previous installation was interrupted (e.g., by script error)
2026-03-02 08:44:59 +01:00
if dpkg --audit 2>& 1 | grep -q .; then
debug_log "Fixing interrupted dpkg operations"
2025-12-05 09:17:19 +01:00
$STD dpkg --configure -a 2>/dev/null || true
fi
2025-10-22 06:25:26 -07:00
# Clean up orphaned sources first
cleanup_orphaned_sources
# Try to update package lists
2026-03-02 08:44:59 +01:00
if ! $STD apt update 2>/dev/null; then
debug_log "First apt update failed, trying recovery steps"
# Step 1: Clear apt lists cache
rm -rf /var/lib/apt/lists/* 2>/dev/null || true
mkdir -p /var/lib/apt/lists/partial
# Step 2: Clean up potentially broken sources
2025-10-22 06:25:26 -07:00
cleanup_orphaned_sources
2026-03-02 08:44:59 +01:00
# Step 3: Try again
if ! $STD apt update 2>/dev/null; then
# Step 4: More aggressive - remove all third-party sources
msg_warn "APT update still failing, removing third-party sources"
find /etc/apt/sources.list.d/ -type f \( -name "*.sources" -o -name "*.list" \) \
! -name "debian.sources" -delete 2>/dev/null || true
# Final attempt
if ! $STD apt update; then
msg_error "Cannot update package lists - APT is critically broken"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
fi
2025-10-22 06:25:26 -07:00
fi
fi
return 0
}
# ------------------------------------------------------------------------------
2025-11-08 11:11:45 -08:00
# Standardized deb822 repository setup (with optional Architectures)
# Always runs apt update after repo creation to ensure package availability
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
setup_deb822_repo() {
local name = " $1 "
local gpg_url = " $2 "
local repo_url = " $3 "
local suite = " $4 "
2025-12-04 07:52:18 +01:00
local component = " ${ 5 :- main } "
2025-11-10 13:58:04 +01:00
local architectures = " ${ 6 - } " # optional
2025-12-16 11:50:28 +01:00
local enabled = " ${ 7 - } " # optional: "true" or "false"
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Validate required parameters
if [[ -z " $name " || -z " $gpg_url " || -z " $repo_url " || -z " $suite " ]] ; then
2025-11-08 11:11:45 -08:00
msg_error "setup_deb822_repo: missing required parameters (name= $name repo= $repo_url suite= $suite )"
2026-03-26 16:07:38 +01:00
return 65
2025-11-05 05:19:35 -08:00
fi
2025-11-08 11:11:45 -08:00
# Cleanup
2025-10-22 06:25:26 -07:00
cleanup_old_repo_files " $name "
cleanup_orphaned_sources
2025-11-05 05:19:35 -08:00
mkdir -p /etc/apt/keyrings || {
2025-11-08 11:11:45 -08:00
msg_error "Failed to create /etc/apt/keyrings"
2026-03-26 16:07:38 +01:00
return 252
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2026-02-12 20:03:33 +01:00
# Import GPG key (auto-detect binary vs ASCII-armored format)
local tmp_gpg
2026-03-26 16:07:38 +01:00
tmp_gpg = $( mktemp) || return 252
2026-02-12 20:03:33 +01:00
curl -fsSL " $gpg_url " -o " $tmp_gpg " || {
msg_error "Failed to download GPG key for ${ name } "
rm -f " $tmp_gpg "
2026-03-26 16:07:38 +01:00
return 7
2025-10-22 06:25:26 -07:00
}
2026-02-12 22:30:34 +01:00
if grep -q "BEGIN PGP" " $tmp_gpg " 2>/dev/null; then
# ASCII-armored — dearmor to binary
2026-02-22 11:54:21 +01:00
gpg --dearmor --yes -o "/etc/apt/keyrings/ ${ name } .gpg" <" $tmp_gpg " || {
2026-02-12 22:30:34 +01:00
msg_error "Failed to install GPG key for ${ name } "
2026-02-12 20:03:33 +01:00
rm -f " $tmp_gpg "
2026-03-26 16:07:38 +01:00
return 251
2026-02-12 20:03:33 +01:00
}
2026-03-02 10:29:04 +01:00
else
# Already binary — copy directly
cp -f " $tmp_gpg " "/etc/apt/keyrings/ ${ name } .gpg" || {
msg_error "Failed to install GPG key for ${ name } "
rm -f " $tmp_gpg "
2026-03-26 16:07:38 +01:00
return 252
2026-03-02 10:29:04 +01:00
}
2026-02-12 20:03:33 +01:00
fi
rm -f " $tmp_gpg "
2026-02-12 22:24:24 +01:00
chmod 644 "/etc/apt/keyrings/ ${ name } .gpg"
2026-02-12 20:03:33 +01:00
2025-11-08 11:11:45 -08:00
# Write deb822
{
echo "Types: deb"
echo "URIs: $repo_url "
echo "Suites: $suite "
2025-12-15 17:29:48 +01:00
# Flat repositories (suite="./" or absolute path) must not have Components
if [[ " $suite " != "./" && -n " $component " ]] ; then
echo "Components: $component "
fi
2025-11-08 11:11:45 -08:00
[[ -n " $architectures " ]] && echo "Architectures: $architectures "
echo "Signed-By: /etc/apt/keyrings/ ${ name } .gpg"
2025-12-16 11:50:28 +01:00
[[ -n " $enabled " ]] && echo "Enabled: $enabled "
2025-11-08 11:11:45 -08:00
} >/etc/apt/sources.list.d/${ name } .sources
2026-02-27 13:59:02 +01:00
$STD apt update || {
msg_warn "apt update failed after adding repository: ${ name } "
}
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# Package version hold/unhold helpers
# ------------------------------------------------------------------------------
hold_package_version() {
local package = " $1 "
2026-02-27 13:59:02 +01:00
$STD apt-mark hold " $package " || {
msg_warn "Failed to hold package version: ${ package } "
}
2025-10-22 06:25:26 -07:00
}
unhold_package_version() {
local package = " $1 "
2026-02-27 13:59:02 +01:00
$STD apt-mark unhold " $package " || {
msg_warn "Failed to unhold package version: ${ package } "
}
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# Safe service restart with verification
# ------------------------------------------------------------------------------
2026-03-02 08:44:59 +01:00
# ------------------------------------------------------------------------------
# Safe service restart with retry logic and wait-for-ready
# Usage: safe_service_restart "nginx" [timeout_seconds]
# ------------------------------------------------------------------------------
2025-10-22 06:25:26 -07:00
safe_service_restart() {
local service = " $1 "
2026-03-02 08:44:59 +01:00
local timeout = " ${ 2 :- 30 } " # Default 30 second timeout
local max_retries = 2
local retry = 0
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
while [[ $retry -le $max_retries ]] ; do
if systemctl is-active --quiet " $service " ; then
$STD systemctl restart " $service "
else
$STD systemctl start " $service "
fi
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
# Wait for service to become active with timeout
local waited = 0
while [[ $waited -lt $timeout ]] ; do
if systemctl is-active --quiet " $service " ; then
return 0
fi
sleep 1
(( waited++))
done
retry = $(( retry + 1 ))
if [[ $retry -le $max_retries ]] ; then
debug_log "Service $service failed to start, retrying ( $retry / $max_retries )..."
# Try to stop completely before retry
systemctl stop " $service " 2>/dev/null || true
sleep 2
fi
done
msg_error "Failed to start $service after $max_retries retries"
systemctl status " $service " --no-pager -l 2>/dev/null | head -20 || true
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# Enable and start service (with error handling)
# ------------------------------------------------------------------------------
enable_and_start_service() {
local service = " $1 "
if ! systemctl enable " $service " & >/dev/null; then
2026-02-27 13:59:02 +01:00
msg_error "Failed to enable service: $service "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
fi
if ! systemctl start " $service " & >/dev/null; then
msg_error "Failed to start $service "
systemctl status " $service " --no-pager
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
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
2026-02-27 13:59:02 +01:00
msg_warn "JSON field ' ${ field } ' is empty in API response"
2026-03-26 16:07:38 +01:00
return 250
2025-10-22 06:25:26 -07:00
fi
if [[ " $strip_v " == "true" ]] ; then
echo " ${ version #v } "
else
echo " $version "
fi
}
2026-03-18 11:15:02 +01:00
# ------------------------------------------------------------------------------
# Get latest GitHub tag (for repos that only publish tags, not releases).
#
# Usage:
# get_latest_gh_tag "owner/repo" [prefix]
#
# Arguments:
# $1 - GitHub repo (owner/repo)
# $2 - Optional prefix filter (e.g., "v" to only match tags starting with "v")
#
# Returns:
# Latest tag name (stdout), or returns 1 on failure
# ------------------------------------------------------------------------------
get_latest_gh_tag() {
local repo = " $1 "
local prefix = " ${ 2 :- } "
local temp_file
temp_file = $( mktemp)
if ! github_api_call "https://api.github.com/repos/ ${ repo } /tags?per_page=50" " $temp_file " ; then
rm -f " $temp_file "
2026-03-26 16:07:38 +01:00
return 22
2026-03-18 11:15:02 +01:00
fi
local tag = ""
if [[ -n " $prefix " ]] ; then
tag = $( jq -r --arg p " $prefix " '[.[] | select(.name | startswith($p))][0].name // empty' " $temp_file " )
else
tag = $( jq -r '.[0].name // empty' " $temp_file " )
fi
rm -f " $temp_file "
if [[ -z " $tag " ]] ; then
msg_error "No tags found for ${ repo } "
2026-03-26 16:07:38 +01:00
return 250
2026-03-18 11:15:02 +01:00
fi
echo " $tag "
}
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-03-02 08:44:59 +01:00
# Get latest GitHub release version with fallback to tags
# Usage: get_latest_github_release "owner/repo" [strip_v] [include_prerelease]
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
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
2026-02-27 13:59:02 +01:00
msg_warn "GitHub API call failed for ${ repo } "
2025-10-22 06:25:26 -07:00
rm -f " $temp_file "
2026-03-26 16:07:38 +01:00
return 22
2025-10-22 06:25:26 -07:00
fi
local version
version = $( extract_version_from_json " $( cat " $temp_file " ) " "tag_name" " $strip_v " )
rm -f " $temp_file "
if [[ -z " $version " ]] ; then
2026-02-25 21:01:45 +01:00
msg_error "Could not determine latest version for ${ repo } "
2026-03-26 16:07:38 +01:00
return 250
2025-10-22 06:25:26 -07:00
fi
echo " $version "
}
2026-02-01 22:27:31 +01:00
# ------------------------------------------------------------------------------
# Get latest Codeberg release version
# ------------------------------------------------------------------------------
get_latest_codeberg_release() {
local repo = " $1 "
local strip_v = " ${ 2 :- true } "
local temp_file = $( mktemp)
# Codeberg API: get all releases and pick the first non-draft/non-prerelease
if ! codeberg_api_call "https://codeberg.org/api/v1/repos/ ${ repo } /releases" " $temp_file " ; then
2026-02-27 13:59:02 +01:00
msg_warn "Codeberg API call failed for ${ repo } "
2026-02-01 22:27:31 +01:00
rm -f " $temp_file "
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
fi
local version
# Codeberg uses same JSON structure but releases endpoint returns array
version = $( jq -r '[.[] | select(.draft==false and .prerelease==false)][0].tag_name // empty' " $temp_file " )
if [[ " $strip_v " == "true" ]] ; then
version = " ${ version #v } "
fi
rm -f " $temp_file "
if [[ -z " $version " ]] ; then
2026-02-25 21:01:45 +01:00
msg_error "Could not determine latest version for ${ repo } "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
echo " $version "
}
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-03-02 08:44:59 +01:00
# Debug logging - using main debug_log function (line 40)
# Supports both TOOLS_DEBUG and DEBUG environment variables
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# 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))
}
# ------------------------------------------------------------------------------
# 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 "
2026-03-26 16:07:38 +01:00
return 65
2025-10-22 06:25:26 -07:00
}
2026-02-24 11:07:40 +01:00
# ------------------------------------------------------------------------------
2026-03-18 07:44:13 +01:00
# Fetches and deploys a GitHub tag-based source tarball.
2026-02-24 11:07:40 +01:00
#
# Description:
2026-03-18 07:44:13 +01:00
# - Downloads the source tarball for a given tag from GitHub
# - Extracts to the target directory
# - Writes the version to ~/.<app>
2026-02-24 11:07:40 +01:00
#
# Usage:
2026-03-18 07:44:13 +01:00
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server"
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "latest" "/opt/guacamole-server"
2026-02-24 11:07:40 +01:00
#
# Arguments:
2026-03-18 07:44:13 +01:00
# $1 - App name (used for version file ~/.<app>)
# $2 - GitHub repo (owner/repo)
# $3 - Tag version (default: "latest" → auto-detect via get_latest_gh_tag)
# $4 - Target directory (default: /opt/$app)
2026-02-24 11:07:40 +01:00
#
# Notes:
2026-03-18 07:44:13 +01:00
# - Supports CLEAN_INSTALL=1 to wipe target before extracting
# - For repos that only publish tags, not GitHub Releases
2026-02-24 11:07:40 +01:00
# ------------------------------------------------------------------------------
2026-03-18 07:44:13 +01:00
fetch_and_deploy_gh_tag() {
local app = " $1 "
local repo = " $2 "
local version = " ${ 3 :- latest } "
local target = " ${ 4 :- /opt/ $app } "
local app_lc = ""
app_lc = " $( echo " ${ app ,, } " | tr -d ' ' ) "
local version_file = " $HOME /. ${ app_lc } "
if [[ " $version " == "latest" ]] ; then
version = $( get_latest_gh_tag " $repo " ) || {
msg_error "Failed to determine latest tag for ${ repo } "
2026-03-26 16:07:38 +01:00
return 250
2026-03-18 07:44:13 +01:00
}
fi
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
local current_version = ""
[[ -f " $version_file " ]] && current_version = $( <" $version_file " )
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
if [[ " $current_version " == " $version " ]] ; then
msg_ok " $app is already up-to-date ( $version )"
return 0
2026-02-25 21:01:45 +01:00
fi
2026-03-18 07:44:13 +01:00
local tmpdir
tmpdir = $( mktemp -d) || return 1
local tarball_url = "https://github.com/ ${ repo } /archive/refs/tags/ ${ version } .tar.gz"
local filename = " ${ app_lc } - ${ version } .tar.gz"
2026-02-25 21:01:45 +01:00
2026-03-18 07:44:13 +01:00
msg_info "Fetching GitHub tag: ${ app } ( ${ version } )"
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
download_file " $tarball_url " " $tmpdir / $filename " || {
msg_error "Download failed: $tarball_url "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 7
2026-03-18 07:44:13 +01:00
}
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
mkdir -p " $target "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ target :? } /" *
fi
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
tar --no-same-owner -xzf " $tmpdir / $filename " -C " $tmpdir " || {
msg_error "Failed to extract tarball"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 251
2026-03-18 07:44:13 +01:00
}
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
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
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
rm -rf " $tmpdir "
echo " $version " >" $version_file "
msg_ok "Deployed ${ app } ${ version } to ${ target } "
2026-02-24 11:07:40 +01:00
return 0
}
2026-03-18 08:00:33 +01:00
# ------------------------------------------------------------------------------
# Checks for new GitHub tag (for repos without releases).
#
# Description:
# - Uses get_latest_gh_tag to fetch the latest tag
# - Compares it to a local cached version (~/.<app>)
# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
#
# Usage:
# if check_for_gh_tag "guacd" "apache/guacamole-server"; then
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "/opt/guacamole-server"
# fi
#
# Notes:
# - For repos that only publish tags, not GitHub Releases
# - Same interface as check_for_gh_release
# ------------------------------------------------------------------------------
check_for_gh_tag() {
local app = " $1 "
local repo = " $2 "
local prefix = " ${ 3 :- } "
local app_lc = ""
app_lc = " $( echo " ${ app ,, } " | tr -d ' ' ) "
local current_file = " $HOME /. ${ app_lc } "
msg_info "Checking for update: ${ app } "
local latest = ""
2026-03-26 16:07:38 +01:00
latest = $( get_latest_gh_tag " $repo " " $prefix " ) || return 22
2026-03-18 08:00:33 +01:00
local current = ""
[[ -f " $current_file " ]] && current = " $( <" $current_file " ) "
if [[ -z " $current " || " $current " != " $latest " ]] ; then
CHECK_UPDATE_RELEASE = " $latest "
msg_ok "Update available: ${ app } ${ current :- not installed } → ${ latest } "
return 0
fi
msg_ok "No update available: ${ app } ( ${ latest } )"
return 1
}
2025-10-22 06:25:26 -07:00
# ==============================================================================
2025-11-10 13:22:59 +01:00
# INSTALL FUNCTIONS
2025-10-22 06:25:26 -07:00
# ==============================================================================
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# Checks for new GitHub release (latest tag).
2025-05-15 15:18:41 +02:00
#
# Description:
2025-09-11 12:44:13 +02:00
# - 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
2025-05-15 15:18:41 +02:00
#
2025-09-11 12:44:13 +02:00
# 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
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
check_for_gh_release() {
local app = " $1 "
local source = " $2 "
local pinned_version_in = " ${ 3 :- } " # optional
2026-03-19 22:18:27 +01:00
local pin_reason = " ${ 4 :- } " # optional reason shown to user
2026-02-03 16:08:34 -05:00
local app_lc = ""
app_lc = " $( echo " ${ app ,, } " | tr -d ' ' ) "
2025-09-11 12:44:13 +02:00
local current_file = " $HOME /. ${ app_lc } "
2025-05-15 15:18:41 +02:00
2025-09-11 12:44:13 +02:00
msg_info "Checking for update: ${ app } "
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# DNS check
if ! getent hosts api.github.com >/dev/null 2>& 1; then
msg_error "Network error: cannot resolve api.github.com"
2026-03-26 16:07:38 +01:00
return 6
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-10-22 06:25:26 -07:00
ensure_dependencies jq
2025-05-27 13:48:34 +02:00
2026-02-22 11:54:21 +01:00
# Build auth header if token is available
local header_args =()
[[ -n " ${ GITHUB_TOKEN :- } " ]] && header_args =( -H "Authorization: Bearer $GITHUB_TOKEN " )
2025-12-09 06:49:43 -08:00
# Try /latest endpoint for non-pinned versions (most efficient)
2026-02-22 11:54:21 +01:00
local releases_json = "" http_code = ""
2025-12-17 14:37:31 +01:00
2026-03-15 20:08:15 +01:00
# For pinned versions, query the specific release tag directly
if [[ -n " $pinned_version_in " ]] ; then
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
" ${ header_args [@] } " \
"https://api.github.com/repos/ ${ source } /releases/tags/ ${ pinned_version_in } " 2>/dev/null) || true
if [[ " $http_code " == "200" ]] && [[ -s /tmp/gh_check.json ]] ; then
releases_json = "[ $( </tmp/gh_check.json) ]"
elif [[ " $http_code " == "401" ]] ; then
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n " ${ GITHUB_TOKEN :- } " ]] ; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
fi
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2026-03-15 20:08:15 +01:00
elif [[ " $http_code " == "403" ]] ; then
msg_error "GitHub API rate limit exceeded (HTTP 403)."
msg_error "To increase the limit, export a GitHub token before running the script:"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2026-03-15 20:08:15 +01:00
fi
rm -f /tmp/gh_check.json
fi
2025-12-09 06:49:43 -08:00
if [[ -z " $pinned_version_in " ]] ; then
2026-02-22 11:54:21 +01:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
2025-12-09 06:49:43 -08:00
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
2026-02-22 11:54:21 +01:00
" ${ header_args [@] } " \
"https://api.github.com/repos/ ${ source } /releases/latest" 2>/dev/null) || true
if [[ " $http_code " == "200" ]] && [[ -s /tmp/gh_check.json ]] ; then
releases_json = "[ $( </tmp/gh_check.json) ]"
2026-02-25 21:01:45 +01:00
elif [[ " $http_code " == "401" ]] ; then
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n " ${ GITHUB_TOKEN :- } " ]] ; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
fi
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2026-02-22 11:54:21 +01:00
elif [[ " $http_code " == "403" ]] ; then
msg_error "GitHub API rate limit exceeded (HTTP 403)."
msg_error "To increase the limit, export a GitHub token before running the script:"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2025-12-09 06:49:43 -08:00
fi
2026-02-22 11:54:21 +01:00
rm -f /tmp/gh_check.json
2025-12-09 06:49:43 -08:00
fi
2025-12-17 14:37:31 +01:00
2025-12-09 06:49:43 -08:00
# If no releases yet (pinned version OR /latest failed), fetch up to 100
if [[ -z " $releases_json " ]] ; then
2026-02-22 11:54:21 +01:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
2025-12-09 06:49:43 -08:00
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
2026-02-22 11:54:21 +01:00
" ${ header_args [@] } " \
"https://api.github.com/repos/ ${ source } /releases?per_page=100" 2>/dev/null) || true
if [[ " $http_code " == "200" ]] && [[ -s /tmp/gh_check.json ]] ; then
releases_json = $( </tmp/gh_check.json)
2026-02-25 21:01:45 +01:00
elif [[ " $http_code " == "401" ]] ; then
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n " ${ GITHUB_TOKEN :- } " ]] ; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
fi
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2026-02-22 11:54:21 +01:00
elif [[ " $http_code " == "403" ]] ; then
msg_error "GitHub API rate limit exceeded (HTTP 403)."
msg_error "To increase the limit, export a GitHub token before running the script:"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2026-02-25 21:01:45 +01:00
elif [[ " $http_code " == "000" || -z " $http_code " ]] ; then
msg_error "GitHub API connection failed (no response)."
msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit"
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 7
2026-02-22 11:54:21 +01:00
else
msg_error "Unable to fetch releases for ${ app } (HTTP ${ http_code } )"
rm -f /tmp/gh_check.json
2026-03-26 16:07:38 +01:00
return 22
2026-02-22 11:54:21 +01:00
fi
rm -f /tmp/gh_check.json
2025-12-09 06:49:43 -08:00
fi
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
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 } "
2026-03-26 16:07:38 +01:00
return 250
2025-09-11 12:44:13 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
local clean_tags =()
for t in " ${ raw_tags [@] } " ; do
2026-03-02 09:16:25 +01:00
# Only strip leading 'v' when followed by a digit (e.g. v1.2.3)
if [[ " $t " = ~ ^v[ 0-9] ]] ; then
clean_tags +=( " ${ t : 1 } " )
else
clean_tags +=( " $t " )
fi
2025-09-11 12:44:13 +02:00
done
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
local latest_raw = " ${ raw_tags [0] } "
local latest_clean = " ${ clean_tags [0] } "
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# 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
2025-07-10 13:58:20 +02:00
fi
2025-09-11 12:44:13 +02:00
done
if [[ -z " $match_raw " ]] ; then
msg_error "Pinned version ${ pinned_version_in } not found upstream"
2026-03-26 16:07:38 +01:00
return 250
2025-07-10 13:58:20 +02:00
fi
2025-09-11 12:44:13 +02:00
if [[ " $current " != " $pin_clean " ]] ; then
CHECK_UPDATE_RELEASE = " $match_raw "
2025-10-23 04:06:13 -07:00
msg_ok "Update available: ${ app } ${ current :- not installed } → ${ pin_clean } "
2025-09-11 12:44:13 +02:00
return 0
2025-04-28 15:25:10 +02:00
fi
2026-03-19 22:18:27 +01:00
if [[ -n " $pin_reason " ]] ; then
msg_ok "No update available: ${ app } ( ${ current } ) - update held back: ${ pin_reason } "
else
msg_ok "No update available: ${ app } ( ${ current } ) - update temporarily held back due to issues with newer releases"
fi
2025-09-11 12:44:13 +02:00
return 1
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
# No pinning → use latest
if [[ -z " $current " || " $current " != " $latest_clean " ]] ; then
CHECK_UPDATE_RELEASE = " $latest_raw "
2025-10-23 04:06:13 -07:00
msg_ok "Update available: ${ app } ${ current :- not installed } → ${ latest_clean } "
2025-09-11 12:44:13 +02:00
return 0
2025-06-26 18:24:43 +02:00
fi
2025-05-05 15:47:43 +02:00
2025-10-23 04:06:13 -07:00
msg_ok "No update available: ${ app } ( ${ latest_clean } )"
2025-09-11 12:44:13 +02:00
return 1
2025-05-05 15:47:43 +02:00
}
2025-04-28 15:25:10 +02:00
2026-02-01 22:27:31 +01:00
# ------------------------------------------------------------------------------
# Checks for new Codeberg release (latest tag).
#
# Description:
# - Queries the Codeberg 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_codeberg_release "autocaliweb" "gelbphoenix/autocaliweb" [optional] "v0.11.3"; 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_codeberg_release() {
local app = " $1 "
local source = " $2 "
local pinned_version_in = " ${ 3 :- } " # optional
2026-03-19 22:18:27 +01:00
local pin_reason = " ${ 4 :- } " # optional reason shown to user
2026-02-01 22:27:31 +01:00
local app_lc = " ${ app ,, } "
local current_file = " $HOME /. ${ app_lc } "
msg_info "Checking for update: ${ app } "
# DNS check
if ! getent hosts codeberg.org >/dev/null 2>& 1; then
msg_error "Network error: cannot resolve codeberg.org"
2026-03-26 16:07:38 +01:00
return 6
2026-02-01 22:27:31 +01:00
fi
ensure_dependencies jq
# Fetch releases from Codeberg API
local releases_json = ""
releases_json = $( curl -fsSL --max-time 20 \
-H 'Accept: application/json' \
"https://codeberg.org/api/v1/repos/ ${ source } /releases" 2>/dev/null) || {
2026-02-27 13:59:02 +01:00
msg_error "Unable to fetch releases for ${ app } (codeberg.org/api/v1/repos/ ${ source } /releases)"
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
}
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 } "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
local clean_tags =()
for t in " ${ raw_tags [@] } " ; do
2026-03-02 09:16:25 +01:00
# Only strip leading 'v' when followed by a digit (e.g. v1.2.3)
if [[ " $t " = ~ ^v[ 0-9] ]] ; then
clean_tags +=( " ${ t : 1 } " )
else
clean_tags +=( " $t " )
fi
2026-02-01 22:27:31 +01:00
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"
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
if [[ " $current " != " $pin_clean " ]] ; then
CHECK_UPDATE_RELEASE = " $match_raw "
msg_ok "Update available: ${ app } ${ current :- not installed } → ${ pin_clean } "
return 0
fi
2026-03-19 22:18:27 +01:00
if [[ -n " $pin_reason " ]] ; then
msg_ok "No update available: ${ app } ( ${ current } ) - update held back: ${ pin_reason } "
else
msg_ok "No update available: ${ app } ( ${ current } ) - update temporarily held back due to issues with newer releases"
fi
2026-02-01 22:27:31 +01:00
return 1
fi
# No pinning → use latest
if [[ -z " $current " || " $current " != " $latest_clean " ]] ; then
CHECK_UPDATE_RELEASE = " $latest_raw "
msg_ok "Update available: ${ app } ${ current :- not installed } → ${ latest_clean } "
return 0
fi
msg_ok "No update available: ${ app } ( ${ latest_clean } )"
return 1
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# Creates and installs self-signed certificates.
2025-05-15 15:18:41 +02:00
#
# Description:
2025-09-11 12:44:13 +02:00
# - Create a self-signed certificate with option to override application name
2025-05-15 15:18:41 +02:00
#
# Variables:
2025-09-11 12:44:13 +02:00
# APP - Application name (default: $APPLICATION variable)
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-10-22 06:25:26 -07:00
create_self_signed_cert() {
local APP_NAME = " ${ 1 :- ${ APPLICATION }} "
2026-03-17 21:19:36 +01:00
local HOSTNAME = " $( hostname -f) "
local IP = " $( hostname -I | awk '{print $1}' ) "
2025-12-01 20:35:12 +01:00
local APP_NAME_LC = $( echo " ${ APP_NAME ,, } " | tr -d ' ' )
local CERT_DIR = "/etc/ssl/ ${ APP_NAME_LC } "
local CERT_KEY = " ${ CERT_DIR } / ${ APP_NAME_LC } .key"
local CERT_CRT = " ${ CERT_DIR } / ${ APP_NAME_LC } .crt"
2025-10-22 06:25:26 -07:00
if [[ -f " $CERT_CRT " && -f " $CERT_KEY " ]] ; then
return 0
fi
2025-11-05 05:19:35 -08:00
# Use ensure_dependencies for cleaner handling
ensure_dependencies openssl || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to install OpenSSL"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 06:25:26 -07:00
}
mkdir -p " $CERT_DIR "
$STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
2026-03-17 21:19:36 +01:00
-subj "/CN= ${ HOSTNAME } " \
-addext "subjectAltName=DNS: ${ HOSTNAME } ,DNS:localhost,IP: ${ IP } ,IP:127.0.0.1" \
2025-10-22 06:25:26 -07:00
-keyout " $CERT_KEY " \
-out " $CERT_CRT " || {
msg_error "Failed to create self-signed certificate"
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
chmod 600 " $CERT_KEY "
chmod 644 " $CERT_CRT "
2025-09-11 12:44:13 +02:00
}
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# Downloads file with optional progress indicator using pv.
#
# Arguments:
# $1 - URL
# $2 - Destination path
# ------------------------------------------------------------------------------
2025-08-28 14:43:56 +02:00
2025-09-11 12:44:13 +02:00
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
2025-10-22 06:25:26 -07:00
ensure_dependencies pv
2025-09-11 12:44:13 +02:00
set -o pipefail
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# Content-Length aus HTTP-Header holen
local content_length
content_length = $( curl -fsSLI " $url " | awk '/Content-Length/ {print $2}' | tr -d '\r' || true )
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
if [[ -z " $content_length " ]] ; then
if ! curl -fL# -o " $output " " $url " ; then
2026-02-27 13:59:02 +01:00
msg_error "Download failed: $url "
2026-03-26 16:07:38 +01:00
return 7
2025-05-15 15:18:41 +02:00
fi
2025-09-11 12:44:13 +02:00
else
if ! curl -fsSL " $url " | pv -s " $content_length " >" $output " ; then
2026-02-27 13:59:02 +01:00
msg_error "Download failed: $url "
2026-03-26 16:07:38 +01:00
return 7
2025-05-13 10:38:19 +02:00
fi
2025-09-11 12:44:13 +02:00
fi
}
2025-05-13 10:38:19 +02:00
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# Ensures /usr/local/bin is permanently in system PATH.
#
# Description:
2026-02-16 10:30:05 +01:00
# - Adds to /etc/profile.d for login shells (SSH, noVNC)
# - Adds to /root/.bashrc for non-login shells (pct enter)
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
2025-05-13 10:38:19 +02:00
2025-09-11 12:44:13 +02:00
function ensure_usr_local_bin_persist() {
2026-02-16 10:30:05 +01:00
# Skip on Proxmox host
command -v pveversion & >/dev/null && return
2025-05-27 14:29:50 +02:00
2026-02-16 10:30:05 +01:00
# Login shells: /etc/profile.d/
local PROFILE_FILE = "/etc/profile.d/custom_path.sh"
if [[ ! -f " $PROFILE_FILE " ]] ; then
2025-09-11 12:44:13 +02:00
echo 'export PATH="/usr/local/bin:$PATH"' >" $PROFILE_FILE "
chmod +x " $PROFILE_FILE "
2025-05-27 14:29:50 +02:00
fi
2026-02-16 10:30:05 +01:00
# Non-login shells (pct enter): /root/.bashrc
local BASHRC = "/root/.bashrc"
if [[ -f " $BASHRC " ]] && ! grep -q '/usr/local/bin' " $BASHRC " ; then
echo 'export PATH="/usr/local/bin:$PATH"' >>" $BASHRC "
fi
2025-04-28 15:25:10 +02:00
}
2026-03-15 10:00:37 +01:00
# ------------------------------------------------------------------------------
# curl_download - Downloads a file with automatic retry and exponential backoff.
#
# Usage: curl_download <output_file> <url>
#
# Retries up to 5 times with increasing --max-time (60/120/240/480/960s).
# Returns 0 on success, 1 if all attempts fail.
# ------------------------------------------------------------------------------
function curl_download() {
local output = " $1 "
local url = " $2 "
local timeouts =( 60 120 240 480 960)
for i in " ${ !timeouts[@] } " ; do
if curl --connect-timeout 15 --max-time " ${ timeouts [ $i ] } " -fsSL -o " $output " " $url " ; then
return 0
fi
if (( i < ${# timeouts [@] } - 1)) ; then
msg_warn "Download timed out after ${ timeouts [ $i ] } s, retrying... (attempt $(( i + 2 )) / ${# timeouts [@] } )"
fi
done
2026-03-26 16:07:38 +01:00
return 7
2026-03-15 10:00:37 +01:00
}
2026-02-01 22:27:31 +01:00
# ------------------------------------------------------------------------------
# Downloads and deploys latest Codeberg release (source, binary, tarball, asset).
#
# Description:
# - Fetches latest release metadata from Codeberg 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)
# - tag: Direct tag download (bypasses Release API)
# - Handles download, extraction/installation and version tracking in ~/.<app>
#
# Parameters:
# $1 APP - Application name (used for install path and version file)
# $2 REPO - Codeberg 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)
# tag → direct tag (bypasses Release API)
# $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
#
# Examples:
# # 1. Minimal: Fetch and deploy source tarball
# fetch_and_deploy_codeberg_release "autocaliweb" "gelbphoenix/autocaliweb"
#
# # 2. Binary install via .deb asset (architecture auto-detected)
# fetch_and_deploy_codeberg_release "myapp" "myuser/myapp" "binary"
#
# # 3. Prebuilt archive (.tar.gz) with asset filename match
# fetch_and_deploy_codeberg_release "myapp" "myuser/myapp" "prebuild" "latest" "/opt/myapp" "myapp_Linux_x86_64.tar.gz"
#
# # 4. Single binary (chmod +x)
# fetch_and_deploy_codeberg_release "myapp" "myuser/myapp" "singlefile" "v1.0.0" "/opt/myapp" "myapp-linux-amd64"
#
# # 5. Explicit tag version
# fetch_and_deploy_codeberg_release "autocaliweb" "gelbphoenix/autocaliweb" "tag" "v0.11.3" "/opt/autocaliweb"
# ------------------------------------------------------------------------------
function fetch_and_deploy_codeberg_release() {
local app = " $1 "
local repo = " $2 "
local mode = " ${ 3 :- tarball } " # tarball | binary | prebuild | singlefile | tag
2026-02-14 16:37:13 +01:00
local version = " ${ var_appversion :- ${ 4 :- latest }} "
2026-02-01 22:27:31 +01:00
local target = " ${ 5 :- /opt/ $app } "
local asset_pattern = " ${ 6 :- } "
local app_lc = $( echo " ${ app ,, } " | tr -d ' ' )
local version_file = " $HOME /. ${ app_lc } "
2026-03-15 10:00:37 +01:00
local api_timeouts =( 60 120 240)
2026-02-01 22:27:31 +01:00
local current_version = ""
[[ -f " $version_file " ]] && current_version = $( <" $version_file " )
ensure_dependencies jq
### Tag Mode (bypass Release API) ###
if [[ " $mode " == "tag" ]] ; then
if [[ " $version " == "latest" ]] ; then
msg_error "Mode 'tag' requires explicit version (not 'latest')"
2026-03-26 16:07:38 +01:00
return 65
2026-02-01 22:27:31 +01:00
fi
local tag_name = " $version "
[[ " $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
# DNS check
if ! getent hosts "codeberg.org" & >/dev/null; then
msg_error "DNS resolution failed for codeberg.org – check /etc/resolv.conf or networking"
2026-03-26 16:07:38 +01:00
return 6
2026-02-01 22:27:31 +01:00
fi
local tmpdir
2026-03-26 16:07:38 +01:00
tmpdir = $( mktemp -d) || return 252
2026-02-01 22:27:31 +01:00
msg_info "Fetching Codeberg tag: $app ( $tag_name )"
local safe_version = " ${ version //@/_ } "
safe_version = " ${ safe_version // \/ /_ } "
local filename = " ${ app_lc } - ${ safe_version } .tar.gz"
local download_success = false
# Codeberg archive URL format: https://codeberg.org/{owner}/{repo}/archive/{tag}.tar.gz
local archive_url = "https://codeberg.org/ $repo /archive/ ${ tag_name } .tar.gz"
2026-03-15 10:00:37 +01:00
if curl_download " $tmpdir / $filename " " $archive_url " ; then
2026-02-01 22:27:31 +01:00
download_success = true
fi
if [[ " $download_success " != "true" ]] ; then
msg_error "Download failed for $app ( $tag_name )"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
mkdir -p " $target "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ target :? } /" *
fi
tar --no-same-owner -xzf " $tmpdir / $filename " -C " $tmpdir " || {
msg_error "Failed to extract tarball"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 251
2026-02-01 22:27:31 +01:00
}
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
echo " $version " >" $version_file "
msg_ok "Deployed: $app ( $version )"
rm -rf " $tmpdir "
return 0
fi
# Codeberg API: https://codeberg.org/api/v1/repos/{owner}/{repo}/releases
local api_url = "https://codeberg.org/api/v1/repos/ $repo /releases"
if [[ " $version " != "latest" ]] ; then
# Get release by tag: /repos/{owner}/{repo}/releases/tags/{tag}
api_url = "https://codeberg.org/api/v1/repos/ $repo /releases/tags/ $version "
fi
# dns pre check
if ! getent hosts "codeberg.org" & >/dev/null; then
msg_error "DNS resolution failed for codeberg.org – check /etc/resolv.conf or networking"
2026-03-26 16:07:38 +01:00
return 6
2026-02-01 22:27:31 +01:00
fi
2026-03-15 10:00:37 +01:00
local attempt = 0 success = false resp http_code
2026-02-01 22:27:31 +01:00
2026-03-15 10:00:37 +01:00
while (( attempt < ${# api_timeouts [@] } )) ; do
resp = $( curl --connect-timeout 10 --max-time " ${ api_timeouts [ $attempt ] } " -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json " $api_url " ) && success = true && break
2026-04-10 11:28:52 +02:00
attempt = $(( attempt + 1 ))
2026-03-15 10:00:37 +01:00
if (( attempt < ${# api_timeouts [@] } )) ; then
2026-03-15 20:08:15 +01:00
msg_warn "API request timed out after ${ api_timeouts [ $(( attempt - 1 )) ] } s, retrying... (attempt $(( attempt + 1 )) / ${# api_timeouts [@] } )"
2026-03-15 10:00:37 +01:00
fi
2026-02-01 22:27:31 +01:00
done
if ! $success ; then
2026-03-15 10:00:37 +01:00
msg_error "Failed to fetch release metadata from $api_url after ${# api_timeouts [@] } attempts"
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
fi
http_code = " ${ resp :(-3) } "
[[ " $http_code " != "200" ]] && {
msg_error "Codeberg API returned HTTP $http_code "
2026-03-26 16:07:38 +01:00
return 22
2026-02-01 22:27:31 +01:00
}
local json tag_name
json = $( </tmp/codeberg_rel.json)
# For "latest", the API returns an array - take the first (most recent) release
if [[ " $version " == "latest" ]] ; then
json = $( echo " $json " | jq '.[0]' )
fi
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
2026-03-26 16:07:38 +01:00
tmpdir = $( mktemp -d) || return 252
2026-02-01 22:27:31 +01:00
local filename = "" url = ""
msg_info "Fetching Codeberg release: $app ( $version )"
### Tarball Mode ###
if [[ " $mode " == "tarball" || " $mode " == "source" ]] ; then
local safe_version = " ${ version //@/_ } "
safe_version = " ${ safe_version // \/ /_ } "
filename = " ${ app_lc } - ${ safe_version } .tar.gz"
local download_success = false
# Codeberg archive URL format
local archive_url = "https://codeberg.org/ $repo /archive/ ${ tag_name } .tar.gz"
2026-03-15 10:00:37 +01:00
if curl_download " $tmpdir / $filename " " $archive_url " ; then
2026-02-01 22:27:31 +01:00
download_success = true
fi
if [[ " $download_success " != "true" ]] ; then
msg_error "Download failed for $app ( $tag_name )"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
mkdir -p " $target "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ target :? } /" *
fi
tar --no-same-owner -xzf " $tmpdir / $filename " -C " $tmpdir " || {
msg_error "Failed to extract tarball"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 251
2026-02-01 22:27:31 +01:00
}
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 = ""
# Codeberg assets are in .assets[].browser_download_url
assets = $( echo " $json " | jq -r '.assets[].browser_download_url' )
# If explicit filename pattern is provided, 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
# 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 "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
fi
filename = " ${ url_match ##*/ } "
2026-03-15 10:00:37 +01:00
curl_download " $tmpdir / $filename " " $url_match " || {
2026-02-01 22:27:31 +01:00
msg_error "Download failed: $url_match "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
}
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 "
2026-03-26 16:07:38 +01:00
return 100
2026-02-01 22:27:31 +01:00
}
}
### 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 "
2026-03-26 16:07:38 +01:00
return 65
2026-02-01 22:27:31 +01:00
}
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 "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
}
filename = " ${ asset_url ##*/ } "
2026-03-15 10:00:37 +01:00
curl_download " $tmpdir / $filename " " $asset_url " || {
2026-02-01 22:27:31 +01:00
msg_error "Download failed: $asset_url "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
}
local unpack_tmp
unpack_tmp = $( mktemp -d)
mkdir -p " $target "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ target :? } /" *
fi
if [[ " $filename " == *.zip ]] ; then
ensure_dependencies unzip
unzip -q " $tmpdir / $filename " -d " $unpack_tmp " || {
msg_error "Failed to extract ZIP archive"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2026-02-01 22:27:31 +01:00
}
elif [[ " $filename " == *.tar.* || " $filename " == *.tgz ]] ; then
tar --no-same-owner -xf " $tmpdir / $filename " -C " $unpack_tmp " || {
msg_error "Failed to extract TAR archive"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2026-02-01 22:27:31 +01:00
}
else
msg_error "Unsupported archive format: $filename "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2026-02-01 22:27:31 +01:00
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
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 "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
}
else
msg_error "Inner directory is empty: $inner_dir "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
fi
shopt -u dotglob nullglob
else
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 "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
}
else
msg_error "Unpacked archive is empty"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
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 "
2026-03-26 16:07:38 +01:00
return 65
2026-02-01 22:27:31 +01:00
}
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 "
2026-03-26 16:07:38 +01:00
return 252
2026-02-01 22:27:31 +01:00
}
filename = " ${ asset_url ##*/ } "
mkdir -p " $target "
local use_filename = " ${ USE_ORIGINAL_FILENAME :- false } "
local target_file = " $app "
[[ " $use_filename " == "true" ]] && target_file = " $filename "
2026-03-15 10:00:37 +01:00
curl_download " $target / $target_file " " $asset_url " || {
2026-02-01 22:27:31 +01:00
msg_error "Download failed: $asset_url "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
}
if [[ " $target_file " != *.jar && -f " $target / $target_file " ]] ; then
chmod +x " $target / $target_file "
fi
else
msg_error "Unknown mode: $mode "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 65
2026-02-01 22:27:31 +01:00
fi
echo " $version " >" $version_file "
msg_ok "Deployed: $app ( $version )"
rm -rf " $tmpdir "
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# Downloads and deploys latest GitHub release (source, binary, tarball, asset).
2025-05-15 15:18:41 +02:00
#
# Description:
2025-09-11 12:44:13 +02:00
# - 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>
2025-05-15 15:18:41 +02:00
#
2025-09-11 12:44:13 +02:00
# 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"
2026-02-07 20:37:50 +01:00
#
# Notes:
# - For binary/prebuild/singlefile modes: if the target release has no
# matching asset, the function scans older releases and prompts the user
# (60s timeout, default yes) to use a previous version that has the asset.
# ------------------------------------------------------------------------------
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2026-02-07 20:37:50 +01:00
# Scans older GitHub releases for a matching asset when the latest release
# is missing the expected file. Used internally by fetch_and_deploy_gh_release.
#
# Arguments:
# $1 - GitHub repo (owner/repo)
# $2 - mode (binary|prebuild|singlefile)
# $3 - asset_pattern (glob pattern for asset filename)
# $4 - tag to skip (the already-checked release)
#
# Output:
# Prints the release JSON of the first older release that has a matching asset.
# Returns 0 on success, 1 if no matching release found or user declined.
# ------------------------------------------------------------------------------
_gh_scan_older_releases() {
local repo = " $1 "
local mode = " $2 "
local asset_pattern = " $3 "
local skip_tag = " $4 "
local header =()
[[ -n " ${ GITHUB_TOKEN :- } " ]] && header =( -H "Authorization: token $GITHUB_TOKEN " )
local releases_list
releases_list = $( curl --connect-timeout 10 --max-time 30 -fsSL \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
" ${ header [@] } " \
2026-02-27 13:59:02 +01:00
"https://api.github.com/repos/ ${ repo } /releases?per_page=15" 2>/dev/null) || {
msg_warn "Failed to fetch older releases for ${ repo } "
2026-03-26 16:07:38 +01:00
return 22
2026-02-27 13:59:02 +01:00
}
2026-02-07 20:37:50 +01:00
local count
count = $( echo " $releases_list " | jq 'length' )
for (( i = 0; i < count; i++)) ; do
local rel_tag rel_draft rel_prerelease
rel_tag = $( echo " $releases_list " | jq -r ".[ $i ].tag_name" )
rel_draft = $( echo " $releases_list " | jq -r ".[ $i ].draft" )
rel_prerelease = $( echo " $releases_list " | jq -r ".[ $i ].prerelease" )
# Skip drafts, prereleases, and the tag we already checked
[[ " $rel_draft " == "true" || " $rel_prerelease " == "true" ]] && continue
[[ " $rel_tag " == " $skip_tag " ]] && continue
local has_match = false
if [[ " $mode " == "binary" ]] ; then
local arch
arch = $( dpkg --print-architecture 2>/dev/null || uname -m)
[[ " $arch " == "x86_64" ]] && arch = "amd64"
[[ " $arch " == "aarch64" ]] && arch = "arm64"
# Check with explicit pattern first, then arch heuristic, then any .deb
if [[ -n " $asset_pattern " ]] ; then
has_match = $( echo " $releases_list " | jq -r --arg pat " $asset_pattern " ".[ $i ].assets[].name" | while read -r name; do
2026-02-22 11:54:21 +01:00
case " $name " in $asset_pattern )
echo true
break
;;
esac
2026-02-07 20:37:50 +01:00
done )
fi
if [[ " $has_match " != "true" ]] ; then
has_match = $( echo " $releases_list " | jq -r ".[ $i ].assets[].browser_download_url" | grep -qE "( $arch |amd64|x86_64|aarch64|arm64).*\.deb $" && echo true )
fi
if [[ " $has_match " != "true" ]] ; then
has_match = $( echo " $releases_list " | jq -r ".[ $i ].assets[].browser_download_url" | grep -qE '\.deb$' && echo true )
fi
elif [[ " $mode " == "prebuild" || " $mode " == "singlefile" ]] ; then
has_match = $( echo " $releases_list " | jq -r ".[ $i ].assets[].name" | while read -r name; do
2026-02-22 11:54:21 +01:00
case " $name " in $asset_pattern )
echo true
break
;;
esac
2026-02-07 20:37:50 +01:00
done )
fi
if [[ " $has_match " == "true" ]] ; then
local rel_version = " $rel_tag "
[[ " $rel_tag " = ~ ^v ]] && rel_version = " ${ rel_tag : 1 } "
local use_fallback = "y"
if [[ -t 0 ]] ; then
msg_warn "Release ${ skip_tag } has no matching asset. Previous release ${ rel_tag } has a compatible asset."
read -rp "Use version ${ rel_tag } instead? [Y/n] (auto-yes in 60s): " -t 60 use_fallback || use_fallback = "y"
use_fallback = " ${ use_fallback :- y } "
fi
if [[ " ${ use_fallback ,, } " == "y" || " ${ use_fallback ,, } " == "yes" ]] ; then
echo " $releases_list " | jq ".[ $i ]"
return 0
else
2026-03-26 16:07:38 +01:00
return 250
2026-02-07 20:37:50 +01:00
fi
fi
done
2026-03-26 16:07:38 +01:00
return 250
2026-02-07 20:37:50 +01:00
}
2025-05-15 15:18:41 +02:00
2025-09-11 12:44:13 +02:00
function fetch_and_deploy_gh_release() {
local app = " $1 "
local repo = " $2 "
local mode = " ${ 3 :- tarball } " # tarball | binary | prebuild | singlefile
2026-02-14 16:37:13 +01:00
local version = " ${ var_appversion :- ${ 4 :- latest }} "
2025-09-11 12:44:13 +02:00
local target = " ${ 5 :- /opt/ $app } "
local asset_pattern = " ${ 6 :- } "
2025-05-05 15:47:43 +02:00
2026-03-02 08:44:59 +01:00
# Validate app name to prevent /root/. directory issues
if [[ -z " $app " ]] ; then
# Derive app name from repo if not provided
app = " ${ repo ##*/ } "
if [[ -z " $app " ]] ; then
msg_error "fetch_and_deploy_gh_release requires app name or valid repo"
2026-03-26 16:07:38 +01:00
return 65
2026-03-02 08:44:59 +01:00
fi
fi
2025-09-11 12:44:13 +02:00
local app_lc = $( echo " ${ app ,, } " | tr -d ' ' )
local version_file = " $HOME /. ${ app_lc } "
2025-07-04 21:22:50 +02:00
2026-03-15 10:00:37 +01:00
local api_timeouts =( 60 120 240)
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
local current_version = ""
[[ -f " $version_file " ]] && current_version = $( <" $version_file " )
2025-05-05 15:47:43 +02:00
2025-10-22 06:25:26 -07:00
ensure_dependencies jq
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
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 " )
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# 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"
2026-03-26 16:07:38 +01:00
return 6
2025-09-11 12:44:13 +02:00
fi
2025-05-05 15:47:43 +02:00
2026-03-15 10:00:37 +01:00
local max_retries = ${# api_timeouts [@] } retry_delay = 2 attempt = 1 success = false http_code
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
while (( attempt <= max_retries)) ; do
2026-03-15 20:08:15 +01:00
http_code = $( curl --connect-timeout 10 --max-time " ${ api_timeouts [ $(( attempt - 1 )) ] :- 240 } " -sSL -w "%{http_code}" -o /tmp/gh_rel.json " ${ header [@] } " " $api_url " 2>/dev/null) || true
2026-02-22 11:54:21 +01:00
if [[ " $http_code " == "200" ]] ; then
success = true
break
2026-03-07 22:14:18 +01:00
elif [[ " $http_code " == "401" ]] ; then
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n " ${ GITHUB_TOKEN :- } " ]] ; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication."
fi
if prompt_for_github_token; then
header =( -H "Authorization: token $GITHUB_TOKEN " )
continue
fi
break
2026-02-22 11:54:21 +01:00
elif [[ " $http_code " == "403" ]] ; then
if (( attempt < max_retries)) ; then
msg_warn "GitHub API rate limit hit, retrying in ${ retry_delay } s... (attempt $attempt / $max_retries )"
sleep " $retry_delay "
retry_delay = $(( retry_delay * 2 ))
2026-03-07 22:14:18 +01:00
else
msg_error "GitHub API rate limit exceeded (HTTP 403)."
if prompt_for_github_token; then
header =( -H "Authorization: token $GITHUB_TOKEN " )
retry_delay = 2
2026-04-10 11:28:52 +02:00
attempt = 1
continue
2026-03-07 22:14:18 +01:00
fi
2026-02-22 11:54:21 +01:00
fi
else
sleep " $retry_delay "
fi
2025-09-11 12:44:13 +02:00
(( attempt++))
done
2025-08-08 10:28:05 +02:00
2025-09-11 12:44:13 +02:00
if ! $success ; then
2026-03-07 22:14:18 +01:00
if [[ " $http_code " == "000" || -z " $http_code " ]] ; then
2026-02-25 21:01:45 +01:00
msg_error "GitHub API connection failed (no response)."
msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit"
2026-03-07 22:14:18 +01:00
elif [[ " $http_code " != "401" ]] ; then
2026-02-25 21:01:45 +01:00
msg_error "Failed to fetch release metadata (HTTP $http_code )"
2026-02-22 11:54:21 +01:00
fi
2026-03-26 16:07:38 +01:00
return 22
2025-08-08 10:28:05 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
local json tag_name
json = $( </tmp/gh_rel.json)
tag_name = $( echo " $json " | jq -r '.tag_name // .name // empty' )
2026-03-02 09:16:25 +01:00
# Only strip leading 'v' when followed by a digit (e.g. v1.2.3), not words like "version/..."
[[ " $tag_name " = ~ ^v[ 0-9] ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
# Sanitize version for use in filenames (replace / with -)
local version_safe = " ${ version // \/ /- } "
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
if [[ " $current_version " == " $version " ]] ; then
$STD msg_ok " $app is already up-to-date (v $version )"
return 0
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
local tmpdir
tmpdir = $( mktemp -d) || return 1
local filename = "" url = ""
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
msg_info "Fetching GitHub release: $app ( $version )"
2025-04-28 15:25:10 +02:00
2025-09-21 16:20:18 +02:00
local clean_install = false
[[ -n " ${ CLEAN_INSTALL :- } " && " $CLEAN_INSTALL " == "1" ]] && clean_install = true
2025-09-11 12:44:13 +02:00
### Tarball Mode ###
if [[ " $mode " == "tarball" || " $mode " == "source" ]] ; then
2025-12-05 23:09:20 +01:00
# GitHub API's tarball_url/zipball_url can return HTTP 300 Multiple Choices
# when a branch and tag share the same name. Use explicit refs/tags/ URL instead.
local direct_tarball_url = "https://github.com/ $repo /archive/refs/tags/ $tag_name .tar.gz"
2026-03-02 09:16:25 +01:00
filename = " ${ app_lc } - ${ version_safe } .tar.gz"
2025-04-28 15:25:10 +02:00
2026-03-15 10:00:37 +01:00
curl_download " $tmpdir / $filename " " $direct_tarball_url " || {
2025-12-05 23:09:20 +01:00
msg_error "Download failed: $direct_tarball_url "
2025-09-11 12:44:13 +02:00
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2025-09-11 12:44:13 +02:00
}
2025-05-15 15:18:41 +02:00
2025-09-11 12:44:13 +02:00
mkdir -p " $target "
2025-09-21 16:20:18 +02:00
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ target :? } /" *
fi
2025-11-05 05:19:35 -08:00
tar --no-same-owner -xzf " $tmpdir / $filename " -C " $tmpdir " || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to extract tarball"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
local unpack_dir
unpack_dir = $( find " $tmpdir " -mindepth 1 -maxdepth 1 -type d | head -n1)
2025-05-15 15:18:41 +02:00
2025-09-11 12:44:13 +02:00
shopt -s dotglob nullglob
cp -r " $unpack_dir " /* " $target /"
shopt -u dotglob nullglob
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
### 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"
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
local assets url_match = ""
assets = $( echo " $json " | jq -r '.assets[].browser_download_url' )
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# 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
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# 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
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# Fallback: any .deb file
if [[ -z " $url_match " ]] ; then
for u in $assets ; do
[[ " $u " = ~ \. deb$ ]] && url_match = " $u " && break
done
fi
2025-05-05 15:47:43 +02:00
2026-02-07 20:37:50 +01:00
# Fallback: scan older releases for a matching .deb asset
if [[ -z " $url_match " ]] ; then
local fallback_json
if fallback_json = $( _gh_scan_older_releases " $repo " "binary" " $asset_pattern " " $tag_name " ) ; then
json = " $fallback_json "
tag_name = $( echo " $json " | jq -r '.tag_name // .name // empty' )
[[ " $tag_name " = ~ ^v ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
msg_info "Fetching GitHub release: $app ( $version )"
assets = $( echo " $json " | jq -r '.assets[].browser_download_url' )
if [[ -n " $asset_pattern " ]] ; then
for u in $assets ; do
2026-02-22 11:54:21 +01:00
case " ${ u ##*/ } " in $asset_pattern )
url_match = " $u "
break
;;
esac
2026-02-07 20:37:50 +01:00
done
fi
if [[ -z " $url_match " ]] ; then
for u in $assets ; do
2026-02-22 11:54:21 +01:00
if [[ " $u " = ~ ( $arch | amd64| x86_64| aarch64| arm64) .*\. deb$ ]] ; then
url_match = " $u "
break
fi
2026-02-07 20:37:50 +01:00
done
fi
if [[ -z " $url_match " ]] ; then
for u in $assets ; do
[[ " $u " = ~ \. deb$ ]] && url_match = " $u " && break
done
fi
fi
fi
2025-09-11 12:44:13 +02:00
if [[ -z " $url_match " ]] ; then
msg_error "No suitable .deb asset found for $app "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
fi
2025-06-16 14:20:46 +02:00
2025-09-11 12:44:13 +02:00
filename = " ${ url_match ##*/ } "
2026-03-15 10:00:37 +01:00
curl_download " $tmpdir / $filename " " $url_match " || {
2025-09-11 12:44:13 +02:00
msg_error "Download failed: $url_match "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2025-09-11 12:44:13 +02:00
}
chmod 644 " $tmpdir / $filename "
2026-01-28 13:25:07 +01:00
# SYSTEMD_OFFLINE=1 prevents systemd-tmpfiles failures in unprivileged LXC (Debian 13+/systemd 257+)
2026-01-29 12:43:54 +01:00
# Support DPKG_CONFOLD/DPKG_CONFNEW env vars for config file handling during .deb upgrades
local dpkg_opts = ""
[[ " ${ DPKG_FORCE_CONFOLD :- } " == "1" ]] && dpkg_opts = "-o Dpkg::Options::=--force-confold"
[[ " ${ DPKG_FORCE_CONFNEW :- } " == "1" ]] && dpkg_opts = "-o Dpkg::Options::=--force-confnew"
DEBIAN_FRONTEND = noninteractive SYSTEMD_OFFLINE = 1 $STD apt install -y $dpkg_opts " $tmpdir / $filename " || {
2026-01-28 13:25:07 +01:00
SYSTEMD_OFFLINE = 1 $STD dpkg -i " $tmpdir / $filename " || {
2025-09-11 12:44:13 +02:00
msg_error "Both apt and dpkg installation failed"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 100
2025-09-11 12:44:13 +02:00
}
}
2025-09-21 16:20:18 +02:00
### Prebuild Mode ###
2025-09-11 12:44:13 +02:00
elif [[ " $mode " == "prebuild" ]] ; then
local pattern = " ${ 6 % \" } "
pattern = " ${ pattern # \" } "
[[ -z " $pattern " ]] && {
msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 65
2025-09-11 12:44:13 +02:00
}
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
2026-02-07 20:37:50 +01:00
# Fallback: scan older releases for a matching asset
if [[ -z " $asset_url " ]] ; then
local fallback_json
if fallback_json = $( _gh_scan_older_releases " $repo " "prebuild" " $pattern " " $tag_name " ) ; then
json = " $fallback_json "
tag_name = $( echo " $json " | jq -r '.tag_name // .name // empty' )
[[ " $tag_name " = ~ ^v ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
msg_info "Fetching GitHub release: $app ( $version )"
for u in $( echo " $json " | jq -r '.assets[].browser_download_url' ) ; do
filename_candidate = " ${ u ##*/ } "
2026-02-22 11:54:21 +01:00
case " $filename_candidate " in $pattern )
asset_url = " $u "
break
;;
esac
2026-02-07 20:37:50 +01:00
done
fi
fi
2025-09-11 12:44:13 +02:00
[[ -z " $asset_url " ]] && {
msg_error "No asset matching ' $pattern ' found"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
}
filename = " ${ asset_url ##*/ } "
2026-03-15 10:00:37 +01:00
curl_download " $tmpdir / $filename " " $asset_url " || {
2025-09-11 12:44:13 +02:00
msg_error "Download failed: $asset_url "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2025-09-11 12:44:13 +02:00
}
local unpack_tmp
unpack_tmp = $( mktemp -d)
mkdir -p " $target "
2025-09-21 16:20:18 +02:00
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ target :? } /" *
fi
2025-09-11 12:44:13 +02:00
if [[ " $filename " == *.zip ]] ; then
2025-10-22 06:25:26 -07:00
ensure_dependencies unzip
unzip -q " $tmpdir / $filename " -d " $unpack_tmp " || {
msg_error "Failed to extract ZIP archive"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2026-01-23 21:54:23 +01:00
elif [[ " $filename " == *.tar.* || " $filename " == *.tgz || " $filename " == *.txz ]] ; then
2025-10-23 06:46:28 -07:00
tar --no-same-owner -xf " $tmpdir / $filename " -C " $unpack_tmp " || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to extract TAR archive"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-07-22 13:14:09 +02:00
else
2025-09-11 12:44:13 +02:00
msg_error "Unsupported archive format: $filename "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 65
2025-07-22 13:14:09 +02:00
fi
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
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 "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
}
else
msg_error "Inner directory is empty: $inner_dir "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
fi
shopt -u dotglob nullglob
2025-07-22 15:55:56 +02:00
else
2025-09-11 12:44:13 +02:00
# 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 "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
}
else
msg_error "Unpacked archive is empty"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
fi
shopt -u dotglob nullglob
2025-05-05 15:47:43 +02:00
fi
2025-09-11 12:44:13 +02:00
### 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 "
2026-03-26 16:07:38 +01:00
return 65
2025-09-11 12:44:13 +02:00
}
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
2026-02-07 20:37:50 +01:00
# Fallback: scan older releases for a matching asset
if [[ -z " $asset_url " ]] ; then
local fallback_json
if fallback_json = $( _gh_scan_older_releases " $repo " "singlefile" " $pattern " " $tag_name " ) ; then
json = " $fallback_json "
tag_name = $( echo " $json " | jq -r '.tag_name // .name // empty' )
[[ " $tag_name " = ~ ^v ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
msg_info "Fetching GitHub release: $app ( $version )"
for u in $( echo " $json " | jq -r '.assets[].browser_download_url' ) ; do
filename_candidate = " ${ u ##*/ } "
2026-02-22 11:54:21 +01:00
case " $filename_candidate " in $pattern )
asset_url = " $u "
break
;;
esac
2026-02-07 20:37:50 +01:00
done
fi
fi
2025-09-11 12:44:13 +02:00
[[ -z " $asset_url " ]] && {
msg_error "No asset matching ' $pattern ' found"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 252
2025-09-11 12:44:13 +02:00
}
filename = " ${ asset_url ##*/ } "
mkdir -p " $target "
local use_filename = " ${ USE_ORIGINAL_FILENAME :- false } "
local target_file = " $app "
[[ " $use_filename " == "true" ]] && target_file = " $filename "
2026-03-15 10:00:37 +01:00
curl_download " $target / $target_file " " $asset_url " || {
2025-09-11 12:44:13 +02:00
msg_error "Download failed: $asset_url "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2025-09-11 12:44:13 +02:00
}
if [[ " $target_file " != *.jar && -f " $target / $target_file " ]] ; then
chmod +x " $target / $target_file "
fi
else
msg_error "Unknown mode: $mode "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 65
2025-05-05 15:47:43 +02:00
fi
2025-09-11 12:44:13 +02:00
echo " $version " >" $version_file "
msg_ok "Deployed: $app ( $version )"
rm -rf " $tmpdir "
}
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# 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() {
if grep -qi alpine /etc/os-release; then
msg_info "Setup Adminer (Alpine)"
mkdir -p /var/www/localhost/htdocs/adminer
2026-03-02 08:44:59 +01:00
if ! curl_with_retry "https://github.com/vrana/adminer/releases/latest/download/adminer.php" "/var/www/localhost/htdocs/adminer/index.php" ; then
2025-09-11 12:44:13 +02:00
msg_error "Failed to download Adminer"
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-10-22 06:25:26 -07:00
cache_installed_version "adminer" "latest-alpine"
msg_ok "Setup Adminer (Alpine)"
2025-09-11 12:44:13 +02:00
else
msg_info "Setup Adminer (Debian/Ubuntu)"
2025-10-22 06:25:26 -07:00
ensure_dependencies adminer
$STD a2enconf adminer || {
msg_error "Failed to enable Adminer Apache config"
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD systemctl reload apache2 || {
msg_error "Failed to reload Apache"
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
local VERSION
2025-12-18 18:38:08 +01:00
VERSION = $( dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}' 2>/dev/null || echo 'unknown' )
2025-10-22 06:25:26 -07:00
cache_installed_version "adminer" " ${ VERSION :- unknown } "
msg_ok "Setup Adminer (Debian/Ubuntu)"
2025-07-22 13:14:09 +02:00
fi
2025-04-28 15:25:10 +02:00
}
2025-08-04 14:12:18 +02:00
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-08-04 14:12:18 +02:00
# Installs or updates Composer globally (robust, idempotent).
2025-05-15 15:18:41 +02:00
#
2025-08-04 14:12:18 +02:00
# - 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)
2025-11-05 05:19:35 -08:00
# - Auto-updates to latest version
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-06-18 12:07:08 +02:00
function setup_composer() {
2025-05-05 15:47:43 +02:00
local COMPOSER_BIN = "/usr/local/bin/composer"
export COMPOSER_ALLOW_SUPERUSER = 1
2025-11-05 05:19:35 -08:00
# Get currently installed version
local INSTALLED_VERSION = ""
if [[ -x " $COMPOSER_BIN " ]] ; then
INSTALLED_VERSION = $( " $COMPOSER_BIN " --version 2>/dev/null | awk '{print $3}' )
fi
# Scenario 1: Already installed - just self-update
if [[ -n " $INSTALLED_VERSION " ]] ; then
msg_info "Update Composer $INSTALLED_VERSION "
2026-02-27 13:59:02 +01:00
$STD " $COMPOSER_BIN " self-update --no-interaction || {
msg_warn "Composer self-update failed, continuing with current version"
}
2025-11-05 05:19:35 -08:00
local UPDATED_VERSION
UPDATED_VERSION = $( " $COMPOSER_BIN " --version 2>/dev/null | awk '{print $3}' )
cache_installed_version "composer" " $UPDATED_VERSION "
msg_ok "Update Composer $UPDATED_VERSION "
return 0
fi
# Scenario 2: Fresh install
2025-10-22 06:25:26 -07:00
msg_info "Setup Composer"
2025-08-04 14:12:18 +02:00
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 "
2026-03-02 08:44:59 +01:00
if ! curl_with_retry "https://getcomposer.org/installer" "/tmp/composer-setup.php" ; then
2025-10-22 06:25:26 -07:00
msg_error "Failed to download Composer installer"
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-05-05 15:47:43 +02:00
2025-10-22 06:25:26 -07:00
$STD php /tmp/composer-setup.php --install-dir= /usr/local/bin --filename= composer || {
msg_error "Failed to install Composer"
rm -f /tmp/composer-setup.php
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
rm -f /tmp/composer-setup.php
2025-05-05 15:47:43 +02:00
2025-08-04 14:12:18 +02:00
if [[ ! -x " $COMPOSER_BIN " ]] ; then
2025-10-22 06:25:26 -07:00
msg_error "Composer installation failed"
2026-03-26 16:07:38 +01:00
return 127
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-05-05 15:47:43 +02:00
chmod +x " $COMPOSER_BIN "
2026-02-27 13:59:02 +01:00
$STD " $COMPOSER_BIN " self-update --no-interaction || {
msg_warn "Composer self-update failed after fresh install"
}
2025-10-22 06:25:26 -07:00
local FINAL_VERSION
FINAL_VERSION = $( " $COMPOSER_BIN " --version 2>/dev/null | awk '{print $3}' )
cache_installed_version "composer" " $FINAL_VERSION "
msg_ok "Setup Composer"
2025-04-28 15:25:10 +02:00
}
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# 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() {
2025-10-22 06:25:26 -07:00
local TMP_DIR = $( mktemp -d)
2025-09-11 12:44:13 +02:00
local GITHUB_REPO = "FFmpeg/FFmpeg"
local VERSION = " ${ FFMPEG_VERSION :- latest } "
local TYPE = " ${ FFMPEG_TYPE :- full } "
local BIN_PATH = "/usr/local/bin/ffmpeg"
2025-11-05 05:19:35 -08:00
# Get currently installed version
local INSTALLED_VERSION = ""
if command -v ffmpeg & >/dev/null; then
INSTALLED_VERSION = $( ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}' )
fi
msg_info "Setup FFmpeg ${ VERSION } ( $TYPE )"
2025-09-11 12:44:13 +02:00
# Binary fallback mode
if [[ " $TYPE " == "binary" ]] ; then
2026-03-02 08:44:59 +01:00
if ! CURL_TIMEOUT = 300 curl_with_retry "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" " $TMP_DIR /ffmpeg.tar.xz" ; then
2025-10-22 06:25:26 -07:00
msg_error "Failed to download FFmpeg binary"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-10-22 06:25:26 -07:00
tar -xf " $TMP_DIR /ffmpeg.tar.xz" -C " $TMP_DIR " || {
msg_error "Failed to extract FFmpeg binary"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
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
2025-11-05 05:19:35 -08:00
local FINAL_VERSION = $( $BIN_PATH -version 2>/dev/null | head -n1 | awk '{print $3}' )
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2025-10-22 06:25:26 -07:00
cache_installed_version "ffmpeg" " $FINAL_VERSION "
ensure_usr_local_bin_persist
2025-11-05 05:19:35 -08:00
[[ -n " $INSTALLED_VERSION " ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION " || msg_ok "Setup FFmpeg $FINAL_VERSION "
2025-10-22 06:25:26 -07:00
return 0
2025-09-11 12:44:13 +02:00
fi
2025-10-22 06:25:26 -07:00
ensure_dependencies jq
2025-09-11 12:44:13 +02:00
# Auto-detect latest stable version if none specified
if [[ " $VERSION " == "latest" || -z " $VERSION " ]] ; then
2025-11-05 05:19:35 -08:00
local ffmpeg_tags
ffmpeg_tags = $( curl -fsSL --max-time 15 "https://api.github.com/repos/ ${ GITHUB_REPO } /tags" 2>/dev/null || echo "" )
if [[ -z " $ffmpeg_tags " ]] ; then
msg_warn "Could not fetch FFmpeg versions from GitHub, trying binary fallback"
VERSION = "" # Will trigger binary fallback below
else
VERSION = $( echo " $ffmpeg_tags " | jq -r '.[].name' 2>/dev/null |
grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' |
sort -V | tail -n1 || echo "" )
fi
2025-09-11 12:44:13 +02:00
fi
if [[ -z " $VERSION " ]] ; then
2025-11-05 05:19:35 -08:00
msg_info "Could not determine FFmpeg source version, using pre-built binary"
VERSION = "" # Will use binary fallback
2025-09-11 12:44:13 +02:00
fi
# Dependency selection
local DEPS =( build-essential yasm nasm pkg-config)
case " $TYPE " in
minimal)
DEPS +=( libx264-dev libvpx-dev libmp3lame-dev)
;;
medium)
DEPS +=( libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev)
;;
full)
DEPS +=(
libx264-dev libx265-dev libvpx-dev libmp3lame-dev
libfreetype6-dev libass-dev libopus-dev libvorbis-dev
libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev
libva-dev libdrm-dev
)
;;
*)
msg_error "Invalid FFMPEG_TYPE: $TYPE "
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 65
2025-09-11 12:44:13 +02:00
;;
esac
2025-10-22 06:25:26 -07:00
ensure_dependencies " ${ DEPS [@] } "
2025-11-05 05:19:35 -08:00
# Try to download source if VERSION is set
if [[ -n " $VERSION " ]] ; then
2026-03-02 08:44:59 +01:00
if ! CURL_TIMEOUT = 300 curl_with_retry "https://github.com/ ${ GITHUB_REPO } /archive/refs/tags/ ${ VERSION } .tar.gz" " $TMP_DIR /ffmpeg.tar.gz" ; then
2025-11-05 05:19:35 -08:00
msg_warn "Failed to download FFmpeg source ${ VERSION } , falling back to pre-built binary"
VERSION = ""
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
fi
# If no source download (either VERSION empty or download failed), use binary
if [[ -z " $VERSION " ]] ; then
msg_info "Setup FFmpeg from pre-built binary"
2026-03-02 08:44:59 +01:00
if ! CURL_TIMEOUT = 300 curl_with_retry "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" " $TMP_DIR /ffmpeg.tar.xz" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download FFmpeg pre-built binary"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
tar -xJf " $TMP_DIR /ffmpeg.tar.xz" -C " $TMP_DIR " || {
msg_error "Failed to extract FFmpeg binary archive"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-11-05 05:19:35 -08:00
}
if ! cp " $TMP_DIR /ffmpeg-" */ffmpeg /usr/local/bin/ffmpeg 2>/dev/null; then
msg_error "Failed to install FFmpeg binary"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-11-05 05:19:35 -08:00
fi
cache_installed_version "ffmpeg" "static"
2025-10-22 06:25:26 -07:00
rm -rf " $TMP_DIR "
2025-11-05 05:19:35 -08:00
msg_ok "Setup FFmpeg from pre-built binary"
return 0
fi
2025-10-22 06:25:26 -07:00
tar -xzf " $TMP_DIR /ffmpeg.tar.gz" -C " $TMP_DIR " || {
2025-11-05 05:19:35 -08:00
msg_error "Failed to extract FFmpeg source"
2025-10-22 06:25:26 -07:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
cd " $TMP_DIR /FFmpeg-" * || {
msg_error "Source extraction failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-09-11 12:44:13 +02:00
}
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
2025-10-22 06:25:26 -07:00
msg_error "FFmpeg configure args array is empty"
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 65
2025-09-11 12:44:13 +02:00
fi
2025-10-22 06:25:26 -07:00
$STD ./configure " ${ args [@] } " || {
msg_error "FFmpeg configure failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD make -j" $( nproc) " || {
msg_error "FFmpeg compilation failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD make install || {
msg_error "FFmpeg installation failed"
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-09-11 12:44:13 +02:00
}
echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf
2025-10-22 06:25:26 -07:00
$STD ldconfig
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
ldconfig -p 2>/dev/null | grep libavdevice >/dev/null || {
2025-09-11 12:44:13 +02:00
msg_error "libavdevice not registered with dynamic linker"
2025-10-22 06:25:26 -07:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-09-11 12:44:13 +02:00
}
if ! command -v ffmpeg & >/dev/null; then
msg_error "FFmpeg installation failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-09-11 12:44:13 +02:00
fi
local FINAL_VERSION
2025-11-05 05:19:35 -08:00
FINAL_VERSION = $( ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}' )
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2025-10-22 06:25:26 -07:00
cache_installed_version "ffmpeg" " $FINAL_VERSION "
2025-09-11 12:44:13 +02:00
ensure_usr_local_bin_persist
2025-11-05 05:19:35 -08:00
[[ -n " $INSTALLED_VERSION " ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION " || msg_ok "Setup FFmpeg $FINAL_VERSION "
2025-09-11 12:44:13 +02:00
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
# 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)
# ------------------------------------------------------------------------------
2025-06-18 12:07:08 +02:00
function setup_go() {
2025-05-05 15:47:43 +02:00
local ARCH
case " $( uname -m) " in
x86_64) ARCH = "amd64" ;;
aarch64) ARCH = "arm64" ;;
*)
msg_error "Unsupported architecture: $( uname -m) "
2026-03-26 16:07:38 +01:00
return 236
2025-05-05 15:47:43 +02:00
;;
esac
2025-11-05 05:19:35 -08:00
# Resolve "latest" version
local GO_VERSION = " ${ GO_VERSION :- latest } "
if [[ " $GO_VERSION " == "latest" ]] ; then
2026-03-02 08:44:59 +01:00
local go_version_tmp
go_version_tmp = $( curl_with_retry "https://go.dev/VERSION?m=text" "-" 2>/dev/null | head -n1 | sed 's/^go//' ) || true
if [[ -z " $go_version_tmp " ]] ; then
2025-05-05 15:47:43 +02:00
msg_error "Could not determine latest Go version"
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
GO_VERSION = " $go_version_tmp "
2025-05-05 15:47:43 +02:00
fi
local GO_BIN = "/usr/local/bin/go"
local GO_INSTALL_DIR = "/usr/local/go"
2025-11-05 05:19:35 -08:00
# Get currently installed version
local CURRENT_VERSION = ""
2025-05-05 15:47:43 +02:00
if [[ -x " $GO_BIN " ]] ; then
2025-11-05 05:19:35 -08:00
CURRENT_VERSION = $( " $GO_BIN " version 2>/dev/null | awk '{print $3}' | sed 's/go//' )
fi
# Scenario 1: Already at target version
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " == " $GO_VERSION " ]] ; then
cache_installed_version "go" " $GO_VERSION "
return 0
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Scenario 2: Different version or not installed
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " != " $GO_VERSION " ]] ; then
msg_info "Upgrade Go from $CURRENT_VERSION to $GO_VERSION "
remove_old_tool_version "go"
else
msg_info "Setup Go $GO_VERSION "
fi
2025-10-22 06:25:26 -07:00
2025-09-11 12:44:13 +02:00
local TARBALL = "go ${ GO_VERSION } .linux- ${ ARCH } .tar.gz"
local URL = "https://go.dev/dl/ ${ TARBALL } "
local TMP_TAR = $( mktemp)
2026-03-02 08:44:59 +01:00
if ! CURL_TIMEOUT = 300 curl_with_retry " $URL " " $TMP_TAR " ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download Go $GO_VERSION "
2025-10-22 06:25:26 -07:00
rm -f " $TMP_TAR "
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-09-11 12:44:13 +02:00
2025-10-22 06:25:26 -07:00
$STD tar -C /usr/local -xzf " $TMP_TAR " || {
msg_error "Failed to extract Go tarball"
rm -f " $TMP_TAR "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-11-05 05:19:35 -08:00
2025-09-11 12:44:13 +02:00
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 "
2025-10-22 06:25:26 -07:00
cache_installed_version "go" " $GO_VERSION "
ensure_usr_local_bin_persist
2025-09-11 12:44:13 +02:00
msg_ok "Setup Go $GO_VERSION "
}
# ------------------------------------------------------------------------------
# Installs or updates Ghostscript (gs) from source.
#
# Description:
# - Fetches latest release
# - Builds and installs system-wide
# ------------------------------------------------------------------------------
function setup_gs() {
2025-10-22 06:25:26 -07:00
local TMP_DIR = $( mktemp -d)
local CURRENT_VERSION = $( gs --version 2>/dev/null || echo "0" )
ensure_dependencies jq
2025-09-11 12:44:13 +02:00
2025-10-22 06:25:26 -07:00
local RELEASE_JSON
2025-11-05 05:19:35 -08:00
RELEASE_JSON = $( curl -fsSL --max-time 15 https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest 2>/dev/null || echo "" )
if [[ -z " $RELEASE_JSON " ]] ; then
msg_warn "Cannot fetch latest Ghostscript version from GitHub API"
# Try to get from current version
if command -v gs & >/dev/null; then
gs --version | head -n1
cache_installed_version "ghostscript" " $CURRENT_VERSION "
return 0
fi
msg_error "Cannot determine Ghostscript version and no existing installation found"
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
fi
2025-10-22 06:25:26 -07:00
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]\+' )
2025-09-11 12:44:13 +02:00
2025-10-22 06:25:26 -07:00
if [[ -z " $LATEST_VERSION " || -z " $LATEST_VERSION_DOTTED " ]] ; then
2025-11-05 05:19:35 -08:00
msg_warn "Could not determine latest Ghostscript version from GitHub - checking system"
# Fallback: try to use system version or return error
if [[ " $CURRENT_VERSION " == "0" ]] ; then
msg_error "Ghostscript not installed and cannot determine latest version"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
fi
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2025-11-05 05:19:35 -08:00
return 0
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
# Scenario 1: Already at latest version
if [[ -n " $LATEST_VERSION_DOTTED " ]] && dpkg --compare-versions " $CURRENT_VERSION " ge " $LATEST_VERSION_DOTTED " 2>/dev/null; then
2025-10-22 06:25:26 -07:00
cache_installed_version "ghostscript" " $LATEST_VERSION_DOTTED "
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2025-10-22 06:25:26 -07:00
return 0
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
# Scenario 2: New install or upgrade
if [[ " $CURRENT_VERSION " != "0" && " $CURRENT_VERSION " != " $LATEST_VERSION_DOTTED " ]] ; then
msg_info "Upgrade Ghostscript from $CURRENT_VERSION to $LATEST_VERSION_DOTTED "
else
msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED "
fi
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
if ! CURL_TIMEOUT = 180 curl_with_retry "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs ${ LATEST_VERSION } /ghostscript- ${ LATEST_VERSION_DOTTED } .tar.gz" " $TMP_DIR /ghostscript.tar.gz" ; then
2025-10-22 06:25:26 -07:00
msg_error "Failed to download Ghostscript"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-09-11 12:44:13 +02:00
if ! tar -xzf " $TMP_DIR /ghostscript.tar.gz" -C " $TMP_DIR " ; then
2025-10-22 06:25:26 -07:00
msg_error "Failed to extract Ghostscript archive"
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
# Verify directory exists before cd
if [[ ! -d " $TMP_DIR /ghostscript- ${ LATEST_VERSION_DOTTED } " ]] ; then
msg_error "Ghostscript source directory not found: $TMP_DIR /ghostscript- ${ LATEST_VERSION_DOTTED } "
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 252
2025-11-05 05:19:35 -08:00
fi
2025-09-11 12:44:13 +02:00
cd " $TMP_DIR /ghostscript- ${ LATEST_VERSION_DOTTED } " || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to enter Ghostscript source directory"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 252
2025-10-22 06:25:26 -07:00
}
ensure_dependencies build-essential libpng-dev zlib1g-dev
$STD ./configure || {
msg_error "Ghostscript configure failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD make -j" $( nproc) " || {
msg_error "Ghostscript compilation failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD make install || {
msg_error "Ghostscript installation failed"
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-09-11 12:44:13 +02:00
}
2025-10-22 06:25:26 -07:00
2025-09-11 12:44:13 +02:00
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 "
2025-10-22 06:25:26 -07:00
cache_installed_version "ghostscript" " $LATEST_VERSION_DOTTED "
ensure_usr_local_bin_persist
msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED "
}
2025-09-11 12:44:13 +02:00
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
# Sets up Hardware Acceleration on debian or ubuntu.
#
# Description:
2026-01-06 19:27:04 +01:00
# - Detects all available GPUs (Intel, AMD, NVIDIA)
# - Allows user to select which GPU(s) to configure (with 60s timeout)
# - Installs the correct libraries and packages for each GPU type
# - Supports: Debian 11/12/13, Ubuntu 22.04/24.04
# - Intel: Legacy (Gen 6-8), Modern (Gen 9+), Arc
# - AMD: Discrete GPUs, APUs, ROCm compute
# - NVIDIA: Version-matched drivers from CUDA repository
2025-10-22 06:25:26 -07:00
#
# Notes:
2026-01-06 19:27:04 +01:00
# - Some Intel packages are fetched from GitHub due to missing Debian packages
# - NVIDIA requires matching host driver version
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
function setup_hwaccel() {
2026-03-18 11:56:32 +01:00
local service_user = " ${ 1 :- } "
2026-01-05 18:03:02 +01:00
# Check if user explicitly disabled GPU in advanced settings
2026-01-06 20:15:22 +01:00
# ENABLE_GPU is exported from build.func
if [[ " ${ ENABLE_GPU :- no } " == "no" ]] ; then
2026-01-05 18:03:02 +01:00
return 0
fi
2025-12-22 23:27:37 +01:00
# Check if GPU passthrough is enabled (device nodes must exist)
if [[ ! -d /dev/dri && ! -e /dev/nvidia0 && ! -e /dev/kfd ]] ; then
msg_warn "No GPU passthrough detected (/dev/dri, /dev/nvidia*, /dev/kfd not found) - skipping hardware acceleration setup"
return 0
fi
2025-10-22 06:25:26 -07:00
msg_info "Setup Hardware Acceleration"
2026-01-06 19:27:04 +01:00
# Install pciutils if needed
2025-10-22 06:25:26 -07:00
if ! command -v lspci & >/dev/null; then
2026-01-06 19:27:04 +01:00
$STD apt -y update || {
msg_warn "Failed to update package list"
2026-01-05 18:03:02 +01:00
return 0
2026-01-06 19:27:04 +01:00
}
$STD apt -y install pciutils || {
msg_warn "Failed to install pciutils"
2026-01-05 18:03:02 +01:00
return 0
2026-01-06 19:27:04 +01:00
}
2025-10-22 06:25:26 -07:00
fi
2026-01-06 19:27:04 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# GPU Detection - Build list of all available GPUs with details
# ═══════════════════════════════════════════════════════════════════════════
local -a GPU_LIST =()
local -a GPU_TYPES =()
local -a GPU_NAMES =()
local gpu_count = 0
# Get all GPU entries from lspci
while IFS = read -r line; do
[[ -z " $line " ]] && continue
local pci_addr gpu_name gpu_type = ""
pci_addr = $( echo " $line " | awk '{print $1}' )
gpu_name = $( echo " $line " | sed 's/^[^ ]* [^:]*: //' )
# Determine GPU type
# Note: Use -w (word boundary) for ATI to avoid matching "CorporATIon"
if echo " $gpu_name " | grep -qi 'Intel' ; then
gpu_type = "INTEL"
# Subtype detection for Intel
# Order matters: Check Arc first, then Gen9+ (UHD/Iris/HD 5xx-6xx), then Legacy (HD 2xxx-5xxx)
# HD Graphics 530/630 = Gen 9 (Skylake/Kaby Lake) - 3 digits
# HD Graphics 4600/5500 = Gen 7-8 (Haswell/Broadwell) - 4 digits starting with 2-5
if echo " $gpu_name " | grep -qiE 'Arc|DG[12]' ; then
gpu_type = "INTEL_ARC"
elif echo " $gpu_name " | grep -qiE 'UHD|Iris|HD Graphics [5-6][0-9]{2}[^0-9]|HD Graphics [5-6][0-9]{2}$' ; then
# HD Graphics 5xx/6xx (3 digits) = Gen 9+ (Skylake onwards)
gpu_type = "INTEL_GEN9+"
elif echo " $gpu_name " | grep -qiE 'HD Graphics [2-5][0-9]{3}' ; then
# HD Graphics 2xxx-5xxx (4 digits) = Gen 6-8 Legacy
gpu_type = "INTEL_LEGACY"
fi
elif echo " $gpu_name " | grep -qiwE 'AMD|ATI|Radeon|Advanced Micro Devices' ; then
gpu_type = "AMD"
elif echo " $gpu_name " | grep -qi 'NVIDIA' ; then
gpu_type = "NVIDIA"
fi
2025-10-22 06:25:26 -07:00
2026-01-06 19:27:04 +01:00
if [[ -n " $gpu_type " ]] ; then
GPU_LIST +=( " $pci_addr " )
GPU_TYPES +=( " $gpu_type " )
GPU_NAMES +=( " $gpu_name " )
2026-01-06 20:32:15 +01:00
(( gpu_count++)) || true
2026-01-06 19:27:04 +01:00
fi
done < <( lspci 2>/dev/null | grep -Ei 'vga|3d|display' )
# Check for AMD APU via CPU vendor if no discrete GPU found
2025-10-22 06:25:26 -07:00
local cpu_vendor
2025-12-18 18:38:08 +01:00
cpu_vendor = $( lscpu 2>/dev/null | grep -i 'Vendor ID' | awk '{print $3}' 2>/dev/null || echo "" )
2025-10-22 06:25:26 -07:00
2026-01-06 19:27:04 +01:00
if [[ $gpu_count -eq 0 ]] ; then
if [[ " $cpu_vendor " == "AuthenticAMD" ]] ; then
GPU_LIST +=( "integrated" )
GPU_TYPES +=( "AMD_APU" )
GPU_NAMES +=( "AMD APU (Integrated Graphics)" )
2026-01-06 20:32:15 +01:00
(( gpu_count++)) || true
2026-01-06 19:27:04 +01:00
else
msg_warn "No GPU detected - skipping hardware acceleration setup"
return 0
fi
fi
# ═══════════════════════════════════════════════════════════════════════════
# GPU Selection - Let user choose which GPU(s) to configure
# ═══════════════════════════════════════════════════════════════════════════
local -a SELECTED_INDICES =()
2026-01-27 23:40:47 +02:00
local install_nvidia_drivers = "yes"
2026-01-06 19:27:04 +01:00
if [[ $gpu_count -eq 1 ]] ; then
# Single GPU - auto-select
SELECTED_INDICES =( 0)
msg_ok "Detected GPU: ${ GPU_NAMES [0] } ( ${ GPU_TYPES [0] } )"
else
# Multiple GPUs - show selection menu
echo ""
2026-01-18 12:43:58 +01:00
msg_custom "⚠" " ${ YW } " "Multiple GPUs detected:"
2026-01-06 19:27:04 +01:00
echo ""
for i in " ${ !GPU_LIST[@] } " ; do
local type_display = " ${ GPU_TYPES [ $i ] } "
case " ${ GPU_TYPES [ $i ] } " in
INTEL_ARC) type_display = "Intel Arc" ;;
INTEL_GEN9+) type_display = "Intel Gen9+" ;;
INTEL_LEGACY) type_display = "Intel Legacy" ;;
INTEL) type_display = "Intel" ;;
AMD) type_display = "AMD" ;;
AMD_APU) type_display = "AMD APU" ;;
NVIDIA) type_display = "NVIDIA" ;;
esac
printf " %d) [%s] %s\n" " $(( i + 1 )) " " $type_display " " ${ GPU_NAMES [ $i ] } "
done
printf " A) Configure ALL GPUs\n"
echo ""
# Read with 60 second timeout
local selection = ""
echo -n "Select GPU(s) to configure (1- ${ gpu_count } , A=all) [timeout 60s, default=all]: "
if read -r -t 60 selection; then
selection = " ${ selection ^^ } " # uppercase
else
echo ""
msg_info "Timeout - configuring all GPUs automatically"
selection = "A"
fi
# Parse selection
if [[ " $selection " == "A" || -z " $selection " ]] ; then
# Select all
for i in " ${ !GPU_LIST[@] } " ; do
SELECTED_INDICES +=( " $i " )
done
elif [[ " $selection " = ~ ^[ 0-9,] +$ ]] ; then
# Parse comma-separated numbers
IFS = ',' read -ra nums <<< " $selection "
for num in " ${ nums [@] } " ; do
num = $( echo " $num " | tr -d ' ' )
if [[ " $num " = ~ ^[ 0-9] +$ ]] && (( num >= 1 && num <= gpu_count)) ; then
SELECTED_INDICES +=( " $(( num - 1 )) " )
fi
done
else
# Invalid - default to all
msg_warn "Invalid selection - configuring all GPUs"
for i in " ${ !GPU_LIST[@] } " ; do
SELECTED_INDICES +=( " $i " )
done
fi
2025-10-22 06:25:26 -07:00
fi
2026-01-27 23:40:47 +02:00
# Ask whether to install NVIDIA drivers in the container
local nvidia_selected = "no"
for idx in " ${ SELECTED_INDICES [@] } " ; do
if [[ " ${ GPU_TYPES [ $idx ] } " == "NVIDIA" ]] ; then
nvidia_selected = "yes"
break
fi
done
if [[ " $nvidia_selected " == "yes" ]] ; then
if [[ -n " ${ INSTALL_NVIDIA_DRIVERS :- } " ]] ; then
install_nvidia_drivers = " ${ INSTALL_NVIDIA_DRIVERS } "
else
echo ""
msg_custom "🎮" " ${ GN } " "NVIDIA GPU passthrough detected"
local nvidia_reply = ""
read -r -t 60 -p " ${ TAB3 } ⚙️ Install NVIDIA driver libraries in the container? [Y/n] (auto-yes in 60s): " nvidia_reply || nvidia_reply = ""
case " ${ nvidia_reply ,, } " in
n | no) install_nvidia_drivers = "no" ;;
*) install_nvidia_drivers = "yes" ;;
esac
fi
fi
2026-01-06 19:27:04 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# OS Detection
# ═══════════════════════════════════════════════════════════════════════════
2025-12-17 14:37:31 +01:00
local os_id os_codename os_version
os_id = $( grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "debian" )
os_codename = $( grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "unknown" )
os_version = $( grep -oP '(?<=^VERSION_ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "" )
[[ -z " $os_id " ]] && os_id = "debian"
2025-10-22 06:25:26 -07:00
local in_ct = " ${ CTTYPE :- 0 } "
2026-01-06 19:27:04 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# Process Selected GPUs
# ═══════════════════════════════════════════════════════════════════════════
for idx in " ${ SELECTED_INDICES [@] } " ; do
local gpu_type = " ${ GPU_TYPES [ $idx ] } "
local gpu_name = " ${ GPU_NAMES [ $idx ] } "
msg_info "Configuring: ${ gpu_name } "
case " $gpu_type " in
# ─────────────────────────────────────────────────────────────────────────
# Intel Arc GPUs (DG1, DG2, Arc A-series)
# ─────────────────────────────────────────────────────────────────────────
INTEL_ARC)
_setup_intel_arc " $os_id " " $os_codename "
;;
# ─────────────────────────────────────────────────────────────────────────
# Intel Gen 9+ (Skylake 2015+: UHD, Iris, HD 6xx+)
# ─────────────────────────────────────────────────────────────────────────
INTEL_GEN9+ | INTEL)
_setup_intel_modern " $os_id " " $os_codename "
;;
# ─────────────────────────────────────────────────────────────────────────
# Intel Legacy (Gen 6-8: HD 2000-5999, Sandy Bridge to Broadwell)
# ─────────────────────────────────────────────────────────────────────────
INTEL_LEGACY)
_setup_intel_legacy " $os_id " " $os_codename "
;;
# ─────────────────────────────────────────────────────────────────────────
# AMD Discrete GPUs
# ─────────────────────────────────────────────────────────────────────────
AMD)
_setup_amd_gpu " $os_id " " $os_codename "
;;
# ─────────────────────────────────────────────────────────────────────────
# AMD APU (Integrated Graphics)
# ─────────────────────────────────────────────────────────────────────────
AMD_APU)
_setup_amd_apu " $os_id " " $os_codename "
;;
# ─────────────────────────────────────────────────────────────────────────
# NVIDIA GPUs
# ─────────────────────────────────────────────────────────────────────────
NVIDIA)
2026-01-27 23:40:47 +02:00
if [[ " $install_nvidia_drivers " == "yes" ]] ; then
_setup_nvidia_gpu " $os_id " " $os_codename " " $os_version "
else
msg_warn "Skipping NVIDIA driver installation (user opted to install manually)"
fi
2026-01-06 19:27:04 +01:00
;;
esac
done
# ═══════════════════════════════════════════════════════════════════════════
# Device Permissions
# ═══════════════════════════════════════════════════════════════════════════
2026-03-18 11:56:32 +01:00
_setup_gpu_permissions " $in_ct " " $service_user "
2026-01-06 19:27:04 +01:00
cache_installed_version "hwaccel" "1.0"
msg_ok "Setup Hardware Acceleration"
}
2026-04-17 21:58:31 +02:00
# ══════════════════════════════════════════════════════════════════════════════
# Resolve the IGC tag that the latest compute-runtime was built against.
# Must be called AFTER a fetch_and_deploy_gh_release for intel/compute-runtime
# so that /tmp/gh_rel.json contains the compute-runtime release metadata.
# Sets the variable named by $1 (default: igc_tag) to the discovered tag.
# ══════════════════════════════════════════════════════════════════════════════
_resolve_igc_tag() {
local -n _out_ref = " ${ 1 :- igc_tag } "
_out_ref = "latest"
if [[ -f /tmp/gh_rel.json ]] ; then
local _body _parsed
_body = $( jq -r '.body // empty' /tmp/gh_rel.json 2>/dev/null) || return 0
_parsed = $( grep -oP 'intel-graphics-compiler/releases/tag/\K[^\s\)]+' <<< " $_body " | head -1)
[[ -n " $_parsed " ]] && _out_ref = " $_parsed "
fi
}
2026-01-06 19:27:04 +01:00
# ══════════════════════════════════════════════════════════════════════════════
# Intel Arc GPU Setup
# ══════════════════════════════════════════════════════════════════════════════
_setup_intel_arc() {
local os_id = " $1 " os_codename = " $2 "
msg_info "Installing Intel Arc GPU drivers"
if [[ " $os_id " == "ubuntu" ]] ; then
# Ubuntu 22.04+ has Arc support in HWE kernel
$STD apt -y install \
intel-media-va-driver-non-free \
intel-opencl-icd \
2026-02-09 11:04:22 +01:00
libmfx-gen1.2 \
2026-01-06 19:27:04 +01:00
vainfo \
intel-gpu-tools 2>/dev/null || msg_warn "Some Intel Arc packages failed"
elif [[ " $os_id " == "debian" ]] ; then
# Add non-free repos
_add_debian_nonfree " $os_codename "
2026-02-23 13:08:39 +01:00
# For Trixie/Sid: Fetch latest drivers from GitHub (Debian repo packages may be too old or missing)
# For Bookworm: Use repo packages (GitHub latest requires libstdc++6 >= 13.1, unavailable on Bookworm)
if [[ " $os_codename " == "trixie" || " $os_codename " == "sid" ]] ; then
msg_info "Fetching Intel compute-runtime from GitHub for Arc support"
2026-01-06 19:27:04 +01:00
2026-04-17 21:58:31 +02:00
# Fetch a compute-runtime package first so /tmp/gh_rel.json is populated,
# then resolve the matching IGC tag from the release notes.
2026-02-23 13:08:39 +01:00
# libigdgmm - bundled in compute-runtime releases
fetch_and_deploy_gh_release "libigdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || true
2026-01-06 20:43:07 +01:00
2026-04-17 21:58:31 +02:00
local igc_tag
_resolve_igc_tag igc_tag
# Intel Graphics Compiler – pinned to the version compute-runtime expects
fetch_and_deploy_gh_release "intel-igc-core" "intel/intel-graphics-compiler" "binary" " $igc_tag " "" "intel-igc-core-2_*_amd64.deb" || true
fetch_and_deploy_gh_release "intel-igc-opencl" "intel/intel-graphics-compiler" "binary" " $igc_tag " "" "intel-igc-opencl-2_*_amd64.deb" || true
2026-01-06 20:43:07 +01:00
2026-02-23 13:08:39 +01:00
# Compute Runtime (depends on IGC and gmmlib)
fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || true
fetch_and_deploy_gh_release "intel-level-zero-gpu" "intel/compute-runtime" "binary" "latest" "" "libze-intel-gpu1_*_amd64.deb" || true
fi
2026-01-06 19:27:04 +01:00
$STD apt -y install \
intel-media-va-driver-non-free \
ocl-icd-libopencl1 \
libvpl2 \
2026-02-09 11:04:22 +01:00
libmfx-gen1.2 \
2026-01-06 19:27:04 +01:00
vainfo \
intel-gpu-tools 2>/dev/null || msg_warn "Some Intel Arc packages failed"
2026-02-23 13:08:39 +01:00
# Bookworm has compatible versions of these packages in repos
[[ " $os_codename " == "bookworm" ]] && $STD apt -y install intel-opencl-icd libigdgmm12 2>/dev/null || true
2026-01-06 19:27:04 +01:00
fi
msg_ok "Intel Arc GPU configured"
}
# ══════════════════════════════════════════════════════════════════════════════
# Intel Modern GPU Setup (Gen 9+)
# ══════════════════════════════════════════════════════════════════════════════
_setup_intel_modern() {
local os_id = " $1 " os_codename = " $2 "
msg_info "Installing Intel Gen 9+ GPU drivers"
if [[ " $os_id " == "ubuntu" ]] ; then
$STD apt -y install \
va-driver-all \
intel-media-va-driver \
ocl-icd-libopencl1 \
vainfo \
intel-gpu-tools 2>/dev/null || msg_warn "Some Intel packages failed"
# Try non-free driver for better codec support
$STD apt -y install intel-media-va-driver-non-free 2>/dev/null || true
$STD apt -y install intel-opencl-icd 2>/dev/null || true
$STD apt -y install libmfx-gen1.2 2>/dev/null || true
elif [[ " $os_id " == "debian" ]] ; then
_add_debian_nonfree " $os_codename "
2026-01-06 20:43:07 +01:00
# For Trixie/Sid: Fetch from GitHub (Debian packages too old or missing)
2026-01-06 19:27:04 +01:00
if [[ " $os_codename " == "trixie" || " $os_codename " == "sid" ]] ; then
2026-01-06 20:43:07 +01:00
msg_info "Fetching Intel compute-runtime from GitHub"
2026-04-17 21:58:31 +02:00
# Fetch a compute-runtime package first so /tmp/gh_rel.json is populated,
# then resolve the matching IGC tag from the release notes.
2026-01-06 20:43:07 +01:00
# libigdgmm first (bundled in compute-runtime releases)
fetch_and_deploy_gh_release "libigdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || true
2026-04-17 21:58:31 +02:00
local igc_tag
_resolve_igc_tag igc_tag
# Intel Graphics Compiler – pinned to the version compute-runtime expects
fetch_and_deploy_gh_release "intel-igc-core" "intel/intel-graphics-compiler" "binary" " $igc_tag " "" "intel-igc-core-2_*_amd64.deb" || true
fetch_and_deploy_gh_release "intel-igc-opencl" "intel/intel-graphics-compiler" "binary" " $igc_tag " "" "intel-igc-opencl-2_*_amd64.deb" || true
2026-01-06 20:43:07 +01:00
# Compute Runtime
2026-01-06 19:27:04 +01:00
fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || true
fi
$STD apt -y install \
intel-media-va-driver-non-free \
ocl-icd-libopencl1 \
vainfo \
libmfx-gen1.2 \
intel-gpu-tools 2>/dev/null || msg_warn "Some Intel packages failed"
2026-01-06 20:43:07 +01:00
# Bookworm has intel-opencl-icd in repos (compatible version)
[[ " $os_codename " == "bookworm" ]] && $STD apt -y install intel-opencl-icd libigdgmm12 2>/dev/null || true
2026-01-06 19:27:04 +01:00
fi
msg_ok "Intel Gen 9+ GPU configured"
}
# ══════════════════════════════════════════════════════════════════════════════
# Intel Legacy GPU Setup (Gen 6-8)
# ══════════════════════════════════════════════════════════════════════════════
_setup_intel_legacy() {
local os_id = " $1 " os_codename = " $2 "
msg_info "Installing Intel Legacy GPU drivers (Gen 6-8)"
# Legacy GPUs use i965 driver - stable repo packages only
$STD apt -y install \
va-driver-all \
i965-va-driver \
mesa-va-drivers \
ocl-icd-libopencl1 \
vainfo \
intel-gpu-tools 2>/dev/null || msg_warn "Some Intel legacy packages failed"
2026-01-10 11:41:44 +01:00
# beignet provides OpenCL for older Intel GPUs (Sandy Bridge to Broadwell)
# Note: beignet-opencl-icd was removed in Debian 12+ and Ubuntu 22.04+
# Check if package is available before attempting installation
if apt-cache show beignet-opencl-icd & >/dev/null; then
$STD apt -y install beignet-opencl-icd 2>/dev/null || msg_warn "beignet-opencl-icd installation failed (optional)"
else
msg_warn "beignet-opencl-icd not available - OpenCL support for legacy Intel GPU limited"
msg_warn "Note: Hardware video encoding/decoding (VA-API) still works without OpenCL"
fi
2026-01-06 19:27:04 +01:00
msg_ok "Intel Legacy GPU configured"
}
# ══════════════════════════════════════════════════════════════════════════════
# AMD Discrete GPU Setup
# ══════════════════════════════════════════════════════════════════════════════
_setup_amd_gpu() {
local os_id = " $1 " os_codename = " $2 "
msg_info "Installing AMD GPU drivers"
# Core Mesa drivers
$STD apt -y install \
mesa-va-drivers \
mesa-vdpau-drivers \
mesa-opencl-icd \
ocl-icd-libopencl1 \
libdrm-amdgpu1 \
vainfo \
clinfo 2>/dev/null || msg_warn "Some AMD packages failed"
# Firmware for AMD GPUs
if [[ " $os_id " == "debian" ]] ; then
_add_debian_nonfree_firmware " $os_codename "
$STD apt -y install firmware-amd-graphics 2>/dev/null || msg_warn "AMD firmware not available"
fi
# Ubuntu includes AMD firmware in linux-firmware by default
2026-03-09 09:37:26 +01:00
# ROCm compute stack (OpenCL + HIP)
_setup_rocm " $os_id " " $os_codename "
2026-01-06 19:27:04 +01:00
msg_ok "AMD GPU configured"
}
# ══════════════════════════════════════════════════════════════════════════════
# AMD APU Setup (Integrated Graphics)
# ══════════════════════════════════════════════════════════════════════════════
_setup_amd_apu() {
local os_id = " $1 " os_codename = " $2 "
msg_info "Installing AMD APU drivers"
$STD apt -y install \
mesa-va-drivers \
mesa-vdpau-drivers \
mesa-opencl-icd \
ocl-icd-libopencl1 \
vainfo 2>/dev/null || msg_warn "Some AMD APU packages failed"
if [[ " $os_id " == "debian" ]] ; then
_add_debian_nonfree_firmware " $os_codename "
$STD apt -y install firmware-amd-graphics 2>/dev/null || true
fi
msg_ok "AMD APU configured"
}
# ══════════════════════════════════════════════════════════════════════════════
2026-03-09 09:37:26 +01:00
# AMD ROCm Compute Setup
# Adds ROCm repository and installs the ROCm compute stack for AMD GPUs/APUs.
# Provides: OpenCL, HIP, rocm-smi, rocminfo
# Supported: Debian 12/13, Ubuntu 22.04/24.04 (amd64 only)
# ══════════════════════════════════════════════════════════════════════════════
_setup_rocm() {
local os_id = " $1 " os_codename = " $2 "
# Only amd64 is supported
if [[ " $( dpkg --print-architecture 2>/dev/null) " != "amd64" ]] ; then
msg_warn "ROCm is only available for amd64 — skipping"
return 0
fi
local ROCM_VERSION = "7.2"
local ROCM_REPO_CODENAME
# Map OS codename to ROCm repository codename (Ubuntu-based repos)
case " ${ os_id } - ${ os_codename } " in
debian-bookworm) ROCM_REPO_CODENAME = "jammy" ;;
debian-trixie | debian-sid) ROCM_REPO_CODENAME = "noble" ;;
ubuntu-jammy) ROCM_REPO_CODENAME = "jammy" ;;
ubuntu-noble) ROCM_REPO_CODENAME = "noble" ;;
*)
msg_warn "ROCm not supported on ${ os_id } ${ os_codename } — skipping"
return 0
;;
esac
msg_info "Installing ROCm ${ ROCM_VERSION } compute stack"
2026-03-09 09:57:28 +01:00
# ROCm main repository (userspace compute libs)
2026-03-09 09:37:26 +01:00
setup_deb822_repo \
"rocm" \
"https://repo.radeon.com/rocm/rocm.gpg.key" \
"https://repo.radeon.com/rocm/apt/ ${ ROCM_VERSION } " \
" ${ ROCM_REPO_CODENAME } " \
"main" \
"amd64" || {
msg_warn "Failed to add ROCm repository — skipping ROCm"
return 0
}
2026-03-17 08:31:42 +01:00
# Note: The amdgpu/latest/ubuntu repo (kernel driver packages) is intentionally
# omitted — kernel drivers are managed by the Proxmox host, not the LXC container.
# Only the ROCm userspace compute stack is needed inside the container.
2026-03-09 09:37:26 +01:00
# Pin ROCm packages to prefer radeon repo
cat <<EOF >/etc/apt/preferences.d/rocm-pin-600
Package: *
Pin: release o=repo.radeon.com
Pin-Priority: 600
EOF
2026-03-17 08:31:42 +01:00
# apt update with retry — repo.radeon.com CDN can be mid-sync (transient size mismatches).
# Run with ERR trap disabled so a transient failure does not abort the entire install.
local _apt_ok = 0
for _attempt in 1 2 3; do
if (
set +e
apt-get update -qq 2>& 1
exit $?
) 2>/dev/null; then
_apt_ok = 1
break
fi
msg_warn "apt update failed (attempt ${ _attempt } /3) — AMD repo may be temporarily unavailable, retrying in 30s…"
sleep 30
done
if [[ $_apt_ok -eq 0 ]] ; then
msg_warn "apt update still failing after 3 attempts — skipping ROCm install"
return 0
fi
2026-03-09 11:14:26 +01:00
# Install only runtime packages — full 'rocm' meta-package includes 15GB+ dev tools
$STD apt install -y rocm-opencl-runtime rocm-hip-runtime rocm-smi-lib 2>/dev/null || {
msg_warn "ROCm runtime install failed — trying minimal set"
2026-03-09 09:37:26 +01:00
$STD apt install -y rocm-opencl-runtime rocm-smi-lib 2>/dev/null || msg_warn "ROCm minimal install also failed"
}
# Group membership for GPU access
usermod -aG render,video root 2>/dev/null || true
# Environment (PATH + LD_LIBRARY_PATH)
if [[ -d /opt/rocm ]] ; then
cat <<'ENVEOF' >/etc/profile.d/rocm.sh
2026-03-12 09:07:33 +01:00
export PATH="$PATH:/opt/rocm/bin"
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}/opt/rocm/lib"
2026-03-09 09:37:26 +01:00
ENVEOF
chmod +x /etc/profile.d/rocm.sh
# Also make available for current session / systemd services
echo "/opt/rocm/lib" >/etc/ld.so.conf.d/rocm.conf
ldconfig 2>/dev/null || true
fi
if [[ -x /opt/rocm/bin/rocminfo ]] ; then
msg_ok "ROCm ${ ROCM_VERSION } installed"
else
msg_warn "ROCm installed but rocminfo not found — GPU may not be available in container"
fi
}
# ══════════════════════════════════════════════════════════════════════════════
2026-01-06 19:27:04 +01:00
# NVIDIA GPU Setup
# ══════════════════════════════════════════════════════════════════════════════
_setup_nvidia_gpu() {
local os_id = " $1 " os_codename = " $2 " os_version = " $3 "
msg_info "Installing NVIDIA GPU drivers"
2026-01-14 15:51:00 +01:00
# Prevent interactive dialogs (e.g., "Mismatching nvidia kernel module" whiptail)
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
2026-01-06 19:27:04 +01:00
# Detect host driver version (passed through via /proc)
2026-01-14 14:39:53 +01:00
# Format varies by driver type:
# Proprietary: "NVRM version: NVIDIA UNIX x86_64 Kernel Module 550.54.14 Thu..."
# Open: "NVRM version: NVIDIA UNIX Open Kernel Module for x86_64 590.48.01 Release..."
2026-03-12 09:07:49 +01:00
# Use regex to extract version number (###.##.## or ###.## pattern)
2026-01-06 19:27:04 +01:00
local nvidia_host_version = ""
if [[ -f /proc/driver/nvidia/version ]] ; then
2026-03-25 13:06:21 +01:00
nvidia_host_version = $( grep -oP '\d{3,}\.\d+(\.\d+)?' /proc/driver/nvidia/version 2>/dev/null | head -1 || true )
2026-01-06 19:27:04 +01:00
fi
if [[ -z " $nvidia_host_version " ]] ; then
msg_warn "NVIDIA host driver version not found in /proc/driver/nvidia/version"
msg_warn "Ensure NVIDIA drivers are installed on host and GPU passthrough is enabled"
2026-01-14 15:51:00 +01:00
$STD apt-get -y install va-driver-all vainfo 2>/dev/null || true
2026-01-06 19:27:04 +01:00
return 0
fi
msg_info "Host NVIDIA driver version: ${ nvidia_host_version } "
if [[ " $os_id " == "debian" ]] ; then
# Enable non-free components
if [[ -f /etc/apt/sources.list.d/debian.sources ]] ; then
if ! grep -q "non-free" /etc/apt/sources.list.d/debian.sources 2>/dev/null; then
sed -i -E 's/Components: (.*)$/Components: \1 contrib non-free non-free-firmware/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true
fi
fi
2026-01-14 15:51:00 +01:00
$STD apt-get -y update 2>/dev/null || msg_warn "apt update failed - continuing anyway"
2026-01-06 19:27:04 +01:00
2026-01-14 14:39:53 +01:00
# For Debian 13 Trixie/Sid: Use Debian's own nvidia packages first (better compatibility)
# NVIDIA's CUDA repo targets Debian 12 and may not have amd64 packages for Trixie
if [[ " $os_codename " == "trixie" || " $os_codename " == "sid" ]] ; then
msg_info "Debian ${ os_codename } : Using Debian's NVIDIA packages"
2026-01-06 19:27:04 +01:00
2026-01-18 12:43:58 +01:00
# Extract major version for flexible matching (580.126.09 -> 580)
local nvidia_major_version = " ${ nvidia_host_version %%.* } "
# Check what versions are actually available
local available_version = ""
available_version = $( apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | grep -E "^ ${ nvidia_major_version } \." | head -1 || true )
if [[ -n " $available_version " ]] ; then
msg_info "Found available NVIDIA version: ${ available_version } "
local nvidia_pkgs = "libcuda1= ${ available_version } libnvcuvid1= ${ available_version } libnvidia-encode1= ${ available_version } libnvidia-ml1= ${ available_version } "
if $STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
msg_ok "Installed NVIDIA libraries ( ${ available_version } )"
else
msg_warn "Failed to install NVIDIA ${ available_version } - trying unversioned"
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null || true
fi
2026-01-06 19:27:04 +01:00
else
2026-01-18 12:43:58 +01:00
# No matching major version - try latest available or unversioned
msg_warn "No NVIDIA packages for version ${ nvidia_major_version } .x found in repos"
available_version = $( apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | head -1 || true )
if [[ -n " $available_version " ]] ; then
msg_info "Trying latest available: ${ available_version } (may cause version mismatch)"
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends \
libcuda1 = " ${ available_version } " libnvcuvid1 = " ${ available_version } " \
libnvidia-encode1= " ${ available_version } " libnvidia-ml1= " ${ available_version } " 2>/dev/null ||
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends \
libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null ||
msg_warn "NVIDIA library installation failed - GPU compute may not work"
2026-01-14 14:39:53 +01:00
else
2026-01-18 12:43:58 +01:00
msg_warn "No NVIDIA packages available in Debian repos - GPU support disabled"
2026-01-14 14:39:53 +01:00
fi
2026-01-06 19:27:04 +01:00
fi
2026-01-14 15:51:00 +01:00
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends nvidia-smi 2>/dev/null || true
2026-01-14 14:39:53 +01:00
else
# Debian 11/12: Use NVIDIA CUDA repository for version matching
local cuda_repo = "debian12"
case " $os_codename " in
bullseye) cuda_repo = "debian11" ;;
bookworm) cuda_repo = "debian12" ;;
esac
2026-01-06 19:27:04 +01:00
2026-01-14 14:39:53 +01:00
# Add NVIDIA CUDA repository
if [[ ! -f /usr/share/keyrings/cuda-archive-keyring.gpg ]] ; then
msg_info "Adding NVIDIA CUDA repository ( ${ cuda_repo } )"
local cuda_keyring
cuda_keyring = " $( mktemp) "
if curl -fsSL -o " $cuda_keyring " "https://developer.download.nvidia.com/compute/cuda/repos/ ${ cuda_repo } /x86_64/cuda-keyring_1.1-1_all.deb" 2>/dev/null; then
$STD dpkg -i " $cuda_keyring " 2>/dev/null || true
else
msg_warn "Failed to download NVIDIA CUDA keyring"
fi
rm -f " $cuda_keyring "
fi
# Pin NVIDIA repo for version matching
cat <<'NVIDIA_PIN' >/etc/apt/preferences.d/nvidia-cuda-pin
2026-01-06 19:27:04 +01:00
Package: *
Pin: origin developer.download.nvidia.com
Pin-Priority: 1001
NVIDIA_PIN
2026-01-14 15:51:00 +01:00
$STD apt-get -y update 2>/dev/null || msg_warn "apt update failed - continuing anyway"
2026-01-06 19:27:04 +01:00
2026-01-18 12:43:58 +01:00
# Extract major version for flexible matching (580.126.09 -> 580)
local nvidia_major_version = " ${ nvidia_host_version %%.* } "
2026-01-06 19:27:04 +01:00
2026-01-18 12:43:58 +01:00
# Check what versions are actually available in CUDA repo
local available_version = ""
available_version = $( apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | grep -E "^ ${ nvidia_major_version } \." | head -1 || true )
if [[ -n " $available_version " ]] ; then
msg_info "Installing NVIDIA libraries (version ${ available_version } )"
local nvidia_pkgs = "libcuda1= ${ available_version } libnvcuvid1= ${ available_version } libnvidia-encode1= ${ available_version } libnvidia-ml1= ${ available_version } "
if $STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
msg_ok "Installed version-matched NVIDIA libraries"
else
msg_warn "Version-pinned install failed - trying unpinned"
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null ||
msg_warn "NVIDIA library installation failed"
fi
2026-01-06 19:27:04 +01:00
else
2026-01-18 12:43:58 +01:00
msg_warn "No NVIDIA packages for version ${ nvidia_major_version } .x in CUDA repo (host: ${ nvidia_host_version } )"
# Try latest available version
available_version = $( apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | head -1 || true )
if [[ -n " $available_version " ]] ; then
msg_info "Trying latest available: ${ available_version } (version mismatch warning)"
if $STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends \
libcuda1 = " ${ available_version } " libnvcuvid1 = " ${ available_version } " \
libnvidia-encode1= " ${ available_version } " libnvidia-ml1= " ${ available_version } " 2>/dev/null; then
msg_ok "Installed NVIDIA libraries ( ${ available_version } ) - version differs from host"
else
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null ||
msg_warn "NVIDIA library installation failed"
fi
2026-01-14 14:39:53 +01:00
else
2026-01-18 12:43:58 +01:00
msg_warn "No NVIDIA packages available in CUDA repo - GPU support disabled"
2026-01-14 14:39:53 +01:00
fi
2026-01-05 18:03:02 +01:00
fi
2026-01-14 15:51:00 +01:00
$STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends nvidia-smi 2>/dev/null || true
2026-01-14 14:39:53 +01:00
fi
2026-01-06 19:27:04 +01:00
elif [[ " $os_id " == "ubuntu" ]] ; then
# Ubuntu versioning
local ubuntu_cuda_repo = ""
case " $os_version " in
22.04) ubuntu_cuda_repo = "ubuntu2204" ;;
24.04) ubuntu_cuda_repo = "ubuntu2404" ;;
*) ubuntu_cuda_repo = "ubuntu2204" ;; # Fallback
esac
# Add NVIDIA CUDA repository for Ubuntu
if [[ ! -f /usr/share/keyrings/cuda-archive-keyring.gpg ]] ; then
msg_info "Adding NVIDIA CUDA repository ( ${ ubuntu_cuda_repo } )"
local cuda_keyring
cuda_keyring = " $( mktemp) "
if curl -fsSL -o " $cuda_keyring " "https://developer.download.nvidia.com/compute/cuda/repos/ ${ ubuntu_cuda_repo } /x86_64/cuda-keyring_1.1-1_all.deb" 2>/dev/null; then
$STD dpkg -i " $cuda_keyring " 2>/dev/null || true
else
msg_warn "Failed to download NVIDIA CUDA keyring"
fi
rm -f " $cuda_keyring "
fi
2026-01-14 15:51:00 +01:00
$STD apt-get -y update 2>/dev/null || msg_warn "apt update failed - continuing anyway"
2026-01-06 19:27:04 +01:00
2026-01-18 12:43:58 +01:00
# Extract major version for flexible matching
local nvidia_major_version = " ${ nvidia_host_version %%.* } "
# Check what versions are available
local available_version = ""
available_version = $( apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | grep -E "^ ${ nvidia_major_version } \." | head -1 || true )
if [[ -n " $available_version " ]] ; then
msg_info "Installing NVIDIA libraries (version ${ available_version } )"
local nvidia_pkgs = "libcuda1= ${ available_version } libnvcuvid1= ${ available_version } libnvidia-encode1= ${ available_version } libnvidia-ml1= ${ available_version } "
if $STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
msg_ok "Installed version-matched NVIDIA libraries"
else
2026-01-24 23:23:10 +01:00
# Fallback to Ubuntu repo packages with versioned nvidia-utils
msg_warn "CUDA repo install failed - trying Ubuntu native packages (nvidia-utils- ${ nvidia_major_version } )"
if $STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends \
libnvidia-decode-${ nvidia_major_version } libnvidia-encode-${ nvidia_major_version } nvidia-utils-${ nvidia_major_version } 2>/dev/null; then
msg_ok "Installed Ubuntu NVIDIA packages ( ${ nvidia_major_version } )"
else
msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils- ${ nvidia_major_version } "
fi
2026-01-18 12:43:58 +01:00
fi
2026-01-06 19:27:04 +01:00
else
2026-01-18 12:43:58 +01:00
msg_warn "No NVIDIA packages for version ${ nvidia_major_version } .x in CUDA repo"
2026-01-24 23:23:10 +01:00
# Fallback to Ubuntu repo packages with versioned nvidia-utils
msg_info "Trying Ubuntu native packages (nvidia-utils- ${ nvidia_major_version } )"
if $STD apt-get -y -o Dpkg::Options::= "--force-confold" install --no-install-recommends \
libnvidia-decode-${ nvidia_major_version } libnvidia-encode-${ nvidia_major_version } nvidia-utils-${ nvidia_major_version } 2>/dev/null; then
msg_ok "Installed Ubuntu NVIDIA packages ( ${ nvidia_major_version } )"
else
msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils- ${ nvidia_major_version } "
fi
2026-01-06 19:27:04 +01:00
fi
fi
# VA-API for hybrid setups (Intel + NVIDIA)
2026-01-14 15:51:00 +01:00
$STD apt-get -y install va-driver-all vainfo 2>/dev/null || true
2026-01-06 19:27:04 +01:00
2026-02-26 22:27:46 +01:00
# Fix GLX alternatives: nvidia-alternative diverts mesa libs but in LXC
# containers the nvidia GLX libs are typically missing, leaving libGL.so.1
# pointing nowhere. Fall back to mesa if nvidia GLX dir is empty/missing.
if command -v update-glx & >/dev/null; then
local nvidia_glx_dir = "/usr/lib/nvidia"
if [[ ! -f " ${ nvidia_glx_dir } /libGL.so.1" ]] && [[ -d /usr/lib/mesa-diverted ]] ; then
msg_info "NVIDIA GLX libs missing in container - falling back to mesa"
$STD update-glx --set glx /usr/lib/mesa-diverted 2>/dev/null || true
ldconfig 2>/dev/null || true
fi
fi
2026-01-06 19:27:04 +01:00
msg_ok "NVIDIA GPU configured"
}
# ══════════════════════════════════════════════════════════════════════════════
# Helper: Add Debian non-free repositories
# ══════════════════════════════════════════════════════════════════════════════
_add_debian_nonfree() {
local os_codename = " $1 "
2026-01-05 18:03:02 +01:00
2026-01-06 19:27:04 +01:00
[[ -f /etc/apt/sources.list.d/non-free.sources ]] && return 0
2026-01-05 18:03:02 +01:00
2026-01-06 19:27:04 +01:00
case " $os_codename " in
bullseye)
cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources
Types: deb
URIs: http://deb.debian.org/debian
Suites: bullseye bullseye-updates
Components: non-free
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2026-01-06 19:27:04 +01:00
EOF
;;
bookworm)
cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources
2025-12-17 14:37:31 +01:00
Types: deb
URIs: http://deb.debian.org/debian
Suites: bookworm bookworm-updates
Components: non-free non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-12-17 14:37:31 +01:00
EOF
2026-01-06 19:27:04 +01:00
;;
trixie | sid)
cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources
2025-12-17 14:37:31 +01:00
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: non-free non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-10-22 06:25:26 -07:00
2025-12-17 14:37:31 +01:00
Types: deb
URIs: http://deb.debian.org/debian-security
Suites: trixie-security
Components: non-free non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-12-17 14:37:31 +01:00
EOF
2025-10-22 06:25:26 -07:00
;;
2026-01-06 19:27:04 +01:00
esac
$STD apt -y update
}
2025-12-17 14:37:31 +01:00
2026-01-06 19:27:04 +01:00
# ══════════════════════════════════════════════════════════════════════════════
# Helper: Add Debian non-free-firmware repository
# ══════════════════════════════════════════════════════════════════════════════
_add_debian_nonfree_firmware() {
local os_codename = " $1 "
[[ -f /etc/apt/sources.list.d/non-free-firmware.sources ]] && return 0
case " $os_codename " in
bullseye)
# Debian 11 uses 'non-free' component (no separate non-free-firmware)
cat <<'EOF' >/etc/apt/sources.list.d/non-free-firmware.sources
Types: deb
URIs: http://deb.debian.org/debian
Suites: bullseye bullseye-updates
Components: non-free
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-10-22 06:25:26 -07:00
2026-01-06 19:27:04 +01:00
Types: deb
URIs: http://deb.debian.org/debian-security
Suites: bullseye-security
Components: non-free
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2026-01-06 19:27:04 +01:00
EOF
;;
bookworm)
cat <<'EOF' >/etc/apt/sources.list.d/non-free-firmware.sources
2025-12-21 23:11:18 +01:00
Types: deb
URIs: http://deb.debian.org/debian
Suites: bookworm bookworm-updates
Components: non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-12-21 23:11:18 +01:00
Types: deb
URIs: http://deb.debian.org/debian-security
Suites: bookworm-security
Components: non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-12-21 23:11:18 +01:00
EOF
2026-01-06 19:27:04 +01:00
;;
trixie | sid)
cat <<'EOF' >/etc/apt/sources.list.d/non-free-firmware.sources
2025-12-21 23:11:18 +01:00
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-12-21 23:11:18 +01:00
Types: deb
URIs: http://deb.debian.org/debian-security
Suites: trixie-security
Components: non-free-firmware
2026-02-23 12:59:42 +01:00
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
2025-12-21 23:11:18 +01:00
EOF
2025-10-22 06:25:26 -07:00
;;
esac
2026-01-06 19:27:04 +01:00
$STD apt -y update
}
# ══════════════════════════════════════════════════════════════════════════════
# Helper: Setup GPU device permissions
# ══════════════════════════════════════════════════════════════════════════════
_setup_gpu_permissions() {
local in_ct = " $1 "
2026-03-18 11:56:32 +01:00
local service_user = " ${ 2 :- } "
2025-10-22 06:25:26 -07:00
2026-01-06 19:27:04 +01:00
# /dev/dri permissions (Intel/AMD)
2025-12-17 14:37:31 +01:00
if [[ " $in_ct " == "0" && -d /dev/dri ]] ; then
2026-01-05 18:03:02 +01:00
if ls /dev/dri/card* /dev/dri/renderD* & >/dev/null; then
chgrp video /dev/dri 2>/dev/null || true
chmod 755 /dev/dri 2>/dev/null || true
chmod 660 /dev/dri/* 2>/dev/null || true
$STD adduser " $( id -u -n) " video 2>/dev/null || true
$STD adduser " $( id -u -n) " render 2>/dev/null || true
2026-01-06 19:27:04 +01:00
# Sync GID with host
2026-01-05 18:03:02 +01:00
local host_video_gid host_render_gid
host_video_gid = $( getent group video | cut -d: -f3)
host_render_gid = $( getent group render | cut -d: -f3)
2026-01-06 19:27:04 +01:00
if [[ -n " $host_video_gid " ]] ; then
2026-01-05 18:03:02 +01:00
sed -i "s/^video:x:[0-9]*:/video:x: $host_video_gid :/" /etc/group 2>/dev/null || true
2026-01-06 19:27:04 +01:00
fi
if [[ -n " $host_render_gid " ]] ; then
2026-01-05 18:03:02 +01:00
sed -i "s/^render:x:[0-9]*:/render:x: $host_render_gid :/" /etc/group 2>/dev/null || true
fi
2025-12-17 14:37:31 +01:00
2026-01-06 19:27:04 +01:00
# Verify VA-API
2026-01-05 18:03:02 +01:00
if command -v vainfo & >/dev/null; then
if vainfo & >/dev/null; then
2026-01-06 19:27:04 +01:00
msg_info "VA-API verified and working"
2026-01-05 18:03:02 +01:00
else
2026-01-06 19:27:04 +01:00
msg_warn "vainfo test failed - check GPU passthrough"
2026-01-05 18:03:02 +01:00
fi
fi
2025-12-17 14:37:31 +01:00
fi
2025-09-11 12:44:13 +02:00
fi
2025-10-22 06:25:26 -07:00
2026-01-06 19:27:04 +01:00
# /dev/nvidia* permissions (NVIDIA)
if ls /dev/nvidia* & >/dev/null 2>& 1; then
msg_info "Configuring NVIDIA device permissions"
for nvidia_dev in /dev/nvidia*; do
[[ -e " $nvidia_dev " ]] && {
chgrp video " $nvidia_dev " 2>/dev/null || true
2026-03-23 22:22:23 +01:00
chmod 660 " $nvidia_dev " 2>/dev/null || true
2026-01-06 19:27:04 +01:00
}
done
if [[ -d /dev/nvidia-caps ]] ; then
chmod 755 /dev/nvidia-caps 2>/dev/null || true
for caps_dev in /dev/nvidia-caps/*; do
[[ -e " $caps_dev " ]] && {
chgrp video " $caps_dev " 2>/dev/null || true
2026-03-23 22:22:23 +01:00
chmod 660 " $caps_dev " 2>/dev/null || true
2026-01-06 19:27:04 +01:00
}
done
fi
# Verify nvidia-smi
if command -v nvidia-smi & >/dev/null; then
if nvidia-smi & >/dev/null; then
msg_info "nvidia-smi verified and working"
else
msg_warn "nvidia-smi test failed - check driver version match"
fi
fi
fi
# /dev/kfd permissions (AMD ROCm)
if [[ -e /dev/kfd ]] ; then
2026-03-23 22:22:23 +01:00
chgrp render /dev/kfd 2>/dev/null || true
chmod 660 /dev/kfd 2>/dev/null || true
2026-01-06 19:27:04 +01:00
msg_info "AMD ROCm compute device configured"
fi
2026-03-18 11:56:32 +01:00
# Add service user to render and video groups for GPU hardware acceleration
if [[ -n " $service_user " ]] ; then
2026-03-19 06:55:56 +01:00
usermod -aG render " $service_user " 2>/dev/null || true
usermod -aG video " $service_user " 2>/dev/null || true
2026-03-18 11:56:32 +01:00
fi
2025-09-11 12:44:13 +02:00
}
# ------------------------------------------------------------------------------
# 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() {
2025-10-22 06:25:26 -07:00
local TMP_DIR = $( mktemp -d)
2025-09-11 12:44:13 +02:00
local BINARY_PATH = "/usr/local/bin/magick"
2025-11-05 05:19:35 -08:00
# Get currently installed version
local INSTALLED_VERSION = ""
2025-09-11 12:44:13 +02:00
if command -v magick & >/dev/null; then
2025-11-05 05:19:35 -08:00
INSTALLED_VERSION = $( magick -version | awk '/^Version/ {print $3}' )
2025-09-11 12:44:13 +02:00
fi
2025-10-22 06:25:26 -07:00
msg_info "Setup ImageMagick"
ensure_dependencies \
2025-09-11 12:44:13 +02:00
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
2025-04-28 15:25:10 +02:00
2026-03-02 08:44:59 +01:00
if ! CURL_TIMEOUT = 180 curl_with_retry "https://imagemagick.org/archive/ImageMagick.tar.gz" " $TMP_DIR /ImageMagick.tar.gz" ; then
2025-10-22 06:25:26 -07:00
msg_error "Failed to download ImageMagick"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-10-22 06:25:26 -07:00
tar -xzf " $TMP_DIR /ImageMagick.tar.gz" -C " $TMP_DIR " || {
msg_error "Failed to extract ImageMagick"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
cd " $TMP_DIR " /ImageMagick-* || {
msg_error "Source extraction failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-05-05 15:47:43 +02:00
}
2025-04-28 15:25:10 +02:00
2025-10-22 06:25:26 -07:00
$STD ./configure --disable-static || {
msg_error "ImageMagick configure failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD make -j" $( nproc) " || {
msg_error "ImageMagick compilation failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
$STD make install || {
msg_error "ImageMagick installation failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
$STD ldconfig /usr/local/lib
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
if [[ ! -x " $BINARY_PATH " ]] ; then
msg_error "ImageMagick installation failed"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
local FINAL_VERSION
FINAL_VERSION = $( " $BINARY_PATH " -version | awk '/^Version/ {print $3}' )
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2025-11-05 05:19:35 -08:00
cache_installed_version "imagemagick" " $FINAL_VERSION "
2025-09-11 12:44:13 +02:00
ensure_usr_local_bin_persist
2025-11-05 05:19:35 -08:00
if [[ -n " $INSTALLED_VERSION " ]] ; then
msg_ok "Upgrade ImageMagick $INSTALLED_VERSION → $FINAL_VERSION "
else
msg_ok "Setup ImageMagick $FINAL_VERSION "
fi
2025-04-28 15:25:10 +02:00
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
# 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)
# ------------------------------------------------------------------------------
2025-06-18 12:07:08 +02:00
function setup_java() {
2025-05-05 15:47:43 +02:00
local JAVA_VERSION = " ${ JAVA_VERSION :- 21 } "
2025-10-22 06:25:26 -07:00
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID = $( awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"' )
2025-05-05 15:47:43 +02:00
DISTRO_CODENAME = $( awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)
local DESIRED_PACKAGE = "temurin- ${ JAVA_VERSION } -jdk"
2025-11-05 05:19:35 -08:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "adoptium" || {
msg_error "Failed to prepare Adoptium repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Add repo if needed
2025-10-22 06:25:26 -07:00
if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]] ; then
local SUITE
SUITE = $( get_fallback_suite " $DISTRO_ID " " $DISTRO_CODENAME " "https://packages.adoptium.net/artifactory/deb" )
setup_deb822_repo \
"adoptium" \
"https://packages.adoptium.net/artifactory/api/gpg/key/public" \
"https://packages.adoptium.net/artifactory/deb" \
" $SUITE " \
2025-11-07 02:49:16 -08:00
"main"
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Get currently installed version
2025-05-05 15:47:43 +02:00
local INSTALLED_VERSION = ""
2026-03-19 21:55:58 +01:00
INSTALLED_VERSION = $( dpkg-query -W -f '${Package}\n' 2>/dev/null | grep -oP '^temurin-\K[0-9]+(?=-jdk$)' | head -n1 || echo "" )
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Scenario 1: Already at correct version
if [[ " $INSTALLED_VERSION " == " $JAVA_VERSION " ]] ; then
msg_info "Update Temurin JDK $JAVA_VERSION "
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
upgrade_packages_with_retry " $DESIRED_PACKAGE " || {
msg_error "Failed to update Temurin JDK"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 06:25:26 -07:00
}
cache_installed_version "temurin-jdk" " $JAVA_VERSION "
2025-11-05 05:19:35 -08:00
msg_ok "Update Temurin JDK $JAVA_VERSION "
return 0
fi
# Scenario 2: Different version - remove old and install new
if [[ -n " $INSTALLED_VERSION " ]] ; then
msg_info "Upgrade Temurin JDK from $INSTALLED_VERSION to $JAVA_VERSION "
$STD apt purge -y "temurin- ${ INSTALLED_VERSION } -jdk" || true
else
msg_info "Setup Temurin JDK $JAVA_VERSION "
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
# Install with retry logic
install_packages_with_retry " $DESIRED_PACKAGE " || {
msg_error "Failed to install Temurin JDK $JAVA_VERSION "
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
cache_installed_version "temurin-jdk" " $JAVA_VERSION "
msg_ok "Setup Temurin JDK $JAVA_VERSION "
2025-09-11 12:44:13 +02:00
}
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# Installs a local IP updater script using networkd-dispatcher.
#
# Description:
# - Stores current IP in /run/local-ip.env
# - Automatically runs on network changes
# ------------------------------------------------------------------------------
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
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"
2025-06-18 12:07:08 +02:00
2025-11-05 05:19:35 -08:00
# Check if already set up
if [[ -f " $SCRIPT_PATH " && -f " $DISPATCHER_SCRIPT " ]] ; then
msg_info "Update Local IP Helper"
cache_installed_version "local-ip-helper" "1.0"
msg_ok "Update Local IP Helper"
else
msg_info "Setup Local IP Helper"
fi
2025-09-11 12:44:13 +02:00
mkdir -p " $BASE_DIR "
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
# Install networkd-dispatcher if not present
if ! dpkg -s networkd-dispatcher >/dev/null 2>& 1; then
2025-11-05 05:19:35 -08:00
ensure_dependencies networkd-dispatcher || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to install networkd-dispatcher"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
fi
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
# Write update_local_ip.sh
cat <<'EOF' >"$SCRIPT_PATH"
#!/bin/bash
set -euo pipefail
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
IP_FILE="/run/local-ip.env"
mkdir -p "$(dirname "$IP_FILE")"
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
get_current_ip() {
local ip
2025-06-30 20:23:25 +02:00
2026-01-23 09:05:17 +01:00
# Try IPv4 targets first
local ipv4_targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default")
for target in "${ipv4_targets[@]}"; do
2025-09-11 12:44:13 +02:00
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)}')
2025-06-30 20:23:25 +02:00
fi
2025-09-11 12:44:13 +02:00
if [[ -n "$ip" ]]; then
echo "$ip"
return 0
fi
done
2025-06-18 12:07:08 +02:00
2026-01-23 09:05:17 +01:00
# IPv6 fallback: Try direct interface lookup for eth0
ip=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
if [[ -n "$ip" && "$ip" =~ : ]]; then
echo "$ip"
return 0
fi
# IPv6 fallback: Use routing table with IPv6 targets (Google DNS, Cloudflare DNS)
local ipv6_targets=("2001:4860:4860::8888" "2606:4700:4700::1111")
for target in "${ipv6_targets[@]}"; do
ip=$(ip -6 route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
if [[ -n "$ip" && "$ip" =~ : ]]; then
echo "$ip"
return 0
fi
done
2025-09-11 12:44:13 +02:00
return 1
}
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
current_ip="$(get_current_ip)"
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
if [[ -z "$current_ip" ]]; then
echo "[ERROR] Could not detect local IP" >&2
2026-03-02 10:55:20 +01:00
exit 123
2025-09-11 12:44:13 +02:00
fi
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
if [[ -f "$IP_FILE" ]]; then
source "$IP_FILE"
[[ "$LOCAL_IP" == "$current_ip" ]] && exit 0
fi
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
echo "LOCAL_IP=$current_ip" > "$IP_FILE"
echo "[INFO] LOCAL_IP updated to $current_ip"
EOF
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
chmod +x " $SCRIPT_PATH "
2025-05-14 16:08:21 +02:00
2025-09-11 12:44:13 +02:00
# Install dispatcher hook
mkdir -p " $( dirname " $DISPATCHER_SCRIPT " ) "
cat <<EOF >"$DISPATCHER_SCRIPT"
#!/bin/bash
$SCRIPT_PATH
EOF
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
chmod +x " $DISPATCHER_SCRIPT "
2025-11-05 05:19:35 -08:00
systemctl enable -q --now networkd-dispatcher.service || {
msg_warn "Failed to enable networkd-dispatcher service"
}
cache_installed_version "local-ip-helper" "1.0"
msg_ok "Setup Local IP Helper"
2025-09-11 12:44:13 +02:00
}
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
2026-01-22 11:33:13 +01:00
# Installs or updates MariaDB.
2025-09-11 12:44:13 +02:00
#
# Description:
2026-01-22 11:33:13 +01:00
# - Uses Debian/Ubuntu distribution packages by default (most reliable)
# - Only uses official MariaDB repository when a specific version is requested
2025-09-11 12:44:13 +02:00
# - Detects current MariaDB version and replaces it if necessary
# - Preserves existing database data
#
# Variables:
2026-01-22 11:33:13 +01:00
# MARIADB_VERSION - MariaDB version to install (optional)
# - Not set or "latest": Uses distribution packages (recommended)
# - Specific version (e.g. "11.4", "12.2"): Uses MariaDB official repo
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
2025-07-09 14:06:33 +02:00
2025-09-11 12:44:13 +02:00
setup_mariadb() {
local MARIADB_VERSION = " ${ MARIADB_VERSION :- latest } "
2026-01-22 11:33:13 +01:00
local USE_DISTRO_PACKAGES = false
2025-09-11 12:44:13 +02:00
2026-01-12 08:48:36 +01:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2026-01-22 11:33:13 +01:00
# Determine installation method:
# - "latest" or empty: Use distribution packages (avoids mirror issues)
# - Specific version: Use MariaDB official repository
if [[ " $MARIADB_VERSION " == "latest" || -z " $MARIADB_VERSION " ]] ; then
USE_DISTRO_PACKAGES = true
msg_info "Setup MariaDB (distribution packages)"
else
msg_info "Setup MariaDB $MARIADB_VERSION (official repository)"
2025-09-11 12:44:13 +02:00
fi
2025-05-14 16:08:21 +02:00
2025-11-05 05:19:35 -08:00
# Get currently installed version
2025-09-11 12:44:13 +02:00
local CURRENT_VERSION = ""
2025-11-05 05:19:35 -08:00
CURRENT_VERSION = $( is_tool_installed "mariadb" 2>/dev/null) || true
2026-01-12 08:48:36 +01:00
# Pre-configure debconf to prevent any interactive prompts during install/upgrade
2026-01-22 11:33:13 +01:00
debconf-set-selections <<EOF
2026-01-12 08:48:36 +01:00
mariadb-server mariadb-server/feedback boolean false
mariadb-server mariadb-server/root_password password
mariadb-server mariadb-server/root_password_again password
EOF
2026-01-22 11:33:13 +01:00
# If specific version requested, also configure version-specific debconf
if [[ " $USE_DISTRO_PACKAGES " == "false" ]] ; then
local MARIADB_MAJOR_MINOR
MARIADB_MAJOR_MINOR = $( echo " $MARIADB_VERSION " | awk -F. '{print $1"."$2}' )
if [[ -n " $MARIADB_MAJOR_MINOR " ]] ; then
debconf-set-selections <<EOF
mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false
mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/root_password password
mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/root_password_again password
EOF
fi
fi
# ============================================================================
# DISTRIBUTION PACKAGES PATH (default, most reliable)
# ============================================================================
if [[ " $USE_DISTRO_PACKAGES " == "true" ]] ; then
# Check if MariaDB was previously installed from official repo
local HAD_MARIADB_REPO = false
if [[ -f /etc/apt/sources.list.d/mariadb.sources ]] || [[ -f /etc/apt/sources.list.d/mariadb.list ]] ; then
HAD_MARIADB_REPO = true
msg_info "Removing MariaDB official repository (switching to distribution packages)"
fi
# Clean up any existing MariaDB repository files to avoid conflicts
cleanup_old_repo_files "mariadb"
# If we had a repo, we need to refresh APT cache
if [[ " $HAD_MARIADB_REPO " == "true" ]] ; then
$STD apt update || msg_warn "APT update had issues, continuing..."
fi
# Ensure APT is working
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-01-22 11:33:13 +01:00
# Check if installed version is from official repo and higher than distro version
# In this case, we keep the existing installation to avoid data issues
if [[ -n " $CURRENT_VERSION " ]] ; then
# Get available distro version
local DISTRO_VERSION = ""
DISTRO_VERSION = $( apt-cache policy mariadb-server 2>/dev/null | grep -E "Candidate:" | awk '{print $2}' | grep -oP '^\d+:\K\d+\.\d+\.\d+' || echo "" )
if [[ -n " $DISTRO_VERSION " ]] ; then
# Compare versions - if current is higher, keep it
local CURRENT_MAJOR DISTRO_MAJOR
CURRENT_MAJOR = $( echo " $CURRENT_VERSION " | awk -F. '{print $1}' )
DISTRO_MAJOR = $( echo " $DISTRO_VERSION " | awk -F. '{print $1}' )
if [[ " $CURRENT_MAJOR " -gt " $DISTRO_MAJOR " ]] ; then
msg_warn "MariaDB $CURRENT_VERSION is already installed (higher than distro $DISTRO_VERSION )"
msg_warn "Keeping existing installation to preserve data integrity"
msg_warn "To use distribution packages, manually remove MariaDB first"
_setup_mariadb_runtime_dir
cache_installed_version "mariadb" " $CURRENT_VERSION "
msg_ok "Setup MariaDB $CURRENT_VERSION (existing installation kept)"
return 0
fi
fi
fi
# Install or upgrade MariaDB from distribution packages
if ! install_packages_with_retry "mariadb-server" "mariadb-client" ; then
msg_error "Failed to install MariaDB packages from distribution"
2026-03-26 16:07:38 +01:00
return 100
2026-01-22 11:33:13 +01:00
fi
# Get installed version for caching
local INSTALLED_VERSION = ""
INSTALLED_VERSION = $( mariadb --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -n1 || echo "distro" )
# Configure runtime directory and finish
_setup_mariadb_runtime_dir
cache_installed_version "mariadb" " $INSTALLED_VERSION "
msg_ok "Setup MariaDB $INSTALLED_VERSION (distribution packages)"
return 0
fi
# ============================================================================
# OFFICIAL REPOSITORY PATH (only when specific version requested)
# ============================================================================
# First, check if there's an old/broken repository that needs cleanup
if [[ -f /etc/apt/sources.list.d/mariadb.sources ]] || [[ -f /etc/apt/sources.list.d/mariadb.list ]] ; then
local OLD_REPO_VERSION = ""
2026-02-22 11:54:21 +01:00
OLD_REPO_VERSION = $( grep -oP 'repo/\K[0-9]+\.[0-9]+(\.[0-9]+)?' /etc/apt/sources.list.d/mariadb.sources 2>/dev/null ||
grep -oP 'repo/\K[0-9]+\.[0-9]+(\.[0-9]+)?' /etc/apt/sources.list.d/mariadb.list 2>/dev/null || echo "" )
2026-01-22 11:33:13 +01:00
# Check if old repo points to a different version
if [[ -n " $OLD_REPO_VERSION " ]] && [[ " ${ OLD_REPO_VERSION %.* } " != " ${ MARIADB_VERSION %.* } " ]] ; then
msg_info "Cleaning up old MariaDB repository (was: $OLD_REPO_VERSION , requested: $MARIADB_VERSION )"
cleanup_old_repo_files "mariadb"
$STD apt update || msg_warn "APT update had issues, continuing..."
fi
2026-01-12 08:48:36 +01:00
fi
2025-11-05 05:19:35 -08:00
# Scenario 1: Already installed at target version - just update packages
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " == " $MARIADB_VERSION " ]] ; then
msg_info "Update MariaDB $MARIADB_VERSION "
# Ensure APT is working
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
# Check if repository needs to be refreshed
if [[ -f /etc/apt/sources.list.d/mariadb.sources ]] ; then
local REPO_VERSION = ""
REPO_VERSION = $( grep -oP 'repo/\K[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.sources 2>/dev/null || echo "" )
if [[ -n " $REPO_VERSION " && " $REPO_VERSION " != " ${ MARIADB_VERSION %.* } " ]] ; then
msg_warn "Repository version mismatch, updating..."
manage_tool_repository "mariadb" " $MARIADB_VERSION " "http://mirror.mariadb.org/repo/ $MARIADB_VERSION " \
"https://mariadb.org/mariadb_release_signing_key.asc" || {
msg_error "Failed to update MariaDB repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
fi
2025-10-22 06:25:26 -07:00
fi
2025-11-05 05:19:35 -08:00
# Perform upgrade with retry logic
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
upgrade_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to upgrade MariaDB packages"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
cache_installed_version "mariadb" " $MARIADB_VERSION "
msg_ok "Update MariaDB $MARIADB_VERSION "
2025-09-11 12:44:13 +02:00
return 0
fi
2025-07-09 14:06:33 +02:00
2026-03-02 08:44:59 +01:00
# Scenario 2b: Different version installed - clean upgrade
2025-11-05 05:19:35 -08:00
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " != " $MARIADB_VERSION " ]] ; then
msg_info "Upgrade MariaDB from $CURRENT_VERSION to $MARIADB_VERSION "
remove_old_tool_version "mariadb"
2025-09-11 12:44:13 +02:00
fi
2025-06-18 12:07:08 +02:00
2026-01-22 11:33:13 +01:00
# Scenario 3: Fresh install or version change with specific version
2025-11-05 05:19:35 -08:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "mariadb" || {
msg_error "Failed to prepare MariaDB repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Install required dependencies first
2025-10-22 06:25:26 -07:00
local mariadb_deps =()
for dep in gawk rsync socat libdbi-perl pv; do
if apt-cache search "^ ${ dep } $" 2>/dev/null | grep -q .; then
mariadb_deps +=( " $dep " )
fi
done
if [[ ${# mariadb_deps [@] } -gt 0 ]] ; then
$STD apt install -y " ${ mariadb_deps [@] } " 2>/dev/null || true
fi
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
# Setup repository
manage_tool_repository "mariadb" " $MARIADB_VERSION " "http://mirror.mariadb.org/repo/ $MARIADB_VERSION " \
"https://mariadb.org/mariadb_release_signing_key.asc" || {
msg_error "Failed to setup MariaDB repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
# Install packages with retry logic
if ! install_packages_with_retry "mariadb-server" "mariadb-client" ; then
2026-01-22 11:33:13 +01:00
# Fallback: try distribution packages
msg_warn "Failed to install MariaDB $MARIADB_VERSION from official repo, falling back to distribution packages..."
2025-10-22 06:25:26 -07:00
cleanup_old_repo_files "mariadb"
2025-11-05 05:19:35 -08:00
$STD apt update || {
msg_warn "APT update also failed, continuing with cache"
}
2026-01-22 11:33:13 +01:00
if install_packages_with_retry "mariadb-server" "mariadb-client" ; then
local FALLBACK_VERSION = ""
FALLBACK_VERSION = $( mariadb --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -n1 || echo "distro" )
msg_warn "Installed MariaDB $FALLBACK_VERSION from distribution instead of requested $MARIADB_VERSION "
_setup_mariadb_runtime_dir
cache_installed_version "mariadb" " $FALLBACK_VERSION "
msg_ok "Setup MariaDB $FALLBACK_VERSION (fallback to distribution packages)"
return 0
else
msg_error "Failed to install MariaDB packages (both official repo and distribution)"
2026-03-26 16:07:38 +01:00
return 100
2026-01-22 11:33:13 +01:00
fi
2025-11-05 05:19:35 -08:00
fi
2025-06-18 12:07:08 +02:00
2026-01-22 11:33:13 +01:00
_setup_mariadb_runtime_dir
cache_installed_version "mariadb" " $MARIADB_VERSION "
msg_ok "Setup MariaDB $MARIADB_VERSION "
}
# ------------------------------------------------------------------------------
# Helper function: Configure MariaDB runtime directory persistence
# ------------------------------------------------------------------------------
_setup_mariadb_runtime_dir() {
2026-01-01 23:53:53 +01:00
# Configure tmpfiles.d to ensure /run/mysqld directory is created on boot
# This fixes the issue where MariaDB fails to start after container reboot
2026-01-05 18:03:02 +01:00
2026-01-01 23:53:53 +01:00
# Create tmpfiles.d configuration with error handling
2026-01-05 18:03:02 +01:00
if ! printf '# Ensure /run/mysqld directory exists with correct permissions for MariaDB\nd /run/mysqld 0755 mysql mysql -\n' >/etc/tmpfiles.d/mariadb.conf; then
2026-01-01 23:53:53 +01:00
msg_warn "Failed to create /etc/tmpfiles.d/mariadb.conf - runtime directory may not persist on reboot"
fi
2026-01-05 18:03:02 +01:00
2026-01-01 23:53:53 +01:00
# Create the directory now if it doesn't exist
# Verify mysql user exists before attempting ownership change
if [[ ! -d /run/mysqld ]] ; then
mkdir -p /run/mysqld
# Set permissions first (works regardless of user existence)
chmod 755 /run/mysqld
# Set ownership only if mysql user exists
if getent passwd mysql >/dev/null 2>& 1; then
chown mysql:mysql /run/mysqld
else
msg_warn "mysql user not found - directory created with correct permissions but ownership not set"
fi
fi
2025-09-11 12:44:13 +02:00
}
2025-06-18 12:07:08 +02:00
2025-11-10 18:51:02 +01:00
# ------------------------------------------------------------------------------
# Creates MariaDB database with user, charset and optional extra grants/modes
#
# Description:
# - Generates password if empty
# - Creates database with utf8mb4_unicode_ci
# - Creates local user with password
# - Grants full access to this DB
# - Optional: apply extra GRANT statements (comma-separated)
# - Optional: apply custom GLOBAL sql_mode
# - Saves credentials to file
# - Exports variables for use in calling script
#
# Usage:
# MARIADB_DB_NAME="myapp_db" MARIADB_DB_USER="myapp_user" setup_mariadb_db
# MARIADB_DB_NAME="domain_monitor" MARIADB_DB_USER="domainmonitor" setup_mariadb_db
# MARIADB_DB_NAME="myapp" MARIADB_DB_USER="myapp" MARIADB_DB_EXTRA_GRANTS="GRANT SELECT ON \`mysql\`.\`time_zone_name\`" setup_mariadb_db
# MARIADB_DB_NAME="ghostfolio" MARIADB_DB_USER="ghostfolio" MARIADB_DB_SQL_MODE="" setup_mariadb_db
#
# Variables:
# MARIADB_DB_NAME - Database name (required)
# MARIADB_DB_USER - Database user (required)
# MARIADB_DB_PASS - User password (optional, auto-generated if empty)
# MARIADB_DB_EXTRA_GRANTS - Comma-separated GRANT statements (optional)
# Example: "GRANT SELECT ON \`mysql\`.\`time_zone_name\`"
# MARIADB_DB_SQL_MODE - Optional global sql_mode override (e.g. "", "STRICT_TRANS_TABLES")
# MARIADB_DB_CREDS_FILE - Credentials file path (optional, default: ~/${APPLICATION}.creds)
#
# Exports:
# MARIADB_DB_NAME, MARIADB_DB_USER, MARIADB_DB_PASS
# ------------------------------------------------------------------------------
function setup_mariadb_db() {
if [[ -z " ${ MARIADB_DB_NAME :- } " || -z " ${ MARIADB_DB_USER :- } " ]] ; then
msg_error "MARIADB_DB_NAME and MARIADB_DB_USER must be set before calling setup_mariadb_db"
2026-03-26 16:07:38 +01:00
return 65
2025-11-10 18:51:02 +01:00
fi
if [[ -z " ${ MARIADB_DB_PASS :- } " ]] ; then
MARIADB_DB_PASS = $( openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
fi
msg_info "Setting up MariaDB Database"
$STD mariadb -u root -e "CREATE DATABASE \` $MARIADB_DB_NAME \` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
$STD mariadb -u root -e "CREATE USER ' $MARIADB_DB_USER '@'localhost' IDENTIFIED BY ' $MARIADB_DB_PASS ';"
$STD mariadb -u root -e "GRANT ALL ON \` $MARIADB_DB_NAME \`.* TO ' $MARIADB_DB_USER '@'localhost';"
# Optional extra grants
if [[ -n " ${ MARIADB_DB_EXTRA_GRANTS :- } " ]] ; then
IFS = ',' read -ra G_LIST <<< " ${ MARIADB_DB_EXTRA_GRANTS :- } "
for g in " ${ G_LIST [@] } " ; do
g = $( echo " $g " | xargs)
$STD mariadb -u root -e " $g TO ' $MARIADB_DB_USER '@'localhost';"
done
fi
# Optional sql_mode override
if [[ -n " ${ MARIADB_DB_SQL_MODE :- } " ]] ; then
$STD mariadb -u root -e "SET GLOBAL sql_mode=' ${ MARIADB_DB_SQL_MODE :- } ';"
fi
$STD mariadb -u root -e "FLUSH PRIVILEGES;"
2025-11-17 16:52:11 +01:00
local app_name = " ${ APPLICATION ,, } "
local CREDS_FILE = " ${ MARIADB_DB_CREDS_FILE :- ${ HOME } / ${ app_name } .creds } "
2025-11-10 18:51:02 +01:00
{
echo "MariaDB Credentials"
echo "Database: $MARIADB_DB_NAME "
echo "User: $MARIADB_DB_USER "
echo "Password: $MARIADB_DB_PASS "
} >>" $CREDS_FILE "
msg_ok "Set up MariaDB Database"
export MARIADB_DB_NAME
export MARIADB_DB_USER
export MARIADB_DB_PASS
}
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# 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)
# ------------------------------------------------------------------------------
2025-07-08 17:19:51 +02:00
2025-09-11 12:44:13 +02:00
function setup_mongodb() {
local MONGO_VERSION = " ${ MONGO_VERSION :- 8 .0 } "
2025-10-22 06:25:26 -07:00
local DISTRO_ID DISTRO_CODENAME
2025-10-22 16:17:20 +02:00
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
2025-07-08 17:19:51 +02:00
2026-01-12 08:48:36 +01:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2025-09-11 12:44:13 +02:00
# 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."
2026-03-26 16:07:38 +01:00
return 236
2025-07-08 17:19:51 +02:00
fi
2025-09-11 12:44:13 +02:00
fi
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
case " $DISTRO_ID " in
ubuntu)
MONGO_BASE_URL = "https://repo.mongodb.org/apt/ubuntu"
;;
debian)
MONGO_BASE_URL = "https://repo.mongodb.org/apt/debian"
;;
*)
msg_error "Unsupported distribution: $DISTRO_ID "
2026-03-26 16:07:38 +01:00
return 238
2025-09-11 12:44:13 +02:00
;;
esac
2025-11-05 05:19:35 -08:00
# Get currently installed version
2025-09-11 12:44:13 +02:00
local INSTALLED_VERSION = ""
2025-11-05 05:19:35 -08:00
INSTALLED_VERSION = $( is_tool_installed "mongodb" 2>/dev/null) || true
2025-06-18 12:07:08 +02:00
2025-11-05 05:19:35 -08:00
# Scenario 1: Already at target version - just update packages
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " == " $MONGO_VERSION " ]] ; then
msg_info "Update MongoDB $MONGO_VERSION "
2025-10-22 06:25:26 -07:00
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
# Perform upgrade with retry logic
upgrade_packages_with_retry "mongodb-org" || {
msg_error "Failed to upgrade MongoDB"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
cache_installed_version "mongodb" " $MONGO_VERSION "
msg_ok "Update MongoDB $MONGO_VERSION "
2025-09-11 12:44:13 +02:00
return 0
fi
2025-11-05 05:19:35 -08:00
# Scenario 2: Different version installed - clean upgrade
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " != " $MONGO_VERSION " ]] ; then
msg_info "Upgrade MongoDB from $INSTALLED_VERSION to $MONGO_VERSION "
remove_old_tool_version "mongodb"
else
msg_info "Setup MongoDB $MONGO_VERSION "
2025-09-11 12:44:13 +02:00
fi
2025-10-22 06:25:26 -07:00
cleanup_orphaned_sources
2025-11-05 05:19:35 -08:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "mongodb" || {
msg_error "Failed to prepare MongoDB repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 16:17:20 +02:00
2025-11-05 05:19:35 -08:00
# Setup repository
manage_tool_repository "mongodb" " $MONGO_VERSION " " $MONGO_BASE_URL " \
"https://www.mongodb.org/static/pgp/server- ${ MONGO_VERSION } .asc" || {
msg_error "Failed to setup MongoDB repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Wait for repo to settle
2025-10-22 06:25:26 -07:00
$STD apt update || {
2025-11-05 05:19:35 -08:00
msg_error "APT update failed — invalid MongoDB repo for ${ DISTRO_ID } - ${ DISTRO_CODENAME } ?"
2026-03-26 16:07:38 +01:00
return 100
2025-09-11 12:44:13 +02:00
}
2025-11-05 05:19:35 -08:00
# Install MongoDB with retry logic
install_packages_with_retry "mongodb-org" || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to install MongoDB packages"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 06:25:26 -07:00
}
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
if ! command -v mongod >/dev/null 2>& 1; then
msg_error "MongoDB binary not found after installation"
2026-03-26 16:07:38 +01:00
return 127
2025-11-05 05:19:35 -08:00
fi
2025-09-11 12:44:13 +02:00
mkdir -p /var/lib/mongodb
chown -R mongodb:mongodb /var/lib/mongodb
2025-11-05 05:19:35 -08:00
$STD systemctl enable mongod || {
msg_warn "Failed to enable mongod service"
}
2025-10-22 06:25:26 -07:00
safe_service_restart mongod
2025-10-22 16:17:20 +02:00
2025-11-05 05:19:35 -08:00
# Verify MongoDB version
local INSTALLED_VERSION
INSTALLED_VERSION = $( mongod --version 2>/dev/null | grep -oP 'db version v\K[0-9]+\.[0-9]+' | head -n1 || echo "0.0" )
verify_tool_version "MongoDB" " $MONGO_VERSION " " $INSTALLED_VERSION " || true
cache_installed_version "mongodb" " $MONGO_VERSION "
2025-09-11 12:44:13 +02:00
msg_ok "Setup MongoDB $MONGO_VERSION "
2025-04-28 15:25:10 +02:00
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2026-03-02 08:44:59 +01:00
# Installs or upgrades MySQL.
2025-05-15 15:18:41 +02:00
#
# Description:
2026-03-02 08:44:59 +01:00
# - By default uses distro repository (Debian/Ubuntu apt) for stability
# - Optionally uses official MySQL repository for specific versions
2025-09-11 12:44:13 +02:00
# - Detects existing MySQL installation
# - Purges conflicting packages before installation
# - Supports clean upgrade
2025-10-22 06:25:26 -07:00
# - Handles Debian Trixie libaio1t64 transition
2025-09-11 12:44:13 +02:00
#
# Variables:
2026-03-02 10:35:30 +01:00
# USE_MYSQL_REPO - Use official MySQL repository (default: true)
# Set to "false" to use distro packages instead
2026-03-02 08:44:59 +01:00
# MYSQL_VERSION - MySQL version to install when using official repo
# (e.g. 8.0, 8.4) (default: 8.0)
#
# Examples:
2026-03-02 10:35:30 +01:00
# setup_mysql # Uses official MySQL repo, 8.0
# MYSQL_VERSION="8.4" setup_mysql # Specific version from MySQL repo
# USE_MYSQL_REPO=false setup_mysql # Uses distro package instead
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
function setup_mysql() {
local MYSQL_VERSION = " ${ MYSQL_VERSION :- 8 .0 } "
2026-03-02 10:35:30 +01:00
local USE_MYSQL_REPO = " ${ USE_MYSQL_REPO :- true } "
2025-10-22 06:25:26 -07:00
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)
2025-04-28 15:25:10 +02:00
2026-01-12 08:48:36 +01:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2025-11-05 05:19:35 -08:00
# Get currently installed version
local CURRENT_VERSION = ""
CURRENT_VERSION = $( is_tool_installed "mysql" 2>/dev/null) || true
2026-03-02 08:44:59 +01:00
# Scenario 1: Use distro repository (default, most stable)
if [[ " $USE_MYSQL_REPO " != "true" && " $USE_MYSQL_REPO " != "TRUE" && " $USE_MYSQL_REPO " != "1" ]] ; then
msg_info "Setup MySQL (distro package)"
# If already installed, just update
if [[ -n " $CURRENT_VERSION " ]] ; then
msg_info "Update MySQL $CURRENT_VERSION "
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
upgrade_packages_with_retry "default-mysql-server" "default-mysql-client" ||
upgrade_packages_with_retry "mysql-server" "mysql-client" ||
upgrade_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to upgrade MySQL/MariaDB packages"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
cache_installed_version "mysql" " $CURRENT_VERSION "
msg_ok "Update MySQL $CURRENT_VERSION "
return 0
fi
# Fresh install from distro repo
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
export DEBIAN_FRONTEND = noninteractive
# Try default-mysql-server first, fallback to mysql-server, then mariadb
if apt-cache search "^default-mysql-server $" 2>/dev/null | grep -q .; then
install_packages_with_retry "default-mysql-server" "default-mysql-client" || {
msg_warn "default-mysql-server failed, trying mysql-server"
install_packages_with_retry "mysql-server" "mysql-client" || {
msg_warn "mysql-server failed, trying mariadb as fallback"
install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install any MySQL/MariaDB from distro repository"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
}
}
elif apt-cache search "^mysql-server $" 2>/dev/null | grep -q .; then
install_packages_with_retry "mysql-server" "mysql-client" || {
msg_warn "mysql-server failed, trying mariadb as fallback"
install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install any MySQL/MariaDB from distro repository"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
}
else
# Distro doesn't have MySQL, use MariaDB
install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install MariaDB from distro repository"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
fi
# Get installed version
local INSTALLED_VERSION = ""
INSTALLED_VERSION = $( is_tool_installed "mysql" 2>/dev/null) || true
if [[ -z " $INSTALLED_VERSION " ]] ; then
INSTALLED_VERSION = $( is_tool_installed "mariadb" 2>/dev/null) || true
fi
cache_installed_version "mysql" " ${ INSTALLED_VERSION :- distro } "
msg_ok "Setup MySQL/MariaDB ${ INSTALLED_VERSION :- from distro } "
return 0
fi
# Scenario 2: Use official MySQL repository (USE_MYSQL_REPO=true)
# Scenario 2a: Already at target version - just update packages
2025-11-05 05:19:35 -08:00
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " == " $MYSQL_VERSION " ]] ; then
msg_info "Update MySQL $MYSQL_VERSION "
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
# Perform upgrade with retry logic (non-fatal if fails)
2026-02-27 13:59:02 +01:00
upgrade_packages_with_retry "mysql-server" "mysql-client" || {
msg_warn "MySQL package upgrade had issues, continuing with current version"
}
2025-11-05 05:19:35 -08:00
cache_installed_version "mysql" " $MYSQL_VERSION "
msg_ok "Update MySQL $MYSQL_VERSION "
return 0
2025-09-11 12:44:13 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Scenario 2: Different version installed - clean upgrade
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " != " $MYSQL_VERSION " ]] ; then
msg_info "Upgrade MySQL from $CURRENT_VERSION to $MYSQL_VERSION "
remove_old_tool_version "mysql"
else
2025-10-22 06:25:26 -07:00
msg_info "Setup MySQL $MYSQL_VERSION "
2025-11-05 05:19:35 -08:00
fi
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "mysql" || {
msg_error "Failed to prepare MySQL repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64, use 8.4 LTS
if [[ " $DISTRO_ID " == "debian" && " $DISTRO_CODENAME " = ~ ^( trixie| forky| sid) $ ]] ; then
msg_info "Debian ${ DISTRO_CODENAME } detected → using MySQL 8.4 LTS (libaio1t64 compatible)"
2025-10-22 06:25:26 -07:00
2026-03-02 08:44:59 +01:00
if ! download_gpg_key "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" "/etc/apt/keyrings/mysql.gpg" "dearmor" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to import MySQL GPG key"
2026-03-26 16:07:38 +01:00
return 7
2025-10-22 06:25:26 -07:00
fi
2025-11-07 02:49:16 -08:00
cat >/etc/apt/sources.list.d/mysql.sources <<EOF
2025-10-22 15:42:41 +02:00
Types: deb
URIs: https://repo.mysql.com/apt/debian/
Suites: bookworm
Components: mysql-8.4-lts
2025-11-07 02:49:16 -08:00
Architectures: $(dpkg --print-architecture)
2025-10-22 15:42:41 +02:00
Signed-By: /etc/apt/keyrings/mysql.gpg
2025-10-22 06:25:26 -07:00
EOF
2025-11-05 05:19:35 -08:00
$STD apt update || {
msg_error "Failed to update APT for MySQL 8.4 LTS"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Install with retry logic
if ! install_packages_with_retry "mysql-community-server" "mysql-community-client" ; then
msg_warn "MySQL 8.4 LTS installation failed – falling back to MariaDB"
cleanup_old_repo_files "mysql"
$STD apt update
install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install database engine (MySQL/MariaDB fallback)"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
msg_ok "Setup Database Engine (MariaDB fallback on Debian ${ DISTRO_CODENAME } )"
return 0
2025-10-22 06:25:26 -07:00
fi
2025-11-05 05:19:35 -08:00
cache_installed_version "mysql" "8.4"
msg_ok "Setup MySQL 8.4 LTS (Debian ${ DISTRO_CODENAME } )"
return 0
fi
# Standard setup for other distributions
local SUITE
if [[ " $DISTRO_ID " == "debian" ]] ; then
case " $DISTRO_CODENAME " in
bookworm | bullseye) SUITE = " $DISTRO_CODENAME " ;;
*) SUITE = "bookworm" ;;
esac
else
SUITE = $( get_fallback_suite " $DISTRO_ID " " $DISTRO_CODENAME " "https://repo.mysql.com/apt/ ${ DISTRO_ID } " )
fi
# Setup repository
manage_tool_repository "mysql" " $MYSQL_VERSION " "https://repo.mysql.com/apt/ ${ DISTRO_ID } " \
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" || {
msg_error "Failed to setup MySQL repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-11-05 05:19:35 -08:00
# Try multiple package names with retry logic
local mysql_install_success = false
if apt-cache search "^mysql-server $" 2>/dev/null | grep -q . &&
install_packages_with_retry "mysql-server" "mysql-client" ; then
mysql_install_success = true
elif apt-cache search "^mysql-community-server $" 2>/dev/null | grep -q . &&
install_packages_with_retry "mysql-community-server" "mysql-community-client" ; then
mysql_install_success = true
elif apt-cache search "^mysql $" 2>/dev/null | grep -q . &&
install_packages_with_retry "mysql" ; then
mysql_install_success = true
fi
if [[ " $mysql_install_success " == false ]] ; then
msg_error "MySQL ${ MYSQL_VERSION } package not available for suite ${ SUITE } "
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
fi
# Verify mysql command is accessible
if ! command -v mysql >/dev/null 2>& 1; then
hash -r
2025-10-22 06:25:26 -07:00
if ! command -v mysql >/dev/null 2>& 1; then
2025-11-05 05:19:35 -08:00
msg_error "MySQL installed but mysql command still not found"
2026-03-26 16:07:38 +01:00
return 127
2025-10-22 06:25:26 -07:00
fi
2025-05-05 15:47:43 +02:00
fi
2025-11-05 05:19:35 -08:00
cache_installed_version "mysql" " $MYSQL_VERSION "
msg_ok "Setup MySQL $MYSQL_VERSION "
2025-09-11 12:44:13 +02:00
}
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
# 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:
2025-12-04 07:52:18 +01:00
# NODE_VERSION - Node.js version to install (default: 24 LTS)
2025-09-11 12:44:13 +02:00
# NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0")
# ------------------------------------------------------------------------------
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
function setup_nodejs() {
2025-12-04 07:52:18 +01:00
local NODE_VERSION = " ${ NODE_VERSION :- 24 } "
2025-09-11 12:44:13 +02:00
local NODE_MODULE = " ${ NODE_MODULE :- } "
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts
cleanup_legacy_install "nodejs"
# Get currently installed version
local CURRENT_NODE_VERSION = ""
CURRENT_NODE_VERSION = $( is_tool_installed "nodejs" 2>/dev/null) || true
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Ensure jq is available for JSON parsing
2025-10-22 16:38:58 +02:00
if ! command -v jq & >/dev/null; then
2025-11-05 05:19:35 -08:00
$STD apt update
$STD apt install -y jq || {
2025-10-22 16:38:58 +02:00
msg_error "Failed to install jq"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 16:38:58 +02:00
}
fi
2025-11-05 05:19:35 -08:00
# Scenario 1: Already installed at target version - just update packages/modules
if [[ -n " $CURRENT_NODE_VERSION " && " $CURRENT_NODE_VERSION " == " $NODE_VERSION " ]] ; then
msg_info "Update Node.js $NODE_VERSION "
2025-04-28 15:25:10 +02:00
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-04-28 15:25:10 +02:00
2026-03-26 10:04:00 +01:00
# Pin npm to 11.11.0 to work around Node.js 22.22.2 regression (nodejs/node#62425)
$STD npm install -g npm@11.11.0 2>/dev/null || true
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
cache_installed_version "nodejs" " $NODE_VERSION "
msg_ok "Update Node.js $NODE_VERSION "
else
# Scenario 2: Different version installed - clean upgrade
if [[ -n " $CURRENT_NODE_VERSION " && " $CURRENT_NODE_VERSION " != " $NODE_VERSION " ]] ; then
msg_info "Upgrade Node.js from $CURRENT_NODE_VERSION to $NODE_VERSION "
remove_old_tool_version "nodejs"
else
msg_info "Setup Node.js $NODE_VERSION "
fi
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Remove ALL Debian nodejs packages BEFORE adding NodeSource repo
if dpkg -l 2>/dev/null | grep -qE "^ii.*(nodejs|libnode|node-cjs|node-acorn|node-balanced|node-brace|node-minimatch|node-undici|node-xtend|node-corepack)" ; then
msg_info "Removing Debian-packaged Node.js and dependencies"
$STD apt purge -y nodejs nodejs-doc libnode* node-* 2>/dev/null || true
$STD apt autoremove -y 2>/dev/null || true
$STD apt clean 2>/dev/null || true
2025-09-11 12:44:13 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-11-05 05:19:35 -08:00
# Remove any APT pinning (not needed)
rm -f /etc/apt/preferences.d/nodesource 2>/dev/null || true
# Prepare repository (cleanup + validation)
prepare_repository_setup "nodesource" || {
msg_error "Failed to prepare Node.js repository"
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
}
# Setup NodeSource repository
manage_tool_repository "nodejs" " $NODE_VERSION " "https://deb.nodesource.com/node_ ${ NODE_VERSION } .x" "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" || {
msg_error "Failed to setup Node.js repository"
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
}
2025-12-04 07:52:18 +01:00
# Force APT cache refresh after repository setup
2026-02-27 13:59:02 +01:00
$STD apt update || {
msg_warn "apt update failed after Node.js repository setup"
}
2025-11-05 05:19:35 -08:00
ensure_dependencies curl ca-certificates gnupg
install_packages_with_retry "nodejs" || {
2025-10-22 16:38:58 +02:00
msg_error "Failed to install Node.js ${ NODE_VERSION } from NodeSource"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
# Verify Node.js was installed correctly
if ! command -v node >/dev/null 2>& 1; then
msg_error "Node.js binary not found after installation"
2026-03-26 16:07:38 +01:00
return 127
2025-10-22 16:21:01 +02:00
fi
2025-11-05 05:19:35 -08:00
local INSTALLED_NODE_VERSION
INSTALLED_NODE_VERSION = $( node -v 2>/dev/null | grep -oP '^v\K[0-9]+' || echo "0" )
verify_tool_version "Node.js" " $NODE_VERSION " " $INSTALLED_NODE_VERSION " || true
# Verify npm is available (should come with NodeSource nodejs)
if ! command -v npm >/dev/null 2>& 1; then
msg_error "npm not found after Node.js installation - repository issue?"
2026-03-26 16:07:38 +01:00
return 127
2025-11-05 05:19:35 -08:00
fi
2026-03-26 10:04:00 +01:00
# Pin npm to 11.11.0 to work around Node.js 22.22.2 regression (nodejs/node#62425)
2025-11-05 05:19:35 -08:00
local NPM_VERSION
NPM_VERSION = $( npm -v 2>/dev/null || echo "0" )
if [[ " $NPM_VERSION " != "0" ]] ; then
2026-03-26 10:04:00 +01:00
$STD npm install -g npm@11.11.0 2>/dev/null || {
msg_warn "Failed to update npm to 11.11.0 (continuing with bundled npm $NPM_VERSION )"
2025-11-05 05:19:35 -08:00
}
fi
2025-10-22 06:25:26 -07:00
cache_installed_version "nodejs" " $NODE_VERSION "
2025-11-05 05:19:35 -08:00
msg_ok "Setup Node.js $NODE_VERSION "
2025-09-11 12:44:13 +02:00
fi
2025-04-28 15:25:10 +02:00
2025-09-11 12:44:13 +02:00
export NODE_OPTIONS = "--max-old-space-size=4096"
2025-10-22 16:38:58 +02:00
# Ensure valid working directory for npm (avoids uv_cwd error)
if [[ ! -d /opt ]] ; then
mkdir -p /opt
fi
cd /opt || {
msg_error "Failed to set safe working directory before npm install"
2026-03-26 16:07:38 +01:00
return 127
2025-09-11 12:44:13 +02:00
}
2025-04-28 15:25:10 +02:00
2025-10-22 16:38:58 +02:00
# Install global Node modules
2025-09-11 12:44:13 +02:00
if [[ -n " $NODE_MODULE " ]] ; then
IFS = ',' read -ra MODULES <<< " $NODE_MODULE "
2025-11-05 05:19:35 -08:00
local failed_modules = 0
2025-09-11 12:44:13 +02:00
for mod in " ${ MODULES [@] } " ; do
local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION
if [[ " $mod " == @*/*@* ]] ; then
2025-10-22 16:38:58 +02:00
# Scoped package with version, e.g. @vue/cli-service@latest
2025-09-11 12:44:13 +02:00
MODULE_NAME = " ${ mod %@* } "
MODULE_REQ_VERSION = " ${ mod ##*@ } "
elif [[ " $mod " == *"@" * ]] ; then
2025-10-22 16:38:58 +02:00
# Unscoped package with version, e.g. yarn@latest
2025-09-11 12:44:13 +02:00
MODULE_NAME = " ${ mod %@* } "
MODULE_REQ_VERSION = " ${ mod ##*@ } "
else
2025-10-22 16:38:58 +02:00
# No version specified
2025-09-11 12:44:13 +02:00
MODULE_NAME = " $mod "
MODULE_REQ_VERSION = "latest"
fi
2025-05-05 15:47:43 +02:00
2025-10-22 16:38:58 +02:00
# Check if the module is already installed
2025-11-05 05:19:35 -08:00
if $STD npm list -g --depth= 0 " $MODULE_NAME " 2>& 1 | grep -q " $MODULE_NAME @" ; then
2025-12-18 18:38:08 +01:00
MODULE_INSTALLED_VERSION = " $( npm list -g --depth= 0 " $MODULE_NAME " 2>& 1 | grep " $MODULE_NAME @" | awk -F@ '{print $2}' 2>/dev/null | tr -d '[:space:]' || echo '' ) "
2025-09-11 12:44:13 +02:00
if [[ " $MODULE_REQ_VERSION " != "latest" && " $MODULE_REQ_VERSION " != " $MODULE_INSTALLED_VERSION " ]] ; then
2025-10-22 16:38:58 +02:00
msg_info "Updating $MODULE_NAME from v $MODULE_INSTALLED_VERSION to v $MODULE_REQ_VERSION "
2025-11-05 05:19:35 -08:00
if ! $STD npm install -g " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } " 2>/dev/null; then
msg_warn "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION "
2026-01-06 20:32:15 +01:00
(( failed_modules++)) || true
2025-11-05 05:19:35 -08:00
continue
2025-10-22 16:38:58 +02:00
fi
2025-09-11 12:44:13 +02:00
elif [[ " $MODULE_REQ_VERSION " == "latest" ]] ; then
2025-10-22 16:38:58 +02:00
msg_info "Updating $MODULE_NAME to latest version"
2025-11-05 05:19:35 -08:00
if ! $STD npm install -g " ${ MODULE_NAME } @latest" 2>/dev/null; then
msg_warn "Failed to update $MODULE_NAME to latest version"
2026-01-06 20:32:15 +01:00
(( failed_modules++)) || true
2025-11-05 05:19:35 -08:00
continue
2025-10-22 16:38:58 +02:00
fi
2025-04-28 15:25:10 +02:00
fi
2025-09-11 12:44:13 +02:00
else
2025-10-22 16:38:58 +02:00
msg_info "Installing $MODULE_NAME @ $MODULE_REQ_VERSION "
2025-11-05 05:19:35 -08:00
if ! $STD npm install -g " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } " 2>/dev/null; then
msg_warn "Failed to install $MODULE_NAME @ $MODULE_REQ_VERSION "
2026-01-06 20:32:15 +01:00
(( failed_modules++)) || true
2025-11-05 05:19:35 -08:00
continue
2025-10-22 16:38:58 +02:00
fi
2025-09-11 12:44:13 +02:00
fi
done
2025-11-05 05:19:35 -08:00
if [[ $failed_modules -eq 0 ]] ; then
msg_ok "Installed Node.js modules: $NODE_MODULE "
else
msg_warn "Installed Node.js modules with $failed_modules failure(s): $NODE_MODULE "
fi
2025-05-05 15:47:43 +02:00
fi
2025-04-28 15:25:10 +02:00
}
2025-04-29 16:04:29 +02:00
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# Installs PHP with selected modules and configures Apache/FPM support.
2025-05-15 15:18:41 +02:00
#
2025-09-11 12:44:13 +02:00
# Description:
# - Adds Sury PHP repo if needed
# - Installs default and user-defined modules
# - Patches php.ini for CLI, Apache, and FPM as needed
2026-01-30 10:29:51 +01:00
# - Handles built-in modules gracefully (e.g., opcache in PHP 8.5+)
# - Skips unavailable packages without failing
2025-09-11 12:44:13 +02:00
#
# 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)
2026-01-30 10:29:51 +01:00
#
# Notes on modules:
# - Base modules (always installed): bcmath, cli, curl, gd, intl, mbstring,
# readline, xml, zip, common
# - Extended modules (commonly needed): mysql, sqlite3, pgsql, redis,
# imagick, bz2, ldap, soap, imap, gmp, apcu
# - Some modules are built-in depending on PHP version:
# * PHP 8.5+: opcache is built-in (no separate package)
# * All versions: ctype, fileinfo, iconv, tokenizer, phar, posix, etc.
# are part of php-common
# - Unavailable modules are skipped with a warning, not an error
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
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 } "
2025-10-22 06:25:26 -07:00
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)
2025-05-05 15:47:43 +02:00
2026-01-30 10:29:51 +01:00
# Parse version for compatibility checks
local PHP_MAJOR = " ${ PHP_VERSION %%.* } "
local PHP_MINOR = " ${ PHP_VERSION #*. } "
PHP_MINOR = " ${ PHP_MINOR %%.* } "
# Modules that are ALWAYS part of php-common (no separate package needed)
# These are either built-in or virtual packages provided by php-common
local BUILTIN_MODULES = "calendar,ctype,exif,ffi,fileinfo,ftp,gettext,iconv,pdo,phar,posix,shmop,sockets,sysvmsg,sysvsem,sysvshm,tokenizer"
# Modules that became built-in in specific PHP versions
# PHP 8.5+: opcache is now part of the core
local BUILTIN_85 = ""
if [[ " $PHP_MAJOR " -gt 8 ]] || [[ " $PHP_MAJOR " -eq 8 && " $PHP_MINOR " -ge 5 ]] ; then
BUILTIN_85 = "opcache"
fi
# Base modules - essential for most PHP applications
# Note: 'common' provides many built-in extensions
2026-01-30 13:05:05 +01:00
local BASE_MODULES = "cli,common,bcmath,curl,dom,gd,gmp,intl,mbstring,readline,xml,zip"
2026-01-30 10:29:51 +01:00
# Add opcache only for PHP < 8.5 (it's built-in starting from 8.5)
if [[ " $PHP_MAJOR " -lt 8 ]] || [[ " $PHP_MAJOR " -eq 8 && " $PHP_MINOR " -lt 5 ]] ; then
BASE_MODULES = " ${ BASE_MODULES } ,opcache"
fi
# Extended default modules - commonly needed by web applications
# These cover ~90% of typical use cases without bloat
local EXTENDED_MODULES = "mysql,sqlite3,pgsql,redis,imagick,bz2,apcu"
local COMBINED_MODULES = " ${ BASE_MODULES } , ${ EXTENDED_MODULES } "
2025-04-29 16:04:29 +02:00
2025-09-11 12:44:13 +02:00
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 } "
2025-05-05 15:47:43 +02:00
2026-01-30 10:29:51 +01:00
# Merge with user-defined modules
2025-09-11 12:44:13 +02:00
if [[ -n " $PHP_MODULE " ]] ; then
2026-01-30 10:29:51 +01:00
COMBINED_MODULES = " ${ COMBINED_MODULES } , ${ PHP_MODULE } "
2025-05-05 15:47:43 +02:00
fi
2025-06-18 12:07:08 +02:00
2026-01-30 10:29:51 +01:00
# Filter out built-in modules (they don't have separate packages)
local FILTERED_MODULES = ""
IFS = ',' read -ra ALL_MODULES <<< " $COMBINED_MODULES "
for mod in " ${ ALL_MODULES [@] } " ; do
mod = $( echo " $mod " | tr -d '[:space:]' )
[[ -z " $mod " ]] && continue
# Skip if it's a known built-in module
if echo ", $BUILTIN_MODULES , $BUILTIN_85 ," | grep -qi ", $mod ," ; then
continue
fi
# Add to filtered list
if [[ -z " $FILTERED_MODULES " ]] ; then
FILTERED_MODULES = " $mod "
else
FILTERED_MODULES = " ${ FILTERED_MODULES } , $mod "
fi
done
2025-09-11 12:44:13 +02:00
# Deduplicate
2026-01-30 10:29:51 +01:00
COMBINED_MODULES = $( echo " $FILTERED_MODULES " | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -)
2025-05-05 15:47:43 +02:00
2025-09-11 12:44:13 +02:00
# Get current PHP-CLI version
local CURRENT_PHP = ""
2025-11-05 05:19:35 -08:00
CURRENT_PHP = $( is_tool_installed "php" 2>/dev/null) || true
2025-06-18 12:07:08 +02:00
2025-12-04 07:52:18 +01:00
# Remove conflicting PHP version before pinning
2025-11-22 13:08:30 +01:00
if [[ -n " $CURRENT_PHP " && " $CURRENT_PHP " != " $PHP_VERSION " ]] ; then
msg_info "Removing conflicting PHP ${ CURRENT_PHP } (need ${ PHP_VERSION } )"
stop_all_services "php.*-fpm"
2025-12-04 07:52:18 +01:00
$STD apt purge -y "php*" 2>/dev/null || true
$STD apt autoremove -y 2>/dev/null || true
2025-11-22 13:08:30 +01:00
fi
2025-11-05 05:19:35 -08:00
2025-11-22 13:08:30 +01:00
# NOW create pinning for the desired version
mkdir -p /etc/apt/preferences.d
cat <<EOF >/etc/apt/preferences.d/php-pin
2025-11-22 13:02:53 +01:00
Package: php${PHP_VERSION}*
Pin: version ${PHP_VERSION}.*
Pin-Priority: 1001
Package: php[0-9].*
Pin: release o=packages.sury.org-php
Pin-Priority: -1
EOF
2025-11-22 13:08:30 +01:00
# Setup repository
prepare_repository_setup "php" "deb.sury.org-php" || {
msg_error "Failed to prepare PHP repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-22 13:08:30 +01:00
}
2025-11-22 13:02:53 +01:00
2026-01-21 15:39:15 +01:00
# Use different repository based on OS
if [[ " $DISTRO_ID " == "ubuntu" ]] ; then
# Ubuntu: Use ondrej/php PPA
msg_info "Adding ondrej/php PPA for Ubuntu"
2026-02-27 13:59:02 +01:00
$STD apt install -y software-properties-common || {
msg_error "Failed to install software-properties-common"
2026-03-26 16:07:38 +01:00
return 100
2026-02-27 13:59:02 +01:00
}
2026-01-28 11:09:30 +01:00
# Don't use $STD for add-apt-repository as it uses background processes
add-apt-repository -y ppa:ondrej/php >>" $( get_active_logfile) " 2>& 1
2026-01-21 15:39:15 +01:00
else
# Debian: Use Sury repository
manage_tool_repository "php" " $PHP_VERSION " "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || {
msg_error "Failed to setup PHP repository"
2026-03-26 16:07:38 +01:00
return 100
2026-01-21 15:39:15 +01:00
}
fi
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-02-27 13:59:02 +01:00
$STD apt update || {
msg_warn "apt update failed after PHP repository setup"
}
2025-05-05 15:47:43 +02:00
2025-11-22 12:52:47 +01:00
# Get available PHP version from repository
local AVAILABLE_PHP_VERSION = ""
2025-12-18 18:38:08 +01:00
AVAILABLE_PHP_VERSION = $( apt-cache show "php ${ PHP_VERSION } " 2>/dev/null | grep -m1 "^Version:" | awk '{print $2}' 2>/dev/null | cut -d- -f1 || true )
2025-11-22 12:52:47 +01:00
if [[ -z " $AVAILABLE_PHP_VERSION " ]] ; then
msg_error "PHP ${ PHP_VERSION } not found in configured repositories"
2026-03-26 16:07:38 +01:00
return 100
2025-11-22 12:52:47 +01:00
fi
2026-01-30 10:29:51 +01:00
# Build module list - verify each package exists before adding
2025-11-22 13:20:18 +01:00
local MODULE_LIST = "php ${ PHP_VERSION } "
2026-01-30 10:29:51 +01:00
local SKIPPED_MODULES = ""
2025-11-22 12:52:47 +01:00
2025-09-11 12:44:13 +02:00
IFS = ',' read -ra MODULES <<< " $COMBINED_MODULES "
for mod in " ${ MODULES [@] } " ; do
2026-01-30 10:29:51 +01:00
mod = $( echo " $mod " | tr -d '[:space:]' )
[[ -z " $mod " ]] && continue
local pkg_name = "php ${ PHP_VERSION } - ${ mod } "
# Check if package exists in repository
if apt-cache show " $pkg_name " & >/dev/null; then
MODULE_LIST += " $pkg_name "
else
# Package doesn't exist - could be built-in or renamed
if [[ -z " $SKIPPED_MODULES " ]] ; then
SKIPPED_MODULES = " $mod "
else
SKIPPED_MODULES = " ${ SKIPPED_MODULES } , $mod "
fi
fi
2025-09-11 12:44:13 +02:00
done
2025-11-22 12:52:47 +01:00
2026-01-30 10:29:51 +01:00
# Log skipped modules (informational, not an error)
if [[ -n " $SKIPPED_MODULES " ]] ; then
msg_info "Skipping unavailable/built-in modules: $SKIPPED_MODULES "
fi
2025-09-11 12:44:13 +02:00
if [[ " $PHP_FPM " == "YES" ]] ; then
2026-01-30 10:29:51 +01:00
if apt-cache show "php ${ PHP_VERSION } -fpm" & >/dev/null; then
MODULE_LIST += " php ${ PHP_VERSION } -fpm"
else
msg_warn "php ${ PHP_VERSION } -fpm not available"
fi
2026-01-28 11:39:48 +01:00
# Create systemd override for PHP-FPM to fix runtime directory issues in LXC containers
mkdir -p /etc/systemd/system/php${ PHP_VERSION } -fpm.service.d/
cat <<EOF >/etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf
[Service]
RuntimeDirectory=php
RuntimeDirectoryMode=0755
EOF
$STD systemctl daemon-reload
2025-06-18 12:07:08 +02:00
fi
2025-09-11 12:44:13 +02:00
# install apache2 with PHP support if requested
if [[ " $PHP_APACHE " == "YES" ]] ; then
2025-11-05 05:19:35 -08:00
if ! dpkg -l 2>/dev/null | grep -q "libapache2-mod-php ${ PHP_VERSION } " ; then
2025-11-22 12:52:47 +01:00
msg_info "Installing Apache with PHP ${ PHP_VERSION } module"
install_packages_with_retry "apache2" || {
msg_error "Failed to install Apache"
2026-03-26 16:07:38 +01:00
return 100
2025-10-22 06:25:26 -07:00
}
2025-11-22 13:20:18 +01:00
install_packages_with_retry "libapache2-mod-php ${ PHP_VERSION } " || {
2025-11-22 12:52:47 +01:00
msg_warn "Failed to install libapache2-mod-php ${ PHP_VERSION } , continuing without Apache module"
}
2025-09-11 12:44:13 +02:00
fi
2025-06-18 12:07:08 +02:00
fi
2025-11-22 13:20:18 +01:00
# Install PHP packages (pinning via preferences.d ensures correct version)
2025-11-22 13:23:40 +01:00
msg_info "Installing PHP ${ PHP_VERSION } packages"
2026-01-30 10:29:51 +01:00
# First attempt: Install all verified packages at once
if ! $STD apt install -y $MODULE_LIST 2>/dev/null; then
msg_warn "Bulk installation failed, attempting individual installation"
2025-11-22 13:23:40 +01:00
2025-11-22 13:08:30 +01:00
# Install main package first (critical)
2026-01-30 10:29:51 +01:00
if ! $STD apt install -y "php ${ PHP_VERSION } " 2>/dev/null; then
2025-11-22 12:52:47 +01:00
msg_error "Failed to install php ${ PHP_VERSION } "
2026-03-26 16:07:38 +01:00
return 100
2026-01-30 10:29:51 +01:00
fi
2025-11-22 12:52:47 +01:00
2025-11-22 13:08:30 +01:00
# Try to install Apache module individually if requested
if [[ " $PHP_APACHE " == "YES" ]] ; then
2026-01-30 10:29:51 +01:00
$STD apt install -y "libapache2-mod-php ${ PHP_VERSION } " 2>/dev/null || {
2025-11-22 13:23:40 +01:00
msg_warn "Could not install libapache2-mod-php ${ PHP_VERSION } "
}
2025-11-22 13:08:30 +01:00
fi
2026-01-30 10:29:51 +01:00
# Try to install each package individually
for pkg in $MODULE_LIST ; do
2026-02-22 11:54:21 +01:00
[[ " $pkg " == "php ${ PHP_VERSION } " ]] && continue # Already installed
2026-01-30 10:29:51 +01:00
$STD apt install -y " $pkg " 2>/dev/null || {
msg_warn "Could not install $pkg - continuing without it"
}
2025-11-22 12:52:47 +01:00
done
fi
2025-10-22 06:25:26 -07:00
cache_installed_version "php" " $PHP_VERSION "
2025-04-29 16:04:29 +02:00
2025-11-05 05:19:35 -08:00
# Patch all relevant php.ini files
2025-09-11 12:44:13 +02:00
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
2025-10-22 06:25:26 -07:00
$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 "
2025-06-18 12:07:08 +02:00
fi
2025-09-11 12:44:13 +02:00
done
2025-06-18 12:07:08 +02:00
2025-11-05 05:19:35 -08:00
# Patch Apache configuration if needed
2025-09-11 12:44:13 +02:00
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
2025-06-18 12:07:08 +02:00
fi
2025-09-11 12:44:13 +02:00
done
$STD a2enmod mpm_prefork
$STD a2enmod "php ${ PHP_VERSION } "
2025-10-22 06:25:26 -07:00
safe_service_restart apache2 || true
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
# Enable and restart PHP-FPM if requested
2025-09-11 12:44:13 +02:00
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
2025-10-22 06:25:26 -07:00
safe_service_restart php${ PHP_VERSION } -fpm
2025-06-18 12:07:08 +02:00
fi
fi
2025-10-22 06:25:26 -07:00
2025-11-22 12:52:47 +01:00
# Verify PHP installation - critical check
if ! command -v php >/dev/null 2>& 1; then
msg_error "PHP installation verification failed - php command not found"
2026-03-26 16:07:38 +01:00
return 127
2025-11-22 12:52:47 +01:00
fi
local INSTALLED_VERSION = $( php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2)
if [[ " $INSTALLED_VERSION " != " $PHP_VERSION " ]] ; then
msg_error "PHP version mismatch: requested ${ PHP_VERSION } but got ${ INSTALLED_VERSION } "
msg_error "This indicates a critical package installation issue"
# Don't cache wrong version
2026-03-26 16:07:38 +01:00
return 127
2025-11-22 12:52:47 +01:00
fi
cache_installed_version "php" " $INSTALLED_VERSION "
msg_ok "Setup PHP ${ INSTALLED_VERSION } "
2025-04-29 16:04:29 +02:00
}
2025-04-29 16:20:23 +02:00
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# Installs or upgrades PostgreSQL and optional extensions/modules.
2025-05-15 15:18:41 +02:00
#
# Description:
2026-03-02 08:44:59 +01:00
# - By default uses distro repository (Debian/Ubuntu apt) for stability
# - Optionally uses official PGDG repository for specific versions
2025-09-11 12:44:13 +02:00
# - Detects existing PostgreSQL version
# - Dumps all databases before upgrade
2026-03-18 16:45:11 +01:00
# - Installs optional PG_MODULES (e.g. postgis, contrib, cron)
2025-09-11 12:44:13 +02:00
# - Restores dumped data post-upgrade
#
# Variables:
2026-03-02 10:35:30 +01:00
# USE_PGDG_REPO - Use official PGDG repository (default: true)
# Set to "false" to use distro packages instead
2026-03-02 08:44:59 +01:00
# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16)
2026-03-18 16:45:11 +01:00
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib,cron")
2026-03-02 08:44:59 +01:00
#
# Examples:
2026-03-18 16:45:11 +01:00
# setup_postgresql # Uses PGDG repo, PG 16
# PG_VERSION="17" setup_postgresql # Specific version from PGDG
# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead
# PG_VERSION="17" PG_MODULES="cron" setup_postgresql # With pg_cron module
2026-03-02 08:44:59 +01:00
# ------------------------------------------------------------------------------
2026-03-18 16:45:11 +01:00
# Internal helper: Configure shared_preload_libraries for pg_cron
_configure_pg_cron_preload() {
local modules = " ${ 1 :- } "
[[ -z " $modules " ]] && return 0
if [[ ", $modules ," == *",cron," * ]] ; then
local current_libs
current_libs = $( sudo -u postgres psql -tAc "SHOW shared_preload_libraries;" 2>/dev/null || echo "" )
if [[ " $current_libs " != *"pg_cron" * ]] ; then
local new_libs = " ${ current_libs :+ ${ current_libs } , } pg_cron"
$STD sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = ' ${ new_libs } ';"
$STD systemctl restart postgresql
fi
fi
}
setup_postgresql() {
2025-09-11 12:44:13 +02:00
local PG_VERSION = " ${ PG_VERSION :- 16 } "
local PG_MODULES = " ${ PG_MODULES :- } "
2026-03-02 10:29:04 +01:00
local USE_PGDG_REPO = " ${ USE_PGDG_REPO :- true } "
2025-10-22 06:25:26 -07:00
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)
2025-05-15 15:18:41 +02:00
2026-01-12 08:48:36 +01:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2025-11-05 05:19:35 -08:00
# Get currently installed version
local CURRENT_PG_VERSION = ""
2025-09-11 12:44:13 +02:00
if command -v psql >/dev/null; then
2025-11-05 05:19:35 -08:00
CURRENT_PG_VERSION = " $( psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1) "
2025-05-05 15:47:43 +02:00
fi
2026-03-02 08:44:59 +01:00
# Scenario 1: Use distro repository (default, most stable)
if [[ " $USE_PGDG_REPO " != "true" && " $USE_PGDG_REPO " != "TRUE" && " $USE_PGDG_REPO " != "1" ]] ; then
msg_info "Setup PostgreSQL (distro package)"
# If already installed, just update
if [[ -n " $CURRENT_PG_VERSION " ]] ; then
msg_info "Update PostgreSQL $CURRENT_PG_VERSION "
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
upgrade_packages_with_retry "postgresql" "postgresql-client" || true
cache_installed_version "postgresql" " $CURRENT_PG_VERSION "
msg_ok "Update PostgreSQL $CURRENT_PG_VERSION "
# Still install modules if specified
if [[ -n " $PG_MODULES " ]] ; then
IFS = ',' read -ra MODULES <<< " $PG_MODULES "
for module in " ${ MODULES [@] } " ; do
$STD apt install -y "postgresql- ${ CURRENT_PG_VERSION } - ${ module } " 2>/dev/null || true
done
fi
2026-03-18 16:45:11 +01:00
_configure_pg_cron_preload " $PG_MODULES "
2026-03-02 08:44:59 +01:00
return 0
fi
# Fresh install from distro repo
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
export DEBIAN_FRONTEND = noninteractive
install_packages_with_retry "postgresql" "postgresql-client" || {
msg_error "Failed to install PostgreSQL from distro repository"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
# Get installed version
local INSTALLED_VERSION = ""
if command -v psql >/dev/null; then
INSTALLED_VERSION = " $( psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1) "
fi
$STD systemctl enable --now postgresql 2>/dev/null || true
# Add PostgreSQL binaries to PATH
if [[ -n " $INSTALLED_VERSION " ]] && ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/' " ${ INSTALLED_VERSION } " '/bin"' >/etc/environment
fi
cache_installed_version "postgresql" " ${ INSTALLED_VERSION :- distro } "
msg_ok "Setup PostgreSQL ${ INSTALLED_VERSION :- from distro } "
# Install optional modules
if [[ -n " $PG_MODULES " && -n " $INSTALLED_VERSION " ]] ; then
IFS = ',' read -ra MODULES <<< " $PG_MODULES "
for module in " ${ MODULES [@] } " ; do
$STD apt install -y "postgresql- ${ INSTALLED_VERSION } - ${ module } " 2>/dev/null || true
done
fi
2026-03-18 16:45:11 +01:00
_configure_pg_cron_preload " $PG_MODULES "
2026-03-02 08:44:59 +01:00
return 0
fi
# Scenario 2: Use official PGDG repository (USE_PGDG_REPO=true)
# Scenario 2a: Already at correct version
2025-11-05 05:19:35 -08:00
if [[ " $CURRENT_PG_VERSION " == " $PG_VERSION " ]] ; then
msg_info "Update PostgreSQL $PG_VERSION "
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Perform upgrade with retry logic (non-fatal if fails)
upgrade_packages_with_retry "postgresql- ${ PG_VERSION } " "postgresql-client- ${ PG_VERSION } " 2>/dev/null || true
cache_installed_version "postgresql" " $PG_VERSION "
msg_ok "Update PostgreSQL $PG_VERSION "
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Still install modules if specified
if [[ -n " $PG_MODULES " ]] ; then
IFS = ',' read -ra MODULES <<< " $PG_MODULES "
for module in " ${ MODULES [@] } " ; do
$STD apt install -y "postgresql- ${ PG_VERSION } - ${ module } " 2>/dev/null || true
done
fi
2026-03-18 16:45:11 +01:00
_configure_pg_cron_preload " $PG_MODULES "
2025-11-05 05:19:35 -08:00
return 0
fi
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Scenario 2: Different version - backup, remove old, install new
if [[ -n " $CURRENT_PG_VERSION " ]] ; then
msg_info "Upgrade PostgreSQL from $CURRENT_PG_VERSION to $PG_VERSION "
msg_info "Creating backup of PostgreSQL $CURRENT_PG_VERSION databases..."
2026-03-02 08:44:59 +01:00
local PG_BACKUP_FILE = "/var/lib/postgresql/backup_ $( date +%F) _v ${ CURRENT_PG_VERSION } .sql"
$STD runuser -u postgres -- pg_dumpall >" $PG_BACKUP_FILE " || {
2025-11-05 05:19:35 -08:00
msg_error "Failed to backup PostgreSQL databases"
2026-03-26 16:07:38 +01:00
return 150
2025-11-05 05:19:35 -08:00
}
$STD systemctl stop postgresql || true
$STD apt purge -y "postgresql- ${ CURRENT_PG_VERSION } " "postgresql-client- ${ CURRENT_PG_VERSION } " 2>/dev/null || true
else
msg_info "Setup PostgreSQL $PG_VERSION "
fi
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Scenario 3: Fresh install or after removal - setup repo and install
prepare_repository_setup "pgdg" "postgresql" || {
msg_error "Failed to prepare PostgreSQL repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
local SUITE
case " $DISTRO_CODENAME " in
trixie | forky | sid)
2026-03-18 16:45:11 +01:00
2025-11-05 05:19:35 -08:00
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg" ; then
SUITE = "trixie-pgdg"
2026-03-18 16:45:11 +01:00
2025-11-05 05:19:35 -08:00
else
2026-03-04 08:39:33 +01:00
msg_warn "PGDG repo not available for ${ DISTRO_CODENAME } , falling back to distro packages"
USE_PGDG_REPO = false setup_postgresql
return $?
2025-11-17 09:42:02 +01:00
fi
2026-03-18 16:45:11 +01:00
2025-11-05 05:19:35 -08:00
;;
*)
SUITE = $( get_fallback_suite " $DISTRO_ID " " $DISTRO_CODENAME " "https://apt.postgresql.org/pub/repos/apt" )
SUITE = " ${ SUITE } -pgdg"
;;
esac
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
setup_deb822_repo \
"pgdg" \
"https://www.postgresql.org/media/keys/ACCC4CF8.asc" \
"https://apt.postgresql.org/pub/repos/apt" \
" $SUITE " \
2025-11-07 02:49:16 -08:00
"main"
2025-05-15 15:18:41 +02:00
2025-11-05 05:19:35 -08:00
if ! $STD apt update; then
msg_error "APT update failed for PostgreSQL repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
# Install ssl-cert dependency if available
if apt-cache search "^ssl-cert $" 2>/dev/null | grep -q .; then
$STD apt install -y ssl-cert 2>/dev/null || true
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
# Try multiple PostgreSQL package patterns with retry logic
local pg_install_success = false
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
if apt-cache search "^postgresql- ${ PG_VERSION } $" 2>/dev/null | grep -q . &&
install_packages_with_retry "postgresql- ${ PG_VERSION } " "postgresql-client- ${ PG_VERSION } " ; then
pg_install_success = true
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
if [[ " $pg_install_success " == false ]] &&
apt-cache search "^postgresql-server- ${ PG_VERSION } $" 2>/dev/null | grep -q . &&
$STD apt install -y "postgresql-server- ${ PG_VERSION } " "postgresql-client- ${ PG_VERSION } " 2>/dev/null; then
pg_install_success = true
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
if [[ " $pg_install_success " == false ]] &&
apt-cache search "^postgresql $" 2>/dev/null | grep -q . &&
$STD apt install -y postgresql postgresql-client 2>/dev/null; then
pg_install_success = true
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
if [[ " $pg_install_success " == false ]] ; then
msg_error "PostgreSQL package not available for suite ${ SUITE } "
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
if ! command -v psql >/dev/null 2>& 1; then
msg_error "PostgreSQL installed but psql command not found"
2026-03-26 16:07:38 +01:00
return 127
2025-11-05 05:19:35 -08:00
fi
2025-05-05 15:47:43 +02:00
2025-11-05 05:19:35 -08:00
# Restore database backup if we upgraded from previous version
2026-03-02 08:44:59 +01:00
if [[ -n " $CURRENT_PG_VERSION " && -n " ${ PG_BACKUP_FILE :- } " && -f " ${ PG_BACKUP_FILE } " ]] ; then
2025-11-05 05:19:35 -08:00
msg_info "Restoring PostgreSQL databases from backup..."
2026-03-02 08:44:59 +01:00
$STD runuser -u postgres -- psql <" $PG_BACKUP_FILE " 2>/dev/null || {
2025-11-05 05:19:35 -08:00
msg_warn "Failed to restore database backup - this may be expected for major version upgrades"
}
fi
2025-09-11 12:44:13 +02:00
2026-02-27 13:59:02 +01:00
$STD systemctl enable --now postgresql 2>/dev/null || {
msg_warn "Failed to enable/start PostgreSQL service"
}
2025-11-05 05:19:35 -08:00
# Add PostgreSQL binaries to PATH
if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/' " ${ PG_VERSION } " '/bin"' >/etc/environment
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
cache_installed_version "postgresql" " $PG_VERSION "
msg_ok "Setup PostgreSQL $PG_VERSION "
# Install optional modules
2025-09-11 12:44:13 +02:00
if [[ -n " $PG_MODULES " ]] ; then
IFS = ',' read -ra MODULES <<< " $PG_MODULES "
for module in " ${ MODULES [@] } " ; do
2026-02-27 13:59:02 +01:00
$STD apt install -y "postgresql- ${ PG_VERSION } - ${ module } " 2>/dev/null || {
msg_warn "Failed to install PostgreSQL module: ${ module } "
}
2025-09-11 12:44:13 +02:00
done
2025-05-05 15:47:43 +02:00
fi
2026-03-18 16:45:11 +01:00
_configure_pg_cron_preload " $PG_MODULES "
2025-04-29 16:20:23 +02:00
}
2025-05-15 15:18:41 +02:00
2025-11-10 18:51:02 +01:00
# ------------------------------------------------------------------------------
# Creates PostgreSQL database with user and optional extensions
#
# Description:
# - Creates PostgreSQL role with login and password
# - Creates database with UTF8 encoding and template0
# - Installs optional extensions (postgis, pgvector, etc.)
# - Configures ALTER ROLE settings for Django/Rails compatibility
# - Saves credentials to file
# - Exports variables for use in calling script
#
# Usage:
# PG_DB_NAME="myapp_db" PG_DB_USER="myapp_user" setup_postgresql_db
# PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
# PG_DB_NAME="ghostfolio" PG_DB_USER="ghostfolio" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
# PG_DB_NAME="adventurelog" PG_DB_USER="adventurelog" PG_DB_EXTENSIONS="postgis" setup_postgresql_db
2026-03-18 16:45:11 +01:00
# PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
2025-11-10 18:51:02 +01:00
#
# Variables:
# PG_DB_NAME - Database name (required)
# PG_DB_USER - Database user (required)
# PG_DB_PASS - Database password (optional, auto-generated if empty)
# PG_DB_EXTENSIONS - Comma-separated list of extensions (optional, e.g. "postgis,pgvector")
# PG_DB_GRANT_SUPERUSER - Grant SUPERUSER privilege (optional, "true" to enable, security risk!)
# PG_DB_SCHEMA_PERMS - Grant schema-level permissions (optional, "true" to enable)
# PG_DB_SKIP_ALTER_ROLE - Skip ALTER ROLE settings (optional, "true" to skip)
# PG_DB_CREDS_FILE - Credentials file path (optional, default: ~/${APPLICATION}.creds)
#
# Exports:
# PG_DB_NAME, PG_DB_USER, PG_DB_PASS - For use in calling script
# ------------------------------------------------------------------------------
function setup_postgresql_db() {
# Validation
if [[ -z " ${ PG_DB_NAME :- } " || -z " ${ PG_DB_USER :- } " ]] ; then
msg_error "PG_DB_NAME and PG_DB_USER must be set before calling setup_postgresql_db"
2026-03-26 16:07:38 +01:00
return 65
2025-11-10 18:51:02 +01:00
fi
# Generate password if not provided
if [[ -z " ${ PG_DB_PASS :- } " ]] ; then
PG_DB_PASS = $( openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
fi
msg_info "Setting up PostgreSQL Database"
$STD sudo -u postgres psql -c "CREATE ROLE $PG_DB_USER WITH LOGIN PASSWORD ' $PG_DB_PASS ';"
$STD sudo -u postgres psql -c "CREATE DATABASE $PG_DB_NAME WITH OWNER $PG_DB_USER ENCODING 'UTF8' TEMPLATE template0;"
2026-03-18 16:45:11 +01:00
# Configure pg_cron database BEFORE creating the extension (must be set before pg_cron loads)
if [[ -n " ${ PG_DB_EXTENSIONS :- } " ]] && [[ ", ${ PG_DB_EXTENSIONS //[[: space :]]/ } ," == *",pg_cron," * ]] ; then
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.database_name = ' ${ PG_DB_NAME } ';"
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.timezone = 'UTC';"
$STD systemctl restart postgresql
fi
2025-11-10 18:51:02 +01:00
# Install extensions (comma-separated)
if [[ -n " ${ PG_DB_EXTENSIONS :- } " ]] ; then
IFS = ',' read -ra EXT_LIST <<< " ${ PG_DB_EXTENSIONS :- } "
for ext in " ${ EXT_LIST [@] } " ; do
ext = $( echo " $ext " | xargs) # Trim whitespace
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "CREATE EXTENSION IF NOT EXISTS $ext ;"
done
fi
2026-03-18 16:45:11 +01:00
# Grant pg_cron schema permissions to DB user
if [[ -n " ${ PG_DB_EXTENSIONS :- } " ]] && [[ ", ${ PG_DB_EXTENSIONS //[[: space :]]/ } ," == *",pg_cron," * ]] ; then
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "GRANT USAGE ON SCHEMA cron TO ${ PG_DB_USER } ;"
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "GRANT ALL ON ALL TABLES IN SCHEMA cron TO ${ PG_DB_USER } ;"
fi
2025-11-10 18:51:02 +01:00
# ALTER ROLE settings for Django/Rails compatibility (unless skipped)
if [[ " ${ PG_DB_SKIP_ALTER_ROLE :- } " != "true" ]] ; then
$STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET client_encoding TO 'utf8';"
$STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET default_transaction_isolation TO 'read committed';"
$STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET timezone TO 'UTC';"
fi
# Schema permissions (if requested)
if [[ " ${ PG_DB_SCHEMA_PERMS :- } " == "true" ]] ; then
$STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $PG_DB_NAME TO $PG_DB_USER ;"
$STD sudo -u postgres psql -c "ALTER USER $PG_DB_USER CREATEDB;"
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "GRANT ALL ON SCHEMA public TO $PG_DB_USER ;"
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "GRANT CREATE ON SCHEMA public TO $PG_DB_USER ;"
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $PG_DB_USER ;"
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $PG_DB_USER ;"
fi
# Superuser grant (if requested - WARNING!)
if [[ " ${ PG_DB_GRANT_SUPERUSER :- } " == "true" ]] ; then
msg_warn "Granting SUPERUSER privilege (security risk!)"
$STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $PG_DB_NAME to $PG_DB_USER ;"
$STD sudo -u postgres psql -c "ALTER USER $PG_DB_USER WITH SUPERUSER;"
fi
# Save credentials
2025-11-17 16:52:11 +01:00
local app_name = " ${ APPLICATION ,, } "
local CREDS_FILE = " ${ PG_DB_CREDS_FILE :- ${ HOME } / ${ app_name } .creds } "
2025-11-10 18:51:02 +01:00
{
echo "PostgreSQL Credentials"
echo "Database: $PG_DB_NAME "
echo "User: $PG_DB_USER "
echo "Password: $PG_DB_PASS "
} >>" $CREDS_FILE "
msg_ok "Set up PostgreSQL Database"
# Export for use in calling script
export PG_DB_NAME
export PG_DB_USER
export PG_DB_PASS
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
# 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)
# ------------------------------------------------------------------------------
2025-06-18 12:07:08 +02:00
function setup_ruby() {
2025-05-15 15:18:41 +02:00
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"
2025-10-22 06:25:26 -07:00
local TMP_DIR = $( mktemp -d)
2025-05-15 15:18:41 +02:00
2025-11-05 05:19:35 -08:00
# Get currently installed Ruby version
local CURRENT_RUBY_VERSION = ""
if [[ -x " $RBENV_BIN " ]] ; then
CURRENT_RUBY_VERSION = $( " $RBENV_BIN " global 2>/dev/null || echo "" )
fi
# Scenario 1: Already at correct Ruby version
if [[ " $CURRENT_RUBY_VERSION " == " $RUBY_VERSION " ]] ; then
msg_info "Update Ruby $RUBY_VERSION "
cache_installed_version "ruby" " $RUBY_VERSION "
msg_ok "Update Ruby $RUBY_VERSION "
return 0
fi
# Scenario 2: Different version - reinstall
if [[ -n " $CURRENT_RUBY_VERSION " ]] ; then
msg_info "Upgrade Ruby from $CURRENT_RUBY_VERSION to $RUBY_VERSION "
else
msg_info "Setup Ruby $RUBY_VERSION "
fi
2025-05-15 15:18:41 +02:00
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Install build dependencies with fallbacks
2025-10-22 06:25:26 -07:00
local ruby_deps =()
local dep_variations =(
"jq"
"autoconf"
"patch"
"build-essential"
"libssl-dev"
"libyaml-dev"
"libreadline-dev|libreadline6-dev"
"zlib1g-dev"
"libgmp-dev"
"libncurses-dev|libncurses5-dev"
"libffi-dev"
"libgdbm-dev"
"libdb-dev"
"uuid-dev"
)
for dep_pattern in " ${ dep_variations [@] } " ; do
if [[ " $dep_pattern " == *"|" * ]] ; then
IFS = '|' read -ra variations <<< " $dep_pattern "
for var in " ${ variations [@] } " ; do
if apt-cache search "^ ${ var } $" 2>/dev/null | grep -q .; then
ruby_deps +=( " $var " )
break
fi
done
else
if apt-cache search "^ ${ dep_pattern } $" 2>/dev/null | grep -q .; then
ruby_deps +=( " $dep_pattern " )
fi
fi
done
if [[ ${# ruby_deps [@] } -gt 0 ]] ; then
$STD apt install -y " ${ ruby_deps [@] } " 2>/dev/null || true
else
msg_error "No Ruby build dependencies available"
2025-05-15 15:18:41 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 100
2025-05-15 15:18:41 +02:00
fi
2025-11-05 05:19:35 -08:00
# Download and build rbenv if needed
if [[ ! -x " $RBENV_BIN " ]] ; then
local RBENV_RELEASE
local rbenv_json
rbenv_json = $( curl -fsSL --max-time 15 https://api.github.com/repos/rbenv/rbenv/releases/latest 2>/dev/null || echo "" )
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
if [[ -z " $rbenv_json " ]] ; then
msg_error "Failed to fetch latest rbenv version from GitHub"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 7
2025-11-05 05:19:35 -08:00
fi
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
RBENV_RELEASE = $( echo " $rbenv_json " | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "" )
2025-05-15 15:18:41 +02:00
2025-11-05 05:19:35 -08:00
if [[ -z " $RBENV_RELEASE " ]] ; then
msg_error "Could not parse rbenv version from GitHub response"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
fi
2026-03-02 08:44:59 +01:00
if ! curl_with_retry "https://github.com/rbenv/rbenv/archive/refs/tags/v ${ RBENV_RELEASE } .tar.gz" " $TMP_DIR /rbenv.tar.gz" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download rbenv"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
tar -xzf " $TMP_DIR /rbenv.tar.gz" -C " $TMP_DIR " || {
msg_error "Failed to extract rbenv"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-11-05 05:19:35 -08:00
}
mkdir -p " $RBENV_DIR "
cp -r " $TMP_DIR /rbenv- ${ RBENV_RELEASE } /." " $RBENV_DIR /"
( cd " $RBENV_DIR " && src/configure && $STD make -C src) || {
msg_error "Failed to build rbenv"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-11-05 05:19:35 -08:00
}
# Setup profile
if ! grep -q 'rbenv init' " $PROFILE_FILE " 2>/dev/null; then
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>" $PROFILE_FILE "
echo 'eval "$(rbenv init -)"' >>" $PROFILE_FILE "
fi
2025-05-15 15:18:41 +02:00
fi
2025-11-05 05:19:35 -08:00
# Install ruby-build plugin
if [[ ! -d " $RBENV_DIR /plugins/ruby-build" ]] ; then
local RUBY_BUILD_RELEASE
local ruby_build_json
ruby_build_json = $( curl -fsSL --max-time 15 https://api.github.com/repos/rbenv/ruby-build/releases/latest 2>/dev/null || echo "" )
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
if [[ -z " $ruby_build_json " ]] ; then
msg_error "Failed to fetch latest ruby-build version from GitHub"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 7
2025-11-05 05:19:35 -08:00
fi
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
RUBY_BUILD_RELEASE = $( echo " $ruby_build_json " | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "" )
2025-05-15 15:18:41 +02:00
2025-11-05 05:19:35 -08:00
if [[ -z " $RUBY_BUILD_RELEASE " ]] ; then
msg_error "Could not parse ruby-build version from GitHub response"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
fi
2026-03-02 08:44:59 +01:00
if ! curl_with_retry "https://github.com/rbenv/ruby-build/archive/refs/tags/v ${ RUBY_BUILD_RELEASE } .tar.gz" " $TMP_DIR /ruby-build.tar.gz" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download ruby-build"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
tar -xzf " $TMP_DIR /ruby-build.tar.gz" -C " $TMP_DIR " || {
msg_error "Failed to extract ruby-build"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 251
2025-11-05 05:19:35 -08:00
}
mkdir -p " $RBENV_DIR /plugins/ruby-build"
cp -r " $TMP_DIR /ruby-build- ${ RUBY_BUILD_RELEASE } /." " $RBENV_DIR /plugins/ruby-build/"
2025-05-15 15:18:41 +02:00
fi
2025-11-05 05:19:35 -08:00
# Setup PATH and install Ruby version
2025-05-15 15:18:41 +02:00
export PATH = " $RBENV_DIR /bin: $PATH "
2025-11-05 05:19:35 -08:00
eval " $( " $RBENV_BIN " init - bash) " 2>/dev/null || true
2025-05-15 15:18:41 +02:00
2025-11-05 05:19:35 -08:00
if ! " $RBENV_BIN " versions --bare 2>/dev/null | grep -qx " $RUBY_VERSION " ; then
2025-10-22 06:25:26 -07:00
$STD " $RBENV_BIN " install " $RUBY_VERSION " || {
msg_error "Failed to install Ruby $RUBY_VERSION "
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-10-22 06:25:26 -07:00
}
2025-05-15 15:18:41 +02:00
fi
2025-11-05 05:19:35 -08:00
" $RBENV_BIN " global " $RUBY_VERSION " || {
msg_error "Failed to set Ruby $RUBY_VERSION as global version"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 150
2025-11-05 05:19:35 -08:00
}
2025-05-15 15:18:41 +02:00
hash -r
2025-11-05 05:19:35 -08:00
# Install Rails if requested
2025-05-15 15:18:41 +02:00
if [[ " $RUBY_INSTALL_RAILS " == "true" ]] ; then
2025-10-22 06:25:26 -07:00
$STD gem install rails || {
2025-11-05 05:19:35 -08:00
msg_warn "Failed to install Rails - Ruby installation successful"
2025-10-22 06:25:26 -07:00
}
2025-05-15 15:18:41 +02:00
fi
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2025-10-22 06:25:26 -07:00
cache_installed_version "ruby" " $RUBY_VERSION "
2025-09-11 12:44:13 +02:00
msg_ok "Setup Ruby $RUBY_VERSION "
2025-05-18 09:27:55 +02:00
}
2025-05-27 14:29:50 +02:00
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-01-28 11:08:52 +01:00
# Installs or updates MeiliSearch search engine.
#
# Description:
# - Fresh install: Downloads binary, creates config/service, starts
# - Update: Checks for new release, updates binary if available
# - Waits for service to be ready before returning
# - Exports API keys for use by caller
#
# Variables:
# MEILISEARCH_BIND - Bind address (default: 127.0.0.1:7700)
# MEILISEARCH_ENV - Environment: production/development (default: production)
# MEILISEARCH_DB_PATH - Database path (default: /var/lib/meilisearch/data)
#
# Exports:
# MEILISEARCH_MASTER_KEY - The master key for admin access
# MEILISEARCH_API_KEY - The default search API key
# MEILISEARCH_API_KEY_UID - The UID of the default API key
#
# Example (install script):
# setup_meilisearch
#
# Example (CT update_script):
# setup_meilisearch
# ------------------------------------------------------------------------------
function setup_meilisearch() {
local MEILISEARCH_BIND = " ${ MEILISEARCH_BIND :- 127 .0.0.1: 7700 } "
local MEILISEARCH_ENV = " ${ MEILISEARCH_ENV :- production } "
local MEILISEARCH_DB_PATH = " ${ MEILISEARCH_DB_PATH :- /var/lib/meilisearch/data } "
local MEILISEARCH_DUMP_DIR = " ${ MEILISEARCH_DUMP_DIR :- /var/lib/meilisearch/dumps } "
local MEILISEARCH_SNAPSHOT_DIR = " ${ MEILISEARCH_SNAPSHOT_DIR :- /var/lib/meilisearch/snapshots } "
# Get bind address for health checks
local MEILISEARCH_HOST = " ${ MEILISEARCH_BIND %%:* } "
local MEILISEARCH_PORT = " ${ MEILISEARCH_BIND ##*: } "
[[ " $MEILISEARCH_HOST " == "0.0.0.0" ]] && MEILISEARCH_HOST = "127.0.0.1"
# Update mode: MeiliSearch already installed
if [[ -f /usr/bin/meilisearch ]] ; then
if check_for_gh_release "meilisearch" "meilisearch/meilisearch" ; then
msg_info "Updating MeiliSearch"
2026-01-30 10:20:57 +01:00
# Get current and new version for compatibility check
local CURRENT_VERSION NEW_VERSION
CURRENT_VERSION = $( /usr/bin/meilisearch --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || CURRENT_VERSION = "0.0.0"
NEW_VERSION = " ${ CHECK_UPDATE_RELEASE #v } "
# Extract major.minor for comparison (Meilisearch requires dump/restore between minor versions)
local CURRENT_MAJOR_MINOR NEW_MAJOR_MINOR
CURRENT_MAJOR_MINOR = $( echo " $CURRENT_VERSION " | cut -d. -f1,2)
NEW_MAJOR_MINOR = $( echo " $NEW_VERSION " | cut -d. -f1,2)
# Determine if migration is needed (different major.minor = incompatible DB format)
local NEEDS_MIGRATION = false
if [[ " $CURRENT_MAJOR_MINOR " != " $NEW_MAJOR_MINOR " ]] ; then
NEEDS_MIGRATION = true
msg_info "MeiliSearch version change detected ( ${ CURRENT_VERSION } → ${ NEW_VERSION } ), preparing data migration"
fi
# Read config values for dump/restore
local MEILI_HOST MEILI_PORT MEILI_MASTER_KEY MEILI_DUMP_DIR
MEILI_HOST = " ${ MEILISEARCH_HOST :- 127 .0.0.1 } "
MEILI_PORT = " ${ MEILISEARCH_PORT :- 7700 } "
MEILI_DUMP_DIR = " ${ MEILISEARCH_DUMP_DIR :- /var/lib/meilisearch/dumps } "
2026-03-25 13:06:21 +01:00
MEILI_MASTER_KEY = $( grep -E "^master_key\s*=" /etc/meilisearch.toml 2>/dev/null | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' || true )
2026-01-30 10:20:57 +01:00
# Create dump before update if migration is needed
local DUMP_UID = ""
if [[ " $NEEDS_MIGRATION " == "true" ]] && [[ -n " $MEILI_MASTER_KEY " ]] ; then
msg_info "Creating MeiliSearch data dump before upgrade"
# Trigger dump creation
local DUMP_RESPONSE
DUMP_RESPONSE = $( curl -s -X POST "http:// ${ MEILI_HOST } : ${ MEILI_PORT } /dumps" \
-H "Authorization: Bearer ${ MEILI_MASTER_KEY } " \
-H "Content-Type: application/json" 2>/dev/null) || true
2026-01-30 10:29:08 +01:00
# The initial response only contains taskUid, not dumpUid
# dumpUid is only available after the task completes
local TASK_UID
TASK_UID = $( echo " $DUMP_RESPONSE " | grep -oP '"taskUid":\s*\K[0-9]+' || true )
if [[ -n " $TASK_UID " ]] ; then
msg_info "Waiting for dump task ${ TASK_UID } to complete..."
local MAX_WAIT = 120
local WAITED = 0
local TASK_RESULT = ""
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
while [[ $WAITED -lt $MAX_WAIT ]] ; do
TASK_RESULT = $( curl -s "http:// ${ MEILI_HOST } : ${ MEILI_PORT } /tasks/ ${ TASK_UID } " \
-H "Authorization: Bearer ${ MEILI_MASTER_KEY } " 2>/dev/null) || true
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
local TASK_STATUS
TASK_STATUS = $( echo " $TASK_RESULT " | grep -oP '"status":\s*"\K[^"]+' || true )
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
if [[ " $TASK_STATUS " == "succeeded" ]] ; then
# Extract dumpUid from the completed task details
DUMP_UID = $( echo " $TASK_RESULT " | grep -oP '"dumpUid":\s*"\K[^"]+' || true )
if [[ -n " $DUMP_UID " ]] ; then
2026-01-30 10:20:57 +01:00
msg_ok "MeiliSearch dump created successfully: ${ DUMP_UID } "
2026-01-30 10:29:08 +01:00
else
msg_warn "Dump task succeeded but could not extract dumpUid"
2026-01-30 10:20:57 +01:00
fi
2026-01-30 10:29:08 +01:00
break
elif [[ " $TASK_STATUS " == "failed" ]] ; then
local ERROR_MSG
ERROR_MSG = $( echo " $TASK_RESULT " | grep -oP '"message":\s*"\K[^"]+' || echo "Unknown error" )
msg_warn "MeiliSearch dump failed: ${ ERROR_MSG } "
break
2026-01-30 10:20:57 +01:00
fi
2026-01-30 10:29:08 +01:00
sleep 2
WAITED = $(( WAITED + 2 ))
done
if [[ $WAITED -ge $MAX_WAIT ]] ; then
msg_warn "MeiliSearch dump timed out after ${ MAX_WAIT } s"
2026-01-30 10:20:57 +01:00
fi
else
2026-01-30 10:29:08 +01:00
msg_warn "Could not trigger MeiliSearch dump (no taskUid in response)"
msg_info "Response was: ${ DUMP_RESPONSE :- empty } "
fi
fi
# If migration is needed but dump failed, we have options:
# 1. Abort the update (safest, but annoying)
# 2. Backup data directory and proceed (allows manual recovery)
# 3. Just proceed and hope for the best (dangerous)
# We choose option 2: backup and proceed with warning
if [[ " $NEEDS_MIGRATION " == "true" ]] && [[ -z " $DUMP_UID " ]] ; then
local MEILI_DB_PATH
2026-03-25 13:06:21 +01:00
MEILI_DB_PATH = $( grep -E "^db_path\s*=" /etc/meilisearch.toml 2>/dev/null | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' || true )
2026-01-30 10:29:08 +01:00
MEILI_DB_PATH = " ${ MEILI_DB_PATH :- /var/lib/meilisearch/data } "
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
if [[ -d " $MEILI_DB_PATH " ]] && [[ -n " $( ls -A " $MEILI_DB_PATH " 2>/dev/null) " ]] ; then
local BACKUP_PATH = " ${ MEILI_DB_PATH } .backup. $( date +%Y%m%d%H%M%S) "
msg_warn "Backing up MeiliSearch data to ${ BACKUP_PATH } "
mv " $MEILI_DB_PATH " " $BACKUP_PATH "
mkdir -p " $MEILI_DB_PATH "
msg_info "Data backed up. After update, you may need to reindex your data."
msg_info "Old data is preserved at: ${ BACKUP_PATH } "
2026-01-30 10:20:57 +01:00
fi
fi
# Stop service and update binary
2026-01-28 11:08:52 +01:00
systemctl stop meilisearch
fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "binary"
2026-01-30 10:20:57 +01:00
# If migration needed and dump was created, remove old data and import dump
if [[ " $NEEDS_MIGRATION " == "true" ]] && [[ -n " $DUMP_UID " ]] ; then
local MEILI_DB_PATH
2026-03-25 13:06:21 +01:00
MEILI_DB_PATH = $( grep -E "^db_path\s*=" /etc/meilisearch.toml 2>/dev/null | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' || true )
2026-01-30 10:20:57 +01:00
MEILI_DB_PATH = " ${ MEILI_DB_PATH :- /var/lib/meilisearch/data } "
msg_info "Removing old MeiliSearch database for migration"
rm -rf " ${ MEILI_DB_PATH :? } " /*
2026-01-30 10:29:08 +01:00
# Import dump using CLI flag (this is the supported method)
local DUMP_FILE = " ${ MEILI_DUMP_DIR } / ${ DUMP_UID } .dump"
if [[ -f " $DUMP_FILE " ]] ; then
msg_info "Importing dump: ${ DUMP_FILE } "
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
# Start meilisearch with --import-dump flag
# This is a one-time import that happens during startup
/usr/bin/meilisearch --config-file-path /etc/meilisearch.toml --import-dump " $DUMP_FILE " &
local MEILI_PID = $!
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
# Wait for meilisearch to become healthy (import happens during startup)
msg_info "Waiting for MeiliSearch to import and start..."
2026-01-30 10:20:57 +01:00
local MAX_WAIT = 300
local WAITED = 0
while [[ $WAITED -lt $MAX_WAIT ]] ; do
2026-01-30 10:29:08 +01:00
if curl -sf "http:// ${ MEILI_HOST } : ${ MEILI_PORT } /health" & >/dev/null; then
msg_ok "MeiliSearch is healthy after import"
2026-01-30 10:20:57 +01:00
break
2026-01-30 10:29:08 +01:00
fi
# Check if process is still running
if ! kill -0 $MEILI_PID 2>/dev/null; then
msg_warn "MeiliSearch process exited during import"
2026-01-30 10:20:57 +01:00
break
fi
sleep 3
WAITED = $(( WAITED + 3 ))
done
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
# Stop the manual process
2026-01-30 10:20:57 +01:00
kill $MEILI_PID 2>/dev/null || true
2026-01-30 10:29:08 +01:00
sleep 2
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
# Start via systemd for proper management
systemctl start meilisearch
2026-02-22 11:54:21 +01:00
2026-01-30 10:29:08 +01:00
if systemctl is-active --quiet meilisearch; then
msg_ok "MeiliSearch migrated successfully"
else
msg_warn "MeiliSearch failed to start after migration - check logs with: journalctl -u meilisearch"
fi
else
msg_warn "Dump file not found: ${ DUMP_FILE } "
2026-01-30 10:20:57 +01:00
systemctl start meilisearch
fi
else
systemctl start meilisearch
fi
2026-01-28 11:08:52 +01:00
msg_ok "Updated MeiliSearch"
fi
return 0
fi
# Fresh install
msg_info "Setup MeiliSearch"
# Install binary
fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "binary" || {
msg_error "Failed to install MeiliSearch binary"
2026-03-26 16:07:38 +01:00
return 250
2026-01-28 11:08:52 +01:00
}
# Download default config
curl -fsSL https://raw.githubusercontent.com/meilisearch/meilisearch/latest/config.toml -o /etc/meilisearch.toml || {
msg_error "Failed to download MeiliSearch config"
2026-03-26 16:07:38 +01:00
return 7
2026-01-28 11:08:52 +01:00
}
# Generate master key
MEILISEARCH_MASTER_KEY = $( openssl rand -base64 12)
export MEILISEARCH_MASTER_KEY
# Configure
sed -i \
-e "s|^env =.*|env = \" ${ MEILISEARCH_ENV } \"|" \
-e "s|^# master_key =.*|master_key = \" ${ MEILISEARCH_MASTER_KEY } \"|" \
-e "s|^db_path =.*|db_path = \" ${ MEILISEARCH_DB_PATH } \"|" \
-e "s|^dump_dir =.*|dump_dir = \" ${ MEILISEARCH_DUMP_DIR } \"|" \
-e "s|^snapshot_dir =.*|snapshot_dir = \" ${ MEILISEARCH_SNAPSHOT_DIR } \"|" \
-e 's|^# no_analytics = true|no_analytics = true|' \
-e "s|^http_addr =.*|http_addr = \" ${ MEILISEARCH_BIND } \"|" \
/etc/meilisearch.toml
# Create data directories
mkdir -p " ${ MEILISEARCH_DB_PATH } " " ${ MEILISEARCH_DUMP_DIR } " " ${ MEILISEARCH_SNAPSHOT_DIR } "
# Create systemd service
cat <<EOF >/etc/systemd/system/meilisearch.service
[Unit]
Description=Meilisearch
After=network.target
[Service]
ExecStart=/usr/bin/meilisearch --config-file-path /etc/meilisearch.toml
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Enable and start service
systemctl daemon-reload
systemctl enable -q --now meilisearch
# Wait for MeiliSearch to be ready (up to 30 seconds)
for i in { 1..30} ; do
if curl -s -o /dev/null -w "%{http_code}" "http:// ${ MEILISEARCH_HOST } : ${ MEILISEARCH_PORT } /health" 2>/dev/null | grep -q "200" ; then
break
fi
sleep 1
done
# Verify service is running
if ! systemctl is-active --quiet meilisearch; then
msg_error "MeiliSearch service failed to start"
2026-03-26 16:07:38 +01:00
return 150
2026-01-28 11:08:52 +01:00
fi
# Get API keys with retry logic
MEILISEARCH_API_KEY = ""
for i in { 1..10} ; do
MEILISEARCH_API_KEY = $( curl -s -X GET "http:// ${ MEILISEARCH_HOST } : ${ MEILISEARCH_PORT } /keys" \
2026-02-22 11:54:21 +01:00
-H "Authorization: Bearer ${ MEILISEARCH_MASTER_KEY } " 2>/dev/null |
2026-01-28 11:08:52 +01:00
grep -o '"key":"[^"]*"' | head -n 1 | sed 's/"key":"//;s/"//' ) || true
[[ -n " $MEILISEARCH_API_KEY " ]] && break
sleep 2
done
MEILISEARCH_API_KEY_UID = $( curl -s -X GET "http:// ${ MEILISEARCH_HOST } : ${ MEILISEARCH_PORT } /keys" \
2026-02-22 11:54:21 +01:00
-H "Authorization: Bearer ${ MEILISEARCH_MASTER_KEY } " 2>/dev/null |
2026-01-28 11:08:52 +01:00
grep -o '"uid":"[^"]*"' | head -n 1 | sed 's/"uid":"//;s/"//' ) || true
export MEILISEARCH_API_KEY
export MEILISEARCH_API_KEY_UID
# Cache version
local MEILISEARCH_VERSION
MEILISEARCH_VERSION = $( /usr/bin/meilisearch --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true
cache_installed_version "meilisearch" " ${ MEILISEARCH_VERSION :- unknown } "
msg_ok "Setup MeiliSearch ${ MEILISEARCH_VERSION :- } "
}
# ------------------------------------------------------------------------------
2025-10-22 06:25:26 -07:00
# 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)
2026-01-12 08:48:36 +01:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2025-11-05 05:19:35 -08:00
# Resolve "latest" version
2025-10-22 06:25:26 -07:00
if [[ " $CLICKHOUSE_VERSION " == "latest" ]] ; then
2025-11-05 05:19:35 -08:00
CLICKHOUSE_VERSION = $( curl -fsSL --max-time 15 https://packages.clickhouse.com/tgz/stable/ 2>/dev/null |
2025-10-22 06:25:26 -07:00
grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
2025-11-05 05:19:35 -08:00
sort -V | tail -n1 || echo "" )
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Fallback to GitHub API if package server failed
2025-10-22 06:25:26 -07:00
if [[ -z " $CLICKHOUSE_VERSION " ]] ; then
2025-11-05 05:19:35 -08:00
CLICKHOUSE_VERSION = $( curl -fsSL --max-time 15 https://api.github.com/repos/ClickHouse/ClickHouse/releases/latest 2>/dev/null |
grep -oP '"tag_name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || echo "" )
2025-10-22 06:25:26 -07:00
fi
2025-11-05 05:19:35 -08:00
[[ -z " $CLICKHOUSE_VERSION " ]] && {
msg_error "Could not determine latest ClickHouse version from any source"
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
fi
2025-11-05 05:19:35 -08:00
# Get currently installed version
2025-10-22 06:25:26 -07:00
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
2025-11-05 05:19:35 -08:00
# Scenario 1: Already at target version - just update packages
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " == " $CLICKHOUSE_VERSION " ]] ; then
msg_info "Update ClickHouse $CLICKHOUSE_VERSION "
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Perform upgrade with retry logic (non-fatal if fails)
2026-02-27 13:59:02 +01:00
upgrade_packages_with_retry "clickhouse-server" "clickhouse-client" || {
msg_warn "ClickHouse package upgrade had issues, continuing with current version"
}
2025-11-05 05:19:35 -08:00
cache_installed_version "clickhouse" " $CLICKHOUSE_VERSION "
msg_ok "Update ClickHouse $CLICKHOUSE_VERSION "
2025-10-22 06:25:26 -07:00
return 0
fi
2025-11-05 05:19:35 -08:00
# Scenario 2: Different version - clean upgrade
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " != " $CLICKHOUSE_VERSION " ]] ; then
msg_info "Upgrade ClickHouse from $CURRENT_VERSION to $CLICKHOUSE_VERSION "
stop_all_services "clickhouse-server"
remove_old_tool_version "clickhouse"
else
msg_info "Setup ClickHouse $CLICKHOUSE_VERSION "
2025-10-22 06:25:26 -07:00
fi
ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg
2025-11-05 05:19:35 -08:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "clickhouse" || {
msg_error "Failed to prepare ClickHouse repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Setup repository (ClickHouse uses 'stable' suite)
2025-10-22 06:25:26 -07:00
setup_deb822_repo \
"clickhouse" \
"https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
"https://packages.clickhouse.com/deb" \
2025-11-05 05:19:35 -08:00
"stable" \
2025-11-07 02:49:16 -08:00
"main"
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
# Install packages with retry logic
$STD apt update || {
2025-10-22 06:25:26 -07:00
msg_error "APT update failed for ClickHouse repository"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
install_packages_with_retry "clickhouse-server" "clickhouse-client" || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to install ClickHouse packages"
2026-03-26 16:07:38 +01:00
return 100
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
# Verify installation
if ! command -v clickhouse-server >/dev/null 2>& 1; then
msg_error "ClickHouse installation completed but clickhouse-server command not found"
2026-03-26 16:07:38 +01:00
return 127
2025-10-22 06:25:26 -07:00
fi
2025-11-05 05:19:35 -08:00
# Setup data directory
2025-10-22 06:25:26 -07:00
mkdir -p /var/lib/clickhouse
if id clickhouse >/dev/null 2>& 1; then
chown -R clickhouse:clickhouse /var/lib/clickhouse
fi
# Enable and start service
2025-11-05 05:19:35 -08:00
$STD systemctl enable clickhouse-server || {
msg_warn "Failed to enable clickhouse-server service"
}
2025-10-22 06:25:26 -07:00
safe_service_restart clickhouse-server || true
cache_installed_version "clickhouse" " $CLICKHOUSE_VERSION "
msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION "
}
2025-05-27 14:29:50 +02:00
# ------------------------------------------------------------------------------
# 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")
# ------------------------------------------------------------------------------
2025-06-18 12:07:08 +02:00
function setup_rust() {
2025-05-27 14:29:50 +02:00
local RUST_TOOLCHAIN = " ${ RUST_TOOLCHAIN :- stable } "
local RUST_CRATES = " ${ RUST_CRATES :- } "
local CARGO_BIN = " ${ HOME } /.cargo/bin"
2025-11-05 05:19:35 -08:00
# Get currently installed version
local CURRENT_VERSION = ""
if command -v rustc & >/dev/null; then
2025-12-15 15:22:32 +01:00
CURRENT_VERSION = $( rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true
2025-11-05 05:19:35 -08:00
fi
# Scenario 1: Rustup not installed - fresh install
2025-05-27 14:29:50 +02:00
if ! command -v rustup & >/dev/null; then
2025-11-05 05:19:35 -08:00
msg_info "Setup Rust ( $RUST_TOOLCHAIN )"
2025-10-22 06:25:26 -07:00
curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain " $RUST_TOOLCHAIN " || {
msg_error "Failed to install Rust"
2026-03-26 16:07:38 +01:00
return 7
2025-10-22 06:25:26 -07:00
}
2025-05-27 14:29:50 +02:00
export PATH = " $CARGO_BIN : $PATH "
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>" $HOME /.profile"
2025-11-13 12:38:04 +01:00
# Verify installation
if ! command -v rustc >/dev/null 2>& 1; then
msg_error "Rust binary not found after installation"
2026-03-26 16:07:38 +01:00
return 127
2025-11-13 12:38:04 +01:00
fi
2025-12-15 15:22:32 +01:00
local RUST_VERSION
RUST_VERSION = $( rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true
2025-11-13 12:38:04 +01:00
if [[ -z " $RUST_VERSION " ]] ; then
msg_error "Failed to determine Rust version"
2026-03-26 16:07:38 +01:00
return 250
2025-11-13 12:38:04 +01:00
fi
2025-10-22 06:25:26 -07:00
cache_installed_version "rust" " $RUST_VERSION "
msg_ok "Setup Rust $RUST_VERSION "
2025-05-27 14:29:50 +02:00
else
2025-11-05 05:19:35 -08:00
# Scenario 2: Rustup already installed - update/maintain
msg_info "Update Rust ( $RUST_TOOLCHAIN )"
2025-11-13 12:38:04 +01:00
# Ensure default toolchain is set
$STD rustup default " $RUST_TOOLCHAIN " 2>/dev/null || {
# If default fails, install the toolchain first
$STD rustup install " $RUST_TOOLCHAIN " || {
msg_error "Failed to install Rust toolchain $RUST_TOOLCHAIN "
2026-03-26 16:07:38 +01:00
return 150
2025-11-13 12:38:04 +01:00
}
$STD rustup default " $RUST_TOOLCHAIN " || {
msg_error "Failed to set default Rust toolchain"
2026-03-26 16:07:38 +01:00
return 150
2025-11-13 12:38:04 +01:00
}
2025-10-22 06:25:26 -07:00
}
2025-11-13 12:38:04 +01:00
# Update to latest patch version
2026-02-27 13:59:02 +01:00
$STD rustup update " $RUST_TOOLCHAIN " </dev/null || {
msg_warn "Rust toolchain update had issues"
}
2025-11-13 12:38:04 +01:00
# Ensure PATH is updated for current shell session
export PATH = " $CARGO_BIN : $PATH "
2025-12-15 15:22:32 +01:00
local RUST_VERSION
RUST_VERSION = $( rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true
2025-11-13 12:38:04 +01:00
if [[ -z " $RUST_VERSION " ]] ; then
msg_error "Failed to determine Rust version after update"
2026-03-26 16:07:38 +01:00
return 250
2025-11-13 12:38:04 +01:00
fi
2025-10-22 06:25:26 -07:00
cache_installed_version "rust" " $RUST_VERSION "
2025-11-05 05:19:35 -08:00
msg_ok "Update Rust $RUST_VERSION "
2025-05-27 14:29:50 +02:00
fi
2025-11-05 05:19:35 -08:00
# Install global crates
2025-05-27 14:29:50 +02:00
if [[ -n " $RUST_CRATES " ]] ; then
2025-11-13 12:38:04 +01:00
msg_info "Processing Rust crates: $RUST_CRATES "
2025-05-27 14:29:50 +02:00
IFS = ',' read -ra CRATES <<< " $RUST_CRATES "
for crate in " ${ CRATES [@] } " ; do
2025-11-13 12:38:04 +01:00
crate = $( echo " $crate " | xargs) # trim whitespace
[[ -z " $crate " ]] && continue # skip empty entries
local NAME VER INSTALLED_VER CRATE_LIST
2025-05-27 14:29:50 +02:00
if [[ " $crate " == *"@" * ]] ; then
NAME = " ${ crate %@* } "
VER = " ${ crate ##*@ } "
else
NAME = " $crate "
VER = ""
fi
2025-11-13 12:38:04 +01:00
# Get list of installed crates once
CRATE_LIST = $( cargo install --list 2>/dev/null || echo "" )
# Check if already installed
if echo " $CRATE_LIST " | grep -q "^ ${ NAME } " ; then
2025-12-18 18:38:08 +01:00
INSTALLED_VER = $( echo " $CRATE_LIST " | grep "^ ${ NAME } " | head -1 | awk '{print $2}' 2>/dev/null | tr -d 'v:' || echo '' )
2025-05-27 14:29:50 +02:00
if [[ -n " $VER " && " $VER " != " $INSTALLED_VER " ]] ; then
2025-11-13 12:38:04 +01:00
msg_info "Upgrading $NAME from v $INSTALLED_VER to v $VER "
$STD cargo install " $NAME " --version " $VER " --force || {
msg_error "Failed to install $NAME @ $VER "
2026-03-26 16:07:38 +01:00
return 150
2025-11-13 12:38:04 +01:00
}
msg_ok "Upgraded $NAME to v $VER "
2025-05-27 14:29:50 +02:00
elif [[ -z " $VER " ]] ; then
2025-11-13 12:38:04 +01:00
msg_info "Upgrading $NAME to latest"
$STD cargo install " $NAME " --force || {
msg_error "Failed to upgrade $NAME "
2026-03-26 16:07:38 +01:00
return 150
2025-11-13 12:38:04 +01:00
}
2025-12-18 18:38:08 +01:00
local NEW_VER = $( cargo install --list 2>/dev/null | grep "^ ${ NAME } " | head -1 | awk '{print $2}' 2>/dev/null | tr -d 'v:' || echo 'unknown' )
2025-11-13 12:38:04 +01:00
msg_ok "Upgraded $NAME to v $NEW_VER "
else
msg_ok " $NAME v $INSTALLED_VER already installed"
2025-05-27 14:29:50 +02:00
fi
else
2025-11-13 12:38:04 +01:00
msg_info "Installing $NAME ${ VER :+@ $VER } "
if [[ -n " $VER " ]] ; then
$STD cargo install " $NAME " --version " $VER " || {
msg_error "Failed to install $NAME @ $VER "
2026-03-26 16:07:38 +01:00
return 150
2025-11-13 12:38:04 +01:00
}
msg_ok "Installed $NAME v $VER "
else
$STD cargo install " $NAME " || {
msg_error "Failed to install $NAME "
2026-03-26 16:07:38 +01:00
return 150
2025-11-13 12:38:04 +01:00
}
2025-12-18 18:38:08 +01:00
local NEW_VER = $( cargo install --list 2>/dev/null | grep "^ ${ NAME } " | head -1 | awk '{print $2}' 2>/dev/null | tr -d 'v:' || echo 'unknown' )
2025-11-13 12:38:04 +01:00
msg_ok "Installed $NAME v $NEW_VER "
fi
2025-05-27 14:29:50 +02:00
fi
done
2025-11-13 12:38:04 +01:00
msg_ok "Processed Rust crates"
2025-05-27 14:29:50 +02:00
fi
}
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# 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
2025-06-18 12:07:08 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
function setup_uv() {
local UV_BIN = "/usr/local/bin/uv"
2025-11-05 05:19:35 -08:00
local UVX_BIN = "/usr/local/bin/uvx"
2025-10-22 06:25:26 -07:00
local TMP_DIR = $( mktemp -d)
local CACHED_VERSION
2025-11-05 05:19:35 -08:00
# trap for TMP Cleanup
trap "rm -rf ' $TMP_DIR '" EXIT
2025-10-22 06:25:26 -07:00
CACHED_VERSION = $( get_cached_version "uv" )
2025-06-18 12:07:08 +02:00
2025-11-05 05:19:35 -08:00
# Architecture Detection
2025-10-22 06:25:26 -07:00
local ARCH = $( uname -m)
2025-11-05 05:19:35 -08:00
local OS_TYPE = ""
local UV_TAR = ""
if grep -qi "alpine" /etc/os-release; then
OS_TYPE = "musl"
else
OS_TYPE = "gnu"
fi
2025-06-18 12:07:08 +02:00
2025-09-11 12:44:13 +02:00
case " $ARCH " in
x86_64)
2025-11-05 05:19:35 -08:00
UV_TAR = "uv-x86_64-unknown-linux- ${ OS_TYPE } .tar.gz"
2025-09-11 12:44:13 +02:00
;;
aarch64)
2025-11-05 05:19:35 -08:00
UV_TAR = "uv-aarch64-unknown-linux- ${ OS_TYPE } .tar.gz"
;;
i686)
UV_TAR = "uv-i686-unknown-linux- ${ OS_TYPE } .tar.gz"
2025-07-08 17:19:51 +02:00
;;
*)
2025-11-05 05:19:35 -08:00
msg_error "Unsupported architecture: $ARCH (supported: x86_64, aarch64, i686)"
2026-03-26 16:07:38 +01:00
return 236
2025-07-08 17:19:51 +02:00
;;
esac
2025-10-22 06:25:26 -07:00
ensure_dependencies jq
2025-11-05 05:19:35 -08:00
# Fetch latest version
local releases_json
releases_json = $( curl -fsSL --max-time 15 \
"https://api.github.com/repos/astral-sh/uv/releases/latest" 2>/dev/null || echo "" )
if [[ -z " $releases_json " ]] ; then
msg_error "Could not fetch latest uv version from GitHub API"
2026-03-26 16:07:38 +01:00
return 7
2025-11-05 05:19:35 -08:00
fi
2025-09-11 12:44:13 +02:00
local LATEST_VERSION
2025-11-05 05:19:35 -08:00
LATEST_VERSION = $( echo " $releases_json " | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' )
2025-07-08 17:19:51 +02:00
2025-09-11 12:44:13 +02:00
if [[ -z " $LATEST_VERSION " ]] ; then
2025-11-05 05:19:35 -08:00
msg_error "Could not parse uv version from GitHub API response"
2026-03-26 16:07:38 +01:00
return 250
2025-07-08 17:19:51 +02:00
fi
2025-11-05 05:19:35 -08:00
# Get currently installed version
local INSTALLED_VERSION = ""
2025-09-11 12:44:13 +02:00
if [[ -x " $UV_BIN " ]] ; then
2025-11-05 05:19:35 -08:00
INSTALLED_VERSION = $( " $UV_BIN " --version 2>/dev/null | awk '{print $2}' )
fi
# Scenario 1: Already at latest version
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " == " $LATEST_VERSION " ]] ; then
cache_installed_version "uv" " $LATEST_VERSION "
# Check if uvx is needed and missing
if [[ " ${ USE_UVX :- NO } " == "YES" ]] && [[ ! -x " $UVX_BIN " ]] ; then
msg_info "Installing uvx wrapper"
2026-03-26 16:07:38 +01:00
_install_uvx_wrapper || return 252
2025-11-05 05:19:35 -08:00
msg_ok "uvx wrapper installed"
2025-09-11 12:44:13 +02:00
fi
2025-11-05 05:19:35 -08:00
return 0
2025-07-08 17:19:51 +02:00
fi
2025-11-05 05:19:35 -08:00
# Scenario 2: New install or upgrade
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " != " $LATEST_VERSION " ]] ; then
msg_info "Upgrade uv from $INSTALLED_VERSION to $LATEST_VERSION "
else
msg_info "Setup uv $LATEST_VERSION "
fi
2025-10-22 06:25:26 -07:00
2025-11-05 05:19:35 -08:00
local UV_URL = "https://github.com/astral-sh/uv/releases/download/ ${ LATEST_VERSION } / ${ UV_TAR } "
2026-03-02 08:44:59 +01:00
if ! curl_with_retry " $UV_URL " " $TMP_DIR /uv.tar.gz" ; then
2025-11-05 05:19:35 -08:00
msg_error "Failed to download uv from $UV_URL "
2026-03-26 16:07:38 +01:00
return 7
2026-03-02 08:44:59 +01:00
fi
2025-07-08 17:19:51 +02:00
2025-11-05 05:19:35 -08:00
# Extract
$STD tar -xzf " $TMP_DIR /uv.tar.gz" -C " $TMP_DIR " || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to extract uv"
2026-03-26 16:07:38 +01:00
return 251
2025-10-22 06:25:26 -07:00
}
2025-07-08 17:19:51 +02:00
2025-11-05 05:19:35 -08:00
# Find and install uv binary (tarball extracts to uv-VERSION-ARCH/ directory)
local UV_BINARY = $( find " $TMP_DIR " -name "uv" -type f -executable | head -n1)
if [[ ! -f " $UV_BINARY " ]] ; then
msg_error "Could not find uv binary in extracted tarball"
2026-03-26 16:07:38 +01:00
return 127
2025-11-05 05:19:35 -08:00
fi
2026-03-26 09:54:17 +01:00
$STD /usr/bin/install -m 755 " $UV_BINARY " " $UV_BIN " || {
2025-09-11 12:44:13 +02:00
msg_error "Failed to install uv binary"
2026-03-26 16:07:38 +01:00
return 252
2025-07-08 17:19:51 +02:00
}
ensure_usr_local_bin_persist
2025-10-22 06:25:26 -07:00
export PATH = "/usr/local/bin: $PATH "
2025-09-23 11:39:46 +02:00
2025-11-05 05:19:35 -08:00
# Optional: Install uvx wrapper
if [[ " ${ USE_UVX :- NO } " == "YES" ]] ; then
msg_info "Installing uvx wrapper"
_install_uvx_wrapper || {
msg_error "Failed to install uvx wrapper"
2026-03-26 16:07:38 +01:00
return 252
2025-11-05 05:19:35 -08:00
}
msg_ok "uvx wrapper installed"
fi
# Optional: Generate shell completions
$STD uv generate-shell-completion bash >/etc/bash_completion.d/uv 2>/dev/null || true
2025-11-12 09:56:24 +01:00
if [[ -d /usr/share/zsh/site-functions ]] ; then
$STD uv generate-shell-completion zsh >/usr/share/zsh/site-functions/_uv 2>/dev/null || true
fi
2025-11-05 05:19:35 -08:00
# Optional: Install specific Python version if requested
if [[ -n " ${ PYTHON_VERSION :- } " ]] ; then
msg_info "Installing Python $PYTHON_VERSION via uv"
$STD uv python install " $PYTHON_VERSION " || {
msg_error "Failed to install Python $PYTHON_VERSION "
2026-03-26 16:07:38 +01:00
return 150
2025-11-05 05:19:35 -08:00
}
msg_ok "Python $PYTHON_VERSION installed"
fi
2025-10-22 06:25:26 -07:00
cache_installed_version "uv" " $LATEST_VERSION "
2025-09-11 12:44:13 +02:00
msg_ok "Setup uv $LATEST_VERSION "
2025-11-05 05:19:35 -08:00
}
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
# Helper function to install uvx wrapper
_install_uvx_wrapper() {
local UVX_BIN = "/usr/local/bin/uvx"
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
cat >" $UVX_BIN " <<'EOF'
#!/bin/bash
# uvx - Run Python applications from PyPI as command-line tools
# Wrapper for: uv tool run
exec /usr/local/bin/uv tool run "$@"
EOF
2025-09-11 12:44:13 +02:00
2025-11-05 05:19:35 -08:00
chmod +x " $UVX_BIN "
return 0
2025-07-08 17:19:51 +02:00
}
2025-08-28 14:43:56 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
# Installs or updates yq (mikefarah/yq - Go version).
2025-08-28 14:43:56 +02:00
#
# Description:
2025-09-11 12:44:13 +02:00
# - Checks if yq is installed and from correct source
# - Compares with latest release on GitHub
# - Updates if outdated or wrong implementation
2025-08-28 14:43:56 +02:00
# ------------------------------------------------------------------------------
2025-09-11 12:44:13 +02:00
function setup_yq() {
2025-10-22 06:25:26 -07:00
local TMP_DIR = $( mktemp -d)
2025-09-11 12:44:13 +02:00
local BINARY_PATH = "/usr/local/bin/yq"
local GITHUB_REPO = "mikefarah/yq"
2025-08-28 14:43:56 +02:00
2025-10-22 06:25:26 -07:00
ensure_dependencies jq
ensure_usr_local_bin_persist
2025-09-23 14:00:48 -04:00
2025-11-05 05:19:35 -08:00
# Remove non-mikefarah implementations
2025-09-11 12:44:13 +02:00
if command -v yq & >/dev/null; then
if ! yq --version 2>& 1 | grep -q 'mikefarah' ; then
rm -f " $( command -v yq) "
fi
2025-09-01 09:38:20 +02:00
fi
2025-08-29 12:29:52 +02:00
2025-09-11 12:44:13 +02:00
local LATEST_VERSION
2025-11-05 05:19:35 -08:00
local releases_json
releases_json = $( curl -fsSL --max-time 15 "https://api.github.com/repos/ ${ GITHUB_REPO } /releases/latest" 2>/dev/null || echo "" )
if [[ -z " $releases_json " ]] ; then
msg_error "Could not fetch latest yq version from GitHub API"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2025-11-05 05:19:35 -08:00
fi
LATEST_VERSION = $( echo " $releases_json " | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "" )
2025-08-29 14:20:12 +02:00
2025-09-11 12:44:13 +02:00
if [[ -z " $LATEST_VERSION " ]] ; then
2025-11-05 05:19:35 -08:00
msg_error "Could not parse yq version from GitHub API response"
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2025-09-01 09:38:20 +02:00
fi
2025-11-05 05:19:35 -08:00
# Get currently installed version
local INSTALLED_VERSION = ""
if command -v yq & >/dev/null && yq --version 2>& 1 | grep -q 'mikefarah' ; then
INSTALLED_VERSION = $( yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//' )
fi
# Scenario 1: Already at latest version
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " == " $LATEST_VERSION " ]] ; then
2025-10-22 06:25:26 -07:00
cache_installed_version "yq" " $LATEST_VERSION "
rm -rf " $TMP_DIR "
return 0
2025-09-11 12:44:13 +02:00
fi
2025-08-29 14:20:12 +02:00
2025-11-05 05:19:35 -08:00
# Scenario 2: New install or upgrade
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " != " $LATEST_VERSION " ]] ; then
msg_info "Upgrade yq from $INSTALLED_VERSION to $LATEST_VERSION "
else
msg_info "Setup yq $LATEST_VERSION "
fi
2026-03-02 08:44:59 +01:00
if ! curl_with_retry "https://github.com/ ${ GITHUB_REPO } /releases/download/v ${ LATEST_VERSION } /yq_linux_amd64" " $TMP_DIR /yq" ; then
2025-10-22 06:25:26 -07:00
msg_error "Failed to download yq"
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 250
2026-03-02 08:44:59 +01:00
fi
2025-10-22 06:25:26 -07:00
2025-09-11 12:44:13 +02:00
chmod +x " $TMP_DIR /yq"
2025-11-05 05:19:35 -08:00
mv " $TMP_DIR /yq" " $BINARY_PATH " || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to install yq"
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
2026-03-26 16:07:38 +01:00
return 252
2025-11-05 05:19:35 -08:00
}
2025-08-28 14:43:56 +02:00
2025-09-11 12:44:13 +02:00
rm -rf " $TMP_DIR "
hash -r
2025-08-29 16:20:21 +02:00
2025-09-11 12:44:13 +02:00
local FINAL_VERSION
2025-10-22 06:25:26 -07:00
FINAL_VERSION = $( " $BINARY_PATH " --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//' )
cache_installed_version "yq" " $FINAL_VERSION "
msg_ok "Setup yq $FINAL_VERSION "
2025-08-28 14:43:56 +02:00
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# Docker Engine Installation and Management (All-In-One)
#
# Description:
2026-03-02 08:44:59 +01:00
# - By default uses distro repository (docker.io) for stability
# - Optionally uses official Docker repository for latest features
2025-12-04 07:52:18 +01:00
# - Detects and migrates old Docker installations
# - Optional: Installs/Updates Portainer CE
# - Updates running containers interactively
# - Cleans up legacy repository files
#
# Usage:
2026-03-02 08:44:59 +01:00
# setup_docker # Uses distro package (recommended)
# USE_DOCKER_REPO=true setup_docker # Uses official Docker repo
2025-12-04 07:52:18 +01:00
# DOCKER_PORTAINER="true" setup_docker
# DOCKER_LOG_DRIVER="json-file" setup_docker
#
# Variables:
2026-03-02 08:44:59 +01:00
# USE_DOCKER_REPO - Set to "true" to use official Docker repository
# (default: false, uses distro docker.io package)
2025-12-04 07:52:18 +01:00
# DOCKER_PORTAINER - Install Portainer CE (optional, "true" to enable)
# DOCKER_LOG_DRIVER - Log driver (optional, default: "journald")
# DOCKER_SKIP_UPDATES - Skip container update check (optional, "true" to skip)
#
# Features:
2026-03-02 08:44:59 +01:00
# - Uses stable distro packages by default
2025-12-04 07:52:18 +01:00
# - Migrates from get.docker.com to repository-based installation
# - Updates Docker Engine if newer version available
# - Interactive container update with multi-select
# - Portainer installation and update support
# ------------------------------------------------------------------------------
function setup_docker() {
local docker_installed = false
local portainer_installed = false
2026-03-02 08:44:59 +01:00
local USE_DOCKER_REPO = " ${ USE_DOCKER_REPO :- false } "
2025-12-04 07:52:18 +01:00
# Check if Docker is already installed
if command -v docker & >/dev/null; then
docker_installed = true
DOCKER_CURRENT_VERSION = $( docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_info "Docker $DOCKER_CURRENT_VERSION detected"
fi
# Check if Portainer is running
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^portainer$' ; then
portainer_installed = true
msg_info "Portainer container detected"
fi
2026-03-02 08:44:59 +01:00
# Scenario 1: Use distro repository (default, most stable)
if [[ " $USE_DOCKER_REPO " != "true" && " $USE_DOCKER_REPO " != "TRUE" && " $USE_DOCKER_REPO " != "1" ]] ; then
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
# Install or upgrade Docker from distro repo
if [ " $docker_installed " = true ] ; then
msg_info "Checking for Docker updates (distro package)"
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
upgrade_packages_with_retry "docker.io" "docker-compose" || true
DOCKER_CURRENT_VERSION = $( docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Docker is up-to-date ( $DOCKER_CURRENT_VERSION )"
else
msg_info "Installing Docker (distro package)"
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
# Install docker.io and docker-compose from distro
if ! install_packages_with_retry "docker.io" ; then
msg_error "Failed to install docker.io from distro repository"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
fi
# docker-compose is optional
$STD apt install -y docker-compose 2>/dev/null || true
DOCKER_CURRENT_VERSION = $( docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION (distro package)"
fi
# Configure daemon.json
local log_driver = " ${ DOCKER_LOG_DRIVER :- journald } "
mkdir -p /etc/docker
if [ ! -f /etc/docker/daemon.json ] ; then
cat <<EOF >/etc/docker/daemon.json
{
"log-driver": "$log_driver"
}
EOF
fi
# Enable and start Docker
systemctl enable -q --now docker
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
# Continue to Portainer section below
else
# Scenario 2: Use official Docker repository (USE_DOCKER_REPO=true)
# Cleanup old repository configurations
if [ -f /etc/apt/sources.list.d/docker.list ] ; then
msg_info "Migrating from old Docker repository format"
rm -f /etc/apt/sources.list.d/docker.list
rm -f /etc/apt/keyrings/docker.asc
fi
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
# Setup/Update Docker repository
msg_info "Setting up Docker Repository"
setup_deb822_repo \
"docker" \
"https://download.docker.com/linux/ $( get_os_info id) /gpg" \
"https://download.docker.com/linux/ $( get_os_info id) " \
" $( get_os_info codename) " \
"stable" \
" $( dpkg --print-architecture) "
# Install or upgrade Docker
if [ " $docker_installed " = true ] ; then
msg_info "Checking for Docker updates"
DOCKER_LATEST_VERSION = $( apt-cache policy docker-ce | grep Candidate | awk '{print $2}' 2>/dev/null | cut -d':' -f2 | cut -d'-' -f1 || echo '' )
if [ " $DOCKER_CURRENT_VERSION " != " $DOCKER_LATEST_VERSION " ] ; then
msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION "
$STD apt install -y --only-upgrade \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin || {
msg_error "Failed to update Docker packages"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
msg_ok "Updated Docker to $DOCKER_LATEST_VERSION "
else
msg_ok "Docker is up-to-date ( $DOCKER_CURRENT_VERSION )"
fi
else
msg_info "Installing Docker"
$STD apt install -y \
2025-12-04 07:52:18 +01:00
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
2026-02-27 13:59:02 +01:00
docker-compose-plugin || {
2026-03-02 08:44:59 +01:00
msg_error "Failed to install Docker packages"
2026-03-26 16:07:38 +01:00
return 100
2026-02-27 13:59:02 +01:00
}
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
DOCKER_CURRENT_VERSION = $( docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION "
fi
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
# Configure daemon.json
local log_driver = " ${ DOCKER_LOG_DRIVER :- journald } "
mkdir -p /etc/docker
if [ ! -f /etc/docker/daemon.json ] ; then
cat <<EOF >/etc/docker/daemon.json
2025-12-04 07:52:18 +01:00
{
"log-driver": "$log_driver"
}
EOF
2026-03-02 08:44:59 +01:00
fi
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
# Enable and start Docker
systemctl enable -q --now docker
fi
2025-12-04 07:52:18 +01:00
2026-03-02 08:44:59 +01:00
# Portainer Management (common for both modes)
2025-12-04 07:52:18 +01:00
if [[ " ${ DOCKER_PORTAINER :- } " == "true" ]] ; then
if [ " $portainer_installed " = true ] ; then
msg_info "Checking for Portainer updates"
PORTAINER_CURRENT = $( docker inspect portainer --format= '{{.Config.Image}}' 2>/dev/null | cut -d':' -f2)
PORTAINER_LATEST = $( curl -fsSL https://registry.hub.docker.com/v2/repositories/portainer/portainer-ce/tags?page_size= 100 | grep -oP '"name":"\K[0-9]+\.[0-9]+\.[0-9]+"' | head -1 | tr -d '"' )
if [ " $PORTAINER_CURRENT " != " $PORTAINER_LATEST " ] ; then
read -r -p " ${ TAB3 } Update Portainer $PORTAINER_CURRENT → $PORTAINER_LATEST ? <y/N> " prompt
if [[ ${ prompt ,, } = ~ ^( y| yes) $ ]] ; then
msg_info "Updating Portainer"
docker stop portainer
docker rm portainer
docker pull portainer/portainer-ce:latest
docker run -d \
-p 9000:9000 \
-p 9443:9443 \
--name= portainer \
--restart= always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
msg_ok "Updated Portainer to $PORTAINER_LATEST "
fi
else
msg_ok "Portainer is up-to-date ( $PORTAINER_CURRENT )"
fi
else
msg_info "Installing Portainer"
docker volume create portainer_data
docker run -d \
-p 9000:9000 \
-p 9443:9443 \
--name= portainer \
--restart= always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
LOCAL_IP = $( hostname -I | awk '{print $1}' )
msg_ok "Installed Portainer (http:// ${ LOCAL_IP } :9000)"
fi
fi
# Interactive Container Update Check
if [[ " ${ DOCKER_SKIP_UPDATES :- } " != "true" ]] && [ " $docker_installed " = true ] ; then
msg_info "Checking for container updates"
# Get list of running containers with update status
local containers_with_updates =()
local container_info =()
local index = 1
while IFS = read -r container; do
local name = $( echo " $container " | awk '{print $1}' )
local image = $( echo " $container " | awk '{print $2}' )
local current_digest = $( docker inspect " $name " --format= '{{.Image}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
# Pull latest image digest
docker pull " $image " >/dev/null 2>& 1
local latest_digest = $( docker inspect " $image " --format= '{{.Id}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
if [ " $current_digest " != " $latest_digest " ] ; then
containers_with_updates +=( " $name " )
container_info +=( " ${ index } ) ${ name } ( ${ image } )" )
(( index++))
fi
done < <( docker ps --format '{{.Names}} {{.Image}}' )
if [ ${# containers_with_updates [@] } -gt 0 ] ; then
echo ""
echo " ${ TAB3 } Container updates available:"
for info in " ${ container_info [@] } " ; do
echo " ${ TAB3 } $info "
done
echo ""
read -r -p " ${ TAB3 } Select containers to update (e.g., 1,3,5 or 'all' or 'none'): " selection
if [[ ${ selection ,, } == "all" ]] ; then
for container in " ${ containers_with_updates [@] } " ; do
msg_info "Updating container: $container "
docker stop " $container "
docker rm " $container "
# Note: This requires the original docker run command - best to recreate via compose
msg_ok "Stopped and removed $container (please recreate with updated image)"
done
elif [[ ${ selection ,, } != "none" ]] ; then
IFS = ',' read -ra SELECTED <<< " $selection "
for num in " ${ SELECTED [@] } " ; do
num = $( echo " $num " | xargs) # trim whitespace
if [[ " $num " = ~ ^[ 0-9] +$ ]] && [ " $num " -ge 1 ] && [ " $num " -le " ${# containers_with_updates [@] } " ] ; then
container = " ${ containers_with_updates [ $(( num - 1 )) ] } "
msg_info "Updating container: $container "
docker stop " $container "
docker rm " $container "
msg_ok "Stopped and removed $container (please recreate with updated image)"
fi
done
fi
else
msg_ok "All containers are up-to-date"
fi
fi
msg_ok "Docker setup completed"
}
2026-01-30 22:25:55 +01:00
# ------------------------------------------------------------------------------
# Fetch and deploy from URL
# Downloads an archive (zip, tar.gz, or .deb) from a URL and extracts/installs it
#
# Usage: fetch_and_deploy_from_url "url" "directory"
# url - URL to the archive (zip, tar.gz, or .deb)
# directory - Destination path where the archive will be extracted
# (not used for .deb packages)
#
# Examples:
# fetch_and_deploy_from_url "https://example.com/app.tar.gz" "/opt/myapp"
# fetch_and_deploy_from_url "https://example.com/app.zip" "/opt/myapp"
# fetch_and_deploy_from_url "https://example.com/package.deb" ""
# ------------------------------------------------------------------------------
function fetch_and_deploy_from_url() {
local url = " $1 "
2026-03-06 14:11:57 +01:00
local directory = " ${ 2 :- } "
2026-01-30 22:25:55 +01:00
if [[ -z " $url " ]] ; then
msg_error "URL parameter is required"
2026-03-26 16:07:38 +01:00
return 65
2026-01-30 22:25:55 +01:00
fi
local filename = " ${ url ##*/ } "
msg_info "Downloading from $url "
local tmpdir
tmpdir = $( mktemp -d) || {
msg_error "Failed to create temporary directory"
2026-03-26 16:07:38 +01:00
return 252
2026-01-30 22:25:55 +01:00
}
curl -fsSL -o " $tmpdir / $filename " " $url " || {
msg_error "Download failed: $url "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 250
2026-01-30 22:25:55 +01:00
}
2026-02-01 13:19:11 +01:00
# Auto-detect archive type using file description
local file_desc
file_desc = $( file -b " $tmpdir / $filename " )
2026-02-22 11:54:21 +01:00
2026-02-01 13:19:11 +01:00
local archive_type = "unknown"
2026-02-22 11:54:21 +01:00
2026-02-01 13:19:11 +01:00
if [[ " $file_desc " = ~ gzip.*compressed| gzip\ compressed\ data ]] ; then
archive_type = "tar"
elif [[ " $file_desc " = ~ Zip.*archive| ZIP\ archive ]] ; then
archive_type = "zip"
elif [[ " $file_desc " = ~ Debian.*package| Debian\ binary\ package ]] ; then
archive_type = "deb"
elif [[ " $file_desc " = ~ POSIX.*tar.*archive| tar\ archive ]] ; then
archive_type = "tar"
else
msg_error "Unsupported or unknown archive type: $file_desc "
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 65
2026-02-01 13:19:11 +01:00
fi
msg_info "Detected archive type: $archive_type (file type: $file_desc )"
2026-01-30 22:25:55 +01:00
if [[ " $archive_type " == "deb" ]] ; then
msg_info "Installing .deb package"
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 "
2026-03-26 16:07:38 +01:00
return 100
2026-01-30 22:25:55 +01:00
}
}
rm -rf " $tmpdir "
msg_ok "Successfully installed .deb package"
return 0
fi
if [[ -z " $directory " ]] ; then
msg_error "Directory parameter is required for archive extraction"
rm -rf " $tmpdir "
2026-03-26 16:07:38 +01:00
return 65
2026-01-30 22:25:55 +01:00
fi
msg_info "Extracting archive to $directory "
mkdir -p " $directory "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
rm -rf " ${ directory :? } /" *
fi
local unpack_tmp
unpack_tmp = $( mktemp -d)
if [[ " $archive_type " == "zip" ]] ; then
ensure_dependencies unzip
unzip -q " $tmpdir / $filename " -d " $unpack_tmp " || {
msg_error "Failed to extract ZIP archive"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2026-01-30 22:25:55 +01:00
}
elif [[ " $archive_type " == "tar" ]] ; then
tar --no-same-owner -xf " $tmpdir / $filename " -C " $unpack_tmp " || {
msg_error "Failed to extract TAR archive"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 251
2026-01-30 22:25:55 +01:00
}
fi
local top_entries
top_entries = $( find " $unpack_tmp " -mindepth 1 -maxdepth 1)
if [[ " $( echo " $top_entries " | wc -l) " -eq 1 && -d " $top_entries " ]] ; then
local inner_dir = " $top_entries "
shopt -s dotglob nullglob
if compgen -G " $inner_dir /*" >/dev/null; then
cp -r " $inner_dir " /* " $directory /" || {
msg_error "Failed to copy contents from $inner_dir to $directory "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2026-01-30 22:25:55 +01:00
}
else
msg_error "Inner directory is empty: $inner_dir "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2026-01-30 22:25:55 +01:00
fi
shopt -u dotglob nullglob
else
shopt -s dotglob nullglob
if compgen -G " $unpack_tmp /*" >/dev/null; then
cp -r " $unpack_tmp " /* " $directory /" || {
msg_error "Failed to copy contents to $directory "
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2026-01-30 22:25:55 +01:00
}
else
msg_error "Unpacked archive is empty"
rm -rf " $tmpdir " " $unpack_tmp "
2026-03-26 16:07:38 +01:00
return 252
2026-01-30 22:25:55 +01:00
fi
shopt -u dotglob nullglob
fi
rm -rf " $tmpdir " " $unpack_tmp "
msg_ok "Successfully deployed archive to $directory "
return 0
}
2026-03-08 13:25:24 +01:00
setup_nonfree() {
local sources_file = "/etc/apt/sources.list.d/debian-nonfree.sources"
if [ ! -f " $sources_file " ] ; then
cat <<EOF >$sources_file
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Types: deb
URIs: http://security.debian.org/debian-security
Suites: trixie-security
Components: main contrib non-free non-free-firmware
EOF
fi
$STD apt update
return 0
}