diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java index 9c3555aefff7..1a94d9d7196b 100644 --- a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java +++ b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java @@ -19,9 +19,12 @@ package org.apache.maven.model.v4; import java.io.InputStream; +import java.util.Arrays; import java.util.Collections; import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; import org.apache.maven.api.model.Model; import org.apache.maven.api.model.Plugin; import org.apache.maven.api.model.PluginExecution; @@ -72,4 +75,73 @@ void testV4ModelPriority() { PluginExecution.newInstance().withPriority(5)))))); assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m)); } + + @Test + void testV4ModelWithNewMaven4Scopes() { + // Test compile-only scope + Dependency compileOnlyDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-only-dep") + .version("1.0.0") + .scope("compile-only") + .build(); + + Model m1 = model.withDependencies(Arrays.asList(compileOnlyDep)); + // Should return "4.1.0" because compile-only scope requires Maven 4.1.0+ + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m1)); + + // Test test-only scope + Dependency testOnlyDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("test-only-dep") + .version("1.0.0") + .scope("test-only") + .build(); + + Model m2 = model.withDependencies(Arrays.asList(testOnlyDep)); + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m2)); + + // Test test-runtime scope + Dependency testRuntimeDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("test-runtime-dep") + .version("1.0.0") + .scope("test-runtime") + .build(); + + Model m3 = model.withDependencies(Arrays.asList(testRuntimeDep)); + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m3)); + + // Test new scopes in dependency management + DependencyManagement depMgmt = DependencyManagement.newBuilder() + .dependencies(Arrays.asList(compileOnlyDep)) + .build(); + + Model m4 = model.withDependencyManagement(depMgmt); + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m4)); + } + + @Test + void testV4ModelWithStandardScopes() { + // Test that standard scopes don't require 4.1.0 + Dependency compileDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-dep") + .version("1.0.0") + .scope("compile") + .build(); + + Model m1 = model.withDependencies(Arrays.asList(compileDep)); + assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m1)); + + Dependency testDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("test-dep") + .version("1.0.0") + .scope("test") + .build(); + + Model m2 = model.withDependencies(Arrays.asList(testDep)); + assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m2)); + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index 3a17f00ed701..ffc8f37c1b92 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -46,6 +46,7 @@ import org.apache.maven.api.services.Sources; import org.apache.maven.api.services.model.LifecycleBindingsInjector; import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration; import org.apache.maven.model.v4.MavenModelVersion; import org.apache.maven.project.MavenProject; import org.eclipse.aether.RepositorySystemSession; @@ -120,6 +121,8 @@ private Model buildEffectiveModel(RepositorySystemSession session, Path src) thr .collect(Collectors.toMap(n -> getDependencyKey(n.getDependency()), Function.identity())); Map directDependencies = model.getDependencies().stream() .filter(dependency -> !"import".equals(dependency.getScope())) + .map(this::transformDependencyForConsumerPom) + .filter(dependency -> dependency != null) // Filter out dependencies that should be omitted .collect(Collectors.toMap( DefaultConsumerPomBuilder::getDependencyKey, Function.identity(), @@ -128,6 +131,8 @@ private Model buildEffectiveModel(RepositorySystemSession session, Path src) thr Map managedDependencies = model.getDependencyManagement().getDependencies().stream() .filter(dependency -> nodes.containsKey(getDependencyKey(dependency)) && !"import".equals(dependency.getScope())) + .map(this::transformDependencyForConsumerPom) + .filter(dependency -> dependency != null) // Filter out dependencies that should be omitted .collect(Collectors.toMap( DefaultConsumerPomBuilder::getDependencyKey, Function.identity(), @@ -175,6 +180,30 @@ private Dependency merge(Dependency dep1, Dependency dep2) { throw new IllegalArgumentException("Duplicate dependency: " + dep1); } + /** + * Transforms a dependency for inclusion in a consumer POM. + * Uses centralized mapping of Maven 4 scopes to Maven 3-compatible values. + * + * @param dependency the original dependency + * @return the transformed dependency, or null if the dependency should be omitted + */ + Dependency transformDependencyForConsumerPom(Dependency dependency) { + if (dependency == null) { + return null; + } + if (dependency.getScope() == null) { + return dependency; + } + String mapped = Maven4ScopeManagerConfiguration.mapScopeForMaven3ConsumerPom(dependency.getScope()); + if (mapped == null) { + return null; // omit + } + if (mapped.equals(dependency.getScope())) { + return dependency; // unchanged + } + return dependency.withScope(mapped); + } + private static String getDependencyKey(org.apache.maven.api.Dependency dependency) { return dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getType().id() + ":" + dependency.getClassifier(); diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java index 62f17df33bb0..43efbfb5ef9e 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java @@ -30,6 +30,7 @@ import org.apache.maven.api.PathScope; import org.apache.maven.api.Session; import org.apache.maven.api.SessionData; +import org.apache.maven.api.model.Dependency; import org.apache.maven.api.model.Model; import org.apache.maven.api.model.Scm; import org.apache.maven.api.services.DependencyResolver; @@ -51,6 +52,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -158,4 +160,85 @@ void testScmInheritance() throws Exception { assertNull(transformed.getScm().getChildScmUrlInheritAppendPath()); assertNull(transformed.getScm().getChildScmDeveloperConnectionInheritAppendPath()); } + + @Test + void testNewMaven4ScopesInConsumerPom() throws Exception { + // Create dependencies with new Maven 4 scopes + Dependency compileOnlyDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-only-dep") + .version("1.0.0") + .scope("compile-only") + .build(); + + Dependency testOnlyDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("test-only-dep") + .version("1.0.0") + .scope("test-only") + .build(); + + Dependency testRuntimeDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("test-runtime-dep") + .version("1.0.0") + .scope("test-runtime") + .build(); + + Dependency compileDep = Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-dep") + .version("1.0.0") + .scope("compile") + .build(); + + // Transform using the consumer POM builder + DefaultConsumerPomBuilder builder = new DefaultConsumerPomBuilder(null); + + // Test the transformation method directly + Dependency transformedCompileOnly = builder.transformDependencyForConsumerPom(compileOnlyDep); + Dependency transformedTestOnly = builder.transformDependencyForConsumerPom(testOnlyDep); + Dependency transformedTestRuntime = builder.transformDependencyForConsumerPom(testRuntimeDep); + Dependency transformedCompile = builder.transformDependencyForConsumerPom(compileDep); + + // New Maven 4 scopes handling + assertNull(transformedCompileOnly, "compile-only dependencies should be omitted from consumer POM"); + assertNull(transformedTestOnly, "test-only dependencies should be omitted from consumer POM"); + assertNotNull( + transformedTestRuntime, "test-runtime dependencies should be preserved as 'test' in consumer POM"); + assertEquals("test", transformedTestRuntime.getScope()); + + // Standard scopes should be preserved + assertNotNull(transformedCompile, "compile dependencies should be preserved in consumer POM"); + assertEquals("compile", transformedCompile.getScope()); + } + + @Test + void testNewMaven4ScopesInDependencyManagement() throws Exception { + // Create managed dependencies with new Maven 4 scopes + Dependency compileOnlyManaged = Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-only-managed") + .version("1.0.0") + .scope("compile-only") + .build(); + + Dependency testOnlyManaged = Dependency.newBuilder() + .groupId("org.example") + .artifactId("test-only-managed") + .version("1.0.0") + .scope("test-only") + .build(); + + // Test the transformation method directly + DefaultConsumerPomBuilder builder = new DefaultConsumerPomBuilder(null); + + Dependency transformedCompileOnlyManaged = builder.transformDependencyForConsumerPom(compileOnlyManaged); + Dependency transformedTestOnlyManaged = builder.transformDependencyForConsumerPom(testOnlyManaged); + + // New Maven 4 scopes should be filtered out even in dependency management + assertNull( + transformedCompileOnlyManaged, "compile-only managed dependencies should be omitted from consumer POM"); + assertNull(transformedTestOnlyManaged, "test-only managed dependencies should be omitted from consumer POM"); + } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java index 896b240053f6..5ce59d17d657 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java @@ -189,6 +189,36 @@ public Collection buildResolutionScope return result; } + /** + * Maps a Maven 4 scope id to a Maven 3-compatible scope id for consumer POMs. + * + *
    + *
  • compile-only -> omitted (returns null)
  • + *
  • test-only -> omitted (returns null)
  • + *
  • test-runtime -> test
  • + *
  • none -> omitted (returns null)
  • + *
  • others -> unchanged
  • + *
+ * + * @param scopeId the Maven 4 scope id (may be null) + * @return the mapped Maven 3 scope id, or null if the dependency should be omitted + */ + public static String mapScopeForMaven3ConsumerPom(String scopeId) { + if (scopeId == null) { + return null; + } + DependencyScope ds = DependencyScope.forId(scopeId); + if (ds == null) { + // Unknown scope: keep as-is (do not attempt to remap strings we don't know) + return scopeId; + } + return switch (ds) { + case COMPILE_ONLY, TEST_ONLY, NONE -> null; // Not meaningful for consumers of the artifact in Maven 3 + case TEST_RUNTIME -> DependencyScope.TEST.id(); + default -> ds.id(); + }; + } + // === public static void main(String... args) { diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm index 0fcca232e44a..a69e5b42e6f2 100644 --- a/src/mdo/model-version.vm +++ b/src/mdo/model-version.vm @@ -50,6 +50,7 @@ import org.apache.maven.api.xml.XmlNode; #foreach ( $class in $model.allClasses ) import ${packageModelV4}.${class.Name}; #end +import ${packageModelV4}.Profile; @Generated public class ${className} { @@ -57,6 +58,11 @@ public class ${className} { public String getModelVersion(${root.name} model) { Objects.requireNonNull(model, "model cannot be null"); + // Check for new Maven 4 dependency scopes first + if (is_4_1_0_custom(model)) { + return "4.1.0"; + } + #set ( $String = $model.getClass().forName("java.lang.String") ) #set ( $Comparator = $model.getClass().forName("java.util.Comparator") ) #set ( $LinkedHashSet = $model.getClass().forName("java.util.LinkedHashSet") ) @@ -190,4 +196,48 @@ public class ${className} { return node != null; } + /** + * Checks if a dependency uses one of the new Maven 4 scopes that require model version 4.1.0+ + */ + private boolean hasNewMaven4Scope(Dependency dependency) { + if (dependency == null || dependency.getScope() == null) { + return false; + } + String scope = dependency.getScope(); + return "compile-only".equals(scope) || "test-only".equals(scope) || "test-runtime".equals(scope); + } + + /** + * Override the generated is_4_1_0 method to add custom logic for new Maven 4 dependency scopes + */ + private boolean is_4_1_0_custom(Model model) { + if (model == null) { + return false; + } + + // Check direct dependencies for new scopes + if (model.getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) { + return true; + } + + // Check dependency management for new scopes + if (model.getDependencyManagement() != null && + model.getDependencyManagement().getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) { + return true; + } + + // Check profiles for new scopes + for (Profile profile : model.getProfiles()) { + if (profile.getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) { + return true; + } + if (profile.getDependencyManagement() != null && + profile.getDependencyManagement().getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) { + return true; + } + } + + return false; + } + }