@@ -42,6 +42,7 @@ import org.apache.ivy.core.module.id.ModuleId
4242import org.apache.ivy.core.module.id.ModuleRevisionId
4343import org.apache.ivy.core.report.ArtifactDownloadReport
4444import org.apache.ivy.core.report.ResolveReport
45+ import org.apache.ivy.core.resolve.IvyNode
4546import org.apache.ivy.core.resolve.ResolveOptions
4647import org.apache.ivy.core.settings.IvySettings
4748import org.apache.ivy.plugins.matcher.ExactPatternMatcher
@@ -102,7 +103,8 @@ class GrapeIvy implements GrapeEngine {
102103 Message . setDefaultLogger(new PlatformLoggingMessageLogger ())
103104
104105 settings = new IvySettings ()
105- settings. setVariable(' user.home.url' , new File (System . getProperty(' user.home' )). toURI(). toURL() as String )
106+ def url = new File (System . getProperty(' user.home' )). toURI(). toURL() as String
107+ settings. setVariable(' user.home.url' , url. endsWith(" /" ) ? url[0 .. -2 ] : url)
106108 File grapeConfig = getLocalGrapeConfig()
107109 if (grapeConfig. exists()) {
108110 try {
@@ -451,7 +453,7 @@ class GrapeIvy implements GrapeEngine {
451453 }
452454
453455 if (report. hasError()) {
454- throw new RuntimeException (" Error grabbing Grapes -- ${ report.getAllProblemMessages()} " )
456+ throw new RuntimeException (" Error grabbing Grapes -- ${ report.getAllProblemMessages()}${ diagnoseHalfPopulatedLocalM2(report) } " )
455457 }
456458 if (report. getDownloadSize() && reportDownloads) {
457459 System . err. println (" Downloaded ${ report.getDownloadSize() >> 10} Kbytes in ${ report.getDownloadTime()} ms:\n ${ report.getAllArtifactsReports()*.toString().join('\n ')} " )
@@ -466,6 +468,66 @@ class GrapeIvy implements GrapeEngine {
466468 report
467469 }
468470
471+ /**
472+ * When a download fails, check whether the local Maven cache has the POM but
473+ * not the primary artifact for any failed dependency. Ivy binds artifact
474+ * downloads to the resolver that resolved the descriptor, so a localm2 with
475+ * only the POM blocks the chain from falling through to Maven Central.
476+ */
477+ private static String diagnoseHalfPopulatedLocalM2 (ResolveReport report ) {
478+ File m2root = new File (System . getProperty(' user.home' ), ' .m2/repository' )
479+ if (! m2root. isDirectory()) return ' '
480+ StringBuilder hint = new StringBuilder ()
481+ Set<String > seen = new LinkedHashSet<String > ()
482+ // Artifact-level failures ("download failed: g#a;v!a.jar") — primary half-population.
483+ for (ArtifactDownloadReport adr : report. getFailedArtifactsReports()) {
484+ String classifier = (String ) adr. getArtifact(). getExtraAttribute(' classifier' )
485+ appendHintForCoord(adr. getArtifact(). getModuleRevisionId(),
486+ adr. getArtifact(). getName(),
487+ adr. getArtifact(). getExt() ?: ' jar' ,
488+ classifier, m2root, hint, seen)
489+ }
490+ // Dependency-level failures ("unresolved dependency: g#a;v not found") — typical when
491+ // the JAR-only-in-localm2 case combines with a transient Central descriptor lookup miss.
492+ for (IvyNode node : report. getUnresolvedDependencies()) {
493+ appendHintForCoord(node. getId(), node. getId(). getName(), ' jar' , null , m2root, hint, seen)
494+ }
495+ return hint. toString()
496+ }
497+
498+ private static void appendHintForCoord (ModuleRevisionId mrid , String artName , String ext ,
499+ String classifier , File m2root , StringBuilder hint ,
500+ Set<String > seen ) {
501+ String org = mrid. getOrganisation()
502+ String mod = mrid. getName()
503+ String rev = mrid. getRevision()
504+ String coord = " ${ org} :${ mod} :${ rev} " . toString()
505+ if (! seen. add(coord)) return
506+ File dir = new File (m2root, " ${ org.replace('.', '/')} /${ mod} /${ rev} " )
507+ if (! dir. isDirectory()) return
508+ File pom = new File (dir, " ${ mod} -${ rev} .pom" )
509+ String suffix = classifier ? " -${ classifier} " : ' '
510+ File primary = new File (dir, " ${ artName} -${ rev}${ suffix} .${ ext} " )
511+ if (pom. exists() && ! primary. exists()) {
512+ File grapeIvyXml = new File (System . getProperty(' user.home' ),
513+ " .groovy/grapes/${ org} /${ mod} /ivy-${ rev} .xml" )
514+ hint. append(' \n Hint: ' ). append(coord)
515+ .append(' has a POM but no ' ). append(ext). append(' in your local Maven cache.\n ' )
516+ .append(' Either run: mvn dependency:get -Dartifact=' ). append(coord). append(' \n ' )
517+ .append(' or remove these so Grape can fetch from Maven Central:\n ' )
518+ .append(' ' ). append(dir). append(' \n ' )
519+ .append(' ' ). append(grapeIvyXml). append(' (and ivy-' )
520+ .append(rev). append(' .xml.original, ivydata-' ). append(rev). append(' .properties)' )
521+ } else if (primary. exists() && ! pom. exists()) {
522+ hint. append(' \n Hint: ' ). append(coord)
523+ .append(' has a ' ). append(ext). append(' but no POM in your local Maven cache.\n ' )
524+ .append(' Ivy needs the POM to resolve the descriptor; without it the chain falls through ' )
525+ .append(' to Maven Central and any transient Central failure surfaces as "not found".\n ' )
526+ .append(' Either run: mvn dependency:get -Dartifact=' ). append(coord). append(' \n ' )
527+ .append(' or remove ' ). append(dir). append(' to force a clean fetch.' )
528+ }
529+ }
530+
469531 private addIvyListener () {
470532 ivyInstance. eventManager. addIvyListener { ivyEvent ->
471533 switch (ivyEvent) {
0 commit comments