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
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 @@ -1540,10 +1540,10 @@ command's integration tests check its output.
| `vendor_lock_descriptor_unlocked` | revert `warnings[]` | vendor `--revert` / rollback / remove (yarn berry, #759): a descriptor of the vendored package was added while it was vendored, and the restored registry version does not satisfy it (or its range is one socket-patch cannot read, such as a dist-tag), so it has no `yarn.lock` entry after the revert. The revert still succeeds and the artifact is removed. Run `yarn install` and commit `yarn.lock`. |
| `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 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. |
| `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), or one with `lock-version` 1.0 / 1.1 (which every Poetry 1.3+ re-solves on install and then refuses the same way, #1503), the package is refused before any write, `failed`, and the detail says which; an already-wired lock is refused the same way instead of reading as in sync. Over a Poetry 2.3+ (or 0.x) 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.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_poetry_source_constraint` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the pyproject pins the redirected package to a named source (`source = "pypi"`) and the lock is in format 2.1+ and was not written by Poetry 1.3 – 2.2 (those writers, and any `lock-version` 1.0 / 1.1 lock, which every Poetry 1.3+ re-solves and refuses (#1503), 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, or over a `lock-version` 1.0 / 1.1 lock, which every Poetry 1.3+ re-solves and refuses the same way (#1503), 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
12 changes: 8 additions & 4 deletions crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,9 +1179,10 @@ fn poetry_vendored_revendors_to_a_superseding_patch() {
/// `six = {version = "1.16.0", source = "pypi"}` leaves no trace in the
/// 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).
/// report a solver failure), and every Poetry 1.3+ re-solves, then refuses,
/// a lock-version 1.0 / 1.1 lock (Poetry 1.0 – 1.2 write those). Over such a lock
/// both modes refuse and leave the lock installable; 2.3+ install the
/// rewired lock, with a warning (#1282, #1447, #1503).
#[test]
#[ignore = "real Poetry + PyPI; run by the CI e2e matrix per Poetry release"]
fn poetry_source_pypi_constraint_never_breaks_install() {
Expand All @@ -1206,7 +1207,10 @@ 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, 3)..(2, 3)).contains(&poetry.major_minor());
// 1.1 and 1.2 install their own lock, but they write lock format 1.1,
// which every Poetry 1.3+ re-solves and then refuses (#1503): refused
// too, since the installer usually outlives the lock.
let refused = poetry.major_minor() < (2, 3);
let mut patched = pristine.clone();
patched.extend_from_slice(MARKER);
let wheel = build_wheel(&patched);
Expand Down
62 changes: 40 additions & 22 deletions crates/socket-patch-core/src/patch/redirect/poetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use super::{DepOverride, FileEdit, RewriteResult, RewriteWarning};
use crate::crawlers::python_crawler::canonicalize_pypi_name;
use crate::utils::poetry_lock::{
generated_by_version, lock_version, lock_wiring, pyproject_source_constraints,
rewrite_poetry_lock_all_with_version, source_constraint_advisory,
source_constraint_breaks_writer, source_constraint_refusal, LockBatch, LockStep, PoetryLockDep,
rewrite_poetry_lock_all_with_version, source_constraint_advisory, source_constraint_break,
source_constraint_refusal, LockBatch, LockStep, PoetryLockDep,
};

/// Whether the lock was written by a Poetry release older than 1.4. Those
Expand Down Expand Up @@ -71,8 +71,9 @@ pub(super) fn rewrite_poetry(
for (path, original) in locks {
// A pyproject `source = "…"` constraint on the dep (#1282). Poetry
// 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.
// and every 1.3+ fails a lock-version 1.0 / 1.1 one (#1503), so such
// a lock is refused; Poetry 2.3+'s are warned. The constraint lives in
// the pyproject beside the lock.
let pyproject_path = format!(
"{}pyproject.toml",
path.strip_suffix("poetry.lock").unwrap_or("")
Expand All @@ -83,7 +84,12 @@ pub(super) fn rewrite_poetry(
.unwrap_or_default();
let constraint = |dep: &DepOverride| constraints.get(&canonicalize_pypi_name(&dep.name));
let mut usable = usable.clone();
if !constraints.is_empty() && source_constraint_breaks_writer(original) {
let breaks = if constraints.is_empty() {
None
} else {
source_constraint_break(original)
};
if let Some(why) = &breaks {
usable.retain(|&(dep, _)| {
// A dep the lock doesn't list keeps its `entry_not_found`
// report; one it lists (even already redirected) is refused.
Expand All @@ -108,6 +114,7 @@ pub(super) fn rewrite_poetry(
source,
"URL",
wired,
why,
)
),
});
Expand Down Expand Up @@ -359,12 +366,13 @@ 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.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.
/// `poetry install` on Poetry 1.3 – 2.2, and on every Poetry 1.3+ over a
/// lock-version 1.0 / 1.1 lock (which they re-solve), so such a lock is
/// refused before any write; Poetry 2.3+'s locks are rewritten with a
/// warning (#1282, #1447, #1503). A sibling lock whose pyproject has no
/// constraint is rewritten as before.
#[test]
fn source_constrained_dependency_is_refused_for_poetry_1_3_to_2_2() {
fn source_constrained_dependency_is_refused_unless_poetry_2_3_installs_it() {
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 @@ -374,18 +382,23 @@ mod equivalence_tests {
"[[tool.poetry.dependencies.urllib3]]\nversion = \"1.26.18\"\npython = \">=3.8\"\nsource = \"pypi\"\n\n[[tool.poetry.dependencies.urllib3]]\nversion = \"1.26.17\"\npython = \"<3.8\"\n",
];
for version in VERSIONS.iter().filter(|version| !version.starts_with("0.")) {
let broken = matches!(
*version,
"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"
);
// Every lock-version 1.0 / 1.1 lock (Poetry 1.0 – 1.2) is
// re-solved by any Poetry 1.3+ on install, which then
// refuses the constraint like 1.3 – 2.2 do (#1503).
let old_format = matches!(*version, "1.0.10" | "1.1.15" | "1.2.2");
let broken = old_format
|| matches!(
*version,
"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);
let files = BTreeMap::from([
Expand Down Expand Up @@ -425,6 +438,11 @@ mod equivalence_tests {
);
assert_eq!(codes, ["redirect_poetry_lock_unsupported"], "{what}");
assert!(got.warnings[0].detail.contains("source ="), "{what}");
assert_eq!(
got.warnings[0].detail.contains("lock-version"),
old_format,
"{what}"
);
// Nothing is wired yet, so re-locking is the whole remedy.
assert!(!got.warnings[0].detail.contains("rollback"), "{what}");
} else {
Expand Down
Loading
Loading