The summary claimed credit for everything in a section's list, including the packages it had just decided to leave alone — so a run that installed nothing still ended with "Command-line tools: lazydocker lazygit starship fastfetch". The announce above it said "nothing, all present" in the same breath. pkg_install and tools_install now record LAST_INSTALLED and LAST_KEPT, and summarise_last turns those into one honest line: Core packages installed: btop tmux (17 already present) Command-line tools: already present, nothing installed Verified all three shapes — everything present, nothing present, and mixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
189 lines
7.4 KiB
Bash
189 lines
7.4 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# machine-setup — distro packages
|
|
# =============================================================================
|
|
#
|
|
# Definitions only, like lib/base.sh. Sourcing this installs nothing.
|
|
#
|
|
# ── The rule: install what is missing, never touch what is there ──
|
|
#
|
|
# `apt-get install <present-package>` is NOT a no-op — it upgrades the package if
|
|
# the repository has a newer one. On a machine somebody already uses, that can
|
|
# move a version they chose deliberately, and the setup script is the last thing
|
|
# that should be doing that behind their back.
|
|
#
|
|
# So every install here goes through pkg_install, which queries the package
|
|
# database first, installs only the subset that is genuinely absent, and prints
|
|
# both lists before doing it. A package already present is never named on a
|
|
# command line at all.
|
|
#
|
|
# ── Why per-package-manager lists rather than a translation table ──
|
|
#
|
|
# The names disagree across distributions (build-essential/base-devel/fd/fd-find)
|
|
# and some packages are not a package elsewhere at all: apt-transport-https,
|
|
# lsb-release and software-properties-common are apt concepts. A canonical-name
|
|
# table with per-manager overrides hides both of those behind indirection. A
|
|
# plain `case $PM` says what each system actually gets, in one place, and matches
|
|
# the shape scripts/setup-old/setup.sh already used.
|
|
|
|
[[ -n "${MACHINE_SETUP_PACKAGES_LOADED:-}" ]] && return 0
|
|
MACHINE_SETUP_PACKAGES_LOADED=1
|
|
|
|
# What the last pkg_install/tools_install actually put on the machine, as opposed
|
|
# to what it was asked for. Read by the caller to write an honest summary line:
|
|
# without it every section reports its whole list as installed, including the
|
|
# packages it deliberately left alone.
|
|
LAST_INSTALLED=()
|
|
LAST_KEPT=()
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# The sections
|
|
# -----------------------------------------------------------------------------
|
|
|
|
# Core: what this script itself would break without, plus the command-line tools
|
|
# that make a machine worth sitting at.
|
|
#
|
|
# The first six are load-bearing and each is used by a later step — curl fetches
|
|
# in nine of them, jq parses the lazygit release API, unzip opens ssh-keys.zip,
|
|
# gnupg dearmors the Docker keyring, git clones the Neovim config, and
|
|
# ca-certificates is what makes any of the fetching work. The rest are the
|
|
# environment: nothing calls them, they are here because a box you use should
|
|
# have them.
|
|
#
|
|
# build-essential is the one entry with any reach beyond itself: it is a
|
|
# meta-package (gcc, g++, make, libc6-dev, dpkg-dev), so on a machine where a
|
|
# specific gcc was pinned it pulls the distribution's default alongside it. It
|
|
# stays in core because anything that compiles a native module needs it, but it
|
|
# is the one to move out first if that ever bites.
|
|
pkgs_core() {
|
|
case "$PM" in
|
|
apt)
|
|
# apt-transport-https, lsb-release and software-properties-common are not
|
|
# tools — they are what lets later steps add the Docker repository and the
|
|
# fastfetch PPA. They have no counterpart on the other systems.
|
|
echo curl ca-certificates gnupg git jq unzip \
|
|
apt-transport-https lsb-release software-properties-common \
|
|
wget zip build-essential btop htop tree tmux ripgrep fd-find net-tools
|
|
;;
|
|
pacman)
|
|
echo curl ca-certificates gnupg git jq unzip \
|
|
wget zip base-devel btop htop tree tmux ripgrep fd net-tools
|
|
;;
|
|
dnf)
|
|
echo curl ca-certificates gnupg2 git jq unzip \
|
|
wget zip btop htop tree tmux ripgrep fd-find net-tools
|
|
;;
|
|
brew)
|
|
# curl, unzip and the TLS roots ship with macOS; the compilers come from
|
|
# the Xcode command line tools, which is not a formula.
|
|
echo gnupg git jq wget btop htop tree tmux ripgrep fd
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Querying
|
|
# -----------------------------------------------------------------------------
|
|
|
|
# Is this package installed right now?
|
|
#
|
|
# dpkg-query on the status field rather than `dpkg -s`, which also succeeds for a
|
|
# package that was removed but left its config behind — that state would be read
|
|
# as "present" and the package would never be reinstalled.
|
|
pkg_is_installed() {
|
|
case "$PM" in
|
|
apt) [[ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" == "installed" ]] ;;
|
|
pacman) pacman -Qi "$1" &>/dev/null ;;
|
|
dnf) rpm -q "$1" &>/dev/null ;;
|
|
brew) brew list --formula "$1" &>/dev/null ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Acting
|
|
# -----------------------------------------------------------------------------
|
|
|
|
# Refresh the package index.
|
|
#
|
|
# DEBIAN_FRONTEND stops debconf opening a dialog on a machine with no terminal to
|
|
# draw it on, and NEEDRESTART_MODE=a stops needrestart — on by default since
|
|
# Ubuntu 22.04 — interrupting to ask which services to restart. Both belong here
|
|
# rather than at each call site, because forgetting one turns an unattended run
|
|
# into one that is silently waiting for a keypress.
|
|
pkg_refresh() {
|
|
case "$PM" in
|
|
apt) DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -y ;;
|
|
pacman) pacman -Sy --noconfirm ;;
|
|
dnf) dnf makecache ;;
|
|
brew) brew update ;;
|
|
esac
|
|
}
|
|
|
|
# Upgrade everything already installed. Separate from pkg_install on purpose:
|
|
# this one DOES move versions, so it is a deliberate step rather than something
|
|
# that happens as a side effect of installing a tool.
|
|
pkg_upgrade_all() {
|
|
case "$PM" in
|
|
apt) DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get upgrade -y ;;
|
|
pacman) pacman -Su --noconfirm ;;
|
|
dnf) dnf upgrade -y ;;
|
|
brew) brew upgrade ;;
|
|
esac
|
|
}
|
|
|
|
# The raw install, with no presence check. Use pkg_install instead.
|
|
pkg_install_now() {
|
|
case "$PM" in
|
|
apt) DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y "$@" ;;
|
|
pacman) pacman -S --noconfirm --needed "$@" ;;
|
|
dnf) dnf install -y "$@" ;;
|
|
brew) brew install "$@" ;;
|
|
esac
|
|
}
|
|
|
|
# Announce a section, then install only what is absent from it.
|
|
#
|
|
# pkg_install "Core packages" $(pkgs_core)
|
|
#
|
|
# Prints both lists before touching anything, so the run says what it is about to
|
|
# do to this machine and what it is deliberately leaving alone. Returns 0 when
|
|
# there was nothing to do.
|
|
pkg_install() {
|
|
local label="$1"
|
|
shift
|
|
|
|
local pkg
|
|
local -a missing=() present=()
|
|
for pkg in "$@"; do
|
|
if pkg_is_installed "$pkg"; then present+=("$pkg"); else missing+=("$pkg"); fi
|
|
done
|
|
|
|
LAST_INSTALLED=("${missing[@]}")
|
|
LAST_KEPT=("${present[@]}")
|
|
|
|
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
|
|
fi
|
|
|
|
echo " to install: ${missing[*]}"
|
|
pkg_install_now "${missing[@]}"
|
|
}
|
|
|
|
# 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
|
|
SUMMARY+=("$label: already present, nothing installed")
|
|
elif ((${#LAST_KEPT[@]} == 0)); then
|
|
SUMMARY+=("$label installed: ${LAST_INSTALLED[*]}")
|
|
else
|
|
SUMMARY+=("$label installed: ${LAST_INSTALLED[*]} (${#LAST_KEPT[@]} already present)")
|
|
fi
|
|
}
|