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
2 changes: 1 addition & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ command's integration tests check its output.
| `redirect_unconfirmed` | `skipped` (`details.mode: "hosted"`) | hosted `scan` / `get` (v5.0, additive): the patch was granted but no lockfile entry pinning it could be rewritten. The status and exit code are unchanged for now, pending the open hosted exit-policy decision (#704); `--silent` hides the human line. |
| `lockfile_unreadable` / `lockfile_unparseable` / `patched_ref_invalid` / `patched_ref_unattributable` | run-level `warnings[]` | vex (every form): lockfile-discovery diagnostics — see "Manifest-less VEX (lockfile discovery)". Never flip the exit on their own. |
| `vex_npm_shrinkwrap_only` | run warning and `failed[].reason` | vex (every form, #899): the patch is wired only in a root `npm-shrinkwrap.json` with no `package-lock.json` twin, which npm >= 12 never reads; no statement until the twin exists. `list` / `rollback` / `remove` still manage the wiring. |
| `vendor_multiple_lockfiles` / `pypi_multiple_lockfiles` | `skipped` (warning) | vendor: a sibling lockfile of another package manager (for PyPI, also a root `requirements.txt` that pins the package beside the wired tool lock) will still install UNPATCHED bytes; names the wired winner + the ignored locks. Beside `Pipfile.lock` or `uv.lock`, an exact registry pin of the package in `requirements.txt` or an in-root `-r` include (`pipenv requirements` or `uv export` output) is wired with the lock instead and never named here (#612, #1368): both files then refer to the committed wheel, the ledger entry records both, every revert restores both, a superseding patch re-wires both, and a re-run over a project whose lock is already wired (a vendor from before, or an export made since) wires the export too (`pypi_requirements_sibling_wired`). A pin the requirements wiring cannot rewrite (a range, extras, an include outside the root) stays a loser. |
| `vendor_multiple_lockfiles` / `pypi_multiple_lockfiles` | `skipped` (warning) | vendor: a sibling lockfile of another package manager (for PyPI, also a root `requirements.txt` that pins the package beside the wired tool lock) will still install UNPATCHED bytes; names the wired winner + the ignored locks. Beside `Pipfile.lock` or `uv.lock`, an exact registry pin of the package in `requirements.txt` or an in-root `-r` include (`pipenv requirements` or `uv export` output) is wired with the lock instead and never named here (#612, #1368): both files then refer to the committed wheel, the ledger entry records both, every revert restores both, a superseding patch re-wires both, and a re-run over a project whose lock is already wired (a vendor from before, or an export made since) wires the export too (`pypi_requirements_sibling_wired`). A pin the requirements wiring cannot rewrite (a range, extras, an include outside the root) stays a loser. Likewise, a `pylock.toml` / `pylock.<name>.toml` that `pipenv lock` writes beside `Pipfile.lock` under `[pipenv] use_pylock = true` is wired with it (#1368), with the same ledger, revert, supersede and re-run behavior (`pypi_pipenv_pylock_wired`); a pylock the wiring cannot rewrite in place (a symlink, a non-registry source) stays a loser. |
| `vendor_npm_shrinkwrap_only` | `skipped` (warning) | vendor / scan / get `--mode vendored` (npm, #899): the package was wired into `npm-shrinkwrap.json`, the only npm lock (also on an in-sync re-run). npm >= 12 never reads the shrinkwrap and installs the unpatched registry bytes; rename the lock to `package-lock.json` (or commit a copy under that name) and re-run. |
| `vendor_workspace_member_skipped` | `skipped` (warning) | vendor / scan / get `--mode vendored` (npm, #688): a lock entry with the package's `name@version` is the project's own source (a workspace member or a `file:` directory), so it is left alone while the lock's registry copies are vendored; patch that source directly if it needs the fix. When it is the only instance, vendoring refuses instead. |
| `vendor_npm_allow_file` | `skipped` (warning, printed as `Warning (vendor_npm_allow_file)`) | vendor / scan / get `--mode vendored` (package-lock, #969): the effective npm `allow-file` setting (env > project `.npmrc` > user > global > builtin) is not `all`, so npm >= 11.14 refuses the vendored `file:` tarball (EALLOWFILE) on install. The setting is respected, never rewritten; the detail names where it comes from and the remedy (`allow-file=all`). `vendor --check` fails the entry for the same reason. |
Expand Down
52 changes: 46 additions & 6 deletions crates/socket-patch-cli/tests/e2e_vex_build/pipenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,10 @@ const PYLOCK_PIPFILE: &str = "[[source]]\nurl = \"https://pypi.org/simple\"\nver
/// `pipenv lock` writes both `Pipfile.lock` and `pylock.toml`:
///
/// * #1122: vendored wires `Pipfile.lock` (what Pipenv installs from when
/// both exist), leaves the pylock alone, and a fresh
/// `pipenv install --deploy` gets the PATCHED six.
/// both exist), and a fresh `pipenv install --deploy` gets the PATCHED
/// six.
/// * #1368: the pylock, which PEP 751 installers (`uv pip install -r
/// pylock.toml`, pip) read, is wired to the same vendored wheel.
/// * #912: the pylock-only checkout (no `Pipfile.lock`) is refused in both
/// modes — Pipenv would drop the `archive` entry and install the upstream
/// release — and the pylock is left byte-identical.
Expand Down Expand Up @@ -422,10 +424,10 @@ fn pylock_flow(pipenv: &Pipenv, root: &Path) {
lock.contains(&format!(".socket/vendor/pypi/{}/", mode.uuid())),
"{what}: Pipfile.lock must point at the vendored wheel: {lock}"
);
assert_eq!(
read(&proj.join("pylock.toml")),
pristine_pylock,
"{what}: the pylock Pipenv ignores is left alone"
let pylock = String::from_utf8(read(&proj.join("pylock.toml"))).unwrap();
assert!(
pylock.contains(&format!(".socket/vendor/pypi/{}/", mode.uuid())),
"{what}: pylock.toml must point at the vendored wheel too: {pylock}"
);
let fresh = root.join("pylock").join("fresh");
copy_tree(&proj, &fresh, &[".venv"]);
Expand All @@ -446,6 +448,44 @@ fn pylock_flow(pipenv: &Pipenv, root: &Path) {
if ok { "pass" } else { "FAIL" },
);
assert!(ok, "{what}: the fresh install must carry the PATCHED six");

// #1368: a PEP 751 installer reading the wired pylock gets the PATCHED
// six too, from the committed wheel.
let pep751 = root.join("pylock").join("pep751");
copy_tree(&proj, &pep751, &[".venv"]);
make_venv(
&pipenv.uv,
python.to_str().unwrap(),
&pep751.join(".venv"),
&[],
)
.unwrap_or_else(|e| panic!("{what}: pylock venv: {e}"));
let py = venv_bin(&pep751.join(".venv"), "python");
let out = tool(
&pipenv.uv,
&pep751,
&[
"pip",
"install",
"--python",
py.to_str().unwrap(),
"-r",
"pylock.toml",
],
&[],
);
assert_ok(&out, &format!("{what}: `uv pip install -r pylock.toml`"));
let (_, bytes, is_patched) = six_oracle(&py, &pep751)
.unwrap_or_else(|| panic!("{what}: six not importable from the pylock install"));
let ok = is_patched && bytes == patched;
record(
"pipenv",
v,
"use_pylock/vendored",
"pylock-install-patched",
if ok { "pass" } else { "FAIL" },
);
assert!(ok, "{what}: the pylock install must carry the PATCHED six");
}

#[test]
Expand Down
45 changes: 29 additions & 16 deletions crates/socket-patch-core/src/vendor/lock_inventory/recover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,13 @@ pub async fn recover_lock_entry(
}
}
}
if entry
.wiring
.iter()
.any(|wiring| wiring.kind == "python_lock_document")
// A Pipenv entry's pylock records (#1368) ride beside its own
// Pipfile.lock fragment, which the fallback below still reads.
if entry.flavor.as_deref() != Some("pipenv")
&& entry
.wiring
.iter()
.any(|wiring| wiring.kind == "python_lock_document")
{
return Err("the pre-vendor Python lock has no hash-pinned pure wheel for this package; reinstall it before repair".to_string());
}
Expand All @@ -183,18 +186,28 @@ pub async fn recover_lock_entry(
// `pdm_lock_package`, poetry `poetry_lock_package`, pipenv
// `pipenv_lock_entry`, bare pip `requirements_line`. Accept them all
// so recovery is not blind to non-uv projects.
let fragment = wiring_original(
entry,
&[
"uv_lock_package",
"python_lock_document",
"pdm_lock_package",
"poetry_lock_package",
"pipenv_lock_entry",
"requirements_line",
],
)
.ok_or_else(|| "no pre-vendor pypi lock fragment recorded".to_string())?;
// A Pipenv entry's own Pipfile.lock fragment first, whatever
// order its pylock records (#1368) sit in.
let pipenv_fragment = if entry.flavor.as_deref() == Some("pipenv") {
wiring_original(entry, &["pipenv_lock_entry"])
} else {
None
};
let fragment = pipenv_fragment
.or_else(|| {
wiring_original(
entry,
&[
"uv_lock_package",
"python_lock_document",
"pdm_lock_package",
"poetry_lock_package",
"pipenv_lock_entry",
"requirements_line",
],
)
})
.ok_or_else(|| "no pre-vendor pypi lock fragment recorded".to_string())?;
// Only uv.lock and pdm's `static_urls` locks inline the wheel's
// registry URL (`url = "…", hash = "sha256:…"`), which is all a
// registry rebuild can fetch from. Default pdm/poetry (`file = …`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,36 @@ async fn recover_pypi_urlless_locks_report_no_fetchable_url() {
.unwrap_err();
assert!(err.contains("no sha256 digests"), "pipenv: {err}");

// #1368: a Pipenv entry also records the pylock wired beside its
// Pipfile.lock. A pylock entry with no hash-pinned wheel must not hide
// the Pipfile.lock fragment's digest set, in either record order.
let mut with_pylock = entry(
"pypi",
"pkg:pypi/six@1.16.0",
vec![
rec(
"python_lock_document",
serde_json::json!(
"lock-version = \"1.0\"\n[[packages]]\nname = \"six\"\nversion = \"1.16.0\"\n"
),
),
rec(
"pipenv_lock_entry",
serde_json::json!({
"hashes": [format!("sha256:{}", "a".repeat(64))],
"version": "==1.16.0",
}),
),
],
);
with_pylock.flavor = Some("pipenv".into());
let recovered = recover_lock_entry(tmp.path(), &with_pylock).await.unwrap();
assert_eq!(
recovered.integrity,
LockIntegrity::Sha256AnyOf(vec!["a".repeat(64)]),
"the Pipfile.lock digest set"
);

// A ledger with no pypi fragment at all is still a hard error.
let bare = entry("pypi", "pkg:pypi/six@1.16.0", vec![]);
assert!(recover_lock_entry(tmp.path(), &bare).await.is_err());
Expand Down
Loading
Loading