Skip to content

fix(gh-9): require both uv and uvx before skipping bootstrap download - #10

Open
oliver-cieliszak-natterbox wants to merge 2 commits into
mainfrom
fix/gh-9-uvx-bootstrap-guard
Open

fix(gh-9): require both uv and uvx before skipping bootstrap download#10
oliver-cieliszak-natterbox wants to merge 2 commits into
mainfrom
fix/gh-9-uvx-bootstrap-guard

Conversation

@oliver-cieliszak-natterbox

@oliver-cieliszak-natterbox oliver-cieliszak-natterbox commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

The bootstrap skip guard in install.sh and install.ps1 now requires both uv and uvx to be present, not just a matching uv version. setup.py gains a _require_bootstrapped() backstop that refuses to configure a prefix whose binaries are missing. TESTING.md gains Tests 9 and 10 plus a Windows counterpart, and the stale idempotency note in README.md is corrected.

Why

Fixes #9. The guard predates uvx (added in v0.10.0, #4) and was never extended, so it decided on uv's version alone:

if [[ -x "$uv_bin" ]] && \
   [[ "$("$uv_bin" --version ...)" == "$uv_version" ]]; then
    _msg "  ✓ uv $uv_version"; return

If prefix/uv already matched the pin but prefix/uvx was absent, uvx was never fetched - while setup.py still created bin/uvx -> ../uvx. The result was a dangling symlink and a broken $..._UVX, with the install printing all the way to Install complete!.

Reproduced on main before touching anything (scratchpad prefix, --isolated):

$ rm -f "$PREFIX/uvx" "$PREFIX/bin/uvx"
$ ./install.sh --prefix "$PREFIX" --python 3.14 --env-prefix TEST --isolated
  ✓ uv 0.10.12              <- download skipped
  ✓ bin/uvx → ../uvx        <- symlink created anyway
$ source "$PREFIX/env.sh" && "$TEST_UVX" --version
no such file or directory: .../uvx

The trigger is upgrading a pre-v0.10.0 prefix to a distro version that pins the same uv release, which is an entirely normal bump to make. Not reachable through the current Atlas bootstrap (v0.9.2 pins uv 0.10.6, v0.11.0 pins 0.10.12), so this is prevention rather than an outage.

How

Both binaries gate the skip. They ship in one archive, so a missing uvx simply re-downloads the pair - no separate fetch path, no new failure mode. The → Downloading uv X.Y.Z message is therefore what you see when only uvx is missing; a comment in each script explains that rather than making the message conditional.

install.bat needed no change - it is a thin shim that shells out to install.ps1, so there is no third copy of the guard.

The setup.py backstop is placed in main(), not in _create_bin(). Three reasons, the third being the one that actually matters:

  1. "No bin/ wrappers, no env files" becomes structural rather than a consequence of statement ordering.
  2. It avoids printing ==> Creating bin/ wrappers immediately before a fatal error.
  3. _validate_cooldown() returns None when uv cannot be run, i.e. it silently skips validation. Before this change, a prefix missing uv combined with a bad --cooldown slipped past cooldown validation entirely. Running the prefix check first makes uv's presence a precondition, so the cooldown check is always genuinely performed. Verified both ways below.

The check covers uv, uvx and the venv Python, branched on _IS_WINDOWS. On Windows the same bug is worse in one respect: uvx.cmd is written unconditionally with no symlink involved, so a missing uvx.exe produces a wrapper that fails later with a confusing error rather than an obviously broken link.

Test Steps

TESTING.md Tests 9 and 10 (Linux/macOS) and "Windows: Test 6". Everything below was run on macOS against a real bootstrap of the pinned uv_version = "0.10.12":

Check Result
Bug reproduced pre-fix Skipped the download, uvx absent, $TEST_UVX --version failed, install still reported success
Fixed (isolated prefix) Same prefix → → Downloading uv 0.10.12, ✓ venv already exists, uvx 0.10.12 (00d72dac7 ...)
Fixed (non-isolated prefix, three-flag form) Same result — → Downloading uv 0.10.12, venv untouched, uvx 0.10.12. _bootstrap_uv does not take isolated, so both modes share the guard, but worth confirming rather than assuming
Idempotency (Test 3) Complete prefix still prints ✓ uv 0.10.12 + ✓ venv already exists - no needless re-download
Fresh non-isolated install (Test 1) Clean, all three wrappers created
Fresh isolated install (Test 2) Clean; Python 3.10.20, bin/uvxuvx 0.10.12
setup.py standalone, uvx removed exit 1, ERROR: bootstrap incomplete ... naming uvx, nothing written
setup.py standalone, uv removed and --cooldown yesterday Guard wins: exit 1 naming uv. Previously the bad cooldown would have gone unvalidated
uv restored, --cooldown yesterday Still rejected with uv's own parser error - the reordering did not weaken it
setup.py standalone on a healthy prefix exit 0, unchanged behaviour
bash -n install.sh, ast.parse(setup.py) Clean
Line length No added line exceeds 120

Warning

