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() {