Skip to content

Commit 2dfab86

Browse files
authored
Fix filterByScope to expand lifecycle scopes for build ordering (#13070)
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.
1 parent 321a9c8 commit 2dfab86

2 files changed

Lines changed: 57 additions & 23 deletions

File tree

impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -702,33 +702,53 @@ private void applyAfterLinks(MojoDescriptor mojoDescriptor, MavenProject project
702702
* all upstream projects are returned. Otherwise, only projects that the given
703703
* project depends on with a matching scope are included.
704704
* <p>
705-
* Matching is exact on the dependency's declared scope string (e.g. "compile",
706-
* "provided", "test"). Maven's default dependency scope is "compile" (when no
707-
* scope is declared), so a null scope in the model is treated as "compile" for
708-
* matching purposes. Note that this does <em>not</em> perform path-scope
709-
* resolution — for example, filtering by "compile" will not include
710-
* "provided"-scoped dependencies even though they contribute to
711-
* {@code PathScope.MAIN_COMPILE}. This keeps the filter simple and predictable;
712-
* broader scope-aware filtering can be added in a follow-up if needed.
705+
* The scope parameter is a lifecycle dependency scope (as declared in
706+
* {@link org.apache.maven.api.DependencyScope}). It is expanded to match all
707+
* artifact scopes that contribute to that dependency scope for build ordering:
708+
* <ul>
709+
* <li>{@code "compile"} matches compile, provided, system, and null-scoped
710+
* (Maven default) dependencies</li>
711+
* <li>{@code "runtime"} matches compile, runtime, and null-scoped dependencies</li>
712+
* <li>{@code "test"} matches all scopes</li>
713+
* <li>{@code "test-only"} matches only test-scoped dependencies</li>
714+
* </ul>
715+
* This ensures that reactor dependencies contributing to a given classpath are
716+
* properly ordered in the build plan (e.g. a provided-scope reactor dependency
717+
* is built before the consumer's compile phase).
713718
*
714719
* @param project the project whose dependencies to check
715720
* @param upstreamProjects the list of upstream reactor projects
716-
* @param scope the dependency scope to filter by, or null/empty for all
721+
* @param scope the lifecycle dependency scope to filter by, or null/empty for all
717722
* @return the filtered list of upstream projects
718723
*/
719724
static List<MavenProject> filterByScope(
720725
MavenProject project, List<MavenProject> upstreamProjects, String scope) {
721726
if (scope == null || scope.isEmpty()) {
722727
return upstreamProjects;
723728
}
729+
Set<String> matchingScopes = expandScope(scope);
724730
return upstreamProjects.stream()
725731
.filter(dep -> project.getDependencies().stream()
726732
.anyMatch(d -> dep.getGroupId().equals(d.getGroupId())
727733
&& dep.getArtifactId().equals(d.getArtifactId())
728-
&& scope.equals(d.getScope() != null ? d.getScope() : "compile")))
734+
&& matchingScopes.contains(d.getScope() != null ? d.getScope() : "compile")))
729735
.collect(Collectors.toList());
730736
}
731737

738+
/**
739+
* Expands a lifecycle dependency scope to the set of artifact scopes that
740+
* contribute to it for build ordering purposes.
741+
*/
742+
private static Set<String> expandScope(String scope) {
743+
return switch (scope) {
744+
case "compile" -> Set.of("compile", "provided", "system");
745+
case "runtime" -> Set.of("compile", "runtime");
746+
case "test" -> Set.of("compile", "provided", "system", "runtime", "test");
747+
case "test-only" -> Set.of("test");
748+
default -> Set.of(scope);
749+
};
750+
}
751+
732752
protected BuildPlan computeForkPlan(BuildStep step, MojoExecution execution, BuildPlan buildPlan) {
733753
MojoDescriptor mojoDescriptor = execution.getMojoDescriptor();
734754
PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor();

impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -239,40 +239,54 @@ void testFilterByScopeNullReturnsAll() {
239239
}
240240

241241
/**
242-
* Tests that {@code filterByScope} filters upstream projects by exact scope match,
243-
* treating null-scoped dependencies as "compile" (Maven default).
242+
* Tests that {@code filterByScope} expands scopes for build ordering.
243+
* "compile" includes compile, provided, system, and null-scoped (Maven default) dependencies
244+
* so that provided/system-scope reactor dependencies are properly ordered.
244245
*/
245246
@Test
246247
void testFilterByScopeMatchesExact() {
247248
MavenProject compileDep = createProjectWithId("g", "compile-dep");
248249
MavenProject providedDep = createProjectWithId("g", "provided-dep");
250+
MavenProject systemDep = createProjectWithId("g", "system-dep");
251+
MavenProject runtimeDep = createProjectWithId("g", "runtime-dep");
249252
MavenProject testDep = createProjectWithId("g", "test-dep");
250253
MavenProject nullScopeDep = createProjectWithId("g", "null-scope-dep");
251-
List<MavenProject> upstream = List.of(compileDep, providedDep, testDep, nullScopeDep);
254+
List<MavenProject> upstream = List.of(compileDep, providedDep, systemDep, runtimeDep, testDep, nullScopeDep);
252255

253256
MavenProject consumer = new MavenProject();
254257
consumer.getDependencies().add(createDependency("g", "compile-dep", "compile"));
255258
consumer.getDependencies().add(createDependency("g", "provided-dep", "provided"));
259+
consumer.getDependencies().add(createDependency("g", "system-dep", "system"));
260+
consumer.getDependencies().add(createDependency("g", "runtime-dep", "runtime"));
256261
consumer.getDependencies().add(createDependency("g", "test-dep", "test"));
257262
consumer.getDependencies().add(createDependency("g", "null-scope-dep", null));
258263

259-
// "compile" matches explicit compile + null-scoped (Maven default is compile)
264+
// "compile" scope expands to compile + provided + system + null-scoped (Maven default)
260265
List<MavenProject> compileFiltered =
261266
BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "compile");
262-
assertEquals(2, compileFiltered.size());
267+
assertEquals(4, compileFiltered.size());
263268
assertTrue(compileFiltered.contains(compileDep));
269+
assertTrue(compileFiltered.contains(providedDep));
270+
assertTrue(compileFiltered.contains(systemDep));
264271
assertTrue(compileFiltered.contains(nullScopeDep));
265272

266-
// "provided" matches only provided-scoped
267-
List<MavenProject> providedFiltered =
268-
BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "provided");
269-
assertEquals(1, providedFiltered.size());
270-
assertTrue(providedFiltered.contains(providedDep));
273+
// "runtime" scope expands to compile + runtime + null-scoped
274+
List<MavenProject> runtimeFiltered =
275+
BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "runtime");
276+
assertEquals(3, runtimeFiltered.size());
277+
assertTrue(runtimeFiltered.contains(compileDep));
278+
assertTrue(runtimeFiltered.contains(runtimeDep));
279+
assertTrue(runtimeFiltered.contains(nullScopeDep));
271280

272-
// "test" matches only test-scoped
281+
// "test" scope expands to all scopes
273282
List<MavenProject> testFiltered = BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "test");
274-
assertEquals(1, testFiltered.size());
275-
assertTrue(testFiltered.contains(testDep));
283+
assertEquals(6, testFiltered.size());
284+
285+
// "test-only" scope matches only test-scoped dependencies
286+
List<MavenProject> testOnlyFiltered =
287+
BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "test-only");
288+
assertEquals(1, testOnlyFiltered.size());
289+
assertTrue(testOnlyFiltered.contains(testDep));
276290
}
277291

278292
/**

0 commit comments

Comments
 (0)