From d0c9a6b18a85400a0123fa1a7259f99781571113 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 29 Feb 2024 08:00:22 +0100 Subject: [PATCH 1/2] [MNG-5668] [MNG-8052] New Lifecycle API with dynamic phases --- .../java/org/apache/maven/api/Lifecycle.java | 113 ++++++- .../maven/api/plugin/annotations/After.java | 66 ++++ .../maven/api/services/LifecycleRegistry.java | 3 + api/maven-api-plugin/pom.xml | 2 +- .../src/main/mdo/lifecycle.mdo | 67 +++- .../internal/impl/standalone/ApiRunner.java | 5 + .../EmptyLifecycleBindingsInjector.java | 10 + .../impl/DefaultLifecycleRegistry.java | 300 ++++++++++++++---- .../maven/internal/impl/Lifecycles.java | 124 +++++++- .../maven/lifecycle/DefaultLifecycles.java | 2 +- .../org/apache/maven/lifecycle/Lifecycle.java | 9 +- .../DefaultLifecycleMappingDelegate.java | 17 +- .../lifecycle/internal/PhaseComparator.java | 74 +++++ .../internal/PhaseExecutionPoint.java | 50 +++ .../maven/lifecycle/internal/PhaseId.java | 142 +++++++++ .../lifecycle/internal/PhaseRecorder.java | 9 +- .../lifecycle/DefaultLifecyclesTest.java | 2 +- .../lifecycle/LifecycleExecutorTest.java | 2 + .../EmptyLifecycleBindingsInjector.java | 10 + maven-plugin-api/pom.xml | 2 +- src/mdo/reader-stax.vm | 5 + src/mdo/writer-stax.vm | 5 + src/mdo/writer.vm | 5 + 23 files changed, 932 insertions(+), 92 deletions(-) create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/After.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java index 13d9b6d12bd..8a1d8102a2e 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.apache.maven.api.annotations.Experimental; import org.apache.maven.api.annotations.Immutable; @@ -35,14 +36,21 @@ @Immutable public interface Lifecycle extends ExtensibleEnum { + // ========================= + // Maven defined lifecycles + // ========================= String CLEAN = "clean"; - String DEFAULT = "default"; - String SITE = "site"; - String WRAPPER = "wrapper"; + // ====================== + // Phase qualifiers + // ====================== + String BEFORE = "before:"; + String AFTER = "after:"; + String AT = "at:"; + /** * Name or identifier of this lifecycle. * @@ -56,6 +64,18 @@ public interface Lifecycle extends ExtensibleEnum { */ Collection phases(); + /** + * Stream of phases containing all child phases recursively. + */ + default Stream allPhases() { + return phases().stream().flatMap(Phase::allPhases); + } + + /** + * Collection of aliases. + */ + Collection aliases(); + /** * Pre-ordered list of phases. * If not provided, a default order will be computed. @@ -68,8 +88,95 @@ default Optional> orderedPhases() { * A phase in the lifecycle. */ interface Phase { + + // ====================== + // Maven defined phases + // ====================== + String BUILD = "build"; + String INITIALIZE = "initialize"; + String VALIDATE = "validate"; + String SOURCES = "sources"; + String RESOURCES = "resources"; + String COMPILE = "compile"; + String READY = "ready"; + String PACKAGE = "package"; + String VERIFY = "verify"; + String UNIT_TEST = "unit-test"; + String TEST_SOURCES = "test-sources"; + String TEST_RESOURCES = "test-resources"; + String TEST_COMPILE = "test-compile"; + String TEST = "test"; + String INTEGRATION_TEST = "integration-test"; + String INSTALL = "install"; + String DEPLOY = "deploy"; + String CLEAN = "clean"; + String name(); List plugins(); + + Collection links(); + + List phases(); + + Stream allPhases(); + } + + /** + * A phase alias, mostly used to support the Maven 3 phases which are mapped + * to dynamic phases in Maven 4. + */ + interface Alias { + String v3Phase(); + + String v4Phase(); + } + + /** + * A link from a phase to another phase, consisting of a type which can be + * {@link Kind#BEFORE} or {@link Kind#AFTER}, and a {@link Pointer} to + * another phase. + */ + interface Link { + enum Kind { + BEFORE, + AFTER + } + + Kind kind(); + + Pointer pointer(); + } + + interface Pointer { + enum Type { + PROJECT, + DEPENDENCIES, + CHILDREN + } + + String phase(); + + Type type(); + } + + interface PhasePointer extends Pointer { + default Type type() { + return Type.PROJECT; + } + } + + interface DependenciesPointer extends Pointer { + String scope(); // default: all + + default Type type() { + return Type.DEPENDENCIES; + } + } + + interface ChildrenPointer extends Pointer { + default Type type() { + return Type.CHILDREN; + } } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/After.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/After.java new file mode 100644 index 00000000000..697b9d48020 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/After.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.plugin.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.maven.api.annotations.Experimental; + +/** + * Specifies that the mojo should be run after the specific phase. + * + * @since 4.0.0 + */ +@Experimental +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface After { + + /** + * Type of pointer. + * @see org.apache.maven.api.Lifecycle.Pointer.Type + */ + enum Type { + PROJECT, + DEPENDENCIES, + CHILDREN + } + + /** + * The phase name. + */ + String phase(); + + /** + * The type of this pointer. + */ + Type type(); + + /** + * The scope for dependencies, only if {@code type() == Type.Dependencies}. + */ + String scope() default ""; +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/LifecycleRegistry.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/LifecycleRegistry.java index 05970e76fce..55efc16ebcd 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/LifecycleRegistry.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/LifecycleRegistry.java @@ -18,6 +18,7 @@ */ package org.apache.maven.api.services; +import java.util.List; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -28,4 +29,6 @@ public interface LifecycleRegistry extends ExtensibleEnumRegistry, It default Stream stream() { return StreamSupport.stream(spliterator(), false); } + + List computePhases(Lifecycle lifecycle); } diff --git a/api/maven-api-plugin/pom.xml b/api/maven-api-plugin/pom.xml index 733c1158e2e..ff47e3d4f56 100644 --- a/api/maven-api-plugin/pom.xml +++ b/api/maven-api-plugin/pom.xml @@ -89,7 +89,7 @@ under the License. generate-sources ${project.basedir}/../../src/mdo - 1.0.0 + 2.0.0 src/main/mdo/lifecycle.mdo diff --git a/api/maven-api-plugin/src/main/mdo/lifecycle.mdo b/api/maven-api-plugin/src/main/mdo/lifecycle.mdo index f469eea4400..8196e3aa505 100644 --- a/api/maven-api-plugin/src/main/mdo/lifecycle.mdo +++ b/api/maven-api-plugin/src/main/mdo/lifecycle.mdo @@ -30,12 +30,12 @@ under the License. LifecycleConfiguration - 1.0.0 + 1.0.0+ Root element of the {@code lifecycle.xml} file. lifecycles - 1.0.0 + 1.0.0+ Lifecycle * @@ -45,19 +45,19 @@ under the License. Lifecycle - 1.0.0 + 1.0.0+ A custom lifecycle mapping definition. id true - 1.0.0 + 1.0.0+ String The ID of this lifecycle, for identification in the mojo descriptor. phases - 1.0.0 + 1.0.0+ The phase mappings for this lifecycle. Phase @@ -68,19 +68,35 @@ under the License. Phase - 1.0.0 + 1.0.0+ A phase mapping definition. id true - 1.0.0 + 1.0.0+ String - The ID of this phase, e.g., <code>generate-sources</code>. + The ID of this phase, e.g., {@code generate-sources}. + + + executionPoint + false + 2.0.0+ + String + + + + + priority + false + 2.0.0+ + int + 0 + If specified, identifies a within phase prioritization of executions. executions - 1.0.0 + 1.0.0+ The goals to execute within the phase. Execution @@ -89,26 +105,51 @@ under the License. configuration - 1.0.0 + 1.0.0+ DOM Configuration to pass to all goals run in this phase. + + + 2.0.0+ + + + Execution - 1.0.0 + 1.0.0+ A set of goals to execute. configuration - 1.0.0 + 1.0.0+ DOM Configuration to pass to the goals. goals - 1.0.0 + 1.0.0+ The goals to execute. String diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java index 1899df2f3b6..e255312c9bc 100644 --- a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java @@ -363,6 +363,11 @@ public Iterator iterator() { public Optional lookup(String id) { return Optional.empty(); } + + @Override + public List computePhases(Lifecycle lifecycle) { + return List.of(); + } }; } diff --git a/maven-compat/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java b/maven-compat/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java index 513bb0236ba..970b74a7501 100644 --- a/maven-compat/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java +++ b/maven-compat/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java @@ -62,6 +62,11 @@ public Iterator iterator() { public Optional lookup(String id) { return Optional.empty(); } + + @Override + public List computePhases(Lifecycle lifecycle) { + return List.of(); + } }; private static final PackagingRegistry emptyPackagingRegistry = new PackagingRegistry() { @@ -140,6 +145,11 @@ public Iterator iterator() { protected LifecycleRegistry getDelegate() { return lifecycleRegistry; } + + @Override + public List computePhases(Lifecycle lifecycle) { + return List.of(); + } } static class WrapperPackagingRegistry implements PackagingRegistry { diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java index acd8fd74c2f..bde84987789 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java @@ -24,6 +24,7 @@ import javax.inject.Singleton; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -47,8 +48,29 @@ import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; -import static java.util.Arrays.asList; -import static java.util.Collections.singleton; +import static org.apache.maven.api.Lifecycle.AFTER; +import static org.apache.maven.api.Lifecycle.AT; +import static org.apache.maven.api.Lifecycle.BEFORE; +import static org.apache.maven.api.Lifecycle.Phase.BUILD; +import static org.apache.maven.api.Lifecycle.Phase.COMPILE; +import static org.apache.maven.api.Lifecycle.Phase.DEPLOY; +import static org.apache.maven.api.Lifecycle.Phase.INITIALIZE; +import static org.apache.maven.api.Lifecycle.Phase.INSTALL; +import static org.apache.maven.api.Lifecycle.Phase.INTEGRATION_TEST; +import static org.apache.maven.api.Lifecycle.Phase.PACKAGE; +import static org.apache.maven.api.Lifecycle.Phase.READY; +import static org.apache.maven.api.Lifecycle.Phase.RESOURCES; +import static org.apache.maven.api.Lifecycle.Phase.SOURCES; +import static org.apache.maven.api.Lifecycle.Phase.TEST; +import static org.apache.maven.api.Lifecycle.Phase.TEST_COMPILE; +import static org.apache.maven.api.Lifecycle.Phase.TEST_RESOURCES; +import static org.apache.maven.api.Lifecycle.Phase.TEST_SOURCES; +import static org.apache.maven.api.Lifecycle.Phase.UNIT_TEST; +import static org.apache.maven.api.Lifecycle.Phase.VALIDATE; +import static org.apache.maven.api.Lifecycle.Phase.VERIFY; +import static org.apache.maven.internal.impl.Lifecycles.after; +import static org.apache.maven.internal.impl.Lifecycles.alias; +import static org.apache.maven.internal.impl.Lifecycles.dependencies; import static org.apache.maven.internal.impl.Lifecycles.phase; import static org.apache.maven.internal.impl.Lifecycles.plugin; @@ -59,6 +81,8 @@ @Singleton public class DefaultLifecycleRegistry implements LifecycleRegistry { + private static final String MAVEN_PLUGINS = "org.apache.maven.plugins:"; + private final List providers; public DefaultLifecycleRegistry() { @@ -73,7 +97,7 @@ public DefaultLifecycleRegistry(List providers) { // validate lifecycle for (Lifecycle lifecycle : this) { Set set = new HashSet<>(); - lifecycle.phases().forEach(phase -> { + lifecycle.allPhases().forEach(phase -> { if (!set.add(phase.name())) { throw new IllegalArgumentException( "Found duplicated phase '" + phase.name() + "' in '" + lifecycle.id() + "' lifecycle"); @@ -97,6 +121,75 @@ public Optional lookup(String id) { return stream().filter(lf -> Objects.equals(id, lf.id())).findAny(); } + public List computePhases(Lifecycle lifecycle) { + Graph graph = new Graph(); + lifecycle.phases().forEach(phase -> addPhase(graph, null, null, phase)); + lifecycle.aliases().forEach(alias -> { + String n = alias.v3Phase(); + String a = alias.v4Phase(); + Graph.Vertex v = graph.addVertex(n); + if (a.startsWith(BEFORE)) { + String u = a.substring(BEFORE.length()); + graph.addEdge(graph.addVertex("$" + u), v); + graph.addEdge(v, graph.addVertex("$$" + u)); + } else if (a.startsWith(AFTER)) { + String u = a.substring(AFTER.length()); + graph.addEdge(graph.addVertex(u), v); + graph.addEdge(v, graph.addVertex("$$$" + u)); + } else { + String u = a.startsWith(AT) ? a.substring(AT.length()) : a; + graph.addEdge(graph.addVertex("$$" + u), v); + graph.addEdge(v, graph.addVertex(u)); + } + }); + List allPhases = graph.visitAll(); + Collections.reverse(allPhases); + List computed = + allPhases.stream().filter(s -> !s.startsWith("$")).collect(Collectors.toList()); + List given = lifecycle.orderedPhases().orElse(null); + if (given != null) { + if (given.size() != computed.size()) { + Set s1 = + given.stream().filter(s -> !computed.contains(s)).collect(Collectors.toSet()); + Set s2 = + computed.stream().filter(s -> !given.contains(s)).collect(Collectors.toSet()); + throw new IllegalStateException( + "List of phases differ in size: expected " + computed.size() + ", but received " + given.size() + + (s1.isEmpty() ? "" : ", missing " + s1) + + (s2.isEmpty() ? "" : ", unexpected " + s2)); + } + return given; + } + return computed; + } + + private static void addPhase( + Graph graph, Graph.Vertex before, Graph.Vertex after, org.apache.maven.api.Lifecycle.Phase phase) { + Graph.Vertex ep0 = graph.addVertex("$" + phase.name()); + Graph.Vertex ep1 = graph.addVertex("$$" + phase.name()); + Graph.Vertex ep2 = graph.addVertex(phase.name()); + Graph.Vertex ep3 = graph.addVertex("$$$" + phase.name()); + graph.addEdge(ep0, ep1); + graph.addEdge(ep1, ep2); + graph.addEdge(ep2, ep3); + if (before != null) { + graph.addEdge(before, ep0); + } + if (after != null) { + graph.addEdge(ep3, after); + } + phase.links().forEach(link -> { + if (link.pointer().type() == Lifecycle.Pointer.Type.PROJECT) { + if (link.kind() == Lifecycle.Link.Kind.AFTER) { + graph.addEdge(graph.addVertex(link.pointer().phase()), ep0); + } else { + graph.addEdge(ep3, graph.addVertex("$" + link.pointer().phase())); + } + } + }); + phase.phases().forEach(child -> addPhase(graph, ep1, ep2, child)); + } + @Named @Singleton public static class LifecycleWrapperProvider implements LifecycleProvider { @@ -140,6 +233,17 @@ public String name() { return name; } + @Override + public List phases() { + return List.of(); + } + + @Override + public Stream allPhases() { + return Stream.concat( + Stream.of(this), phases().stream().flatMap(Lifecycle.Phase::allPhases)); + } + @Override public List plugins() { Map lfPhases = lifecycle.getDefaultLifecyclePhases(); @@ -151,16 +255,26 @@ public List plugins() { } return List.of(); } + + @Override + public Collection links() { + return List.of(); + } }) .toList(); } + + @Override + public Collection aliases() { + return Collections.emptyList(); + } }; } } static class WrappedLifecycle extends org.apache.maven.lifecycle.Lifecycle { - WrappedLifecycle(Lifecycle lifecycle) { - super(lifecycle); + WrappedLifecycle(LifecycleRegistry registry, Lifecycle lifecycle) { + super(registry, lifecycle); } } @@ -178,7 +292,7 @@ abstract static class BaseLifecycleProvider implements Provider phases() { - return asList( - phase("pre-clean"), - phase( - "clean", - plugin( - "org.apache.maven.plugins:maven-clean-plugin:" + MAVEN_CLEAN_PLUGIN_VERSION - + ":clean", - "clean")), - phase("post-clean")); + return List.of(phase( + Phase.CLEAN, + plugin( + MAVEN_PLUGINS + "maven-clean-plugin:" + MAVEN_CLEAN_PLUGIN_VERSION + ":clean", + Phase.CLEAN))); + } + + @Override + public Collection aliases() { + return List.of(alias("pre-clean", BEFORE + Phase.CLEAN), alias("post-clean", AFTER + Phase.CLEAN)); } } @@ -248,36 +367,101 @@ public String id() { @Override public Collection phases() { - return asList( - phase("validate"), - phase("initialize"), - phase("generate-sources"), - phase("process-sources"), - phase("generate-resources"), - phase("process-resources"), - phase("compile"), - phase("process-classes"), - phase("generate-test-sources"), - phase("process-test-sources"), - phase("generate-test-resources"), - phase("process-test-resources"), - phase("test-compile"), - phase("process-test-classes"), - phase("test"), - phase("prepare-package"), - phase("package"), - phase("pre-integration-test"), - phase("integration-test"), - phase("post-integration-test"), - phase("verify"), - phase("install"), - phase("deploy")); + return List.of(phase( + "all", + phase(INITIALIZE, phase(VALIDATE)), + phase( + BUILD, + after(VALIDATE), + phase(SOURCES), + phase(RESOURCES), + phase(COMPILE, after(SOURCES), dependencies(COMPILE, READY)), + phase(READY, after(COMPILE), after(RESOURCES)), + phase(PACKAGE, after(READY), dependencies("runtime", PACKAGE))), + phase( + VERIFY, + after(VALIDATE), + phase( + UNIT_TEST, + phase(TEST_SOURCES), + phase(TEST_RESOURCES), + phase( + TEST_COMPILE, + after(TEST_SOURCES), + after(READY), + dependencies("test-only", READY)), + phase( + TEST, + after(TEST_COMPILE), + after(TEST_RESOURCES), + dependencies("test", READY))), + phase(INTEGRATION_TEST)), + phase(INSTALL, after(PACKAGE)), + phase(DEPLOY, after(PACKAGE)))); + } + + @Override + public Collection aliases() { + return List.of( + alias("generate-sources", AT + SOURCES), + alias("process-sources", AFTER + SOURCES), + alias("generate-resources", AT + RESOURCES), + alias("process-resources", AFTER + RESOURCES), + alias("process-classes", AFTER + COMPILE), + alias("generate-test-sources", AT + TEST_SOURCES), + alias("process-test-sources", AFTER + TEST_SOURCES), + alias("generate-test-resources", AT + TEST_RESOURCES), + alias("process-test-resources", AFTER + TEST_RESOURCES), + alias("process-test-classes", AFTER + TEST_COMPILE), + alias("prepare-package", BEFORE + PACKAGE), + alias("pre-integration-test", BEFORE + INTEGRATION_TEST), + alias("post-integration-test", AFTER + INTEGRATION_TEST)); + } + + @Override + public Optional> orderedPhases() { + return Optional.of(Arrays.asList( + VALIDATE, + INITIALIZE, + "generate-sources", + SOURCES, + "process-sources", + "generate-resources", + RESOURCES, + "process-resources", + COMPILE, + "process-classes", + READY, + "generate-test-sources", + TEST_SOURCES, + "process-test-sources", + "generate-test-resources", + TEST_RESOURCES, + "process-test-resources", + TEST_COMPILE, + "process-test-classes", + TEST, + UNIT_TEST, + "prepare-package", + PACKAGE, + BUILD, + "pre-integration-test", + INTEGRATION_TEST, + "post-integration-test", + VERIFY, + INSTALL, + DEPLOY, + "all")); } } static class SiteLifecycle implements Lifecycle { private static final String MAVEN_SITE_PLUGIN_VERSION = "3.12.1"; + private static final String MAVEN_SITE_PLUGIN = + MAVEN_PLUGINS + "maven-site-plugin:" + MAVEN_SITE_PLUGIN_VERSION + ":"; + private static final String PHASE_SITE = "site"; + private static final String PHASE_SITE_DEPLOY = "site-deploy"; @Override public String id() { @@ -286,26 +470,24 @@ public String id() { @Override public Collection phases() { - return asList( - phase("pre-site"), - phase( - "site", - plugin( - "org.apache.maven.plugins:maven-site-plugin:" + MAVEN_SITE_PLUGIN_VERSION + ":site", - "site")), - phase("post-site"), + return List.of( + phase(PHASE_SITE, plugin(MAVEN_SITE_PLUGIN + "site", PHASE_SITE)), phase( - "site-deploy", - plugin( - "org.apache.maven.plugins:maven-site-plugin:" + MAVEN_SITE_PLUGIN_VERSION - + ":deploy", - "site-deploy"))); + PHASE_SITE_DEPLOY, + after(PHASE_SITE), + plugin(MAVEN_SITE_PLUGIN + "deploy", PHASE_SITE_DEPLOY))); + } + + @Override + public Collection aliases() { + return List.of(alias("pre-site", BEFORE + PHASE_SITE), alias("post-site", AFTER + PHASE_SITE)); } } static class WrapperLifecycle implements Lifecycle { private static final String MAVEN_WRAPPER_PLUGIN_VERSION = "3.2.0"; + private static final String PHASE_WRAPPER = "wrapper"; @Override public String id() { @@ -314,12 +496,16 @@ public String id() { @Override public Collection phases() { - return singleton(phase( - "wrapper", + return List.of(phase( + PHASE_WRAPPER, plugin( - "org.apache.maven.plugins:maven-wrapper-plugin:" + MAVEN_WRAPPER_PLUGIN_VERSION - + ":wrapper", - "wrapper"))); + MAVEN_PLUGINS + "maven-wrapper-plugin:" + MAVEN_WRAPPER_PLUGIN_VERSION + ":wrapper", + PHASE_WRAPPER))); + } + + @Override + public Collection aliases() { + return List.of(); } } } diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/Lifecycles.java b/maven-core/src/main/java/org/apache/maven/internal/impl/Lifecycles.java index 42da23b290f..714194659b8 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/Lifecycles.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/Lifecycles.java @@ -18,21 +18,52 @@ */ package org.apache.maven.internal.impl; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import org.apache.maven.api.Lifecycle; import org.apache.maven.api.model.Plugin; import org.apache.maven.api.model.PluginExecution; +import static java.util.Arrays.asList; + public class Lifecycles { static Lifecycle.Phase phase(String name) { - return new DefaultPhase(name, Collections.emptyList(), Collections.emptyList()); + return new DefaultPhase(name, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + + static Lifecycle.Phase phase(String name, Lifecycle.Phase... phases) { + return new DefaultPhase(name, Collections.emptyList(), Collections.emptyList(), asList(phases)); + } + + static Lifecycle.Phase phase(String name, Lifecycle.Link link, Lifecycle.Phase... phases) { + return new DefaultPhase(name, Collections.emptyList(), Collections.singletonList(link), asList(phases)); } static Lifecycle.Phase phase(String name, Plugin plugin) { - return new DefaultPhase(name, Collections.singletonList(plugin), Collections.emptyList()); + return new DefaultPhase( + name, Collections.singletonList(plugin), Collections.emptyList(), Collections.emptyList()); + } + + static Lifecycle.Phase phase(String name, Lifecycle.Link link, Plugin plugin) { + return new DefaultPhase( + name, Collections.singletonList(plugin), Collections.singletonList(link), Collections.emptyList()); + } + + static Lifecycle.Phase phase(String name, Lifecycle.Link link1, Lifecycle.Link link2, Lifecycle.Phase... phases) { + return new DefaultPhase(name, Collections.emptyList(), asList(link1, link2), asList(phases)); + } + + static Lifecycle.Phase phase( + String name, Lifecycle.Link link1, Lifecycle.Link link2, Lifecycle.Link link3, Lifecycle.Phase... phases) { + return new DefaultPhase(name, Collections.emptyList(), asList(link1, link2, link3), asList(phases)); + } + + static Lifecycle.Phase phase(String name, Collection links, Lifecycle.Phase... phases) { + return new DefaultPhase(name, Collections.emptyList(), links, asList(phases)); } static Plugin plugin(String coord, String phase) { @@ -49,14 +80,66 @@ static Plugin plugin(String coord, String phase) { .build(); } + /** Indicates the phase is after the phases given in arguments */ + static Lifecycle.Link after(String b) { + return new Lifecycle.Link() { + @Override + public Kind kind() { + return Kind.AFTER; + } + + @Override + public Lifecycle.Pointer pointer() { + return new Lifecycle.PhasePointer() { + @Override + public String phase() { + return b; + } + }; + } + }; + } + + /** Indicates the phase is after the phases for the dependencies in the given scope */ + static Lifecycle.Link dependencies(String scope, String phase) { + return new Lifecycle.Link() { + @Override + public Kind kind() { + return Kind.AFTER; + } + + @Override + public Lifecycle.Pointer pointer() { + return new Lifecycle.DependenciesPointer() { + @Override + public String phase() { + return phase; + } + + @Override + public String scope() { + return scope; + } + }; + } + }; + } + + static Lifecycle.Alias alias(String v3Phase, String v4Phase) { + return new DefaultAlias(v3Phase, v4Phase); + } + static class DefaultPhase implements Lifecycle.Phase { private final String name; private final List plugins; + private final Collection links; private final List phases; - DefaultPhase(String name, List plugins, List phases) { + DefaultPhase( + String name, List plugins, Collection links, List phases) { this.name = name; this.plugins = plugins; + this.links = links; this.phases = phases; } @@ -69,5 +152,40 @@ public String name() { public List plugins() { return plugins; } + + @Override + public Collection links() { + return links; + } + + @Override + public List phases() { + return phases; + } + + @Override + public Stream allPhases() { + return Stream.concat(Stream.of(this), phases().stream().flatMap(Lifecycle.Phase::allPhases)); + } + } + + static class DefaultAlias implements Lifecycle.Alias { + private final String v3Phase; + private final String v4Phase; + + DefaultAlias(String v3Phase, String v4Phase) { + this.v3Phase = v3Phase; + this.v4Phase = v4Phase; + } + + @Override + public String v3Phase() { + return v3Phase; + } + + @Override + public String v4Phase() { + return v4Phase; + } } } diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/DefaultLifecycles.java b/maven-core/src/main/java/org/apache/maven/lifecycle/DefaultLifecycles.java index 91e1adbc645..e3536df7f92 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/DefaultLifecycles.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/DefaultLifecycles.java @@ -156,7 +156,7 @@ private Map lookupLifecycles() { // Lifecycles cannot be cached as extensions might add custom lifecycles later in the execution. try { return registry != null - ? registry.stream().collect(Collectors.toMap(lf -> lf.id(), lf -> new Lifecycle(lf))) + ? registry.stream().collect(Collectors.toMap(lf -> lf.id(), lf -> new Lifecycle(registry, lf))) : Map.of(); } catch (LookupException e) { throw new IllegalStateException("Unable to lookup lifecycles from the plexus container", e); diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/Lifecycle.java b/maven-core/src/main/java/org/apache/maven/lifecycle/Lifecycle.java index d953d9f669a..30c81946b04 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/Lifecycle.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/Lifecycle.java @@ -38,12 +38,11 @@ public Lifecycle(String id, List phases, Map def this.defaultPhases = defaultPhases; } - public Lifecycle(org.apache.maven.api.Lifecycle lifecycle) { + public Lifecycle( + org.apache.maven.api.services.LifecycleRegistry registry, org.apache.maven.api.Lifecycle lifecycle) { this.lifecycle = lifecycle; this.id = lifecycle.id(); - this.phases = lifecycle.phases().stream() - .map(org.apache.maven.api.Lifecycle.Phase::name) - .toList(); + this.phases = registry.computePhases(lifecycle); this.defaultPhases = getDefaultPhases(lifecycle); } @@ -77,7 +76,7 @@ public List getPhases() { static Map getDefaultPhases(org.apache.maven.api.Lifecycle lifecycle) { Map> goals = new HashMap<>(); - lifecycle.phases().forEach(phase -> phase.plugins() + lifecycle.allPhases().forEach(phase -> phase.plugins() .forEach(plugin -> plugin.getExecutions().forEach(exec -> exec.getGoals() .forEach(goal -> goals.computeIfAbsent(phase.name(), n -> new ArrayList<>()) .add(plugin.getGroupId() + ":" + plugin.getArtifactId() + ":" + plugin.getVersion() diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java index 6ce06531e57..def69162084 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleMappingDelegate.java @@ -70,7 +70,8 @@ public Map> calculateLifecycleMappings( * is interested in, i.e. all phases up to and including the specified phase. */ - Map>> mappings = new LinkedHashMap<>(); + Map>> mappings = + new TreeMap<>(new PhaseComparator(lifecycle.getPhases())); for (String phase : lifecycle.getPhases()) { Map> phaseBindings = new TreeMap<>(); @@ -94,7 +95,7 @@ public Map> calculateLifecycleMappings( // if the phase is specified then I don't have to go fetch the plugin yet and pull it down // to examine the phase it is associated to. if (execution.getPhase() != null) { - Map> phaseBindings = mappings.get(execution.getPhase()); + Map> phaseBindings = getPhaseBindings(mappings, execution.getPhase()); if (phaseBindings != null) { for (String goal : execution.getGoals()) { MojoExecution mojoExecution = new MojoExecution(plugin, goal, execution.getId()); @@ -109,7 +110,8 @@ public Map> calculateLifecycleMappings( MojoDescriptor mojoDescriptor = pluginManager.getMojoDescriptor( plugin, goal, project.getRemotePluginRepositories(), session.getRepositorySession()); - Map> phaseBindings = mappings.get(mojoDescriptor.getPhase()); + Map> phaseBindings = + getPhaseBindings(mappings, mojoDescriptor.getPhase()); if (phaseBindings != null) { MojoExecution mojoExecution = new MojoExecution(mojoDescriptor, execution.getId()); mojoExecution.setLifecyclePhase(mojoDescriptor.getPhase()); @@ -135,6 +137,15 @@ public Map> calculateLifecycleMappings( return lifecycleMappings; } + private Map> getPhaseBindings( + Map>> mappings, String phase) { + if (phase != null) { + PhaseId id = PhaseId.of(phase); + return mappings.get(id.phase()); + } + return null; + } + private void addMojoExecution( Map> phaseBindings, MojoExecution mojoExecution, int priority) { List mojoExecutions = phaseBindings.computeIfAbsent(priority, k -> new ArrayList<>()); diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java new file mode 100644 index 00000000000..db5c4aa75da --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseComparator.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal; + +import java.util.Comparator; +import java.util.List; + +/** + * Compares phases within the context of a specific lifecycle with secondary sorting based on the {@link PhaseId}. + */ +public class PhaseComparator implements Comparator { + /** + * The lifecycle phase ordering. + */ + private final List lifecyclePhases; + + /** + * Constructor. + * + * @param lifecyclePhases the lifecycle phase ordering. + */ + public PhaseComparator(List lifecyclePhases) { + this.lifecyclePhases = lifecyclePhases; + } + + @Override + public int compare(String o1, String o2) { + PhaseId p1 = PhaseId.of(o1); + PhaseId p2 = PhaseId.of(o2); + int i1 = lifecyclePhases.indexOf(p1.phase()); + int i2 = lifecyclePhases.indexOf(p2.phase()); + if (i1 == -1 && i2 == -1) { + // unknown phases, leave in existing order + return 0; + } + if (i1 == -1) { + // second one is known, so it comes first + return 1; + } + if (i2 == -1) { + // first one is known, so it comes first + return -1; + } + int rv = Integer.compare(i1, i2); + if (rv != 0) { + return rv; + } + // same phase, now compare execution points + i1 = p1.executionPoint().ordinal(); + i2 = p2.executionPoint().ordinal(); + rv = Integer.compare(i1, i2); + if (rv != 0) { + return rv; + } + // same execution point, now compare priorities (highest wins, so invert) + return -Integer.compare(p1.priority(), p2.priority()); + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java new file mode 100644 index 00000000000..78e8981c91f --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseExecutionPoint.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal; + +/** + * Represents where a dynamic phase should be executed within a static phase. + */ +public enum PhaseExecutionPoint { + /** + * Execution must occur before any executions of the phase proper. Failure of any {@code #BEFORE} dynamic phase + * execution will prevent the {@link #AT} phase but will not prevent any {@link #AFTER} dynamic phases. + */ + BEFORE("before:"), + /** + * Execution is the execution of the phase proper. Failure of any {@code #AT} dynamic phase execution will fail + * the phase. Any {@link #AFTER} phases will still be executed. + */ + AT(""), + /** + * Guaranteed execution dynamic phases on completion of the static phase. All {@code #AFTER} dynamic phases will + * be executed provided at least one {@link #BEFORE} or {@link #AT} dynamic phase has started execution. + */ + AFTER("after:"); + + private final String prefix; + + PhaseExecutionPoint(String prefix) { + this.prefix = prefix; + } + + public String prefix() { + return prefix; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java new file mode 100644 index 00000000000..b52a989b7c8 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseId.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal; + +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; + +/** + * Represents a parsed phase identifier. + */ +public class PhaseId { + /** + * Interned {@link PhaseId} instances. + */ + private static final Map INSTANCES = new WeakHashMap<>(); + + /** + * The execution point of this {@link PhaseId}. + */ + private final PhaseExecutionPoint executionPoint; + + /** + * The static phase that this dynamic phase belongs to. + */ + private final String phase; + + /** + * The priority of this dynamic phase within the static phase. + */ + private final int priority; + + /** + * Parses the phase identifier. + * + * @param phase the phase identifier. + * @return the {@link PhaseId}. + */ + public static synchronized PhaseId of(String phase) { + return INSTANCES.computeIfAbsent(phase, PhaseId::new); + } + + /** + * Constructor. + * + * @param phase the phase identifier string. + */ + private PhaseId(String phase) { + int phaseStart; + if (phase.startsWith(PhaseExecutionPoint.BEFORE.prefix())) { + executionPoint = PhaseExecutionPoint.BEFORE; + phaseStart = PhaseExecutionPoint.BEFORE.prefix().length(); + } else if (phase.startsWith(PhaseExecutionPoint.AFTER.prefix())) { + executionPoint = PhaseExecutionPoint.AFTER; + phaseStart = PhaseExecutionPoint.AFTER.prefix().length(); + } else { + executionPoint = PhaseExecutionPoint.AT; + phaseStart = 0; + } + int phaseEnd = phase.indexOf('['); + if (phaseEnd == -1) { + priority = 0; + this.phase = phase.substring(phaseStart); + } else { + int priorityEnd = phase.lastIndexOf(']'); + boolean hasPriority; + int priority; + if (priorityEnd < phaseEnd + 1) { + priority = 0; + hasPriority = false; + } else { + try { + priority = Integer.parseInt(phase.substring(phaseEnd + 1, priorityEnd)); + hasPriority = true; + } catch (NumberFormatException e) { + // priority must be an integer + priority = 0; + hasPriority = false; + } + } + if (hasPriority) { + this.phase = phase.substring(phaseStart, phaseEnd); + this.priority = priority; + } else { + this.phase = phase.substring(phaseStart); + this.priority = 0; + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o == null || getClass() != o.getClass()) { + return false; + } else { + PhaseId phaseId = (PhaseId) o; + return Objects.equals(executionPoint(), phaseId.executionPoint()) + && Objects.equals(phase(), phaseId.phase()) + && Objects.equals(priority(), phaseId.priority()); + } + } + + @Override + public int hashCode() { + return Objects.hash(executionPoint(), phase(), priority()); + } + + @Override + public String toString() { + return executionPoint().prefix() + phase() + (priority() != 0 ? "[" + priority() + ']' : ""); + } + + public PhaseExecutionPoint executionPoint() { + return executionPoint; + } + + public String phase() { + return phase; + } + + public int priority() { + return priority; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java index c037af5c4df..a5f8f468254 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/PhaseRecorder.java @@ -38,11 +38,12 @@ public void observeExecution(MojoExecution mojoExecution) { String lifecyclePhase = mojoExecution.getLifecyclePhase(); if (lifecyclePhase != null) { + PhaseId phaseId = PhaseId.of(lifecyclePhase); if (lastLifecyclePhase == null) { - lastLifecyclePhase = lifecyclePhase; - } else if (!lifecyclePhase.equals(lastLifecyclePhase)) { + lastLifecyclePhase = phaseId.phase(); + } else if (!phaseId.phase().equals(lastLifecyclePhase)) { project.addLifecyclePhase(lastLifecyclePhase); - lastLifecyclePhase = lifecyclePhase; + lastLifecyclePhase = phaseId.phase(); } } @@ -56,6 +57,6 @@ public boolean isDifferentPhase(MojoExecution nextMojoExecution) { if (lifecyclePhase == null) { return lastLifecyclePhase != null; } - return !lifecyclePhase.equals(lastLifecyclePhase); + return !PhaseId.of(lifecyclePhase).phase().equals(lastLifecyclePhase); } } diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java index ba12bcade0e..3e2cf8e8e88 100644 --- a/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java +++ b/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java @@ -59,7 +59,7 @@ void testDefaultLifecycles() { void testDefaultLifecycle() { final Lifecycle lifecycle = getLifeCycleById("default"); assertThat(lifecycle.getId(), is("default")); - assertThat(lifecycle.getPhases(), hasSize(23)); + assertThat(lifecycle.getPhases(), hasSize(31)); } @Test diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/LifecycleExecutorTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/LifecycleExecutorTest.java index 883530e702c..6673e0567b3 100644 --- a/maven-core/src/test/java/org/apache/maven/lifecycle/LifecycleExecutorTest.java +++ b/maven-core/src/test/java/org/apache/maven/lifecycle/LifecycleExecutorTest.java @@ -152,6 +152,7 @@ List getExecutions(MavenExecutionPlan mavenExecutionPlan) { } // We need to take in multiple lifecycles + @Test public void testCalculationOfBuildPlanTasksOfTheCleanLifecycleAndTheInstallLifecycle() throws Exception { File pom = getProject("project-with-additional-lifecycle-elements"); MavenSession session = createMavenSession(pom); @@ -195,6 +196,7 @@ public void testCalculationOfBuildPlanTasksOfTheCleanLifecycleAndTheInstallLifec } // We need to take in multiple lifecycles + @Test public void testCalculationOfBuildPlanWithMultipleExecutionsOfModello() throws Exception { File pom = getProject("project-with-multiple-executions"); MavenSession session = createMavenSession(pom); diff --git a/maven-core/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java b/maven-core/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java index 747f2b8c3f9..3a7bc1a2bf1 100644 --- a/maven-core/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java +++ b/maven-core/src/test/java/org/apache/maven/project/EmptyLifecycleBindingsInjector.java @@ -61,6 +61,11 @@ public Iterator iterator() { public Optional lookup(String id) { return Optional.empty(); } + + @Override + public List computePhases(Lifecycle lifecycle) { + return List.of(); + } }; private static final PackagingRegistry emptyPackagingRegistry = new PackagingRegistry() { @@ -139,6 +144,11 @@ public Iterator iterator() { protected LifecycleRegistry getDelegate() { return lifecycleRegistry; } + + @Override + public List computePhases(Lifecycle lifecycle) { + return List.of(); + } } static class WrapperPackagingRegistry implements PackagingRegistry { diff --git a/maven-plugin-api/pom.xml b/maven-plugin-api/pom.xml index 148f3dc375d..f3b63da0cb6 100644 --- a/maven-plugin-api/pom.xml +++ b/maven-plugin-api/pom.xml @@ -117,7 +117,7 @@ under the License. ../api/maven-api-plugin/src/main/mdo/lifecycle.mdo - 1.0.0 + 2.0.0 diff --git a/src/mdo/reader-stax.vm b/src/mdo/reader-stax.vm index 692bd2a8a64..319a450a6e2 100644 --- a/src/mdo/reader-stax.vm +++ b/src/mdo/reader-stax.vm @@ -551,6 +551,9 @@ public class ${className} { #foreach ( $field in $allFields ) #if ( $Helper.xmlFieldMetadata( $field ).attribute ) #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName ) + #if ( ! $fieldTagName ) + #set ( $fieldTagName = $field.name ) + #end #set ( $fieldCapName = $Helper.capitalise( $field.name ) ) } else if ("$fieldTagName".equals(name)) { #if ( $locationTracking ) @@ -562,6 +565,8 @@ public class ${className} { ${classLcapName}.${field.name}(interpolatedTrimmed(value, "$fieldTagName")); #elseif ( $field.type == "boolean" || $field.type == "Boolean" ) ${classLcapName}.${field.name}(getBooleanValue(interpolatedTrimmed(value, "$fieldTagName"), "$fieldTagName", parser, ${field.defaultValue})); + #elseif ( $field.type == "int" || $field.type == "Integer" ) + ${classLcapName}.${field.name}(getIntegerValue(interpolatedTrimmed(value, "$fieldTagName"), "$fieldTagName", parser, strict, ${field.defaultValue})); #else // TODO: type=${field.type} to=${field.to} multiplicity=${field.multiplicity} #end diff --git a/src/mdo/writer-stax.vm b/src/mdo/writer-stax.vm index dc30f61446e..e4c544357e3 100644 --- a/src/mdo/writer-stax.vm +++ b/src/mdo/writer-stax.vm @@ -226,6 +226,9 @@ public class ${className} { #foreach ( $field in $allFields ) #if ( $Helper.xmlFieldMetadata( $field ).attribute && ! $Helper.xmlFieldMetadata( $field ).format ) #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName ) + #if ( ! $fieldTagName ) + #set ( $fieldTagName = $field.name ) + #end #set ( $fieldCapName = $Helper.capitalise( $field.name ) ) #if ( $field.type == "String" ) writeAttr("$fieldTagName", ${classLcapName}.get${fieldCapName}(), serializer); @@ -236,6 +239,8 @@ public class ${className} { #else writeAttr("$fieldTagName", ${classLcapName}.is${fieldCapName}() ? "true" : null, serializer); #end + #elseif ( $field.type == "int" || $field.type == "Integer" ) + writeAttr("$fieldTagName", Integer.toString(${classLcapName}.get${fieldCapName}()), serializer); #else // TODO: type=${field.type} to=${field.to} multiplicity=${field.multiplicity} #end diff --git a/src/mdo/writer.vm b/src/mdo/writer.vm index 33a36899930..6c3d28b5a0e 100644 --- a/src/mdo/writer.vm +++ b/src/mdo/writer.vm @@ -152,6 +152,9 @@ public class ${className} { #foreach ( $field in $allFields ) #if ( $Helper.xmlFieldMetadata( $field ).attribute ) #set ( $fieldTagName = $Helper.xmlFieldMetadata( $field ).tagName ) + #if ( ! $fieldTagName ) + #set ( $fieldTagName = $field.name ) + #end #set ( $fieldCapName = $Helper.capitalise( $field.name ) ) #if ( $field.type == "String" ) writeAttr("$fieldTagName", ${classLcapName}.get${fieldCapName}(), serializer); @@ -162,6 +165,8 @@ public class ${className} { #else writeAttr("$fieldTagName", ${classLcapName}.is${fieldCapName}() ? "true" : null, serializer); #end + #elseif ( $field.type == "int" || $field.type == "Integer" ) + writeAttr("$fieldTagName", Integer.toString(${classLcapName}.get${fieldCapName}()), serializer); #else // TODO: type=${field.type} to=${field.to} multiplicity=${field.multiplicity} #end From a2f97e56fdf23cf315c1b77e432287c4de662cc4 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 29 Feb 2024 08:00:54 +0100 Subject: [PATCH 2/2] [MNG-8052] Concurrently lifecycle executor --- maven-core/pom.xml | 8 + .../apache/maven/execution/BuildFailure.java | 14 +- .../apache/maven/execution/BuildSuccess.java | 13 +- .../apache/maven/execution/BuildSummary.java | 36 +- .../CompoundProjectExecutionListener.java | 4 +- .../lifecycle/internal/MojoExecutor.java | 91 +- .../internal/concurrent/BuildPlan.java | 166 ++++ .../concurrent/BuildPlanExecutor.java | 939 ++++++++++++++++++ .../internal/concurrent/BuildPlanLogger.java | 173 ++++ .../internal/concurrent/BuildStep.java | 133 +++ .../ConcurrentLifecycleStarter.java | 191 ++++ .../concurrent/ConcurrentLogOutput.java | 81 ++ .../internal/concurrent/MojoExecutor.java | 59 ++ .../internal/concurrent/PhasingExecutor.java | 54 + .../internal/concurrent/PluginLifecycle.java | 94 ++ .../concurrent/BuildPlanCreatorTest.java | 144 +++ .../concurrent/PhasingExecutorTest.java | 45 + 17 files changed, 2198 insertions(+), 47 deletions(-) create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java create mode 100644 maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java create mode 100644 maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java diff --git a/maven-core/pom.xml b/maven-core/pom.xml index f79a0d40df1..39eca041458 100644 --- a/maven-core/pom.xml +++ b/maven-core/pom.xml @@ -91,6 +91,14 @@ under the License. org.apache.maven maven-api-impl + + org.apache.maven + maven-jline + + + org.apache.maven + maven-slf4j-provider + org.apache.maven.resolver maven-resolver-api diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java b/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java index cd7155076d5..2c43f1e943e 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java @@ -39,7 +39,19 @@ public class BuildFailure extends BuildSummary { * @param cause The cause of the build failure, may be {@code null}. */ public BuildFailure(MavenProject project, long time, Throwable cause) { - super(project, time); + this(project, time, time, cause); + } + + /** + * Creates a new build summary for the specified project. + * + * @param project The project being summarized, must not be {@code null}. + * @param execTime The exec time of the project in milliseconds. + * @param wallTime The wall time of the project in milliseconds. + * @param cause The cause of the build failure, may be {@code null}. + */ + public BuildFailure(MavenProject project, long execTime, long wallTime, Throwable cause) { + super(project, execTime, wallTime); this.cause = cause; } diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java b/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java index 0f0310d3cde..a2a4546b23b 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java @@ -33,6 +33,17 @@ public class BuildSuccess extends BuildSummary { * @param time The build time of the project in milliseconds. */ public BuildSuccess(MavenProject project, long time) { - super(project, time); + super(project, time, time); + } + + /** + * Creates a new build summary for the specified project. + * + * @param project The project being summarized, must not be {@code null}. + * @param wallTime The wall time of the project in milliseconds. + * @param execTime The exec time of the project in milliseconds. + */ + public BuildSuccess(MavenProject project, long wallTime, long execTime) { + super(project, wallTime, execTime); } } diff --git a/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java b/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java index 657afb31ebe..efc0da1d8be 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java +++ b/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java @@ -36,7 +36,12 @@ public abstract class BuildSummary { /** * The build time of the project in milliseconds. */ - private final long time; + private final long wallTime; + + /** + * The total amount of time spent for to run mojos in milliseconds. + */ + private final long execTime; /** * Creates a new build summary for the specified project. @@ -45,9 +50,21 @@ public abstract class BuildSummary { * @param time The build time of the project in milliseconds. */ protected BuildSummary(MavenProject project, long time) { + this(project, time, time); + } + + /** + * Creates a new build summary for the specified project. + * + * @param project The project being summarized, must not be {@code null}. + * @param execTime The exec time of the project in milliseconds. + * @param wallTime The wall time of the project in milliseconds. + */ + protected BuildSummary(MavenProject project, long execTime, long wallTime) { this.project = Objects.requireNonNull(project, "project cannot be null"); // TODO Validate for < 0? - this.time = time; + this.execTime = execTime; + this.wallTime = wallTime; } /** @@ -60,11 +77,20 @@ public MavenProject getProject() { } /** - * Gets the build time of the project in milliseconds. + * Gets the wall time of the project in milliseconds. * - * @return The build time of the project in milliseconds. + * @return The wall time of the project in milliseconds. */ public long getTime() { - return time; + return execTime; + } + + /** + * Gets the exec time of the project in milliseconds. + * + * @return The exec time of the project in milliseconds. + */ + public long getWallTime() { + return wallTime; } } diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java index 34cee12b956..33eb878aabf 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java @@ -24,10 +24,10 @@ import org.apache.maven.execution.ProjectExecutionListener; import org.apache.maven.lifecycle.LifecycleExecutionException; -class CompoundProjectExecutionListener implements ProjectExecutionListener { +public class CompoundProjectExecutionListener implements ProjectExecutionListener { private final Collection listeners; - CompoundProjectExecutionListener(Collection listeners) { + public CompoundProjectExecutionListener(Collection listeners) { this.listeners = listeners; // NB this is live injected collection } diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java index 5f442555bc8..86a9ce856eb 100644 --- a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java @@ -213,6 +213,13 @@ private void execute(MavenSession session, MojoExecution mojoExecution, Dependen doExecute(session, mojoExecution, dependencyContext); } + protected static class NoLock implements NoExceptionCloseable { + public NoLock() {} + + @Override + public void close() {} + } + /** * Aggregating mojo executions (possibly) modify all MavenProjects, including those that are currently in use * by concurrently running mojo executions. To prevent race conditions, an aggregating execution will block @@ -221,54 +228,45 @@ private void execute(MavenSession session, MojoExecution mojoExecution, Dependen * TODO: ideally, the builder should take care of the ordering in a smarter way * TODO: and concurrency issues fixed with MNG-7157 */ - private class ProjectLock implements AutoCloseable { + protected class ProjectLock implements NoExceptionCloseable { final Lock acquiredAggregatorLock; final OwnerReentrantLock acquiredProjectLock; ProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) { mojos.put(Thread.currentThread(), mojoDescriptor); - if (session.getRequest().getDegreeOfConcurrency() > 1) { - boolean aggregator = mojoDescriptor.isAggregator(); - acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock(); - acquiredProjectLock = getProjectLock(session); - if (!acquiredAggregatorLock.tryLock()) { - Thread owner = aggregatorLock.getOwner(); - MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null; - String str = ownerMojo != null ? " The " + ownerMojo.getId() : "An"; - String msg = str + " aggregator mojo is already being executed " - + "in this parallel build, those kind of mojos require exclusive access to " - + "reactor to prevent race conditions. This mojo execution will be blocked " - + "until the aggregator mojo is done."; - warn(msg); - acquiredAggregatorLock.lock(); - } - if (!acquiredProjectLock.tryLock()) { - Thread owner = acquiredProjectLock.getOwner(); - MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null; - String str = ownerMojo != null ? " The " + ownerMojo.getId() : "A"; - String msg = str + " mojo is already being executed " - + "on the project " + session.getCurrentProject().getGroupId() - + ":" + session.getCurrentProject().getArtifactId() + ". " - + "This mojo execution will be blocked " - + "until the mojo is done."; - warn(msg); - acquiredProjectLock.lock(); - } - } else { - acquiredAggregatorLock = null; - acquiredProjectLock = null; + boolean aggregator = mojoDescriptor.isAggregator(); + acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock(); + acquiredProjectLock = getProjectLock(session); + if (!acquiredAggregatorLock.tryLock()) { + Thread owner = aggregatorLock.getOwner(); + MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null; + String str = ownerMojo != null ? " The " + ownerMojo.getId() : "An"; + String msg = str + " aggregator mojo is already being executed " + + "in this parallel build, those kind of mojos require exclusive access to " + + "reactor to prevent race conditions. This mojo execution will be blocked " + + "until the aggregator mojo is done."; + warn(msg); + acquiredAggregatorLock.lock(); + } + if (!acquiredProjectLock.tryLock()) { + Thread owner = acquiredProjectLock.getOwner(); + MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null; + String str = ownerMojo != null ? " The " + ownerMojo.getId() : "A"; + String msg = str + " mojo is already being executed " + + "on the project " + session.getCurrentProject().getGroupId() + + ":" + session.getCurrentProject().getArtifactId() + ". " + + "This mojo execution will be blocked " + + "until the mojo is done."; + warn(msg); + acquiredProjectLock.lock(); } } @Override public void close() { // release the lock in the reverse order of the acquisition - if (acquiredProjectLock != null) { - acquiredProjectLock.unlock(); - } - if (acquiredAggregatorLock != null) { - acquiredAggregatorLock.unlock(); - } + acquiredProjectLock.unlock(); + acquiredAggregatorLock.unlock(); mojos.remove(Thread.currentThread()); } @@ -307,7 +305,7 @@ private void doExecute(MavenSession session, MojoExecution mojoExecution, Depend ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext); - try (ProjectLock lock = new ProjectLock(session, mojoDescriptor)) { + try (NoExceptionCloseable lock = getProjectLock(session, mojoDescriptor)) { doExecute2(session, mojoExecution); } finally { for (MavenProject forkedProject : forkedProjects) { @@ -316,6 +314,23 @@ private void doExecute(MavenSession session, MojoExecution mojoExecution, Depend } } + protected interface NoExceptionCloseable extends AutoCloseable { + @Override + void close(); + } + + protected NoExceptionCloseable getProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) { + if (useProjectLock(session)) { + return new ProjectLock(session, mojoDescriptor); + } else { + return new NoLock(); + } + } + + protected boolean useProjectLock(MavenSession session) { + return session.getRequest().getDegreeOfConcurrency() > 1; + } + private void doExecute2(MavenSession session, MojoExecution mojoExecution) throws LifecycleExecutionException { eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution); try { diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java new file mode 100644 index 00000000000..222a1ecd77e --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.project.MavenProject; + +public class BuildPlan { + + private final Map> plan = new LinkedHashMap<>(); + private final Map> projects; + private final Map aliases = new HashMap<>(); + private volatile Set duplicateIds; + private volatile List sortedNodes; + + BuildPlan() { + this.projects = null; + } + + public BuildPlan(Map> projects) { + this.projects = projects; + } + + public Map> getAllProjects() { + return projects; + } + + public Map aliases() { + return aliases; + } + + public Stream projects() { + return plan.keySet().stream(); + } + + public void addProject(MavenProject project, Map steps) { + plan.put(project, steps); + } + + public void addStep(MavenProject project, String name, BuildStep step) { + plan.get(project).put(name, step); + } + + public Stream allSteps() { + return plan.values().stream().flatMap(m -> m.values().stream()); + } + + public Stream steps(MavenProject project) { + return Optional.ofNullable(plan.get(project)) + .map(m -> m.values().stream()) + .orElse(Stream.empty()); + } + + public Optional step(MavenProject project, String name) { + return Optional.ofNullable(plan.get(project)).map(m -> m.get(name)); + } + + public BuildStep requiredStep(MavenProject project, String name) { + return step(project, name).get(); + } + + // add a follow-up plan to this one + public void then(BuildPlan step) { + step.plan.forEach((k, v) -> plan.merge(k, v, this::merge)); + aliases.putAll(step.aliases); + } + + private Map merge(Map org, Map add) { + // all new phases should be added after the existing ones + List lasts = + org.values().stream().filter(b -> b.successors.isEmpty()).collect(Collectors.toList()); + List firsts = + add.values().stream().filter(b -> b.predecessors.isEmpty()).collect(Collectors.toList()); + firsts.stream() + .filter(addNode -> !org.containsKey(addNode.name)) + .forEach(addNode -> lasts.forEach(orgNode -> addNode.executeAfter(orgNode))); + add.forEach((name, node) -> org.merge(name, node, this::merge)); + return org; + } + + private BuildStep merge(BuildStep node1, BuildStep node2) { + node1.predecessors.addAll(node2.predecessors); + node1.successors.addAll(node2.successors); + node2.mojos.forEach((k, v) -> node1.mojos.merge(k, v, this::mergeMojos)); + return node1; + } + + private Map mergeMojos(Map l1, Map l2) { + l2.forEach(l1::putIfAbsent); + return l1; + } + + // gather artifactIds which are not unique so that the respective thread names can be extended with the groupId + public Set duplicateIds() { + if (duplicateIds == null) { + synchronized (this) { + if (duplicateIds == null) { + duplicateIds = projects() + .map(MavenProject::getArtifactId) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) + .entrySet() + .stream() + .filter(p -> p.getValue() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + } + } + return duplicateIds; + } + + public List sortedNodes() { + if (sortedNodes == null) { + synchronized (this) { + if (sortedNodes == null) { + List sortedNodes = new ArrayList<>(); + Set visited = new HashSet<>(); + // Visit each unvisited node + allSteps().forEach(node -> visitNode(node, visited, sortedNodes)); + // Reverse the sorted nodes to get the correct order + Collections.reverse(sortedNodes); + this.sortedNodes = sortedNodes; + } + } + } + return sortedNodes; + } + + // Helper method to visit a node + private static void visitNode(BuildStep node, Set visited, List sortedNodes) { + if (visited.add(node)) { + // For each successor of the current node, visit unvisited successors + node.successors.forEach(successor -> visitNode(successor, visited, sortedNodes)); + sortedNodes.add(node); + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java new file mode 100644 index 00000000000..6f77b927815 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java @@ -0,0 +1,939 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.xml.stream.XMLStreamException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.api.Lifecycle; +import org.apache.maven.api.services.LifecycleRegistry; +import org.apache.maven.api.services.MavenException; +import org.apache.maven.api.xml.XmlNode; +import org.apache.maven.execution.BuildFailure; +import org.apache.maven.execution.BuildSuccess; +import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.execution.ProjectDependencyGraph; +import org.apache.maven.execution.ProjectExecutionEvent; +import org.apache.maven.execution.ProjectExecutionListener; +import org.apache.maven.internal.MultilineMessageHelper; +import org.apache.maven.internal.transformation.ConsumerPomArtifactTransformer; +import org.apache.maven.internal.xml.XmlNodeImpl; +import org.apache.maven.lifecycle.LifecycleExecutionException; +import org.apache.maven.lifecycle.LifecycleNotFoundException; +import org.apache.maven.lifecycle.LifecyclePhaseNotFoundException; +import org.apache.maven.lifecycle.MojoExecutionConfigurator; +import org.apache.maven.lifecycle.internal.BuildThreadFactory; +import org.apache.maven.lifecycle.internal.CompoundProjectExecutionListener; +import org.apache.maven.lifecycle.internal.DefaultLifecyclePluginAnalyzer; +import org.apache.maven.lifecycle.internal.ExecutionEventCatapult; +import org.apache.maven.lifecycle.internal.GoalTask; +import org.apache.maven.lifecycle.internal.LifecycleTask; +import org.apache.maven.lifecycle.internal.MojoDescriptorCreator; +import org.apache.maven.lifecycle.internal.MojoExecutor; +import org.apache.maven.lifecycle.internal.ReactorContext; +import org.apache.maven.lifecycle.internal.Task; +import org.apache.maven.lifecycle.internal.TaskSegment; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; +import org.apache.maven.plugin.MavenPluginManager; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoNotFoundException; +import org.apache.maven.plugin.PluginDescriptorParsingException; +import org.apache.maven.plugin.descriptor.MojoDescriptor; +import org.apache.maven.plugin.descriptor.Parameter; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.classworlds.realm.ClassRealm; +import org.eclipse.aether.repository.RemoteRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.maven.api.Lifecycle.AFTER; +import static org.apache.maven.api.Lifecycle.AT; +import static org.apache.maven.api.Lifecycle.BEFORE; +import static org.apache.maven.api.Lifecycle.Phase.PACKAGE; +import static org.apache.maven.api.Lifecycle.Phase.READY; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.CREATED; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.EXECUTED; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.FAILED; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLAN; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLANNING; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SCHEDULED; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SETUP; +import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.TEARDOWN; + +/** + * Builds the full lifecycle in weave-mode (phase by phase as opposed to project-by-project). + *

+ * This builder uses a number of threads equal to the minimum of the degree of concurrency (which is the thread count + * set with -T on the command-line) and the number of projects to build. As such, building a single project + * will always result in a sequential build, regardless of the thread count. + *

+ * NOTE: This class is not part of any public api and can be changed or deleted without prior notice. + * + * @since 3.0 + * Builds one or more lifecycles for a full module + * NOTE: This class is not part of any public api and can be changed or deleted without prior notice. + */ +@Named +public class BuildPlanExecutor { + + private static final Object GLOBAL = new Object(); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final MojoExecutor mojoExecutor; + private final ExecutionEventCatapult eventCatapult; + private final ProjectExecutionListener projectExecutionListener; + private final ConsumerPomArtifactTransformer consumerPomArtifactTransformer; + private final BuildPlanLogger buildPlanLogger; + private final Map mojoExecutionConfigurators; + private final MavenPluginManager mavenPluginManager; + private final MojoDescriptorCreator mojoDescriptorCreator; + private final LifecycleRegistry lifecycles; + + @Inject + @SuppressWarnings("checkstyle:ParameterNumber") + public BuildPlanExecutor( + @Named("concurrent") MojoExecutor mojoExecutor, + ExecutionEventCatapult eventCatapult, + List listeners, + ConsumerPomArtifactTransformer consumerPomArtifactTransformer, + BuildPlanLogger buildPlanLogger, + Map mojoExecutionConfigurators, + MavenPluginManager mavenPluginManager, + MojoDescriptorCreator mojoDescriptorCreator, + LifecycleRegistry lifecycles) { + this.mojoExecutor = mojoExecutor; + this.eventCatapult = eventCatapult; + this.projectExecutionListener = new CompoundProjectExecutionListener(listeners); + this.consumerPomArtifactTransformer = consumerPomArtifactTransformer; + this.buildPlanLogger = buildPlanLogger; + this.mojoExecutionConfigurators = mojoExecutionConfigurators; + this.mavenPluginManager = mavenPluginManager; + this.mojoDescriptorCreator = mojoDescriptorCreator; + this.lifecycles = lifecycles; + } + + public void execute(MavenSession session, ReactorContext reactorContext, List taskSegments) + throws ExecutionException, InterruptedException { + try (BuildContext ctx = new BuildContext(session, reactorContext, taskSegments)) { + ctx.execute(); + } + } + + class BuildContext implements AutoCloseable { + final MavenSession session; + final ReactorContext reactorContext; + final PhasingExecutor executor; + final ConcurrentLogOutput appender; + final Map clocks = new ConcurrentHashMap<>(); + final ReadWriteLock lock = new ReentrantReadWriteLock(); + final int threads; + BuildPlan plan; + + BuildContext(MavenSession session, ReactorContext reactorContext, List taskSegments) { + this.session = session; + this.reactorContext = reactorContext; + this.threads = Math.min( + session.getRequest().getDegreeOfConcurrency(), + session.getProjects().size()); + // Propagate the parallel flag to the root session + session.setParallel(threads > 1); + this.executor = new PhasingExecutor(Executors.newFixedThreadPool(threads, new BuildThreadFactory())); + this.appender = new ConcurrentLogOutput(); + + // build initial plan + this.plan = buildInitialPlan(taskSegments); + } + + BuildContext() { + this.session = null; + this.reactorContext = null; + this.threads = 1; + this.executor = null; + this.appender = null; + this.plan = null; + } + + public BuildPlan buildInitialPlan(List taskSegments) { + int nThreads = Math.min( + session.getRequest().getDegreeOfConcurrency(), + session.getProjects().size()); + boolean parallel = nThreads > 1; + // Propagate the parallel flag to the root session + session.setParallel(parallel); + + ProjectDependencyGraph dependencyGraph = session.getProjectDependencyGraph(); + MavenProject rootProject = session.getTopLevelProject(); + + Map> allProjects = new LinkedHashMap<>(); + dependencyGraph + .getSortedProjects() + .forEach(p -> allProjects.put(p, dependencyGraph.getUpstreamProjects(p, false))); + + BuildPlan plan = new BuildPlan(allProjects); + for (TaskSegment taskSegment : taskSegments) { + Map> projects = taskSegment.isAggregating() + ? Collections.singletonMap(rootProject, allProjects.get(rootProject)) + : allProjects; + + BuildPlan segment = calculateMojoExecutions(projects, taskSegment.getTasks()); + plan.then(segment); + } + + // Create plan, setup and teardown + for (MavenProject project : plan.getAllProjects().keySet()) { + BuildStep pplan = new BuildStep(PLAN, project, null); + pplan.status.set(PLANNING); // the plan step always need planning + BuildStep setup = new BuildStep(SETUP, project, null); + BuildStep teardown = new BuildStep(TEARDOWN, project, null); + setup.executeAfter(pplan); + plan.steps(project).forEach(step -> { + if (step.predecessors.isEmpty()) { + step.executeAfter(setup); + } else if (step.successors.isEmpty()) { + teardown.executeAfter(step); + } + }); + Stream.of(pplan, setup, teardown).forEach(step -> plan.addStep(project, step.name, step)); + } + + return plan; + } + + private void checkUnboundVersions(BuildPlan buildPlan) { + String defaulModelId = DefaultLifecyclePluginAnalyzer.DEFAULTLIFECYCLEBINDINGS_MODELID; + List unversionedPlugins = buildPlan + .allSteps() + .flatMap(step -> step.mojos.values().stream().flatMap(map -> map.values().stream())) + .map(MojoExecution::getPlugin) + .filter(p -> p.getLocation("version") != null + && p.getLocation("version").getSource() != null + && defaulModelId.equals( + p.getLocation("version").getSource().getModelId())) + .distinct() + .map(Plugin::getArtifactId) // managed by us, groupId is always o.a.m.plugins + .toList(); + if (!unversionedPlugins.isEmpty()) { + logger.warn("Version not locked for default bindings plugins " + unversionedPlugins + + ", you should define versions in pluginManagement section of your " + "pom.xml or parent"); + } + } + + private void checkThreadSafety(BuildPlan buildPlan) { + if (threads > 1) { + Set unsafeExecutions = buildPlan + .allSteps() + .flatMap(step -> step.mojos.values().stream().flatMap(map -> map.values().stream())) + .filter(execution -> !execution.getMojoDescriptor().isV4Api()) + .collect(Collectors.toSet()); + if (!unsafeExecutions.isEmpty()) { + for (String s : MultilineMessageHelper.format( + """ + Your build is requesting concurrent execution, but this project contains the \ + following plugin(s) that have goals not built with Maven 4 to support concurrent \ + execution. While this /may/ work fine, please look for plugin updates and/or \ + request plugins be made thread-safe. If reporting an issue, report it against the \ + plugin in question, not against Apache Maven.""")) { + logger.warn(s); + } + if (logger.isDebugEnabled()) { + Set unsafeGoals = unsafeExecutions.stream() + .map(MojoExecution::getMojoDescriptor) + .collect(Collectors.toSet()); + logger.warn("The following goals are not Maven 4 goals:"); + for (MojoDescriptor unsafeGoal : unsafeGoals) { + logger.warn(" " + unsafeGoal.getId()); + } + } else { + Set unsafePlugins = unsafeExecutions.stream() + .map(MojoExecution::getPlugin) + .collect(Collectors.toSet()); + logger.warn("The following plugins are not Maven 4 plugins:"); + for (Plugin unsafePlugin : unsafePlugins) { + logger.warn(" " + unsafePlugin.getId()); + } + logger.warn(""); + logger.warn("Enable verbose output (-X) to see precisely which goals are not marked as" + + " thread-safe."); + } + logger.warn(MultilineMessageHelper.separatorLine()); + } + } + } + + void execute() { + try { + plan(); + executePlan(); + executor.await(); + } catch (Exception e) { + session.getResult().addException(e); + } + } + + @Override + public void close() { + this.appender.close(); + this.executor.close(); + } + + private void executePlan() { + if (reactorContext.getReactorBuildStatus().isHalted()) { + return; + } + Clock global = clocks.computeIfAbsent(GLOBAL, p -> new Clock()); + global.start(); + lock.readLock().lock(); + try { + plan.sortedNodes().stream() + .filter(step -> step.status.get() == CREATED) + .filter(step -> step.predecessors.stream().allMatch(s -> s.status.get() == EXECUTED)) + .filter(step -> step.status.compareAndSet(CREATED, SCHEDULED)) + .forEach(step -> { + boolean nextIsPlanning = step.successors.stream().anyMatch(st -> PLAN.equals(st.name)); + executor.execute(() -> { + try (AutoCloseable ctx = appender.build(step.project)) { + executeStep(step); + if (nextIsPlanning) { + lock.writeLock().lock(); + try { + plan(); + } finally { + lock.writeLock().unlock(); + } + } + executePlan(); + } catch (Exception e) { + step.status.compareAndSet(SCHEDULED, FAILED); + global.stop(); + handleBuildError(reactorContext, session, step.project, e, global); + } + }); + }); + } finally { + lock.readLock().unlock(); + } + } + + private void executeStep(BuildStep step) throws IOException, LifecycleExecutionException { + Clock clock = getClock(step.project); + switch (step.name) { + case PLAN: + // Planning steps should be executed out of normal execution + throw new IllegalStateException(); + case SETUP: + consumerPomArtifactTransformer.injectTransformedArtifacts( + session.getRepositorySession(), step.project); + projectExecutionListener.beforeProjectExecution(new ProjectExecutionEvent(session, step.project)); + eventCatapult.fire(ExecutionEvent.Type.ProjectStarted, session, null); + break; + case TEARDOWN: + projectExecutionListener.afterProjectExecutionSuccess( + new ProjectExecutionEvent(session, step.project, Collections.emptyList())); + reactorContext + .getResult() + .addBuildSummary(new BuildSuccess(step.project, clock.wallTime(), clock.execTime())); + eventCatapult.fire(ExecutionEvent.Type.ProjectSucceeded, session, null); + break; + default: + List executions = step.executions().collect(Collectors.toList()); + if (!executions.isEmpty()) { + attachToThread(step.project); + session.setCurrentProject(step.project); + clock.start(); + executions.forEach(mojoExecution -> { + mojoExecutionConfigurator(mojoExecution).configure(step.project, mojoExecution, true); + finalizeMojoConfiguration(mojoExecution); + }); + mojoExecutor.execute(session, executions); + clock.stop(); + } + break; + } + step.status.compareAndSet(SCHEDULED, EXECUTED); + } + + private Clock getClock(Object key) { + return clocks.computeIfAbsent(key, p -> new Clock()); + } + + private void plan() { + lock.writeLock().lock(); + try { + Set planSteps = plan.allSteps() + .filter(st -> PLAN.equals(st.name)) + .filter(step -> step.predecessors.stream().allMatch(s -> s.status.get() == EXECUTED)) + .filter(step -> step.status.compareAndSet(PLANNING, SCHEDULED)) + .collect(Collectors.toSet()); + for (BuildStep step : planSteps) { + MavenProject project = step.project; + for (Plugin plugin : project.getBuild().getPlugins()) { + for (PluginExecution execution : plugin.getExecutions()) { + for (String goal : execution.getGoals()) { + MojoDescriptor mojoDescriptor = getMojoDescriptor(project, plugin, goal); + String phase = + execution.getPhase() != null ? execution.getPhase() : mojoDescriptor.getPhase(); + String tmpResolvedPhase = plan.aliases().getOrDefault(phase, phase); + String resolvedPhase = tmpResolvedPhase.startsWith(AT) + ? tmpResolvedPhase.substring(AT.length()) + : tmpResolvedPhase; + plan.step(project, resolvedPhase).ifPresent(n -> { + MojoExecution mojoExecution = new MojoExecution(mojoDescriptor, execution.getId()); + mojoExecution.setLifecyclePhase(phase); + n.addMojo(mojoExecution, execution.getPriority()); + if (mojoDescriptor.getDependencyCollectionRequired() != null + || mojoDescriptor.getDependencyResolutionRequired() != null) { + for (MavenProject p : + plan.getAllProjects().get(project)) { + plan.step(p, AFTER + PACKAGE) + .ifPresent(a -> plan.requiredStep(project, resolvedPhase) + .executeAfter(a)); + } + } + }); + } + } + } + } + + BuildPlan buildPlan = plan; + for (BuildStep step : + planSteps.stream().flatMap(p -> plan.steps(p.project)).toList()) { + for (MojoExecution execution : step.executions().toList()) { + buildPlan = computeForkPlan(step, execution, buildPlan); + } + } + + for (BuildStep step : planSteps) { + MavenProject project = step.project; + buildPlanLogger.writePlan(plan, project); + step.status.compareAndSet(SCHEDULED, EXECUTED); + } + + checkThreadSafety(plan); + checkUnboundVersions(plan); + } finally { + lock.writeLock().unlock(); + } + } + + protected BuildPlan computeForkPlan(BuildStep step, MojoExecution execution, BuildPlan buildPlan) { + MojoDescriptor mojoDescriptor = execution.getMojoDescriptor(); + PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor(); + String forkedGoal = mojoDescriptor.getExecuteGoal(); + String phase = mojoDescriptor.getExecutePhase(); + // We have a fork goal + if (forkedGoal != null && !forkedGoal.isEmpty()) { + MojoDescriptor forkedMojoDescriptor = pluginDescriptor.getMojo(forkedGoal); + if (forkedMojoDescriptor == null) { + throw new MavenException(new MojoNotFoundException(forkedGoal, pluginDescriptor)); + } + + List toFork = new ArrayList<>(); + toFork.add(step.project); + if (mojoDescriptor.isAggregator() && step.project.getCollectedProjects() != null) { + toFork.addAll(step.project.getCollectedProjects()); + } + + BuildPlan plan = new BuildPlan(); + for (MavenProject project : toFork) { + BuildStep st = new BuildStep(forkedGoal, project, null); + MojoExecution mojoExecution = new MojoExecution(forkedMojoDescriptor, forkedGoal); + st.addMojo(mojoExecution, 0); + Map n = new HashMap<>(); + n.put(forkedGoal, st); + plan.addProject(project, n); + } + + for (BuildStep astep : plan.allSteps().toList()) { + for (MojoExecution aexecution : astep.executions().toList()) { + plan = computeForkPlan(astep, aexecution, plan); + } + } + + return plan; + + } else if (phase != null && !phase.isEmpty()) { + String forkedLifecycle = mojoDescriptor.getExecuteLifecycle(); + Lifecycle lifecycle; + if (forkedLifecycle != null && !forkedLifecycle.isEmpty()) { + org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle lifecycleOverlay; + try { + lifecycleOverlay = pluginDescriptor.getLifecycleMapping(forkedLifecycle); + } catch (IOException | XMLStreamException e) { + throw new MavenException(new PluginDescriptorParsingException( + pluginDescriptor.getPlugin(), pluginDescriptor.getSource(), e)); + } + if (lifecycleOverlay == null) { + Optional lf = lifecycles.lookup(forkedLifecycle); + if (lf.isPresent()) { + lifecycle = lf.get(); + } else { + throw new MavenException(new LifecycleNotFoundException(forkedLifecycle)); + } + } else { + lifecycle = new PluginLifecycle(lifecycleOverlay, pluginDescriptor); + } + } else { + if (execution.getLifecyclePhase() != null) { + String n = execution.getLifecyclePhase(); + String phaseName = n.startsWith(BEFORE) + ? n.substring(BEFORE.length()) + : n.startsWith(AFTER) ? n.substring(AFTER.length()) : n; + lifecycle = lifecycles.stream() + .filter(l -> l.allPhases().anyMatch(p -> phaseName.equals(p.name()))) + .findFirst() + .orElse(null); + if (lifecycle == null) { + throw new IllegalStateException(); + } + } else { + lifecycle = lifecycles.require(Lifecycle.DEFAULT); + } + } + + String resolvedPhase = getResolvedPhase(lifecycle, phase); + + Map> map = Collections.singletonMap( + step.project, plan.getAllProjects().get(step.project)); + BuildPlan forkedPlan = calculateLifecycleMappings(map, lifecycle, resolvedPhase); + forkedPlan.then(buildPlan); + return forkedPlan; + } else { + return buildPlan; + } + } + + private String getResolvedPhase(Lifecycle lifecycle, String phase) { + return lifecycle.aliases().stream() + .filter(a -> phase.equals(a.v3Phase())) + .findFirst() + .map(Lifecycle.Alias::v4Phase) + .orElse(phase); + } + + private String getResolvedPhase(String phase) { + return lifecycles.stream() + .flatMap(l -> l.aliases().stream()) + .filter(a -> phase.equals(a.v3Phase())) + .findFirst() + .map(Lifecycle.Alias::v4Phase) + .orElse(phase); + } + + protected void handleBuildError( + final ReactorContext buildContext, + final MavenSession session, + final MavenProject mavenProject, + Throwable t, + final Clock clock) { + // record the error and mark the project as failed + buildContext.getResult().addException(t); + buildContext + .getResult() + .addBuildSummary(new BuildFailure(mavenProject, clock.execTime(), clock.wallTime(), t)); + + // notify listeners about "soft" project build failures only + if (t instanceof Exception && !(t instanceof RuntimeException)) { + eventCatapult.fire(ExecutionEvent.Type.ProjectFailed, session, null, (Exception) t); + } + + // reactor failure modes + if (t instanceof RuntimeException || !(t instanceof Exception)) { + // fail fast on RuntimeExceptions, Errors and "other" Throwables + // assume these are system errors and further build is meaningless + buildContext.getReactorBuildStatus().halt(); + } else if (MavenExecutionRequest.REACTOR_FAIL_NEVER.equals(session.getReactorFailureBehavior())) { + // continue the build + } else if (MavenExecutionRequest.REACTOR_FAIL_AT_END.equals(session.getReactorFailureBehavior())) { + // continue the build but ban all projects that depend on the failed one + buildContext.getReactorBuildStatus().blackList(mavenProject); + } else if (MavenExecutionRequest.REACTOR_FAIL_FAST.equals(session.getReactorFailureBehavior())) { + buildContext.getReactorBuildStatus().halt(); + } else { + logger.error("invalid reactor failure behavior " + session.getReactorFailureBehavior()); + buildContext.getReactorBuildStatus().halt(); + } + } + + public BuildPlan calculateMojoExecutions(Map> projects, List tasks) { + BuildPlan buildPlan = new BuildPlan(projects); + + for (Task task : tasks) { + BuildPlan step; + + if (task instanceof GoalTask) { + String pluginGoal = task.getValue(); + + String executionId = "default-cli"; + int executionIdx = pluginGoal.indexOf('@'); + if (executionIdx > 0) { + executionId = pluginGoal.substring(executionIdx + 1); + } + + step = new BuildPlan(); + for (MavenProject project : projects.keySet()) { + BuildStep st = new BuildStep(pluginGoal, project, null); + MojoDescriptor mojoDescriptor = getMojoDescriptor(project, pluginGoal); + MojoExecution mojoExecution = + new MojoExecution(mojoDescriptor, executionId, MojoExecution.Source.CLI); + st.addMojo(mojoExecution, 0); + Map n = new HashMap<>(); + n.put(pluginGoal, st); + step.addProject(project, n); + } + } else if (task instanceof LifecycleTask) { + String lifecyclePhase = task.getValue(); + + step = calculateLifecycleMappings(projects, lifecyclePhase); + + } else { + throw new IllegalStateException("unexpected task " + task); + } + + buildPlan.then(step); + } + + return buildPlan; + } + + private MojoDescriptor getMojoDescriptor(MavenProject project, Plugin plugin, String goal) { + try { + return mavenPluginManager.getMojoDescriptor( + plugin, goal, project.getRemotePluginRepositories(), session.getRepositorySession()); + } catch (MavenException e) { + throw e; + } catch (Exception e) { + throw new MavenException(e); + } + } + + private MojoDescriptor getMojoDescriptor(MavenProject project, String task) { + try { + return mojoDescriptorCreator.getMojoDescriptor(task, session, project); + } catch (MavenException e) { + throw e; + } catch (Exception e) { + throw new MavenException(e); + } + } + + public BuildPlan calculateLifecycleMappings( + Map> projects, String lifecyclePhase) { + + String resolvedPhase = getResolvedPhase(lifecyclePhase); + String mainPhase = resolvedPhase.startsWith(BEFORE) + ? resolvedPhase.substring(BEFORE.length()) + : resolvedPhase.startsWith(AFTER) + ? resolvedPhase.substring(AFTER.length()) + : resolvedPhase.startsWith(AT) ? resolvedPhase.substring(AT.length()) : resolvedPhase; + + /* + * Determine the lifecycle that corresponds to the given phase. + */ + Lifecycle lifecycle = lifecycles.stream() + .filter(l -> l.allPhases().anyMatch(p -> mainPhase.equals(p.name()))) + .findFirst() + .orElse(null); + + if (lifecycle == null) { + throw new MavenException(new LifecyclePhaseNotFoundException( + "Unknown lifecycle phase \"" + lifecyclePhase + + "\". You must specify a valid lifecycle phase" + + " or a goal in the format : or" + + " :[:]:. Available lifecycle phases are: " + + lifecycles.stream() + .flatMap(l -> l.orderedPhases() + .map(List::stream) + .orElseGet(() -> l.allPhases().map(Lifecycle.Phase::name))) + .collect(Collectors.joining(", ")) + + ".", + lifecyclePhase)); + } + + return calculateLifecycleMappings(projects, lifecycle, resolvedPhase); + } + + public BuildPlan calculateLifecycleMappings( + Map> projects, Lifecycle lifecycle, String lifecyclePhase) { + BuildPlan plan = new BuildPlan(projects); + + for (MavenProject project : projects.keySet()) { + // For each phase, create and sequence the pre, run and post steps + Map steps = lifecycle + .allPhases() + .flatMap(phase -> { + BuildStep a = new BuildStep(BEFORE + phase.name(), project, phase); + BuildStep b = new BuildStep(phase.name(), project, phase); + BuildStep c = new BuildStep(AFTER + phase.name(), project, phase); + b.executeAfter(a); + c.executeAfter(b); + return Stream.of(a, b, c); + }) + .collect(Collectors.toMap(n -> n.name, n -> n)); + // for each phase, make sure children phases are execute between pre and post steps + lifecycle.allPhases().forEach(phase -> phase.phases().forEach(child -> { + steps.get(BEFORE + child.name()).executeAfter(steps.get(BEFORE + phase.name())); + steps.get(AFTER + phase.name()).executeAfter(steps.get(AFTER + child.name())); + })); + // for each phase, create links between this project phases + lifecycle.allPhases().forEach(phase -> { + phase.links().stream() + .filter(l -> l.pointer().type() == Lifecycle.Pointer.Type.PROJECT) + .forEach(link -> { + String n1 = phase.name(); + String n2 = link.pointer().phase(); + if (link.kind() == Lifecycle.Link.Kind.AFTER) { + steps.get(BEFORE + n1).executeAfter(steps.get(AFTER + n2)); + } else { + steps.get(BEFORE + n2).executeAfter(steps.get(AFTER + n1)); + } + }); + }); + + // Only keep mojo executions before the end phase + String endPhase = lifecyclePhase.startsWith(BEFORE) || lifecyclePhase.startsWith(AFTER) + ? lifecyclePhase + : lifecyclePhase.startsWith(AT) + ? lifecyclePhase.substring(AT.length()) + : AFTER + lifecyclePhase; + Set toKeep = steps.get(endPhase).allPredecessors().collect(Collectors.toSet()); + steps.values().stream().filter(n -> !toKeep.contains(n)).forEach(BuildStep::skip); + + plan.addProject(project, steps); + } + + // Create inter project dependencies + plan.allSteps().filter(step -> step.phase != null).forEach(step -> { + Lifecycle.Phase phase = step.phase; + MavenProject project = step.project; + phase.links().stream() + .filter(l -> l.pointer().type() != Lifecycle.Pointer.Type.PROJECT) + .forEach(link -> { + String n1 = phase.name(); + String n2 = link.pointer().phase(); + // for each project, if the phase in the build, link after it + getLinkedProjects(projects, project, link).forEach(p -> plan.step(p, AFTER + n2) + .ifPresent(a -> plan.requiredStep(project, BEFORE + n1) + .executeAfter(a))); + }); + }); + + // Keep projects in reactors by GAV + Map reactorGavs = + projects.keySet().stream().collect(Collectors.toMap(BuildPlanExecutor::gav, p -> p)); + + // Go through all plugins + List toResolve = new ArrayList<>(); + projects.keySet().forEach(project -> project.getBuild().getPlugins().forEach(plugin -> { + MavenProject pluginProject = reactorGavs.get(gav(plugin)); + if (pluginProject != null) { + // In order to plan the project, we need all its plugins... + plan.requiredStep(project, PLAN).executeAfter(plan.requiredStep(pluginProject, READY)); + } else { + toResolve.add(() -> resolvePlugin(session, project.getRemotePluginRepositories(), plugin)); + } + })); + + // Eagerly resolve all plugins in parallel + toResolve.parallelStream().forEach(Runnable::run); + + // Keep track of phase aliases + lifecycle.aliases().forEach(alias -> plan.aliases().put(alias.v3Phase(), alias.v4Phase())); + + return plan; + } + + private List getLinkedProjects( + Map> projects, MavenProject project, Lifecycle.Link link) { + if (link.pointer().type() == Lifecycle.Pointer.Type.DEPENDENCIES) { + // TODO: String scope = ((Lifecycle.DependenciesPointer) link.pointer()).scope(); + return projects.get(project); + } else if (link.pointer().type() == Lifecycle.Pointer.Type.CHILDREN) { + return project.getCollectedProjects(); + } else { + throw new IllegalArgumentException( + "Unsupported pointer type: " + link.pointer().type()); + } + } + } + + private void resolvePlugin(MavenSession session, List repositories, Plugin plugin) { + try { + mavenPluginManager.getPluginDescriptor(plugin, repositories, session.getRepositorySession()); + } catch (Exception e) { + throw new MavenException(e); + } + } + + private static String gav(MavenProject p) { + return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion(); + } + + private static String gav(Plugin p) { + return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion(); + } + + /** + * Post-processes the effective configuration for the specified mojo execution. This step discards all parameters + * from the configuration that are not applicable to the mojo and injects the default values for any missing + * parameters. + * + * @param mojoExecution The mojo execution whose configuration should be finalized, must not be {@code null}. + */ + private void finalizeMojoConfiguration(MojoExecution mojoExecution) { + MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor(); + + XmlNode executionConfiguration = mojoExecution.getConfiguration() != null + ? mojoExecution.getConfiguration().getDom() + : null; + if (executionConfiguration == null) { + executionConfiguration = new XmlNodeImpl("configuration"); + } + + XmlNode defaultConfiguration = getMojoConfiguration(mojoDescriptor); + + List children = new ArrayList<>(); + if (mojoDescriptor.getParameters() != null) { + for (Parameter parameter : mojoDescriptor.getParameters()) { + XmlNode parameterConfiguration = executionConfiguration.getChild(parameter.getName()); + + if (parameterConfiguration == null) { + parameterConfiguration = executionConfiguration.getChild(parameter.getAlias()); + } + + XmlNode parameterDefaults = defaultConfiguration.getChild(parameter.getName()); + + if (parameterConfiguration != null) { + parameterConfiguration = parameterConfiguration.merge(parameterDefaults, Boolean.TRUE); + } else { + parameterConfiguration = parameterDefaults; + } + + if (parameterConfiguration != null) { + Map attributes = new HashMap<>(parameterConfiguration.getAttributes()); + + String attributeForImplementation = parameterConfiguration.getAttribute("implementation"); + String parameterForImplementation = parameter.getImplementation(); + if ((attributeForImplementation == null || attributeForImplementation.isEmpty()) + && ((parameterForImplementation != null) && !parameterForImplementation.isEmpty())) { + attributes.put("implementation", parameter.getImplementation()); + } + + parameterConfiguration = new XmlNodeImpl( + parameter.getName(), + parameterConfiguration.getValue(), + attributes, + parameterConfiguration.getChildren(), + parameterConfiguration.getInputLocation()); + + children.add(parameterConfiguration); + } + } + } + XmlNode finalConfiguration = new XmlNodeImpl("configuration", null, null, children, null); + + mojoExecution.setConfiguration(finalConfiguration); + } + + private XmlNode getMojoConfiguration(MojoDescriptor mojoDescriptor) { + if (mojoDescriptor.isV4Api()) { + return MojoDescriptorCreator.convert(mojoDescriptor.getMojoDescriptorV4()); + } else { + return MojoDescriptorCreator.convert(mojoDescriptor).getDom(); + } + } + + private MojoExecutionConfigurator mojoExecutionConfigurator(MojoExecution mojoExecution) { + String configuratorId = mojoExecution.getMojoDescriptor().getComponentConfigurator(); + if (configuratorId == null) { + configuratorId = "default"; + } + + MojoExecutionConfigurator mojoExecutionConfigurator = mojoExecutionConfigurators.get(configuratorId); + + if (mojoExecutionConfigurator == null) { + // + // The plugin has a custom component configurator but does not have a custom mojo execution configurator + // so fall back to the default mojo execution configurator. + // + mojoExecutionConfigurator = mojoExecutionConfigurators.get("default"); + } + return mojoExecutionConfigurator; + } + + public static void attachToThread(MavenProject currentProject) { + ClassRealm projectRealm = currentProject.getClassRealm(); + if (projectRealm != null) { + Thread.currentThread().setContextClassLoader(projectRealm); + } + } + + protected static class Clock { + long start; + long end; + long resumed; + long exec; + + protected void start() { + if (start == 0) { + start = System.nanoTime(); + resumed = start; + } else { + resumed = System.nanoTime(); + } + } + + protected void stop() { + end = System.nanoTime(); + exec += end - resumed; + } + + protected long wallTime() { + return TimeUnit.NANOSECONDS.toMillis(end - start); + } + + protected long execTime() { + return TimeUnit.NANOSECONDS.toMillis(exec); + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java new file mode 100644 index 00000000000..78d3f1b76bf --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import javax.inject.Named; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

+ * Logs debug output from the various lifecycle phases. + *

+ * NOTE: This class is not part of any public api and can be changed or deleted without prior notice. + * + * @since 3.0 + */ +@Named +public class BuildPlanLogger { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public void writePlan(BuildPlan plan) { + writePlan(logger::info, plan); + } + + public void writePlan(BuildPlan plan, MavenProject project) { + writePlan(logger::info, plan, project); + } + + public void writePlan(Consumer writer, BuildPlan plan) { + plan.projects().forEach(project -> writePlan(writer, plan, project)); + } + + public void writePlan(Consumer writer, BuildPlan plan, MavenProject project) { + writer.accept("=== PROJECT BUILD PLAN ================================================"); + writer.accept("Project: " + getKey(project)); + writer.accept("Repositories (dependencies): " + project.getRemoteProjectRepositories()); + writer.accept("Repositories (plugins): " + project.getRemotePluginRepositories()); + + Optional planStep = plan.step(project, BuildStep.PLAN); + if (planStep.isPresent() && planStep.get().status.get() == BuildStep.PLANNING) { + writer.accept("Build plan will be lazily computed"); + } else { + plan.steps(project) + .filter(step -> + step.phase != null && step.executions().findAny().isPresent()) + .sorted(Comparator.comparingInt(plan.sortedNodes()::indexOf)) + .forEach(step -> { + writer.accept("\t-----------------------------------------------------------------------"); + writer.accept("\tPhase: " + step.name); + if (!step.predecessors.isEmpty()) { + writer.accept("\tPredecessors: " + + nonEmptyPredecessors(step) + .map(n -> phase(project, n, plan.duplicateIds())) + .collect(Collectors.joining(", "))); + } + /* + if (!node.successors.isEmpty()) { + writer.accept("\tSuccessors: " + + node.successors.stream() + .map(n -> phase(currentProject, n, duplicateIds)) + .collect(Collectors.joining(", "))); + } + */ + step.mojos.values().stream() + .flatMap(m -> m.values().stream()) + .forEach(mojo -> mojo(writer, mojo)); + }); + } + + writer.accept("======================================================================="); + } + + protected Stream nonEmptyPredecessors(BuildStep step) { + HashSet preds = new HashSet<>(); + nonEmptyPredecessors(step, preds, new HashSet<>()); + return preds.stream(); + } + + private void nonEmptyPredecessors(BuildStep step, Set preds, Set visited) { + if (visited.add(step)) { + step.predecessors.forEach(ch -> { + if (ch.executions().findAny().isPresent()) { + preds.add(ch); + } else { + nonEmptyPredecessors(ch, preds, visited); + } + }); + } + } + + protected String phase(MavenProject currentProject, BuildStep step, Set duplicateIds) { + if (step.project == currentProject) { + return step.name; + } else { + String artifactId = step.project.getArtifactId(); + if (duplicateIds.contains(artifactId)) { + return step.name + "(" + step.project.getGroupId() + ":" + artifactId + ")"; + } else { + return step.name + "(:" + artifactId + ")"; + } + } + } + + protected void mojo(Consumer writer, MojoExecution mojoExecution) { + String mojoExecId = + mojoExecution.getGroupId() + ':' + mojoExecution.getArtifactId() + ':' + mojoExecution.getVersion() + + ':' + mojoExecution.getGoal() + " (" + mojoExecution.getExecutionId() + ')'; + + Map> forkedExecutions = mojoExecution.getForkedExecutions(); + if (!forkedExecutions.isEmpty()) { + for (Map.Entry> fork : forkedExecutions.entrySet()) { + writer.accept("\t--- init fork of " + fork.getKey() + " for " + mojoExecId + " ---"); + + for (MojoExecution forkedExecution : fork.getValue()) { + mojo(writer, forkedExecution); + } + + writer.accept("\t--- exit fork of " + fork.getKey() + " for " + mojoExecId + " ---"); + } + } + + writer.accept("\t\t-----------------------------------------------------------------------"); + if (mojoExecution.getMojoDescriptor().isAggregator()) { + writer.accept("\t\tAggregator goal: " + mojoExecId); + } else { + writer.accept("\t\tGoal: " + mojoExecId); + } + if (mojoExecution.getConfiguration() != null) { + writer.accept("\t\tConfiguration: " + mojoExecution.getConfiguration()); + } + if (mojoExecution.getMojoDescriptor().getDependencyCollectionRequired() != null) { + writer.accept("\t\tDependencies (collect): " + + mojoExecution.getMojoDescriptor().getDependencyCollectionRequired()); + } + if (mojoExecution.getMojoDescriptor().getDependencyResolutionRequired() != null) { + writer.accept("\t\tDependencies (resolve): " + + mojoExecution.getMojoDescriptor().getDependencyResolutionRequired()); + } + } + + protected String getKey(MavenProject project) { + return project.getGroupId() + ':' + project.getArtifactId() + ':' + project.getVersion(); + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java new file mode 100644 index 00000000000..e0b7c9598a0 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.apache.maven.api.Lifecycle; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.project.MavenProject; + +public class BuildStep { + public static final int CREATED = 0; + public static final int PLANNING = 1; + public static final int SCHEDULED = 2; + public static final int EXECUTED = 3; + public static final int FAILED = 4; + + public static final String PLAN = "$plan$"; + public static final String SETUP = "$setup$"; + public static final String TEARDOWN = "$teardown$"; + + final MavenProject project; + final String name; + final Lifecycle.Phase phase; + final Map> mojos = new TreeMap<>(); + final Collection predecessors = new HashSet<>(); + final Collection successors = new HashSet<>(); + final AtomicInteger status = new AtomicInteger(); + final AtomicBoolean skip = new AtomicBoolean(); + + public BuildStep(String name, MavenProject project, Lifecycle.Phase phase) { + this.name = name; + this.project = project; + this.phase = phase; + } + + public Stream allPredecessors() { + return preds(new HashSet<>()).stream(); + } + + private Set preds(Set preds) { + if (preds.add(this)) { + this.predecessors.forEach(n -> n.preds(preds)); + } + return preds; + } + + public boolean isSuccessorOf(BuildStep step) { + return isSuccessorOf(new HashSet<>(), step); + } + + private boolean isSuccessorOf(Set visited, BuildStep step) { + if (this == step) { + return true; + } else if (visited.add(this)) { + return this.predecessors.stream().anyMatch(n -> n.isSuccessorOf(visited, step)); + } else { + return false; + } + } + + public void skip() { + skip.set(true); + mojos.clear(); + } + + public void addMojo(MojoExecution mojo, int priority) { + if (!skip.get()) { + mojos.computeIfAbsent(priority, k -> new LinkedHashMap<>()) + .put(mojo.getGoal() + ":" + mojo.getExecutionId(), mojo); + } + } + + public void executeAfter(BuildStep stepToExecuteBefore) { + if (!isSuccessorOf(stepToExecuteBefore)) { + predecessors.add(stepToExecuteBefore); + stepToExecuteBefore.successors.add(this); + } + } + + public Stream executions() { + return mojos.values().stream().flatMap(m -> m.values().stream()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BuildStep that = (BuildStep) o; + return Objects.equals(project, that.project) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(project, name); + } + + @Override + public String toString() { + return "BuildStep[" + "project=" + + project.getGroupId() + ":" + project.getArtifactId() + ", phase=" + + name + ']'; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java new file mode 100644 index 00000000000..e4422ffc1cd --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.lifecycle.DefaultLifecycles; +import org.apache.maven.lifecycle.MissingProjectException; +import org.apache.maven.lifecycle.NoGoalSpecifiedException; +import org.apache.maven.lifecycle.internal.ExecutionEventCatapult; +import org.apache.maven.lifecycle.internal.GoalTask; +import org.apache.maven.lifecycle.internal.LifecyclePluginResolver; +import org.apache.maven.lifecycle.internal.LifecycleStarter; +import org.apache.maven.lifecycle.internal.LifecycleTask; +import org.apache.maven.lifecycle.internal.MojoDescriptorCreator; +import org.apache.maven.lifecycle.internal.ReactorBuildStatus; +import org.apache.maven.lifecycle.internal.ReactorContext; +import org.apache.maven.lifecycle.internal.TaskSegment; +import org.apache.maven.plugin.descriptor.MojoDescriptor; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Objects.requireNonNull; + +/** + * Starts the build life cycle + */ +@Named("concurrent") +@Singleton +public class ConcurrentLifecycleStarter implements LifecycleStarter { + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ExecutionEventCatapult eventCatapult; + private final DefaultLifecycles defaultLifeCycles; + private final BuildPlanExecutor executor; + private final LifecyclePluginResolver lifecyclePluginResolver; + private final MojoDescriptorCreator mojoDescriptorCreator; + + @Inject + public ConcurrentLifecycleStarter( + ExecutionEventCatapult eventCatapult, + DefaultLifecycles defaultLifeCycles, + BuildPlanExecutor executor, + LifecyclePluginResolver lifecyclePluginResolver, + MojoDescriptorCreator mojoDescriptorCreator) { + this.eventCatapult = eventCatapult; + this.defaultLifeCycles = defaultLifeCycles; + this.executor = executor; + this.lifecyclePluginResolver = lifecyclePluginResolver; + this.mojoDescriptorCreator = mojoDescriptorCreator; + } + + public void execute(MavenSession session) { + eventCatapult.fire(ExecutionEvent.Type.SessionStarted, session, null); + + try { + if (requiresProject(session) && projectIsNotPresent(session)) { + throw new MissingProjectException("The goal you specified requires a project to execute" + + " but there is no POM in this directory (" + session.getTopDirectory() + ")." + + " Please verify you invoked Maven from the correct directory."); + } + + List taskSegments = calculateTaskSegments(session); + if (taskSegments.isEmpty()) { + throw new NoGoalSpecifiedException("No goals have been specified for this build." + + " You must specify a valid lifecycle phase or a goal in the format : or" + + " :[:]:." + + " Available lifecycle phases are: " + defaultLifeCycles.getLifecyclePhaseList() + "."); + } + + int degreeOfConcurrency = session.getRequest().getDegreeOfConcurrency(); + if (degreeOfConcurrency > 1) { + logger.info(""); + logger.info(String.format( + "Using the %s implementation with a thread count of %d", + executor.getClass().getSimpleName(), degreeOfConcurrency)); + } + + ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader(); + ReactorBuildStatus reactorBuildStatus = new ReactorBuildStatus(session.getProjectDependencyGraph()); + ReactorContext reactorContext = + new ReactorContext(session.getResult(), oldContextClassLoader, reactorBuildStatus); + executor.execute(session, reactorContext, taskSegments); + + } catch (Exception e) { + session.getResult().addException(e); + } finally { + eventCatapult.fire(ExecutionEvent.Type.SessionEnded, session, null); + } + } + + public List calculateTaskSegments(MavenSession session) throws Exception { + + MavenProject rootProject = session.getTopLevelProject(); + + List tasks = requireNonNull(session.getGoals()); // session never returns null, but empty list + + if (tasks.isEmpty() + && (rootProject.getDefaultGoal() != null + && !rootProject.getDefaultGoal().isEmpty())) { + tasks = Stream.of(rootProject.getDefaultGoal().split("\\s+")) + .filter(g -> !g.isEmpty()) + .collect(Collectors.toList()); + } + + return calculateTaskSegments(session, tasks); + } + + public List calculateTaskSegments(MavenSession session, List tasks) throws Exception { + List taskSegments = new ArrayList<>(tasks.size()); + + TaskSegment currentSegment = null; + + for (String task : tasks) { + if (isGoalSpecification(task)) { + // "pluginPrefix[:version]:goal" or "groupId:artifactId[:version]:goal" + + lifecyclePluginResolver.resolveMissingPluginVersions(session.getTopLevelProject(), session); + + MojoDescriptor mojoDescriptor = + mojoDescriptorCreator.getMojoDescriptor(task, session, session.getTopLevelProject()); + + boolean aggregating = mojoDescriptor.isAggregator() || !mojoDescriptor.isProjectRequired(); + + if (currentSegment == null || currentSegment.isAggregating() != aggregating) { + currentSegment = new TaskSegment(aggregating); + taskSegments.add(currentSegment); + } + + currentSegment.getTasks().add(new GoalTask(task)); + } else { + // lifecycle phase + + if (currentSegment == null || currentSegment.isAggregating()) { + currentSegment = new TaskSegment(false); + taskSegments.add(currentSegment); + } + + currentSegment.getTasks().add(new LifecycleTask(task)); + } + } + + return taskSegments; + } + + private boolean projectIsNotPresent(MavenSession session) { + return !session.getRequest().isProjectPresent(); + } + + private boolean requiresProject(MavenSession session) { + List goals = session.getGoals(); + if (goals != null) { + for (String goal : goals) { + if (!isGoalSpecification(goal)) { + return true; + } + } + } + return false; + } + + private boolean isGoalSpecification(String task) { + return task.indexOf(':') >= 0; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java new file mode 100644 index 00000000000..5c5342ba8ef --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.maven.project.MavenProject; +import org.apache.maven.slf4j.MavenSimpleLogger; + +/** + * Forwards log messages to the client. + */ +public class ConcurrentLogOutput implements AutoCloseable { + + private static final ThreadLocal CONTEXT = new InheritableThreadLocal<>(); + + public ConcurrentLogOutput() { + MavenSimpleLogger.setLogSink(this::accept); + } + + protected void accept(String message) { + ProjectExecutionContext context = CONTEXT.get(); + if (context != null) { + context.accept(message); + } else { + System.out.println(message); + } + } + + @Override + public void close() { + MavenSimpleLogger.setLogSink(null); + } + + public AutoCloseable build(MavenProject project) { + return new ProjectExecutionContext(project); + } + + private static class ProjectExecutionContext implements AutoCloseable { + final MavenProject project; + final List messages = new CopyOnWriteArrayList<>(); + boolean closed; + + ProjectExecutionContext(MavenProject project) { + this.project = project; + CONTEXT.set(this); + } + + void accept(String message) { + if (!closed) { + this.messages.add(message); + } else { + System.out.println(message); + } + } + + @Override + public void close() { + closed = true; + CONTEXT.set(null); + this.messages.forEach(System.out::println); + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java new file mode 100644 index 00000000000..273035b82c1 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.apache.maven.api.services.MessageBuilderFactory; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.lifecycle.internal.ExecutionEventCatapult; +import org.apache.maven.lifecycle.internal.LifecycleDependencyResolver; +import org.apache.maven.plugin.BuildPluginManager; +import org.apache.maven.plugin.MavenPluginManager; +import org.apache.maven.plugin.MojosExecutionStrategy; + +@Named("concurrent") +@Singleton +public class MojoExecutor extends org.apache.maven.lifecycle.internal.MojoExecutor { + + @Inject + public MojoExecutor( + BuildPluginManager pluginManager, + MavenPluginManager mavenPluginManager, + LifecycleDependencyResolver lifeCycleDependencyResolver, + ExecutionEventCatapult eventCatapult, + Provider mojosExecutionStrategy, + MessageBuilderFactory messageBuilderFactory) { + super( + pluginManager, + mavenPluginManager, + lifeCycleDependencyResolver, + eventCatapult, + mojosExecutionStrategy, + messageBuilderFactory); + } + + @Override + protected boolean useProjectLock(MavenSession session) { + return false; + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java new file mode 100644 index 00000000000..c27f5ce5b3f --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Phaser; + +public class PhasingExecutor implements Executor, AutoCloseable { + private final ExecutorService executor; + private final Phaser phaser = new Phaser(); + + public PhasingExecutor(ExecutorService executor) { + this.executor = executor; + this.phaser.register(); + } + + @Override + public void execute(Runnable command) { + phaser.register(); + executor.submit(() -> { + try { + command.run(); + } finally { + phaser.arriveAndDeregister(); + } + }); + } + + public void await() { + phaser.arriveAndAwaitAdvance(); + } + + @Override + public void close() { + executor.shutdownNow(); + } +} diff --git a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java new file mode 100644 index 00000000000..81a319b7aea --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.api.Lifecycle; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.plugin.descriptor.PluginDescriptor; + +class PluginLifecycle implements Lifecycle { + private final org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle lifecycleOverlay; + private final PluginDescriptor pluginDescriptor; + + PluginLifecycle( + org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle lifecycleOverlay, + PluginDescriptor pluginDescriptor) { + this.lifecycleOverlay = lifecycleOverlay; + this.pluginDescriptor = pluginDescriptor; + } + + @Override + public String id() { + return lifecycleOverlay.getId(); + } + + @Override + public Collection phases() { + return lifecycleOverlay.getPhases().stream() + .map(phase -> new Phase() { + @Override + public String name() { + return phase.getId(); + } + + @Override + public List plugins() { + return Collections.singletonList(Plugin.newBuilder() + .groupId(pluginDescriptor.getGroupId()) + .artifactId(pluginDescriptor.getArtifactId()) + .version(pluginDescriptor.getVersion()) + .configuration(phase.getConfiguration()) + .executions(phase.getExecutions().stream() + .map(exec -> org.apache.maven.api.model.PluginExecution.newBuilder() + .goals(exec.getGoals()) + .configuration(exec.getConfiguration()) + .build()) + .collect(Collectors.toList())) + .build()); + } + + @Override + public Collection links() { + return Collections.emptyList(); + } + + @Override + public List phases() { + return Collections.emptyList(); + } + + @Override + public Stream allPhases() { + return Stream.concat(Stream.of(this), phases().stream().flatMap(Phase::allPhases)); + } + }) + .collect(Collectors.toList()); + } + + @Override + public Collection aliases() { + return Collections.emptyList(); + } +} diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java new file mode 100644 index 00000000000..0e3fd5dd0ce --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.maven.internal.impl.DefaultLifecycleRegistry; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.project.MavenProject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BuildPlanCreatorTest { + + @Test + void testMulti() { + MavenProject project = new MavenProject(); + Map> projects = Collections.singletonMap(project, Collections.emptyList()); + + BuildPlan plan = calculateLifecycleMappings(projects, "package"); + + new BuildPlanLogger().writePlan(System.out::println, plan); + } + + @Test + void testCondense() { + MavenProject p1 = new MavenProject(); + p1.setArtifactId("p1"); + MavenProject p2 = new MavenProject(); + p2.setArtifactId("p2"); + Map> projects = new HashMap<>(); + projects.put(p1, Collections.emptyList()); + projects.put(p2, Collections.singletonList(p1)); + + BuildPlan plan = calculateLifecycleMappings(projects, "verify"); + plan.then(calculateLifecycleMappings(projects, "install")); + + Stream.of(p1, p2).forEach(project -> { + plan.requiredStep(project, "after:resources").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "after:test-resources").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "compile").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "test-compile").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "test").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "package").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "install").addMojo(new MojoExecution(null), 0); + }); + + new BuildPlanLogger() { + @Override + protected void mojo(Consumer writer, MojoExecution mojoExecution) {} + }.writePlan(System.out::println, plan); + + plan.allSteps().forEach(phase -> { + phase.predecessors.forEach( + pred -> assertTrue(plan.step(pred.project, pred.name).isPresent(), "Phase not present: " + pred)); + }); + } + + @Test + void testAlias() { + MavenProject p1 = new MavenProject(); + p1.setArtifactId("p1"); + Map> projects = Collections.singletonMap(p1, Collections.emptyList()); + + BuildPlan plan = calculateLifecycleMappings(projects, "generate-resources"); + } + + private BuildPlan calculateLifecycleMappings(Map> projects, String phase) { + DefaultLifecycleRegistry lifecycles = new DefaultLifecycleRegistry(Collections.emptyList()); + BuildPlanExecutor builder = new BuildPlanExecutor(null, null, null, null, null, null, null, null, lifecycles); + BuildPlanExecutor.BuildContext context = builder.new BuildContext(); + return context.calculateLifecycleMappings(projects, phase); + } + + /* + @Test + void testPlugins() { + DefaultLifecycleRegistry lifecycles = + new DefaultLifecycleRegistry(Collections.emptyList(), Collections.emptyMap()); + BuildPlanCreator builder = new BuildPlanCreator(null, null, null, null, null, lifecycles); + MavenProject p1 = new MavenProject(); + p1.setGroupId("g"); + p1.setArtifactId("p1"); + p1.getBuild().getPlugins().add(new Plugin(org.apache.maven.api.model.Plugin.newBuilder() + .groupId("g").artifactId("p2") + . + .build())) + MavenProject p2 = new MavenProject(); + p2.setGroupId("g"); + p2.setArtifactId("p2"); + + Map> projects = new HashMap<>(); + projects.put(p1, Collections.emptyList()); + projects.put(p2, Collections.singletonList(p1)); + Lifecycle lifecycle = lifecycles.require("default"); + BuildPlan plan = builder.calculateLifecycleMappings(null, projects, lifecycle, "verify"); + plan.then(builder.calculateLifecycleMappings(null, projects, lifecycle, "install")); + + Stream.of(p1, p2).forEach(project -> { + plan.requiredStep(project, "post:resources").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "post:test-resources").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "compile").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "test-compile").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "test").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "package").addMojo(new MojoExecution(null), 0); + plan.requiredStep(project, "install").addMojo(new MojoExecution(null), 0); + }); + + plan.condense(); + + new BuildPlanLogger() { + @Override + protected void mojo(Consumer writer, MojoExecution mojoExecution) {} + }.writePlan(System.out::println, plan); + + plan.allSteps().forEach(phase -> { + phase.predecessors.forEach( + pred -> assertTrue(plan.step(pred.project, pred.name).isPresent(), "Phase not present: " + pred)); + }); + } + */ +} diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java new file mode 100644 index 00000000000..5c038cd072e --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.lifecycle.internal.concurrent; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.Test; + +public class PhasingExecutorTest { + + @Test + void testPhaser() { + PhasingExecutor p = new PhasingExecutor(Executors.newFixedThreadPool(4)); + p.execute(() -> waitSomeTime(p, 2)); + p.await(); + } + + private void waitSomeTime(Executor executor, int nb) { + try { + Thread.sleep(10); + if (nb > 0) { + executor.execute(() -> waitSomeTime(executor, nb - 1)); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +}