Repository navigation
Fix vendored uv/Hatch re-vendor to a newer patch (#742, #650) - #943
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A uv project, a uv script lock (or pylock) and a Hatch project vendored with one patch could not move to a newer patch for the same package: scan --mode vendored, vendor and get --mode vendored failed with pypi_uv_source_already_exists, pypi_lock_source_already_exists or pypi_hatch_unsupported, and the project kept installing the old patch. The guards treated socket-patch's own wiring as a user source. When that wiring belongs to an older patch uuid of the same release and the vendor ledger still records it, vendor now replays the older entry's revert, keeping its wheel, and wires the new wheel fresh over the restored files. The new entry records the user's real pre-vendor originals, so vendor --revert still restores them byte for byte. If the old wiring was edited since vendoring, or the restored files refuse the new wiring, every file is put back and the run fails as before. Without a ledger entry it still refuses before writing. (#742, #650) Assisted-by: Claude Code:claude-opus-5-5
Drive the real binary through vendor with patch A, then vendor with patch B for a uv project, a uv script lock and a Hatch project. Each must move to patch B, remove patch A's wheel, settle on a re-run and restore the user's files on vendor --revert. (#742, #650) Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
|
[agent] Generated by Claude Code |
|
BugBot review Generated by Claude Code |
Hatch reports its uv-installer and Hatch-version guards under the same pypi_hatch_unsupported code as a foreign direct reference, so a superseding patch could unwind patch A's wiring only for the fresh plan to refuse on a guard unrelated to it. Run those guards (pypi_hatch:: preflight) once a superseded entry is found, before anything is touched. restore_snapshot now attempts every file and names any it could not write back in the reported failure, instead of dropping the error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SECNaPEKiAJVRwYMMVAcLx
|
bugbot run Generated by Claude Code |
|
[burn-down agent] Ready for review at head
Generated by Claude Code |
Main's #825 (#769) changed WiringPlan::Pipenv to carry the superseded older-uuid ledger entry for its in-place Pipfile.lock re-wire, while this branch added WiringPlan::Supersede for uv, script-lock and Hatch projects (#742, #650). Keep both variants: Pipenv keeps its own re-wire path and the new Supersede plan stays limited to the pyproject-family refusals it was written for. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
bugbot run Generated by Claude Code |
hatch::plan refuses vendored wheels when an environment sets installer = "uv" or a non-empty uv-path, but the supersede preflight only checked HATCH_ENV_TYPE_VIRTUAL_UV_PATH. A project switched to Hatch's uv installer after vendoring therefore had patch A's wiring unwound and patch B's wheel built before the same refusal put the tree back. Share that check as hatch::require_pip_installer and run it in preflight too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SECNaPEKiAJVRwYMMVAcLx
|
bugbot run Generated by Claude Code |
|
[agent] 6 jobs in the sbt / Mill / scala-cli compatibility workflow failed on These failures aren't from this PR's changes:
No code fix applies. I'll re-run the failed jobs once when the workflow finishes, since a re-run can't start while other jobs in the run are still going. If they fail again after that re-run, I'll treat it as a real failure and investigate. Generated by Claude Code |
|
bugbot run |
A superseding re-vendor turned the flavor guard's refusal into a Supersede plan, and a dry run returned success right after acquiring the wheel, so it never ran the unwind. Drifted older wiring, an unsafe wiring path, or restored files that refuse a fresh plan therefore failed only on the wet run, while --dry-run reported success. The dry run now runs the same unwind and fresh plan (probe_supersede) inside a throwaway group commit that is dropped unwritten, restoring the snapshot as well so an enclosing group (the takeover probe) is left as it was. It reports the refusal the wet run would hit, and writes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SECNaPEKiAJVRwYMMVAcLx
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 43ce45d. Configure here.
|
[agent]
I've re-run the failed jobs once. If the job fails on that re-run, I'll treat it as a real failure and investigate it. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #742
Fixes #650
Summary
A uv project, a uv PEP 723 script lock (or pylock) and a Hatch project that
were vendored with one patch now move to a newer patch for the same package on
the next
vendor/scan --mode vendored/get --mode vendored. Beforethis, the run failed with exit 1 (
pypi_uv_source_already_exists,pypi_lock_source_already_existsorpypi_hatch_unsupported) and told theuser to revert wiring socket-patch had written itself. The project kept
installing the old patch.
#743 fixed the hosted half of both issues. This PR fixes the vendored
half, which is what keeps both issues open. #766 did the same for
requirements.txt, and #825 does it for Pipenv.
Root cause
The three vendored backends' pre-flight guards (
check_target_guardsinvendor/pypi_uv.rs,load_python_locksinvendor/pypi_lock.rs, andload→hatch::replacementinvendor/pypi_hatch.rs) accept only wiringfor the current patch uuid. They treat socket-patch's own
.socket/vendor/pypi/<older uuid>/source as a foreign one. Nothing everunwound the older uuid's wiring, even though the CLI contract says "a package
the ledger holds at an older patch uuid is still re-vendored automatically".
Fix
All in
crates/socket-patch-core/src/vendor/pypi.rs, shared by the threeflavors:
supersede_or_refuse: when one of those guards refuses, check the vendorledger. If it holds exactly one entry for the same name and version under
another uuid, with this flavor and recorded wiring, and a project file still
references that uuid dir, the plan becomes
WiringPlan::Supersede.Otherwise the refusal stands unchanged.
pypi_hatch_unsupportedcode. For Hatch,pypi_hatch::preflightchecksthose guards before anything is unwound, so a refusal unrelated to the
old wiring never touches the project.
unwire_superseded, at wiring time after the new wheel is built:group-commit-aware
utils::fsreaders. Wiring paths from the tamper-ableledger must be plain relative paths outside
.socket/.keep_artifact. The CLI alreadysweeps the old uuid dir (
vendor_stale_artifact_removed) once the newledger entry lands.
to the old uuid dir.
(
fresh_pyproject_plan) and wires the new wheel through the normal path.The new entry therefore records the user's real pre-vendor originals, and
vendor --revertrestores them byte for byte.Any failure (drifted wiring, a fresh guard refusing, a wiring error) puts
every snapshotted file back and sweeps the new wheel, so the tree is left as
it was. In a
vendorrun those writes sit inside the group commit. Any filethat can't be restored is named in the reported error, never dropped. With
no ledger entry for the old uuid it still refuses before writing, because
there would be no recorded original to restore.
docs/testing/hatch.mdnow notes the vendored re-vendor.CHANGELOG.mdisuntouched (release-time only).
Also ported: 13f6eee is #878's Gradle digest routing. Main has been red on
production_digests_go_through_the_helperssince #865. The port becomes ano-op once #878 lands.
Tests (red → green)
vendor::pypi::tests::pyproject_flavors_revendor_to_a_superseding_uuid(each flavor: re-vendor to uuid B, originals carried, no uuid A left, revert byte-exact)tests/mode_migration_pypi.rs::pyproject_flavors_vendored_revendor_superseding_patch(real CLI:vendorA →vendorB → in-sync re-run →vendor --revert;vendor_stale_artifact_removed, ledger on B only)pypi_uv_source_already_exists(exit 1)pyproject_flavors_superseding_uuid_with_drifted_wiring_refuses(hand-edited wiring: refuses, files byte-identical, no uuid B dir)pyproject_flavors_superseding_uuid_without_ledger_refuses,uv_stale_uuid_vendor_refuses_through_orchestrator(no ledger entry: still refuses before writing)hatch_unrelated_guard_refuses_superseding_patch_before_unwinding(CLI, installer guard set: files, ledger, artifact A unchanged, no uuid B dir)restore_snapshot_reports_unrestored_files_and_restores_the_restLocal checks:
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test --workspace --all-features --no-fail-fast: 10822 passed, 12failed. All 12 are chmod 0o555 permission-denial tests that can't fail when
run as root, which this sandbox is (uid 0). The same 12 are noted on Fix uv/Hatch hosted re-pin to a newer patch (#742, #650) #743,
and CI runs them as non-root.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SECNaPEKiAJVRwYMMVAcLx
Note
Medium Risk
Changes lockfile/vendor ledger mutation and revert ordering for three PyPI flavors; mistakes could leave projects half-wired, though snapshots and group-commit restore aim to keep failures atomic.
Overview
Vendored uv projects, PEP 723 script locks, and Hatch projects no longer fail when a newer patch targets the same package/version while the ledger still wires an older patch UUID. Instead of treating socket-patch’s own
.socket/vendor/pypi/<old-uuid>/wiring as a foreign source (pypi_uv_source_already_exists,pypi_lock_source_already_exists, orpypi_hatch_unsupported), the PyPI vendor path can supersede the prior entry: snapshot affected files, replay the old entry’s revert (keeping the old wheel until the new ledger lands), re-plan wiring on the restored pre-vendor files, then wire the new patch. Failures roll back snapshots and remove the new wheel; dry-run probes the same unwind without writing.For Hatch, pip-installer checks are factored into
require_pip_installer/preflightso unrelated guards (e.g. uv installer via env orpyproject.toml) still refuse before any unwind. Docs note vendored scans re-vendor like hosted rotation. CLI and core tests cover happy-path re-vendor, stale artifact removal, revert byte-exact originals, drift and missing-ledger refusals, and Hatch guard ordering.Reviewed by Cursor Bugbot for commit 43ce45d. Configure here.
Generated by Claude Code