This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch ci-issue-11796-default-phases in repository https://gitbox.apache.org/repos/asf/maven.git
commit ce628236122c7a85c91447258f67aa96a9b33fae Author: Guillaume Nodet <[email protected]> AuthorDate: Fri Apr 3 23:09:46 2026 +0200 Fix #11796: Preserve cross-lifecycle default phase bindings from components.xml When a plugin registers a custom lifecycle via components.xml with <default-phases> binding goals to standard lifecycle phases (e.g. process-sources), these cross-lifecycle bindings were silently dropped during the legacy-to-API Lifecycle conversion. The wrap() method only created Phase objects for the custom lifecycle's own phases, losing any bindings to phases from other lifecycles. The fix separates v3phases() (used by computePhases() for phase ordering) from phases() (used by allPhases() for extracting plugin bindings). Cross-lifecycle phase bindings are now included in phases() so they survive the round-trip conversion back to a legacy Lifecycle, while v3phases() returns only the lifecycle's own phases to avoid polluting the phase-to-lifecycle map. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../internal/impl/DefaultLifecycleRegistry.java | 138 +++++++++++++-------- .../maven/lifecycle/DefaultLifecyclesTest.java | 48 +++++++ 2 files changed, 131 insertions(+), 55 deletions(-) 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 a0b9ff8a0a..a5532a69a3 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 @@ -225,73 +225,101 @@ public String id() { return lifecycle.getId(); } + @Override + public Collection<Phase> v3phases() { + return buildOwnPhases(); + } + @Override public Collection<Phase> phases() { + List<Phase> phases = new ArrayList<>(buildOwnPhases()); + // Include phases from getDefaultLifecyclePhases() that bind to phases + // from other lifecycles (e.g. process-sources from the default lifecycle). + // In Maven 3, <default-phases> could bind plugin goals to standard lifecycle + // phases. These cross-lifecycle bindings must be preserved so they survive + // the round-trip conversion back to a legacy Lifecycle. + Map<String, LifecyclePhase> defaultPhases = lifecycle.getDefaultLifecyclePhases(); + if (defaultPhases != null) { + Set<String> ownPhases = new HashSet<>(lifecycle.getPhases()); + for (String phaseName : defaultPhases.keySet()) { + if (!ownPhases.contains(phaseName)) { + phases.add(createPhase(phaseName, null)); + } + } + } + return phases; + } + + private List<Phase> buildOwnPhases() { List<String> names = lifecycle.getPhases(); List<Phase> phases = new ArrayList<>(); for (int i = 0; i < names.size(); i++) { String name = names.get(i); String prev = i > 0 ? names.get(i - 1) : null; - phases.add(new Phase() { - @Override - public String name() { - return name; - } - - @Override - public List<Phase> phases() { - return List.of(); - } + phases.add(createPhase(name, prev)); + } + return phases; + } - @Override - public Stream<Phase> allPhases() { - return Stream.concat( - Stream.of(this), phases().stream().flatMap(Lifecycle.Phase::allPhases)); + private Phase createPhase(String name, String prev) { + return new Phase() { + @Override + public String name() { + return name; + } + + @Override + public List<Phase> phases() { + return List.of(); + } + + @Override + public Stream<Phase> allPhases() { + return Stream.concat( + Stream.of(this), phases().stream().flatMap(Lifecycle.Phase::allPhases)); + } + + @Override + public List<Plugin> plugins() { + Map<String, LifecyclePhase> lfPhases = lifecycle.getDefaultLifecyclePhases(); + LifecyclePhase phase = lfPhases != null ? lfPhases.get(name) : null; + if (phase != null) { + Map<String, Plugin> plugins = new LinkedHashMap<>(); + DefaultPackagingRegistry.parseLifecyclePhaseDefinitions(plugins, name, phase); + return plugins.values().stream().toList(); } + return List.of(); + } - @Override - public List<Plugin> plugins() { - Map<String, LifecyclePhase> lfPhases = lifecycle.getDefaultLifecyclePhases(); - LifecyclePhase phase = lfPhases != null ? lfPhases.get(name) : null; - if (phase != null) { - Map<String, Plugin> plugins = new LinkedHashMap<>(); - DefaultPackagingRegistry.parseLifecyclePhaseDefinitions(plugins, name, phase); - return plugins.values().stream().toList(); - } + @Override + public Collection<Link> links() { + if (prev == null) { return List.of(); + } else { + return List.of(new Link() { + @Override + public Kind kind() { + return Kind.AFTER; + } + + @Override + public Pointer pointer() { + return new Pointer() { + @Override + public String phase() { + return prev; + } + + @Override + public Type type() { + return Type.PROJECT; + } + }; + } + }); } - - @Override - public Collection<Link> links() { - if (prev == null) { - return List.of(); - } else { - return List.of(new Link() { - @Override - public Kind kind() { - return Kind.AFTER; - } - - @Override - public Pointer pointer() { - return new Pointer() { - @Override - public String phase() { - return prev; - } - - @Override - public Type type() { - return Type.PROJECT; - } - }; - } - }); - } - } - }); - } - return phases; + } + }; } @Override 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 0b36378871..d5a3b9433c 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,23 @@ 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.assertFalse; +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 +101,49 @@ void testCustomLifecycle() throws ComponentLookupException { assertEquals("etl", dl.getLifeCycles().get(3).getId()); } + @Test + void testCustomLifecycleWithCrossLifecycleDefaultPhases() throws ComponentLookupException { + // Simulates a plugin that registers a custom lifecycle via components.xml + // with <default-phases> binding goals to standard lifecycle phases (e.g. process-sources) + // rather than to phases of the custom lifecycle itself. This is the Maven 3 mechanism + // for extension plugins to bind goals to standard phases without requiring <executions>. + Map<String, LifecyclePhase> defaultPhases = new HashMap<>(); + defaultPhases.put("process-sources", new LifecyclePhase("com.example:my-plugin:1.0:touch")); + + Lifecycle customLifecycle = new Lifecycle("my-custom-lifecycle", Arrays.asList("custom-phase"), defaultPhases); + + List<Lifecycle> myLifecycles = new ArrayList<>(); + myLifecycles.add(customLifecycle); + myLifecycles.addAll(defaultLifeCycles.getLifeCycles()); + + Map<String, Lifecycle> lifeCycles = myLifecycles.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)); + + Lifecycle resolved = dl.getLifeCycles().stream() + .filter(l -> "my-custom-lifecycle".equals(l.getId())) + .findFirst() + .orElseThrow(); + + // Cross-lifecycle default phase bindings must survive the round-trip conversion + Map<String, LifecyclePhase> resolvedDefaultPhases = resolved.getDefaultLifecyclePhases(); + assertNotNull(resolvedDefaultPhases); + assertTrue( + resolvedDefaultPhases.containsKey("process-sources"), + "Cross-lifecycle binding to 'process-sources' should be preserved"); + + // The lifecycle's own phase list should NOT include cross-lifecycle phases + assertFalse( + resolved.getPhases().contains("process-sources"), + "Cross-lifecycle phase should not appear in the lifecycle's own phase list"); + assertTrue(resolved.getPhases().contains("custom-phase"), "Lifecycle's own phase should be present"); + } + private Lifecycle getLifeCycleById(String id) { return defaultLifeCycles.getLifeCycles().stream() .filter(l -> id.equals(l.getId()))
