#!/usr/bin/env bash # lib/mackup.sh — Mackup install + backup/restore. # Note: upstream mackup is in low-maintenance mode; we keep it because it # remains the simplest way to sync dotfiles for a wide range of apps. The # user can swap engines via ~/.mackup.cfg without changing this script. ssr::mackup::ensure_installed() { if command -v mackup >/dev/null 2>&1; then return 0 fi ssr::log "Installing mackup via brew" brew install mackup } # Writes ~/.mackup.cfg pointing the engine at the cloud directory. # Idempotent: rewrites only if content differs. ssr::mackup::configure() { local mackup_dir="$1" local cfg="$HOME/.mackup.cfg" local desired desired="$(printf '[storage]\nengine = file_system\npath = %s\ndirectory = mackup\n' "$mackup_dir")" if [[ -f "$cfg" ]] && [[ "$(cat "$cfg")" == "$desired" ]]; then return 0 fi if [[ -L "$cfg" || -f "$cfg" ]]; then cp -f "$cfg" "$cfg.bak.$(date +%s)" 2>/dev/null || true rm -f "$cfg" fi printf '%s\n' "$desired" > "$cfg" ssr::ok "Wrote $cfg" } # Force iCloud Drive to materialize placeholder (.icloud) files under the # Mackup storage directory before mackup walks them. The most common ssr sync # crash is `OSError [Errno 66] Directory not empty` raised by shutil.rmtree # inside mackup when iCloud lazily re-creates dataless placeholders between # listdir() and rmdir(). Pre-downloading sidesteps the race. ssr::mackup::_materialize_icloud() { local mackup_root="$1" [[ -d "$mackup_root" ]] || return 0 case "$mackup_root" in *Mobile\ Documents*) ;; # only worth doing for iCloud *) return 0 ;; esac # `brctl download` was removed in macOS Sonoma (remaining brctl subcommands: # diagnose/log/dump/status/accounts/quota/monitor). The current way to # trigger per-file iCloud download from the CLI is `open -g `, # which asks the iCloud daemon to fetch the file in the background. find "$mackup_root" -name '*.icloud' -type f 2>/dev/null | while IFS= read -r stub; do local dir base real dir="$(dirname "$stub")" base="$(basename "$stub")" real="${base#.}" # strip leading dot real="${real%.icloud}" # strip .icloud suffix open -g "$dir/$real" 2>/dev/null || true done # Brief pause so the iCloud daemon can start fetching before mackup reads. sleep 1 # Belt-and-braces: prune any stubs that couldn't be resolved. find "$mackup_root" -name '*.icloud' -type f -delete 2>/dev/null || true } # Try `mackup -f backup` with a small retry budget. iCloud-induced races are # transient: a 2-second sleep is usually enough for the provider to settle. # We deliberately do NOT abort `ssr sync` on persistent failure — the user # still benefits from brew dump, cleanup, and the unmanaged-apps scan that # run afterwards. The failure is surfaced loudly via ssr::warn and syslog. ssr::mackup::backup() { ssr::log "mackup backup" # Derive mackup root from ~/.mackup.cfg (path + directory). local cfg="$HOME/.mackup.cfg" mackup_root="" path dir if [[ -f "$cfg" ]]; then path="$(awk -F= '/^path/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")" dir="$( awk -F= '/^directory/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")" [[ -n "$path" && -n "$dir" ]] && mackup_root="$path/$dir" fi [[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root" local attempt rc=0 for attempt in 1 2 3; do if mackup -f backup; then return 0 fi rc=$? ssr::warn "mackup backup failed (attempt $attempt/3, rc=$rc)" if [[ $attempt -lt 3 ]]; then [[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root" sleep 2 fi done ssr::warn "mackup backup did not complete after 3 attempts (rc=$rc)." ssr::warn "Sync will continue without it; rerun 'mackup -f backup' manually," ssr::warn "or switch ~/.mackup.cfg to a non-iCloud storage path if this persists." ssr::syslog "mackup backup failed after retries (rc=$rc)" return 0 # soft-fail so the rest of ssr sync still runs } ssr::mackup::restore() { ssr::log "mackup restore" # -f: answer "yes" to all prompts (consistent with backup behaviour). mackup -f restore }