Not tested on Windows. pwsh is not available on this machine. The install.ps1 change is a mirror of the shell one - a Test-Path $UvxExe conjunct on the guard, plus a post-extraction existence check and a reuse of the new $UvxExe variable. Worth a pass through "Windows: Test 6" before the next release.

Other Notes

  • Reversibility: two-way door. One guard condition per installer, plus a fail-fast precondition. No contract change, no generated-file change.
  • Existing broken installs self-heal on the next installer run - that is precisely what the guard now does.
  • No version bump - RELEASING.md gives release.py ownership of distro.toml version. This is patch-worthy whenever you next cut a release.
  • Follow-up, your call: the fix only reaches consumers on a new tag, and Atlas PR redmatter/atlas#677 currently pins v0.11.0. If you want this in the Atlas bootstrap, the path is: merge this → cut v0.11.1 → re-point #677. Not doing that unprompted.
  • Still open from earlier work: this repo has no root CLAUDE.md (deliberately removed in 70617da) though our standards now expect one, and origin/develop is stale (3 commits, no PR, no releases). Both left alone here.

Review & provenance

Risk tier T2 — bug fix to installer control flow plus a fail-fast precondition; no auth, IAM, secrets, PII, dependency or write-path surface, and no destructive operation on existing prefixes. Not yet challenged by an independent AI review — see Deviations
Generated by Claude Opus 5 (1M context), via Claude Code. Dispatch brief: "fix #9, add yurii and codemedic as reviewers" — issue #9 being my own filing from the Atlas v0.11.0 bump (redmatter/atlas#677)
Orchestrated by Claude Opus 5 (1M context), same session — single-agent change with one dispatched python-standards sub-agent review
Human owner Oliver Cieliszak
AI reviews python-standards sub-agent (Claude, same family as the generator — does not satisfy §3): 0 blocking, 2 suggestions, both accepted — Google-style Args:/Raises: sections added, and the guard relocated from _create_bin() into main(). The second suggestion is what surfaced the _validate_cooldown() silent-skip interaction, which is now the strongest reason for the placement
Gates run No CI gates exist on this repo beyond the release-tag version check, and there is no test harness — its testing model is manual TESTING.md. Actually run: bash -n install.sh, ast.parse(setup.py), a 120-column check over added lines, and the eleven manual checks in the table above. Not run: any PowerShell execution (no pwsh on macOS), any Windows test
Deviations / overrides One. §3 requires the reviewing model to be from a different family than the generator; the only reviewer available in-session was Claude, so independence is not satisfied. Recorded rather than papered over — this PR needs a genuinely independent review, which is part of what the two human reviewers are being asked for

Worth human attention

  • The _require_bootstrapped() placement is a behaviour change beyond issue uvx not downloaded when uv version already matches, leaving bin/uvx dangling #9. Moving it ahead of _validate_cooldown() means a prefix missing uv now fails hard where it previously proceeded with an unvalidated cooldown. That is the correct outcome, but it is a second fix riding along with the first and deserves a look on its own terms.
  • Hard-fail versus warn. Issue uvx not downloaded when uv version already matches, leaving bin/uvx dangling #9 suggested _create_bin "warn (or fail)". I chose sys.exit(1), on the view that a wrapper pointing at a non-existent binary is never a valid install. If anyone has a legitimate partial-prefix workflow, this breaks it — I could find none.
  • install.ps1 is unexecuted. Every claim about the Windows half is by inspection only. The guard is under Set-StrictMode -Version Latest, so Test-Path on the pre-computed $UvxExe was chosen deliberately over anything that could trip strict mode.
  • The skip message is now slightly misleading by design. When only uvx is missing, the script prints → Downloading uv X.Y.Z even though uv is already current. I judged one honest comment cheaper than conditional messaging; disagree freely.
  • The idempotency note in README.md was stale and is corrected in the second commit. If anyone is relying on "uv download skipped if pinned version already installed" as documented behaviour, that sentence is no longer true and this is the line to argue with.

Jira: n/a — tracked in GitHub

Closes #9

🤖 Generated with Claude Code

The skip guard in install.sh and install.ps1 checked uv's version only, so a
prefix created before uvx existed could match the pinned version and skip the
download, leaving uvx missing while setup.py still wrapped it - a dangling
bin/uvx and a broken $..._UVX, with the install reporting success throughout.

Both guards now require uv and uvx to be present. Since they ship in one
archive, a missing uvx simply re-downloads the pair.

setup.py gains _require_bootstrapped() as a backstop for standalone runs: it
refuses to configure a prefix whose binaries are absent. It runs before the
cooldown check on purpose - _validate_cooldown silently no-ops when uv cannot
be run, so validating the prefix first keeps that check honest.

TESTING.md gains Tests 9 and 10 plus a Windows counterpart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@orca-security-eu orca-security-eu Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed SAST high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca

🛠️ Fix a finding or handle a false positive

  • Auto-Fix: Run /orca-pr-scan-fix to automatically remediate or suppress PR findings.
  • In-Code: Suppress via inline comments or exception files (see CLI References).
  • In Orca: Dismiss or snooze directly via Changing Alert Statuses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

uvx not downloaded when uv version already matches, leaving bin/uvx dangling

1 participant