#!/bin/bash # # script/stow-package # # Wrapper around `stow --restow --no-folding` that handles pre-existing files # in $HOME by moving them into a timestamped backup dir before stowing. # Intended for the first-time install / migration case where $HOME already # contains real config files or foreign symlinks (from a previous dotfile # manager or hand-authored) that would otherwise make stow abort. # # Behavior: # 1. Scans the package for target paths in $HOME that are already occupied. # 2. Moves every conflicting real file into # ~/.dotfiles-backup///, preserving structure. # 3. Runs the real stow. # # Conflicting user files are never deleted — if the new setup is wrong you can # rsync the backup dir back over $HOME. Restowing only removes obsolete # symlinks owned by this package. Invoked by the Makefile's stow targets and # by script/install-darwin. set -euo pipefail pkg="${1:?usage: script/stow-package }" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" if [ ! -d "$pkg" ]; then echo "stow-package: no such package: $pkg" >&2 exit 1 fi # Enumerate conflicts by inspecting the filesystem directly. With # --no-folding every file in the package becomes its own symlink in $HOME, # so a conflict is simply: something already occupies a target path and it # isn't what we're about to put there. Deriving this ourselves keeps the # script independent of stow's human-readable diagnostics, whose wording # differs between stow 2.3.x (Debian/Ubuntu) and 2.4+ (Homebrew) and is not # a stable interface in any case. conflicts=$( find "$pkg" -mindepth 1 -print0 | while IFS= read -r -d '' src; do rel="${src#"$pkg"/}" target="$HOME/$rel" # Stow always ignores its package-local ignore file. Runtime paths in # that file are mirrored in the repository's .gitignore, so skip them # here too instead of moving aside host-local state Stow will not link. [ "$rel" = ".stow-local-ignore" ] && continue git check-ignore -q -- "$src" 2>/dev/null && continue # -e is false for a dangling symlink, so test -L too: a broken link # still occupies the path and still has to be moved aside. [ -e "$target" ] || [ -L "$target" ] || continue if [ -d "$src" ] && [ ! -L "$src" ]; then if [ -L "$target" ]; then # An owned folded directory is safe: --restow combined with # --no-folding will replace it with real target directories. # A link to any other directory is a migration conflict. [ "$target" -ef "$src" ] || printf '%s\n' "$rel" else # stow just needs a real directory here; anything else blocks it. [ -d "$target" ] || printf '%s\n' "$rel" fi else # -ef compares device+inode through symlinks, so this is true # when the target is already the very file the package provides # — a re-stow, not a conflict. [ "$target" -ef "$src" ] || printf '%s\n' "$rel" fi done ) if [ -n "$conflicts" ]; then ts=$(date +%Y%m%d-%H%M%S) backup_root="$HOME/.dotfiles-backup/$ts" echo "stow-package: $pkg has conflicts; moving aside to $backup_root" while IFS= read -r rel; do [ -n "$rel" ] || continue src="$HOME/$rel" dst="$backup_root/$rel" mkdir -p "$(dirname "$dst")" mv "$src" "$dst" echo " moved $rel" done <<< "$conflicts" fi stow --restow --no-folding -t "$HOME" "$pkg" echo "stow-package: stowed $pkg"