diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java index 2f4916f41fa6..a427ce595bca 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoal.java @@ -22,10 +22,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import eu.maveniverse.domtrip.Document; import eu.maveniverse.domtrip.DomTripException; +import eu.maveniverse.domtrip.Element; +import eu.maveniverse.domtrip.Parser; import eu.maveniverse.domtrip.maven.MavenPomElements; import org.apache.maven.api.cli.mvnup.UpgradeOptions; import org.apache.maven.api.di.Inject; @@ -208,6 +212,9 @@ protected int doUpgrade(UpgradeContext context, String targetModel, Map + *
  • os-maven-plugin: Replaced with Maveniverse Nisse extension + * (compatible with both Maven 3 and 4). Also adds {@code -Dnisse.compat.osDetector} + * to {@code .mvn/maven.config} for drop-in compatibility.
  • + *
  • Develocity/Gradle Enterprise extension: Removed because it depends + * on {@code org.slf4j.impl.SimpleLogger} which is not available in Maven 4.
  • + * + */ + protected void fixIncompatibleExtensions(UpgradeContext context) { + Path startingDirectory = context.options().directory().map(Paths::get).orElse(context.invokerRequest.cwd()); + Path extensionsXml = startingDirectory.resolve(MVN_DIRECTORY).resolve("extensions.xml"); + + if (!Files.exists(extensionsXml)) { + return; + } + + context.info(""); + context.info("Checking .mvn/extensions.xml for Maven 4 incompatible extensions..."); + context.indent(); + + try { + String content = Files.readString(extensionsXml); + Document doc = new Parser().parse(content); + Element root = doc.root(); + boolean modified = false; + boolean needsNisseCompat = false; + + List extensions = root.children("extension").toList(); + List toRemove = new ArrayList<>(); + + for (Element ext : extensions) { + String groupId = ext.childTextTrimmed("groupId"); + String artifactId = ext.childTextTrimmed("artifactId"); + + if ("kr.motd.maven".equals(groupId) && "os-maven-plugin".equals(artifactId)) { + DomUtils.updateOrCreateChildElement(ext, "groupId", "eu.maveniverse.maven.nisse"); + DomUtils.updateOrCreateChildElement(ext, "artifactId", "extension"); + DomUtils.updateOrCreateChildElement(ext, "version", "0.4.4"); + context.detail( + "Replaced kr.motd.maven:os-maven-plugin with eu.maveniverse.maven.nisse:extension:0.4.4"); + modified = true; + needsNisseCompat = true; + } else if ("com.gradle".equals(groupId) + && ("develocity-maven-extension".equals(artifactId) + || "gradle-enterprise-maven-extension".equals(artifactId))) { + toRemove.add(ext); + context.detail("Removed incompatible extension: " + groupId + ":" + artifactId); + modified = true; + } + } + + for (Element ext : toRemove) { + DomUtils.removeElement(ext); + } + + if (modified) { + if (shouldSaveModifications()) { + String modifiedXml = DomUtils.toXml(doc); + Files.writeString(extensionsXml, modifiedXml); + context.success("Updated .mvn/extensions.xml"); + + if (needsNisseCompat) { + addNisseCompatFlag(startingDirectory, context); + } + } else { + context.action("Would update .mvn/extensions.xml"); + if (needsNisseCompat) { + context.action("Would add -Dnisse.compat.osDetector to .mvn/maven.config"); + } + } + } else { + context.success("No incompatible extensions found"); + } + } catch (Exception e) { + context.failure("Failed to process .mvn/extensions.xml: " + e.getMessage()); + } finally { + context.unindent(); + } + } + + private void addNisseCompatFlag(Path startingDirectory, UpgradeContext context) { + Path mavenConfig = startingDirectory.resolve(MVN_DIRECTORY).resolve("maven.config"); + try { + String configContent = Files.exists(mavenConfig) ? Files.readString(mavenConfig) : ""; + if (!configContent.contains("-Dnisse.compat.osDetector")) { + String separator = configContent.isEmpty() || configContent.endsWith("\n") ? "" : "\n"; + Files.writeString(mavenConfig, configContent + separator + "-Dnisse.compat.osDetector\n"); + context.success("Added -Dnisse.compat.osDetector to .mvn/maven.config"); + } + } catch (IOException e) { + context.failure("Failed to update .mvn/maven.config: " + e.getMessage()); + } + } } diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java index 969b0cb43ec5..a3b7a86b2ad2 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeGoalTest.java @@ -38,6 +38,7 @@ import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -272,6 +273,266 @@ void shouldHandleMvnDirectoryCreationFailureGracefully() throws Exception { } } + @Nested + @DisplayName("Incompatible Extension Fixes") + class IncompatibleExtensionFixTests { + + @Test + @DisplayName("should replace os-maven-plugin with Maveniverse Nisse") + void shouldReplaceOsMavenPluginWithNisse() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String result = Files.readString(mvnDir.resolve("extensions.xml")); + assertTrue(result.contains("eu.maveniverse.maven.nisse"), "Should contain Nisse groupId"); + assertTrue(result.contains("extension"), "Should contain Nisse artifactId"); + assertTrue(result.contains("0.4.4"), "Should contain Nisse version"); + assertFalse(result.contains("kr.motd.maven"), "Should not contain os-maven-plugin groupId"); + assertFalse(result.contains("os-maven-plugin"), "Should not contain os-maven-plugin artifactId"); + + // Should also create maven.config with Nisse compat flag + Path mavenConfig = mvnDir.resolve("maven.config"); + assertTrue(Files.exists(mavenConfig), "maven.config should be created"); + String configContent = Files.readString(mavenConfig); + assertTrue( + configContent.contains("-Dnisse.compat.osDetector"), + "maven.config should contain Nisse compat flag"); + } + + @Test + @DisplayName("should remove Develocity extension") + void shouldRemoveDevelocityExtension() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + com.gradle + develocity-maven-extension + 1.21 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String result = Files.readString(mvnDir.resolve("extensions.xml")); + assertFalse(result.contains("develocity-maven-extension"), "Should not contain Develocity extension"); + assertFalse(result.contains("com.gradle"), "Should not contain com.gradle groupId"); + } + + @Test + @DisplayName("should remove Gradle Enterprise extension") + void shouldRemoveGradleEnterpriseExtension() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + com.gradle + gradle-enterprise-maven-extension + 1.18 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String result = Files.readString(mvnDir.resolve("extensions.xml")); + assertFalse( + result.contains("gradle-enterprise-maven-extension"), + "Should not contain Gradle Enterprise extension"); + } + + @Test + @DisplayName("should handle both os-maven-plugin and Develocity together") + void shouldHandleBothOsMavenPluginAndDevelocity() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + com.gradle + develocity-maven-extension + 1.21 + + + org.apache.maven.extensions + maven-build-cache-extension + 1.0.0 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String result = Files.readString(mvnDir.resolve("extensions.xml")); + assertTrue(result.contains("eu.maveniverse.maven.nisse"), "Should contain Nisse replacement"); + assertFalse(result.contains("develocity-maven-extension"), "Should not contain Develocity"); + assertTrue(result.contains("maven-build-cache-extension"), "Should preserve compatible extensions"); + } + + @Test + @DisplayName("should append to existing maven.config") + void shouldAppendToExistingMavenConfig() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + Files.writeString(mvnDir.resolve("maven.config"), "-Xmx2g\n"); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String configContent = Files.readString(mvnDir.resolve("maven.config")); + assertTrue(configContent.contains("-Xmx2g"), "Should preserve existing config"); + assertTrue(configContent.contains("-Dnisse.compat.osDetector"), "Should add Nisse compat flag"); + } + + @Test + @DisplayName("should not duplicate Nisse compat flag in maven.config") + void shouldNotDuplicateNisseCompatFlag() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + Files.writeString(mvnDir.resolve("maven.config"), "-Dnisse.compat.osDetector\n"); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String configContent = Files.readString(mvnDir.resolve("maven.config")); + int count = configContent.split("-Dnisse.compat.osDetector", -1).length - 1; + assertEquals(1, count, "Should not duplicate Nisse compat flag"); + } + + @Test + @DisplayName("should be no-op when no extensions.xml exists") + void shouldBeNoOpWhenNoExtensionsXml() throws Exception { + Path projectDir = tempDir.resolve("project"); + Files.createDirectories(projectDir); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + int result = upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + assertEquals(0, result, "Should succeed with no extensions.xml"); + assertFalse( + Files.exists(projectDir.resolve(".mvn/maven.config")), + "Should not create maven.config when no extensions needed fixing"); + } + + @Test + @DisplayName("should be no-op when no incompatible extensions found") + void shouldBeNoOpWhenNoIncompatibleExtensions() throws Exception { + Path projectDir = tempDir.resolve("project"); + Path mvnDir = projectDir.resolve(".mvn"); + Files.createDirectories(mvnDir); + + String extensionsXml = """ + + + + org.apache.maven.extensions + maven-build-cache-extension + 1.0.0 + + + """; + Files.writeString(mvnDir.resolve("extensions.xml"), extensionsXml); + + UpgradeContext context = createMockContext(projectDir); + when(mockOrchestrator.executeStrategies(Mockito.any(), Mockito.any())) + .thenReturn(UpgradeResult.empty()); + + upgradeGoal.testExecuteWithTargetModel(context, "4.0.0"); + + String result = Files.readString(mvnDir.resolve("extensions.xml")); + assertTrue(result.contains("maven-build-cache-extension"), "Should preserve compatible extensions"); + assertFalse( + Files.exists(mvnDir.resolve("maven.config")), + "Should not create maven.config when no extensions needed fixing"); + } + } + /** * Testable subclass that exposes protected methods for testing. */