From 42cbce4ca07d61182a67615c26d62d999c97501d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 30 Jul 2026 16:31:51 +0200 Subject: [PATCH 01/12] Auto-select JDK toolchain when running JDK cannot compile the project's source level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 elements and legacy maven.compiler.release/source properties - Prefers the newest compatible JDK from configured toolchains Co-Authored-By: Claude Opus 4.6 --- .../maven/impl/DefaultToolchainManager.java | 138 ++++++- .../maven/impl/JdkSourceLevelSupport.java | 116 ++++++ .../impl/DefaultToolchainManagerTest.java | 369 ++++++++++++++++++ .../maven/impl/JdkSourceLevelSupportTest.java | 141 +++++++ 4 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/JdkSourceLevelSupport.java create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/JdkSourceLevelSupportTest.java 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 cc2cb0360bc3..ec87370e3286 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 getToolchainFromBuildContext(@Nonnull Session session throws ToolchainManagerException { Map 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 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. + *

+ * Searches configured toolchains for the newest JDK that supports the required + * source level. If found, emits a warning and returns it. + */ + Optional 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 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 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 } 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 current = session.getService(Lookup.class).lookupOptional(Project.class); + if (current.isEmpty()) { + return -1; + } + + Project project = current.get(); + + // Check Model 4.1.0 elements + Build build = project.getModel().getBuild(); + if (build != null) { + List 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 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 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 000000000000..30a1fecac4c1 --- /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. + *

+ * Maps JDK major versions to their supported {@code --source}/{@code --release} levels, + * based on the javac retirement schedule defined in + * JEP 182 and subsequent JDK releases. + *

+ * The retirement schedule follows these milestones: + *

    + *
  • JDK 9: removed {@code --source 1} through {@code 5}, minimum is {@code 6}
  • + *
  • JDK 12: removed {@code --source 6}, minimum is {@code 7}
  • + *
  • JDK 21: removed {@code --source 7}, minimum is {@code 8}
  • + *
+ */ +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. + *

+ * Handles legacy formats: + *

    + *
  • {@code "1.5"} → {@code 5}
  • + *
  • {@code "1.8"} → {@code 8}
  • + *
  • {@code "11"} → {@code 11}
  • + *
  • {@code "21.0.1"} → {@code 21}
  • + *
