Files
Oliver Surke 1d18dd6a48 fix: prevent brew bundle hanging on large cask installs
HOMEBREW_NO_QUARANTINE=1 — skips the post-install xattr quarantine walk
  that Gatekeeper runs on every binary in the app bundle. For large apps
  like WhatsApp (237 MB, many nested binaries) this walk blocks for several
  minutes. Apps will show the standard one-time Gatekeeper dialog instead.

HOMEBREW_NO_INSTALL_CLEANUP=1 — skips per-package cache cleanup after each
  install. A single brew cleanup already runs in ssr-sync afterwards.

mdimport — now runs in background (&) so Spotlight indexing never blocks
  the restore flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:52:24 +02:00

175 lines
6.5 KiB
Bash

#!/usr/bin/env bash
# lib/brew.sh — Homebrew install + Brewfile dump/restore.
ssr::brew::ensure_installed() {
if command -v brew >/dev/null 2>&1; then
return 0
fi
# Homebrew's install script requires sudo on macOS to write to /opt/homebrew
# (Apple Silicon) or /usr/local (Intel). Check before running it so we can
# give a clear error instead of a cryptic failure.
if ! sudo -n true 2>/dev/null; then
ssr::warn "Homebrew is not installed and sudo access is required to install it."
ssr::warn "Please run the following as an admin user and re-run ssr restore:"
ssr::warn " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
ssr::die "Cannot install Homebrew without sudo access."
fi
ssr::log "Homebrew not found — installing"
# NONINTERACTIVE=1 skips the confirmation prompt; sudo is available (checked above).
NONINTERACTIVE=1 /bin/bash -c \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Ensure brew is on PATH for both Apple Silicon and Intel layouts.
if [[ -x /opt/homebrew/bin/brew ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -x /usr/local/bin/brew ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
command -v brew >/dev/null 2>&1 || ssr::die "brew still not on PATH after install"
}
ssr::brew::update() {
ssr::log "brew update"
brew update --quiet
}
ssr::brew::upgrade() {
[[ "${SSR_BREW_UPGRADE:-true}" == "true" ]] || return 0
ssr::log "brew upgrade"
brew upgrade --quiet || ssr::warn "brew upgrade reported errors (continuing)"
}
ssr::brew::cleanup() {
[[ "${SSR_BREW_CLEANUP:-true}" == "true" ]] || return 0
ssr::log "brew cleanup"
brew cleanup --quiet || true
}
# Homebrew taps that have been deprecated and emptied upstream. Keeping them
# in the Brewfile causes "Tapping X has failed!" errors on every restore.
# brew bundle dump still emits them if they are locally present, so we strip
# them from the file after dumping.
_SSR_DEPRECATED_TAPS=(
homebrew/core
homebrew/cask
homebrew/services
homebrew/test-bot
homebrew/bundle
homebrew/cask-fonts
homebrew/cask-drivers
homebrew/cask-versions
)
_ssr_brew_strip_deprecated_taps() {
local file="$1"
local pattern
# Build an alternation pattern: homebrew/core|homebrew/cask|…
pattern="$(printf '%s|' "${_SSR_DEPRECATED_TAPS[@]}")"
pattern="${pattern%|}" # strip trailing |
local tmp; tmp="$(mktemp)"
# Match lines like: tap "homebrew/services"
grep -vE "^tap \"(${pattern})\"" "$file" > "$tmp" || true
mv -f "$tmp" "$file"
}
# Dumps Brewfile to the cloud backup dir with a timestamped rotation copy.
ssr::brew::dump() {
local backup_dir="$1" brewfile="$1/Brewfile"
ssr::ensure_dir "$backup_dir"
if [[ -f "$brewfile" ]]; then
cp -f "$brewfile" "$brewfile.bak"
fi
ssr::log "brew bundle dump → $brewfile"
(
cd "$backup_dir"
brew bundle dump --describe --quiet --force --file=Brewfile
)
# Remove deprecated taps that would fail on restore.
local removed
removed="$(grep -cE "^tap \"($(printf '%s|' "${_SSR_DEPRECATED_TAPS[@]}" | sed 's/|$//'))\"" \
"$brewfile" 2>/dev/null || echo 0)"
_ssr_brew_strip_deprecated_taps "$brewfile"
[[ "$removed" -gt 0 ]] && ssr::log "Stripped $removed deprecated tap(s) from Brewfile."
ssr::ok "Brewfile written ($(wc -l < "$brewfile" | tr -d ' ') lines)"
}
# Pre-authenticate sudo once and keep credentials alive in the background.
# macOS caches sudo credentials for 5 minutes (300s); the keepalive loop
# refreshes every 60s so Homebrew cask installers never hit a timeout mid-run.
#
# Usage:
# _ssr_sudo_keepalive_start # prompts once, starts background loop
# … long-running commands …
# _ssr_sudo_keepalive_stop # kills the loop
_SSR_SUDO_KEEPALIVE_PID=""
_ssr_sudo_keepalive_start() {
# Check whether sudo is available at all (may already be cached).
if ! sudo -v 2>/dev/null; then
ssr::warn "sudo authentication failed — cask installs may prompt per-package."
return 0
fi
ssr::ok "sudo credentials cached — Homebrew casks will not prompt during install."
# Background loop: re-validate every 60s to prevent 5-minute timeout.
(
while true; do
sleep 60
sudo -n true 2>/dev/null || break
done
) &
_SSR_SUDO_KEEPALIVE_PID=$!
}
_ssr_sudo_keepalive_stop() {
if [[ -n "$_SSR_SUDO_KEEPALIVE_PID" ]]; then
kill "$_SSR_SUDO_KEEPALIVE_PID" 2>/dev/null || true
wait "$_SSR_SUDO_KEEPALIVE_PID" 2>/dev/null || true
_SSR_SUDO_KEEPALIVE_PID=""
fi
}
ssr::brew::restore() {
local brewfile="$1"
[[ -f "$brewfile" ]] || ssr::die "Brewfile not found: $brewfile"
ssr::log "brew bundle install ← $brewfile"
# Authenticate sudo once up front so Homebrew cask installers don't
# prompt individually for each package that needs admin privileges.
_ssr_sudo_keepalive_start
# HOMEBREW_NO_QUARANTINE=1: skip the post-install `xattr -d com.apple.quarantine`
# pass that Homebrew runs on every cask. That xattr walk can block for
# several minutes on large apps (WhatsApp, Electron apps, …) because macOS
# Gatekeeper verifies every binary in the bundle. Apps will show a standard
# one-time Gatekeeper dialog on first launch instead — normal macOS behaviour.
#
# HOMEBREW_NO_INSTALL_CLEANUP=1: skip per-package cache cleanup after each
# install. A single `brew cleanup` runs in ssr-sync after the full bundle.
#
# MAS_NO_AUTO_INDEX=1: suppress per-app mdimport after every App Store app.
# One batch mdimport runs in the background below instead.
local rc=0
HOMEBREW_NO_QUARANTINE=1 \
HOMEBREW_NO_INSTALL_CLEANUP=1 \
MAS_NO_AUTO_INDEX=1 \
brew bundle install --file="$brewfile" --verbose || rc=$?
_ssr_sudo_keepalive_stop
# Trigger Spotlight indexing in the background so it never blocks the restore.
ssr::log "Triggering background Spotlight index of /Applications"
mdimport /Applications &>/dev/null &
[[ -d "$HOME/Applications" ]] && mdimport "$HOME/Applications" &>/dev/null &
if [[ $rc -ne 0 ]]; then
ssr::warn "brew bundle install finished with errors (rc=$rc) — some packages were not installed."
ssr::warn "Rerun manually: brew bundle install --file=\"$brewfile\""
else
ssr::ok "brew bundle install completed successfully."
fi
}