Skip to content
Open
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
4 changes: 3 additions & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1530,8 +1530,10 @@ command's integration tests check its output.
| `vendor_lock_entry_pruned_kept` | revert `warnings[]` | vendor `--revert` / rollback (yarn berry, #737): vendoring dropped a lock entry only the patched package's implicit `node-gyp` dependency reached (or one descriptor of a shared entry), and the lock has re-added or changed that entry since; it is left as it is (not drift: the artifact is still removed). Run `yarn install` if the lock is incomplete. |
| `pypi_{poetry,pdm,pipenv}_no_lockfile` | `failed` | vendor (pypi): a lock-less tool marker with no `requirements.txt` fallback — run `<tool> lock`. |
| `pypi_poetry_integrity_unverified` | `skipped` (warning) | vendor (pypi / poetry): the lock was written by Poetry < 1.4 (0.12 `[metadata.hashes]`, lock 1.0/1.1, or a 2.0 lock without a `@generated by Poetry X.Y.Z` header — 1.3 wrote those). That installer does not verify local wheel hashes (the committed wheel bytes are the protection) and does not replace an already-installed package at the same version; recreate the virtualenv or `pip uninstall` the package before `poetry install`, or upgrade Poetry. |
| `pypi_poetry_source_constraint` | `failed` / `skipped` (warning) | vendor (pypi / poetry): the pyproject pins the package to a named source (`source = "pypi"`), which Poetry records nothing for in the lock. Over a lock stamped by Poetry 1.4 – 2.2 (whose installer refuses that constraint on a file-sourced package, so every `poetry install` would fail) the package is refused before any write, `failed`; an already-wired lock is refused the same way instead of reading as in sync. Over any other lock the wiring stands and this is a warning naming the 1.4 – 2.2 range. The detail names the remedy: drop the `source` key or upgrade to Poetry >= 2.3, re-lock and re-run; when the lock is already wired it first says to run `socket-patch vendor --revert` (which reverts EVERY vendored package), since re-locking discards the wiring. |
| `redirect_poetry_stale_install_risk` | `warnings[]` (warning) | scan `--mode hosted` (poetry): same writer test as above — a warm virtualenv keeps the upstream package after the redirect on Poetry < 1.4 (1.4+ re-installs from the new source); fresh installs pick up the patched wheel. Emitted once per rewritten lock, only on the run that rewrites it. |
| `redirect_poetry_entry_not_found` / `redirect_poetry_missing_sha256` / `redirect_poetry_lock_unsupported` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the lock has no `[[package]]` at the granted version (uv-parity twin of `redirect_uv_entry_not_found`); the grant carries no SHA-256 (gated once per dep, not per lock); the lock is refused — Poetry 0.12 layout (URL sources ignored), an unsupported `lock-version`, a forked package listed at several versions, a user-authored `[package.source]` on another origin (an earlier Socket URL for the same wheel is superseded in place), a malformed `[metadata.files]`/`[metadata.hashes]`, or a wheel whose filename does not match the locked package. Exit code and `status` unchanged (hosted-refusal posture). A vendored → hosted takeover over a Poetry 0.x lock is retracted with this code (wet and `--dry-run`, see **Staged takeover**): the purl stays vendored and patched and is skipped with this code as `skipped` (hosted event). The detail names the remedy and its reach: run `socket-patch vendor --revert` (it reverts EVERY vendored package in the project, not just this one), upgrade to Poetry >= 1.0 and re-lock, then re-run `scan --mode hosted`. |
| `redirect_poetry_source_constraint` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the pyproject pins the redirected package to a named source (`source = "pypi"`) and the lock was not written by Poetry 1.4 – 2.2 (those writers are refused with `redirect_poetry_lock_unsupported`, also when the lock is already redirected and before a vendored → hosted takeover reverts anything). The rewrite stands, and an already-redirected lock is still confirmed; the detail says a Poetry 1.4 – 2.2 installer would refuse the lock and to drop the `source` key if the project is installed with one. Exit code and `status` unchanged. |
| `redirect_poetry_entry_not_found` / `redirect_poetry_missing_sha256` / `redirect_poetry_lock_unsupported` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the lock has no `[[package]]` at the granted version (uv-parity twin of `redirect_uv_entry_not_found`); the grant carries no SHA-256 (gated once per dep, not per lock); the lock is refused — Poetry 0.12 layout (URL sources ignored), an unsupported `lock-version`, a forked package listed at several versions, a user-authored `[package.source]` on another origin (an earlier Socket URL for the same wheel is superseded in place), a package the pyproject pins to a named source (`source = "pypi"`) over a lock stamped by Poetry 1.4 – 2.2, whose installer refuses that constraint on a URL-sourced package, a malformed `[metadata.files]`/`[metadata.hashes]`, or a wheel whose filename does not match the locked package. Exit code and `status` unchanged (hosted-refusal posture). A vendored → hosted takeover over a Poetry 0.x lock is retracted with this code (wet and `--dry-run`, see **Staged takeover**): the purl stays vendored and patched and is skipped with this code as `skipped` (hosted event). The detail names the remedy and its reach: run `socket-patch vendor --revert` (it reverts EVERY vendored package in the project, not just this one), upgrade to Poetry >= 1.0 and re-lock, then re-run `scan --mode hosted`. |
| `redirect_pdm_refused` / `redirect_pdm_legacy_sync_required` | `warnings[]` (warning) | scan `--mode hosted` (pdm): the `pdm.lock` rewrite was refused — an unsupported `[metadata] lock_version` (the identity-losing `3.1` / `4.0`–`4.2` formats or an untested future format), an unsupported `strategy`, a package listed at several versions (fork) or absent, a user-authored `url`/`path`/VCS/`editable` source, hash-less or malformed `files`, or a wheel whose filename does not match the locked package (`redirect_pdm_refused`); or the lock was written in format `2` (PDM 0.12–1.4), whose upstream freshness bug lets `pdm install` regenerate the lock — use `pdm sync` (`redirect_pdm_legacy_sync_required`). A refused uuid is withheld from every other PyPI rewriter when `pdm.lock` is the install driver, and its patch is not confirmed. Exit code and `status` unchanged (hosted-refusal posture). |
| `redirect_bun_lock_unsupported` | `warnings[]` (warning) | scan/get `--mode hosted` (bun): the text lock's `lockfileVersion` is not 0, 1 or 2 (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2 — the shared gate's text, identical to vendored's `vendor_lockfile_version_unsupported`), or its `packages` section is not bun's single-line grammar. Nothing rewritten; exit 0 (hosted-refusal posture). |
| `redirect_bun_workspace_unsupported` | `warnings[]` (warning) | scan/get `--mode hosted` (bun): a lockfileVersion-0 lock (Bun 1.1.39–1.1.45 `--save-text-lockfile`) holds `workspace:` packages; frozen installs of that grammar cannot keep the hosted tuple. Detail: "Bun version-0 workspace locks cannot preserve hosted tarballs on frozen installs; delete bun.lock and re-run `bun install` with Bun >= 1.2 (which writes lockfileVersion 1, accepted by hosted mode) — a plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root -> member); otherwise it keeps version 0 or fails to resolve" (measured: Bun 1.2.0 keeps 0, 1.2.23–1.4.2 exit 1 "failed to resolve" on a root that does not depend on its members). Version-1/2 workspace locks are rewritten. Exit 0. |
Expand Down
102 changes: 101 additions & 1 deletion crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,18 @@ fn empty_venv(project: &Path) {
/// `six.py` bytes from a pristine install in a scratch sibling. `None`
/// (after [`skip`]) when PyPI is unreachable.
fn locked_project(poetry: &Poetry, project: &Path, probe: &Path) -> Option<(Vec<u8>, String)> {
locked_project_with(poetry, project, probe, PYPROJECT)
}

/// [`locked_project`] of an arbitrary `pyproject`.
fn locked_project_with(
poetry: &Poetry,
project: &Path,
probe: &Path,
pyproject: &str,
) -> Option<(Vec<u8>, String)> {
std::fs::create_dir_all(project).unwrap();
std::fs::write(project.join("pyproject.toml"), PYPROJECT).unwrap();
std::fs::write(project.join("pyproject.toml"), pyproject).unwrap();
let out = poetry.run(project, &["lock", "-n"]);
if !out.status.success() {
skip(&format!("poetry lock (needs PyPI): {}", text(&out)));
Expand Down Expand Up @@ -1164,3 +1174,93 @@ fn poetry_vendored_revendors_to_a_superseding_patch() {
"revert restores the pristine lock"
);
}

/// `six = {version = "1.16.0", source = "pypi"}` leaves no trace in the
/// lock, and Poetry 1.4 – 2.2 refuse to install a lock that then sources six
/// from a URL or file ("Repository \"pypi\" does not exist."). Over a lock
/// one of them wrote, both modes refuse and leave the lock installable;
/// other releases install the rewired lock, with a warning (#1282).
#[test]
#[ignore = "real Poetry + PyPI; run by the CI e2e matrix per Poetry release"]
fn poetry_source_pypi_constraint_never_breaks_install() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
std::fs::create_dir_all(&home).unwrap();
let Some(poetry) = Poetry::find(&home) else {
return;
};
if poetry.major_minor() < (1, 1) {
println!("Poetry {} predates per-dependency sources", poetry.version);
return;
}
let pyproject = PYPROJECT.replace(
"six = \"1.16.0\"",
"six = {version = \"1.16.0\", source = \"pypi\"}",
);
let project = tmp.path().join("proj");
let Some((pristine, pristine_lock)) =
locked_project_with(&poetry, &project, &tmp.path().join("probe"), &pyproject)
else {
return;
};
// The lock was written by this Poetry, so it is also the installer.
let refused = ((1, 4)..(2, 3)).contains(&poetry.major_minor());
let mut patched = pristine.clone();
patched.extend_from_slice(MARKER);
let wheel = build_wheel(&patched);

for (mode, uuid, refusal) in [
("hosted", HOSTED_UUID, "redirect_poetry_lock_unsupported"),
("vendored", VENDORED_UUID, "pypi_poetry_source_constraint"),
] {
let work = tmp.path().join(mode);
fresh_checkout(&project, &work);
let service = PatchService::start(uuid, &pristine, &patched, &wheel);
let out = poetry.install(&work);
assert!(
out.status.success(),
"{mode}: pristine install: {}",
text(&out)
);
let mut args = vec!["scan", "--mode", mode];
if mode == "vendored" {
args.extend(["--vendor-source", "service"]);
}
let (code, env) = socket_patch(&work, &poetry, &service, &args);
let lock = std::fs::read_to_string(work.join("poetry.lock")).unwrap();
let report = env.to_string();
if refused {
assert_eq!(
lock, pristine_lock,
"{mode}: refused before any write: {env}"
);
assert!(
report.contains(refusal) && report.contains("source ="),
"{mode}: names the constraint ({code:?}): {env}"
);
} else {
assert_eq!(code, Some(0), "{mode}: {env}");
assert_ne!(lock, pristine_lock, "{mode}: rewired: {env}");
assert!(
report.contains("source_constraint"),
"{mode}: warns about Poetry 1.4 – 2.2: {env}"
);
}
// Either way a fresh install works, and installs the patch whenever
// the lock was rewired.
let fresh = tmp.path().join(format!("{mode}-fresh"));
fresh_checkout(&work, &fresh);
let out = poetry.install(&fresh);
assert!(
out.status.success(),
"{mode}: fresh install: {}",
text(&out)
);
let installed = std::fs::read(installed_six(&fresh).expect("six installed")).unwrap();
assert_eq!(
installed,
*if refused { &pristine } else { &patched },
"{mode}: installed six"
);
}
}
Loading
Loading