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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,7 @@ jobs:
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.5', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.0.10'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.1.15'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.3.2'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.8.5'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.0.1'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '1.4.5'}
Expand Down
6 changes: 3 additions & 3 deletions crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1536,10 +1536,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. |
| `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 written by Poetry 1.3 – 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.3 – 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_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_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.3 – 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.3 – 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 written by Poetry 1.3 – 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
13 changes: 7 additions & 6 deletions crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1176,10 +1176,11 @@ fn poetry_vendored_revendors_to_a_superseding_patch() {
}

/// `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).
/// lock, and Poetry 1.3 – 2.2 refuse to install a lock that then sources six
/// from a URL or file ("Repository \"pypi\" does not exist."; 1.3 and 1.4
/// report a solver failure). 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, #1447).
#[test]
#[ignore = "real Poetry + PyPI; run by the CI e2e matrix per Poetry release"]
fn poetry_source_pypi_constraint_never_breaks_install() {
Expand All @@ -1204,7 +1205,7 @@ fn poetry_source_pypi_constraint_never_breaks_install() {
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 refused = ((1, 3)..(2, 3)).contains(&poetry.major_minor());
let mut patched = pristine.clone();
patched.extend_from_slice(MARKER);
let wheel = build_wheel(&patched);
Expand Down Expand Up @@ -1243,7 +1244,7 @@ fn poetry_source_pypi_constraint_never_breaks_install() {
assert_ne!(lock, pristine_lock, "{mode}: rewired: {env}");
assert!(
report.contains("source_constraint"),
"{mode}: warns about Poetry 1.4 – 2.2: {env}"
"{mode}: warns about Poetry 1.3 – 2.2: {env}"
);
}
// Either way a fresh install works, and installs the patch whenever
Expand Down
42 changes: 33 additions & 9 deletions crates/socket-patch-core/src/patch/redirect/poetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub(super) fn rewrite_poetry(
}
for (path, original) in locks {
// A pyproject `source = "…"` constraint on the dep (#1282). Poetry
// 1.4 – 2.2 fail every install of a lock that sources it from a URL,
// 1.3 – 2.2 fail every install of a lock that sources it from a URL,
// so a lock one of them wrote is refused; other writers are warned.
// The constraint lives in the pyproject beside the lock.
let pyproject_path = format!(
Expand Down Expand Up @@ -345,12 +345,12 @@ mod equivalence_tests {

/// A dependency the pyproject pins to a named source (`source = "pypi"`)
/// has no trace in the lock. Rewriting it to a URL source breaks every
/// `poetry install` on Poetry 1.4 – 2.2, so a lock stamped by one of
/// those is refused before any write; other writers are rewritten with a
/// warning (#1282). A sibling lock whose pyproject has no constraint is
/// `poetry install` on Poetry 1.3 – 2.2, so a lock one of those wrote is
/// refused before any write; other writers are rewritten with a warning
/// (#1282, #1447). A sibling lock whose pyproject has no constraint is
/// rewritten as before.
#[test]
fn source_constrained_dependency_is_refused_for_poetry_1_4_to_2_2() {
fn source_constrained_dependency_is_refused_for_poetry_1_3_to_2_2() {
const PYPROJECTS: &[&str] = &[
"[tool.poetry.dependencies]\nurllib3 = {version = \"1.26.18\", source = \"pypi\"}\n",
"[tool.poetry.dependencies.URLLib3]\nversion = \"1.26.18\"\nsource = \"PyPI\"\n",
Expand All @@ -362,7 +362,15 @@ mod equivalence_tests {
for version in VERSIONS.iter().filter(|version| !version.starts_with("0.")) {
let broken = matches!(
*version,
"1.4.2" | "1.5.1" | "1.6.1" | "1.7.1" | "1.8.5" | "2.0.1" | "2.1.4" | "2.2.1"
"1.3.2"
| "1.4.2"
| "1.5.1"
| "1.6.1"
| "1.7.1"
| "1.8.5"
| "2.0.1"
| "2.1.4"
| "2.2.1"
);
for pyproject in PYPROJECTS {
let lock = fixture(version);
Expand All @@ -379,7 +387,18 @@ mod equivalence_tests {
let got = run(rewrite_poetry, &files, &deps);
let what = format!("{version} {pyproject:?}: {:?}", got.warnings);
assert!(got.files.contains_key("sub/poetry.lock"), "{what}");
let codes: Vec<&str> = got.warnings.iter().map(|w| w.code.as_str()).collect();
// The sibling is rewritten, and a pre-1.4 one (1.3) also
// warns about stale installs; nothing else may name it.
let sibling_stale = |w: &RewriteWarning| {
w.code == "redirect_poetry_stale_install_risk"
&& w.detail.starts_with("sub/poetry.lock ")
};
let codes: Vec<&str> = got
.warnings
.iter()
.filter(|w| !sibling_stale(w))
.map(|w| w.code.as_str())
.collect();
if broken {
assert!(!got.files.contains_key("poetry.lock"), "{what}");
assert!(
Expand All @@ -404,11 +423,16 @@ mod equivalence_tests {
}

// A lock a pre-#1282 run already redirected: still refused
// (never confirmed) on 1.4 – 2.2, still warned elsewhere.
// (never confirmed) on 1.3 – 2.2, still warned elsewhere.
let mut again = files.clone();
again.insert("poetry.lock".into(), got.files["sub/poetry.lock"].clone());
let got = run(rewrite_poetry, &again, &deps);
let codes: Vec<&str> = got.warnings.iter().map(|w| w.code.as_str()).collect();
let codes: Vec<&str> = got
.warnings
.iter()
.filter(|w| !sibling_stale(w))
.map(|w| w.code.as_str())
.collect();
assert!(!got.files.contains_key("poetry.lock"), "{what}");
if broken {
assert!(
Expand Down
27 changes: 20 additions & 7 deletions crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ fn recorded_unit_version(unit: &str) -> Option<String> {

/// Hosted mode refuses every Poetry 0.x lock (Poetry 0.12 ignores URL
/// sources), which vendored mode supports (#945), and a package the
/// pyproject pins to a named source over a lock Poetry 1.4 – 2.2 wrote
/// pyproject pins to a named source over a lock Poetry 1.3 – 2.2 wrote
/// (#1282). The revert never changes the lock's format, stamp or the
/// pyproject, so the files on disk decide.
async fn preflight_poetry_takeover(root: &Path, entry: &VendorEntry) -> Result<(), RewriteWarning> {
Expand Down Expand Up @@ -357,18 +357,31 @@ mod tests {
}

/// #1282: hosted mode refuses a source-pinned package over a lock
/// Poetry 1.4 – 2.2 wrote, so the takeover must keep it vendored rather
/// Poetry 1.3 – 2.2 wrote, so the takeover must keep it vendored rather
/// than revert it into neither mode. Other writers are admitted.
#[tokio::test]
async fn poetry_source_pinned_package_follows_the_writer() {
const PYPROJECT: &str =
"[tool.poetry.dependencies]\nsix = {version = \"1.16.0\", source = \"pypi\"}\n";
for (stamp, refused) in [("1.8.5", true), ("2.2.1", true), ("2.3.4", false)] {
let stamp = |version: &str| {
format!(
"# This file is automatically @generated by Poetry {version} and should not \
be changed by hand.\n\n{POETRY_2}"
)
};
// Poetry 1.3 stamps no version and writes lock 2.0 (#1447).
let poetry_1_3 = format!(
"# This file is automatically @generated by Poetry and should not be changed \
by hand.\n\n{}",
POETRY_2.replace("lock-version = \"2.1\"", "lock-version = \"2.0\"")
);
for (stamp, lock, refused) in [
("1.3.2", poetry_1_3, true),
("1.8.5", stamp("1.8.5"), true),
("2.2.1", stamp("2.2.1"), true),
("2.3.4", stamp("2.3.4"), false),
] {
let tmp = tempfile::tempdir().unwrap();
let lock = format!(
"# This file is automatically @generated by Poetry {stamp} and should not be \
changed by hand.\n\n{POETRY_2}"
);
std::fs::write(tmp.path().join("poetry.lock"), lock).unwrap();
std::fs::write(tmp.path().join("pyproject.toml"), PYPROJECT).unwrap();
let got = preflight_pypi_takeover(tmp.path(), &poetry_entry()).await;
Expand Down
Loading
Loading