View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.toolchain.jdk;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.IOException;
25  import java.io.Reader;
26  import java.io.Writer;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.Comparator;
34  import java.util.HashMap;
35  import java.util.LinkedHashMap;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.Objects;
40  import java.util.Optional;
41  import java.util.Set;
42  import java.util.concurrent.ConcurrentHashMap;
43  import java.util.function.Function;
44  import java.util.stream.Collectors;
45  import java.util.stream.Stream;
46  
47  import org.apache.maven.toolchain.model.PersistedToolchains;
48  import org.apache.maven.toolchain.model.ToolchainModel;
49  import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Reader;
50  import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Writer;
51  import org.codehaus.plexus.util.xml.Xpp3Dom;
52  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import static java.util.Comparator.comparing;
57  import static org.apache.maven.plugins.toolchain.jdk.SelectJdkToolchainMojo.TOOLCHAIN_TYPE_JDK;
58  
59  /**
60   * Toolchain discoverer service: tries {@code JAVA{xx}_HOME} environment variables, third party installers and
61   * OS-specific locations.
62   *
63   * @since 3.2.0
64   */
65  @Named
66  @Singleton
67  public class ToolchainDiscoverer {
68  
69      public static final String JAVA = "java.";
70      public static final String VERSION = "version";
71      public static final String RUNTIME_NAME = "runtime.name";
72      public static final String RUNTIME_VERSION = "runtime.version";
73      public static final String VENDOR = "vendor";
74      public static final String VENDOR_VERSION = "vendor.version";
75      public static final String[] PROPERTIES = {VERSION, RUNTIME_NAME, RUNTIME_VERSION, VENDOR, VENDOR_VERSION};
76  
77      public static final String CURRENT = "current";
78      public static final String ENV = "env";
79      public static final String LTS = "lts";
80  
81      public static final List<String> SORTED_PROVIDES = Collections.unmodifiableList(
82              Arrays.asList(VERSION, RUNTIME_NAME, RUNTIME_VERSION, VENDOR, VENDOR_VERSION, CURRENT, LTS, ENV));
83  
84      public static final String DISCOVERED_TOOLCHAINS_CACHE_XML = ".m2/discovered-jdk-toolchains-cache.xml";
85  
86      public static final String JDK_HOME = "jdkHome";
87      public static final String JAVA_HOME = "java.home";
88  
89      private static final String COMMA = ",";
90      public static final String USER_HOME = "user.home";
91  
92      private final Logger log = LoggerFactory.getLogger(getClass());
93  
94      private volatile Map<Path, ToolchainModel> cache;
95      private volatile boolean cacheModified;
96      private volatile Set<Path> foundJdks;
97  
98      /**
99       * Build the model for the current JDK toolchain
100      */
101     public Optional<ToolchainModel> getCurrentJdkToolchain() {
102         Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty(JAVA_HOME)));
103         if (!hasJavaC(currentJdkHome)) {
104             // in case the current JVM is not a JDK
105             return Optional.empty();
106         }
107         ToolchainModel model = new ToolchainModel();
108         model.setType(TOOLCHAIN_TYPE_JDK);
109         Stream.of(PROPERTIES).forEach(k -> {
110             String v = System.getProperty(JAVA + k);
111             if (v != null) {
112                 model.addProvide(k, v);
113             }
114         });
115         model.addProvide(CURRENT, "true");
116         Xpp3Dom config = new Xpp3Dom("configuration");
117         Xpp3Dom jdkHome = new Xpp3Dom(JDK_HOME);
118         jdkHome.setValue(currentJdkHome.toString());
119         config.addChild(jdkHome);
120         model.setConfiguration(config);
121         return Optional.of(model);
122     }
123 
124     public PersistedToolchains discoverToolchains() {
125         return discoverToolchains(LTS + COMMA + VERSION + COMMA + VENDOR);
126     }
127 
128     /**
129      * Returns a PersistedToolchains object containing a list of discovered toolchains,
130      * never <code>null</code>.
131      */
132     public PersistedToolchains discoverToolchains(String comparator) {
133         try {
134             Set<Path> jdks = findJdks();
135             log.info("Found " + jdks.size() + " possible jdks: " + jdks);
136             readCache();
137             Map<Path, Map<String, String>> flags = new HashMap<>();
138             Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty(JAVA_HOME)));
139             flags.computeIfAbsent(currentJdkHome, p -> new HashMap<>()).put(CURRENT, "true");
140             // check environment variables for JAVA{xx}_HOME
141             System.getenv().entrySet().stream()
142                     .filter(e -> e.getKey().startsWith("JAVA") && e.getKey().endsWith("_HOME"))
143                     .forEach(e -> {
144                         Path path = getCanonicalPath(Paths.get(e.getValue()));
145                         Map<String, String> f = flags.computeIfAbsent(path, p -> new HashMap<>());
146                         String val = f.getOrDefault(ENV, "");
147                         f.put(ENV, (val.isEmpty() ? "" : val + ",") + e.getKey());
148                     });
149 
150             List<ToolchainModel> tcs = jdks.parallelStream()
151                     .map(s -> {
152                         ToolchainModel tc = getToolchainModel(s);
153                         flags.getOrDefault(s, Collections.emptyMap())
154                                 .forEach((k, v) -> tc.getProvides().setProperty(k, v));
155                         String version = tc.getProvides().getProperty(VERSION);
156                         if (isLts(version)) {
157                             tc.getProvides().setProperty(LTS, "true");
158                         }
159                         return tc;
160                     })
161                     .sorted(getToolchainModelComparator(comparator))
162                     .collect(Collectors.toList());
163             writeCache();
164             PersistedToolchains ps = new PersistedToolchains();
165             ps.setToolchains(tcs);
166             return ps;
167         } catch (Exception e) {
168             if (log.isDebugEnabled()) {
169                 log.warn("Error discovering toolchains: " + e, e);
170             } else {
171                 log.warn("Error discovering toolchains (enable debug level for more information): " + e);
172             }
173             return new PersistedToolchains();
174         }
175     }
176 
177     private static boolean isLts(String version) {
178         return Stream.of("1.8", "8", "11", "17", "21", "25")
179                 .anyMatch(v -> version.equals(v) || version.startsWith(v + "."));
180     }
181 
182     private synchronized void readCache() {
183         if (cache == null) {
184             try {
185                 cache = new ConcurrentHashMap<>();
186                 cacheModified = false;
187                 Path cacheFile = getCacheFile();
188                 if (Files.isRegularFile(cacheFile)) {
189                     try (Reader r = Files.newBufferedReader(cacheFile)) {
190                         PersistedToolchains pt = new MavenToolchainsXpp3Reader().read(r, false);
191                         cache = pt.getToolchains().stream()
192                                 // Remove stale entries
193                                 .filter(tc -> {
194                                     // If the bin/java executable is not available anymore, remove this TC
195                                     if (!hasJavaC(getJdkHome(tc))) {
196                                         cacheModified = true;
197                                         return false;
198                                     } else {
199                                         return true;
200                                     }
201                                 })
202                                 .collect(Collectors.toConcurrentMap(this::getJdkHome, Function.identity()));
203                     }
204                 }
205             } catch (IOException | XmlPullParserException e) {
206                 log.debug("Error reading toolchains cache: " + e, e);
207             }
208         }
209     }
210 
211     private synchronized void writeCache() {
212         if (cacheModified) {
213             try {
214                 Path cacheFile = getCacheFile();
215                 Files.createDirectories(cacheFile.getParent());
216                 try (Writer w = Files.newBufferedWriter(cacheFile)) {
217                     PersistedToolchains pt = new PersistedToolchains();
218                     pt.setToolchains(cache.values().stream()
219                             .map(tc -> {
220                                 ToolchainModel model = tc.clone();
221                                 // Remove transient information
222                                 model.getProvides().remove(CURRENT);
223                                 model.getProvides().remove(ENV);
224                                 return model;
225                             })
226                             .sorted(version().thenComparing(vendor()))
227                             .collect(Collectors.toList()));
228                     new MavenToolchainsXpp3Writer().write(w, pt);
229                 }
230             } catch (IOException e) {
231                 log.debug("Error writing toolchains cache: " + e, e);
232             }
233             cacheModified = false;
234         }
235     }
236 
237     ToolchainModel getToolchainModel(Path jdk) {
238         ToolchainModel model = cache.get(jdk);
239         if (model == null) {
240             model = doGetToolchainModel(jdk);
241             cache.put(jdk, model);
242             cacheModified = true;
243         }
244         return model;
245     }
246 
247     private static Path getCacheFile() {
248         return Paths.get(System.getProperty(USER_HOME)).resolve(DISCOVERED_TOOLCHAINS_CACHE_XML);
249     }
250 
251     public Path getJdkHome(ToolchainModel toolchain) {
252         Xpp3Dom dom = (Xpp3Dom) toolchain.getConfiguration();
253         Xpp3Dom javahome = dom != null ? dom.getChild(JDK_HOME) : null;
254         String jdk = javahome != null ? javahome.getValue() : null;
255         return Paths.get(Objects.requireNonNull(jdk));
256     }
257 
258     ToolchainModel doGetToolchainModel(Path jdk) {
259         Path java = jdk.resolve("bin").resolve("java");
260         if (!Files.exists(java)) {
261             java = jdk.resolve("bin").resolve("java.exe");
262             if (!Files.exists(java)) {
263                 log.debug("JDK toolchain discovered at " + jdk
264                         + " will be ignored: unable to find bin/java or bin\\java.exe");
265                 return null;
266             }
267         }
268         if (!java.toFile().canExecute()) {
269             log.debug("JDK toolchain discovered at " + jdk
270                     + " will be ignored: the bin/java or bin\\java.exe is not executable");
271             return null;
272         }
273         List<String> lines;
274         try {
275             Path temp = Files.createTempFile("jdk-opts-", ".out");
276             try {
277                 new ProcessBuilder()
278                         .command(java.toString(), "-XshowSettings:properties", "-version")
279                         .redirectError(temp.toFile())
280                         .start()
281                         .waitFor();
282                 lines = Files.readAllLines(temp);
283             } finally {
284                 Files.delete(temp);
285             }
286         } catch (IOException | InterruptedException e) {
287             log.debug("JDK toolchain discovered at " + jdk + " will be ignored: error executing java: " + e);
288             return null;
289         }
290 
291         Map<String, String> properties = new LinkedHashMap<>();
292         Stream.of(PROPERTIES).forEach(name -> {
293             lines.stream()
294                     .filter(l -> l.contains(JAVA + name))
295                     .map(l -> l.replaceFirst(".*=\\s*(.*)", "$1"))
296                     .findFirst()
297                     .ifPresent(value -> properties.put(name, value));
298         });
299         if (!properties.containsKey(VERSION)) {
300             log.debug("JDK toolchain discovered at " + jdk + " will be ignored: could not obtain " + JAVA + VERSION);
301             return null;
302         }
303 
304         ToolchainModel model = new ToolchainModel();
305         model.setType(TOOLCHAIN_TYPE_JDK);
306         properties.forEach(model::addProvide);
307         Xpp3Dom configuration = new Xpp3Dom("configuration");
308         Xpp3Dom jdkHome = new Xpp3Dom(JDK_HOME);
309         jdkHome.setValue(jdk.toString());
310         configuration.addChild(jdkHome);
311         model.setConfiguration(configuration);
312         return model;
313     }
314 
315     private static Path getCanonicalPath(Path path) {
316         try {
317             return path.toRealPath();
318         } catch (IOException e) {
319             return getCanonicalPath(path.getParent()).resolve(path.getFileName());
320         }
321     }
322 
323     Comparator<ToolchainModel> getToolchainModelComparator(String comparator) {
324         Comparator<ToolchainModel> c = null;
325         for (String part : comparator.split(COMMA)) {
326             c = c == null ? getComparator(part) : c.thenComparing(getComparator(part));
327         }
328         return c;
329     }
330 
331     private Comparator<ToolchainModel> getComparator(String part) {
332         switch (part.trim().toLowerCase(Locale.ROOT)) {
333             case LTS:
334                 return lts();
335             case VENDOR:
336                 return vendor();
337             case ENV:
338                 return env();
339             case CURRENT:
340                 return current();
341             case VERSION:
342                 return version();
343             default:
344                 throw new IllegalArgumentException("Unsupported comparator: " + part
345                         + ". Supported comparators are: vendor, env, current, lts and version.");
346         }
347     }
348 
349     Comparator<ToolchainModel> lts() {
350         return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(LTS) ? -1 : +1);
351     }
352 
353     Comparator<ToolchainModel> vendor() {
354         return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VENDOR));
355     }
356 
357     Comparator<ToolchainModel> env() {
358         return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(ENV) ? -1 : +1);
359     }
360 
361     Comparator<ToolchainModel> current() {
362         return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(CURRENT) ? -1 : +1);
363     }
364 
365     Comparator<ToolchainModel> version() {
366         return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VERSION), (v1, v2) -> {
367                     String[] a = v1.split("\\.");
368                     String[] b = v2.split("\\.");
369                     int length = Math.min(a.length, b.length);
370                     for (int i = 0; i < length; i++) {
371                         String oa = a[i];
372                         String ob = b[i];
373                         if (!Objects.equals(oa, ob)) {
374                             // A null element is less than a non-null element
375                             if (oa == null || ob == null) {
376                                 return oa == null ? -1 : 1;
377                             }
378                             int v = oa.compareTo(ob);
379                             if (v != 0) {
380                                 return v;
381                             }
382                         }
383                     }
384                     return a.length - b.length;
385                 })
386                 .reversed();
387     }
388 
389     private Set<Path> findJdks() {
390         if (foundJdks == null) {
391             synchronized (this) {
392                 if (foundJdks == null) {
393                     foundJdks = doFindJdks();
394                 }
395             }
396         }
397         return foundJdks;
398     }
399 
400     /**
401      * Find JDKs in known classical locations.
402      *
403      * @return a set of path where JDKs were found.
404      */
405     private Set<Path> doFindJdks() {
406         List<Path> dirsToTest = new ArrayList<>();
407 
408         // add current JDK
409         dirsToTest.add(Paths.get(System.getProperty(JAVA_HOME)));
410 
411         // check environment variables for JAVA{xx}_HOME
412         System.getenv().entrySet().stream()
413                 .filter(e -> e.getKey().startsWith("JAVA") && e.getKey().endsWith("_HOME"))
414                 .map(e -> Paths.get(e.getValue()))
415                 .forEach(dirsToTest::add);
416 
417         final Path userHome = Paths.get(System.getProperty(USER_HOME));
418         List<Path> installedDirs = new ArrayList<>();
419 
420         // JDK installed by third-party tool managers
421         installedDirs.add(userHome.resolve(".jdks"));
422         installedDirs.add(userHome.resolve(".m2").resolve("jdks"));
423         installedDirs.add(userHome.resolve(".sdkman").resolve("candidates").resolve("java"));
424         installedDirs.add(userHome.resolve(".gradle").resolve("jdks"));
425         installedDirs.add(userHome.resolve(".jenv").resolve("versions"));
426         installedDirs.add(userHome.resolve(".jbang").resolve("cache").resolve("jdks"));
427         installedDirs.add(userHome.resolve(".asdf").resolve("installs"));
428         installedDirs.add(userHome.resolve(".jabba").resolve("jdk"));
429 
430         // OS related directories
431         String osname = System.getProperty("os.name").toLowerCase(Locale.ROOT);
432         boolean macos = osname.startsWith("mac");
433         boolean win = osname.startsWith("win");
434         if (macos) {
435             installedDirs.add(Paths.get("/Library/Java/JavaVirtualMachines"));
436             installedDirs.add(userHome.resolve("Library/Java/JavaVirtualMachines"));
437         } else if (win) {
438             installedDirs.add(Paths.get("C:\\Program Files\\Java\\"));
439             Path scoop = userHome.resolve("scoop").resolve("apps");
440             if (Files.isDirectory(scoop)) {
441                 try (Stream<Path> stream = Files.list(scoop)) {
442                     stream.forEach(installedDirs::add);
443                 } catch (IOException e) {
444                     // ignore
445                 }
446             }
447         } else {
448             installedDirs.add(Paths.get("/usr/jdk"));
449             installedDirs.add(Paths.get("/usr/java"));
450             installedDirs.add(Paths.get("/opt/java"));
451             installedDirs.add(Paths.get("/usr/lib/jvm"));
452         }
453 
454         for (Path dest : installedDirs) {
455             if (Files.isDirectory(dest)) {
456                 try (Stream<Path> stream = Files.list(dest)) {
457                     stream.forEach(dir -> {
458                         dirsToTest.add(dir);
459                         if (macos) {
460                             dirsToTest.add(dir.resolve("Contents").resolve("Home"));
461                         }
462                     });
463                 } catch (IOException e) {
464                     // ignore
465                 }
466             }
467         }
468 
469         // only keep directories that have a javac file
470         return dirsToTest.stream()
471                 .filter(ToolchainDiscoverer::hasJavaC)
472                 .map(ToolchainDiscoverer::getCanonicalPath)
473                 .collect(Collectors.toSet());
474     }
475 
476     private static boolean hasJavaC(Path subdir) {
477         return Files.exists(subdir.resolve(Paths.get("bin", "javac")))
478                 || Files.exists(subdir.resolve(Paths.get("bin", "javac.exe")));
479     }
480 }