From c9625f15bb56e6b03f75415e1b927958807081de Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 09:55:02 -0400 Subject: [PATCH 1/6] Vendor single-module Maven poms through the suffixed jvm planner (#973) A root pom.xml with no is now a Maven reactor of one: the dependency is pinned to -socket., served from the committed .socket/vendor/maven2 tree, with .mvn/maven.config and the fallback socket-patch-vendor repository. Shape::Other now means no pom.xml and no Gradle, sbt or scala-cli build, refused as vendor_jvm_shape_unsupported (reason no_build_file). The legacy same-GAV forward path is deleted: MavenPrelude, vendor_maven_single, materialise_and_write, acquire_upstream_pom, artifact_in_sync, build_repo_edit and its anchor helpers, the comment-stripping declares_modules (#716), project_has_gradle, local_cache_shadow_warning and the jvm_shape/legacy_mixed_root bridge. Only the revert of maven_pom_repository entries stays, so pre-v5 ledgers still unwind byte for byte. A root whose ledger still holds a maven_pom_repository entry is refused whole (vendor_jvm_shape_unsupported, reason legacy_maven_root) with nothing written; service_preflight plans no download for it and jvm_gate_preflight keeps a hosted pin there. The remedy is `socket-patch vendor --revert`, then vendor again. sbt detection treats any planner-wired root pom (not only a multi-module one) as already wired by the Maven backend. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/vendor/jvm/maven_reactor.rs | 5 +- .../socket-patch-core/src/vendor/jvm/mod.rs | 42 +- .../socket-patch-core/src/vendor/jvm/sbt.rs | 28 +- .../src/vendor/maven_repo.rs | 2568 +++-------------- crates/socket-patch-core/src/vendor/mod.rs | 2 +- .../src/vex/discover/maven.rs | 32 +- 6 files changed, 461 insertions(+), 2216 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index 4d311b2ee..df2a71998 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -3815,7 +3815,10 @@ mod tests { "; assert!(!declares_modules(ear)); let read = |p: &str| (p == "pom.xml").then(|| ear.as_bytes().to_vec()); - assert_eq!(super::super::detect(&read), Shape::Other); + let builds = super::super::detect_builds(&read); + assert_eq!(builds.maven, Some(super::super::MavenShape::Single)); + // A single pom is a reactor of one (#973). + assert_eq!(builds.shape(), Shape::MavenReactor); } #[test] diff --git a/crates/socket-patch-core/src/vendor/jvm/mod.rs b/crates/socket-patch-core/src/vendor/jvm/mod.rs index e0fb7cb67..4299f1976 100644 --- a/crates/socket-patch-core/src/vendor/jvm/mod.rs +++ b/crates/socket-patch-core/src/vendor/jvm/mod.rs @@ -1,7 +1,8 @@ //! The v5 vendored JVM backend (`docs/design/maven-vendoring.md`). //! -//! Handles multi-module Maven reactors and Gradle builds automatically. -//! Single-POM builds retain the legacy backend. +//! Handles every Maven root (a single-module pom is planned as a reactor +//! of one), Gradle builds, mixed Maven + Gradle roots, sbt builds and +//! scala-cli directory builds. //! //! The planners are pure: they read project files through a [`ReadFn`] and //! return the full post-vendor bytes of every file they touch plus one @@ -207,7 +208,8 @@ impl<'a> JvmPatch<'a> { /// Which JVM build the project root holds. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Shape { - /// A root `pom.xml` that declares ``. + /// A root `pom.xml`, whether it declares `` or is a single + /// module (a reactor of one). MavenReactor, /// No root `pom.xml`, and a Gradle settings or build script. Gradle, @@ -218,7 +220,7 @@ pub enum Shape { Sbt, /// A scala-cli directory build ([`scala_cli::detect`]). ScalaCli, - /// Anything else (including a single-module pom, which stays legacy). + /// No root `pom.xml` and no Gradle, sbt or scala-cli build. Other, } @@ -309,18 +311,18 @@ pub struct Detected { } impl Detected { - /// The backend's planner shape: a single pom alone stays on the legacy - /// backend; next to a Gradle build it is planned as a one-pom reactor, - /// so both halves share one ledger entry. + /// The backend's planner shape. Any root pom, single-module or not, is + /// planned as a reactor; next to a Gradle build both halves are planned + /// together, so they share one ledger entry. pub fn shape(&self) -> Shape { if let Some(scala) = self.scala { return scala; } match (self.maven, self.gradle) { (Some(_), true) => Shape::Mixed, - (Some(MavenShape::Reactor), false) => Shape::MavenReactor, + (Some(_), false) => Shape::MavenReactor, (None, true) => Shape::Gradle, - _ => Shape::Other, + (None, false) => Shape::Other, } } } @@ -382,11 +384,17 @@ pub fn plan_with_config( } Shape::Sbt => sbt::plan(read, patch), Shape::ScalaCli => scala_cli::plan(read, patch), - Shape::Other => Err(JvmRefusal { - code: "vendor_jvm_shape_unsupported", - detail: "reason: no_build_file: not a multi-module Maven reactor or a Gradle build" - .to_string(), - }), + Shape::Other => Err(no_build_file_refusal()), + } +} + +/// The refusal of a [`Shape::Other`] root. +pub fn no_build_file_refusal() -> JvmRefusal { + JvmRefusal { + code: "vendor_jvm_shape_unsupported", + detail: "reason: no_build_file: no pom.xml, Gradle, sbt or scala-cli build at the \ + project root" + .to_string(), } } @@ -836,7 +844,7 @@ mod tests { }; assert_eq!(detect(&reactor), Shape::MavenReactor); let single = |p: &str| (p == "pom.xml").then(|| b"".to_vec()); - assert_eq!(detect(&single), Shape::Other); + assert_eq!(detect(&single), Shape::MavenReactor); let gradle = |p: &str| (p == "settings.gradle.kts").then(Vec::new); assert_eq!(detect(&gradle), Shape::Gradle); let empty = |_: &str| None; @@ -844,7 +852,7 @@ mod tests { } /// #395: a `pom.xml` next to a Gradle build is both, whichever kind of - /// pom it is; a single pom alone stays on the legacy backend. + /// pom it is; a single pom alone is a reactor of one (#973). #[test] fn detect_reports_both_builds_of_a_mixed_root() { let files = |pom: &'static [u8], gradle: Option<&'static str>| { @@ -878,7 +886,7 @@ mod tests { Shape::Mixed, ), (&reactor[..], None, MavenShape::Reactor, Shape::MavenReactor), - (&single[..], None, MavenShape::Single, Shape::Other), + (&single[..], None, MavenShape::Single, Shape::MavenReactor), ] { let read = files(pom, gradle); let d = detect_builds(&read); diff --git a/crates/socket-patch-core/src/vendor/jvm/sbt.rs b/crates/socket-patch-core/src/vendor/jvm/sbt.rs index 2af147ed4..7236d745d 100644 --- a/crates/socket-patch-core/src/vendor/jvm/sbt.rs +++ b/crates/socket-patch-core/src/vendor/jvm/sbt.rs @@ -61,7 +61,7 @@ fn text<'a>(read: ReadFn<'a>) -> impl Fn(&str) -> Option + 'a { } /// [`Shape::Sbt`] for a directory holding an sbt marker, unless the -/// project is already vendored through the Maven reactor, the legacy +/// project is already vendored through the Maven reactor, the pre-v5 /// single-pom path, the Gradle backend or the scala-cli backend (its wiring /// stays on that backend; a new pin there then plans as before). pub fn detect(read: ReadFn<'_>) -> Option { @@ -89,8 +89,9 @@ fn scala_cli_wired(read: ReadFn<'_>) -> bool { read(super::scala_cli::ROOT_FILE).is_some() || read(super::coursier_tree::INDEX_REL).is_some() } -/// The legacy single-pom path (`vendor/maven_repo.rs`, `Shape::Other`) -/// already wired the root pom: its `` id. +/// The pre-v5 single-pom backend already wired the root pom: its +/// `` id. The root stays off sbt so that `vendor --revert` can +/// unwind it first (vendoring it is refused as `legacy_maven_root`). fn legacy_pom_wired(read: ReadFn<'_>) -> bool { read("pom.xml").is_some_and(|b| { let pom = String::from_utf8_lossy(&b); @@ -102,17 +103,17 @@ fn legacy_pom_wired(read: ReadFn<'_>) -> bool { }) } -/// The Maven reactor backend already wired this root: its tagged block or -/// pin in the root pom, or its repository tail in `.mvn/maven.config`. +/// The Maven reactor backend already wired this root (a multi-module +/// reactor or a single pom): its tagged block or pin in the root pom, or +/// its repository tail in `.mvn/maven.config`. fn reactor_wired(read: ReadFn<'_>) -> bool { let Some(pom) = read("pom.xml").map(|b| String::from_utf8_lossy(&b).into_owned()) else { return false; }; - maven_reactor::declares_modules(&pom) - && (pom.contains("") - || pom.contains("") + || pom.contains("` comment and outside `` (a -/// profile-scoped `` is only consulted when that profile is -/// activated, so it can never serve the always-on vendored repository). -/// `None` when every occurrence is masked. -fn find_wireable_anchor(text: &str, needle: &str) -> Option { - let mut masked = comment_spans(text); - masked.extend(profiles_spans(text, &masked)); - find_outside(text, needle, 0, &masked) -} - -/// Byte spans of `` comments (an unterminated comment runs to EOF — -/// the same drop-the-tail discipline as [`strip_xml_comments`]). -fn comment_spans(text: &str) -> Vec<(usize, usize)> { - let mut spans = Vec::new(); - let mut from = 0; - while let Some(rel) = text[from..].find("") { - Some(rel_end) => { - let end = start + 4 + rel_end + 3; - spans.push((start, end)); - from = end; - } - None => { - spans.push((start, text.len())); - break; - } - } - } - spans -} - -/// Byte spans covered by `…` elements, with the tags -/// themselves matched outside `comments`. A self-closing `` spans -/// nothing; an unclosed element masks through EOF (fail-closed — better to -/// refuse than to wire a block Maven may never read). -fn profiles_spans(text: &str, comments: &[(usize, usize)]) -> Vec<(usize, usize)> { - const OPEN: &str = "` is not ``. - let after = &text[open + OPEN.len()..]; - let boundary_ok = match after.chars().next() { - None => true, - Some(c) => c == '>' || c == '/' || c.is_whitespace(), - }; - if !boundary_ok { - from = open + OPEN.len(); - continue; - } - // A self-closing `` (or ``) has no interior. - if let Some(gt) = text[open..].find('>').map(|r| open + r) { - if text[..gt].ends_with('/') { - from = gt + 1; - continue; - } - } - match find_outside(text, CLOSE, open, comments) { - Some(close) => { - let end = close + CLOSE.len(); - spans.push((open, end)); - from = end; - } - None => { - spans.push((open, text.len())); - break; - } - } - } - spans -} - -/// First occurrence of `needle` at/after `from` whose start lies outside every -/// `spans` range; `None` when only masked occurrences remain. -fn find_outside(text: &str, needle: &str, from: usize, spans: &[(usize, usize)]) -> Option { - let mut at = from; - while let Some(rel) = text[at..].find(needle) { - let pos = at + rel; - if !spans.iter().any(|&(s, e)| pos >= s && pos < e) { - return Some(pos); - } - at = pos + needle.len(); - } - None -} - -/// `insert_before` at a known byte offset: insert `insertion` (already -/// newline-terminated) at the start of the line containing `at`. -fn insert_block_at(haystack: &str, at: usize, insertion: &str) -> String { - let line_start = haystack[..at].rfind('\n').map(|n| n + 1).unwrap_or(0); - let mut out = String::with_capacity(haystack.len() + insertion.len()); - out.push_str(&haystack[..line_start]); - out.push_str(insertion); - out.push_str(&haystack[line_start..]); - out -} - /// The `` element served from the committed maven2 repo. The URL /// uses `${project.basedir}` so it resolves relative to the pom on any checkout; /// `checksumPolicy=fail` makes Maven hard-fail on a jar/pom that doesn't match @@ -2072,92 +1437,6 @@ fn repository_block(repo_id: &str, uuid_dir_rel: &str) -> String { ) } -/// True when the pom declares a real (non-commented) `` element — an -/// aggregator/multi-module root. Comments are stripped first so a commented-out -/// `` never triggers a refusal, and the open tag is boundary-matched -/// so `` is not mistaken for it. -fn declares_modules(pom_text: &str) -> bool { - let stripped = strip_xml_comments(pom_text); - real_open_tag(&stripped, "modules") -} - -/// Remove every `` span (comments do not nest in XML). Used before -/// tag detection so commented-out markup is never matched. -fn strip_xml_comments(text: &str) -> String { - let mut out = String::with_capacity(text.len()); - let mut rest = text; - loop { - match rest.find("") { - Some(end) => rest = &rest[start + end + 3..], - None => return out, // unterminated comment: drop the tail - } - } - None => { - out.push_str(rest); - return out; - } - } - } -} - -/// True when `text` contains a real opening tag for `element` — ``, -/// ``, or `` — where the char after the name is a tag -/// boundary (`>`, `/`, or whitespace). Prefix matches (``) do not -/// count. Mirrors the maven crawler's `opening_tag` boundary discipline. -fn real_open_tag(text: &str, element: &str) -> bool { - let needle = format!("<{element}"); - let mut from = 0; - while let Some(rel) = text[from..].find(&needle) { - let pos = from + rel; - let after = &text[pos + needle.len()..]; - match after.chars().next() { - None => return true, // name runs to end of input - Some(c) if c == '>' || c == '/' || c.is_whitespace() => return true, - _ => from = pos + needle.len(), - } - } - false -} - -/// Whether the project root carries a Gradle build marker (used only to give a -/// gradle-only project the specific `vendor_gradle_unsupported` refusal). -async fn project_has_gradle(project_root: &Path) -> bool { - for marker in [ - "build.gradle", - "build.gradle.kts", - "settings.gradle", - "settings.gradle.kts", - ] { - if tokio::fs::metadata(project_root.join(marker)).await.is_ok() { - return true; - } - } - false -} - -/// The always-on `vendor_maven_local_cache_shadow` advisory carrying the purge -/// one-liner. -fn local_cache_shadow_warning( - group_id: &str, - artifact_id: &str, - version: &str, - group_path: &str, -) -> VendorWarning { - VendorWarning::new( - "vendor_maven_local_cache_shadow", - format!( - "Maven resolves the local repository (~/.m2) BEFORE any configured , so a \ - warm ~/.m2 copy of {group_id}:{artifact_id}:{version} silently shadows the vendored \ - patched artifact. Purge it with: \ - mvn dependency:purge-local-repository -DmanualInclude={group_id}:{artifact_id} \ - (or delete ~/.m2/repository/{group_path}/{artifact_id}/{version})" - ), - ) -} - /// Revert our `` wiring from `pom.xml`. `Ok(true)` = reverted (or /// would be on dry run) / already gone; `Ok(false)` = drifted (the live pom no /// longer carries our repository block), left alone; `Err` = a real I/O failure. @@ -2167,7 +1446,7 @@ fn local_cache_shadow_warning( /// wrote (`w.new`) — nothing has changed since vendoring. Otherwise — a sibling /// patch added another `` into the same ``, or the /// user hand-edited the pom AFTER vendoring — we surgically excise ONLY the -/// exact `` block we authored (`build_repo_edit` renders it +/// exact `` block we authored ([`repository_block`] renders it /// deterministically, so we reproduce it verbatim from the repo id + uuid dir) /// and leave every other byte (sibling wiring, user edits) intact. If we /// created the `` section and excising our block leaves it empty, @@ -2236,9 +1515,9 @@ async fn revert_repo_record( } /// After excising our ``, drop a `` section left with -/// no children (the section we created for the first vendored package). Matches -/// `build_repo_edit`'s ` \n… \n` rendering so a -/// section it created is removed byte-for-byte; a section that still holds a +/// no children (the section the pre-v5 backend created for the first vendored +/// package, rendered as ` \n` + blocks + ` \n` +/// just before ``), byte for byte; a section that still holds a /// sibling `` is untouched (its inner bytes are non-whitespace). fn strip_empty_repositories(pom: &str) -> String { let open = " \n"; @@ -2267,9 +1546,13 @@ mod tests { use std::io::{Read as _, Write as _}; use std::path::PathBuf; + use std::collections::HashMap; + use super::*; + use crate::crawlers::maven_crawler::is_safe_maven_coordinate; use crate::hash::git_sha256::compute_git_sha256_from_bytes; - use crate::vendor::state::VENDOR_MARKER_FILE; + use crate::manifest::schema::PatchFileInfo; + use crate::patch::apply::ApplyResult; const UUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; const PURL: &str = "pkg:maven/org.apache.commons/commons-text@1.10.0"; @@ -2497,171 +1780,6 @@ mod tests { Some(out) } - /// In-memory repack equivalence: keeping the jar's members in memory must rebuild the - /// EXACT bytes the extract-to-disk rebuild produced — the artifact's sha1 - /// sidecar and every downstream pin ride on them. Driven twice over one - /// fixture, once with the in-memory repack forced off. - - /// A jar whose entry escapes the stage must be refused the same way - /// whichever staging path ran — the traversal guard is the one thing both - /// readers have to agree on before anything is written. - - #[tokio::test] - #[serial_test::serial] - async fn happy_path_wires_repo_jar_pom_sidecars() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - - let (result, entry, warnings) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success, "{:?}", result.error); - - // Artifact: rebuilt jar with the patched NOTICE.txt at the maven2 leaf. - let jar = tokio::fs::read(root.join(jar_rel())).await.unwrap(); - assert_eq!(read_jar_entry(&jar, JAR_FILE).as_deref(), Some(PATCHED)); - assert!(read_jar_entry(&jar, "META-INF/MANIFEST.MF").is_some()); - - // Real upstream pom copied verbatim (carries the transitive dep). - let pom = tokio::fs::read(root.join(format!("{}/commons-text-1.10.0.pom", leaf_rel()))) - .await - .unwrap(); - assert_eq!(pom, UPSTREAM_POM); - assert!( - String::from_utf8_lossy(&pom).contains("commons-lang3"), - "vendored pom keeps the transitive declaration" - ); - - // sha1 sidecars for both, matching the bytes. - let jar_sha1 = tokio::fs::read_to_string(root.join(format!("{}.sha1", jar_rel()))) - .await - .unwrap(); - assert_eq!(jar_sha1.trim(), sha1_hex(&jar)); - let pom_sha1 = tokio::fs::read_to_string( - root.join(format!("{}/commons-text-1.10.0.pom.sha1", leaf_rel())), - ) - .await - .unwrap(); - assert_eq!(pom_sha1.trim(), sha1_hex(UPSTREAM_POM)); - - // Marker present. - assert!(root - .join(format!(".socket/vendor/maven/{UUID}/{VENDOR_MARKER_FILE}")) - .exists()); - - // pom.xml wired with our (id + file:// url + checksumPolicy). - let pom_xml = tokio::fs::read_to_string(root.join(PROJECT_POM)) - .await - .unwrap(); - assert!(pom_xml.contains(&format!("socket-patch-vendor-{UUID}"))); - assert!(pom_xml.contains(&format!( - "file://${{project.basedir}}/.socket/vendor/maven/{UUID}" - ))); - assert!(pom_xml.contains("fail")); - assert!(pom_xml.contains("")); - - // The always-on shadow advisory fired. - assert!( - warnings - .iter() - .any(|w| w.code == "vendor_maven_local_cache_shadow"), - "shadow warning must always fire: {warnings:?}" - ); - assert!( - warnings - .iter() - .any(|w| w.code == "vendor_maven_local_cache_shadow" - && w.detail.contains("purge-local-repository")), - "shadow warning carries the purge one-liner" - ); - - // Ledger entry shape. - let entry = entry.expect("success carries a ledger entry"); - assert_eq!(entry.ecosystem, "maven"); - assert_eq!(entry.base_purl, PURL); - assert_eq!(entry.artifact.path, jar_rel()); - assert_eq!(entry.wiring.len(), 1); - assert_eq!(entry.wiring[0].kind, REPO_WIRING_KIND); - assert_eq!(entry.wiring[0].action, WiringAction::Added); - assert_eq!( - entry.wiring[0].key.as_deref(), - Some(format!("socket-patch-vendor-{UUID}").as_str()) - ); - } - - #[tokio::test] - #[serial_test::serial] - async fn rerun_is_idempotent_no_rerecord() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - - let (r1, e1, _) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - assert!(e1.is_some()); - let pom_xml1 = tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(); - let jar1 = tokio::fs::read(root.join(jar_rel())).await.unwrap(); - - let (r2, e2, w2) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r2.success); - assert!(e2.is_none(), "in-sync rerun must not re-record the ledger"); - assert_eq!( - tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(), - pom_xml1 - ); - assert_eq!( - tokio::fs::read(root.join(jar_rel())).await.unwrap(), - jar1, - "re-zip is deterministic" - ); - assert!( - w2.iter() - .any(|w| w.code == "vendor_maven_local_cache_shadow"), - "shadow warning fires on the hot path too" - ); - } - - #[tokio::test] - #[serial_test::serial] - async fn wired_missing_artifact_rebuilds_only() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - - let (r1, e1, _) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - assert!(e1.is_some()); - let pom_xml1 = tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(); - let jar1 = tokio::fs::read(root.join(jar_rel())).await.unwrap(); - - // Simulate the fresh-clone hole: the committed artifact is gone. - remove_tree(&root.join(format!(".socket/vendor/maven/{UUID}"))) - .await - .unwrap(); - - let (r2, e2, w2) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r2.success, "{:?}", r2.error); - // A refreshed fingerprint with NO wiring of its own: re-recording - // would clobber the pre-vendor pom.xml (the caller carries it). - let e2 = e2.expect("the rebuild refreshes the ledger fingerprint"); - assert!( - e2.wiring.is_empty(), - "no re-recorded wiring: {:?}", - e2.wiring - ); - assert!( - w2.iter().any(|w| w.code == "vendor_artifact_rebuilt"), - "rebuild is surfaced: {w2:?}" - ); - assert_eq!( - tokio::fs::read(root.join(jar_rel())).await.unwrap(), - jar1, - "rebuilt jar is byte-identical" - ); - assert_eq!( - tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(), - pom_xml1, - "pom.xml untouched by the rebuild" - ); - } - #[tokio::test] #[serial_test::serial] async fn missing_reactor_module_is_refused_without_writes() { @@ -2682,10 +1800,11 @@ mod tests { assert!(!root.join(".socket").exists(), "refusal writes nothing"); } + /// #716: a commented-out `` is not a reactor's; the pom is + /// planned as a single module. #[tokio::test] #[serial_test::serial] async fn commented_modules_do_not_refuse() { - // A commented-out must NOT trigger the aggregator refusal. let commented = "\n\ \x20 4.0.0\n\ \x20 com.example\n\ @@ -2695,13 +1814,17 @@ mod tests { \n"; let (dir, blobs, installed, record) = fixture(Some(commented), true, true).await; let root = dir.path(); - let (result, _e, _w) = + let (result, entry, _w) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); assert!( result.success, "commented must not refuse: {:?}", result.error ); + assert_eq!(entry.unwrap().ecosystem, "jvm"); + let pom = std::fs::read_to_string(root.join(PROJECT_POM)).unwrap(); + assert!(pom.contains("old -->"), "{pom}"); + assert!(pom.contains("1.10.0-socket."), "{pom}"); } #[tokio::test] @@ -2720,32 +1843,6 @@ mod tests { assert!(root.join(".socket/vendor/gradle-index.tsv").is_file()); } - #[tokio::test] - #[serial_test::serial] - async fn refuses_pom_unavailable() { - // pom.xml present, local jar present, but NO upstream pom (and no - // service) → refuse rather than author a minimal pom. - let (dir, blobs, installed, record) = - fixture(Some(project_pom()), true, /*with_local_pom=*/ false).await; - let root = dir.path(); - let (code, detail) = - unwrap_refused(run_vendor(root, &blobs, &installed, &record, false).await); - assert_eq!(code, "vendor_maven_pom_unavailable"); - assert!( - detail.contains("minimal pom"), - "refusal explains why: {detail}" - ); - assert!( - !root.join(format!(".socket/vendor/maven/{UUID}")).exists(), - "a partial artifact must be cleaned up on the pom refusal" - ); - // pom.xml never wired. - let pom_xml = tokio::fs::read_to_string(root.join(PROJECT_POM)) - .await - .unwrap(); - assert!(!pom_xml.contains("socket-patch-vendor")); - } - #[tokio::test] #[serial_test::serial] async fn refuses_unsafe_coordinates() { @@ -2795,22 +1892,19 @@ mod tests { assert!( warnings .iter() - .any(|w| w.code == "vendor_maven_local_cache_shadow"), - "dry run predicts the shadow advisory" + .any(|w| w.detail.starts_with("reason: maven_mirror_of_all: ")), + "dry run predicts the wrapper-less warnings: {warnings:?}" ); } #[tokio::test] #[serial_test::serial] async fn revert_restores_pom_byte_identical() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); let pom_before = tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(); - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; assert_ne!( tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(), pom_before, @@ -2841,13 +1935,10 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn revert_drift_leaves_pom_alone() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; // Third-party drift: the user regenerated pom.xml without our repo. tokio::fs::write(root.join(PROJECT_POM), project_pom()) @@ -2885,13 +1976,10 @@ mod tests { // section (simulated by inserting before ). Reverting // us must excise ONLY our block and keep the sibling's wiring intact — // a whole-file restore would wipe it. - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; // A sibling patch's lands in the section we created. let wired = tokio::fs::read_to_string(root.join(PROJECT_POM)) @@ -2939,13 +2027,10 @@ mod tests { // The user edits the pom AFTER vendoring (adds a block). // Revert must remove our (and the section we created) yet // keep the user's edit — the whole-file restore would have discarded it. - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; let wired = tokio::fs::read_to_string(root.join(PROJECT_POM)) .await @@ -2989,13 +2074,10 @@ mod tests { // The user regenerated the pom, dropping our block but keeping a // hand-written . Our exact block is absent → drift, and // we must NOT touch their section. - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; let regenerated = "\n\ \x20 4.0.0\n\ @@ -3051,47 +2133,6 @@ mod tests { ); } - #[test] - #[serial_test::serial] - fn declares_modules_boundary_and_comment_discipline() { - assert!(declares_modules( - "a" - )); - assert!(declares_modules( - "\n\n\n" - )); - // Prefix decoy: is not . - assert!(!declares_modules( - "x" - )); - // Commented-out modules must not count. - assert!(!declares_modules( - "" - )); - } - - #[test] - #[serial_test::serial] - fn repo_edit_extends_existing_repositories() { - let orig = "\n \n corp\n \n\n"; - let out = build_repo_edit(orig, "socket-patch-vendor-x", ".socket/vendor/maven/x").unwrap(); - // Original corp repo survives, ours added before . - assert!(out.contains("corp")); - assert!(out.contains("socket-patch-vendor-x")); - assert_eq!(out.matches("").count(), 1); - } - - #[test] - #[serial_test::serial] - fn repo_edit_creates_repositories_section() { - let orig = "\n app\n\n"; - let out = build_repo_edit(orig, "socket-patch-vendor-x", ".socket/vendor/maven/x").unwrap(); - assert!(out.contains("")); - assert!(out.contains("")); - assert!(out.contains("socket-patch-vendor-x")); - assert!(out.trim_end().ends_with("")); - } - #[test] #[serial_test::serial] fn group_id_path_and_safety() { @@ -3106,127 +2147,12 @@ mod tests { assert!(!is_safe_group_id("a:b")); } - #[tokio::test] - #[serial_test::serial] - async fn wires_outside_commented_repositories() { - // A commented-out section must not capture the insert: - // a block landing inside the comment is invisible to Maven, so the - // build would silently resolve the UNPATCHED jar while vendor reports - // success. - let commented = "\n\ - \x20 4.0.0\n\ - \x20 com.example\n\ - \x20 app\n\ - \x20 1.0.0\n\ - \x20 \n\ - \n"; - let (dir, blobs, installed, record) = fixture(Some(commented), true, true).await; - let root = dir.path(); - let (result, _e, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success, "{:?}", result.error); - let wired = tokio::fs::read_to_string(root.join(PROJECT_POM)) - .await - .unwrap(); - assert!( - strip_xml_comments(&wired).contains(&format!("socket-patch-vendor-{UUID}")), - "the vendored must be outside comments (Maven-visible): {wired}" - ); - } - - #[tokio::test] - #[serial_test::serial] - async fn wires_project_root_not_profile_repositories() { - // A inside is only consulted when that - // profile is activated; anchoring our block there leaves the default - // build silently resolving the UNPATCHED jar. - let profiled = "\n\ - \x20 4.0.0\n\ - \x20 com.example\n\ - \x20 app\n\ - \x20 1.0.0\n\ - \x20 \n\ - \x20 \n\ - \x20 internal\n\ - \x20 \n\ - \x20 corphttps://corp/repo\n\ - \x20 \n\ - \x20 \n\ - \x20 \n\ - \n"; - let (dir, blobs, installed, record) = fixture(Some(profiled), true, true).await; - let root = dir.path(); - let (result, _e, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success, "{:?}", result.error); - let wired = tokio::fs::read_to_string(root.join(PROJECT_POM)) - .await - .unwrap(); - let id = format!("socket-patch-vendor-{UUID}"); - assert!(wired.contains(&id), "wired: {wired}"); - let p_open = wired.find("").unwrap(); - let p_close = wired.find("").unwrap(); - assert!( - !wired[p_open..p_close].contains(&id), - "the vendored must not land inside : {wired}" - ); - } - - #[test] - #[serial_test::serial] - fn repo_edit_skips_commented_and_profile_anchors() { - // Only a commented → a NEW real section is created. - let commented = "\n\n\n"; - let out = - build_repo_edit(commented, "socket-patch-vendor-x", ".socket/vendor/maven/x").unwrap(); - assert!( - strip_xml_comments(&out).contains("socket-patch-vendor-x"), - "block must be Maven-visible: {out}" - ); - // Only a profile-scoped → likewise anchored at . - let profiled = "\n \n \n \n \ - \n \n \n\n"; - let out = - build_repo_edit(profiled, "socket-patch-vendor-x", ".socket/vendor/maven/x").unwrap(); - let p_close = out.find("").unwrap(); - let id_at = out.find("socket-patch-vendor-x").unwrap(); - assert!(id_at > p_close, "block must land after : {out}"); - } - - #[cfg(unix)] - #[tokio::test] - #[serial_test::serial] - async fn wire_preserves_pom_xml_mode() { - use std::os::unix::fs::PermissionsExt as _; - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - let pom_path = root.join(PROJECT_POM); - tokio::fs::set_permissions(&pom_path, std::fs::Permissions::from_mode(0o600)) - .await - .unwrap(); - - let (result, _e, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success, "{:?}", result.error); - let mode = tokio::fs::metadata(&pom_path) - .await - .unwrap() - .permissions() - .mode() - & 0o7777; - assert_eq!(mode, 0o600, "wiring must not reset the user's pom.xml mode"); - } - #[cfg(unix)] #[tokio::test] #[serial_test::serial] async fn revert_preserves_pom_xml_mode() { use std::os::unix::fs::PermissionsExt as _; - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); let pom_path = root.join(PROJECT_POM); @@ -3235,10 +2161,7 @@ mod tests { } // Byte-identical fast path (whole-file restore). - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; tokio::fs::set_permissions(&pom_path, std::fs::Permissions::from_mode(0o600)) .await .unwrap(); @@ -3251,10 +2174,7 @@ mod tests { ); // Re-vendor, drift with a user edit, revert → the excise path writes too. - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; let wired = tokio::fs::read_to_string(&pom_path).await.unwrap(); let edited = wired.replacen( "", @@ -3274,28 +2194,6 @@ mod tests { ); } - #[tokio::test] - #[serial_test::serial] - async fn wired_rebuild_reports_vendored_jar_path() { - // The wired-but-missing-artifact rebuild leg must report the vendored - // jar path, not the (deleted) temp stage the rebuild ran in. - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - let (r1, _e, _w) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - remove_tree(&root.join(format!(".socket/vendor/maven/{UUID}"))) - .await - .unwrap(); - let (r2, _e2, _w2) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r2.success, "{:?}", r2.error); - assert_eq!( - r2.package_path, - root.join(jar_rel()).display().to_string(), - "rebuild leg must report the vendored jar, not the temp stage" - ); - } - #[cfg(unix)] fn mkfifo(path: &Path) { use std::os::unix::ffi::OsStrExt; @@ -3330,49 +2228,9 @@ mod tests { } } - /// A FIFO planted as the committed vendored pom (the tamper-able tree) - /// must read as out-of-sync — triggering the artifact rebuild that - /// atomically replaces it — instead of wedging the in-sync hot path - /// forever in an `open(2)` waiting for a writer. The jar half of this - /// probe (`read_zip_artifact` + `zip_bytes_match_after_hashes`) is - /// already guarded in common.rs; this pins the `sidecar_matches` half. - #[cfg(unix)] - #[tokio::test] - #[serial_test::serial] - async fn fifo_vendored_pom_fails_fast_and_rebuilds_on_hot_path() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - let (r1, _e, _w) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - let vendored_pom = root.join(format!("{}/commons-text-1.10.0.pom", leaf_rel())); - tokio::fs::remove_file(&vendored_pom).await.unwrap(); - mkfifo(&vendored_pom); - - let outcome = expect_fast( - run_vendor(root, &blobs, &installed, &record, false), - &vendored_pom, - "the in-sync probe must fail fast on a FIFO vendored pom, not wedge", - ) - .await; - let (r2, e2, w2) = unwrap_done(outcome); - assert!(r2.success, "{:?}", r2.error); - assert!( - e2.is_some_and(|e| e.wiring.is_empty()), - "artifact-only rebuild refreshes the fingerprint, never the wiring" - ); - assert!( - w2.iter().any(|w| w.code == "vendor_artifact_rebuilt"), - "FIFO pom must read as stale and trigger the rebuild: {w2:?}" - ); - assert_eq!( - tokio::fs::read(&vendored_pom).await.unwrap(), - UPSTREAM_POM, - "the rebuild must atomically replace the FIFO with the real pom" - ); - } - - /// A FIFO planted as the project `pom.xml` must surface the fail-closed - /// unreadable refusal instead of wedging every vendor run forever. + /// A FIFO planted as the project `pom.xml` reads as no build file (the + /// planner reads regular files only) instead of wedging every vendor + /// run forever. #[cfg(unix)] #[tokio::test] #[serial_test::serial] @@ -3388,38 +2246,9 @@ mod tests { "vendor must fail fast on a FIFO pom.xml, not wedge", ) .await; - let (code, _d) = unwrap_refused(outcome); - assert_eq!(code, "vendor_maven_pom_unreadable"); - } - - /// A FIFO planted as the cached `~/.m2` pom must map to the - /// pom-unavailable refusal fast instead of wedging the pom copy forever. - #[cfg(unix)] - #[tokio::test] - #[serial_test::serial] - async fn fifo_local_pom_fails_fast_in_acquire_upstream_pom() { - let (dir, blobs, installed, record) = - fixture(Some(project_pom()), true, /*with_local_pom=*/ false).await; - let root = dir.path(); - let fifo_pom = installed.join("commons-text-1.10.0.pom"); - mkfifo(&fifo_pom); - - let outcome = expect_fast( - run_vendor(root, &blobs, &installed, &record, false), - &fifo_pom, - "the pom copy must fail fast on a FIFO local pom, not wedge", - ) - .await; let (code, detail) = unwrap_refused(outcome); - assert_eq!(code, "vendor_maven_pom_unavailable"); - assert!( - detail.contains("unreadable local pom"), - "refusal names the unreadable pom: {detail}" - ); - assert!( - !root.join(format!(".socket/vendor/maven/{UUID}")).exists(), - "no partial artifact may survive the refusal" - ); + assert_eq!(code, "vendor_jvm_shape_unsupported"); + assert!(detail.starts_with("reason: no_build_file: "), "{detail}"); } /// Maven Central blocks/rate-limits user agents containing "socket" — @@ -3471,12 +2300,9 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn fifo_project_pom_fails_fast_in_revert() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(result.success); - let entry = entry.unwrap(); + let entry = legacy_vendor(root, &record).await; let pom_path = root.join(PROJECT_POM); tokio::fs::remove_file(&pom_path).await.unwrap(); @@ -3607,287 +2433,31 @@ mod tests { assert!(!root.join(".socket").exists(), "refusal writes nothing"); } - /// No pom.xml AND no gradle marker → the plain missing-pom refusal (the - /// gradle sibling is tested above; this pins the non-gradle arm and - /// `project_has_gradle` returning false through all four probes). + /// No pom.xml and no Gradle, sbt or scala-cli build: the planner's + /// `no_build_file` refusal, with nothing written and no service call + /// planned. #[tokio::test] #[serial_test::serial] - async fn refuses_pom_project_missing_without_gradle_marker() { + async fn refuses_a_root_with_no_build_file() { let (dir, blobs, installed, record) = fixture(None, true, true).await; let root = dir.path(); let (code, detail) = unwrap_refused(run_vendor(root, &blobs, &installed, &record, false).await); - assert_eq!(code, "vendor_maven_pom_project_missing"); - assert!( - detail.contains("pom.xml"), - "refusal names the missing project file: {detail}" - ); + assert_eq!(code, "vendor_jvm_shape_unsupported"); + assert!(detail.starts_with("reason: no_build_file: "), "{detail}"); assert!(!root.join(".socket").exists(), "refusal writes nothing"); + assert!(service_preflight(PURL, root, &record).await.is_none()); } - /// Wired hot path, stale artifact, and the cached ~/.m2 jar is ALSO gone - /// (fresh clone with an empty local cache): the rebuild's refusal bubbles - /// out while pom.xml keeps our (now-dangling) — documenting - /// the wired-but-refused state. + /// Revert fail-closed on a non-canonical uuid in the (tamper-able) + /// state.json entry — refused before any disk access. #[tokio::test] #[serial_test::serial] - async fn wired_missing_jar_bubbles_refusal_keeping_wiring() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; + async fn revert_refuses_non_canonical_uuid() { + let (dir, _, _, record) = fixture(Some(project_pom()), true, true).await; let root = dir.path(); - let (r1, _e, _w) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - let wired = tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(); - - remove_tree(&root.join(format!(".socket/vendor/maven/{UUID}"))) - .await - .unwrap(); - tokio::fs::remove_file(installed.join("commons-text-1.10.0.jar")) - .await - .unwrap(); - - let (code, _d) = unwrap_refused(run_vendor(root, &blobs, &installed, &record, false).await); - assert_eq!(code, "vendor_prebuilt_required"); - assert_eq!( - tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(), - wired, - "the refusal must not touch the wired pom.xml" - ); - assert!( - !root.join(format!(".socket/vendor/maven/{UUID}")).exists(), - "nothing may be recreated on the refusal" - ); - } - - /// Wired hot path, stale artifact, and the rebuild fails non-fatally (the - /// patch blob is gone): the un-successful result is reported with no - /// ledger re-record, pom.xml untouched, and no partial uuid dir. - #[tokio::test] - #[serial_test::serial] - async fn wired_rebuild_failure_reports_unsuccessful_result() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - let (r1, _e, _w) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - let wired = tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(); - - remove_tree(&root.join(format!(".socket/vendor/maven/{UUID}"))) - .await - .unwrap(); - tokio::fs::remove_file(blobs.join(compute_git_sha256_from_bytes(PATCHED))) - .await - .unwrap(); - - let (r2, e2, _w2) = crate::vendor::test_support::expect_failed( - run_vendor(root, &blobs, &installed, &record, false).await, - ); - assert!(!r2.success, "a blob-less rebuild cannot succeed"); - assert!(r2.error.is_some(), "the failure carries a detail"); - assert!(e2.is_none(), "a failed rebuild must not re-record"); - assert_eq!( - tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(), - wired, - "pom.xml untouched by the failed rebuild" - ); - assert!( - !root.join(format!(".socket/vendor/maven/{UUID}")).exists(), - "no partial uuid dir may be left behind" - ); - } - - /// Wired hot path + stale artifact + --dry-run: falls through to the - /// verify-only preview — nothing is rebuilt or written. - #[tokio::test] - #[serial_test::serial] - async fn wired_stale_artifact_dry_run_previews_without_writing() { - let (dir, blobs, installed, record) = fixture(Some(project_pom()), true, true).await; - let root = dir.path(); - let (r1, _e, _w) = unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(r1.success); - remove_tree(&root.join(format!(".socket/vendor/maven/{UUID}"))) - .await - .unwrap(); - let wired = tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(); - - let (r2, e2, w2) = unwrap_done(run_vendor(root, &blobs, &installed, &record, true).await); - assert!(r2.success, "{:?}", r2.error); - assert!(e2.is_none(), "dry run records nothing"); - assert!( - !root.join(format!(".socket/vendor/maven/{UUID}")).exists(), - "dry run must not rebuild the artifact" - ); - assert_eq!( - tokio::fs::read(root.join(PROJECT_POM)).await.unwrap(), - wired, - "dry run must not touch pom.xml" - ); - assert!( - !w2.iter().any(|w| w.code == "vendor_artifact_rebuilt"), - "the preview must not claim a rebuild happened: {w2:?}" - ); - } - - /// build_repo_edit failure AFTER the artifact was materialised (a pom.xml - /// with no ): success flips false, the detail is carried, and - /// the uuid dir is removed so no orphan artifact survives. - #[tokio::test] - #[serial_test::serial] - async fn unwireable_pom_fails_after_materialise_and_cleans_up() { - let broken = "\n app\n"; - let (dir, blobs, installed, record) = fixture(Some(broken), true, true).await; - let root = dir.path(); - - let (result, entry, _w) = - unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await); - assert!(!result.success, "an unwireable pom cannot succeed"); - assert!( - result - .error - .as_deref() - .is_some_and(|e| e.contains("no ")), - "failure names the missing anchor: {:?}", - result.error - ); - assert!(entry.is_none()); - assert!( - !root.join(format!(".socket/vendor/maven/{UUID}")).exists(), - "the materialised artifact must be cleaned up when wiring fails" - ); - assert_eq!( - tokio::fs::read_to_string(root.join(PROJECT_POM)) - .await - .unwrap(), - broken, - "the unwireable pom.xml is left as-is" - ); - } - - /// Unit legs of the wiring-failure class: no at all, and an - /// unterminated comment masking BOTH anchors to EOF (fail-closed). - #[test] - #[serial_test::serial] - fn repo_edit_errors_without_unmasked_project_close() { - let err = build_repo_edit( - "x", - "socket-patch-vendor-x", - ".socket/vendor/maven/x", - ) - .unwrap_err(); - assert!(err.contains("no "), "{err}"); - - // The unterminated comment swallows AND . - let err = build_repo_edit( - "` / `` + markers. +- `.mvn/maven.config`: two lines that let Maven 3.9.2+ read the committed tree. +- `.socket/vendor/maven2///-socket./`: the + patched jar, its pom and checksums. This replaces `.socket/vendor/maven//`. + +A project vendored before v5 still has the old wiring: a +`socket-patch-vendor-` `` in `pom.xml` and a +`maven_pom_repository` entry in `.socket/vendor/state.json`. v5 does not +migrate it. `vendor`, `scan --mode vendored` and `get --mode vendored` refuse +that project with `vendor_jvm_shape_unsupported` (reason `legacy_maven_root`) +and change nothing. Migrate it once: + +```sh +socket-patch vendor --revert # restores pom.xml byte for byte +socket-patch vendor # or: socket-patch scan --mode vendored +``` + +`remove`, `rollback` and switching to hosted mode still unwind the old wiring. + +Without a Maven Wrapper (`.mvn/wrapper/maven-wrapper.properties`) the CLI can't +tell which Maven builds the project, so `vendor` reports two +`vendor_jvm_degraded` warnings, `maven_f_outside_root` and +`maven_mirror_of_all`. The patch is still applied. To clear them, add a Maven +Wrapper pinned to Maven 3.9.9 or later. On older Maven, make sure no +`mirrorOf *` mirror captures the `socket-patch-vendor` repository. + +These codes are no longer emitted: `vendor_maven_local_cache_shadow` (a cached +original version can't shadow the suffixed pin), +`vendor_maven_multimodule_unsupported`, `vendor_maven_pom_project_missing` and +`vendor_gradle_unsupported` (a root with no JVM build is now +`vendor_jvm_shape_unsupported` with reason `no_build_file`), +`vendor_maven_pom_unreadable`, `vendor_maven_pom_unavailable` and +`vendor_maven_pom_downloaded`. `legacy_maven_root` is now a refusal for the +whole root, not a `vendor_jvm_degraded` warning on mixed Maven + Gradle roots. + ## Retired spellings | Removed | Replacement | diff --git a/docs/usage.md b/docs/usage.md index 338b63f17..10355ba31 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -110,12 +110,14 @@ Repair cannot reconstruct a lost vendor ledger. Restore it from version control. The N-API crate and in-memory patch engine remain available for hosted GitHub App workflows. Removing local vendoring builders does not remove those APIs. -### Maven reactors and Gradle - -Maven reactors use suffixed versions in `.socket/vendor/maven2`; Gradle 6.8+ -keeps its coordinates and lockfiles, with settings wiring and a configuration-time -SHA-256 check. Existing Gradle verification files are updated. Single-POM Maven -projects retain their existing vendoring behavior. +### Maven and Gradle + +Maven projects, single-module or reactor, use suffixed versions in +`.socket/vendor/maven2`; Gradle 6.8+ keeps its coordinates and lockfiles, with +settings wiring and a configuration-time SHA-256 check. Existing Gradle +verification files are updated. A project vendored before v5 through the +single-POM `` wiring must run `socket-patch vendor --revert` before +vendoring again. ```sh socket-patch vendor --check # read-only offline artifact and wiring audit From fa0383c725929020c83b6932ccd18e1b14057e51 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 11:36:40 -0400 Subject: [PATCH 4/6] Document ledger-only VEX for v5 Maven vendoring (#973) The VEX discovery table still described the pre-v5 wiring as the vendored Maven reference. Mark it pre-v5 only and say a v5 suffixed pin is gated by its ledger entry. The migration guide now says to commit .socket/vendor/state.json, since a single-module project vendored by v5 is attested only from it. Rename the e2e test whose name still claimed it re-attests without a ledger. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-cli/CLI_CONTRACT.md | 2 +- crates/socket-patch-cli/tests/e2e_vex_lockfile/maven.rs | 2 +- docs/migrating-to-v5.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 3f688cca1..0495e2351 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -370,7 +370,7 @@ Discovery is read-only, never touches the network, and never fails the run: a ma | pypi | `uv.lock` (confirmed by `pyproject.toml` `[tool.uv.sources]` when present), PEP 723 `