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:
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
Reproducible macOS setup in one command. `ssr` snapshots your Homebrew bundle and application configs to a cloud drive on a schedule, and can rebuild a fresh Mac from that snapshot.
|
Reproducible macOS setup in one command. `ssr` snapshots your Homebrew bundle and application configs to a cloud drive on a schedule, and can rebuild a fresh Mac from that snapshot.
|
||||||
|
|
||||||
- **Backup** (`ssr sync`): dumps `Brewfile` and runs `mackup link install` to the configured cloud directory (symlink-based).
|
- **Backup** (`ssr sync`): dumps `Brewfile` and runs `mackup backup` to the configured cloud directory (copy-based; no symlinks).
|
||||||
- **Restore** (`ssr restore`): installs Homebrew, replays the `Brewfile`, runs `mackup link`, applies macOS defaults.
|
- **Restore** (`ssr restore`): installs Homebrew, replays the `Brewfile`, runs `mackup restore`, applies macOS defaults.
|
||||||
|
- **Legacy Mackup symlinks**: if a previous install used `mackup link`, run `ssr mackup unlink` once before relying on copy-based sync.
|
||||||
- **Scheduled sync**: native macOS `launchd` agent (daily by default).
|
- **Scheduled sync**: native macOS `launchd` agent (daily by default).
|
||||||
- **Cloud storage**: iCloud Drive, Dropbox, Google Drive, OneDrive, or any custom path — selectable via config.
|
- **Cloud storage**: iCloud Drive, Dropbox, Google Drive, OneDrive, or any custom path — selectable via config.
|
||||||
- **No secrets**: all state lives in your cloud drive; the repo only contains code and config templates.
|
- **No secrets**: all state lives in your cloud drive; the repo only contains code and config templates.
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ Usage:
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
install First-time install: links binary, seeds config, schedules sync.
|
install First-time install: links binary, seeds config, schedules sync.
|
||||||
sync Run a backup: brew bundle dump + mackup link install → cloud.
|
sync Run a backup: brew bundle dump + mackup backup → cloud.
|
||||||
restore [Brewfile] Restore a machine: brew bundle + mackup link + macOS defaults.
|
restore [Brewfile] Restore a machine: brew bundle + mackup restore + macOS defaults.
|
||||||
update [--dry-run] Pull latest ssr code, refresh launchd if needed.
|
update [--dry-run] Pull latest ssr code, refresh launchd if needed.
|
||||||
schedule on|off Enable/disable the scheduled launchd sync job.
|
schedule on|off Enable/disable the scheduled launchd sync job.
|
||||||
config show|edit Print or edit the active config file.
|
config show|edit Print or edit the active config file.
|
||||||
|
|||||||
+9
-9
@@ -5,8 +5,8 @@
|
|||||||
# scan Per-item prompts for new ~/.mackup/ssr-*.cfg files.
|
# scan Per-item prompts for new ~/.mackup/ssr-*.cfg files.
|
||||||
# list List existing ~/.mackup/ssr-*.cfg files with the path they cover.
|
# list List existing ~/.mackup/ssr-*.cfg files with the path they cover.
|
||||||
# clean Remove all generated ssr-*.cfg files (prompts first).
|
# clean Remove all generated ssr-*.cfg files (prompts first).
|
||||||
# unlink Run `mackup link uninstall` — copy configs back from Mackup
|
# unlink Run `mackup link uninstall` — only for undoing symlink-based Mackup
|
||||||
# storage and remove symlinks (fixes broken iTerm / shell paths).
|
# (legacy SSR or manual `mackup link`); copies from storage and removes symlinks.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
@@ -31,14 +31,14 @@ Subcommands:
|
|||||||
scan Suggest new ~/.mackup/ssr-*.cfg files (confirm each item).
|
scan Suggest new ~/.mackup/ssr-*.cfg files (confirm each item).
|
||||||
list List existing generated files.
|
list List existing generated files.
|
||||||
clean Remove all generated ssr-*.cfg files.
|
clean Remove all generated ssr-*.cfg files.
|
||||||
unlink Revert Mackup symlinks (copy from cloud → real files in ~).
|
unlink Undo symlink-based Mackup only (`mackup link uninstall`).
|
||||||
|
|
||||||
Run 'ssr mackup scan' when new dot-dirs appear; commit ~/.mackup/ssr-*.cfg
|
Run 'ssr mackup scan' when new dot-dirs appear; commit ~/.mackup/ssr-*.cfg
|
||||||
with your dotfiles if you use them.
|
with your dotfiles if you use them.
|
||||||
|
|
||||||
If Terminal/iTerm or other apps break after sync, run:
|
If you previously used symlink-based Mackup and apps break, run:
|
||||||
ssr mackup unlink
|
ssr mackup unlink
|
||||||
That runs Mackup's official undo for symlink-based installs.
|
Current SSR uses copy-based `mackup backup` / `restore` instead.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,11 +259,11 @@ cmd_unlink() {
|
|||||||
This runs: mackup -f link uninstall
|
This runs: mackup -f link uninstall
|
||||||
|
|
||||||
Mackup copies each managed path from your Mackup storage folder back into
|
Mackup copies each managed path from your Mackup storage folder back into
|
||||||
$HOME and removes the symlinks. Use when settings broke after `ssr sync` /
|
$HOME and removes the symlinks. Use only if you still have symlink-based
|
||||||
`mackup link install` (e.g. iTerm2, shell init files pointing at iCloud).
|
Mackup from an older SSR or from manual `mackup link` / `link install`.
|
||||||
|
|
||||||
After unlink, apps use normal local files again. Run `ssr sync` only when
|
After unlink, apps use normal local files again. Ongoing `ssr sync` uses
|
||||||
you are ready to re-apply symlink-based backup.
|
`mackup backup` (copy to cloud), not symlinks.
|
||||||
EOF
|
EOF
|
||||||
printf '\n'
|
printf '\n'
|
||||||
ssr::confirm "Run mackup link uninstall now?" 120 || { printf 'Aborted.\n'; return 0; }
|
ssr::confirm "Run mackup link uninstall now?" 120 || { printf 'Aborted.\n'; return 0; }
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ssr-sync — back up Homebrew + Mackup state to the configured cloud dir.
|
# ssr-sync — back up Homebrew + Mackup state to the configured cloud dir (copy-based mackup backup).
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ _ssr() {
|
|||||||
'scan[Suggest and confirm ssr-*.cfg files]' \
|
'scan[Suggest and confirm ssr-*.cfg files]' \
|
||||||
'list[List generated ssr-*.cfg files]' \
|
'list[List generated ssr-*.cfg files]' \
|
||||||
'clean[Remove all generated ssr-*.cfg files]' \
|
'clean[Remove all generated ssr-*.cfg files]' \
|
||||||
'unlink[mackup link uninstall — revert symlinks]'
|
'unlink[undo symlink Mackup — legacy recovery]'
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ SSR_CLOUD_PROVIDER=icloud
|
|||||||
SSR_BACKUP_SUBDIR=BACKUP
|
SSR_BACKUP_SUBDIR=BACKUP
|
||||||
SSR_MACKUP_SUBDIR=Mackup
|
SSR_MACKUP_SUBDIR=Mackup
|
||||||
|
|
||||||
# Optional iTerm2 race workaround: pre-symlink DynamicProfiles → Mackup cloud
|
# SSR uses `mackup backup` / `mackup restore` (copy, not symlinks) for reliability
|
||||||
# before `mackup link install`. Default is off — enabling can break iTerm if
|
# on newer macOS. If you still have Mackup symlinks from an older setup, run:
|
||||||
# the cloud copy is incomplete. Prefer `ssr mackup unlink` to recover.
|
# ssr mackup unlink
|
||||||
# SSR_MACKUP_PRESYMLINK=1
|
|
||||||
|
|
||||||
# Subdirs (relative to $HOME) to delete before each mackup backup attempt.
|
# Subdirs (relative to $HOME) to delete before each mackup backup attempt.
|
||||||
# Handles transient dirs with dangling symlinks or sockets (debug sessions,
|
# Handles transient dirs with dangling symlinks or sockets (debug sessions,
|
||||||
# log dirs) that shutil.copytree cannot copy.
|
# log dirs) that should not be copied to cloud.
|
||||||
# Built-in defaults: .claude/debug .cursor/logs .vscode/logs .npm/_logs
|
# Built-in defaults: .claude/debug .cursor/logs .vscode/logs .npm/_logs
|
||||||
# SSR_MACKUP_PRECLEAN_PATHS=".my-app/tmp .other-app/run"
|
# SSR_MACKUP_PRECLEAN_PATHS=".my-app/tmp .other-app/run"
|
||||||
|
|
||||||
|
|||||||
+27
-71
@@ -5,10 +5,9 @@
|
|||||||
# user can swap engines via ~/.mackup.cfg without changing this script.
|
# user can swap engines via ~/.mackup.cfg without changing this script.
|
||||||
#
|
#
|
||||||
# Mackup 0.10.2 command mapping (importantly different from older versions):
|
# Mackup 0.10.2 command mapping (importantly different from older versions):
|
||||||
# mackup backup → copy only; NO symlinks created in ~/
|
# mackup backup → copy managed files from ~/ into storage (no symlinks)
|
||||||
# mackup restore → copy only; NO symlinks created in ~/
|
# mackup restore → copy from storage back into ~/
|
||||||
# mackup link install → copy to cloud + create symlinks in ~/ ← SSR uses this
|
# mackup link* → symlink-based flow; unreliable on newer macOS — SSR does not use it
|
||||||
# mackup link → create symlinks in ~/ (cloud files must exist) ← SSR restore
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Custom Mackup app definition auto-generator
|
# 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
|
find "$mackup_root" -name '*.icloud' -type f -delete 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run `mackup -f link install` — this is the correct "backup with symlinks"
|
# Run `mackup -f backup` — copies managed paths from ~/ into the configured
|
||||||
# command in Mackup 0.10.2. Unlike `mackup backup` (which only copies),
|
# storage directory (no symlinks). Copy mode avoids macOS issues seen with
|
||||||
# `link install` moves each managed file to the cloud folder and replaces it
|
# `mackup link` / `link install` on recent OS releases.
|
||||||
# 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
|
# Light preclean removes known transient dirs so junk is not copied to cloud.
|
||||||
# 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
|
# 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.
|
# the unmanaged-apps scan still run. Failure is surfaced via ssr::warn.
|
||||||
ssr::mackup::backup() {
|
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).
|
# Derive mackup root from ~/.mackup.cfg (path + directory).
|
||||||
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
|
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
|
||||||
@@ -363,47 +357,11 @@ ssr::mackup::backup() {
|
|||||||
fi
|
fi
|
||||||
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
|
[[ -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
|
# Light preclean — remove transient directories so their ephemeral contents
|
||||||
# (debug symlinks, log files) are not included in the cloud copy.
|
# (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() {
|
_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=(
|
local -a transient_dirs=(
|
||||||
.claude/debug # Claude Code debug session links
|
.claude/debug # Claude Code debug session links
|
||||||
.cursor/logs # Cursor IDE logs
|
.cursor/logs # Cursor IDE logs
|
||||||
@@ -422,28 +380,28 @@ ssr::mackup::backup() {
|
|||||||
|
|
||||||
local attempt rc=0
|
local attempt rc=0
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
_ssr_mackup_preclean "$mackup_root"
|
_ssr_mackup_preclean
|
||||||
if mackup -f link install; then
|
if mackup -f backup; then
|
||||||
ssr::ok "mackup link install complete — symlinks created in ~/"
|
ssr::ok "mackup backup complete — configs copied to cloud storage"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
rc=$?
|
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
|
if [[ $attempt -lt 3 ]]; then
|
||||||
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
|
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
|
||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
ssr::warn "mackup link install did not complete after 3 attempts (rc=$rc)."
|
ssr::warn "mackup backup did not complete after 3 attempts (rc=$rc)."
|
||||||
ssr::warn "Sync will continue without it; run 'mackup -f link install' manually,"
|
ssr::warn "Sync will continue without it; run 'mackup -f backup' manually,"
|
||||||
ssr::warn "or check ~/.mackup.cfg if this persists."
|
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
|
return 0 # soft-fail so the rest of ssr sync still runs
|
||||||
}
|
}
|
||||||
|
|
||||||
ssr::mackup::restore() {
|
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
|
# Pre-materialize iCloud placeholders so mackup's isfile/isdir checks
|
||||||
# see real files rather than .icloud stubs (which read as non-existent).
|
# see real files rather than .icloud stubs (which read as non-existent).
|
||||||
@@ -467,9 +425,9 @@ ssr::mackup::restore() {
|
|||||||
local stub_total
|
local stub_total
|
||||||
stub_total="$(find "${mackup_root:-/dev/null}" -name '*.icloud' -type f 2>/dev/null | wc -l | tr -d ' ')"
|
stub_total="$(find "${mackup_root:-/dev/null}" -name '*.icloud' -type f 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
if [[ "$stub_total" -gt 0 ]]; then
|
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
|
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.
|
# Background watchdog: prints elapsed time every 15s.
|
||||||
local start_ts rc=0 done_flag watchdog_pid
|
local start_ts rc=0 done_flag watchdog_pid
|
||||||
@@ -479,16 +437,14 @@ ssr::mackup::restore() {
|
|||||||
while [[ -f "$done_flag" ]]; do
|
while [[ -f "$done_flag" ]]; do
|
||||||
sleep 15
|
sleep 15
|
||||||
[[ -f "$done_flag" ]] || break
|
[[ -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
|
"$(( $(date +%s) - start_ts ))" >&2
|
||||||
done
|
done
|
||||||
) &
|
) &
|
||||||
watchdog_pid=$!
|
watchdog_pid=$!
|
||||||
|
|
||||||
# `mackup link` creates symlinks ~/file → cloud/file for all managed apps.
|
# Copy managed files from storage into ~/. -f auto-confirms overwrites.
|
||||||
# Unlike `mackup restore` (which copies), this keeps files in iCloud and
|
mackup -f restore || rc=$?
|
||||||
# ensures ongoing sync. -f auto-confirms replacement of existing home files.
|
|
||||||
mackup -f link || rc=$?
|
|
||||||
|
|
||||||
rm -f "$done_flag"
|
rm -f "$done_flag"
|
||||||
kill "$watchdog_pid" 2>/dev/null || true
|
kill "$watchdog_pid" 2>/dev/null || true
|
||||||
@@ -496,16 +452,16 @@ ssr::mackup::restore() {
|
|||||||
|
|
||||||
local elapsed=$(( $(date +%s) - start_ts ))
|
local elapsed=$(( $(date +%s) - start_ts ))
|
||||||
if [[ $rc -ne 0 ]]; then
|
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
|
else
|
||||||
ssr::ok "mackup link completed in ${elapsed}s — symlinks created in ~/"
|
ssr::ok "mackup restore completed in ${elapsed}s — configs copied to ~/"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reverse `mackup link` / `link install`: copy each managed path from the
|
# Undo a previous `mackup link` / `link install` (symlinks → real files in ~/).
|
||||||
# Mackup storage tree back into $HOME and remove the symlinks. Use when apps
|
# SSR now uses copy-based backup/restore; keep this for machines that still
|
||||||
# (e.g. iTerm2) misbehave after symlink-based sync or iCloud produced a bad
|
# have symlink-based Mackup from older SSR or manual `mackup link` use.
|
||||||
# tree. Invoked by `ssr mackup unlink` (interactive confirm).
|
# Invoked by `ssr mackup unlink` (interactive confirm).
|
||||||
ssr::mackup::link_uninstall() {
|
ssr::mackup::link_uninstall() {
|
||||||
ssr::mackup::ensure_installed
|
ssr::mackup::ensure_installed
|
||||||
ssr::log "mackup link uninstall (revert symlinks → local files)"
|
ssr::log "mackup link uninstall (revert symlinks → local files)"
|
||||||
|
|||||||
Reference in New Issue
Block a user