Files
easyzsh/tests/installers.sh
Yuhao Qing d5afd25bbe perf(pyenv): initialize shell integration once without rehash
Login shells source the fragment twice to restore shim precedence after the system profile. Reapplying PATH does not require repeating shell integration or rebuilding shims.
2026-09-08 00:41:08 +08:00

282 lines
14 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 '${path:#$HOME/.grok/bin}' "$ROOT/.zshenv" || fail ".zshenv does not drop ~/.grok/bin"
grep -Fq '${path:#$HOME/.grok/bin}' "$ROOT/.zshrc" || fail ".zshrc does not drop ~/.grok/bin"
grep -Fq 'alias agent=$HOME/.local/bin/cursor-agent' "$ROOT/.zshrc" || fail ".zshrc does not alias agent to Cursor"
if grep -Eq '^[[:space:]]*(export[[:space:]]+)?PATH=.*\.grok/bin' "$ROOT/.zshenv" "$ROOT/.zshrc"; then
fail "easyzsh stubs must not prepend ~/.grok/bin"
fi
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"
# Grok's `agent` must not keep the name when Cursor is absent.
printf '%s\n' '#!/bin/sh' 'echo grok-agent' >"$deploy_home/.grok/bin/agent"
chmod +x "$deploy_home/.grok/bin/agent"
ln -sfn "$deploy_home/.grok/bin/agent" "$deploy_home/.local/bin/agent"
HOME="$deploy_home" bash "$ROOT/deploy.sh" --home "$deploy_home" >/dev/null
[[ ! -e "$deploy_home/.local/bin/agent" ]] || fail "deploy.sh left Grok bound to agent without Cursor"
# Cursor takes `agent`; Grok stays on `grok`.
printf '%s\n' '#!/bin/sh' 'echo cursor-agent' >"$deploy_home/.local/bin/cursor-agent"
chmod +x "$deploy_home/.local/bin/cursor-agent"
ln -sfn "$deploy_home/.grok/bin/agent" "$deploy_home/.local/bin/agent"
HOME="$deploy_home" bash "$ROOT/deploy.sh" --home "$deploy_home" >/dev/null
[[ "$(readlink "$deploy_home/.local/bin/agent")" == "$deploy_home/.local/bin/cursor-agent" ]] ||
fail "deploy.sh did not point agent at cursor-agent"
[[ "$(readlink "$deploy_home/.local/bin/grok")" == "$deploy_home/.grok/bin/grok" ]] ||
fail "deploy.sh did not keep the grok shim"
# .zshenv repairs a Grok-stolen agent without deploy.
ln -sfn "$deploy_home/.grok/bin/agent" "$deploy_home/.local/bin/agent"
HOME="$deploy_home" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv' ||
fail ".zshenv failed while repairing agent"
[[ "$(readlink "$deploy_home/.local/bin/agent")" == "$deploy_home/.local/bin/cursor-agent" ]] ||
fail ".zshenv did not retarget a Grok-stolen agent"
# 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"
# Interactive initialization runs once while repeated sourcing restores PATH.
mkdir -p "$pyenv_env_home/.pyenv/bin"
cat >"$pyenv_env_home/.pyenv/bin/pyenv" <<'PYENV'
#!/bin/sh
printf '%s\n' "$*" >> "$PYENV_ROOT/init-calls"
printf '%s\n' 'pyenv() { command pyenv "$@"; }'
PYENV
chmod +x "$pyenv_env_home/.pyenv/bin/pyenv"
HOME="$pyenv_env_home" PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \
zsh -dfic '
source "$1"
(( $+functions[pyenv] )) || exit 1
path=(/usr/bin /bin)
source "$1"
[[ $(command -v python3) == "$PYENV_ROOT/shims/python3" ]]
' zsh "$ROOT/zsh/pyenv.zsh" ||
fail "interactive pyenv initialization did not preserve its function and Python PATH"
printf '%s\n' 'init - --no-rehash zsh' | cmp -s - "$pyenv_env_home/.pyenv/init-calls" ||
fail "pyenv initialization must run once without a startup rehash"
# 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"