Skip to content

Commit 1559a9d

Browse files
committed
turn on ivy debugging temporarily
1 parent 160c9d5 commit 1559a9d

3 files changed

Lines changed: 361 additions & 1 deletion

File tree

build-logic/src/main/groovy/org.apache.groovy-tested.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ tasks.withType(Test).configureEach {
6767
// Harmless when TestLens isn't on the classpath (no entries match).
6868
systemProperty 'groovy.junit6.forked.excludeClasspath', 'junit-platform-instrumentation'
6969
// systemProperty 'groovy.grape.report.downloads', 'true'
70-
// systemProperty 'ivy.message.logger.level', '4'
70+
systemProperty 'ivy.message.logger.level', '4'
7171

7272
jvmArgumentProviders.add(new TestCommandLineArgumentProvider(
7373
grapeRoot: grapeDirectory,
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package groovy.grape.ivy
20+
21+
import groovy.transform.CompileStatic
22+
import org.apache.ivy.core.module.descriptor.DependencyDescriptor
23+
import org.apache.ivy.core.module.id.ModuleRevisionId
24+
import org.apache.ivy.core.resolve.ResolveData
25+
import org.apache.ivy.plugins.resolver.IBiblioResolver
26+
import org.apache.ivy.plugins.resolver.util.ResolvedResource
27+
28+
import java.util.regex.Pattern
29+
30+
/**
31+
* IBiblioResolver subclass that, for a local Maven repository (file://), validates
32+
* that the primary artifact actually exists alongside the POM before reporting
33+
* the descriptor as found. Without this, a half-populated local m2 entry (POM
34+
* present, JAR missing — common after Apache release-vote staging workflows or
35+
* partial Maven downloads) causes Ivy's chain to bind resolution to localm2 and
36+
* then fail to download the missing JAR, never falling through to Maven Central.
37+
*
38+
* Override is on findIvyFileRef rather than getDependency to avoid grape-cache
39+
* poisoning: returning null at descriptor-lookup time prevents Ivy from caching
40+
* the descriptor with resolver=localm2.
41+
*
42+
* Strictness is gated by -Dgroovy.grape.strict-localm2=true (default false for
43+
* this initial release; planned to flip to default-true in 6.1, drop the flag
44+
* in 7.0). Strictness is also automatically skipped for snapshot revisions
45+
* (Maven uses timestamp-suffixed filenames there), for non-m2-compatible
46+
* configurations, and when the resolver root is not a file URL.
47+
*/
48+
@CompileStatic
49+
class StrictLocalM2Resolver extends IBiblioResolver {
50+
51+
private static final String ENABLE_PROPERTY = 'groovy.grape.strict-localm2'
52+
53+
/** Maven packaging values whose primary artifact has a non-jar extension. */
54+
private static final Map<String, String> NON_JAR_PACKAGING_EXT = [
55+
'war': 'war',
56+
'ear': 'ear',
57+
'aar': 'aar',
58+
'rar': 'rar',
59+
'zip': 'zip',
60+
].asImmutable()
61+
62+
private static final Pattern PACKAGING_PATTERN =
63+
~/(?ms)<packaging>\s*([\w-]+)\s*<\/packaging>/
64+
65+
@Override
66+
ResolvedResource findIvyFileRef(DependencyDescriptor dd, ResolveData data) {
67+
ResolvedResource pom = super.findIvyFileRef(dd, data)
68+
if (pom == null) return null
69+
ModuleRevisionId mrid = dd.getDependencyRevisionId()
70+
File pomFile = resolvedPomAsFile(pom)
71+
if (shouldRejectAsHalfPopulated(mrid, pomFile)) {
72+
return null
73+
}
74+
return pom
75+
}
76+
77+
/**
78+
* Visible for testing: decide whether to reject this localm2 lookup as
79+
* half-populated. Returns true iff strictness is enabled, the revision is
80+
* not a snapshot, the resolver root is a file URL, and no primary artifact
81+
* matching the POM's packaging exists alongside the POM.
82+
*/
83+
boolean shouldRejectAsHalfPopulated(ModuleRevisionId mrid, File pomFile) {
84+
if (!shouldEnforce()) return false
85+
if (mrid == null) return false
86+
String rev = mrid.revision
87+
if (rev != null && rev.endsWith('-SNAPSHOT')) return false
88+
File m2dir = computeM2Dir(mrid)
89+
if (m2dir == null) return false
90+
File jar = new File(m2dir, "${mrid.name}-${rev}.jar")
91+
if (jar.exists()) return false
92+
String packaging = (pomFile != null && pomFile.isFile()) ? readPackaging(pomFile) : null
93+
if (packaging == 'pom') return false
94+
String ext = NON_JAR_PACKAGING_EXT.getOrDefault(packaging, 'jar')
95+
File primary = new File(m2dir, "${mrid.name}-${rev}.${ext}")
96+
return !primary.exists()
97+
}
98+
99+
private boolean shouldEnforce() {
100+
if (!isM2compatible()) return false
101+
Boolean.parseBoolean(System.getProperty(ENABLE_PROPERTY, 'false'))
102+
}
103+
104+
/**
105+
* Visible for testing: compute the local m2 directory for a module
106+
* revision based on the resolver's root.
107+
*/
108+
File computeM2Dir(ModuleRevisionId mrid) {
109+
if (mrid == null) return null
110+
String rootStr = getRoot()
111+
if (rootStr == null || !rootStr.startsWith('file:')) return null
112+
// The rendered ${user.home.url} can produce double-slashes; normalize them.
113+
String path = rootStr.substring('file:'.length()).replaceAll(/\/+/, '/')
114+
File rootDir = new File(path)
115+
if (!rootDir.isDirectory()) return null
116+
new File(rootDir, "${mrid.organisation.replace('.', '/')}/${mrid.name}/${mrid.revision}")
117+
}
118+
119+
/**
120+
* Visible for testing: read the {@code <packaging>} element from a POM file.
121+
* Returns null if the element is absent or the file cannot be read.
122+
*/
123+
static String readPackaging(File pomFile) {
124+
try {
125+
String text = pomFile.getText('UTF-8')
126+
def matcher = PACKAGING_PATTERN.matcher(text)
127+
return matcher.find() ? matcher.group(1) : null
128+
} catch (Exception ignored) {
129+
return null
130+
}
131+
}
132+
133+
private static File resolvedPomAsFile(ResolvedResource pom) {
134+
try {
135+
String name = pom.resource.name
136+
if (name?.startsWith('file:')) {
137+
String path = name.substring('file:'.length()).replaceAll(/\/+/, '/')
138+
return new File(path)
139+
}
140+
} catch (Exception ignored) {
141+
// fall through
142+
}
143+
null
144+
}
145+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package groovy.grape.ivy
20+
21+
import org.apache.ivy.core.module.id.ModuleRevisionId
22+
import org.junit.jupiter.api.AfterEach
23+
import org.junit.jupiter.api.BeforeEach
24+
import org.junit.jupiter.api.Test
25+
import org.junit.jupiter.api.io.TempDir
26+
27+
import static org.junit.jupiter.api.Assertions.assertEquals
28+
import static org.junit.jupiter.api.Assertions.assertFalse
29+
import static org.junit.jupiter.api.Assertions.assertNull
30+
import static org.junit.jupiter.api.Assertions.assertTrue
31+
32+
final class StrictLocalM2ResolverTest {
33+
34+
private static final String ENABLE_PROPERTY = 'groovy.grape.strict-localm2'
35+
36+
@TempDir
37+
File m2
38+
39+
StrictLocalM2Resolver resolver
40+
41+
@BeforeEach
42+
void setUp() {
43+
resolver = new StrictLocalM2Resolver()
44+
resolver.setRoot(m2.toURI().toURL().toString())
45+
resolver.setM2compatible(true)
46+
System.setProperty(ENABLE_PROPERTY, 'true')
47+
}
48+
49+
@AfterEach
50+
void tearDown() {
51+
System.clearProperty(ENABLE_PROPERTY)
52+
}
53+
54+
@Test
55+
void rejects_pomOnly_packagingJar_default() {
56+
File dir = layout('com.example', 'foo', '1.0')
57+
writePom(dir, 'foo', '1.0', null)
58+
// no JAR
59+
assertTrue resolver.shouldRejectAsHalfPopulated(
60+
ModuleRevisionId.newInstance('com.example', 'foo', '1.0'),
61+
new File(dir, 'foo-1.0.pom'))
62+
}
63+
64+
@Test
65+
void rejects_pomOnly_packagingJar_explicit() {
66+
File dir = layout('com.example', 'foo', '1.0')
67+
writePom(dir, 'foo', '1.0', 'jar')
68+
assertTrue resolver.shouldRejectAsHalfPopulated(
69+
ModuleRevisionId.newInstance('com.example', 'foo', '1.0'),
70+
new File(dir, 'foo-1.0.pom'))
71+
}
72+
73+
@Test
74+
void accepts_pomOnly_packagingPom() {
75+
File dir = layout('com.example', 'parent', '1.0')
76+
writePom(dir, 'parent', '1.0', 'pom')
77+
assertFalse resolver.shouldRejectAsHalfPopulated(
78+
ModuleRevisionId.newInstance('com.example', 'parent', '1.0'),
79+
new File(dir, 'parent-1.0.pom'))
80+
}
81+
82+
@Test
83+
void accepts_pomAndJar_present() {
84+
File dir = layout('com.example', 'foo', '1.0')
85+
writePom(dir, 'foo', '1.0', 'jar')
86+
new File(dir, 'foo-1.0.jar') << 'fake-jar-bytes'
87+
assertFalse resolver.shouldRejectAsHalfPopulated(
88+
ModuleRevisionId.newInstance('com.example', 'foo', '1.0'),
89+
new File(dir, 'foo-1.0.pom'))
90+
}
91+
92+
@Test
93+
void rejects_pomOnly_packagingWar_warAbsent() {
94+
File dir = layout('com.example', 'webapp', '1.0')
95+
writePom(dir, 'webapp', '1.0', 'war')
96+
// no .war file
97+
assertTrue resolver.shouldRejectAsHalfPopulated(
98+
ModuleRevisionId.newInstance('com.example', 'webapp', '1.0'),
99+
new File(dir, 'webapp-1.0.pom'))
100+
}
101+
102+
@Test
103+
void accepts_pomOnly_packagingWar_warPresent() {
104+
File dir = layout('com.example', 'webapp', '1.0')
105+
writePom(dir, 'webapp', '1.0', 'war')
106+
new File(dir, 'webapp-1.0.war') << 'fake-war-bytes'
107+
assertFalse resolver.shouldRejectAsHalfPopulated(
108+
ModuleRevisionId.newInstance('com.example', 'webapp', '1.0'),
109+
new File(dir, 'webapp-1.0.pom'))
110+
}
111+
112+
@Test
113+
void accepts_packagingBundle_jarPresent() {
114+
// OSGi bundle packaging produces a .jar file in Maven layout.
115+
File dir = layout('com.example', 'osgi-thing', '1.0')
116+
writePom(dir, 'osgi-thing', '1.0', 'bundle')
117+
new File(dir, 'osgi-thing-1.0.jar') << 'fake-jar-bytes'
118+
assertFalse resolver.shouldRejectAsHalfPopulated(
119+
ModuleRevisionId.newInstance('com.example', 'osgi-thing', '1.0'),
120+
new File(dir, 'osgi-thing-1.0.pom'))
121+
}
122+
123+
@Test
124+
void accepts_snapshotRevision_unconditionally() {
125+
// Snapshot filenames are timestamp-based; literal name check would
126+
// false-fail. Skip strictness for snapshots.
127+
File dir = layout('com.example', 'snap', '1.0-SNAPSHOT')
128+
writePom(dir, 'snap', '1.0-SNAPSHOT', 'jar')
129+
// no JAR alongside the POM (yet still accept)
130+
assertFalse resolver.shouldRejectAsHalfPopulated(
131+
ModuleRevisionId.newInstance('com.example', 'snap', '1.0-SNAPSHOT'),
132+
new File(dir, 'snap-1.0-SNAPSHOT.pom'))
133+
}
134+
135+
@Test
136+
void accepts_when_strictness_disabled_via_system_property() {
137+
System.setProperty(ENABLE_PROPERTY, 'false')
138+
File dir = layout('com.example', 'foo', '1.0')
139+
writePom(dir, 'foo', '1.0', 'jar')
140+
// No JAR; would normally reject.
141+
assertFalse resolver.shouldRejectAsHalfPopulated(
142+
ModuleRevisionId.newInstance('com.example', 'foo', '1.0'),
143+
new File(dir, 'foo-1.0.pom'))
144+
}
145+
146+
@Test
147+
void accepts_when_not_m2compatible() {
148+
resolver.setM2compatible(false)
149+
File dir = layout('com.example', 'foo', '1.0')
150+
writePom(dir, 'foo', '1.0', 'jar')
151+
assertFalse resolver.shouldRejectAsHalfPopulated(
152+
ModuleRevisionId.newInstance('com.example', 'foo', '1.0'),
153+
new File(dir, 'foo-1.0.pom'))
154+
}
155+
156+
@Test
157+
void readPackaging_findsLiteralValue() {
158+
File pom = File.createTempFile('pom-', '.xml')
159+
pom.deleteOnExit()
160+
pom.text = '<project><packaging>war</packaging></project>'
161+
assertEquals 'war', StrictLocalM2Resolver.readPackaging(pom)
162+
}
163+
164+
@Test
165+
void readPackaging_returnsNullWhenAbsent() {
166+
File pom = File.createTempFile('pom-', '.xml')
167+
pom.deleteOnExit()
168+
pom.text = '<project><groupId>x</groupId></project>'
169+
assertNull StrictLocalM2Resolver.readPackaging(pom)
170+
}
171+
172+
@Test
173+
void readPackaging_returnsNullForMalformedFile() {
174+
File pom = File.createTempFile('pom-', '.xml')
175+
pom.deleteOnExit()
176+
pom.text = 'not a valid xml file'
177+
assertNull StrictLocalM2Resolver.readPackaging(pom)
178+
}
179+
180+
@Test
181+
void computeM2Dir_returnsExpectedPath() {
182+
File dir = resolver.computeM2Dir(
183+
ModuleRevisionId.newInstance('org.apache.commons', 'commons-lang3', '3.9'))
184+
assertEquals new File(m2, 'org/apache/commons/commons-lang3/3.9'), dir
185+
}
186+
187+
@Test
188+
void computeM2Dir_handlesDoubleSlashRoot() {
189+
// Mimic the rendered ${user.home.url}/.m2/repository/ which can yield
190+
// a double-slash mid-path (e.g. file:/Users/x//.m2/repository/).
191+
resolver.setRoot('file:' + m2.absolutePath.replaceFirst('/', '//'))
192+
File dir = resolver.computeM2Dir(
193+
ModuleRevisionId.newInstance('com.example', 'foo', '1.0'))
194+
assertEquals new File(m2, 'com/example/foo/1.0'), dir
195+
}
196+
197+
private File layout(String org, String mod, String rev) {
198+
File dir = new File(m2, "${org.replace('.', '/')}/${mod}/${rev}")
199+
dir.mkdirs()
200+
dir
201+
}
202+
203+
private static void writePom(File dir, String mod, String rev, String packaging) {
204+
StringBuilder pom = new StringBuilder()
205+
pom << '<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
206+
pom << " <groupId>com.example</groupId>\n"
207+
pom << " <artifactId>${mod}</artifactId>\n"
208+
pom << " <version>${rev}</version>\n"
209+
if (packaging != null) {
210+
pom << " <packaging>${packaging}</packaging>\n"
211+
}
212+
pom << '</project>\n'
213+
new File(dir, "${mod}-${rev}.pom").text = pom.toString()
214+
}
215+
}

0 commit comments

Comments
 (0)