#!/bin/bash # Set up this account on a machine: the tools, checkouts and nvim plugins that # the config link.sh links expects. Run it by hand, after `./link.sh yes`, once # per machine and again whenever the Brewfile changes; every step only adds # what is missing. Unlike link.sh it is slow and may stop to ask for a password, # so nothing runs it on a timer. # # ~/src/soarez/dot/install.sh # # On a new Mac, also run ./macos-defaults.sh, once -- see its header. A private # layer's bootstrap may call both for you. # # Runs under macOS's /bin/bash 3.2. set -euo pipefail dotfiles_dir="$(cd "$(dirname "$0")" && pwd)" # 1. Homebrew packages, on macOS. Linux machines get their packages from # whatever provisions them, not from here. if [[ "$(uname)" == "Darwin" ]]; then if ! command -v brew >/dev/null 2>&1; then for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do if [[ -x "${b}" ]]; then eval "$("${b}" shellenv)" break fi done fi # Install Homebrew rather than print the command and stop: on a machine whose # whole setup is this repo there is nothing else to run it, and everything # below depends on it. NONINTERACTIVE so it does not wait on a RETURN it will # never see; it still asks for the sudo password, which is why this script is # run by hand and not on a timer. if ! command -v brew >/dev/null 2>&1; then echo "Homebrew is not installed; installing it now" NONINTERACTIVE=1 /bin/bash -c \ "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do if [[ -x "${b}" ]]; then eval "$("${b}" shellenv)" break fi done fi if ! command -v brew >/dev/null 2>&1; then >&2 echo "ERROR: Homebrew still not on PATH after installing it. Install it" >&2 echo "by hand, then re-run this script:" # shellcheck disable=SC2016 # printed for a human to paste, not expanded >&2 echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' exit 1 fi # One cask in the Brewfile comes from its author's own tap. Homebrew gates # unofficial taps behind `brew trust`, recorded per machine in # $XDG_CONFIG_HOME/homebrew/trust.json, so it cannot live in the Brewfile. # Measured on 2026-10-01 rather than assumed: the gate refused an untrusted # *formula* outright, while this cask installed through brew bundle without # it. So this is a precaution that keeps working if the gate tightens, not a # step the install depends on today. Naming the cask rather than the tap # keeps the trust to this one file and not whatever the tap adds later. Older # Homebrews have no `trust` subcommand and need nothing, hence the guard. if brew help trust >/dev/null 2>&1; then brew trust --cask b451c/quickmd/quickmd >/dev/null || \ >&2 echo "WARNING: could not trust b451c/quickmd/quickmd; brew bundle will skip it" fi # --no-upgrade: install what is missing and leave what is there alone. Plain # `brew bundle` also upgrades every listed package that is out of date. # # Not fatal: brew bundle fails the whole run if a single entry will not # install, and on a managed machine that is routine -- a cask can be blocked # by policy ("has been disabled because it does not pass the macOS Gatekeeper # check"). One unavailable app is no reason to leave the checkouts, nvim # plugins and colours undone, so this warns and carries on. brew has already # said which entry failed, right above. if ! brew bundle --no-upgrade --file="${dotfiles_dir}/Brewfile"; then >&2 echo >&2 echo "WARNING: some Brewfile entries did not install (see brew's output" >&2 echo "above). Continuing with the rest of the setup." >&2 echo fi # Clearing a quarantine flag puts an app past Gatekeeper, so here it happens # only under one rule: the file is pinned by a hash reviewed in this repo, # its code signature is intact -- nothing changed since it was signed -- and # the app is named explicitly, never matched by a loop. Both checks run # below; failing either leaves the app uninstalled. A Developer ID is not # required, which is the point: an ad-hoc signed app can still be exactly # the bytes that were reviewed. What the rule refuses is an app that cannot # be pinned or whose bundle is altered -- toggleMute, dropped from the # Brewfile, failed it on the signature. # # Alacritty: Homebrew disabled its cask on 2026-09-01 because upstream builds # are ad-hoc signed and not notarized. The hash is hard-coded rather than # looked up from the release: a checksum fetched from the same place as the # file catches a corrupt download, not a swapped one. Bump both together, by # hand, after checking the new hash against GitHub's published asset digest. # Fresh installs only; an existing Alacritty is left alone. if [[ ! -d /Applications/Alacritty.app ]] then # Every step exits on failure by hand. `set -e` would look like it does # the same, but bash ignores it inside anything on the left of `||` -- # this subshell included -- and with it every check below became advisory: # a wrong hash still installed the app, which is how this was found. ( version="0.17.0" sha256="ad8d7de35fb38e43184776cac6dfee05ca325caa0b6639a06a55e54e4b026620" dmg="$(mktemp -d)/Alacritty-v${version}.dmg" || exit 1 curl -fsSL -o "$dmg" \ "https://github.com/alacritty/alacritty/releases/download/v${version}/Alacritty-v${version}.dmg" \ || exit 1 echo "${sha256} ${dmg}" | shasum -a 256 -c - >/dev/null || exit 1 mnt="$(hdiutil attach -nobrowse -readonly "$dmg" | awk -F'\t' '/\/Volumes\//{print $NF}')" [[ -n "$mnt" ]] || exit 1 trap 'hdiutil detach -quiet "$mnt" 2>/dev/null || true' EXIT codesign --verify --deep --strict "$mnt/Alacritty.app" || exit 1 cp -R "$mnt/Alacritty.app" /Applications/ || exit 1 xattr -dr com.apple.quarantine /Applications/Alacritty.app || exit 1 echo "INFO: installed $(/Applications/Alacritty.app/Contents/MacOS/alacritty --version)" ) || >&2 echo "WARNING: Alacritty not installed: download, hash or signature check failed" fi fi # 2. Checkouts the config reads: the pure prompt (config/zshrc falls back to a # plain one without it) and the extra colour schemes bin/theme lists. Over # https: both are public, so a fresh machine needs no ssh key for them. # Existing checkouts are left alone. clone() { local url="$1" dest="$2" if [[ -d "${dest}/.git" ]]; then return fi mkdir -p "$(dirname "${dest}")" git clone --quiet "${url}" "${dest}" echo "cloned ${dest}" } clone https://github.com/sindresorhus/pure.git "${HOME}/src/sindresorhus/pure" clone https://github.com/alacritty/alacritty-theme.git "${HOME}/src/alacritty/alacritty-theme" # 3. nvim's plugins, so the first real launch does not stop to install them. # `install`, not `sync`: install only fetches plugins that are missing, and # leaves installed ones where this host's lock file (~/.config/nvim/ # lazy-lock.json, not in the repo) has them. sync would also update every # plugin to its latest version and rewrite that lock file. if command -v nvim >/dev/null 2>&1; then nvim --headless "+Lazy! install" +qa || true fi # 4. The colours the terminals, tmux, nvim and bat read. bin/theme writes those # files under $XDG_DATA_HOME; until it has run once they are absent, which # every config tolerates -- nothing is themed, that is all. Seeded here only # when this machine has none, with whatever mode and colourscheme `theme` # reports (its own defaults on a fresh machine), so re-running this script # never disturbs a machine whose theme is already set. theme_bin="${dotfiles_dir}/bin/theme" if [[ ! -e "${XDG_DATA_HOME:-${HOME}/.local/share}/tmux/color.conf" && -x "${theme_bin}" ]]; then "${theme_bin}" --set "$("${theme_bin}" --mode)" "$("${theme_bin}" --colors)" || true fi if [[ "$(uname)" == "Darwin" ]]; then echo echo "Setting up a new Mac? Also run this, once:" echo " ${dotfiles_dir}/macos-defaults.sh" echo "It applies the baseline Finder, Dock and keyboard settings, and resets the Dock." fi