diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java index a0b9ff8a0aaf..50b625e2cc8d 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java @@ -227,6 +227,64 @@ public String id() { @Override public Collection phases() { + List phases = new ArrayList<>(buildOwnPhases()); + // Also include phases for default-phases entries that reference phases + // not defined in this lifecycle (e.g., standard lifecycle phases like + // process-sources). This preserves plugin bindings from components.xml + // that map goals to standard lifecycle phases. + Map lfPhases = lifecycle.getDefaultLifecyclePhases(); + if (lfPhases != null) { + Set ownPhaseNames = new HashSet<>(lifecycle.getPhases()); + for (Map.Entry entry : lfPhases.entrySet()) { + if (!ownPhaseNames.contains(entry.getKey())) { + String phaseName = entry.getKey(); + LifecyclePhase lfPhase = entry.getValue(); + phases.add(new Phase() { + @Override + public String name() { + return phaseName; + } + + @Override + public List phases() { + return List.of(); + } + + @Override + public Stream allPhases() { + return Stream.of(this); + } + + @Override + public List plugins() { + Map plugins = new LinkedHashMap<>(); + DefaultPackagingRegistry.parseLifecyclePhaseDefinitions( + plugins, phaseName, lfPhase); + return plugins.values().stream().toList(); + } + + @Override + public Collection links() { + return List.of(); + } + }); + } + } + } + return phases; + } + + @Override + public Collection v3phases() { + return buildOwnPhases(); + } + + @Override + public Collection aliases() { + return Collections.emptyList(); + } + + private List buildOwnPhases() { List names = lifecycle.getPhases(); List phases = new ArrayList<>(); for (int i = 0; i < names.size(); i++) { @@ -293,11 +351,6 @@ public Type type() { } return phases; } - - @Override - public Collection aliases() { - return Collections.emptyList(); - } }; } } diff --git a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java index 0b36378871a7..7754e8b96e2d 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java @@ -23,18 +23,22 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.maven.internal.impl.DefaultLifecycleRegistry; import org.apache.maven.internal.impl.DefaultLookup; +import org.apache.maven.lifecycle.mapping.LifecyclePhase; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.codehaus.plexus.testing.PlexusTest; 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; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -96,6 +100,52 @@ void testCustomLifecycle() throws ComponentLookupException { assertEquals("etl", dl.getLifeCycles().get(3).getId()); } + /** + * Test for MNG-11796: + * custom lifecycle with {@code } binding goals to standard lifecycle phases. + */ + @Test + void testCustomLifecycleDefaultPhasesForStandardPhases() throws ComponentLookupException { + // Create a custom lifecycle with default-phases mapping to a standard lifecycle phase + Map defaultPhases = new HashMap<>(); + defaultPhases.put("process-sources", new LifecyclePhase("com.example:my-plugin:touch")); + + Lifecycle customLifecycle = new Lifecycle("my-extension", Arrays.asList("my-dummy-phase"), defaultPhases); + + List allLifecycles = new ArrayList<>(); + allLifecycles.add(customLifecycle); + allLifecycles.addAll(defaultLifeCycles.getLifeCycles()); + + Map lifeCycles = allLifecycles.stream().collect(Collectors.toMap(Lifecycle::getId, l -> l)); + PlexusContainer mockedPlexusContainer = mock(PlexusContainer.class); + when(mockedPlexusContainer.lookupMap(Lifecycle.class)).thenReturn(lifeCycles); + + DefaultLifecycles dl = new DefaultLifecycles( + new DefaultLifecycleRegistry( + List.of(new DefaultLifecycleRegistry.LifecycleWrapperProvider(mockedPlexusContainer))), + new DefaultLookup(mockedPlexusContainer)); + + // Find the custom lifecycle + Lifecycle result = dl.getLifeCycles().stream() + .filter(l -> "my-extension".equals(l.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("Custom lifecycle not found")); + + // Verify that getPhases() only contains the custom phase (not standard phases) + assertTrue(result.getPhases().contains("my-dummy-phase"), "Custom lifecycle should contain its own phase"); + + // Verify that defaultLifecyclePhases includes the standard phase binding + Map resultDefaultPhases = result.getDefaultLifecyclePhases(); + assertNotNull(resultDefaultPhases, "Default lifecycle phases should not be null"); + assertTrue( + resultDefaultPhases.containsKey("process-sources"), + "Default lifecycle phases should contain 'process-sources' binding"); + String goalSpec = resultDefaultPhases.get("process-sources").toString(); + assertTrue( + goalSpec.contains("com.example") && goalSpec.contains("my-plugin") && goalSpec.contains("touch"), + "The process-sources binding should map to the correct goal, got: " + goalSpec); + } + private Lifecycle getLifeCycleById(String id) { return defaultLifeCycles.getLifeCycles().stream() .filter(l -> id.equals(l.getId())) diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng11796DefaultPhasesStandardLifecycleTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng11796DefaultPhasesStandardLifecycleTest.java new file mode 100644 index 000000000000..f1b96b05cc12 --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng11796DefaultPhasesStandardLifecycleTest.java @@ -0,0 +1,57 @@ +/* + * 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-11796. + *

+ * Verifies that {@code } in a custom lifecycle's {@code components.xml} + * correctly binds plugin goals to standard lifecycle phases (e.g., {@code process-sources}). + */ +class MavenITmng11796DefaultPhasesStandardLifecycleTest extends AbstractMavenIntegrationTestCase { + + /** + * Verify that a plugin extension with {@code } mapping a goal + * to the standard {@code process-sources} phase causes that goal to execute + * during {@code mvn compile}. + */ + @Test + void testDefaultPhasesBindToStandardLifecyclePhases() throws Exception { + File testDir = extractResources("/mng-11796-default-phases-standard-lifecycle"); + + // Install the extension plugin + Verifier verifier = newVerifier(new File(testDir, "extension-plugin").getAbsolutePath()); + verifier.addCliArgument("install"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // Run compile on the consumer project - the touch goal should execute at process-sources + verifier = newVerifier(new File(testDir, "consumer-project").getAbsolutePath()); + verifier.addCliArgument("compile"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog("MNG-11796 touch goal executed"); + verifier.verifyFilePresent("target/touch.txt"); + } +} diff --git a/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/consumer-project/pom.xml b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/consumer-project/pom.xml new file mode 100644 index 000000000000..75b1e9ef8dd6 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/consumer-project/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + org.apache.maven.its.mng11796 + consumer-project + 1.0-SNAPSHOT + jar + + MNG-11796 Consumer Project + + + + + org.apache.maven.its.mng11796 + extension-plugin + 1.0-SNAPSHOT + true + + + + diff --git a/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/pom.xml b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/pom.xml new file mode 100644 index 000000000000..68be77394e15 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.apache.maven.its.mng11796 + extension-plugin + 1.0-SNAPSHOT + maven-plugin + MNG-11796 Extension Plugin + + + + org.apache.maven + maven-plugin-api + 3.2.5 + provided + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.4 + + + + descriptor + + + mng11796 + + + + + + + diff --git a/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/java/org/apache/maven/its/mng11796/TouchMojo.java b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/java/org/apache/maven/its/mng11796/TouchMojo.java new file mode 100644 index 000000000000..abed805eaf0e --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/java/org/apache/maven/its/mng11796/TouchMojo.java @@ -0,0 +1,47 @@ +/* + * 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.mng11796; + +import java.io.File; +import java.io.IOException; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; + +/** + * Creates a marker file to prove the goal was executed. + * @goal touch + */ +public class TouchMojo extends AbstractMojo { + /** + * @parameter default-value="${project.build.directory}" + */ + private File outputDirectory; + + public void execute() throws MojoExecutionException { + getLog().info("MNG-11796 touch goal executed"); + File touchFile = new File(outputDirectory, "touch.txt"); + touchFile.getParentFile().mkdirs(); + try { + touchFile.createNewFile(); + } catch (IOException e) { + throw new MojoExecutionException("Failed to create touch file", e); + } + } +} diff --git a/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/resources/META-INF/plexus/components.xml b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 000000000000..dffe42cf06f5 --- /dev/null +++ b/its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,20 @@ + + + + org.apache.maven.lifecycle.Lifecycle + mng11796 + org.apache.maven.lifecycle.Lifecycle + + mng11796 + + mng11796-dummy-phase + + + + org.apache.maven.its.mng11796:extension-plugin:touch + + + + + +