feat: add command for backup/recover brewfile

Signed-off-by: Li Tang <iamtangli42@gmail.com>
This commit is contained in:
2026-05-25 15:44:49 +08:00
parent 5f9534ad00
commit d72cb9fdf1
3 changed files with 156 additions and 15 deletions

View File

@@ -9,8 +9,10 @@ Use the setup script instead of ad hoc symlink changes:
- `./setup.sh install`: link managed files into `$HOME` and `$HOME/.config`. - `./setup.sh install`: link managed files into `$HOME` and `$HOME/.config`.
- `./setup.sh check`: verify expected tools, links, and directories. - `./setup.sh check`: verify expected tools, links, and directories.
- `./setup.sh full-recover`: bootstrap a new machine end to end. - `./setup.sh full-recover`: bootstrap a new machine end to end.
- `./setup.sh backup`: refresh tracked backup artifacts such as `Brewfile`. - `./setup.sh brew-backup`: dump Homebrew packages to `brewfiles/<hostname>/Brewfile`.
- `brew bundle install`: install CLI tools and apps from [Brewfile](/Users/d0zingcat/.dotfiles/Brewfile). - `./setup.sh brew-install`: install packages from the current machine's Brewfile.
- `./setup.sh backup`: full backup (includes brew-backup plus git/ssh/vscode exports).
- Per-machine Brewfiles live under [brewfiles/](/Users/d0zingcat/.dotfiles/brewfiles/).
- `tmux/plugins/tpm/tests/test_plugin_installation.sh`: example TPM plugin test entrypoint when working inside the submodule. - `tmux/plugins/tpm/tests/test_plugin_installation.sh`: example TPM plugin test entrypoint when working inside the submodule.
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions

28
brewfiles/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Per-Machine Brewfiles
Each Mac keeps its own Homebrew bundle under `brewfiles/<hostname>/Brewfile`, where `<hostname>` is the short hostname (`hostname -s`, e.g. `stardewvalley`).
## Commands
```bash
# Dump only (frequent updates)
./setup.sh brew-backup
# Install packages for this machine
./setup.sh brew-install
# Full backup still includes brew-backup
./setup.sh backup
```
## New machine / full recover
`./setup.sh full-recover` runs `brew-install` automatically when a Brewfile exists for the current hostname.
## Hostname changed?
Point at another machine's Brewfile:
```bash
BREWFILE_HOST=other-mac ./setup.sh brew-install
```

137
setup.sh
View File

