diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13b5f25c6..8c01ed6dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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'} diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 6506b62ad..9ca3a892f 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -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 ` 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. | diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs b/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs index 6b45a22ec..e68ed3273 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs @@ -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() { @@ -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); @@ -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 diff --git a/crates/socket-patch-core/src/patch/redirect/poetry.rs b/crates/socket-patch-core/src/patch/redirect/poetry.rs index c875d9f20..321ef67bc 100644 --- a/crates/socket-patch-core/src/patch/redirect/poetry.rs +++ b/crates/socket-patch-core/src/patch/redirect/poetry.rs @@ -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!( @@ -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", @@ -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); @@ -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!( @@ -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!( diff --git a/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs b/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs index c05f54aa9..271bf09e5 100644 --- a/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs +++ b/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs @@ -119,7 +119,7 @@ fn recorded_unit_version(unit: &str) -> Option { /// 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> { @@ -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; diff --git a/crates/socket-patch-core/src/utils/poetry_lock.rs b/crates/socket-patch-core/src/utils/poetry_lock.rs index c7280c770..3647478fa 100644 --- a/crates/socket-patch-core/src/utils/poetry_lock.rs +++ b/crates/socket-patch-core/src/utils/poetry_lock.rs @@ -310,7 +310,7 @@ pub fn source_constraint_refusal( }; format!( "{pyproject_path} pins {name} to the {source:?} source (`source = {source:?}`), and \ - {lock_path} was written by Poetry 1.4 – 2.2, whose installer refuses that constraint \ + {lock_path} was written by Poetry 1.3 – 2.2, whose installer refuses that constraint \ on a package the lock sources from a {kind}: every `poetry install` would fail. To \ patch it, {first}drop the `source` key or upgrade to Poetry >= 2.3, re-lock, and \ re-run" @@ -330,7 +330,7 @@ pub enum Wired { } /// The advisory for a source-pinned package rewired over a lock that a -/// Poetry outside 1.4 – 2.2 wrote. +/// Poetry outside 1.3 – 2.2 wrote. pub fn source_constraint_advisory( pyproject_path: &str, lock_path: &str, @@ -339,19 +339,35 @@ pub fn source_constraint_advisory( ) -> String { format!( "{pyproject_path} pins {name} to the {source:?} source (`source = {source:?}`): \ - Poetry 1.4 – 2.2 refuse to install {lock_path} with that constraint (1.1 – 1.3 and \ + Poetry 1.3 – 2.2 refuse to install {lock_path} with that constraint (1.1 – 1.2 and \ 2.3+ install it); drop the `source` key if this project is installed with one of them" ) } -/// Whether the lock's writer is a Poetry release (1.4 – 2.2) whose installer +/// The `(major, minor)` of the Poetry that wrote the lock, when the lock +/// says: the version in the `@generated by Poetry X.Y` stamp (1.4 on), else +/// 1.3 for `lock-version = "2.0"`. Only 1.3 – 1.8 write lock 2.0, and 1.4 – +/// 1.8 stamp their version, so an unversioned 2.0 lock is 1.3's (it stamps +/// the sentence without a version, #1447) or one whose stamp was edited +/// away; both read as the oldest writer of that format, as +/// `pre_1_4_writer` reads them. `None` for older formats. +pub fn lock_writer_version(lock_text: &str) -> Option<(u64, u64)> { + if let Some(version) = generated_by_version(lock_text) { + return Some(version); + } + let lock = lock_text.parse::().ok()?; + (lock_version(&lock).ok()? == "2.0").then_some((1, 3)) +} + +/// Whether the lock's writer is a Poetry release (1.3 – 2.2) whose installer /// refuses a pyproject `source = "…"` constraint on a package the lock /// sources from a URL or file: every `poetry install` fails ("Repository -/// \"pypi\" does not exist."; 1.4 reports a solver failure). 1.1 – 1.3 and -/// 2.3+ install it. Keyed on the `@generated by Poetry X.Y` stamp, so it -/// judges the writer, which is usually but not always the installer. +/// \"pypi\" does not exist."; 1.3 and 1.4 report a solver failure). 1.1 – +/// 1.2 and 2.3+ install it. Keyed on the lock's header +/// ([`lock_writer_version`]), so it judges the writer, which is usually but +/// not always the installer. pub fn source_constraint_breaks_writer(lock_text: &str) -> bool { - matches!(generated_by_version(lock_text), Some(v) if ((1, 4)..(2, 3)).contains(&v)) + matches!(lock_writer_version(lock_text), Some(v) if ((1, 3)..(2, 3)).contains(&v)) } /// Rewrite the `[[package]]` for `name`@`version` to install the wheel at @@ -861,12 +877,13 @@ grouped = {version = "*", source = "pypi"} } #[test] - fn source_constraint_breaks_only_poetry_1_4_to_2_2_writers() { + fn source_constraint_breaks_only_poetry_1_3_to_2_2_writers() { let stamp = |v: &str| { format!("# This file is automatically @generated by Poetry {v} and should not be changed by hand.\n") }; for (v, want) in [ - ("1.3.2", false), + ("1.2.2", false), + ("1.3.0", true), ("1.4.0", true), ("1.8.5", true), ("2.2.1", true), @@ -876,6 +893,45 @@ grouped = {version = "*", source = "pypi"} assert_eq!(source_constraint_breaks_writer(&stamp(v)), want, "{v}"); } assert!(!source_constraint_breaks_writer("[[package]]\n")); + + // Poetry 1.3 writes the stamp without a version (#1447). Its lock is + // still recognizable: only 1.3 – 1.8 write `lock-version = "2.0"`, + // and 1.4 – 1.8 stamp their version. + assert!(source_constraint_breaks_writer(&fixture("1.3.2"))); + for version in ["0.12.17", "1.0.10", "1.1.15", "1.2.2", "2.3.4", "2.4.3"] { + assert!( + !source_constraint_breaks_writer(&fixture(version)), + "{version}" + ); + } + let versionless = "# This file is automatically @generated by Poetry and should not be \ + changed by hand.\n\n"; + let lock_body = |lock_version: &str| { + format!("[metadata]\nlock-version = \"{lock_version}\"\npython-versions = \"*\"\ncontent-hash = \"x\"\n") + }; + for (header, lock_version, want) in [ + (versionless, "2.0", true), + // A 2.0 lock whose stamp was edited away is still 1.3 – 1.8's. + ("", "2.0", true), + ("# a comment above the stamp\n", "2.0", true), + // The versionless stamp judges nothing over other formats. + (versionless, "1.1", false), + (versionless, "2.1", false), + ("", "1.1", false), + ("", "2.1", false), + ] { + let lock = format!("{header}{}", lock_body(lock_version)); + assert_eq!( + source_constraint_breaks_writer(&lock), + want, + "{header:?} {lock_version}" + ); + assert_eq!( + source_constraint_breaks_writer(&lock.replace('\n', "\r\n")), + want, + "CRLF {header:?} {lock_version}" + ); + } } const WHEEL: &str = "urllib3-1.26.18-py2.py3-none-any.whl"; diff --git a/crates/socket-patch-core/src/vendor/pypi_poetry.rs b/crates/socket-patch-core/src/vendor/pypi_poetry.rs index f75d0531d..c8fa8bd0e 100644 --- a/crates/socket-patch-core/src/vendor/pypi_poetry.rs +++ b/crates/socket-patch-core/src/vendor/pypi_poetry.rs @@ -197,8 +197,8 @@ fn source_constraint(p: &PoetryProject, canon_name: &str) -> Option { } /// The advisory for a source-constrained package wired over a lock that a -/// Poetry outside 1.4 – 2.2 wrote: the writer installs it, but a 1.4 – 2.2 -/// installer would refuse it. [`check_target_guards`] refuses the 1.4 – 2.2 +/// Poetry outside 1.3 – 2.2 wrote: the writer installs it, but a 1.3 – 2.2 +/// installer would refuse it. [`check_target_guards`] refuses the 1.3 – 2.2 /// writers outright. pub(super) fn source_constraint_warning( p: &PoetryProject, @@ -212,7 +212,7 @@ pub(super) fn source_constraint_warning( } /// Refuse a source-constrained package over a lock written by Poetry -/// 1.4 – 2.2: its installer fails every install once the lock sources the +/// 1.3 – 2.2: its installer fails every install once the lock sources the /// package from the vendored wheel (#1282). fn source_constraint_guard( p: &PoetryProject, @@ -319,7 +319,7 @@ pub(super) fn check_target_guards( )); } // A pyproject `source = "…"` pin breaks every install of the wired lock - // on Poetry 1.4 – 2.2; refuse when one of those wrote the lock. + // on Poetry 1.3 – 2.2; refuse when one of those wrote the lock. source_constraint_guard(p, canon_name, version, poetry_lock::Wired::No)?; Ok(PoetryTarget::Fresh) } @@ -892,18 +892,19 @@ content-hash = "4b42a89b7ff7b26511b06acdc458dbd85312e5083db8f212b017482bc68cdd01 /// A pyproject `source = "pypi"` constraint on the vendored package has /// no trace in the lock. Rewiring it to a file source breaks every - /// `poetry install` on Poetry 1.4 – 2.2, so a lock stamped by one of - /// those is refused before the wheel is built; other writers wire it with - /// a warning (#1282). + /// `poetry install` on Poetry 1.3 – 2.2, so a lock one of those wrote is + /// refused before the wheel is built; other writers wire it with a + /// warning (#1282, #1447). #[tokio::test] - async fn source_constrained_dependency_is_refused_for_poetry_1_4_to_2_2() { + async fn source_constrained_dependency_is_refused_for_poetry_1_3_to_2_2() { const PYPROJECT: &str = "[tool.poetry.dependencies]\n\ python = \">=3.8\"\n\ urllib3 = {version = \"1.26.18\", source = \"pypi\"}\n"; for (version, broken) in [ ("0.12.17", false), ("1.1.15", false), - ("1.3.2", false), + ("1.2.2", false), + ("1.3.2", true), ("1.4.2", true), ("1.6.1", true), ("1.8.5", true), diff --git a/docs/testing/poetry-compatibility.md b/docs/testing/poetry-compatibility.md index dc554d072..d57f1dec2 100644 --- a/docs/testing/poetry-compatibility.md +++ b/docs/testing/poetry-compatibility.md @@ -38,14 +38,16 @@ locked package. A dependency the pyproject pins to a named source (`six = {version = "1.16.0", source = "pypi"}`, in the main, legacy dev or any group table) -leaves no trace in the lock. Poetry 1.4 – 2.2 refuse to install a lock that -then sources it from a URL or file (`Repository "pypi" does not exist.`; 1.4 -reports a solver failure); 1.1 – 1.3 and 2.3+ install it (measured, #1282). -So when the lock's `@generated by Poetry X.Y` stamp is 1.4 – 2.2, both modes -refuse that package before any write (`redirect_poetry_lock_unsupported` -hosted, `pypi_poetry_source_constraint` vendored); otherwise they rewire it -and warn (`redirect_poetry_source_constraint` / `pypi_poetry_source_constraint`) -that a 1.4 – 2.2 installer would refuse the lock. The stamp names the writer, +leaves no trace in the lock. Poetry 1.3 – 2.2 refuse to install a lock that +then sources it from a URL or file (`Repository "pypi" does not exist.`; 1.3 +and 1.4 report a solver failure); 1.1 – 1.2 and 2.3+ install it (measured, +#1282, #1447). So when the lock names a 1.3 – 2.2 writer (its `@generated +by Poetry X.Y` stamp, or, with no versioned stamp, `lock-version = "2.0"`, +which only 1.3 – 1.8 write; 1.3 stamps no version), both modes refuse that +package before any write (`redirect_poetry_lock_unsupported` hosted, +`pypi_poetry_source_constraint` vendored); otherwise they rewire it and warn +(`redirect_poetry_source_constraint` / `pypi_poetry_source_constraint`) that +a 1.3 – 2.2 installer would refuse the lock. The header names the writer, which is usually but not always the installer. ## Installer boundaries (measured)