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.wrapper;
20  
21  import javax.inject.Inject;
22  
23  import java.io.BufferedWriter;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.DirectoryStream;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.nio.file.Paths;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.Properties;
34  
35  import org.apache.maven.Maven;
36  import org.apache.maven.execution.MavenSession;
37  import org.apache.maven.plugin.AbstractMojo;
38  import org.apache.maven.plugin.MojoExecutionException;
39  import org.apache.maven.plugins.annotations.Component;
40  import org.apache.maven.plugins.annotations.Mojo;
41  import org.apache.maven.plugins.annotations.Parameter;
42  import org.apache.maven.settings.Mirror;
43  import org.apache.maven.settings.Settings;
44  import org.codehaus.plexus.archiver.UnArchiver;
45  import org.codehaus.plexus.components.io.fileselectors.FileSelector;
46  import org.eclipse.aether.RepositorySystem;
47  import org.eclipse.aether.artifact.Artifact;
48  import org.eclipse.aether.artifact.DefaultArtifact;
49  import org.eclipse.aether.resolution.ArtifactRequest;
50  import org.eclipse.aether.resolution.ArtifactResolutionException;
51  import org.eclipse.aether.resolution.ArtifactResult;
52  
53  import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
54  
55  /**
56   * Unpacks the maven-wrapper distribution files to the current project source tree.
57   *
58   * @since 3.0.0
59   */
60  @Mojo(name = "wrapper", aggregator = true, requiresProject = false)
61  public class WrapperMojo extends AbstractMojo {
62      private static final String MVNW_REPOURL = "MVNW_REPOURL";
63  
64      protected static final String DEFAULT_REPOURL = "https://repo.maven.apache.org/maven2";
65  
66      // CONFIGURATION PARAMETERS
67  
68      /**
69       * The version of Maven to require, default value is the Runtime version of Maven.
70       * Can be any valid release above 2.0.9
71       *
72       * @since 3.0.0
73       */
74      @Parameter(property = "maven")
75      private String mavenVersion;
76  
77      /**
78       * The version of Maven Daemon to require.
79       *
80       * @since 3.2.0
81       */
82      @Parameter(property = "mvnd")
83      private String mvndVersion;
84  
85      /**
86       * The Maven Wrapper distribution type.
87       * <p>
88       * Options are:
89       *
90       * <dl>
91       *   <dt>script</dt>
92       *   <dd>only mvnw scripts</dd>
93       *   <dt>bin</dt>
94       *   <dd>precompiled and packaged code</dd>
95       *   <dt>source</dt>
96       *   <dd>Java source code, will be compiled on the fly</dd>
97       *   <dt>only-script (default)</dt>
98       *   <dd>the new lite implementation of mvnw/mvnw.cmd scripts downloads the maven directly and skips maven-wrapper.jar - since 3.2.0</dd>
99       * </dl>
100      *
101      * If {@code -Dtype={type}} is not explicitly provided, then {@code distributionType} from
102      * {@code .mvn/wrapper/maven-wrapper.properties} is used, if it exists.
103      * Otherwise, {@code only-script} is used as the default distribution type.
104      * <p>
105      * This value will be used as the classifier of the downloaded file.
106      *
107      * @since 3.0.0
108      */
109     @Parameter(property = "type")
110     private String distributionType;
111 
112     /**
113      * Include <code>mvnwDebug*</code> scripts?
114      *
115      * @since 3.0.0
116      */
117     @Parameter(defaultValue = "false", property = "includeDebug")
118     private boolean includeDebugScript;
119 
120     /**
121      * The expected SHA-256 checksum of the <i>maven-wrapper.jar</i> that is
122      * used to load the configured Maven distribution.
123      *
124      * @since 3.2.0
125      */
126     @Parameter(property = "wrapperSha256Sum")
127     private String wrapperSha256Sum;
128 
129     /**
130      * The expected SHA-256 checksum of the Maven distribution that is
131      * executed by the installed wrapper.
132      *
133      * @since 3.2.0
134      */
135     @Parameter(property = "distributionSha256Sum")
136     private String distributionSha256Sum;
137 
138     /**
139      * Determines if the Maven distribution should be downloaded
140      * on every execution of the Maven wrapper.
141      *
142      * @since 3.2.0
143      */
144     @Parameter(defaultValue = "false", property = "alwaysDownload")
145     private boolean alwaysDownload;
146 
147     /**
148      * Determines if the Maven distribution should be unpacked
149      * on every execution of the Maven wrapper.
150      *
151      * @since 3.2.0
152      */
153     @Parameter(defaultValue = "false", property = "alwaysUnpack")
154     private boolean alwaysUnpack;
155 
156     // READONLY PARAMETERS
157 
158     @Component
159     private MavenSession session;
160 
161     // CONSTANTS
162 
163     private static final String WRAPPER_DISTRIBUTION_GROUP_ID = "org.apache.maven.wrapper";
164 
165     private static final String WRAPPER_DISTRIBUTION_ARTIFACT_ID = "maven-wrapper-distribution";
166 
167     private static final String WRAPPER_DISTRIBUTION_EXTENSION = "zip";
168 
169     private static final String WRAPPER_DIR = ".mvn/wrapper";
170 
171     private static final String WRAPPER_PROPERTIES_FILENAME = "maven-wrapper.properties";
172 
173     private static final String DISTRIBUTION_TYPE_PROPERTY_NAME = "distributionType";
174 
175     private static final String TYPE_ONLY_SCRIPT = "only-script";
176 
177     private static final String DEFAULT_DISTRIBUTION_TYPE = TYPE_ONLY_SCRIPT;
178 
179     // COMPONENTS
180 
181     @Inject
182     private RepositorySystem repositorySystem;
183 
184     @Inject
185     private Map<String, UnArchiver> unarchivers;
186 
187     @Override
188     public void execute() throws MojoExecutionException {
189         final Path baseDir = Paths.get(session.getRequest().getBaseDirectory());
190         final Path wrapperDir = baseDir.resolve(WRAPPER_DIR);
191 
192         if (distributionType == null) {
193             distributionType = determineDistributionType(wrapperDir);
194         }
195 
196         if (mvndVersion != null && mvndVersion.length() > 0 && !TYPE_ONLY_SCRIPT.equals(distributionType)) {
197             throw new MojoExecutionException("maven-wrapper with type=" + distributionType
198                     + " cannot work with mvnd, please set type to '" + TYPE_ONLY_SCRIPT + "'.");
199         }
200 
201         mavenVersion = getVersion(mavenVersion, Maven.class, "org.apache.maven/maven-core");
202         String wrapperVersion = getVersion(null, this.getClass(), "org.apache.maven.plugins/maven-wrapper-plugin");
203 
204         final Artifact artifact = downloadWrapperDistribution(wrapperVersion);
205 
206         createDirectories(wrapperDir);
207         cleanup(wrapperDir);
208         unpack(artifact, baseDir);
209         replaceProperties(wrapperVersion, wrapperDir);
210     }
211 
212     private String determineDistributionType(final Path wrapperDir) {
213         final String typeFromMavenWrapperProperties = distributionTypeFromExistingMavenWrapperProperties(wrapperDir);
214         if (typeFromMavenWrapperProperties != null) {
215             return typeFromMavenWrapperProperties;
216         }
217 
218         return DEFAULT_DISTRIBUTION_TYPE;
219     }
220 
221     private String distributionTypeFromExistingMavenWrapperProperties(final Path wrapperDir) {
222         final Path mavenWrapperProperties = wrapperDir.resolve(WRAPPER_PROPERTIES_FILENAME);
223         try (InputStream inputStream = Files.newInputStream(mavenWrapperProperties)) {
224             Properties properties = new Properties();
225             properties.load(inputStream);
226             return properties.getProperty(DISTRIBUTION_TYPE_PROPERTY_NAME);
227         } catch (IOException e) {
228             return null;
229         }
230     }
231 
232     private void createDirectories(Path dir) throws MojoExecutionException {
233         try {
234             Files.createDirectories(dir);
235         } catch (IOException ioe) {
236             throw new MojoExecutionException(ioe.getMessage(), ioe);
237         }
238     }
239 
240     private void cleanup(Path wrapperDir) throws MojoExecutionException {
241         try (DirectoryStream<Path> dsClass = Files.newDirectoryStream(wrapperDir, "*.class")) {
242             for (Path file : dsClass) {
243                 // Cleanup old compiled *.class
244                 Files.deleteIfExists(file);
245             }
246             Files.deleteIfExists(wrapperDir.resolve("MavenWrapperDownloader.java"));
247             Files.deleteIfExists(wrapperDir.resolve("maven-wrapper.jar"));
248         } catch (IOException ioe) {
249             throw new MojoExecutionException(ioe.getMessage(), ioe);
250         }
251     }
252 
253     private Artifact downloadWrapperDistribution(String wrapperVersion) throws MojoExecutionException {
254 
255         Artifact artifact = new DefaultArtifact(
256                 WRAPPER_DISTRIBUTION_GROUP_ID,
257                 WRAPPER_DISTRIBUTION_ARTIFACT_ID,
258                 distributionType,
259                 WRAPPER_DISTRIBUTION_EXTENSION,
260                 wrapperVersion);
261 
262         ArtifactRequest request = new ArtifactRequest();
263         request.setRepositories(session.getCurrentProject().getRemotePluginRepositories());
264         request.setArtifact(artifact);
265 
266         try {
267             ArtifactResult artifactResult = repositorySystem.resolveArtifact(session.getRepositorySession(), request);
268             return artifactResult.getArtifact();
269 
270         } catch (ArtifactResolutionException e) {
271             throw new MojoExecutionException("artifact: " + artifact + " not resolved.", e);
272         }
273     }
274 
275     private void unpack(Artifact artifact, Path targetFolder) {
276         UnArchiver unarchiver = unarchivers.get(WRAPPER_DISTRIBUTION_EXTENSION);
277         unarchiver.setDestDirectory(targetFolder.toFile());
278         unarchiver.setSourceFile(artifact.getFile());
279         if (!includeDebugScript) {
280             unarchiver.setFileSelectors(
281                     new FileSelector[] {fileInfo -> !fileInfo.getName().contains("Debug")});
282         }
283         unarchiver.extract();
284         getLog().info("Unpacked " + buffer().strong(distributionType) + " type wrapper distribution " + artifact);
285     }
286 
287     /**
288      * As long as the content only contains the license and the distributionUrl, we can simply replace it.
289      * No need to look for other properties, restore them, respecting comments, etc.
290      *
291      * @param wrapperVersion the wrapper version
292      * @param targetFolder   the folder containing the wrapper.properties
293      * @throws MojoExecutionException if writing fails
294      */
295     private void replaceProperties(String wrapperVersion, Path targetFolder) throws MojoExecutionException {
296         String repoUrl = getRepoUrl();
297 
298         String distributionUrl = repoUrl + "/org/apache/maven/apache-maven/" + mavenVersion + "/apache-maven-"
299                 + mavenVersion + "-bin.zip";
300         String wrapperUrl = repoUrl + "/org/apache/maven/wrapper/maven-wrapper/" + wrapperVersion + "/maven-wrapper-"
301                 + wrapperVersion + ".jar";
302 
303         if (mvndVersion != null && mvndVersion.length() > 0) {
304             // now maven-mvnd is not published to the central repo.
305             distributionUrl = "https://archive.apache.org/dist/maven/mvnd/" + mvndVersion + "/maven-mvnd-" + mvndVersion
306                     + "-bin.zip";
307         }
308 
309         Path wrapperPropertiesFile = targetFolder.resolve("maven-wrapper.properties");
310 
311         getLog().info("Configuring .mvn/wrapper/maven-wrapper.properties to use "
312                 + buffer().strong("Maven " + mavenVersion) + " and download from " + repoUrl);
313 
314         final String license = "# Licensed to the Apache Software Foundation (ASF) under one%n"
315                 + "# or more contributor license agreements.  See the NOTICE file%n"
316                 + "# distributed with this work for additional information%n"
317                 + "# regarding copyright ownership.  The ASF licenses this file%n"
318                 + "# to you under the Apache License, Version 2.0 (the%n"
319                 + "# \"License\"); you may not use this file except in compliance%n"
320                 + "# with the License.  You may obtain a copy of the License at%n"
321                 + "#%n"
322                 + "#   http://www.apache.org/licenses/LICENSE-2.0%n"
323                 + "#%n"
324                 + "# Unless required by applicable law or agreed to in writing,%n"
325                 + "# software distributed under the License is distributed on an%n"
326                 + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY%n"
327                 + "# KIND, either express or implied.  See the License for the%n"
328                 + "# specific language governing permissions and limitations%n"
329                 + "# under the License.%n";
330 
331         try (BufferedWriter out = Files.newBufferedWriter(wrapperPropertiesFile, StandardCharsets.UTF_8)) {
332             out.append(String.format(Locale.ROOT, license));
333             out.append("wrapperVersion=" + wrapperVersion + System.lineSeparator());
334             out.append(DISTRIBUTION_TYPE_PROPERTY_NAME + "=" + distributionType + System.lineSeparator());
335             out.append("distributionUrl=" + distributionUrl + System.lineSeparator());
336             if (distributionSha256Sum != null) {
337                 out.append("distributionSha256Sum=" + distributionSha256Sum + System.lineSeparator());
338             }
339             if (!distributionType.equals(TYPE_ONLY_SCRIPT)) {
340                 out.append("wrapperUrl=" + wrapperUrl + System.lineSeparator());
341             }
342             if (wrapperSha256Sum != null) {
343                 out.append("wrapperSha256Sum=" + wrapperSha256Sum + System.lineSeparator());
344             }
345             if (alwaysDownload) {
346                 out.append("alwaysDownload=" + Boolean.TRUE + System.lineSeparator());
347             }
348             if (alwaysUnpack) {
349                 out.append("alwaysUnpack=" + Boolean.TRUE + System.lineSeparator());
350             }
351         } catch (IOException ioe) {
352             throw new MojoExecutionException("Can't create maven-wrapper.properties", ioe);
353         }
354     }
355 
356     private String getVersion(String defaultVersion, Class<?> clazz, String path) {
357         String version = defaultVersion;
358         if (version == null || version.trim().length() == 0 || "true".equals(version)) {
359             Properties props = new Properties();
360             try (InputStream is = clazz.getResourceAsStream("/META-INF/maven/" + path + "/pom.properties")) {
361                 if (is != null) {
362                     props.load(is);
363                     version = props.getProperty("version");
364                 }
365             } catch (IOException e) {
366                 // noop
367             }
368         }
369         return version;
370     }
371 
372     /**
373      * Determine the repository URL to download Wrapper and Maven from.
374      */
375     private String getRepoUrl() {
376         // adapt to also support MVNW_REPOURL as supported by mvnw scripts from maven-wrapper
377         String envRepoUrl = System.getenv(MVNW_REPOURL);
378         final String repoUrl = determineRepoUrl(envRepoUrl, session.getSettings());
379 
380         getLog().debug("Determined repo URL to use as " + repoUrl);
381 
382         return repoUrl;
383     }
384 
385     protected String determineRepoUrl(String envRepoUrl, Settings settings) {
386 
387         if (envRepoUrl != null && !envRepoUrl.trim().isEmpty() && envRepoUrl.length() > 4) {
388             String repoUrl = envRepoUrl.trim();
389 
390             if (repoUrl.endsWith("/")) {
391                 repoUrl = repoUrl.substring(0, repoUrl.length() - 1);
392             }
393 
394             getLog().debug("Using repo URL from " + MVNW_REPOURL + " environment variable.");
395 
396             return repoUrl;
397         }
398 
399         // otherwise mirror from settings
400         if (settings.getMirrors() != null && !settings.getMirrors().isEmpty()) {
401             for (Mirror current : settings.getMirrors()) {
402                 if ("*".equals(current.getMirrorOf())) {
403                     getLog().debug("Using repo URL from * mirror in settings file.");
404                     return current.getUrl();
405                 }
406             }
407         }
408 
409         return DEFAULT_REPOURL;
410     }
411 }