@@ -7,6 +7,8 @@
# init - Initialize a brand new macOS system # init - Initialize a brand new macOS system
# install - Install dotfiles symlinks # install - Install dotfiles symlinks
# backup - Backup current configuration # backup - Backup current configuration
# brew-backup - Dump Homebrew Brewfile for this machine (hostname-specific)
# brew-install - Install Homebrew packages from this machine's Brewfile
# sync - Sync from git repository # sync - Sync from git repository
# full-recover - Full recovery on new machine (init + install + sync) # full-recover - Full recovery on new machine (init + install + sync)
# check - Check current setup status # check - Check current setup status
@@ -25,6 +27,9 @@ NC='\033[0m' # No Color
DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}"
WORKING_DIR=$(cd "$(dirname "$0")" && pwd) WORKING_DIR=$(cd "$(dirname "$0")" && pwd)
HOME_DIR="$HOME" HOME_DIR="$HOME"
BREWFILES_DIR="$WORKING_DIR/brewfiles"
# Override hostname when restoring on a machine whose Brewfile was created elsewhere
BREWFILE_HOST="${BREWFILE_HOST:-}"
# Files to link # Files to link
FILES=( FILES=(
@@ -106,6 +111,80 @@ function ask() {
[[ "$response" =~ ^[Yy] ]] [[ "$response" =~ ^[Yy] ]]
} }
# Short hostname for brewfiles/<host>/ (override with BREWFILE_HOST).
# Use /bin/hostname — bare "hostname" in a zsh function is parsed as host + $name.
function dotfiles_host() {
local id
if [[ -n "$BREWFILE_HOST" ]]; then
id="$BREWFILE_HOST"
else
id=$(/bin/hostname -s 2>/dev/null || /bin/hostname)
fi
echo "${id//\//-}"
}
function brewfile_for_machine() {
local host="${1:-$(dotfiles_host)}"
echo "$BREWFILES_DIR/$host/Brewfile"
}
# Prefer hostname-specific Brewfile; fall back to legacy repo-root Brewfile
function resolve_brewfile() {
local path
path=$(brewfile_for_machine)
if [ -f "$path" ]; then
echo "$path"
return 0
fi
if [ -f "$WORKING_DIR/Brewfile" ]; then
echo "$WORKING_DIR/Brewfile"
return 0
fi
return 1
}
function cmd_brew_backup() {
print_header "Backing Up Brewfile ($(dotfiles_host))"
if ! command_exists brew; then
print_error "Homebrew not found"
return 1
fi
local brewfile
brewfile=$(brewfile_for_machine)
mkdir -p "$(dirname "$brewfile")"
print_warning "Dumping to $brewfile..."
brew bundle dump -f --file="$brewfile"
print_success "Brewfile saved: $brewfile"
echo ""
echo "Commit on this machine:"
echo " git add brewfiles/$(dotfiles_host)/Brewfile"
echo " git commit -m 'backup: update brewfile for $(dotfiles_host)'"
}
function cmd_brew_install() {
print_header "Installing Homebrew Packages ($(dotfiles_host))"
if ! command_exists brew; then
print_error "Homebrew not found — run ./setup.sh init first"
return 1
fi
local brewfile
if ! brewfile=$(resolve_brewfile); then
print_error "No Brewfile for host $(dotfiles_host) under $BREWFILES_DIR"
echo " Run: ./setup.sh brew-backup"
echo " Or set BREWFILE_HOST=<other-host> if this machine was renamed"
return 1
fi
print_warning "Installing from $brewfile..."
brew bundle install --file="$brewfile"
print_success "Homebrew packages installed"
}
# ============================================================================= # =============================================================================
# Commands # Commands
# ============================================================================= # =============================================================================
@@ -120,6 +199,8 @@ Commands:
init Initialize a brand new macOS system (Homebrew, Xcode, etc.) init Initialize a brand new macOS system (Homebrew, Xcode, etc.)
install Install dotfiles symlinks to home directory install Install dotfiles symlinks to home directory
backup Backup current configuration to dotfiles repo backup Backup current configuration to dotfiles repo
brew-backup Dump Brewfile for this machine (brewfiles/<hostname>/)
brew-install Install packages from this machine's Brewfile
sync Sync from git repository sync Sync from git repository
full-recover Full recovery on new machine (init + install + sync) full-recover Full recovery on new machine (init + install + sync)
check Check current setup status check Check current setup status
@@ -129,6 +210,8 @@ Examples:
./setup.sh init # Initialize new system ./setup.sh init # Initialize new system
./setup.sh install # Link dotfiles ./setup.sh install # Link dotfiles
./setup.sh full-recover # Full setup on new machine ./setup.sh full-recover # Full setup on new machine
./setup.sh brew-backup # Update Brewfile for this Mac only
./setup.sh brew-install # Install packages from hostname Brewfile
./setup.sh check # Check what's installed ./setup.sh check # Check what's installed
For more information, see README.md For more information, see README.md
@@ -294,7 +377,7 @@ function cmd_install() {
"$(brew --prefix)/opt/fzf/install" --key-bindings --completion --no-update-rc 2>/dev/null || true "$(brew --prefix)/opt/fzf/install" --key-bindings --completion --no-update-rc 2>/dev/null || true
print_success "fzf shell integration installed" print_success "fzf shell integration installed"
else else
print_warning "fzf binary not found yet; run: brew bundle install" print_warning "fzf binary not found yet; run: ./setup.sh brew-install"
fi fi
fi fi
@@ -303,7 +386,7 @@ function cmd_install() {
echo "Next steps:" echo "Next steps:"
echo " 1. Configure git: git config --file ~/.gitconfig user.name 'Your Name'" echo " 1. Configure git: git config --file ~/.gitconfig user.name 'Your Name'"
echo " 2. Restart terminal or run: source ~/.zshrc" echo " 2. Restart terminal or run: source ~/.zshrc"
echo " 3. Run: brew bundle install (to install packages)" echo " 3. Run: ./setup.sh brew-install (to install packages)"
} }
function cmd_backup() { function cmd_backup() {
@@ -313,12 +396,15 @@ function cmd_backup() {
local backup_warnings=0 local backup_warnings=0
# ========================================================================== # ==========================================================================
# 1. Brewfile 备份 # 1. Brewfile 备份(按 hostname 分目录)
# ========================================================================== # ==========================================================================
print_warning "Dumping Brewfile..." local brewfile
brewfile=$(brewfile_for_machine)
print_warning "Dumping Brewfile for $(dotfiles_host)..."
if command_exists brew; then if command_exists brew; then
brew bundle dump -f --file="$WORKING_DIR/Brewfile" mkdir -p "$(dirname "$brewfile")"
print_success "Brewfile saved" brew bundle dump -f --file="$brewfile"
print_success "Brewfile saved: $brewfile"
backup_count=$((backup_count + 1)) backup_count=$((backup_count + 1))
else else
print_error "Homebrew not found" print_error "Homebrew not found"
@@ -464,7 +550,7 @@ EOF
| Item | Status | | Item | Status |
|------|--------| |------|--------|
| Brewfile | $([ -f "$WORKING_DIR/Brewfile" ] && echo "✅ Saved" || echo "❌ Failed") | | Brewfile ($(dotfiles_host)) | $([ -f "$brewfile" ] && echo "✅ Saved" || echo "❌ Failed") |
| Git config summary | $([ -f "$WORKING_DIR/.git_config_summary.txt" ] && echo "✅ Saved" || echo "❌ Failed") | | Git config summary | $([ -f "$WORKING_DIR/.git_config_summary.txt" ] && echo "✅ Saved" || echo "❌ Failed") |
| SSH public keys | $([ -d "$ssh_backup_dir" ] && echo "✅ Saved to $ssh_backup_dir" || echo "⚠️ Not found") | | SSH public keys | $([ -d "$ssh_backup_dir" ] && echo "✅ Saved to $ssh_backup_dir" || echo "⚠️ Not found") |
| 1Password config | $([ -f "$WORKING_DIR/.1password_config.txt" ] && echo "✅ Documented" || echo "⚠️ Not configured") | | 1Password config | $([ -f "$WORKING_DIR/.1password_config.txt" ] && echo "✅ Documented" || echo "⚠️ Not configured") |
@@ -473,7 +559,7 @@ EOF
## Files Created ## Files Created
- Brewfile (updated) - brewfiles/$(dotfiles_host)/Brewfile (updated)
- .git_config_summary.txt ⚠️ DO NOT COMMIT - .git_config_summary.txt ⚠️ DO NOT COMMIT
- .1password_config.txt - .1password_config.txt
- .kube_contexts.txt - .kube_contexts.txt
@@ -483,7 +569,7 @@ $( [ -d "$ssh_backup_dir" ] && echo "- $ssh_backup_dir/ ⚠️ DO NOT COMMIT" )
## Next Steps ## Next Steps
1. Review changes: git status 1. Review changes: git status
2. Commit safe files: git add Brewfile .1password_config.txt .kube_contexts.txt .vscode_extensions.txt 2. Commit safe files: git add brewfiles/$(dotfiles_host)/Brewfile .1password_config.txt .kube_contexts.txt .vscode_extensions.txt
3. git commit -m 'backup: update dotfiles' 3. git commit -m 'backup: update dotfiles'
4. ⚠️ DO NOT commit: .git_config_summary.txt, ssh_backup_*/ 4. ⚠️ DO NOT commit: .git_config_summary.txt, ssh_backup_*/
EOF EOF
@@ -498,12 +584,12 @@ EOF
echo " Warnings: $backup_warnings" echo " Warnings: $backup_warnings"
echo "" echo ""
echo "Files to review:" echo "Files to review:"
echo " ✅ Safe to commit: Brewfile, .1password_config.txt, .kube_contexts.txt, .vscode_extensions.txt" echo " ✅ Safe to commit: brewfiles/$(dotfiles_host)/Brewfile, .1password_config.txt, .kube_contexts.txt, .vscode_extensions.txt"
echo " ⚠️ DO NOT COMMIT: .git_config_summary.txt, ssh_backup_*/" echo " ⚠️ DO NOT COMMIT: .git_config_summary.txt, ssh_backup_*/"
echo "" echo ""
echo "Next steps:" echo "Next steps:"
echo " 1. Review: git status" echo " 1. Review: git status"
echo " 2. Commit safe files: git add Brewfile .1password_config.txt .kube_contexts.txt .vscode_extensions.txt" echo " 2. Commit safe files: git add brewfiles/$(dotfiles_host)/Brewfile .1password_config.txt .kube_contexts.txt .vscode_extensions.txt"
echo " 3. git commit -m 'backup: update dotfiles'" echo " 3. git commit -m 'backup: update dotfiles'"
echo "==========================================" echo "=========================================="
} }
@@ -559,6 +645,17 @@ function cmd_full_recover() {
# Run sync # Run sync
cmd_sync cmd_sync
# Install Homebrew packages for this hostname
if command_exists brew; then
if resolve_brewfile >/dev/null; then
print_warning "Installing Homebrew packages for $(dotfiles_host)..."
cmd_brew_install || print_warning "brew-install had errors — review output above"
else
print_warning "No Brewfile for $(dotfiles_host) under brewfiles/ — skip package install"
echo " After first brew-backup on another Mac, git pull then run: ./setup.sh brew-install"
fi
fi
print_header "Full Recovery Complete!" print_header "Full Recovery Complete!"
echo "" echo ""
echo "IMPORTANT: Configure your personal settings:" echo "IMPORTANT: Configure your personal settings:"
@@ -573,8 +670,8 @@ function cmd_full_recover() {
echo " # Generate new SSH keys" echo " # Generate new SSH keys"
echo " ssh-keygen -t ed25519 -C 'your@email.com'" echo " ssh-keygen -t ed25519 -C 'your@email.com'"
echo "" echo ""
echo " # Install Homebrew packages" echo " # Install Homebrew packages (if not already done above)"
echo " brew bundle install" echo " ./setup.sh brew-install"
echo "" echo ""
} }
@@ -592,6 +689,14 @@ function cmd_check() {
missing=$((missing + 1)) missing=$((missing + 1))
fi fi
echo -n "Brewfile ($(dotfiles_host)): "
local brewfile_check
if brewfile_check=$(resolve_brewfile 2>/dev/null); then
print_success "$brewfile_check"
else
print_warning "not found — run ./setup.sh brew-backup"
fi
# Check zsh # Check zsh
echo -n "Zsh: " echo -n "Zsh: "
if command_exists zsh; then if command_exists zsh; then
@@ -721,6 +826,12 @@ case "$COMMAND" in
backup) backup)
cmd_backup cmd_backup
;; ;;
brew-backup)
cmd_brew_backup
;;
brew-install)
cmd_brew_install
;;
sync) sync)
cmd_sync cmd_sync
;; ;;