1d18dd6a48
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>
175 lines
6.5 KiB
Bash
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
|
|
}
|