This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven-toolchains-plugin.git
The following commit(s) were added to refs/heads/master by this push: new e22b3cf [MTOOLCHAINS-49] Automatic discovery of JDK toolchains (#14) e22b3cf is described below commit e22b3cf1d8de62fdea8673134d9e172ec454f449 Author: Guillaume Nodet <gno...@gmail.com> AuthorDate: Wed Mar 20 09:00:12 2024 +0100 [MTOOLCHAINS-49] Automatic discovery of JDK toolchains (#14) --- pom.xml | 28 ++ .../jdk/DisplayDiscoveredJdkToolchainsMojo.java | 75 ++++ .../jdk/GenerateJdkToolchainsXmlMojo.java | 75 ++++ .../toolchain/jdk/SelectJdkToolchainMojo.java | 265 ++++++++++++ .../plugins/toolchain/jdk/ToolchainDiscoverer.java | 465 +++++++++++++++++++++ src/site/apt/index.apt.vm | 21 +- src/site/apt/toolchains/discovery.apt.vm | 147 +++++++ src/site/apt/toolchains/jdk.apt.vm | 1 + src/site/site.xml | 4 + .../toolchain/jdk/ToolchainDiscovererTest.java | 54 +++ 10 files changed, 1133 insertions(+), 2 deletions(-) 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. <artifactId>maven-plugin-annotations</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-utils</artifactId> + <version>4.0.0</version> + </dependency> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-xml</artifactId> + <version>3.0.0</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.7.36</version> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>5.10.2</version> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -114,5 +136,11 @@ under the License. </plugin> </plugins> </pluginManagement> + <plugins> + <plugin> + <groupId>org.eclipse.sisu</groupId> + <artifactId>sisu-maven-plugin</artifactId> + </plugin> + </plugins> </build> </project> 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: + * <ul> + * <li>{@code lts}: prefer JDK with LTS version</li> + * <li>{@code current}: prefer the current JDK</li> + * <li>{@code env}: prefer JDKs defined using {@code JAVA\{xx\}_HOME} environment variables</li> + * <li>{@code version}: prefer JDK with higher versions</li> + * <li>{@code vendor}: order JDK by vendor name (usually as a last comparator to ensure a stable order)</li> + * </ul> + */ + @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<ToolchainModel> 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: + * <ul> + * <li>{@code lts}: prefer JDK with LTS version</li> + * <li>{@code current}: prefer the current JDK</li> + * <li>{@code env}: prefer JDKs defined using {@code JAVA\{xx\}_HOME} environment variables</li> + * <li>{@code version}: prefer JDK with higher versions</li> + * <li>{@code vendor}: order JDK by vendor name (usually as a last comparator to ensure a stable order)</li> + * </ul> + */ + @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<String, String> 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<String, String> requirements) { + ToolchainModel model = tc.getModel(); + for (Map.Entry<String, String> 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<String> 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<Path, ToolchainModel> cache; + private volatile boolean cacheModified; + private volatile Set<Path> foundJdks; + + /** + * Build the model for the current JDK toolchain + */ + public Optional<ToolchainModel> 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 <code>null</code>. + */ + public PersistedToolchains discoverToolchains(String comparator) { + try { + Set<Path> jdks = findJdks(); + log.info("Found " + jdks.size() + " possible jdks: " + jdks); + readCache(); + Map<Path, Map<String, String>> 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<String, String> f = flags.computeIfAbsent(path, p -> new HashMap<>()); + String val = f.getOrDefault(ENV, ""); + f.put(ENV, (val.isEmpty() ? "" : val + ",") + e.getKey()); + }); + + List<ToolchainModel> 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<String> 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<String, String> 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<ToolchainModel> getToolchainModelComparator(String comparator) { + Comparator<ToolchainModel> c = null; + for (String part : comparator.split(COMMA)) { + c = c == null ? getComparator(part) : c.thenComparing(getComparator(part)); + } + return c; + } + + private Comparator<ToolchainModel> 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<ToolchainModel> lts() { + return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(LTS) ? -1 : +1); + } + + Comparator<ToolchainModel> vendor() { + return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VENDOR)); + } + + Comparator<ToolchainModel> env() { + return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(ENV) ? -1 : +1); + } + + Comparator<ToolchainModel> current() { + return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(CURRENT) ? -1 : +1); + } + + Comparator<ToolchainModel> 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<Path> findJdks() { + if (foundJdks == null) { + synchronized (this) { + if (foundJdks == null) { + foundJdks = doFindJdks(); + } + } + } + return foundJdks; + } + + private Set<Path> doFindJdks() { + List<Path> 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<Path> 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<Path> 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<Path> 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 + <<<select-jdk-toolchain>>> 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 <<<toolchains.xml>>> containing discovered JDKs. + + The previous <<<toolchain>>> 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 + <<<mvn org.apache.maven.plugins:maven-toolchains-plugin:${project.version}:display-discovered-jdk-toolchains>>>. + 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: + + * <<<version>>>: the JDK version + + * <<<runtime.name>>>: the name of the JDK runtime + + * <<<runtime.version>>>: the version of the JDK runtime + + * <<<vendor>>>: the vendor name + + * <<<vendor.version>>>: the vendor version + + * <<<current>>>: set to <<<true>>> if this is the running JDK + + * <<<lts>>>: set to <<<true>>> if JDK version is a long-term supported version + + * <<<env>>>: set to the comma separated list of <<<JAVA\{xyz\}_HOME>>>> matching environment variables + + + The <<<select-jdk-toolchain>>> 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. + ++---+ +<properties> + <toolchain.jdk.version>[17,)</toolchain.jdk.version> +<properties> + +<plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-toolchains-plugin</artifactId> + <version>${project.version}</version> + <executions> + <execution> + <goals> + <goal>select-jdk-toolchain</goal> + </goals> + </execution> + </executions> +</plugin> ++---+ + + If you use environment variables to configure your JDKs, you can use the following configuration to select + the toolchain which is configured using the <<<JAVA17_HOME>>> environment variable. + ++---+ +<properties> + <toolchain.jdk.version>JAVA17_HOME</toolchain.jdk.version> +<properties> ++---+ + +* Selection mechanism + + Several properties can express requirements to match against discovered JDK toolchains: + + * <<<version>>> / <<<toolchain.jdk.version>>>: a version range such as <<<[17,18)>>> to match against the JDK version + + * <<<runtimeName>>> / <<<toolchain.jdk.runtime.name>>> + + * <<<runtimeVersion>>> / <<<toolchain.jdk.runtime.version>>> + + * <<<vendor>>> / <<<toolchain.jdk.vendor>>> + + * <<<env>>> / <<<toolchain.jdk.env>>>: the name of an environment variable that the JDK toolchain must match + + The <<<useJdk>>> 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 <<<comparator>>> configuration which is a + comma separated list of criteria amongst the following: + + * <<<lts>>>: prefer LTS toolchains + + * <<<current>>>: prefer the current JDK + + * <<<env>>>: prefer toolchains discovered from environment variables + + * <<<version>>>: prefer higher JDK versions + + * <<<vendor>>>: sort alphabetically by vendor name + + The default value is <<<lts,current,env,version,vendor>>>. + +* Toolchains XML file + + The generation of the <<<toolchains.xml>>> file is not necessary to use discovered toolchains. + The <<<select-jdk-toolchain>>> 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 <<<generate-jdk-toolchains-xml>>> to generate a toolchain XML. This can be used in + conjunction with the <<<discoverToolchains=false>>> 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. <menu name="Overview"> <item name="Introduction" href="index.html"/> <item name="Goals" href="plugin-info.html"> + <item name="toolchains:select-jdk-toolchain" href="select-jdk-toolchain-mojo.html"/> <item name="toolchains:toolchain" href="toolchain-mojo.html"/> + <item name="toolchains:display-discovered-jdk-toolchains-xml" href="display-discovered-jdk-toolchains-xml-mojo.html"/> + <item name="toolchains:generate-jdk-toolchains-xml" href="generate-jdk-toolchains-xml-mojo.html"/> <item name="toolchains:help" href="help-mojo.html"/> </item> <item name="Usage" href="usage.html"> + <item name="Discovery mechanism" href="toolchains/discovery.html"/> <item name="Standard Toolchains" href="toolchains/index.html"/> <item name="JDK Standard Toolchain" href="toolchains/jdk.html"/> <item name="Custom Toolchains" href="toolchains/custom.html"/> diff --git a/src/test/java/org/apache/maven/plugins/toolchain/jdk/ToolchainDiscovererTest.java b/src/test/java/org/apache/maven/plugins/toolchain/jdk/ToolchainDiscovererTest.java new file mode 100644 index 0000000..11657c0 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/toolchain/jdk/ToolchainDiscovererTest.java @@ -0,0 +1,54 @@ +/* + * 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 org.apache.maven.toolchain.model.PersistedToolchains; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.JRE; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.CURRENT; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ToolchainDiscovererTest { + + final Logger logger = LoggerFactory.getLogger(getClass()); + + @Test + @DisabledOnJre(JRE.JAVA_8) // java 8 often has jdk != jre + void testDiscovery() { + ToolchainDiscoverer discoverer = new ToolchainDiscoverer(); + PersistedToolchains persistedToolchains = discoverer.discoverToolchains(); + assertNotNull(persistedToolchains); + + persistedToolchains.getToolchains().forEach(model -> { + logger.info(" - " + + ((Xpp3Dom) model.getConfiguration()).getChild("jdkHome").getValue()); + logger.info(" provides:"); + model.getProvides().forEach((k, v) -> logger.info(" " + k + ": " + v)); + }); + + assertTrue(persistedToolchains.getToolchains().stream() + .anyMatch(tc -> tc.getProvides().containsKey(CURRENT))); + } +}