+ * + * @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 cec13a201948..11cdf9be4d9b 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) @@ -123,4 +130,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 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 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 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 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 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 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 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 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 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 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 result = manager.getToolchainFromBuildContext(session, "jdk"); + + assertTrue(result.isPresent()); + assertEquals(mockToolchain, result.get()); + } + + @Test + void getToolchainFromBuildContextNonJdkTypeDoesNotAutoSelect() { + // Auto-selection should only apply to "jdk" type + Map 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 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 000000000000..e3b215f8ade4 --- /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); + } +} From fb8ebb64fd0f8aadb30dafc9f975303311acac01 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 30 Jul 2026 17:43:18 +0200 Subject: [PATCH 02/12] 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 --- .../maven/impl/DefaultToolchainManager.java | 5 + .../it/MavenITAutoJdkToolchainSelectTest.java | 141 ++++++++++++++++++ .../auto-jdk-toolchain-no-select/pom.xml | 61 ++++++++ .../toolchains.xml | 13 ++ .../auto-jdk-toolchain-select/pom.xml | 61 ++++++++ .../auto-jdk-toolchain-select/toolchains.xml | 13 ++ 6 files changed, 294 insertions(+) create mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java create mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml create mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml create mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml create mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml 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 ec87370e3286..88430004eb8e 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 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 000000000000..10d0776ce30f --- /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. + *

+ * 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. + *

+ * 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 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. + *

+ * 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 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 000000000000..872a38e92ff9 --- /dev/null +++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + org.apache.maven.its.auto-toolchain + test-no-select + 1.0-SNAPSHOT + + Maven Integration Test :: Auto JDK Toolchain No Selection + + Test that Maven does NOT auto-select a JDK toolchain when the running JDK + already supports the project's required source level. + + + + + 11 + + + + + + org.apache.maven.its.plugins + maven-it-plugin-toolchain + 2.1-SNAPSHOT + + + find-tool + + find-tool + + initialize + + target/tool.properties + jdk + javac + + + + + + + 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 000000000000..268626012778 --- /dev/null +++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml @@ -0,0 +1,13 @@ + + + + + jdk + + 8 + + + @javaHome@ + + + 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 000000000000..c945c9f77698 --- /dev/null +++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + org.apache.maven.its.auto-toolchain + test-auto-select + 1.0-SNAPSHOT + + Maven Integration Test :: Auto JDK Toolchain Selection + + Test that Maven auto-selects a JDK toolchain when the running JDK + does not support the project's required source level. + + + + + 6 + + + + + + org.apache.maven.its.plugins + maven-it-plugin-toolchain + 2.1-SNAPSHOT + + + find-tool + + find-tool + + initialize + + target/tool.properties + jdk + javac + + + + + + + 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 000000000000..4bc2f45593c8 --- /dev/null +++ b/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml @@ -0,0 +1,13 @@ + + + + + jdk + + 11 + + + @javaHome@ + + + From 8ec56c4ab4e4bfc5db6182258a0c6ffe4295a252 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 31 Jul 2026 07:44:29 +0200 Subject: [PATCH 03/12] Remove public modifiers from IT class and methods Co-Authored-By: Claude Opus 4.6 --- .../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 10d0776ce30f..894d832ff4d0 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) From 4d88482097f5b37136c77a906f282876370f1534 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 31 Jul 2026 10:46:19 +0200 Subject: [PATCH 04/12] 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 --- .../maven/impl/DefaultToolchainManager.java | 76 +++-- .../maven/impl/JdkSourceLevelSupport.java | 22 +- .../maven/impl/JdkToolchainDiscoverer.java | 310 ++++++++++++++++++ .../impl/JdkToolchainDiscovererTest.java | 180 ++++++++++ 4 files changed, 566 insertions(+), 22 deletions(-) create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java 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 88430004eb8e..45df52a465e0 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 factories; + private final JdkToolchainDiscoverer discoverer; private final Logger logger; @Inject - public DefaultToolchainManager(Map factories) { - this(factories, null); + public DefaultToolchainManager(Map factories, JdkToolchainDiscoverer discoverer) { + this(factories, discoverer, null); } /** - * Used for tests only + * Used for tests only (no discoverer) + */ + protected DefaultToolchainManager(Map factories) { + this(factories, null, null); + } + + /** + * Used for tests only (no discoverer, custom logger) */ protected DefaultToolchainManager(Map factories, Logger logger) { + this(factories, null, logger); + } + + /** + * Used for tests only (full control) + */ + protected DefaultToolchainManager( + Map 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. *

- * 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 autoSelectJdkToolchain(Session session) { int requiredSourceLevel = getProjectRequiredSourceLevel(session); @@ -138,22 +156,19 @@ Optional autoSelectJdkToolchain(Session session) { return Optional.empty(); } - // Search available toolchains for a compatible JDK, preferring the newest - List allToolchains = getToolchains(session, "jdk", null); - Toolchain bestMatch = null; - int bestVersion = 0; + // 1. Search configured toolchains (from toolchains.xml) + List 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 discoveredModels = discoverer.discoverToolchains(); + List discoveredToolchains = discoveredModels.stream() + .map(this::createToolchain) + .flatMap(Optional::stream) + .toList(); + bestMatch = findNewestCompatible(discoveredToolchains, requiredSourceLevel); } if (bestMatch != null) { @@ -174,6 +189,27 @@ Optional autoSelectJdkToolchain(Session session) { return Optional.empty(); } + /** + * Finds the newest JDK toolchain that supports the given source level. + */ + private Toolchain findNewestCompatible(List 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 } 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 30a1fecac4c1..3ae3878d0dad 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 000000000000..78524a7e6c44 --- /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. + *

+ * 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. + *

+ * 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 cachedToolchains; + + /** + * Returns discovered JDK toolchain models. Results are cached after first invocation. + */ + public List discoverToolchains() { + List result = cachedToolchains; + if (result == null) { + synchronized (this) { + result = cachedToolchains; + if (result == null) { + result = doDiscover(); + cachedToolchains = result; + } + } + } + return result; + } + + private List doDiscover() { + Set candidates = new LinkedHashSet<>(); + collectFromEnvironment(candidates); + collectFromToolManagers(candidates); + collectFromSystemDirectories(candidates); + + List toolchains = new ArrayList<>(); + for (Path candidate : candidates) { + try { + Path jdkHome = resolveJdkHome(candidate); + if (jdkHome != null && isValidJdkHome(jdkHome)) { + Optional 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 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 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 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 candidates) { + if (Os.IS_WINDOWS) { + collectWindowsDirectories(candidates); + } else if (Os.isFamily("mac")) { + collectMacDirectories(candidates); + } else { + collectLinuxDirectories(candidates); + } + } + + private void collectLinuxDirectories(Set 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 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 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 candidates, Path directory) { + if (!Files.isDirectory(directory)) { + return; + } + try (DirectoryStream 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 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 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 000000000000..041a9cce1d15 --- /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 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 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 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; + } +} From 2b628ca4e0ba4706cf53f179591900855fc94844 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 31 Jul 2026 11:01:47 +0200 Subject: [PATCH 05/12] 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 --- .../maven/impl/DefaultToolchainManager.java | 2 +- .../maven/impl/JdkToolchainDiscoverer.java | 77 +++++++++++-------- .../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 45df52a465e0..91692529834f 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 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 discoveredModels = discoverer.discoverToolchains(); + List discoveredModels = discoverer.discoverToolchains(session.getSystemProperties()); List 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 78524a7e6c44..1d7dfe767b0a 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 @@ *

* 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. + *

+ * 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 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 discoverToolchains() { + public List discoverToolchains(Map properties) { List 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 discoverToolchains() { return result; } - private List doDiscover() { + private List doDiscover(Map properties) { Set candidates = new LinkedHashSet<>(); - collectFromEnvironment(candidates); - collectFromToolManagers(candidates); - collectFromSystemDirectories(candidates); + collectFromEnvironment(candidates, properties); + collectFromToolManagers(candidates, properties); + collectFromSystemDirectories(candidates, properties); List toolchains = new ArrayList<>(); for (Path candidate : candidates) { @@ -100,34 +110,33 @@ private List 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 candidates) { - // Current JDK - String javaHome = System.getProperty("java.home"); + void collectFromEnvironment(Set candidates, Map 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 entry : System.getenv().entrySet()) { + // env.JAVA*_HOME env vars (e.g. env.JAVA11_HOME, env.JAVA17_HOME, env.JAVA_HOME) + for (Map.Entry 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 candidates) { - Path userHome = Paths.get(System.getProperty("user.home")); + void collectFromToolManagers(Set candidates, Map 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 candidates) { /** * Collects JDK candidates from OS-specific system directories. */ - void collectFromSystemDirectories(Set candidates) { + void collectFromSystemDirectories(Set candidates, Map 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 candidates) { scanSubdirectories(candidates, Paths.get("/opt/hostedtoolcache")); } - private void collectMacDirectories(Set candidates) { - Path userHome = Paths.get(System.getProperty("user.home")); + private void collectMacDirectories(Set candidates, Map 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 candidates) { + private void collectWindowsDirectories(Set candidates, Map 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 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 041a9cce1d15..76d0d6e576d3 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 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 properties = Map.of( + "java.home", jdkHome.toString(), + "env.JAVA17_HOME", tempDir.resolve("jdk-17-env").toString()); + + Set 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 properties = Map.of("user.home", fakeHome.toString()); + + Set 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 properties = Map.of(); + + Set 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"); From 3cd5544a6ac421f63dc05e9b7a7d910735e93871 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 31 Jul 2026 11:56:06 +0200 Subject: [PATCH 06/12] 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 and directly in the maven-compiler-plugin block rather than as properties. Added getSourceLevelFromCompilerPlugin() to check both and 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 --- .../toolchain/ToolchainManagerFactory.java | 5 +- .../maven/impl/DefaultToolchainManager.java | 53 +++++++++++++++++++ .../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 5123ff11d081..153cfd0384e0 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 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 91692529834f..890d0f5d1560 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 (, ) + int pluginLevel = getSourceLevelFromCompilerPlugin(build); + if (pluginLevel > 0) { + return pluginLevel; + } + + return -1; + } + + /** + * Reads the source level from the maven-compiler-plugin configuration. + * Checks both {@code } and {@code } elements in the plugin's + * {@code } 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) { + // takes precedence over + 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 1d7dfe767b0a..964441a1edad 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 doDiscover(Map 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 candidates, Map properties) { // Current JDK (from java.home system property) @@ -123,7 +126,15 @@ void collectFromEnvironment(Set candidates, Map properties for (Map.Entry 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); + } } } } From 617ee27a4ad35a555570838d105cd066dfb94afa Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 31 Jul 2026 11:59:39 +0200 Subject: [PATCH 07/12] 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 --- .../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 890d0f5d1560..df1a9be98e11 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 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 factories) { + public DefaultToolchainManager(Map 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 factories, Logger logger) { + DefaultToolchainManager(Map factories, Logger logger) { this(factories, null, logger); } /** - * Used for tests only (full control) + * Full-control constructor. Used by tests. */ - protected DefaultToolchainManager( - Map factories, JdkToolchainDiscoverer discoverer, Logger logger) { + DefaultToolchainManager(Map factories, JdkToolchainDiscoverer discoverer, Logger logger) { this.factories = factories; this.discoverer = discoverer; this.logger = logger != null ? logger : LoggerFactory.getLogger(DefaultToolchainManager.class); From 48d0b6e48fd881eb4320a559ede60bd06ffba146 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 31 Jul 2026 12:04:21 +0200 Subject: [PATCH 08/12] 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 --- .../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 df1a9be98e11..0e9e24b4b4a5 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 factories) { /** * Convenience constructor without a discoverer, with custom logger. - * Used by tests. + * Used by the compatibility layer and tests. */ - DefaultToolchainManager(Map factories, Logger logger) { + public DefaultToolchainManager(Map 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 factories, JdkToolchainDiscoverer discoverer, Logger logger) { + public DefaultToolchainManager( + Map factories, JdkToolchainDiscoverer discoverer, Logger logger) { this.factories = factories; this.discoverer = discoverer; this.logger = logger != null ? logger : LoggerFactory.getLogger(DefaultToolchainManager.class); From 4d0b8b2be6ee133b8d8ae01daaefd48ef17bae91 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 5 Aug 2026 14:16:56 +0200 Subject: [PATCH 09/12] 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 --- .../toolchain/ToolchainManagerFactory.java | 5 +- .../maven/impl/DefaultToolchainManager.java | 125 ++----- .../maven/impl/JdkSourceLevelSupport.java | 28 ++ .../maven/impl/JdkToolchainDiscoverer.java | 336 ------------------ .../impl/DefaultToolchainManagerTest.java | 237 ++++-------- .../maven/impl/JdkSourceLevelSupportTest.java | 37 ++ .../impl/JdkToolchainDiscovererTest.java | 226 ------------ .../it/MavenITAutoJdkToolchainSelectTest.java | 141 -------- .../auto-jdk-toolchain-no-select/pom.xml | 61 ---- .../toolchains.xml | 13 - .../auto-jdk-toolchain-select/pom.xml | 61 ---- .../auto-jdk-toolchain-select/toolchains.xml | 13 - 12 files changed, 163 insertions(+), 1120 deletions(-) delete mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/JdkToolchainDiscoverer.java delete mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/JdkToolchainDiscovererTest.java delete mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITAutoJdkToolchainSelectTest.java delete mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml delete mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml delete mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml delete mode 100644 its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml 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 153cfd0384e0..5123ff11d081 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 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 0e9e24b4b4a5..73a0e2db3599 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 factories; - private final JdkToolchainDiscoverer discoverer; private final Logger logger; @Inject - public DefaultToolchainManager(Map 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 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 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 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 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 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. *

- * 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 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 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 discoveredModels = discoverer.discoverToolchains(session.getSystemProperties()); - List 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 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 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 } 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 } elements, legacy properties + * ({@code maven.compiler.release}, {@code maven.compiler.source}), + * or compiler plugin configuration ({@code }, {@code }). * * @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 3ae3878d0dad..56e64895d7a7 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. + *

+ * Based on the retirement schedule: + *

    + *
  • source 1–5 → last supported by JDK 8
  • + *
  • source 6 → last supported by JDK 11
  • + *
  • source 7 → last supported by JDK 20
  • + *
  • source 8+ → still supported by current JDKs
  • + *
+ * + * @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 964441a1edad..000000000000 --- 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. - *

- * 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. - *

- * 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. - *

- * 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 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 discoverToolchains(Map properties) { - List result = cachedToolchains; - if (result == null) { - synchronized (this) { - result = cachedToolchains; - if (result == null) { - result = doDiscover(properties); - cachedToolchains = result; - } - } - } - return result; - } - - private List doDiscover(Map properties) { - Set candidates = new LinkedHashSet<>(); - collectFromEnvironment(candidates, properties); - collectFromToolManagers(candidates, properties); - collectFromSystemDirectories(candidates, properties); - - List toolchains = new ArrayList<>(); - for (Path candidate : candidates) { - try { - Path jdkHome = resolveJdkHome(candidate); - if (jdkHome != null && isValidJdkHome(jdkHome)) { - Optional 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 candidates, Map 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 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 candidates, Map 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 candidates, Map properties) { - if (Os.IS_WINDOWS) { - collectWindowsDirectories(candidates, properties); - } else if (Os.isFamily("mac")) { - collectMacDirectories(candidates, properties); - } else { - collectLinuxDirectories(candidates); - } - } - - private void collectLinuxDirectories(Set 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 candidates, Map 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 candidates, Map 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 candidates, Path directory) { - if (!Files.isDirectory(directory)) { - return; - } - try (DirectoryStream 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 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 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 11cdf9be4d9b..b471453c1b48 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; @@ -131,54 +131,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 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 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 @@ -187,44 +165,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 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 @@ -242,47 +199,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 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; } }; @@ -295,24 +229,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 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 @@ -328,26 +257,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 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 @@ -363,26 +285,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 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 context = new ConcurrentHashMap<>(); SessionData data = mock(SessionData.class); @@ -406,28 +322,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 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 context = new ConcurrentHashMap<>(); SessionData data = mock(SessionData.class); toolchainModel = ToolchainModel.newBuilder().type("jdk").build(); @@ -440,10 +350,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 result = manager.getToolchainFromBuildContext(session, "jdk"); assertTrue(result.isPresent()); @@ -451,8 +358,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 context = new ConcurrentHashMap<>(); SessionData data = mock(SessionData.class); @@ -461,8 +368,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 result = manager.getToolchainFromBuildContext(session, "otherType"); assertTrue(result.isEmpty()); } @@ -470,8 +375,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() @@ -482,7 +385,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 e3b215f8ade4..a93a37793b66 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 76d0d6e576d3..000000000000 --- 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 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 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 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 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 properties = Map.of( - "java.home", jdkHome.toString(), - "env.JAVA17_HOME", tempDir.resolve("jdk-17-env").toString()); - - Set 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 properties = Map.of("user.home", fakeHome.toString()); - - Set 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 properties = Map.of(); - - Set 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 894d832ff4d0..000000000000 --- 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. - *

- * 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. - *

- * 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 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. - *

- * 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 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 872a38e92ff9..000000000000 --- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - 4.0.0 - - org.apache.maven.its.auto-toolchain - test-no-select - 1.0-SNAPSHOT - - Maven Integration Test :: Auto JDK Toolchain No Selection - - Test that Maven does NOT auto-select a JDK toolchain when the running JDK - already supports the project's required source level. - - - - - 11 - - - - - - org.apache.maven.its.plugins - maven-it-plugin-toolchain - 2.1-SNAPSHOT - - - find-tool - - find-tool - - initialize - - target/tool.properties - jdk - javac - - - - - - - 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 268626012778..000000000000 --- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-no-select/toolchains.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - jdk - - 8 - - - @javaHome@ - - - 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 c945c9f77698..000000000000 --- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - 4.0.0 - - org.apache.maven.its.auto-toolchain - test-auto-select - 1.0-SNAPSHOT - - Maven Integration Test :: Auto JDK Toolchain Selection - - Test that Maven auto-selects a JDK toolchain when the running JDK - does not support the project's required source level. - - - - - 6 - - - - - - org.apache.maven.its.plugins - maven-it-plugin-toolchain - 2.1-SNAPSHOT - - - find-tool - - find-tool - - initialize - - target/tool.properties - jdk - javac - - - - - - - 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 4bc2f45593c8..000000000000 --- a/its/core-it-suite/src/test/resources/auto-jdk-toolchain-select/toolchains.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - jdk - - 11 - - - @javaHome@ - - - From 269a21fe20462503168bda66981982f5a0da3388 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 5 Aug 2026 15:33:26 +0200 Subject: [PATCH 10/12] Add mvnup ToolchainPluginStrategy: auto-add maven-toolchains-plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../mvnup/goals/ToolchainPluginStrategy.java | 341 ++++++++++++ .../goals/ToolchainPluginStrategyTest.java | 527 ++++++++++++++++++ .../maven/impl/JdkSourceLevelSupport.java | 12 +- 3 files changed, 874 insertions(+), 6 deletions(-) create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java create mode 100644 impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java 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 000000000000..fb0dd8f0671c --- /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. + * + *

This strategy detects the project's source level from: + *

    + *
  1. {@code maven.compiler.release} property
  2. + *
  3. {@code maven.compiler.source} property
  4. + *
  5. Compiler plugin {@code } or {@code }
  6. + *
+ * + *

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 JEP 182: Policy for Retiring javac -source and -target Options + */ +@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 pomMap) { + Set processedPoms = new HashSet<>(); + Set modifiedPoms = new HashSet<>(); + Set errorPoms = new HashSet<>(); + + int runningJdkMajor = getRunningJdkMajor(); + + for (Map.Entry 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 + int level = detectFromPluginSection(build.childElement(PLUGINS).orElse(null)); + if (level > 0) { + return level; + } + + // Check + 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) { + // 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 and + 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 } 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 000000000000..d92824457508 --- /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 = """ + + + 4.0.0 + com.example + test + 1.0 + + 6 + + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + 1.6 + + + """; + Document doc = Document.of(pomXml); + assertEquals(6, strategy.detectSourceLevel(doc)); + } + + @Test + @DisplayName("release property takes precedence over source property") + void releaseTakesPrecedenceOverSource() { + String pomXml = """ + + + 4.0.0 + com.example + test + 1.0 + + 7 + 6 + + + """; + Document doc = Document.of(pomXml); + assertEquals(7, strategy.detectSourceLevel(doc)); + } + + @Test + @DisplayName("should detect from compiler plugin ") + void detectFromCompilerPluginRelease() { + String pomXml = """ + + + 4.0.0 + com.example + test + 1.0 + + + + maven-compiler-plugin + + 6 + + + + + + """; + Document doc = Document.of(pomXml); + assertEquals(6, strategy.detectSourceLevel(doc)); + } + + @Test + @DisplayName("should detect from compiler plugin ") + void detectFromCompilerPluginSource() { + String pomXml = """ + + + 4.0.0 + com.example + test + 1.0 + + + + maven-compiler-plugin + + 1.5 + + + + + + """; + Document doc = Document.of(pomXml); + assertEquals(5, strategy.detectSourceLevel(doc)); + } + + @Test + @DisplayName("should return -1 when no source level configured") + void noSourceLevel() { + String pomXml = """ + + + 4.0.0 + com.example + test + 1.0 + + """; + Document doc = Document.of(pomXml); + assertEquals(-1, strategy.detectSourceLevel(doc)); + } + + @Test + @DisplayName("should detect from pluginManagement") + void detectFromPluginManagement() { + String pomXml = """ + + + 4.0.0 + com.example + test + 1.0 + + + + + maven-compiler-plugin + + 6 + + + + + + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + + + org.apache.maven.plugins + maven-toolchains-plugin + + + + select-jdk-toolchain + + + + + + + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + + + org.apache.maven.plugins + maven-toolchains-plugin + + + + toolchain + + + + + + + + """; + Document doc = Document.of(pomXml); + assertFalse(strategy.hasToolchainsPluginWithSelectGoal(doc)); + } + + @Test + @DisplayName("should not detect when no toolchains plugin present") + void noToolchainsPlugin() { + String pomXml = """ + + + 4.0.0 + com.example + test + 1.0 + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + + + maven-compiler-plugin + + + + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + 6 + + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + 11 + + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + """; + 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 = """ + + + 4.0.0 + com.example + test + 1.0 + + 6 + + + + + org.apache.maven.plugins + maven-toolchains-plugin + + + + select-jdk-toolchain + + + + + + + + """; + 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 56e64895d7a7..7571829a7da4 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 @@ *

  • JDK 21: removed {@code --source 7}, minimum is {@code 8}
  • * */ -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(); } } From 3a43015e5731460b3b807a82a88aec13177e5a78 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 5 Aug 2026 23:47:09 +0200 Subject: [PATCH 11/12] Add version constraint to select-jdk-toolchain configuration The select-jdk-toolchain goal needs a 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. (,8] 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 --- .../invoker/mvnup/goals/ToolchainPluginStrategy.java | 12 +++++++++--- .../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 fb0dd8f0671c..42ff9250abc5 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 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 } section. + * to the POM's {@code } 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 d92824457508..7414d262a124 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 = """ @@ -354,13 +354,15 @@ void addToEmptyPom() { """; Document doc = Document.of(pomXml); - strategy.addToolchainsPlugin(doc); + strategy.addToolchainsPlugin(doc, 11); assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc)); + String output = doc.toXml(); + assertTrue(output.contains("(,11]"), "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 = """ @@ -379,9 +381,11 @@ void addToExistingBuild() { """; Document doc = Document.of(pomXml); - strategy.addToolchainsPlugin(doc); + strategy.addToolchainsPlugin(doc, 8); assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc)); + String output = doc.toXml(); + assertTrue(output.contains("(,8]"), "Expected version constraint in output: " + output); } } From f956cc2e4a2f67d0722150c38c73e4ab2acb2227 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 5 Aug 2026 23:57:19 +0200 Subject: [PATCH 12/12] 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 --- its/core-it-suite/pom.xml | 6 +- ...venITMvnupToolchainPluginStrategyTest.java | 88 +++++++++++++++++++ .../mvnup-toolchain-plugin-strategy/pom.xml | 12 +++ 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITMvnupToolchainPluginStrategyTest.java create mode 100644 its/core-it-suite/src/test/resources/mvnup-toolchain-plugin-strategy/pom.xml diff --git a/its/core-it-suite/pom.xml b/its/core-it-suite/pom.xml index 5871a3570f4e..4ae52e82eeae 100644 --- a/its/core-it-suite/pom.xml +++ b/its/core-it-suite/pom.xml @@ -519,7 +519,7 @@ under the License. - + @@ -749,7 +749,7 @@ under the License. - + @@ -804,7 +804,7 @@ under the License. - + 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 000000000000..718b6db66d8d --- /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}. + *

    + * 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 } 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("(,8]"), + "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 000000000000..6fa0b3dc18ac --- /dev/null +++ b/its/core-it-suite/src/test/resources/mvnup-toolchain-plugin-strategy/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + com.example + old-source-test + 1.0-SNAPSHOT + + 1.5 + 1.5 + +