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.
This commit is contained in:
Frank Qing
2026-08-07 18:34:50 +08:00
parent 9257eceb47
commit e41d2b6e93
5 changed files with 54 additions and 7 deletions
+3 -1
View File
@@ -41,7 +41,9 @@ From a git checkout (shell stubs only):
| `~/.config/zshrc/*.zsh` | Personal/tool overlays before oh-my-zsh (interactive) | Left alone |
| `~/.local/bin/` | User tools + shims (e.g. `grok``~/.grok/bin/grok`) | Left alone |
easyzsh does **not** manage `~/.zprofile`. Put machine/login hooks in `local.zsh`.
Core deploy does not manage `~/.zprofile`. The pyenv add-on appends one login
hook so macOS `path_helper` cannot outrank pyenv shims. Put other machine hooks
in `local.zsh`.
Do **not** put `~/.grok/bin` on PATH wholesale.
+9 -1
View File
@@ -68,6 +68,11 @@ is_easyzsh_zprofile_stub() {
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 \
@@ -101,7 +106,10 @@ fi
# Optional: fold a real (non-stub) ~/.zprofile into local.zsh.
if [[ "$MIGRATE_ZPROFILE" -eq 1 && -f "$ZPROFILE" ]]; then
if is_easyzsh_zprofile_stub "$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
+6
View File
@@ -23,5 +23,11 @@ fi
# Always-on fragment: ~/.config/zsh/pyenv.zsh (sourced from ~/.zshenv)
curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- zsh/pyenv
# macOS path_helper runs after ~/.zshenv in login shells, so reapply pyenv afterwards.
PYENV_LOGIN_HOOK='[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh" ]] && . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh"'
if ! grep -Fqx "$PYENV_LOGIN_HOOK" "$HOME/.zprofile" 2>/dev/null; then
[[ ! -s "$HOME/.zprofile" ]] || printf '\n' >> "$HOME/.zprofile"
printf '%s\n' "$PYENV_LOGIN_HOOK" >> "$HOME/.zprofile"
fi
# Drop legacy interactive-only copy if present
rm -f "$HOME/.config/zshrc/pyenv.zsh"
+33 -3
View File
@@ -165,14 +165,44 @@ HOME="$fnm_env_home" PATH=/usr/bin:/bin \
# Non-interactive zsh must resolve pyenv's global Python via .zshenv.
pyenv_env_home="$TEMP_DIR/pyenv-env-home"
mkdir -p "$pyenv_env_home/.pyenv/shims" "$pyenv_env_home/.config/zsh"
pyenv_env_xdg="$pyenv_env_home/xdg"
mkdir -p "$pyenv_env_home/.pyenv/shims" "$pyenv_env_xdg/zsh"
ln -s /usr/bin/true "$pyenv_env_home/.pyenv/shims/python3"
cp "$ROOT/zsh/pyenv.zsh" "$pyenv_env_home/.config/zsh/pyenv.zsh"
cp "$ROOT/zsh/pyenv.zsh" "$pyenv_env_xdg/zsh/pyenv.zsh"
HOME="$pyenv_env_home" bash "$ROOT/deploy.sh" --home "$pyenv_env_home" >/dev/null
HOME="$pyenv_env_home" PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \
HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \
PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \
zsh -c '[[ "$(command -v python3)" == "$HOME/.pyenv/shims/python3" ]]' ||
fail "pyenv Python is not available to non-interactive zsh via .zshenv"
# Login zsh must reapply pyenv after the system profile changes PATH.
pyenv_login_hook=$(sed -n "s/^PYENV_LOGIN_HOOK='\(.*\)'$/\1/p" "$ROOT/install_pyenv.sh")
[[ -n "$pyenv_login_hook" ]] || fail "install_pyenv.sh is missing its login hook"
printf '%s\n' "$pyenv_login_hook" > "$pyenv_env_home/.zprofile"
HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \
PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \
zsh -lc '[[ "$(command -v python3)" == "$HOME/.pyenv/shims/python3" ]]' ||
fail "pyenv Python is not restored in login zsh after the system profile"
# A later easyzsh migration must not move the login hook before the system profile.
HOME="$pyenv_env_home" bash "$ROOT/deploy.sh" --home "$pyenv_env_home" --migrate-zprofile >/dev/null
grep -Fqx "$pyenv_login_hook" "$pyenv_env_home/.zprofile" ||
fail "deploy.sh migrated the pyenv login hook out of .zprofile"
HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \
PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \
zsh -lc '[[ "$(command -v python3)" == "$HOME/.pyenv/shims/python3" ]]' ||
fail "pyenv Python was lost after a later easyzsh migration"
# A comment mentioning the fragment is not an active pyenv login hook.
profile_comment_home="$TEMP_DIR/profile-comment-home"
mkdir -p "$profile_comment_home"
printf '%s\n' '# pyenv config: ~/.config/zsh/pyenv.zsh' 'ordinary-login-hook' > "$profile_comment_home/.zprofile"
HOME="$profile_comment_home" bash "$ROOT/deploy.sh" --home "$profile_comment_home" --migrate-zprofile >/dev/null
[[ ! -e "$profile_comment_home/.zprofile" ]] ||
fail "deploy.sh treated a pyenv path comment as an active login hook"
grep -Fxq 'ordinary-login-hook' "$profile_comment_home/.config/zsh/local.zsh" ||
fail "deploy.sh did not migrate an ordinary profile mentioning pyenv"
# patch.sh path form installs into ~/.config/zsh/
path_patch_home="$TEMP_DIR/path-patch-home"
path_bin="$TEMP_DIR/path-bin"
+3 -2
View File
@@ -1,7 +1,8 @@
# pyenv (Python Version Manager), always-on via ~/.zshenv
export PYENV_ROOT="${PYENV_ROOT:-$HOME/.pyenv}"
[[ -d "$PYENV_ROOT/bin" ]] && export PATH="$PYENV_ROOT/bin:$PATH"
[[ -d "$PYENV_ROOT/shims" ]] && export PATH="$PYENV_ROOT/shims:$PATH"
[[ -d "$PYENV_ROOT/bin" ]] && path=("$PYENV_ROOT/bin" ${path:#$PYENV_ROOT/bin})
[[ -d "$PYENV_ROOT/shims" ]] && path=("$PYENV_ROOT/shims" ${path:#$PYENV_ROOT/shims})
export PATH
if [[ -o interactive ]] && command -v pyenv &> /dev/null; then
eval "$(pyenv init - zsh)"
fi