From 098233ec053a072bd99c6e1b05c5311d0624488c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Labont=C3=A9?= Date: Tue, 3 Nov 2026 08:20:10 -0500 Subject: [PATCH] fix(component-contract): check-gen-contract.sh finds the engine beside specfuse The script only tried `python3`, so it failed on a pipx or uv install, where the engine lives in the tool's own environment, and told the user to pip-install into an interpreter that may refuse it. Under the loop that is a failed gate on every attempt. Try the interpreter beside the `specfuse` command first, when it has the engine, and name pipx and uv in the hint. SPECFUSE_PYTHON still wins. CI pip-installs the engine into python3, the one case that worked, so the template test stubs python3. Seeded scripts are owned by the component, so existing ones keep the old lookup; the guide says how to pick this up. Co-Authored-By: Claude Opus 5.5 --- docs/csharp-component-guide.md | 2 +- scripts/test-csharp-contract-template.sh | 40 +++++++++++++++++++ templates/component-contract/csharp/README.md | 2 +- .../csharp/scripts/check-gen-contract.sh | 37 ++++++++++++++--- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/docs/csharp-component-guide.md b/docs/csharp-component-guide.md index 23c59fd..df59541 100644 --- a/docs/csharp-component-guide.md +++ b/docs/csharp-component-guide.md @@ -318,7 +318,7 @@ The kit ships a starter for C# in [`templates/component-contract/csharp/`](../te **The filesets assume the layout of §1:** project folders at the repo root, named `.Domain`, `.Infrastructure` and `.Api`, test projects ending in `.Test` or `Tests`. A repo laid out differently (projects under `src/`, a layer named otherwise) edits the `filesets:` block once when seeding; a scope that matches nothing reports an error rather than passing, so a wrong glob is never silent. -The engine is `specfuse.loop.gen_contract` (specfuse-loop 0.30+, part of `pipx install specfuse`). The seeded `scripts/check-gen-contract.sh` runs it; `scripts/test-gen-contract.sh` is the mutation test. +The engine is `specfuse.loop.gen_contract` (specfuse-loop 0.30+, part of `pipx install specfuse` or `uv tool install specfuse`). The seeded `scripts/check-gen-contract.sh` runs it, finding the interpreter beside the `specfuse` command; a script seeded from kit 0.32.0 or earlier does not look there, so set `SPECFUSE_PYTHON` or copy the script from the template again; `scripts/test-gen-contract.sh` is the mutation test. ```bash ./scripts/check-gen-contract.sh # every rule; 0 green, 1 violations, 2 cannot run diff --git a/scripts/test-csharp-contract-template.sh b/scripts/test-csharp-contract-template.sh index 7d5772c..4de6413 100755 --- a/scripts/test-csharp-contract-template.sh +++ b/scripts/test-csharp-contract-template.sh @@ -229,4 +229,44 @@ expect_patches 1 "no 'Generator-Issue:' line" --check git checkout -q -- "$PATCH" git reset -q --hard HEAD~1 +# ── The script finds the engine wherever `specfuse` is installed ─────────── +# CI pip-installs the engine into python3, the one case that always worked, so +# python3 is stubbed here. `specfuse` refuses `gen-contract`, as the CLI does +# until it dispatches it. +echo "── the script finds the engine beside the specfuse command" +REAL_PY="$(command -v "$PY")" +TOOLS="$(mktemp -d "${TMPDIR:-/tmp}/csharp-contract-tools.XXXXXX")" +trap 'rm -rf "$WORK" "$TOOLS"' EXIT +stub() { mkdir -p "$(dirname "$1")"; printf '#!/bin/sh\n%s\n' "$2" > "$1"; chmod +x "$1"; } +stub "$TOOLS/no-engine/python3" 'exit 1' +stub "$TOOLS/env/bin/specfuse" 'exit 2' +stub "$TOOLS/env/bin/python" "exec \"$REAL_PY\" \"\$@\"" +stub "$TOOLS/old-env/bin/specfuse" 'exit 2' +stub "$TOOLS/old-env/bin/python" 'exit 1' +stub "$TOOLS/no-python/specfuse" 'exit 2' +mkdir -p "$TOOLS/shim" +ln -s ../env/bin/specfuse "$TOOLS/shim/specfuse" + +expect_find() { # + local want="$1" text="$2" prefix="$3" chosen="$4" out rc=0 + out="$(env -u SPECFUSE_PYTHON ${chosen:+SPECFUSE_PYTHON="$chosen"} PATH="$prefix:$PATH" \ + ./scripts/check-gen-contract.sh --list 2>&1)" || rc=$? + if [ "$rc" -ne "$want" ] || ! grep -qF -- "$text" <<<"$out"; then + echo "FAIL: PATH prefix $prefix, SPECFUSE_PYTHON '$chosen': wanted exit $want and '$text', got exit $rc:" >&2 + printf '%s\n' "$out" >&2 + exit 1 + fi +} + +# Through a relative symlink, as pipx and uv install it; python3 has no engine. +expect_find 0 "" "$TOOLS/shim:$TOOLS/no-engine" "" +# Directly on PATH, as a plain virtualenv has it. +expect_find 0 "" "$TOOLS/env/bin:$TOOLS/no-engine" "" +# A specfuse too old to carry the engine must not hide a python3 that has it. +expect_find 0 "" "$TOOLS/old-env/bin:$(dirname "$REAL_PY")" "" +# Nothing beside specfuse and no engine in python3: exit 2, naming the fix. +expect_find 2 "uv tool install specfuse" "$TOOLS/no-python:$TOOLS/no-engine" "" +# An explicit choice is not second-guessed. +expect_find 2 "not importable by $TOOLS/no-engine/python3" "$TOOLS/shim" "$TOOLS/no-engine/python3" + echo "OK: the C# starter contract is green on a fresh seed and every rule bites." diff --git a/templates/component-contract/csharp/README.md b/templates/component-contract/csharp/README.md index ab174dd..90fc99a 100644 --- a/templates/component-contract/csharp/README.md +++ b/templates/component-contract/csharp/README.md @@ -37,7 +37,7 @@ cp "$KIT/.github/workflows/gen-contract.yml" .github/workflows/ sed -i.bak 's/__Prefix__/HelloOrders/g' gen-contract.yml docs/generated-code-expectations.md && rm -f *.bak docs/*.bak mkdir -p gen-patches && cp "$KIT/gen-patches/README.md" gen-patches/ chmod +x scripts/check-gen-contract.sh scripts/test-gen-contract.sh scripts/apply-gen-patches.sh -python3 -m pip install 'specfuse-loop>=0.30' # or: pipx install specfuse +pipx install specfuse # or: uv tool install specfuse ./scripts/check-gen-contract.sh # must be green ./scripts/test-gen-contract.sh # every rule must bite ``` diff --git a/templates/component-contract/csharp/scripts/check-gen-contract.sh b/templates/component-contract/csharp/scripts/check-gen-contract.sh index ec265b1..dfefc0b 100755 --- a/templates/component-contract/csharp/scripts/check-gen-contract.sh +++ b/templates/component-contract/csharp/scripts/check-gen-contract.sh @@ -11,14 +11,39 @@ # Exit 0 clean, 1 violations, 2 the checker could not run. # # The engine is `specfuse.loop.gen_contract`, shipped in specfuse-loop 0.30+ -# (`pipx install specfuse` brings it). Until the umbrella CLI dispatches -# `specfuse gen-contract`, the module is run directly. Set SPECFUSE_PYTHON to -# the interpreter that has specfuse-loop installed when it is not `python3`. +# (`pipx install specfuse` or `uv tool install specfuse` brings it). Until the +# umbrella CLI dispatches `specfuse gen-contract`, the module is run directly, +# by the first of: SPECFUSE_PYTHON, the interpreter installed beside the +# `specfuse` command when it has the engine, `python3`. set -uo pipefail cd "$(git rev-parse --show-toplevel)" -PY="${SPECFUSE_PYTHON:-python3}" +# The Python beside the `specfuse` on PATH, following the link pipx and uv put +# there. Not the script's shebang: a long install path makes it `#!/bin/sh`. +python_beside_specfuse() { + local bin target + bin="$(command -v specfuse 2>/dev/null)" || return 0 + while [ -L "$bin" ]; do + target="$(readlink "$bin")" + case "$target" in + /*) bin="$target" ;; + *) bin="$(dirname "$bin")/$target" ;; + esac + done + if [ -x "$(dirname "$bin")/python" ]; then + printf '%s\n' "$(dirname "$bin")/python" + fi +} + +PY="${SPECFUSE_PYTHON:-}" +if [ -z "$PY" ]; then + PY=python3 + BESIDE="$(python_beside_specfuse)" + if [ -n "$BESIDE" ] && "$BESIDE" -c "import specfuse.loop.gen_contract" >/dev/null 2>&1; then + PY="$BESIDE" + fi +fi if command -v specfuse >/dev/null 2>&1 && specfuse gen-contract --help >/dev/null 2>&1; then exec specfuse gen-contract "$@" @@ -32,8 +57,8 @@ fi if ! "$PY" -c "import specfuse.loop.gen_contract" >/dev/null 2>&1; then cat >&2 <=0.30' - Or point SPECFUSE_PYTHON at an interpreter that has it (pipx: ~/.local/pipx/venvs/specfuse/bin/python). + Install it: pipx install specfuse (or: uv tool install specfuse) + Or point SPECFUSE_PYTHON at an interpreter that has it. EOF exit 2 fi