From 6685c83730336438de09fc076fa03995aabbbbcb Mon Sep 17 00:00:00 2001 From: Sal Date: Fri, 18 Sep 2026 08:02:37 +0100 Subject: [PATCH] fix(loader): keep helpers when an optional stage fails zzinit removed its helper functions and itself before checking the status of the completion and zpmod stages, so a failed zmodload left the user with a "run `zzinit` again" contract and nothing to call. Return before the unset when either optional stage failed, remove the helpers only on success, and extend the zpmod diagnostic with the retry hint the source-stage failure path already gives. Add a fixture that plants an unloadable zpmod.so, observes the failed run keeping zzinit defined, and proves the retry succeeds and cleans up. Closes #200 --- public/checksum.txt | 2 +- public/zsh/init.zsh | 9 ++++++--- tests/installers.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/public/checksum.txt b/public/checksum.txt index ae73b25..2735178 100644 --- a/public/checksum.txt +++ b/public/checksum.txt @@ -1,4 +1,4 @@ 6de66efba021ebcf462e7672577d9f6876f13db55691f04e7009ff7e78d658b8 public/sh/install_zpmod.sh 6e753a0314ba9f2e4f0a2c429798ca3767af4fb94c669e6923d161d6af7aa745 public/sh/install.sh 08cc893ceb982fc99d17db1966c6c30790cc571e16e4f5392352d995f5252952 public/sh/sync-init.sh -e5db3eb099daf0098bed263f4f1960adc9d3d0074a19260fae7ab3df8bf08573 public/zsh/init.zsh +c979e39748d1d86ace17a61ff2b1bf6e1224a43291c25bf7fad985d1e9e11af1 public/zsh/init.zsh diff --git a/public/zsh/init.zsh b/public/zsh/init.zsh index 0847867..ce998e1 100755 --- a/public/zsh/init.zsh +++ b/public/zsh/init.zsh @@ -295,7 +295,7 @@ _zi_pmod() { module_path+=( "$module_dir" ) if ! zmodload zi/zpmod 2>/dev/null; then [[ ${ZI[MUTE_WARNINGS]} == 1 ]] || - _zi_err "zpmod.so is present but zmodload zi/zpmod failed; rebuild it with \`zi module build\`." + _zi_err "zpmod.so is present but zmodload zi/zpmod failed; rebuild it with \`zi module build\`, then run \`zzinit\` again." return 1 fi return 0 @@ -329,8 +329,11 @@ zzinit() { _zi_pmod status_pmod=$? + # Zi is usable at this point, but an optional stage reported a problem. Keep + # the helpers so the diagnostics above can be acted on and zzinit re-run. + (( status_comps == 0 && status_pmod == 0 )) || return 1 + # Helpers are only removed on success so a failed run stays retryable. unset -f _zi_err _zi_fetch _zi_check_stream _zi_setup _zi_source _zi_comps _zi_pmod zzinit 2>/dev/null - - (( status_comps == 0 && status_pmod == 0 )) + return 0 } diff --git a/tests/installers.sh b/tests/installers.sh index f27d812..e8955d2 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -191,6 +191,45 @@ test_init_rejects_invalid_stream() { pass "invalid ZI[STREAM] is rejected before reaching git" } +test_init_keeps_helpers_when_zpmod_fails() { + values_log="${TMP_ROOT}/init-zpmod-values" + err_log="${TMP_ROOT}/init-zpmod-err" + + # zi.zsh loads, but an optional stage fails: zpmod.so exists and cannot be + # loaded. The helpers and zzinit itself must survive so that, once the module + # is rebuilt, the advertised `zzinit` retry has something to call. + zsh -f -c ' + typeset -ghA ZI + ZI[LOADER_HISTORY]=0 + ZI[HOME_DIR]="$4/zpmod-home" + ZI[BIN_DIR]="$4/zpmod-home/bin" + module_dir="${ZI[HOME_DIR]}/zmodules/zpmod/Src/zi" + command mkdir -p "${ZI[BIN_DIR]}" "$module_dir" + print -r -- "# fake zi.zsh" > "${ZI[BIN_DIR]}/zi.zsh" + # A file that is not a shared object: zmodload refuses it. + print -r -- "not a shared object" > "$module_dir/zpmod.so" + + source "$1" + zzinit 2>"$3" + print -r -- "first_status:$?" >"$2" + print -r -- "first_retryable:${+functions[zzinit]}" >>"$2" + + command rm -f -- "$module_dir/zpmod.so" + zzinit 2>>"$3" + print -r -- "second_status:$?" >>"$2" + print -r -- "second_retryable:${+functions[zzinit]}" >>"$2" + ' zsh "${ROOT}/public/zsh/init.zsh" "${values_log}" "${err_log}" "${TMP_ROOT}" + + contains "${values_log}" 'first_status:1' + contains "${err_log}" 'rebuild it with' + # The failed run keeps zzinit defined so the user can rebuild and retry. + contains "${values_log}" 'first_retryable:1' + # The retry succeeds and only then removes the helpers. + contains "${values_log}" 'second_status:0' + contains "${values_log}" 'second_retryable:0' + pass "optional zpmod failure keeps zzinit retryable" +} + test_init_progress_filter_url() { # The loader downloads this file and executes it. A 404 previously aborted # every clean install with no diagnostic, so the path is asserted here. @@ -639,6 +678,7 @@ test_init_defaults_are_single_arguments test_init_preserves_caller_options test_init_history_opt_out test_init_rejects_invalid_stream +test_init_keeps_helpers_when_zpmod_fails test_init_progress_filter_url test_init_uses_private_tempdir test_init_path_resolution