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 crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,7 @@ command's integration tests check its output.
| `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 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_uv_override_requires_uv_0_5_6` / `redirect_uv_constraints_require_uv_0_5_6` | `warnings[]` (warning) | scan/get `--mode hosted` (uv project and script locks, #1569): the hosted twins of vendored's `pypi_uv_override_requires_uv_0_5_6` / `pypi_uv_constraints_require_uv_0_5_6`. The run wired a transitive package through a `[tool.uv] override-dependencies` entry, its own marked one or the user's (uv before 0.5.5 re-resolves it and a plain `uv sync` reinstalls the upstream wheel), or repointed a `[manifest]` `constraints` / `build-constraints` entry (uv before 0.5.5 rejects it under `uv sync --locked`). The CLI cannot tell those uv releases apart from the lock, so these advise and the redirect still counts. Emitted on a run that writes the package's wiring (a first pin or a re-pin to a superseding patch); a settled re-run is silent. Exit 0. |
| `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: 12 additions & 1 deletion crates/socket-patch-cli/src/commands/scan/hosted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2338,13 +2338,16 @@ fn describe_skip_reason(reason: &str) -> String {
/// package has no lock entry: the count `format_unredirected` turns into its
/// "(see the warning below)" hint. `redirect_bun_default_trust_lost` rides in
/// the same vector but is about a pin that *was* written (Bun's default trust
/// lost on the hosted URL, #371), so it never explains a missing entry.
/// lost on the hosted URL, #371), so it never explains a missing entry. The
/// uv < 0.5.6 advisories (#1569) likewise describe a written pin.
fn lock_entry_warning_count(
warnings: &[socket_patch_core::patch::redirect::RewriteWarning],
) -> usize {
use socket_patch_core::patch::redirect::UV_OLD_RELEASE_ADVISORIES;
warnings
.iter()
.filter(|w| w.code != "redirect_bun_default_trust_lost")
.filter(|w| !UV_OLD_RELEASE_ADVISORIES.contains(&w.code.as_str()))
.count()
}

Expand Down Expand Up @@ -4396,6 +4399,14 @@ mod tests {
lock_entry_warning_count(&[w("redirect_bun_default_trust_lost")]),
0
);
// Nor the uv < 0.5.6 advisories, which follow a written pin (#1569).
assert_eq!(
lock_entry_warning_count(&[
w("redirect_uv_override_requires_uv_0_5_6"),
w("redirect_uv_constraints_require_uv_0_5_6"),
]),
0
);
assert_eq!(
lock_entry_warning_count(&[
w("redirect_bun_default_trust_lost"),
Expand Down
63 changes: 53 additions & 10 deletions crates/socket-patch-cli/src/commands/scan/hosted/takeover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ const LANDED_PIN_ADVISORIES: &[&str] = &[
/// wired outside the root file #699, a uv entry pinned down from another
/// locked version #723, a Poetry 0.x lock #945); a rewriter warning naming
/// the package; the rewrite's first warning (a lock-level refusal names no
/// package), skipping [`LANDED_PIN_ADVISORIES`]; else [`NOT_PINNED`].
/// package), skipping [`LANDED_PIN_ADVISORIES`] and the uv < 0.5.6
/// advisories (#1569); else [`NOT_PINNED`].
async fn explain(
root: &std::path::Path,
entry: &VendorEntry,
Expand All @@ -447,19 +448,22 @@ async fn explain(
if let Err(w) = socket_patch_core::patch::redirect::preflight_pypi_takeover(root, entry).await {
return (w.code.clone(), vec![w]);
}
if let Some(w) = dep.and_then(|dep| {
warnings
.iter()
.find(|w| names_package(&w.detail, &dep.name))
}) {
// The uv < 0.5.6 advisories ride on a pin that landed, and their
// wording ("wheel", "uv", "sync") can read as another package's name,
// so neither pass may pick one (#1569).
let candidates = || {
warnings.iter().filter(|w| {
!socket_patch_core::patch::redirect::UV_OLD_RELEASE_ADVISORIES
.contains(&w.code.as_str())
})
};
if let Some(w) = dep.and_then(|dep| candidates().find(|w| names_package(&w.detail, &dep.name)))
{
return (w.code.clone(), vec![w.clone()]);
}
// A pin that did land can carry a success advisory; it never explains
// a pin that did not.
match warnings
Comment thread
mikolalysenko marked this conversation as resolved.
.iter()
.find(|w| !LANDED_PIN_ADVISORIES.contains(&w.code.as_str()))
{
match candidates().find(|w| !LANDED_PIN_ADVISORIES.contains(&w.code.as_str())) {
Some(w) => (w.code.clone(), vec![w.clone()]),
None => (NOT_PINNED.to_string(), Vec::new()),
}
Expand Down Expand Up @@ -674,4 +678,43 @@ mod tests {
assert_eq!(code, "redirect_lock_refused");
assert_eq!(explained, vec![warn("redirect_lock_refused")]);
}

/// #1569: another pin's uv advisory says "wheel" and "uv sync", whole
/// words that are also package names; it never explains a staged
/// package that was not pinned, by name or as the lock-wide fallback.
#[tokio::test]
async fn uv_old_release_advisories_never_explain_a_skip() {
let entry: VendorEntry = serde_json::from_value(serde_json::json!({
"ecosystem": "npm",
"basePurl": "pkg:npm/wheel@1.0.0",
"uuid": "u1",
"artifact": { "path": ".socket/vendor/npm/u1/wheel.tgz" },
"wiring": [],
}))
.expect("minimal vendor entry");
let dep = socket_patch_core::patch::redirect::DepOverride {
ecosystem: "npm".into(),
name: "wheel".into(),
namespace: None,
version: "1.0.0".into(),
token: String::new(),
patch_uuid: "u1".into(),
artifact_url: String::new(),
registry_override: None,
integrity: Default::default(),
};
let warnings: Vec<RewriteWarning> =
socket_patch_core::patch::redirect::UV_OLD_RELEASE_ADVISORIES
.iter()
.map(|code| RewriteWarning {
code: code.to_string(),
detail: "uv.lock: repointed at the hosted wheel; uv sync".into(),
})
.collect();
let root = std::path::Path::new(".");
assert_eq!(
explain(root, &entry, Some(&dep), &warnings).await.0,
NOT_PINNED
);
}
}
18 changes: 18 additions & 0 deletions crates/socket-patch-cli/tests/vex_e2e_common/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,24 @@ pub fn run_lane(suite: &str, uv: &Uv, mode: Mode, lane: Lane) {
"{}: the stale pristine install is not reported: {env:#}",
report.what("scan --mode hosted")
);
// #1569: the wiring shapes uv before 0.5.5 does not honour carry
// the same advisory vendored gives, in its hosted spelling.
let advisory = match lane {
Lane::Transitive => Some("redirect_uv_override_requires_uv_0_5_6"),
Lane::Constraints => Some("redirect_uv_constraints_require_uv_0_5_6"),
_ => None,
};
for code in [
"redirect_uv_override_requires_uv_0_5_6",
"redirect_uv_constraints_require_uv_0_5_6",
] {
assert_eq!(
env.to_string().contains(code),
advisory == Some(code),
"{}: `{code}` advisory expected only on its own lane: {env:#}",
report.what("scan --mode hosted")
);
}
assert!(
!embedded_doc.exists(),
"a failed in-run --vex wrote a document"
Expand Down
Loading
Loading