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 index 697b9d480206..3e86bfa2c02c 100644 --- 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 @@ -21,6 +21,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -37,6 +38,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited +@Repeatable(Afters.class) public @interface After { /** @@ -57,7 +59,7 @@ enum Type { /** * The type of this pointer. */ - Type type(); + Type type() default Type.PROJECT; /** * The scope for dependencies, only if {@code type() == Type.Dependencies}. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Afters.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Afters.java new file mode 100644 index 000000000000..c00ee72de848 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/Afters.java @@ -0,0 +1,46 @@ +/* + * 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; + +/** + * Container annotation for repeatable {@link After} annotations. + * + * @since 4.0.0 + */ +@Experimental +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Afters { + + /** + * The contained {@link After} annotations. + */ + After[] value(); +} diff --git a/api/maven-api-core/src/test/java/org/apache/maven/api/plugin/annotations/AfterAnnotationTest.java b/api/maven-api-core/src/test/java/org/apache/maven/api/plugin/annotations/AfterAnnotationTest.java new file mode 100644 index 000000000000..4c681b778d4a --- /dev/null +++ b/api/maven-api-core/src/test/java/org/apache/maven/api/plugin/annotations/AfterAnnotationTest.java @@ -0,0 +1,97 @@ +/* + * 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.Repeatable; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests the {@link After} annotation, including its {@link Repeatable} behavior. + */ +class AfterAnnotationTest { + + @After(phase = "compile", type = After.Type.PROJECT) + static class SingleAfterMojo {} + + @After(phase = "sources") + static class DefaultTypeMojo {} + + @After(phase = "compile", type = After.Type.PROJECT) + @After(phase = "ready", type = After.Type.DEPENDENCIES, scope = "compile") + @After(phase = "package", type = After.Type.CHILDREN) + static class MultipleAfterMojo {} + + @Test + void afterIsRepeatable() { + assertTrue(After.class.isAnnotationPresent(Repeatable.class)); + assertEquals(Afters.class, After.class.getAnnotation(Repeatable.class).value()); + } + + @Test + void singleAfterAnnotation() { + After after = SingleAfterMojo.class.getAnnotation(After.class); + assertNotNull(after); + assertEquals("compile", after.phase()); + assertEquals(After.Type.PROJECT, after.type()); + assertEquals("", after.scope()); + } + + @Test + void multipleAfterAnnotations() { + After[] afters = MultipleAfterMojo.class.getAnnotationsByType(After.class); + assertNotNull(afters); + assertEquals(3, afters.length); + + assertEquals("compile", afters[0].phase()); + assertEquals(After.Type.PROJECT, afters[0].type()); + + assertEquals("ready", afters[1].phase()); + assertEquals(After.Type.DEPENDENCIES, afters[1].type()); + assertEquals("compile", afters[1].scope()); + + assertEquals("package", afters[2].phase()); + assertEquals(After.Type.CHILDREN, afters[2].type()); + } + + @Test + void aftersContainerAnnotation() { + Afters afters = MultipleAfterMojo.class.getAnnotation(Afters.class); + assertNotNull(afters); + assertEquals(3, afters.value().length); + } + + @Test + void typeDefaultsToProject() { + After after = DefaultTypeMojo.class.getAnnotation(After.class); + assertNotNull(after); + assertEquals("sources", after.phase()); + assertEquals(After.Type.PROJECT, after.type()); + } + + @Test + void scopeDefaultsToEmpty() { + After after = SingleAfterMojo.class.getAnnotation(After.class); + assertEquals("", after.scope()); + } +} diff --git a/api/maven-api-plugin/src/main/mdo/plugin.mdo b/api/maven-api-plugin/src/main/mdo/plugin.mdo index 5eeaaf331573..d987ed29b11a 100644 --- a/api/maven-api-plugin/src/main/mdo/plugin.mdo +++ b/api/maven-api-plugin/src/main/mdo/plugin.mdo @@ -425,6 +425,21 @@ under the License. String the full goal name + + afterLinks + 2.0.0+ + + Lifecycle ordering constraints declared by {@code @After} annotations on the Mojo class. + Each entry specifies that this Mojo's bound phase should execute after a given target phase, + with a pointer type ({@code PROJECT}, {@code DEPENDENCIES}, or {@code CHILDREN}) that controls + how the ordering applies across project boundaries. + @since 4.0.0 + + + AfterLink + * + + @@ -585,6 +600,46 @@ under the License. + + AfterLink + 2.0.0+ + + A lifecycle ordering constraint from an {@code @After} annotation on a Mojo class. + Specifies that the Mojo's bound phase should execute after a given target phase, + with a pointer type controlling how the ordering applies across project boundaries. + @since 4.0.0 + + + + phase + true + 2.0.0+ + String + The target phase name that this Mojo should run after. + + + type + true + 2.0.0+ + String + + The type of pointer: {@code PROJECT} (same-project phase ordering), + {@code DEPENDENCIES} (cross-project dependency ordering), + or {@code CHILDREN} (parent-child module ordering). + + + + scope + 2.0.0+ + String + + The dependency scope, only meaningful when type is {@code DEPENDENCIES}. + Examples: {@code compile}, {@code runtime}, {@code test}. + + + + + Resolution 2.0.0+ diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java index 3ed6c002d1bd..f38a62c36542 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java +++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java @@ -43,6 +43,7 @@ import org.apache.maven.api.Lifecycle; import org.apache.maven.api.MonotonicClock; +import org.apache.maven.api.plugin.descriptor.AfterLink; import org.apache.maven.api.services.LifecycleRegistry; import org.apache.maven.api.services.MavenException; import org.apache.maven.api.xml.XmlNode; @@ -607,6 +608,8 @@ private void plan() { .executeAfter(a)); } } + // Apply @After annotation ordering constraints from the mojo descriptor + applyAfterLinks(mojoDescriptor, project, resolvedPhase); }); } } @@ -634,6 +637,83 @@ private void plan() { } } + /** + * Applies lifecycle ordering constraints from {@code @After} annotations on a mojo descriptor. + * Each {@link AfterLink} is translated into build step ordering edges, matching the same + * semantics as {@link Lifecycle.Link} processing in {@code calculateLifecycleMappings}. + * + * @param mojoDescriptor the mojo descriptor that may contain after links + * @param project the project the mojo is bound to + * @param resolvedPhase the resolved phase the mojo is bound to + */ + private void applyAfterLinks(MojoDescriptor mojoDescriptor, MavenProject project, String resolvedPhase) { + List afterLinks = mojoDescriptor.getMojoDescriptorV4().getAfterLinks(); + if (afterLinks == null || afterLinks.isEmpty()) { + return; + } + for (AfterLink afterLink : afterLinks) { + String targetPhase = afterLink.getPhase(); + String type = afterLink.getType(); + if ("PROJECT".equals(type)) { + // Same-project ordering: this phase starts after target phase completes + plan.step(project, AFTER + targetPhase) + .ifPresent(targetAfter -> plan.requiredStep(project, BEFORE + resolvedPhase) + .executeAfter(targetAfter)); + } else if ("DEPENDENCIES".equals(type)) { + // Cross-project ordering: this phase starts after each dependency's target phase completes + String scope = afterLink.getScope(); + for (MavenProject dep : + filterByScope(project, plan.getAllProjects().get(project), scope)) { + plan.step(dep, AFTER + targetPhase) + .ifPresent(depAfter -> plan.requiredStep(project, BEFORE + resolvedPhase) + .executeAfter(depAfter)); + } + } else if ("CHILDREN".equals(type)) { + // Parent-child ordering: bidirectional coordination with child modules + BuildStep before = plan.requiredStep(project, BEFORE + resolvedPhase); + BuildStep after = plan.requiredStep(project, AFTER + resolvedPhase); + if (project.getCollectedProjects() != null) { + project.getCollectedProjects().forEach(child -> { + plan.step(child, BEFORE + targetPhase).ifPresent(before::executeBefore); + plan.step(child, AFTER + targetPhase).ifPresent(after::executeAfter); + }); + } + } + } + } + + /** + * Filters upstream projects by dependency scope. If the scope is null or empty, + * all upstream projects are returned. Otherwise, only projects that the given + * project depends on with a matching scope are included. + *

