install.sh: merge PERSONAL_DIRS contents instead of wholesale replace

The previous deploy_one() did `mv $HOME/<dir> $BACKUP_DIR/<dir>` then
`cp -a $YADR_DIR/<dir> $HOME/<dir>` for every entry in PERSONAL_DIRS.
For dirs the repo only partially populates (notably .local — repo only
tracks .local/bin/), this swept away unrelated user data: the most
recent re-bootstrap moved ~/.local/share/fonts/ (Intel One Mono, Open
Gorton, Roboto Mono, GALLAUDET, code128) into the timestamped backup,
making them appear missing.

Rework deploy logic:
- deploy_file: copies one file/symlink, backing up only the conflicting
  destination (if any). Idempotent via paths_equivalent() so re-runs
  with no changes produce no output and no spurious backups.
- deploy_dir: walks the repo's tree for that dir and deploys each leaf
  via deploy_file. Files in $HOME the repo doesn't know about are left
  untouched. Subdirs are mkdir'd as needed.

Also: track the personal fonts at .local/share/fonts/ so they redeploy
on every install, and run fc-cache -f at the end so apps see them
without a logout/login.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 06:50:52 -07:00
parent 065686f341
commit 1d7df94a1d
15 changed files with 74 additions and 14 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -30,7 +30,11 @@ PERSONAL_FILES=(
.zprofile .zshenv .zshrc .zprofile .zshenv .zshrc
) )
# Directories deployed wholesale to $HOME/<dir>/. # Directories whose contents are merged into $HOME/<dir>/, file by file. Files
# the repo provides replace conflicting on-disk versions (with the displaced
# version preserved in $BACKUP_DIR); files outside the repo's tree are left
# untouched. This deliberately avoids wholesale-replacing $HOME/.local etc.,
# which would sweep away unrelated user data (fonts, app state, ...).
PERSONAL_DIRS=( PERSONAL_DIRS=(
.fonts .irssi .nano .themes .local .mplayer .fonts .irssi .nano .themes .local .mplayer
) )
@@ -83,31 +87,80 @@ else
fi fi
# 3. Deploy personal dotfiles # 3. Deploy personal dotfiles
backup_one() {
local src_name="$1" # Move $HOME/<rel> aside into $BACKUP_DIR, preserving its sub-path.
local dst="$HOME/$src_name" backup_one_path() {
local rel="$1"
local dst="$HOME/$rel"
if [ -e "$dst" ] || [ -L "$dst" ]; then if [ -e "$dst" ] || [ -L "$dst" ]; then
mkdir -p "$BACKUP_DIR/$(dirname "$src_name")" mkdir -p "$BACKUP_DIR/$(dirname "$rel")"
mv "$dst" "$BACKUP_DIR/$src_name" mv "$dst" "$BACKUP_DIR/$rel"
fi fi
} }
deploy_one() { # Returns 0 iff $1 and $2 are equivalent (same symlink target, or identical
local name="$1" # regular-file content). Used to make re-runs idempotent: unchanged files get
local src="$YADR_DIR/$name" # skipped instead of needlessly backed up.
local dst="$HOME/$name" paths_equivalent() {
local a="$1" b="$2"
if [ -L "$a" ] && [ -L "$b" ]; then
[ "$(readlink "$a")" = "$(readlink "$b")" ]
elif [ -L "$a" ] || [ -L "$b" ]; then
return 1
elif [ -f "$a" ] && [ -f "$b" ]; then
cmp -s "$a" "$b"
else
return 1
fi
}
# Deploy one file or symlink at $YADR_DIR/<rel> to $HOME/<rel>. If a different
# file/symlink is already at the destination, it's moved into $BACKUP_DIR first.
deploy_file() {
local rel="$1"
local src="$YADR_DIR/$rel"
local dst="$HOME/$rel"
if [ ! -e "$src" ] && [ ! -L "$src" ]; then if [ ! -e "$src" ] && [ ! -L "$src" ]; then
return return
fi fi
backup_one "$name" if paths_equivalent "$src" "$dst"; then
return
fi
backup_one_path "$rel"
mkdir -p "$(dirname "$dst")" mkdir -p "$(dirname "$dst")"
cp -a "$src" "$dst" cp -a "$src" "$dst"
info "$name" info "$rel"
}
# Deploy a directory by walking its tree and deploying each file/symlink in
# turn. Directories get mkdir'd; existing entries inside $HOME/<rel> that the
# repo doesn't know about are left alone. This is the merge semantics that
# protects e.g. $HOME/.local/share/fonts from being clobbered when the repo
# only tracks $HOME/.local/bin.
deploy_dir() {
local rel="$1"
local src="$YADR_DIR/$rel"
if [ ! -d "$src" ] || [ -L "$src" ]; then
return
fi
mkdir -p "$HOME/$rel"
local sub sub_rel src_path
while IFS= read -r -d '' sub; do
sub="${sub#./}"
[ "$sub" = "." ] && continue
sub_rel="$rel/$sub"
src_path="$YADR_DIR/$sub_rel"
if [ -d "$src_path" ] && [ ! -L "$src_path" ]; then
mkdir -p "$HOME/$sub_rel"
continue
fi
deploy_file "$sub_rel"
done < <(cd "$src" && find . -mindepth 1 -print0)
} }
log "Deploying personal dotfiles" log "Deploying personal dotfiles"
for f in "${PERSONAL_FILES[@]}"; do deploy_one "$f"; done for f in "${PERSONAL_FILES[@]}"; do deploy_file "$f"; done
for d in "${PERSONAL_DIRS[@]}"; do deploy_one "$d"; done for d in "${PERSONAL_DIRS[@]}"; do deploy_dir "$d"; done
# .local/bin/claude is a relative symlink to ../share/claude/versions/... # .local/bin/claude is a relative symlink to ../share/claude/versions/...
# If that target isn't present on this machine, remove the dangling link so # If that target isn't present on this machine, remove the dangling link so
@@ -137,6 +190,13 @@ if [ -d "$HOME/.local/bin" ]; then
find "$HOME/.local/bin" -maxdepth 1 -type f -exec chmod +x {} \; 2>/dev/null || true find "$HOME/.local/bin" -maxdepth 1 -type f -exec chmod +x {} \; 2>/dev/null || true
fi fi
# 5. Refresh font cache so newly-deployed .fonts/ and .local/share/fonts/
# entries are visible to apps without a logout/login.
if have fc-cache; then
log "Refreshing font cache"
fc-cache -f >/dev/null 2>&1 || warn "fc-cache returned non-zero; continuing"
fi
echo echo
log "Done." log "Done."
if [ -d "$BACKUP_DIR" ]; then if [ -d "$BACKUP_DIR" ]; then