From 27affff09090708dc70d09563ec60324dcecc323 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 3 Apr 2026 22:58:06 +0200 Subject: [PATCH] Fix #11796: Preserve default-phases bindings for standard lifecycle phases When a custom lifecycle registered via components.xml maps plugin goals to standard lifecycle phases (e.g., process-sources) via , the LifecycleWrapperProvider now includes those entries as additional phases in the wrapped API Lifecycle. The v3phases() method is overridden to return only the custom lifecycle's own phases, ensuring computePhases() is unaffected while getDefaultPhases() correctly picks up the bindings. Co-Authored-By: Claude Opus 4.6 --- .../impl/DefaultLifecycleRegistry.java | 63 +++++++++++++++++-- .../lifecycle/DefaultLifecyclesTest.java | 50 +++++++++++++++ ...796DefaultPhasesStandardLifecycleTest.java | 57 +++++++++++++++++ .../consumer-project/pom.xml | 22 +++++++ .../extension-plugin/pom.xml | 39 ++++++++++++ .../apache/maven/its/mng11796/TouchMojo.java | 47 ++++++++++++++ .../resources/META-INF/plexus/components.xml | 20 ++++++ 7 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng11796DefaultPhasesStandardLifecycleTest.java create mode 100644 its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/consumer-project/pom.xml create mode 100644 its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/pom.xml create mode 100644 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 create mode 100644 its/core-it-suite/src/test/resources/mng-11796-default-phases-standard-lifecycle/extension-plugin/src/main/resources/META-INF/plexus/components.xml 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 + + + + + +