diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs
index 18b749a51..da87561fe 100644
--- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs
+++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs
@@ -1156,7 +1156,8 @@ fn gradle_vendor_533_unpatched_classifier_copy_withholds_vex() {
/// #513 / #459: a reactor `${prop}` version that can't be pinned with
/// certainty (undefined in the checkout, or raised by a profile) is left
/// as is with a warning, and `vex` does not attest the package the build
-/// still takes unpatched.
+/// still takes unpatched. With nothing else to wire, `vendor` refuses
+/// rather than counting the patch as applied (#513).
#[test]
fn maven_reactor_uncertain_property_version_withholds_vex() {
let cases = [
@@ -1199,6 +1200,26 @@ fn maven_reactor_uncertain_property_version_withholds_vex() {
let b = pristine.replace("a", "b");
write(root, "proj/b/pom.xml", b.as_bytes());
}
+ let vex = root.join("vex.json");
+ if !sibling {
+ assert_refused(root, "nothing_wired", reason);
+ let (_, env) = socket(
+ root,
+ &[
+ "vex",
+ "-O",
+ vex.to_str().unwrap(),
+ "--product",
+ "pkg:generic/x@1",
+ ],
+ );
+ assert!(!env.to_string().contains("not_affected"), "{reason}: {env}");
+ if let Ok(bytes) = std::fs::read(&vex) {
+ let doc = String::from_utf8_lossy(&bytes);
+ assert!(!doc.contains("not_affected"), "{reason}: {env}");
+ }
+ continue;
+ }
let env = ok(root, &["vendor"]);
assert!(
env.to_string().contains(&format!("reason: {reason}")),
@@ -1208,12 +1229,7 @@ fn maven_reactor_uncertain_property_version_withholds_vex() {
std::fs::read_to_string(root.join("proj/a/pom.xml")).unwrap(),
a
);
- assert_eq!(
- wiring_text(root).contains("-socket."),
- sibling,
- "{reason}: wired"
- );
- let vex = root.join("vex.json");
+ assert!(wiring_text(root).contains("-socket."), "{reason}: wired");
let (_, env) = socket(
root,
&[
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 38a1fa623..a6f9ffd6d 100644
--- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
+++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
@@ -478,6 +478,65 @@ pub fn wired_checked(
})
}
+/// Refuse `plan` when, written over `read`, no pom of the reactor would
+/// name `c`'s suffixed version: every declaration was left as is and no
+/// root pinned (#513). Vendoring it would count the patch as applied while
+/// the build keeps resolving the base; the refusal carries the degraded
+/// reasons that left it unwired.
+pub fn refuse_unwired(
+ read: ReadFn<'_>,
+ list: ListFn<'_>,
+ c: &Coords<'_>,
+ plan: &JvmPlan,
+) -> Result<(), JvmRefusal> {
+ let planned = |rel: &str| match plan.writes.iter().find(|w| w.rel == rel) {
+ Some(w) => Some(w.bytes.clone()),
+ None => read(rel),
+ };
+ if wired_checked(&planned, list, c)? {
+ return Ok(());
+ }
+ // Only the reasons that leave a declaration or a root alone; the
+ // repository and config notes (and a mixed root's Gradle warnings)
+ // say nothing about why the reactor is unwired.
+ const UNWIRING: [&str; 7] = [
+ "property_unresolved",
+ "conflicting_literal_version",
+ "conflicting_managed_version",
+ "management_unresolved",
+ "range",
+ "coordinates_overridden",
+ "classifier_declared",
+ ];
+ let why: Vec<&str> = plan
+ .warnings
+ .iter()
+ .filter(|w| w.code == DEGRADED)
+ .map(|w| w.detail.as_str())
+ .filter(|d| {
+ UNWIRING
+ .iter()
+ .any(|r| d.starts_with(&format!("reason: {r}: ")))
+ })
+ .collect();
+ let why = if why.is_empty() {
+ "no declaration or pin of the reactor can take it".to_string()
+ } else {
+ why.join("; ")
+ };
+ Err(JvmRefusal {
+ code: SHAPE_UNSUPPORTED,
+ detail: format!(
+ "reason: nothing_wired: no pom of the reactor would build {}:{}:{}, so Maven \
+ keeps resolving {} ({why})",
+ c.group_id,
+ c.artifact_id,
+ c.suffixed_version(),
+ c.version
+ ),
+ })
+}
+
/// [`wired_checked`], and no declaration of `c`'s g:a is left at a version
/// that may still read the base: a `${...}` the checkout does not define
/// or that an inheriting pom or a profile moves (#513, #459). Such a
@@ -5376,6 +5435,51 @@ mod tests {
assert!(unwired(&after));
}
+ /// #513: a plan that leaves every declaration and root alone wires
+ /// nothing, so `vendor` refuses it with the reasons instead of
+ /// counting the patch as applied.
+ #[test]
+ fn a_plan_that_wires_nothing_is_refused() {
+ let parent = "com.corpcorp-parent\
+ 3";
+ let mut files = external_reactor(parent, "");
+ let a = text(&files, "a/pom.xml").replace(
+ "",
+ "${ct.version}",
+ );
+ files.insert("a/pom.xml".into(), a.into_bytes());
+ let plan = run(&files).unwrap();
+ let read = |p: &str| files.get(p).cloned();
+ let r = refuse_unwired(&read, &lister(&files), &patch().coords(), &plan).unwrap_err();
+ assert_eq!(r.code, SHAPE_UNSUPPORTED);
+ assert_eq!(refusal_reason(&r), "nothing_wired");
+ assert!(
+ r.detail.contains("reason: property_unresolved: "),
+ "{}",
+ r.detail
+ );
+ assert!(!r.detail.contains("maven_f_outside_root"), "{}", r.detail);
+ assert!(!r.detail.contains("maven_mirror_of_all"), "{}", r.detail);
+ }
+
+ /// The same check passes a plan that pins the root or rewrites a
+ /// declaration, and an already vendored reactor whose re-plan writes
+ /// nothing.
+ #[test]
+ fn a_wired_plan_is_not_refused() {
+ let files = fs(&[
+ ("pom.xml", ROOT),
+ ("a/pom.xml", &module("a", &dep("1.10.0"))),
+ ("b/pom.xml", &module("b", "")),
+ ]);
+ let (plan, after) = vendor(&files);
+ let read = |p: &str| files.get(p).cloned();
+ refuse_unwired(&read, &lister(&files), &patch().coords(), &plan).unwrap();
+ let again = run(&after).unwrap();
+ let read = |p: &str| after.get(p).cloned();
+ refuse_unwired(&read, &lister(&after), &patch().coords(), &again).unwrap();
+ }
+
/// #513 case 2: Maven 4.1.0 infers an empty `` from
/// `../pom.xml`, so the root's property resolves and is rewritten.
#[test]
diff --git a/crates/socket-patch-core/src/vendor/maven_repo.rs b/crates/socket-patch-core/src/vendor/maven_repo.rs
index 7bb6d3567..b11e8e432 100644
--- a/crates/socket-patch-core/src/vendor/maven_repo.rs
+++ b/crates/socket-patch-core/src/vendor/maven_repo.rs
@@ -779,10 +779,20 @@ async fn vendor_maven_jvm(
);
}
// A declared classifier is sourced only for a real patch.
- if let Err(e) = plan {
- if !e.detail.starts_with("reason: classifier_unavailable:") {
+ match plan {
+ Err(e) if !e.detail.starts_with("reason: classifier_unavailable:") => {
return refused(e.code, e.detail);
}
+ Ok(plan) if matches!(shape, Shape::MavenReactor | Shape::Mixed) => {
+ let read = |rel: &str| reader.read(rel);
+ let list = |dir: &str| reader.list(dir);
+ if let Err(e) =
+ super::jvm::maven_reactor::refuse_unwired(&read, &list, &patch.coords(), &plan)
+ {
+ return refused(e.code, e.detail);
+ }
+ }
+ _ => {}
}
return done(
synthesized_result(purl, &display_path, Vec::new(), true, None),
@@ -976,6 +986,13 @@ async fn vendor_maven_jvm(
Ok(plan) => plan,
Err(refusal) => return refused(refusal.code, refusal.detail),
};
+ if matches!(shape, Shape::MavenReactor | Shape::Mixed) {
+ if let Err(refusal) =
+ super::jvm::maven_reactor::refuse_unwired(&read, &list, &patch.coords(), &plan)
+ {
+ return refused(refusal.code, refusal.detail);
+ }
+ }
if gradle
&& reader
.read(super::jvm::gradle::VERIFICATION_REL)