# shellcheck shell=bash
# lib/lib-install.sh -- shared install hardening for ops-toolkit
# (sourced, never executed -- hence no shebang; the directive above declares
# the dialect for linting)
#
# Sourced (never executed) by bootstrap.sh and every pack installer.
# Mutates NOTHING by itself; provides gated helpers the installers call.
# All mutating helpers respect TOOLKIT_DRY_RUN=1 (print, do not act).
#
# Exit-code contract for scripts USING this lib:
# 0 = PROCEED (all steps ok or correctly no-oped)
# 1 = HOLD (a step failed)
# 2 = precondition missing (tool absent, version below floor)
#
# ASCII + LF.
# Guard against double-source
if [ -n "${_OPS_TOOLKIT_LIB_LOADED:-}" ]; then
return 0 2>/dev/null || exit 0
fi
_OPS_TOOLKIT_LIB_LOADED=1
FENCE_BEGIN="# >>> ops-toolkit managed block >>>"
FENCE_END="# <<< ops-toolkit managed block <<<"
# ---------------------------------------------------------------- logging ---
info() { printf 'INFO: %s\n' "$*"; }
ok() { printf 'OK: %s\n' "$*"; }
warn() { printf 'WARN: %s\n' "$*"; }
fail() { printf 'FAIL: %s\n' "$*" >&2; }
# ------------------------------------------------------- dry-run plumbing ---
# run_mut CMD [ARGS...] -- execute a mutating command, or print it in dry-run.
# Returns the command's status (0 in dry-run).
run_mut() {
if [ "${TOOLKIT_DRY_RUN:-0}" = "1" ]; then
# NB: not "$*" -- callers set IFS to newline+tab, which would join
# the args with newlines and truncate the preview to one token.
printf 'DRY-RUN:'
printf ' %s' "$@"
printf '\n'
return 0
fi
"$@"
}
# -------------------------------------------------------- prerequisites -----
# require_cmd CMD [HINT] -- exit 2 with a hint if CMD is not on PATH.
require_cmd() {
local cmd=$1 hint=${2:-}
if ! command -v "$cmd" >/dev/null 2>&1; then
fail "required command not found: $cmd${hint:+ ($hint)}"
exit 2
fi
ok "found: $cmd"
}
# version_ge INSTALLED MINIMUM -- true if INSTALLED >= MINIMUM (sort -V).
version_ge() {
[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]
}
# require_version NAME INSTALLED MINIMUM -- exit 2 if below the floor.
# Floors, not pins: we check a minimum, we do not demand an exact version.
require_version() {
local name=$1 installed=$2 minimum=$3
if [ -z "$installed" ]; then
fail "$name: could not determine installed version"
exit 2
fi
if version_ge "$installed" "$minimum"; then
ok "$name $installed (floor $minimum)"
else
fail "$name $installed is below the required floor $minimum"
exit 2
fi
}
# ------------------------------------------------------------ apt helper ----
# apt_install PKG... -- install packages, with sudo when not root.
apt_install() {
local sudo_cmd=""
if [ "$(id -u)" -ne 0 ]; then
require_cmd sudo "needed to apt-get install as non-root"
sudo_cmd="sudo"
fi
run_mut $sudo_cmd apt-get install -y "$@"
}
# ------------------------------------------------------- symlink installs ---
# backup_then_link SRC DST
# - SRC must exist (repo file). DST is the live location (e.g. ~/.tmux.conf).
# - If DST already symlinks to SRC: no-op, say so (idempotency made visible).
# - If DST exists as anything else: move to DST.bak.<timestamp>, then link.
backup_then_link() {
local src=$1 dst=$2
if [ ! -e "$src" ]; then
fail "link source missing: $src"
return 2
fi
local src_real
src_real=$(readlink -f "$src" || true)
if [ -L "$dst" ]; then
local dst_target
dst_target=$(readlink -f "$dst" || true)
if [ "$dst_target" = "$src_real" ]; then
ok "already linked: $dst -> $src_real"
return 0
fi
fi
if [ -e "$dst" ] || [ -L "$dst" ]; then
local bak
bak="${dst}.bak.$(date +%Y%m%d-%H%M%S)"
run_mut mv "$dst" "$bak" || { fail "could not back up $dst"; return 1; }
warn "existing $dst backed up to $bak"
fi
run_mut ln -s "$src_real" "$dst" || { fail "could not link $dst"; return 1; }
ok "linked: $dst -> $src_real"
}
# copy_if_absent SRC DST -- for user-editable files (never clobber user edits).
copy_if_absent() {
local src=$1 dst=$2
if [ ! -e "$src" ]; then
fail "copy source missing: $src"
return 2
fi
if [ -e "$dst" ]; then
ok "exists, left untouched (user-owned): $dst"
return 0
fi
run_mut cp "$src" "$dst" || { fail "could not copy to $dst"; return 1; }
ok "installed: $dst (copy; edit freely, re-install never overwrites)"
}
# ensure_dir DIR
ensure_dir() {
local dir=$1
if [ -d "$dir" ]; then
return 0
fi
run_mut mkdir -p "$dir" || { fail "could not create $dir"; return 1; }
ok "created: $dir"
}
# ------------------------------------------------------------ rc fencing ----
# bashrc_fence TARGET (desired block content on stdin, WITHOUT markers)
# Replace-or-insert the single fenced ops-toolkit block in TARGET.
# - Identical content already fenced: no-op, say so.
# - Otherwise: back up TARGET once, strip any existing fence, append the
# new fenced block at the end. Never append-duplicates.
bashrc_fence() {
local target=$1
local content current tmp
content=$(cat)
if [ -f "$target" ]; then
current=$(awk -v m1="$FENCE_BEGIN" -v m2="$FENCE_END" '
$0 == m1 { inblk = 1; next }
$0 == m2 { inblk = 0; next }
inblk { print }
' "$target")
if [ "$current" = "$content" ]; then
ok "fence up to date in $target"
return 0
fi
fi
if [ "${TOOLKIT_DRY_RUN:-0}" = "1" ]; then
printf 'DRY-RUN: would write ops-toolkit fence into %s\n' "$target"
return 0
fi
if [ -f "$target" ]; then
local bak
bak="${target}.bak.$(date +%Y%m%d-%H%M%S)"
cp "$target" "$bak" || { fail "could not back up $target"; return 1; }
warn "backed up $target to $bak"
else
: > "$target" || { fail "could not create $target"; return 1; }
fi
tmp=$(mktemp) || { fail "mktemp failed"; return 1; }
awk -v m1="$FENCE_BEGIN" -v m2="$FENCE_END" '
$0 == m1 { inblk = 1; next }
$0 == m2 { inblk = 0; next }
!inblk { print }
' "$target" > "$tmp"
{
printf '%s\n' "$FENCE_BEGIN"
printf '%s\n' "$content"
printf '%s\n' "$FENCE_END"
} >> "$tmp"
mv "$tmp" "$target" || { fail "could not update $target"; return 1; }
ok "fence written to $target"
}