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
2026-06-21 23:10:08 +02:00
# version_matches_spec() - Compare installed semver against spec (8.0 matches 8.0.40)
2025-11-05 05:19:35 -08:00
# 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-07-13 11:37:39 +02:00
# Guard against double-sourcing (core.func uses the same pattern)
[[ -n " ${ _TOOLS_FUNC_LOADED :- } " ]] && return 0
_TOOLS_FUNC_LOADED = 1
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 "
2026-07-13 11:37:39 +02:00
# Build curl command as array to avoid command injection via extra_opts
local -a curl_args =( curl -fsSL --connect-timeout " $connect_timeout " --max-time " $timeout " )
[[ -n " $extra_opts " ]] && read -ra _extra <<< " $extra_opts " && curl_args +=( " ${ _extra [@] } " )
2026-03-02 08:44:59 +01:00
if [[ " $output " == "-" ]] ; then
2026-07-13 11:37:39 +02:00
if " ${ curl_args [@] } " " $url " ; then
2026-03-02 08:44:59 +01:00
success = true
break
fi
else
2026-07-13 11:37:39 +02:00
if " ${ curl_args [@] } " -o " $output " " $url " ; then
2026-03-02 08:44:59 +01:00
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 "
2026-07-13 11:37:39 +02:00
# Build curl command as array to avoid command injection via extra_opts
local -a curl_args =( curl -fsSL --connect-timeout " $connect_timeout " --max-time " $timeout " -w '%{http_code}' )
[[ -n " $extra_opts " ]] && read -ra _extra <<< " $extra_opts " && curl_args +=( " ${ _extra [@] } " )
2026-03-02 08:44:59 +01:00
if [[ -n " $body_file " ]] ; then
2026-07-13 11:37:39 +02:00
http_code = $( " ${ curl_args [@] } " -o " $body_file " " $url " 2>/dev/null) || true
2026-03-02 08:44:59 +01:00
else
# Capture body and http_code separately
local tmp_body = "/tmp/curl_api_body_ $$ "
2026-07-13 11:37:39 +02:00
http_code = $( " ${ curl_args [@] } " -o " $tmp_body " " $url " 2>/dev/null) || true
2026-03-02 08:44:59 +01:00
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
}
2026-06-29 23:12:04 +02:00
# ------------------------------------------------------------------------------
# Edit a key-value pair in a YAML configuration file.
#
# Description:
# - Finds the first occurrence of the given key in the YAML file and replaces
# its value in-place using sed.
# - Handles both quoted and unquoted values on the same line.
# - The key is matched at the start of a line (optionally preceded by spaces),
# followed by a colon and optional whitespace.
# - If the key is not found, the function exits with a non-zero status and
# prints an error message.
#
# Usage:
# edit_yaml_config "/opt/myapp/config.yml" "database_host" "localhost"
# edit_yaml_config "/opt/myapp/config.yml" "port" "5432"
#
# Parameters:
# $1 - Path to the YAML file
# $2 - Key to find (e.g. "database_host")
# $3 - New value to set (e.g. "localhost")
#
# Returns: 0 on success, 1 if the file does not exist or the key is not found
# ------------------------------------------------------------------------------
edit_yaml_config() {
local file = " $1 "
local key = " $2 "
local value = " $3 "
if [[ ! -f " $file " ]] ; then
msg_error "edit_yaml_config: file not found: $file "
return 1
fi
if ! grep -qE "^[[:space:]]* ${ key } [[:space:]]*:" " $file " ; then
msg_error "edit_yaml_config: key ' ${ key } ' not found in $file "
return 1
fi
2026-07-13 11:37:39 +02:00
# Escape sed metacharacters in value (| and &) to prevent injection
local escaped_value = " ${ value //|/ \\ | } "
escaped_value = " ${ escaped_value //&/ \\ & } "
sed -i "s|^\([[:space:]]* ${ key } [[:space:]]*:\).*|\1 ${ escaped_value } |" " $file "
2026-06-29 23:12:04 +02: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
2026-06-21 23:10:27 +02:00
return 1
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
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
}
2026-06-21 23:10:08 +02:00
# ------------------------------------------------------------------------------
# Compare installed semver against a version spec at the spec's precision.
# Returns: 0 if match (e.g. spec 8.0 matches installed 8.0.40), 1 otherwise
# Usage: version_matches_spec "8.0.40" "8.0"
# ------------------------------------------------------------------------------
version_matches_spec() {
local installed = " $1 "
local spec = " $2 "
local spec_depth prefix i
local -a spec_parts installed_parts
[[ -n " $installed " && -n " $spec " ]] || return 1
IFS = '.' read -ra spec_parts <<< " $spec "
spec_depth = ${# spec_parts [@] }
(( spec_depth > 0)) || return 1
if (( spec_depth == 1)) ; then
[[ " ${ installed %%.* } " == " $spec " ]] && return 0
return 1
fi
IFS = '.' read -ra installed_parts <<< " $installed "
prefix = ""
for (( i = 0; i < spec_depth && i < ${# installed_parts [@] } ; i++)) ; do
[[ -n " $prefix " ]] && prefix += "."
prefix += " ${ installed_parts [i] } "
done
[[ " $prefix " == " $spec " ]]
}
2025-11-05 05:19:35 -08:00
# ------------------------------------------------------------------------------
# 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 [[ ${# failed [@] } -lt ${# packages [@] } ]] ; then
if [[ ${# failed [@] } -gt 0 ]] ; then
2026-06-21 23:10:27 +02:00
msg_error "Partial install — failed packages: ${ failed [*] } "
return 100
2026-03-02 08:44:59 +01:00
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-05-26 08:06:08 +02:00
# Provide a quick diagnostic: check if package exists in any configured repo
local _os_codename
_os_codename = $( get_os_info codename)
local _unavailable =()
for _pkg in " ${ packages [@] } " ; do
if ! apt-cache show " $_pkg " & >/dev/null; then
_unavailable +=( " $_pkg " )
fi
done
if [[ ${# _unavailable [@] } -gt 0 ]] ; then
msg_error "Package(s) not found in any configured repository: ${ _unavailable [*] } "
msg_error "Hint: These packages may not be available for ' ${ _os_codename } '. Check repository configuration or package names."
else
msg_error "Hint: Package(s) exist in the repo but could not be installed — run 'apt-get install -f' inside the container or check for dependency conflicts."
fi
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-05-26 08:06:08 +02:00
msg_error "Hint: The package may be held back, have conflicting dependencies, or the repository is unreachable. Check 'apt-cache policy ${ packages [*] } ' inside the container."
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
2026-08-26 21:24:33 +02:00
installed_version = $( mongod --version 2>/dev/null | awk '/db version/{print $3}' | sed 's/^v//' | cut -d. -f1,2)
2025-11-05 05:19:35 -08:00
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)
}
2026-08-26 21:24:33 +02:00
# ------------------------------------------------------------------------------
# Checks whether purging the given package glob(s) would also remove packages
# outside those globs (i.e. reverse dependents, such as an app depending on
# mongodb-org-server). Uses `apt-get -s` (simulate) with the stable apt-get
# output format, not `apt`, whose output is explicitly unstable for scripting.
# Returns 0 (safe to purge) or 1 (unsafe - prints the unexpected packages).
# Usage: _purge_is_safe 'mongodb*' ['other-glob*' ...]
# ------------------------------------------------------------------------------
_purge_is_safe() {
local -a globs =( " $@ " )
local -a collateral =()
local pkg glob matched
local sim_out
sim_out = $( apt-get -s purge -y " ${ globs [@] } " 2>/dev/null) || return 0
while IFS = read -r pkg; do
[[ -z " $pkg " ]] && continue
matched = 0
for glob in " ${ globs [@] } " ; do
# shellcheck disable=SC2053
[[ " $pkg " == $glob ]] && {
matched = 1
break
}
done
(( matched)) || collateral +=( " $pkg " )
done < <( awk '/^(Remv|Purg) /{print $2}' <<< " $sim_out " )
if (( ${# collateral [@] } > 0)) ; then
msg_warn "Refusing purge of ' ${ globs [*] } ': would also remove ${ collateral [*] } "
return 1
fi
return 0
}
2025-11-05 05:19:35 -08:00
# ------------------------------------------------------------------------------
# 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"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'mariadb*' || return 1
2025-11-05 05:19:35 -08:00
$STD apt purge -y 'mariadb*' >/dev/null 2>& 1 || true
cleanup_tool_keyrings "mariadb"
;;
mysql)
stop_all_services "mysql"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'mysql*' || return 1
2025-11-05 05:19:35 -08:00
$STD apt purge -y 'mysql*' >/dev/null 2>& 1 || true
2026-06-21 23:10:08 +02:00
# Keep data directory for safety (remove manually if needed)
# rm -rf /var/lib/mysql 2>/dev/null || true
2025-11-05 05:19:35 -08:00
cleanup_tool_keyrings "mysql"
;;
mongodb)
stop_all_services "mongod"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'mongodb*' || return 1
2025-11-05 05:19:35 -08:00
$STD apt purge -y 'mongodb*' >/dev/null 2>& 1 || true
2026-06-21 23:10:08 +02:00
# Keep data directory for safety (remove manually if needed)
# rm -rf /var/lib/mongodb 2>/dev/null || true
2025-11-05 05:19:35 -08:00
cleanup_tool_keyrings "mongodb"
;;
node | nodejs)
2026-08-26 21:24:33 +02:00
_purge_is_safe nodejs npm || return 1
2025-11-05 05:19:35 -08:00
$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"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'php*' || return 1
2025-11-05 05:19:35 -08:00
$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"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'postgresql*' || return 1
2025-11-05 05:19:35 -08:00
$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)
2026-08-26 21:24:33 +02:00
_purge_is_safe 'temurin*' 'adoptium*' 'openjdk*' || return 1
2025-11-05 05:19:35 -08:00
$STD apt purge -y 'temurin*' 'adoptium*' 'openjdk*' >/dev/null 2>& 1 || true
cleanup_tool_keyrings "adoptium"
;;
ruby)
cleanup_legacy_install "ruby"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'ruby*' || return 1
2025-11-05 05:19:35 -08:00
$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"
2026-08-26 21:24:33 +02:00
_purge_is_safe 'clickhouse*' || return 1
2025-11-05 05:19:35 -08:00
$STD apt purge -y 'clickhouse*' >/dev/null 2>& 1 || true
2026-06-21 23:10:08 +02:00
# Keep data directory for safety (remove manually if needed)
# rm -rf /var/lib/clickhouse 2>/dev/null || true
2025-11-05 05:19:35 -08:00
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
2026-06-21 23:10:08 +02:00
# If versions match at the requested precision, no update needed
if version_matches_spec " $current_version " " $target_version " ; then
2025-11-05 05:19:35 -08:00
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
2026-05-26 08:06:08 +02:00
distro_id = $( get_os_info id)
2025-11-05 05:19:35 -08:00
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
2026-05-26 08:06:08 +02:00
distro_codename = $( get_os_info codename)
2025-11-05 05:19:35 -08:00
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-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to downloads.mongodb.org or verify MongoDB version ${ version } is still available"
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
2026-05-26 08:06:08 +02:00
distro_codename = $( get_os_info codename)
2025-11-06 07:57:30 +01:00
2026-06-21 08:21:58 +10:00
if [[ " $distro_id " == "debian" && " $( dpkg --print-architecture) " == "arm64" ]] ; then
2026-06-22 04:45:41 +10:00
case " $distro_codename " in
trixie | forky) suite = "noble" ;;
bookworm) suite = "jammy" ;;
*) suite = "noble" ;;
esac
distro_id = "ubuntu"
2026-06-21 08:21:58 +10:00
repo_url = "https://repo.mongodb.org/apt/ubuntu"
elif [[ " $distro_id " == "debian" ]] ; then
2025-11-06 07:57:30 +01:00
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
2026-05-26 08:06:08 +02:00
distro_codename = $( get_os_info codename)
2025-11-05 05:19:35 -08:00
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-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to packages.sury.org (packages.sury.org/php)"
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
2026-05-26 08:06:08 +02:00
distro_codename = $( get_os_info codename)
2025-11-05 05:19:35 -08:00
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
2026-06-21 23:10:08 +02:00
EOF
return 0
;;
mysql)
if [[ -z " $gpg_key_url " ]] ; then
msg_error "MySQL repository requires gpg_key_url"
return 65
fi
cleanup_old_repo_files "mysql"
if ! download_gpg_key " $gpg_key_url " "/etc/apt/keyrings/mysql.gpg" "dearmor" ; then
msg_error "Failed to import MySQL GPG key"
return 7
fi
local distro_codename suite component
distro_codename = $( get_os_info codename)
if [[ " $distro_id " == "debian" ]] ; then
case " $distro_codename " in
trixie | forky | sid) suite = "bookworm" ;;
bookworm | bullseye) suite = " $distro_codename " ;;
*) suite = "bookworm" ;;
esac
else
suite = $( get_fallback_suite " $distro_id " " $distro_codename " " $repo_url " )
fi
case " $version " in
8.4 | 8.4.*) component = "mysql-8.4-lts" ;;
8.0 | 8.0.*) component = "mysql-8.0" ;;
*) component = "mysql- ${ version } " ;;
esac
cat <<EOF >/etc/apt/sources.list.d/mysql.sources
Types: deb
URIs: ${repo_url}/
Suites: ${suite}
Components: ${component}
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/mysql.gpg
2025-11-05 05:19:35 -08:00
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
2026-05-26 08:06:08 +02:00
distro_codename = $( get_os_info codename)
2025-11-05 05:19:35 -08:00
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
2026-06-21 23:10:27 +02:00
# Appends to a shared list so existing EXIT traps are preserved.
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-06-21 23:10:27 +02:00
_tools_temp_dirs =()
_tools_cleanup_temp_dirs() {
local d
for d in " ${ _tools_temp_dirs [@] } " ; do
rm -rf " $d " 2>/dev/null || true
done
}
_tools_register_temp_cleanup() {
[[ " ${ _TOOLS_TEMP_TRAP_SET :- } " == "1" ]] && return 0
_TOOLS_TEMP_TRAP_SET = 1
local existing
existing = $( trap -p EXIT 2>/dev/null | sed -n "s/^trap -- '\\(.*\\)' EXIT/\1/p" || true )
if [[ -n " $existing " && " $existing " != "_tools_cleanup_temp_dirs" ]] ; then
trap "_tools_cleanup_temp_dirs; ${ existing } " EXIT ERR INT TERM
else
trap _tools_cleanup_temp_dirs EXIT ERR INT TERM
fi
}
2025-10-22 06:25:26 -07:00
create_temp_dir() {
2026-06-21 23:10:27 +02:00
local tmp_dir
tmp_dir = $( mktemp -d) || return 1
_tools_temp_dirs +=( " $tmp_dir " )
_tools_register_temp_cleanup
2025-10-22 06:25:26 -07:00
echo " $tmp_dir "
}
2026-06-12 10:23:28 +02:00
# ------------------------------------------------------------------------------
# create_backup <path> [<path> ...] / restore_backup
#
# Standardized data backup helpers for update_script(). They replace the
# hand-rolled "cp/mv to a sibling dir, update, copy back, rm" dance that is
# duplicated across the ct/*.sh update functions.
#
# create_backup <path> [<path> ...]
# - Copies each given file/directory into a persistent store at
# /opt/<NSAPP>.backup, mirroring its absolute path inside the store, and
# records it in a manifest so restore_backup needs no arguments.
2026-08-04 16:54:05 +02:00
# - Idempotent per path: a path already recorded in the manifest (from this
# run or a previous failed one) is left untouched, keeping the
# last-known-good copy instead of overwriting it with now-partially-updated
# data on retry. Any requested path NOT yet in the manifest is backed up
# now and appended - so a manifest left incomplete by an interrupted prior
# run gets completed instead of silently missing paths on restore.
2026-06-12 10:23:28 +02:00
# - Missing source paths are skipped with a warning (not fatal).
# - Aborts the update on copy failure: if any file/dir cannot be backed up,
2026-08-04 16:54:05 +02:00
# the script exits before the update runs against unprotected data (the
# paths already recorded are left in place, so a retry only redoes the one
# that failed).
2026-06-12 10:23:28 +02:00
#
# restore_backup
# - Copies every path recorded in the manifest back to its origin (replacing
# whatever the update left there), then deletes the store.
# - No-op (with a warning) if no store exists.
#
# Override the store location with BACKUP_DIR if the default does not fit.
# ------------------------------------------------------------------------------
create_backup() {
local store manifest path dest
store = " ${ BACKUP_DIR :- /opt/ ${ NSAPP :- app } .backup } "
manifest = " ${ store } /.manifest"
[[ $# -eq 0 ]] && {
msg_warn "create_backup called without any paths"
return 0
}
2026-08-04 16:54:05 +02:00
if ! mkdir -p " $store " || ! touch " $manifest " ; then
2026-06-12 10:23:28 +02:00
msg_error "Backup failed: could not create store at ${ store } - aborting update"
exit 1
fi
2026-08-04 16:54:05 +02:00
msg_info "Backing up data"
2026-06-12 10:23:28 +02:00
for path in " $@ " ; do
path = " ${ path %/ } "
2026-08-04 16:54:05 +02:00
if grep -qxF " $path " " $manifest " 2>/dev/null; then
continue
fi
2026-06-12 10:23:28 +02:00
if [[ ! -e " $path " ]] ; then
msg_warn "Skipping backup of ' ${ path } ' (not found)"
continue
fi
2026-08-16 13:58:07 +02:00
if mountpoint -q " $path " 2>/dev/null; then
msg_warn "Skipping backup of ' ${ path } ' (is a mount point)"
continue
fi
2026-06-12 10:23:28 +02:00
dest = " ${ store } /files ${ path } "
if ! mkdir -p " $( dirname " $dest " ) " || ! cp -a " $path " " $dest " ; then
msg_error "Backup of ' ${ path } ' failed - aborting update"
exit 1
fi
echo " $path " >>" $manifest "
done
msg_ok "Backed up data to ${ store } "
}
restore_backup() {
local store manifest path src
store = " ${ BACKUP_DIR :- /opt/ ${ NSAPP :- app } .backup } "
manifest = " ${ store } /.manifest"
if [[ ! -f " $manifest " ]] ; then
msg_warn "No backup found to restore"
return 0
fi
msg_info "Restoring data"
while IFS = read -r path; do
[[ -z " $path " ]] && continue
src = " ${ store } /files ${ path } "
[[ -e " $src " ]] || continue
mkdir -p " $( dirname " $path " ) "
rm -rf " $path "
cp -a " $src " " $path "
done <" $manifest "
rm -rf " $store "
msg_ok "Restored data"
}
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
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
2026-08-03 16:57:55 +02:00
read -rp " ${ TAB } Would you like to enter a GitHub Personal Access Token (PAT)? [y/N]: " reply </dev/tty
2026-03-07 22:14:18 +01:00
reply = " ${ reply :- n } "
if [[ ! " ${ reply ,, } " = ~ ^( y| yes) $ ]] ; then
return 1
fi
local token
while true; do
2026-08-03 16:57:55 +02:00
read -rp " ${ TAB } Enter your GitHub PAT: " token </dev/tty
2026-03-07 22:14:18 +01:00
# 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" ]]
}
# ------------------------------------------------------------------------------
# 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-05-26 08:06:08 +02:00
msg_error "Hint: Check network connectivity or DNS resolution. The server may be unreachable or the URL may have changed."
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-05-26 08:06:08 +02:00
msg_error "Hint: Another process (apt, dpkg, unattended-upgrades) may hold a lock. Check: ps aux | grep -E 'apt|dpkg'"
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 " )
2026-08-04 16:55:08 +02:00
# NEVER remove the distro's own base sources file (debian.sources,
# ubuntu.sources, ...)
if [[ " $basename_file " == " $( get_os_info id) .sources" ]] ; then
2025-10-22 06:25:26 -07:00
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" \) \
2026-08-04 16:55:08 +02:00
! -name " $( get_os_info id) .sources" -delete 2>/dev/null || true
2026-03-02 08:44:59 +01:00
# 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 " || {
2026-05-26 08:06:08 +02:00
msg_error "Failed to download GPG key for ${ name } from: ${ gpg_url } "
msg_error "Hint: Check network connectivity. If behind a proxy or firewall, ensure HTTPS access to $( echo " $gpg_url " | grep -oE 'https?://[^/]+' ) is allowed."
2026-02-12 20:03:33 +01:00
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 "
2026-04-26 22:12:20 +02:00
# Flat repositories (suite ending with "/" or "./") must not have Components
if [[ " $suite " != *"/" && -n " $component " ]] ; then
2025-12-15 17:29:48 +01:00
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-06-21 23:10:27 +02:00
if ! $STD apt update; then
msg_error "apt update failed after adding repository: ${ name } "
msg_error "Hint: Verify suite ' ${ suite } ' and URI ' ${ repo_url } ' are valid for this distribution."
return 100
fi
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)
local tag = ""
2026-05-04 22:13:49 +02:00
2026-03-18 11:15:02 +01:00
if [[ -n " $prefix " ]] ; then
2026-05-04 22:13:49 +02:00
# Use git/matching-refs API for server-side prefix filtering. This avoids
# paging through unrelated tags (e.g. mongodb/mongo-tools where 100.x tags
# only appear after page 4 of /tags). Returns ALL tags matching the prefix
# in a single call, sorted lexicographically ascending; we pick the
# highest version using `sort -V`.
if ! github_api_call "https://api.github.com/repos/ ${ repo } /git/matching-refs/tags/ ${ prefix } " " $temp_file " ; then
rm -f " $temp_file "
return 22
fi
local count
count = $( jq 'length' " $temp_file " 2>/dev/null || echo 0)
if [[ " $count " -gt 0 ]] ; then
2026-05-08 15:39:20 +02:00
tag = $( jq -r '.[].ref' " $temp_file " |
sed 's|^refs/tags/||' |
sort -V |
tail -n1)
2026-05-04 22:13:49 +02:00
fi
2026-03-18 11:15:02 +01:00
else
2026-05-04 22:13:49 +02:00
# No prefix: just take the first (newest) tag from /tags
if ! github_api_call "https://api.github.com/repos/ ${ repo } /tags?per_page=1" " $temp_file " ; then
rm -f " $temp_file "
return 22
fi
2026-03-18 11:15:02 +01:00
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 } "
2026-07-13 11:37:39 +02:00
local end_time
end_time = $( date +%s)
2025-10-22 06:25:26 -07:00
local duration = $(( end_time - start_time))
2026-07-13 11:37:39 +02:00
echo " ${ label } took ${ duration } s"
2025-10-22 06:25:26 -07:00
}
# ------------------------------------------------------------------------------
# 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-07-19 14:53:46 +02:00
# ------------------------------------------------------------------------------
# _download_source_tarball <url> <dest> [curl args...]
#
# Downloads a source .tar.gz to <dest> and verifies the gzip stream is complete
# before returning. Source forges (GitHub/GitLab/Codeberg) build these archives
# on the fly; for large repos the response is occasionally a truncated-but-
# cleanly-closed gzip that curl accepts as success (HTTP 200) and which then
# fails at extraction time. We validate with `gzip -t` and re-download on
# failure, and abort stalled transfers (--speed-time) so a hung generation
# retries instead of blocking for minutes. Extra args pass through to curl
# (e.g. auth headers like -H "PRIVATE-TOKEN: ...").
#
# Returns: 0 on success, 250 on persistent failure.
# ------------------------------------------------------------------------------
_download_source_tarball() {
local url = " $1 " dest = " $2 "
shift 2
local attempt max = 3
for (( attempt = 1; attempt <= max; attempt++)) ; do
if curl --connect-timeout 15 --max-time 900 --speed-limit 1024 --speed-time 60 \
-fsSL " $@ " -o " $dest " " $url " && gzip -t " $dest " 2>/dev/null; then
return 0
fi
rm -f " $dest "
(( attempt < max)) && {
msg_warn "Source archive download failed or incomplete (attempt ${ attempt } / ${ max } ), retrying..."
sleep $(( attempt * 3 ))
}
done
return 250
}
# ------------------------------------------------------------------------------
# _deploy_source_tarball <tarball_path> <target> <workdir>
#
# Shared tail for the *source tarball* modes of the fetch_and_deploy_* helpers
# (GitHub/GitLab/Codeberg archive tarballs that contain a single top-level
# directory). Extracts <tarball_path> into <workdir>, then copies the contents
# of that top-level directory into <target>.
#
2026-08-16 13:58:07 +02:00
# - Honors CLEAN_INSTALL=1 (wipes <target> first, dotfiles included, mount
# points preserved — back up config dotfiles like .env via create_backup
# and call restore_backup BEFORE any build step that sources them).
2026-07-19 14:53:46 +02:00
# - Does NOT own <workdir>: the caller creates it and is responsible for its
# cleanup (typically via a RETURN trap on its tmpdir).
# - cp failures are non-fatal here, matching the previous inline behavior.
#
# Returns: 0 on success (or non-fatal cp failure), 251 on extraction failure.
# ------------------------------------------------------------------------------
_deploy_source_tarball() {
local tarball = " $1 " target = " $2 " workdir = " $3 "
mkdir -p " $target "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
2026-08-16 22:51:27 +02:00
find " ${ target :? } " -mindepth 1 \( -type d -exec mountpoint -q {} \; -prune \) -o -print0 | xargs -0 rm -rf --
2026-07-19 14:53:46 +02:00
fi
tar --no-same-owner -xzf " $tarball " -C " $workdir " || {
msg_error "Failed to extract tarball"
return 251
}
local unpack_dir
unpack_dir = $( find " $workdir " -mindepth 1 -maxdepth 1 -type d | head -n1)
shopt -s dotglob nullglob
cp -r " $unpack_dir " /* " $target /"
shopt -u dotglob nullglob
return 0
}
# ------------------------------------------------------------------------------
# _deploy_unpacked_archive <archive_path> <target> <workdir>
#
# Shared tail for the *prebuild* modes of the fetch_and_deploy_*_release helpers
# (release assets shipped as .zip / .tar.* / .tgz / .txz). Extracts the archive
# into <workdir>, then copies its payload into <target>. If the archive contains
# a single top-level directory, that directory is stripped (its contents land
# directly in <target>); otherwise the archive contents are copied as-is.
#
2026-08-16 13:58:07 +02:00
# - Honors CLEAN_INSTALL=1 (wipes <target> first, dotfiles included, mount
# points preserved — back up config dotfiles like .env via create_backup
# and call restore_backup BEFORE any build step that sources them).
2026-07-19 14:53:46 +02:00
# - Does NOT own <workdir>: the caller creates it and cleans it up.
#
# Returns: 0 on success, 65 on unsupported format, 251 on extraction failure,
# 252 on copy failure / empty archive.
# ------------------------------------------------------------------------------
_deploy_unpacked_archive() {
local archive = " $1 " target = " $2 " workdir = " $3 "
local filename = " ${ archive ##*/ } "
if [[ " $filename " == *.zip ]] ; then
ensure_dependencies unzip
unzip -q " $archive " -d " $workdir " || {
msg_error "Failed to extract ZIP archive"
return 251
}
elif [[ " $filename " == *.tar.* || " $filename " == *.tgz || " $filename " == *.txz ]] ; then
tar --no-same-owner -xf " $archive " -C " $workdir " || {
msg_error "Failed to extract TAR archive"
return 251
}
2026-07-20 11:57:41 +02:00
elif [[ " $filename " == *.7z ]] ; then
if [[ -f /etc/alpine-release ]] ; then
ensure_dependencies 7zip
else
ensure_dependencies p7zip-full
fi
7z x -y -o" $workdir " " $archive " >/dev/null 2>& 1 || {
msg_error "Failed to extract 7z archive"
return 251
}
2026-07-19 14:53:46 +02:00
else
msg_error "Unsupported archive format: $filename "
return 65
fi
2026-08-06 20:50:12 +01:00
local top_entries inner_dir source_dir
2026-07-19 14:53:46 +02:00
top_entries = $( find " $workdir " -mindepth 1 -maxdepth 1)
if [[ " $( echo " $top_entries " | wc -l) " -eq 1 && -d " $top_entries " ]] ; then
inner_dir = " $top_entries "
2026-08-06 20:50:12 +01:00
source_dir = " $inner_dir "
else
source_dir = " $workdir "
fi
shopt -s dotglob nullglob
if ! compgen -G " $source_dir /*" >/dev/null; then
if [[ -n " $inner_dir " ]] ; then
2026-07-19 14:53:46 +02:00
msg_error "Inner directory is empty: $inner_dir "
2026-08-06 20:50:12 +01:00
else
msg_error "Unpacked archive is empty"
2026-07-19 14:53:46 +02:00
fi
shopt -u dotglob nullglob
2026-08-06 20:50:12 +01:00
return 252
fi
# <target> is only touched once the payload is known good. Wiping earlier left
# CLEAN_INSTALL callers with an empty target directory whenever the download
# was truncated, the archive was unreadable, or it unpacked to nothing.
mkdir -p " $target "
if [[ " ${ CLEAN_INSTALL :- 0 } " == "1" ]] ; then
2026-08-16 22:51:27 +02:00
find " ${ target :? } " -mindepth 1 \( -type d -exec mountpoint -q {} \; -prune \) -o -print0 | xargs -0 rm -rf --
2026-08-06 20:50:12 +01:00
fi
if ! cp -r " $source_dir " /* " $target /" ; then
if [[ -n " $inner_dir " ]] ; then
msg_error "Failed to copy contents from $inner_dir to $target "
2026-07-19 14:53:46 +02:00
else
2026-08-06 20:50:12 +01:00
msg_error "Failed to copy contents to $target "
2026-07-19 14:53:46 +02:00
fi
shopt -u dotglob nullglob
2026-08-06 20:50:12 +01:00
return 252
2026-07-19 14:53:46 +02:00
fi
2026-08-06 20:50:12 +01:00
shopt -u dotglob nullglob
2026-07-19 14:53:46 +02:00
return 0
}
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
2026-07-21 22:49:58 +02:00
trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN
2026-03-18 07:44:13 +01:00
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-07-19 14:53:46 +02:00
_download_source_tarball " $tarball_url " " $tmpdir / $filename " || {
2026-03-18 07:44:13 +01:00
msg_error "Download failed: $tarball_url "
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-07-19 14:53:46 +02:00
_deploy_source_tarball " $tmpdir / $filename " " $target " " $tmpdir " || return 251
2026-02-24 11:07:40 +01:00
2026-03-18 07:44:13 +01:00
echo " $version " >" $version_file "
msg_ok "Deployed ${ app } ${ version } to ${ target } "
2026-02-24 11:07:40 +01:00
return 0
}
2026-07-11 20:00:29 +02:00
# ------------------------------------------------------------------------------
# Get the latest GitLab repository tag matching a glob pattern.
#
# Description:
# - Queries the GitLab repository tags API (up to 100 tags per page)
# - Filters tag names against a shell glob pattern (e.g. "web-v*" or "mobile-v*")
# - Always excludes pre-release tags (those containing alpha, beta, or rc)
# - Sorts matching tags with `sort -V` and returns the highest one
# - Supports GITLAB_TOKEN for private/rate-limited projects
#
# Usage:
# get_latest_gl_tag "owner/repo" "web-v*"
# get_latest_gl_tag "owner/repo" "mobile-v*"
# get_latest_gl_tag "owner/repo" # returns newest tag (no filter)
#
# Arguments:
# $1 - GitLab repo path (namespace/project, e.g. "mygroup/myapp")
# $2 - Optional glob pattern to filter tag names (e.g. "web-v*")
#
# Returns:
# Latest matching tag name on stdout, or non-zero on failure
# ------------------------------------------------------------------------------
get_latest_gl_tag() {
local repo = " $1 "
local pattern = " ${ 2 :- } "
local repo_encoded
repo_encoded = $( printf '%s' " $repo " | sed 's|/|%2F|g' )
2026-07-30 14:40:59 +02:00
local gitlab_url = " ${ GITLAB_URL :- https ://gitlab.com } "
local api_base = " ${ gitlab_url } /api/v4/projects/ ${ repo_encoded } /repository/tags"
2026-07-11 20:00:29 +02:00
local api_timeout = "--connect-timeout 10 --max-time 60"
local header =()
[[ -n " ${ GITLAB_TOKEN :- } " ]] && header =( -H "PRIVATE-TOKEN: $GITLAB_TOKEN " )
# If a pattern is given, pass it as a regex search to reduce server-side results.
# GitLab ?search= supports anchored regex; convert leading glob prefix to regex anchor.
local search_param = ""
if [[ -n " $pattern " ]] ; then
# Strip trailing wildcard for the search hint (server-side prefix filter).
local prefix = " ${ pattern %% \* * } "
[[ -n " $prefix " ]] && search_param = "?search=^ ${ prefix } &per_page=100" || search_param = "?per_page=100"
else
search_param = "?per_page=100"
fi
local temp_file
temp_file = $( mktemp) || return 1
local http_code
http_code = $( curl $api_timeout -sSL -w "%{http_code}" -o " $temp_file " \
" ${ header [@] } " " ${ api_base }${ search_param } " 2>/dev/null) || true
if [[ " $http_code " != "200" ]] ; then
rm -f " $temp_file "
msg_error "GitLab tags API returned HTTP $http_code for $repo "
return 22
fi
local tag = ""
if [[ -n " $pattern " ]] ; then
# Client-side glob filter + pre-release exclusion, then version-sort to pick the highest match.
tag = $( jq -r '.[].name' " $temp_file " 2>/dev/null | while IFS = read -r t; do
case " $t " in
*alpha* | *beta* | *rc*) continue ;;
$pattern ) echo " $t " ;;
esac
done | sort -V | tail -n1)
else
# No pattern: skip pre-release tags, take the first remaining (newest) one.
tag = $( jq -r '.[].name' " $temp_file " 2>/dev/null |
grep -Eiv '(alpha|beta|rc)' |
head -n1)
fi
rm -f " $temp_file "
if [[ -z " $tag " ]] ; then
msg_error "No tags matching ' ${ pattern :- * } ' found for ${ repo } "
return 250
fi
echo " $tag "
}
# ------------------------------------------------------------------------------
# Fetches and deploys a GitLab tag-based source tarball.
#
# Description:
# - Resolves the latest tag matching the given glob pattern via get_latest_gl_tag
# (or uses the exact tag if one is provided instead of "latest")
# - Downloads the GitLab source tarball for that tag
# - Extracts it to the target directory
# - Writes the resolved tag to ~/.<app> for update-checking
#
# Usage:
# fetch_and_deploy_gl_tag "myapp" "mygroup/myrepo" "web-v*"
# fetch_and_deploy_gl_tag "myapp" "mygroup/myrepo" "mobile-v*" "/opt/myapp"
# fetch_and_deploy_gl_tag "myapp" "mygroup/myrepo" "v*" # any v-tag
# fetch_and_deploy_gl_tag "myapp" "mygroup/myrepo" "web-v3.0*" # narrow version range
#
# Arguments:
# $1 - App name (used for version file ~/.<app> and lowercase tarball name)
# $2 - GitLab repo path (namespace/project, e.g. "mygroup/myapp")
# $3 - Tag pattern: glob (e.g. "web-v*") or exact tag (e.g. "web-v3.0.0").
# Use "latest" to fetch the single newest tag with no pattern filter.
# $4 - Target directory (default: /opt/$app)
#
# Notes:
# - Supports CLEAN_INSTALL=1 to wipe target before extracting
# - Supports GITLAB_TOKEN for private/rate-limited projects
2026-07-30 14:40:59 +02:00
# - Supports GITLAB_URL for self-hosted GitLab (default: https://gitlab.com)
2026-07-11 20:00:29 +02:00
# - For repos that only publish tags, not formal GitLab Releases
# (use fetch_and_deploy_gl_release for proper Releases with assets)
# ------------------------------------------------------------------------------
fetch_and_deploy_gl_tag() {
local app = " $1 "
local repo = " $2 "
local tag_pattern = " ${ 3 :- latest } "
local target = " ${ 4 :- /opt/ $app } "
local app_lc = ""
app_lc = " $( echo " ${ app ,, } " | tr -d ' ' ) "
local version_file = " $HOME /. ${ app_lc } "
local api_timeout = "--connect-timeout 10 --max-time 60"
local download_timeout = "--connect-timeout 15 --max-time 900"
local header =()
[[ -n " ${ GITLAB_TOKEN :- } " ]] && header =( -H "PRIVATE-TOKEN: $GITLAB_TOKEN " )
# Resolve the tag: if caller passed a glob/latest, query the API.
# If caller passed an exact tag (no wildcards), use it directly.
local resolved_tag = " $tag_pattern "
if [[ " $tag_pattern " == "latest" || " $tag_pattern " == *"*" * || " $tag_pattern " == *"?" * ]] ; then
local glob_arg = ""
[[ " $tag_pattern " != "latest" ]] && glob_arg = " $tag_pattern "
resolved_tag = $( get_latest_gl_tag " $repo " " $glob_arg " ) || {
msg_error "Failed to determine latest tag matching ' ${ tag_pattern } ' for ${ repo } "
return 250
}
fi
local current_version = ""
[[ -f " $version_file " ]] && current_version = $( <" $version_file " )
if [[ " $current_version " == " $resolved_tag " ]] ; then
msg_ok " $app is already up-to-date ( $resolved_tag )"
return 0
fi
local repo_encoded
repo_encoded = $( printf '%s' " $repo " | sed 's|/|%2F|g' )
# GitLab source tarball URL (no release needed, works for any tag).
2026-07-30 14:40:59 +02:00
local gitlab_url = " ${ GITLAB_URL :- https ://gitlab.com } "
2026-07-11 20:00:29 +02:00
local version_safe = " ${ resolved_tag // \/ /- } "
2026-07-30 14:40:59 +02:00
local tarball_url = " ${ gitlab_url } / ${ repo } /-/archive/ ${ resolved_tag } / ${ app_lc } - ${ version_safe } .tar.gz"
2026-07-11 20:00:29 +02:00
local tmpdir
tmpdir = $( mktemp -d) || return 1
2026-07-21 22:49:58 +02:00
trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN
2026-07-11 20:00:29 +02:00
local filename = " ${ app_lc } - ${ version_safe } .tar.gz"
msg_info "Fetching GitLab tag: ${ app } ( ${ resolved_tag } )"
2026-07-19 14:53:46 +02:00
_download_source_tarball " $tarball_url " " $tmpdir / $filename " " ${ header [@] } " || {
2026-07-11 20:00:29 +02:00
msg_error "Download failed: $tarball_url "
return 7
}
2026-07-19 14:53:46 +02:00
_deploy_source_tarball " $tmpdir / $filename " " $target " " $tmpdir " || return 251
2026-07-11 20:00:29 +02:00
echo " $resolved_tag " >" $version_file "
msg_ok "Deployed ${ app } ${ resolved_tag } to ${ target } "
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
# ------------------------------------------------------------------------------
2026-06-21 23:09:51 +02:00
TOOLS_GH_REL_JSON = ""
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-07-13 11:56:12 +02:00
local tag_prefix = " ${ 5 :- } " # optional tag prefix filter (e.g. web-v2)
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-07-01 22:35:15 +02:00
local gh_check_json = ""
2026-06-26 15:47:48 +10:00
gh_check_json = $( mktemp /tmp/tools-gh-check-XXXXXX) || return 73
2026-07-21 22:49:58 +02:00
trap 'rm -f "${gh_check_json:-}"; trap - RETURN' RETURN
2026-06-21 23:09:51 +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
2026-05-13 22:16:13 +02:00
local pinned_version_encoded = " ${ pinned_version_in // \/ /%2F } "
2026-06-21 23:09:51 +02:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o " $gh_check_json " \
2026-03-15 20:08:15 +01:00
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
" ${ header_args [@] } " \
2026-05-13 22:16:13 +02:00
"https://api.github.com/repos/ ${ source } /releases/tags/ ${ pinned_version_encoded } " 2>/dev/null) || true
2026-03-15 20:08:15 +01:00
2026-06-21 23:09:51 +02:00
if [[ " $http_code " == "200" ]] && [[ -s " $gh_check_json " ]] ; then
releases_json = "[ $( <" $gh_check_json " ) ]"
2026-03-15 20:08:15 +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
2026-06-21 23:09:51 +02:00
rm -f " $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\""
2026-06-21 23:09:51 +02:00
rm -f " $gh_check_json "
2026-03-26 16:07:38 +01:00
return 22
2026-03-15 20:08:15 +01:00
fi
2026-06-21 23:09:51 +02:00
rm -f " $gh_check_json "
2026-03-15 20:08:15 +01:00
fi
2026-07-13 11:56:12 +02:00
if [[ -z " $pinned_version_in " && -z " $tag_prefix " ]] ; then
2026-06-21 23:09:51 +02:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o " $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
2026-06-21 23:09:51 +02:00
if [[ " $http_code " == "200" ]] && [[ -s " $gh_check_json " ]] ; then
releases_json = "[ $( <" $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
2026-06-21 23:09:51 +02:00
rm -f " $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\""
2026-06-21 23:09:51 +02:00
rm -f " $gh_check_json "
2026-03-26 16:07:38 +01:00
return 22
2025-12-09 06:49:43 -08:00
fi
2026-06-21 23:09:51 +02:00
rm -f " $gh_check_json "
2025-12-09 06:49:43 -08:00
fi
2025-12-17 14:37:31 +01:00
2026-07-13 11:56:12 +02:00
# If no releases yet (pinned version, tag prefix, OR /latest failed), fetch up to 100
2025-12-09 06:49:43 -08:00
if [[ -z " $releases_json " ]] ; then
2026-06-21 23:09:51 +02:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o " $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
2026-06-21 23:09:51 +02:00
if [[ " $http_code " == "200" ]] && [[ -s " $gh_check_json " ]] ; then
releases_json = $( <" $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
2026-06-21 23:09:51 +02:00
rm -f " $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\""
2026-06-21 23:09:51 +02:00
rm -f " $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"
2026-06-21 23:09:51 +02:00
rm -f " $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 } )"
2026-06-21 23:09:51 +02:00
rm -f " $gh_check_json "
2026-03-26 16:07:38 +01:00
return 22
2026-02-22 11:54:21 +01:00
fi
2026-06-21 23:09:51 +02:00
rm -f " $gh_check_json "
2025-12-09 06:49:43 -08:00
fi
2025-04-28 15:25:10 +02:00
2026-07-13 11:56:12 +02:00
if [[ -n " $tag_prefix " ]] ; then
mapfile -t raw_tags < <( jq -r --arg p " $tag_prefix " \
'.[] | select(.draft==false and .prerelease==false) | select(.tag_name | startswith($p)) | .tag_name' <<< " $releases_json " )
else
mapfile -t raw_tags < <( jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<< " $releases_json " )
fi
2025-09-11 12:44:13 +02:00
if (( ${# raw_tags [@] } == 0)) ; then
2026-07-13 11:56:12 +02:00
if [[ -n " $tag_prefix " ]] ; then
msg_error "No stable releases matching prefix ' ${ tag_prefix } ' found for ${ app } "
else
msg_error "No stable releases found for ${ app } "
fi
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
2026-05-13 22:16:13 +02:00
if [[ " $current " = ~ ^v[ 0-9] ]] ; then
current = " ${ current : 1 } "
fi
2025-09-11 12:44:13 +02:00
# Pinned version handling
if [[ -n " $pinned_version_in " ]] ; then
2026-05-13 22:16:13 +02:00
local pin_clean
if [[ " $pinned_version_in " = ~ ^v[ 0-9] ]] ; then
pin_clean = " ${ pinned_version_in : 1 } "
else
pin_clean = " $pinned_version_in "
fi
2025-09-11 12:44:13 +02:00
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
}
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
2026-05-26 08:06:08 +02:00
download_with_progress() {
2025-09-11 12:44:13 +02:00
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
2026-05-26 08:06:08 +02:00
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
# ------------------------------------------------------------------------------
2026-05-17 14:01:56 +02:00
# curl_download - Downloads a file with stall detection and retry.
2026-03-15 10:00:37 +01:00
#
# Usage: curl_download <output_file> <url>
#
2026-05-17 14:01:56 +02:00
# Uses --speed-limit / --speed-time instead of a hard --max-time cap so that
# slow but progressing downloads (e.g. large .deb files from slow mirrors) are
# never aborted mid-transfer. Only aborts when throughput drops below 1 KB/s
# for 60 consecutive seconds (i.e. a genuine stall or dead connection).
# Retries up to 3 times on failure.
# Returns 0 on success, 7 if all attempts fail.
2026-03-15 10:00:37 +01:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
curl_download() {
2026-03-15 10:00:37 +01:00
local output = " $1 "
local url = " $2 "
2026-05-17 14:01:56 +02:00
local retries = 3
local attempt = 1
2026-03-15 10:00:37 +01:00
2026-05-17 14:01:56 +02:00
while (( attempt <= retries)) ; do
if curl --connect-timeout 15 \
--speed-limit 1024 \
--speed-time 60 \
-fsSL -o " $output " " $url " ; then
2026-03-15 10:00:37 +01:00
return 0
fi
2026-05-17 14:01:56 +02:00
if (( attempt < retries)) ; then
msg_warn "Download failed or stalled (attempt ${ attempt } / ${ retries } ), retrying..."
2026-03-15 10:00:37 +01:00
fi
2026-05-17 14:01:56 +02:00
(( attempt++))
2026-03-15 10:00:37 +01:00
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"
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# ------------------------------------------------------------------------------
# _diagnose_deb_failure()
#
# - Called when both apt and dpkg fail to install a .deb package
# - Extracts package metadata and detects common failure patterns
# - Outputs enhanced error messages with actionable hints:
# * PostgreSQL version conflicts (e.g., postgresql-16-foo with pg17 active)
# * Missing declared dependencies
# * Generic fallback hint pointing to the log
#
# Usage: _diagnose_deb_failure "/path/to/file.deb"
# Returns: always 0 (diagnostic only — caller must return the error code)
# ------------------------------------------------------------------------------
_diagnose_deb_failure() {
local deb_path = " $1 "
local filename = " ${ deb_path ##*/ } "
local pkg_name pkg_deps pkg_version
pkg_name = $( dpkg-deb -f " $deb_path " Package 2>/dev/null || echo " ${ filename %%_* } " )
pkg_version = $( dpkg-deb -f " $deb_path " Version 2>/dev/null || true )
pkg_deps = $( dpkg-deb -f " $deb_path " Depends 2>/dev/null || true )
msg_error "Failed to install ' ${ pkg_name }${ pkg_version :+ ( ${ pkg_version } ) } ' — both apt and dpkg reported errors"
# Detect PostgreSQL version conflict (e.g., postgresql-16-vchord while pg17 is active)
local pg_ver_needed pg_ver_installed
pg_ver_needed = $( echo " $filename " | grep -oP '(?<=postgresql-)[0-9]+(?=-)' | head -1 || true )
if [[ -n " $pg_ver_needed " ]] ; then
pg_ver_installed = $( psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1 || true )
if [[ -n " $pg_ver_installed " && " $pg_ver_needed " != " $pg_ver_installed " ]] ; then
msg_error "Version conflict: ' ${ pkg_name } ' is built for PostgreSQL ${ pg_ver_needed } , but PostgreSQL ${ pg_ver_installed } is installed on this system."
msg_error "Hint: Your distribution installed a different PostgreSQL version than expected. The script may need updating to use postgresql- ${ pg_ver_installed } -* packages."
return 0
fi
fi
# Show which declared dependencies are not satisfied
if [[ -n " $pkg_deps " ]] ; then
local missing_deps =()
while IFS = ',' read -ra dep_list; do
for dep_entry in " ${ dep_list [@] } " ; do
local dep_pkg
dep_pkg = $( echo " $dep_entry " | awk '{print $1}' | tr -d ' ' )
[[ -z " $dep_pkg " || " $dep_pkg " == "(" * ]] && continue
if ! dpkg-query -W -f= '${Status}' " $dep_pkg " 2>/dev/null | grep -q "install ok installed" ; then
missing_deps +=( " $dep_pkg " )
fi
done
done <<< " $pkg_deps "
if [[ ${# missing_deps [@] } -gt 0 ]] ; then
msg_error "Unmet dependencies: ${ missing_deps [*] } "
msg_error "Hint: Run 'apt-get install -f' inside the container to attempt automatic dependency resolution."
else
msg_error "Hint: Declared dependencies appear present but installation still failed. Check the log above for the exact error."
fi
else
msg_error "Hint: Check the installation log above for the exact dependency or configuration error."
fi
}
fetch_and_deploy_codeberg_release() {
2026-02-01 22:27:31 +01:00
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-07-21 22:49:58 +02:00
trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN
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-07-19 14:53:46 +02:00
if _download_source_tarball " $archive_url " " $tmpdir / $filename " ; 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 )"
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
2026-07-19 14:53:46 +02:00
_deploy_source_tarball " $tmpdir / $filename " " $target " " $tmpdir " || return 251
2026-02-01 22:27:31 +01:00
echo " $version " >" $version_file "
msg_ok "Deployed: $app ( $version )"
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-06-21 23:09:51 +02:00
local codeberg_rel_json
2026-06-26 15:47:48 +10:00
codeberg_rel_json = $( mktemp /tmp/tools-codeberg-rel-XXXXXX) || return 73
2026-07-21 22:49:58 +02:00
trap 'rm -f "$codeberg_rel_json"; rm -rf "${tmpdir:-}" "${unpack_tmp:-}"; trap - RETURN' RETURN
2026-06-21 23:09:51 +02:00
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
2026-06-21 23:09:51 +02:00
resp = $( curl --connect-timeout 10 --max-time " ${ api_timeouts [ $attempt ] } " -fsSL -w "%{http_code}" -o " $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
2026-06-21 23:09:51 +02:00
json = $( <" $codeberg_rel_json " )
2026-02-01 22:27:31 +01:00
# 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-07-19 14:53:46 +02:00
if _download_source_tarball " $archive_url " " $tmpdir / $filename " ; 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 )"
2026-03-26 16:07:38 +01:00
return 250
2026-02-01 22:27:31 +01:00
fi
2026-07-19 14:53:46 +02:00
_deploy_source_tarball " $tmpdir / $filename " " $target " " $tmpdir " || return 251
2026-02-01 22:27:31 +01: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"
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
2026-06-11 16:14:32 +10:00
[[ " $u " = ~ \. deb$ ]] || continue
if [[ " ${ arch ,, } " = ~ ^( amd64| x86_64) $ ]] ; then
[[ " $u " = ~ ( amd64| x86_64) .*\. deb$ ]] || continue
elif [[ " ${ arch ,, } " = ~ ^( arm64| aarch64) $ ]] ; then
[[ " $u " = ~ ( arm64| aarch64) .*\. deb$ ]] || continue
2026-02-01 22:27:31 +01:00
fi
2026-06-11 16:14:32 +10:00
url_match = " $u "
break
2026-02-01 22:27:31 +01:00
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 "
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 "
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 " || {
2026-05-26 08:06:08 +02:00
_diagnose_deb_failure " $tmpdir / $filename "
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)"
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"
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 "
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)
2026-07-19 14:53:46 +02:00
_deploy_unpacked_archive " $tmpdir / $filename " " $target " " $unpack_tmp " || return
2026-02-01 22:27:31 +01:00
### Singlefile Mode ###
elif [[ " $mode " == "singlefile" ]] ; then
local pattern = " ${ 6 % \" } "
pattern = " ${ pattern # \" } "
[[ -z " $pattern " ]] && {
msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)"
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"
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 "
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 "
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 )"
}
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
2026-06-11 16:14:32 +10:00
if [[ " ${ arch ,, } " = ~ ^( amd64| x86_64) $ ]] ; then
has_match = $( echo " $releases_list " | jq -r ".[ $i ].assets[].browser_download_url" | grep -qE '(amd64|x86_64).*\.deb$' && echo true )
elif [[ " ${ arch ,, } " = ~ ^( arm64| aarch64) $ ]] ; then
has_match = $( echo " $releases_list " | jq -r ".[ $i ].assets[].browser_download_url" | grep -qE '(arm64|aarch64).*\.deb$' && echo true )
fi
2026-02-07 20:37:50 +01:00
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."
2026-08-03 16:57:55 +02:00
read -rp "Use version ${ rel_tag } instead? [Y/n] (auto-yes in 60s): " -t 60 use_fallback </dev/tty || use_fallback = "y"
2026-02-07 20:37:50 +01:00
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
2026-05-26 08:06:08 +02:00
fetch_and_deploy_gh_release() {
2025-09-11 12:44:13 +02:00
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 :- } "
2026-07-13 11:56:12 +02:00
local tag_prefix = " ${ 7 :- } "
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
2026-06-21 23:09:51 +02:00
if [[ -n " ${ TOOLS_GH_REL_JSON :- } " && -f " $TOOLS_GH_REL_JSON " ]] ; then
rm -f " $TOOLS_GH_REL_JSON "
fi
local gh_rel_json
2026-06-26 15:47:48 +10:00
gh_rel_json = $( mktemp /tmp/tools-gh-rel-XXXXXX) || return 73
2026-06-21 23:09:51 +02:00
TOOLS_GH_REL_JSON = " $gh_rel_json "
2025-09-11 12:44:13 +02:00
local api_url = "https://api.github.com/repos/ $repo /releases"
2026-07-13 11:56:12 +02:00
if [[ " $version " != "latest" ]] ; then
api_url = " $api_url /tags/ $version "
elif [[ -n " $tag_prefix " ]] ; then
api_url = " $api_url ?per_page=100"
else
api_url = " $api_url /latest"
fi
2025-09-11 12:44:13 +02:00
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-06-21 23:09:51 +02:00
http_code = $( curl --connect-timeout 10 --max-time " ${ api_timeouts [ $(( attempt - 1 )) ] :- 240 } " -sSL -w "%{http_code}" -o " $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
2026-06-21 23:09:51 +02:00
json = $( <" $gh_rel_json " )
2026-07-13 11:56:12 +02:00
if [[ " $version " == "latest" && -n " $tag_prefix " ]] ; then
json = $( echo " $json " | jq --arg p " $tag_prefix " \
'[.[] | select(.draft==false and .prerelease==false) | select(.tag_name | startswith($p))][0] // empty' )
if [[ -z " $json " || " $json " == "null" ]] ; then
msg_error "No stable release matching prefix ' ${ tag_prefix } ' found for $repo on GitHub"
return 1
fi
fi
2025-09-11 12:44:13 +02:00
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
2026-07-21 22:49:58 +02:00
trap 'rm -rf "$tmpdir" "${unpack_tmp:-}"; trap - RETURN' RETURN
2025-09-11 12:44:13 +02:00
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-07-19 14:53:46 +02:00
_download_source_tarball " $direct_tarball_url " " $tmpdir / $filename " || {
2025-12-05 23:09:20 +01:00
msg_error "Download failed: $direct_tarball_url "
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
2026-07-19 14:53:46 +02:00
_deploy_source_tarball " $tmpdir / $filename " " $target " " $tmpdir " || return 251
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
2026-06-11 16:14:32 +10:00
[[ " $u " = ~ \. deb$ ]] || continue
if [[ " ${ arch ,, } " = ~ ^( amd64| x86_64) $ ]] ; then
[[ " $u " = ~ ( amd64| x86_64) .*\. deb$ ]] || continue
elif [[ " ${ arch ,, } " = ~ ^( arm64| aarch64) $ ]] ; then
[[ " $u " = ~ ( arm64| aarch64) .*\. deb$ ]] || continue
2025-09-11 12:44:13 +02:00
fi
2026-06-11 16:14:32 +10:00
url_match = " $u "
break
2025-09-11 12:44:13 +02:00
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-06-11 16:14:32 +10:00
[[ " $u " = ~ \. deb$ ]] || continue
if [[ " ${ arch ,, } " = ~ ^( amd64| x86_64) $ ]] ; then
[[ " $u " = ~ ( amd64| x86_64) .*\. deb$ ]] || continue
elif [[ " ${ arch ,, } " = ~ ^( arm64| aarch64) $ ]] ; then
[[ " $u " = ~ ( arm64| aarch64) .*\. deb$ ]] || continue
2026-02-22 11:54:21 +01:00
fi
2026-06-11 16:14:32 +10:00
url_match = " $u "
break
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 "
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 "
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 " || {
2026-05-26 08:06:08 +02:00
_diagnose_deb_failure " $tmpdir / $filename "
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)"
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"
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 "
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)
2026-07-19 14:53:46 +02:00
_deploy_unpacked_archive " $tmpdir / $filename " " $target " " $unpack_tmp " || return
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)"
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"
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 "
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 "
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 )"
}
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
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_adminer() {
2025-09-11 12:44:13 +02:00
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-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to github.com/vrana/adminer (GitHub Releases)"
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
2026-05-26 08:06:08 +02: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)
# ------------------------------------------------------------------------------
setup_clickhouse() {
local CLICKHOUSE_VERSION = " ${ CLICKHOUSE_VERSION :- latest } "
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
# Resolve "latest" version
if [[ " $CLICKHOUSE_VERSION " == "latest" ]] ; then
CLICKHOUSE_VERSION = $( curl -fsSL --max-time 15 https://packages.clickhouse.com/tgz/stable/ 2>/dev/null |
grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
sort -V | tail -n1 || echo "" )
# Fallback to GitHub API if package server failed
if [[ -z " $CLICKHOUSE_VERSION " ]] ; then
CLICKHOUSE_VERSION = $( get_latest_github_release "ClickHouse/ClickHouse" ) || true
fi
[[ -z " $CLICKHOUSE_VERSION " ]] && {
msg_error "Could not determine latest ClickHouse version from any source"
return 250
}
fi
# Get currently installed version
local CURRENT_VERSION = ""
if command -v clickhouse-server >/dev/null 2>& 1; then
CURRENT_VERSION = $( clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
fi
# Scenario 1: Already at target version - just update packages
if [[ -n " $CURRENT_VERSION " && " $CURRENT_VERSION " == " $CLICKHOUSE_VERSION " ]] ; then
msg_info "Update ClickHouse $CLICKHOUSE_VERSION "
ensure_apt_working || return 100
# Perform upgrade with retry logic (non-fatal if fails)
upgrade_packages_with_retry "clickhouse-server" "clickhouse-client" || {
msg_warn "ClickHouse package upgrade had issues, continuing with current version"
}
cache_installed_version "clickhouse" " $CLICKHOUSE_VERSION "
msg_ok "Update ClickHouse $CLICKHOUSE_VERSION "
return 0
fi
# 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"
2026-08-26 21:24:33 +02:00
remove_old_tool_version "clickhouse" || {
msg_error "Aborting ClickHouse upgrade: another package depends on it"
return 1
}
2026-05-26 08:06:08 +02:00
else
msg_info "Setup ClickHouse $CLICKHOUSE_VERSION "
fi
ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg
# Prepare repository (cleanup + validation)
prepare_repository_setup "clickhouse" || {
msg_error "Failed to prepare ClickHouse repository"
return 100
}
# Setup repository (ClickHouse uses 'stable' suite)
setup_deb822_repo \
"clickhouse" \
"https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
"https://packages.clickhouse.com/deb" \
"stable" \
"main"
# Install packages with retry logic
$STD apt update || {
msg_error "APT update failed for ClickHouse repository"
return 100
}
install_packages_with_retry "clickhouse-server" "clickhouse-client" || {
msg_error "Failed to install ClickHouse packages"
return 100
}
# Verify installation
if ! command -v clickhouse-server >/dev/null 2>& 1; then
msg_error "ClickHouse installation completed but clickhouse-server command not found"
return 127
fi
# Setup data directory
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
$STD systemctl enable clickhouse-server || {
msg_warn "Failed to enable clickhouse-server service"
}
safe_service_restart clickhouse-server || true
cache_installed_version "clickhouse" " $CLICKHOUSE_VERSION "
msg_ok "Setup ClickHouse $CLICKHOUSE_VERSION "
}
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
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
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-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to getcomposer.org"
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
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# Docker Engine Installation and Management (All-In-One)
2025-09-11 12:44:13 +02:00
#
# Description:
2026-05-26 08:06:08 +02:00
# - By default uses distro repository (docker.io) for stability
# - Optionally uses official Docker repository for latest features
# - Detects and migrates old Docker installations
# - Updates running containers interactively
# - Cleans up legacy repository files
2025-09-11 12:44:13 +02:00
#
2026-05-26 08:06:08 +02:00
# Usage:
2026-07-15 21:37:10 +02:00
# setup_docker # Uses official Docker repo (recommended)
# USE_DOCKER_REPO=false setup_docker # Uses distro docker.io package
2026-05-26 08:06:08 +02:00
# DOCKER_LOG_DRIVER="json-file" setup_docker
#
# Variables:
2026-07-15 21:37:10 +02:00
# USE_DOCKER_REPO - Set to "false" to use distro docker.io package
# (default: true, uses official Docker repository)
2026-05-26 08:06:08 +02:00
# DOCKER_LOG_DRIVER - Log driver (optional, default: "journald")
# DOCKER_SKIP_UPDATES - Skip container update check (optional, "true" to skip)
#
# Features:
2026-07-15 21:37:10 +02:00
# - Uses official Docker repository by default
2026-05-26 08:06:08 +02:00
# - Migrates from get.docker.com to repository-based installation
# - Updates Docker Engine if newer version available
2026-07-06 14:43:25 +02:00
# - Interactive per-container update prompt (Y/N, 60 s auto-no)
# - Compose-managed containers: full restart via docker compose
# - Standalone containers: image pull only, no destructive stop/rm
2026-08-02 19:57:57 +02:00
# - Portainer is no longer managed here; use tools/addon/portainer.sh
2026-06-21 23:09:51 +02:00
# - Set DOCKER_NONINTERACTIVE=1 to skip interactive prompts (CI/unattended)
2025-09-11 12:44:13 +02:00
# ------------------------------------------------------------------------------
2026-06-21 23:09:51 +02:00
_docker_is_noninteractive() {
[[ " ${ DOCKER_NONINTERACTIVE :- } " == "1" || " ${ DOCKER_NONINTERACTIVE :- } " == "true" || " ${ DOCKER_NONINTERACTIVE :- } " == "TRUE" ]] || [[ ! -t 0 ]]
}
2026-08-04 21:02:03 +02:00
# ------------------------------------------------------------------------------
# ensure_docker()
#
# - Docker precondition for addon scripts that ship a compose stack
# - Reuses an existing installation, otherwise offers to install Docker
# (Debian/Ubuntu via setup_docker, Alpine via apk)
# - Aborts when the user declines or Compose is unavailable afterwards
# ------------------------------------------------------------------------------
ensure_docker() {
if command -v docker & >/dev/null; then
msg_ok "Docker $( docker --version | cut -d' ' -f3 | tr -d ',' ) is available"
if docker compose version & >/dev/null; then
msg_ok "Docker Compose is available"
return 0
fi
msg_error "Docker Compose plugin is not available. Please install it."
exit 237
fi
msg_warn "Docker is not installed."
echo -n " ${ TAB :- } Install Docker now? (y/N): "
local install_docker_prompt
read -r install_docker_prompt
if [[ ! " ${ install_docker_prompt ,, } " = ~ ^( y| yes) $ ]] ; then
msg_error "Docker is required for ${ APP :- this script } . Exiting."
exit 254
fi
if is_alpine; then
msg_info "Installing Docker"
$STD apk add --no-cache docker docker-cli-compose
$STD rc-update add docker default
$STD rc-service docker start
msg_ok "Installed Docker"
else
setup_docker || {
msg_error "Docker installation failed."
exit 237
}
fi
if ! command -v docker & >/dev/null || ! docker compose version & >/dev/null; then
msg_error "Docker or the Compose plugin is still unavailable after installation."
exit 237
fi
}
2026-05-26 08:06:08 +02:00
setup_docker() {
local docker_installed = false
2026-07-15 21:37:10 +02:00
local USE_DOCKER_REPO = " ${ USE_DOCKER_REPO :- true } "
2025-09-11 12:44:13 +02:00
2026-05-26 08:06:08 +02: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"
2025-11-05 05:19:35 -08:00
fi
2026-07-15 21:37:10 +02:00
# Scenario 1: Use distro repository (opt-out via USE_DOCKER_REPO=false)
2026-05-26 08:06:08 +02:00
if [[ " $USE_DOCKER_REPO " != "true" && " $USE_DOCKER_REPO " != "TRUE" && " $USE_DOCKER_REPO " != "1" ]] ; then
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Install or upgrade Docker from distro repo
if [ " $docker_installed " = true ] ; then
msg_info "Checking for Docker updates (distro package)"
ensure_apt_working || return 100
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 )"
2025-11-05 05:19:35 -08:00
else
2026-05-26 08:06:08 +02:00
msg_info "Installing Docker (distro package)"
ensure_apt_working || return 100
# 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"
return 100
fi
# docker-compose is optional
$STD apt install -y docker-compose 2>/dev/null ||
msg_warn "Optional docker-compose not available from distro repository — use 'docker compose' plugin instead if needed"
DOCKER_CURRENT_VERSION = $( docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION (distro package)"
2025-11-05 05:19:35 -08:00
fi
2026-05-26 08:06:08 +02: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
{
"log-driver": "$log_driver"
}
EOF
fi
# Enable and start Docker
systemctl enable -q --now docker
# 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
# 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"
return 100
}
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 \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin || {
msg_error "Failed to install Docker packages"
return 100
}
DOCKER_CURRENT_VERSION = $( docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION "
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
fi
2026-07-06 14:43:25 +02:00
# Container Update Check
# - Skipped entirely when running unattended / non-interactive
2026-08-07 11:43:32 +02:00
# - Lists containers with a newer image available and the command to update
# them; does not pull/recreate anything itself
2026-07-06 14:43:25 +02:00
if [ " $docker_installed " = true ] && ! _docker_is_noninteractive; then
msg_info "Checking for container updates"
local name image compose_workdir compose_service current_digest latest_digest
local compose_updates =()
local standalone_updates =()
while IFS = read -r line; do
name = $( echo " $line " | awk '{print $1}' )
image = $( echo " $line " | awk '{print $2}' )
2026-08-02 19:57:57 +02:00
# Portainer is managed by its own addon script, not this generic loop
2026-07-06 14:43:25 +02:00
[[ " $name " == "portainer" || " $name " == "portainer_agent" ]] && continue
current_digest = $( docker inspect " $name " --format= '{{.Image}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
docker pull " $image " >/dev/null 2>& 1 || continue
latest_digest = $( docker inspect " $image " --format= '{{.Id}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
[[ -z " $latest_digest " || " $current_digest " == " $latest_digest " ]] && continue
compose_workdir = $( docker inspect " $name " \
--format= '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' 2>/dev/null || true )
compose_service = $( docker inspect " $name " \
--format= '{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || true )
if [[ -n " $compose_workdir " && -n " $compose_service " && -d " $compose_workdir " ]] ; then
compose_updates +=( " ${ name } | ${ image } | ${ compose_workdir } | ${ compose_service } " )
else
standalone_updates +=( " ${ name } | ${ image } " )
fi
done < <( docker ps --format '{{.Names}} {{.Image}}' )
# Stop spinner before any interactive prompt
stop_spinner
if [[ ${# compose_updates [@] } -eq 0 && ${# standalone_updates [@] } -eq 0 ]] ; then
msg_ok "All containers are up-to-date"
else
2026-08-07 11:43:32 +02:00
msg_warn "Updates available (not applied automatically):"
2026-07-06 14:43:25 +02:00
for entry in " ${ compose_updates [@] } " ; do
IFS = '|' read -r name image compose_workdir compose_service <<< " $entry "
2026-08-07 11:43:32 +02:00
echo -e " ${ TAB3 }${ name } ( ${ image } ) — update via: cd ${ compose_workdir } && docker compose pull ${ compose_service } && docker compose up -d ${ compose_service } "
2026-07-06 14:43:25 +02:00
done
for entry in " ${ standalone_updates [@] } " ; do
IFS = '|' read -r name image <<< " $entry "
2026-08-07 11:43:32 +02:00
echo -e " ${ TAB3 }${ name } ( ${ image } ) — update via: docker pull ${ image } && docker restart ${ name } "
2026-07-06 14:43:25 +02:00
done
fi
fi
2026-05-26 08:06:08 +02:00
msg_ok "Docker setup completed"
}
# ------------------------------------------------------------------------------
# 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
# ------------------------------------------------------------------------------
setup_ffmpeg() {
local TMP_DIR = $( mktemp -d)
local GITHUB_REPO = "FFmpeg/FFmpeg"
local VERSION = " ${ FFMPEG_VERSION :- latest } "
local TYPE = " ${ FFMPEG_TYPE :- full } "
local BIN_PATH = "/usr/local/bin/ffmpeg"
# 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 )"
# Binary fallback mode
if [[ " $TYPE " == "binary" ]] ; then
2026-06-11 16:14:32 +10:00
local ffmpeg_arch
case " $( dpkg --print-architecture 2>/dev/null || echo amd64) " in
arm64) ffmpeg_arch = "arm64" ;;
*) ffmpeg_arch = "amd64" ;;
esac
if ! CURL_TIMEOUT = 300 curl_with_retry "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release- ${ ffmpeg_arch } -static.tar.xz" " $TMP_DIR /ffmpeg.tar.xz" ; then
2026-05-26 08:06:08 +02:00
msg_error "Failed to download FFmpeg binary"
msg_error "Hint: Check connectivity to johnvansickle.com/ffmpeg (static builds, may be slow — large file)"
rm -rf " $TMP_DIR "
return 250
fi
tar -xf " $TMP_DIR /ffmpeg.tar.xz" -C " $TMP_DIR " || {
msg_error "Failed to extract FFmpeg binary"
rm -rf " $TMP_DIR "
return 251
}
local EXTRACTED_DIR
EXTRACTED_DIR = $( find " $TMP_DIR " -maxdepth 1 -type d -name "ffmpeg-*" )
cp " $EXTRACTED_DIR /ffmpeg" " $BIN_PATH "
cp " $EXTRACTED_DIR /ffprobe" /usr/local/bin/ffprobe
chmod +x " $BIN_PATH " /usr/local/bin/ffprobe
local FINAL_VERSION = $( $BIN_PATH -version 2>/dev/null | head -n1 | awk '{print $3}' )
rm -rf " $TMP_DIR "
cache_installed_version "ffmpeg" " $FINAL_VERSION "
ensure_usr_local_bin_persist
[[ -n " $INSTALLED_VERSION " ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION " || msg_ok "Setup FFmpeg $FINAL_VERSION "
return 0
fi
ensure_dependencies jq
# Auto-detect latest stable version if none specified
if [[ " $VERSION " == "latest" || -z " $VERSION " ]] ; then
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
fi
2025-09-11 12:44:13 +02:00
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
2026-04-24 14:29:55 +02:00
libdav1d-dev zlib1g-dev libnuma-dev
2025-09-11 12:44:13 +02:00
libva-dev libdrm-dev
)
2026-04-24 14:21:16 +02:00
if apt-cache show libsvtav1enc-dev & >/dev/null; then
DEPS +=( libsvtav1enc-dev)
elif apt-cache show libsvtav1-dev & >/dev/null; then
DEPS +=( libsvtav1-dev)
fi
2025-09-11 12:44:13 +02:00
;;
*)
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-06-11 16:14:32 +10:00
local ffmpeg_arch detected_arch
detected_arch = " $( dpkg --print-architecture 2>/dev/null || true ) "
if [[ -z " $detected_arch " ]] ; then
detected_arch = " $( uname -m 2>/dev/null || true ) "
fi
case " $detected_arch " in
arm64 | aarch64) ffmpeg_arch = "arm64" ;;
amd64 | x86_64) ffmpeg_arch = "amd64" ;;
*) ffmpeg_arch = "amd64" ;;
esac
if ! CURL_TIMEOUT = 300 curl_with_retry "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release- ${ ffmpeg_arch } -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"
2026-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to johnvansickle.com/ffmpeg (large file, may timeout on slow connections)"
2025-11-05 05:19:35 -08:00
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)
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
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 "
2026-08-26 21:24:33 +02:00
remove_old_tool_version "go" || {
msg_error "Aborting Go upgrade: another package depends on it"
return 1
}
2025-11-05 05:19:35 -08:00
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 "
2026-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to go.dev/dl — URL: $URL "
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
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
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
2026-05-25 08:50:00 +02:00
local LATEST_VERSION
# Tags are "gs10.05.1" format — fetch without stripping "v", then strip "gs"
LATEST_VERSION = $( get_latest_github_release "ArtifexSoftware/ghostpdl-downloads" "false" ) || {
2025-11-05 05:19:35 -08:00
msg_warn "Cannot fetch latest Ghostscript version from GitHub API"
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
2026-05-25 08:50:00 +02:00
}
LATEST_VERSION = " ${ LATEST_VERSION #gs } "
2026-05-27 08:14:17 +02:00
local LATEST_VERSION_DOTTED = $( echo " $LATEST_VERSION " | sed 's/\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{1\}\)/\1.\2.\3/' )
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"
2026-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to github.com/ArtifexSoftware — may be a GitHub rate-limit, set GITHUB_TOKEN"
2025-10-22 06:25:26 -07:00
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
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
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]: "
2026-08-03 16:57:55 +02:00
if read -r -t 60 selection </dev/tty; then
2026-01-06 19:27:04 +01:00
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 = ""
2026-08-03 16:57:55 +02:00
read -r -t 60 -p " ${ TAB3 } ⚙️ Install NVIDIA driver libraries in the container? [Y/n] (auto-yes in 60s): " nvidia_reply </dev/tty || nvidia_reply = ""
2026-01-27 23:40:47 +02:00
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
2026-05-26 08:06:08 +02:00
os_id = $( get_os_info id)
os_codename = $( get_os_info codename)
os_version = $( get_os_info version)
2025-12-17 14:37:31 +01:00
[[ -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
2026-06-21 23:09:51 +02:00
# so that TOOLS_GH_REL_JSON contains the compute-runtime release metadata.
2026-04-17 21:58:31 +02:00
# 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"
2026-06-21 23:09:51 +02:00
if [[ -n " ${ TOOLS_GH_REL_JSON :- } " && -f " $TOOLS_GH_REL_JSON " ]] ; then
2026-04-17 21:58:31 +02:00
local _body _parsed
2026-06-21 23:09:51 +02:00
_body = $( jq -r '.body // empty' " $TOOLS_GH_REL_JSON " 2>/dev/null) || return 0
2026-04-17 21:58:31 +02:00
_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-06-15 10:09:14 +02:00
mesa-vulkan-drivers \
vulkan-tools \
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-06-21 23:09:51 +02:00
# Fetch a compute-runtime package first so TOOLS_GH_REL_JSON is populated,
2026-04-17 21:58:31 +02:00
# 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-06-15 10:09:14 +02:00
mesa-vulkan-drivers \
vulkan-tools \
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 \
2026-06-15 10:09:14 +02:00
mesa-vulkan-drivers \
vulkan-tools \
2026-01-06 19:27:04 +01:00
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-06-21 23:09:51 +02:00
# Fetch a compute-runtime package first so TOOLS_GH_REL_JSON is populated,
2026-04-17 21:58:31 +02:00
# 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 \
2026-06-15 10:09:14 +02:00
mesa-vulkan-drivers \
vulkan-tools \
2026-01-06 19:27:04 +01:00
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.
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_imagemagick() {
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-06-30 10:19:05 +02:00
# Fetch and deploy source tarball from GitHub Releases
fetch_and_deploy_gh_release "imagemagick" "ImageMagick/ImageMagick" "tarball" || {
msg_error "Failed to download ImageMagick source from GitHub"
msg_error "Hint: Check connectivity to github.com/ImageMagick/ImageMagick"
2026-03-26 16:07:38 +01:00
return 250
2025-10-22 06:25:26 -07:00
}
2026-06-30 10:19:05 +02:00
cd /opt/imagemagick || {
msg_error "Failed to enter ImageMagick source directory"
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"
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"
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"
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"
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}' )
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)
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
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
2026-05-26 08:06:08 +02:00
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
2025-05-05 15:47:43 +02:00
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
2026-05-26 08:06:08 +02:00
setup_local_ip_helper() {
2025-09-11 12:44:13 +02:00
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
2026-06-21 23:10:08 +02:00
if [[ -n " $CURRENT_VERSION " ]] && version_matches_spec " $CURRENT_VERSION " " $MARIADB_VERSION " ; then
2025-11-05 05:19:35 -08:00
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
2026-06-21 23:10:08 +02:00
if [[ -n " $CURRENT_VERSION " ]] && ! version_matches_spec " $CURRENT_VERSION " " $MARIADB_VERSION " ; then
2025-11-05 05:19:35 -08:00
msg_info "Upgrade MariaDB from $CURRENT_VERSION to $MARIADB_VERSION "
2026-08-26 21:24:33 +02:00
remove_old_tool_version "mariadb" || {
msg_error "Aborting MariaDB upgrade: another package depends on it"
return 1
}
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
2026-05-26 08:06:08 +02:00
fi
2025-04-28 15:25:10 +02:00
}
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# Creates MariaDB database with user, charset and optional extra grants/modes
2025-05-15 15:18:41 +02:00
#
# Description:
2026-05-26 08:06:08 +02:00
# - 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
2025-09-11 12:44:13 +02:00
#
# Variables:
2026-05-26 08:06:08 +02:00
# 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)
2026-03-02 08:44:59 +01:00
#
2026-05-26 08:06:08 +02:00
# Exports:
# MARIADB_DB_NAME, MARIADB_DB_USER, MARIADB_DB_PASS
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
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"
return 65
2026-03-02 08:44:59 +01:00
fi
2026-05-26 08:06:08 +02:00
if [[ -z " ${ MARIADB_DB_PASS :- } " ]] ; then
MARIADB_DB_PASS = $( openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
msg_info "Setting up MariaDB Database"
2025-11-05 05:19:35 -08:00
2026-07-13 11:37:39 +02:00
# Use --defaults-extra-file to pass credentials safely and escape identifiers
# to prevent SQL injection via DB name / user / password
$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';"
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# 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
2025-09-11 12:44:13 +02:00
fi
2025-04-28 15:25:10 +02:00
2026-05-26 08:06:08 +02:00
# Optional sql_mode override
if [[ -n " ${ MARIADB_DB_SQL_MODE :- } " ]] ; then
$STD mariadb -u root -e "SET GLOBAL sql_mode=' ${ MARIADB_DB_SQL_MODE :- } ';"
2025-11-05 05:19:35 -08:00
fi
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
$STD mariadb -u root -e "FLUSH PRIVILEGES;"
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
local app_name = " ${ APPLICATION ,, } "
local CREDS_FILE = " ${ MARIADB_DB_CREDS_FILE :- ${ HOME } / ${ app_name } .creds } "
{
echo "MariaDB Credentials"
echo "Database: $MARIADB_DB_NAME "
echo "User: $MARIADB_DB_USER "
echo "Password: $MARIADB_DB_PASS "
} >>" $CREDS_FILE "
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
msg_ok "Set up MariaDB Database"
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
export MARIADB_DB_NAME
export MARIADB_DB_USER
export MARIADB_DB_PASS
}
2025-10-22 06:25:26 -07:00
2026-07-30 14:40:59 +02:00
# ------------------------------------------------------------------------------
# Creates a MySQL database and user (for apps that require MySQL, not MariaDB).
#
# Description:
# - Creates database, user and grants using the mysql root socket login
# - The user is created with host '%' because MySQL treats 'localhost'
# (socket) and '127.0.0.1' (TCP) as distinct hosts; apps connecting over
# TCP to 127.0.0.1 would not match a 'localhost' account. mysql-server
# binds to 127.0.0.1 by default, so '%' stays local-only.
#
# Variables:
# MYSQL_DB_NAME - Database name (required)
# MYSQL_DB_USER - Database user (required)
# MYSQL_DB_PASS - Password (optional, generated if unset)
#
# Exports:
# MYSQL_DB_NAME, MYSQL_DB_USER, MYSQL_DB_PASS
#
# Example:
# MYSQL_DB_NAME="fleet" MYSQL_DB_USER="fleet" setup_mysql_db
# ------------------------------------------------------------------------------
setup_mysql_db() {
if [[ -z " ${ MYSQL_DB_NAME :- } " || -z " ${ MYSQL_DB_USER :- } " ]] ; then
msg_error "MYSQL_DB_NAME and MYSQL_DB_USER must be set before calling setup_mysql_db"
return 65
fi
if [[ -z " ${ MYSQL_DB_PASS :- } " ]] ; then
MYSQL_DB_PASS = $( openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
fi
msg_info "Setting up MySQL Database"
$STD mysql -u root -e "CREATE DATABASE \` ${ MYSQL_DB_NAME // \` / \`\` } \` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
$STD mysql -u root -e "CREATE USER ' ${ MYSQL_DB_USER // \' / \'\' } '@'%' IDENTIFIED BY ' ${ MYSQL_DB_PASS // \' / \'\' } ';"
$STD mysql -u root -e "GRANT ALL ON \` ${ MYSQL_DB_NAME // \` / \`\` } \`.* TO ' ${ MYSQL_DB_USER // \' / \'\' } '@'%';"
$STD mysql -u root -e "FLUSH PRIVILEGES;"
local app_name = " ${ APPLICATION ,, } "
local CREDS_FILE = " ${ MYSQL_DB_CREDS_FILE :- ${ HOME } / ${ app_name } .creds } "
{
echo "MySQL Credentials"
echo "Database: $MYSQL_DB_NAME "
echo "User: $MYSQL_DB_USER "
echo "Password: $MYSQL_DB_PASS "
} >>" $CREDS_FILE "
msg_ok "Set up MySQL Database"
export MYSQL_DB_NAME
export MYSQL_DB_USER
export MYSQL_DB_PASS
}
2026-05-26 08:06:08 +02: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
# ------------------------------------------------------------------------------
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
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 } "
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# 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"
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Update mode: MeiliSearch already installed
if [[ -f /usr/bin/meilisearch ]] ; then
if check_for_gh_release "meilisearch" "meilisearch/meilisearch" ; then
msg_info "Updating MeiliSearch"
# 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 } "
2025-11-05 05:19:35 -08:00
2026-07-20 11:53:27 +02:00
# Extract major.minor for comparison (Meilisearch changes its on-disk DB format between minor versions)
2026-05-26 08:06:08 +02:00
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)
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# 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
2025-11-05 05:19:35 -08:00
2026-07-20 11:53:27 +02:00
# Read connection + storage paths for the in-place (dumpless) upgrade
local MEILI_HOST MEILI_PORT MEILI_DB_PATH
2026-05-26 08:06:08 +02:00
MEILI_HOST = " ${ MEILISEARCH_HOST :- 127 .0.0.1 } "
MEILI_PORT = " ${ MEILISEARCH_PORT :- 7700 } "
2026-07-20 11:53:27 +02:00
MEILI_DB_PATH = $( grep -E "^db_path\s*=" /etc/meilisearch.toml 2>/dev/null | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' || true )
MEILI_DB_PATH = " ${ MEILI_DB_PATH :- /var/lib/meilisearch/data } "
2025-11-05 05:19:35 -08:00
2026-07-20 11:53:27 +02:00
systemctl stop meilisearch
2025-04-28 15:25:10 +02:00
2026-07-20 11:53:27 +02:00
# Safety backup of the data dir before any in-place migration.
# Dumpless upgrade mutates the DB in place and cannot be rolled back on
# failure, so keep a tarball to restore from. (No master_key/API needed.)
local MEILI_BACKUP = ""
if [[ " $NEEDS_MIGRATION " == "true" ]] ; then
MEILI_BACKUP = "/var/lib/meilisearch/pre-upgrade- ${ CURRENT_VERSION } .tar.gz"
msg_info "Backing up data dir before upgrade → ${ MEILI_BACKUP } "
if ! tar -czf " $MEILI_BACKUP " -C " $MEILI_DB_PATH " . 2>/dev/null; then
msg_error "MeiliSearch data backup failed — aborting upgrade to avoid data loss"
systemctl start meilisearch
2026-06-21 23:09:51 +02:00
return 100
2026-05-26 08:06:08 +02:00
fi
2026-07-20 11:53:27 +02:00
msg_ok "Backup created: ${ MEILI_BACKUP } "
2026-05-26 08:06:08 +02:00
fi
2025-04-28 15:25:10 +02:00
2026-07-20 11:53:27 +02:00
# Replace the binary
2026-06-21 02:56:49 +10:00
if [[ " $( arch_resolve) " == "arm64" ]] ; then
fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "singlefile" "latest" "/usr/bin" "meilisearch-linux-aarch64"
else
fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "binary"
fi
2025-04-28 15:25:10 +02:00
2026-07-20 11:53:27 +02:00
if [[ " $NEEDS_MIGRATION " == "true" ]] ; then
2026-07-27 19:12:31 +02:00
# One-time in-place migration via --experimental-dumpless-upgrade (replaced with --upgrade-db)
2026-07-20 11:53:27 +02:00
# Meilisearch migrates the on-disk DB during this startup; subsequent
# normal (systemd) starts run without the flag.
msg_info "Migrating data in place (dumpless upgrade ${ CURRENT_VERSION } → ${ NEW_VERSION } )"
2026-07-27 19:12:31 +02:00
/usr/bin/meilisearch --config-file-path /etc/meilisearch.toml --upgrade-db >/dev/null 2>& 1 &
2026-07-20 11:53:27 +02:00
local MEILI_PID = $!
local MAX_WAIT = 300
local WAITED = 0
local MIGRATED = false
while [[ $WAITED -lt $MAX_WAIT ]] ; do
if curl -sf "http:// ${ MEILI_HOST } : ${ MEILI_PORT } /health" & >/dev/null; then
MIGRATED = true
break
fi
# Bail early if the migration process died
if ! kill -0 $MEILI_PID 2>/dev/null; then
msg_warn "MeiliSearch process exited during migration"
break
fi
sleep 3
WAITED = $(( WAITED + 3 ))
done
2025-11-05 05:19:35 -08:00
2026-07-20 11:53:27 +02:00
# Stop the one-shot migration process and hand over to systemd
kill $MEILI_PID 2>/dev/null || true
wait $MEILI_PID 2>/dev/null || true
sleep 2
systemctl start meilisearch
2025-11-05 05:19:35 -08:00
2026-07-20 11:53:27 +02:00
if [[ " $MIGRATED " == "true" ]] && systemctl is-active --quiet meilisearch; then
msg_ok "MeiliSearch migrated successfully (backup kept at ${ MEILI_BACKUP } )"
2026-05-26 08:06:08 +02:00
else
2026-07-20 11:53:27 +02:00
msg_error "MeiliSearch migration failed. Restore with: systemctl stop meilisearch; rm -rf ${ MEILI_DB_PATH :? } /*; tar -xzf ${ MEILI_BACKUP } -C ${ MEILI_DB_PATH } ; then reinstall the previous version"
2026-05-26 08:06:08 +02:00
fi
else
systemctl start meilisearch
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
msg_ok "Updated MeiliSearch"
fi
return 0
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Fresh install
msg_info "Setup MeiliSearch"
2026-06-21 02:56:49 +10:00
# Install binary (meilisearch publishes only an amd64 .deb; arm64 uses the raw linux-aarch64 binary)
if [[ " $( arch_resolve) " == "arm64" ]] ; then
fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "singlefile" "latest" "/usr/bin" "meilisearch-linux-aarch64" || {
msg_error "Failed to install MeiliSearch binary"
return 250
}
else
fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "binary" || {
msg_error "Failed to install MeiliSearch binary"
return 250
}
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02: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"
msg_error "Hint: Check connectivity to raw.githubusercontent.com/meilisearch/meilisearch"
return 7
}
2025-10-22 16:21:01 +02:00
2026-05-26 08:06:08 +02:00
# Generate master key
MEILISEARCH_MASTER_KEY = $( openssl rand -base64 12)
export MEILISEARCH_MASTER_KEY
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# 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
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Create data directories
mkdir -p " ${ MEILISEARCH_DB_PATH } " " ${ MEILISEARCH_DUMP_DIR } " " ${ MEILISEARCH_SNAPSHOT_DIR } "
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# Create systemd service
cat <<EOF >/etc/systemd/system/meilisearch.service
[Unit]
Description=Meilisearch
After=network.target
2025-04-28 15:25:10 +02:00
2026-05-26 08:06:08 +02:00
[Service]
ExecStart=/usr/bin/meilisearch --config-file-path /etc/meilisearch.toml
Restart=always
2026-04-23 22:17:25 +02:00
2026-05-26 08:06:08 +02:00
[Install]
WantedBy=multi-user.target
EOF
2026-04-23 22:17:25 +02:00
2026-05-26 08:06:08 +02:00
# 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
2026-04-23 22:17:25 +02:00
fi
2026-05-26 08:06:08 +02:00
sleep 1
done
2026-04-23 22:17:25 +02:00
2026-05-26 08:06:08 +02:00
# Verify service is running
if ! systemctl is-active --quiet meilisearch; then
msg_error "MeiliSearch service failed to start"
return 150
2026-04-23 22:17:25 +02:00
fi
2025-10-22 16:38:58 +02:00
2026-05-26 08:06:08 +02:00
# 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" \
-H "Authorization: Bearer ${ MEILISEARCH_MASTER_KEY } " 2>/dev/null |
grep -o '"key":"[^"]*"' | head -n 1 | sed 's/"key":"//;s/"//' ) || true
[[ -n " $MEILISEARCH_API_KEY " ]] && break
sleep 2
done
2025-04-28 15:25:10 +02:00
2026-05-26 08:06:08 +02:00
MEILISEARCH_API_KEY_UID = $( curl -s -X GET "http:// ${ MEILISEARCH_HOST } : ${ MEILISEARCH_PORT } /keys" \
-H "Authorization: Bearer ${ MEILISEARCH_MASTER_KEY } " 2>/dev/null |
grep -o '"uid":"[^"]*"' | head -n 1 | sed 's/"uid":"//;s/"//' ) || true
2026-05-10 20:05:03 +02:00
2026-05-26 08:06:08 +02:00
export MEILISEARCH_API_KEY
export MEILISEARCH_API_KEY_UID
2026-05-10 20:05:03 +02:00
2026-05-26 08:06:08 +02:00
# 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 } "
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
msg_ok "Setup MeiliSearch ${ MEILISEARCH_VERSION :- } "
2025-04-28 15:25:10 +02:00
}
2025-04-29 16:04:29 +02:00
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# Installs or updates MongoDB to specified version.
2025-05-15 15:18:41 +02:00
#
2025-09-11 12:44:13 +02:00
# Description:
2026-05-26 08:06:08 +02:00
# - Preserves data across installations
# - Adds official MongoDB repo
2025-09-11 12:44:13 +02:00
#
# Variables:
2026-05-26 08:06:08 +02:00
# MONGO_VERSION - MongoDB version to install (e.g. 7.0, 8.2)
2025-05-15 15:18:41 +02:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_mongodb() {
local MONGO_VERSION = " ${ MONGO_VERSION :- 8 .0 } "
2025-10-22 06:25:26 -07:00
local DISTRO_ID DISTRO_CODENAME
2026-05-26 08:06:08 +02:00
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2026-06-19 17:53:08 +10:00
# Check CPU requirements: AVX on x86_64, NEON/ASIMD on arm64
case " $( uname -m) " in
aarch64)
if ! grep -qm1 'asimd' /proc/cpuinfo; then
msg_error "MongoDB ${ MONGO_VERSION } requires ARMv8.2-A support, which is not available on this system."
2026-05-26 08:06:08 +02:00
return 236
fi
2026-06-19 17:53:08 +10:00
;;
*)
if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then
local major = " ${ MONGO_VERSION %%.* } "
if (( major > 5)) ; then
msg_error "MongoDB ${ MONGO_VERSION } requires AVX support, which is not available on this system."
return 236
fi
fi
;;
esac
2026-05-26 08:06:08 +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 "
return 238
;;
esac
# Get currently installed version
local INSTALLED_VERSION = ""
INSTALLED_VERSION = $( is_tool_installed "mongodb" 2>/dev/null) || true
# Scenario 1: Already at target version - just update packages
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " == " $MONGO_VERSION " ]] ; then
msg_info "Update MongoDB $MONGO_VERSION "
ensure_apt_working || return 100
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
# Perform upgrade with retry logic
upgrade_packages_with_retry "mongodb-org" || {
msg_error "Failed to upgrade MongoDB"
return 100
}
cache_installed_version "mongodb" " $MONGO_VERSION "
msg_ok "Update MongoDB $MONGO_VERSION "
return 0
fi
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02: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 "
2026-08-26 21:24:33 +02:00
remove_old_tool_version "mongodb" || {
msg_error "Aborting MongoDB upgrade: another package depends on it (e.g. an app using it)"
return 1
}
2026-05-26 08:06:08 +02:00
else
msg_info "Setup MongoDB $MONGO_VERSION "
2026-01-30 10:29:51 +01:00
fi
2026-05-26 08:06:08 +02:00
cleanup_orphaned_sources
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "mongodb" || {
msg_error "Failed to prepare MongoDB repository"
return 100
}
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
# Setup repository
# MongoDB 8.x versions beyond 8.0 reuse the server-8.0.asc PGP key
local MONGO_KEY_VERSION = " ${ MONGO_VERSION } "
[[ " ${ MONGO_VERSION } " == 8.[ 1-9] * ]] && MONGO_KEY_VERSION = "8.0"
manage_tool_repository "mongodb" " $MONGO_VERSION " " $MONGO_BASE_URL " \
"https://www.mongodb.org/static/pgp/server- ${ MONGO_KEY_VERSION } .asc" || {
msg_error "Failed to setup MongoDB repository"
return 100
}
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
# Wait for repo to settle
$STD apt update || {
msg_error "APT update failed — invalid MongoDB repo for ${ DISTRO_ID } - ${ DISTRO_CODENAME } ?"
return 100
}
2025-04-29 16:04:29 +02:00
2026-05-26 08:06:08 +02:00
# Install MongoDB with retry logic
install_packages_with_retry "mongodb-org" || {
msg_error "Failed to install MongoDB packages"
return 100
}
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
if ! command -v mongod >/dev/null 2>& 1; then
msg_error "MongoDB binary not found after installation"
return 127
2025-05-05 15:47:43 +02:00
fi
2025-06-18 12:07:08 +02:00
2026-05-26 08:06:08 +02:00
mkdir -p /var/lib/mongodb
chown -R mongodb:mongodb /var/lib/mongodb
2026-01-30 10:29:51 +01:00
2026-07-17 12:53:05 +02:00
local KERNEL_VERSION MONGO_MAJOR
KERNEL_VERSION = $( uname -r | cut -d- -f1)
MONGO_MAJOR = " ${ MONGO_VERSION %%.* } "
if (( MONGO_MAJOR >= 8)) && [[ " $( printf '%s\n' "6.19" " $KERNEL_VERSION " | sort -V | head -n1) " == "6.19" ]] ; then
mkdir -p /etc/systemd/system/mongod.service.d
cat <<EOF >/etc/systemd/system/mongod.service.d/rseq.conf
[Service]
Environment=GLIBC_TUNABLES=glibc.pthread.rseq=1
EOF
systemctl daemon-reload
fi
2026-05-26 08:06:08 +02:00
$STD systemctl enable mongod || {
msg_warn "Failed to enable mongod service"
}
safe_service_restart mongod
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02: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
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
cache_installed_version "mongodb" " $MONGO_VERSION "
msg_ok "Setup MongoDB $MONGO_VERSION "
}
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
# ------------------------------------------------------------------------------
# Installs or upgrades MySQL.
#
# Description:
# - By default uses distro repository (Debian/Ubuntu apt) for stability
# - Optionally uses official MySQL repository for specific versions
# - Detects existing MySQL installation
# - Purges conflicting packages before installation
# - Supports clean upgrade
# - Handles Debian Trixie libaio1t64 transition
#
# Variables:
# USE_MYSQL_REPO - Use official MySQL repository (default: true)
# Set to "false" to use distro packages instead
# MYSQL_VERSION - MySQL version to install when using official repo
# (e.g. 8.0, 8.4) (default: 8.0)
#
# Examples:
# 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-06-18 12:07:08 +02:00
2026-05-26 08:06:08 +02:00
setup_mysql() {
local MYSQL_VERSION = " ${ MYSQL_VERSION :- 8 .0 } "
local USE_MYSQL_REPO = " ${ USE_MYSQL_REPO :- true } "
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2025-11-22 13:02:53 +01:00
2026-05-26 08:06:08 +02:00
# Get currently installed version
local CURRENT_VERSION = ""
CURRENT_VERSION = $( is_tool_installed "mysql" 2>/dev/null) || true
2025-11-22 13:02:53 +01:00
2026-05-26 08:06:08 +02: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)"
2025-11-22 13:02:53 +01:00
2026-05-26 08:06:08 +02:00
# If already installed, just update
if [[ -n " $CURRENT_VERSION " ]] ; then
msg_info "Update MySQL $CURRENT_VERSION "
ensure_apt_working || return 100
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"
return 100
}
cache_installed_version "mysql" " $CURRENT_VERSION "
msg_ok "Update MySQL $CURRENT_VERSION "
return 0
fi
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
# Fresh install from distro repo
ensure_apt_working || return 100
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02: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"
return 100
}
}
}
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"
return 100
}
}
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"
return 100
}
fi
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02:00
# 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
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02:00
# Scenario 2: Use official MySQL repository (USE_MYSQL_REPO=true)
# Scenario 2a: Already at target version - just update packages
2026-06-21 23:10:08 +02:00
if [[ -n " $CURRENT_VERSION " ]] && version_matches_spec " $CURRENT_VERSION " " $MYSQL_VERSION " ; then
2026-05-26 08:06:08 +02:00
msg_info "Update MySQL $MYSQL_VERSION "
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
ensure_apt_working || return 100
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
# Perform upgrade with retry logic (non-fatal if fails)
upgrade_packages_with_retry "mysql-server" "mysql-client" || {
msg_warn "MySQL package upgrade had issues, continuing with current version"
}
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02:00
cache_installed_version "mysql" " $MYSQL_VERSION "
msg_ok "Update MySQL $MYSQL_VERSION "
return 0
2026-01-30 10:29:51 +01:00
fi
2026-05-26 08:06:08 +02:00
# Scenario 2: Different version installed - clean upgrade
2026-06-21 23:10:08 +02:00
if [[ -n " $CURRENT_VERSION " ]] && ! version_matches_spec " $CURRENT_VERSION " " $MYSQL_VERSION " ; then
2026-05-26 08:06:08 +02:00
msg_info "Upgrade MySQL from $CURRENT_VERSION to $MYSQL_VERSION "
2026-08-26 21:24:33 +02:00
remove_old_tool_version "mysql" || {
msg_error "Aborting MySQL upgrade: another package depends on it"
return 1
}
2026-05-26 08:06:08 +02:00
else
msg_info "Setup MySQL $MYSQL_VERSION "
2025-06-18 12:07:08 +02:00
fi
2026-05-26 08:06:08 +02:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "mysql" || {
msg_error "Failed to prepare MySQL repository"
return 100
}
2025-06-18 12:07:08 +02:00
2026-05-26 08:06:08 +02: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)"
2026-01-30 10:29:51 +01:00
2026-05-26 08:06:08 +02:00
if ! download_gpg_key "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" "/etc/apt/keyrings/mysql.gpg" "dearmor" ; then
msg_error "Failed to import MySQL GPG key"
return 7
fi
2025-11-22 13:23:40 +01:00
2026-05-26 08:06:08 +02:00
cat >/etc/apt/sources.list.d/mysql.sources <<EOF
Types: deb
URIs: https://repo.mysql.com/apt/debian/
Suites: bookworm
Components: mysql-8.4-lts
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/mysql.gpg
EOF
$STD apt update || {
msg_error "Failed to update APT for MySQL 8.4 LTS"
2026-03-26 16:07:38 +01:00
return 100
2026-05-26 08:06:08 +02:00
}
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02: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)"
return 100
2025-11-22 13:23:40 +01:00
}
2026-05-26 08:06:08 +02:00
msg_ok "Setup Database Engine (MariaDB fallback on Debian ${ DISTRO_CODENAME } )"
return 0
2025-11-22 13:08:30 +01:00
fi
2026-05-26 08:06:08 +02:00
cache_installed_version "mysql" "8.4"
msg_ok "Setup MySQL 8.4 LTS (Debian ${ DISTRO_CODENAME } )"
return 0
2025-11-22 12:52:47 +01:00
fi
2025-06-18 12:07:08 +02:00
2026-05-26 08:06:08 +02:00
# 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 } " )
2025-09-11 12:44:13 +02:00
fi
2026-05-26 08:06:08 +02:00
# Setup repository
manage_tool_repository "mysql" " $MYSQL_VERSION " "https://repo.mysql.com/apt/ ${ DISTRO_ID } " \
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" || {
msg_error "Failed to setup MySQL repository"
return 100
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
ensure_apt_working || return 100
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02:00
# Try multiple package names with retry logic
local mysql_install_success = false
2025-11-22 12:52:47 +01:00
2026-05-26 08:06:08 +02:00
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
2025-11-22 12:52:47 +01:00
fi
2026-05-26 08:06:08 +02:00
if [[ " $mysql_install_success " == false ]] ; then
msg_error "MySQL ${ MYSQL_VERSION } package not available for suite ${ SUITE } "
return 100
fi
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Verify mysql command is accessible
if ! command -v mysql >/dev/null 2>& 1; then
hash -r
if ! command -v mysql >/dev/null 2>& 1; then
msg_error "MySQL installed but mysql command still not found"
return 127
2026-03-18 16:45:11 +01:00
fi
fi
2026-01-12 08:48:36 +01:00
2026-05-26 08:06:08 +02:00
cache_installed_version "mysql" " $MYSQL_VERSION "
msg_ok "Setup MySQL $MYSQL_VERSION "
}
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +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:
2026-06-21 22:24:10 +02:00
# NODE_VERSION - Node.js version to install (default: 24 LTS)
# NODE_MODULE - Comma-separated global modules (e.g. "yarn,corepack,pnpm@10")
# NODE_COREPACK_ENABLE - Run corepack enable after install (default: 1 when corepack is used)
2026-05-26 08:06:08 +02:00
# ------------------------------------------------------------------------------
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
setup_nodejs() {
local NODE_VERSION = " ${ NODE_VERSION :- 24 } "
local NODE_MODULE = " ${ NODE_MODULE :- } "
2026-06-21 22:24:10 +02:00
local NODE_COREPACK_ENABLE = " ${ NODE_COREPACK_ENABLE :- 1 } "
local node_major = " ${ NODE_VERSION %%.* } "
local wants_corepack = 0
local node_setup_ok_msg = ""
2026-03-02 08:44:59 +01:00
2026-07-03 22:22:06 +02:00
# Corepack must run fully non-interactive. Without this it prints
# "Corepack is about to download X. Do you want to continue? [Y/n]" and blocks
# the whole install waiting for keyboard input - both here and in the calling
# script's later `corepack prepare` / `corepack <pm>` calls, which run in this
# same shell and inherit the export.
export COREPACK_ENABLE_DOWNLOAD_PROMPT = 0
2026-05-26 08:06:08 +02:00
# ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts
cleanup_legacy_install "nodejs"
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Get currently installed version
local CURRENT_NODE_VERSION = ""
CURRENT_NODE_VERSION = $( is_tool_installed "nodejs" 2>/dev/null) || true
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Ensure jq is available for JSON parsing
if ! command -v jq & >/dev/null; then
$STD apt update
$STD apt install -y jq || {
msg_error "Failed to install jq"
2026-03-26 16:07:38 +01:00
return 100
2026-03-02 08:44:59 +01:00
}
2026-05-26 08:06:08 +02:00
fi
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Scenario 1: Already installed at target version - upgrade to latest minor/patch + update packages/modules
if [[ -n " $CURRENT_NODE_VERSION " && " $CURRENT_NODE_VERSION " == " $NODE_VERSION " ]] ; then
msg_info "Update Node.js $NODE_VERSION "
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
ensure_apt_working || return 100
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Upgrade to the latest minor/patch release from NodeSource
$STD apt-get install -y --only-upgrade nodejs 2>/dev/null || true
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02: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 "
2026-06-21 22:24:10 +02:00
node_setup_ok_msg = "Upgrade Node.js to $NODE_VERSION "
2026-08-26 21:24:33 +02:00
remove_old_tool_version "nodejs" || {
msg_error "Aborting Node.js upgrade: another package depends on it"
return 1
}
2026-05-26 08:06:08 +02:00
else
msg_info "Setup Node.js $NODE_VERSION "
2026-06-21 22:24:10 +02:00
node_setup_ok_msg = "Setup Node.js $NODE_VERSION "
2026-03-02 08:44:59 +01:00
fi
2026-05-26 08:06:08 +02: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
2026-06-21 22:24:10 +02:00
msg_ok "Removed Debian-packaged Node.js"
2026-05-26 08:06:08 +02:00
fi
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# Remove any APT pinning (not needed)
rm -f /etc/apt/preferences.d/nodesource 2>/dev/null || true
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# Prepare repository (cleanup + validation)
prepare_repository_setup "nodesource" || {
msg_error "Failed to prepare Node.js repository"
return 250
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02: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"
return 250
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# Force APT cache refresh after repository setup
$STD apt update || {
msg_warn "apt update failed after Node.js repository setup"
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
ensure_dependencies curl ca-certificates gnupg
2026-03-18 16:45:11 +01:00
2026-05-26 08:06:08 +02:00
install_packages_with_retry "nodejs" || {
msg_error "Failed to install Node.js ${ NODE_VERSION } from NodeSource"
return 100
}
2026-03-18 16:45:11 +01:00
2026-05-26 08:06:08 +02: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"
return 127
2025-11-17 09:42:02 +01:00
fi
2026-03-18 16:45:11 +01:00
2026-05-26 08:06:08 +02: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
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# 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?"
return 127
fi
2025-05-15 15:18:41 +02:00
2026-05-26 08:06:08 +02:00
cache_installed_version "nodejs" " $NODE_VERSION "
2026-06-21 22:24:10 +02:00
msg_ok " $node_setup_ok_msg "
fi
# Node 22.22.2 bundles npm 10.9.7 which cannot self-upgrade (nodejs/node#62425)
if [[ " $NODE_VERSION " == "22" && " $( npm -v 2>/dev/null) " == "10.9.7" ]] ; then
msg_info "Updating npm (Node 22 regression fix)"
$STD npm install -g npm@10.9.8 2>/dev/null || true
if $STD npm install -g npm@latest 2>/dev/null; then
msg_ok "Updated npm ( $( npm -v) )"
else
msg_warn "npm update failed on Node 22.22.2"
fi
2025-11-05 05:19:35 -08:00
fi
2025-05-05 15:47:43 +02:00
2026-08-08 04:35:03 +02:00
if [[ " $( npm -v 2>/dev/null | cut -d. -f1) " -ge 11 ]] ; then
$STD npm config set dangerously-allow-all-scripts true --location= global 2>/dev/null || true
fi
2026-05-26 08:06:08 +02:00
# Set a safe default heap limit for Node.js builds if not explicitly provided.
# Priority:
# 1) NODE_OPTIONS (caller/user override)
# 2) NODE_MAX_OLD_SPACE_SIZE (explicit MB override)
# 3) var_ram (LXC memory setting, MB)
# 4) /proc/meminfo (runtime memory detection)
# Auto value is clamped to 1024..12288 MB.
if [[ -z " ${ NODE_OPTIONS :- } " ]] ; then
local node_heap_mb = ""
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
if [[ -n " ${ NODE_MAX_OLD_SPACE_SIZE :- } " ]] && [[ " ${ NODE_MAX_OLD_SPACE_SIZE } " = ~ ^[ 0-9] +$ ]] ; then
node_heap_mb = " ${ NODE_MAX_OLD_SPACE_SIZE } "
elif [[ -n " ${ var_ram :- } " ]] && [[ " ${ var_ram } " = ~ ^[ 0-9] +$ ]] ; then
node_heap_mb = $(( var_ram * 75 / 100 ))
else
local total_mem_kb = ""
total_mem_kb = $( awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null || echo "" )
if [[ " $total_mem_kb " = ~ ^[ 0-9] +$ ]] ; then
local total_mem_mb = $(( total_mem_kb / 1024 ))
node_heap_mb = $(( total_mem_mb * 75 / 100 ))
fi
fi
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
if [[ -z " $node_heap_mb " ]] || (( node_heap_mb < 1024)) ; then
node_heap_mb = 1024
elif (( node_heap_mb > 12288)) ; then
node_heap_mb = 12288
fi
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
export NODE_OPTIONS = "--max-old-space-size= ${ node_heap_mb } "
2025-11-05 05:19:35 -08:00
fi
2025-05-05 15:47:43 +02:00
2026-05-26 08:06:08 +02:00
# Ensure valid working directory for npm (avoids uv_cwd error)
if [[ ! -d /opt ]] ; then
mkdir -p /opt
2025-11-05 05:19:35 -08:00
fi
2026-05-26 08:06:08 +02:00
cd /opt || {
msg_error "Failed to set safe working directory before npm install"
2026-03-26 16:07:38 +01:00
return 127
2026-02-27 13:59:02 +01:00
}
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Install global Node modules
2026-06-26 21:58:08 +02:00
if [[ -n " $NODE_MODULE " ]] || (( node_major >= 25)) ; then
if (( node_major >= 25)) && [[ ", ${ NODE_MODULE } ," != *",corepack," * ]] && [[ " $NODE_MODULE " != corepack* ]] ; then
2026-06-21 22:24:10 +02:00
NODE_MODULE = " ${ NODE_MODULE :+ $NODE_MODULE , } corepack"
fi
2026-05-26 08:06:08 +02:00
IFS = ',' read -ra MODULES <<< " $NODE_MODULE "
2025-09-11 12:44:13 +02:00
2026-07-03 22:22:06 +02:00
local corepack_spec = "corepack@latest"
2026-05-26 08:06:08 +02:00
for i in " ${ !MODULES[@] } " ; do
2026-06-21 22:24:10 +02:00
if [[ " ${ MODULES [ $i ] } " == "corepack" || " ${ MODULES [ $i ] } " == corepack@* ]] ; then
wants_corepack = 1
2026-07-03 22:22:06 +02:00
[[ " ${ MODULES [ $i ] } " == corepack@* ]] && corepack_spec = " ${ MODULES [ $i ] } "
2026-06-21 22:24:10 +02:00
fi
if [[ " ${ MODULES [ $i ] } " == "pnpm" ]] ; then
2026-05-26 08:06:08 +02:00
MODULES[ $i ]= "pnpm@^10"
fi
done
2025-11-05 05:19:35 -08:00
2026-07-03 22:22:06 +02:00
# When corepack is requested, install and enable it FIRST. The corepack npm
# package owns the global yarn/pnpm/pnpx/yarnpkg bin shims, so a second
# `npm install -g yarn`/`pnpm` collides on /usr/bin/<tool> (EEXIST). With
# corepack ready we provision those package managers THROUGH corepack instead.
local corepack_ready = 0
if (( wants_corepack)) ; then
msg_info "Installing corepack"
if $STD npm install -g " $corepack_spec " 2>/dev/null ||
$STD npm install -g --force " $corepack_spec " 2>/dev/null; then
msg_ok "Installed corepack"
else
msg_warn "Failed to install corepack"
fi
if [[ " $NODE_COREPACK_ENABLE " == "1" ]] && command -v corepack >/dev/null 2>& 1; then
msg_info "Enabling corepack"
if $STD corepack enable 2>/dev/null; then
corepack_ready = 1
msg_ok "Enabled corepack"
else
msg_warn "corepack enable failed"
fi
fi
fi
2026-05-26 08:06:08 +02:00
local failed_modules = 0
for mod in " ${ MODULES [@] } " ; do
2026-07-03 22:22:06 +02:00
# corepack itself is already handled above
if [[ " $mod " == "corepack" || " $mod " == corepack@* ]] ; then
continue
fi
2026-05-26 08:06:08 +02:00
local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION
if [[ " $mod " == @*/*@* ]] ; then
# Scoped package with version, e.g. @vue/cli-service@latest
MODULE_NAME = " ${ mod %@* } "
MODULE_REQ_VERSION = " ${ mod ##*@ } "
elif [[ " $mod " == *"@" * ]] ; then
# Unscoped package with version, e.g. yarn@latest
MODULE_NAME = " ${ mod %@* } "
MODULE_REQ_VERSION = " ${ mod ##*@ } "
else
# No version specified
MODULE_NAME = " $mod "
MODULE_REQ_VERSION = "latest"
fi
2026-07-03 22:22:06 +02:00
# Provision pnpm/yarn through corepack when it is active, so we never fight
# corepack over the /usr/bin/{yarn,pnpm} shim locations it owns.
if (( corepack_ready)) && [[ " $MODULE_NAME " == "pnpm" || " $MODULE_NAME " == "yarn" ]] ; then
local corepack_pkg = " $MODULE_NAME "
[[ " $MODULE_REQ_VERSION " != "latest" ]] && corepack_pkg = " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } "
msg_info "Provisioning $MODULE_NAME via corepack"
if $STD corepack prepare " $corepack_pkg " --activate 2>/dev/null || command -v " $MODULE_NAME " >/dev/null 2>& 1; then
msg_ok "Provisioned $MODULE_NAME via corepack"
else
msg_warn "Failed to provision $MODULE_NAME via corepack"
(( failed_modules++)) || true
fi
continue
fi
# For the npm-global path, drop any corepack-provided shim first so the
# bin link can be created without an EEXIST collision.
if [[ " $MODULE_NAME " == "pnpm" || " $MODULE_NAME " == "yarn" || " $MODULE_NAME " == "yarnpkg" ]] ; then
rm -f /usr/bin/" $MODULE_NAME " /usr/local/bin/" $MODULE_NAME " 2>/dev/null || true
fi
2026-05-26 08:06:08 +02:00
# Check if the module is already installed
if $STD npm list -g --depth= 0 " $MODULE_NAME " 2>& 1 | grep -q " $MODULE_NAME @" ; then
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 '' ) "
if [[ " $MODULE_REQ_VERSION " != "latest" && " $MODULE_REQ_VERSION " != " $MODULE_INSTALLED_VERSION " ]] ; then
2026-06-21 22:24:10 +02:00
msg_info "Updating $MODULE_NAME to v $MODULE_REQ_VERSION "
2026-07-03 09:34:21 +02:00
# Retry with --force to overwrite corepack-provided shims (pnpm/yarn), which now
# ship with recent corepack and cause EEXIST on /usr/bin/<tool>
if $STD npm install -g " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } " 2>/dev/null ||
$STD npm install -g --force " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } " 2>/dev/null; then
2026-06-21 22:24:10 +02:00
msg_ok "Updated $MODULE_NAME "
else
2026-05-26 08:06:08 +02:00
msg_warn "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION "
(( failed_modules++)) || true
fi
elif [[ " $MODULE_REQ_VERSION " == "latest" ]] ; then
msg_info "Updating $MODULE_NAME to latest version"
2026-07-03 09:34:21 +02:00
if $STD npm install -g " ${ MODULE_NAME } @latest" 2>/dev/null ||
$STD npm install -g --force " ${ MODULE_NAME } @latest" 2>/dev/null; then
2026-06-21 22:24:10 +02:00
msg_ok "Updated $MODULE_NAME "
else
2026-05-26 08:06:08 +02:00
msg_warn "Failed to update $MODULE_NAME to latest version"
(( failed_modules++)) || true
fi
fi
else
msg_info "Installing $MODULE_NAME @ $MODULE_REQ_VERSION "
2026-07-03 09:34:21 +02:00
# Retry with --force to overwrite corepack-provided shims (pnpm/yarn), which now
# ship with recent corepack and cause EEXIST on /usr/bin/<tool>
if $STD npm install -g " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } " 2>/dev/null ||
$STD npm install -g --force " ${ MODULE_NAME } @ ${ MODULE_REQ_VERSION } " 2>/dev/null; then
2026-06-21 22:24:10 +02:00
msg_ok "Installed $MODULE_NAME "
else
2026-05-26 08:06:08 +02:00
msg_warn "Failed to install $MODULE_NAME @ $MODULE_REQ_VERSION "
(( failed_modules++)) || true
fi
fi
2025-09-11 12:44:13 +02:00
done
2026-06-26 21:58:08 +02:00
if (( failed_modules > 0)) ; then
2026-06-21 22:24:10 +02:00
msg_warn " $failed_modules Node.js module(s) failed: $NODE_MODULE "
fi
fi
2026-07-03 22:22:06 +02:00
# pnpm v10+ blocks dependency build scripts by default. Do NOT force
# `dangerouslyAllowAllBuilds` globally: pnpm implements that flag as an empty
# `neverBuiltDependencies`, which then clashes with any project that ships its
# own `onlyBuiltDependencies` (every create-t3-app based app, e.g. Split Pro)
# and aborts with ERR_PNPM_CONFIG_CONFLICT_BUILT_DEPENDENCIES. Instead relax the
# strict check so an unapproved build is a warning, not a fatal error; scripts
# that truly need every build script executed enable that themselves.
2026-07-02 09:07:04 +02:00
if command -v pnpm >/dev/null 2>& 1; then
2026-07-03 22:22:06 +02:00
pnpm config set --global strictDepBuilds false >/dev/null 2>& 1 || true
2025-05-05 15:47:43 +02:00
fi
2025-04-29 16:20:23 +02:00
}
2025-05-15 15:18:41 +02:00
2025-11-10 18:51:02 +01:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# Installs PHP with selected modules and configures Apache/FPM support.
2025-11-10 18:51:02 +01:00
#
# Description:
2026-05-26 08:06:08 +02:00
# - Adds Sury PHP repo if needed
# - Installs default and user-defined modules
# - Patches php.ini for CLI, Apache, and FPM as needed
# - Handles built-in modules gracefully (e.g., opcache in PHP 8.5+)
# - Skips unavailable packages without failing
2025-11-10 18:51:02 +01:00
#
# Variables:
2026-05-26 08:06:08 +02:00
# 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)
2025-11-10 18:51:02 +01:00
#
2026-05-26 08:06:08 +02: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-11-10 18:51:02 +01:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_php() {
local PHP_VERSION = " ${ PHP_VERSION :- 8 .4 } "
local PHP_MODULE = " ${ PHP_MODULE :- } "
local PHP_APACHE = " ${ PHP_APACHE :- NO } "
local PHP_FPM = " ${ PHP_FPM :- NO } "
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02:00
# Parse version for compatibility checks
local PHP_MAJOR = " ${ PHP_VERSION %%.* } "
local PHP_MINOR = " ${ PHP_VERSION #*. } "
PHP_MINOR = " ${ PHP_MINOR %%.* } "
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02:00
# 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"
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02:00
# 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"
2026-03-18 16:45:11 +01:00
fi
2026-05-26 08:06:08 +02:00
# Base modules - essential for most PHP applications
# Note: 'common' provides many built-in extensions
local BASE_MODULES = "cli,common,bcmath,curl,dom,gd,gmp,intl,mbstring,readline,xml,zip"
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02: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"
2026-03-18 16:45:11 +01:00
fi
2026-05-26 08:06:08 +02:00
# 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"
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02:00
local COMBINED_MODULES = " ${ BASE_MODULES } , ${ EXTENDED_MODULES } "
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +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 } "
# Merge with user-defined modules
if [[ -n " $PHP_MODULE " ]] ; then
COMBINED_MODULES = " ${ COMBINED_MODULES } , ${ PHP_MODULE } "
2025-11-10 18:51:02 +01:00
fi
2026-05-26 08:06:08 +02: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
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02:00
# Skip if it's a known built-in module
if echo ", $BUILTIN_MODULES , $BUILTIN_85 ," | grep -qi ", $mod ," ; then
continue
fi
2025-11-10 18:51:02 +01:00
2026-05-26 08:06:08 +02:00
# Add to filtered list
if [[ -z " $FILTERED_MODULES " ]] ; then
FILTERED_MODULES = " $mod "
else
FILTERED_MODULES = " ${ FILTERED_MODULES } , $mod "
fi
done
2025-05-15 15:18:41 +02:00
2026-05-26 08:06:08 +02:00
# Deduplicate
COMBINED_MODULES = $( echo " $FILTERED_MODULES " | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -)
2025-05-15 15:18:41 +02:00
2026-05-26 08:06:08 +02:00
# Get current PHP-CLI version
local CURRENT_PHP = ""
CURRENT_PHP = $( is_tool_installed "php" 2>/dev/null) || true
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Remove conflicting PHP version before pinning
if [[ -n " $CURRENT_PHP " && " $CURRENT_PHP " != " $PHP_VERSION " ]] ; then
msg_info "Removing conflicting PHP ${ CURRENT_PHP } (need ${ PHP_VERSION } )"
stop_all_services "php.*-fpm"
$STD apt purge -y "php*" 2>/dev/null || true
$STD apt autoremove -y 2>/dev/null || true
2025-11-05 05:19:35 -08:00
fi
2026-05-26 08:06:08 +02:00
# NOW create pinning for the desired version
mkdir -p /etc/apt/preferences.d
cat <<EOF >/etc/apt/preferences.d/php-pin
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
# Setup repository
prepare_repository_setup "php" "deb.sury.org-php" || {
msg_error "Failed to prepare PHP repository"
return 100
}
# Use different repository based on OS
if [[ " $DISTRO_ID " == "ubuntu" ]] ; then
# Ubuntu: Use ondrej/php PPA
msg_info "Adding ondrej/php PPA for Ubuntu"
$STD apt install -y software-properties-common || {
msg_error "Failed to install software-properties-common"
return 100
}
# 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
2025-11-05 05:19:35 -08:00
else
2026-05-26 08:06:08 +02:00
# 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"
return 100
}
2025-11-05 05:19:35 -08:00
fi
2026-03-26 16:07:38 +01:00
ensure_apt_working || return 100
2026-05-26 08:06:08 +02:00
$STD apt update || {
msg_warn "apt update failed after PHP repository setup"
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# Get available PHP version from repository
local AVAILABLE_PHP_VERSION = ""
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-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
if [[ -z " $AVAILABLE_PHP_VERSION " ]] ; then
msg_error "PHP ${ PHP_VERSION } not found in configured repositories"
return 100
fi
# Build module list - verify each package exists before adding
local MODULE_LIST = "php ${ PHP_VERSION } "
local SKIPPED_MODULES = ""
IFS = ',' read -ra MODULES <<< " $COMBINED_MODULES "
for mod in " ${ MODULES [@] } " ; do
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 "
2025-10-22 06:25:26 -07:00
else
2026-05-26 08:06:08 +02:00
# Package doesn't exist - could be built-in or renamed
if [[ -z " $SKIPPED_MODULES " ]] ; then
SKIPPED_MODULES = " $mod "
else
SKIPPED_MODULES = " ${ SKIPPED_MODULES } , $mod "
2025-10-22 06:25:26 -07:00
fi
fi
done
2026-05-26 08:06:08 +02:00
# Log skipped modules (informational, not an error)
if [[ -n " $SKIPPED_MODULES " ]] ; then
msg_info "Skipping unavailable/built-in modules: $SKIPPED_MODULES "
2025-05-15 15:18:41 +02:00
fi
2026-05-26 08:06:08 +02:00
if [[ " $PHP_FPM " == "YES" ]] ; then
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"
2026-03-02 08:44:59 +01:00
fi
2026-05-26 08:06:08 +02: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
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# install apache2 with PHP support if requested
if [[ " $PHP_APACHE " == "YES" ]] ; then
if ! dpkg -l 2>/dev/null | grep -q "libapache2-mod-php ${ PHP_VERSION } " ; then
msg_info "Installing Apache with PHP ${ PHP_VERSION } module"
install_packages_with_retry "apache2" || {
msg_error "Failed to install Apache"
return 100
}
install_packages_with_retry "libapache2-mod-php ${ PHP_VERSION } " || {
msg_warn "Failed to install libapache2-mod-php ${ PHP_VERSION } , continuing without Apache module"
}
2025-11-05 05:19:35 -08:00
fi
2025-05-15 15:18:41 +02:00
fi
2026-05-26 08:06:08 +02:00
# Install PHP packages (pinning via preferences.d ensures correct version)
msg_info "Installing PHP ${ PHP_VERSION } packages"
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02: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"
# Install main package first (critical)
if ! $STD apt install -y "php ${ PHP_VERSION } " 2>/dev/null; then
msg_error "Failed to install php ${ PHP_VERSION } "
return 100
2026-03-02 08:44:59 +01:00
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Try to install Apache module individually if requested
if [[ " $PHP_APACHE " == "YES" ]] ; then
$STD apt install -y "libapache2-mod-php ${ PHP_VERSION } " 2>/dev/null || {
msg_warn "Could not install libapache2-mod-php ${ PHP_VERSION } "
}
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02:00
# Try to install each package individually
for pkg in $MODULE_LIST ; do
[[ " $pkg " == "php ${ PHP_VERSION } " ]] && continue # Already installed
$STD apt install -y " $pkg " 2>/dev/null || {
msg_warn "Could not install $pkg - continuing without it"
}
done
2025-05-15 15:18:41 +02:00
fi
2026-05-26 08:06:08 +02:00
cache_installed_version "php" " $PHP_VERSION "
2025-05-15 15:18:41 +02:00
2026-05-26 08:06:08 +02:00
# Patch all relevant php.ini files
local PHP_INI_PATHS =( "/etc/php/ ${ PHP_VERSION } /cli/php.ini" )
[[ " $PHP_FPM " == "YES" ]] && PHP_INI_PATHS +=( "/etc/php/ ${ PHP_VERSION } /fpm/php.ini" )
[[ " $PHP_APACHE " == "YES" ]] && PHP_INI_PATHS +=( "/etc/php/ ${ PHP_VERSION } /apache2/php.ini" )
for ini in " ${ PHP_INI_PATHS [@] } " ; do
if [[ -f " $ini " ]] ; then
$STD sed -i "s|^memory_limit = .*|memory_limit = ${ PHP_MEMORY_LIMIT } |" " $ini "
$STD sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${ PHP_UPLOAD_MAX_FILESIZE } |" " $ini "
$STD sed -i "s|^post_max_size = .*|post_max_size = ${ PHP_POST_MAX_SIZE } |" " $ini "
$STD sed -i "s|^max_execution_time = .*|max_execution_time = ${ PHP_MAX_EXECUTION_TIME } |" " $ini "
fi
done
2025-05-15 15:18:41 +02:00
2026-05-26 08:06:08 +02:00
# Patch Apache configuration if needed
if [[ " $PHP_APACHE " == "YES" ]] ; then
for mod in $( ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//' ) ; do
if [[ " $mod " != "php ${ PHP_VERSION } " ]] ; then
$STD a2dismod " $mod " || true
fi
done
$STD a2enmod mpm_prefork
$STD a2enmod "php ${ PHP_VERSION } "
safe_service_restart apache2 || true
2025-05-15 15:18:41 +02:00
fi
2026-05-26 08:06:08 +02:00
# Enable and restart PHP-FPM if requested
if [[ " $PHP_FPM " == "YES" ]] ; then
if systemctl list-unit-files | grep -q "php ${ PHP_VERSION } -fpm.service" ; then
$STD systemctl enable php${ PHP_VERSION } -fpm
safe_service_restart php${ PHP_VERSION } -fpm
fi
fi
2025-11-05 05:19:35 -08:00
2026-05-26 08:06:08 +02: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"
return 127
fi
2025-05-15 15:18:41 +02:00
2026-05-26 08:06:08 +02:00
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
return 127
2025-05-15 15:18:41 +02:00
fi
2026-05-26 08:06:08 +02:00
cache_installed_version "php" " $INSTALLED_VERSION "
msg_ok "Setup PHP ${ INSTALLED_VERSION } "
2025-05-18 09:27:55 +02:00
}
2025-05-27 14:29:50 +02:00
2026-07-28 19:47:13 +02:00
# ------------------------------------------------------------------------------
# Resolves the path of the active PHP-FPM unix socket.
#
# Description:
# - Prefers the socket of ${PHP_VERSION} when that variable is set, which is
# the deterministic case directly after setup_php ran
# - Falls back to the highest version found below /run/php
# - Webserver-agnostic: works for nginx (fastcgi_pass) and Caddy (php_fastcgi)
#
# Notes:
# - Prints the path to stdout, diagnostics go to stderr, so the result can be
# captured via command substitution
# - Deliberately avoids `head -n1`: it closes the pipe early and with
# `set -o pipefail` (see catch_errors) SIGPIPE would abort the install
# - php-fpm has to be running, the socket only exists after the service start
#
# Usage:
# PHP_SOCK=$(get_php_fpm_socket)
# echo "fastcgi_pass unix:${PHP_SOCK};"
# ------------------------------------------------------------------------------
get_php_fpm_socket() {
local sock
if [[ -n " ${ PHP_VERSION :- } " && -S "/run/php/php ${ PHP_VERSION } -fpm.sock" ]] ; then
echo "/run/php/php ${ PHP_VERSION } -fpm.sock"
return 0
fi
sock = $( find /run/php -maxdepth 1 -name "php*-fpm.sock" -type s 2>/dev/null | sort -V | tail -1)
if [[ -z " $sock " ]] ; then
msg_error "No active PHP-FPM socket found under /run/php"
return 1
fi
echo " $sock "
}
# ------------------------------------------------------------------------------
# Enables an nginx site, validates the configuration and reloads the service.
#
# Description:
# - Removes the packaged default vhost (idempotent, unlike `unlink`)
# - Links the site from sites-available into sites-enabled
# - Runs `nginx -t` and aborts before a broken config reaches a running service
# - Enables the unit so nginx survives a reboot, then restarts it via
# safe_service_restart so a failed start is actually reported
#
# Variables:
# $1 - site name, has to exist as /etc/nginx/sites-available/<name>
#
# Usage:
# cat <<EOF >/etc/nginx/sites-available/myapp
# ...
# EOF
# nginx_enable_site myapp
# ------------------------------------------------------------------------------
nginx_enable_site() {
local site = " ${ 1 :- } "
if [[ -z " $site " ]] ; then
msg_error "nginx_enable_site: no site name given"
return 1
fi
if [[ ! -f "/etc/nginx/sites-available/ ${ site } " ]] ; then
msg_error "nginx site config not found: /etc/nginx/sites-available/ ${ site } "
return 1
fi
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default
ln -sf "/etc/nginx/sites-available/ ${ site } " "/etc/nginx/sites-enabled/ ${ site } "
if ! $STD nginx -t; then
msg_error "nginx configuration test failed for site ' ${ site } '"
return 1
fi
$STD systemctl enable -q nginx
safe_service_restart nginx
}
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# Installs or upgrades PostgreSQL and optional extensions/modules.
2026-01-28 11:08:52 +01:00
#
# Description:
2026-05-26 08:06:08 +02:00
# - By default uses distro repository (Debian/Ubuntu apt) for stability
# - Optionally uses official PGDG repository for specific versions
# - Detects existing PostgreSQL version
# - Dumps all databases before upgrade
# - Installs optional PG_MODULES (e.g. postgis, contrib, cron)
# - Restores dumped data post-upgrade
2026-01-28 11:08:52 +01:00
#
# Variables:
2026-05-26 08:06:08 +02:00
# USE_PGDG_REPO - Use official PGDG repository (default: true)
# Set to "false" to use distro packages instead
# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16)
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib,cron")
2026-01-28 11:08:52 +01:00
#
2026-05-26 08:06:08 +02:00
# Examples:
# 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-01-28 11:08:52 +01:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02: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
}
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
setup_postgresql() {
local PG_VERSION = " ${ PG_VERSION :- 16 } "
local PG_MODULES = " ${ PG_MODULES :- } "
local USE_PGDG_REPO = " ${ USE_PGDG_REPO :- true } "
local DISTRO_ID DISTRO_CODENAME
DISTRO_ID = $( get_os_info id)
DISTRO_CODENAME = $( get_os_info codename)
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
# Ensure non-interactive mode for all apt operations
export DEBIAN_FRONTEND = noninteractive
export NEEDRESTART_MODE = a
export NEEDRESTART_SUSPEND = 1
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
# Get currently installed version
local CURRENT_PG_VERSION = ""
if command -v psql >/dev/null; then
CURRENT_PG_VERSION = " $( psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1) "
fi
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02: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)"
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
# If already installed, just update
if [[ -n " $CURRENT_PG_VERSION " ]] ; then
msg_info "Update PostgreSQL $CURRENT_PG_VERSION "
ensure_apt_working || return 100
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 ||
msg_warn "Optional PostgreSQL module ' ${ module } ' not available for PG ${ CURRENT_PG_VERSION } on $( get_os_info codename) — skipping"
done
2026-01-30 10:20:57 +01:00
fi
2026-05-26 08:06:08 +02:00
_configure_pg_cron_preload " $PG_MODULES "
return 0
fi
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
# Fresh install from distro repo
ensure_apt_working || return 100
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
export DEBIAN_FRONTEND = noninteractive
install_packages_with_retry "postgresql" "postgresql-client" || {
msg_error "Failed to install PostgreSQL from distro repository"
return 100
}
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02: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
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
$STD systemctl enable --now postgresql 2>/dev/null || true
2026-01-30 10:29:08 +01:00
2026-05-26 08:06:08 +02:00
# 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
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02:00
cache_installed_version "postgresql" " ${ INSTALLED_VERSION :- distro } "
msg_ok "Setup PostgreSQL ${ INSTALLED_VERSION :- from distro } "
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02:00
# 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 ||
msg_warn "Optional PostgreSQL module ' ${ module } ' not available for PG ${ INSTALLED_VERSION } on $( get_os_info codename) — skipping"
done
fi
_configure_pg_cron_preload " $PG_MODULES "
return 0
fi
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02:00
# Scenario 2: Use official PGDG repository (USE_PGDG_REPO=true)
# Scenario 2a: Already at correct version
if [[ " $CURRENT_PG_VERSION " == " $PG_VERSION " ]] ; then
msg_info "Update PostgreSQL $PG_VERSION "
ensure_apt_working || return 100
# 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 "
# 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 ||
msg_warn "Optional PostgreSQL module ' ${ module } ' not available for PG ${ PG_VERSION } on $( get_os_info codename) — skipping"
done
fi
_configure_pg_cron_preload " $PG_MODULES "
return 0
fi
# Scenario 2: Different version - backup, remove old, install new
if [[ -n " $CURRENT_PG_VERSION " ]] ; then
msg_info "Upgrade PostgreSQL from $CURRENT_PG_VERSION to $PG_VERSION "
msg_info "Creating backup of PostgreSQL $CURRENT_PG_VERSION databases..."
local PG_BACKUP_FILE = "/var/lib/postgresql/backup_ $( date +%F) _v ${ CURRENT_PG_VERSION } .sql"
$STD runuser -u postgres -- pg_dumpall >" $PG_BACKUP_FILE " || {
msg_error "Failed to backup PostgreSQL databases"
return 150
}
$STD systemctl stop postgresql || true
$STD apt purge -y "postgresql- ${ CURRENT_PG_VERSION } " "postgresql-client- ${ CURRENT_PG_VERSION } " 2>/dev/null || true
else
msg_info "Setup PostgreSQL $PG_VERSION "
fi
# Scenario 3: Fresh install or after removal - setup repo and install
prepare_repository_setup "pgdg" "postgresql" || {
msg_error "Failed to prepare PostgreSQL repository"
return 100
}
2026-01-30 10:29:08 +01:00
2026-05-26 08:06:08 +02:00
local SUITE
case " $DISTRO_CODENAME " in
trixie | forky | sid)
2026-01-30 10:29:08 +01:00
2026-05-26 08:06:08 +02:00
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg" ; then
SUITE = "trixie-pgdg"
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02:00
else
local _distro_pg_ver
_distro_pg_ver = $( apt-cache show postgresql 2>/dev/null | awk '/^Version:/{print $2; exit}' | grep -oE '^[0-9]+' || true )
msg_warn "PGDG repository not available for ${ DISTRO_CODENAME } — falling back to distro-provided PostgreSQL packages"
if [[ -n " $_distro_pg_ver " ]] ; then
msg_warn "Distro will install PostgreSQL ${ _distro_pg_ver } (not the requested ${ PG_VERSION } )."
msg_warn "Any PostgreSQL extension packages (e.g. vchord, pgvector) must be built for PostgreSQL ${ _distro_pg_ver } on ${ DISTRO_CODENAME } ."
2026-01-30 10:20:57 +01:00
fi
2026-05-26 08:06:08 +02:00
USE_PGDG_REPO = false setup_postgresql
return $?
fi
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
;;
*)
SUITE = $( get_fallback_suite " $DISTRO_ID " " $DISTRO_CODENAME " "https://apt.postgresql.org/pub/repos/apt" )
SUITE = " ${ SUITE } -pgdg"
;;
esac
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
setup_deb822_repo \
"pgdg" \
"https://www.postgresql.org/media/keys/ACCC4CF8.asc" \
"https://apt.postgresql.org/pub/repos/apt" \
" $SUITE " \
"main"
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
if ! $STD apt update; then
msg_error "APT update failed for PostgreSQL repository"
return 100
fi
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02: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 ||
msg_warn "Optional ssl-cert package could not be installed — continuing without it"
fi
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02:00
# Try multiple PostgreSQL package patterns with retry logic
local pg_install_success = false
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02: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
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02: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
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02: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
2026-02-22 11:54:21 +01:00
2026-05-26 08:06:08 +02:00
if [[ " $pg_install_success " == false ]] ; then
msg_error "PostgreSQL package not available for suite ${ SUITE } "
return 100
fi
2026-01-30 10:20:57 +01:00
2026-05-26 08:06:08 +02:00
if ! command -v psql >/dev/null 2>& 1; then
msg_error "PostgreSQL installed but psql command not found"
return 127
2026-01-28 11:08:52 +01:00
fi
2026-05-26 08:06:08 +02:00
# Restore database backup if we upgraded from previous version
if [[ -n " $CURRENT_PG_VERSION " && -n " ${ PG_BACKUP_FILE :- } " && -f " ${ PG_BACKUP_FILE } " ]] ; then
msg_info "Restoring PostgreSQL databases from backup..."
$STD runuser -u postgres -- psql <" $PG_BACKUP_FILE " 2>/dev/null || {
msg_warn "Failed to restore database backup - this may be expected for major version upgrades"
}
fi
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
$STD systemctl enable --now postgresql 2>/dev/null || {
msg_warn "Failed to enable/start PostgreSQL service"
2026-01-28 11:08:52 +01:00
}
2026-05-26 08:06:08 +02: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
fi
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
cache_installed_version "postgresql" " $PG_VERSION "
msg_ok "Setup PostgreSQL $PG_VERSION "
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
# Install optional modules
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 || {
msg_warn "Failed to install PostgreSQL module: ${ module } "
}
done
fi
_configure_pg_cron_preload " $PG_MODULES "
}
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02: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
# PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
#
# 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
# ------------------------------------------------------------------------------
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
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"
return 65
fi
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
# 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
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
msg_info "Setting up PostgreSQL Database"
2026-07-13 11:37:39 +02:00
# Escape single quotes in identifiers to prevent SQL injection
local _pg_user_escaped _pg_pass_escaped _pg_db_escaped
_pg_user_escaped = " ${ PG_DB_USER // \' / \'\' } "
_pg_pass_escaped = " ${ PG_DB_PASS // \' / \'\' } "
_pg_db_escaped = " ${ PG_DB_NAME // \' / \'\' } "
$STD sudo -u postgres psql -c "CREATE ROLE $_pg_user_escaped WITH LOGIN PASSWORD ' $_pg_pass_escaped ';"
$STD sudo -u postgres psql -c "CREATE DATABASE $_pg_db_escaped WITH OWNER $_pg_user_escaped ENCODING 'UTF8' TEMPLATE template0;"
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02: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
# 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
2026-06-13 18:17:21 +02:00
$STD sudo -u postgres psql -d " $PG_DB_NAME " -c "CREATE EXTENSION IF NOT EXISTS \" $ext \";"
2026-05-26 08:06:08 +02:00
done
fi
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02: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
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02: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';"
2026-01-28 11:08:52 +01:00
fi
2026-05-26 08:06:08 +02:00
# 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
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
# 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
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
# Save credentials
local app_name = " ${ APPLICATION ,, } "
local CREDS_FILE = " ${ PG_DB_CREDS_FILE :- ${ HOME } / ${ app_name } .creds } "
{
echo "PostgreSQL Credentials"
echo "Database: $PG_DB_NAME "
echo "User: $PG_DB_USER "
echo "Password: $PG_DB_PASS "
} >>" $CREDS_FILE "
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
msg_ok "Set up PostgreSQL Database"
2026-01-28 11:08:52 +01:00
2026-05-26 08:06:08 +02:00
# Export for use in calling script
export PG_DB_NAME
export PG_DB_USER
export PG_DB_PASS
2026-01-28 11:08:52 +01:00
}
2026-07-17 12:53:13 +02:00
2026-01-28 11:08:52 +01:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
2025-10-22 06:25:26 -07:00
#
# Description:
2026-05-26 08:06:08 +02:00
# - Downloads rbenv and ruby-build from GitHub
# - Compiles and installs target Ruby version
# - Optionally installs Rails via gem
2025-10-22 06:25:26 -07:00
#
# Variables:
2026-05-26 08:06:08 +02:00
# RUBY_VERSION - Ruby version to install (default: 3.4.4)
# RUBY_INSTALL_RAILS - true/false to install Rails (default: true)
2025-10-22 06:25:26 -07:00
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_ruby() {
local RUBY_VERSION = " ${ RUBY_VERSION :- 3 .4.4 } "
local RUBY_INSTALL_RAILS = " ${ RUBY_INSTALL_RAILS :- true } "
local RBENV_DIR = " $HOME /.rbenv"
local RBENV_BIN = " $RBENV_DIR /bin/rbenv"
local PROFILE_FILE = " $HOME /.profile"
2026-07-17 12:53:13 +02:00
local BASH_PROFILE_FILE = " $HOME /.bash_profile"
local BASHRC_FILE = " $HOME /.bashrc"
2026-05-26 08:06:08 +02:00
local TMP_DIR = $( mktemp -d)
2025-10-22 06:25:26 -07:00
2026-07-19 14:53:46 +02:00
# Ensure HOME exists: callers may pass a not-yet-created home (e.g. a service
# user that is created later in the install), so the profile writes below do
# not fail on a missing directory.
mkdir -p " $HOME "
2026-07-17 12:53:13 +02:00
if ! grep -q 'rbenv init' " $PROFILE_FILE " 2>/dev/null; then
cat <<'EOF' >>"$PROFILE_FILE"
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
EOF
fi
if ! grep -q '.rbenv/shims' " $PROFILE_FILE " 2>/dev/null; then
echo 'export PATH="$HOME/.rbenv/shims:$HOME/.rbenv/bin:$PATH"' >>" $PROFILE_FILE "
fi
if [[ -f " $BASH_PROFILE_FILE " ]] && ! grep -q '.rbenv/shims' " $BASH_PROFILE_FILE " ; then
echo 'export PATH="$HOME/.rbenv/shims:$HOME/.rbenv/bin:$PATH"' >>" $BASH_PROFILE_FILE "
fi
if [[ -f " $BASHRC_FILE " ]] && ! grep -q '.rbenv/shims' " $BASHRC_FILE " ; then
echo 'export PATH="$HOME/.rbenv/shims:$HOME/.rbenv/bin:$PATH"' >>" $BASHRC_FILE "
fi
2026-05-26 08:06:08 +02: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
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# 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
2025-10-22 06:25:26 -07:00
fi
2026-05-26 08:06:08 +02:00
# 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 "
2025-10-22 06:25:26 -07:00
fi
2026-05-26 08:06:08 +02:00
ensure_apt_working || return 100
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
# Install build dependencies with fallbacks
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"
)
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
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 ||
msg_warn "Some Ruby build dependencies could not be installed — compilation may fail"
2025-11-05 05:19:35 -08:00
else
2026-05-26 08:06:08 +02:00
msg_error "No Ruby build dependencies available"
rm -rf " $TMP_DIR "
return 100
2025-10-22 06:25:26 -07:00
fi
2026-05-26 08:06:08 +02:00
# Download and build rbenv if needed
if [[ ! -x " $RBENV_BIN " ]] ; then
local RBENV_RELEASE
RBENV_RELEASE = $( get_latest_github_release "rbenv/rbenv" ) || {
msg_error "Failed to fetch latest rbenv version from GitHub"
rm -rf " $TMP_DIR "
return 7
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
if ! curl_with_retry "https://github.com/rbenv/rbenv/archive/refs/tags/v ${ RBENV_RELEASE } .tar.gz" " $TMP_DIR /rbenv.tar.gz" ; then
msg_error "Failed to download rbenv"
msg_error "Hint: Check connectivity to github.com/rbenv/rbenv"
rm -rf " $TMP_DIR "
return 7
fi
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
tar -xzf " $TMP_DIR /rbenv.tar.gz" -C " $TMP_DIR " || {
msg_error "Failed to extract rbenv"
rm -rf " $TMP_DIR "
return 251
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02: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 "
return 150
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02: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
fi
# Install ruby-build plugin
2026-06-22 22:01:40 +02:00
_install_ruby_build_plugin() {
2026-05-26 08:06:08 +02:00
local RUBY_BUILD_RELEASE
RUBY_BUILD_RELEASE = $( get_latest_github_release "rbenv/ruby-build" ) || {
msg_error "Failed to fetch latest ruby-build version from GitHub"
return 7
}
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
msg_error "Failed to download ruby-build"
msg_error "Hint: Check connectivity to github.com/rbenv/ruby-build"
return 7
fi
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
tar -xzf " $TMP_DIR /ruby-build.tar.gz" -C " $TMP_DIR " || {
msg_error "Failed to extract ruby-build"
return 251
}
mkdir -p " $RBENV_DIR /plugins/ruby-build"
cp -r " $TMP_DIR /ruby-build- ${ RUBY_BUILD_RELEASE } /." " $RBENV_DIR /plugins/ruby-build/"
2026-06-22 22:01:40 +02:00
return 0
}
if [[ ! -d " $RBENV_DIR /plugins/ruby-build" ]] ; then
_install_ruby_build_plugin || {
rm -rf " $TMP_DIR "
return 7
}
2025-10-22 06:25:26 -07:00
fi
2026-05-26 08:06:08 +02:00
# Setup PATH and install Ruby version
export PATH = " $RBENV_DIR /bin: $PATH "
eval " $( " $RBENV_BIN " init - bash) " 2>/dev/null || true
if ! " $RBENV_BIN " versions --bare 2>/dev/null | grep -qx " $RUBY_VERSION " ; then
2026-06-22 22:01:40 +02:00
if [[ ! -f " $RBENV_DIR /plugins/ruby-build/share/ruby-build/ $RUBY_VERSION " ]] ; then
msg_info "Updating ruby-build definitions"
_install_ruby_build_plugin || {
rm -rf " $TMP_DIR "
return 7
}
msg_ok "Updated ruby-build definitions"
fi
2026-05-26 08:06:08 +02:00
$STD " $RBENV_BIN " install " $RUBY_VERSION " || {
msg_error "Failed to install Ruby $RUBY_VERSION "
rm -rf " $TMP_DIR "
return 150
}
2025-10-22 06:25:26 -07:00
fi
2026-05-26 08:06:08 +02:00
" $RBENV_BIN " global " $RUBY_VERSION " || {
msg_error "Failed to set Ruby $RUBY_VERSION as global version"
rm -rf " $TMP_DIR "
return 150
2025-11-05 05:19:35 -08:00
}
2025-10-22 06:25:26 -07:00
2026-05-26 08:06:08 +02:00
hash -r
# Install Rails if requested
if [[ " $RUBY_INSTALL_RAILS " == "true" ]] ; then
$STD gem install rails || {
msg_warn "Failed to install Rails - Ruby installation successful"
}
fi
rm -rf " $TMP_DIR "
cache_installed_version "ruby" " $RUBY_VERSION "
msg_ok "Setup Ruby $RUBY_VERSION "
2025-10-22 06:25:26 -07:00
}
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)
2026-06-02 09:03:53 +02:00
# RUST_PROFILE - Rust installation profile (default: default, e.g. minimal)
2025-05-27 14:29:50 +02:00
# RUST_CRATES - Comma-separated list of crates (e.g. "cargo-edit,wasm-pack@0.12.1")
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_rust() {
2025-05-27 14:29:50 +02:00
local RUST_TOOLCHAIN = " ${ RUST_TOOLCHAIN :- stable } "
2026-06-02 09:03:53 +02:00
local RUST_PROFILE = " ${ RUST_PROFILE :- default } "
2025-05-27 14:29:50 +02:00
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
2026-06-02 09:03:53 +02:00
msg_info "Setup Rust ( $RUST_TOOLCHAIN , profile: $RUST_PROFILE )"
curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --profile " $RUST_PROFILE " --default-toolchain " $RUST_TOOLCHAIN " || {
2025-10-22 06:25:26 -07:00
msg_error "Failed to install Rust"
2026-05-26 08:06:08 +02:00
msg_error "Hint: Check connectivity to sh.rustup.rs and static.rust-lang.org"
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
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_uv() {
2025-09-11 12:44:13 +02:00
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)
2025-11-05 05:19:35 -08:00
# trap for TMP Cleanup
trap "rm -rf ' $TMP_DIR '" EXIT
# 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
2026-06-22 21:46:52 +02:00
# Fetch target version (pinned via UV_VERSION or latest release)
local TARGET_VERSION = " ${ UV_VERSION :- } "
if [[ -z " $TARGET_VERSION " ]] ; then
TARGET_VERSION = $( get_latest_github_release "astral-sh/uv" ) || {
msg_error "Could not fetch latest uv version from GitHub API"
return 7
}
fi
2025-07-08 17:19:51 +02:00
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
2026-06-22 21:46:52 +02:00
# Scenario 1: Already at target version
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " == " $TARGET_VERSION " ]] ; then
cache_installed_version "uv" " $TARGET_VERSION "
2025-11-05 05:19:35 -08:00
# 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
2026-07-19 00:10:06 +02:00
# Install specific Python version if requested (even when uv is already up to date)
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 "
return 150
}
msg_ok "Python $PYTHON_VERSION installed"
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
2026-06-22 21:46:52 +02:00
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " != " $TARGET_VERSION " ]] ; then
msg_info "Upgrade uv from $INSTALLED_VERSION to $TARGET_VERSION "
2025-11-05 05:19:35 -08:00
else
2026-06-22 21:46:52 +02:00
msg_info "Setup uv $TARGET_VERSION "
2025-11-05 05:19:35 -08:00
fi
2025-10-22 06:25:26 -07:00
2026-06-22 21:46:52 +02:00
local UV_URL = "https://github.com/astral-sh/uv/releases/download/ ${ TARGET_VERSION } / ${ UV_TAR } "
2025-11-05 05:19:35 -08:00
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-05-26 08:06:08 +02:00
msg_error "Hint: GitHub Releases — check connectivity or set GITHUB_TOKEN to avoid rate-limiting"
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-05-26 08:06:08 +02:00
return 252
}
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
ensure_usr_local_bin_persist
export PATH = "/usr/local/bin: $PATH "
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02: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"
return 252
}
msg_ok "uvx wrapper installed"
fi
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Optional: Generate shell completions
$STD uv generate-shell-completion bash >/etc/bash_completion.d/uv 2>/dev/null || true
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-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02: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 "
return 150
}
msg_ok "Python $PYTHON_VERSION installed"
fi
2026-03-02 08:44:59 +01:00
2026-06-22 21:46:52 +02:00
cache_installed_version "uv" " $TARGET_VERSION "
msg_ok "Setup uv $TARGET_VERSION "
2026-05-26 08:06:08 +02:00
}
2026-03-02 08:44:59 +01:00
2026-05-26 08:06:08 +02:00
# Helper function to install uvx wrapper
_install_uvx_wrapper() {
local UVX_BIN = "/usr/local/bin/uvx"
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02: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-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
chmod +x " $UVX_BIN "
return 0
2025-12-04 07:52:18 +01:00
}
2026-05-26 08:06:08 +02:00
# ------------------------------------------------------------------------------
# Installs or updates yq (mikefarah/yq - Go version).
#
# Description:
# - Checks if yq is installed and from correct source
# - Compares with latest release on GitHub
# - Updates if outdated or wrong implementation
# ------------------------------------------------------------------------------
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
setup_yq() {
local TMP_DIR = $( mktemp -d)
local BINARY_PATH = "/usr/local/bin/yq"
local GITHUB_REPO = "mikefarah/yq"
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
ensure_dependencies jq
ensure_usr_local_bin_persist
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
# Remove non-mikefarah implementations
if command -v yq & >/dev/null; then
if ! yq --version 2>& 1 | grep -q 'mikefarah' ; then
rm -f " $( command -v yq) "
2025-12-04 07:52:18 +01:00
fi
fi
2026-05-26 08:06:08 +02:00
local LATEST_VERSION
LATEST_VERSION = $( get_latest_github_release " ${ GITHUB_REPO } " ) || {
msg_error "Could not fetch latest yq version from GitHub API"
rm -rf " $TMP_DIR "
return 250
}
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02: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
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
# Scenario 1: Already at latest version
if [[ -n " $INSTALLED_VERSION " && " $INSTALLED_VERSION " == " $LATEST_VERSION " ]] ; then
cache_installed_version "yq" " $LATEST_VERSION "
rm -rf " $TMP_DIR "
return 0
fi
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02: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
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
local yq_arch detected_arch
if command -v dpkg & >/dev/null; then
detected_arch = " $( dpkg --print-architecture 2>/dev/null) "
else
detected_arch = " $( uname -m 2>/dev/null) "
fi
case " $detected_arch " in
arm64 | aarch64) yq_arch = "arm64" ;;
amd64 | x86_64) yq_arch = "amd64" ;;
*) yq_arch = "amd64" ;;
esac
if ! curl_with_retry "https://github.com/ ${ GITHUB_REPO } /releases/download/v ${ LATEST_VERSION } /yq_linux_ ${ yq_arch } " " $TMP_DIR /yq" ; then
2026-05-26 08:06:08 +02:00
msg_error "Failed to download yq"
msg_error "Hint: Check connectivity to github.com/ ${ GITHUB_REPO } — set GITHUB_TOKEN to avoid rate-limiting"
rm -rf " $TMP_DIR "
return 250
fi
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
chmod +x " $TMP_DIR /yq"
mv " $TMP_DIR /yq" " $BINARY_PATH " || {
msg_error "Failed to install yq"
rm -rf " $TMP_DIR "
return 252
}
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
rm -rf " $TMP_DIR "
hash -r
2025-12-04 07:52:18 +01:00
2026-05-26 08:06:08 +02:00
local FINAL_VERSION
FINAL_VERSION = $( " $BINARY_PATH " --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//' )
cache_installed_version "yq" " $FINAL_VERSION "
msg_ok "Setup yq $FINAL_VERSION "
2025-12-04 07:52:18 +01:00
}
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" ""
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
fetch_and_deploy_from_url() {
2026-01-30 22:25:55 +01:00
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
}
2026-07-21 22:49:58 +02:00
trap 'rm -rf "$tmpdir" "${unpack_tmp:-}"; trap - RETURN' RETURN
2026-01-30 22:25:55 +01:00
curl -fsSL -o " $tmpdir / $filename " " $url " || {
msg_error "Download failed: $url "
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"
2026-08-26 21:25:51 +02:00
elif [[ " $file_desc " = ~ XZ\ compressed\ data ]] ; then
archive_type = "tar"
2026-02-01 13:19:11 +01:00
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 "
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 " || {
2026-05-26 08:06:08 +02:00
_diagnose_deb_failure " $tmpdir / $filename "
2026-03-26 16:07:38 +01:00
return 100
2026-01-30 22:25:55 +01:00
}
}
msg_ok "Successfully installed .deb package"
return 0
fi
if [[ -z " $directory " ]] ; then
msg_error "Directory parameter is required for archive extraction"
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
2026-08-16 22:51:27 +02:00
find " ${ directory :? } " -mindepth 1 \( -type d -exec mountpoint -q {} \; -prune \) -o -print0 | xargs -0 rm -rf --
2026-01-30 22:25:55 +01:00
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"
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"
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 "
2026-07-19 14:53:46 +02:00
shopt -u dotglob nullglob
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 "
2026-07-19 14:53:46 +02:00
shopt -u dotglob nullglob
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 "
2026-07-19 14:53:46 +02:00
shopt -u dotglob nullglob
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"
2026-07-19 14:53:46 +02:00
shopt -u dotglob nullglob
2026-03-26 16:07:38 +01:00
return 252
2026-01-30 22:25:55 +01:00
fi
shopt -u dotglob nullglob
fi
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
}
2026-05-04 20:32:15 +02:00
# ------------------------------------------------------------------------------
# Get latest GitLab release version.
# Usage: get_latest_gitlab_release "owner/repo" [strip_v]
# ------------------------------------------------------------------------------
get_latest_gitlab_release() {
local repo = " $1 "
local strip_v = " ${ 2 :- true } "
local repo_encoded
repo_encoded = $( printf '%s' " $repo " | sed 's|/|%2F|g' )
local header =()
[[ -n " ${ GITLAB_TOKEN :- } " ]] && header =( -H "PRIVATE-TOKEN: $GITLAB_TOKEN " )
local temp_file
temp_file = $( mktemp)
2026-07-30 14:40:59 +02:00
local gitlab_url = " ${ GITLAB_URL :- https ://gitlab.com } "
2026-05-04 20:32:15 +02:00
local http_code
http_code = $( curl --connect-timeout 10 --max-time 30 -sSL \
-w "%{http_code}" -o " $temp_file " \
" ${ header [@] } " \
2026-07-30 14:40:59 +02:00
" ${ gitlab_url } /api/v4/projects/ $repo_encoded /releases?per_page=1&order_by=released_at&sort=desc" 2>/dev/null) || true
2026-05-04 20:32:15 +02:00
if [[ " $http_code " != "200" ]] ; then
rm -f " $temp_file "
msg_warn "GitLab API call failed for ${ repo } (HTTP ${ http_code } )"
return 22
fi
local version
version = $( jq -r '.[0].tag_name // empty' " $temp_file " )
rm -f " $temp_file "
if [[ -z " $version " ]] ; then
msg_error "Could not determine latest version for ${ repo } "
return 250
fi
if [[ " $strip_v " == "true" ]] ; then
[[ " $version " = ~ ^v[ 0-9] ]] && version = " ${ version : 1 } "
fi
echo " $version "
}
# ------------------------------------------------------------------------------
# Checks for new GitLab release (latest tag).
#
# Description:
# - Queries the GitLab 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_gl_release "myapp" "owner/repo" [optional] "v1.2.3"; then
# # trigger update...
# fi
# exit 0
# } (end of update_script not from the function)
#
# Notes:
# - Requires `jq` (auto-installed if missing)
# - Supports GITLAB_TOKEN env var for private/rate-limited repos
2026-07-30 14:40:59 +02:00
# - Supports GITLAB_URL for self-hosted GitLab (default: https://gitlab.com)
2026-05-04 20:32:15 +02:00
# - Does not modify anything, only checks version state
# ------------------------------------------------------------------------------
check_for_gl_release() {
local app = " $1 "
local source = " $2 "
local pinned_version_in = " ${ 3 :- } " # optional
local pin_reason = " ${ 4 :- } " # optional reason shown to user
2026-07-13 11:56:12 +02:00
local tag_prefix = " ${ 5 :- } " # optional tag prefix filter (e.g. web-v2)
2026-05-04 20:32:15 +02:00
local app_lc = " ${ app ,, } "
local current_file = " $HOME /. ${ app_lc } "
2026-07-30 14:40:59 +02:00
local gitlab_url = " ${ GITLAB_URL :- https ://gitlab.com } "
local gitlab_hostname = " ${ gitlab_url #*:// } "
gitlab_hostname = " ${ gitlab_hostname %%/* } "
2026-05-04 20:32:15 +02:00
msg_info "Checking for update: ${ app } "
# DNS check
2026-07-30 14:40:59 +02:00
if ! getent hosts " $gitlab_hostname " >/dev/null 2>& 1; then
msg_error "Network error: cannot resolve $gitlab_hostname "
2026-05-04 20:32:15 +02:00
return 6
fi
ensure_dependencies jq
2026-06-21 23:09:51 +02:00
local gl_check_json
2026-06-26 15:47:48 +10:00
gl_check_json = $( mktemp /tmp/tools-gl-check-XXXXXX) || return 73
2026-07-21 22:49:58 +02:00
trap 'rm -f "$gl_check_json"; trap - RETURN' RETURN
2026-06-21 23:09:51 +02:00
2026-05-04 20:32:15 +02:00
local repo_encoded
repo_encoded = $( printf '%s' " $source " | sed 's|/|%2F|g' )
local header =()
[[ -n " ${ GITLAB_TOKEN :- } " ]] && header =( -H "PRIVATE-TOKEN: $GITLAB_TOKEN " )
local releases_json = "" http_code = ""
# For pinned versions, try to fetch the specific release tag first
if [[ -n " $pinned_version_in " ]] ; then
local pinned_encoded = " ${ pinned_version_in // \/ /%2F } "
2026-06-21 23:09:51 +02:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o " $gl_check_json " \
2026-05-04 20:32:15 +02:00
" ${ header [@] } " \
2026-07-30 14:40:59 +02:00
" ${ gitlab_url } /api/v4/projects/ $repo_encoded /releases/ $pinned_encoded " 2>/dev/null) || true
2026-06-21 23:09:51 +02:00
if [[ " $http_code " == "200" ]] && [[ -s " $gl_check_json " ]] ; then
releases_json = "[ $( <" $gl_check_json " ) ]"
2026-05-04 20:32:15 +02:00
fi
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
fi
# Fetch full releases list if needed
if [[ -z " $releases_json " ]] ; then
2026-06-21 23:09:51 +02:00
http_code = $( curl -sSL --max-time 20 -w "%{http_code}" -o " $gl_check_json " \
2026-05-04 20:32:15 +02:00
" ${ header [@] } " \
2026-07-30 14:40:59 +02:00
" ${ gitlab_url } /api/v4/projects/ $repo_encoded /releases?per_page=100&order_by=released_at&sort=desc" 2>/dev/null) || true
2026-05-04 20:32:15 +02:00
2026-06-21 23:09:51 +02:00
if [[ " $http_code " == "200" ]] && [[ -s " $gl_check_json " ]] ; then
releases_json = $( <" $gl_check_json " )
2026-05-04 20:32:15 +02:00
elif [[ " $http_code " == "401" ]] ; then
msg_error "GitLab API authentication failed (HTTP 401)."
if [[ -n " ${ GITLAB_TOKEN :- } " ]] ; then
msg_error "Your GITLAB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITLAB_TOKEN=\"glpat-your_token\""
fi
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
return 22
elif [[ " $http_code " == "404" ]] ; then
msg_error "GitLab project not found (HTTP 404). Ensure ' ${ source } ' is correct and publicly accessible."
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
return 22
elif [[ " $http_code " == "429" ]] ; then
msg_error "GitLab API rate limit exceeded (HTTP 429)."
msg_error "To increase the limit, export a GitLab token: export GITLAB_TOKEN=\"glpat-your_token_here\""
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
return 22
elif [[ " $http_code " == "000" || -z " $http_code " ]] ; then
msg_error "GitLab API connection failed (no response)."
2026-07-30 14:40:59 +02:00
msg_error "Check your network/DNS: curl -sSL ${ gitlab_url } /api/v4/version"
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
return 7
else
msg_error "Unable to fetch releases for ${ app } (HTTP ${ http_code } )"
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
return 22
fi
2026-06-21 23:09:51 +02:00
rm -f " $gl_check_json "
2026-05-04 20:32:15 +02:00
fi
2026-07-13 11:56:12 +02:00
if [[ -n " $tag_prefix " ]] ; then
mapfile -t raw_tags < <( jq -r --arg p " $tag_prefix " \
'.[] | select(.tag_name | startswith($p)) | .tag_name' <<< " $releases_json " )
else
mapfile -t raw_tags < <( jq -r '.[] | .tag_name' <<< " $releases_json " )
fi
2026-05-04 20:32:15 +02:00
if (( ${# raw_tags [@] } == 0)) ; then
2026-07-13 11:56:12 +02:00
if [[ -n " $tag_prefix " ]] ; then
msg_error "No releases matching prefix ' ${ tag_prefix } ' found for ${ app } on GitLab"
else
msg_error "No releases found for ${ app } on GitLab"
fi
2026-05-04 20:32:15 +02:00
return 250
fi
local clean_tags =()
for t in " ${ raw_tags [@] } " ; do
# 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
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
if [[ " $current " = ~ ^v[ 0-9] ]] ; then
current = " ${ current : 1 } "
fi
# Pinned version handling
if [[ -n " $pinned_version_in " ]] ; then
local pin_clean
if [[ " $pinned_version_in " = ~ ^v[ 0-9] ]] ; then
pin_clean = " ${ pinned_version_in : 1 } "
else
pin_clean = " $pinned_version_in "
fi
local match_raw = ""
for i in " ${ !clean_tags[@] } " ; do
if [[ " ${ clean_tags [ $i ] } " == " $pin_clean " ]] ; then
match_raw = " ${ raw_tags [ $i ] } "
break
fi
done
if [[ -z " $match_raw " ]] ; then
msg_error "Pinned version ${ pinned_version_in } not found upstream"
return 250
fi
if [[ " $current " != " $pin_clean " ]] ; then
CHECK_UPDATE_RELEASE = " $match_raw "
msg_ok "Update available: ${ app } ${ current :- not installed } → ${ pin_clean } "
return 0
fi
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
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
}
# ------------------------------------------------------------------------------
# Scan older GitLab releases for a matching asset (fallback helper).
#
# Description:
# When the latest release does not contain the expected asset
# (e.g. .deb for the current arch, or a custom pattern), walks back
# through up to 15 recent releases and returns the first release JSON
# that has a matching asset. Used internally by fetch_and_deploy_gl_release.
#
# Usage (internal):
# _gl_scan_older_releases "owner/repo" "owner%2Frepo" "https://gitlab.com" \
# "binary|prebuild|singlefile" "$asset_pattern" "$skip_tag"
#
# Returns:
# - stdout: JSON of the matching release (single object) on success
# - 0 on success, 22 on API error, 250 if no match found
# ------------------------------------------------------------------------------
_gl_scan_older_releases() {
local repo = " $1 "
local repo_encoded = " $2 "
local base_url = " ${ 3 :- https ://gitlab.com } "
local mode = " $4 "
local asset_pattern = " $5 "
local skip_tag = " $6 "
local header =()
[[ -n " ${ GITLAB_TOKEN :- } " ]] && header =( -H "PRIVATE-TOKEN: $GITLAB_TOKEN " )
local releases_list
releases_list = $( curl --connect-timeout 10 --max-time 30 -fsSL \
" ${ header [@] } " \
" ${ base_url } /api/v4/projects/ ${ repo_encoded } /releases?per_page=15&order_by=released_at&sort=desc" 2>/dev/null) || {
msg_warn "Failed to fetch older releases for ${ repo } "
return 22
}
local count
count = $( echo " $releases_list " | jq 'length' 2>/dev/null || echo 0)
[[ " $count " -eq 0 ]] && return 250
for (( i = 0; i < count; i++)) ; do
local rel_tag
rel_tag = $( echo " $releases_list " | jq -r ".[ $i ].tag_name" )
# Skip the tag we already checked
[[ " $rel_tag " == " $skip_tag " ]] && continue
# Asset URLs for this release (direct_asset_url preferred, fallback to url)
local asset_urls
asset_urls = $( echo " $releases_list " | jq -r ".[ $i ].assets.links // [] | .[] | .direct_asset_url // .url" )
[[ -z " $asset_urls " ]] && 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
while read -r u; do
case " ${ u ##*/ } " in $asset_pattern )
has_match = true
break
;;
esac
done <<< " $asset_urls "
fi
if [[ " $has_match " != "true" ]] ; then
echo " $asset_urls " | grep -qE "( $arch |amd64|x86_64|aarch64|arm64).*\.deb $" && has_match = true
fi
if [[ " $has_match " != "true" ]] ; then
echo " $asset_urls " | grep -qE '\.deb$' && has_match = true
fi
elif [[ " $mode " == "prebuild" || " $mode " == "singlefile" ]] ; then
while read -r u; do
case " ${ u ##*/ } " in $asset_pattern )
has_match = true
break
;;
esac
done <<< " $asset_urls "
fi
if [[ " $has_match " == "true" ]] ; then
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."
2026-08-03 16:57:55 +02:00
read -rp "Use version ${ rel_tag } instead? [Y/n] (auto-yes in 60s): " -t 60 use_fallback </dev/tty || use_fallback = "y"
2026-05-04 20:32:15 +02:00
use_fallback = " ${ use_fallback :- y } "
fi
if [[ " ${ use_fallback ,, } " == "y" || " ${ use_fallback ,, } " == "yes" ]] ; then
echo " $releases_list " | jq ".[ $i ]"
return 0
else
return 250
fi
fi
done
return 250
}
2026-05-26 08:06:08 +02:00
fetch_and_deploy_gl_release() {
2026-05-04 20:32:15 +02:00
local app = " $1 "
local repo = " $2 "
local mode = " ${ 3 :- tarball } "
local version = " ${ var_appversion :- ${ 4 :- latest }} "
local target = " ${ 5 :- /opt/ $app } "
local asset_pattern = " ${ 6 :- } "
2026-07-13 11:56:12 +02:00
local tag_prefix = " ${ 7 :- } "
2026-05-04 20:32:15 +02:00
if [[ -z " $app " ]] ; then
app = " ${ repo ##*/ } "
if [[ -z " $app " ]] ; then
msg_error "fetch_and_deploy_gl_release requires app name or valid repo"
return 1
fi
fi
local app_lc = $( echo " ${ app ,, } " | tr -d ' ' )
local version_file = " $HOME /. ${ app_lc } "
local api_timeout = "--connect-timeout 10 --max-time 60"
local download_timeout = "--connect-timeout 15 --max-time 900"
local current_version = ""
[[ -f " $version_file " ]] && current_version = $( <" $version_file " )
ensure_dependencies jq
2026-06-21 23:09:51 +02:00
local gl_rel_json
2026-06-26 15:47:48 +10:00
gl_rel_json = $( mktemp /tmp/tools-gl-rel-XXXXXX) || return 73
2026-07-21 22:49:58 +02:00
trap 'rm -f "$gl_rel_json"; rm -rf "${tmpdir:-}" "${unpack_tmp:-}"; trap - RETURN' RETURN
2026-06-21 23:09:51 +02:00
2026-05-04 20:32:15 +02:00
local repo_encoded
repo_encoded = $( printf '%s' " $repo " | sed 's|/|%2F|g' )
2026-07-30 14:40:59 +02:00
local gitlab_url = " ${ GITLAB_URL :- https ://gitlab.com } "
local api_base = " ${ gitlab_url } /api/v4/projects/ $repo_encoded /releases"
2026-05-04 20:32:15 +02:00
local api_url
if [[ " $version " != "latest" ]] ; then
api_url = " $api_base / $version "
2026-07-13 11:56:12 +02:00
elif [[ -n " $tag_prefix " ]] ; then
api_url = " $api_base ?per_page=100&order_by=released_at&sort=desc"
2026-05-04 20:32:15 +02:00
else
api_url = " $api_base ?per_page=1&order_by=released_at&sort=desc"
fi
local header =()
[[ -n " ${ GITLAB_TOKEN :- } " ]] && header =( -H "PRIVATE-TOKEN: $GITLAB_TOKEN " )
local max_retries = 3 retry_delay = 2 attempt = 1 success = false http_code
while (( attempt <= max_retries)) ; do
2026-06-21 23:09:51 +02:00
http_code = $( curl $api_timeout -sSL -w "%{http_code}" -o " $gl_rel_json " " ${ header [@] } " " $api_url " 2>/dev/null) || true
2026-05-04 20:32:15 +02:00
if [[ " $http_code " == "200" ]] ; then
success = true
break
elif [[ " $http_code " == "429" ]] ; then
if (( attempt < max_retries)) ; then
msg_warn "GitLab API rate limit hit, retrying in ${ retry_delay } s... (attempt $attempt / $max_retries )"
sleep " $retry_delay "
retry_delay = $(( retry_delay * 2 ))
fi
else
sleep " $retry_delay "
fi
(( attempt++))
done
if ! $success ; then
if [[ " $http_code " == "401" ]] ; then
msg_error "GitLab API authentication failed (HTTP 401)."
if [[ -n " ${ GITLAB_TOKEN :- } " ]] ; then
msg_error "Your GITLAB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITLAB_TOKEN=\"glpat-your_token\""
fi
elif [[ " $http_code " == "404" ]] ; then
msg_error "GitLab project or release not found (HTTP 404)."
msg_error "Ensure ' $repo ' is correct and the project is accessible."
elif [[ " $http_code " == "429" ]] ; then
msg_error "GitLab API rate limit exceeded (HTTP 429)."
msg_error "To increase the limit, export a GitLab token before running the script:"
msg_error " export GITLAB_TOKEN=\"glpat-your_token_here\""
elif [[ " $http_code " == "000" || -z " $http_code " ]] ; then
msg_error "GitLab API connection failed (no response)."
2026-07-30 14:40:59 +02:00
msg_error "Check your network/DNS: curl -sSL ${ gitlab_url } /api/v4/version"
2026-05-04 20:32:15 +02:00
else
msg_error "Failed to fetch release metadata (HTTP $http_code )"
fi
return 1
fi
local json tag_name
2026-06-21 23:09:51 +02:00
json = $( <" $gl_rel_json " )
2026-05-04 20:32:15 +02:00
if [[ " $version " == "latest" ]] ; then
2026-07-13 11:56:12 +02:00
if [[ -n " $tag_prefix " ]] ; then
json = $( echo " $json " | jq --arg p " $tag_prefix " \
'[.[] | select(.tag_name | startswith($p))][0] // empty' )
else
json = $( echo " $json " | jq '.[0] // empty' )
fi
2026-05-04 20:32:15 +02:00
if [[ -z " $json " || " $json " == "null" ]] ; then
2026-07-13 11:56:12 +02:00
if [[ -n " $tag_prefix " ]] ; then
msg_error "No release matching prefix ' ${ tag_prefix } ' found for $repo on GitLab"
else
msg_error "No releases found for $repo on GitLab"
fi
2026-05-04 20:32:15 +02:00
return 1
fi
fi
tag_name = $( echo " $json " | jq -r '.tag_name // empty' )
if [[ -z " $tag_name " ]] ; then
msg_error "Could not determine tag name from release metadata"
return 1
fi
[[ " $tag_name " = ~ ^v[ 0-9] ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
local version_safe = " ${ version // \/ /- } "
if [[ " $current_version " == " $version " ]] ; then
$STD msg_ok " $app is already up-to-date (v $version )"
return 0
fi
local tmpdir
tmpdir = $( mktemp -d) || return 1
local filename = ""
msg_info "Fetching GitLab release: $app ( $version )"
_gl_asset_urls() {
local release_json = " $1 "
echo " $release_json " | jq -r '
(.assets.links // [])[] | .direct_asset_url // .url
'
}
### Tarball Mode ###
if [[ " $mode " == "tarball" || " $mode " == "source" ]] ; then
2026-07-30 14:40:59 +02:00
local direct_tarball_url = " ${ gitlab_url } / $repo /-/archive/ $tag_name / ${ app_lc } - ${ version_safe } .tar.gz"
2026-05-04 20:32:15 +02:00
filename = " ${ app_lc } - ${ version_safe } .tar.gz"
2026-07-19 14:53:46 +02:00
_download_source_tarball " $direct_tarball_url " " $tmpdir / $filename " " ${ header [@] } " || {
2026-05-04 20:32:15 +02:00
msg_error "Download failed: $direct_tarball_url "
return 1
}
2026-07-19 14:53:46 +02:00
_deploy_source_tarball " $tmpdir / $filename " " $target " " $tmpdir " || return 1
2026-05-04 20:32:15 +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"
local assets url_match = ""
assets = $( _gl_asset_urls " $json " )
if [[ -n " $asset_pattern " ]] ; then
for u in $assets ; do
case " ${ u ##*/ } " in
$asset_pattern )
url_match = " $u "
break
;;
esac
done
fi
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
if [[ -z " $url_match " ]] ; then
for u in $assets ; do
[[ " $u " = ~ \. deb$ ]] && url_match = " $u " && break
done
fi
if [[ -z " $url_match " ]] ; then
local fallback_json
2026-07-30 14:40:59 +02:00
if fallback_json = $( _gl_scan_older_releases " $repo " " $repo_encoded " " $gitlab_url " "binary" " $asset_pattern " " $tag_name " ) ; then
2026-05-04 20:32:15 +02:00
json = " $fallback_json "
tag_name = $( echo " $json " | jq -r '.tag_name // empty' )
[[ " $tag_name " = ~ ^v[ 0-9] ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
msg_info "Fetching GitLab release: $app ( $version )"
assets = $( _gl_asset_urls " $json " )
if [[ -n " $asset_pattern " ]] ; then
for u in $assets ; do
case " ${ u ##*/ } " in $asset_pattern )
url_match = " $u "
break
;;
esac
done
fi
if [[ -z " $url_match " ]] ; then
for u in $assets ; do
[[ " $u " = ~ ( $arch | amd64| x86_64| aarch64| arm64) .*\. deb$ ]] && url_match = " $u " && break
done
fi
if [[ -z " $url_match " ]] ; then
for u in $assets ; do
[[ " $u " = ~ \. deb$ ]] && url_match = " $u " && break
done
fi
fi
fi
if [[ -z " $url_match " ]] ; then
msg_error "No suitable .deb asset found for $app "
return 1
fi
filename = " ${ url_match ##*/ } "
curl $download_timeout -fsSL " ${ header [@] } " -o " $tmpdir / $filename " " $url_match " || {
msg_error "Download failed: $url_match "
return 1
}
chmod 644 " $tmpdir / $filename "
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 " || {
SYSTEMD_OFFLINE = 1 $STD dpkg -i " $tmpdir / $filename " || {
2026-05-26 08:06:08 +02:00
_diagnose_deb_failure " $tmpdir / $filename "
2026-05-04 20:32:15 +02:00
return 1
}
}
### Prebuild Mode ###
elif [[ " $mode " == "prebuild" ]] ; then
local pattern = " ${ 6 % \" } "
pattern = " ${ pattern # \" } "
[[ -z " $pattern " ]] && {
msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)"
return 1
}
local asset_url = ""
for u in $( _gl_asset_urls " $json " ) ; do
filename_candidate = " ${ u ##*/ } "
case " $filename_candidate " in
$pattern )
asset_url = " $u "
break
;;
esac
done
if [[ -z " $asset_url " ]] ; then
local fallback_json
2026-07-30 14:40:59 +02:00
if fallback_json = $( _gl_scan_older_releases " $repo " " $repo_encoded " " $gitlab_url " "prebuild" " $pattern " " $tag_name " ) ; then
2026-05-04 20:32:15 +02:00
json = " $fallback_json "
tag_name = $( echo " $json " | jq -r '.tag_name // empty' )
[[ " $tag_name " = ~ ^v[ 0-9] ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
msg_info "Fetching GitLab release: $app ( $version )"
for u in $( _gl_asset_urls " $json " ) ; do
filename_candidate = " ${ u ##*/ } "
case " $filename_candidate " in $pattern )
asset_url = " $u "
break
;;
esac
done
fi
fi
[[ -z " $asset_url " ]] && {
msg_error "No asset matching ' $pattern ' found"
return 1
}
filename = " ${ asset_url ##*/ } "
curl $download_timeout -fsSL " ${ header [@] } " -o " $tmpdir / $filename " " $asset_url " || {
msg_error "Download failed: $asset_url "
return 1
}
local unpack_tmp
unpack_tmp = $( mktemp -d)
2026-07-19 14:53:46 +02:00
_deploy_unpacked_archive " $tmpdir / $filename " " $target " " $unpack_tmp " || return
2026-05-04 20:32:15 +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)"
return 1
}
local asset_url = ""
for u in $( _gl_asset_urls " $json " ) ; do
filename_candidate = " ${ u ##*/ } "
case " $filename_candidate " in
$pattern )
asset_url = " $u "
break
;;
esac
done
if [[ -z " $asset_url " ]] ; then
local fallback_json
2026-07-30 14:40:59 +02:00
if fallback_json = $( _gl_scan_older_releases " $repo " " $repo_encoded " " $gitlab_url " "singlefile" " $pattern " " $tag_name " ) ; then
2026-05-04 20:32:15 +02:00
json = " $fallback_json "
tag_name = $( echo " $json " | jq -r '.tag_name // empty' )
[[ " $tag_name " = ~ ^v[ 0-9] ]] && version = " ${ tag_name : 1 } " || version = " $tag_name "
msg_info "Fetching GitLab release: $app ( $version )"
for u in $( _gl_asset_urls " $json " ) ; do
filename_candidate = " ${ u ##*/ } "
case " $filename_candidate " in $pattern )
asset_url = " $u "
break
;;
esac
done
fi
fi
[[ -z " $asset_url " ]] && {
msg_error "No asset matching ' $pattern ' found"
return 1
}
filename = " ${ asset_url ##*/ } "
mkdir -p " $target "
local use_filename = " ${ USE_ORIGINAL_FILENAME :- false } "
local target_file = " $app "
[[ " $use_filename " == "true" ]] && target_file = " $filename "
curl $download_timeout -fsSL " ${ header [@] } " -o " $target / $target_file " " $asset_url " || {
msg_error "Download failed: $asset_url "
return 1
}
if [[ " $target_file " != *.jar && -f " $target / $target_file " ]] ; then
chmod +x " $target / $target_file "
fi
else
msg_error "Unknown mode: $mode "
return 1
fi
echo " $version " >" $version_file "
msg_ok "Deployed: $app ( $version )"
}
2026-05-08 15:39:20 +02:00
# ------------------------------------------------------------------------------
# Download NLTK data packages directly from GitHub, bypassing Python.
# Avoids CPU-instruction failures (SIGILL) on older hardware lacking AVX.
#
# Usage:
# setup_nltk "averaged_perceptron_tagger_eng" "/nltk_data"
# setup_nltk "snowball_data stopwords punkt_tab" "/usr/share/nltk_data"
#
# Parameters:
# $1 - Space-separated list of NLTK package IDs
# $2 - Target directory (default: /usr/share/nltk_data)
#
# Returns: 0 on success, non-zero if any package failed
# ------------------------------------------------------------------------------
2026-05-26 08:06:08 +02:00
setup_nltk() {
2026-05-08 15:39:20 +02:00
local packages = " ${ 1 :?setup_nltk requires at least one package name } "
local target_dir = " ${ 2 :- /usr/share/nltk_data } "
local NLTK_INDEX_URL = "https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml"
local index_xml rc = 0
ensure_dependencies unzip
index_xml = $( curl_with_retry " $NLTK_INDEX_URL " "-" ) || {
msg_error "Failed to fetch NLTK package index"
return 1
}
local pkg
for pkg in $packages ; do
msg_info "Downloading NLTK: $pkg "
local pkg_line subdir pkg_url do_unzip tmp_zip
pkg_line = $( echo " $index_xml " | grep "id=\" ${ pkg } \"" | head -1)
if [[ -z " $pkg_line " ]] ; then
msg_error "NLTK package not found in index: $pkg "
rc = 1
continue
fi
subdir = $( echo " $pkg_line " | grep -oP 'subdir="\K[^"]+' )
pkg_url = $( echo " $pkg_line " | grep -oP 'url="\K[^"]+' )
do_unzip = $( echo " $pkg_line " | grep -oP 'unzip="\K[^"]+' )
if [[ -z " $subdir " || -z " $pkg_url " ]] ; then
msg_error "Could not parse NLTK index entry for: $pkg "
rc = 1
continue
fi
mkdir -p " ${ target_dir } / ${ subdir } "
2026-06-26 15:47:48 +10:00
tmp_zip = $( mktemp)
2026-05-08 15:39:20 +02:00
if CURL_TIMEOUT = 120 curl_with_retry " $pkg_url " " $tmp_zip " ; then
if [[ " $do_unzip " == "1" ]] ; then
$STD unzip -q -o " $tmp_zip " -d " ${ target_dir } / ${ subdir } /"
rm -f " $tmp_zip "
else
mv " $tmp_zip " " ${ target_dir } / ${ subdir } / ${ pkg } .zip"
fi
msg_ok "Downloaded NLTK: $pkg "
else
msg_error "Failed to download NLTK package: $pkg "
rm -f " $tmp_zip "
rc = 1
fi
done
return $rc
}