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