Resolve the symlink in lint_python.sh's SCRIPT_DIR

The previous commit broke `./lint_python.sh` from the repo root. That path
is a symlink into meta/, and `dirname` does not resolve symlinks, so
SCRIPT_DIR became the repo root and the new `source "$SCRIPT_DIR/lib/..."`
line pointed at a lib/ that does not exist -- an instant exit under
set -euo pipefail.

The bug was latent before the split: the same unresolved SCRIPT_DIR only
fed PROJECT_ROOT, so a root-path run degraded quietly. Adding a source
line is what turned a wrong value into a hard failure.

It was invisible because every check I ran used `bash meta/lint_python.sh`
-- the one entry point that could not fail. Both paths are now verified:
`./lint_python.sh` exits 0 through the symlink, and the direct invocation
still diffs byte-identical against the pre-split baseline. No CI job, hook
or wrapper calls it, so nothing automated was broken in the interim.

Recorded in docs/shell-split-verification.md as its own trap, with the
commands to enumerate a script's other entry points before splitting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 23:13:07 +02:00
parent d740d1513f
commit 0a14c451d7
3 changed files with 77 additions and 1 deletions

View File

@ -114,6 +114,33 @@ Note the knock-on: renaming the nameref local hides the array from shellcheck
through a variable (`CODE_FILES_MAP="LANG_CODE_FILES"`) so the reference is
real, rather than suppressing.
### Verify through every entry point, especially symlinks
Adding a `source "$SCRIPT_DIR/lib/..."` line to `meta/lint_python.sh` broke
`./lint_python.sh` from the repo root, because that root path is a **symlink**
into `meta/` (so are `pyproject.toml`, `requirements.txt`, `run.sh`, `.fvmrc`).
`SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` does not resolve
symlinks — `dirname` is string manipulation — so it became the repo root, where
`lib/` does not exist, and `set -euo pipefail` turned that into an instant exit.
It was invisible because every check used `bash meta/lint_python.sh`, the one
path that cannot fail. Before splitting a script, ask how else it is reachable:
```bash
find . -maxdepth 2 -type l -lname '*<script>*' # symlinks pointing at it
grep -rn '<script>' .github/workflows/ .pre-commit-config.yaml meta/run.sh
```
The fix is the pattern already used elsewhere in this repo:
```bash
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
```
Note the pre-split script had the same unresolved `SCRIPT_DIR`; it only fed
`PROJECT_ROOT`, so the bug was latent. Adding a `source` line is what turns a
quiet wrong value into a hard failure — which is exactly what a split does.
### Do not make a clean file shfmt-dirty
157 of 298 `linux_configuration` shell files already fail `shfmt` and there is

View File

@ -0,0 +1,45 @@
{
"intent": "Fix `./lint_python.sh` from the repo root, which the preceding split broke: the root path is a symlink into meta/, and SCRIPT_DIR did not resolve it, so the new source line pointed at a lib/ that does not exist.",
"scope": [
"meta/lint_python.sh",
"Non-goal: no change to which linters run or to any output"
],
"changes": [
"Resolve SCRIPT_DIR with `dirname \"$(readlink -f \"${BASH_SOURCE[0]}\")\"` instead of a bare dirname, matching the pattern nvidia_troubleshoot.sh already uses in this repo."
],
"verification": [
{
"command": "./lint_python.sh --help (from the repo root, via the symlink) -- BEFORE the fix",
"result": "fail",
"evidence": "exit 1: './lint_python.sh: line 31: /home/kuhy/testsAndMisc/lib/lint_output.sh: No such file or directory'. Under set -euo pipefail this is an immediate hard exit. Pre-split, SCRIPT_DIR only fed PROJECT_ROOT, so a root-path invocation degraded quietly instead of dying."
},
{
"command": "./lint_python.sh --help and ./lint_python.sh scripts/check_requirements_sync.py (after the fix)",
"result": "pass",
"evidence": "exit 0 both times; the full-mode run reports all six available linters passing (ruff-lint, ruff-format-check, mypy, pylint, flake8, pycodestyle)."
},
{
"command": "bash meta/lint_python.sh scripts/check_requirements_sync.py, diffed against the pre-split baseline captured in a detached worktree",
"result": "pass",
"evidence": "DIRECT PATH IDENTICAL TO PRE-SPLIT BASELINE -- byte-identical, so resolving the symlink did not shift PROJECT_ROOT for the direct invocation."
},
{
"command": "grep -rn 'lint_python' meta/run.sh .github/workflows/ .pre-commit-config.yaml",
"result": "pass",
"evidence": "No hits: no CI job, hook or wrapper invokes it, so the broken commit did not break an automated path. It is documented as a root-path entry point in README.md and CLAUDE.md, which is how a human would hit it."
},
{
"command": "shellcheck -x and shfmt -d on meta/lint_python.sh",
"result": "pass",
"evidence": "Both exit 0."
}
],
"risks": [
"PROJECT_ROOT is now meta/ for symlink invocations too, rather than the repo root. That is the same value the direct invocation always used, and the direct-path output is byte-identical to the pre-split baseline, so the two entry points now agree instead of diverging.",
"The general lesson, recorded in the handoff: a script reachable through a symlink must be verified through that symlink. Every check on the original split used `bash meta/lint_python.sh`, the one path that could not fail."
],
"rollback": [
"Revert to the bare dirname; `./lint_python.sh` from the repo root breaks again.",
"Verify with `./lint_python.sh --help` expecting exit 0."
]
}

View File

@ -23,7 +23,11 @@ NC='\033[0m' # No Color
BOLD='\033[1m'
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# readlink -f, not a bare dirname: the repo root carries a lint_python.sh
# symlink into meta/, and `dirname` on the symlink path yields the repo root,
# where lib/ does not exist. That made `./lint_python.sh` die on the source
# line below.
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
PROJECT_ROOT="${SCRIPT_DIR}"
# Sourced before argument parsing: --help calls usage() from here.