From 8057fbac4d0cc7284b15ea55fabe4800ff4284dc Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 28 Jul 2026 04:32:38 +0300 Subject: [PATCH] skeleton: launcher, entrypoint, distro/log/prompt libs --- install.sh | 34 +++++++++++++++++++ lib/detect.sh | 29 ++++++++++++++++ lib/log.sh | 22 ++++++++++++ lib/prompt.sh | 19 +++++++++++ setup.sh | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+) create mode 100755 install.sh create mode 100755 lib/detect.sh create mode 100755 lib/log.sh create mode 100755 lib/prompt.sh create mode 100755 setup.sh diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..2ed3e2f --- /dev/null +++ b/install.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# bootstrap launcher — curl | bash +# +# This file is intentionally tiny and stable: its only job is to clone the +# bootstrap repo (or update an existing clone) and hand off to setup.sh. +# All real logic lives in the repo so you can update it with `git pull`. +# +# Usage: +# curl -fsSL | bash +# curl -fsSL | bash -s -- --yes +# curl -fsSL | bash -s -- --only packages + +set -euo pipefail + +REPO_URL="${BOOTSTRAP_REPO:-https://g.mk.fo/bootstrap/main.git}" +CLONE_DIR="${BOOTSTRAP_HOME:-$HOME/.bootstrap}" + +need() { command -v "$1" >/dev/null 2>&1 || { echo "bootstrap: missing required command: $1" >&2; exit 1; }; } +need git + +if [[ -d "$CLONE_DIR/.git" ]]; then + echo ">> existing bootstrap at $CLONE_DIR — pulling updates" + git -C "$CLONE_DIR" pull --ff-only +else + if [[ -d "$CLONE_DIR" ]]; then + mv "$CLONE_DIR" "${CLONE_DIR}.bak.$(date +%Y%m%d-%H%M%S)" + echo ">> backed up old $CLONE_DIR" + fi + echo ">> cloning $REPO_URL -> $CLONE_DIR" + git clone --depth 1 "$REPO_URL" "$CLONE_DIR" +fi + +exec bash "$CLONE_DIR/setup.sh" "$@" \ No newline at end of file diff --git a/lib/detect.sh b/lib/detect.sh new file mode 100755 index 0000000..8a3b81b --- /dev/null +++ b/lib/detect.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# detect.sh — distro + arch detection. Exports DISTRO and ARCH. + +detect_distro() { + DISTRO="unknown" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + esac + + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + case "${ID:-}" in + debian|ubuntu) DISTRO="debian" ;; + arch) DISTRO="arch" ;; + *) + case "${ID_LIKE:-}" in + *debian*) DISTRO="debian" ;; + *arch*) DISTRO="arch" ;; + esac ;; + esac + fi + + export DISTRO ARCH +} + +detect_distro \ No newline at end of file diff --git a/lib/log.sh b/lib/log.sh new file mode 100755 index 0000000..2c669fe --- /dev/null +++ b/lib/log.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# log.sh — tiny color log helpers. + +log() { printf '\033[1;34m>>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; } +err() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; } +ok() { printf '\033[1;32mok\033[0m %s\n' "$*"; } + +# run a command as root when not already root +sudo_() { if (( EUID == 0 )); then "$@"; else sudo "$@"; fi; } + +# run a command AS the invoking (non-root) user — used for AUR builds, etc. +# when root: drop to SUDO_USER (or USER) via runuser; when not root: run directly. +as_user() { + local u="$USER" + if (( EUID == 0 )); then + u="${SUDO_USER:-$USER}" + command -v runuser >/dev/null 2>&1 && runuser -u "$u" -- "$@" || su "$u" -c "$*" + else + "$@" + fi +} \ No newline at end of file diff --git a/lib/prompt.sh b/lib/prompt.sh new file mode 100755 index 0000000..d301a7b --- /dev/null +++ b/lib/prompt.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# prompt.sh — plain y/N prompts. + +# ask "prompt?" [default y|n] -> sets REPLY +ask() { + local p="$1" d="${2:-y}" hint + if [[ "$d" == "y" ]]; then hint="[Y/n]"; else hint="[y/N]"; fi + read -rp "$p $hint " REPLY + [[ -z "$REPLY" ]] && REPLY="$d" +} + +# yn "prompt?" [default] -> returns 0 (true) if answer is yes +yn() { + ask "$1" "${2:-y}" + [[ "$REPLY" =~ ^[Yy]$ ]] +} + +# confirm_once "prompt?" default -> for destructive single confirm +confirm_once() { yn "$1" "${2:-n}"; } \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..25d68b9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# +# setup.sh — bootstrap entrypoint. +# Detects distro/arch, lists modular steps, asks y/N per step, runs them. +# +# Flags: +# --yes run all steps without prompting +# --only NAME run only the step matching NAME +# --skip NAME run all steps except NAME (repeatable in future) +# -h|--help usage + +set -euo pipefail + +BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck disable=SC1091 +source "$BOOT_DIR/lib/detect.sh" +source "$BOOT_DIR/lib/log.sh" +source "$BOOT_DIR/lib/prompt.sh" + +ALL=0; ONLY=""; SKIP="" +while (( $# )); do + case "$1" in + --yes|-y) ALL=1; shift ;; + --only) ONLY="${2:-}"; shift 2 ;; + --skip) SKIP="${2:-}"; shift 2 ;; + -h|--help) + echo "usage: setup.sh [--yes] [--only NAME] [--skip NAME]" + exit 0 ;; + *) echo "unknown arg: $1" >&2; shift ;; + esac +done + +# ---------------------------------------------------------------- collect steps +NAMES=(); DESCS=(); RUNS=() +for f in "$BOOT_DIR"/modules/*.sh; do + step_name=""; step_desc=""; step_run="" + # shellcheck disable=SC1090 + source "$f" + [[ -z "$step_name" ]] && continue + NAMES+=("$step_name") + DESCS+=("$step_desc") + RUNS+=("$step_run") +done + +# ----------------------------------------------------------------- print plan +echo +printf 'Detected: \033[1mdistro\033[0m=%s \033[1march\033[0m=%s \033[1muser\033[0m=%s\n' "$DISTRO" "$ARCH" "$USER" +echo +echo "Steps:" +for i in "${!NAMES[@]}"; do + printf ' [%02d] %-12s — %s\n' "$((i+1))" "${NAMES[$i]}" "${DESCS[$i]}" +done +echo + +# ----------------------------------------------------------------- decide run +run_step() { # name + local n="$1" i + for i in "${!NAMES[@]}"; do + if [[ "${NAMES[$i]}" == "$n" ]]; then + echo; echo "── ${NAMES[$i]} ──────────────────────────────────────────────" + # shellcheck disable=SCSC2086 + "${RUNS[$i]}" + return $? + fi + done + err "no step named '$n'"; return 1 +} + +if [[ -n "$ONLY" ]]; then + run_step "$ONLY" +elif (( ALL )); then + for n in "${NAMES[@]}"; do + [[ -n "$SKIP" && "$n" == "$SKIP" ]] && continue + run_step "$n" + done +else + for i in "${!NAMES[@]}"; do + local_run=1 + [[ -n "$SKIP" && "${NAMES[$i]}" == "$SKIP" ]] && local_run=0 + if (( local_run )) && yn "Run step [$(printf %02d $((i+1)))] ${NAMES[$i]}?" y; then + run_step "${NAMES[$i]}" || warn "step ${NAMES[$i]} had an error (continuing)" + else + echo " skip ${NAMES[$i]}" + fi + done +fi + +echo +ok "bootstrap done." +echo " re-run any step: bash $BOOT_DIR/setup.sh --only " +echo " run everything: bash $BOOT_DIR/setup.sh --yes" \ No newline at end of file