Skip to content

Commit 81d789f

Browse files
Fixes for regression after pr #379
Issues: 411 #417 - Improved dependency resolution by adding checks for dynamic versions and system-scoped dependencies. - Added comprehensive assertions in BuildInfoTest for validating build XML round-trip functionality.
1 parent 2412761 commit 81d789f

File tree

3 files changed

+287
-5
lines changed

3 files changed

+287
-5
lines changed

src/main/java/org/apache/maven/buildcache/CacheUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static boolean isPom(Dependency dependency) {
7070
}
7171

7272
public static boolean isSnapshot(String version) {
73-
return version.endsWith(SNAPSHOT_VERSION) || version.endsWith(LATEST_VERSION);
73+
return version != null && (version.endsWith(SNAPSHOT_VERSION) || version.endsWith(LATEST_VERSION));
7474
}
7575

7676
public static String normalizedName(Artifact artifact) {

src/main/java/org/apache/maven/buildcache/checksum/MavenProjectInput.java

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.HashSet;
4040
import java.util.List;
4141
import java.util.Map;
42+
import java.util.Objects;
4243
import java.util.Optional;
4344
import java.util.Properties;
4445
import java.util.Set;
@@ -55,6 +56,7 @@
5556
import org.apache.maven.artifact.handler.ArtifactHandler;
5657
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
5758
import org.apache.maven.artifact.resolver.filter.ExcludesArtifactFilter;
59+
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
5860
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
5961
import org.apache.maven.artifact.versioning.VersionRange;
6062
import org.apache.maven.buildcache.CacheUtils;
@@ -776,11 +778,23 @@ private SortedMap<String, String> getMutableDependenciesHashes(String keyPrefix,
776778
continue;
777779
}
778780

781+
final String versionSpec = dependency.getVersion();
782+
779783
// saved to index by the end of dependency build
780-
MavenProject dependencyProject = multiModuleSupport
781-
.tryToResolveProject(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion())
782-
.orElse(null);
783-
boolean isSnapshot = isSnapshot(dependency.getVersion());
784+
MavenProject dependencyProject = versionSpec == null
785+
? null
786+
: multiModuleSupport
787+
.tryToResolveProject(dependency.getGroupId(), dependency.getArtifactId(), versionSpec)
788+
.orElse(null);
789+
790+
// for dynamic versions (LATEST/RELEASE/ranges), reactor artifacts can be part of the build
791+
// but cannot be resolved yet from the workspace (not built), so Aether may try remote download.
792+
// If a matching reactor module exists, treat it as multi-module dependency and use project checksum.
793+
if (dependencyProject == null && isDynamicVersion(versionSpec)) {
794+
dependencyProject = tryResolveReactorProjectByGA(dependency).orElse(null);
795+
}
796+
797+
boolean isSnapshot = isSnapshot(versionSpec);
784798
if (dependencyProject == null && !isSnapshot) {
785799
// external immutable dependency, should skip
786800
continue;
@@ -810,6 +824,19 @@ private SortedMap<String, String> getMutableDependenciesHashes(String keyPrefix,
810824
private DigestItem resolveArtifact(final Dependency dependency)
811825
throws IOException, ArtifactResolutionException, InvalidVersionSpecificationException {
812826

827+
// system-scoped dependencies are local files (systemPath) and must NOT be resolved via Aether.
828+
if (Artifact.SCOPE_SYSTEM.equals(dependency.getScope()) && dependency.getSystemPath() != null) {
829+
final Path systemPath = Paths.get(dependency.getSystemPath()).normalize();
830+
if (!Files.exists(systemPath)) {
831+
throw new DependencyNotResolvedException(
832+
"System dependency file does not exist: " + systemPath + " for dependency: " + dependency);
833+
}
834+
final HashAlgorithm algorithm = config.getHashFactory().createAlgorithm();
835+
final String hash = algorithm.hash(systemPath);
836+
final Artifact artifact = createDependencyArtifact(dependency);
837+
return DtoUtils.createDigestedFile(artifact, hash);
838+
}
839+
813840
org.eclipse.aether.artifact.Artifact dependencyArtifact = new org.eclipse.aether.artifact.DefaultArtifact(
814841
dependency.getGroupId(),
815842
dependency.getArtifactId(),
@@ -847,6 +874,54 @@ private DigestItem resolveArtifact(final Dependency dependency)
847874
return DtoUtils.createDigestedFile(artifact, hash);
848875
}
849876

877+
private static boolean isDynamicVersion(String versionSpec) {
878+
if (versionSpec == null) {
879+
return true;
880+
}
881+
if ("LATEST".equals(versionSpec) || "RELEASE".equals(versionSpec)) {
882+
return true;
883+
}
884+
// Maven version ranges: [1.0,2.0), (1.0,), etc.
885+
return versionSpec.startsWith("[") || versionSpec.startsWith("(") || versionSpec.contains(",");
886+
}
887+
888+
private Optional<MavenProject> tryResolveReactorProjectByGA(Dependency dependency) {
889+
final List<MavenProject> projects = session.getAllProjects();
890+
if (projects == null || projects.isEmpty()) {
891+
return Optional.empty();
892+
}
893+
894+
final String groupId = dependency.getGroupId();
895+
final String artifactId = dependency.getArtifactId();
896+
final String versionSpec = dependency.getVersion();
897+
898+
for (MavenProject candidate : projects) {
899+
if (!Objects.equals(groupId, candidate.getGroupId())
900+
|| !Objects.equals(artifactId, candidate.getArtifactId())) {
901+
continue;
902+
}
903+
904+
// For null/LATEST/RELEASE, accept the reactor module directly.
905+
if (versionSpec == null || "LATEST".equals(versionSpec) || "RELEASE".equals(versionSpec)) {
906+
return Optional.of(candidate);
907+
}
908+
909+
// For ranges, only accept if reactor version fits the range.
910+
if (versionSpec.startsWith("[") || versionSpec.startsWith("(") || versionSpec.contains(",")) {
911+
try {
912+
VersionRange range = VersionRange.createFromVersionSpec(versionSpec);
913+
if (range.containsVersion(new DefaultArtifactVersion(candidate.getVersion()))) {
914+
return Optional.of(candidate);
915+
}
916+
} catch (InvalidVersionSpecificationException e) {
917+
// If the spec is not parseable as range, don't guess.
918+
return Optional.empty();
919+
}
920+
}
921+
}
922+
return Optional.empty();
923+
}
924+
850925
/**
851926
* PathIgnoringCaseComparator
852927
*/
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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 org.apache.maven.buildcache.checksum;
20+
21+
import java.lang.reflect.Method;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.Properties;
29+
import java.util.SortedMap;
30+
31+
import org.apache.maven.artifact.handler.ArtifactHandler;
32+
import org.apache.maven.buildcache.MultiModuleSupport;
33+
import org.apache.maven.buildcache.NormalizedModelProvider;
34+
import org.apache.maven.buildcache.ProjectInputCalculator;
35+
import org.apache.maven.buildcache.RemoteCacheRepository;
36+
import org.apache.maven.buildcache.hash.HashFactory;
37+
import org.apache.maven.buildcache.xml.CacheConfig;
38+
import org.apache.maven.buildcache.xml.build.DigestItem;
39+
import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
40+
import org.apache.maven.execution.MavenSession;
41+
import org.apache.maven.model.Dependency;
42+
import org.apache.maven.project.MavenProject;
43+
import org.eclipse.aether.RepositorySystem;
44+
import org.eclipse.aether.RepositorySystemSession;
45+
import org.junit.jupiter.api.BeforeEach;
46+
import org.junit.jupiter.api.Test;
47+
import org.junit.jupiter.api.extension.ExtendWith;
48+
import org.junit.jupiter.api.io.TempDir;
49+
import org.mockito.Mock;
50+
import org.mockito.junit.jupiter.MockitoExtension;
51+
import org.mockito.junit.jupiter.MockitoSettings;
52+
import org.mockito.quality.Strictness;
53+
54+
import static org.junit.jupiter.api.Assertions.assertEquals;
55+
import static org.mockito.Mockito.mock;
56+
import static org.mockito.Mockito.never;
57+
import static org.mockito.Mockito.verify;
58+
import static org.mockito.Mockito.when;
59+
60+
/**
61+
* Regression tests for:
62+
* - #411: avoid resolving/downloading reactor dependencies when their versions are dynamic (e.g. LATEST)
63+
* - #417: system-scoped dependencies must be hashed from systemPath without Aether resolution
64+
*/
65+
@ExtendWith(MockitoExtension.class)
66+
@MockitoSettings(strictness = Strictness.LENIENT)
67+
class MavenProjectInputReactorAndSystemScopeRegressionTest {
68+
69+
@Mock
70+
private MavenProject project;
71+
72+
@Mock
73+
private MavenSession session;
74+
75+
@Mock
76+
private RepositorySystem repoSystem;
77+
78+
@Mock
79+
private RepositorySystemSession repositorySystemSession;
80+
81+
@Mock
82+
private NormalizedModelProvider normalizedModelProvider;
83+
84+
@Mock
85+
private MultiModuleSupport multiModuleSupport;
86+
87+
@Mock
88+
private ProjectInputCalculator projectInputCalculator;
89+
90+
@Mock
91+
private CacheConfig config;
92+
93+
@Mock
94+
private RemoteCacheRepository remoteCache;
95+
96+
@Mock
97+
private org.apache.maven.artifact.handler.manager.ArtifactHandlerManager artifactHandlerManager;
98+
99+
@TempDir
100+
Path tempDir;
101+
102+
private MavenProjectInput mavenProjectInput;
103+
104+
@BeforeEach
105+
void setUp() {
106+
when(session.getRepositorySession()).thenReturn(repositorySystemSession);
107+
when(project.getBasedir()).thenReturn(tempDir.toFile());
108+
when(project.getProperties()).thenReturn(new Properties());
109+
when(config.getDefaultGlob()).thenReturn("*");
110+
when(config.isProcessPlugins()).thenReturn("false");
111+
when(config.getGlobalExcludePaths()).thenReturn(new ArrayList<>());
112+
when(config.calculateProjectVersionChecksum()).thenReturn(Boolean.FALSE);
113+
when(config.getHashFactory()).thenReturn(HashFactory.SHA1);
114+
115+
org.apache.maven.model.Build build = new org.apache.maven.model.Build();
116+
build.setDirectory(tempDir.toString());
117+
build.setOutputDirectory(tempDir.resolve("target/classes").toString());
118+
build.setTestOutputDirectory(tempDir.resolve("target/test-classes").toString());
119+
build.setSourceDirectory(tempDir.resolve("src/main/java").toString());
120+
build.setTestSourceDirectory(tempDir.resolve("src/test/java").toString());
121+
build.setResources(new ArrayList<>());
122+
build.setTestResources(new ArrayList<>());
123+
when(project.getBuild()).thenReturn(build);
124+
125+
when(project.getDependencies()).thenReturn(new ArrayList<>());
126+
when(project.getBuildPlugins()).thenReturn(new ArrayList<>());
127+
when(project.getModules()).thenReturn(new ArrayList<>());
128+
when(project.getPackaging()).thenReturn("jar");
129+
130+
ArtifactHandler handler = mock(ArtifactHandler.class);
131+
when(handler.getClassifier()).thenReturn(null);
132+
when(handler.getExtension()).thenReturn("jar");
133+
when(artifactHandlerManager.getArtifactHandler(org.mockito.ArgumentMatchers.anyString()))
134+
.thenReturn(handler);
135+
136+
mavenProjectInput = new MavenProjectInput(
137+
project,
138+
normalizedModelProvider,
139+
multiModuleSupport,
140+
projectInputCalculator,
141+
session,
142+
config,
143+
repoSystem,
144+
remoteCache,
145+
artifactHandlerManager);
146+
}
147+
148+
@Test
149+
void testSystemScopeDependencyHashedFromSystemPathWithoutAetherResolution() throws Exception {
150+
Path systemJar = tempDir.resolve("local-lib.jar");
151+
Files.write(systemJar, "abc".getBytes(StandardCharsets.UTF_8));
152+
153+
Dependency dependency = new Dependency();
154+
dependency.setGroupId("com.example");
155+
dependency.setArtifactId("local-lib");
156+
dependency.setVersion("1.0");
157+
dependency.setType("jar");
158+
dependency.setScope("system");
159+
dependency.setSystemPath(systemJar.toString());
160+
dependency.setOptional(true);
161+
162+
Method resolveArtifact = MavenProjectInput.class.getDeclaredMethod("resolveArtifact", Dependency.class);
163+
resolveArtifact.setAccessible(true);
164+
DigestItem digest = (DigestItem) resolveArtifact.invoke(mavenProjectInput, dependency);
165+
166+
String expectedHash = HashFactory.SHA1.createAlgorithm().hash(systemJar);
167+
assertEquals(expectedHash, digest.getHash());
168+
169+
verify(repoSystem, never())
170+
.resolveArtifact(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
171+
}
172+
173+
@Test
174+
void testDynamicVersionReactorDependencyUsesProjectChecksumAndAvoidsAetherResolution() throws Exception {
175+
Dependency dependency = new Dependency();
176+
dependency.setGroupId("com.example");
177+
dependency.setArtifactId("reactor-artifact");
178+
dependency.setVersion("LATEST");
179+
dependency.setType("jar");
180+
181+
when(multiModuleSupport.tryToResolveProject("com.example", "reactor-artifact", "LATEST"))
182+
.thenReturn(java.util.Optional.empty());
183+
184+
MavenProject reactorProject = mock(MavenProject.class);
185+
when(reactorProject.getGroupId()).thenReturn("com.example");
186+
when(reactorProject.getArtifactId()).thenReturn("reactor-artifact");
187+
when(reactorProject.getVersion()).thenReturn("1.0-SNAPSHOT");
188+
when(session.getAllProjects()).thenReturn(Collections.singletonList(reactorProject));
189+
190+
ProjectsInputInfo projectInfo = mock(ProjectsInputInfo.class);
191+
when(projectInfo.getChecksum()).thenReturn("reactorChecksum");
192+
when(projectInputCalculator.calculateInput(reactorProject)).thenReturn(projectInfo);
193+
194+
Method getMutableDependenciesHashes =
195+
MavenProjectInput.class.getDeclaredMethod("getMutableDependenciesHashes", String.class, List.class);
196+
getMutableDependenciesHashes.setAccessible(true);
197+
198+
SortedMap<String, String> hashes = (SortedMap<String, String>)
199+
getMutableDependenciesHashes.invoke(mavenProjectInput, "", Collections.singletonList(dependency));
200+
201+
assertEquals("reactorChecksum", hashes.get("com.example:reactor-artifact:jar"));
202+
203+
verify(repoSystem, never())
204+
.resolveArtifact(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
205+
verify(projectInputCalculator).calculateInput(reactorProject);
206+
}
207+
}

0 commit comments

Comments
 (0)