Files
easyzsh/deploy.sh
T
Frank Qing e41d2b6e93 fix(pyenv): preserve shims in login shells
macOS path_helper runs after .zshenv and can move system Python ahead of pyenv. Add an idempotent login hook and preserve it across later easyzsh migrations.
2026-08-07 18:34:50 +08:00

165 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install easyzsh shell stubs into a home directory.
# Overwrites: ~/.zshenv ~/.zshrc
# Never overwrites: ~/.config/zsh/local.zsh ~/.config/zshrc/** ~/.local/bin/**
# Does not manage ~/.zprofile (machine/login hooks belong in local.zsh).
#
# Usage:
# ./deploy.sh
# ./deploy.sh --home /path/to/home
# ./deploy.sh --migrate-zprofile # fold real ~/.zprofile into local.zsh, then remove stub
set -euo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
TARGET_HOME="${HOME}"
MIGRATE_ZPROFILE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--home)
TARGET_HOME="$2"
shift 2
;;
--migrate-zprofile)
MIGRATE_ZPROFILE=1
shift
;;
-h|--help)
sed -n '2,12p' "$0"
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 2
;;
esac
done
if [[ ! -d "$TARGET_HOME" ]]; then
echo "Home does not exist: $TARGET_HOME" >&2
exit 1
fi
ZSH_CFG="${TARGET_HOME}/.config/zsh"
LOCAL_ZSH="${ZSH_CFG}/local.zsh"
ZPROFILE="${TARGET_HOME}/.zprofile"
ts=$(date +%Y%m%d-%H%M%S)
backup_file() {
local f="$1"
if [[ -e "$f" || -L "$f" ]]; then
cp -Pp "$f" "${f}.backup.${ts}"
echo "Backed up $f -> ${f}.backup.${ts}"
fi
}
file_has_content() {
[[ -f "$1" ]] && grep -q '[^[:space:]]' "$1"
}
is_easyzsh_zprofile_stub() {
local f="$1"
[[ -f "$f" ]] || return 1
# Comment-only placeholder from earlier easyzsh deploys.
if grep -Eqv '^[[:space:]]*(#|$)' "$f"; then
return 1
fi
grep -Eq 'easyzsh|PATH lives in|Leave this file empty|prefer.*local\.zsh' "$f"
}
has_pyenv_login_hook() {
local hook='[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh" ]] && . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh"'
[[ -f "$1" ]] && grep -Fqx "$hook" "$1"
}
echo "Deploying easyzsh shell stubs into $TARGET_HOME"
mkdir -p \
"${ZSH_CFG}" \
"${TARGET_HOME}/.config/zshrc" \
"${TARGET_HOME}/.cache/zsh" \
"${TARGET_HOME}/.local/bin"
# Migrate legacy fragments into local.zsh once.
if [[ ! -f "$LOCAL_ZSH" ]]; then
tmp=$(mktemp)
: >"$tmp"
if file_has_content "${TARGET_HOME}/.config/zshenv.local"; then
cat "${TARGET_HOME}/.config/zshenv.local" >>"$tmp"
printf '\n' >>"$tmp"
fi
if [[ -d "${ZSH_CFG}/local" ]]; then
for part in env.local.zsh login.local.zsh interactive.local.zsh; do
if file_has_content "${ZSH_CFG}/local/${part}"; then
cat "${ZSH_CFG}/local/${part}" >>"$tmp"
printf '\n' >>"$tmp"
fi
done
fi
if file_has_content "$tmp"; then
cp "$tmp" "$LOCAL_ZSH"
echo "Migrated machine-local fragments -> ~/.config/zsh/local.zsh"
fi
rm -f "$tmp"
fi
# Optional: fold a real (non-stub) ~/.zprofile into local.zsh.
if [[ "$MIGRATE_ZPROFILE" -eq 1 && -f "$ZPROFILE" ]]; then
if has_pyenv_login_hook "$ZPROFILE"; then
# ponytail: keep the whole profile; split migration only if per-line ownership is needed.
echo "Keeping ~/.zprofile: contains the pyenv login hook"
elif is_easyzsh_zprofile_stub "$ZPROFILE"; then
echo "Removing easyzsh ~/.zprofile stub (hooks live in local.zsh / .zshenv)"
rm -f "$ZPROFILE"
elif [[ ! -f "$LOCAL_ZSH" ]]; then
zprofile_extract=$(mktemp)
awk '
/Created by `pipx`/ { skip_pipx=1; next }
skip_pipx && /local\/bin/ { skip_pipx=0; next }
skip_pipx { next }
{ print }
' "$ZPROFILE" >"$zprofile_extract"
if file_has_content "$zprofile_extract"; then
cp "$zprofile_extract" "$LOCAL_ZSH"
echo "Migrated ~/.zprofile body -> ~/.config/zsh/local.zsh"
rm -f "$ZPROFILE"
echo "Removed ~/.zprofile after migration"
fi
rm -f "$zprofile_extract"
elif is_easyzsh_zprofile_stub "$ZPROFILE"; then
rm -f "$ZPROFILE"
fi
fi
# Always drop the comment-only easyzsh stub if present (even without --migrate).
if is_easyzsh_zprofile_stub "$ZPROFILE"; then
rm -f "$ZPROFILE"
echo "Removed empty easyzsh ~/.zprofile stub"
fi
backup_file "${TARGET_HOME}/.zshenv"
backup_file "${TARGET_HOME}/.zshrc"
install -m 644 "${ROOT}/.zshenv" "${TARGET_HOME}/.zshenv"
install -m 644 "${ROOT}/.zshrc" "${TARGET_HOME}/.zshrc"
# Remove obsolete managed-tree layout and examples from earlier deploys.
/bin/rm -rf \
"${ZSH_CFG}/env" \
"${ZSH_CFG}/interactive" \
"${ZSH_CFG}/login" \
"${ZSH_CFG}/local"
/bin/rm -f \
"${ZSH_CFG}/local.zsh.example" \
"${TARGET_HOME}/.config/zshenv.local"
# Ensure intentional grok shim when the binary tree exists.
if [[ -e "${TARGET_HOME}/.grok/bin/grok" ]]; then
ln -sfn "${TARGET_HOME}/.grok/bin/grok" "${TARGET_HOME}/.local/bin/grok"
echo "Ensured ~/.local/bin/grok -> ~/.grok/bin/grok"
fi
echo "Deploy complete."
echo "Verify: env -i HOME=\"$TARGET_HOME\" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv; command -v grok'"