You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/06 08:53:16 UTC
(maven) branch fix-module-name-strategy created (now f78d16a6fd)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a change to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
at f78d16a6fd Add ModuleNameFixStrategy to fix invalid Automatic-Module-Name entries
This branch includes the following new commits:
new 0321e93b3e Auto-select JDK toolchain when running JDK cannot compile the project's source level
new ef443e5780 Add integration tests for automatic JDK toolchain selection
new cfc0ba21a0 Remove public modifiers from IT class and methods
new a800a88ca0 Add lazy JDK discovery from filesystem for auto-selection fallback
new 3eb3fdd5ff Use Session properties instead of System.* in JdkToolchainDiscoverer
new 209791942b Fix auto-toolchain selection: compiler plugin config, compat layer, and JDK discovery
new cc0c0409ea Fix DefaultToolchainManager(Map) constructor visibility for IT harness
new 7c25b1d3e3 Fix DefaultToolchainManager constructor visibility for compat layer and IT harness
new eaf607bdc3 Simplify JDK source-level check: replace auto-discovery with clear error
new 9c857e8b19 Add mvnup ToolchainPluginStrategy: auto-add maven-toolchains-plugin
new 526ef4d459 Add version constraint to select-jdk-toolchain configuration
new 1ac850b3d6 Add integration test for mvnup ToolchainPluginStrategy
new 0e13f82312 Fix mvnup ToolchainPluginStrategy producing invalid version range (,-1]
new f78d16a6fd Add ModuleNameFixStrategy to fix invalid Automatic-Module-Name entries
The 14 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven) 03/14: Remove public modifiers from IT class and methods
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit cfc0ba21a0ec166b2579149c09a50c36c0cb4f23
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 31 07:44:29 2026 +0200
Remove public modifiers from IT class and methods
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
index 10d0776ce3..894d832ff4 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
@@ -34,7 +34,7 @@
* When the running JDK does not support the project's required {@code --source}/{@code --release}
* level, Maven should automatically search configured toolchains and select a compatible JDK.
*/
-public class MavenITAutoJdkToolchainSelectTest extends AbstractMavenIntegrationTestCase {
+class MavenITAutoJdkToolchainSelectTest extends AbstractMavenIntegrationTestCase {
/**
* Verifies that Maven auto-selects a JDK toolchain when the running JDK
@@ -45,7 +45,7 @@ public class MavenITAutoJdkToolchainSelectTest extends AbstractMavenIntegrationT
* A JDK 11 toolchain is configured in toolchains.xml and should be auto-selected.
*/
@Test
- public void testAutoSelectToolchainWhenSourceLevelUnsupported() throws Exception {
+ void testAutoSelectToolchainWhenSourceLevelUnsupported() throws Exception {
Path testDir = extractResources("auto-jdk-toolchain-select");
// Create a fake JDK home with bin/javac for the toolchain
@@ -96,7 +96,7 @@ public void testAutoSelectToolchainWhenSourceLevelUnsupported() throws Exception
* by JDK 12+ (all current CI JDKs). No auto-selection should occur.
*/
@Test
- public void testNoAutoSelectWhenSourceLevelSupported() throws Exception {
+ void testNoAutoSelectWhenSourceLevelSupported() throws Exception {
Path testDir = extractResources("auto-jdk-toolchain-no-select");
// Create a fake JDK home (needed to create a valid toolchain)
(maven) 02/14: Add integration tests for automatic JDK toolchain selection
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit ef443e5780a6af97730e70d396569c402b3f0894
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Thu Jul 30 17:43:18 2026 +0200
Add integration tests for automatic JDK toolchain selection
Two ITs verify the auto-selection behavior end-to-end through Maven's
compat bridge:
- testAutoSelectToolchainWhenSourceLevelUnsupported: project requires
source 6 (unsupported by JDK 12+), verifies Maven auto-selects a
compatible JDK 11 toolchain and logs a warning
- testNoAutoSelectWhenSourceLevelSupported: project requires source 11
(supported by JDK 12+), verifies no auto-selection occurs
Also adds debug logging to the auto-selection path for diagnostics.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../apache/maven/impl/DefaultToolchainManager.java | 5 +
.../it/MavenITAutoJdkToolchainSelectTest.java | 141 +++++++++++++++++++++
.../resources/auto-jdk-toolchain-no-select/pom.xml | 61 +++++++++
.../auto-jdk-toolchain-no-select/toolchains.xml | 13 ++
.../resources/auto-jdk-toolchain-select/pom.xml | 61 +++++++++
.../auto-jdk-toolchain-select/toolchains.xml | 13 ++
6 files changed, 294 insertions(+)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index ec87370e32..88430004eb 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -124,11 +124,16 @@ public void storeToolchainToBuildContext(@Nonnull Session session, @Nonnull Tool
*/
Optional<Toolchain> autoSelectJdkToolchain(Session session) {
int requiredSourceLevel = getProjectRequiredSourceLevel(session);
+ logger.debug("Auto-select JDK toolchain: requiredSourceLevel={}", requiredSourceLevel);
if (requiredSourceLevel <= 0) {
return Optional.empty();
}
int runningJdkMajor = getRunningJdkMajor();
+ logger.debug(
+ "Auto-select JDK toolchain: runningJdkMajor={}, supportsLevel={}",
+ runningJdkMajor,
+ JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, requiredSourceLevel));
if (JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, requiredSourceLevel)) {
return Optional.empty();
}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
new file mode 100644
index 0000000000..10d0776ce3
--- /dev/null
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Integration tests for automatic JDK toolchain selection.
+ * <p>
+ * When the running JDK does not support the project's required {@code --source}/{@code --release}
+ * level, Maven should automatically search configured toolchains and select a compatible JDK.
+ */
+public class MavenITAutoJdkToolchainSelectTest extends AbstractMavenIntegrationTestCase {
+
+ /**
+ * Verifies that Maven auto-selects a JDK toolchain when the running JDK
+ * does not support the project's required source level.
+ * <p>
+ * The project declares {@code maven.compiler.source=6}, which is not supported
+ * by JDK 12+ (minimum source level 7 for JDK 12-20, 8 for JDK 21+).
+ * A JDK 11 toolchain is configured in toolchains.xml and should be auto-selected.
+ */
+ @Test
+ public void testAutoSelectToolchainWhenSourceLevelUnsupported() throws Exception {
+ Path testDir = extractResources("auto-jdk-toolchain-select");
+
+ // Create a fake JDK home with bin/javac for the toolchain
+ Path javaHome = testDir.resolve("fakeJdk11");
+ Path binDir = javaHome.resolve("bin");
+ Files.createDirectories(binDir);
+ if (!Files.exists(binDir.resolve("javac"))) {
+ ItUtils.createFile(binDir.resolve("javac"));
+ }
+ if (!Files.exists(binDir.resolve("javac.exe"))) {
+ ItUtils.createFile(binDir.resolve("javac.exe"));
+ }
+
+ Verifier verifier = newVerifier(testDir);
+ // Clear the default compiler properties set by newVerifier() — the POM defines
+ // maven.compiler.source=6 and we need the effective model to reflect that, not
+ // the system property override of 8 from MAVEN_OPTS.
+ verifier.getSystemProperties().remove("maven.compiler.source");
+ verifier.getSystemProperties().remove("maven.compiler.target");
+ verifier.getSystemProperties().remove("maven.compiler.release");
+
+ Map<String, String> filterProps = verifier.newDefaultFilterMap();
+ filterProps.put("@javaHome@", javaHome.toString());
+ verifier.filterFile("toolchains.xml", "toolchains.xml", filterProps);
+
+ verifier.setAutoclean(false);
+ verifier.deleteDirectory("target");
+ verifier.addCliArgument("--toolchains");
+ verifier.addCliArgument("toolchains.xml");
+ verifier.addCliArgument("initialize");
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+
+ // Verify the auto-selection warning was logged
+ verifier.verifyTextInLog("Automatically selected JDK");
+
+ // Verify the toolchain was auto-selected and find-tool found javac
+ verifier.verifyFilePresent("target/tool.properties");
+ Properties toolProps = verifier.loadProperties("target/tool.properties");
+ assertEquals("jdk", toolProps.getProperty("toolchain.type"), "Auto-selected toolchain type should be 'jdk'");
+ }
+
+ /**
+ * Verifies that Maven does NOT auto-select a JDK toolchain when the running
+ * JDK already supports the project's required source level.
+ * <p>
+ * The project declares {@code maven.compiler.source=11}, which is supported
+ * by JDK 12+ (all current CI JDKs). No auto-selection should occur.
+ */
+ @Test
+ public void testNoAutoSelectWhenSourceLevelSupported() throws Exception {
+ Path testDir = extractResources("auto-jdk-toolchain-no-select");
+
+ // Create a fake JDK home (needed to create a valid toolchain)
+ Path javaHome = testDir.resolve("fakeJdk8");
+ Path binDir = javaHome.resolve("bin");
+ Files.createDirectories(binDir);
+ if (!Files.exists(binDir.resolve("javac"))) {
+ ItUtils.createFile(binDir.resolve("javac"));
+ }
+ if (!Files.exists(binDir.resolve("javac.exe"))) {
+ ItUtils.createFile(binDir.resolve("javac.exe"));
+ }
+
+ Verifier verifier = newVerifier(testDir);
+ // Clear the default compiler properties to let the POM properties be used
+ verifier.getSystemProperties().remove("maven.compiler.source");
+ verifier.getSystemProperties().remove("maven.compiler.target");
+ verifier.getSystemProperties().remove("maven.compiler.release");
+
+ Map<String, String> filterProps = verifier.newDefaultFilterMap();
+ filterProps.put("@javaHome@", javaHome.toString());
+ verifier.filterFile("toolchains.xml", "toolchains.xml", filterProps);
+
+ verifier.setAutoclean(false);
+ verifier.deleteDirectory("target");
+ verifier.addCliArgument("--toolchains");
+ verifier.addCliArgument("toolchains.xml");
+ verifier.addCliArgument("initialize");
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+
+ // Verify no auto-selection warning was logged
+ verifier.verifyTextNotInLog("Automatically selected JDK");
+
+ // Verify no toolchain was auto-selected (find-tool returns nothing)
+ verifier.verifyFilePresent("target/tool.properties");
+ Properties toolProps = verifier.loadProperties("target/tool.properties");
+ assertNull(
+ toolProps.getProperty("toolchain.type"),
+ "No toolchain should be auto-selected when running JDK supports the source level");
+ }
+}
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml
new file mode 100644
index 0000000000..872a38e92f
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.maven.its.auto-toolchain</groupId>
+ <artifactId>test-no-select</artifactId>
+ <version>1.0-SNAPSHOT</version>
+
+ <name>Maven Integration Test :: Auto JDK Toolchain No Selection</name>
+ <description>
+ Test that Maven does NOT auto-select a JDK toolchain when the running JDK
+ already supports the project's required source level.
+ </description>
+
+ <properties>
+ <!-- Source level 11 is supported by JDK 12+, so no auto-selection should occur -->
+ <maven.compiler.source>11</maven.compiler.source>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.its.plugins</groupId>
+ <artifactId>maven-it-plugin-toolchain</artifactId>
+ <version>2.1-SNAPSHOT</version>
+ <executions>
+ <execution>
+ <id>find-tool</id>
+ <goals>
+ <goal>find-tool</goal>
+ </goals>
+ <phase>initialize</phase>
+ <configuration>
+ <outputFile>target/tool.properties</outputFile>
+ <type>jdk</type>
+ <tool>javac</tool>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml
new file mode 100644
index 0000000000..2686260127
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<toolchains>
+ <toolchain>
+ <type>jdk</type>
+ <provides>
+ <version>8</version>
+ </provides>
+ <configuration>
+ <jdkHome>@javaHome@</jdkHome>
+ </configuration>
+ </toolchain>
+</toolchains>
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml
new file mode 100644
index 0000000000..c945c9f776
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.maven.its.auto-toolchain</groupId>
+ <artifactId>test-auto-select</artifactId>
+ <version>1.0-SNAPSHOT</version>
+
+ <name>Maven Integration Test :: Auto JDK Toolchain Selection</name>
+ <description>
+ Test that Maven auto-selects a JDK toolchain when the running JDK
+ does not support the project's required source level.
+ </description>
+
+ <properties>
+ <!-- Source level 6 is not supported by JDK 12+, triggering auto-selection -->
+ <maven.compiler.source>6</maven.compiler.source>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.its.plugins</groupId>
+ <artifactId>maven-it-plugin-toolchain</artifactId>
+ <version>2.1-SNAPSHOT</version>
+ <executions>
+ <execution>
+ <id>find-tool</id>
+ <goals>
+ <goal>find-tool</goal>
+ </goals>
+ <phase>initialize</phase>
+ <configuration>
+ <outputFile>target/tool.properties</outputFile>
+ <type>jdk</type>
+ <tool>javac</tool>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml
new file mode 100644
index 0000000000..4bc2f45593
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<toolchains>
+ <toolchain>
+ <type>jdk</type>
+ <provides>
+ <version>11</version>
+ </provides>
+ <configuration>
+ <jdkHome>@javaHome@</jdkHome>
+ </configuration>
+ </toolchain>
+</toolchains>
(maven) 09/14: Simplify JDK source-level check: replace auto-discovery with clear error
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit eaf607bdc3fd285056205f59dba33afc7b6151fa
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Aug 5 14:16:56 2026 +0200
Simplify JDK source-level check: replace auto-discovery with clear error
Remove the JdkToolchainDiscoverer and automatic JDK toolchain selection.
Instead, when the running JDK cannot honour the project's --source/--release
level, emit a clear, actionable error message telling the user which JDK
version they need and suggesting 'mvnup' to add the maven-toolchains-plugin
with automatic discovery.
This avoids duplicating the maven-toolchains-plugin's discovery mechanism
in core and follows a simpler "detect and error" approach.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../maven/toolchain/ToolchainManagerFactory.java | 5 +-
.../apache/maven/impl/DefaultToolchainManager.java | 125 ++------
.../apache/maven/impl/JdkSourceLevelSupport.java | 28 ++
.../apache/maven/impl/JdkToolchainDiscoverer.java | 336 ---------------------
.../maven/impl/DefaultToolchainManagerTest.java | 237 +++++----------
.../maven/impl/JdkSourceLevelSupportTest.java | 37 +++
.../maven/impl/JdkToolchainDiscovererTest.java | 226 --------------
.../it/MavenITAutoJdkToolchainSelectTest.java | 141 ---------
.../resources/auto-jdk-toolchain-no-select/pom.xml | 61 ----
.../auto-jdk-toolchain-no-select/toolchains.xml | 13 -
.../resources/auto-jdk-toolchain-select/pom.xml | 61 ----
.../auto-jdk-toolchain-select/toolchains.xml | 13 -
12 files changed, 163 insertions(+), 1120 deletions(-)
diff --git a/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java b/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java
index 153cfd0384..5123ff11d0 100644
--- a/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java
+++ b/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java
@@ -155,10 +155,7 @@ public Optional<org.apache.maven.api.Toolchain> createDefaultToolchain()
});
}
allFactories.putAll(v4Factories);
- org.apache.maven.impl.JdkToolchainDiscoverer discoverer = lookup.lookupOptional(
- org.apache.maven.impl.JdkToolchainDiscoverer.class)
- .orElse(null);
- return new org.apache.maven.impl.DefaultToolchainManager(allFactories, discoverer, logger) {};
+ return new org.apache.maven.impl.DefaultToolchainManager(allFactories, logger) {};
}
public class DefaultToolchainManagerV4 implements org.apache.maven.api.services.ToolchainManager {
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index 0e9e24b4b4..73a0e2db35 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -26,7 +26,6 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
-import org.apache.maven.api.JavaToolchain;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
@@ -53,38 +52,18 @@
@Singleton
public class DefaultToolchainManager implements ToolchainManager {
private final Map<String, ToolchainFactory> factories;
- private final JdkToolchainDiscoverer discoverer;
private final Logger logger;
@Inject
- public DefaultToolchainManager(Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer) {
- this(factories, discoverer, null);
- }
-
- /**
- * Convenience constructor without a discoverer — auto-selection will skip
- * filesystem discovery. Used by tests and IT harnesses.
- */
public DefaultToolchainManager(Map<String, ToolchainFactory> factories) {
- this(factories, null, null);
+ this(factories, (Logger) null);
}
/**
- * Convenience constructor without a discoverer, with custom logger.
- * Used by the compatibility layer and tests.
+ * Constructor with custom logger. Used by the compatibility layer and tests.
*/
public DefaultToolchainManager(Map<String, ToolchainFactory> factories, Logger logger) {
- this(factories, null, logger);
- }
-
- /**
- * Full-control constructor with all parameters.
- * Used by the compatibility layer ({@code ToolchainManagerFactory}) and tests.
- */
- public DefaultToolchainManager(
- Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer, Logger logger) {
this.factories = factories;
- this.discoverer = discoverer;
this.logger = logger != null ? logger : LoggerFactory.getLogger(DefaultToolchainManager.class);
}
@@ -118,14 +97,10 @@ public Optional<Toolchain> getToolchainFromBuildContext(@Nonnull Session session
return createToolchain(model);
}
- // For JDK type, try auto-selection based on project's target version
+ // For JDK type, check if the running JDK supports the project's source level
+ // and emit a clear, actionable error if not
if ("jdk".equals(type)) {
- Optional<Toolchain> autoSelected = autoSelectJdkToolchain(session);
- if (autoSelected.isPresent()) {
- // Cache the selection so subsequent calls for this project return the same toolchain
- context.put("toolchain-" + type, autoSelected.get().getModel());
- }
- return autoSelected;
+ checkJdkSourceLevelCompatibility(session);
}
return Optional.empty();
@@ -138,87 +113,41 @@ public void storeToolchainToBuildContext(@Nonnull Session session, @Nonnull Tool
}
/**
- * Attempts to automatically select a JDK toolchain when the running JDK
- * does not support the project's required {@code --source}/{@code --release} level.
+ * Checks whether the running JDK supports the project's required {@code --source}/{@code --release}
+ * level. If not, emits a clear, actionable error message instead of letting the build fail later
+ * with a cryptic javac error.
* <p>
- * First searches configured toolchains (from {@code toolchains.xml}), then falls back
- * to lazy filesystem discovery. Normal builds pay zero cost — discovery only runs
- * when the running JDK is incompatible and no configured toolchain matches.
+ * The error tells the user exactly which JDK version they need and how to fix it
+ * (run {@code mvnup} to add the {@code maven-toolchains-plugin} with automatic JDK discovery).
*/
- Optional<Toolchain> autoSelectJdkToolchain(Session session) {
+ void checkJdkSourceLevelCompatibility(Session session) {
int requiredSourceLevel = getProjectRequiredSourceLevel(session);
- logger.debug("Auto-select JDK toolchain: requiredSourceLevel={}", requiredSourceLevel);
if (requiredSourceLevel <= 0) {
- return Optional.empty();
+ return;
}
int runningJdkMajor = getRunningJdkMajor();
- logger.debug(
- "Auto-select JDK toolchain: runningJdkMajor={}, supportsLevel={}",
- runningJdkMajor,
- JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, requiredSourceLevel));
if (JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, requiredSourceLevel)) {
- return Optional.empty();
- }
-
- // 1. Search configured toolchains (from toolchains.xml)
- List<Toolchain> configuredToolchains = getToolchains(session, "jdk", null);
- Toolchain bestMatch = findNewestCompatible(configuredToolchains, requiredSourceLevel);
-
- // 2. Fall back to lazy filesystem discovery
- if (bestMatch == null && discoverer != null) {
- logger.debug("No compatible JDK in configured toolchains, discovering JDKs from filesystem...");
- List<ToolchainModel> discoveredModels = discoverer.discoverToolchains(session.getSystemProperties());
- List<Toolchain> discoveredToolchains = discoveredModels.stream()
- .map(this::createToolchain)
- .flatMap(Optional::stream)
- .toList();
- bestMatch = findNewestCompatible(discoveredToolchains, requiredSourceLevel);
+ return;
}
- if (bestMatch != null) {
- JavaToolchain jtc = (JavaToolchain) bestMatch;
- logger.warn(
- "Project requires --source {} which is not supported by JDK {}.",
- requiredSourceLevel,
- runningJdkMajor);
- logger.warn(
- "Automatically selected JDK {} (discovered at {}) for compilation.",
- jtc.getJavaVersion(),
- jtc.getJavaHome());
- logger.warn("To suppress this warning, configure the maven-toolchains-plugin explicitly");
- logger.warn("or set <targetVersion> to a value supported by your JDK.");
- return Optional.of(bestMatch);
- }
-
- return Optional.empty();
- }
-
- /**
- * Finds the newest JDK toolchain that supports the given source level.
- */
- private Toolchain findNewestCompatible(List<Toolchain> toolchains, int requiredSourceLevel) {
- Toolchain bestMatch = null;
- int bestVersion = 0;
- for (Toolchain tc : toolchains) {
- if (tc instanceof JavaToolchain jtc && jtc.getJavaVersion() != null) {
- int tcMajor = JdkSourceLevelSupport.normalizeSourceLevel(
- jtc.getJavaVersion().toString());
- if (tcMajor > 0 && JdkSourceLevelSupport.supportsSourceLevel(tcMajor, requiredSourceLevel)) {
- if (tcMajor > bestVersion) {
- bestVersion = tcMajor;
- bestMatch = tc;
- }
- }
- }
- }
- return bestMatch;
+ // Running JDK is incompatible — emit clear, actionable error
+ int latestJdk = JdkSourceLevelSupport.latestJdkForSourceLevel(requiredSourceLevel);
+ logger.error(
+ "Project requires --source {} which needs JDK <= {}, but the running JDK {} no longer supports it.",
+ requiredSourceLevel,
+ latestJdk,
+ runningJdkMajor);
+ logger.error("To fix: run 'mvnup' to add the maven-toolchains-plugin with automatic JDK discovery,");
+ logger.error(
+ "or install JDK {} and configure it in toolchains.xml or via the maven-toolchains-plugin.", latestJdk);
}
/**
- * Reads the project's required source level from either Model 4.1.0
- * {@code <source><targetVersion>} elements or legacy properties
- * ({@code maven.compiler.release}, {@code maven.compiler.source}).
+ * Reads the project's required source level from Model 4.1.0
+ * {@code <source><targetVersion>} elements, legacy properties
+ * ({@code maven.compiler.release}, {@code maven.compiler.source}),
+ * or compiler plugin configuration ({@code <release>}, {@code <source>}).
*
* @return the required source level as a major version, or {@code -1} if none is specified
*/
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
index 3ae3878d0d..56e64895d7 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
@@ -123,6 +123,34 @@ private static int indexOfNonDigit(String s) {
return -1;
}
+ /**
+ * Returns the latest JDK major version that still supports the given {@code --source} level.
+ * <p>
+ * Based on the retirement schedule:
+ * <ul>
+ * <li>source 1–5 → last supported by JDK 8</li>
+ * <li>source 6 → last supported by JDK 11</li>
+ * <li>source 7 → last supported by JDK 20</li>
+ * <li>source 8+ → still supported by current JDKs</li>
+ * </ul>
+ *
+ * @param sourceLevel the source level
+ * @return the latest JDK major version that supports it, or {@code -1} if the source level
+ * is still supported by all current JDKs
+ */
+ static int latestJdkForSourceLevel(int sourceLevel) {
+ if (sourceLevel <= 5) {
+ return 8;
+ }
+ if (sourceLevel == 6) {
+ return 11;
+ }
+ if (sourceLevel == 7) {
+ return 20;
+ }
+ return -1; // still supported
+ }
+
/**
* Returns the major version of the currently running JDK.
*
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
deleted file mode 100644
index 964441a1ed..0000000000
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-import org.apache.maven.api.di.Named;
-import org.apache.maven.api.di.Singleton;
-import org.apache.maven.api.toolchain.ToolchainModel;
-import org.apache.maven.api.xml.XmlNode;
-import org.apache.maven.impl.util.Os;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Discovers JDK installations on the local filesystem by scanning well-known
- * directories, environment variables, and tool manager locations.
- * <p>
- * This is used by {@link DefaultToolchainManager} as a lazy fallback when auto-selection
- * needs a compatible JDK but none are configured in {@code toolchains.xml}.
- * Discovery only runs when the running JDK cannot compile the project's source level
- * and no configured toolchain matches — normal builds pay zero cost.
- * <p>
- * JDK version is read from the {@code release} file present in every JDK since Java 9
- * (and backported to JDK 8u updates), avoiding the need to execute {@code java} processes.
- * <p>
- * All environment and system property access goes through the {@code properties} map
- * supplied by the caller (typically {@link org.apache.maven.api.Session#getSystemProperties()}),
- * where environment variables are available as {@code env.VAR_NAME} entries and JVM system
- * properties as plain keys ({@code java.home}, {@code user.home}, etc.).
- */
-@Named
-@Singleton
-public class JdkToolchainDiscoverer {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(JdkToolchainDiscoverer.class);
-
- private volatile List<ToolchainModel> cachedToolchains;
-
- /**
- * Returns discovered JDK toolchain models. Results are cached after first invocation
- * (the properties map from the first call wins; subsequent calls with different
- * properties still return the cached result, since JDK locations don't change mid-build).
- *
- * @param properties system properties map (from {@code Session.getSystemProperties()}).
- * Environment variables are expected as {@code env.VAR_NAME} entries.
- */
- public List<ToolchainModel> discoverToolchains(Map<String, String> properties) {
- List<ToolchainModel> result = cachedToolchains;
- if (result == null) {
- synchronized (this) {
- result = cachedToolchains;
- if (result == null) {
- result = doDiscover(properties);
- cachedToolchains = result;
- }
- }
- }
- return result;
- }
-
- private List<ToolchainModel> doDiscover(Map<String, String> properties) {
- Set<Path> candidates = new LinkedHashSet<>();
- collectFromEnvironment(candidates, properties);
- collectFromToolManagers(candidates, properties);
- collectFromSystemDirectories(candidates, properties);
-
- List<ToolchainModel> toolchains = new ArrayList<>();
- for (Path candidate : candidates) {
- try {
- Path jdkHome = resolveJdkHome(candidate);
- if (jdkHome != null && isValidJdkHome(jdkHome)) {
- Optional<ToolchainModel> model = buildToolchainModel(jdkHome);
- model.ifPresent(toolchains::add);
- }
- } catch (Exception e) {
- LOGGER.debug("Skipping JDK candidate {}: {}", candidate, e.getMessage());
- }
- }
-
- LOGGER.debug("Discovered {} JDK installation(s) on the filesystem", toolchains.size());
- return List.copyOf(toolchains);
- }
-
- /**
- * Collects JDK candidates from environment variables matching {@code JAVA*_HOME}.
- * Environment variables are read from the properties map as {@code env.VAR_NAME} entries.
- * Also scans the parent directory of {@code JAVA_HOME} for sibling JDK installations
- * (common in CI environments and container images where multiple JDKs are installed
- * under the same parent directory).
- */
- void collectFromEnvironment(Set<Path> candidates, Map<String, String> properties) {
- // Current JDK (from java.home system property)
- String javaHome = properties.get("java.home");
- if (javaHome != null) {
- addCandidate(candidates, Paths.get(javaHome));
- }
-
- // env.JAVA*_HOME env vars (e.g. env.JAVA11_HOME, env.JAVA17_HOME, env.JAVA_HOME)
- for (Map.Entry<String, String> entry : properties.entrySet()) {
- String name = entry.getKey();
- if (name.startsWith("env.JAVA") && name.endsWith("_HOME")) {
- Path jdkPath = Paths.get(entry.getValue());
- addCandidate(candidates, jdkPath);
- // Also scan sibling directories — in CI and container environments,
- // multiple JDKs are often installed under the same parent directory
- // (e.g. /toolchain/jdk-8, /toolchain/jdk-11, /toolchain/jdk-21)
- Path parent = jdkPath.getParent();
- if (parent != null) {
- scanSubdirectories(candidates, parent);
- }
- }
- }
- }
-
- /**
- * Collects JDK candidates from common tool manager directories under the user's home.
- */
- void collectFromToolManagers(Set<Path> candidates, Map<String, String> properties) {
- String userHomeProp = properties.get("user.home");
- if (userHomeProp == null) {
- return;
- }
- Path userHome = Paths.get(userHomeProp);
-
- // IntelliJ IDEA / common
- scanSubdirectories(candidates, userHome.resolve(".jdks"));
- // Maven-managed JDKs
- scanSubdirectories(candidates, userHome.resolve(".m2").resolve("jdks"));
- // SDKMAN
- scanSubdirectories(
- candidates, userHome.resolve(".sdkman").resolve("candidates").resolve("java"));
- // Gradle
- scanSubdirectories(candidates, userHome.resolve(".gradle").resolve("jdks"));
- // jEnv
- scanSubdirectories(candidates, userHome.resolve(".jenv").resolve("versions"));
- // JBang
- scanSubdirectories(
- candidates, userHome.resolve(".jbang").resolve("cache").resolve("jdks"));
- // asdf
- scanSubdirectories(
- candidates, userHome.resolve(".asdf").resolve("installs").resolve("java"));
- // Jabba
- scanSubdirectories(candidates, userHome.resolve(".jabba").resolve("jdk"));
- // mise (formerly rtx)
- scanSubdirectories(
- candidates,
- userHome.resolve(".local")
- .resolve("share")
- .resolve("mise")
- .resolve("installs")
- .resolve("java"));
- }
-
- /**
- * Collects JDK candidates from OS-specific system directories.
- */
- void collectFromSystemDirectories(Set<Path> candidates, Map<String, String> properties) {
- if (Os.IS_WINDOWS) {
- collectWindowsDirectories(candidates, properties);
- } else if (Os.isFamily("mac")) {
- collectMacDirectories(candidates, properties);
- } else {
- collectLinuxDirectories(candidates);
- }
- }
-
- private void collectLinuxDirectories(Set<Path> candidates) {
- scanSubdirectories(candidates, Paths.get("/usr/lib/jvm"));
- scanSubdirectories(candidates, Paths.get("/usr/lib64/jvm"));
- scanSubdirectories(candidates, Paths.get("/usr/jdk"));
- scanSubdirectories(candidates, Paths.get("/usr/java"));
- scanSubdirectories(candidates, Paths.get("/usr/local/java"));
- scanSubdirectories(candidates, Paths.get("/opt/java"));
- scanSubdirectories(candidates, Paths.get("/opt/hostedtoolcache"));
- }
-
- private void collectMacDirectories(Set<Path> candidates, Map<String, String> properties) {
- scanSubdirectories(candidates, Paths.get("/Library/Java/JavaVirtualMachines"));
- String userHomeProp = properties.get("user.home");
- if (userHomeProp != null) {
- Path userHome = Paths.get(userHomeProp);
- scanSubdirectories(
- candidates, userHome.resolve("Library").resolve("Java").resolve("JavaVirtualMachines"));
- }
- }
-
- private void collectWindowsDirectories(Set<Path> candidates, Map<String, String> properties) {
- Path progFiles = Paths.get("C:\\Program Files");
- scanSubdirectories(candidates, progFiles.resolve("Java"));
- scanSubdirectories(candidates, progFiles.resolve("Eclipse Adoptium"));
- scanSubdirectories(candidates, progFiles.resolve("Zulu"));
- scanSubdirectories(candidates, progFiles.resolve("Amazon Corretto"));
- scanSubdirectories(candidates, progFiles.resolve("BellSoft"));
- // Scoop
- String userHomeProp = properties.get("user.home");
- if (userHomeProp != null) {
- Path userHome = Paths.get(userHomeProp);
- scanSubdirectories(candidates, userHome.resolve("scoop").resolve("apps"));
- }
- }
-
- /**
- * Lists immediate subdirectories of the given directory and adds them as candidates.
- */
- private void scanSubdirectories(Set<Path> candidates, Path directory) {
- if (!Files.isDirectory(directory)) {
- return;
- }
- try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, Files::isDirectory)) {
- for (Path child : stream) {
- addCandidate(candidates, child);
- }
- } catch (IOException e) {
- LOGGER.debug("Cannot scan directory {}: {}", directory, e.getMessage());
- }
- }
-
- private void addCandidate(Set<Path> candidates, Path path) {
- try {
- candidates.add(path.toRealPath());
- } catch (IOException e) {
- // Broken symlink or inaccessible — add normalized path as fallback
- candidates.add(path.normalize().toAbsolutePath());
- }
- }
-
- /**
- * Resolves the actual JDK home from a candidate path.
- * On macOS, JDKs may be nested under {@code Contents/Home}.
- */
- Path resolveJdkHome(Path candidate) {
- if (isValidJdkHome(candidate)) {
- return candidate;
- }
- // macOS bundle layout: /path/to/jdk-17.jdk/Contents/Home
- Path contentsHome = candidate.resolve("Contents").resolve("Home");
- if (isValidJdkHome(contentsHome)) {
- return contentsHome;
- }
- return null;
- }
-
- /**
- * Checks if a directory is a valid JDK home by looking for {@code bin/javac}.
- */
- boolean isValidJdkHome(Path jdkHome) {
- Path bin = jdkHome.resolve("bin");
- return Files.exists(bin.resolve("javac")) || Files.exists(bin.resolve("javac.exe"));
- }
-
- /**
- * Builds a {@link ToolchainModel} from a validated JDK home by reading the {@code release} file.
- *
- * @return the model, or empty if the version cannot be determined
- */
- Optional<ToolchainModel> buildToolchainModel(Path jdkHome) {
- String version = readVersionFromRelease(jdkHome);
- if (version == null) {
- LOGGER.debug("Cannot determine version for JDK at {}, skipping", jdkHome);
- return Optional.empty();
- }
-
- int majorVersion = JdkSourceLevelSupport.normalizeSourceLevel(version);
- if (majorVersion <= 0) {
- LOGGER.debug("Cannot parse major version from '{}' for JDK at {}, skipping", version, jdkHome);
- return Optional.empty();
- }
-
- XmlNode jdkHomeNode = XmlNode.newInstance("jdkHome", jdkHome.toString());
- XmlNode configuration = XmlNode.newInstance("configuration", List.of(jdkHomeNode));
-
- ToolchainModel model = ToolchainModel.newBuilder()
- .type("jdk")
- .provides(Map.of("version", String.valueOf(majorVersion)))
- .configuration(configuration)
- .build();
-
- LOGGER.debug("Discovered JDK {} at {}", majorVersion, jdkHome);
- return Optional.of(model);
- }
-
- /**
- * Reads the {@code JAVA_VERSION} property from the JDK's {@code release} file.
- * The release file format uses shell-style assignments: {@code JAVA_VERSION="17.0.2"}.
- *
- * @return the version string (e.g. "17.0.2"), or null if not found
- */
- String readVersionFromRelease(Path jdkHome) {
- Path releaseFile = jdkHome.resolve("release");
- if (!Files.exists(releaseFile)) {
- return null;
- }
- try {
- for (String line : Files.readAllLines(releaseFile)) {
- if (line.startsWith("JAVA_VERSION=")) {
- String value = line.substring("JAVA_VERSION=".length()).trim();
- // Remove surrounding quotes
- if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
- value = value.substring(1, value.length() - 1);
- }
- return value.isEmpty() ? null : value;
- }
- }
- } catch (IOException e) {
- LOGGER.debug("Cannot read release file at {}: {}", releaseFile, e.getMessage());
- }
- return null;
- }
-}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java
index 951d297afd..fc23192629 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java
@@ -23,12 +23,10 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
-import org.apache.maven.api.JavaToolchain;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.Toolchain;
-import org.apache.maven.api.Version;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Source;
@@ -46,7 +44,9 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -130,54 +130,32 @@ void getToolchainsWithNullType() {
assertThrows(NullPointerException.class, () -> manager.getToolchains(session, null, null));
}
- // --- Auto-selection tests ---
+ // --- Source level compatibility check tests ---
@Test
- void autoSelectJdkToolchainWhenNoTargetVersion() {
- // Project has no targetVersion configured — should not auto-select
- when(session.getService(Lookup.class)).thenReturn(lookup);
- when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
- Model model = Model.newBuilder().build(Build.newBuilder().build()).build();
- when(project.getModel()).thenReturn(model);
-
- DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory)) {
+ void checkCompatibilityNoTargetVersion() {
+ // Project has no targetVersion configured — no error
+ Logger testLogger = mock(Logger.class);
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
@Override
int getRunningJdkMajor() {
return 17;
}
};
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
- assertTrue(result.isEmpty());
- }
-
- @Test
- void autoSelectJdkToolchainWhenRunningJdkSupportsLevel() {
- // Project targets source 11, running JDK 17 supports it — no auto-select
when(session.getService(Lookup.class)).thenReturn(lookup);
when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
- Model model = Model.newBuilder()
- .build(Build.newBuilder()
- .sources(List.of(Source.newBuilder().targetVersion("11").build()))
- .build())
- .build();
+ Model model = Model.newBuilder().build(Build.newBuilder().build()).build();
when(project.getModel()).thenReturn(model);
- DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory)) {
- @Override
- int getRunningJdkMajor() {
- return 17;
- }
- };
+ testManager.checkJdkSourceLevelCompatibility(session);
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
- assertTrue(result.isEmpty());
+ verify(testLogger, never()).error(any(String.class), any(), any(), any());
}
@Test
- void autoSelectJdkToolchainWhenRunningJdkDoesNotSupportLevel() {
- // Project targets source 6, running JDK 17 doesn't support it
- // JDK 11 toolchain available and supports source 6
+ void checkCompatibilityRunningJdkSupportsLevel() {
+ // Project targets source 11, running JDK 17 supports it — no error
Logger testLogger = mock(Logger.class);
DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
@Override
@@ -186,44 +164,23 @@ int getRunningJdkMajor() {
}
};
- // Set up project with targetVersion 6
when(session.getService(Lookup.class)).thenReturn(lookup);
when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
Model model = Model.newBuilder()
.build(Build.newBuilder()
- .sources(List.of(Source.newBuilder().targetVersion("6").build()))
+ .sources(List.of(Source.newBuilder().targetVersion("11").build()))
.build())
.build();
when(project.getModel()).thenReturn(model);
- // Set up available JDK 11 toolchain
- JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
- Version jdk11Version = mock(Version.class);
- when(jdk11Version.toString()).thenReturn("11");
- when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
- when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
-
- ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
- when(session.getToolchains()).thenReturn(List.of(jdk11Model));
- when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
- when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
-
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+ testManager.checkJdkSourceLevelCompatibility(session);
- assertTrue(result.isPresent());
- assertEquals(jdk11Toolchain, result.get());
- verify(testLogger).warn("Project requires --source {} which is not supported by JDK {}.", 6, 17);
- verify(testLogger)
- .warn(
- "Automatically selected JDK {} (discovered at {}) for compilation.",
- jdk11Version,
- "/usr/lib/jvm/java-11");
+ verify(testLogger, never()).error(any(String.class), any(), any(), any());
}
@Test
- void autoSelectJdkToolchainPrefersNewestCompatible() {
- // Project targets source 6, running JDK 17
- // JDK 8 and JDK 11 both support source 6; should select JDK 11 (newest)
+ void checkCompatibilityEmitsErrorWhenIncompatible() {
+ // Project targets source 6, running JDK 17 doesn't support it — should emit error
Logger testLogger = mock(Logger.class);
DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
@Override
@@ -241,47 +198,24 @@ int getRunningJdkMajor() {
.build();
when(project.getModel()).thenReturn(model);
- // JDK 8 toolchain
- JavaToolchain jdk8Toolchain = mock(JavaToolchain.class);
- Version jdk8Version = mock(Version.class);
- when(jdk8Version.toString()).thenReturn("8");
- when(jdk8Toolchain.getJavaVersion()).thenReturn(jdk8Version);
-
- // JDK 11 toolchain
- JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
- Version jdk11Version = mock(Version.class);
- when(jdk11Version.toString()).thenReturn("11");
- when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
- when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
-
- // Use distinct provides so ToolchainModel.equals() distinguishes them
- ToolchainModel jdk8Model = ToolchainModel.newBuilder()
- .type("jdk")
- .provides(Map.of("version", "8"))
- .build();
- ToolchainModel jdk11Model = ToolchainModel.newBuilder()
- .type("jdk")
- .provides(Map.of("version", "11"))
- .build();
- when(session.getToolchains()).thenReturn(List.of(jdk8Model, jdk11Model));
- when(jdkFactory.createToolchain(jdk8Model)).thenReturn(jdk8Toolchain);
- when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
- when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+ testManager.checkJdkSourceLevelCompatibility(session);
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
-
- assertTrue(result.isPresent());
- assertEquals(jdk11Toolchain, result.get());
+ verify(testLogger)
+ .error(
+ "Project requires --source {} which needs JDK <= {}, but the running JDK {} no longer supports it.",
+ 6,
+ 11,
+ 17);
}
@Test
- void autoSelectJdkToolchainNoCompatibleToolchainAvailable() {
- // Project targets source 5, running JDK 17
- // Only JDK 11 toolchain available (min source 6, doesn't support 5)
- DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory)) {
+ void checkCompatibilityEmitsErrorForSource5() {
+ // Project targets source 5, running JDK 21 — max JDK is 8
+ Logger testLogger = mock(Logger.class);
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
@Override
int getRunningJdkMajor() {
- return 17;
+ return 21;
}
};
@@ -294,24 +228,19 @@ int getRunningJdkMajor() {
.build();
when(project.getModel()).thenReturn(model);
- // JDK 11 doesn't support source 5
- JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
- Version jdk11Version = mock(Version.class);
- when(jdk11Version.toString()).thenReturn("11");
- when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+ testManager.checkJdkSourceLevelCompatibility(session);
- ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
- when(session.getToolchains()).thenReturn(List.of(jdk11Model));
- when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
- when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
-
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
- assertTrue(result.isEmpty());
+ verify(testLogger)
+ .error(
+ "Project requires --source {} which needs JDK <= {}, but the running JDK {} no longer supports it.",
+ 5,
+ 8,
+ 21);
}
@Test
- void autoSelectJdkToolchainFromLegacyProperties() {
- // Project uses maven.compiler.release=6 (legacy property), running JDK 17
+ void checkCompatibilityFromLegacyProperties() {
+ // Project uses maven.compiler.release=6, running JDK 17
Logger testLogger = mock(Logger.class);
DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
@Override
@@ -327,26 +256,19 @@ int getRunningJdkMajor() {
.build();
when(project.getModel()).thenReturn(model);
- JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
- Version jdk11Version = mock(Version.class);
- when(jdk11Version.toString()).thenReturn("11");
- when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
- when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
-
- ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
- when(session.getToolchains()).thenReturn(List.of(jdk11Model));
- when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
- when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
-
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+ testManager.checkJdkSourceLevelCompatibility(session);
- assertTrue(result.isPresent());
- assertEquals(jdk11Toolchain, result.get());
+ verify(testLogger)
+ .error(
+ "Project requires --source {} which needs JDK <= {}, but the running JDK {} no longer supports it.",
+ 6,
+ 11,
+ 17);
}
@Test
- void autoSelectJdkToolchainFromLegacySourceProperty() {
- // Project uses maven.compiler.source=1.6 (legacy property), running JDK 17
+ void checkCompatibilityFromLegacySourceProperty() {
+ // Project uses maven.compiler.source=1.6, running JDK 17
Logger testLogger = mock(Logger.class);
DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
@Override
@@ -362,26 +284,20 @@ int getRunningJdkMajor() {
.build();
when(project.getModel()).thenReturn(model);
- JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
- Version jdk11Version = mock(Version.class);
- when(jdk11Version.toString()).thenReturn("11");
- when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
- when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
-
- ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
- when(session.getToolchains()).thenReturn(List.of(jdk11Model));
- when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
- when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
-
- Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+ testManager.checkJdkSourceLevelCompatibility(session);
- assertTrue(result.isPresent());
- assertEquals(jdk11Toolchain, result.get());
+ verify(testLogger)
+ .error(
+ eq(
+ "Project requires --source {} which needs JDK <= {}, but the running JDK {} no longer supports it."),
+ eq(6),
+ eq(11),
+ eq(17));
}
@Test
- void getToolchainFromBuildContextAutoSelectsFallback() {
- // Verify getToolchainFromBuildContext calls auto-selection when no explicit toolchain
+ void getToolchainFromBuildContextChecksCompatibility() {
+ // Verify getToolchainFromBuildContext calls compatibility check for jdk type
Logger testLogger = mock(Logger.class);
Map<String, Object> context = new ConcurrentHashMap<>();
SessionData data = mock(SessionData.class);
@@ -405,28 +321,22 @@ int getRunningJdkMajor() {
.build();
when(project.getModel()).thenReturn(model);
- JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
- Version jdk11Version = mock(Version.class);
- when(jdk11Version.toString()).thenReturn("11");
- when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
- when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
-
- ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
- when(jdk11Toolchain.getModel()).thenReturn(jdk11Model);
- when(session.getToolchains()).thenReturn(List.of(jdk11Model));
- when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
- when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
-
Optional<Toolchain> result = testManager.getToolchainFromBuildContext(session, "jdk");
- assertTrue(result.isPresent());
- assertEquals(jdk11Toolchain, result.get());
+ // Should return empty (no auto-selection) but emit error
+ assertTrue(result.isEmpty());
+ verify(testLogger)
+ .error(
+ "Project requires --source {} which needs JDK <= {}, but the running JDK {} no longer supports it.",
+ 6,
+ 11,
+ 17);
}
@Test
- void getToolchainFromBuildContextReturnsExplicitOverAutoSelect() {
+ void getToolchainFromBuildContextReturnsExplicitToolchain() {
// When an explicit toolchain is stored via storeToolchainToBuildContext,
- // it takes precedence over auto-selection
+ // it takes precedence — no compatibility check needed
Map<String, Object> context = new ConcurrentHashMap<>();
SessionData data = mock(SessionData.class);
toolchainModel = ToolchainModel.newBuilder().type("jdk").build();
@@ -439,10 +349,7 @@ void getToolchainFromBuildContextReturnsExplicitOverAutoSelect() {
when(mockToolchain.getModel()).thenReturn(toolchainModel);
when(jdkFactory.createToolchain(any(ToolchainModel.class))).thenReturn(mockToolchain);
- // Store explicit toolchain using the proper API
manager.storeToolchainToBuildContext(session, mockToolchain);
-
- // Now retrieve — should get the explicit one, not auto-select
Optional<Toolchain> result = manager.getToolchainFromBuildContext(session, "jdk");
assertTrue(result.isPresent());
@@ -450,8 +357,8 @@ void getToolchainFromBuildContextReturnsExplicitOverAutoSelect() {
}
@Test
- void getToolchainFromBuildContextNonJdkTypeDoesNotAutoSelect() {
- // Auto-selection should only apply to "jdk" type
+ void getToolchainFromBuildContextNonJdkTypeNoCheck() {
+ // Compatibility check should only apply to "jdk" type
Map<String, Object> context = new ConcurrentHashMap<>();
SessionData data = mock(SessionData.class);
@@ -460,8 +367,6 @@ void getToolchainFromBuildContextNonJdkTypeDoesNotAutoSelect() {
when(session.getData()).thenReturn(data);
when(data.computeIfAbsent(any(), any())).thenReturn(context);
- // No "otherType" factory registered; getToolchainFromBuildContext should return empty
- // without attempting auto-selection
Optional<Toolchain> result = manager.getToolchainFromBuildContext(session, "otherType");
assertTrue(result.isEmpty());
}
@@ -469,8 +374,6 @@ void getToolchainFromBuildContextNonJdkTypeDoesNotAutoSelect() {
@Test
void getProjectRequiredSourceLevelTargetVersionTakesPrecedence() {
// targetVersion in sources should take precedence over properties
- DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory));
-
when(session.getService(Lookup.class)).thenReturn(lookup);
when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
Model model = Model.newBuilder()
@@ -481,7 +384,7 @@ void getProjectRequiredSourceLevelTargetVersionTakesPrecedence() {
.build();
when(project.getModel()).thenReturn(model);
- assertEquals(8, testManager.getProjectRequiredSourceLevel(session));
+ assertEquals(8, manager.getProjectRequiredSourceLevel(session));
}
@Test
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java
index e3b215f8ad..a93a37793b 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java
@@ -134,6 +134,43 @@ void normalizeSourceLevelWithWhitespace() {
assertEquals(8, JdkSourceLevelSupport.normalizeSourceLevel(" 1.8 "));
}
+ @Test
+ void normalizeSourceLevelLegacyFormatWithPatchVersion() {
+ // "1.8.0_392" format from some JDK distributions
+ assertEquals(8, JdkSourceLevelSupport.normalizeSourceLevel("1.8.0_392"));
+ assertEquals(7, JdkSourceLevelSupport.normalizeSourceLevel("1.7.0_80"));
+ assertEquals(6, JdkSourceLevelSupport.normalizeSourceLevel("1.6.0_45"));
+ }
+
+ @Test
+ void latestJdkForSourceLevelRetiredLevels() {
+ // source 1-5 → last supported by JDK 8
+ assertEquals(8, JdkSourceLevelSupport.latestJdkForSourceLevel(1));
+ assertEquals(8, JdkSourceLevelSupport.latestJdkForSourceLevel(3));
+ assertEquals(8, JdkSourceLevelSupport.latestJdkForSourceLevel(5));
+ }
+
+ @Test
+ void latestJdkForSourceLevel6() {
+ // source 6 → last supported by JDK 11
+ assertEquals(11, JdkSourceLevelSupport.latestJdkForSourceLevel(6));
+ }
+
+ @Test
+ void latestJdkForSourceLevel7() {
+ // source 7 → last supported by JDK 20
+ assertEquals(20, JdkSourceLevelSupport.latestJdkForSourceLevel(7));
+ }
+
+ @Test
+ void latestJdkForSourceLevelStillSupported() {
+ // source 8+ → still supported by current JDKs
+ assertEquals(-1, JdkSourceLevelSupport.latestJdkForSourceLevel(8));
+ assertEquals(-1, JdkSourceLevelSupport.latestJdkForSourceLevel(11));
+ assertEquals(-1, JdkSourceLevelSupport.latestJdkForSourceLevel(17));
+ assertEquals(-1, JdkSourceLevelSupport.latestJdkForSourceLevel(21));
+ }
+
@Test
void getRunningJdkMajorReturnsPositive() {
assertTrue(JdkSourceLevelSupport.getRunningJdkMajor() > 0);
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
deleted file mode 100644
index 76d0d6e576..0000000000
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-import org.apache.maven.api.toolchain.ToolchainModel;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-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.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class JdkToolchainDiscovererTest {
-
- @TempDir
- Path tempDir;
-
- private final JdkToolchainDiscoverer discoverer = new JdkToolchainDiscoverer();
-
- @Test
- void isValidJdkHomeWithJavac() throws IOException {
- Path jdkHome = createFakeJdk(tempDir, "jdk-17");
- assertTrue(discoverer.isValidJdkHome(jdkHome));
- }
-
- @Test
- void isValidJdkHomeWithoutJavac() throws IOException {
- Path jdkHome = tempDir.resolve("jdk-no-javac");
- Files.createDirectories(jdkHome.resolve("bin"));
- assertFalse(discoverer.isValidJdkHome(jdkHome));
- }
-
- @Test
- void isValidJdkHomeNonExistent() {
- assertFalse(discoverer.isValidJdkHome(tempDir.resolve("nonexistent")));
- }
-
- @Test
- void readVersionFromReleaseFile() throws IOException {
- Path jdkHome = tempDir.resolve("jdk-17");
- Files.createDirectories(jdkHome);
- Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=\"17.0.2\"\nIMPLEMENTOR=\"Eclipse Adoptium\"\n");
-
- assertEquals("17.0.2", discoverer.readVersionFromRelease(jdkHome));
- }
-
- @Test
- void readVersionFromReleaseFileJdk8Format() throws IOException {
- Path jdkHome = tempDir.resolve("jdk-8");
- Files.createDirectories(jdkHome);
- Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=\"1.8.0_392\"\n");
-
- assertEquals("1.8.0_392", discoverer.readVersionFromRelease(jdkHome));
- }
-
- @Test
- void readVersionFromReleaseFileNoQuotes() throws IOException {
- Path jdkHome = tempDir.resolve("jdk-11");
- Files.createDirectories(jdkHome);
- Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=11.0.21\n");
-
- assertEquals("11.0.21", discoverer.readVersionFromRelease(jdkHome));
- }
-
- @Test
- void readVersionFromMissingReleaseFile() {
- assertNull(discoverer.readVersionFromRelease(tempDir.resolve("no-release")));
- }
-
- @Test
- void readVersionFromReleaseFileWithoutJavaVersion() throws IOException {
- Path jdkHome = tempDir.resolve("jdk-bad");
- Files.createDirectories(jdkHome);
- Files.writeString(jdkHome.resolve("release"), "IMPLEMENTOR=\"Some Vendor\"\n");
-
- assertNull(discoverer.readVersionFromRelease(jdkHome));
- }
-
- @Test
- void buildToolchainModelFromValidJdk() throws IOException {
- Path jdkHome = createFakeJdkWithRelease(tempDir, "jdk-17", "17.0.2");
-
- Optional<ToolchainModel> result = discoverer.buildToolchainModel(jdkHome);
-
- assertTrue(result.isPresent());
- ToolchainModel model = result.get();
- assertEquals("jdk", model.getType());
- assertEquals("17", model.getProvides().get("version"));
- assertNotNull(model.getConfiguration());
- assertEquals(
- jdkHome.toString(), model.getConfiguration().child("jdkHome").value());
- }
-
- @Test
- void buildToolchainModelFromJdk8() throws IOException {
- Path jdkHome = createFakeJdkWithRelease(tempDir, "jdk-8", "1.8.0_392");
-
- Optional<ToolchainModel> result = discoverer.buildToolchainModel(jdkHome);
-
- assertTrue(result.isPresent());
- assertEquals("8", result.get().getProvides().get("version"));
- }
-
- @Test
- void buildToolchainModelWithoutReleaseFile() throws IOException {
- Path jdkHome = createFakeJdk(tempDir, "jdk-old");
-
- Optional<ToolchainModel> result = discoverer.buildToolchainModel(jdkHome);
-
- assertFalse(result.isPresent());
- }
-
- @Test
- void resolveJdkHomeDirectPath() throws IOException {
- Path jdkHome = createFakeJdk(tempDir, "jdk-17");
-
- assertEquals(jdkHome, discoverer.resolveJdkHome(jdkHome));
- }
-
- @Test
- void resolveJdkHomeMacOsBundle() throws IOException {
- Path bundleRoot = tempDir.resolve("jdk-17.jdk");
- Path contentsHome = bundleRoot.resolve("Contents").resolve("Home");
- Files.createDirectories(contentsHome.resolve("bin"));
- Files.createFile(contentsHome.resolve("bin").resolve("javac"));
-
- assertEquals(contentsHome, discoverer.resolveJdkHome(bundleRoot));
- }
-
- @Test
- void resolveJdkHomeInvalidPath() {
- assertNull(discoverer.resolveJdkHome(tempDir.resolve("nonexistent")));
- }
-
- @Test
- void discoverToolchainsIsCached() {
- Map<String, String> properties = Map.of("user.home", tempDir.toString());
- // Two calls should return the same list instance (cached)
- var first = discoverer.discoverToolchains(properties);
- var second = discoverer.discoverToolchains(properties);
- assertNotNull(first);
- assertTrue(first == second, "Expected cached result (same instance)");
- }
-
- @Test
- void collectFromEnvironmentUsesProperties() throws IOException {
- Path jdkHome = createFakeJdk(tempDir, "jdk-21");
- Map<String, String> properties = Map.of(
- "java.home", jdkHome.toString(),
- "env.JAVA17_HOME", tempDir.resolve("jdk-17-env").toString());
-
- Set<Path> candidates = new LinkedHashSet<>();
- discoverer.collectFromEnvironment(candidates, properties);
-
- assertTrue(candidates.stream().anyMatch(p -> p.toString().contains("jdk-21")), "Should include java.home");
- assertTrue(
- candidates.stream().anyMatch(p -> p.toString().contains("jdk-17-env")),
- "Should include env.JAVA17_HOME");
- }
-
- @Test
- void collectFromToolManagersUsesProperties() throws IOException {
- Path fakeHome = tempDir.resolve("fakehome");
- Path jdksDir = fakeHome.resolve(".jdks");
- Path jdk21 = jdksDir.resolve("temurin-21");
- Files.createDirectories(jdk21);
-
- Map<String, String> properties = Map.of("user.home", fakeHome.toString());
-
- Set<Path> candidates = new LinkedHashSet<>();
- discoverer.collectFromToolManagers(candidates, properties);
-
- assertTrue(
- candidates.stream().anyMatch(p -> p.toString().contains("temurin-21")), "Should find JDK in ~/.jdks");
- }
-
- @Test
- void collectFromToolManagersSkipsWhenNoUserHome() {
- Map<String, String> properties = Map.of();
-
- Set<Path> candidates = new LinkedHashSet<>();
- discoverer.collectFromToolManagers(candidates, properties);
-
- assertTrue(candidates.isEmpty(), "Should collect nothing without user.home");
- }
-
- private Path createFakeJdk(Path parent, String name) throws IOException {
- Path jdkHome = parent.resolve(name);
- Path binDir = jdkHome.resolve("bin");
- Files.createDirectories(binDir);
- Files.createFile(binDir.resolve("javac"));
- return jdkHome;
- }
-
- private Path createFakeJdkWithRelease(Path parent, String name, String version) throws IOException {
- Path jdkHome = createFakeJdk(parent, name);
- Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=\"" + version + "\"\n");
- return jdkHome;
- }
-}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
deleted file mode 100644
index 894d832ff4..0000000000
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Map;
-import java.util.Properties;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-/**
- * Integration tests for automatic JDK toolchain selection.
- * <p>
- * When the running JDK does not support the project's required {@code --source}/{@code --release}
- * level, Maven should automatically search configured toolchains and select a compatible JDK.
- */
-class MavenITAutoJdkToolchainSelectTest extends AbstractMavenIntegrationTestCase {
-
- /**
- * Verifies that Maven auto-selects a JDK toolchain when the running JDK
- * does not support the project's required source level.
- * <p>
- * The project declares {@code maven.compiler.source=6}, which is not supported
- * by JDK 12+ (minimum source level 7 for JDK 12-20, 8 for JDK 21+).
- * A JDK 11 toolchain is configured in toolchains.xml and should be auto-selected.
- */
- @Test
- void testAutoSelectToolchainWhenSourceLevelUnsupported() throws Exception {
- Path testDir = extractResources("auto-jdk-toolchain-select");
-
- // Create a fake JDK home with bin/javac for the toolchain
- Path javaHome = testDir.resolve("fakeJdk11");
- Path binDir = javaHome.resolve("bin");
- Files.createDirectories(binDir);
- if (!Files.exists(binDir.resolve("javac"))) {
- ItUtils.createFile(binDir.resolve("javac"));
- }
- if (!Files.exists(binDir.resolve("javac.exe"))) {
- ItUtils.createFile(binDir.resolve("javac.exe"));
- }
-
- Verifier verifier = newVerifier(testDir);
- // Clear the default compiler properties set by newVerifier() — the POM defines
- // maven.compiler.source=6 and we need the effective model to reflect that, not
- // the system property override of 8 from MAVEN_OPTS.
- verifier.getSystemProperties().remove("maven.compiler.source");
- verifier.getSystemProperties().remove("maven.compiler.target");
- verifier.getSystemProperties().remove("maven.compiler.release");
-
- Map<String, String> filterProps = verifier.newDefaultFilterMap();
- filterProps.put("@javaHome@", javaHome.toString());
- verifier.filterFile("toolchains.xml", "toolchains.xml", filterProps);
-
- verifier.setAutoclean(false);
- verifier.deleteDirectory("target");
- verifier.addCliArgument("--toolchains");
- verifier.addCliArgument("toolchains.xml");
- verifier.addCliArgument("initialize");
- verifier.execute();
- verifier.verifyErrorFreeLog();
-
- // Verify the auto-selection warning was logged
- verifier.verifyTextInLog("Automatically selected JDK");
-
- // Verify the toolchain was auto-selected and find-tool found javac
- verifier.verifyFilePresent("target/tool.properties");
- Properties toolProps = verifier.loadProperties("target/tool.properties");
- assertEquals("jdk", toolProps.getProperty("toolchain.type"), "Auto-selected toolchain type should be 'jdk'");
- }
-
- /**
- * Verifies that Maven does NOT auto-select a JDK toolchain when the running
- * JDK already supports the project's required source level.
- * <p>
- * The project declares {@code maven.compiler.source=11}, which is supported
- * by JDK 12+ (all current CI JDKs). No auto-selection should occur.
- */
- @Test
- void testNoAutoSelectWhenSourceLevelSupported() throws Exception {
- Path testDir = extractResources("auto-jdk-toolchain-no-select");
-
- // Create a fake JDK home (needed to create a valid toolchain)
- Path javaHome = testDir.resolve("fakeJdk8");
- Path binDir = javaHome.resolve("bin");
- Files.createDirectories(binDir);
- if (!Files.exists(binDir.resolve("javac"))) {
- ItUtils.createFile(binDir.resolve("javac"));
- }
- if (!Files.exists(binDir.resolve("javac.exe"))) {
- ItUtils.createFile(binDir.resolve("javac.exe"));
- }
-
- Verifier verifier = newVerifier(testDir);
- // Clear the default compiler properties to let the POM properties be used
- verifier.getSystemProperties().remove("maven.compiler.source");
- verifier.getSystemProperties().remove("maven.compiler.target");
- verifier.getSystemProperties().remove("maven.compiler.release");
-
- Map<String, String> filterProps = verifier.newDefaultFilterMap();
- filterProps.put("@javaHome@", javaHome.toString());
- verifier.filterFile("toolchains.xml", "toolchains.xml", filterProps);
-
- verifier.setAutoclean(false);
- verifier.deleteDirectory("target");
- verifier.addCliArgument("--toolchains");
- verifier.addCliArgument("toolchains.xml");
- verifier.addCliArgument("initialize");
- verifier.execute();
- verifier.verifyErrorFreeLog();
-
- // Verify no auto-selection warning was logged
- verifier.verifyTextNotInLog("Automatically selected JDK");
-
- // Verify no toolchain was auto-selected (find-tool returns nothing)
- verifier.verifyFilePresent("target/tool.properties");
- Properties toolProps = verifier.loadProperties("target/tool.properties");
- assertNull(
- toolProps.getProperty("toolchain.type"),
- "No toolchain should be auto-selected when running JDK supports the source level");
- }
-}
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml
deleted file mode 100644
index 872a38e92f..0000000000
--- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0">
- <modelVersion>4.0.0</modelVersion>
-
- <groupId>org.apache.maven.its.auto-toolchain</groupId>
- <artifactId>test-no-select</artifactId>
- <version>1.0-SNAPSHOT</version>
-
- <name>Maven Integration Test :: Auto JDK Toolchain No Selection</name>
- <description>
- Test that Maven does NOT auto-select a JDK toolchain when the running JDK
- already supports the project's required source level.
- </description>
-
- <properties>
- <!-- Source level 11 is supported by JDK 12+, so no auto-selection should occur -->
- <maven.compiler.source>11</maven.compiler.source>
- </properties>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.its.plugins</groupId>
- <artifactId>maven-it-plugin-toolchain</artifactId>
- <version>2.1-SNAPSHOT</version>
- <executions>
- <execution>
- <id>find-tool</id>
- <goals>
- <goal>find-tool</goal>
- </goals>
- <phase>initialize</phase>
- <configuration>
- <outputFile>target/tool.properties</outputFile>
- <type>jdk</type>
- <tool>javac</tool>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml
deleted file mode 100644
index 2686260127..0000000000
--- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<toolchains>
- <toolchain>
- <type>jdk</type>
- <provides>
- <version>8</version>
- </provides>
- <configuration>
- <jdkHome>@javaHome@</jdkHome>
- </configuration>
- </toolchain>
-</toolchains>
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml
deleted file mode 100644
index c945c9f776..0000000000
--- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0">
- <modelVersion>4.0.0</modelVersion>
-
- <groupId>org.apache.maven.its.auto-toolchain</groupId>
- <artifactId>test-auto-select</artifactId>
- <version>1.0-SNAPSHOT</version>
-
- <name>Maven Integration Test :: Auto JDK Toolchain Selection</name>
- <description>
- Test that Maven auto-selects a JDK toolchain when the running JDK
- does not support the project's required source level.
- </description>
-
- <properties>
- <!-- Source level 6 is not supported by JDK 12+, triggering auto-selection -->
- <maven.compiler.source>6</maven.compiler.source>
- </properties>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.its.plugins</groupId>
- <artifactId>maven-it-plugin-toolchain</artifactId>
- <version>2.1-SNAPSHOT</version>
- <executions>
- <execution>
- <id>find-tool</id>
- <goals>
- <goal>find-tool</goal>
- </goals>
- <phase>initialize</phase>
- <configuration>
- <outputFile>target/tool.properties</outputFile>
- <type>jdk</type>
- <tool>javac</tool>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml
deleted file mode 100644
index 4bc2f45593..0000000000
--- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<toolchains>
- <toolchain>
- <type>jdk</type>
- <provides>
- <version>11</version>
- </provides>
- <configuration>
- <jdkHome>@javaHome@</jdkHome>
- </configuration>
- </toolchain>
-</toolchains>
(maven) 05/14: Use Session properties instead of System.* in JdkToolchainDiscoverer
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 3eb3fdd5fffdf5ced5f0008f0b81c7fe980c7ec1
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 31 11:01:47 2026 +0200
Use Session properties instead of System.* in JdkToolchainDiscoverer
Replace direct System.getProperty/System.getenv calls with the
properties map from Session.getSystemProperties(), where env vars
are available as "env.VAR_NAME" entries. This follows Maven's
convention of accessing environment through the Session and makes
the discoverer fully testable without relying on JVM global state.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../apache/maven/impl/DefaultToolchainManager.java | 2 +-
.../apache/maven/impl/JdkToolchainDiscoverer.java | 77 +++++++++++++---------
.../maven/impl/JdkToolchainDiscovererTest.java | 50 +++++++++++++-
3 files changed, 95 insertions(+), 34 deletions(-)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index 45df52a465..9169252983 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -163,7 +163,7 @@ Optional<Toolchain> autoSelectJdkToolchain(Session session) {
// 2. Fall back to lazy filesystem discovery
if (bestMatch == null && discoverer != null) {
logger.debug("No compatible JDK in configured toolchains, discovering JDKs from filesystem...");
- List<ToolchainModel> discoveredModels = discoverer.discoverToolchains();
+ List<ToolchainModel> discoveredModels = discoverer.discoverToolchains(session.getSystemProperties());
List<Toolchain> discoveredToolchains = discoveredModels.stream()
.map(this::createToolchain)
.flatMap(Optional::stream)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
index 78524a7e6c..1d7dfe767b 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
@@ -49,6 +49,11 @@
* <p>
* JDK version is read from the {@code release} file present in every JDK since Java 9
* (and backported to JDK 8u updates), avoiding the need to execute {@code java} processes.
+ * <p>
+ * All environment and system property access goes through the {@code properties} map
+ * supplied by the caller (typically {@link org.apache.maven.api.Session#getSystemProperties()}),
+ * where environment variables are available as {@code env.VAR_NAME} entries and JVM system
+ * properties as plain keys ({@code java.home}, {@code user.home}, etc.).
*/
@Named
@Singleton
@@ -59,15 +64,20 @@ public class JdkToolchainDiscoverer {
private volatile List<ToolchainModel> cachedToolchains;
/**
- * Returns discovered JDK toolchain models. Results are cached after first invocation.
+ * Returns discovered JDK toolchain models. Results are cached after first invocation
+ * (the properties map from the first call wins; subsequent calls with different
+ * properties still return the cached result, since JDK locations don't change mid-build).
+ *
+ * @param properties system properties map (from {@code Session.getSystemProperties()}).
+ * Environment variables are expected as {@code env.VAR_NAME} entries.
*/
- public List<ToolchainModel> discoverToolchains() {
+ public List<ToolchainModel> discoverToolchains(Map<String, String> properties) {
List<ToolchainModel> result = cachedToolchains;
if (result == null) {
synchronized (this) {
result = cachedToolchains;
if (result == null) {
- result = doDiscover();
+ result = doDiscover(properties);
cachedToolchains = result;
}
}
@@ -75,11 +85,11 @@ public List<ToolchainModel> discoverToolchains() {
return result;
}
- private List<ToolchainModel> doDiscover() {
+ private List<ToolchainModel> doDiscover(Map<String, String> properties) {
Set<Path> candidates = new LinkedHashSet<>();
- collectFromEnvironment(candidates);
- collectFromToolManagers(candidates);
- collectFromSystemDirectories(candidates);
+ collectFromEnvironment(candidates, properties);
+ collectFromToolManagers(candidates, properties);
+ collectFromSystemDirectories(candidates, properties);
List<ToolchainModel> toolchains = new ArrayList<>();
for (Path candidate : candidates) {
@@ -100,34 +110,33 @@ private List<ToolchainModel> doDiscover() {
/**
* Collects JDK candidates from environment variables matching {@code JAVA*_HOME}.
+ * Environment variables are read from the properties map as {@code env.VAR_NAME} entries.
*/
- void collectFromEnvironment(Set<Path> candidates) {
- // Current JDK
- String javaHome = System.getProperty("java.home");
+ void collectFromEnvironment(Set<Path> candidates, Map<String, String> properties) {
+ // Current JDK (from java.home system property)
+ String javaHome = properties.get("java.home");
if (javaHome != null) {
addCandidate(candidates, Paths.get(javaHome));
}
- // JAVA*_HOME env vars (e.g. JAVA11_HOME, JAVA17_HOME)
- for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
+ // env.JAVA*_HOME env vars (e.g. env.JAVA11_HOME, env.JAVA17_HOME, env.JAVA_HOME)
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
String name = entry.getKey();
- if (name.startsWith("JAVA") && name.endsWith("_HOME")) {
+ if (name.startsWith("env.JAVA") && name.endsWith("_HOME")) {
addCandidate(candidates, Paths.get(entry.getValue()));
}
}
-
- // JAVA_HOME
- String envJavaHome = System.getenv("JAVA_HOME");
- if (envJavaHome != null) {
- addCandidate(candidates, Paths.get(envJavaHome));
- }
}
/**
* Collects JDK candidates from common tool manager directories under the user's home.
*/
- void collectFromToolManagers(Set<Path> candidates) {
- Path userHome = Paths.get(System.getProperty("user.home"));
+ void collectFromToolManagers(Set<Path> candidates, Map<String, String> properties) {
+ String userHomeProp = properties.get("user.home");
+ if (userHomeProp == null) {
+ return;
+ }
+ Path userHome = Paths.get(userHomeProp);
// IntelliJ IDEA / common
scanSubdirectories(candidates, userHome.resolve(".jdks"));
@@ -161,11 +170,11 @@ void collectFromToolManagers(Set<Path> candidates) {
/**
* Collects JDK candidates from OS-specific system directories.
*/
- void collectFromSystemDirectories(Set<Path> candidates) {
+ void collectFromSystemDirectories(Set<Path> candidates, Map<String, String> properties) {
if (Os.IS_WINDOWS) {
- collectWindowsDirectories(candidates);
+ collectWindowsDirectories(candidates, properties);
} else if (Os.isFamily("mac")) {
- collectMacDirectories(candidates);
+ collectMacDirectories(candidates, properties);
} else {
collectLinuxDirectories(candidates);
}
@@ -181,14 +190,17 @@ private void collectLinuxDirectories(Set<Path> candidates) {
scanSubdirectories(candidates, Paths.get("/opt/hostedtoolcache"));
}
- private void collectMacDirectories(Set<Path> candidates) {
- Path userHome = Paths.get(System.getProperty("user.home"));
+ private void collectMacDirectories(Set<Path> candidates, Map<String, String> properties) {
scanSubdirectories(candidates, Paths.get("/Library/Java/JavaVirtualMachines"));
- scanSubdirectories(
- candidates, userHome.resolve("Library").resolve("Java").resolve("JavaVirtualMachines"));
+ String userHomeProp = properties.get("user.home");
+ if (userHomeProp != null) {
+ Path userHome = Paths.get(userHomeProp);
+ scanSubdirectories(
+ candidates, userHome.resolve("Library").resolve("Java").resolve("JavaVirtualMachines"));
+ }
}
- private void collectWindowsDirectories(Set<Path> candidates) {
+ private void collectWindowsDirectories(Set<Path> candidates, Map<String, String> properties) {
Path progFiles = Paths.get("C:\\Program Files");
scanSubdirectories(candidates, progFiles.resolve("Java"));
scanSubdirectories(candidates, progFiles.resolve("Eclipse Adoptium"));
@@ -196,8 +208,11 @@ private void collectWindowsDirectories(Set<Path> candidates) {
scanSubdirectories(candidates, progFiles.resolve("Amazon Corretto"));
scanSubdirectories(candidates, progFiles.resolve("BellSoft"));
// Scoop
- Path userHome = Paths.get(System.getProperty("user.home"));
- scanSubdirectories(candidates, userHome.resolve("scoop").resolve("apps"));
+ String userHomeProp = properties.get("user.home");
+ if (userHomeProp != null) {
+ Path userHome = Paths.get(userHomeProp);
+ scanSubdirectories(candidates, userHome.resolve("scoop").resolve("apps"));
+ }
}
/**
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
index 041a9cce1d..76d0d6e576 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
@@ -21,7 +21,10 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.LinkedHashSet;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import org.apache.maven.api.toolchain.ToolchainModel;
import org.junit.jupiter.api.Test;
@@ -157,13 +160,56 @@ void resolveJdkHomeInvalidPath() {
@Test
void discoverToolchainsIsCached() {
+ Map<String, String> properties = Map.of("user.home", tempDir.toString());
// Two calls should return the same list instance (cached)
- var first = discoverer.discoverToolchains();
- var second = discoverer.discoverToolchains();
+ var first = discoverer.discoverToolchains(properties);
+ var second = discoverer.discoverToolchains(properties);
assertNotNull(first);
assertTrue(first == second, "Expected cached result (same instance)");
}
+ @Test
+ void collectFromEnvironmentUsesProperties() throws IOException {
+ Path jdkHome = createFakeJdk(tempDir, "jdk-21");
+ Map<String, String> properties = Map.of(
+ "java.home", jdkHome.toString(),
+ "env.JAVA17_HOME", tempDir.resolve("jdk-17-env").toString());
+
+ Set<Path> candidates = new LinkedHashSet<>();
+ discoverer.collectFromEnvironment(candidates, properties);
+
+ assertTrue(candidates.stream().anyMatch(p -> p.toString().contains("jdk-21")), "Should include java.home");
+ assertTrue(
+ candidates.stream().anyMatch(p -> p.toString().contains("jdk-17-env")),
+ "Should include env.JAVA17_HOME");
+ }
+
+ @Test
+ void collectFromToolManagersUsesProperties() throws IOException {
+ Path fakeHome = tempDir.resolve("fakehome");
+ Path jdksDir = fakeHome.resolve(".jdks");
+ Path jdk21 = jdksDir.resolve("temurin-21");
+ Files.createDirectories(jdk21);
+
+ Map<String, String> properties = Map.of("user.home", fakeHome.toString());
+
+ Set<Path> candidates = new LinkedHashSet<>();
+ discoverer.collectFromToolManagers(candidates, properties);
+
+ assertTrue(
+ candidates.stream().anyMatch(p -> p.toString().contains("temurin-21")), "Should find JDK in ~/.jdks");
+ }
+
+ @Test
+ void collectFromToolManagersSkipsWhenNoUserHome() {
+ Map<String, String> properties = Map.of();
+
+ Set<Path> candidates = new LinkedHashSet<>();
+ discoverer.collectFromToolManagers(candidates, properties);
+
+ assertTrue(candidates.isEmpty(), "Should collect nothing without user.home");
+ }
+
private Path createFakeJdk(Path parent, String name) throws IOException {
Path jdkHome = parent.resolve(name);
Path binDir = jdkHome.resolve("bin");
(maven) 12/14: Add integration test for mvnup ToolchainPluginStrategy
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 1ac850b3d62e21a81edc171c1c236b2e7f9f74b4
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Aug 5 23:57:19 2026 +0200
Add integration test for mvnup ToolchainPluginStrategy
Tests the full end-to-end flow: mvnup apply on a POM with
maven.compiler.source=1.5 adds the maven-toolchains-plugin with
select-jdk-toolchain goal and version constraint (,8], then verifies
a second run is idempotent.
Also adds mvnup to the chmod list in the IT suite's antrun config
so it gets execute permission when extracted from the zip.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
its/core-it-suite/pom.xml | 35 ++++++++-
.../MavenITMvnupToolchainPluginStrategyTest.java | 88 ++++++++++++++++++++++
.../mvnup-toolchain-plugin-strategy/pom.xml | 12 +++
3 files changed, 133 insertions(+), 2 deletions(-)
diff --git a/its/core-it-suite/pom.xml b/its/core-it-suite/pom.xml
index a6538a0935..e7f958d5a6 100644
--- a/its/core-it-suite/pom.xml
+++ b/its/core-it-suite/pom.xml
@@ -558,6 +558,37 @@ under the License.
<excludedEnvironmentVariable>MAVEN_OPTS</excludedEnvironmentVariable>
</excludedEnvironmentVariables>
</configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- Default behavior: extract and use the Maven distribution built by the current build -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>unpack-built-maven-distro</id>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <phase>process-test-resources</phase>
+ <configuration>
+ <target>
+ <delete dir="${mavenHome}" />
+ <unzip dest="${mavenHome}" src="${sessionRootDirectory}/apache-maven/target/apache-maven-${project.version}-bin.zip">
+ <regexpmapper from="^([^/]+)/(.*)$" handledirsep="true" to="\2" />
+ </unzip>
+ <chmod dir="${mavenHome}/bin" includes="mvn,mvnDebug,mvnenc,mvnup" perm="ugo+rx" />
+ </target>
+ </configuration>
+ </execution>
+ </executions>
</plugin>
</plugins>
</build>
@@ -737,7 +768,7 @@ under the License.
<unzip dest="${mavenHome}" src="${project.build.directory}/maven-bin.zip">
<globmapper from="apache-maven-${mavenVersion}/*" handledirsep="true" to="*" />
</unzip>
- <chmod dir="${mavenHome}/bin" includes="mvn,mvnDebug,mvnenc" perm="ugo+rx" />
+ <chmod dir="${mavenHome}/bin" includes="mvn,mvnDebug,mvnenc,mvnup" perm="ugo+rx" />
</target>
</configuration>
</execution>
@@ -780,7 +811,7 @@ under the License.
<unzip dest="${mavenHome}" src="${mavenDistro}">
<regexpmapper from="^([^/]+)/(.*)$" handledirsep="true" to="\2" />
</unzip>
- <chmod dir="${mavenHome}/bin" includes="mvn,mvnDebug,mvnenc" perm="ugo+rx" />
+ <chmod dir="${mavenHome}/bin" includes="mvn,mvnDebug,mvnenc,mvnup" perm="ugo+rx" />
</target>
</configuration>
</execution>
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITMvnupToolchainPluginStrategyTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITMvnupToolchainPluginStrategyTest.java
new file mode 100644
index 0000000000..718b6db66d
--- /dev/null
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITMvnupToolchainPluginStrategyTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration test for the {@code ToolchainPluginStrategy} in {@code mvnup}.
+ * <p>
+ * Verifies that running {@code mvnup apply} on a project with an old
+ * {@code --source} level (unsupported by the running JDK) automatically adds
+ * the {@code maven-toolchains-plugin} with the {@code select-jdk-toolchain}
+ * goal and the correct {@code <version>} constraint, and that a second run
+ * is idempotent.
+ *
+ * @since 4.1.0
+ */
+class MavenITMvnupToolchainPluginStrategyTest extends AbstractMavenIntegrationTestCase {
+
+ /**
+ * Verify that mvnup adds the maven-toolchains-plugin with select-jdk-toolchain
+ * goal and version constraint when the project's source level is unsupported
+ * by the running JDK, and that a second run is idempotent.
+ */
+ @Test
+ void testMvnupAddsToolchainsPluginForOldSourceLevel() throws Exception {
+ Path testDir = extractResources("mvnup-toolchain-plugin-strategy");
+
+ // First run — should add the plugin
+ Verifier verifier = newVerifier(testDir);
+ verifier.setForkJvm(true);
+ verifier.setLogFileName("first-run.txt");
+ verifier.setExecutable("mvnup");
+ verifier.addCliArgument("apply");
+ verifier.addCliArgument("-d");
+ verifier.addCliArgument(testDir.toString());
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+
+ // Verify mvnup reported adding the plugin
+ verifier.verifyTextInLog("Added maven-toolchains-plugin with select-jdk-toolchain goal");
+
+ // Verify the POM was modified to include the toolchains plugin
+ String pomContent = Files.readString(testDir.resolve("pom.xml"));
+ assertTrue(
+ pomContent.contains("maven-toolchains-plugin"),
+ "POM should contain maven-toolchains-plugin after mvnup apply");
+ assertTrue(
+ pomContent.contains("select-jdk-toolchain"),
+ "POM should contain select-jdk-toolchain goal after mvnup apply");
+ assertTrue(
+ pomContent.contains("<version>(,8]</version>"),
+ "POM should contain version constraint (,8] for source 5 after mvnup apply");
+
+ // Second run — should be idempotent (skip, already present)
+ verifier = newVerifier(testDir);
+ verifier.setForkJvm(true);
+ verifier.setLogFileName("second-run.txt");
+ verifier.setExecutable("mvnup");
+ verifier.addCliArgument("apply");
+ verifier.addCliArgument("-d");
+ verifier.addCliArgument(testDir.toString());
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+ verifier.verifyTextInLog("maven-toolchains-plugin with select-jdk-toolchain goal already configured");
+ }
+}
diff --git a/its/core-it-suite/src/test/resources/mvnup-toolchain-plugin-strategy/pom.xml b/its/core-it-suite/src/test/resources/mvnup-toolchain-plugin-strategy/pom.xml
new file mode 100644
index 0000000000..6fa0b3dc18
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/mvnup-toolchain-plugin-strategy/pom.xml
@@ -0,0 +1,12 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>old-source-test</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <properties>
+ <maven.compiler.source>1.5</maven.compiler.source>
+ <maven.compiler.target>1.5</maven.compiler.target>
+ </properties>
+</project>
(maven) 06/14: Fix auto-toolchain selection: compiler plugin config, compat layer, and JDK discovery
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 209791942bb89fb7218102b4ea9c5159e0c9952a
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 31 11:56:06 2026 +0200
Fix auto-toolchain selection: compiler plugin config, compat layer, and JDK discovery
Three fixes for automatic JDK toolchain selection:
1. Read source level from compiler plugin configuration: many projects set
<source> and <release> directly in the maven-compiler-plugin <configuration>
block rather than as properties. Added getSourceLevelFromCompilerPlugin()
to check both <release> and <source> elements.
2. Pass JdkToolchainDiscoverer to compat layer: ToolchainManagerFactory was
creating DefaultToolchainManager without the discoverer, so old plugins
going through the compat layer could detect incompatibility but had no
discoverer to find alternative JDKs.
3. Scan JAVA_HOME parent directory for sibling JDKs: in CI and container
environments, multiple JDKs are often installed as siblings (e.g.
/toolchain/jdk-8, /toolchain/jdk-11, /toolchain/jdk-21). Added parent
directory scanning from each JAVA*_HOME env var.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../maven/toolchain/ToolchainManagerFactory.java | 5 +-
.../apache/maven/impl/DefaultToolchainManager.java | 53 ++++++++++++++++++++++
.../apache/maven/impl/JdkToolchainDiscoverer.java | 13 +++++-
3 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java b/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java
index 5123ff11d0..153cfd0384 100644
--- a/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java
+++ b/compat/maven-compat/src/main/java/org/apache/maven/toolchain/ToolchainManagerFactory.java
@@ -155,7 +155,10 @@ public Optional<org.apache.maven.api.Toolchain> createDefaultToolchain()
});
}
allFactories.putAll(v4Factories);
- return new org.apache.maven.impl.DefaultToolchainManager(allFactories, logger) {};
+ org.apache.maven.impl.JdkToolchainDiscoverer discoverer = lookup.lookupOptional(
+ org.apache.maven.impl.JdkToolchainDiscoverer.class)
+ .orElse(null);
+ return new org.apache.maven.impl.DefaultToolchainManager(allFactories, discoverer, logger) {};
}
public class DefaultToolchainManagerV4 implements org.apache.maven.api.services.ToolchainManager {
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index 9169252983..890d0f5d15 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -37,6 +37,7 @@
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Build;
+import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.Source;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.ToolchainFactory;
@@ -44,6 +45,7 @@
import org.apache.maven.api.services.ToolchainManager;
import org.apache.maven.api.services.ToolchainManagerException;
import org.apache.maven.api.toolchain.ToolchainModel;
+import org.apache.maven.api.xml.XmlNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -264,6 +266,57 @@ int getProjectRequiredSourceLevel(Session session) {
}
}
+ // Fall back to compiler plugin configuration (<release>, <source>)
+ int pluginLevel = getSourceLevelFromCompilerPlugin(build);
+ if (pluginLevel > 0) {
+ return pluginLevel;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Reads the source level from the maven-compiler-plugin configuration.
+ * Checks both {@code <release>} and {@code <source>} elements in the plugin's
+ * {@code <configuration>} block.
+ *
+ * @return the source level, or {@code -1} if not configured
+ */
+ private int getSourceLevelFromCompilerPlugin(Build build) {
+ if (build == null) {
+ return -1;
+ }
+ for (Plugin plugin : build.getPlugins()) {
+ if ("maven-compiler-plugin".equals(plugin.getArtifactId())
+ && (plugin.getGroupId() == null
+ || plugin.getGroupId().isEmpty()
+ || "org.apache.maven.plugins".equals(plugin.getGroupId()))) {
+ XmlNode config = plugin.getConfiguration();
+ if (config != null) {
+ // <release> takes precedence over <source>
+ XmlNode releaseNode = config.child("release");
+ if (releaseNode != null
+ && releaseNode.value() != null
+ && !releaseNode.value().isBlank()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(
+ releaseNode.value().trim());
+ if (level > 0) {
+ return level;
+ }
+ }
+ XmlNode sourceNode = config.child("source");
+ if (sourceNode != null
+ && sourceNode.value() != null
+ && !sourceNode.value().isBlank()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(
+ sourceNode.value().trim());
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
return -1;
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
index 1d7dfe767b..964441a1ed 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
@@ -111,6 +111,9 @@ private List<ToolchainModel> doDiscover(Map<String, String> properties) {
/**
* Collects JDK candidates from environment variables matching {@code JAVA*_HOME}.
* Environment variables are read from the properties map as {@code env.VAR_NAME} entries.
+ * Also scans the parent directory of {@code JAVA_HOME} for sibling JDK installations
+ * (common in CI environments and container images where multiple JDKs are installed
+ * under the same parent directory).
*/
void collectFromEnvironment(Set<Path> candidates, Map<String, String> properties) {
// Current JDK (from java.home system property)
@@ -123,7 +126,15 @@ void collectFromEnvironment(Set<Path> candidates, Map<String, String> properties
for (Map.Entry<String, String> entry : properties.entrySet()) {
String name = entry.getKey();
if (name.startsWith("env.JAVA") && name.endsWith("_HOME")) {
- addCandidate(candidates, Paths.get(entry.getValue()));
+ Path jdkPath = Paths.get(entry.getValue());
+ addCandidate(candidates, jdkPath);
+ // Also scan sibling directories — in CI and container environments,
+ // multiple JDKs are often installed under the same parent directory
+ // (e.g. /toolchain/jdk-8, /toolchain/jdk-11, /toolchain/jdk-21)
+ Path parent = jdkPath.getParent();
+ if (parent != null) {
+ scanSubdirectories(candidates, parent);
+ }
}
}
}
(maven) 13/14: Fix mvnup ToolchainPluginStrategy producing invalid version range (,-1]
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 0e13f823122ffa47ef093f6cd0089cebcfc07023
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Thu Aug 6 09:17:36 2026 +0200
Fix mvnup ToolchainPluginStrategy producing invalid version range (,-1]
When the project's source level is higher than the running JDK (e.g.
--source 20 on JDK 17), the project needs a NEWER JDK, not an older
one. Previously, latestJdkForSourceLevel() returned -1 ("still
supported") and addToolchainsPlugin() blindly used it, producing the
invalid version range (,-1] which caused:
Cannot find matching toolchain definitions for the following
toolchain types:{version=(,-1]}
Now skip adding the toolchains plugin entirely when the source level
is still supported by modern JDKs — the issue is the running JDK
being too old, not a source retirement problem.
Discovered via netbeans-html4j which has maven.compiler.source=20
and failed on JDK 17 runners.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../mvnup/goals/ToolchainPluginStrategy.java | 8 +++++
.../mvnup/goals/ToolchainPluginStrategyTest.java | 34 ++++++++++++++++++++++
2 files changed, 42 insertions(+)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
index 42ff9250ab..8360c0908f 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
@@ -140,6 +140,14 @@ protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomM
}
int latestJdk = JdkSourceLevelSupport.latestJdkForSourceLevel(sourceLevel);
+ if (latestJdk <= 0) {
+ // Source level is still supported by modern JDKs — the project needs a newer JDK,
+ // not an older one. Don't add the toolchains plugin (it would produce an invalid
+ // version range like "(,-1]"). Let the build fail with a clear "unsupported source" error.
+ context.success("Project requires --source " + sourceLevel + " which needs a newer JDK than "
+ + runningJdkMajor + "; upgrade the JDK rather than adding toolchains");
+ continue;
+ }
addToolchainsPlugin(pomDocument, latestJdk);
modifiedPoms.add(pomPath);
context.success("Added maven-toolchains-plugin with " + SELECT_JDK_TOOLCHAIN_GOAL + " goal (--source "
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
index 7414d262a1..87020a9d15 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
@@ -483,6 +483,40 @@ int getRunningJdkMajor() {
assertEquals(0, result.modifiedPoms().size());
}
+ @Test
+ @DisplayName("should not add toolchains plugin when source level is higher than running JDK")
+ void noToolchainWhenSourceTooHigh() {
+ // Simulate running JDK 17, project targets source 20
+ // The project needs a NEWER JDK, not an older one — adding a toolchain
+ // with version range (,-1] would be invalid
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.source>20</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ assertFalse(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+
@Test
@DisplayName("should not add duplicate plugin when already present")
void noDuplicatePlugin() {
(maven) 07/14: Fix DefaultToolchainManager(Map) constructor visibility for IT harness
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit cc0c0409ea27052ad2ff73701abe7fe1aabfb30a
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 31 11:59:39 2026 +0200
Fix DefaultToolchainManager(Map) constructor visibility for IT harness
The gh-11055 DI service injection IT calls new DefaultToolchainManager(Map.of())
from outside the package, so the single-arg Map constructor must be public.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../org/apache/maven/impl/DefaultToolchainManager.java | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index 890d0f5d15..df1a9be98e 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -62,24 +62,25 @@ public DefaultToolchainManager(Map<String, ToolchainFactory> factories, JdkToolc
}
/**
- * Used for tests only (no discoverer)
+ * Convenience constructor without a discoverer — auto-selection will skip
+ * filesystem discovery. Used by tests and IT harnesses.
*/
- protected DefaultToolchainManager(Map<String, ToolchainFactory> factories) {
+ public DefaultToolchainManager(Map<String, ToolchainFactory> factories) {
this(factories, null, null);
}
/**
- * Used for tests only (no discoverer, custom logger)
+ * Convenience constructor without a discoverer, with custom logger.
+ * Used by tests.
*/
- protected DefaultToolchainManager(Map<String, ToolchainFactory> factories, Logger logger) {
+ DefaultToolchainManager(Map<String, ToolchainFactory> factories, Logger logger) {
this(factories, null, logger);
}
/**
- * Used for tests only (full control)
+ * Full-control constructor. Used by tests.
*/
- protected DefaultToolchainManager(
- Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer, Logger logger) {
+ DefaultToolchainManager(Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer, Logger logger) {
this.factories = factories;
this.discoverer = discoverer;
this.logger = logger != null ? logger : LoggerFactory.getLogger(DefaultToolchainManager.class);
(maven) 10/14: Add mvnup ToolchainPluginStrategy: auto-add maven-toolchains-plugin
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 9c857e8b19a0892eadbaeefa45a6736b36efeadc
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Aug 5 15:33:26 2026 +0200
Add mvnup ToolchainPluginStrategy: auto-add maven-toolchains-plugin
When mvnup detects a project's --source/--release level is no longer
supported by the running JDK (per JEP 182), automatically add the
maven-toolchains-plugin with the select-jdk-toolchain goal. This
leverages the plugin's built-in JDK discovery mechanism instead of
duplicating it in core.
Also makes JdkSourceLevelSupport methods public so they can be reused
across modules (maven-impl → maven-cli).
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../mvnup/goals/ToolchainPluginStrategy.java | 341 +++++++++++++
.../mvnup/goals/ToolchainPluginStrategyTest.java | 527 +++++++++++++++++++++
.../apache/maven/impl/JdkSourceLevelSupport.java | 12 +-
3 files changed, 874 insertions(+), 6 deletions(-)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
new file mode 100644
index 0000000000..fb0dd8f067
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
@@ -0,0 +1,341 @@
+/*
+ * 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.cling.invoker.mvnup.goals;
+
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.Element;
+import org.apache.maven.api.cli.mvnup.UpgradeOptions;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Priority;
+import org.apache.maven.api.di.Singleton;
+import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+import org.apache.maven.impl.JdkSourceLevelSupport;
+
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.CONFIGURATION;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.GROUP_ID;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROPERTIES;
+
+/**
+ * Strategy for adding the {@code maven-toolchains-plugin} with the {@code select-jdk-toolchain}
+ * goal when the project's required {@code --source}/{@code --release} level is no longer supported
+ * by the running JDK.
+ *
+ * <p>This strategy detects the project's source level from:
+ * <ol>
+ * <li>{@code maven.compiler.release} property</li>
+ * <li>{@code maven.compiler.source} property</li>
+ * <li>Compiler plugin {@code <configuration><release>} or {@code <source>}</li>
+ * </ol>
+ *
+ * <p>If the running JDK does not support the detected source level (per JEP 182 retirement
+ * schedule), and the {@code maven-toolchains-plugin} is not already configured with the
+ * {@code select-jdk-toolchain} goal, this strategy adds it so that the plugin's built-in
+ * JDK discovery mechanism can find a compatible JDK at build time.
+ *
+ * @see <a href="https://openjdk.org/jeps/182">JEP 182: Policy for Retiring javac -source and -target Options</a>
+ */
+@Named
+@Singleton
+@Priority(15)
+public class ToolchainPluginStrategy extends AbstractUpgradeStrategy {
+
+ static final String MAVEN_TOOLCHAINS_PLUGIN = "maven-toolchains-plugin";
+ static final String TOOLCHAINS_PLUGIN_GROUP_ID = "org.apache.maven.plugins";
+ static final String SELECT_JDK_TOOLCHAIN_GOAL = "select-jdk-toolchain";
+
+ private static final String MAVEN_COMPILER_RELEASE = "maven.compiler.release";
+ private static final String MAVEN_COMPILER_SOURCE = "maven.compiler.source";
+ private static final String MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin";
+
+ @Override
+ public boolean isApplicable(UpgradeContext context) {
+ UpgradeOptions options = getOptions(context);
+
+ if (options.all().orElse(false)) {
+ return true;
+ }
+
+ // Same default logic as CompatibilityFixStrategy: run when no specific options
+ boolean noOptionsSpecified = options.all().isEmpty()
+ && options.infer().isEmpty()
+ && options.model().isEmpty()
+ && options.plugins().isEmpty()
+ && options.modelVersion().isEmpty();
+
+ if (noOptionsSpecified) {
+ return true;
+ }
+
+ // Run when --model is explicitly set
+ if (options.model().isPresent()) {
+ return options.model().get();
+ }
+
+ return false;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Adding maven-toolchains-plugin for JDK source level compatibility";
+ }
+
+ @Override
+ protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
+ Set<Path> processedPoms = new HashSet<>();
+ Set<Path> modifiedPoms = new HashSet<>();
+ Set<Path> errorPoms = new HashSet<>();
+
+ int runningJdkMajor = getRunningJdkMajor();
+
+ for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
+ Path pomPath = entry.getKey();
+ Document pomDocument = entry.getValue();
+ processedPoms.add(pomPath);
+
+ context.info(pomPath + " (checking JDK source level compatibility)");
+ context.indent();
+
+ try {
+ int sourceLevel = detectSourceLevel(pomDocument);
+ if (sourceLevel <= 0) {
+ context.success("No source level configured");
+ continue;
+ }
+
+ if (JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, sourceLevel)) {
+ context.success("Running JDK " + runningJdkMajor + " supports --source " + sourceLevel);
+ continue;
+ }
+
+ if (hasToolchainsPluginWithSelectGoal(pomDocument)) {
+ context.success(
+ "maven-toolchains-plugin with " + SELECT_JDK_TOOLCHAIN_GOAL + " goal already configured");
+ continue;
+ }
+
+ int latestJdk = JdkSourceLevelSupport.latestJdkForSourceLevel(sourceLevel);
+ addToolchainsPlugin(pomDocument);
+ modifiedPoms.add(pomPath);
+ context.success("Added maven-toolchains-plugin with " + SELECT_JDK_TOOLCHAIN_GOAL + " goal (--source "
+ + sourceLevel + " requires JDK <= " + latestJdk + ")");
+ } catch (Exception e) {
+ context.failure("Failed to add toolchains plugin: " + e.getMessage());
+ errorPoms.add(pomPath);
+ } finally {
+ context.unindent();
+ }
+ }
+
+ return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
+ }
+
+ /**
+ * Detects the project's required source level from properties or compiler plugin configuration.
+ *
+ * @return the source level as a major version, or {@code -1} if none is configured
+ */
+ int detectSourceLevel(Document pomDocument) {
+ Element root = pomDocument.root();
+
+ // Check properties: maven.compiler.release takes precedence
+ Element properties = root.childElement(PROPERTIES).orElse(null);
+ if (properties != null) {
+ Element releaseElement =
+ properties.childElement(MAVEN_COMPILER_RELEASE).orElse(null);
+ if (releaseElement != null) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(
+ releaseElement.textContent().trim());
+ if (level > 0) {
+ return level;
+ }
+ }
+
+ Element sourceElement =
+ properties.childElement(MAVEN_COMPILER_SOURCE).orElse(null);
+ if (sourceElement != null) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(
+ sourceElement.textContent().trim());
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+
+ // Check compiler plugin configuration
+ return detectSourceLevelFromCompilerPlugin(root);
+ }
+
+ private int detectSourceLevelFromCompilerPlugin(Element root) {
+ Element build = root.childElement(BUILD).orElse(null);
+ if (build == null) {
+ return -1;
+ }
+
+ // Check <build><plugins>
+ int level = detectFromPluginSection(build.childElement(PLUGINS).orElse(null));
+ if (level > 0) {
+ return level;
+ }
+
+ // Check <build><pluginManagement><plugins>
+ Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (pluginManagement != null) {
+ level = detectFromPluginSection(
+ pluginManagement.childElement(PLUGINS).orElse(null));
+ }
+
+ return level;
+ }
+
+ private int detectFromPluginSection(Element pluginsElement) {
+ if (pluginsElement == null) {
+ return -1;
+ }
+
+ for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
+ String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
+ String groupId = plugin.childTextTrimmed(GROUP_ID);
+ if (MAVEN_COMPILER_PLUGIN.equals(artifactId)
+ && (groupId == null || groupId.isEmpty() || TOOLCHAINS_PLUGIN_GROUP_ID.equals(groupId))) {
+ Element config = plugin.childElement(CONFIGURATION).orElse(null);
+ if (config != null) {
+ // <release> takes precedence
+ Element releaseNode = config.childElement("release").orElse(null);
+ if (releaseNode != null
+ && releaseNode.textContent() != null
+ && !releaseNode.textContent().isBlank()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(
+ releaseNode.textContent().trim());
+ if (level > 0) {
+ return level;
+ }
+ }
+ Element sourceNode = config.childElement("source").orElse(null);
+ if (sourceNode != null
+ && sourceNode.textContent() != null
+ && !sourceNode.textContent().isBlank()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(
+ sourceNode.textContent().trim());
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Checks whether the POM already has the {@code maven-toolchains-plugin}
+ * configured with the {@code select-jdk-toolchain} goal.
+ */
+ boolean hasToolchainsPluginWithSelectGoal(Document pomDocument) {
+ Element root = pomDocument.root();
+ Element build = root.childElement(BUILD).orElse(null);
+ if (build == null) {
+ return false;
+ }
+
+ // Check both <plugins> and <pluginManagement><plugins>
+ if (hasSelectGoalInPluginSection(build.childElement(PLUGINS).orElse(null))) {
+ return true;
+ }
+
+ Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (pluginManagement != null) {
+ return hasSelectGoalInPluginSection(
+ pluginManagement.childElement(PLUGINS).orElse(null));
+ }
+
+ return false;
+ }
+
+ private boolean hasSelectGoalInPluginSection(Element pluginsElement) {
+ if (pluginsElement == null) {
+ return false;
+ }
+
+ for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
+ String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
+ String groupId = plugin.childTextTrimmed(GROUP_ID);
+ if (MAVEN_TOOLCHAINS_PLUGIN.equals(artifactId)
+ && (groupId == null || groupId.isEmpty() || TOOLCHAINS_PLUGIN_GROUP_ID.equals(groupId))) {
+ // Check if it has the select-jdk-toolchain goal in any execution
+ Element executions = plugin.childElement("executions").orElse(null);
+ if (executions != null) {
+ for (Element execution :
+ executions.childElements("execution").toList()) {
+ Element goals = execution.childElement("goals").orElse(null);
+ if (goals != null) {
+ for (Element goal : goals.childElements("goal").toList()) {
+ if (SELECT_JDK_TOOLCHAIN_GOAL.equals(goal.textContentTrimmed())) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds the {@code maven-toolchains-plugin} with {@code select-jdk-toolchain} goal
+ * to the POM's {@code <build><plugins>} section.
+ */
+ void addToolchainsPlugin(Document pomDocument) {
+ Element root = pomDocument.root();
+ Element build = root.childElement(BUILD).orElse(null);
+ if (build == null) {
+ build = DomUtils.insertNewElement(BUILD, root);
+ }
+ Element plugins = build.childElement(PLUGINS).orElse(null);
+ if (plugins == null) {
+ plugins = DomUtils.insertNewElement(PLUGINS, build);
+ }
+
+ Element plugin = DomUtils.createPlugin(plugins, TOOLCHAINS_PLUGIN_GROUP_ID, MAVEN_TOOLCHAINS_PLUGIN, null);
+ Element executions = DomUtils.insertNewElement("executions", plugin);
+ Element execution = DomUtils.insertNewElement("execution", executions);
+ Element goals = DomUtils.insertNewElement("goals", execution);
+ DomUtils.insertContentElement(goals, "goal", SELECT_JDK_TOOLCHAIN_GOAL);
+ }
+
+ /**
+ * Returns the major version of the running JDK.
+ * Extracted as a method so tests can override it.
+ */
+ int getRunningJdkMajor() {
+ return JdkSourceLevelSupport.getRunningJdkMajor();
+ }
+}
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
new file mode 100644
index 0000000000..d928244575
--- /dev/null
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
@@ -0,0 +1,527 @@
+/*
+ * 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.cling.invoker.mvnup.goals;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import eu.maveniverse.domtrip.Document;
+import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+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.assertTrue;
+
+/**
+ * Unit tests for the {@link ToolchainPluginStrategy} class.
+ */
+@DisplayName("ToolchainPluginStrategy")
+class ToolchainPluginStrategyTest {
+
+ private static final Path POM_PATH = Paths.get("/project/pom.xml");
+
+ @Nested
+ @DisplayName("Applicability")
+ class ApplicabilityTests {
+
+ @Test
+ @DisplayName("should be applicable when --all option is true")
+ void applicableWithAll() {
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
+ assertTrue(strategy.isApplicable(TestUtils.createMockContext(TestUtils.createOptionsWithAll(true))));
+ }
+
+ @Test
+ @DisplayName("should be applicable with default options (no flags)")
+ void applicableWithDefaults() {
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
+ assertTrue(strategy.isApplicable(TestUtils.createMockContext()));
+ }
+
+ @Test
+ @DisplayName("should be applicable when --model is true")
+ void applicableWithModel() {
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
+ assertTrue(strategy.isApplicable(TestUtils.createMockContext(TestUtils.createOptionsWithFixModel(true))));
+ }
+
+ @Test
+ @DisplayName("should not be applicable when --model is false")
+ void notApplicableWithModelFalse() {
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
+ assertFalse(strategy.isApplicable(TestUtils.createMockContext(TestUtils.createOptionsWithFixModel(false))));
+ }
+ }
+
+ @Nested
+ @DisplayName("Source level detection")
+ class SourceLevelDetectionTests {
+
+ private ToolchainPluginStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ strategy = new ToolchainPluginStrategy();
+ }
+
+ @Test
+ @DisplayName("should detect source level from maven.compiler.release property")
+ void detectFromRelease() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>6</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(6, strategy.detectSourceLevel(doc));
+ }
+
+ @Test
+ @DisplayName("should detect source level from maven.compiler.source property")
+ void detectFromSource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.source>1.6</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(6, strategy.detectSourceLevel(doc));
+ }
+
+ @Test
+ @DisplayName("release property takes precedence over source property")
+ void releaseTakesPrecedenceOverSource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>7</maven.compiler.release>
+ <maven.compiler.source>6</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(7, strategy.detectSourceLevel(doc));
+ }
+
+ @Test
+ @DisplayName("should detect from compiler plugin <release>")
+ void detectFromCompilerPluginRelease() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>6</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(6, strategy.detectSourceLevel(doc));
+ }
+
+ @Test
+ @DisplayName("should detect from compiler plugin <source>")
+ void detectFromCompilerPluginSource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(5, strategy.detectSourceLevel(doc));
+ }
+
+ @Test
+ @DisplayName("should return -1 when no source level configured")
+ void noSourceLevel() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(-1, strategy.detectSourceLevel(doc));
+ }
+
+ @Test
+ @DisplayName("should detect from pluginManagement")
+ void detectFromPluginManagement() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>6</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertEquals(6, strategy.detectSourceLevel(doc));
+ }
+ }
+
+ @Nested
+ @DisplayName("Toolchains plugin detection")
+ class ToolchainsPluginDetectionTests {
+
+ private ToolchainPluginStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ strategy = new ToolchainPluginStrategy();
+ }
+
+ @Test
+ @DisplayName("should detect existing select-jdk-toolchain goal")
+ void detectExistingGoal() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-toolchains-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>select-jdk-toolchain</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+
+ @Test
+ @DisplayName("should not detect toolchains plugin without select-jdk-toolchain goal")
+ void noSelectGoal() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-toolchains-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>toolchain</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertFalse(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+
+ @Test
+ @DisplayName("should not detect when no toolchains plugin present")
+ void noToolchainsPlugin() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ assertFalse(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+ }
+
+ @Nested
+ @DisplayName("Plugin addition")
+ class PluginAdditionTests {
+
+ private ToolchainPluginStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ strategy = new ToolchainPluginStrategy();
+ }
+
+ @Test
+ @DisplayName("should add toolchains plugin to POM without build section")
+ void addToEmptyPom() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ strategy.addToolchainsPlugin(doc);
+
+ assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+
+ @Test
+ @DisplayName("should add toolchains plugin to POM with existing build section")
+ void addToExistingBuild() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ strategy.addToolchainsPlugin(doc);
+
+ assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+ }
+
+ @Nested
+ @DisplayName("Full apply")
+ class ApplyTests {
+
+ @Test
+ @DisplayName("should add plugin when source level is incompatible with running JDK")
+ void addsPluginWhenIncompatible() {
+ // Simulate running JDK 21, project targets source 6
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+ @Override
+ int getRunningJdkMajor() {
+ return 21;
+ }
+ };
+
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>6</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(1, result.modifiedPoms().size());
+ assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ }
+
+ @Test
+ @DisplayName("should not modify POM when source level is compatible")
+ void noModificationWhenCompatible() {
+ // Simulate running JDK 17, project targets source 11
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>11</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ }
+
+ @Test
+ @DisplayName("should not modify POM when no source level configured")
+ void noModificationWithoutSourceLevel() {
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+ @Override
+ int getRunningJdkMajor() {
+ return 21;
+ }
+ };
+
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ }
+
+ @Test
+ @DisplayName("should not add duplicate plugin when already present")
+ void noDuplicatePlugin() {
+ ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+ @Override
+ int getRunningJdkMajor() {
+ return 21;
+ }
+ };
+
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>6</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-toolchains-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>select-jdk-toolchain</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ }
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
index 56e64895d7..7571829a7d 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
@@ -32,7 +32,7 @@
* <li>JDK 21: removed {@code --source 7}, minimum is {@code 8}</li>
* </ul>
*/
-final class JdkSourceLevelSupport {
+public final class JdkSourceLevelSupport {
private JdkSourceLevelSupport() {}
@@ -42,7 +42,7 @@ private JdkSourceLevelSupport() {}
* @param jdkMajor the JDK major version (e.g., {@code 17}, {@code 21})
* @return the minimum supported source level
*/
- static int minimumSupportedSourceLevel(int jdkMajor) {
+ public static int minimumSupportedSourceLevel(int jdkMajor) {
if (jdkMajor <= 8) {
return 1;
}
@@ -62,7 +62,7 @@ static int minimumSupportedSourceLevel(int jdkMajor) {
* @param sourceLevel the desired source level
* @return {@code true} if the JDK supports the source level
*/
- static boolean supportsSourceLevel(int jdkMajor, int sourceLevel) {
+ public static boolean supportsSourceLevel(int jdkMajor, int sourceLevel) {
return sourceLevel >= minimumSupportedSourceLevel(jdkMajor) && sourceLevel <= jdkMajor;
}
@@ -80,7 +80,7 @@ static boolean supportsSourceLevel(int jdkMajor, int sourceLevel) {
* @param version the source level string
* @return the normalized major version, or {@code -1} if the string cannot be parsed
*/
- static int normalizeSourceLevel(String version) {
+ public static int normalizeSourceLevel(String version) {
if (version == null || version.isEmpty()) {
return -1;
}
@@ -138,7 +138,7 @@ private static int indexOfNonDigit(String s) {
* @return the latest JDK major version that supports it, or {@code -1} if the source level
* is still supported by all current JDKs
*/
- static int latestJdkForSourceLevel(int sourceLevel) {
+ public static int latestJdkForSourceLevel(int sourceLevel) {
if (sourceLevel <= 5) {
return 8;
}
@@ -156,7 +156,7 @@ static int latestJdkForSourceLevel(int sourceLevel) {
*
* @return the running JDK major version
*/
- static int getRunningJdkMajor() {
+ public static int getRunningJdkMajor() {
return Runtime.version().feature();
}
}
(maven) 11/14: Add version constraint to select-jdk-toolchain configuration
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 526ef4d459e224940fbdd739590073e7e673ba75
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Aug 5 23:47:09 2026 +0200
Add version constraint to select-jdk-toolchain configuration
The select-jdk-toolchain goal needs a <version> constraint to know
which JDK to select. Without it, it picks the latest LTS (e.g. 21)
which defeats the purpose. Now generates e.g. <version>(,8]</version>
based on latestJdkForSourceLevel() so the plugin matches a JDK that
actually supports the project's source level.
Verified end-to-end: mvnup on servicemix-utils (source 1.5) adds
the plugin with (,8], and with JAVA8_HOME set the build succeeds
with forked compilation via JDK 8.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../cling/invoker/mvnup/goals/ToolchainPluginStrategy.java | 12 +++++++++---
.../invoker/mvnup/goals/ToolchainPluginStrategyTest.java | 12 ++++++++----
2 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
index fb0dd8f067..42ff9250ab 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
@@ -140,7 +140,7 @@ protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomM
}
int latestJdk = JdkSourceLevelSupport.latestJdkForSourceLevel(sourceLevel);
- addToolchainsPlugin(pomDocument);
+ addToolchainsPlugin(pomDocument, latestJdk);
modifiedPoms.add(pomPath);
context.success("Added maven-toolchains-plugin with " + SELECT_JDK_TOOLCHAIN_GOAL + " goal (--source "
+ sourceLevel + " requires JDK <= " + latestJdk + ")");
@@ -311,9 +311,13 @@ private boolean hasSelectGoalInPluginSection(Element pluginsElement) {
/**
* Adds the {@code maven-toolchains-plugin} with {@code select-jdk-toolchain} goal
- * to the POM's {@code <build><plugins>} section.
+ * to the POM's {@code <build><plugins>} section, configured with a version constraint
+ * so the plugin selects a JDK that supports the project's source level.
+ *
+ * @param pomDocument the POM document to modify
+ * @param maxJdkVersion the latest JDK major version that supports the source level
*/
- void addToolchainsPlugin(Document pomDocument) {
+ void addToolchainsPlugin(Document pomDocument, int maxJdkVersion) {
Element root = pomDocument.root();
Element build = root.childElement(BUILD).orElse(null);
if (build == null) {
@@ -329,6 +333,8 @@ void addToolchainsPlugin(Document pomDocument) {
Element execution = DomUtils.insertNewElement("execution", executions);
Element goals = DomUtils.insertNewElement("goals", execution);
DomUtils.insertContentElement(goals, "goal", SELECT_JDK_TOOLCHAIN_GOAL);
+ Element configuration = DomUtils.insertNewElement(CONFIGURATION, execution);
+ DomUtils.insertContentElement(configuration, "version", "(," + maxJdkVersion + "]");
}
/**
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
index d928244575..7414d262a1 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
@@ -342,7 +342,7 @@ void setUp() {
}
@Test
- @DisplayName("should add toolchains plugin to POM without build section")
+ @DisplayName("should add toolchains plugin with version constraint to POM without build section")
void addToEmptyPom() {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
@@ -354,13 +354,15 @@ void addToEmptyPom() {
</project>
""";
Document doc = Document.of(pomXml);
- strategy.addToolchainsPlugin(doc);
+ strategy.addToolchainsPlugin(doc, 11);
assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ String output = doc.toXml();
+ assertTrue(output.contains("<version>(,11]</version>"), "Expected version constraint in output: " + output);
}
@Test
- @DisplayName("should add toolchains plugin to POM with existing build section")
+ @DisplayName("should add toolchains plugin with version constraint to POM with existing build section")
void addToExistingBuild() {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
@@ -379,9 +381,11 @@ void addToExistingBuild() {
</project>
""";
Document doc = Document.of(pomXml);
- strategy.addToolchainsPlugin(doc);
+ strategy.addToolchainsPlugin(doc, 8);
assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+ String output = doc.toXml();
+ assertTrue(output.contains("<version>(,8]</version>"), "Expected version constraint in output: " + output);
}
}
(maven) 14/14: Add ModuleNameFixStrategy to fix invalid Automatic-Module-Name entries
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit f78d16a6fd29eca127378fb0e5b4a1c72d74e2a1
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Thu Aug 6 10:53:02 2026 +0200
Add ModuleNameFixStrategy to fix invalid Automatic-Module-Name entries
When mvnup upgrades maven-jar-plugin to 3.5.0, the newer maven-archiver
(3.3.0+, MSHARED-773) validates Automatic-Module-Name manifest entries
using SourceVersion.isName(). Projects that copied their hyphenated
artifact ID into the module name (e.g. "org.example.my-lib") now fail.
This strategy scans packaging plugin configs for <archive><manifestEntries>
<Automatic-Module-Name> entries with dashes and replaces them with dots,
producing valid Java module names.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../invoker/mvnup/goals/ModuleNameFixStrategy.java | 280 +++++++++++++++++
.../mvnup/goals/ModuleNameFixStrategyTest.java | 335 +++++++++++++++++++++
2 files changed, 615 insertions(+)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModuleNameFixStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModuleNameFixStrategy.java
new file mode 100644
index 0000000000..6ed315e270
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ModuleNameFixStrategy.java
@@ -0,0 +1,280 @@
+/*
+ * 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.cling.invoker.mvnup.goals;
+
+import javax.lang.model.SourceVersion;
+
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.Element;
+import org.apache.maven.api.cli.mvnup.UpgradeOptions;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Priority;
+import org.apache.maven.api.di.Singleton;
+import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.CONFIGURATION;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.GROUP_ID;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
+import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
+
+/**
+ * Strategy for fixing invalid {@code Automatic-Module-Name} manifest entries in POM files.
+ *
+ * <p>Java module names (as validated by {@link SourceVersion#isName(CharSequence)}) must be
+ * dot-separated sequences of valid Java identifiers. Dashes ({@code -}) are the most common
+ * illegal character because Maven artifact IDs conventionally use them, and projects often
+ * copy the artifact ID into the {@code Automatic-Module-Name} manifest entry without
+ * sanitizing it.
+ *
+ * <p>Since {@code maven-archiver 3.3.0}
+ * (<a href="https://issues.apache.org/jira/browse/MSHARED-773">MSHARED-773</a>),
+ * the archiver validates {@code Automatic-Module-Name} entries and fails the build on
+ * invalid names. When {@code mvnup} upgrades {@code maven-jar-plugin} (or other packaging
+ * plugins) to versions that pull in the newer archiver, previously-unchecked invalid names
+ * surface as build failures.
+ *
+ * <p>This strategy scans all packaging plugin configurations for
+ * {@code <archive><manifestEntries><Automatic-Module-Name>} elements whose values
+ * contain dashes, and replaces the dashes with dots to produce a valid module name.
+ */
+@Named
+@Singleton
+@Priority(12) // Run after plugin upgrades (10) but before compatibility fixes (20)
+public class ModuleNameFixStrategy extends AbstractUpgradeStrategy {
+
+ private static final String ARCHIVE = "archive";
+ private static final String MANIFEST_ENTRIES = "manifestEntries";
+ private static final String AUTOMATIC_MODULE_NAME = "Automatic-Module-Name";
+
+ /**
+ * Plugins that use {@code maven-archiver} and support
+ * {@code <configuration><archive><manifestEntries>}.
+ */
+ private static final Set<String> PACKAGING_PLUGINS = Set.of(
+ "maven-jar-plugin",
+ "maven-war-plugin",
+ "maven-ejb-plugin",
+ "maven-ear-plugin",
+ "maven-rar-plugin",
+ "maven-shade-plugin");
+
+ @Override
+ public boolean isApplicable(UpgradeContext context) {
+ UpgradeOptions options = getOptions(context);
+ return isOptionEnabled(options, options.plugins(), true);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Fixing invalid Automatic-Module-Name manifest entries";
+ }
+
+ @Override
+ protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
+ Set<Path> processedPoms = new HashSet<>();
+ Set<Path> modifiedPoms = new HashSet<>();
+ Set<Path> errorPoms = new HashSet<>();
+
+ for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
+ Path pomPath = entry.getKey();
+ Document pomDocument = entry.getValue();
+ processedPoms.add(pomPath);
+
+ context.info(pomPath + " (checking Automatic-Module-Name entries)");
+ context.indent();
+
+ try {
+ boolean modified = fixModuleNames(pomDocument, context);
+ if (modified) {
+ modifiedPoms.add(pomPath);
+ } else {
+ context.success("No invalid Automatic-Module-Name entries found");
+ }
+ } catch (Exception e) {
+ context.failure("Failed to fix module names: " + e.getMessage());
+ errorPoms.add(pomPath);
+ } finally {
+ context.unindent();
+ }
+ }
+
+ return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
+ }
+
+ /**
+ * Scans all plugin configurations in the POM for invalid {@code Automatic-Module-Name}
+ * entries and fixes them.
+ *
+ * @return true if any entries were modified
+ */
+ private boolean fixModuleNames(Document pomDocument, UpgradeContext context) {
+ Element root = pomDocument.root();
+ boolean modified = false;
+
+ // Check <build><plugins> and <build><pluginManagement><plugins>
+ Element build = root.childElement(BUILD).orElse(null);
+ if (build != null) {
+ modified |= fixInPluginSection(build.childElement(PLUGINS).orElse(null), context);
+
+ Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (pluginManagement != null) {
+ modified |= fixInPluginSection(
+ pluginManagement.childElement(PLUGINS).orElse(null), context);
+ }
+ }
+
+ // Check profiles
+ Element profiles = root.childElement(PROFILES).orElse(null);
+ if (profiles != null) {
+ for (Element profile : profiles.childElements(PROFILE).toList()) {
+ Element profileBuild = profile.childElement(BUILD).orElse(null);
+ if (profileBuild != null) {
+ modified |= fixInPluginSection(
+ profileBuild.childElement(PLUGINS).orElse(null), context);
+
+ Element profilePluginMgmt =
+ profileBuild.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (profilePluginMgmt != null) {
+ modified |= fixInPluginSection(
+ profilePluginMgmt.childElement(PLUGINS).orElse(null), context);
+ }
+ }
+ }
+ }
+
+ return modified;
+ }
+
+ private boolean fixInPluginSection(Element pluginsElement, UpgradeContext context) {
+ if (pluginsElement == null) {
+ return false;
+ }
+
+ boolean modified = false;
+ for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
+ String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
+ String groupId = plugin.childTextTrimmed(GROUP_ID);
+
+ if (isPackagingPlugin(groupId, artifactId)) {
+ modified |= fixInPluginConfig(plugin, context, artifactId);
+ }
+ }
+ return modified;
+ }
+
+ private boolean fixInPluginConfig(Element plugin, UpgradeContext context, String pluginArtifactId) {
+ Element config = plugin.childElement(CONFIGURATION).orElse(null);
+ if (config == null) {
+ return false;
+ }
+
+ Element archive = config.childElement(ARCHIVE).orElse(null);
+ if (archive == null) {
+ return false;
+ }
+
+ Element manifestEntries = archive.childElement(MANIFEST_ENTRIES).orElse(null);
+ if (manifestEntries == null) {
+ return false;
+ }
+
+ Element moduleNameElement =
+ manifestEntries.childElement(AUTOMATIC_MODULE_NAME).orElse(null);
+ if (moduleNameElement == null) {
+ return false;
+ }
+
+ String moduleName = moduleNameElement.textContent();
+ if (moduleName == null || moduleName.isBlank()) {
+ return false;
+ }
+
+ moduleName = moduleName.trim();
+ if (SourceVersion.isName(moduleName)) {
+ return false; // Already valid
+ }
+
+ String fixed = sanitizeModuleName(moduleName);
+ if (fixed.equals(moduleName) || !SourceVersion.isName(fixed)) {
+ // Cannot fix automatically — warn
+ context.failure("Cannot automatically fix invalid Automatic-Module-Name '" + moduleName + "' in "
+ + pluginArtifactId);
+ return false;
+ }
+
+ moduleNameElement.textContent(fixed);
+ context.success(
+ "Fixed Automatic-Module-Name in " + pluginArtifactId + ": '" + moduleName + "' → '" + fixed + "'");
+ return true;
+ }
+
+ /**
+ * Checks if the plugin is one of the packaging plugins that support
+ * {@code <archive><manifestEntries>}.
+ */
+ private static boolean isPackagingPlugin(String groupId, String artifactId) {
+ if (artifactId == null) {
+ return false;
+ }
+ // Accept both explicit groupId and implicit (null/empty = default)
+ if (groupId != null && !groupId.isEmpty() && !"org.apache.maven.plugins".equals(groupId)) {
+ return false;
+ }
+ return PACKAGING_PLUGINS.contains(artifactId);
+ }
+
+ /**
+ * Sanitizes a module name by replacing dashes with dots.
+ *
+ * <p>Java module names are dot-separated identifiers. Dashes are the most common
+ * illegal character because Maven artifact IDs use them as separators. Replacing
+ * dashes with dots is the standard convention (used by the JDK itself when deriving
+ * automatic module names from JAR file names).
+ *
+ * @param moduleName the original (possibly invalid) module name
+ * @return the sanitized module name
+ */
+ static String sanitizeModuleName(String moduleName) {
+ // Replace dashes with dots (standard convention for automatic module names)
+ String sanitized = moduleName.replace('-', '.');
+
+ // Collapse consecutive dots that may result from replacements like "foo--bar"
+ sanitized = sanitized.replaceAll("\\.{2,}", ".");
+
+ // Remove leading/trailing dots
+ if (sanitized.startsWith(".")) {
+ sanitized = sanitized.substring(1);
+ }
+ if (sanitized.endsWith(".")) {
+ sanitized = sanitized.substring(0, sanitized.length() - 1);
+ }
+
+ return sanitized;
+ }
+}
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModuleNameFixStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModuleNameFixStrategyTest.java
new file mode 100644
index 0000000000..f87deb324d
--- /dev/null
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ModuleNameFixStrategyTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.cling.invoker.mvnup.goals;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import eu.maveniverse.domtrip.Document;
+import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+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.assertTrue;
+
+/**
+ * Unit tests for the {@link ModuleNameFixStrategy} class.
+ */
+@DisplayName("ModuleNameFixStrategy")
+class ModuleNameFixStrategyTest {
+
+ private static final Path POM_PATH = Paths.get("/project/pom.xml");
+
+ @Nested
+ @DisplayName("sanitizeModuleName")
+ class SanitizeTests {
+
+ @Test
+ @DisplayName("should replace dashes with dots")
+ void replacesDashes() {
+ assertEquals(
+ "org.apache.geronimo.arthur.integration.test",
+ ModuleNameFixStrategy.sanitizeModuleName("org.apache.geronimo.arthur.integration-test"));
+ }
+
+ @Test
+ @DisplayName("should replace multiple dashes")
+ void replacesMultipleDashes() {
+ assertEquals(
+ "org.apache.winegrower.cepages.winegrower.cepage.osgi.cdi",
+ ModuleNameFixStrategy.sanitizeModuleName(
+ "org.apache.winegrower.cepages.winegrower-cepage-osgi-cdi"));
+ }
+
+ @Test
+ @DisplayName("should collapse consecutive dots")
+ void collapsesConsecutiveDots() {
+ assertEquals("foo.bar", ModuleNameFixStrategy.sanitizeModuleName("foo--bar"));
+ }
+
+ @Test
+ @DisplayName("should leave valid names unchanged")
+ void leavesValidUnchanged() {
+ assertEquals("org.example.valid.name", ModuleNameFixStrategy.sanitizeModuleName("org.example.valid.name"));
+ }
+
+ @Test
+ @DisplayName("should handle leading dash")
+ void handlesLeadingDash() {
+ assertEquals("foo.bar", ModuleNameFixStrategy.sanitizeModuleName("-foo.bar"));
+ }
+
+ @Test
+ @DisplayName("should handle trailing dash")
+ void handlesTrailingDash() {
+ assertEquals("foo.bar", ModuleNameFixStrategy.sanitizeModuleName("foo.bar-"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Apply")
+ class ApplyTests {
+
+ @Test
+ @DisplayName("should fix Automatic-Module-Name with dashes in jar plugin config")
+ void fixesDashInJarPlugin() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>integration-test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>org.example.integration-test</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(1, result.modifiedPoms().size());
+ // Verify the fix was applied
+ String xml = doc.toXml();
+ assertTrue(xml.contains("org.example.integration.test"));
+ assertFalse(xml.contains("org.example.integration-test"));
+ }
+
+ @Test
+ @DisplayName("should not modify valid Automatic-Module-Name")
+ void noChangeForValid() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>my-module</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>org.example.mymodule</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ }
+
+ @Test
+ @DisplayName("should fix in pluginManagement section")
+ void fixesInPluginManagement() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>org.example.my-lib</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(1, result.modifiedPoms().size());
+ assertTrue(doc.toXml().contains("org.example.my.lib"));
+ }
+
+ @Test
+ @DisplayName("should handle POM without any plugins")
+ void noPlugins() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ }
+
+ @Test
+ @DisplayName("should fix in shade plugin configuration")
+ void fixesInShadePlugin() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>org.example.shaded-lib</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(1, result.modifiedPoms().size());
+ assertTrue(doc.toXml().contains("org.example.shaded.lib"));
+ }
+
+ @Test
+ @DisplayName("should ignore non-packaging plugins")
+ void ignoresNonPackagingPlugins() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.example</groupId>
+ <artifactId>custom-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>org.example.my-lib</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(0, result.modifiedPoms().size());
+ }
+
+ @Test
+ @DisplayName("should fix in profile plugin configuration")
+ void fixesInProfile() {
+ ModuleNameFixStrategy strategy = new ModuleNameFixStrategy();
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <profiles>
+ <profile>
+ <id>release</id>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>org.example.my-release</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+ </project>
+ """;
+ Document doc = Document.of(pomXml);
+ UpgradeContext context = TestUtils.createMockContext();
+ UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+ assertEquals(1, result.modifiedPoms().size());
+ assertTrue(doc.toXml().contains("org.example.my.release"));
+ }
+ }
+}
(maven) 01/14: Auto-select JDK toolchain when running JDK cannot compile the project's source level
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 0321e93b3e4e6424a965e08bed39d65b3746069e
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Thu Jul 30 16:31:51 2026 +0200
Auto-select JDK toolchain when running JDK cannot compile the project's source level
When the running JDK does not support the project's --source/--release level
(e.g., JDK 17 cannot compile --source 6), Maven now automatically searches
configured toolchains for a compatible JDK and selects it for compilation.
The auto-selection hooks into getToolchainFromBuildContext() — when no
explicit toolchain has been set via maven-toolchains-plugin, the manager
checks the project's targetVersion (Model 4.1.0) or legacy properties
(maven.compiler.release, maven.compiler.source) against the running JDK's
supported source levels. If the running JDK cannot handle the required
level, it picks the newest compatible JDK from configured toolchains,
caches the selection, and emits a warning.
Key changes:
- New JdkSourceLevelSupport utility mapping JDK versions to supported
--source levels based on the javac retirement schedule (JEP 182)
- DefaultToolchainManager.getToolchainFromBuildContext() now falls back
to auto-selection for "jdk" type when no explicit toolchain is stored
- Reads source level from Model 4.1.0 <source><targetVersion> elements
and legacy maven.compiler.release/source properties
- Prefers the newest compatible JDK from configured toolchains
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../apache/maven/impl/DefaultToolchainManager.java | 138 +++++++-
.../apache/maven/impl/JdkSourceLevelSupport.java | 116 +++++++
.../maven/impl/DefaultToolchainManagerTest.java | 369 +++++++++++++++++++++
.../maven/impl/JdkSourceLevelSupportTest.java | 141 ++++++++
4 files changed, 763 insertions(+), 1 deletion(-)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index cc2cb0360b..ec87370e32 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -26,6 +26,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
+import org.apache.maven.api.JavaToolchain;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
@@ -35,6 +36,8 @@
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
+import org.apache.maven.api.model.Build;
+import org.apache.maven.api.model.Source;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.ToolchainFactory;
import org.apache.maven.api.services.ToolchainFactoryException;
@@ -89,7 +92,21 @@ public Optional<Toolchain> getToolchainFromBuildContext(@Nonnull Session session
throws ToolchainManagerException {
Map<String, Object> context = retrieveContext(session);
ToolchainModel model = (ToolchainModel) context.get("toolchain-" + type);
- return Optional.ofNullable(model).flatMap(this::createToolchain);
+ if (model != null) {
+ return createToolchain(model);
+ }
+
+ // For JDK type, try auto-selection based on project's target version
+ if ("jdk".equals(type)) {
+ Optional<Toolchain> autoSelected = autoSelectJdkToolchain(session);
+ if (autoSelected.isPresent()) {
+ // Cache the selection so subsequent calls for this project return the same toolchain
+ context.put("toolchain-" + type, autoSelected.get().getModel());
+ }
+ return autoSelected;
+ }
+
+ return Optional.empty();
}
@Override
@@ -98,6 +115,125 @@ public void storeToolchainToBuildContext(@Nonnull Session session, @Nonnull Tool
context.put("toolchain-" + toolchain.getType(), toolchain.getModel());
}
+ /**
+ * Attempts to automatically select a JDK toolchain when the running JDK
+ * does not support the project's required {@code --source}/{@code --release} level.
+ * <p>
+ * Searches configured toolchains for the newest JDK that supports the required
+ * source level. If found, emits a warning and returns it.
+ */
+ Optional<Toolchain> autoSelectJdkToolchain(Session session) {
+ int requiredSourceLevel = getProjectRequiredSourceLevel(session);
+ if (requiredSourceLevel <= 0) {
+ return Optional.empty();
+ }
+
+ int runningJdkMajor = getRunningJdkMajor();
+ if (JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, requiredSourceLevel)) {
+ return Optional.empty();
+ }
+
+ // Search available toolchains for a compatible JDK, preferring the newest
+ List<Toolchain> allToolchains = getToolchains(session, "jdk", null);
+ Toolchain bestMatch = null;
+ int bestVersion = 0;
+
+ for (Toolchain tc : allToolchains) {
+ if (tc instanceof JavaToolchain jtc && jtc.getJavaVersion() != null) {
+ int tcMajor = JdkSourceLevelSupport.normalizeSourceLevel(
+ jtc.getJavaVersion().toString());
+ if (tcMajor > 0 && JdkSourceLevelSupport.supportsSourceLevel(tcMajor, requiredSourceLevel)) {
+ if (tcMajor > bestVersion) {
+ bestVersion = tcMajor;
+ bestMatch = tc;
+ }
+ }
+ }
+ }
+
+ if (bestMatch != null) {
+ JavaToolchain jtc = (JavaToolchain) bestMatch;
+ logger.warn(
+ "Project requires --source {} which is not supported by JDK {}.",
+ requiredSourceLevel,
+ runningJdkMajor);
+ logger.warn(
+ "Automatically selected JDK {} (discovered at {}) for compilation.",
+ jtc.getJavaVersion(),
+ jtc.getJavaHome());
+ logger.warn("To suppress this warning, configure the maven-toolchains-plugin explicitly");
+ logger.warn("or set <targetVersion> to a value supported by your JDK.");
+ return Optional.of(bestMatch);
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Reads the project's required source level from either Model 4.1.0
+ * {@code <source><targetVersion>} elements or legacy properties
+ * ({@code maven.compiler.release}, {@code maven.compiler.source}).
+ *
+ * @return the required source level as a major version, or {@code -1} if none is specified
+ */
+ int getProjectRequiredSourceLevel(Session session) {
+ Optional<Project> current = session.getService(Lookup.class).lookupOptional(Project.class);
+ if (current.isEmpty()) {
+ return -1;
+ }
+
+ Project project = current.get();
+
+ // Check Model 4.1.0 <source><targetVersion> elements
+ Build build = project.getModel().getBuild();
+ if (build != null) {
+ List<Source> sources = build.getSources();
+ if (sources != null) {
+ for (Source source : sources) {
+ String targetVersion = source.getTargetVersion();
+ if (targetVersion != null && !targetVersion.isEmpty()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(targetVersion);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
+
+ // Fall back to legacy properties
+ Map<String, String> properties = project.getModel().getProperties();
+ if (properties != null) {
+ // maven.compiler.release takes precedence
+ String release = properties.get("maven.compiler.release");
+ if (release != null && !release.isEmpty()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(release);
+ if (level > 0) {
+ return level;
+ }
+ }
+
+ // Then maven.compiler.source
+ String source = properties.get("maven.compiler.source");
+ if (source != null && !source.isEmpty()) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(source);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the major version of the running JDK.
+ * Extracted as a method so tests can override it.
+ */
+ int getRunningJdkMajor() {
+ return JdkSourceLevelSupport.getRunningJdkMajor();
+ }
+
private Optional<Toolchain> createToolchain(ToolchainModel model) {
String type = Objects.requireNonNull(model.getType(), "model.getType()");
ToolchainFactory factory = factories.get(type);
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
new file mode 100644
index 0000000000..30a1fecac4
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
@@ -0,0 +1,116 @@
+/*
+ * 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.impl;
+
+/**
+ * Utility class for JDK source level compatibility checks.
+ * <p>
+ * Maps JDK major versions to their supported {@code --source}/{@code --release} levels,
+ * based on the javac retirement schedule defined in
+ * <a href="https://openjdk.org/jeps/182">JEP 182</a> and subsequent JDK releases.
+ * <p>
+ * The retirement schedule follows these milestones:
+ * <ul>
+ * <li>JDK 9: removed {@code --source 1} through {@code 5}, minimum is {@code 6}</li>
+ * <li>JDK 12: removed {@code --source 6}, minimum is {@code 7}</li>
+ * <li>JDK 21: removed {@code --source 7}, minimum is {@code 8}</li>
+ * </ul>
+ */
+final class JdkSourceLevelSupport {
+
+ private JdkSourceLevelSupport() {}
+
+ /**
+ * Returns the minimum {@code --source} level supported by a given JDK major version.
+ *
+ * @param jdkMajor the JDK major version (e.g., {@code 17}, {@code 21})
+ * @return the minimum supported source level
+ */
+ static int minimumSupportedSourceLevel(int jdkMajor) {
+ if (jdkMajor <= 8) {
+ return 1;
+ }
+ if (jdkMajor <= 11) {
+ return 6;
+ }
+ if (jdkMajor <= 20) {
+ return 7;
+ }
+ return 8;
+ }
+
+ /**
+ * Returns whether a given JDK version supports the specified {@code --source} level.
+ *
+ * @param jdkMajor the JDK major version
+ * @param sourceLevel the desired source level
+ * @return {@code true} if the JDK supports the source level
+ */
+ static boolean supportsSourceLevel(int jdkMajor, int sourceLevel) {
+ return sourceLevel >= minimumSupportedSourceLevel(jdkMajor) && sourceLevel <= jdkMajor;
+ }
+
+ /**
+ * Normalizes a source level string to a major version number.
+ * <p>
+ * Handles legacy formats:
+ * <ul>
+ * <li>{@code "1.5"} → {@code 5}</li>
+ * <li>{@code "1.8"} → {@code 8}</li>
+ * <li>{@code "11"} → {@code 11}</li>
+ * <li>{@code "21.0.1"} → {@code 21}</li>
+ * </ul>
+ *
+ * @param version the source level string
+ * @return the normalized major version, or {@code -1} if the string cannot be parsed
+ */
+ static int normalizeSourceLevel(String version) {
+ if (version == null || version.isEmpty()) {
+ return -1;
+ }
+ version = version.trim();
+ // Handle "1.x" legacy format (e.g., "1.5", "1.8")
+ if (version.startsWith("1.") && version.length() > 2) {
+ try {
+ return Integer.parseInt(version.substring(2));
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+ // Handle dotted versions like "21.0.1" — take the first segment
+ int dotIndex = version.indexOf('.');
+ if (dotIndex > 0) {
+ version = version.substring(0, dotIndex);
+ }
+ try {
+ return Integer.parseInt(version);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the major version of the currently running JDK.
+ *
+ * @return the running JDK major version
+ */
+ static int getRunningJdkMajor() {
+ return Runtime.version().feature();
+ }
+}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java
index c916d0ca5f..951d297afd 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultToolchainManagerTest.java
@@ -23,10 +23,15 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import org.apache.maven.api.JavaToolchain;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.Toolchain;
+import org.apache.maven.api.Version;
+import org.apache.maven.api.model.Build;
+import org.apache.maven.api.model.Model;
+import org.apache.maven.api.model.Source;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.ToolchainFactory;
import org.apache.maven.api.toolchain.ToolchainModel;
@@ -35,12 +40,14 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.slf4j.Logger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -122,4 +129,366 @@ void retrieveContextWithoutProject() {
void getToolchainsWithNullType() {
assertThrows(NullPointerException.class, () -> manager.getToolchains(session, null, null));
}
+
+ // --- Auto-selection tests ---
+
+ @Test
+ void autoSelectJdkToolchainWhenNoTargetVersion() {
+ // Project has no targetVersion configured — should not auto-select
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder().build(Build.newBuilder().build()).build();
+ when(project.getModel()).thenReturn(model);
+
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory)) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void autoSelectJdkToolchainWhenRunningJdkSupportsLevel() {
+ // Project targets source 11, running JDK 17 supports it — no auto-select
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .build(Build.newBuilder()
+ .sources(List.of(Source.newBuilder().targetVersion("11").build()))
+ .build())
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory)) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void autoSelectJdkToolchainWhenRunningJdkDoesNotSupportLevel() {
+ // Project targets source 6, running JDK 17 doesn't support it
+ // JDK 11 toolchain available and supports source 6
+ Logger testLogger = mock(Logger.class);
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ // Set up project with targetVersion 6
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .build(Build.newBuilder()
+ .sources(List.of(Source.newBuilder().targetVersion("6").build()))
+ .build())
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ // Set up available JDK 11 toolchain
+ JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
+ Version jdk11Version = mock(Version.class);
+ when(jdk11Version.toString()).thenReturn("11");
+ when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+ when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
+
+ ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
+ when(session.getToolchains()).thenReturn(List.of(jdk11Model));
+ when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
+ when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+
+ assertTrue(result.isPresent());
+ assertEquals(jdk11Toolchain, result.get());
+ verify(testLogger).warn("Project requires --source {} which is not supported by JDK {}.", 6, 17);
+ verify(testLogger)
+ .warn(
+ "Automatically selected JDK {} (discovered at {}) for compilation.",
+ jdk11Version,
+ "/usr/lib/jvm/java-11");
+ }
+
+ @Test
+ void autoSelectJdkToolchainPrefersNewestCompatible() {
+ // Project targets source 6, running JDK 17
+ // JDK 8 and JDK 11 both support source 6; should select JDK 11 (newest)
+ Logger testLogger = mock(Logger.class);
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .build(Build.newBuilder()
+ .sources(List.of(Source.newBuilder().targetVersion("6").build()))
+ .build())
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ // JDK 8 toolchain
+ JavaToolchain jdk8Toolchain = mock(JavaToolchain.class);
+ Version jdk8Version = mock(Version.class);
+ when(jdk8Version.toString()).thenReturn("8");
+ when(jdk8Toolchain.getJavaVersion()).thenReturn(jdk8Version);
+
+ // JDK 11 toolchain
+ JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
+ Version jdk11Version = mock(Version.class);
+ when(jdk11Version.toString()).thenReturn("11");
+ when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+ when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
+
+ // Use distinct provides so ToolchainModel.equals() distinguishes them
+ ToolchainModel jdk8Model = ToolchainModel.newBuilder()
+ .type("jdk")
+ .provides(Map.of("version", "8"))
+ .build();
+ ToolchainModel jdk11Model = ToolchainModel.newBuilder()
+ .type("jdk")
+ .provides(Map.of("version", "11"))
+ .build();
+ when(session.getToolchains()).thenReturn(List.of(jdk8Model, jdk11Model));
+ when(jdkFactory.createToolchain(jdk8Model)).thenReturn(jdk8Toolchain);
+ when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
+ when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+
+ assertTrue(result.isPresent());
+ assertEquals(jdk11Toolchain, result.get());
+ }
+
+ @Test
+ void autoSelectJdkToolchainNoCompatibleToolchainAvailable() {
+ // Project targets source 5, running JDK 17
+ // Only JDK 11 toolchain available (min source 6, doesn't support 5)
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory)) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .build(Build.newBuilder()
+ .sources(List.of(Source.newBuilder().targetVersion("5").build()))
+ .build())
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ // JDK 11 doesn't support source 5
+ JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
+ Version jdk11Version = mock(Version.class);
+ when(jdk11Version.toString()).thenReturn("11");
+ when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+
+ ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
+ when(session.getToolchains()).thenReturn(List.of(jdk11Model));
+ when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
+ when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void autoSelectJdkToolchainFromLegacyProperties() {
+ // Project uses maven.compiler.release=6 (legacy property), running JDK 17
+ Logger testLogger = mock(Logger.class);
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .properties(Map.of("maven.compiler.release", "6"))
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
+ Version jdk11Version = mock(Version.class);
+ when(jdk11Version.toString()).thenReturn("11");
+ when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+ when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
+
+ ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
+ when(session.getToolchains()).thenReturn(List.of(jdk11Model));
+ when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
+ when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+
+ assertTrue(result.isPresent());
+ assertEquals(jdk11Toolchain, result.get());
+ }
+
+ @Test
+ void autoSelectJdkToolchainFromLegacySourceProperty() {
+ // Project uses maven.compiler.source=1.6 (legacy property), running JDK 17
+ Logger testLogger = mock(Logger.class);
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .properties(Map.of("maven.compiler.source", "1.6"))
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
+ Version jdk11Version = mock(Version.class);
+ when(jdk11Version.toString()).thenReturn("11");
+ when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+ when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
+
+ ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
+ when(session.getToolchains()).thenReturn(List.of(jdk11Model));
+ when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
+ when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+
+ Optional<Toolchain> result = testManager.autoSelectJdkToolchain(session);
+
+ assertTrue(result.isPresent());
+ assertEquals(jdk11Toolchain, result.get());
+ }
+
+ @Test
+ void getToolchainFromBuildContextAutoSelectsFallback() {
+ // Verify getToolchainFromBuildContext calls auto-selection when no explicit toolchain
+ Logger testLogger = mock(Logger.class);
+ Map<String, Object> context = new ConcurrentHashMap<>();
+ SessionData data = mock(SessionData.class);
+
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory), testLogger) {
+ @Override
+ int getRunningJdkMajor() {
+ return 17;
+ }
+ };
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ when(session.getData()).thenReturn(data);
+ when(data.computeIfAbsent(any(), any())).thenReturn(context);
+
+ Model model = Model.newBuilder()
+ .build(Build.newBuilder()
+ .sources(List.of(Source.newBuilder().targetVersion("6").build()))
+ .build())
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ JavaToolchain jdk11Toolchain = mock(JavaToolchain.class);
+ Version jdk11Version = mock(Version.class);
+ when(jdk11Version.toString()).thenReturn("11");
+ when(jdk11Toolchain.getJavaVersion()).thenReturn(jdk11Version);
+ when(jdk11Toolchain.getJavaHome()).thenReturn("/usr/lib/jvm/java-11");
+
+ ToolchainModel jdk11Model = ToolchainModel.newBuilder().type("jdk").build();
+ when(jdk11Toolchain.getModel()).thenReturn(jdk11Model);
+ when(session.getToolchains()).thenReturn(List.of(jdk11Model));
+ when(jdkFactory.createToolchain(jdk11Model)).thenReturn(jdk11Toolchain);
+ when(jdkFactory.createDefaultToolchain()).thenReturn(Optional.empty());
+
+ Optional<Toolchain> result = testManager.getToolchainFromBuildContext(session, "jdk");
+
+ assertTrue(result.isPresent());
+ assertEquals(jdk11Toolchain, result.get());
+ }
+
+ @Test
+ void getToolchainFromBuildContextReturnsExplicitOverAutoSelect() {
+ // When an explicit toolchain is stored via storeToolchainToBuildContext,
+ // it takes precedence over auto-selection
+ Map<String, Object> context = new ConcurrentHashMap<>();
+ SessionData data = mock(SessionData.class);
+ toolchainModel = ToolchainModel.newBuilder().type("jdk").build();
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ when(session.getData()).thenReturn(data);
+ when(data.computeIfAbsent(any(), any())).thenReturn(context);
+ when(mockToolchain.getType()).thenReturn("jdk");
+ when(mockToolchain.getModel()).thenReturn(toolchainModel);
+ when(jdkFactory.createToolchain(any(ToolchainModel.class))).thenReturn(mockToolchain);
+
+ // Store explicit toolchain using the proper API
+ manager.storeToolchainToBuildContext(session, mockToolchain);
+
+ // Now retrieve — should get the explicit one, not auto-select
+ Optional<Toolchain> result = manager.getToolchainFromBuildContext(session, "jdk");
+
+ assertTrue(result.isPresent());
+ assertEquals(mockToolchain, result.get());
+ }
+
+ @Test
+ void getToolchainFromBuildContextNonJdkTypeDoesNotAutoSelect() {
+ // Auto-selection should only apply to "jdk" type
+ Map<String, Object> context = new ConcurrentHashMap<>();
+ SessionData data = mock(SessionData.class);
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ when(session.getData()).thenReturn(data);
+ when(data.computeIfAbsent(any(), any())).thenReturn(context);
+
+ // No "otherType" factory registered; getToolchainFromBuildContext should return empty
+ // without attempting auto-selection
+ Optional<Toolchain> result = manager.getToolchainFromBuildContext(session, "otherType");
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void getProjectRequiredSourceLevelTargetVersionTakesPrecedence() {
+ // targetVersion in sources should take precedence over properties
+ DefaultToolchainManager testManager = new DefaultToolchainManager(Map.of("jdk", jdkFactory));
+
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.of(project));
+ Model model = Model.newBuilder()
+ .properties(Map.of("maven.compiler.release", "11"))
+ .build(Build.newBuilder()
+ .sources(List.of(Source.newBuilder().targetVersion("8").build()))
+ .build())
+ .build();
+ when(project.getModel()).thenReturn(model);
+
+ assertEquals(8, testManager.getProjectRequiredSourceLevel(session));
+ }
+
+ @Test
+ void getProjectRequiredSourceLevelNoProject() {
+ when(session.getService(Lookup.class)).thenReturn(lookup);
+ when(lookup.lookupOptional(Project.class)).thenReturn(Optional.empty());
+
+ assertEquals(-1, manager.getProjectRequiredSourceLevel(session));
+ }
}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java
new file mode 100644
index 0000000000..e3b215f8ad
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.impl;
+
+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.assertTrue;
+
+class JdkSourceLevelSupportTest {
+
+ @Test
+ void minimumSupportedSourceLevelJdk8AndEarlier() {
+ assertEquals(1, JdkSourceLevelSupport.minimumSupportedSourceLevel(7));
+ assertEquals(1, JdkSourceLevelSupport.minimumSupportedSourceLevel(8));
+ }
+
+ @Test
+ void minimumSupportedSourceLevelJdk9To11() {
+ assertEquals(6, JdkSourceLevelSupport.minimumSupportedSourceLevel(9));
+ assertEquals(6, JdkSourceLevelSupport.minimumSupportedSourceLevel(10));
+ assertEquals(6, JdkSourceLevelSupport.minimumSupportedSourceLevel(11));
+ }
+
+ @Test
+ void minimumSupportedSourceLevelJdk12To20() {
+ assertEquals(7, JdkSourceLevelSupport.minimumSupportedSourceLevel(12));
+ assertEquals(7, JdkSourceLevelSupport.minimumSupportedSourceLevel(15));
+ assertEquals(7, JdkSourceLevelSupport.minimumSupportedSourceLevel(17));
+ assertEquals(7, JdkSourceLevelSupport.minimumSupportedSourceLevel(20));
+ }
+
+ @Test
+ void minimumSupportedSourceLevelJdk21AndLater() {
+ assertEquals(8, JdkSourceLevelSupport.minimumSupportedSourceLevel(21));
+ assertEquals(8, JdkSourceLevelSupport.minimumSupportedSourceLevel(22));
+ assertEquals(8, JdkSourceLevelSupport.minimumSupportedSourceLevel(25));
+ }
+
+ @Test
+ void supportsSourceLevelJdk8() {
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(8, 1));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(8, 5));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(8, 6));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(8, 8));
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(8, 9));
+ }
+
+ @Test
+ void supportsSourceLevelJdk11() {
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(11, 5));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(11, 6));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(11, 8));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(11, 11));
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(11, 12));
+ }
+
+ @Test
+ void supportsSourceLevelJdk17() {
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(17, 5));
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(17, 6));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(17, 7));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(17, 8));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(17, 11));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(17, 17));
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(17, 18));
+ }
+
+ @Test
+ void supportsSourceLevelJdk21() {
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(21, 6));
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(21, 7));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(21, 8));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(21, 11));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(21, 17));
+ assertTrue(JdkSourceLevelSupport.supportsSourceLevel(21, 21));
+ assertFalse(JdkSourceLevelSupport.supportsSourceLevel(21, 22));
+ }
+
+ @Test
+ void normalizeSourceLevelLegacyFormat() {
+ assertEquals(5, JdkSourceLevelSupport.normalizeSourceLevel("1.5"));
+ assertEquals(6, JdkSourceLevelSupport.normalizeSourceLevel("1.6"));
+ assertEquals(7, JdkSourceLevelSupport.normalizeSourceLevel("1.7"));
+ assertEquals(8, JdkSourceLevelSupport.normalizeSourceLevel("1.8"));
+ }
+
+ @Test
+ void normalizeSourceLevelModernFormat() {
+ assertEquals(5, JdkSourceLevelSupport.normalizeSourceLevel("5"));
+ assertEquals(6, JdkSourceLevelSupport.normalizeSourceLevel("6"));
+ assertEquals(8, JdkSourceLevelSupport.normalizeSourceLevel("8"));
+ assertEquals(9, JdkSourceLevelSupport.normalizeSourceLevel("9"));
+ assertEquals(11, JdkSourceLevelSupport.normalizeSourceLevel("11"));
+ assertEquals(17, JdkSourceLevelSupport.normalizeSourceLevel("17"));
+ assertEquals(21, JdkSourceLevelSupport.normalizeSourceLevel("21"));
+ }
+
+ @Test
+ void normalizeSourceLevelDottedVersion() {
+ assertEquals(21, JdkSourceLevelSupport.normalizeSourceLevel("21.0.1"));
+ assertEquals(17, JdkSourceLevelSupport.normalizeSourceLevel("17.0.2"));
+ assertEquals(11, JdkSourceLevelSupport.normalizeSourceLevel("11.0.3"));
+ }
+
+ @Test
+ void normalizeSourceLevelInvalid() {
+ assertEquals(-1, JdkSourceLevelSupport.normalizeSourceLevel(null));
+ assertEquals(-1, JdkSourceLevelSupport.normalizeSourceLevel(""));
+ assertEquals(-1, JdkSourceLevelSupport.normalizeSourceLevel("abc"));
+ assertEquals(-1, JdkSourceLevelSupport.normalizeSourceLevel("${java.version}"));
+ }
+
+ @Test
+ void normalizeSourceLevelWithWhitespace() {
+ assertEquals(11, JdkSourceLevelSupport.normalizeSourceLevel(" 11 "));
+ assertEquals(8, JdkSourceLevelSupport.normalizeSourceLevel(" 1.8 "));
+ }
+
+ @Test
+ void getRunningJdkMajorReturnsPositive() {
+ assertTrue(JdkSourceLevelSupport.getRunningJdkMajor() > 0);
+ }
+}
(maven) 04/14: Add lazy JDK discovery from filesystem for auto-selection fallback
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit a800a88ca0b81b74854418d735afc0ca6ed20b12
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 31 10:46:19 2026 +0200
Add lazy JDK discovery from filesystem for auto-selection fallback
When no compatible JDK is found in toolchains.xml, Maven now lazily
discovers JDKs from the filesystem by scanning well-known locations:
JAVA*_HOME env vars, SDKMan, IntelliJ .jdks/, Gradle, jEnv, JBang,
asdf, mise, and OS-specific paths (/usr/lib/jvm, etc.).
Discovery only runs on the failure path (running JDK incompatible
AND no configured toolchain matches), so normal builds pay zero cost.
JDK version is read from the release file (no java process spawned).
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../apache/maven/impl/DefaultToolchainManager.java | 76 +++--
.../apache/maven/impl/JdkSourceLevelSupport.java | 22 +-
.../apache/maven/impl/JdkToolchainDiscoverer.java | 310 +++++++++++++++++++++
.../maven/impl/JdkToolchainDiscovererTest.java | 180 ++++++++++++
4 files changed, 566 insertions(+), 22 deletions(-)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index 88430004eb..45df52a465 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -51,18 +51,35 @@
@Singleton
public class DefaultToolchainManager implements ToolchainManager {
private final Map<String, ToolchainFactory> factories;
+ private final JdkToolchainDiscoverer discoverer;
private final Logger logger;
@Inject
- public DefaultToolchainManager(Map<String, ToolchainFactory> factories) {
- this(factories, null);
+ public DefaultToolchainManager(Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer) {
+ this(factories, discoverer, null);
}
/**
- * Used for tests only
+ * Used for tests only (no discoverer)
+ */
+ protected DefaultToolchainManager(Map<String, ToolchainFactory> factories) {
+ this(factories, null, null);
+ }
+
+ /**
+ * Used for tests only (no discoverer, custom logger)
*/
protected DefaultToolchainManager(Map<String, ToolchainFactory> factories, Logger logger) {
+ this(factories, null, logger);
+ }
+
+ /**
+ * Used for tests only (full control)
+ */
+ protected DefaultToolchainManager(
+ Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer, Logger logger) {
this.factories = factories;
+ this.discoverer = discoverer;
this.logger = logger != null ? logger : LoggerFactory.getLogger(DefaultToolchainManager.class);
}
@@ -119,8 +136,9 @@ public void storeToolchainToBuildContext(@Nonnull Session session, @Nonnull Tool
* Attempts to automatically select a JDK toolchain when the running JDK
* does not support the project's required {@code --source}/{@code --release} level.
* <p>
- * Searches configured toolchains for the newest JDK that supports the required
- * source level. If found, emits a warning and returns it.
+ * First searches configured toolchains (from {@code toolchains.xml}), then falls back
+ * to lazy filesystem discovery. Normal builds pay zero cost — discovery only runs
+ * when the running JDK is incompatible and no configured toolchain matches.
*/
Optional<Toolchain> autoSelectJdkToolchain(Session session) {
int requiredSourceLevel = getProjectRequiredSourceLevel(session);
@@ -138,22 +156,19 @@ Optional<Toolchain> autoSelectJdkToolchain(Session session) {
return Optional.empty();
}
- // Search available toolchains for a compatible JDK, preferring the newest
- List<Toolchain> allToolchains = getToolchains(session, "jdk", null);
- Toolchain bestMatch = null;
- int bestVersion = 0;
+ // 1. Search configured toolchains (from toolchains.xml)
+ List<Toolchain> configuredToolchains = getToolchains(session, "jdk", null);
+ Toolchain bestMatch = findNewestCompatible(configuredToolchains, requiredSourceLevel);
- for (Toolchain tc : allToolchains) {
- if (tc instanceof JavaToolchain jtc && jtc.getJavaVersion() != null) {
- int tcMajor = JdkSourceLevelSupport.normalizeSourceLevel(
- jtc.getJavaVersion().toString());
- if (tcMajor > 0 && JdkSourceLevelSupport.supportsSourceLevel(tcMajor, requiredSourceLevel)) {
- if (tcMajor > bestVersion) {
- bestVersion = tcMajor;
- bestMatch = tc;
- }
- }
- }
+ // 2. Fall back to lazy filesystem discovery
+ if (bestMatch == null && discoverer != null) {
+ logger.debug("No compatible JDK in configured toolchains, discovering JDKs from filesystem...");
+ List<ToolchainModel> discoveredModels = discoverer.discoverToolchains();
+ List<Toolchain> discoveredToolchains = discoveredModels.stream()
+ .map(this::createToolchain)
+ .flatMap(Optional::stream)
+ .toList();
+ bestMatch = findNewestCompatible(discoveredToolchains, requiredSourceLevel);
}
if (bestMatch != null) {
@@ -174,6 +189,27 @@ Optional<Toolchain> autoSelectJdkToolchain(Session session) {
return Optional.empty();
}
+ /**
+ * Finds the newest JDK toolchain that supports the given source level.
+ */
+ private Toolchain findNewestCompatible(List<Toolchain> toolchains, int requiredSourceLevel) {
+ Toolchain bestMatch = null;
+ int bestVersion = 0;
+ for (Toolchain tc : toolchains) {
+ if (tc instanceof JavaToolchain jtc && jtc.getJavaVersion() != null) {
+ int tcMajor = JdkSourceLevelSupport.normalizeSourceLevel(
+ jtc.getJavaVersion().toString());
+ if (tcMajor > 0 && JdkSourceLevelSupport.supportsSourceLevel(tcMajor, requiredSourceLevel)) {
+ if (tcMajor > bestVersion) {
+ bestVersion = tcMajor;
+ bestMatch = tc;
+ }
+ }
+ }
+ }
+ return bestMatch;
+ }
+
/**
* Reads the project's required source level from either Model 4.1.0
* {@code <source><targetVersion>} elements or legacy properties
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
index 30a1fecac4..3ae3878d0d 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java
@@ -85,10 +85,16 @@ static int normalizeSourceLevel(String version) {
return -1;
}
version = version.trim();
- // Handle "1.x" legacy format (e.g., "1.5", "1.8")
+ // Handle "1.x" legacy format (e.g., "1.5", "1.8", "1.8.0_392")
if (version.startsWith("1.") && version.length() > 2) {
+ String rest = version.substring(2);
+ // Strip any trailing qualifiers (e.g. "8.0_392" → "8")
+ int sep = indexOfNonDigit(rest);
+ if (sep > 0) {
+ rest = rest.substring(0, sep);
+ }
try {
- return Integer.parseInt(version.substring(2));
+ return Integer.parseInt(rest);
} catch (NumberFormatException e) {
return -1;
}
@@ -105,6 +111,18 @@ static int normalizeSourceLevel(String version) {
}
}
+ /**
+ * Returns the index of the first non-digit character in the string, or -1 if all characters are digits.
+ */
+ private static int indexOfNonDigit(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isDigit(s.charAt(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
/**
* Returns the major version of the currently running JDK.
*
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
new file mode 100644
index 0000000000..78524a7e6c
--- /dev/null
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java
@@ -0,0 +1,310 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
+import org.apache.maven.api.toolchain.ToolchainModel;
+import org.apache.maven.api.xml.XmlNode;
+import org.apache.maven.impl.util.Os;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovers JDK installations on the local filesystem by scanning well-known
+ * directories, environment variables, and tool manager locations.
+ * <p>
+ * This is used by {@link DefaultToolchainManager} as a lazy fallback when auto-selection
+ * needs a compatible JDK but none are configured in {@code toolchains.xml}.
+ * Discovery only runs when the running JDK cannot compile the project's source level
+ * and no configured toolchain matches — normal builds pay zero cost.
+ * <p>
+ * JDK version is read from the {@code release} file present in every JDK since Java 9
+ * (and backported to JDK 8u updates), avoiding the need to execute {@code java} processes.
+ */
+@Named
+@Singleton
+public class JdkToolchainDiscoverer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JdkToolchainDiscoverer.class);
+
+ private volatile List<ToolchainModel> cachedToolchains;
+
+ /**
+ * Returns discovered JDK toolchain models. Results are cached after first invocation.
+ */
+ public List<ToolchainModel> discoverToolchains() {
+ List<ToolchainModel> result = cachedToolchains;
+ if (result == null) {
+ synchronized (this) {
+ result = cachedToolchains;
+ if (result == null) {
+ result = doDiscover();
+ cachedToolchains = result;
+ }
+ }
+ }
+ return result;
+ }
+
+ private List<ToolchainModel> doDiscover() {
+ Set<Path> candidates = new LinkedHashSet<>();
+ collectFromEnvironment(candidates);
+ collectFromToolManagers(candidates);
+ collectFromSystemDirectories(candidates);
+
+ List<ToolchainModel> toolchains = new ArrayList<>();
+ for (Path candidate : candidates) {
+ try {
+ Path jdkHome = resolveJdkHome(candidate);
+ if (jdkHome != null && isValidJdkHome(jdkHome)) {
+ Optional<ToolchainModel> model = buildToolchainModel(jdkHome);
+ model.ifPresent(toolchains::add);
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Skipping JDK candidate {}: {}", candidate, e.getMessage());
+ }
+ }
+
+ LOGGER.debug("Discovered {} JDK installation(s) on the filesystem", toolchains.size());
+ return List.copyOf(toolchains);
+ }
+
+ /**
+ * Collects JDK candidates from environment variables matching {@code JAVA*_HOME}.
+ */
+ void collectFromEnvironment(Set<Path> candidates) {
+ // Current JDK
+ String javaHome = System.getProperty("java.home");
+ if (javaHome != null) {
+ addCandidate(candidates, Paths.get(javaHome));
+ }
+
+ // JAVA*_HOME env vars (e.g. JAVA11_HOME, JAVA17_HOME)
+ for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
+ String name = entry.getKey();
+ if (name.startsWith("JAVA") && name.endsWith("_HOME")) {
+ addCandidate(candidates, Paths.get(entry.getValue()));
+ }
+ }
+
+ // JAVA_HOME
+ String envJavaHome = System.getenv("JAVA_HOME");
+ if (envJavaHome != null) {
+ addCandidate(candidates, Paths.get(envJavaHome));
+ }
+ }
+
+ /**
+ * Collects JDK candidates from common tool manager directories under the user's home.
+ */
+ void collectFromToolManagers(Set<Path> candidates) {
+ Path userHome = Paths.get(System.getProperty("user.home"));
+
+ // IntelliJ IDEA / common
+ scanSubdirectories(candidates, userHome.resolve(".jdks"));
+ // Maven-managed JDKs
+ scanSubdirectories(candidates, userHome.resolve(".m2").resolve("jdks"));
+ // SDKMAN
+ scanSubdirectories(
+ candidates, userHome.resolve(".sdkman").resolve("candidates").resolve("java"));
+ // Gradle
+ scanSubdirectories(candidates, userHome.resolve(".gradle").resolve("jdks"));
+ // jEnv
+ scanSubdirectories(candidates, userHome.resolve(".jenv").resolve("versions"));
+ // JBang
+ scanSubdirectories(
+ candidates, userHome.resolve(".jbang").resolve("cache").resolve("jdks"));
+ // asdf
+ scanSubdirectories(
+ candidates, userHome.resolve(".asdf").resolve("installs").resolve("java"));
+ // Jabba
+ scanSubdirectories(candidates, userHome.resolve(".jabba").resolve("jdk"));
+ // mise (formerly rtx)
+ scanSubdirectories(
+ candidates,
+ userHome.resolve(".local")
+ .resolve("share")
+ .resolve("mise")
+ .resolve("installs")
+ .resolve("java"));
+ }
+
+ /**
+ * Collects JDK candidates from OS-specific system directories.
+ */
+ void collectFromSystemDirectories(Set<Path> candidates) {
+ if (Os.IS_WINDOWS) {
+ collectWindowsDirectories(candidates);
+ } else if (Os.isFamily("mac")) {
+ collectMacDirectories(candidates);
+ } else {
+ collectLinuxDirectories(candidates);
+ }
+ }
+
+ private void collectLinuxDirectories(Set<Path> candidates) {
+ scanSubdirectories(candidates, Paths.get("/usr/lib/jvm"));
+ scanSubdirectories(candidates, Paths.get("/usr/lib64/jvm"));
+ scanSubdirectories(candidates, Paths.get("/usr/jdk"));
+ scanSubdirectories(candidates, Paths.get("/usr/java"));
+ scanSubdirectories(candidates, Paths.get("/usr/local/java"));
+ scanSubdirectories(candidates, Paths.get("/opt/java"));
+ scanSubdirectories(candidates, Paths.get("/opt/hostedtoolcache"));
+ }
+
+ private void collectMacDirectories(Set<Path> candidates) {
+ Path userHome = Paths.get(System.getProperty("user.home"));
+ scanSubdirectories(candidates, Paths.get("/Library/Java/JavaVirtualMachines"));
+ scanSubdirectories(
+ candidates, userHome.resolve("Library").resolve("Java").resolve("JavaVirtualMachines"));
+ }
+
+ private void collectWindowsDirectories(Set<Path> candidates) {
+ Path progFiles = Paths.get("C:\\Program Files");
+ scanSubdirectories(candidates, progFiles.resolve("Java"));
+ scanSubdirectories(candidates, progFiles.resolve("Eclipse Adoptium"));
+ scanSubdirectories(candidates, progFiles.resolve("Zulu"));
+ scanSubdirectories(candidates, progFiles.resolve("Amazon Corretto"));
+ scanSubdirectories(candidates, progFiles.resolve("BellSoft"));
+ // Scoop
+ Path userHome = Paths.get(System.getProperty("user.home"));
+ scanSubdirectories(candidates, userHome.resolve("scoop").resolve("apps"));
+ }
+
+ /**
+ * Lists immediate subdirectories of the given directory and adds them as candidates.
+ */
+ private void scanSubdirectories(Set<Path> candidates, Path directory) {
+ if (!Files.isDirectory(directory)) {
+ return;
+ }
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, Files::isDirectory)) {
+ for (Path child : stream) {
+ addCandidate(candidates, child);
+ }
+ } catch (IOException e) {
+ LOGGER.debug("Cannot scan directory {}: {}", directory, e.getMessage());
+ }
+ }
+
+ private void addCandidate(Set<Path> candidates, Path path) {
+ try {
+ candidates.add(path.toRealPath());
+ } catch (IOException e) {
+ // Broken symlink or inaccessible — add normalized path as fallback
+ candidates.add(path.normalize().toAbsolutePath());
+ }
+ }
+
+ /**
+ * Resolves the actual JDK home from a candidate path.
+ * On macOS, JDKs may be nested under {@code Contents/Home}.
+ */
+ Path resolveJdkHome(Path candidate) {
+ if (isValidJdkHome(candidate)) {
+ return candidate;
+ }
+ // macOS bundle layout: /path/to/jdk-17.jdk/Contents/Home
+ Path contentsHome = candidate.resolve("Contents").resolve("Home");
+ if (isValidJdkHome(contentsHome)) {
+ return contentsHome;
+ }
+ return null;
+ }
+
+ /**
+ * Checks if a directory is a valid JDK home by looking for {@code bin/javac}.
+ */
+ boolean isValidJdkHome(Path jdkHome) {
+ Path bin = jdkHome.resolve("bin");
+ return Files.exists(bin.resolve("javac")) || Files.exists(bin.resolve("javac.exe"));
+ }
+
+ /**
+ * Builds a {@link ToolchainModel} from a validated JDK home by reading the {@code release} file.
+ *
+ * @return the model, or empty if the version cannot be determined
+ */
+ Optional<ToolchainModel> buildToolchainModel(Path jdkHome) {
+ String version = readVersionFromRelease(jdkHome);
+ if (version == null) {
+ LOGGER.debug("Cannot determine version for JDK at {}, skipping", jdkHome);
+ return Optional.empty();
+ }
+
+ int majorVersion = JdkSourceLevelSupport.normalizeSourceLevel(version);
+ if (majorVersion <= 0) {
+ LOGGER.debug("Cannot parse major version from '{}' for JDK at {}, skipping", version, jdkHome);
+ return Optional.empty();
+ }
+
+ XmlNode jdkHomeNode = XmlNode.newInstance("jdkHome", jdkHome.toString());
+ XmlNode configuration = XmlNode.newInstance("configuration", List.of(jdkHomeNode));
+
+ ToolchainModel model = ToolchainModel.newBuilder()
+ .type("jdk")
+ .provides(Map.of("version", String.valueOf(majorVersion)))
+ .configuration(configuration)
+ .build();
+
+ LOGGER.debug("Discovered JDK {} at {}", majorVersion, jdkHome);
+ return Optional.of(model);
+ }
+
+ /**
+ * Reads the {@code JAVA_VERSION} property from the JDK's {@code release} file.
+ * The release file format uses shell-style assignments: {@code JAVA_VERSION="17.0.2"}.
+ *
+ * @return the version string (e.g. "17.0.2"), or null if not found
+ */
+ String readVersionFromRelease(Path jdkHome) {
+ Path releaseFile = jdkHome.resolve("release");
+ if (!Files.exists(releaseFile)) {
+ return null;
+ }
+ try {
+ for (String line : Files.readAllLines(releaseFile)) {
+ if (line.startsWith("JAVA_VERSION=")) {
+ String value = line.substring("JAVA_VERSION=".length()).trim();
+ // Remove surrounding quotes
+ if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
+ value = value.substring(1, value.length() - 1);
+ }
+ return value.isEmpty() ? null : value;
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.debug("Cannot read release file at {}: {}", releaseFile, e.getMessage());
+ }
+ return null;
+ }
+}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
new file mode 100644
index 0000000000..041a9cce1d
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import org.apache.maven.api.toolchain.ToolchainModel;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JdkToolchainDiscovererTest {
+
+ @TempDir
+ Path tempDir;
+
+ private final JdkToolchainDiscoverer discoverer = new JdkToolchainDiscoverer();
+
+ @Test
+ void isValidJdkHomeWithJavac() throws IOException {
+ Path jdkHome = createFakeJdk(tempDir, "jdk-17");
+ assertTrue(discoverer.isValidJdkHome(jdkHome));
+ }
+
+ @Test
+ void isValidJdkHomeWithoutJavac() throws IOException {
+ Path jdkHome = tempDir.resolve("jdk-no-javac");
+ Files.createDirectories(jdkHome.resolve("bin"));
+ assertFalse(discoverer.isValidJdkHome(jdkHome));
+ }
+
+ @Test
+ void isValidJdkHomeNonExistent() {
+ assertFalse(discoverer.isValidJdkHome(tempDir.resolve("nonexistent")));
+ }
+
+ @Test
+ void readVersionFromReleaseFile() throws IOException {
+ Path jdkHome = tempDir.resolve("jdk-17");
+ Files.createDirectories(jdkHome);
+ Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=\"17.0.2\"\nIMPLEMENTOR=\"Eclipse Adoptium\"\n");
+
+ assertEquals("17.0.2", discoverer.readVersionFromRelease(jdkHome));
+ }
+
+ @Test
+ void readVersionFromReleaseFileJdk8Format() throws IOException {
+ Path jdkHome = tempDir.resolve("jdk-8");
+ Files.createDirectories(jdkHome);
+ Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=\"1.8.0_392\"\n");
+
+ assertEquals("1.8.0_392", discoverer.readVersionFromRelease(jdkHome));
+ }
+
+ @Test
+ void readVersionFromReleaseFileNoQuotes() throws IOException {
+ Path jdkHome = tempDir.resolve("jdk-11");
+ Files.createDirectories(jdkHome);
+ Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=11.0.21\n");
+
+ assertEquals("11.0.21", discoverer.readVersionFromRelease(jdkHome));
+ }
+
+ @Test
+ void readVersionFromMissingReleaseFile() {
+ assertNull(discoverer.readVersionFromRelease(tempDir.resolve("no-release")));
+ }
+
+ @Test
+ void readVersionFromReleaseFileWithoutJavaVersion() throws IOException {
+ Path jdkHome = tempDir.resolve("jdk-bad");
+ Files.createDirectories(jdkHome);
+ Files.writeString(jdkHome.resolve("release"), "IMPLEMENTOR=\"Some Vendor\"\n");
+
+ assertNull(discoverer.readVersionFromRelease(jdkHome));
+ }
+
+ @Test
+ void buildToolchainModelFromValidJdk() throws IOException {
+ Path jdkHome = createFakeJdkWithRelease(tempDir, "jdk-17", "17.0.2");
+
+ Optional<ToolchainModel> result = discoverer.buildToolchainModel(jdkHome);
+
+ assertTrue(result.isPresent());
+ ToolchainModel model = result.get();
+ assertEquals("jdk", model.getType());
+ assertEquals("17", model.getProvides().get("version"));
+ assertNotNull(model.getConfiguration());
+ assertEquals(
+ jdkHome.toString(), model.getConfiguration().child("jdkHome").value());
+ }
+
+ @Test
+ void buildToolchainModelFromJdk8() throws IOException {
+ Path jdkHome = createFakeJdkWithRelease(tempDir, "jdk-8", "1.8.0_392");
+
+ Optional<ToolchainModel> result = discoverer.buildToolchainModel(jdkHome);
+
+ assertTrue(result.isPresent());
+ assertEquals("8", result.get().getProvides().get("version"));
+ }
+
+ @Test
+ void buildToolchainModelWithoutReleaseFile() throws IOException {
+ Path jdkHome = createFakeJdk(tempDir, "jdk-old");
+
+ Optional<ToolchainModel> result = discoverer.buildToolchainModel(jdkHome);
+
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ void resolveJdkHomeDirectPath() throws IOException {
+ Path jdkHome = createFakeJdk(tempDir, "jdk-17");
+
+ assertEquals(jdkHome, discoverer.resolveJdkHome(jdkHome));
+ }
+
+ @Test
+ void resolveJdkHomeMacOsBundle() throws IOException {
+ Path bundleRoot = tempDir.resolve("jdk-17.jdk");
+ Path contentsHome = bundleRoot.resolve("Contents").resolve("Home");
+ Files.createDirectories(contentsHome.resolve("bin"));
+ Files.createFile(contentsHome.resolve("bin").resolve("javac"));
+
+ assertEquals(contentsHome, discoverer.resolveJdkHome(bundleRoot));
+ }
+
+ @Test
+ void resolveJdkHomeInvalidPath() {
+ assertNull(discoverer.resolveJdkHome(tempDir.resolve("nonexistent")));
+ }
+
+ @Test
+ void discoverToolchainsIsCached() {
+ // Two calls should return the same list instance (cached)
+ var first = discoverer.discoverToolchains();
+ var second = discoverer.discoverToolchains();
+ assertNotNull(first);
+ assertTrue(first == second, "Expected cached result (same instance)");
+ }
+
+ private Path createFakeJdk(Path parent, String name) throws IOException {
+ Path jdkHome = parent.resolve(name);
+ Path binDir = jdkHome.resolve("bin");
+ Files.createDirectories(binDir);
+ Files.createFile(binDir.resolve("javac"));
+ return jdkHome;
+ }
+
+ private Path createFakeJdkWithRelease(Path parent, String name, String version) throws IOException {
+ Path jdkHome = createFakeJdk(parent, name);
+ Files.writeString(jdkHome.resolve("release"), "JAVA_VERSION=\"" + version + "\"\n");
+ return jdkHome;
+ }
+}
(maven) 08/14: Fix DefaultToolchainManager constructor visibility for compat layer and IT harness
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-module-name-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 7c25b1d3e35666e6fbda8a19ea236c495f3c29b8
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 31 12:04:21 2026 +0200
Fix DefaultToolchainManager constructor visibility for compat layer and IT harness
The gh-11055 DI service injection IT calls new DefaultToolchainManager(Map.of())
from outside the package, and the compat layer ToolchainManagerFactory calls
the 3-arg constructor (factories, discoverer, logger). All constructors must be
accessible from outside the package.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../java/org/apache/maven/impl/DefaultToolchainManager.java | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
index df1a9be98e..0e9e24b4b4 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainManager.java
@@ -71,16 +71,18 @@ public DefaultToolchainManager(Map<String, ToolchainFactory> factories) {
/**
* Convenience constructor without a discoverer, with custom logger.
- * Used by tests.
+ * Used by the compatibility layer and tests.
*/
- DefaultToolchainManager(Map<String, ToolchainFactory> factories, Logger logger) {
+ public DefaultToolchainManager(Map<String, ToolchainFactory> factories, Logger logger) {
this(factories, null, logger);
}
/**
- * Full-control constructor. Used by tests.
+ * Full-control constructor with all parameters.
+ * Used by the compatibility layer ({@code ToolchainManagerFactory}) and tests.
*/
- DefaultToolchainManager(Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer, Logger logger) {
+ public DefaultToolchainManager(
+ Map<String, ToolchainFactory> factories, JdkToolchainDiscoverer discoverer, Logger logger) {
this.factories = factories;
this.discoverer = discoverer;
this.logger = logger != null ? logger : LoggerFactory.getLogger(DefaultToolchainManager.class);