From 34cd0ec5e8673d83ac1e4b82cbb53cab27b81919 Mon Sep 17 00:00:00 2001 From: maximiln Date: Sun, 14 Dec 2025 12:29:30 +0100 Subject: [PATCH] Fixes for regression after pr #379 Issues: #411 #417 - Improved dependency resolution by adding checks for dynamic versions and system-scoped dependencies. --- .../apache/maven/buildcache/CacheUtils.java | 2 +- .../checksum/MavenProjectInput.java | 83 ++++++- ...utReactorAndSystemScopeRegressionTest.java | 207 ++++++++++++++++++ 3 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/apache/maven/buildcache/checksum/MavenProjectInputReactorAndSystemScopeRegressionTest.java diff --git a/src/main/java/org/apache/maven/buildcache/CacheUtils.java b/src/main/java/org/apache/maven/buildcache/CacheUtils.java index 67d639a4..df3bb495 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 d305a129..5cc53d84 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 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 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 tryResolveReactorProjectByGA(Dependency dependency) { + final List 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 00000000..d9df1100 --- /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 hashes = (SortedMap) + 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); + } +}