From b50e2d7c8793d643d42c2414e0001f35f70c9b76 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 May 2023 19:32:10 +0200 Subject: [PATCH 01/11] Automatic discovery of JDK Toolchains --- .../src/main/mdo/toolchains.mdo | 6 +- maven-toolchain-builder/pom.xml | 4 + .../building/DefaultToolchainsBuilder.java | 24 +- .../discovery/DefaultToolchainDiscoverer.java | 147 +++++++ .../toolchain/discovery/JavaHomeFinder.java | 68 +++ .../discovery/JavaHomeFinderBasic.java | 410 ++++++++++++++++++ .../discovery/JavaHomeFinderMac.java | 102 +++++ .../discovery/JavaHomeFinderWindows.java | 105 +++++ .../discovery/ToolchainDiscoverer.java | 28 ++ .../discovery/ToolchainDiscovererTest.java | 37 ++ 10 files changed, 928 insertions(+), 3 deletions(-) create mode 100644 maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java create mode 100644 maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinder.java create mode 100644 maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java create mode 100644 maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderMac.java create mode 100644 maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderWindows.java create mode 100644 maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java create mode 100644 maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/discovery/ToolchainDiscovererTest.java diff --git a/api/maven-api-toolchain/src/main/mdo/toolchains.mdo b/api/maven-api-toolchain/src/main/mdo/toolchains.mdo index 322d7501f25a..0386ebbcb493 100644 --- a/api/maven-api-toolchain/src/main/mdo/toolchains.mdo +++ b/api/maven-api-toolchain/src/main/mdo/toolchains.mdo @@ -61,6 +61,7 @@ org.codehaus.plexus plexus-interpolation + + org.slf4j + slf4j-api + org.mockito mockito-core diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java index 3e7e40d8425d..e4d4e422db6f 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java @@ -33,6 +33,7 @@ import org.apache.maven.building.ProblemCollector; import org.apache.maven.building.ProblemCollectorFactory; import org.apache.maven.building.Source; +import org.apache.maven.toolchain.discovery.ToolchainDiscoverer; import org.apache.maven.toolchain.io.ToolchainsParseException; import org.apache.maven.toolchain.io.ToolchainsReader; import org.apache.maven.toolchain.io.ToolchainsWriter; @@ -54,23 +55,44 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder { private final MavenToolchainMerger toolchainsMerger = new MavenToolchainMerger(); private final ToolchainsWriter toolchainsWriter; private final ToolchainsReader toolchainsReader; + private final List toolchainDiscoverers; @Inject - public DefaultToolchainsBuilder(ToolchainsWriter toolchainsWriter, ToolchainsReader toolchainsReader) { + public DefaultToolchainsBuilder( + ToolchainsWriter toolchainsWriter, + ToolchainsReader toolchainsReader, + List toolchainDiscoverers) { this.toolchainsWriter = toolchainsWriter; this.toolchainsReader = toolchainsReader; + this.toolchainDiscoverers = toolchainDiscoverers; } @Override public ToolchainsBuildingResult build(ToolchainsBuildingRequest request) throws ToolchainsBuildingException { ProblemCollector problems = ProblemCollectorFactory.newInstance(null); + PersistedToolchains discoveredToolchains = null; + if (toolchainDiscoverers != null) { + for (ToolchainDiscoverer discoverer : toolchainDiscoverers) { + PersistedToolchains toolchains = discoverer.discoverToolchains(); + if (toolchains != null) { + if (discoveredToolchains == null) { + discoveredToolchains = toolchains; + } else { + toolchainsMerger.merge(discoveredToolchains, toolchains, TrackableBase.DISCOVERED_LEVEL); + } + } + } + } + PersistedToolchains globalToolchains = readToolchains(request.getGlobalToolchainsSource(), request, problems); PersistedToolchains userToolchains = readToolchains(request.getUserToolchainsSource(), request, problems); toolchainsMerger.merge(userToolchains, globalToolchains, TrackableBase.GLOBAL_LEVEL); + toolchainsMerger.merge(userToolchains, discoveredToolchains, TrackableBase.DISCOVERED_LEVEL); + problems.setSource(""); userToolchains = interpolate(userToolchains, problems); diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java new file mode 100644 index 000000000000..9828300edae6 --- /dev/null +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java @@ -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. + */ +package org.apache.maven.toolchain.discovery; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.IOException; +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.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.maven.api.toolchain.ToolchainModel; +import org.apache.maven.internal.xml.XmlNodeImpl; +import org.apache.maven.toolchain.model.PersistedToolchains; + +/** + * Implementation of ToolchainDiscoverer service + */ +@Named +@Singleton +public class DefaultToolchainDiscoverer implements ToolchainDiscoverer { + + @Override + public PersistedToolchains discoverToolchains() { + try { + Set jdks = JavaHomeFinder.suggestHomePaths(true); + List tcs = new ArrayList<>(); + for (Path jdk : jdks.stream().map(Paths::get).collect(Collectors.toList())) { + ToolchainModel tc = getToolchainModel(jdk); + if (tc != null) { + tcs.add(tc); + } + } + tcs.sort(getToolchainModelComparator()); + return new PersistedToolchains(org.apache.maven.api.toolchain.PersistedToolchains.newBuilder() + .toolchains(tcs) + .build()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + ToolchainModel getToolchainModel(Path jdk) throws IOException, InterruptedException { + Path bin = jdk.resolve("bin"); + Path java = bin.resolve("java"); + if (!Files.isRegularFile(java)) { + java = bin.resolve("java.exe"); + if (!Files.isRegularFile(java)) { + System.err.println("Unable to find java executable for " + jdk); + return null; + } + } + Path temp = Files.createTempFile("jdk-opts-", ".out"); + new ProcessBuilder() + .command(java.toString(), "-XshowSettings:properties", "-version") + .redirectError(temp.toFile()) + .start() + .waitFor(); + List lines = Files.readAllLines(temp); + Files.delete(temp); + String jdkHome = lines.stream() + .filter(l -> l.contains("java.home")) + .map(l -> l.replaceFirst(".*=\\s*(.*)", "$1")) + .findFirst() + .get(); + + Map properties = new LinkedHashMap<>(); + for (String name : Arrays.asList( + "java.version", "java.runtime.name", "java.runtime.version", "java.vendor", "java.vendor.version")) { + String v = lines.stream() + .filter(l -> l.contains(name)) + .map(l -> l.replaceFirst(".*=\\s*(.*)", "$1")) + .findFirst() + .orElse(null); + String k = name.substring(5); + if (v != null) { + properties.put(k, v); + } + } + return ToolchainModel.newBuilder() + .type("jdk") + .provides(properties) + .configuration(new XmlNodeImpl( + "configuration", + null, + null, + Collections.singletonList(new XmlNodeImpl("jdkHome", jdkHome)), + null)) + .build(); + } + + Comparator getToolchainModelComparator() { + return Comparator.comparing((ToolchainModel tc) -> tc.getProvides().get("vendor")) + .thenComparing(tc -> tc.getProvides().get("version"), this::compareVersion); + } + + int compareVersion(String v1, String v2) { + String[] s1 = v1.split("\\."); + String[] s2 = v2.split("\\."); + return compare(s1, s2); + } + + static > int compare(T[] a, T[] b) { + int length = Math.min(a.length, b.length); + for (int i = 0; i < length; i++) { + T oa = a[i]; + T ob = b[i]; + if (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; + } +} diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinder.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinder.java new file mode 100644 index 000000000000..5af2a0661f54 --- /dev/null +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinder.java @@ -0,0 +1,68 @@ +/* + * 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.toolchain.discovery; + +import java.util.Set; + +import org.codehaus.plexus.util.Os; + +public abstract class JavaHomeFinder { + + static final boolean IS_WINDOWS = Os.isFamily(Os.FAMILY_WINDOWS); + static final boolean IS_MAC = Os.isFamily(Os.FAMILY_MAC); + static final boolean IS_LINUX = Os.OS_NAME.startsWith("linux"); + static final boolean IS_SUNOS = Os.OS_NAME.startsWith("sunos"); + + /** + * Tries to find existing Java SDKs on this computer. + * If no JDK found, returns possible directories to start file chooser. + * @return suggested sdk home paths (sorted) + */ + public static Set suggestHomePaths() { + return suggestHomePaths(false); + } + + /** + * Do the same as {@link #suggestHomePaths()} but always considers the embedded JRE, + * for using in tests that are performed when the registry is not properly initialized + * or that need the embedded JetBrains Runtime. + */ + public static Set suggestHomePaths(boolean forceEmbeddedJava) { + JavaHomeFinderBasic javaFinder = getFinder().checkEmbeddedJava(forceEmbeddedJava); + return javaFinder.findExistingJdks(); + } + + public static JavaHomeFinderBasic getFinder() { + if (IS_WINDOWS) { + return new JavaHomeFinderWindows(true); + } + if (IS_MAC) { + return new JavaHomeFinderMac(); + } + if (IS_LINUX) { + return new JavaHomeFinderBasic().checkSpecifiedPaths(DEFAULT_JAVA_LINUX_PATHS); + } + if (IS_SUNOS) { + return new JavaHomeFinderBasic().checkSpecifiedPaths("/usr/jdk"); + } + return new JavaHomeFinderBasic(); + } + + public static final String[] DEFAULT_JAVA_LINUX_PATHS = {"/usr/java", "/opt/java", "/usr/lib/jvm"}; +} diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java new file mode 100644 index 000000000000..090e70e822f7 --- /dev/null +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java @@ -0,0 +1,410 @@ +/* + * 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.toolchain.discovery; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JavaHomeFinderBasic { + protected final Logger log = LoggerFactory.getLogger(getClass()); + private final List>> myFinders = new ArrayList<>(); + + private boolean myCheckEmbeddedJava = false; + + private String[] mySpecifiedPaths = new String[0]; + + public JavaHomeFinderBasic() { + myFinders.add(this::findInPATH); + myFinders.add(this::findInJavaHome); + myFinders.add(this::findInSpecifiedPaths); + myFinders.add(this::findJavaInstalledBySdkMan); + myFinders.add(this::findJavaInstalledByAsdfJava); + myFinders.add(this::findJavaInstalledByGradle); + + myFinders.add(() -> myCheckEmbeddedJava ? scanAll(getJavaHome(), false) : Collections.emptySet()); + } + + static String execCommand(String command) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(command); + try (InputStream is = process.getInputStream(); + ByteArrayOutputStream os = new ByteArrayOutputStream()) { + byte[] buffer = new byte[128]; + for (int length = is.read(buffer); length > 0; length = is.read(buffer)) { + os.write(buffer, 0, length); + } + return new String(os.toByteArray(), StandardCharsets.UTF_8); + } finally { + process.waitFor(); + } + } + + public JavaHomeFinderBasic checkEmbeddedJava(boolean value) { + myCheckEmbeddedJava = value; + return this; + } + + public JavaHomeFinderBasic checkSpecifiedPaths(String... paths) { + mySpecifiedPaths = paths; + return this; + } + + private Set findInSpecifiedPaths() { + List paths = Stream.of(mySpecifiedPaths).map(Paths::get).collect(Collectors.toList()); + return scanAll(paths, true); + } + + protected void registerFinder(Supplier> finder) { + myFinders.add(finder); + } + + public final Set findExistingJdks() { + Set result = new TreeSet<>(); + + for (Supplier> action : myFinders) { + try { + result.addAll(action.get()); + } catch (Exception e) { + log.warn("Failed to find Java Home. " + e.getMessage(), e); + } + } + + return result; + } + + private Set findInJavaHome() { + String javaHome = getEnvironmentVariable("JAVA_HOME"); + return javaHome != null ? scanAll(Paths.get(javaHome), false) : Collections.emptySet(); + } + + private Set findInPATH() { + try { + String pathVarString = getEnvironmentVariable("PATH"); + if (pathVarString == null || pathVarString.isEmpty()) { + return Collections.emptySet(); + } + + Set dirsToCheck = new HashSet<>(); + for (String p : pathVarString.split(File.pathSeparator)) { + Path dir = Paths.get(p); + String fileName = dir.getFileName().toString(); + if (!JavaHomeFinder.IS_WINDOWS && !JavaHomeFinder.IS_MAC) { + fileName = fileName.toLowerCase(Locale.ROOT); + } + if (!"bin".equals(fileName)) { + continue; + } + + Path parentFile = dir.getParent(); + if (parentFile == null) { + continue; + } + + dirsToCheck.addAll(listPossibleJdkInstallRootsFromHomes(parentFile)); + } + + return scanAll(dirsToCheck, false); + } catch (Exception e) { + log.warn("Failed to scan PATH for JDKs. " + e.getMessage(), e); + return Collections.emptySet(); + } + } + + protected Set scanAll(Path file, boolean includeNestDirs) { + if (file == null) { + return Collections.emptySet(); + } + return scanAll(Collections.singleton(file), includeNestDirs); + } + + protected Set scanAll(Collection files, boolean includeNestDirs) { + Set result = new HashSet<>(); + for (Path root : new HashSet<>(files)) { + scanFolder(root, includeNestDirs, result); + } + return result; + } + + protected void scanFolder(Path folder, boolean includeNestDirs, Collection result) { + if (checkForJdk(folder)) { + result.add(folder.toAbsolutePath().toString()); + return; + } + + if (!includeNestDirs) { + return; + } + try (Stream files = Files.list(folder)) { + files.forEach(candidate -> { + for (Path adjusted : listPossibleJdkHomesFromInstallRoot(candidate)) { + scanFolder(adjusted, false, result); + } + }); + } catch (IOException ignore) { + } + } + + protected List listPossibleJdkHomesFromInstallRoot(Path path) { + return Collections.singletonList(path); + } + + protected List listPossibleJdkInstallRootsFromHomes(Path file) { + return Collections.singletonList(file); + } + + private static Path getJavaHome() { + Path javaHome = Paths.get(System.getProperty("java.home")); + return Files.isDirectory(javaHome) ? javaHome : null; + } + + /** + * Finds Java home directories installed by SDKMAN + */ + private Set findJavaInstalledBySdkMan() { + try { + Path candidatesDir = findSdkManCandidatesDir(); + if (candidatesDir == null) { + return Collections.emptySet(); + } + Path javasDir = candidatesDir.resolve("java"); + if (!Files.isDirectory(javasDir)) { + return Collections.emptySet(); + } + return listJavaHomeDirsInstalledBySdkMan(javasDir); + } catch (Exception e) { + log.warn( + "Unexpected exception while looking for Sdkman directory: " + + e.getClass().getSimpleName() + ": " + e.getMessage(), + e); + return Collections.emptySet(); + } + } + + private Set findJavaInstalledByGradle() { + Path jdks = getPathInUserHome(".gradle/jdks"); + return jdks != null && Files.isDirectory(jdks) ? scanAll(jdks, true) : Collections.emptySet(); + } + + private Path findSdkManCandidatesDir() { + // first, try the special environment variable + String candidatesPath = getEnvironmentVariable("SDKMAN_CANDIDATES_DIR"); + if (candidatesPath != null) { + Path candidatesDir = Paths.get(candidatesPath); + if (Files.isDirectory(candidatesDir)) { + return candidatesDir; + } + } + + // then, try to use its 'primary' variable + String primaryPath = getEnvironmentVariable("SDKMAN_DIR"); + if (primaryPath != null) { + Path candidatesDir = Paths.get(primaryPath, "candidates"); + if (Files.isDirectory(candidatesDir)) { + return candidatesDir; + } + } + + // finally, try the usual location in UNIX + if (!(this instanceof JavaHomeFinderWindows)) { + Path candidates = getPathInUserHome(".sdkman/candidates"); + if (candidates != null && Files.isDirectory(candidates)) { + return candidates; + } + } + + // no chances + return null; + } + + protected String getEnvironmentVariable(String name) { + // TODO: + // https://github.com/JetBrains/intellij-community/blob/cc98eb4ac9cdc9bff55384f1c880d0d82e8b05fd/platform/util/src/com/intellij/util/EnvironmentUtil.java#L72C4-L81 + return System.getenv(name); + } + + protected Path getPathInUserHome(String relativePath) { + Path userHome = Paths.get(System.getProperty("user.home")); + return userHome.resolve(relativePath); + } + + private Set listJavaHomeDirsInstalledBySdkMan(Path javasDir) { + boolean mac = this instanceof JavaHomeFinderMac; + HashSet result = new HashSet<>(); + + try (Stream stream = Files.list(javasDir)) { + List innerDirectories = stream.filter(Files::isDirectory).collect(Collectors.toList()); + for (Path innerDir : innerDirectories) { + Path home = innerDir; + Path releaseFile = home.resolve("release"); + if (!safeExists(releaseFile)) { + continue; + } + + if (mac) { + // Zulu JDK on macOS has a rogue layout, with which Gradle failed to operate (see the bugreport + // IDEA-253051), + // and in order to get Gradle working with Zulu JDK we should use it's second home (when symbolic + // links are resolved). + try { + if (Files.isSymbolicLink(releaseFile)) { + Path realReleaseFile = releaseFile.toRealPath(); + if (!safeExists(realReleaseFile)) { + log.warn("Failed to resolve the target file (it doesn't exist) for: " + releaseFile); + continue; + } + Path realHome = realReleaseFile.getParent(); + if (realHome == null) { + log.warn( + "Failed to resolve the target file (it has no parent dir) for: " + releaseFile); + continue; + } + home = realHome; + } + } catch (IOException ioe) { + log.warn("Failed to resolve the target file for: " + releaseFile + ": " + ioe.getMessage()); + continue; + } catch (Exception e) { + log.warn("Failed to resolve the target file for: " + releaseFile + ": Unexpected exception " + + e.getClass().getSimpleName() + ": " + e.getMessage()); + continue; + } + } + + result.add(home.toString()); + } + } catch (IOException ioe) { + log.warn("I/O exception while listing Java home directories installed by Sdkman: " + ioe.getMessage(), ioe); + return Collections.emptySet(); + } catch (Exception e) { + log.warn( + "Unexpected exception while listing Java home directories installed by Sdkman: " + + e.getClass().getSimpleName() + + ": " + + e.getMessage(), + e); + return Collections.emptySet(); + } + + return result; + } + + /** + * Finds Java home directories installed by asdf-java + */ + private Set findJavaInstalledByAsdfJava() { + Path installsDir = findAsdfInstallsDir(); + if (installsDir == null) { + return Collections.emptySet(); + } + Path javasDir = installsDir.resolve("java"); + return safeIsDirectory(javasDir) ? scanAll(javasDir, true) : Collections.emptySet(); + } + + private Path findAsdfInstallsDir() { + // try to use environment variable for custom data directory + // https://asdf-vm.com/#/core-configuration?id=environment-variables + String dataDir = getEnvironmentVariable("ASDF_DATA_DIR"); + if (dataDir != null) { + Path primaryDir = Paths.get(dataDir); + if (safeIsDirectory(primaryDir)) { + Path installsDir = primaryDir.resolve("installs"); + if (safeIsDirectory(installsDir)) { + return installsDir; + } + } + } + + // finally, try the usual location in Unix or macOS + if (!(this instanceof JavaHomeFinderWindows)) { + Path installsDir = getPathInUserHome(".asdf/installs"); + if (installsDir != null && safeIsDirectory(installsDir)) { + return installsDir; + } + } + + // no chances + return null; + } + + private boolean safeIsDirectory(Path dir) { + try { + return Files.isDirectory(dir); + } catch (SecurityException se) { + return false; // when a directory is not accessible we should ignore it + } catch (Exception e) { + log.debug( + "Failed to check directory existence: unexpected exception " + + e.getClass().getSimpleName() + ": " + e.getMessage(), + e); + return false; + } + } + + private boolean safeExists(Path path) { + try { + return Files.exists(path); + } catch (Exception e) { + log.debug( + "Failed to check file existence: unexpected exception " + + e.getClass().getSimpleName() + ": " + e.getMessage(), + e); + return false; + } + } + + public static boolean checkForJdk(Path homePath) { + return (Files.exists(homePath.resolve("bin/javac")) || Files.exists(homePath.resolve("bin/javac.exe"))) + && (isModularRuntime(homePath) + || // Jigsaw JDK/JRE + Files.exists(homePath.resolve("jre/lib/rt.jar")) + || // pre-modular JDK + Files.isDirectory(homePath.resolve("classes")) + || // custom build + Files.exists(homePath.resolve("jre/lib/vm.jar")) + || // IBM JDK + Files.exists(homePath.resolve("../Classes/classes.jar"))); // Apple JDK + } + + public static boolean isModularRuntime(Path homePath) { + return Files.isRegularFile(homePath.resolve("lib/jrt-fs.jar")) || isExplodedModularRuntime(homePath); + } + + public static boolean isExplodedModularRuntime(Path homePath) { + return Files.isDirectory(homePath.resolve("modules/java.base")); + } +} diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderMac.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderMac.java new file mode 100644 index 000000000000..f16c05848237 --- /dev/null +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderMac.java @@ -0,0 +1,102 @@ +/* + * 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.toolchain.discovery; + +import java.io.File; +import java.nio.file.FileSystems; +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.List; +import java.util.Set; +import java.util.TreeSet; + +public class JavaHomeFinderMac extends JavaHomeFinderBasic { + public static final String JAVA_HOME_FIND_UTIL = "/usr/libexec/java_home"; + + static String defaultJavaLocation = "/Library/Java/JavaVirtualMachines"; + + public JavaHomeFinderMac() { + super(); + + registerFinder(() -> { + Set result = new TreeSet<>(); + Iterable roots = FileSystems.getDefault().getRootDirectories(); + roots.forEach(root -> { + result.addAll(scanAll(root.resolve(defaultJavaLocation), true)); + }); + roots.forEach(root -> { + result.addAll(scanAll(root.resolve("System/Library/Java/JavaVirtualMachines"), true)); + }); + return result; + }); + + registerFinder(() -> { + Path jdk = getPathInUserHome("Library/Java/JavaVirtualMachines"); + return jdk != null ? scanAll(jdk, true) : Collections.emptySet(); + }); + registerFinder(() -> scanAll(getSystemDefaultJavaHome(), false)); + } + + protected Path getSystemDefaultJavaHome() { + String homePath = null; + if (new File(JAVA_HOME_FIND_UTIL).canExecute()) { + try { + homePath = execCommand(JAVA_HOME_FIND_UTIL); + } catch (Exception e) { + // TODO: log ? + } + } + if (homePath != null) { + return Paths.get(homePath); + } + return null; + } + + @Override + protected List listPossibleJdkHomesFromInstallRoot(Path path) { + return Arrays.asList(path, path.resolve("/Home"), path.resolve("Contents/Home")); + } + + @Override + protected List listPossibleJdkInstallRootsFromHomes(Path file) { + List result = new ArrayList<>(); + result.add(file); + + Path home = file.getFileName(); + if (home != null && home.toString().equalsIgnoreCase("Home")) { + Path parentFile = file.getParent(); + if (parentFile != null) { + result.add(parentFile); + + Path contents = parentFile.getFileName(); + if (contents != null && contents.toString().equalsIgnoreCase("Contents")) { + Path parentParentFile = parentFile.getParent(); + if (parentParentFile != null) { + result.add(parentParentFile); + } + } + } + } + + return result; + } +} diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderWindows.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderWindows.java new file mode 100644 index 000000000000..12aa6044d30c --- /dev/null +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderWindows.java @@ -0,0 +1,105 @@ +/* + * 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.toolchain.discovery; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Named("windows") +@Singleton +class JavaHomeFinderWindows extends JavaHomeFinderBasic { + + private static final String REG_COMMAND = "reg query HKLM\\SOFTWARE\\JavaSoft\\JDK /s /v JavaHome"; + + private static final Pattern JAVA_HOME_PATTERN = + Pattern.compile("^\\s+JavaHome\\s+REG_SZ\\s+(\\S.+\\S)\\s*$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); + + private static Set gatherHomePaths(CharSequence text) { + Set paths = new TreeSet<>(); + Matcher m = JAVA_HOME_PATTERN.matcher(text); + while (m.find()) { + paths.add(m.group(1)); + } + return paths; + } + + JavaHomeFinderWindows(boolean registeredJdks) { + if (registeredJdks) { + // Whether the OS is 64-bit (**important**: it's not the same as [com.intellij.util.system.CpuArch]). + String pfx86 = getEnvironmentVariable("ProgramFiles(x86)"); + boolean os64bit = pfx86 != null && !pfx86.trim().isEmpty(); + if (os64bit) { + registerFinder(() -> readRegisteredLocations(" /reg:64")); + registerFinder(() -> readRegisteredLocations(" /reg:32")); + } else { + registerFinder(() -> readRegisteredLocations("")); + } + } + registerFinder(this::guessPossibleLocations); + } + + private Set readRegisteredLocations(String bitness) { + String cmd = REG_COMMAND + bitness; + try { + CharSequence registryLines = execCommand(cmd); + Set registeredPaths = gatherHomePaths(registryLines); + Set folders = new TreeSet<>(); + for (String rp : registeredPaths) { + Path r = Paths.get(rp); + Path parent = r.getParent(); + if (parent != null && Files.exists(parent)) { + folders.add(parent); + } else if (Files.exists(r)) { + folders.add(r); + } + } + return scanAll(folders, true); + } catch (InterruptedException ie) { + return Collections.emptySet(); + } catch (Exception e) { + log.warn("Unable to detect registered JDK using the following command: $cmd", e); + return Collections.emptySet(); + } + } + + private Set guessPossibleLocations() { + Iterable fsRoots = FileSystems.getDefault().getRootDirectories(); + Set roots = new HashSet<>(); + for (Path root : fsRoots) { + if (Files.exists(root)) { + roots.add(root.resolve("Program Files/Java")); + roots.add(root.resolve("Program Files (x86)/Java")); + roots.add(root.resolve("Java")); + } + } + Path puh = getPathInUserHome(".jdks"); + if (puh != null) { + roots.add(puh); + } + return scanAll(roots, true); + } +} diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java new file mode 100644 index 000000000000..89421c714f9b --- /dev/null +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java @@ -0,0 +1,28 @@ +/* + * 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.toolchain.discovery; + +import org.apache.maven.toolchain.model.PersistedToolchains; + +/** + * Service used to discover JDK toolchains + */ +public interface ToolchainDiscoverer { + PersistedToolchains discoverToolchains(); +} diff --git a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/discovery/ToolchainDiscovererTest.java b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/discovery/ToolchainDiscovererTest.java new file mode 100644 index 000000000000..2104cd676469 --- /dev/null +++ b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/discovery/ToolchainDiscovererTest.java @@ -0,0 +1,37 @@ +/* + * 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.toolchain.discovery; + +import java.io.IOException; +import java.io.StringWriter; + +import org.apache.maven.toolchain.io.DefaultToolchainsWriter; +import org.apache.maven.toolchain.model.PersistedToolchains; +import org.junit.jupiter.api.Test; + +class ToolchainDiscovererTest { + + @Test + void testDiscovery() throws IOException { + PersistedToolchains pt = new DefaultToolchainDiscoverer().discoverToolchains(); + StringWriter writer = new StringWriter(); + new DefaultToolchainsWriter().write(writer, null, pt); + System.err.println(writer); + } +} From 16a4b4b0c1c761801b251b2f9b8712bc446ea204 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:49:29 +0200 Subject: [PATCH 02/11] Simplify code --- .../DefaultMavenExecutionRequestPopulator.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequestPopulator.java b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequestPopulator.java index aee374816a19..7cd3cb09625e 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequestPopulator.java +++ b/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequestPopulator.java @@ -23,11 +23,9 @@ import javax.inject.Singleton; import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.apache.maven.artifact.InvalidRepositoryException; import org.apache.maven.artifact.repository.ArtifactRepository; @@ -60,17 +58,8 @@ public DefaultMavenExecutionRequestPopulator(MavenRepositorySystem repositorySys public MavenExecutionRequest populateFromToolchains(MavenExecutionRequest request, PersistedToolchains toolchains) throws MavenExecutionRequestPopulationException { if (toolchains != null) { - Map> groupedToolchains = new HashMap<>(2); - - for (ToolchainModel model : toolchains.getToolchains()) { - if (!groupedToolchains.containsKey(model.getType())) { - groupedToolchains.put(model.getType(), new ArrayList<>()); - } - - groupedToolchains.get(model.getType()).add(model); - } - - request.setToolchains(groupedToolchains); + request.setToolchains( + toolchains.getToolchains().stream().collect(Collectors.groupingBy(ToolchainModel::getType))); } return request; } From 24420c647381381e855ee4eae30e4feb49449c88 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:50:04 +0200 Subject: [PATCH 03/11] Add cache and compute in parallel --- .../discovery/DefaultToolchainDiscoverer.java | 173 ++++++++++++++---- .../discovery/JavaHomeFinderBasic.java | 22 +-- 2 files changed, 149 insertions(+), 46 deletions(-) diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java index 9828300edae6..03b648f9bac8 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java @@ -22,6 +22,8 @@ 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; @@ -32,12 +34,21 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.maven.api.toolchain.ToolchainModel; import org.apache.maven.internal.xml.XmlNodeImpl; import org.apache.maven.toolchain.model.PersistedToolchains; +import org.apache.maven.toolchain.model.ToolchainModel; +import org.apache.maven.toolchain.v4.MavenToolchainsXpp3Reader; +import org.apache.maven.toolchain.v4.MavenToolchainsXpp3Writer; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Implementation of ToolchainDiscoverer service @@ -46,49 +57,130 @@ @Singleton public class DefaultToolchainDiscoverer implements ToolchainDiscoverer { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultToolchainDiscoverer.class); + + private Map cache; + private boolean cacheModified; + @Override public PersistedToolchains discoverToolchains() { try { Set jdks = JavaHomeFinder.suggestHomePaths(true); - List tcs = new ArrayList<>(); - for (Path jdk : jdks.stream().map(Paths::get).collect(Collectors.toList())) { - ToolchainModel tc = getToolchainModel(jdk); - if (tc != null) { - tcs.add(tc); - } + LOGGER.info("Found {} possible jdks: {}", jdks.size(), jdks); + cacheModified = false; + readCache(); + Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty("java.home"))); + List tcs = jdks.parallelStream() + .map(s -> getToolchainModel(currentJdkHome, s)) + .filter(Objects::nonNull) + .sorted(getToolchainModelComparator()) + .collect(Collectors.toList()); + if (this.cacheModified) { + writeCache(); } - tcs.sort(getToolchainModelComparator()); - return new PersistedToolchains(org.apache.maven.api.toolchain.PersistedToolchains.newBuilder() - .toolchains(tcs) - .build()); + PersistedToolchains ps = new PersistedToolchains(); + ps.setToolchains(tcs); + return ps; } catch (Exception e) { - throw new RuntimeException(e); + if (LOGGER.isDebugEnabled()) { + LOGGER.warn("Error discovering toolchains: " + e, e); + } else { + LOGGER.warn("Error discovering toolchains (enable debug level for more information): " + e); + } + return new PersistedToolchains(); } } - ToolchainModel getToolchainModel(Path jdk) throws IOException, InterruptedException { + private void readCache() { + cache = new ConcurrentHashMap<>(); + Path cacheFile = + Paths.get(System.getProperty("user.home")).resolve(".m2").resolve("toolchains-cache.xml"); + try { + if (Files.isRegularFile(cacheFile)) { + try (Reader r = Files.newBufferedReader(cacheFile)) { + PersistedToolchains pt = new PersistedToolchains(new MavenToolchainsXpp3Reader().read(r, false)); + cache = pt.getToolchains().stream() + .collect(Collectors.toConcurrentMap(this::getJdkHome, Function.identity())); + } + } + } catch (IOException | XmlPullParserException e) { + LOGGER.warn("Error reading toolchains cache: " + e); + } + } + + private void writeCache() { + Path cacheFile = Paths.get(System.getProperty("user.home")).resolve(".m2/toolchains-cache.xml"); + try { + Files.createDirectories(cacheFile.getParent()); + try (Writer w = Files.newBufferedWriter(cacheFile)) { + PersistedToolchains pt = new PersistedToolchains(); + List toolchains = new ArrayList<>(); + for (ToolchainModel tc : cache.values()) { + tc = new ToolchainModel(tc.getDelegate()); + tc.getProvides().remove("current"); + toolchains.add(tc); + } + pt.setToolchains(toolchains); + new MavenToolchainsXpp3Writer().write(w, pt.getDelegate()); + } + } catch (IOException e) { + LOGGER.warn("Error writing toolchains cache: " + e); + } + } + + private Path getJdkHome(ToolchainModel toolchain) { + Xpp3Dom dom = (Xpp3Dom) toolchain.getConfiguration(); + Xpp3Dom javahome = dom != null ? dom.getChild("jdkHome") : null; + String jdk = javahome != null ? javahome.getValue() : null; + return Paths.get(Objects.requireNonNull(jdk)); + } + + ToolchainModel getToolchainModel(Path currentJdkHome, String jdkPath) { + LOGGER.debug("Computing model for " + jdkPath); + Path jdk = getCanonicalPath(Paths.get(jdkPath)); + + ToolchainModel model = cache.get(jdk); + if (model == null) { + model = doGetToolchainModel(jdk); + cache.put(jdk, model); + cacheModified = true; + } + + if (Objects.equals(jdk, currentJdkHome) + || currentJdkHome.getFileName().toString().equals("jre") + && Objects.equals(jdk, currentJdkHome.getParent())) { + model.getProvides().setProperty("current", "true"); + } + return model; + } + + ToolchainModel doGetToolchainModel(Path jdk) { Path bin = jdk.resolve("bin"); Path java = bin.resolve("java"); - if (!Files.isRegularFile(java)) { + if (!java.toFile().canExecute()) { java = bin.resolve("java.exe"); - if (!Files.isRegularFile(java)) { - System.err.println("Unable to find java executable for " + jdk); + if (!java.toFile().canExecute()) { + LOGGER.debug("JDK toolchain discovered at {} will be ignored: unable to find java executable", jdk); return null; } } - Path temp = Files.createTempFile("jdk-opts-", ".out"); - new ProcessBuilder() - .command(java.toString(), "-XshowSettings:properties", "-version") - .redirectError(temp.toFile()) - .start() - .waitFor(); - List lines = Files.readAllLines(temp); - Files.delete(temp); - String jdkHome = lines.stream() - .filter(l -> l.contains("java.home")) - .map(l -> l.replaceFirst(".*=\\s*(.*)", "$1")) - .findFirst() - .get(); + 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) { + LOGGER.debug("JDK toolchain discovered at {} will be ignored: unable to execute java: " + e, jdk); + return null; + } Map properties = new LinkedHashMap<>(); for (String name : Arrays.asList( @@ -103,21 +195,34 @@ ToolchainModel getToolchainModel(Path jdk) throws IOException, InterruptedExcept properties.put(k, v); } } - return ToolchainModel.newBuilder() + if (!properties.containsKey("version")) { + LOGGER.debug("JDK toolchain discovered at {} will be ignored: could not obtain java.version", jdk); + return null; + } + + return new ToolchainModel(org.apache.maven.api.toolchain.ToolchainModel.newBuilder() .type("jdk") .provides(properties) .configuration(new XmlNodeImpl( "configuration", null, null, - Collections.singletonList(new XmlNodeImpl("jdkHome", jdkHome)), + Collections.singletonList(new XmlNodeImpl("jdkHome", jdk.toString())), null)) - .build(); + .build()); + } + + private static Path getCanonicalPath(Path path) { + try { + return path.toRealPath(); + } catch (IOException e) { + return getCanonicalPath(path.getParent()).resolve(path.getFileName()); + } } Comparator getToolchainModelComparator() { - return Comparator.comparing((ToolchainModel tc) -> tc.getProvides().get("vendor")) - .thenComparing(tc -> tc.getProvides().get("version"), this::compareVersion); + return Comparator.comparing((ToolchainModel tc) -> tc.getProvides().getProperty("vendor")) + .thenComparing(tc -> tc.getProvides().getProperty("version"), this::compareVersion); } int compareVersion(String v1, String v2) { diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java index 090e70e822f7..65be17cfa19c 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/JavaHomeFinderBasic.java @@ -33,7 +33,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.TreeSet; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -94,17 +93,16 @@ protected void registerFinder(Supplier> finder) { } public final Set findExistingJdks() { - Set result = new TreeSet<>(); - - for (Supplier> action : myFinders) { - try { - result.addAll(action.get()); - } catch (Exception e) { - log.warn("Failed to find Java Home. " + e.getMessage(), e); - } - } - - return result; + return myFinders.parallelStream() + .flatMap(finder -> { + try { + return finder.get().stream(); + } catch (Exception e) { + log.warn("Failed to find Java Home. " + e.getMessage(), e); + return Stream.empty(); + } + }) + .collect(Collectors.toSet()); } private Set findInJavaHome() { From 01293cf3e38845a62ad2d48a17d3b8ff2baa5e3c Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:50:34 +0200 Subject: [PATCH 04/11] Ensure non null arguments and simplify code --- .../building/DefaultToolchainsBuilder.java | 23 +++++++------------ .../DefaultToolchainsBuilderTest.java | 6 +++++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java index e4d4e422db6f..151767617373 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import org.apache.maven.building.Problem; import org.apache.maven.building.ProblemCollector; @@ -62,27 +63,19 @@ public DefaultToolchainsBuilder( ToolchainsWriter toolchainsWriter, ToolchainsReader toolchainsReader, List toolchainDiscoverers) { - this.toolchainsWriter = toolchainsWriter; - this.toolchainsReader = toolchainsReader; - this.toolchainDiscoverers = toolchainDiscoverers; + this.toolchainsWriter = Objects.requireNonNull(toolchainsWriter); + this.toolchainsReader = Objects.requireNonNull(toolchainsReader); + this.toolchainDiscoverers = Objects.requireNonNull(toolchainDiscoverers); } @Override public ToolchainsBuildingResult build(ToolchainsBuildingRequest request) throws ToolchainsBuildingException { ProblemCollector problems = ProblemCollectorFactory.newInstance(null); - PersistedToolchains discoveredToolchains = null; - if (toolchainDiscoverers != null) { - for (ToolchainDiscoverer discoverer : toolchainDiscoverers) { - PersistedToolchains toolchains = discoverer.discoverToolchains(); - if (toolchains != null) { - if (discoveredToolchains == null) { - discoveredToolchains = toolchains; - } else { - toolchainsMerger.merge(discoveredToolchains, toolchains, TrackableBase.DISCOVERED_LEVEL); - } - } - } + PersistedToolchains discoveredToolchains = new PersistedToolchains(); + for (ToolchainDiscoverer discoverer : toolchainDiscoverers) { + PersistedToolchains toolchains = discoverer.discoverToolchains(); + toolchainsMerger.merge(discoveredToolchains, toolchains, TrackableBase.DISCOVERED_LEVEL); } PersistedToolchains globalToolchains = readToolchains(request.getGlobalToolchainsSource(), request, problems); diff --git a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java index 80e44ec6c8cc..cc926b0183af 100644 --- a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java +++ b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java @@ -20,12 +20,15 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.maven.building.StringSource; +import org.apache.maven.toolchain.discovery.ToolchainDiscoverer; import org.apache.maven.toolchain.io.DefaultToolchainsReader; import org.apache.maven.toolchain.io.DefaultToolchainsWriter; import org.apache.maven.toolchain.io.ToolchainsParseException; @@ -55,6 +58,9 @@ class DefaultToolchainsBuilderTest { @Spy private DefaultToolchainsWriter toolchainsWriter; + @Spy + private List toolchainDiscoverers = new ArrayList<>(); + @InjectMocks private DefaultToolchainsBuilder toolchainBuilder; From 642c67c19f6b53d6fd4f363270722564e01666ee Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:51:30 +0200 Subject: [PATCH 05/11] Use Path in Toolchain#findTool, allow wrapping JavaToolchain --- .../java/org/apache/maven/api/Toolchain.java | 3 ++- .../impl/DefaultToolchainManager.java | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Toolchain.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Toolchain.java index 053f23065185..4d56a21a6d4c 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Toolchain.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Toolchain.java @@ -18,6 +18,7 @@ */ package org.apache.maven.api; +import java.nio.file.Path; import java.util.Map; import org.apache.maven.api.annotations.Experimental; @@ -42,7 +43,7 @@ public interface Toolchain { * @param toolName the tool platform independent tool name * @return file representing the tool executable, or null if the tool cannot be found */ - String findTool(String toolName); + Path findTool(String toolName); /** * Let the toolchain decide if it matches requirements defined diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultToolchainManager.java b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultToolchainManager.java index 2ccbbba29db5..10e9dfc86cba 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultToolchainManager.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultToolchainManager.java @@ -22,11 +22,14 @@ import javax.inject.Named; import javax.inject.Singleton; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.maven.api.JavaToolchain; import org.apache.maven.api.Session; import org.apache.maven.api.Toolchain; import org.apache.maven.api.services.ToolchainManager; @@ -35,6 +38,7 @@ import org.apache.maven.toolchain.DefaultToolchainManagerPrivate; import org.apache.maven.toolchain.MisconfiguredToolchainException; import org.apache.maven.toolchain.ToolchainPrivate; +import org.apache.maven.toolchain.java.JavaToolchainImpl; @Named @Singleton @@ -70,7 +74,7 @@ public List getToolchainsForType(Session session, String type) throws ToolchainPrivate[] toolchains = toolchainManagerPrivate.getToolchainsForType(type, s); return new MappedList<>(Arrays.asList(toolchains), this::toToolchain); } catch (MisconfiguredToolchainException e) { - throw new ToolchainManagerException("Unable to get toochains for type " + type, e); + throw new ToolchainManagerException("Unable to get toolchains for type " + type, e); } } @@ -83,11 +87,13 @@ public void storeToolchainToBuildContext(Session session, Toolchain toolchain) t } private Toolchain toToolchain(org.apache.maven.toolchain.Toolchain toolchain) { - return new ToolchainWrapper(toolchain); + return toolchain instanceof JavaToolchainImpl + ? new JavaToolchainWrapper((JavaToolchainImpl) toolchain) + : new ToolchainWrapper(toolchain); } private static class ToolchainWrapper implements Toolchain { - private final org.apache.maven.toolchain.Toolchain toolchain; + protected final org.apache.maven.toolchain.Toolchain toolchain; ToolchainWrapper(org.apache.maven.toolchain.Toolchain toolchain) { this.toolchain = toolchain; @@ -99,8 +105,8 @@ public String getType() { } @Override - public String findTool(String toolName) { - return toolchain.findTool(toolName); + public Path findTool(String toolName) { + return Paths.get(toolchain.findTool(toolName)); } @Override @@ -108,4 +114,15 @@ public boolean matchesRequirements(Map requirements) { return ((ToolchainPrivate) toolchain).matchesRequirements(requirements); } } + + private static class JavaToolchainWrapper extends ToolchainWrapper implements JavaToolchain { + JavaToolchainWrapper(org.apache.maven.toolchain.java.JavaToolchainImpl toolchain) { + super(toolchain); + } + + @Override + public String getJavaHome() { + return ((org.apache.maven.toolchain.java.JavaToolchainImpl) toolchain).getJavaHome(); + } + } } From dd58fc068d6b51584c84eff8db6c386fa10bc89e Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:51:40 +0200 Subject: [PATCH 06/11] Add javadoc --- .../maven/toolchain/discovery/ToolchainDiscoverer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java index 89421c714f9b..c30b13b43f47 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/ToolchainDiscoverer.java @@ -24,5 +24,10 @@ * Service used to discover JDK toolchains */ public interface ToolchainDiscoverer { + + /** + * Returns a PersistedToolchains object containing a list of discovered toolchains, + * never null. + */ PersistedToolchains discoverToolchains(); } From ea97c755d9b6c0ec8fda765e653bf6699abbd634 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:51:53 +0200 Subject: [PATCH 07/11] Add logging framework for tests --- maven-toolchain-builder/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/maven-toolchain-builder/pom.xml b/maven-toolchain-builder/pom.xml index 20b9d89bae41..c39cd991198a 100644 --- a/maven-toolchain-builder/pom.xml +++ b/maven-toolchain-builder/pom.xml @@ -60,6 +60,11 @@ under the License. mockito-core test + + org.slf4j + slf4j-simple + test + From b5cb1a6817c7caa4c654ad03ef4cd79a480b36e3 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 29 May 2023 20:52:02 +0200 Subject: [PATCH 08/11] Add constant for JDK type --- .../java/org/apache/maven/api/services/ToolchainManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ToolchainManager.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ToolchainManager.java index e428f819a4f9..278ff11a326a 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ToolchainManager.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ToolchainManager.java @@ -36,6 +36,11 @@ @Experimental public interface ToolchainManager extends Service { + /** + * The type identifying JDK toolchains + */ + String TYPE_JDK = "jdk"; + /** * * @param session From 15e85e99a91d44aab2b1068a478b79957bac7156 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 30 May 2023 09:06:55 +0200 Subject: [PATCH 09/11] Cleanup --- .../discovery/DefaultToolchainDiscoverer.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java index 03b648f9bac8..28b857988dcd 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/discovery/DefaultToolchainDiscoverer.java @@ -58,6 +58,7 @@ public class DefaultToolchainDiscoverer implements ToolchainDiscoverer { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultToolchainDiscoverer.class); + private static final String DISCOVERED_TOOLCHAINS_CACHE_XML = ".m2/discovered-toolchains-cache.xml"; private Map cache; private boolean cacheModified; @@ -92,10 +93,9 @@ public PersistedToolchains discoverToolchains() { } private void readCache() { - cache = new ConcurrentHashMap<>(); - Path cacheFile = - Paths.get(System.getProperty("user.home")).resolve(".m2").resolve("toolchains-cache.xml"); try { + cache = new ConcurrentHashMap<>(); + Path cacheFile = getCacheFile(); if (Files.isRegularFile(cacheFile)) { try (Reader r = Files.newBufferedReader(cacheFile)) { PersistedToolchains pt = new PersistedToolchains(new MavenToolchainsXpp3Reader().read(r, false)); @@ -109,8 +109,8 @@ private void readCache() { } private void writeCache() { - Path cacheFile = Paths.get(System.getProperty("user.home")).resolve(".m2/toolchains-cache.xml"); try { + Path cacheFile = getCacheFile(); Files.createDirectories(cacheFile.getParent()); try (Writer w = Files.newBufferedWriter(cacheFile)) { PersistedToolchains pt = new PersistedToolchains(); @@ -128,6 +128,10 @@ private void writeCache() { } } + private static Path getCacheFile() { + return Paths.get(System.getProperty("user.home")).resolve(DISCOVERED_TOOLCHAINS_CACHE_XML); + } + private Path getJdkHome(ToolchainModel toolchain) { Xpp3Dom dom = (Xpp3Dom) toolchain.getConfiguration(); Xpp3Dom javahome = dom != null ? dom.getChild("jdkHome") : null; From 1d3d305b268116f114dced49578a10f5afb81ebd Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 30 May 2023 09:07:10 +0200 Subject: [PATCH 10/11] Fix possible NPE --- .../maven/toolchain/DefaultToolchain.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/maven-core/src/main/java/org/apache/maven/toolchain/DefaultToolchain.java b/maven-core/src/main/java/org/apache/maven/toolchain/DefaultToolchain.java index 8453b4e7af95..bc8a87d513b0 100644 --- a/maven-core/src/main/java/org/apache/maven/toolchain/DefaultToolchain.java +++ b/maven-core/src/main/java/org/apache/maven/toolchain/DefaultToolchain.java @@ -83,18 +83,20 @@ public final void addProvideToken(String type, RequirementMatcher matcher) { @Override public boolean matchesRequirements(Map requirements) { - for (Map.Entry requirement : requirements.entrySet()) { - String key = requirement.getKey(); - - RequirementMatcher matcher = provides.get(key); - - if (matcher == null) { - getLog().debug("Toolchain " + this + " is missing required property: " + key); - return false; - } - if (!matcher.matches(requirement.getValue())) { - getLog().debug("Toolchain " + this + " doesn't match required property: " + key); - return false; + if (requirements != null) { + for (Map.Entry requirement : requirements.entrySet()) { + String key = requirement.getKey(); + + RequirementMatcher matcher = provides.get(key); + + if (matcher == null) { + getLog().debug("Toolchain " + this + " is missing required property: " + key); + return false; + } + if (!matcher.matches(requirement.getValue())) { + getLog().debug("Toolchain " + this + " doesn't match required property: " + key); + return false; + } } } return true; From 1034fb72629b1822205867955d9eee71d427d94f Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 30 May 2023 11:54:11 +0200 Subject: [PATCH 11/11] Add a discovery mode config option --- .../java/org/apache/maven/cli/MavenCli.java | 11 +++++++++++ .../building/DefaultToolchainsBuilder.java | 19 ++++++++++--------- .../DefaultToolchainsBuildingRequest.java | 13 +++++++++++++ .../building/ToolchainsBuildingRequest.java | 14 ++++++++++++++ .../DefaultToolchainsBuilderTest.java | 4 ++-- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index 9a2ef7ac4cd3..936b8e4614c1 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -91,6 +91,7 @@ import org.apache.maven.shared.utils.logging.MessageUtils; import org.apache.maven.toolchain.building.DefaultToolchainsBuildingRequest; import org.apache.maven.toolchain.building.ToolchainsBuilder; +import org.apache.maven.toolchain.building.ToolchainsBuildingRequest; import org.apache.maven.toolchain.building.ToolchainsBuildingResult; import org.codehaus.plexus.ContainerConfiguration; import org.codehaus.plexus.DefaultContainerConfiguration; @@ -133,6 +134,8 @@ public class MavenCli { public static final String MULTIMODULE_PROJECT_DIRECTORY = "maven.multiModuleProjectDirectory"; + public static final String TOOLCHAINS_DISCOVERY_MODE = "maven.toolchainsDiscoveryMode"; + public static final String USER_HOME = System.getProperty("user.home"); public static final File USER_MAVEN_CONFIGURATION_HOME = new File(USER_HOME, ".m2"); @@ -1198,6 +1201,14 @@ void toolchains(CliRequest cliRequest) throws Exception { if (userToolchainsFile.isFile()) { toolchainsRequest.setUserToolchainsSource(new FileSource(userToolchainsFile)); } + String discoveryModeStr = cliRequest.getUserProperties().getProperty(TOOLCHAINS_DISCOVERY_MODE); + if (discoveryModeStr == null) { + discoveryModeStr = cliRequest.getSystemProperties().getProperty(TOOLCHAINS_DISCOVERY_MODE); + } + if (discoveryModeStr == null) { + discoveryModeStr = ToolchainsBuildingRequest.DiscoveryMode.IfNoneConfigured.toString(); + } + toolchainsRequest.setDiscoveryMode(ToolchainsBuildingRequest.DiscoveryMode.valueOf(discoveryModeStr)); eventSpyDispatcher.onEvent(toolchainsRequest); diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java index 151767617373..0adf1964ff80 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java @@ -72,19 +72,20 @@ public DefaultToolchainsBuilder( public ToolchainsBuildingResult build(ToolchainsBuildingRequest request) throws ToolchainsBuildingException { ProblemCollector problems = ProblemCollectorFactory.newInstance(null); - PersistedToolchains discoveredToolchains = new PersistedToolchains(); - for (ToolchainDiscoverer discoverer : toolchainDiscoverers) { - PersistedToolchains toolchains = discoverer.discoverToolchains(); - toolchainsMerger.merge(discoveredToolchains, toolchains, TrackableBase.DISCOVERED_LEVEL); - } - - PersistedToolchains globalToolchains = readToolchains(request.getGlobalToolchainsSource(), request, problems); - PersistedToolchains userToolchains = readToolchains(request.getUserToolchainsSource(), request, problems); + PersistedToolchains globalToolchains = readToolchains(request.getGlobalToolchainsSource(), request, problems); toolchainsMerger.merge(userToolchains, globalToolchains, TrackableBase.GLOBAL_LEVEL); - toolchainsMerger.merge(userToolchains, discoveredToolchains, TrackableBase.DISCOVERED_LEVEL); + if (request.getDiscoveryMode() == ToolchainsBuildingRequest.DiscoveryMode.Always + || request.getDiscoveryMode() == ToolchainsBuildingRequest.DiscoveryMode.IfNoneConfigured + && userToolchains.getToolchains().isEmpty()) { + + for (ToolchainDiscoverer discoverer : toolchainDiscoverers) { + PersistedToolchains toolchains = discoverer.discoverToolchains(); + toolchainsMerger.merge(userToolchains, toolchains, TrackableBase.DISCOVERED_LEVEL); + } + } problems.setSource(""); diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuildingRequest.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuildingRequest.java index 4014904ddd84..ef8e7fd125ba 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuildingRequest.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuildingRequest.java @@ -31,6 +31,8 @@ public class DefaultToolchainsBuildingRequest implements ToolchainsBuildingReque private Source userToolchainsSource; + private DiscoveryMode discoveryMode = DiscoveryMode.IfNoneConfigured; + @Override public Source getGlobalToolchainsSource() { return globalToolchainsSource; @@ -52,4 +54,15 @@ public ToolchainsBuildingRequest setUserToolchainsSource(Source userToolchainsSo this.userToolchainsSource = userToolchainsSource; return this; } + + @Override + public DiscoveryMode getDiscoveryMode() { + return discoveryMode; + } + + @Override + public ToolchainsBuildingRequest setDiscoveryMode(DiscoveryMode discoveryMode) { + this.discoveryMode = discoveryMode; + return this; + } } diff --git a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/ToolchainsBuildingRequest.java b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/ToolchainsBuildingRequest.java index 41868cf5ba0d..8e318e590077 100644 --- a/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/ToolchainsBuildingRequest.java +++ b/maven-toolchain-builder/src/main/java/org/apache/maven/toolchain/building/ToolchainsBuildingRequest.java @@ -28,6 +28,16 @@ */ public interface ToolchainsBuildingRequest { + /** Toolchains discovery mode */ + enum DiscoveryMode { + /** Always add discovered toolchains */ + Always, + /** Discover toolchains if none are configured */ + IfNoneConfigured, + /** Never discover toolchains */ + Never + } + /** * Gets the global toolchains source. * @@ -59,4 +69,8 @@ public interface ToolchainsBuildingRequest { * @return This request, never {@code null}. */ ToolchainsBuildingRequest setUserToolchainsSource(Source userToolchainsSource); + + DiscoveryMode getDiscoveryMode(); + + ToolchainsBuildingRequest setDiscoveryMode(DiscoveryMode discoveryMode); } diff --git a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java index cc926b0183af..c5f8f454e29e 100644 --- a/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java +++ b/maven-toolchain-builder/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java @@ -169,8 +169,8 @@ void testBuildRequestWithBothToolchains() throws Exception { PersistedToolchains globalResult = new PersistedToolchains(); globalResult.setToolchains(Collections.singletonList(toolchain)); - doReturn(globalResult) - .doReturn(userResult) + doReturn(userResult) + .doReturn(globalResult) .when(toolchainsReader) .read(any(InputStream.class), ArgumentMatchers.anyMap());