+ * Matching is exact on the dependency's declared scope string (e.g. "compile", + * "provided", "test"). Maven's default dependency scope is "compile" (when no + * scope is declared), so a null scope in the model is treated as "compile" for + * matching purposes. Note that this does not perform path-scope + * resolution — for example, filtering by "compile" will not include + * "provided"-scoped dependencies even though they contribute to + * {@code PathScope.MAIN_COMPILE}. This keeps the filter simple and predictable; + * broader scope-aware filtering can be added in a follow-up if needed. + * + * @param project the project whose dependencies to check + * @param upstreamProjects the list of upstream reactor projects + * @param scope the dependency scope to filter by, or null/empty for all + * @return the filtered list of upstream projects + */ + static List filterByScope( + MavenProject project, List upstreamProjects, String scope) { + if (scope == null || scope.isEmpty()) { + return upstreamProjects; + } + return upstreamProjects.stream() + .filter(dep -> project.getDependencies().stream() + .anyMatch(d -> dep.getGroupId().equals(d.getGroupId()) + && dep.getArtifactId().equals(d.getArtifactId()) + && scope.equals(d.getScope() != null ? d.getScope() : "compile"))) + .collect(Collectors.toList()); + } + protected BuildPlan computeForkPlan(BuildStep step, MojoExecution execution, BuildPlan buildPlan) { MojoDescriptor mojoDescriptor = execution.getMojoDescriptor(); PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor(); @@ -951,8 +1031,8 @@ public BuildPlan calculateLifecycleMappings( if (pointer instanceof Lifecycle.DependenciesPointer) { // For dependencies: ensure current project's phase starts after dependency's phase completes // Example: project's compile starts after dependency's package completes - // TODO: String scope = ((Lifecycle.DependenciesPointer) pointer).scope(); - projects.get(project) + String scope = ((Lifecycle.DependenciesPointer) pointer).scope(); + filterByScope(project, projects.get(project), scope) .forEach(p -> plan.step(p, AFTER + n2).ifPresent(before::executeAfter)); } else if (pointer instanceof Lifecycle.ChildrenPointer) { // For children: ensure bidirectional phase coordination diff --git a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java index 1881f2dcbda1..38c7912f9fa2 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java @@ -26,10 +26,14 @@ import java.util.stream.Stream; import org.apache.maven.internal.impl.DefaultLifecycleRegistry; +import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.project.MavenProject; import org.junit.jupiter.api.Test; +import static org.apache.maven.api.Lifecycle.AFTER; +import static org.apache.maven.api.Lifecycle.BEFORE; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -129,6 +133,181 @@ private BuildPlan calculateLifecycleMappings(Map + * This is a real constraint: in the V4 lifecycle, "compile" and "resources" are + * parallel siblings (compile depends on sources, not resources), so the @After + * link creates a genuine ordering edge that doesn't exist naturally. + */ + @Test + void testAfterLinkProjectOrdering() { + MavenProject project = new MavenProject(); + project.setCollectedProjects(List.of()); + Map> projects = Collections.singletonMap(project, Collections.emptyList()); + + BuildPlan plan = calculateLifecycleMappings(projects, "package"); + + // Simulate @After(phase="resources", type=PROJECT) on a mojo bound to "compile" + // This means: compile's BEFORE step must wait for resources' AFTER step + // This is a real constraint since compile and resources are parallel in the lifecycle + BuildStep compileBefore = plan.requiredStep(project, BEFORE + "compile"); + BuildStep resourcesAfter = plan.requiredStep(project, AFTER + "resources"); + compileBefore.executeAfter(resourcesAfter); + + // Verify: compile is now a successor of resources (via the after link) + assertIsSuccessor(resourcesAfter, compileBefore); + } + + /** + * Tests that DEPENDENCIES-type @After link ordering constraints work correctly. + * Simulates what {@code applyAfterLinks} does for {@code @After(phase="ready", type=DEPENDENCIES)}. + */ + @Test + void testAfterLinkDependenciesOrdering() { + MavenProject p1 = new MavenProject(); + p1.setArtifactId("p1"); + p1.setCollectedProjects(List.of()); + MavenProject p2 = new MavenProject(); + p2.setArtifactId("p2"); + p2.setCollectedProjects(List.of()); + Map> projects = new HashMap<>(); + projects.put(p1, Collections.emptyList()); + projects.put(p2, Collections.singletonList(p1)); + + BuildPlan plan = calculateLifecycleMappings(projects, "package"); + + // Simulate @After(phase="ready", type=DEPENDENCIES, scope="compile") on p2's compile phase + // This means: p2's compile BEFORE must wait for p1's ready AFTER + BuildStep p2CompileBefore = plan.requiredStep(p2, BEFORE + "compile"); + BuildStep p1ReadyAfter = plan.requiredStep(p1, AFTER + "ready"); + + // Apply the DEPENDENCIES link (same logic as applyAfterLinks) + for (MavenProject dep : projects.get(p2)) { + plan.step(dep, AFTER + "ready").ifPresent(p2CompileBefore::executeAfter); + } + + // Verify: p2's compile is now a successor of p1's ready + assertIsSuccessor(p1ReadyAfter, p2CompileBefore); + } + + /** + * Tests that CHILDREN-type @After link ordering constraints work correctly. + * Simulates what {@code applyAfterLinks} does for {@code @After(phase="package", type=CHILDREN)}. + */ + @Test + void testAfterLinkChildrenOrdering() { + MavenProject child = new MavenProject(); + child.setArtifactId("child"); + child.setCollectedProjects(List.of()); + MavenProject parent = new MavenProject(); + parent.setArtifactId("parent"); + parent.setCollectedProjects(List.of(child)); + Map> projects = Map.of(parent, List.of(), child, List.of()); + + BuildPlan plan = calculateLifecycleMappings(projects, "install"); + + // Simulate @After(phase="package", type=CHILDREN) on parent's install phase + // This means: parent waits for children's package before its install completes + BuildStep parentInstallBefore = plan.requiredStep(parent, BEFORE + "install"); + BuildStep parentInstallAfter = plan.requiredStep(parent, AFTER + "install"); + + // Apply the CHILDREN link (same logic as applyAfterLinks) + parent.getCollectedProjects().forEach(c -> { + plan.step(c, BEFORE + "package").ifPresent(parentInstallBefore::executeBefore); + plan.step(c, AFTER + "package").ifPresent(parentInstallAfter::executeAfter); + }); + + // Verify: parent's install after waits for child's package after + BuildStep childPackageAfter = plan.requiredStep(child, AFTER + "package"); + assertIsSuccessor(childPackageAfter, parentInstallAfter); + } + + /** + * Tests that {@code filterByScope} returns all upstream projects when scope is null or empty. + */ + @Test + void testFilterByScopeNullReturnsAll() { + MavenProject p1 = createProjectWithId("g", "p1"); + MavenProject p2 = createProjectWithId("g", "p2"); + List upstream = List.of(p1, p2); + + MavenProject consumer = new MavenProject(); + assertEquals(upstream, BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, null)); + assertEquals(upstream, BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "")); + } + + /** + * Tests that {@code filterByScope} filters upstream projects by exact scope match, + * treating null-scoped dependencies as "compile" (Maven default). + */ + @Test + void testFilterByScopeMatchesExact() { + MavenProject compileDep = createProjectWithId("g", "compile-dep"); + MavenProject providedDep = createProjectWithId("g", "provided-dep"); + MavenProject testDep = createProjectWithId("g", "test-dep"); + MavenProject nullScopeDep = createProjectWithId("g", "null-scope-dep"); + List upstream = List.of(compileDep, providedDep, testDep, nullScopeDep); + + MavenProject consumer = new MavenProject(); + consumer.getDependencies().add(createDependency("g", "compile-dep", "compile")); + consumer.getDependencies().add(createDependency("g", "provided-dep", "provided")); + consumer.getDependencies().add(createDependency("g", "test-dep", "test")); + consumer.getDependencies().add(createDependency("g", "null-scope-dep", null)); + + // "compile" matches explicit compile + null-scoped (Maven default is compile) + List compileFiltered = + BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "compile"); + assertEquals(2, compileFiltered.size()); + assertTrue(compileFiltered.contains(compileDep)); + assertTrue(compileFiltered.contains(nullScopeDep)); + + // "provided" matches only provided-scoped + List providedFiltered = + BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "provided"); + assertEquals(1, providedFiltered.size()); + assertTrue(providedFiltered.contains(providedDep)); + + // "test" matches only test-scoped + List testFiltered = BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "test"); + assertEquals(1, testFiltered.size()); + assertTrue(testFiltered.contains(testDep)); + } + + /** + * Tests that {@code filterByScope} excludes upstream projects not declared as dependencies. + */ + @Test + void testFilterByScopeExcludesNonDependencies() { + MavenProject dep = createProjectWithId("g", "dep"); + MavenProject nonDep = createProjectWithId("g", "non-dep"); + List upstream = List.of(dep, nonDep); + + MavenProject consumer = new MavenProject(); + consumer.getDependencies().add(createDependency("g", "dep", "compile")); + + List filtered = BuildPlanExecutor.BuildContext.filterByScope(consumer, upstream, "compile"); + assertEquals(1, filtered.size()); + assertTrue(filtered.contains(dep)); + } + + private static MavenProject createProjectWithId(String groupId, String artifactId) { + MavenProject project = new MavenProject(); + project.setGroupId(groupId); + project.setArtifactId(artifactId); + project.setCollectedProjects(List.of()); + return project; + } + + private static Dependency createDependency(String groupId, String artifactId, String scope) { + Dependency dep = new Dependency(); + dep.setGroupId(groupId); + dep.setArtifactId(artifactId); + dep.setScope(scope); + return dep; + } + /* @Test void testPlugins() { diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng12534AfterAnnotationTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng12534AfterAnnotationTest.java new file mode 100644 index 000000000000..1a4b5800cf4c --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng12534AfterAnnotationTest.java @@ -0,0 +1,70 @@ +/* + * 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.it; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +/** + * This is a test for + * MNG-12534. + *

+ * Verifies that {@code afterLinks} in a V2 plugin descriptor + * ({@code http://maven.apache.org/PLUGIN/2.0.0}) are correctly loaded + * and processed by the build plan executor without errors. + *

+ * The test plugin carries a handcrafted V2 plugin.xml with a + * {@code } of type PROJECT. The build plan executor + * must parse the descriptor, create the ordering edges, and + * execute the mojo successfully. + */ +class MavenITmng12534AfterAnnotationTest extends AbstractMavenIntegrationTestCase { + + MavenITmng12534AfterAnnotationTest() { + super("[4.0.0-rc-6,)"); + } + + /** + * Verify that a plugin with {@code afterLinks} in its V2 descriptor + * is correctly loaded and the mojo executes under the concurrent builder. + */ + @Test + void testAfterLinksLoadedAndMojoExecutes() throws Exception { + File testDir = extractResources("/mng-12534-after-annotation"); + + // Step 1: install the test plugin with a handcrafted V2 plugin descriptor + Verifier pluginVerifier = newVerifier(new File(testDir, "plugin").getAbsolutePath()); + pluginVerifier.addCliArgument("install"); + pluginVerifier.execute(); + pluginVerifier.verifyErrorFreeLog(); + + // Step 2: build the consumer project using the concurrent builder + Verifier consumerVerifier = newVerifier(new File(testDir, "consumer").getAbsolutePath()); + consumerVerifier.addCliArgument("-b"); + consumerVerifier.addCliArgument("concurrent"); + consumerVerifier.addCliArgument("compile"); + consumerVerifier.execute(); + consumerVerifier.verifyErrorFreeLog(); + + // Verify the mojo actually executed + consumerVerifier.verifyTextInLog("[MNG-12534] touch goal executed - afterLinks wired correctly"); + consumerVerifier.verifyFilePresent("target/touch.txt"); + } +} diff --git a/its/core-it-suite/src/test/resources/mng-12534-after-annotation/consumer/pom.xml b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/consumer/pom.xml new file mode 100644 index 000000000000..23e8c5616163 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/consumer/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.apache.maven.its.mng12534 + consumer + 1.0-SNAPSHOT + + MNG-12534 Consumer + Consumes the test plugin with afterLinks + + + + + org.apache.maven.its.mng12534 + mng12534-plugin + 1.0-SNAPSHOT + + + touch + + touch + + + + + + + diff --git a/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/pom.xml b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/pom.xml new file mode 100644 index 000000000000..a1b3cc173238 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.apache.maven.its.mng12534 + mng12534-plugin + 1.0-SNAPSHOT + maven-plugin + MNG-12534 Test Plugin + V4 test plugin with afterLinks in V2 descriptor to verify @After annotation wiring + + + 17 + 4.0.0-SNAPSHOT + + + + + org.apache.maven + maven-api-core + ${mavenVersion} + provided + + + org.apache.maven + maven-api-di + ${mavenVersion} + provided + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 4.0.0-beta-1 + + + default-descriptor + none + + + + + + diff --git a/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/java/org/apache/maven/its/mng12534/TouchMojo.java b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/java/org/apache/maven/its/mng12534/TouchMojo.java new file mode 100644 index 000000000000..01ac60c4ef16 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/java/org/apache/maven/its/mng12534/TouchMojo.java @@ -0,0 +1,63 @@ +/* + * 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.its.mng12534; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.maven.api.Project; +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.plugin.annotations.After; +import org.apache.maven.api.plugin.annotations.Mojo; + +/** + * V4 Mojo that creates a marker file to prove the goal was executed. + * The {@code @After} annotation declares that this mojo must run after + * the "resources" phase. This is a real ordering constraint because + * in the V4 lifecycle, "compile" and "resources" are parallel siblings + * (compile depends on sources, not resources) — so without this + * {@code @After} link, compile could start before resources completes. + *

+ * The handcrafted V2 plugin descriptor mirrors this as an + * {@code } element — once maven-plugin-tools learns to + * scan {@code @After}, the descriptor will be generated automatically. + */ +@Mojo(name = "touch", defaultPhase = "compile") +@After(phase = "resources") +public class TouchMojo implements org.apache.maven.api.plugin.Mojo { + + @Inject + private Log log; + + @Inject + private Project project; + + @Override + public void execute() throws Exception { + log.info("[MNG-12534] touch goal executed - afterLinks wired correctly"); + Path targetDir = project.getBasedir().resolve("target"); + Files.createDirectories(targetDir); + Path touchFile = targetDir.resolve("touch.txt"); + if (!Files.exists(touchFile)) { + Files.createFile(touchFile); + } + } +} diff --git a/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/java/org/apache/maven/its/mng12534/TouchMojoFactory.java b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/java/org/apache/maven/its/mng12534/TouchMojoFactory.java new file mode 100644 index 000000000000..b4d1f3d58350 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/java/org/apache/maven/its/mng12534/TouchMojoFactory.java @@ -0,0 +1,32 @@ +/* + * 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.its.mng12534; + +import org.apache.maven.api.di.Named; + +/** + * DI factory for the TouchMojo. Normally maven-plugin-plugin generates this class + * at build time, but since we use a handcrafted V2 plugin descriptor (to include + * afterLinks), we must provide the factory manually. + * + * The @Named value must match MojoDescriptor.getRoleHint() at runtime: + * "groupId:artifactId:version:goal" + */ +@Named("org.apache.maven.its.mng12534:mng12534-plugin:1.0-SNAPSHOT:touch") +public class TouchMojoFactory extends TouchMojo {} diff --git a/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/resources/META-INF/maven/org.apache.maven.api.di.Inject b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/resources/META-INF/maven/org.apache.maven.api.di.Inject new file mode 100644 index 000000000000..450d5b29cc31 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/resources/META-INF/maven/org.apache.maven.api.di.Inject @@ -0,0 +1 @@ +org.apache.maven.its.mng12534.TouchMojoFactory diff --git a/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/resources/META-INF/maven/plugin.xml b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/resources/META-INF/maven/plugin.xml new file mode 100644 index 000000000000..1b4009ec3016 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-12534-after-annotation/plugin/src/main/resources/META-INF/maven/plugin.xml @@ -0,0 +1,23 @@ + + + MNG-12534 Test Plugin + org.apache.maven.its.mng12534 + mng12534-plugin + 1.0-SNAPSHOT + mng12534 + + + touch + compile + org.apache.maven.its.mng12534.TouchMojo + java + Creates a marker file; has afterLinks to test @After wiring + + + resources + PROJECT + + + + +