diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8279c79..4a71b91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,14 @@ jobs: with: configFile: commitlint.config.mjs - # Lint install.sh: POSIX-only shellcheck plus canonical shfmt formatting. + # Lint the shell that puts a binary on someone's PATH: POSIX-only shellcheck + # plus canonical shfmt formatting. Both scripts here are install-path logic + # guarded by D-0004 — install.sh places a released binary, dev-install.sh + # symlinks a build-tree one — which is why they are held to a standard the + # rest of the repo's shell is not. # Lightweight — stays on GitHub-hosted runners, same as commit-lint. shell-lint: - name: Shell Lint (install.sh) + name: Shell Lint (install scripts) runs-on: ubuntu-latest timeout-minutes: 5 if: github.event.pull_request.draft == false @@ -39,10 +43,10 @@ jobs: uses: taiki-e/install-action@v2 with: tool: shfmt - - name: shellcheck -s sh install.sh - run: shellcheck -s sh install.sh - - name: shfmt -d -s -ln posix -i 2 install.sh - run: shfmt -d -s -ln posix -i 2 install.sh + - name: shellcheck -s sh install.sh dev-install.sh + run: shellcheck -s sh install.sh dev-install.sh + - name: shfmt -d -s -ln posix -i 2 install.sh dev-install.sh + run: shfmt -d -s -ln posix -i 2 install.sh dev-install.sh # Lint install.ps1: PSScriptAnalyzer at Warning severity, install.sh's Windows sibling. # Lightweight — stays on GitHub-hosted runners, same as shell-lint above. `shell: pwsh` diff --git a/.zavet/decisions/D-0004-never-overwrite-a-development-install.md b/.zavet/decisions/D-0004-never-overwrite-a-development-install.md index 7bace5c..603f48d 100644 --- a/.zavet/decisions/D-0004-never-overwrite-a-development-install.md +++ b/.zavet/decisions/D-0004-never-overwrite-a-development-install.md @@ -6,7 +6,7 @@ guards: - cli/dira/src/update/replace.rs - install.sh - install.ps1 - - justfile + - dev-install.sh origin: recorded verified: true --- @@ -20,8 +20,8 @@ only. Never a build-tree binary. ## Why -`just install` symlinks `target/release/{dira,dirad}` into `~/.local/bin`, so -a contributor's PATH entry points into their build tree. Silently replacing +`just install` symlinks `target/release/{dira,dirad}` into `~/.local/bin` (via +`dev-install.sh`), so a contributor's PATH entry points into their build tree. Silently replacing that symlink with a released binary destroys their dev loop in a way that is confusing to diagnose: `cargo build` keeps succeeding, the binary on PATH just stops changing. Overwriting a file *inside* `target/` is worse. The @@ -47,5 +47,11 @@ sees it. never treat `current_exe()` as authoritative for this check. - Any new install-like path must reuse `discover_install` rather than re-deriving the rule. +- The dev install must stay a **symlink**. `dev-install.sh` exists so that + rule sits in one guarded, shellcheck'd file: copying the binaries onto PATH + instead would leave a dev install indistinguishable from a managed one and + silently defeat every refusal above. This guard used to name the whole + `justfile`, which fired on unrelated recipes and taught readers to skim past + it. - Error text must name `just install` as the way to update a dev build, and give the exact commands to switch to released binaries. diff --git a/dev-install.sh b/dev-install.sh new file mode 100644 index 0000000..b93f088 --- /dev/null +++ b/dev-install.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Create the development install: point the PATH entry at the build tree. +# +# Driven by `just link` (and so by `just install`); not a user-facing +# installer. install.sh is the one contributors and users run — this is its +# opposite number, the thing that deliberately puts a build-tree binary on +# PATH. +# +# The symlink is load-bearing, not a convenience. D-0004 has install.sh and +# `dira update` refuse to overwrite a development install, and the only thing +# that distinguishes one is that the PATH entry is a symlink into `target/`: +# `discover_install` reads it with `symlink_metadata`, because `current_exe()` +# resolves symlinks on both Linux and macOS and would see an ordinary managed +# install. Copy the binaries here instead of linking them and that detection +# goes blind — `dira update` would overwrite a contributor's dev build and +# `cargo build` would silently stop affecting the binary on PATH, which is the +# exact failure D-0004 exists to prevent. Keep it a symlink. +# +# Idempotent: safe to re-run, and `ln -sf` re-points an existing link rather +# than nesting one inside it. +set -eu + +repo_dir="${1:?usage: dev-install.sh }" +bin_dir="${2:?usage: dev-install.sh }" + +for name in dira dirad; do + built="$repo_dir/target/release/$name" + if [ ! -x "$built" ]; then + echo "dev-install: $built is missing — run \`just release\` first." >&2 + exit 1 + fi +done + +mkdir -p "$bin_dir" +for name in dira dirad; do + ln -sf "$repo_dir/target/release/$name" "$bin_dir/$name" +done + +echo "Linked dira + dirad -> $bin_dir" diff --git a/justfile b/justfile index d88408f..74e0589 100644 --- a/justfile +++ b/justfile @@ -89,11 +89,15 @@ install: release link daemon-restart @echo "Installed. `dira` + `dirad` are live from {{bin_dir}} (latest build)." # Symlink dira + dirad into {{bin_dir}} (idempotent; safe to re-run). +# +# The symlinking itself lives in dev-install.sh rather than inline here. It is +# install-path logic — D-0004's dev-install detection reads the PATH entry with +# `symlink_metadata` and goes blind the moment these become copies — so it +# belongs in a shellcheck'd script beside install.sh, under that decision's +# guard, instead of in a recipe the guard could only reach by claiming the +# whole justfile and every unrelated recipe in it. link: - mkdir -p "{{bin_dir}}" - ln -sf "{{justfile_directory()}}/target/release/dira" "{{bin_dir}}/dira" - ln -sf "{{justfile_directory()}}/target/release/dirad" "{{bin_dir}}/dirad" - @echo "Linked dira + dirad -> {{bin_dir}}" + sh dev-install.sh "{{justfile_directory()}}" "{{bin_dir}}" # Restart the resident daemon from the freshly built binary. #