From f2d1682005072498d77fd241816b44bcec603eda Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 7 Sep 2026 21:37:31 +0200 Subject: [PATCH] Fix filterByScope to expand lifecycle scopes for build ordering The concurrent builder's filterByScope used exact string matching between the lifecycle dependency scope (e.g. "compile") and the declared artifact scope. This meant a provided-scope reactor dependency was not ordered before the consumer's compile phase, since "compile" != "provided". Expand the lifecycle scope to match all artifact scopes that contribute to it: - "compile" -> compile, provided, system (+ null defaults to compile) - "runtime" -> compile, runtime (+ null) - "test" -> all scopes - "test-only" -> test only This ensures provided/system-scope reactor projects are built before downstream modules that need them for compilation. --- .../concurrent/BuildPlanExecutor.java | 40 ++++++++++++++----- .../concurrent/BuildPlanCreatorTest.java | 40 +++++++++++++------ 2 files changed, 57 insertions(+), 23 deletions(-) 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 55c10f8fa4e5..553d616b8cba 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 @@ -702,18 +702,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( @@ -721,14 +726,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 38c7912f9fa2..e21c446edfb1 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 @@ -239,40 +239,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)); } /**