TLDR
_bootstrap_uv decides whether to download by checking uv's version only. If prefix/uv already matches the pinned version but prefix/uvx is missing, uvx is never fetched - yet setup.py still creates bin/uvx -> ../uvx, leaving a dangling symlink and a broken $..._UVX env var. Reported as an observation from an upgrade path, not a live outage.
Reproduction (confirmed)
$ # a prefix whose uv already matches the pin, but with no uvx
$ rm -f "$PREFIX/uvx" "$PREFIX/bin/uvx"
$ ./install.sh --prefix "$PREFIX" --python 3.14 --env-prefix NATTERBOX --isolated
✓ uv 0.10.12 <- download skipped
✓ bin/uvx → ../uvx <- symlink created anyway
$ ls "$PREFIX/uvx"
ls: no such file or directory
$ source "$PREFIX/env.sh" && "$NATTERBOX_UVX" --version
bash: .../uvx: No such file or directory
The install reports success throughout. Nothing warns that uvx is missing.
Cause
install.sh _bootstrap_uv (and the equivalent block in install.ps1):
if [[ -x "$uv_bin" ]] && \
[[ "$("$uv_bin" --version 2>/dev/null | awk '{print $2}')" == "$uv_version" ]]; then
_msg " ✓ uv $uv_version"; return
fi
The guard predates uvx (added in v0.10.0, #4) and was never extended to account for the second binary. _create_bin in setup.py then unconditionally symlinks bin/uvx, so a missing uvx becomes a dangling link rather than an error.
When it bites
Upgrading from a pre-v0.10.0 prefix (no uvx) to a version that pins the same uv release the prefix already has. The version check short-circuits, uvx is never downloaded, and the resulting install looks healthy but has a broken uvx.
Not currently reachable via the Atlas bootstrap - v0.9.2 pins uv 0.10.6 and v0.11.0 pins 0.10.12, so uv is re-downloaded and uvx arrives with it (verified). It becomes reachable the moment a distro bump keeps uv_version unchanged, which is a perfectly normal thing to do.
Suggested fix
Extend the skip condition to require both binaries, so a missing uvx forces the download:
if [[ -x "$uv_bin" ]] && [[ -x "${prefix}/uvx" ]] && \
[[ "$("$uv_bin" --version ...)" == "$uv_version" ]]; then
Same change in install.ps1 for uv.exe / uvx.exe. Optionally, have _create_bin warn (or fail) when a symlink target does not exist - a broken bin/ entry is never a valid outcome.
Reversibility
Two-way door - one guard condition per installer script.
Note
Found while bumping the Atlas install-dev-machine pin to v0.11.0 (redmatter/atlas#677). Filing rather than fixing, since it is outside that task's scope.
TLDR
_bootstrap_uvdecides whether to download by checkinguv's version only. Ifprefix/uvalready matches the pinned version butprefix/uvxis missing,uvxis never fetched - yetsetup.pystill createsbin/uvx -> ../uvx, leaving a dangling symlink and a broken$..._UVXenv var. Reported as an observation from an upgrade path, not a live outage.Reproduction (confirmed)
The install reports success throughout. Nothing warns that
uvxis missing.Cause
install.sh_bootstrap_uv(and the equivalent block ininstall.ps1):The guard predates
uvx(added in v0.10.0, #4) and was never extended to account for the second binary._create_bininsetup.pythen unconditionally symlinksbin/uvx, so a missinguvxbecomes a dangling link rather than an error.When it bites
Upgrading from a pre-v0.10.0 prefix (no
uvx) to a version that pins the same uv release the prefix already has. The version check short-circuits,uvxis never downloaded, and the resulting install looks healthy but has a brokenuvx.Not currently reachable via the Atlas bootstrap - v0.9.2 pins uv
0.10.6and v0.11.0 pins0.10.12, so uv is re-downloaded anduvxarrives with it (verified). It becomes reachable the moment a distro bump keepsuv_versionunchanged, which is a perfectly normal thing to do.Suggested fix
Extend the skip condition to require both binaries, so a missing
uvxforces the download:Same change in
install.ps1foruv.exe/uvx.exe. Optionally, have_create_binwarn (or fail) when a symlink target does not exist - a brokenbin/entry is never a valid outcome.Reversibility
Two-way door - one guard condition per installer script.
Note
Found while bumping the Atlas
install-dev-machinepin to v0.11.0 (redmatter/atlas#677). Filing rather than fixing, since it is outside that task's scope.