Blueforce-Linux/admin/bf-bootstrap.sh
2026-09-17 13:22:26 +03:00

369 lines
14 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# bf-bootstrap.sh — one-command install of the Blueforce Linux admin toolbox.
#
# Documented entry points:
# curl -fsSL https://raw.githubusercontent.com/emirhanmamak-blueforce/Blueforce-Linux/main/admin/bf-bootstrap.sh | sudo bash
# sudo admin/bf-bootstrap.sh # from a checkout
#
# Steps, in order:
# 1. root check (apply mode only; --check is read-only and needs no root),
# 2. Ubuntu release check against the supported baseline (24.04 / 26.04),
# 3. dependency install: git, curl, ansible-core,
# 4. clone the repository into <dir> (default /opt/blueforce-linux) or
# fast-forward an existing checkout (never resets, never discards work),
# 5. link admin/bf-*, scripts/diagnostics/bf-* and scripts/maintenance/bf-*
# into /usr/local/bin (idempotent; a re-run refreshes the links),
# 6. make admin/bf executable so `sudo bf` starts the Python console,
# 7. print a clear summary.
#
# Safety
# * This script writes no secret and installs code only: credentials stay on
# the device and are never fetched by the installer (see admin/bf-creds).
# * Re-running it is safe: the checkout is fast-forwarded and the tool links
# are re-created. Nothing is deleted and no `git reset` is ever performed,
# so local work in the checkout is never discarded.
# * --check reports the same plan without touching the system.
#
set -euo pipefail
REPO_URL="${BF_REPO_URL:-https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git}"
BRANCH_DEFAULT="main"
INSTALL_DIR_DEFAULT="/opt/blueforce-linux"
BIN_DIR="${BF_BIN_DIR:-/usr/local/bin}"
SUPPORTED_UBUNTU=("24.04" "26.04")
# Command name / apt package name pairs for the installer prerequisites.
DEPENDENCY_COMMANDS=(git curl ansible-playbook)
DEPENDENCY_PACKAGES=(git curl ansible-core)
CHECK_MODE=0
ASSUME_YES=0
BRANCH="$BRANCH_DEFAULT"
DIR=""
LINKED=0
MENU_READY=0
usage() {
cat <<'EOF'
Usage: sudo bf-bootstrap.sh [--check] [--dir DIR] [--branch BRANCH] [--yes] [--help]
Install (or refresh) the Blueforce Linux admin toolbox from git: fetch the
repository, install the prerequisites, link the bf-* tools into
/usr/local/bin, and make `sudo bf` available for the on-device setup.
Options:
--check Read-only: report what would happen. It never mutates
anything and never requires root.
--dir DIR Checkout directory. Default: the checkout this script lives
in, otherwise /opt/blueforce-linux.
--branch BRANCH Branch to track (default: main).
--yes, -y Do not ask for confirmation.
--help, -h Show this help and exit.
Environment:
BF_REPO_URL Repository URL (default: the upstream GitHub repository).
BF_INSTALL_DIR Checkout directory; same as --dir.
BF_BIN_DIR Tool link directory (default: /usr/local/bin).
Notes:
* Installer prerequisites are git, curl and ansible-core (ansible-playbook).
* Supported baseline is Ubuntu 24.04 / 26.04; any other release only warns.
* This script installs code only. It never writes, fetches, or prints a
secret; device credentials are handled by admin/bf-creds.
EOF
}
say() { printf 'bf-bootstrap: %s\n' "$*"; }
warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; }
die() { printf 'bf-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; }
# Git-LFS smudge is disabled for every git operation this script performs.
#
# Why: the offline package pool is ~884 MB of .deb payload tracked through Git
# LFS, and a default clone materialises all of it (measured: 2.7 GB working tree
# against 20 MB with smudge off, a 135x difference). A device that only needs to
# reach the console and the manifest pays that cost on every clone for bytes it
# may never install from.
#
# The trade is explicit: with smudge off the pool files on disk are 131-byte
# pointer texts, and the installer refuses such a bundle rather than installing
# from it ("Nokta dosyası" below). Fetching the payload is therefore a separate,
# deliberate step -- `git lfs pull --include='provisioning/offline-repo/built/pool/*'`
# -- so that "I cloned the repository" and "I have the packages" stay two facts.
#
# This is deliberately the smudge-off half only. Enabling a full `git lfs pull`
# here would reintroduce the 884 MB download this prevents.
export GIT_LFS_SKIP_SMUDGE=1
run() {
if [[ "$CHECK_MODE" -eq 1 ]]; then
printf 'bf-bootstrap: check: would run: %s\n' "$*"
return 0
fi
"$@"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--check) CHECK_MODE=1; shift ;;
--dir) DIR="${2:?missing value for --dir}"; shift 2 ;;
--branch) BRANCH="${2:?missing value for --branch}"; shift 2 ;;
--yes|-y) ASSUME_YES=1; shift ;;
--help|-h) usage; exit 0 ;;
*) printf 'bf-bootstrap: ERROR: unknown argument: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done
# --------------------------------------------------------------------------- #
# Where does this script live? When it runs from a checkout, that checkout is
# the default target; the piped one-command form has no checkout, so the
# default install directory is used.
# --------------------------------------------------------------------------- #
SCRIPT_SOURCE="${BASH_SOURCE[0]:-}"
if [[ -n "$SCRIPT_SOURCE" && -r "$SCRIPT_SOURCE" ]]; then
SCRIPT_REAL="$(readlink -f "$SCRIPT_SOURCE" 2>/dev/null || printf '%s' "$SCRIPT_SOURCE")"
CANDIDATE="$(cd "$(dirname "$SCRIPT_REAL")/.." 2>/dev/null && pwd || true)"
if [[ -n "$CANDIDATE" && -f "$CANDIDATE/admin/bf-bootstrap.sh" && -d "$CANDIDATE/scripts/diagnostics" ]]; then
CHECKOUT_ROOT="$CANDIDATE"
fi
fi
if [[ -z "$DIR" ]]; then
DIR="${BF_INSTALL_DIR:-${CHECKOUT_ROOT:-$INSTALL_DIR_DEFAULT}}"
fi
if [[ -z "$BRANCH" ]]; then
die 'branch must not be empty'
fi
# --------------------------------------------------------------------------- #
# 1. Root check. --check only reports, so it works as an unprivileged user.
# --------------------------------------------------------------------------- #
if [[ "$CHECK_MODE" -eq 1 ]]; then
if [[ "$(id -u)" -ne 0 ]]; then
say 'check: not running as root; apply mode will require sudo'
fi
else
if [[ "$(id -u)" -ne 0 ]]; then
die "must run as root: sudo $0 (or: sudo bf-bootstrap.sh)"
fi
fi
# --------------------------------------------------------------------------- #
# 2. Ubuntu release check. A different release is only a warning: the goal is
# to install the toolbox, not to block a working host.
# --------------------------------------------------------------------------- #
DISTRO_ID=""
DISTRO_VERSION=""
DISTRO_PRETTY="unknown"
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO_ID="${ID:-}"
DISTRO_VERSION="${VERSION_ID:-}"
DISTRO_PRETTY="${PRETTY_NAME:-unknown}"
fi
if [[ "$DISTRO_ID" == "ubuntu" ]]; then
SUPPORTED=0
for release in "${SUPPORTED_UBUNTU[@]}"; do
if [[ "$DISTRO_VERSION" == "$release" ]]; then
SUPPORTED=1
fi
done
if [[ "$SUPPORTED" -eq 1 ]]; then
say "ubuntu ${DISTRO_VERSION} is a supported baseline"
else
warn "Ubuntu ${DISTRO_VERSION:-unknown} is outside the tested baseline (${SUPPORTED_UBUNTU[*]}); continuing"
fi
else
warn "this is not an Ubuntu host (ID=${DISTRO_ID:-unknown}, ${DISTRO_PRETTY}); the Field OS baseline is Ubuntu ${SUPPORTED_UBUNTU[*]}"
fi
# --------------------------------------------------------------------------- #
# Operator confirmation. Only a terminal prompts: the documented one-command
# form (`curl ... | sudo bash`) has no terminal on stdin and never blocks.
# --------------------------------------------------------------------------- #
if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 && -t 0 ]]; then
printf 'Install or update the Blueforce toolbox in %s? [y/N] ' "$DIR"
ANSWER=""
read -r ANSWER || ANSWER=""
if [[ ! "$ANSWER" =~ ^[Yy]([Ee][Ss])?$ ]]; then
say 'aborted by the operator'
exit 2
fi
fi
# --------------------------------------------------------------------------- #
# 3. Dependencies.
# --------------------------------------------------------------------------- #
MISSING_COMMANDS=()
MISSING_PACKAGES=()
say 'dependency check'
for index in "${!DEPENDENCY_COMMANDS[@]}"; do
if ! command -v "${DEPENDENCY_COMMANDS[$index]}" >/dev/null 2>&1; then
MISSING_COMMANDS+=("${DEPENDENCY_COMMANDS[$index]}")
MISSING_PACKAGES+=("${DEPENDENCY_PACKAGES[$index]}")
fi
done
if [[ ${#MISSING_PACKAGES[@]} -eq 0 ]]; then
say "dependencies present: ${DEPENDENCY_COMMANDS[*]}"
elif [[ "$CHECK_MODE" -eq 1 ]]; then
say "check: would install with apt-get: ${MISSING_PACKAGES[*]} (missing: ${MISSING_COMMANDS[*]})"
else
if ! command -v apt-get >/dev/null 2>&1; then
die "apt-get not found; install these packages first: ${MISSING_PACKAGES[*]}"
fi
say "installing dependencies: ${MISSING_PACKAGES[*]}"
export DEBIAN_FRONTEND=noninteractive
run apt-get update
run apt-get install -y --no-install-recommends "${MISSING_PACKAGES[@]}"
fi
# --------------------------------------------------------------------------- #
# 4. Repository: clone once, then fast-forward only.
# --------------------------------------------------------------------------- #
update_checkout() {
local remote="" origin_url="" dirty=""
if ! git -C "$DIR" rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
warn "checkout has no commits yet: $DIR; skipping the update"
return 0
fi
# Prefer `origin`, otherwise accept the checkout's single remote (a local
# clone of this project may name it `github`, as the upstream checkout does).
if git -C "$DIR" remote get-url origin >/dev/null 2>&1; then
remote="origin"
else
remote="$(git -C "$DIR" remote 2>/dev/null | head -n 1 || true)"
fi
if [[ -z "$remote" ]]; then
warn "checkout has no git remote: $DIR; skipping the update"
return 0
fi
origin_url="$(git -C "$DIR" remote get-url "$remote" 2>/dev/null || true)"
if [[ "$origin_url" != *"Blueforce-Linux"* ]]; then
warn "remote '$remote' looks unexpected: $origin_url"
fi
dirty="$(git -C "$DIR" status --porcelain 2>/dev/null || true)"
if [[ -n "$dirty" ]]; then
warn "local changes present in $DIR; skipping the update (nothing was reset or stashed)"
return 0
fi
if [[ "$CHECK_MODE" -eq 1 ]]; then
say "check: would fast-forward $DIR to $remote/$BRANCH"
return 0
fi
say "updating $DIR (fast-forward only, remote $remote)"
if ! git -C "$DIR" fetch --prune --quiet "$remote" "$BRANCH"; then
warn "fetch failed (offline?); keeping the current checkout"
return 0
fi
if ! git -C "$DIR" merge --ff-only --quiet "$remote/$BRANCH"; then
warn "cannot fast-forward to $remote/$BRANCH; keeping the current checkout (no reset performed)"
fi
return 0
}
if [[ -e "$DIR" && ! -d "$DIR" ]]; then
die "install directory exists and is not a directory: $DIR"
fi
if [[ -d "$DIR" ]]; then
if [[ -d "$DIR/.git" ]]; then
say "existing checkout: $DIR"
update_checkout
else
die "directory exists but is not a git checkout; refusing to touch it: $DIR"
fi
elif [[ "$CHECK_MODE" -eq 1 ]]; then
say "check: would clone $REPO_URL (branch $BRANCH) into $DIR"
else
say "cloning $REPO_URL (branch $BRANCH) into $DIR"
run install -d -m 0755 "$(dirname "$DIR")"
run git clone --branch "$BRANCH" --single-branch -- "$REPO_URL" "$DIR"
fi
# --------------------------------------------------------------------------- #
# 5. Link the tools into /usr/local/bin. Idempotent: `ln -sfn` refreshes an
# existing link, and a re-run after `git pull` needs no extra step.
# --------------------------------------------------------------------------- #
link_tools() {
local source="" base=""
for source in "$DIR"/admin/bf-* "$DIR"/scripts/diagnostics/bf-* "$DIR"/scripts/maintenance/bf-*; do
if [[ ! -f "$source" ]]; then
continue
fi
base="$(basename "$source")"
if [[ "$CHECK_MODE" -eq 1 ]]; then
say "check: would link $source -> $BIN_DIR/$base"
else
chmod 0755 "$source"
ln -sfn "$source" "$BIN_DIR/$base"
fi
LINKED=$((LINKED + 1))
done
}
if [[ -d "$DIR" ]]; then
run install -d -m 0755 "$BIN_DIR"
link_tools
if [[ "$LINKED" -eq 0 ]]; then
warn "no bf-* tools found under $DIR (admin/, scripts/diagnostics/, scripts/maintenance/)"
elif [[ "$CHECK_MODE" -eq 0 ]]; then
say "linked $LINKED tools into $BIN_DIR"
fi
elif [[ "$CHECK_MODE" -eq 1 ]]; then
say "check: would link the bf-* tools from $DIR into $BIN_DIR"
fi
# --------------------------------------------------------------------------- #
# 6. Operator console entry point.
#
# The Python console (`admin/bf` -> bfos) is the only operator path.
# --------------------------------------------------------------------------- #
PY_CONSOLE="$DIR/admin/bf"
if [[ -f "$PY_CONSOLE" ]]; then
if [[ "$CHECK_MODE" -eq 1 ]]; then
say "check: would make $PY_CONSOLE executable and link $BIN_DIR/bf"
else
chmod 0755 "$PY_CONSOLE"
ln -sfn "$PY_CONSOLE" "$BIN_DIR/bf"
say "bf ready: $BIN_DIR/bf (Python console)"
fi
else
warn "admin/bf is not present in $DIR; the Python console arrives with the next revision of the checkout"
fi
# --------------------------------------------------------------------------- #
# 7. Summary.
# --------------------------------------------------------------------------- #
if [[ "$CHECK_MODE" -eq 1 ]]; then
printf '\nbf-bootstrap: check complete -- no changes were made.\n'
printf ' checkout : %s\n' "$DIR"
printf ' branch : %s\n' "$BRANCH"
printf ' tools : %s tool(s) would be linked into %s\n' "$LINKED" "$BIN_DIR"
printf ' next : sudo bf-bootstrap.sh --yes\n'
exit 0
fi
REVISION=""
if [[ -d "$DIR/.git" ]]; then
REVISION="$(git -C "$DIR" rev-parse --short HEAD 2>/dev/null || true)"
fi
printf '\nbf-bootstrap: summary\n'
printf ' repository : %s\n' "$DIR"
printf ' branch : %s\n' "$BRANCH"
if [[ -n "$REVISION" ]]; then
printf ' revision : %s\n' "$REVISION"
fi
printf ' tools : %s linked into %s\n' "$LINKED" "$BIN_DIR"
if [[ -x "$BIN_DIR/bf" ]]; then
printf ' console : %s (Python, primary)\n' "$BIN_DIR/bf"
else
printf ' console : pending (admin/bf is not in the checkout yet)\n'
fi
if [[ -x "$BIN_DIR/bf" ]]; then
printf ' next : sudo bf\n'
else
printf ' next : sudo admin/bf\n'
fi
printf '\nInstallation complete. To get started: sudo bf\n'
printf 'Kurulum tamam. Başlamak için: sudo bf\n'