suse-professional-package-i.../lib/ce-common.sh

219 lines
7.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# Created by John A. Hoeven with the ethical assistance of Claude AI
# ---------------------------------------------------------------------------
# ce-common.sh
# /home/john/projects/suse-professional-package-installer/lib/ce-common.sh
# Version: v0.1.0 | Status: DEVELOPMENT
# ---------------------------------------------------------------------------
# Purpose: Shared logging, privilege-command detection, and zypper wrapper
# functions for this installer's subscripts.
# Target: SUSE family (zypper). Not a multi-distro abstraction — that's a
# deliberate scope choice, see spec/main_spec.md.
# Entry: source ce-common.sh — not executed directly.
# Depends: N/A
# ---------------------------------------------------------------------------
# Guard against double-sourcing (main.sh sources this directly, then each
# subscript it invokes also sources it for standalone-run capability).
[[ -n "${_CE_COMMON_SOURCED:-}" ]] && return 0
readonly _CE_COMMON_SOURCED=1
# ── Logging ──────────────────────────────────────────────────────────────
readonly LOG_DIR="${HOME}/.local/logs/ceos_installer"
mkdir -p "${LOG_DIR}"
_ce_log_date="$(date '+%Y-%m-%d')"
readonly LOG_FILE="${LOG_DIR}/${CE_INSTALLER_NAME:-suse-pkg-install}-${_ce_log_date}.log"
_log() {
local level="${1}"
local msg="${2}"
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
printf '[%s] [%s] %s\n' "${ts}" "${level}" "${msg}" | tee -a "${LOG_FILE}"
}
log_info() { _log "INFO " "${1}"; }
log_warn() { _log "WARN " "${1}" >&2; }
log_error() { _log "ERROR" "${1}" >&2; }
log_debug() { [[ "${CE_DEBUG:-0}" == "1" ]] && _log "DEBUG" "${1}"; }
# ── Test helper ──────────────────────────────────────────────────────────
# shellcheck disable=SC2016
# ^ run_test()'s cmd args are single-quoted at call sites throughout this
# project — deferred-eval expressions passed to `eval`, not meant to
# expand at the call site.
run_test() {
local desc="${1}"
local cmd="${2}"
local mode="${3:-fail}" # fail | warn
if eval "${cmd}" >/dev/null 2>&1; then
log_info " PASS: ${desc}"
return 0
fi
if [[ "${mode}" == "fail" ]]; then
log_error " FAIL: ${desc}"
exit 1
fi
log_warn " WARN: ${desc}"
return 1
}
# ── Privilege command detection ─────────────────────────────────────────
# Never assume doas or sudo — detect per source-discipline doc.
detect_priv_cmd() {
if command -v doas >/dev/null 2>&1; then
PRIV_CMD="doas"
elif command -v sudo >/dev/null 2>&1; then
PRIV_CMD="sudo"
else
log_error "Neither doas nor sudo found — cannot perform privileged operations"
exit 1
fi
log_info "Privilege command: ${PRIV_CMD}"
}
# ── zypper wrapper ───────────────────────────────────────────────────────
detect_package_manager() {
if ! command -v zypper >/dev/null 2>&1; then
log_error "zypper not found — this installer targets the SUSE family only"
exit 1
fi
log_info "Package manager: zypper"
}
test_package_available() {
local pkg="${1:?test_package_available: pkg required}"
rpm -q "${pkg}" >/dev/null 2>&1 && return 0
zypper --non-interactive search -x "${pkg}" 2>/dev/null | grep -q " ${pkg} " && return 0
log_warn "Package '${pkg}' not available via zypper"
return 1
}
package_installed() {
local pkg="${1:?package_installed: pkg required}"
rpm -q "${pkg}" >/dev/null 2>&1
}
install_packages() {
local -n pkg_list="${1:?install_packages: pkg_list required}"
local failed=()
local unavailable=()
local skipped=()
for pkg in "${pkg_list[@]}"; do
if package_installed "${pkg}"; then
log_info "Already installed, skipping: ${pkg}"
skipped+=( "${pkg}" )
continue
fi
if ! test_package_available "${pkg}"; then
unavailable+=( "${pkg}" )
continue
fi
if ! ${PRIV_CMD} zypper --non-interactive install "${pkg}"; then
log_error "Failed to install: ${pkg}"
failed+=( "${pkg}" )
else
log_info "Installed: ${pkg}"
CE_PKG_MODIFIED=1
run_test "${pkg} present after install" \
"package_installed '${pkg}'" \
warn
fi
done
if [[ ${#unavailable[@]} -gt 0 ]]; then
log_warn "Unavailable: ${unavailable[*]}"
fi
if [[ ${#failed[@]} -gt 0 ]]; then
log_error "Installation failures: ${failed[*]}"
return 1
fi
return 0
}
# ── Package-list file parsing ────────────────────────────────────────────
# One package name per line; '#'-prefixed comments and blank lines ignored.
read_package_list() {
local list_file="${1:?read_package_list: list_file required}"
local -n out_array="${2:?read_package_list: out_array required}"
if [[ ! -f "${list_file}" ]]; then
log_error "Package list not found: ${list_file}"
exit 1
fi
out_array=()
while IFS= read -r line; do
line="${line%%#*}"
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "${line}" ]] && continue
out_array+=( "${line}" )
done < "${list_file}"
if [[ ${#out_array[@]} -eq 0 ]]; then
log_error "Package list is empty: ${list_file}"
exit 1
fi
}
# ── Cleanup trap (shared) ────────────────────────────────────────────────
# Registered unconditionally, before any work begins (style guide §2.9).
# Temp-dir removal always runs (harmless no-op if CE_TEMP_DIRS is empty).
# The privileged cache-clean step only runs if CE_PKG_MODIFIED was
# actually set — i.e. this run really touched zypper — not inferred from
# an unrelated side effect.
CE_TEMP_DIRS=()
CE_PKG_MODIFIED=0
_CE_CLEANUP_TRAP_SET=0
register_temp_dir() {
CE_TEMP_DIRS+=( "${1:?register_temp_dir: path required}" )
}
_ce_cleanup() {
local exit_code="${?}"
for d in "${CE_TEMP_DIRS[@]}"; do
if [[ -n "${d}" && -d "${d}" ]]; then
rm -rf "${d}"
log_info "Removed temp dir: ${d}"
fi
done
if [[ "${CE_PKG_MODIFIED}" -eq 1 && -n "${PRIV_CMD:-}" ]]; then
if ${PRIV_CMD} zypper clean --all 2>/dev/null; then
log_info "Package cache cleared"
else
log_warn "Package cache clear skipped or failed (non-fatal)"
fi
fi
log_info "Cleanup complete (exit code: ${exit_code})"
}
ensure_cleanup_trap() {
[[ "${_CE_CLEANUP_TRAP_SET}" -eq 1 ]] && return 0
trap _ce_cleanup EXIT INT TERM HUP
_CE_CLEANUP_TRAP_SET=1
}
ensure_cleanup_trap
# ── User confirmation ────────────────────────────────────────────────────
confirm_proceed() {
local prompt="${1:-Proceed with installation?}"
printf '\n%s [y/N] ' "${prompt}"
read -r response
case "${response}" in
[yY]|[yY][eE][sS]) return 0 ;;
*) log_info "Aborted by Organic Humanoid I/O Device unit ID ${USER}"; exit 0 ;;
esac
}