#!/bin/sh # Compare the installed packages against the lists for this machine's role. # # packages.sh what is missing and what is extra # packages.sh install install what is missing # # It never removes anything. Extra packages are reported and left alone: on a # server, individual packages carry mail and web services, and a tool that # tidies up on its own is a tool that eventually takes one of them down. The # same caution is why the BookStack upgrade playbook refuses to remove # anything either. # # The lists are data, not code - plain names, one per line. That is deliberate: # they map onto home-manager and environment.systemPackages almost unchanged if # this ever moves to Nix, whereas an install script would not. set -eu ROLE=$(cat "$HOME/.config/dotconfs/role" 2>/dev/null || echo desktop) DIR="$(cd "$(dirname "$0")/../packages" 2>/dev/null && pwd)" || { echo "packages/ not found next to $0" >&2 exit 1 } # Comments and blank lines are for the reader, not the package manager. read_list() { [ -f "$1" ] || return 0 sed -E 's/#.*//' "$1" | tr -d '\r' | awk 'NF' } case "$(uname -s)" in Linux) command -v pacman >/dev/null 2>&1 || { echo "not an Arch machine" >&2; exit 1; } WANT=$(read_list "$DIR/arch-base.txt"; read_list "$DIR/arch-$ROLE.txt" || true) HAVE=$(pacman -Qqe) INSTALL="yay -S --needed --noconfirm" command -v yay >/dev/null 2>&1 || INSTALL="sudo pacman -S --needed" ;; Darwin) command -v brew >/dev/null 2>&1 || { echo "Homebrew not in PATH - is this a login shell?" >&2; exit 1; } WANT=$(read_list "$DIR/brew-$ROLE.txt") HAVE=$(brew leaves) INSTALL="brew install" ;; *) echo "unsupported platform: $(uname -s)" >&2 exit 1 ;; esac # Temporary files rather than process substitution: <(...) is not POSIX, and # /bin/sh is dash on Debian and a POSIX-mode bash on macOS. This script has to # run on both. TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT INT TERM printf '%s\n' "$WANT" | awk 'NF' | sort -u > "$TMP/want" printf '%s\n' "$HAVE" | awk 'NF' | sort -u > "$TMP/have" MISSING=$(comm -23 "$TMP/want" "$TMP/have") EXTRA=$(comm -13 "$TMP/want" "$TMP/have") WANT=$(cat "$TMP/want") HAVE=$(cat "$TMP/have") count() { printf '%s\n' "$1" | awk 'NF' | wc -l | tr -d ' '; } case "${1:-check}" in check) echo "role: $ROLE wanted: $(count "$WANT") installed: $(count "$HAVE")" echo echo "missing ($(count "$MISSING")):" printf '%s\n' "$MISSING" | awk 'NF' | sed 's/^/ /' echo echo "extra, not touched ($(count "$EXTRA")):" printf '%s\n' "$EXTRA" | awk 'NF' | sed 's/^/ /' ;; install) if [ -z "$(printf '%s\n' "$MISSING" | awk 'NF')" ]; then echo "nothing to install" exit 0 fi echo "installing:" printf '%s\n' "$MISSING" | awk 'NF' | sed 's/^/ /' # shellcheck disable=SC2086 $INSTALL $(printf '%s ' $MISSING) ;; *) echo "usage: $0 [check|install]" >&2 exit 1 ;; esac