This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push:
new 8d918723d5 Fix incompatible extensions in mvnup for Maven 4
compatibility (#12120)
8d918723d5 is described below
commit 8d918723d5783768904ba1c987cd7a752d9a5fa2
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed May 20 16:30:44 2026 +0200
Fix incompatible extensions in mvnup for Maven 4 compatibility (#12120)
Add extension handling to mvnup that automatically fixes known
Maven 4-incompatible extensions in .mvn/extensions.xml:
- Replace os-maven-plugin (kr.motd.maven) with Maveniverse Nisse
(eu.maveniverse.maven.nisse:extension:0.4.4) which works with
both Maven 3 and 4. Also adds -Dnisse.compat.osDetector to
.mvn/maven.config for drop-in compatibility.
- Remove Develocity/Gradle Enterprise extensions (com.gradle)
which depend on org.slf4j.impl.SimpleLogger not available in
Maven 4.
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../invoker/mvnup/goals/AbstractUpgradeGoal.java | 104 ++++++++
.../mvnup/goals/AbstractUpgradeGoalTest.java | 261 +++++++++++++++++++++
2 files changed, 365 insertions(+)
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 2f4916f41f..a427ce595b 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<Path, Do
// This is needed for both 4.0.0 and 4.1.0 to help Maven find the
project root
createMvnDirectoryIfNeeded(context);
+ // Fix incompatible extensions in .mvn/extensions.xml
+ fixIncompatibleExtensions(context);
+
return result.errorPoms().isEmpty() ? 0 : 1;
} catch (Exception e) {
context.failure("Strategy execution failed: " + e.getMessage());
@@ -272,4 +279,101 @@ protected void createMvnDirectoryIfNeeded(UpgradeContext
context) {
context.failure("Failed to create .mvn directory: " +
e.getMessage());
}
}
+
+ /**
+ * Fixes incompatible extensions in .mvn/extensions.xml for Maven 4
compatibility.
+ *
+ * <ul>
+ * <li><strong>os-maven-plugin</strong>: 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.</li>
+ * <li><strong>Develocity/Gradle Enterprise extension</strong>: Removed
because it depends
+ * on {@code org.slf4j.impl.SimpleLogger} which is not available in
Maven 4.</li>
+ * </ul>
+ */
+ 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<Element> extensions = root.children("extension").toList();
+ List<Element> 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 969b0cb43e..a3b7a86b2a 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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>1.7.1</version>
+ </extension>
+ </extensions>
+ """;
+ 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("<artifactId>extension</artifactId>"),
"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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>com.gradle</groupId>
+ <artifactId>develocity-maven-extension</artifactId>
+ <version>1.21</version>
+ </extension>
+ </extensions>
+ """;
+ 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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>com.gradle</groupId>
+
<artifactId>gradle-enterprise-maven-extension</artifactId>
+ <version>1.18</version>
+ </extension>
+ </extensions>
+ """;
+ 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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>1.7.1</version>
+ </extension>
+ <extension>
+ <groupId>com.gradle</groupId>
+ <artifactId>develocity-maven-extension</artifactId>
+ <version>1.21</version>
+ </extension>
+ <extension>
+ <groupId>org.apache.maven.extensions</groupId>
+
<artifactId>maven-build-cache-extension</artifactId>
+ <version>1.0.0</version>
+ </extension>
+ </extensions>
+ """;
+ 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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>1.7.1</version>
+ </extension>
+ </extensions>
+ """;
+ 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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>1.7.1</version>
+ </extension>
+ </extensions>
+ """;
+ 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 = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <extensions>
+ <extension>
+ <groupId>org.apache.maven.extensions</groupId>
+
<artifactId>maven-build-cache-extension</artifactId>
+ <version>1.0.0</version>
+ </extension>
+ </extensions>
+ """;
+ 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.
*/