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.
232 lines
12 KiB
Bash
Executable File
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"
|