Skip to content

Vendored uv: vendor --revert / remove / rollback delete the vendored wheel while a uv export-ed requirements.txt or pylock.toml still points at it (exit 0), so installs from the exported file fail #996

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

After a uv project is vendored, uv export (requirements.txt, or --format pylock.toml) writes the vendored wheel path into the exported file. That's what uv does with a path source:

./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl \
    --hash=sha256:…

socket-patch already knows this file wires the patch: vex reports requirements.txt: pkg:pypi/six@1.16.0 is wired to Socket patch <uuid>, but uv.lock resolves the same version from elsewhere.

But vendor --revert, remove pkg:pypi/six@1.16.0 and rollback restore pyproject.toml / uv.lock byte-identically and then delete .socket/vendor/pypi/<uuid>/ with no warning, status: success, exit 0. The --dry-run revert previews the same clean removed. The exported file still names the deleted wheel, so every install from it fails.

The uv flavor revert (revert_uv) has no residual-reference check. The Python dispatcher already has a probe, unwired_pypi_reference_clause, that lists the root requirements.txt (plus -r includes), uv.lock, pylock*.toml and *.py.lock and refuses while any of them mentions the uuid dir. But it only runs for ledger entries with no wiring (guard_unwired_pypi_revert). An entry that does have wiring skips it.

Impact

A common uv workflow is to keep an exported requirements.txt / pylock.toml for Docker images, Dependabot or a pip-only deploy. After a revert or remove that reports success, uv pip install -r requirements.txt (and pip install -r, and uv pip sync pylock.toml) fails on every machine with Distribution not found at: file:///…/.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl. The ledger entry is gone too, so repair can't bring the wheel back. The user has to restore it from git or re-export.

Repro (Linux, main 9c43dfc, CPython 3.11)

This uses the repo's own tests/prebuilt_common::Server as the vendoring service (SOCKET_VENDOR_URL) and a staged .socket/manifest.json + blob, the same as e2e_vendor_pypi_build.rs::stage_patch.

printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "certifi"]\n' > pyproject.toml
uv lock && uv sync
# stage .socket/manifest.json + blob for pkg:pypi/six@1.16.0 (six.py + marker)
socket-patch vendor --json                     # exit 0, applied 1
uv export --frozen --no-emit-project -o requirements.txt   # six line = ./.socket/vendor/pypi/<uuid>/six-…whl --hash=…
uv venv fresh && uv pip install -p fresh/bin/python -r requirements.txt   # control: installs the patched six (SOCKET_PATCHED == 1)

socket-patch vendor --revert --json            # exit 0, status success, events: [removed]; no warning
#   pyproject.toml + uv.lock byte-identical to pre-vendor; .socket/vendor/ deleted
grep socket/vendor requirements.txt            # still ./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl
uv venv fresh2 && uv pip install -p fresh2/bin/python -r requirements.txt
# error: Distribution not found at: file:///…/.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl

socket-patch remove pkg:pypi/six@1.16.0 --yes (events vendor_reverted, removed) and socket-patch rollback --yes give the same result. So does uv export --format pylock.toml -o pylock.toml (uv ≥ 0.8) followed by uv pip install -r pylock.toml.

Expected vs actual

  • docs/testing/uv-compatibility.md (Limits): "vendor --revert refuses to delete a vendored Python wheel while uv.lock, a PEP 751 lock, a script, or requirements.txt still references it…". The guard's own doc comment in vendor/pypi.rs gives the reason: deleting the artifact "while uv.lock, the pylock, the script, or requirements.txt still resolve through the vendored wheel — every later --frozen / --offline install fails".
  • CLI_CONTRACT (vendor revert): composer / maven / nuget "keep the artifact exactly while the live … still names its .socket/vendor/<eco>/<uuid> dir — a file that no longer references it is warned about and the artifact removed"; the requirements flavor keeps it on vendor_revert_residual_reference.
  • Expected: after restoring the uv pair, the revert keeps the artifact and the entry while another project file still references the uuid dir, with a warning naming the file (e.g. vendor_revert_residual_reference / vendor_artifact_kept), or at least warns. The dry run previews that.
  • Actual: the artifact is deleted silently, exit 0, and the exported file is left pointing at nothing.

OS × version

OS uv exported file vendor --revert remove rollback
Linux 0.5.31 requirements.txt reproduces reproduces reproduces
Linux 0.8.17 requirements.txt / pylock.toml reproduces / reproduces – –
Linux 0.12.23 requirements.txt (×2) / pylock.toml reproduces / reproduces reproduces reproduces
macOS / Windows – not probed: the missing check is in pure Rust revert logic with no OS-specific path, and uv fails on a missing local wheel on every OS

First bad version

Not bisected. revert_uv has never swept for references outside its own wiring records.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_uv.rs:1013-1050: the end of revert_uv writes the pair and returns kept_artifact: false without checking other files for the uuid dir.
  • crates/socket-patch-core/src/vendor/pypi.rs:1596 / 1412-1440: guard_unwired_pypi_revert → unwired_pypi_reference_clause already enumerates exactly the right files (root requirements.txt + includes, uv.lock, pylock*.toml, *.py.lock), but it's gated on the entry having no wiring. Running the same probe after a successful wired flavor revert (pypi.rs:~1620, next to the vendor_revert_residual_reference keep) would cover this.
  • Related but distinct: Vendored requirements.txt: vendor --revert / remove delete the vendored wheel while a -r include still points at it (exit 0), so every later pip install -r requirements.txt fails #867 (pip flavor: the residual sweep in revert_requirements misses -r includes). This one is the uv flavor, which has no sweep at all, and the trigger is uv's own uv export.

No probe runs: Linux only (see the table).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions