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.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.DirectoryStream;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
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.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   * @author Robert Scholte
59   * @since 3.0.0
60   */
61  @Mojo(name = "wrapper", aggregator = true, requiresDirectInvocation = true)
62  public class WrapperMojo extends AbstractMojo {
63      private static final String MVNW_REPOURL = "MVNW_REPOURL";
64  
65      private 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 (default)</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</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 = "bin", 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     @Parameter(defaultValue = "${session}", readonly = true, required = true)
152     private MavenSession session;
153 
154     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
155     private Settings settings;
156 
157     // Waiting for https://github.com/eclipse/sisu.inject/pull/39 PathTypeConverter
158     @Parameter(defaultValue = "${project.basedir}", readonly = true, required = true)
159     private File basedir;
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     // COMPONENTS
170 
171     @Inject
172     private RepositorySystem repositorySystem;
173 
174     @Inject
175     private Map<String, UnArchiver> unarchivers;
176 
177     @Override
178     public void execute() throws MojoExecutionException, MojoFailureException {
179         if (mvndVersion != null && mvndVersion.length() > 0 && !"only-script".equals(distributionType)) {
180             throw new MojoExecutionException("maven-wrapper with type=" + distributionType
181                     + " cannot work with mvnd, please set type to 'only-script'.");
182         }
183 
184         mavenVersion = getVersion(mavenVersion, Maven.class, "org.apache.maven/maven-core");
185         String wrapperVersion = getVersion(null, this.getClass(), "org.apache.maven.plugins/maven-wrapper-plugin");
186 
187         final Artifact artifact = downloadWrapperDistribution(wrapperVersion);
188         final Path wrapperDir = createDirectories(basedir.toPath().resolve(".mvn/wrapper"));
189 
190         cleanup(wrapperDir);
191         unpack(artifact, basedir.toPath());
192         replaceProperties(wrapperVersion, wrapperDir);
193     }
194 
195     private Path createDirectories(Path dir) throws MojoExecutionException {
196         try {
197             return Files.createDirectories(dir);
198         } catch (IOException ioe) {
199             throw new MojoExecutionException(ioe.getMessage(), ioe);
200         }
201     }
202 
203     private void cleanup(Path wrapperDir) throws MojoExecutionException {
204         try (DirectoryStream<Path> dsClass = Files.newDirectoryStream(wrapperDir, "*.class")) {
205             for (Path file : dsClass) {
206                 // Cleanup old compiled *.class
207                 Files.deleteIfExists(file);
208             }
209             Files.deleteIfExists(wrapperDir.resolve("MavenWrapperDownloader.java"));
210             Files.deleteIfExists(wrapperDir.resolve("maven-wrapper.jar"));
211         } catch (IOException ioe) {
212             throw new MojoExecutionException(ioe.getMessage(), ioe);
213         }
214     }
215 
216     private Artifact downloadWrapperDistribution(String wrapperVersion) throws MojoExecutionException {
217 
218         Artifact artifact = new DefaultArtifact(
219                 WRAPPER_DISTRIBUTION_GROUP_ID,
220                 WRAPPER_DISTRIBUTION_ARTIFACT_ID,
221                 distributionType,
222                 WRAPPER_DISTRIBUTION_EXTENSION,
223                 wrapperVersion);
224 
225         ArtifactRequest request = new ArtifactRequest();
226         request.setRepositories(session.getCurrentProject().getRemotePluginRepositories());
227         request.setArtifact(artifact);
228 
229         try {
230             ArtifactResult artifactResult = repositorySystem.resolveArtifact(session.getRepositorySession(), request);
231             return artifactResult.getArtifact();
232 
233         } catch (ArtifactResolutionException e) {
234             throw new MojoExecutionException("artifact: " + artifact + " not resolved.", e);
235         }
236     }
237 
238     private void unpack(Artifact artifact, Path targetFolder) {
239         UnArchiver unarchiver = unarchivers.get(WRAPPER_DISTRIBUTION_EXTENSION);
240         unarchiver.setDestDirectory(targetFolder.toFile());
241         unarchiver.setSourceFile(artifact.getFile());
242         if (!includeDebugScript) {
243             unarchiver.setFileSelectors(
244                     new FileSelector[] {fileInfo -> !fileInfo.getName().contains("Debug")});
245         }
246         unarchiver.extract();
247         getLog().info("Unpacked " + buffer().strong(distributionType) + " type wrapper distribution " + artifact);
248     }
249 
250     /**
251      * As long as the content only contains the license and the distributionUrl, we can simply replace it.
252      * No need to look for other properties, restore them, respecting comments, etc.
253      *
254      * @param wrapperVersion the wrapper version
255      * @param targetFolder   the folder containing the wrapper.properties
256      * @throws MojoExecutionException if writing fails
257      */
258     private void replaceProperties(String wrapperVersion, Path targetFolder) throws MojoExecutionException {
259         String repoUrl = getRepoUrl();
260 
261         String distributionUrl = repoUrl + "/org/apache/maven/apache-maven/" + mavenVersion + "/apache-maven-"
262                 + mavenVersion + "-bin.zip";
263         String wrapperUrl = repoUrl + "/org/apache/maven/wrapper/maven-wrapper/" + wrapperVersion + "/maven-wrapper-"
264                 + wrapperVersion + ".jar";
265 
266         if (mvndVersion != null && mvndVersion.length() > 0) {
267             // now maven-mvnd is not published to the central repo.
268             distributionUrl = "https://archive.apache.org/dist/maven/mvnd/" + mvndVersion + "/maven-mvnd-" + mvndVersion
269                     + "-bin.zip";
270         }
271 
272         Path wrapperPropertiesFile = targetFolder.resolve("maven-wrapper.properties");
273 
274         getLog().info("Configuring .mvn/wrapper/maven-wrapper.properties to use "
275                 + buffer().strong("Maven " + mavenVersion) + " and download from " + repoUrl);
276 
277         final String license = "# Licensed to the Apache Software Foundation (ASF) under one%n"
278                 + "# or more contributor license agreements.  See the NOTICE file%n"
279                 + "# distributed with this work for additional information%n"
280                 + "# regarding copyright ownership.  The ASF licenses this file%n"
281                 + "# to you under the Apache License, Version 2.0 (the%n"
282                 + "# \"License\"); you may not use this file except in compliance%n"
283                 + "# with the License.  You may obtain a copy of the License at%n"
284                 + "#%n"
285                 + "#   http://www.apache.org/licenses/LICENSE-2.0%n"
286                 + "#%n"
287                 + "# Unless required by applicable law or agreed to in writing,%n"
288                 + "# software distributed under the License is distributed on an%n"
289                 + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY%n"
290                 + "# KIND, either express or implied.  See the License for the%n"
291                 + "# specific language governing permissions and limitations%n"
292                 + "# under the License.%n";
293 
294         try (BufferedWriter out = Files.newBufferedWriter(wrapperPropertiesFile, StandardCharsets.UTF_8)) {
295             out.append(String.format(Locale.ROOT, license));
296             out.append("distributionUrl=" + distributionUrl + System.lineSeparator());
297             if (distributionSha256Sum != null) {
298                 out.append("distributionSha256Sum=" + distributionSha256Sum + System.lineSeparator());
299             }
300             out.append("wrapperUrl=" + wrapperUrl + System.lineSeparator());
301             if (wrapperSha256Sum != null) {
302                 out.append("wrapperSha256Sum=" + wrapperSha256Sum + System.lineSeparator());
303             }
304             if (alwaysDownload) {
305                 out.append("alwaysDownload=" + Boolean.TRUE + System.lineSeparator());
306             }
307             if (alwaysUnpack) {
308                 out.append("alwaysUnpack=" + Boolean.TRUE + System.lineSeparator());
309             }
310         } catch (IOException ioe) {
311             throw new MojoExecutionException("Can't create maven-wrapper.properties", ioe);
312         }
313     }
314 
315     private String getVersion(String defaultVersion, Class<?> clazz, String path) {
316         String version = defaultVersion;
317         if (version == null || version.trim().length() == 0 || "true".equals(version)) {
318             Properties props = new Properties();
319             try (InputStream is = clazz.getResourceAsStream("/META-INF/maven/" + path + "/pom.properties")) {
320                 if (is != null) {
321                     props.load(is);
322                     version = props.getProperty("version");
323                 }
324             } catch (IOException e) {
325                 // noop
326             }
327         }
328         return version;
329     }
330 
331     /**
332      * Determine the repository URL to download Wrapper and Maven from.
333      */
334     private String getRepoUrl() {
335         // default
336         String repoUrl = DEFAULT_REPOURL;
337 
338         // adapt to also support MVNW_REPOURL as supported by mvnw scripts from maven-wrapper
339         String mvnwRepoUrl = System.getenv(MVNW_REPOURL);
340         if (mvnwRepoUrl != null && !mvnwRepoUrl.isEmpty()) {
341             repoUrl = mvnwRepoUrl;
342             getLog().debug("Using repo URL from " + MVNW_REPOURL + " environment variable.");
343         }
344         // otherwise mirror from settings
345         else if (settings.getMirrors() != null && !settings.getMirrors().isEmpty()) {
346             for (Mirror current : settings.getMirrors()) {
347                 if ("*".equals(current.getMirrorOf())) {
348                     repoUrl = current.getUrl();
349                     break;
350                 }
351             }
352             getLog().debug("Using repo URL from * mirror in settings file.");
353         }
354 
355         getLog().debug("Determined repo URL to use as " + repoUrl);
356 
357         return repoUrl;
358     }
359 }