diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java index db81f5685ff1..85c05e97beea 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java +++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java @@ -729,18 +729,23 @@ private void applyAfterLinks(MojoDescriptor mojoDescriptor, MavenProject project * all upstream projects are returned. Otherwise, only projects that the given * project depends on with a matching scope are included. *

- * Matching is exact on the dependency's declared scope string (e.g. "compile", - * "provided", "test"). Maven's default dependency scope is "compile" (when no - * scope is declared), so a null scope in the model is treated as "compile" for - * matching purposes. Note that this does not perform path-scope - * resolution — for example, filtering by "compile" will not include - * "provided"-scoped dependencies even though they contribute to - * {@code PathScope.MAIN_COMPILE}. This keeps the filter simple and predictable; - * broader scope-aware filtering can be added in a follow-up if needed. + * The scope parameter is a lifecycle dependency scope (as declared in + * {@link org.apache.maven.api.DependencyScope}). It is expanded to match all + * artifact scopes that contribute to that dependency scope for build ordering: + *

+ * This ensures that reactor dependencies contributing to a given classpath are + * properly ordered in the build plan (e.g. a provided-scope reactor dependency + * is built before the consumer's compile phase). * * @param project the project whose dependencies to check * @param upstreamProjects the list of upstream reactor projects - * @param scope the dependency scope to filter by, or null/empty for all + * @param scope the lifecycle dependency scope to filter by, or null/empty for all * @return the filtered list of upstream projects */ static List filterByScope( @@ -748,14 +753,29 @@ static List filterByScope( if (scope == null || scope.isEmpty()) { return upstreamProjects; } + Set matchingScopes = expandScope(scope); return upstreamProjects.stream() .filter(dep -> project.getDependencies().stream() .anyMatch(d -> dep.getGroupId().equals(d.getGroupId()) && dep.getArtifactId().equals(d.getArtifactId()) - && scope.equals(d.getScope() != null ? d.getScope() : "compile"))) + && matchingScopes.contains(d.getScope() != null ? d.getScope() : "compile"))) .collect(Collectors.toList()); } + /** + * Expands a lifecycle dependency scope to the set of artifact scopes that + * contribute to it for build ordering purposes. + */ + private static Set expandScope(String scope) { + return switch (scope) { + case "compile" -> Set.of("compile", "provided", "system"); + case "runtime" -> Set.of("compile", "runtime"); + case "test" -> Set.of("compile", "provided", "system", "runtime", "test"); + case "test-only" -> Set.of("test"); + default -> Set.of(scope); + }; + } + protected BuildPlan computeForkPlan(BuildStep step, MojoExecution execution, BuildPlan buildPlan) { MojoDescriptor mojoDescriptor = execution.getMojoDescriptor(); PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor(); diff --git a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java index 6a08b7ee35ed..3a8cb0afb9c6 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java @@ -240,40 +240,54 @@ void testFilterByScopeNullReturnsAll() { } /** - * Tests that {@code filterByScope} filters upstream projects by exact scope match, - * treating null-scoped dependencies as "compile" (Maven default). + * Tests that {@code filterByScope} expands scopes for build ordering. + * "compile" includes compile, provided, system, and null-scoped (Maven default) dependencies + * so that provided/system-scope reactor dependencies are properly ordered. */ @Test void testFilterByScopeMatchesExact() { MavenProject compileDep = createProjectWithId("g", "compile-dep"); MavenProject providedDep = createProjectWithId("g", "provided-dep"); + MavenProject systemDep = createProjectWithId("g", "system-dep"); + MavenProject runtimeDep = createProjectWithId("g", "runtime-dep"); MavenProject testDep = createProjectWithId("g", "test-dep"); MavenProject nullScopeDep = createProjectWithId("g", "null-scope-dep"); - List upstream = List.of(compileDep, providedDep, testDep, nullScopeDep); + List upstream = List.of(compileDep, providedDep, systemDep, runtimeDep, testDep, nullScopeDep); MavenProject consumer = new MavenProject(); consumer.getDependencies().add(createDependency("g", "compile-dep", "compile")); consumer.getDependencies().add(createDependency("g", "provided-dep", "provided")); + consumer.getDependencies().add(createDependency("g", "system-dep", "system")); + consumer.getDependencies().add(createDependency("g", "runtime-dep", "runtime")); consumer.getDependencies().add(createDependency("g", "test-dep", "test")); consumer.getDependencies().add(createDependency("g", "null-scope-dep", null)); - // "compile" matches explicit compile + null-scoped (Maven default is compile) + // "compile" scope expands to compile + provided + system + null-scoped (Maven default) List compileFiltered = BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "compile"); - assertEquals(2, compileFiltered.size()); + assertEquals(4, compileFiltered.size()); assertTrue(compileFiltered.contains(compileDep)); + assertTrue(compileFiltered.contains(providedDep)); + assertTrue(compileFiltered.contains(systemDep)); assertTrue(compileFiltered.contains(nullScopeDep)); - // "provided" matches only provided-scoped - List providedFiltered = - BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "provided"); - assertEquals(1, providedFiltered.size()); - assertTrue(providedFiltered.contains(providedDep)); + // "runtime" scope expands to compile + runtime + null-scoped + List runtimeFiltered = + BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "runtime"); + assertEquals(3, runtimeFiltered.size()); + assertTrue(runtimeFiltered.contains(compileDep)); + assertTrue(runtimeFiltered.contains(runtimeDep)); + assertTrue(runtimeFiltered.contains(nullScopeDep)); - // "test" matches only test-scoped + // "test" scope expands to all scopes List testFiltered = BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "test"); - assertEquals(1, testFiltered.size()); - assertTrue(testFiltered.contains(testDep)); + assertEquals(6, testFiltered.size()); + + // "test-only" scope matches only test-scoped dependencies + List testOnlyFiltered = + BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "test-only"); + assertEquals(1, testOnlyFiltered.size()); + assertTrue(testOnlyFiltered.contains(testDep)); } /**