split machine-setup into an entry point and a base library
Structure before the work rather than during it: scripts/machine-setup.sh becomes
scripts/setup/machine-setup/, with the script itself as the entry point and
lib/base.sh holding what every part of it needs.
machine-setup.sh pre-flight and the numbered sections, for now
lib/base.sh shared state, output, the step/resume machine, prompts,
and OS detection
The rule for lib/ is definitions only — nothing there installs, writes or
restarts anything, so sourcing it is safe from anywhere. That is why the ERR
trap stayed in the entry point: a trap is a side effect on whoever sources it.
Behaviour is unchanged. Verified by diffing the moved region against the previous
commit: identical set of functions, and the only differences are added comments,
section banners, fail() reformatted onto three lines, and one new line — a guard
against double-sourcing, which matters because steps will source this directly
once they move out, and a second pass would reset SUMMARY.
The sections are still one 1111-line block below pre-flight; they move into
steps/ as each is worked through. The script also still reads ssh-keys.zip,
.tmux.conf and ufw-docker-rules.conf from SCRIPT_DIR, which is now this
directory, so those three steps warn and skip until the files follow it here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# machine-setup — shared foundation
|
||||
# =============================================================================
|
||||
#
|
||||
# Sourced by machine-setup.sh before anything runs. DEFINITIONS ONLY: this file
|
||||
# declares state and functions and must never install, write or restart
|
||||
# anything. Sourcing it has to be safe at any point, including from a step that
|
||||
# is only being read for its variables.
|
||||
#
|
||||
# The one thing it expects from its caller, because they are facts about the
|
||||
# entry point rather than about this library:
|
||||
#
|
||||
# SCRIPT_DIR directory of the script being run
|
||||
# PROGRESS_FILE where completed step names are recorded
|
||||
#
|
||||
# Everything else below is owned here.
|
||||
|
||||
# Guard against being sourced twice — steps will eventually source this
|
||||
# directly so they can be run on their own, and re-running it would reset
|
||||
# SUMMARY and lose everything recorded so far.
|
||||
[[ -n "${MACHINE_SETUP_BASE_LOADED:-}" ]] && return 0
|
||||
MACHINE_SETUP_BASE_LOADED=1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Shared state
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
SUMMARY=() # what was done, printed at the end
|
||||
ERRORS=() # non-fatal failures, printed at the end
|
||||
CURRENT_STEP=""
|
||||
SKIP_STEP=false
|
||||
|
||||
# What machine this is. Filled in by detect_os() before any step runs; every step
|
||||
# after that branches on these rather than assuming apt on x86_64.
|
||||
OS="" # os-release ID: ubuntu | debian | arch | fedora | macos | …
|
||||
OS_NAME="" # pretty name, for the banner
|
||||
OS_VERSION="" # version id; empty on rolling releases
|
||||
PM="" # apt | pacman | dnf | brew
|
||||
ARCH="" # amd64 | arm64, normalised — upstream tarballs disagree on spelling
|
||||
IS_WSL=false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Output
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}::${NC} $*"; }
|
||||
ok() { echo -e " ${GREEN}OK${NC}: $*"; }
|
||||
warn() { echo -e " ${YELLOW}WARN${NC}: $*"; }
|
||||
fail() {
|
||||
echo -e " ${RED}FAIL${NC}: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Steps and resume
|
||||
# -----------------------------------------------------------------------------
|
||||
#
|
||||
# A step announces itself, and is skipped when its name is already in the
|
||||
# progress file. step_ok records it. The pattern at each call site is:
|
||||
#
|
||||
# step "Name"
|
||||
# if ! skip; then
|
||||
# …
|
||||
# step_ok
|
||||
# fi
|
||||
|
||||
step() {
|
||||
CURRENT_STEP="$1"
|
||||
if grep -qxF "$1" "$PROGRESS_FILE" 2>/dev/null; then
|
||||
echo -e " ${GREEN}SKIP${NC}: $1 (already done)"
|
||||
SKIP_STEP=true
|
||||
return
|
||||
fi
|
||||
SKIP_STEP=false
|
||||
echo ""
|
||||
echo -e "${BOLD}── $1 ──${NC}"
|
||||
}
|
||||
|
||||
skip() { [[ "$SKIP_STEP" == true ]]; }
|
||||
|
||||
step_ok() {
|
||||
echo "$CURRENT_STEP" >>"$PROGRESS_FILE"
|
||||
}
|
||||
|
||||
# Try a command, log error but don't exit
|
||||
try() {
|
||||
local label="$1"
|
||||
shift
|
||||
if "$@" 2>&1; then
|
||||
ok "$label"
|
||||
else
|
||||
warn "$label — failed (non-critical, continuing)"
|
||||
ERRORS+=("$label")
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Input
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
prompt_value() {
|
||||
local varname="$1" message="$2" default="$3"
|
||||
# If env var already set, use it silently
|
||||
if [[ -n "${!varname:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
local input
|
||||
if [[ -n "$default" ]]; then
|
||||
read -rp "$message [$default]: " input
|
||||
eval "$varname=\"\${input:-$default}\""
|
||||
else
|
||||
read -rp "$message: " input
|
||||
eval "$varname=\"\$input\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Run a block as the created user (login shell, inherits HOME)
|
||||
as_user() {
|
||||
sudo -u "$USERNAME" -i bash -c "$1"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Operating system detection
|
||||
# -----------------------------------------------------------------------------
|
||||
#
|
||||
# Read one key out of /etc/os-release without leaking the rest of it into this
|
||||
# script. That file defines NAME, VERSION and ID — all generic enough to collide
|
||||
# with something here — so it is sourced in a subshell and only the one value
|
||||
# asked for comes back.
|
||||
os_release() {
|
||||
[[ -r /etc/os-release ]] || return 1
|
||||
# shellcheck disable=SC1091
|
||||
(
|
||||
. /etc/os-release 2>/dev/null
|
||||
printf '%s' "${!1:-}"
|
||||
)
|
||||
}
|
||||
|
||||
# Identify the machine, or refuse to guess.
|
||||
#
|
||||
# /etc/os-release rather than probing for a binary: a box can have more than one
|
||||
# package manager on PATH (a Homebrew install on Linux, a leftover apt on a
|
||||
# converted box), and only os-release can say which distribution the machine
|
||||
# actually IS, or give a version worth reporting.
|
||||
#
|
||||
# ID_LIKE is the fallback so derivatives resolve without being listed by name —
|
||||
# Pop!_OS, Mint and EndeavourOS all answer correctly without appearing below.
|
||||
detect_os() {
|
||||
local kernel like
|
||||
kernel="$(uname -s)"
|
||||
|
||||
case "$kernel" in
|
||||
Darwin)
|
||||
OS="macos"
|
||||
OS_VERSION="$(sw_vers -productVersion 2>/dev/null || true)"
|
||||
OS_NAME="macOS ${OS_VERSION}"
|
||||
PM="brew"
|
||||
;;
|
||||
Linux)
|
||||
OS="$(os_release ID || true)"
|
||||
OS_NAME="$(os_release PRETTY_NAME || true)"
|
||||
OS_VERSION="$(os_release VERSION_ID || true)"
|
||||
like="$(os_release ID_LIKE || true)"
|
||||
|
||||
case "$OS" in
|
||||
ubuntu | debian | linuxmint | pop | raspbian | elementary) PM="apt" ;;
|
||||
arch | manjaro | endeavouros | cachyos | garuda) PM="pacman" ;;
|
||||
fedora | rhel | centos | rocky | almalinux) PM="dnf" ;;
|
||||
*)
|
||||
case " $like " in
|
||||
*" debian "* | *" ubuntu "*) PM="apt" ;;
|
||||
*" arch "*) PM="pacman" ;;
|
||||
*" fedora "* | *" rhel "*) PM="dnf" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
# WSL reports itself as Linux, but has no real systemd session: masking
|
||||
# sleep targets, restarting logind and anything touching the boot path
|
||||
# either fail or silently do nothing. Worth knowing before those steps run.
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=true; fi
|
||||
;;
|
||||
MINGW* | MSYS* | CYGWIN*)
|
||||
fail "Windows is not supported. Run this inside WSL2 with an Ubuntu image instead."
|
||||
;;
|
||||
*)
|
||||
fail "Unrecognised kernel '$kernel' — cannot tell what this machine is."
|
||||
;;
|
||||
esac
|
||||
|
||||
# Normalised once here because upstream projects spell it differently:
|
||||
# Neovim ships aarch64, Go and Docker ship arm64, and lazygit ships x86_64.
|
||||
case "$(uname -m)" in
|
||||
x86_64 | amd64) ARCH="amd64" ;;
|
||||
aarch64 | arm64) ARCH="arm64" ;;
|
||||
*) fail "Unsupported CPU architecture '$(uname -m)' — this script installs amd64/arm64 binaries only." ;;
|
||||
esac
|
||||
|
||||
[[ -n "$OS" ]] || fail "Could not identify this distribution (no readable /etc/os-release)."
|
||||
[[ -n "$OS_NAME" ]] || OS_NAME="$OS${OS_VERSION:+ $OS_VERSION}"
|
||||
}
|
||||
@@ -2,178 +2,27 @@
|
||||
set -e
|
||||
|
||||
# =============================================================================
|
||||
# Ubuntu Server Setup Script
|
||||
# Single-pass, non-interactive (install-everything) provisioning for a fresh
|
||||
# Ubuntu server. Run as root: sudo ./setup-ubuntu.sh
|
||||
# machine-setup — provisioning for a fresh machine
|
||||
#
|
||||
# Brings a blank box up to a usable state: users, SSH, networking, firewall,
|
||||
# Docker, shell and editor tooling, language runtimes.
|
||||
#
|
||||
# Run as root: sudo scripts/setup/machine-setup/machine-setup.sh
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROGRESS_FILE="$SCRIPT_DIR/.setup-progress"
|
||||
SUMMARY=()
|
||||
ERRORS=()
|
||||
CURRENT_STEP=""
|
||||
SKIP_STEP=false
|
||||
|
||||
# What machine this is. Filled in by detect_os() before any step runs; every step
|
||||
# after that branches on these rather than assuming apt on x86_64.
|
||||
OS="" # os-release ID: ubuntu | debian | arch | fedora | macos | …
|
||||
OS_NAME="" # pretty name, for the banner
|
||||
OS_VERSION="" # version id; empty on rolling releases
|
||||
PM="" # apt | pacman | dnf | brew
|
||||
ARCH="" # amd64 | arm64, normalised — upstream tarballs disagree on spelling
|
||||
IS_WSL=false
|
||||
# Shared state, output helpers, the step/resume machine and OS detection. Kept in
|
||||
# lib/ so a step can eventually be read — or run — on its own without dragging the
|
||||
# whole script in. Definitions only; nothing in there acts.
|
||||
# shellcheck source=lib/base.sh
|
||||
source "$SCRIPT_DIR/lib/base.sh"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}::${NC} $*"; }
|
||||
ok() { echo -e " ${GREEN}OK${NC}: $*"; }
|
||||
warn() { echo -e " ${YELLOW}WARN${NC}: $*"; }
|
||||
fail() { echo -e " ${RED}FAIL${NC}: $*"; exit 1; }
|
||||
|
||||
step() {
|
||||
CURRENT_STEP="$1"
|
||||
if grep -qxF "$1" "$PROGRESS_FILE" 2>/dev/null; then
|
||||
echo -e " ${GREEN}SKIP${NC}: $1 (already done)"
|
||||
SKIP_STEP=true
|
||||
return
|
||||
fi
|
||||
SKIP_STEP=false
|
||||
echo ""
|
||||
echo -e "${BOLD}── $1 ──${NC}"
|
||||
}
|
||||
|
||||
skip() { [[ "$SKIP_STEP" == true ]]; }
|
||||
|
||||
step_ok() {
|
||||
echo "$CURRENT_STEP" >> "$PROGRESS_FILE"
|
||||
}
|
||||
|
||||
# Trap errors with context
|
||||
# Trap errors with context. Installed here rather than in lib/base.sh, because
|
||||
# that file is definitions only and a trap is a side effect on whoever sources it.
|
||||
trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║ SETUP FAILED${NC}"; echo -e "${RED}║ Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║ Line: $LINENO${NC}"; echo -e "${RED}║ Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR
|
||||
|
||||
# Try a command, log error but don't exit
|
||||
try() {
|
||||
local label="$1"
|
||||
shift
|
||||
if "$@" 2>&1; then
|
||||
ok "$label"
|
||||
else
|
||||
warn "$label — failed (non-critical, continuing)"
|
||||
ERRORS+=("$label")
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_value() {
|
||||
local varname="$1" message="$2" default="$3"
|
||||
# If env var already set, use it silently
|
||||
if [[ -n "${!varname:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
local input
|
||||
if [[ -n "$default" ]]; then
|
||||
read -rp "$message [$default]: " input
|
||||
eval "$varname=\"\${input:-$default}\""
|
||||
else
|
||||
read -rp "$message: " input
|
||||
eval "$varname=\"\$input\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Run a block as the created user (login shell, inherits HOME)
|
||||
as_user() {
|
||||
sudo -u "$USERNAME" -i bash -c "$1"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Operating system detection
|
||||
# -----------------------------------------------------------------------------
|
||||
#
|
||||
# Read one key out of /etc/os-release without leaking the rest of it into this
|
||||
# script. That file defines NAME, VERSION and ID — all generic enough to collide
|
||||
# with something here — so it is sourced in a subshell and only the one value
|
||||
# asked for comes back.
|
||||
os_release() {
|
||||
[[ -r /etc/os-release ]] || return 1
|
||||
# shellcheck disable=SC1091
|
||||
(
|
||||
. /etc/os-release 2>/dev/null
|
||||
printf '%s' "${!1:-}"
|
||||
)
|
||||
}
|
||||
|
||||
# Identify the machine, or refuse to guess.
|
||||
#
|
||||
# /etc/os-release rather than probing for a binary: a box can have more than one
|
||||
# package manager on PATH (a Homebrew install on Linux, a leftover apt on a
|
||||
# converted box), and only os-release can say which distribution the machine
|
||||
# actually IS, or give a version worth reporting.
|
||||
#
|
||||
# ID_LIKE is the fallback so derivatives resolve without being listed by name —
|
||||
# Pop!_OS, Mint and EndeavourOS all answer correctly without appearing below.
|
||||
detect_os() {
|
||||
local kernel like
|
||||
kernel="$(uname -s)"
|
||||
|
||||
case "$kernel" in
|
||||
Darwin)
|
||||
OS="macos"
|
||||
OS_VERSION="$(sw_vers -productVersion 2>/dev/null || true)"
|
||||
OS_NAME="macOS ${OS_VERSION}"
|
||||
PM="brew"
|
||||
;;
|
||||
Linux)
|
||||
OS="$(os_release ID || true)"
|
||||
OS_NAME="$(os_release PRETTY_NAME || true)"
|
||||
OS_VERSION="$(os_release VERSION_ID || true)"
|
||||
like="$(os_release ID_LIKE || true)"
|
||||
|
||||
case "$OS" in
|
||||
ubuntu | debian | linuxmint | pop | raspbian | elementary) PM="apt" ;;
|
||||
arch | manjaro | endeavouros | cachyos | garuda) PM="pacman" ;;
|
||||
fedora | rhel | centos | rocky | almalinux) PM="dnf" ;;
|
||||
*)
|
||||
case " $like " in
|
||||
*" debian "* | *" ubuntu "*) PM="apt" ;;
|
||||
*" arch "*) PM="pacman" ;;
|
||||
*" fedora "* | *" rhel "*) PM="dnf" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
# WSL reports itself as Linux, but has no real systemd session: masking
|
||||
# sleep targets, restarting logind and anything touching the boot path
|
||||
# either fail or silently do nothing. Worth knowing before those steps run.
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=true; fi
|
||||
;;
|
||||
MINGW* | MSYS* | CYGWIN*)
|
||||
fail "Windows is not supported. Run this inside WSL2 with an Ubuntu image instead."
|
||||
;;
|
||||
*)
|
||||
fail "Unrecognised kernel '$kernel' — cannot tell what this machine is."
|
||||
;;
|
||||
esac
|
||||
|
||||
# Normalised once here because upstream projects spell it differently:
|
||||
# Neovim ships aarch64, Go and Docker ship arm64, and lazygit ships x86_64.
|
||||
case "$(uname -m)" in
|
||||
x86_64 | amd64) ARCH="amd64" ;;
|
||||
aarch64 | arm64) ARCH="arm64" ;;
|
||||
*) fail "Unsupported CPU architecture '$(uname -m)' — this script installs amd64/arm64 binaries only." ;;
|
||||
esac
|
||||
|
||||
[[ -n "$OS" ]] || fail "Could not identify this distribution (no readable /etc/os-release)."
|
||||
[[ -n "$OS_NAME" ]] || OS_NAME="$OS${OS_VERSION:+ $OS_VERSION}"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# 1. Pre-flight
|
||||
# =============================================================================
|
||||
Reference in New Issue
Block a user