diff --git a/.github/workflows/reusable-deb-build.yml b/.github/workflows/reusable-deb-build.yml index 7357cdf..5192db7 100644 --- a/.github/workflows/reusable-deb-build.yml +++ b/.github/workflows/reusable-deb-build.yml @@ -62,7 +62,7 @@ jobs: tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT dpkg-deb -x "$pkg" "$tmpdir" - ./scripts/verify-package-assets.sh "$tmpdir" + ./scripts/verify-package-assets.sh "$tmpdir" usr/share/zsh/vendor-completions - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: diff --git a/.github/workflows/reusable-rpm-build.yml b/.github/workflows/reusable-rpm-build.yml index 2edd096..fe82dec 100644 --- a/.github/workflows/reusable-rpm-build.yml +++ b/.github/workflows/reusable-rpm-build.yml @@ -60,7 +60,7 @@ jobs: tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT (cd "$tmpdir" && rpm2cpio "$pkg" | cpio -idm --quiet) - ./scripts/verify-package-assets.sh "$tmpdir" + ./scripts/verify-package-assets.sh "$tmpdir" usr/share/zsh/site-functions build-rpm-fedora: runs-on: ubuntu-24.04 @@ -92,7 +92,7 @@ jobs: tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT (cd "$tmpdir" && rpm2cpio "$pkg" | cpio -idm --quiet) - ./scripts/verify-package-assets.sh "$tmpdir" + ./scripts/verify-package-assets.sh "$tmpdir" usr/share/zsh/site-functions - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: diff --git a/Makefile b/Makefile index 445d0a9..54eee07 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,11 @@ LINT_TMP_DIR := ./.tmp LINT_GO_CACHE := $(LINT_TMP_DIR)/go-build LINT_GOLANGCI_CACHE := $(LINT_TMP_DIR)/golangci-lint-cache BASE_REF ?= +# Completion install roots follow the XDG base directory spec, like +# scripts/clean-state.sh, so install and cleanup agree when XDG_DATA_HOME is set. +DATA_HOME ?= $(or $(XDG_DATA_HOME),$(HOME)/.local/share) +ZSH_COMPLETION_DIR := $(DATA_HOME)/zsh/site-functions +BASH_COMPLETION_DIR := $(DATA_HOME)/bash-completion/completions check-go: @command -v go >/dev/null 2>&1 || { echo "Go is not installed. Install Go 1.24+ from https://go.dev/dl/"; exit 1; } @@ -36,6 +41,7 @@ build: check-go completions: mkdir -p $(COMPLETIONS_DIR) $(BIN_DIR)/$(BINARY) completion bash > $(COMPLETIONS_DIR)/enclave + $(BIN_DIR)/$(BINARY) completion zsh > $(COMPLETIONS_DIR)/_enclave # make install now installs only the self-contained binary. This fixed path is # used solely for conservative cleanup of assets staged by older source @@ -62,8 +68,13 @@ install-binary: clean-legacy-assets mv -f "$(INSTALL_BIN)/$(BINARY).new" "$(INSTALL_BIN)/$(BINARY)" ifeq ($(UNAME_S),Linux) # freedesktop shell completion (Linux only). - mkdir -p "$(HOME)/.local/share/bash-completion/completions" - cp $(COMPLETIONS_DIR)/enclave "$(HOME)/.local/share/bash-completion/completions/enclave" + mkdir -p "$(BASH_COMPLETION_DIR)" + cp $(COMPLETIONS_DIR)/enclave "$(BASH_COMPLETION_DIR)/enclave" + mkdir -p "$(ZSH_COMPLETION_DIR)" + cp $(COMPLETIONS_DIR)/_enclave "$(ZSH_COMPLETION_DIR)/_enclave" + @# zsh has no user-level directory on its default fpath, so a completion + @# installed under the home directory is only found once ~/.zshrc adds it. + @command -v zsh >/dev/null 2>&1 && echo 'zsh completion installed; it is only picked up if ~/.zshrc has "fpath=($(ZSH_COMPLETION_DIR) $$fpath)" before whatever runs compinit' || true endif @echo "Installed $(INSTALL_BINARY_LABEL) to $(INSTALL_BIN)/$(BINARY)" @@ -73,7 +84,9 @@ clean-legacy-assets: uninstall: rm -f "$(INSTALL_BIN)/$(BINARY)" ifeq ($(UNAME_S),Linux) - rm -f "$(HOME)/.local/share/bash-completion/completions/enclave" + rm -f "$(BASH_COMPLETION_DIR)/enclave" + rm -f "$(ZSH_COMPLETION_DIR)/_enclave" + @command -v zsh >/dev/null 2>&1 && echo 'zsh completion removed; the matching "fpath=($(ZSH_COMPLETION_DIR) ...)" line in ~/.zshrc is now unused' || true endif @echo "Uninstalled $(BINARY)" diff --git a/README.md b/README.md index 616822d..b9ea58c 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,24 @@ is on your `PATH`. On macOS the default is `/usr/local/bin/`; copying there may need `sudo` or a writable `/usr/local/bin`. Override the destination with `make install INSTALL_BIN=...`. +On Linux it also installs shell completions: bash into +`~/.local/share/bash-completion/completions/`, which is picked up automatically, +and zsh into `~/.local/share/zsh/site-functions/`, which zsh only searches once +you add it to `fpath` in `~/.zshrc`, before whatever runs `compinit`: + +```zsh +fpath=(~/.local/share/zsh/site-functions $fpath) +``` + +Frameworks such as oh-my-zsh and prezto call `compinit` themselves, so put the +line above their setup and leave the rest to them. On a plain zsh setup that +does not call it anywhere, add `autoload -U compinit && compinit` after the +`fpath` line. Both completion paths honor `XDG_DATA_HOME` if you set it. + +The `.deb` and `.rpm` packages install both completions into system directories +that need no such setup. Any shell can also generate its own script on demand +with `enclave completion `. + ## Quick Start Run in any project directory: diff --git a/debian/rules b/debian/rules index a5063e1..a8f93fb 100755 --- a/debian/rules +++ b/debian/rules @@ -23,6 +23,7 @@ override_dh_auto_build: go build -tags enclave_no_embed -mod=vendor -o bin/enclave ./cmd/enclave mkdir -p completions bin/enclave completion bash > completions/enclave + bin/enclave completion zsh > completions/_enclave override_dh_auto_install: # Install binary @@ -70,9 +71,12 @@ override_dh_auto_install: cp -r "$$path" "$$dest/"; \ done < $(GATEWAY_PROXY_SOURCE_PATHS_FILE) - # Install bash completions + # Install shell completions. Debian searches zsh/vendor-completions for + # distro-packaged functions; zsh/site-functions under /usr is not on fpath. install -D -m 0644 completions/enclave \ $(CURDIR)/debian/enclave/usr/share/bash-completion/completions/enclave + install -D -m 0644 completions/_enclave \ + $(CURDIR)/debian/enclave/usr/share/zsh/vendor-completions/_enclave # Install documentation install -D -m 0644 README.md $(CURDIR)/debian/enclave/usr/share/doc/enclave/README.md diff --git a/packaging/rpm/enclave.spec b/packaging/rpm/enclave.spec index 6b2b64f..2200f88 100644 --- a/packaging/rpm/enclave.spec +++ b/packaging/rpm/enclave.spec @@ -46,6 +46,7 @@ mkdir -p bin completions # cgo so RPMs built on Debian or Ubuntu do not acquire host glibc requirements. CGO_ENABLED=0 go build -tags enclave_no_embed -mod=vendor -o bin/enclave ./cmd/enclave bin/enclave completion bash > completions/enclave +bin/enclave completion zsh > completions/_enclave %install app_root="%{buildroot}%{_datadir}/%{name}" @@ -88,6 +89,8 @@ done < internal/gateway/gateway_proxy_build_inputs.txt install -D -m 0644 completions/enclave \ "%{buildroot}%{_datadir}/bash-completion/completions/enclave" +install -D -m 0644 completions/_enclave \ + "%{buildroot}%{_datadir}/zsh/site-functions/_enclave" install -D -m 0644 README.md "$doc_root/README.md" install -D -m 0644 docs/ARCHITECTURE.md "$doc_root/ARCHITECTURE.md" @@ -107,6 +110,7 @@ rm -rf "%{buildroot}" %{_bindir}/enclave %{_datadir}/%{name} %{_datadir}/bash-completion/completions/enclave +%{_datadir}/zsh/site-functions/_enclave %dir %{_datadir}/doc/%{name} %doc %{_datadir}/doc/%{name}/README.md %doc %{_datadir}/doc/%{name}/ARCHITECTURE.md diff --git a/scripts/check-license-headers.sh b/scripts/check-license-headers.sh index b6ed203..6fb2180 100755 --- a/scripts/check-license-headers.sh +++ b/scripts/check-license-headers.sh @@ -24,7 +24,7 @@ is_candidate() { *.go | *.sh | *.yaml | *.yml | *.js | *.ts | *.css | *.html | *.vue | *.puml | *.toml | *.conf | *.spec) return 0 ;; - go.mod | Makefile | completions/enclave | .dockerignore | */.gitignore | .gitignore | */Dockerfile | Dockerfile | Dockerfile.* | debian/rules | debian/control) + go.mod | Makefile | completions/enclave | completions/_enclave | .dockerignore | */.gitignore | .gitignore | */Dockerfile | Dockerfile | Dockerfile.* | debian/rules | debian/control) return 0 ;; esac diff --git a/scripts/clean-state.sh b/scripts/clean-state.sh index 62b4e6e..95f217f 100755 --- a/scripts/clean-state.sh +++ b/scripts/clean-state.sh @@ -241,6 +241,9 @@ add_data_install_paths() { [[ -n "$data_root" ]] || return 0 add_install_path "${data_root}/${APP}" add_install_path "${data_root}/bash-completion/completions/${APP}" + # Fedora and source installs use site-functions, Debian vendor-completions. + add_install_path "${data_root}/zsh/site-functions/_${APP}" + add_install_path "${data_root}/zsh/vendor-completions/_${APP}" add_install_path "${data_root}/applications/${APP}.desktop" for size in "${ICON_SIZES[@]}"; do add_install_path "${data_root}/icons/hicolor/${size}x${size}/apps/${APP}.png" diff --git a/scripts/verify-package-assets.sh b/scripts/verify-package-assets.sh index 368cd50..636ba7f 100755 --- a/scripts/verify-package-assets.sh +++ b/scripts/verify-package-assets.sh @@ -11,13 +11,14 @@ set -eu CDPATH= export CDPATH -if [ "$#" -ne 1 ]; then - echo "usage: $0 " >&2 +if [ "$#" -ne 2 ]; then + echo "usage: $0 " >&2 exit 2 fi repo_root=$(cd -P "$(dirname "$0")/.." && pwd) package_root=$1 +zsh_completion_dir=$2 app_root="$package_root/usr/share/enclave" [ -f "$app_root/.dockerignore" ] @@ -29,6 +30,15 @@ diff -qr "$repo_root/docs" "$app_root/docs" [ -f "$package_root/usr/share/doc/enclave/LICENSE.md" ] [ -f "$package_root/usr/share/doc/enclave/NOTICE.md" ] +# Completions are only found if they sit in a directory the shell searches by +# default, and for zsh that directory differs per distro: Debian carries +# zsh/vendor-completions on its default fpath and deliberately omits +# zsh/site-functions, Fedora the other way round. The caller passes the layout +# its packaging is expected to produce so the wrong one fails instead of +# silently shipping a completion no shell ever loads. +[ -f "$package_root/usr/share/bash-completion/completions/enclave" ] +[ -f "$package_root/$zsh_completion_dir/_enclave" ] + # Assets must reach the package byte-for-byte. Packaging toolchains rewrite # files they mistake for host executables -- Fedora's brp-mangle-shebangs # rewrites /bin/sh to /usr/bin/sh, which does not exist in the Alpine gateway