Files
easyzsh/tests/installers.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

232 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
assert_download_failure_is_reported() {
local home="$TEMP_DIR/home-${1%.sh}"
mkdir -p "$home"
if HOME="$home" PATH="$TEMP_DIR/bin:/usr/bin:/bin" /bin/bash "$ROOT/$1" >"$home/output" 2>&1; then
fail "$1 returned success after curl failed"
fi
}
mkdir -p "$TEMP_DIR/bin"
ln -s /usr/bin/false "$TEMP_DIR/bin/curl"
ln -s /usr/bin/true "$TEMP_DIR/bin/unzip"
for script in install_fnm.sh install_nvm.sh install_pyenv.sh install_merlin_worker.sh; do
assert_download_failure_is_reported "$script"
done
patch_home="$TEMP_DIR/patch-home"
mkdir -p "$patch_home/.config/zshrc"
cp "$ROOT/README.md" "$patch_home/.config/zshrc/missing.zsh"
if HOME="$patch_home" PATH="$TEMP_DIR/bin:/usr/bin:/bin" /bin/bash "$ROOT/patch.sh" missing >/dev/null 2>&1; then
fail "patch.sh returned success after curl failed"
fi
cmp -s "$ROOT/README.md" "$patch_home/.config/zshrc/missing.zsh" ||
fail "patch.sh replaced an existing config after curl failed"
if HOME="$patch_home" PATH="$TEMP_DIR/bin:/usr/bin:/bin" /bin/bash "$ROOT/patch.sh" ../invalid >/dev/null 2>&1; then
fail "patch.sh accepted an unsafe config name"
fi
main_bin="$TEMP_DIR/main-bin"
main_home="$TEMP_DIR/main-home"
mkdir -p "$main_bin" "$main_home"
for command_name in zsh git curl tar fc-cache eza zoxide autoconf trash; do
ln -s /usr/bin/true "$main_bin/$command_name"
done
ln -sf "$(command -v install)" "$main_bin/install"
ln -sf "$(command -v cp)" "$main_bin/cp"
ln -sf "$(command -v mkdir)" "$main_bin/mkdir"
ln -sf "$(command -v chmod)" "$main_bin/chmod"
ln -sf "$(command -v ln)" "$main_bin/ln"
ln -sf "$(command -v rm)" "$main_bin/rm" 2>/dev/null || ln -sf /bin/rm "$main_bin/rm"
ln -sf "$(command -v bash)" "$main_bin/bash"
ln -sf "$(command -v mktemp)" "$main_bin/mktemp"
ln -sf "$(command -v dirname)" "$main_bin/dirname"
ln -sf "$(command -v date)" "$main_bin/date"
ln -sf "$(command -v sed)" "$main_bin/sed"
ln -sf "$(command -v grep)" "$main_bin/grep"
ln -sf "$(command -v awk)" "$main_bin/awk"
ln -s /usr/bin/false "$main_bin/wget"
cp "$ROOT/README.md" "$main_home/.zshrc"
printf '%s\n' 'existing-zshenv' >"$main_home/.zshenv"
if HOME="$main_home" PATH="$main_bin:/usr/bin:/bin" /bin/bash "$ROOT/install.sh" --non-interactive >/dev/null 2>&1; then
fail "install.sh returned success after the zshrc download failed"
fi
cmp -s "$ROOT/README.md" "$main_home/.zshrc" ||
fail "install.sh replaced .zshrc after its download failed"
printf '%s\n' 'existing-zshenv' | cmp -s - "$main_home/.zshenv" ||
fail "install.sh replaced .zshenv after a download failed"
fnm_home="$TEMP_DIR/fnm-home"
mkdir -p "$fnm_home/.local/share/fnm"
ln -s /usr/bin/true "$fnm_home/.local/share/fnm/fnm"
HOME="$fnm_home" PATH=/usr/bin:/bin zsh -c 'source "$1"; command -v fnm >/dev/null' zsh "$ROOT/zsh/fnm.zsh" ||
fail "fnm is not restored to PATH in a new Zsh session"
for script in "$ROOT"/*.sh; do
bash -n "$script"
done
for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$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/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"
[[ ! -e "$ROOT/zshrc/pyenv.zsh" ]] || fail "legacy interactive pyenv fragment still exists"
grep -q 'NVM_VERSION="v0.40.4"' "$ROOT/install_nvm.sh" || fail "nvm version is stale"
grep -q 'apt-get install -y unzip' "$ROOT/install_fnm.sh" || fail "Ubuntu unzip installation is missing"
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 '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"
grep -Fq 'libglib2.0-bin' "$ROOT/install.sh" || fail "Ubuntu Trash support is not installed"
grep -Fq 'deploy.sh' "$ROOT/install.sh" || fail "install.sh does not invoke deploy.sh"
grep -Fq 'OPTIONAL_ADDONS=' "$ROOT/install.sh" || fail "install.sh missing OPTIONAL_ADDONS catalog"
grep -Fq -- '--with' "$ROOT/install.sh" || fail "install.sh missing --with option"
# --with parses known add-ons and rejects unknown ones without network.
if HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --with nosuch >/dev/null 2>&1; then
fail "install.sh accepted an unknown --with add-on"
fi
if ! HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --help >/dev/null; then
fail "install.sh --help failed"
fi
grep -Fq 'brew shellenv' "$ROOT/.zshenv" || fail "Homebrew is not initialized in .zshenv"
grep -Fq '/usr/local/bin/brew' "$ROOT/.zshenv" || fail "Intel Homebrew path is missing from .zshenv"
grep -Fq '$HOME/.local/bin' "$ROOT/.zshenv" || fail "~/.local/bin is not added in .zshenv"
grep -Fq 'local.zsh' "$ROOT/.zshenv" || fail "local.zsh is not sourced from .zshenv"
grep -Fq 'ZSH_ALWAYS_ON_DIR' "$ROOT/.zshenv" || fail "always-on ~/.config/zsh/*.zsh glob is missing from .zshenv"
grep -Fq 'zsh/fnm' "$ROOT/install_fnm.sh" || fail "install_fnm.sh does not patch always-on zsh/fnm"
grep -Fq 'zsh/pyenv' "$ROOT/install_pyenv.sh" || fail "install_pyenv.sh does not patch always-on zsh/pyenv"
if grep -Fq 'brew shellenv' "$ROOT/.zshrc"; then
fail "Homebrew still initializes from interactive-only .zshrc"
fi
[[ ! -e "$ROOT/install/fnm.sh" ]] || fail "vendored fnm installer still exists"
if grep -Eq '^[[:space:]]*export MLX_USER_TOKEN=' "$ROOT"/zshrc/*.zsh; then
fail "a static MLX user token remains"
fi
# No multi-file managed tree under config/
[[ ! -d "$ROOT/config" ]] || fail "obsolete config/ managed tree still exists"
# deploy.sh installs stubs and preserves local.zsh; removes obsolete tree
deploy_home="$TEMP_DIR/deploy-home"
mkdir -p "$deploy_home/.config/zsh/env" "$deploy_home/.local/bin" "$deploy_home/.grok/bin"
printf '%s\n' 'machine-only' >"$deploy_home/.config/zsh/local.zsh"
printf '%s\n' 'stale' >"$deploy_home/.config/zsh/env/00-path.zsh"
printf '%s\n' '#!/bin/sh' 'echo grok-ok' >"$deploy_home/.grok/bin/grok"
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 -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"
[[ ! -e "$deploy_home/.config/zsh/local.zsh.example" ]] || fail "deploy.sh left local.zsh.example in home"
[[ -L "$deploy_home/.local/bin/grok" ]] || fail "deploy.sh did not create grok shim"
# Non-interactive zsh must resolve ~/.local/bin without sourcing .zshrc.
local_bin_home="$TEMP_DIR/local-bin-home"
mkdir -p "$local_bin_home/.local/bin"
printf '%s\n' '#!/bin/sh' 'echo ok' >"$local_bin_home/.local/bin/easyzsh-path-probe"
chmod +x "$local_bin_home/.local/bin/easyzsh-path-probe"
HOME="$local_bin_home" bash "$ROOT/deploy.sh" --home "$local_bin_home" >/dev/null
HOME="$local_bin_home" PATH=/usr/bin:/bin \
zsh -c '. ~/.zshenv; command -v easyzsh-path-probe >/dev/null' ||
fail "~/.local/bin is not available to non-interactive zsh via .zshenv"
# Non-interactive zsh must load always-on ~/.config/zsh/*.zsh (fnm) via .zshenv.
fnm_env_home="$TEMP_DIR/fnm-env-home"
mkdir -p "$fnm_env_home/.local/share/fnm" "$fnm_env_home/.config/zsh"
ln -s /usr/bin/true "$fnm_env_home/.local/share/fnm/fnm"
cp "$ROOT/zsh/fnm.zsh" "$fnm_env_home/.config/zsh/fnm.zsh"
HOME="$fnm_env_home" bash "$ROOT/deploy.sh" --home "$fnm_env_home" >/dev/null
HOME="$fnm_env_home" PATH=/usr/bin:/bin \
zsh -c 'command -v fnm >/dev/null' ||
fail "always-on fnm is not available to non-interactive zsh via .zshenv"
# Non-interactive zsh must resolve pyenv's global Python via .zshenv.
pyenv_env_home="$TEMP_DIR/pyenv-env-home"
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_xdg/zsh/pyenv.zsh"
HOME="$pyenv_env_home" bash "$ROOT/deploy.sh" --home "$pyenv_env_home" >/dev/null
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"
mkdir -p "$path_patch_home" "$path_bin"
cat >"$path_bin/curl" <<'CURL'
#!/bin/bash
out=""
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
[[ "$url" == */zsh/fnm.zsh ]] || exit 1
printf '%s\n' '# patched-fnm' >"$out"
CURL
chmod +x "$path_bin/curl"
# Prefer real coreutils for mktemp/mkdir used by patch.sh
HOME="$path_patch_home" XDG_CONFIG_HOME="$path_patch_home/xdg" PATH="$path_bin:/usr/bin:/bin" \
bash "$ROOT/patch.sh" zsh/fnm >/dev/null
grep -Fxq '# patched-fnm' "$path_patch_home/xdg/zsh/fnm.zsh" ||
fail "patch.sh zsh/fnm did not install ~/.config/zsh/fnm.zsh"
echo "installer checks passed"