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..a5532a69a3f4 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 v3phases() { + return buildOwnPhases(); + } + @Override public Collection phases() { + List 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, 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 defaultPhases = lifecycle.getDefaultLifecyclePhases(); + if (defaultPhases != null) { + Set ownPhases = new HashSet<>(lifecycle.getPhases()); + for (String phaseName : defaultPhases.keySet()) { + if (!ownPhases.contains(phaseName)) { + phases.add(createPhase(phaseName, null)); + } + } + } + return phases; + } + + private List buildOwnPhases() { List names = lifecycle.getPhases(); List 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 phases() { - return List.of(); - } + phases.add(createPhase(name, prev)); + } + return phases; + } - @Override - public Stream 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 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(); + LifecyclePhase phase = lfPhases != null ? lfPhases.get(name) : null; + if (phase != null) { + Map plugins = new LinkedHashMap<>(); + DefaultPackagingRegistry.parseLifecyclePhaseDefinitions(plugins, name, phase); + return plugins.values().stream().toList(); } + return List.of(); + } - @Override - public List plugins() { - Map lfPhases = lifecycle.getDefaultLifecyclePhases(); - LifecyclePhase phase = lfPhases != null ? lfPhases.get(name) : null; - if (phase != null) { - Map plugins = new LinkedHashMap<>(); - DefaultPackagingRegistry.parseLifecyclePhaseDefinitions(plugins, name, phase); - return plugins.values().stream().toList(); - } + @Override + public Collection 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 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 0b36378871a7..d5a3b9433ccb 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 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 . + Map 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 myLifecycles = new ArrayList<>(); + myLifecycles.add(customLifecycle); + myLifecycles.addAll(defaultLifeCycles.getLifeCycles()); + + Map 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 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()))