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: + * + */ + @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: + * + */ + @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. + + + + 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))); + } +}