all repos — dot @ 95056eacdff97a710129bbb29e707ab83f5faa72

Shell, editor and terminal configuration, and the tools that go with them

install.sh (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
#!/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