fix: use mackup link install/link instead of backup/restore

In Mackup 0.10.2, `mackup backup` only copies files to cloud — it never
creates symlinks.  Symlinks require `mackup link install` (sync) or
`mackup link` (restore).  This is why no symlinks appeared in ~/ after
running ssr sync.

Changes:
- ssr::mackup::backup()  → now calls `mackup -f link install`
  Moves managed files to cloud and creates symlinks in ~/
  Naturally skips sockets and dangling symlinks via isfile/isdir guard,
  so the pre-cleanup is now a light transient-dir purge only.

- ssr::mackup::restore() → now calls `mackup -f link`
  Creates symlinks ~/file → cloud/file (files must already be in cloud).
  Keeps files in iCloud for ongoing sync, unlike the copy-based restore.

- Added Mackup 0.10.2 command-mapping comment to file header.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-12 15:37:43 +02:00
parent 61091aa6bf
commit 953c213ded
+48 -49
View File
@@ -3,6 +3,12 @@
# 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.
#
# 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
# ---------------------------------------------------------------------------
# Custom Mackup app definition auto-generator
@@ -292,13 +298,21 @@ ssr::mackup::_materialize_icloud() {
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.
# 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.
#
# 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.
#
# 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 backup"
ssr::log "mackup link install (backup + symlink)"
# Derive mackup root from ~/.mackup.cfg (path + directory).
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
@@ -310,14 +324,12 @@ ssr::mackup::backup() {
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
# ---------------------------------------------------------------------------
# Pre-backup cleanup — remove artefacts that shutil.copytree cannot handle.
# Run this immediately before each mackup attempt so that apps (e.g. Claude
# Code) that recreate debug symlinks very quickly don't sneak back in.
# 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() {
# Known transient subdirectories that apps recreate constantly.
# These contain debug logs / sockets, never real config.
# Extend via SSR_MACKUP_PRECLEAN_PATHS (space-separated $HOME-relative).
local -a transient_dirs=(
.claude/debug # Claude Code debug session links
.cursor/logs # Cursor IDE logs
@@ -325,55 +337,42 @@ ssr::mackup::backup() {
.vscode-insiders/logs
.npm/_logs # npm debug logs
)
local extra dir
local extra d
for extra in ${SSR_MACKUP_PRECLEAN_PATHS:-}; do
transient_dirs+=("$extra")
done
for dir in "${transient_dirs[@]}"; do
[[ -e "$HOME/$dir" ]] && rm -rf "$HOME/$dir" 2>/dev/null || true
for d in "${transient_dirs[@]}"; do
[[ -e "$HOME/$d" ]] && rm -rf "${HOME:?}/$d" 2>/dev/null || true
done
# 1. Unix socket files (-type s) — [Errno 102] Operation not supported.
local n; n="$(find "$HOME" -type s 2>/dev/null | wc -l | tr -d ' ')"
if [[ "$n" -gt 0 ]]; then
ssr::log " pre-clean: removing $n socket file(s)"
find "$HOME" -type s -delete 2>/dev/null || true
fi
# 2. Dangling symlinks (-type l ! -e) — [Errno 2] No such file.
n="$(find "$HOME" -type l ! -e 2>/dev/null | wc -l | tr -d ' ')"
if [[ "$n" -gt 0 ]]; then
ssr::log " pre-clean: removing $n dangling symlink(s)"
find "$HOME" -type l ! -e -delete 2>/dev/null || true
fi
}
local attempt rc=0
for attempt in 1 2 3; do
_ssr_mackup_preclean # clean immediately before each attempt
if mackup -f backup; then
_ssr_mackup_preclean
if mackup -f link install; then
ssr::ok "mackup link install complete — symlinks created in ~/"
return 0
fi
rc=$?
ssr::warn "mackup backup failed (attempt $attempt/3, rc=$rc)"
ssr::warn "mackup link install 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)"
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 "or check ~/.mackup.cfg if this persists."
ssr::syslog "mackup link install failed after retries (rc=$rc)"
return 0 # soft-fail so the rest of ssr sync still runs
}
ssr::mackup::restore() {
ssr::log "mackup restore"
ssr::log "mackup link (restore via symlinks)"
# Pre-materialize iCloud placeholders so mackup doesn't have to wait for
# each file to download one-by-one during the restore walk.
# Pre-materialize iCloud placeholders so mackup's isfile/isdir checks
# see real files rather than .icloud stubs (which read as non-existent).
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
if [[ -f "$cfg" ]]; then
path="$(awk -F= '/^path/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
@@ -383,7 +382,6 @@ ssr::mackup::restore() {
if [[ -n "$mackup_root" ]]; then
ssr::log "Materializing iCloud files in $mackup_root (may take a moment)…"
ssr::mackup::_materialize_icloud "$mackup_root"
# Count remaining stubs; if there are many, give iCloud more time.
local stub_count
stub_count="$(find "$mackup_root" -name '*.icloud' -type f 2>/dev/null | wc -l | tr -d ' ')"
if [[ "$stub_count" -gt 0 ]]; then
@@ -395,12 +393,11 @@ 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 from iCloud — restore may pause between files."
ssr::warn "$stub_total file(s) still need downloading — symlinks may be skipped for those."
fi
ssr::log "Running mackup restore (long pauses are normal while iCloud fetches files)…"
ssr::log "Running mackup link (long pauses normal while iCloud fetches files)…"
# Background watchdog: prints elapsed time every 15s so the user knows the
# process is still alive during iCloud download pauses.
# Background watchdog: prints elapsed time every 15s.
local start_ts rc=0 done_flag watchdog_pid
start_ts="$(date +%s)"
done_flag="$(mktemp)"
@@ -408,14 +405,16 @@ ssr::mackup::restore() {
while [[ -f "$done_flag" ]]; do
sleep 15
[[ -f "$done_flag" ]] || break
printf ' … mackup restore still running (%ds elapsed — waiting for iCloud)\n' \
printf ' … mackup link still running (%ds elapsed — waiting for iCloud)\n' \
"$(( $(date +%s) - start_ts ))" >&2
done
) &
watchdog_pid=$!
# -f: answer "yes" to all prompts (consistent with backup behaviour).
mackup -f restore || rc=$?
# `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=$?
rm -f "$done_flag"
kill "$watchdog_pid" 2>/dev/null || true
@@ -423,8 +422,8 @@ ssr::mackup::restore() {
local elapsed=$(( $(date +%s) - start_ts ))
if [[ $rc -ne 0 ]]; then
ssr::warn "mackup restore finished with errors (rc=$rc, ${elapsed}s elapsed)"
ssr::warn "mackup link finished with errors (rc=$rc, ${elapsed}s elapsed)"
else
ssr::ok "mackup restore completed in ${elapsed}s"
ssr::ok "mackup link completed in ${elapsed}s — symlinks created in ~/"
fi
}