refactor(mackup): transition to copy-based backup and restore

- Updated `ssr sync` and `ssr restore` commands to use `mackup backup` and `mackup restore`, replacing the previous symlink-based approach.
- Added guidance for users with legacy symlink setups to run `ssr mackup unlink` for recovery.
- Revised documentation in README, completions, and configuration examples to reflect the new copy-based methodology for improved reliability on newer macOS versions.
- Enhanced comments in the codebase to clarify the changes and their implications for users.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-13 09:17:25 +02:00
parent 51fb589ef2
commit c31338dba9
7 changed files with 47 additions and 91 deletions
+27 -71
View File
@@ -5,10 +5,9 @@
# user can swap engines via ~/.mackup.cfg without changing this script.
#
# Mackup 0.10.2 command mapping (importantly different from older versions):
# mackup backup → copy only; NO symlinks created in ~/
# mackup restore → copy only; NO symlinks created in ~/
# mackup link install → copy to cloud + create symlinks in ~/ ← SSR uses this
# mackup link → create symlinks in ~/ (cloud files must exist) ← SSR restore
# mackup backup → copy managed files from ~/ into storage (no symlinks)
# mackup restore → copy from storage back into ~/
# mackup link* → symlink-based flow; unreliable on newer macOS — SSR does not use it
# ---------------------------------------------------------------------------
# Custom Mackup app definition auto-generator
@@ -338,21 +337,16 @@ ssr::mackup::_materialize_icloud() {
find "$mackup_root" -name '*.icloud' -type f -delete 2>/dev/null || true
}
# Run `mackup -f link install` — this is the correct "backup with symlinks"
# command in Mackup 0.10.2. Unlike `mackup backup` (which only copies),
# `link install` moves each managed file to the cloud folder and replaces it
# with a symlink. On subsequent runs it detects already-linked files and
# skips them efficiently.
# Run `mackup -f backup` — copies managed paths from ~/ into the configured
# storage directory (no symlinks). Copy mode avoids macOS issues seen with
# `mackup link` / `link install` on recent OS releases.
#
# Sockets and dangling symlinks are naturally skipped by `link install` because
# Python's os.path.isfile/isdir returns False for those types. We still run a
# light preclean for known transient directories so their contents don't appear
# as stale files in the cloud copy.
# Light preclean removes known transient dirs so junk is not copied to cloud.
#
# We do NOT abort `ssr sync` on persistent failure — brew dump, cleanup, and
# the unmanaged-apps scan still run. Failure is surfaced via ssr::warn.
ssr::mackup::backup() {
ssr::log "mackup link install (backup + symlink)"
ssr::log "mackup backup (copy to cloud storage)"
# Derive mackup root from ~/.mackup.cfg (path + directory).
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
@@ -363,47 +357,11 @@ ssr::mackup::backup() {
fi
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
# ---------------------------------------------------------------------------
# Optional pre-symlink (OFF by default): iTerm2 can recreate
# DynamicProfiles/ so fast that `mackup link install` loses the race.
# Forcing a symlink here can break iTerm if the cloud copy is wrong or
# empty — only enable after you trust the Mackup cloud tree.
#
# export SSR_MACKUP_PRESYMLINK=1 # in ssr.conf or the shell
#
# Format: home-relative path (same as in the Mackup app definition).
# ---------------------------------------------------------------------------
local -a _presymlink=()
case "${SSR_MACKUP_PRESYMLINK:-}" in
1|true|yes|on)
_presymlink=(.config/iterm2/AppSupport/DynamicProfiles)
;;
esac
# ---------------------------------------------------------------------------
# Light preclean — remove transient directories so their ephemeral contents
# (debug symlinks, log files) are not included in the cloud copy.
# `link install` already skips sockets and dangling symlinks via its own
# os.path.isfile/isdir guard, so we only need to handle known directories.
# ---------------------------------------------------------------------------
_ssr_mackup_preclean() {
local mr="$1" # mackup root
# 1. Pre-symlink race-condition paths.
local rel home_p cloud_p
for rel in "${_presymlink[@]}"; do
home_p="$HOME/$rel"
cloud_p="$mr/$rel"
[[ -d "$cloud_p" ]] || continue # no cloud copy yet — skip
if [[ -L "$home_p" ]] && [[ "$(readlink "$home_p")" == "$cloud_p" ]]; then
continue # already correct symlink
fi
ssr::log " pre-symlink: $rel → cloud (prevents iTerm2-style race)"
rm -rf "$home_p" 2>/dev/null || true
ln -s "$cloud_p" "$home_p" 2>/dev/null || true
done
# 2. Transient directories whose contents should never go into cloud.
local -a transient_dirs=(
.claude/debug # Claude Code debug session links
.cursor/logs # Cursor IDE logs
@@ -422,28 +380,28 @@ ssr::mackup::backup() {
local attempt rc=0
for attempt in 1 2 3; do
_ssr_mackup_preclean "$mackup_root"
if mackup -f link install; then
ssr::ok "mackup link install complete — symlinks created in ~/"
_ssr_mackup_preclean
if mackup -f backup; then
ssr::ok "mackup backup complete — configs copied to cloud storage"
return 0
fi
rc=$?
ssr::warn "mackup link install failed (attempt $attempt/3, rc=$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 link install did not complete after 3 attempts (rc=$rc)."
ssr::warn "Sync will continue without it; run 'mackup -f link install' manually,"
ssr::warn "mackup backup did not complete after 3 attempts (rc=$rc)."
ssr::warn "Sync will continue without it; run 'mackup -f backup' manually,"
ssr::warn "or check ~/.mackup.cfg if this persists."
ssr::syslog "mackup link install failed after retries (rc=$rc)"
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 link (restore via symlinks)"
ssr::log "mackup restore (copy from cloud storage to ~/)"
# Pre-materialize iCloud placeholders so mackup's isfile/isdir checks
# see real files rather than .icloud stubs (which read as non-existent).
@@ -467,9 +425,9 @@ ssr::mackup::restore() {
local stub_total
stub_total="$(find "${mackup_root:-/dev/null}" -name '*.icloud' -type f 2>/dev/null | wc -l | tr -d ' ')"
if [[ "$stub_total" -gt 0 ]]; then
ssr::warn "$stub_total file(s) still need downloading — symlinks may be skipped for those."
ssr::warn "$stub_total file(s) still need downloading — restore may skip those paths."
fi
ssr::log "Running mackup link (long pauses normal while iCloud fetches files)…"
ssr::log "Running mackup restore (long pauses normal while iCloud fetches files)…"
# Background watchdog: prints elapsed time every 15s.
local start_ts rc=0 done_flag watchdog_pid
@@ -479,16 +437,14 @@ ssr::mackup::restore() {
while [[ -f "$done_flag" ]]; do
sleep 15
[[ -f "$done_flag" ]] || break
printf ' … mackup link still running (%ds elapsed — waiting for iCloud)\n' \
printf ' … mackup restore still running (%ds elapsed — waiting for iCloud)\n' \
"$(( $(date +%s) - start_ts ))" >&2
done
) &
watchdog_pid=$!
# `mackup link` creates symlinks ~/file → cloud/file for all managed apps.
# Unlike `mackup restore` (which copies), this keeps files in iCloud and
# ensures ongoing sync. -f auto-confirms replacement of existing home files.
mackup -f link || rc=$?
# Copy managed files from storage into ~/. -f auto-confirms overwrites.
mackup -f restore || rc=$?
rm -f "$done_flag"
kill "$watchdog_pid" 2>/dev/null || true
@@ -496,16 +452,16 @@ ssr::mackup::restore() {
local elapsed=$(( $(date +%s) - start_ts ))
if [[ $rc -ne 0 ]]; then
ssr::warn "mackup link finished with errors (rc=$rc, ${elapsed}s elapsed)"
ssr::warn "mackup restore finished with errors (rc=$rc, ${elapsed}s elapsed)"
else
ssr::ok "mackup link completed in ${elapsed}s — symlinks created in ~/"
ssr::ok "mackup restore completed in ${elapsed}s — configs copied to ~/"
fi
}
# Reverse `mackup link` / `link install`: copy each managed path from the
# Mackup storage tree back into $HOME and remove the symlinks. Use when apps
# (e.g. iTerm2) misbehave after symlink-based sync or iCloud produced a bad
# tree. Invoked by `ssr mackup unlink` (interactive confirm).
# Undo a previous `mackup link` / `link install` (symlinks → real files in ~/).
# SSR now uses copy-based backup/restore; keep this for machines that still
# have symlink-based Mackup from older SSR or manual `mackup link` use.
# Invoked by `ssr mackup unlink` (interactive confirm).
ssr::mackup::link_uninstall() {
ssr::mackup::ensure_installed
ssr::log "mackup link uninstall (revert symlinks → local files)"