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.Inject;
22  import javax.inject.Named;
23  
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.Optional;
28  import java.util.stream.Stream;
29  
30  import org.apache.maven.execution.MavenSession;
31  import org.apache.maven.plugin.AbstractMojo;
32  import org.apache.maven.plugin.MojoFailureException;
33  import org.apache.maven.plugins.annotations.LifecyclePhase;
34  import org.apache.maven.plugins.annotations.Mojo;
35  import org.apache.maven.plugins.annotations.Parameter;
36  import org.apache.maven.toolchain.MisconfiguredToolchainException;
37  import org.apache.maven.toolchain.RequirementMatcherFactory;
38  import org.apache.maven.toolchain.ToolchainFactory;
39  import org.apache.maven.toolchain.ToolchainManagerPrivate;
40  import org.apache.maven.toolchain.ToolchainPrivate;
41  import org.apache.maven.toolchain.model.PersistedToolchains;
42  import org.apache.maven.toolchain.model.ToolchainModel;
43  import org.codehaus.plexus.util.xml.Xpp3Dom;
44  
45  import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.ENV;
46  import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.RUNTIME_NAME;
47  import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.RUNTIME_VERSION;
48  import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.VENDOR;
49  import static org.apache.maven.plugins.toolchain.jdk.ToolchainDiscoverer.VERSION;
50  
51  /**
52   * Discover JDK toolchains and select a matching one.
53   *
54   * @since 3.2.0
55   */
56  @Mojo(name = "select-jdk-toolchain", defaultPhase = LifecyclePhase.VALIDATE)
57  public class SelectJdkToolchainMojo extends AbstractMojo {
58  
59      public static final String TOOLCHAIN_TYPE_JDK = "jdk";
60  
61      /** Jdk usage mode */
62      public enum JdkMode {
63          /** always ignore the current JDK */
64          Never,
65          /** to not use a toolchain if the toolchains that would be selected is the current JDK */
66          IfSame,
67          /** favor the current JDK if it matches the requirements */
68          IfMatch
69      }
70  
71      /**
72       * The version constraint for the JDK toolchain to select.
73       */
74      @Parameter(property = "toolchain.jdk.version")
75      private String version;
76  
77      /**
78       * The runtime name constraint for the JDK toolchain to select.
79       */
80      @Parameter(property = "toolchain.jdk.runtime.name")
81      private String runtimeName;
82  
83      /**
84       * The runtime version constraint for the JDK toolchain to select.
85       */
86      @Parameter(property = "toolchain.jdk.runtime.version")
87      private String runtimeVersion;
88  
89      /**
90       * The vendor constraint for the JDK toolchain to select.
91       */
92      @Parameter(property = "toolchain.jdk.vendor")
93      private String vendor;
94  
95      /**
96       * The env constraint for the JDK toolchain to select.
97       * To match the constraint, an environment variable with the given name must point to the JDK.
98       * For example, if you define {@code JAVA11_HOME=~/jdks/my-jdk-11.0.1}, you can specify
99       * {@code env=JAVA11_HOME} to match the given JDK.
100      */
101     @Parameter(property = "toolchain.jdk.env")
102     private String env;
103 
104     /**
105      * The matching mode, either {@code IfMatch} (the default), {@code IfSame}, or {@code Never}.
106      * If {@code IfMatch} is used, a toolchain will not be selected if the running JDK does
107      * match the provided constraints. This is the default and provides better performances as it
108      * avoids forking a different process when it's not required. The {@code IfSame} avoids
109      * selecting a toolchain if the toolchain selected is exactly the same as the running JDK.
110      * THe {@code Never} option will always select the toolchain.
111      */
112     @Parameter(property = "toolchain.jdk.mode", defaultValue = "IfMatch")
113     private JdkMode useJdk = JdkMode.IfMatch;
114 
115     /**
116      * Automatically discover JDK toolchains using the built-in heuristic.
117      * The default value is {@code true}.
118      */
119     @Parameter(property = "toolchain.jdk.discover", defaultValue = "true")
120     private boolean discoverToolchains = true;
121 
122     /**
123      * Comparator used to sort JDK toolchains for selection.
124      * This property is a comma separated list of values which may contains:
125      * <ul>
126      * <li>{@code lts}: prefer JDK with LTS version</li>
127      * <li>{@code current}: prefer the current JDK</li>
128      * <li>{@code env}: prefer JDKs defined using {@code JAVA\{xx\}_HOME} environment variables</li>
129      * <li>{@code version}: prefer JDK with higher versions</li>
130      * <li>{@code vendor}: order JDK by vendor name (usually as a last comparator to ensure a stable order)</li>
131      * </ul>
132      */
133     @Parameter(property = "toolchain.jdk.comparator", defaultValue = "lts,current,env,version,vendor")
134     private String comparator;
135 
136     /**
137      * Toolchain manager
138      */
139     @Inject
140     private ToolchainManagerPrivate toolchainManager;
141 
142     /**
143      * Toolchain factory
144      */
145     @Inject
146     @Named(TOOLCHAIN_TYPE_JDK)
147     ToolchainFactory factory;
148 
149     /**
150      * The current build session instance. This is used for toolchain manager API calls.
151      */
152     @Inject
153     private MavenSession session;
154 
155     /**
156      * Toolchain discoverer
157      */
158     @Inject
159     ToolchainDiscoverer discoverer;
160 
161     @Override
162     public void execute() throws MojoFailureException {
163         try {
164             doExecute();
165         } catch (MisconfiguredToolchainException e) {
166             throw new MojoFailureException("Unable to select toolchain: " + e, e);
167         }
168     }
169 
170     private void doExecute() throws MisconfiguredToolchainException, MojoFailureException {
171         if (version == null && runtimeName == null && runtimeVersion == null && vendor == null && env == null) {
172             return;
173         }
174 
175         Map<String, String> requirements = new HashMap<>();
176         Optional.ofNullable(version).ifPresent(v -> requirements.put(VERSION, v));
177         Optional.ofNullable(runtimeName).ifPresent(v -> requirements.put(RUNTIME_NAME, v));
178         Optional.ofNullable(runtimeVersion).ifPresent(v -> requirements.put(RUNTIME_VERSION, v));
179         Optional.ofNullable(vendor).ifPresent(v -> requirements.put(VENDOR, v));
180         Optional.ofNullable(env).ifPresent(v -> requirements.put(ENV, v));
181 
182         ToolchainModel currentJdkToolchainModel =
183                 discoverer.getCurrentJdkToolchain().orElse(null);
184         ToolchainPrivate currentJdkToolchain =
185                 currentJdkToolchainModel != null ? factory.createToolchain(currentJdkToolchainModel) : null;
186 
187         if (useJdk == JdkMode.IfMatch && currentJdkToolchain != null && matches(currentJdkToolchain, requirements)) {
188             getLog().info("Not using an external toolchain as the current JDK matches the requirements.");
189             return;
190         }
191 
192         ToolchainPrivate toolchain = Stream.of(toolchainManager.getToolchainsForType(TOOLCHAIN_TYPE_JDK, session))
193                 .filter(tc -> matches(tc, requirements))
194                 .findFirst()
195                 .orElse(null);
196         if (toolchain != null) {
197             getLog().info("Found matching JDK toolchain: " + toolchain);
198         }
199 
200         if (toolchain == null && discoverToolchains) {
201             getLog().debug("No matching toolchains configured, trying to discover JDK toolchains");
202             PersistedToolchains persistedToolchains = discoverer.discoverToolchains(comparator);
203             getLog().debug("Discovered " + persistedToolchains.getToolchains().size() + " JDK toolchains");
204 
205             for (ToolchainModel tcm : persistedToolchains.getToolchains()) {
206                 ToolchainPrivate tc = factory.createToolchain(tcm);
207                 if (tc != null && matches(tc, requirements)) {
208                     toolchain = tc;
209                     getLog().debug("Discovered matching JDK toolchain: " + toolchain);
210                     break;
211                 }
212             }
213         }
214 
215         if (toolchain == null) {
216             throw new MojoFailureException(
217                     "Cannot find matching toolchain definitions for the following toolchain types:" + requirements
218                             + System.lineSeparator()
219                             + "Define the required toolchains in your ~/.m2/toolchains.xml file.");
220         }
221 
222         if (useJdk == JdkMode.IfSame
223                 && currentJdkToolchain != null
224                 && Objects.equals(getJdkHome(currentJdkToolchain), getJdkHome(toolchain))) {
225             getLog().debug("Not using an external toolchain as the current JDK has been selected.");
226             return;
227         }
228 
229         toolchainManager.storeToolchainToBuildContext(toolchain, session);
230         getLog().debug("Found matching JDK toolchain: " + toolchain);
231     }
232 
233     private boolean matches(ToolchainPrivate tc, Map<String, String> requirements) {
234         ToolchainModel model = tc.getModel();
235         for (Map.Entry<String, String> req : requirements.entrySet()) {
236             String key = req.getKey();
237             String reqVal = req.getValue();
238             String tcVal = model.getProvides().getProperty(key);
239             if (tcVal == null) {
240                 getLog().debug("Toolchain " + tc + " is missing required property: " + key);
241                 return false;
242             }
243             if (!matches(key, reqVal, tcVal)) {
244                 getLog().debug("Toolchain " + tc + " doesn't match required property: " + key);
245                 return false;
246             }
247         }
248         return true;
249     }
250 
251     private boolean matches(String key, String reqVal, String tcVal) {
252         switch (key) {
253             case VERSION:
254                 return RequirementMatcherFactory.createVersionMatcher(tcVal).matches(reqVal);
255             case ENV:
256                 return reqVal.matches("(.*,|^)\\Q" + tcVal + "\\E(,.*|$)");
257             default:
258                 return RequirementMatcherFactory.createExactMatcher(tcVal).matches(reqVal);
259         }
260     }
261 
262     private String getJdkHome(ToolchainPrivate toolchain) {
263         return ((Xpp3Dom) toolchain.getModel().getConfiguration())
264                 .getChild("jdkHome")
265                 .getValue();
266     }
267 }