diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5168cdeaf..bd0f1e206 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ you would make is not already covered. - [A note on AI-assisted contributions](#a-note-on-ai-assisted-contributions) - [Use the Search, Luke](#use-the-search-luke) - [Commit Guidelines](#commit-guidelines) + - [Plugin And Theme Bootstrap Compatibility](#plugin-and-theme-bootstrap-compatibility) - [Format](#format) - [Style](#style) - [Volunteer](#volunteer) @@ -176,6 +177,24 @@ specification. The automatic changelog tool uses these to automatically generate a changelog based on the commit messages. Here's a guide to writing a commit message to allow this: +### Plugin And Theme Bootstrap Compatibility + +Oh My Zsh now provides a shared bootstrap entrypoint at `lib/bootstrap.zsh` for plugin-manager setups that don't +source `oh-my-zsh.sh`. + +When working on plugins/themes: + +- assume bootstrap has already run in manager-driven setups +- rely on bootstrap guarantees (`ZSH*` defaults, cache/completions setup, required `fpath` entries) +- avoid adding per-plugin/per-theme copies of common setup unless truly plugin-specific + +Common patterns we should migrate away from over time: + +- repeated cache setup logic (`mkdir -p "$ZSH_CACHE_DIR/completions"`) +- repeated base `fpath` bootstrapping for OMZ core/custom directories + +If your plugin/theme still needs custom initialization, keep it scoped to that plugin/theme behavior only. + ### Format ``` diff --git a/README.md b/README.md index d7f143397..af63bc16c 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Twitter), and join us on [Discord](https://discord.gg/ohmyzsh). - [Manual Installation](#manual-installation) - [Installation Problems](#installation-problems) - [Custom Plugins And Themes](#custom-plugins-and-themes) + - [Using Plugin Managers Without `oh-my-zsh.sh`](#using-plugin-managers-without-oh-my-zshsh) - [Enable GNU ls In macOS And FreeBSD Systems](#enable-gnu-ls-in-macos-and-freebsd-systems) - [Skip Aliases](#skip-aliases) - [Async git prompt](#async-git-prompt) @@ -349,6 +350,54 @@ If you have many functions that go well together, you can put them as a `XYZ.plu If you would like to override the functionality of a plugin distributed with Oh My Zsh, create a plugin of the same name in the `custom/plugins/` directory and it will be loaded instead of the one in `plugins/`. +### Using Plugin Managers Without `oh-my-zsh.sh` + +Some plugin managers load individual OMZ plugins/themes directly and do not source `oh-my-zsh.sh`. +For those setups, source this bootstrap entrypoint before loading any OMZ plugin or theme: + +```zsh +source "$ZSH/lib/bootstrap.zsh" +``` + +#### Bootstrap compatibility contract + +`lib/bootstrap.zsh` is idempotent and guarantees: + +- `ZSH`, `ZSH_CUSTOM`, and `ZSH_CACHE_DIR` defaults +- writable cache fallback to `${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh` +- `$ZSH_CACHE_DIR/completions` directory creation +- required OMZ `fpath` entries for plugins/themes: + - `$ZSH/functions` + - `$ZSH/completions` + - `$ZSH_CUSTOM/functions` + - `$ZSH_CUSTOM/completions` + - `$ZSH_CACHE_DIR/completions` +- public signal: `OMZ_IS_BOOTSTRAPPED=true` + +What still requires `oh-my-zsh.sh`: + +- update checks +- plugin auto-discovery from the `plugins=(...)` list +- `compinit`/`compfix` orchestration and zcompdump metadata refresh +- alias filtering via `:omz:*` zstyles during file sourcing +- final full-init signal: `OMZ_IS_LOADED=true` + +#### Plugin manager hook guidance + +Run bootstrap once, before any OMZ plugin/theme load: + +- **Antigen:** add a one-time pre-bundle hook to source `lib/bootstrap.zsh`. +- **Zinit:** use a pre-load/`atinit` hook for OMZ snippets/plugins. +- **zplug:** use a load hook (`hook-load`) before OMZ plugin/theme sourcing. +- **zgen / zulu:** source `lib/bootstrap.zsh` immediately before OMZ plugin/theme load lines. + +Compatibility matrix: + +| Load mode | `OMZ_IS_BOOTSTRAPPED` | `OMZ_IS_LOADED` | Suitable for | +| :-- | :--: | :--: | :-- | +| `source $ZSH/oh-my-zsh.sh` | ✅ | ✅ | Full OMZ framework features | +| `source $ZSH/lib/bootstrap.zsh` + manager-loaded OMZ plugins/themes | ✅ | ❌ | Plugin/theme prerequisites only | + ### Enable GNU ls In macOS And FreeBSD Systems diff --git a/lib/bootstrap.zsh b/lib/bootstrap.zsh new file mode 100644 index 000000000..c08022b61 --- /dev/null +++ b/lib/bootstrap.zsh @@ -0,0 +1,44 @@ +# +# Shared Oh My Zsh bootstrap entrypoint. +# +# This file can be sourced by plugin managers that don't source `oh-my-zsh.sh`. +# It is safe to source multiple times. +# + +# Keep source path for callers that invoke omz_bootstrap again later. +typeset -g _OMZ_BOOTSTRAP_SOURCE="${${(%):-%x}:a}" + +omz_bootstrap() { + # If ZSH is not defined, infer from this file location. + [[ -n "${ZSH:-}" ]] || export ZSH="${_OMZ_BOOTSTRAP_SOURCE:h:h}" + + # Set ZSH_CUSTOM to the path where custom config files and plugins exist. + [[ -n "${ZSH_CUSTOM:-}" ]] || ZSH_CUSTOM="$ZSH/custom" + + # Set cache directory. + [[ -n "${ZSH_CACHE_DIR:-}" ]] || ZSH_CACHE_DIR="$ZSH/cache" + + # Ensure cache dir is writable, otherwise fallback to a HOME-based cache. + if [[ ! -w "$ZSH_CACHE_DIR" ]]; then + ZSH_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh" + fi + + # Create cache and completions dir. + command mkdir -p "$ZSH_CACHE_DIR/completions" + + # Add required OMZ search paths. + local dir + for dir in \ + "$ZSH/functions" \ + "$ZSH/completions" \ + "$ZSH_CUSTOM/functions" \ + "$ZSH_CUSTOM/completions" \ + "$ZSH_CACHE_DIR/completions"; do + (( ${fpath[(Ie)$dir]} )) || fpath=("$dir" $fpath) + done + + # Public signal: OMZ bootstrap has completed. + typeset -g OMZ_IS_BOOTSTRAPPED=true +} + +omz_bootstrap "$@" diff --git a/lib/tests/bootstrap.test.zsh b/lib/tests/bootstrap.test.zsh new file mode 100644 index 000000000..baf1cb919 --- /dev/null +++ b/lib/tests/bootstrap.test.zsh @@ -0,0 +1,88 @@ +#!/usr/bin/zsh -df + +set -u + +bootstrap_file="${0:A:h:h}/bootstrap.zsh" + +_assert() { + local condition="$1" message="$2" + if ! eval "$condition"; then + print -u2 "\e[31mError\e[0m: $message" + return 1 + fi +} + +test_bootstrap_sets_defaults_and_paths() { + local tmp==(:) + mkdir -p "$tmp/ohmyzsh"/{functions,completions,cache} "$tmp/ohmyzsh/custom"/{functions,completions} + + ( + set -e + export ZSH="$tmp/ohmyzsh" + unset ZSH_CUSTOM ZSH_CACHE_DIR OMZ_IS_BOOTSTRAPPED + fpath=() + source "$bootstrap_file" + + _assert '[[ "$ZSH_CUSTOM" == "'"$tmp/ohmyzsh/custom"'" ]]' "ZSH_CUSTOM default should be set" + _assert '[[ "$ZSH_CACHE_DIR" == "'"$tmp/ohmyzsh/cache"'" ]]' "ZSH_CACHE_DIR default should be set" + _assert '[[ -d "'"$tmp/ohmyzsh/cache/completions"'" ]]' "completions dir should be created" + _assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/functions"']}" -gt 0 ]]' "fpath should include OMZ functions dir" + _assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/completions"']}" -gt 0 ]]' "fpath should include OMZ completions dir" + _assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/custom/functions"']}" -gt 0 ]]' "fpath should include custom functions dir" + _assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/custom/completions"']}" -gt 0 ]]' "fpath should include custom completions dir" + _assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/cache/completions"']}" -gt 0 ]]' "fpath should include cache completions dir" + _assert '[[ "$OMZ_IS_BOOTSTRAPPED" == true ]]' "bootstrap signal should be set" + ) +} + +test_bootstrap_is_idempotent() { + local tmp==(:) + mkdir -p "$tmp/ohmyzsh"/{functions,completions,cache} "$tmp/ohmyzsh/custom"/{functions,completions} + + ( + set -e + export ZSH="$tmp/ohmyzsh" + unset ZSH_CUSTOM ZSH_CACHE_DIR OMZ_IS_BOOTSTRAPPED + fpath=() + source "$bootstrap_file" + source "$bootstrap_file" + omz_bootstrap + + _assert '[[ ${#fpath} -eq ${#${(u)fpath}} ]]' "fpath entries should not duplicate after repeated bootstrap" + _assert '[[ "$OMZ_IS_BOOTSTRAPPED" == true ]]' "bootstrap signal should remain true" + ) +} + +test_bootstrap_uses_writable_cache_fallback() { + local tmp==(:) + mkdir -p "$tmp/ohmyzsh"/{functions,completions} "$tmp/ohmyzsh/custom"/{functions,completions} + mkdir -p "$tmp/no-write" "$tmp/xdg-cache" + chmod 0555 "$tmp/no-write" + + ( + set -e + export ZSH="$tmp/ohmyzsh" + export XDG_CACHE_HOME="$tmp/xdg-cache" + export ZSH_CACHE_DIR="$tmp/no-write" + unset ZSH_CUSTOM OMZ_IS_BOOTSTRAPPED + fpath=() + source "$bootstrap_file" + + _assert '[[ "$ZSH_CACHE_DIR" == "'"$tmp/xdg-cache/oh-my-zsh"'" ]]' "cache dir should fallback when not writable" + _assert '[[ -d "'"$tmp/xdg-cache/oh-my-zsh/completions"'" ]]' "fallback completions dir should be created" + _assert '[[ "${fpath[(Ie)'"$tmp/xdg-cache/oh-my-zsh/completions"']}" -gt 0 ]]' "fpath should include fallback completions dir" + ) +} + +tests=( + test_bootstrap_sets_defaults_and_paths + test_bootstrap_is_idempotent + test_bootstrap_uses_writable_cache_fallback +) + +for test_name in $tests; do + print -u2 "Test: $test_name" + "$test_name" || exit 1 + print -u2 "\e[32mSuccess\e[0m" + print -u2 "" +done diff --git a/oh-my-zsh.sh b/oh-my-zsh.sh index b4e95e0ff..6538f8400 100644 --- a/oh-my-zsh.sh +++ b/oh-my-zsh.sh @@ -50,31 +50,14 @@ unset -f omz_f # If ZSH is not defined, use the current script's directory. [[ -n "$ZSH" ]] || export ZSH="${${(%):-%x}:a:h}" -# Set ZSH_CUSTOM to the path where your custom config files -# and plugins exists, or else we will use the default custom/ -[[ -n "$ZSH_CUSTOM" ]] || ZSH_CUSTOM="$ZSH/custom" - -# Set ZSH_CACHE_DIR to the path where cache files should be created -# or else we will use the default cache/ -[[ -n "$ZSH_CACHE_DIR" ]] || ZSH_CACHE_DIR="$ZSH/cache" - -# Make sure $ZSH_CACHE_DIR is writable, otherwise use a directory in $HOME -if [[ ! -w "$ZSH_CACHE_DIR" ]]; then - ZSH_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh" -fi - -# Create cache and completions dir and add to $fpath -mkdir -p "$ZSH_CACHE_DIR/completions" -(( ${fpath[(Ie)$ZSH_CACHE_DIR/completions]} )) || fpath=("$ZSH_CACHE_DIR/completions" $fpath) +# Shared, manager-agnostic bootstrap. +source "$ZSH/lib/bootstrap.zsh" # Check for updates on initial load... source "$ZSH/tools/check_for_upgrade.sh" # Initializes Oh My Zsh -# add a function path -fpath=($ZSH/{functions,completions} $ZSH_CUSTOM/{functions,completions} $fpath) - # Load all stock functions (from $fpath files) called below. autoload -U compaudit compinit zrecompile @@ -234,3 +217,6 @@ fi # set completion colors to be the same as `ls`, after theme has been loaded [[ -z "$LS_COLORS" ]] || zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" + +# Public signal: full Oh My Zsh init script has completed. +typeset -g OMZ_IS_LOADED=true