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:
+ *
+ * - {@code "compile"} matches compile, provided, system, and null-scoped
+ * (Maven default) dependencies
+ * - {@code "runtime"} matches compile, runtime, and null-scoped dependencies
+ * - {@code "test"} matches all scopes
+ * - {@code "test-only"} matches only test-scoped dependencies
+ *
+ * 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));
}
/**