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