From 767aaebd6e28d9bded5c823a99a48b91da420cdc Mon Sep 17 00:00:00 2001 From: Sal Date: Thu, 3 Sep 2026 04:19:55 +0100 Subject: [PATCH] fix(init): complete home preparation and keep it retryable .zi-prepare-home set ZI[HOME_READY]=1 on its second line, before doing any directory work, and created ZI[SERVICES_DIR] only inside the branch guarded on ZI[SNIPPETS_DIR] being absent. A tree with snippets/ present and services/ missing, which a migration or a manual repair can leave behind, therefore completed without creating the services directory and was still recorded as ready: PROBE before: snippets=1 services=0 PROBE prepare rc=0 PROBE after: services=0 PROBE HOME_READY=1 Service startup then places locks and FIFOs below a directory that does not exist, and the flag prevents any later call from repairing the layout. Give the services directory its own condition, validate the required set before recording readiness, and set ZI[HOME_READY] only after that passes. A failure now reports which directory is missing and leaves the flag unset, so a later call retries. $ZPFX and the manual directory stay best-effort. They are user-facing install prefixes rather than Zi's own state, and the manual tree has always tolerated a creation failure through `mkdir 2>/dev/null'. Treating them as required would turn a read-only prefix into a startup failure for shells that work today. tests/home-preparation.zsh covers the partial layout, a required directory that cannot be created, the retry after the cause is removed, and the no-op on an already-ready home. It skips itself under root, where the permission case cannot hold. Against the previous function it reports `partial layout: the services directory was not created'. Closes #448 --- .github/workflows/zsh-n.yml | 13 +++++ tests/home-preparation.zsh | 98 +++++++++++++++++++++++++++++++++++++ zi.zsh | 27 +++++++++- 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tests/home-preparation.zsh diff --git a/.github/workflows/zsh-n.yml b/.github/workflows/zsh-n.yml index fbf550d..152105b 100644 --- a/.github/workflows/zsh-n.yml +++ b/.github/workflows/zsh-n.yml @@ -13,6 +13,7 @@ on: - "tests/atinit-deferred-marker.zsh" - "tests/archive-extraction.zsh" - "tests/completion-refresh.zsh" + - "tests/home-preparation.zsh" - "tests/hook-ownership.zsh" - "tests/load-object-status.zsh" - "tests/message-formatting.zsh" @@ -40,6 +41,7 @@ on: - "tests/atinit-deferred-marker.zsh" - "tests/archive-extraction.zsh" - "tests/completion-refresh.zsh" + - "tests/home-preparation.zsh" - "tests/hook-ownership.zsh" - "tests/load-object-status.zsh" - "tests/message-formatting.zsh" @@ -291,6 +293,17 @@ jobs: - name: Test completion refresh run: zsh tests/completion-refresh.zsh + home-preparation: + name: Home Preparation + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Zsh + run: sudo apt update && sudo apt-get install -yq zsh + - name: Test home preparation + run: zsh -f tests/home-preparation.zsh + hook-ownership: name: Hook ownership runs-on: ubuntu-latest diff --git a/tests/home-preparation.zsh b/tests/home-preparation.zsh new file mode 100644 index 0000000..3b2f463 --- /dev/null +++ b/tests/home-preparation.zsh @@ -0,0 +1,98 @@ +#!/usr/bin/env zsh +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et + +builtin emulate -R zsh +setopt pipe_fail + +fail() { + builtin print -u2 -r -- "not ok - $1" + exit 1 +} + +typeset project_root="${ZI_TEST_CHECKOUT:-${0:A:h:h}}" +typeset temp_root +temp_root="$(command mktemp -d "${TMPDIR:-/tmp}/zi-home-prep-test.XXXXXXXX")" || + fail "create temporary directory" +# The failure case makes a directory read-only; restore it so cleanup can work. +trap 'command chmod -R u+rwX -- "$temp_root" 2>/dev/null; command rm -rf -- "$temp_root"' EXIT INT TERM + +command mkdir -p \ + "${temp_root}/home" \ + "${temp_root}/cache" \ + "${temp_root}/config" \ + "${temp_root}/data" \ + "${temp_root}/zdotdir" || fail "create isolated environment" + +# Running as root would bypass the permission case entirely. +if [[ ${EUID:-$UID} -eq 0 ]]; then + builtin print -r -- "ok - skipped, the permission case is meaningless as root" + exit 0 +fi + +env \ + HOME="${temp_root}/home" \ + XDG_CACHE_HOME="${temp_root}/cache" \ + XDG_CONFIG_HOME="${temp_root}/config" \ + XDG_DATA_HOME="${temp_root}/data" \ + ZDOTDIR="${temp_root}/zdotdir" \ + ZI_TEST_CHECKOUT="$project_root" \ + zsh -f <<'ZSH' || fail "home preparation is not complete, propagating or retryable" +builtin emulate -R zsh +setopt pipe_fail + +builtin source "${ZI_TEST_CHECKOUT}/zi.zsh" >/dev/null 2>&1 || return 1 + +# A partial layout. The services directory used to be created only inside the +# branch guarded on the snippets directory being absent, so a tree with +# snippets/ present and services/ missing was accepted as ready. +command rm -rf "${ZI[SERVICES_DIR]}" +[[ -d ${ZI[SNIPPETS_DIR]} ]] || { + builtin print -u2 -r -- "fixture: the snippets directory should still exist" + return 1 +} +unset 'ZI[HOME_READY]' +.zi-prepare-home || { + builtin print -u2 -r -- "partial layout: preparation reported failure" + return 1 +} +[[ -d ${ZI[SERVICES_DIR]} ]] || { + builtin print -u2 -r -- "partial layout: the services directory was not created" + return 1 +} + +# A required directory that cannot be created has to fail, and must not mark the +# home ready, so a later call can retry. +command rm -rf "${ZI[SERVICES_DIR]}" +command chmod 500 "${ZI[HOME_DIR]}" +unset 'ZI[HOME_READY]' +.zi-prepare-home 2>/dev/null && { + command chmod 700 "${ZI[HOME_DIR]}" + builtin print -u2 -r -- "unwritable home: preparation reported success" + return 1 +} +[[ -z ${ZI[HOME_READY]} ]] || { + command chmod 700 "${ZI[HOME_DIR]}" + builtin print -u2 -r -- "unwritable home: ZI[HOME_READY] was set despite failure" + return 1 +} + +# Retry after the cause is removed. +command chmod 700 "${ZI[HOME_DIR]}" +.zi-prepare-home || { + builtin print -u2 -r -- "retry: preparation still reported failure" + return 1 +} +[[ -d ${ZI[SERVICES_DIR]} && -n ${ZI[HOME_READY]} ]] || { + builtin print -u2 -r -- "retry: the home was not completed" + return 1 +} + +# An already-ready home is a cheap no-op. +.zi-prepare-home || { + builtin print -u2 -r -- "second call on a ready home reported failure" + return 1 +} +ZSH + +builtin print -r -- "ok - home preparation completes partial layouts, propagates failure and stays retryable" diff --git a/zi.zsh b/zi.zsh index 82d749d..6f09b97 100644 --- a/zi.zsh +++ b/zi.zsh @@ -1380,8 +1380,7 @@ builtin setopt no_aliases # FUNCTION: .zi-prepare-home. [[[ # Establish all required directories. .zi-prepare-home() { - [[ -n ${ZI[HOME_READY]} ]] && return - ZI[HOME_READY]=1 + [[ -n ${ZI[HOME_READY]} ]] && return 0 if [[ ! -d ${ZI[HOME_DIR]} ]]; then command mkdir -p "${ZI[HOME_DIR]}" command chmod 700 "${ZI[HOME_DIR]}" @@ -1440,9 +1439,33 @@ builtin setopt no_aliases command mkdir -p "${ZI[SNIPPETS_DIR]}/OMZ::plugins" command chmod go-w "${ZI[SNIPPETS_DIR]}" ( builtin cd -q ${ZI[SNIPPETS_DIR]}; command ln -s OMZ::plugins plugins; ) + fi + # Independently of the snippets directory. These were nested, so a layout with + # snippets/ already present and services/ missing, which a migration or a + # manual repair can leave behind, never created the services directory and was + # still accepted as ready. Service startup then tried to place locks and FIFOs + # below a directory that did not exist. + if [[ ! -d ${ZI[SERVICES_DIR]} ]]; then command mkdir -p "${ZI[SERVICES_DIR]}" command chmod go-w "${ZI[SERVICES_DIR]}" fi + # Validate before declaring the home ready, and leave ZI[HOME_READY] unset on + # failure so a later call can retry. $ZPFX and the manual directory stay + # best-effort: they are user-facing install prefixes rather than Zi's own + # state, and the manual tree has always tolerated a creation failure. + local ___required + for ___required ( + ${ZI[HOME_DIR]} ${ZI[CACHE_DIR]} ${ZI[CONFIG_DIR]} ${ZI[LOG_DIR]} + ${ZI[ZMODULES_DIR]} ${ZI[PLUGINS_DIR]} ${ZI[PLUGINS_DIR]}/_local---zi + ${ZI[COMPLETIONS_DIR]} ${ZI[SNIPPETS_DIR]} ${ZI[SERVICES_DIR]} + ) { + [[ -d $___required ]] || { + builtin print -u2 -r -- "zi: home preparation incomplete, missing: ${___required}" + return 1 + } + } + ZI[HOME_READY]=1 + return 0 } # ]]] # FUNCTION: .zi-load-object. [[[ .zi-load-object() {