[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
uv pip compile --universal (and --generate-hashes) writes one requirements line per marker branch whenever a package resolves to different versions on different Pythons or platforms:
six==1.16.0 ; python_full_version < '3.12'
six==1.17.0 ; python_full_version >= '3.12'
The patch is for six@1.16.0. scan --mode vendored refuses the whole package with:
pypi_requirement_not_pinned: requirements.txt: six is not pinned to ==1.16.0; pin it exactly or use agent mode (`scan --mode agent` + `socket-patch apply`) instead
That message is false: the file pins six to ==1.16.0 exactly. find_pin classifies the other branch's ==1.17.0 line as Range, and Range outranks Exact ("a file that names the package ambiguously is never rewritten"). But the marker already separates the two lines, so nothing is ambiguous:
- Hosted mode rewrites only the 1.16.0 line and leaves 1.17.0 alone (docs: "Hosted requirements select exact
==/=== pins … Other versions remain unchanged").
- Vendored mode on
uv pip compile --universal -o pylock.toml of the same input wires only the six 1.16.0 entry (docs: "Standalone PEP 751 rewriting selects the exact package version").
On top of that, scan --mode vendored --dry-run previews "action": "would_vendor" (exit 0, no warning), and the real run then fails (exit 1, partial_failure). The vendored dry-run preview never runs the requirements preflight (preflight_requirements).
Impact
- Any vendored project that compiles universal requirements with uv can't vendor a patch for a package that splits by version across markers. This is common for packages that drop old Pythons. The only remedy the CLI offers ("pin it exactly") is impossible, because the file already pins it exactly.
- CI that gates on
--dry-run sees a clean preview, then the real run fails.
- Nothing is written, so there's no corruption. Other packages in the same run still vendor (
partial_failure).
Repro (Linux, uv 0.12.23; same on 0.5.31 / 0.8.17)
printf 'six==1.16.0 ; python_version < "3.12"\nsix==1.17.0 ; python_version >= "3.12"\n' > req.in
uv pip compile -q --universal req.in -o requirements.txt # optionally --generate-hashes
socket-patch scan --mode vendored --dry-run --json # exit 0, six: "would_vendor"
socket-patch scan --mode vendored --yes --json # exit 1, pypi_requirement_not_pinned, file unchanged, no .socket/
socket-patch scan --mode hosted --yes --json # exit 0, rewrites only the 1.16.0 line
(Run against a local mock patch API serving a patched six 1.16.0 wheel, with --api-url/--patch-server-url/--vendor-url pointing at it.)
The output vendored mode should write already works with uv. Vendoring a file holding only the first line and then appending the 1.17.0 line back gives:
./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl ; python_full_version < '3.12' # socket-patch vendor: six==1.16.0
six==1.17.0 ; python_full_version >= '3.12'
| uv |
uv pip sync on py3.11 |
uv pip sync on py3.12 |
| 0.5.31 |
six 1.16.0, patched |
six 1.17.0 (upstream, correct) |
| 0.12.23 |
six 1.16.0, patched |
six 1.17.0 (upstream, correct) |
Expected vs actual
- Expected: vendored requirements behave like hosted requirements and vendored pylock: rewrite every exact
==1.16.0 line (keeping its marker, as single-line markers already are) and leave lines that pin another version unchanged. At minimum, the refusal should say the package is split across versions (not "not pinned"), and --dry-run should preview would_refuse with the same code.
- Actual: the wet run refuses with a false "not pinned" message (exit 1), and the dry run previews
would_vendor (exit 0).
Matrix (Linux)
| Input (generated by) |
uv 0.5.31 |
uv 0.8.17 |
uv 0.12.23 |
--universal requirements.txt, vendored |
fail |
fail |
fail (×2, plus --generate-hashes with a py3.11 .venv) |
same, --dry-run |
would_vendor (wrong) |
would_vendor (wrong) |
would_vendor (wrong) |
| same, hosted |
– |
– |
pass (only the 1.16.0 line rewritten; patched install) |
--universal pylock.toml, vendored |
– |
– |
pass (1.16.0 entry only; py3.11 patched, py3.12 upstream; byte-identical revert) |
| single marker line, vendored |
pass |
– |
pass |
six==1.16.0 ; sys_platform=='linux' + six==1.16.0 ; sys_platform=='win32', vendored |
– |
– |
pass (both lines rewritten) |
Not OS-specific: the refusal happens in pure planning code before any write. Not bisected: v4.0.0 can't vendor against the harness (no_local_source, see ledger #310).
Suspect code
crates/socket-patch-core/src/vendor/pypi_requirements.rs:85-90: scan_pins sets found_range for any pin of the same name to another version (six==1.17.0), regardless of marker.
crates/socket-patch-core/src/vendor/pypi_requirements.rs:101-110: find_pin lets Range outrank Exact.
crates/socket-patch-core/src/vendor/pypi_requirements.rs:539-548: the "not pinned to =={version}" message.
crates/socket-patch-cli/src/commands/scan/vendor_flow.rs:100-146: the dry-run preview has no requirements refusal arm, so it falls through to would_vendor.
- Unit test
find_pin_classifies_every_shape covers six==1.15.0 alone (correctly Range) but not an exact pin next to another version's pin.
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
uv pip compile --universal(and--generate-hashes) writes one requirements line per marker branch whenever a package resolves to different versions on different Pythons or platforms:The patch is for
six@1.16.0.scan --mode vendoredrefuses the whole package with:That message is false: the file pins six to
==1.16.0exactly.find_pinclassifies the other branch's==1.17.0line asRange, andRangeoutranksExact("a file that names the package ambiguously is never rewritten"). But the marker already separates the two lines, so nothing is ambiguous:==/===pins … Other versions remain unchanged").uv pip compile --universal -o pylock.tomlof the same input wires only the six 1.16.0 entry (docs: "Standalone PEP 751 rewriting selects the exact package version").On top of that,
scan --mode vendored --dry-runpreviews"action": "would_vendor"(exit 0, no warning), and the real run then fails (exit 1,partial_failure). The vendored dry-run preview never runs the requirements preflight (preflight_requirements).Impact
--dry-runsees a clean preview, then the real run fails.partial_failure).Repro (Linux, uv 0.12.23; same on 0.5.31 / 0.8.17)
(Run against a local mock patch API serving a patched six 1.16.0 wheel, with
--api-url/--patch-server-url/--vendor-urlpointing at it.)The output vendored mode should write already works with uv. Vendoring a file holding only the first line and then appending the 1.17.0 line back gives:
uv pip syncon py3.11uv pip syncon py3.12Expected vs actual
==1.16.0line (keeping its marker, as single-line markers already are) and leave lines that pin another version unchanged. At minimum, the refusal should say the package is split across versions (not "not pinned"), and--dry-runshould previewwould_refusewith the same code.would_vendor(exit 0).Matrix (Linux)
--universalrequirements.txt, vendored--generate-hasheswith a py3.11.venv)--dry-run--universalpylock.toml, vendoredsix==1.16.0 ; sys_platform=='linux'+six==1.16.0 ; sys_platform=='win32', vendoredNot OS-specific: the refusal happens in pure planning code before any write. Not bisected: v4.0.0 can't vendor against the harness (
no_local_source, see ledger #310).Suspect code
crates/socket-patch-core/src/vendor/pypi_requirements.rs:85-90:scan_pinssetsfound_rangefor any pin of the same name to another version (six==1.17.0), regardless of marker.crates/socket-patch-core/src/vendor/pypi_requirements.rs:101-110:find_pinletsRangeoutrankExact.crates/socket-patch-core/src/vendor/pypi_requirements.rs:539-548: the "not pinned to =={version}" message.crates/socket-patch-cli/src/commands/scan/vendor_flow.rs:100-146: the dry-run preview has no requirements refusal arm, so it falls through towould_vendor.find_pin_classifies_every_shapecoverssix==1.15.0alone (correctlyRange) but not an exact pin next to another version's pin.