219 lines
7.6 KiB
Bash
Executable file
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
|
|
}
|