#!/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 }