From 7c5b4253608ecc465be49ed37b1c22998d621d1e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:30:47 +0000 Subject: [PATCH 1/4] Start fix for #928 Assisted-by: Claude Code:claude-opus-5-5 From 0b18553682a52c5db147eef2b3d82b5f1385e176 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:40:28 +0000 Subject: [PATCH 2/4] Vendor marker-split requirements pins `uv pip compile --universal` writes one requirements line per marker branch when a package resolves to different versions per Python: six==1.16.0 ; python_full_version < '3.12' six==1.17.0 ; python_full_version >= '3.12' Vendored mode refused the whole file with "six is not pinned to ==1.16.0", because any same-name pin to another version counted as an ambiguous range. pip installs exactly one branch, so the other pin is not ambiguous when it and every target pin carry a marker. Vendored requirements now rewrite only the target branch (keeping its marker and hash mode) and leave the other branch alone, matching hosted requirements and vendored pylock. Revert is byte-identical. An unmarked split, a range, `===`, a wildcard, or a file with no target pin is still refused. Fixes #928 Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-core/src/utils/pep440.rs | 16 +++ .../src/vendor/pypi_requirements.rs | 123 +++++++++++++++++- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-core/src/utils/pep440.rs b/crates/socket-patch-core/src/utils/pep440.rs index a46ce6277..50543d5b4 100644 --- a/crates/socket-patch-core/src/utils/pep440.rs +++ b/crates/socket-patch-core/src/utils/pep440.rs @@ -120,6 +120,16 @@ pub(crate) fn is_exact_pin_of(specifier: &str, version: &str) -> bool { .is_some_and(|pinned| versions_equal(pinned, version)) } +/// Whether `specifier` (whitespace already stripped) is an exact `==` pin +/// of SOME valid release — the shape [`is_exact_pin_of`] accepts, for any +/// version. Arbitrary equality (`===`) and wildcards are not exact pins. +pub(crate) fn is_exact_pin(specifier: &str) -> bool { + specifier + .strip_prefix("==") + .filter(|pinned| !pinned.starts_with('=') && !pinned.contains('*')) + .is_some_and(|pinned| parse(pinned).is_some()) +} + #[cfg(test)] mod tests { use super::*; @@ -183,5 +193,11 @@ mod tests { assert!(!is_exact_pin_of("~=1.16.0", "1.16.0")); assert!(!is_exact_pin_of("==1.15", "1.16.0")); assert!(!is_exact_pin_of("1.16.0", "1.16.0")); + assert!(is_exact_pin("==1.17.0")); + assert!(is_exact_pin("==1.16")); + assert!(!is_exact_pin("===1.17.0")); + assert!(!is_exact_pin("==1.17.*")); + assert!(!is_exact_pin(">=1.17")); + assert!(!is_exact_pin("==not a version")); } } diff --git a/crates/socket-patch-core/src/vendor/pypi_requirements.rs b/crates/socket-patch-core/src/vendor/pypi_requirements.rs index be8602984..ebbaae606 100644 --- a/crates/socket-patch-core/src/vendor/pypi_requirements.rs +++ b/crates/socket-patch-core/src/vendor/pypi_requirements.rs @@ -64,11 +64,20 @@ type PinSpan = (usize, usize, Option, bool); /// Scan one file for the target package: every clean exact /// `canon_name==version` pin, plus whether any occurrence carries extras or -/// a non-exact specifier. +/// names the package ambiguously. +/// +/// An exact pin of a DIFFERENT version is not ambiguous when it sits on its +/// own environment-marker branch and every target pin does too: that is +/// the shape `uv pip compile --universal` writes when a package resolves +/// differently per Python (#928), and pip installs exactly one branch. +/// Only the target branches are rewritten; the other branch is left alone, +/// as hosted requirements and vendored pylock already do. Without markers +/// on both sides pip could see both pins at once, so it stays a refusal. fn scan_pins(content: &str, canon_name: &str, version: &str) -> (Vec, bool, bool) { - let mut exact = Vec::new(); + let mut exact: Vec = Vec::new(); let mut found_extras = false; let mut found_range = false; + let mut other_branches = false; for ll in logical_lines(content) { let Some(req) = parse_requirement_line(&ll.text) else { continue; @@ -88,16 +97,22 @@ fn scan_pins(content: &str, canon_name: &str, version: &str) -> (Vec, b // pip resolves `==` under PEP 440 (`==1.16` installs 1.16.0). if crate::utils::pep440::is_exact_pin_of(&spec_no_ws, version) { exact.push((ll.start, ll.physical.len(), req.marker, req.hashed)); + } else if req.marker.is_some() && crate::utils::pep440::is_exact_pin(&spec_no_ws) { + other_branches = true; } else { found_range = true; } } + if other_branches && (exact.is_empty() || exact.iter().any(|(_, _, m, _)| m.is_none())) { + found_range = true; + } (exact, found_extras, found_range) } /// Find the target pin in one file's content. Precedence is fail-closed: /// any extras occurrence wins over any non-pin occurrence wins over a clean -/// exact pin — a file that names the package ambiguously is never rewritten. +/// exact pin — a file that names the package ambiguously is never rewritten +/// (a marker-split other version is not ambiguous; see [`scan_pins`]). fn find_pin(content: &str, canon_name: &str, version: &str) -> PinSearch { let (exact, found_extras, found_range) = scan_pins(content, canon_name, version); if found_extras { @@ -1268,6 +1283,54 @@ mod tests { assert_eq!(find_pin("six\n", "six", "1.16.0"), PinSearch::Range); // Pinned, but to a different version than the one being vendored. assert_eq!(find_pin("six==1.15.0\n", "six", "1.16.0"), PinSearch::Range); + // #928: `uv pip compile --universal` splits a package across marker + // branches; the other branch's exact pin is disjoint, not ambiguous. + match find_pin( + "six==1.16.0 ; python_full_version < '3.12'\n\ + six==1.17.0 ; python_full_version >= '3.12'\n", + "six", + "1.16.0", + ) { + PinSearch::Exact { + line_start, marker, .. + } => { + assert_eq!(line_start, 0); + assert_eq!(marker.as_deref(), Some("python_full_version < '3.12'")); + } + other => panic!("expected Exact, got {other:?}"), + } + // The other branch first in the file is the same split. + assert!(matches!( + find_pin( + "six==1.17.0 ; python_version >= \"3.12\"\nsix==1.16.0 ; python_version < \"3.12\"\n", + "six", + "1.16.0" + ), + PinSearch::Exact { line_start: 1, .. } + )); + // Still fail-closed whenever pip could see both pins at once, or + // the other branch is not itself an exact pin. + for content in [ + // Other version unmarked. + "six==1.16.0 ; python_version < \"3.12\"\nsix==1.17.0\n", + // Target unmarked. + "six==1.16.0\nsix==1.17.0 ; python_version >= \"3.12\"\n", + // One target occurrence unmarked among marked ones. + "six==1.16.0 ; python_version < \"3.12\"\nsix==1.16.0\n\ + six==1.17.0 ; python_version >= \"3.12\"\n", + // Other branch is a range / arbitrary equality / wildcard. + "six==1.16.0 ; python_version < \"3.12\"\nsix>=1.17 ; python_version >= \"3.12\"\n", + "six==1.16.0 ; python_version < \"3.12\"\nsix===1.17.0 ; python_version >= \"3.12\"\n", + "six==1.16.0 ; python_version < \"3.12\"\nsix==1.17.* ; python_version >= \"3.12\"\n", + // No target pin at all: appending an unmarked line would clash. + "six==1.17.0 ; python_version >= \"3.12\"\n", + ] { + assert_eq!( + find_pin(content, "six", "1.16.0"), + PinSearch::Range, + "{content}" + ); + } assert_eq!( find_pin("requests==2.31.0\n", "six", "1.16.0"), PinSearch::Absent @@ -1411,6 +1474,60 @@ mod tests { ); } + /// #928: a `uv pip compile --universal --generate-hashes` file splits six + /// across marker branches. Only the vendored version's branch is + /// rewritten (marker kept); the other branch is left alone, and revert + /// is byte-identical. + #[tokio::test] + async fn marker_split_rewrites_only_the_vendored_branch() { + let original = "six==1.16.0 ; python_full_version < '3.12' \\\r\n \ + --hash=sha256:1111\r\n\ + six==1.17.0 ; python_full_version >= '3.12' \\\r\n \ + --hash=sha256:2222\r\n"; + let tmp = write_root(original).await; + let wiring = wire_requirements(tmp.path(), "six", "1.16.0", REL_WHEEL, SHA) + .await + .unwrap(); + assert_eq!(wiring.len(), 1); + assert_eq!( + read_root(tmp.path()).await, + format!( + "./{REL_WHEEL} ; python_full_version < '3.12' --hash=sha256:{SHA} \ + # socket-patch vendor: six==1.16.0\r\n\ + six==1.17.0 ; python_full_version >= '3.12' \\\r\n \ + --hash=sha256:2222\r\n" + ) + ); + + let outcome = revert_requirements(&entry_for(wiring), tmp.path(), false).await; + assert!(outcome.success, "{:?}", outcome.error); + assert_eq!(read_root(tmp.path()).await, original); + } + + /// #928: the split with no hashes, as `uv pip compile --universal` + /// writes it, also passes the preflight the orchestrator runs first. + #[tokio::test] + async fn marker_split_preflights_fresh_and_wires_unhashed() { + let original = "six==1.16.0 ; python_full_version < '3.12'\n\ + six==1.17.0 ; python_full_version >= '3.12'\n"; + let tmp = write_root(original).await; + assert!(matches!( + preflight_requirements(tmp.path(), "six", "1.16.0", "u").await, + Ok(RequirementsTarget::Fresh) + )); + wire_requirements(tmp.path(), "six", "1.16.0", REL_WHEEL, SHA) + .await + .unwrap(); + assert_eq!( + read_root(tmp.path()).await, + format!( + "./{REL_WHEEL} ; python_full_version < '3.12' \ + # socket-patch vendor: six==1.16.0\n\ + six==1.17.0 ; python_full_version >= '3.12'\n" + ) + ); + } + #[tokio::test] async fn absent_package_appends_managed_transitive_line() { let tmp = write_root("python-dateutil==2.8.2\n").await; From 038f558d9fda092e753946e1dc9ffc1acd3a1dfe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:41:35 +0000 Subject: [PATCH 3/4] Add pip e2e for vendoring a marker-split pin Covers #928 end to end with real pip: a hashed `uv pip compile --universal` style requirements.txt that splits six across marker branches vendors only the matching branch, a fresh --no-index --require-hashes checkout installs the patched wheel, manifest-less VEX attests it, and revert restores the file byte-identical. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vendor_pypi_build.rs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index 3ea510e2d..ac9f700fa 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -1538,3 +1538,104 @@ fn pip_vendored_requirements_evaluate_environment_markers() { ); } } + +/// #928: `uv pip compile --universal` splits a package across marker +/// branches when it resolves differently per Python. Vendoring the patched +/// 1.16.0 must rewrite only that branch (marker and hash kept) and leave +/// the other version's branch alone, so a fresh `--no-index` checkout +/// installs the patched wheel on the matching Python, and revert is +/// byte-identical. Before the fix the other branch made vendored refuse +/// with a false `pypi_requirement_not_pinned`. +#[test] +#[serial_test::serial] +fn pip_vendored_requirements_marker_split_rewrites_matching_branch() { + let python = find_python().expect("Python is required for the pip marker-split regression"); + let tmp = tempfile::tempdir().unwrap(); + let project = tmp.path().join("proj"); + std::fs::create_dir_all(&project).unwrap(); + assert_tool_ok( + &tool(Path::new(python), &project, &["-m", "venv", ".venv"], &[]), + "create source venv", + ); + let venv = project.join(".venv"); + assert_tool_ok( + &tool( + &venv.join("bin/pip"), + &project, + &["install", "--disable-pip-version-check", "six==1.16.0"], + &[], + ), + "install upstream six", + ); + let patched = stage_patch(&project, &site_packages(&venv).join("six.py")); + // The `uv pip compile --universal --generate-hashes` shape; the 1.17.0 + // branch can never match the running interpreter. + let original = "six==1.16.0 ; python_full_version >= '3' \\\n \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254\n\ + six==1.17.0 ; python_full_version < '3' \\\n \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274\n"; + std::fs::write(project.join("requirements.txt"), original).unwrap(); + + let (code, stdout, stderr) = run_vendored(&VendorDriver::VendorOffline, &project); + assert_eq!(code, 0, "vendor failed: {stdout}\n{stderr}"); + assert_vendored_applied(&parse_envelope(&stdout)); + let requirements = std::fs::read_to_string(project.join("requirements.txt")).unwrap(); + assert!( + requirements.ends_with( + "six==1.17.0 ; python_full_version < '3' \\\n \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274\n" + ), + "the other branch must stay untouched:\n{requirements}" + ); + let vendor_line = requirements.lines().next().unwrap(); + assert!( + vendor_line.starts_with("./.socket/vendor/pypi/") + && vendor_line.contains("; python_full_version >= '3' --hash=sha256:"), + "the 1.16.0 branch becomes a hashed, marker-carrying vendor line: {vendor_line}" + ); + + let fresh = project.join("fresh"); + std::fs::create_dir_all(&fresh).unwrap(); + std::fs::copy( + project.join("requirements.txt"), + fresh.join("requirements.txt"), + ) + .unwrap(); + copy_dir_recursive(&project.join(".socket"), &fresh.join(".socket")); + assert_tool_ok( + &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), + "create fresh venv", + ); + let fresh_venv = fresh.join(".venv"); + assert_tool_ok( + &tool( + &fresh_venv.join("bin/pip"), + &fresh, + &[ + "install", + "--disable-pip-version-check", + "--no-index", + "--require-hashes", + "-r", + "requirements.txt", + ], + &[], + ), + "install vendored marker-split requirements", + ); + assert_eq!(python_oracle(&fresh_venv, &fresh), "1"); + manifestless_vex( + &fresh, + "pip vendored marker split", + &patched, + original.as_bytes(), + ); + + let (code, stdout, stderr) = + run_socket(&project, &["vendor", "--revert", "--offline", "--json"]); + assert_eq!(code, 0, "revert failed: {stdout}\n{stderr}"); + assert_eq!( + std::fs::read_to_string(project.join("requirements.txt")).unwrap(), + original + ); +} From ec0326029da1da4508b72a09d96b5d0cb44389c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 10:01:19 +0000 Subject: [PATCH 4/4] Port #878 digest helper fix for main's red CI main's CI fails `utils::digest::tests::production_digests_go_through_ the_helpers`, because three Gradle/Maven call sites hash with sha1/sha2 directly instead of the digest helpers. This is the same change as #878 (agent/ci-gradle-digest-helpers), ported so this PR's CI can go green. It becomes a no-op once #878 lands on main. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-core/src/crawlers/gradle_cache.rs | 9 ++++----- crates/socket-patch-core/src/patch/jvm_jar.rs | 7 ++----- crates/socket-patch-core/src/patch/sidecars/maven.rs | 4 +--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/socket-patch-core/src/crawlers/gradle_cache.rs b/crates/socket-patch-core/src/crawlers/gradle_cache.rs index ef295ee27..afd7c4fba 100644 --- a/crates/socket-patch-core/src/crawlers/gradle_cache.rs +++ b/crates/socket-patch-core/src/crawlers/gradle_cache.rs @@ -70,8 +70,7 @@ pub fn hash_eq(dir_name: &str, sha1_hex: &str) -> bool { /// Whether `bytes` are the pristine download Gradle stored in the hash /// directory `dir_name` (their sha1 names it). pub fn pristine(dir_name: &str, bytes: &[u8]) -> bool { - use sha1::{Digest, Sha1}; - hash_eq(dir_name, &hex::encode(Sha1::digest(bytes))) + hash_eq(dir_name, &crate::utils::digest::sha1_hex_of(bytes)) } /// Whether `path` is a version directory of a `files-2.1` tree @@ -432,8 +431,6 @@ impl DerivedIndex { /// The [`DerivedCopies`] of the jar `jar_leaf` whose pristine bytes /// hash to `pristine_sha1`. pub fn query(&self, jar_leaf: &str, pristine_sha1: &str) -> DerivedCopies { - use sha1::{Digest, Sha1}; - let instrumented = format!("instrumented-{jar_leaf}"); let mut out = DerivedCopies { incomplete: self.incomplete, @@ -460,7 +457,9 @@ impl DerivedIndex { out.stale.push(path.clone()); } else if name == jar_leaf || name == instrumented { match crate::utils::fs::read_regular_to_bytes_sync(path) { - Ok(bytes) if hash_eq(&hex::encode(Sha1::digest(&bytes)), pristine_sha1) => { + Ok(bytes) + if hash_eq(&crate::utils::digest::sha1_hex_of(&bytes), pristine_sha1) => + { out.stale.push(path.clone()) } Ok(_) => out.unknown.push(path.clone()), diff --git a/crates/socket-patch-core/src/patch/jvm_jar.rs b/crates/socket-patch-core/src/patch/jvm_jar.rs index 82d679406..f38a84403 100644 --- a/crates/socket-patch-core/src/patch/jvm_jar.rs +++ b/crates/socket-patch-core/src/patch/jvm_jar.rs @@ -25,8 +25,6 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use sha1::Digest as _; - use crate::crawlers::gradle_cache; use crate::hash::git_sha256::compute_git_sha256_from_bytes; use crate::manifest::schema::PatchFileInfo; @@ -353,12 +351,11 @@ fn unpatched_members( } fn sha256_hex(bytes: &[u8]) -> String { - use sha2::Digest as _; - hex::encode(sha2::Sha256::digest(bytes)) + crate::utils::digest::sha256_hex_of(bytes) } fn sha1_hex(bytes: &[u8]) -> String { - hex::encode(sha1::Sha1::digest(bytes)) + crate::utils::digest::sha1_hex_of(bytes) } /// `/jvm-originals/.jar`. diff --git a/crates/socket-patch-core/src/patch/sidecars/maven.rs b/crates/socket-patch-core/src/patch/sidecars/maven.rs index f2f5a2466..8798bfce6 100644 --- a/crates/socket-patch-core/src/patch/sidecars/maven.rs +++ b/crates/socket-patch-core/src/patch/sidecars/maven.rs @@ -17,8 +17,6 @@ use std::path::{Path, PathBuf}; -use sha1::Digest as _; - use super::{ SidecarAdvisory, SidecarAdvisoryCode, SidecarError, SidecarFile, SidecarFileAction, SidecarPayload, SidecarSeverity, @@ -44,7 +42,7 @@ impl Algo { fn digest(self, bytes: &[u8]) -> String { match self { - Algo::Sha1 => hex::encode(sha1::Sha1::digest(bytes)), + Algo::Sha1 => crate::utils::digest::sha1_hex_of(bytes), Algo::Md5 => hex::encode(md5(bytes)), } }