diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java index f9f788e00316..89e4a205eb55 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java @@ -66,6 +66,26 @@ public enum DependencyScope { */ COMPILE("compile", true), + /** + * Compile, runtime and test, transitively exposed to consumers. + * Semantically equivalent to {@code compile}, but explicitly declares + * that this dependency forms part of the project's public API. + * Only valid for {@code modelVersion 4.2.0+}. + * + * @since 4.2.0 + */ + API("api", true), + + /** + * Compile, runtime and test, but not transitively exposed to consumers. + * Use for dependencies that are internal implementation details. + * Mapped to {@code runtime} scope in consumer POMs for Maven 3 compatibility. + * Only valid for {@code modelVersion 4.2.0+}. + * + * @since 4.2.0 + */ + IMPLEMENTATION("implementation", false), + /** * Runtime and test. */ diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java index bd8a6f1bf51c..42ec56cc63ed 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java @@ -58,15 +58,24 @@ public interface PathScope extends ExtensibleEnum { ProjectScope.MAIN, DependencyScope.COMPILE_ONLY, DependencyScope.COMPILE, + DependencyScope.API, + DependencyScope.IMPLEMENTATION, DependencyScope.PROVIDED); - PathScope MAIN_RUNTIME = - pathScope("main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.RUNTIME); + PathScope MAIN_RUNTIME = pathScope( + "main-runtime", + ProjectScope.MAIN, + DependencyScope.COMPILE, + DependencyScope.API, + DependencyScope.IMPLEMENTATION, + DependencyScope.RUNTIME); PathScope TEST_COMPILE = pathScope( "test-compile", ProjectScope.TEST, DependencyScope.COMPILE, + DependencyScope.API, + DependencyScope.IMPLEMENTATION, DependencyScope.PROVIDED, DependencyScope.TEST_ONLY, DependencyScope.TEST); @@ -75,6 +84,8 @@ public interface PathScope extends ExtensibleEnum { "test-runtime", ProjectScope.TEST, DependencyScope.COMPILE, + DependencyScope.API, + DependencyScope.IMPLEMENTATION, DependencyScope.RUNTIME, DependencyScope.PROVIDED, DependencyScope.TEST, 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..d89b14146772 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 @@ -20,8 +20,11 @@ import java.io.InputStream; import java.util.Collections; +import java.util.List; 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,69 @@ void testV4ModelPriority() { PluginExecution.newInstance().withPriority(5)))))); assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m)); } + + @Test + void testApiScopeDependencyRequires420() { + // A model with an api-scoped dependency should require modelVersion 4.2.0 + Model m = model.withDependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("api-lib") + .version("1.0") + .scope("api") + .build())); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testImplementationScopeDependencyRequires420() { + // A model with an implementation-scoped dependency should require modelVersion 4.2.0 + Model m = model.withDependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("impl-lib") + .version("1.0") + .scope("implementation") + .build())); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testApiScopeInDependencyManagementRequires420() { + // A model with an api-scoped dependency in dependencyManagement should require 4.2.0 + Model m = model.withDependencyManagement(DependencyManagement.newBuilder() + .dependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("api-lib") + .version("1.0") + .scope("api") + .build())) + .build()); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testCompileScopeDependencyRemains400() { + // A model with only compile-scoped dependencies should stay at 4.0.0 + Model m = model.withDependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-lib") + .version("1.0") + .scope("compile") + .build())); + assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testApiScopeInProfileRequires420() { + // A model with an api-scoped dependency in a profile should require 4.2.0 + Model m = model.withProfiles(List.of(org.apache.maven.api.model.Profile.newBuilder() + .id("my-profile") + .dependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("api-lib") + .version("1.0") + .scope("api") + .build())) + .build())); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); + } } diff --git a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java index a38833076355..52e61fa9bbf6 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java +++ b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java @@ -90,6 +90,10 @@ public Collection buildDependencyScope ArrayList result = new ArrayList<>(); result.add(internalScopeManager.createDependencyScope( DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.API.id(), DependencyScope.API.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.IMPLEMENTATION.id(), DependencyScope.IMPLEMENTATION.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.RUNTIME.id(), DependencyScope.RUNTIME.isTransitive(), 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 67a869302a29..6119f9ab73d1 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 @@ -582,8 +582,10 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject } return dependency; }); - // Only keep transitive scopes (null/empty => COMPILE) + // Only keep consumer-visible scopes (compile, api, runtime, implementation) directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope); + // Map 4.2.0 scopes to their 4.0.0 consumer POM equivalents (api→compile, implementation→runtime) + directDependencies.replaceAll((k, v) -> mapScopeForConsumerPom(v)); managedDependencies.keySet().removeAll(directDependencies.keySet()); model = model.withDependencyManagement( @@ -600,15 +602,17 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject Function.identity(), this::merge, LinkedHashMap::new)); - // Only keep transitive scopes + // Only keep consumer-visible scopes (compile, api, runtime, implementation) directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope); + // Map 4.2.0 scopes to their 4.0.0 consumer POM equivalents (api→compile, implementation→runtime) + directDependencies.replaceAll((k, v) -> mapScopeForConsumerPom(v)); model = model.withDependencies(directDependencies.isEmpty() ? null : directDependencies.values()); } return model; } - private static boolean hasDependencyScope(Dependency dependency) { + static boolean hasDependencyScope(Dependency dependency) { String scopeId = dependency.getScope(); DependencyScope scope; if (scopeId == null || scopeId.isEmpty()) { @@ -616,7 +620,27 @@ private static boolean hasDependencyScope(Dependency dependency) { } else { scope = DependencyScope.forId(scopeId); } - return scope == null || !scope.isTransitive(); + return scope != DependencyScope.COMPILE + && scope != DependencyScope.RUNTIME + && scope != DependencyScope.API + && scope != DependencyScope.IMPLEMENTATION; + } + + /** + * Maps a 4.2.0 dependency scope to its 4.0.0 consumer POM equivalent. + * + */ + static Dependency mapScopeForConsumerPom(Dependency dependency) { + String scope = dependency.getScope(); + if (DependencyScope.API.id().equals(scope)) { + return dependency.withScope("compile"); + } else if (DependencyScope.IMPLEMENTATION.id().equals(scope)) { + return dependency.withScope("runtime"); + } + return dependency; } private Dependency merge(Dependency dep1, Dependency dep2) { 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 5f6661a66563..2d3a6ff8656e 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 @@ -1499,4 +1499,110 @@ void testHasNonModelPropertiesReturnsFalseForEnvAndSettingsProperties() { DefaultConsumerPomBuilder.hasNonModelProperties("${env.HOME}-${ext.qualifier}", Map.of()), "Should return true when at least one property is not built-in or in the model"); } + + @Test + void testConsumerPomRetainsCompileApiRuntimeDeps() throws Exception { + // Consumer POMs must retain compile, api, and runtime dependencies + org.apache.maven.api.model.Dependency compileDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("compile-dep") + .version("1") + .scope("compile") + .build(); + org.apache.maven.api.model.Dependency apiDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("api-dep") + .version("1") + .scope("api") + .build(); + org.apache.maven.api.model.Dependency runtimeDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("runtime-dep") + .version("1") + .scope("runtime") + .build(); + org.apache.maven.api.model.Dependency unscopedDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("unscoped-dep") + .version("1") + .build(); + + Model model = Model.newBuilder() + .groupId("test") + .artifactId("test") + .version("1.0") + .dependencies(List.of(compileDep, apiDep, runtimeDep, unscopedDep)) + .build(); + + Model transformed = DefaultConsumerPomBuilder.transformNonPom(model, null); + assertNotNull(transformed.getDependencies()); + // All four should be retained + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "compile-dep".equals(d.getArtifactId())), + "compile-scoped dep should be retained"); + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "api-dep".equals(d.getArtifactId())), + "api-scoped dep should be retained"); + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "runtime-dep".equals(d.getArtifactId())), + "runtime-scoped dep should be retained"); + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "unscoped-dep".equals(d.getArtifactId())), + "unscoped (default compile) dep should be retained"); + } + + @Test + void testConsumerPomScopeFilter() { + // hasDependencyScope returns true for deps to REMOVE + // compile/runtime/api/implementation should be KEPT (returns false) + assertFalse(DefaultConsumerPomBuilder.hasDependencyScope(dep("compile")), "compile should be kept"); + assertFalse(DefaultConsumerPomBuilder.hasDependencyScope(dep("runtime")), "runtime should be kept"); + assertFalse(DefaultConsumerPomBuilder.hasDependencyScope(dep("api")), "api should be kept"); + assertFalse( + DefaultConsumerPomBuilder.hasDependencyScope(dep("implementation")), "implementation should be kept"); + assertFalse( + DefaultConsumerPomBuilder.hasDependencyScope(dep(null)), + "unscoped (defaults to compile) should be kept"); + assertFalse( + DefaultConsumerPomBuilder.hasDependencyScope(dep("")), + "empty scope (defaults to compile) should be kept"); + // provided/test/system should be STRIPPED (returns true) + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("provided")), "provided should be stripped"); + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("test")), "test should be stripped"); + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("system")), "system should be stripped"); + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("test-only")), "test-only should be stripped"); + } + + @Test + void testConsumerPomScopeMappings() { + // api → compile, implementation → runtime, others unchanged + assertEquals( + "compile", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("api")).getScope(), + "api should map to compile in consumer POM"); + assertEquals( + "runtime", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("implementation")) + .getScope(), + "implementation should map to runtime in consumer POM"); + assertEquals( + "compile", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("compile")).getScope(), + "compile should remain compile"); + assertEquals( + "runtime", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("runtime")).getScope(), + "runtime should remain runtime"); + } + + private static org.apache.maven.api.model.Dependency dep(String scope) { + org.apache.maven.api.model.Dependency.Builder b = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("a") + .version("1"); + if (scope != null) { + b.scope(scope); + } + return b.build(); + } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java index f17ee0c8cdeb..2bffb9a7be2d 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java @@ -500,6 +500,8 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { } boolean isModelVersion41OrMore = !Objects.equals(ModelBuilder.MODEL_VERSION_4_0_0, model.getModelVersion()); + boolean isModelVersion42OrMore = isModelVersion41OrMore + && !Objects.equals(ModelBuilder.MODEL_VERSION_4_1_0, model.getModelVersion()); if (isModelVersion41OrMore) { validateStringNoExpression("groupId", problems, Severity.FATAL, Version.V41, model.getGroupId(), model); @@ -547,6 +549,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { "dependencies.dependency.", EMPTY, isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); validate20RawDependenciesSelfReferencing( @@ -559,6 +562,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { "dependencyManagement.dependencies.dependency.", EMPTY, isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); } @@ -603,6 +607,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { prefix, "dependencies.dependency.", isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); if (profile.getDependencyManagement() != null) { @@ -612,6 +617,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { prefix, "dependencyManagement.dependencies.dependency.", isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); } @@ -1183,6 +1189,7 @@ private void validate20RawDependencies( String prefix, String prefix2, boolean is41OrBeyond, + boolean is42OrBeyond, int validationLevel) { Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0); Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1); @@ -1270,6 +1277,22 @@ private void validate20RawDependencies( dependency); } } + // MNG-8099: api and implementation scopes require modelVersion 4.2.0+ + if (!is42OrBeyond) { + String scope = dependency.getScope(); + if (DependencyScope.API.id().equals(scope) + || DependencyScope.IMPLEMENTATION.id().equals(scope)) { + addViolation( + problems, + Severity.ERROR, + Version.V20, + prefix + prefix2 + "scope", + SourceHint.dependencyManagementKey(dependency), + "scope '" + scope + "' is not supported with modelVersion 4.0.0 or 4.1.0; " + + "use modelVersion 4.2.0 or remove this scope.", + dependency); + } + } if (equals("LATEST", dependency.getVersion()) || equals("RELEASE", dependency.getVersion())) { addViolation( 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..f88edd1ab6f0 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 @@ -88,6 +88,10 @@ public Collection buildDependencyScope ArrayList result = new ArrayList<>(); result.add(internalScopeManager.createDependencyScope( DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.API.id(), DependencyScope.API.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.IMPLEMENTATION.id(), DependencyScope.IMPLEMENTATION.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.RUNTIME.id(), DependencyScope.RUNTIME.isTransitive(), diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm index 0fcca232e44a..c3f67ac32ac8 100644 --- a/src/mdo/model-version.vm +++ b/src/mdo/model-version.vm @@ -161,11 +161,35 @@ public class ${className} { #end #set ( $pfx = "||" ) #end + #if ( $v == "4_2_0" && $class.name == "Model" ) + $pfx hasNewScopes(${var}) // Dependency scopes api / implementation + #end ); } #end #end + private boolean hasNewScopes(Model model) { + return hasNewScopes((ModelBase) model) + || model.getProfiles().stream().anyMatch(this::hasNewScopes); + } + + private boolean hasNewScopes(Profile profile) { + return hasNewScopes((ModelBase) profile); + } + + private boolean hasNewScopes(ModelBase model) { + return model != null + && (model.getDependencies().stream().anyMatch(dependency -> + "api".equals(dependency.getScope()) || "implementation".equals(dependency.getScope())) + || (model.getDependencyManagement() != null + && model.getDependencyManagement().getDependencies().stream() + .anyMatch(dependency -> + "api".equals(dependency.getScope()) + || "implementation".equals(dependency.getScope())))); + } + } + private boolean has(String str) { return str != null; }