diff --git a/scripts/setup/machine-setup/lib/base.sh b/scripts/setup/machine-setup/lib/base.sh index d7b08e0e..5308972c 100644 --- a/scripts/setup/machine-setup/lib/base.sh +++ b/scripts/setup/machine-setup/lib/base.sh @@ -135,6 +135,34 @@ prompt_value() { fi } +# Ask before acting. Every section that changes the machine goes through this, so +# a run is a sequence of things you agreed to rather than a wall of output you +# read afterwards to find out what happened. +# +# Enter means yes — unlike the machine-role question, which has no default. These +# are "do the thing you already asked for", and making twenty of them require a +# deliberate keystroke would train people to hold the y key down. +# +# ASSUME_YES=1 answers all of them, for an unattended run. +confirm() { + local message="${1:-Proceed?}" answer + [[ "${ASSUME_YES:-}" == "1" ]] && return 0 + + while true; do + # EOF is not a yes. Without this an unattended run without ASSUME_YES would + # spin here forever. + if ! read -rp " ${message} [Y/n]: " answer; then + echo "" + fail "No answer. Set ASSUME_YES=1 to run without prompts." + fi + case "$answer" in + "" | y | Y | yes | Yes) return 0 ;; + n | N | no | No) return 1 ;; + *) warn "Answer y or n." ;; + esac + done +} + # Run a block as the created user (login shell, inherits HOME) as_user() { sudo -u "$USERNAME" -i bash -c "$1" diff --git a/scripts/setup/machine-setup/lib/packages.sh b/scripts/setup/machine-setup/lib/packages.sh index c5244882..52acdcd4 100644 --- a/scripts/setup/machine-setup/lib/packages.sh +++ b/scripts/setup/machine-setup/lib/packages.sh @@ -35,6 +35,7 @@ MACHINE_SETUP_PACKAGES_LOADED=1 # packages it deliberately left alone. LAST_INSTALLED=() LAST_KEPT=() +LAST_SKIPPED=() # ----------------------------------------------------------------------------- # The sections @@ -155,6 +156,7 @@ pkg_install() { local pkg local -a missing=() present=() + LAST_SKIPPED=() for pkg in "$@"; do if pkg_is_installed "$pkg"; then present+=("$pkg"); else missing+=("$pkg"); fi done @@ -162,23 +164,50 @@ pkg_install() { LAST_INSTALLED=("${missing[@]}") LAST_KEPT=("${present[@]}") - info "$label — installs what is missing, keeps what you already have" - ((${#present[@]})) && echo " already here: ${present[*]}" + announce_plan "$label" present missing || return 0 + pkg_install_now "${missing[@]}" +} - if ((${#missing[@]} == 0)); then +# Print what a section is about to do and ask permission for it. +# +# Takes the NAMES of the two arrays rather than their contents, because a list +# passed by value cannot be told apart from an empty one once it has been through +# word splitting. +# +# Returns non-zero when there is nothing to do, or when the answer was no — in +# both cases the caller should skip its action. LAST_INSTALLED is cleared on a +# refusal so the summary does not claim work that never happened. +announce_plan() { + local label="$1" + local -n _present="$2" + local -n _missing="$3" + + echo "" + info "${label} — installs what is missing, keeps what you already have" + ((${#_present[@]})) && echo " already here: ${_present[*]}" + + if ((${#_missing[@]} == 0)); then echo " to install: nothing, all present" - return 0 + return 1 fi - echo " to install: ${missing[*]}" - pkg_install_now "${missing[@]}" + echo " to install: ${_missing[*]}" + if ! confirm "Proceed?"; then + warn "skipped by request" + LAST_INSTALLED=() + LAST_SKIPPED=("${_missing[@]}") + return 1 + fi + return 0 } # One summary line describing what a section actually did, from LAST_INSTALLED # and LAST_KEPT. Call straight after pkg_install or tools_install. summarise_last() { local label="$1" - if ((${#LAST_INSTALLED[@]} == 0)); then + if ((${#LAST_SKIPPED[@]})); then + SUMMARY+=("$label: SKIPPED by request — ${LAST_SKIPPED[*]}") + elif ((${#LAST_INSTALLED[@]} == 0)); then SUMMARY+=("$label: already present, nothing installed") elif ((${#LAST_KEPT[@]} == 0)); then SUMMARY+=("$label installed: ${LAST_INSTALLED[*]}") diff --git a/scripts/setup/machine-setup/lib/tools.sh b/scripts/setup/machine-setup/lib/tools.sh index c49df44d..d12e3fc4 100644 --- a/scripts/setup/machine-setup/lib/tools.sh +++ b/scripts/setup/machine-setup/lib/tools.sh @@ -109,6 +109,7 @@ tools_install() { local tool local -a missing=() present=() + LAST_SKIPPED=() for tool in "$@"; do if tool_is_installed "$tool"; then present+=("$tool"); else missing+=("$tool"); fi done @@ -116,15 +117,8 @@ tools_install() { LAST_INSTALLED=("${missing[@]}") LAST_KEPT=("${present[@]}") - info "$label — installs what is missing, keeps what you already have" - ((${#present[@]})) && echo " already here: ${present[*]}" + announce_plan "$label" present missing || return 0 - if ((${#missing[@]} == 0)); then - echo " to install: nothing, all present" - return 0 - fi - - echo " to install: ${missing[*]}" for tool in "${missing[@]}"; do info " installing ${tool}..." "tool_install_${tool}" diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index 7540532e..165cc7bd 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -75,35 +75,56 @@ fi USER_HOME="/home/$USERNAME" # ============================================================================= -# 2. System Update & Essentials +# 2. System update # ============================================================================= +# +# Its own section because it is the only thing in the script that moves versions +# of software already on the machine. Everything else only ever adds what is +# absent, so this is the one that deserves to be refused on its own. -step "System Update & Essentials" +step "System update" if ! skip; then info "Refreshing the package index..." - pkg_refresh + pkg_refresh >/dev/null - # The one place that deliberately moves versions of things already installed. - # Everything else in this script only ever adds what is absent. - info "Upgrading installed packages..." - pkg_upgrade_all + echo "" + info "System update — upgrades packages already installed to their latest versions" + echo " this is the only step that changes software already on this machine" - # shellcheck disable=SC2046 # word splitting is how the list is passed - pkg_install "Core packages" $(pkgs_core) - - summarise_last "Core packages" - ok "System updated and core packages in place" + if confirm "Proceed?"; then + pkg_upgrade_all + ok "System upgraded" + SUMMARY+=("System packages upgraded") + else + warn "skipped by request" + SUMMARY+=("System update: SKIPPED by request") + fi step_ok fi # ============================================================================= -# 2b. Command-line tools +# 3. Core utils # ============================================================================= # -# Separate from the packages above because they are a different thing: upstream -# binaries on their own release cadence, not anything the distribution ships. -# Lumping them in made a run look like it was installing system packages and -# then start downloading tarballs unannounced. +# What the distribution provides: the six this script would break without, and +# the command-line tools that make a machine worth sitting at. + +step "Core utils" +if ! skip; then + # shellcheck disable=SC2046 # word splitting is how the list is passed + pkg_install "Core utils" $(pkgs_core) + summarise_last "Core utils" + step_ok +fi + +# ============================================================================= +# 4. Command-line tools +# ============================================================================= +# +# A different thing from core utils, and kept apart from them: upstream binaries +# fetched from upstream, on their own release cadence, none of which the +# distribution ships. Lumping them in made a run look like it was installing +# system packages and then start pulling tarballs unannounced. step "Command-line tools" if ! skip; then