fix(shell): restore always-on PATH after macOS path_helper

path_helper runs after ~/.zshenv in login shells and puts Homebrew ahead of
fnm, pyenv, and ~/.local/bin. Re-source ~/.zshenv from ~/.zprofile and make
prepends idempotent so login and zsh -c resolve the same binaries.
This commit is contained in:
Frank Qing committed 2026-09-07 22:07:58 +08:00
1 parent 82c6a57c3a
commit 34d11b5aa4
8 files changed
+198 -73

No files matched your search

+83 -12
View File
@@ -81,11 +81,13 @@ for script in "$ROOT"/*.sh; do
bash -n "$script"
done
for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT"/zshrc/*.zsh "$ROOT"/zsh/*.zsh; do
for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT/.zprofile" "$ROOT"/zshrc/*.zsh "$ROOT"/zsh/*.zsh; do
[[ -f "$config" ]] || continue
zsh -n "$config"
done
[[ ! -e "$ROOT/.zprofile" ]] || fail "easyzsh should not ship a managed .zprofile"
[[ -f "$ROOT/.zprofile" ]] || fail "easyzsh should ship a managed .zprofile"
grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$ROOT/.zprofile" ||
fail ".zprofile is missing the path_helper reapply marker"
[[ -f "$ROOT/zsh/fnm.zsh" ]] || fail "always-on fnm fragment is missing"
[[ ! -e "$ROOT/zshrc/fnm.zsh" ]] || fail "legacy interactive fnm fragment still exists"
[[ -f "$ROOT/zsh/pyenv.zsh" ]] || fail "always-on pyenv fragment is missing"
@@ -96,6 +98,7 @@ grep -q 'apt-get install -y unzip' "$ROOT/install_fnm.sh" || fail "Ubuntu unzip
grep -q 'tail -1 || true' "$ROOT/install_merlin_worker.sh" || fail "a missing PYTHONPATH remains fatal"
grep -Fq 'mktemp "$HOME/.zshrc.download.' "$ROOT/install.sh" || fail ".zshrc is not downloaded on its target filesystem"
grep -Fq 'mktemp "$HOME/.zshenv.download.' "$ROOT/install.sh" || fail ".zshenv is not downloaded on its target filesystem"
grep -Fq '.zprofile' "$ROOT/install.sh" || fail "install.sh does not download .zprofile"
grep -Fq 'mktemp "$(dirname "$destination")/.easyzsh-' "$ROOT/patch.sh" || fail "patch downloads can be sourced while incomplete"
grep -Fq '"$ZSH_CONFIGS_DIR"/*(DN)' "$ROOT/.zshrc" || fail "existing extensionless Zsh configs are ignored"
grep -Fq '_resolve_trash_cmd' "$ROOT/.zshrc" || fail "trash-backed rm helper is missing"
@@ -143,6 +146,8 @@ chmod +x "$deploy_home/.grok/bin/grok"
HOME="$deploy_home" bash "$ROOT/deploy.sh" --home "$deploy_home"
[[ -f "$deploy_home/.zshenv" ]] || fail "deploy.sh did not install .zshenv"
[[ -f "$deploy_home/.zshrc" ]] || fail "deploy.sh did not install .zshrc"
grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$deploy_home/.zprofile" ||
fail "deploy.sh did not install .zprofile"
grep -Fxq '# machine-only' "$deploy_home/.config/zsh/local.zsh" ||
fail "deploy.sh overwrote local.zsh"
[[ ! -e "$deploy_home/.config/zsh/env" ]] || fail "deploy.sh left obsolete env/ tree"
@@ -205,34 +210,100 @@ HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \
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"
# Login zsh must reapply always-on PATH after the system profile (path_helper).
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.
# A later easyzsh migration still restores pyenv; the old pyenv-only hook is replaced.
pyenv_login_hook='[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh" ]] && . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh"'
printf '%s\n' "$pyenv_login_hook" > "$pyenv_env_home/.zprofile"
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"
grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$pyenv_env_home/.zprofile" ||
fail "deploy.sh did not replace the pyenv-only login hook with the path_helper reapply"
if grep -Fqx "$pyenv_login_hook" "$pyenv_env_home/.zprofile"; then
fail "deploy.sh left the obsolete pyenv login hook in .zprofile"
fi
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.
# Extra login lines are folded into local.zsh on --migrate-zprofile; .zprofile stays managed.
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 -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$profile_comment_home/.zprofile" ||
fail "deploy.sh did not install the managed .zprofile after migrating extra lines"
if grep -Fxq 'ordinary-login-hook' "$profile_comment_home/.zprofile"; then
fail "deploy.sh left migrated login lines in .zprofile"
fi
grep -Fxq 'ordinary-login-hook' "$profile_comment_home/.config/zsh/local.zsh" ||
fail "deploy.sh did not migrate an ordinary profile mentioning pyenv"
# Without --migrate-zprofile, extra login lines stay in .zprofile below the reapply block.
profile_keep_home="$TEMP_DIR/profile-keep-home"
mkdir -p "$profile_keep_home"
printf '%s\n' 'ordinary-login-hook' > "$profile_keep_home/.zprofile"
HOME="$profile_keep_home" bash "$ROOT/deploy.sh" --home "$profile_keep_home" >/dev/null
grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$profile_keep_home/.zprofile" ||
fail "deploy.sh did not install the managed .zprofile in front of extra login lines"
grep -Fxq 'ordinary-login-hook' "$profile_keep_home/.zprofile" ||
fail "deploy.sh dropped extra login lines without --migrate-zprofile"
# ~/.local/bin must beat path_helper-promoted system binaries in login zsh.
tee_login_home="$TEMP_DIR/tee-login-home"
mkdir -p "$tee_login_home/.local/bin"
printf '%s\n' '#!/bin/sh' 'echo local-tee' >"$tee_login_home/.local/bin/tee"
chmod +x "$tee_login_home/.local/bin/tee"
HOME="$tee_login_home" bash "$ROOT/deploy.sh" --home "$tee_login_home" >/dev/null
HOME="$tee_login_home" PATH=/usr/bin:/bin \
zsh -c '[[ "$(command -v tee)" == "$HOME/.local/bin/tee" ]]' ||
fail "~/.local/bin/tee is not preferred in non-login zsh"
HOME="$tee_login_home" PATH=/usr/bin:/bin \
zsh -lc '[[ "$(command -v tee)" == "$HOME/.local/bin/tee" ]]' ||
fail "~/.local/bin/tee is not restored in login zsh after the system profile"
# Login zsh must prefer fnm-managed npm over Homebrew/system npm.
fnm_login_home="$TEMP_DIR/fnm-login-home"
mkdir -p "$fnm_login_home/.local/share/fnm/shim/bin" "$fnm_login_home/.config/zsh"
cat >"$fnm_login_home/.local/share/fnm/fnm" <<'EOF'
#!/bin/bash
if [[ "$1" == "env" ]]; then
shim="$HOME/.local/share/fnm/shim"
printf '%s\n' "export PATH=\"${shim}/bin:\$PATH\""
printf '%s\n' "export FNM_MULTISHELL_PATH=\"${shim}\""
exit 0
fi
exit 0
EOF
chmod +x "$fnm_login_home/.local/share/fnm/fnm"
printf '%s\n' '#!/bin/sh' 'echo fnm-npm' >"$fnm_login_home/.local/share/fnm/shim/bin/npm"
chmod +x "$fnm_login_home/.local/share/fnm/shim/bin/npm"
cp "$ROOT/zsh/fnm.zsh" "$fnm_login_home/.config/zsh/fnm.zsh"
HOME="$fnm_login_home" bash "$ROOT/deploy.sh" --home "$fnm_login_home" >/dev/null
HOME="$fnm_login_home" PATH=/usr/bin:/bin \
zsh -c '[[ "$(command -v npm)" == "$HOME/.local/share/fnm/shim/bin/npm" ]]' ||
fail "fnm npm is not preferred in non-login zsh"
HOME="$fnm_login_home" PATH=/usr/bin:/bin \
zsh -lc '[[ "$(command -v npm)" == "$HOME/.local/share/fnm/shim/bin/npm" ]]' ||
fail "fnm npm is not restored in login zsh after the system profile"
# Re-sourcing fnm.zsh must reuse the existing multishell instead of stacking PATH.
HOME="$fnm_login_home" PATH=/usr/bin:/bin zsh -c '
source "$HOME/.config/zsh/fnm.zsh"
first=$FNM_MULTISHELL_PATH
source "$HOME/.config/zsh/fnm.zsh"
[[ $FNM_MULTISHELL_PATH == $first ]] || exit 1
n=0
for p in $path; do
[[ $p == "$first/bin" ]] && n=$((n + 1))
done
(( n == 1 ))
' || fail "fnm.zsh stacked a second multishell on PATH when re-sourced"
# patch.sh path form installs into ~/.config/zsh/
path_patch_home="$TEMP_DIR/path-patch-home"
path_bin="$TEMP_DIR/path-bin"