diff --git a/README.md b/README.md index 49e9ef6..97813e1 100644 --- a/README.md +++ b/README.md @@ -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. -- **Backup** (`ssr sync`): dumps `Brewfile` and runs `mackup link install` to the configured cloud directory (symlink-based). -- **Restore** (`ssr restore`): installs Homebrew, replays the `Brewfile`, runs `mackup link`, applies macOS defaults. +- **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 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). - **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. diff --git a/bin/ssr b/bin/ssr index 74babcc..e711932 100755 --- a/bin/ssr +++ b/bin/ssr @@ -27,8 +27,8 @@ Usage: Commands: install First-time install: links binary, seeds config, schedules sync. - sync Run a backup: brew bundle dump + mackup link install → cloud. - restore [Brewfile] Restore a machine: brew bundle + mackup link + macOS defaults. + sync Run a backup: brew bundle dump + mackup backup → cloud. + restore [Brewfile] Restore a machine: brew bundle + mackup restore + macOS defaults. update [--dry-run] Pull latest ssr code, refresh launchd if needed. schedule on|off Enable/disable the scheduled launchd sync job. config show|edit Print or edit the active config file. diff --git a/bin/ssr-mackup b/bin/ssr-mackup index 924d3c3..1c8dfb4 100755 --- a/bin/ssr-mackup +++ b/bin/ssr-mackup @@ -5,8 +5,8 @@ # scan Per-item prompts for new ~/.mackup/ssr-*.cfg files. # list List existing ~/.mackup/ssr-*.cfg files with the path they cover. # clean Remove all generated ssr-*.cfg files (prompts first). -# unlink Run `mackup link uninstall` — copy configs back from Mackup -# storage and remove symlinks (fixes broken iTerm / shell paths). +# unlink Run `mackup link uninstall` — only for undoing symlink-based Mackup +# (legacy SSR or manual `mackup link`); copies from storage and removes symlinks. set -euo pipefail SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -31,14 +31,14 @@ Subcommands: scan Suggest new ~/.mackup/ssr-*.cfg files (confirm each item). list List existing generated 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 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 -That runs Mackup's official undo for symlink-based installs. +Current SSR uses copy-based `mackup backup` / `restore` instead. EOF } @@ -259,11 +259,11 @@ cmd_unlink() { This runs: mackup -f link uninstall Mackup copies each managed path from your Mackup storage folder back into -$HOME and removes the symlinks. Use when settings broke after `ssr sync` / -`mackup link install` (e.g. iTerm2, shell init files pointing at iCloud). +$HOME and removes the symlinks. Use only if you still have symlink-based +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 -you are ready to re-apply symlink-based backup. +After unlink, apps use normal local files again. Ongoing `ssr sync` uses +`mackup backup` (copy to cloud), not symlinks. EOF printf '\n' ssr::confirm "Run mackup link uninstall now?" 120 || { printf 'Aborted.\n'; return 0; } diff --git a/bin/ssr-sync b/bin/ssr-sync index c3da91b..2ec03f1 100755 --- a/bin/ssr-sync +++ b/bin/ssr-sync @@ -1,5 +1,5 @@ #!/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 SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" diff --git a/completions/_ssr b/completions/_ssr index a4493f5..77eac59 100644 --- a/completions/_ssr +++ b/completions/_ssr @@ -65,7 +65,7 @@ _ssr() { 'scan[Suggest and confirm ssr-*.cfg files]' \ 'list[List generated ssr-*.cfg files]' \ 'clean[Remove all generated ssr-*.cfg files]' \ - 'unlink[mackup link uninstall — revert symlinks]' + 'unlink[undo symlink Mackup — legacy recovery]' ;; esac ;; diff --git a/config/ssr.conf.example b/config/ssr.conf.example index abd12e9..4100fb2 100644 --- a/config/ssr.conf.example +++ b/config/ssr.conf.example @@ -16,14 +16,13 @@ SSR_CLOUD_PROVIDER=icloud SSR_BACKUP_SUBDIR=BACKUP SSR_MACKUP_SUBDIR=Mackup -# Optional iTerm2 race workaround: pre-symlink DynamicProfiles → Mackup cloud -# before `mackup link install`. Default is off — enabling can break iTerm if -# the cloud copy is incomplete. Prefer `ssr mackup unlink` to recover. -# SSR_MACKUP_PRESYMLINK=1 +# SSR uses `mackup backup` / `mackup restore` (copy, not symlinks) for reliability +# on newer macOS. If you still have Mackup symlinks from an older setup, run: +# ssr mackup unlink # Subdirs (relative to $HOME) to delete before each mackup backup attempt. # 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 # SSR_MACKUP_PRECLEAN_PATHS=".my-app/tmp .other-app/run" diff --git a/lib/mackup.sh b/lib/mackup.sh index c90b4f1..ba66591 100644 --- a/lib/mackup.sh +++ b/lib/mackup.sh @@ -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)"