Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2537,15 +2537,25 @@ impl StagedSource {
/// neither the pristine release nor this patch's output. The re-vendor
/// then takes the pristine artifact from the lock / registry / service, as
/// a lock-only checkout does, instead of reporting it not installed.
///
/// A PyPI install under a live hosted pin (`hosted_pinned`) is the same
/// story after a hosted scan (#1105): `uv sync` / `poetry install` from
/// the hosted lock installs a hosted patch's bytes, whichever generation
/// the pin names now. The pin names no release variant, so it only answers
/// for a sole candidate; gems keep their installed dir, which the gem
/// backend reads to refuse a platform gem.
fn superseded_install(
ledger: &VendorState,
candidate: &str,
record: &PatchRecord,
sole_candidate: bool,
hosted_pinned: bool,
) -> bool {
lookup_entry_kv(&ledger.entries, candidate).is_some_and(|(key, entry)| {
entry.uuid != record.uuid && (sole_candidate || key == candidate)
})
}) || (sole_candidate
&& hosted_pinned
&& matches!(Ecosystem::from_purl(candidate), Some(Ecosystem::Pypi)))
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -2605,8 +2615,13 @@ async fn plan_service_downloads(
if !variant_matches_installed(Some(
&verify_file_patch(dir, file, info).await.status,
)) {
if !superseded_install(ledger, candidate, record, candidates.len() == 1)
{
if !superseded_install(
ledger,
candidate,
record,
candidates.len() == 1,
takeover_blocked(candidate),
) {
continue;
}
source_path = &uninstalled;
Expand Down Expand Up @@ -3093,8 +3108,13 @@ pub(crate) async fn vendor_records_reusing(
.await
.status,
)) {
if !superseded_install(&state, candidate, record, candidates.len() == 1)
{
if !superseded_install(
&state,
candidate,
record,
candidates.len() == 1,
hosted_pin_of(candidate).is_some(),
) {
continue;
}
candidate_source = PackageSource::Installed(&uninstalled);
Expand Down Expand Up @@ -4909,6 +4929,28 @@ mod plan_gate_tests {
}
}

/// #1105: a live hosted pin marks a PyPI install superseded (a hosted
/// sync installed a hosted patch's bytes), so the takeover vendors the
/// record instead of reporting it not installed. The pin names no
/// variant, so only a sole candidate counts, and gems keep their
/// installed dir for the platform-gem refusal.
#[test]
fn hosted_pin_marks_a_pypi_install_superseded() {
let ledger = VendorState::default();
let pypi = "pkg:pypi/six@1.16.0";
let b = record(UUID_B);
assert!(superseded_install(&ledger, pypi, &b, true, true));
assert!(!superseded_install(&ledger, pypi, &b, false, true));
assert!(!superseded_install(&ledger, pypi, &b, true, false));
assert!(!superseded_install(
&ledger,
"pkg:gem/nokogiri@1.16.0",
&b,
true,
true
));
}

/// The download plan runs every record through its backend's own gate:
/// a package the backend refuses before its first service call is
/// never planned, wherever it sits in the loop order. Here the refused
Expand Down
61 changes: 61 additions & 0 deletions crates/socket-patch-cli/tests/mode_migration_pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,67 @@ async fn poetry_revendors_to_a_superseding_patch_over_a_patched_venv() {
);
}

/// #1105: a project hosted at patch A whose venv was then synced from the
/// hosted lock (`uv sync`, `poetry install`, `uv pip install -r`), so six
/// holds A's patched bytes, moves to vendored at the superseding patch B.
/// The installed copy matches neither the pristine release nor B's
/// output; it used to be dropped as `package_not_installed` (exit 1, the
/// project left hosted on A) while `scan --mode vendored --dry-run`
/// previewed success. The takeover must restore the upstream entry and
/// vendor B.
#[tokio::test]
async fn hosted_to_vendored_takes_a_superseding_patch_over_a_hosted_venv() {
let lanes: [(&str, StageFn); 3] = [
("uv", stage_uv_with_sibling),
("poetry", stage_poetry),
("requirements", stage_requirements),
];
for (lane, stage) in lanes {
let (_tmp, root) = project();
let files = stage(&root);
let server = MockServer::start().await;
let hosted_url = mount_hosted_api(&server, true).await;
let (code, env) = hosted_scan(&root, &server);
assert_eq!(code, 0, "{lane}: hosted scan at A: {env:#}");
assert!(
files.iter().any(|f| std::fs::read_to_string(root.join(f))
.unwrap()
.contains(&hosted_url)),
"{lane}: hosted mode pins A: {env:#}"
);

let venv = root.join("../hosted-venv");
venv_installed_from_patch_a(&venv);
stage_manifest_with(&root, UUID_B, PATCHED_B);
prebuilt_common::mount_project(&server, &root).await;
mount_pypi_json(&server).await;
let uri = server.uri();
let pypi = format!("{uri}/pypi");
let extra = [
("VIRTUAL_ENV", venv.to_str().unwrap()),
("SOCKET_PYPI_JSON_API", pypi.as_str()),
];
let args = ["vendor", "--patch-server-url", &uri, "--vendor-url", &uri];

let (code, env) = run_cli(&root, &args, &extra);
assert_eq!(code, 0, "{lane}: takeover onto B: {env:#}");
assert!(
!env.to_string().contains("package_not_installed"),
"{lane}: the installed copy is not reported missing: {env:#}"
);
let wired: String = files
.iter()
.map(|f| std::fs::read_to_string(root.join(f)).unwrap())
.collect();
assert!(
wired.contains(&format!(".socket/vendor/pypi/{UUID_B}/"))
&& !wired.contains(&hosted_url)
&& !wired.contains(UUID),
"{lane}: wired to vendored B, hosted A gone:\n{wired}"
);
}
}

const UV_LOCK: &str = r#"version = 1
revision = 2
requires-python = ">=3.9"
Expand Down
Loading