diff --git a/pom.xml b/pom.xml
index 3bc53f3..59c0165 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,6 +90,28 @@ under the License.
maven-plugin-annotations
provided
+
+ org.codehaus.plexus
+ plexus-utils
+ 4.0.0
+
+
+ org.codehaus.plexus
+ plexus-xml
+ 3.0.0
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+ test
+
@@ -114,5 +136,11 @@ under the License.
+
+
+ org.eclipse.sisu
+ sisu-maven-plugin
+
+
diff --git a/src/main/java/org/apache/maven/plugins/toolchain/jdk/DisplayDiscoveredJdkToolchainsMojo.java b/src/main/java/org/apache/maven/plugins/toolchain/jdk/DisplayDiscoveredJdkToolchainsMojo.java
new file mode 100644
index 0000000..bf3b46c
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/toolchain/jdk/DisplayDiscoveredJdkToolchainsMojo.java
@@ -0,0 +1,75 @@
+/*
+ * 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.plugins.toolchain.jdk;
+
+import javax.inject.Inject;
+
+import java.util.List;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.ToolchainModel;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+
+import static java.util.Comparator.comparing;
+import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.SORTED_PROVIDES;
+
+/**
+ * Discover the JDK toolchains and print them to the console.
+ */
+@Mojo(name = "display-discovered-jdk-toolchains", requiresProject = false)
+public class DisplayDiscoveredJdkToolchainsMojo extends AbstractMojo {
+
+ /**
+ * Comparator used to sort JDK toolchains for selection.
+ * This property is a comma separated list of values which may contains:
+ *
+ * - {@code lts}: prefer JDK with LTS version
+ * - {@code current}: prefer the current JDK
+ * - {@code env}: prefer JDKs defined using {@code JAVA\{xx\}_HOME} environment variables
+ * - {@code version}: prefer JDK with higher versions
+ * - {@code vendor}: order JDK by vendor name (usually as a last comparator to ensure a stable order)
+ *
+ */
+ @Parameter(property = "toolchain.jdk.comparator", defaultValue = "lts,current,env,version,vendor")
+ String comparator;
+
+ /**
+ * Toolchain discoverer
+ */
+ @Inject
+ ToolchainDiscoverer discoverer;
+
+ @Override
+ public void execute() {
+ PersistedToolchains toolchains = discoverer.discoverToolchains(comparator);
+ List models = toolchains.getToolchains();
+ getLog().info("Discovered " + models.size() + " JDK toolchains:");
+ for (ToolchainModel model : models) {
+ getLog().info(" - "
+ + ((Xpp3Dom) model.getConfiguration()).getChild("jdkHome").getValue());
+ getLog().info(" provides:");
+ model.getProvides().entrySet().stream()
+ .sorted(comparing(e -> SORTED_PROVIDES.indexOf(e.getKey().toString())))
+ .forEach(e -> getLog().info(" " + e.getKey() + ": " + e.getValue()));
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/toolchain/jdk/GenerateJdkToolchainsXmlMojo.java b/src/main/java/org/apache/maven/plugins/toolchain/jdk/GenerateJdkToolchainsXmlMojo.java
new file mode 100644
index 0000000..450eb95
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/toolchain/jdk/GenerateJdkToolchainsXmlMojo.java
@@ -0,0 +1,75 @@
+/*
+ * 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.plugins.toolchain.jdk;
+
+import javax.inject.Inject;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Writer;
+
+/**
+ * Run the JDK toolchain discovery mechanism and generates a toolchains XML.
+ */
+@Mojo(name = "generate-jdk-toolchains-xml", requiresProject = false)
+public class GenerateJdkToolchainsXmlMojo extends AbstractMojo {
+
+ /**
+ * The path and name pf the toolchain XML file that will be generated.
+ * If not provided, the XML will be written to the standard output.
+ */
+ @Parameter(property = "toolchain.file")
+ String file;
+
+ /**
+ * Toolchain discoverer
+ */
+ @Inject
+ ToolchainDiscoverer discoverer;
+
+ @Override
+ public void execute() throws MojoFailureException {
+ try {
+ PersistedToolchains toolchains = discoverer.discoverToolchains();
+ if (file != null) {
+ Path file = Paths.get(this.file).toAbsolutePath();
+ Files.createDirectories(file.getParent());
+ try (Writer writer = Files.newBufferedWriter(file)) {
+ new MavenToolchainsXpp3Writer().write(writer, toolchains);
+ }
+ } else {
+ StringWriter writer = new StringWriter();
+ new MavenToolchainsXpp3Writer().write(writer, toolchains);
+ System.out.println(writer);
+ }
+ } catch (IOException e) {
+ throw new MojoFailureException("Unable to generate toolchains.xml", e);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/toolchain/jdk/SelectJdkToolchainMojo.java b/src/main/java/org/apache/maven/plugins/toolchain/jdk/SelectJdkToolchainMojo.java
new file mode 100644
index 0000000..02efff9
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/toolchain/jdk/SelectJdkToolchainMojo.java
@@ -0,0 +1,265 @@
+/*
+ * 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.plugins.toolchain.jdk;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.toolchain.MisconfiguredToolchainException;
+import org.apache.maven.toolchain.RequirementMatcherFactory;
+import org.apache.maven.toolchain.ToolchainFactory;
+import org.apache.maven.toolchain.ToolchainManagerPrivate;
+import org.apache.maven.toolchain.ToolchainPrivate;
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.ToolchainModel;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+
+import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.ENV;
+import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.RUNTIME_NAME;
+import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.RUNTIME_VERSION;
+import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.VENDOR;
+import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.VERSION;
+
+/**
+ * Discover JDK toolchains and select a matching one.
+ */
+@Mojo(name = "select-jdk-toolchain", defaultPhase = LifecyclePhase.VALIDATE)
+public class SelectJdkToolchainMojo extends AbstractMojo {
+
+ public static final String TOOLCHAIN_TYPE_JDK = "jdk";
+
+ /** Jdk usage mode */
+ public enum JdkMode {
+ /** always ignore the current JDK */
+ Never,
+ /** to not use a toolchain if the toolchains that would be selected is the current JDK */
+ IfSame,
+ /** favor the current JDK if it matches the requirements */
+ IfMatch
+ }
+
+ /**
+ * The version constraint for the JDK toolchain to select.
+ */
+ @Parameter(property = "toolchain.jdk.version")
+ private String version;
+
+ /**
+ * The runtime name constraint for the JDK toolchain to select.
+ */
+ @Parameter(property = "toolchain.jdk.runtime.name")
+ private String runtimeName;
+
+ /**
+ * The runtime version constraint for the JDK toolchain to select.
+ */
+ @Parameter(property = "toolchain.jdk.runtime.version")
+ private String runtimeVersion;
+
+ /**
+ * The vendor constraint for the JDK toolchain to select.
+ */
+ @Parameter(property = "toolchain.jdk.vendor")
+ private String vendor;
+
+ /**
+ * The env constraint for the JDK toolchain to select.
+ * To match the constraint, an environment variable with the given name must point to the JDK.
+ * For example, if you define {@code JAVA11_HOME=~/jdks/my-jdk-11.0.1}, you can specify
+ * {@code env=JAVA11_HOME} to match the given JDK.
+ */
+ @Parameter(property = "toolchain.jdk.env")
+ private String env;
+
+ /**
+ * The matching mode, either {@code IfMatch} (the default), {@code IfSame}, or {@code Never}.
+ * If {@code IfMatch} is used, a toolchain will not be selected if the running JDK does
+ * match the provided constraints. This is the default and provides better performances as it
+ * avoids forking a different process when it's not required. The {@code IfSame} avoids
+ * selecting a toolchain if the toolchain selected is exactly the same as the running JDK.
+ * THe {@code Never} option will always select the toolchain.
+ */
+ @Parameter(property = "toolchain.jdk.mode", defaultValue = "IfMatch")
+ private JdkMode useJdk = JdkMode.IfMatch;
+
+ /**
+ * Automatically discover JDK toolchains using the built-in heuristic.
+ * The default value is {@code true}.
+ */
+ @Parameter(property = "toolchain.jdk.discover", defaultValue = "true")
+ private boolean discoverToolchains = true;
+
+ /**
+ * Comparator used to sort JDK toolchains for selection.
+ * This property is a comma separated list of values which may contains:
+ *
+ * - {@code lts}: prefer JDK with LTS version
+ * - {@code current}: prefer the current JDK
+ * - {@code env}: prefer JDKs defined using {@code JAVA\{xx\}_HOME} environment variables
+ * - {@code version}: prefer JDK with higher versions
+ * - {@code vendor}: order JDK by vendor name (usually as a last comparator to ensure a stable order)
+ *
+ */
+ @Parameter(property = "toolchain.jdk.comparator", defaultValue = "lts,current,env,version,vendor")
+ private String comparator;
+
+ /**
+ * Toolchain manager
+ */
+ @Inject
+ private ToolchainManagerPrivate toolchainManager;
+
+ /**
+ * Toolchain factory
+ */
+ @Inject
+ @Named(TOOLCHAIN_TYPE_JDK)
+ ToolchainFactory factory;
+
+ /**
+ * The current build session instance. This is used for toolchain manager API calls.
+ */
+ @Inject
+ private MavenSession session;
+
+ /**
+ * Toolchain discoverer
+ */
+ @Inject
+ ToolchainDiscoverer discoverer;
+
+ @Override
+ public void execute() throws MojoFailureException {
+ try {
+ doExecute();
+ } catch (MisconfiguredToolchainException e) {
+ throw new MojoFailureException("Unable to select toolchain: " + e, e);
+ }
+ }
+
+ private void doExecute() throws MisconfiguredToolchainException, MojoFailureException {
+ if (version == null && runtimeName == null && runtimeVersion == null && vendor == null && env == null) {
+ return;
+ }
+
+ Map requirements = new HashMap<>();
+ Optional.ofNullable(version).ifPresent(v -> requirements.put(VERSION, v));
+ Optional.ofNullable(runtimeName).ifPresent(v -> requirements.put(RUNTIME_NAME, v));
+ Optional.ofNullable(runtimeVersion).ifPresent(v -> requirements.put(RUNTIME_VERSION, v));
+ Optional.ofNullable(vendor).ifPresent(v -> requirements.put(VENDOR, v));
+ Optional.ofNullable(env).ifPresent(v -> requirements.put(ENV, v));
+
+ ToolchainModel currentJdkToolchainModel =
+ discoverer.getCurrentJdkToolchain().orElse(null);
+ ToolchainPrivate currentJdkToolchain =
+ currentJdkToolchainModel != null ? factory.createToolchain(currentJdkToolchainModel) : null;
+
+ if (useJdk == JdkMode.IfMatch && currentJdkToolchain != null && matches(currentJdkToolchain, requirements)) {
+ getLog().info("Not using an external toolchain as the current JDK matches the requirements.");
+ return;
+ }
+
+ ToolchainPrivate toolchain = Stream.of(toolchainManager.getToolchainsForType(TOOLCHAIN_TYPE_JDK, session))
+ .filter(tc -> matches(tc, requirements))
+ .findFirst()
+ .orElse(null);
+ if (toolchain != null) {
+ getLog().info("Found matching JDK toolchain: " + toolchain);
+ }
+
+ if (toolchain == null && discoverToolchains) {
+ getLog().debug("No matching toolchains configured, trying to discover JDK toolchains");
+ PersistedToolchains persistedToolchains = discoverer.discoverToolchains(comparator);
+ getLog().debug("Discovered " + persistedToolchains.getToolchains().size() + " JDK toolchains");
+
+ for (ToolchainModel tcm : persistedToolchains.getToolchains()) {
+ ToolchainPrivate tc = factory.createToolchain(tcm);
+ if (tc != null && matches(tc, requirements)) {
+ toolchain = tc;
+ getLog().debug("Discovered matching JDK toolchain: " + toolchain);
+ break;
+ }
+ }
+ }
+
+ if (toolchain == null) {
+ throw new MojoFailureException(
+ "Cannot find matching toolchain definitions for the following toolchain types:" + requirements
+ + System.lineSeparator()
+ + "Define the required toolchains in your ~/.m2/toolchains.xml file.");
+ }
+
+ if (useJdk == JdkMode.IfSame
+ && currentJdkToolchain != null
+ && Objects.equals(getJdkHome(currentJdkToolchain), getJdkHome(toolchain))) {
+ getLog().debug("Not using an external toolchain as the current JDK has been selected.");
+ return;
+ }
+
+ toolchainManager.storeToolchainToBuildContext(toolchain, session);
+ getLog().debug("Found matching JDK toolchain: " + toolchain);
+ }
+
+ private boolean matches(ToolchainPrivate tc, Map requirements) {
+ ToolchainModel model = tc.getModel();
+ for (Map.Entry req : requirements.entrySet()) {
+ String key = req.getKey();
+ String reqVal = req.getValue();
+ String tcVal = model.getProvides().getProperty(key);
+ if (tcVal == null) {
+ getLog().debug("Toolchain " + tc + " is missing required property: " + key);
+ return false;
+ }
+ if (!matches(key, reqVal, tcVal)) {
+ getLog().debug("Toolchain " + tc + " doesn't match required property: " + key);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean matches(String key, String reqVal, String tcVal) {
+ switch (key) {
+ case VERSION:
+ return RequirementMatcherFactory.createVersionMatcher(tcVal).matches(reqVal);
+ case ENV:
+ return reqVal.matches("(.*,|^)\\Q" + tcVal + "\\E(,.*|$)");
+ default:
+ return RequirementMatcherFactory.createExactMatcher(tcVal).matches(reqVal);
+ }
+ }
+
+ private String getJdkHome(ToolchainPrivate toolchain) {
+ return ((Xpp3Dom) toolchain.getModel().getConfiguration())
+ .getChild("jdkHome")
+ .getValue();
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/toolchain/jdk/ToolchainDiscoverer.java b/src/main/java/org/apache/maven/plugins/toolchain/jdk/ToolchainDiscoverer.java
new file mode 100644
index 0000000..f138565
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/toolchain/jdk/ToolchainDiscoverer.java
@@ -0,0 +1,465 @@
+/*
+ * 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.plugins.toolchain.jdk;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.toolchain.model.PersistedToolchains;
+import org.apache.maven.toolchain.model.ToolchainModel;
+import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Reader;
+import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Writer;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Comparator.comparing;
+import static org.apache.maven.plugins.toolchain.jdk.SelectJdkToolchainMojo.TOOLCHAIN_TYPE_JDK;
+
+/**
+ * Toolchain discoverer service
+ */
+@Named
+@Singleton
+public class ToolchainDiscoverer {
+
+ public static final String JAVA = "java.";
+ public static final String VERSION = "version";
+ public static final String RUNTIME_NAME = "runtime.name";
+ public static final String RUNTIME_VERSION = "runtime.version";
+ public static final String VENDOR = "vendor";
+ public static final String VENDOR_VERSION = "vendor.version";
+ public static final String[] PROPERTIES = {VERSION, RUNTIME_NAME, RUNTIME_VERSION, VENDOR, VENDOR_VERSION};
+
+ public static final String CURRENT = "current";
+ public static final String ENV = "env";
+ public static final String LTS = "lts";
+
+ public static final List SORTED_PROVIDES = Collections.unmodifiableList(
+ Arrays.asList(VERSION, RUNTIME_NAME, RUNTIME_VERSION, VENDOR, VENDOR_VERSION, CURRENT, LTS, ENV));
+
+ public static final String DISCOVERED_TOOLCHAINS_CACHE_XML = ".m2/discovered-toolchains-cache.xml";
+
+ public static final String JDK_HOME = "jdkHome";
+ public static final String JAVA_HOME = "java.home";
+
+ private static final String COMMA = ",";
+ public static final String USER_HOME = "user.home";
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ private volatile Map cache;
+ private volatile boolean cacheModified;
+ private volatile Set foundJdks;
+
+ /**
+ * Build the model for the current JDK toolchain
+ */
+ public Optional getCurrentJdkToolchain() {
+ Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty(JAVA_HOME)));
+ if (!hasJavaC(currentJdkHome)) {
+ // in case the current JVM is not a JDK
+ return Optional.empty();
+ }
+ ToolchainModel model = new ToolchainModel();
+ model.setType(TOOLCHAIN_TYPE_JDK);
+ Stream.of(PROPERTIES).forEach(k -> {
+ String v = System.getProperty(JAVA + k);
+ if (v != null) {
+ model.addProvide(k, v);
+ }
+ });
+ model.addProvide(CURRENT, "true");
+ Xpp3Dom config = new Xpp3Dom("configuration");
+ Xpp3Dom jdkHome = new Xpp3Dom(JDK_HOME);
+ jdkHome.setValue(currentJdkHome.toString());
+ config.addChild(jdkHome);
+ model.setConfiguration(config);
+ return Optional.of(model);
+ }
+
+ public PersistedToolchains discoverToolchains() {
+ return discoverToolchains(LTS + COMMA + VERSION + COMMA + VENDOR);
+ }
+
+ /**
+ * Returns a PersistedToolchains object containing a list of discovered toolchains,
+ * never null.
+ */
+ public PersistedToolchains discoverToolchains(String comparator) {
+ try {
+ Set jdks = findJdks();
+ log.info("Found " + jdks.size() + " possible jdks: " + jdks);
+ readCache();
+ Map> flags = new HashMap<>();
+ Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty(JAVA_HOME)));
+ flags.computeIfAbsent(currentJdkHome, p -> new HashMap<>()).put(CURRENT, "true");
+ // check environment variables for JAVA{xx}_HOME
+ System.getenv().entrySet().stream()
+ .filter(e -> e.getKey().startsWith("JAVA") && e.getKey().endsWith("_HOME"))
+ .forEach(e -> {
+ Path path = getCanonicalPath(Paths.get(e.getValue()));
+ Map f = flags.computeIfAbsent(path, p -> new HashMap<>());
+ String val = f.getOrDefault(ENV, "");
+ f.put(ENV, (val.isEmpty() ? "" : val + ",") + e.getKey());
+ });
+
+ List tcs = jdks.parallelStream()
+ .map(s -> {
+ ToolchainModel tc = getToolchainModel(s);
+ flags.getOrDefault(s, Collections.emptyMap())
+ .forEach((k, v) -> tc.getProvides().setProperty(k, v));
+ String version = tc.getProvides().getProperty(VERSION);
+ if (isLts(version)) {
+ tc.getProvides().setProperty(LTS, "true");
+ }
+ return tc;
+ })
+ .sorted(getToolchainModelComparator(comparator))
+ .collect(Collectors.toList());
+ writeCache();
+ PersistedToolchains ps = new PersistedToolchains();
+ ps.setToolchains(tcs);
+ return ps;
+ } catch (Exception e) {
+ if (log.isDebugEnabled()) {
+ log.warn("Error discovering toolchains: " + e, e);
+ } else {
+ log.warn("Error discovering toolchains (enable debug level for more information): " + e);
+ }
+ return new PersistedToolchains();
+ }
+ }
+
+ private static boolean isLts(String version) {
+ return Stream.of("1.8", "8", "11", "17", "21", "25")
+ .anyMatch(v -> version.equals(v) || version.startsWith(v + "."));
+ }
+
+ private synchronized void readCache() {
+ if (cache == null) {
+ try {
+ cache = new ConcurrentHashMap<>();
+ cacheModified = false;
+ Path cacheFile = getCacheFile();
+ if (Files.isRegularFile(cacheFile)) {
+ try (Reader r = Files.newBufferedReader(cacheFile)) {
+ PersistedToolchains pt = new MavenToolchainsXpp3Reader().read(r, false);
+ cache = pt.getToolchains().stream()
+ // Remove stale entries
+ .filter(tc -> {
+ // If the bin/java executable is not available anymore, remove this TC
+ if (!hasJavaC(getJdkHome(tc))) {
+ cacheModified = true;
+ return false;
+ } else {
+ return true;
+ }
+ })
+ .collect(Collectors.toConcurrentMap(this::getJdkHome, Function.identity()));
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ log.debug("Error reading toolchains cache: " + e, e);
+ }
+ }
+ }
+
+ private synchronized void writeCache() {
+ if (cacheModified) {
+ try {
+ Path cacheFile = getCacheFile();
+ Files.createDirectories(cacheFile.getParent());
+ try (Writer w = Files.newBufferedWriter(cacheFile)) {
+ PersistedToolchains pt = new PersistedToolchains();
+ pt.setToolchains(cache.values().stream()
+ .map(tc -> {
+ ToolchainModel model = tc.clone();
+ // Remove transient information
+ model.getProvides().remove(CURRENT);
+ model.getProvides().remove(ENV);
+ return model;
+ })
+ .sorted(version().thenComparing(vendor()))
+ .collect(Collectors.toList()));
+ new MavenToolchainsXpp3Writer().write(w, pt);
+ }
+ } catch (IOException e) {
+ log.debug("Error writing toolchains cache: " + e, e);
+ }
+ cacheModified = false;
+ }
+ }
+
+ ToolchainModel getToolchainModel(Path jdk) {
+ ToolchainModel model = cache.get(jdk);
+ if (model == null) {
+ model = doGetToolchainModel(jdk);
+ cache.put(jdk, model);
+ cacheModified = true;
+ }
+ return model;
+ }
+
+ private static Path getCacheFile() {
+ return Paths.get(System.getProperty(USER_HOME)).resolve(DISCOVERED_TOOLCHAINS_CACHE_XML);
+ }
+
+ public Path getJdkHome(ToolchainModel toolchain) {
+ Xpp3Dom dom = (Xpp3Dom) toolchain.getConfiguration();
+ Xpp3Dom javahome = dom != null ? dom.getChild(JDK_HOME) : null;
+ String jdk = javahome != null ? javahome.getValue() : null;
+ return Paths.get(Objects.requireNonNull(jdk));
+ }
+
+ ToolchainModel doGetToolchainModel(Path jdk) {
+ Path java = jdk.resolve("bin").resolve("java");
+ if (!Files.exists(java)) {
+ java = jdk.resolve("bin").resolve("java.exe");
+ if (!Files.exists(java)) {
+ log.debug("JDK toolchain discovered at " + jdk
+ + " will be ignored: unable to find bin/java or bin\\java.exe");
+ return null;
+ }
+ }
+ if (!java.toFile().canExecute()) {
+ log.debug("JDK toolchain discovered at " + jdk
+ + " will be ignored: the bin/java or bin\\java.exe is not executable");
+ return null;
+ }
+ List lines;
+ try {
+ Path temp = Files.createTempFile("jdk-opts-", ".out");
+ try {
+ new ProcessBuilder()
+ .command(java.toString(), "-XshowSettings:properties", "-version")
+ .redirectError(temp.toFile())
+ .start()
+ .waitFor();
+ lines = Files.readAllLines(temp);
+ } finally {
+ Files.delete(temp);
+ }
+ } catch (IOException | InterruptedException e) {
+ log.debug("JDK toolchain discovered at " + jdk + " will be ignored: error executing java: " + e);
+ return null;
+ }
+
+ Map properties = new LinkedHashMap<>();
+ Stream.of(PROPERTIES).forEach(name -> {
+ lines.stream()
+ .filter(l -> l.contains(JAVA + name))
+ .map(l -> l.replaceFirst(".*=\\s*(.*)", "$1"))
+ .findFirst()
+ .ifPresent(value -> properties.put(name, value));
+ });
+ if (!properties.containsKey(VERSION)) {
+ log.debug("JDK toolchain discovered at " + jdk + " will be ignored: could not obtain " + JAVA + VERSION);
+ return null;
+ }
+
+ ToolchainModel model = new ToolchainModel();
+ model.setType(TOOLCHAIN_TYPE_JDK);
+ properties.forEach(model::addProvide);
+ Xpp3Dom configuration = new Xpp3Dom("configuration");
+ Xpp3Dom jdkHome = new Xpp3Dom(JDK_HOME);
+ jdkHome.setValue(jdk.toString());
+ configuration.addChild(jdkHome);
+ model.setConfiguration(configuration);
+ return model;
+ }
+
+ private static Path getCanonicalPath(Path path) {
+ try {
+ return path.toRealPath();
+ } catch (IOException e) {
+ return getCanonicalPath(path.getParent()).resolve(path.getFileName());
+ }
+ }
+
+ Comparator getToolchainModelComparator(String comparator) {
+ Comparator c = null;
+ for (String part : comparator.split(COMMA)) {
+ c = c == null ? getComparator(part) : c.thenComparing(getComparator(part));
+ }
+ return c;
+ }
+
+ private Comparator getComparator(String part) {
+ switch (part.trim().toLowerCase(Locale.ROOT)) {
+ case LTS:
+ return lts();
+ case VENDOR:
+ return vendor();
+ case ENV:
+ return env();
+ case CURRENT:
+ return current();
+ case VERSION:
+ return version();
+ default:
+ throw new IllegalArgumentException("Unsupported comparator: " + part
+ + ". Supported comparators are: vendor, env, current, lts and version.");
+ }
+ }
+
+ Comparator lts() {
+ return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(LTS) ? -1 : +1);
+ }
+
+ Comparator vendor() {
+ return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VENDOR));
+ }
+
+ Comparator env() {
+ return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(ENV) ? -1 : +1);
+ }
+
+ Comparator current() {
+ return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(CURRENT) ? -1 : +1);
+ }
+
+ Comparator version() {
+ return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VERSION), (v1, v2) -> {
+ String[] a = v1.split("\\.");
+ String[] b = v2.split("\\.");
+ int length = Math.min(a.length, b.length);
+ for (int i = 0; i < length; i++) {
+ String oa = a[i];
+ String ob = b[i];
+ if (!Objects.equals(oa, ob)) {
+ // A null element is less than a non-null element
+ if (oa == null || ob == null) {
+ return oa == null ? -1 : 1;
+ }
+ int v = oa.compareTo(ob);
+ if (v != 0) {
+ return v;
+ }
+ }
+ }
+ return a.length - b.length;
+ })
+ .reversed();
+ }
+
+ private Set findJdks() {
+ if (foundJdks == null) {
+ synchronized (this) {
+ if (foundJdks == null) {
+ foundJdks = doFindJdks();
+ }
+ }
+ }
+ return foundJdks;
+ }
+
+ private Set doFindJdks() {
+ List dirsToTest = new ArrayList<>();
+ // add current JDK
+ dirsToTest.add(Paths.get(System.getProperty(JAVA_HOME)));
+ // check environment variables for JAVA{xx}_HOME
+ System.getenv().entrySet().stream()
+ .filter(e -> e.getKey().startsWith("JAVA") && e.getKey().endsWith("_HOME"))
+ .map(e -> Paths.get(e.getValue()))
+ .forEach(dirsToTest::add);
+ final Path userHome = Paths.get(System.getProperty(USER_HOME));
+ List installedDirs = new ArrayList<>();
+ // jdk installed by third
+ installedDirs.add(userHome.resolve(".jdks"));
+ installedDirs.add(userHome.resolve(".m2").resolve("jdks"));
+ installedDirs.add(userHome.resolve(".sdkman").resolve("candidates").resolve("java"));
+ installedDirs.add(userHome.resolve(".gradle").resolve("jdks"));
+ installedDirs.add(userHome.resolve(".jenv").resolve("versions"));
+ installedDirs.add(userHome.resolve(".jbang").resolve("cache").resolve("jdks"));
+ installedDirs.add(userHome.resolve(".asdf").resolve("installs"));
+ installedDirs.add(userHome.resolve(".jabba").resolve("jdk"));
+ // os related directories
+ String osname = System.getProperty("os.name").toLowerCase(Locale.ROOT);
+ boolean macos = osname.startsWith("mac");
+ boolean win = osname.startsWith("win");
+ if (macos) {
+ installedDirs.add(Paths.get("/Library/Java/JavaVirtualMachines"));
+ installedDirs.add(userHome.resolve("Library/Java/JavaVirtualMachines"));
+ } else if (win) {
+ installedDirs.add(Paths.get("C:\\Program Files\\Java\\"));
+ Path scoop = userHome.resolve("scoop").resolve("apps");
+ if (Files.isDirectory(scoop)) {
+ try (Stream stream = Files.list(scoop)) {
+ stream.forEach(installedDirs::add);
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ } else {
+ installedDirs.add(Paths.get("/usr/jdk"));
+ installedDirs.add(Paths.get("/usr/java"));
+ installedDirs.add(Paths.get("/opt/java"));
+ installedDirs.add(Paths.get("/usr/lib/jvm"));
+ }
+ for (Path dest : installedDirs) {
+ if (Files.isDirectory(dest)) {
+ try (Stream stream = Files.list(dest)) {
+ stream.forEach(dir -> {
+ dirsToTest.add(dir);
+ if (macos) {
+ dirsToTest.add(dir.resolve("Contents").resolve("Home"));
+ }
+ });
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ // only keep directories that have a javac file
+ return dirsToTest.stream()
+ .filter(ToolchainDiscoverer::hasJavaC)
+ .map(ToolchainDiscoverer::getCanonicalPath)
+ .collect(Collectors.toSet());
+ }
+
+ private static boolean hasJavaC(Path subdir) {
+ return Files.exists(subdir.resolve(Paths.get("bin", "javac")))
+ || Files.exists(subdir.resolve(Paths.get("bin", "javac.exe")));
+ }
+}
diff --git a/src/site/apt/index.apt.vm b/src/site/apt/index.apt.vm
index b5bf949..56250dc 100644
--- a/src/site/apt/index.apt.vm
+++ b/src/site/apt/index.apt.vm
@@ -30,11 +30,28 @@ ${project.name}
Similarly to the maven-enforcer-plugin, it allows you to control environmental constraints in the build.
+* Discovery mechanism
+
+ Since version 3.2.0, a new toolchains mechanism is provided. This relies on an automatic discovery mechanism based
+ on an internal heuristic which tries to detect JDK from known locations. This mechanism is to be used with the
+ <<>> goal, read about the {{{./toolchains/discovery.html}discovery mechanism}} for more
+ informations.
+
* Goals Overview
- The Toolchains plugin has one goal:
+ Since version 3.2.0, a new toolchains mechanism is provided. This relies on an automatic discovery mechanism based
+ on an internal heuristic which tries to detect JDK from known locations. This mechanism is to be used with the goal:
+
+ * {{{./toolchain-mojo.html}toolchains:select-jdk-toolchain}} discover and selects a matching toolchain.
+
+ Two helper goals are also provided:
+
+ * {{{./toolchain-mojo.html}toolchains:display-discovered-jdk-toolchains}} displays discovered toolchains to the console.
+ * {{{./toolchain-mojo.html}toolchains:generate-jdk-toolchains-xml}} can be used to write a <<>> containing discovered JDKs.
+
+ The previous <<>> goal is still available:
- * {{{./toolchain-mojo.html}toolchains:toolchain}} selects a toolchain based on configured build requirements and stores it in build context for later retrieval by other plugins.
+ * {{{./toolchain-mojo.html}toolchains:toolchain}} selects a toolchain based on configured build requirements and stores it in build context for later retrieval by other plugins.
* Usage
diff --git a/src/site/apt/toolchains/discovery.apt.vm b/src/site/apt/toolchains/discovery.apt.vm
new file mode 100644
index 0000000..e528bc6
--- /dev/null
+++ b/src/site/apt/toolchains/discovery.apt.vm
@@ -0,0 +1,147 @@
+~~ 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.
+
+ ------
+ Discovery mechanism
+ ------
+ Guillaume Nodet
+ ------
+ 2024-02-28
+ ------
+
+JDK Toolchain discovery mechanism
+
+ Since version 3.2.0, the plugin provides a heuristic to discover installed JDK toolchains, by looking
+ at known installation directories and at environment variables.
+
+ The list of discovered toolchains can be easily displayed using the command
+ <<>>.
+ This will print something like:
+
++---+
+[INFO] Discovered 10 JDK toolchains:
+[INFO] - /Users/gnodet/.sdkman/candidates/java/21.0.2-graalce
+[INFO] provides:
+[INFO] version: 21.0.2
+[INFO] runtime.name: OpenJDK Runtime Environment
+[INFO] runtime.version: 21.0.2+13-jvmci-23.1-b30
+[INFO] vendor: GraalVM Community
+[INFO] vendor.version: GraalVM CE 21.0.2+13.1
+[INFO] current: true
+[INFO] lts: true
+[INFO] env: JAVA_HOME,JAVA21_HOME
+...
++---+
+
+ If you have installed JDKs using known installers and they are not found by the plugin,
+ feel free to {{{../issue-management.html}raise an issue}}.
+
+ The discovery mechanism provides information for each discovered JDK:
+
+ * <<>>: the JDK version
+
+ * <<>>: the name of the JDK runtime
+
+ * <<>>: the version of the JDK runtime
+
+ * <<>>: the vendor name
+
+ * <<>>: the vendor version
+
+ * <<>>: set to <<>> if this is the running JDK
+
+ * <<>>: set to <<>> if JDK version is a long-term supported version
+
+ * <<>>: set to the comma separated list of <<>>> matching environment variables
+
+
+ The <<>> goal finds a matching JDK.
+ The config below allows using the current JDK, or any other discovered JDK >= 17.
+ The current JDK can be kept for speed, but JDK 17 or higher will be used if the current JDK is older than 17.
+
++---+
+
+ [17,)
+
+
+
+ org.apache.maven.plugins
+ maven-toolchains-plugin
+ ${project.version}
+
+
+
+ select-jdk-toolchain
+
+
+
+
++---+
+
+ If you use environment variables to configure your JDKs, you can use the following configuration to select
+ the toolchain which is configured using the <<>> environment variable.
+
++---+
+
+ JAVA17_HOME
+
++---+
+
+* Selection mechanism
+
+ Several properties can express requirements to match against discovered JDK toolchains:
+
+ * <<>> / <<>>: a version range such as <<<[17,18)>>> to match against the JDK version
+
+ * <<>> / <<>>
+
+ * <<>> / <<>>
+
+ * <<>> / <<>>
+
+ * <<>> / <<>>: the name of an environment variable that the JDK toolchain must match
+
+ The <<>> can be used to define whether the current JDK can be used if it matches the requirements.
+
+* Sorting
+
+ Multiple discovered JDK toolchains may satisfy the requirements. In such a case, you can express
+ preferences for sorting the toolchains. This can be done using the <<>> configuration which is a
+ comma separated list of criteria amongst the following:
+
+ * <<>>: prefer LTS toolchains
+
+ * <<>>: prefer the current JDK
+
+ * <<>>: prefer toolchains discovered from environment variables
+
+ * <<>>: prefer higher JDK versions
+
+ * <<>>: sort alphabetically by vendor name
+
+ The default value is <<>>.
+
+* Toolchains XML file
+
+ The generation of the <<>> file is not necessary to use discovered toolchains.
+ The <<>> will select a toolchain amongst explicitly configured toolchains and discovered
+ toolchains. Discovered toolchains are cached in <<<~/.m2/discovered-toolchains-cache.xml>>> file
+ by default, to speed up builds.
+
+ If you prefer, you can use the <<>> to generate a toolchain XML. This can be used in
+ conjunction with the <<>> configuration to disable discovery and only use explicitly
+ configured toolchains.
diff --git a/src/site/apt/toolchains/jdk.apt.vm b/src/site/apt/toolchains/jdk.apt.vm
index 85f3804..9b5a824 100644
--- a/src/site/apt/toolchains/jdk.apt.vm
+++ b/src/site/apt/toolchains/jdk.apt.vm
@@ -25,6 +25,7 @@
JDK Toolchain
+Note that this page refers to hand-written JDK toolchains. For a simpler setup, look at the {{{./discovery.html}discovery mechanism}}.
* Toolchain Description
diff --git a/src/site/site.xml b/src/site/site.xml
index 5be1462..2989a7b 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -26,10 +26,14 @@ under the License.