This is an automated email from the ASF dual-hosted git repository.
olamy pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/maven-build-cache-extension.git
The following commit(s) were added to refs/heads/master by this push:
new db0ce24 Fixes for regression after pr #379 (#419)
db0ce24 is described below
commit db0ce2428e8dbf1a53d1be70606538667aefdf34
Author: maximiln <[email protected]>
AuthorDate: Thu Dec 25 10:02:39 2025 +0100
Fixes for regression after pr #379 (#419)
Issues: #411 #417
- Improved dependency resolution by adding checks for dynamic versions and
system-scoped dependencies.
Co-authored-by: maximiln <[email protected]>
---
.../org/apache/maven/buildcache/CacheUtils.java | 2 +-
.../buildcache/checksum/MavenProjectInput.java | 83 ++++++++-
...ctInputReactorAndSystemScopeRegressionTest.java | 207 +++++++++++++++++++++
3 files changed, 287 insertions(+), 5 deletions(-)
diff --git a/src/main/java/org/apache/maven/buildcache/CacheUtils.java
b/src/main/java/org/apache/maven/buildcache/CacheUtils.java
index 67d639a..df3bb49 100644
--- a/src/main/java/org/apache/maven/buildcache/CacheUtils.java
+++ b/src/main/java/org/apache/maven/buildcache/CacheUtils.java
@@ -70,7 +70,7 @@ public static boolean isPom(Dependency dependency) {
}
public static boolean isSnapshot(String version) {
- return version.endsWith(SNAPSHOT_VERSION) ||
version.endsWith(LATEST_VERSION);
+ return version != null && (version.endsWith(SNAPSHOT_VERSION) ||
version.endsWith(LATEST_VERSION));
}
public static String normalizedName(Artifact artifact) {
diff --git
a/src/main/java/org/apache/maven/buildcache/checksum/MavenProjectInput.java
b/src/main/java/org/apache/maven/buildcache/checksum/MavenProjectInput.java
index d305a12..5cc53d8 100644
--- a/src/main/java/org/apache/maven/buildcache/checksum/MavenProjectInput.java
+++ b/src/main/java/org/apache/maven/buildcache/checksum/MavenProjectInput.java
@@ -39,6 +39,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
@@ -55,6 +56,7 @@
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.artifact.resolver.filter.ExcludesArtifactFilter;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import
org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.buildcache.CacheUtils;
@@ -776,11 +778,23 @@ private SortedMap<String, String>
getMutableDependenciesHashes(String keyPrefix,
continue;
}
+ final String versionSpec = dependency.getVersion();
+
// saved to index by the end of dependency build
- MavenProject dependencyProject = multiModuleSupport
- .tryToResolveProject(dependency.getGroupId(),
dependency.getArtifactId(), dependency.getVersion())
- .orElse(null);
- boolean isSnapshot = isSnapshot(dependency.getVersion());
+ MavenProject dependencyProject = versionSpec == null
+ ? null
+ : multiModuleSupport
+ .tryToResolveProject(dependency.getGroupId(),
dependency.getArtifactId(), versionSpec)
+ .orElse(null);
+
+ // for dynamic versions (LATEST/RELEASE/ranges), reactor artifacts
can be part of the build
+ // but cannot be resolved yet from the workspace (not built), so
Aether may try remote download.
+ // If a matching reactor module exists, treat it as multi-module
dependency and use project checksum.
+ if (dependencyProject == null && isDynamicVersion(versionSpec)) {
+ dependencyProject =
tryResolveReactorProjectByGA(dependency).orElse(null);
+ }
+
+ boolean isSnapshot = isSnapshot(versionSpec);
if (dependencyProject == null && !isSnapshot) {
// external immutable dependency, should skip
continue;
@@ -810,6 +824,19 @@ private SortedMap<String, String>
getMutableDependenciesHashes(String keyPrefix,
private DigestItem resolveArtifact(final Dependency dependency)
throws IOException, ArtifactResolutionException,
InvalidVersionSpecificationException {
+ // system-scoped dependencies are local files (systemPath) and must
NOT be resolved via Aether.
+ if (Artifact.SCOPE_SYSTEM.equals(dependency.getScope()) &&
dependency.getSystemPath() != null) {
+ final Path systemPath =
Paths.get(dependency.getSystemPath()).normalize();
+ if (!Files.exists(systemPath)) {
+ throw new DependencyNotResolvedException(
+ "System dependency file does not exist: " + systemPath
+ " for dependency: " + dependency);
+ }
+ final HashAlgorithm algorithm =
config.getHashFactory().createAlgorithm();
+ final String hash = algorithm.hash(systemPath);
+ final Artifact artifact = createDependencyArtifact(dependency);
+ return DtoUtils.createDigestedFile(artifact, hash);
+ }
+
org.eclipse.aether.artifact.Artifact dependencyArtifact = new
org.eclipse.aether.artifact.DefaultArtifact(
dependency.getGroupId(),
dependency.getArtifactId(),
@@ -847,6 +874,54 @@ private DigestItem resolveArtifact(final Dependency
dependency)
return DtoUtils.createDigestedFile(artifact, hash);
}
+ private static boolean isDynamicVersion(String versionSpec) {
+ if (versionSpec == null) {
+ return true;
+ }
+ if ("LATEST".equals(versionSpec) || "RELEASE".equals(versionSpec)) {
+ return true;
+ }
+ // Maven version ranges: [1.0,2.0), (1.0,), etc.
+ return versionSpec.startsWith("[") || versionSpec.startsWith("(") ||
versionSpec.contains(",");
+ }
+
+ private Optional<MavenProject> tryResolveReactorProjectByGA(Dependency
dependency) {
+ final List<MavenProject> projects = session.getAllProjects();
+ if (projects == null || projects.isEmpty()) {
+ return Optional.empty();
+ }
+
+ final String groupId = dependency.getGroupId();
+ final String artifactId = dependency.getArtifactId();
+ final String versionSpec = dependency.getVersion();
+
+ for (MavenProject candidate : projects) {
+ if (!Objects.equals(groupId, candidate.getGroupId())
+ || !Objects.equals(artifactId, candidate.getArtifactId()))
{
+ continue;
+ }
+
+ // For null/LATEST/RELEASE, accept the reactor module directly.
+ if (versionSpec == null || "LATEST".equals(versionSpec) ||
"RELEASE".equals(versionSpec)) {
+ return Optional.of(candidate);
+ }
+
+ // For ranges, only accept if reactor version fits the range.
+ if (versionSpec.startsWith("[") || versionSpec.startsWith("(") ||
versionSpec.contains(",")) {
+ try {
+ VersionRange range =
VersionRange.createFromVersionSpec(versionSpec);
+ if (range.containsVersion(new
DefaultArtifactVersion(candidate.getVersion()))) {
+ return Optional.of(candidate);
+ }
+ } catch (InvalidVersionSpecificationException e) {
+ // If the spec is not parseable as range, don't guess.
+ return Optional.empty();
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
/**
* PathIgnoringCaseComparator
*/
diff --git
a/src/test/java/org/apache/maven/buildcache/checksum/MavenProjectInputReactorAndSystemScopeRegressionTest.java
b/src/test/java/org/apache/maven/buildcache/checksum/MavenProjectInputReactorAndSystemScopeRegressionTest.java
new file mode 100644
index 0000000..d9df110
--- /dev/null
+++
b/src/test/java/org/apache/maven/buildcache/checksum/MavenProjectInputReactorAndSystemScopeRegressionTest.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.buildcache.checksum;
+
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.SortedMap;
+
+import org.apache.maven.artifact.handler.ArtifactHandler;
+import org.apache.maven.buildcache.MultiModuleSupport;
+import org.apache.maven.buildcache.NormalizedModelProvider;
+import org.apache.maven.buildcache.ProjectInputCalculator;
+import org.apache.maven.buildcache.RemoteCacheRepository;
+import org.apache.maven.buildcache.hash.HashFactory;
+import org.apache.maven.buildcache.xml.CacheConfig;
+import org.apache.maven.buildcache.xml.build.DigestItem;
+import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Regression tests for:
+ * - #411: avoid resolving/downloading reactor dependencies when their
versions are dynamic (e.g. LATEST)
+ * - #417: system-scoped dependencies must be hashed from systemPath without
Aether resolution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class MavenProjectInputReactorAndSystemScopeRegressionTest {
+
+ @Mock
+ private MavenProject project;
+
+ @Mock
+ private MavenSession session;
+
+ @Mock
+ private RepositorySystem repoSystem;
+
+ @Mock
+ private RepositorySystemSession repositorySystemSession;
+
+ @Mock
+ private NormalizedModelProvider normalizedModelProvider;
+
+ @Mock
+ private MultiModuleSupport multiModuleSupport;
+
+ @Mock
+ private ProjectInputCalculator projectInputCalculator;
+
+ @Mock
+ private CacheConfig config;
+
+ @Mock
+ private RemoteCacheRepository remoteCache;
+
+ @Mock
+ private org.apache.maven.artifact.handler.manager.ArtifactHandlerManager
artifactHandlerManager;
+
+ @TempDir
+ Path tempDir;
+
+ private MavenProjectInput mavenProjectInput;
+
+ @BeforeEach
+ void setUp() {
+
when(session.getRepositorySession()).thenReturn(repositorySystemSession);
+ when(project.getBasedir()).thenReturn(tempDir.toFile());
+ when(project.getProperties()).thenReturn(new Properties());
+ when(config.getDefaultGlob()).thenReturn("*");
+ when(config.isProcessPlugins()).thenReturn("false");
+ when(config.getGlobalExcludePaths()).thenReturn(new ArrayList<>());
+
when(config.calculateProjectVersionChecksum()).thenReturn(Boolean.FALSE);
+ when(config.getHashFactory()).thenReturn(HashFactory.SHA1);
+
+ org.apache.maven.model.Build build = new
org.apache.maven.model.Build();
+ build.setDirectory(tempDir.toString());
+ build.setOutputDirectory(tempDir.resolve("target/classes").toString());
+
build.setTestOutputDirectory(tempDir.resolve("target/test-classes").toString());
+ build.setSourceDirectory(tempDir.resolve("src/main/java").toString());
+
build.setTestSourceDirectory(tempDir.resolve("src/test/java").toString());
+ build.setResources(new ArrayList<>());
+ build.setTestResources(new ArrayList<>());
+ when(project.getBuild()).thenReturn(build);
+
+ when(project.getDependencies()).thenReturn(new ArrayList<>());
+ when(project.getBuildPlugins()).thenReturn(new ArrayList<>());
+ when(project.getModules()).thenReturn(new ArrayList<>());
+ when(project.getPackaging()).thenReturn("jar");
+
+ ArtifactHandler handler = mock(ArtifactHandler.class);
+ when(handler.getClassifier()).thenReturn(null);
+ when(handler.getExtension()).thenReturn("jar");
+
when(artifactHandlerManager.getArtifactHandler(org.mockito.ArgumentMatchers.anyString()))
+ .thenReturn(handler);
+
+ mavenProjectInput = new MavenProjectInput(
+ project,
+ normalizedModelProvider,
+ multiModuleSupport,
+ projectInputCalculator,
+ session,
+ config,
+ repoSystem,
+ remoteCache,
+ artifactHandlerManager);
+ }
+
+ @Test
+ void
testSystemScopeDependencyHashedFromSystemPathWithoutAetherResolution() throws
Exception {
+ Path systemJar = tempDir.resolve("local-lib.jar");
+ Files.write(systemJar, "abc".getBytes(StandardCharsets.UTF_8));
+
+ Dependency dependency = new Dependency();
+ dependency.setGroupId("com.example");
+ dependency.setArtifactId("local-lib");
+ dependency.setVersion("1.0");
+ dependency.setType("jar");
+ dependency.setScope("system");
+ dependency.setSystemPath(systemJar.toString());
+ dependency.setOptional(true);
+
+ Method resolveArtifact =
MavenProjectInput.class.getDeclaredMethod("resolveArtifact", Dependency.class);
+ resolveArtifact.setAccessible(true);
+ DigestItem digest = (DigestItem)
resolveArtifact.invoke(mavenProjectInput, dependency);
+
+ String expectedHash =
HashFactory.SHA1.createAlgorithm().hash(systemJar);
+ assertEquals(expectedHash, digest.getHash());
+
+ verify(repoSystem, never())
+ .resolveArtifact(org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any());
+ }
+
+ @Test
+ void
testDynamicVersionReactorDependencyUsesProjectChecksumAndAvoidsAetherResolution()
throws Exception {
+ Dependency dependency = new Dependency();
+ dependency.setGroupId("com.example");
+ dependency.setArtifactId("reactor-artifact");
+ dependency.setVersion("LATEST");
+ dependency.setType("jar");
+
+ when(multiModuleSupport.tryToResolveProject("com.example",
"reactor-artifact", "LATEST"))
+ .thenReturn(java.util.Optional.empty());
+
+ MavenProject reactorProject = mock(MavenProject.class);
+ when(reactorProject.getGroupId()).thenReturn("com.example");
+ when(reactorProject.getArtifactId()).thenReturn("reactor-artifact");
+ when(reactorProject.getVersion()).thenReturn("1.0-SNAPSHOT");
+
when(session.getAllProjects()).thenReturn(Collections.singletonList(reactorProject));
+
+ ProjectsInputInfo projectInfo = mock(ProjectsInputInfo.class);
+ when(projectInfo.getChecksum()).thenReturn("reactorChecksum");
+
when(projectInputCalculator.calculateInput(reactorProject)).thenReturn(projectInfo);
+
+ Method getMutableDependenciesHashes =
+
MavenProjectInput.class.getDeclaredMethod("getMutableDependenciesHashes",
String.class, List.class);
+ getMutableDependenciesHashes.setAccessible(true);
+
+ SortedMap<String, String> hashes = (SortedMap<String, String>)
+ getMutableDependenciesHashes.invoke(mavenProjectInput, "",
Collections.singletonList(dependency));
+
+ assertEquals("reactorChecksum",
hashes.get("com.example:reactor-artifact:jar"));
+
+ verify(repoSystem, never())
+ .resolveArtifact(org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any());
+ verify(projectInputCalculator).calculateInput(reactorProject);
+ }
+}