#!/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 (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-menu executable so `sudo bf-menu` starts the on-device # installer menu, # 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-menu` 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 primary path; the bash # `bf-menu` stays linked as a fallback so a host without python3 can still be # operated. # --------------------------------------------------------------------------- # PY_CONSOLE="$DIR/admin/bf" MENU_SOURCE="$DIR/admin/bf-menu" MENU_READY=0 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 MENU_READY=1 else warn "admin/bf is not present in $DIR; the Python console arrives with the next revision of the checkout" fi if [[ -f "$MENU_SOURCE" ]]; then if [[ "$CHECK_MODE" -eq 1 ]]; then say "check: would make $MENU_SOURCE executable and link $BIN_DIR/bf-menu" else chmod 0755 "$MENU_SOURCE" ln -sfn "$MENU_SOURCE" "$BIN_DIR/bf-menu" say "bf-menu ready: $BIN_DIR/bf-menu (bash fallback)" fi MENU_READY=1 else warn "admin/bf-menu is not present in $DIR; the bash fallback will work once it is in the checkout" fi if [[ "$MENU_READY" -eq 0 ]]; then warn "no operator console was found in $DIR; link one of admin/bf or admin/bf-menu manually" 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-menu" ]]; then printf ' fallback : %s (bash)\n' "$BIN_DIR/bf-menu" else printf ' fallback : pending (admin/bf-menu is not in the checkout yet)\n' fi # The Python console is the recommended entry point; the bash menu stays as the # fallback for a host without python3. if [[ -x "$BIN_DIR/bf" ]]; then printf ' next : sudo bf\n' else printf ' next : sudo bf-menu\n' fi printf '\nInstallation complete. To get started: sudo bf\n' printf 'Kurulum tamam. Başlamak için: sudo bf\n'