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.plugin.compiler;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Path;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Map.Entry;
32  import java.util.Objects;
33  import java.util.Optional;
34  import java.util.Set;
35  
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.plugin.MojoExecutionException;
38  import org.apache.maven.plugins.annotations.LifecyclePhase;
39  import org.apache.maven.plugins.annotations.Mojo;
40  import org.apache.maven.plugins.annotations.Parameter;
41  import org.apache.maven.plugins.annotations.ResolutionScope;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.shared.utils.StringUtils;
44  import org.apache.maven.shared.utils.logging.MessageUtils;
45  import org.apache.maven.toolchain.Toolchain;
46  import org.apache.maven.toolchain.java.DefaultJavaToolChain;
47  import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
48  import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
49  import org.codehaus.plexus.compiler.util.scan.StaleSourceScanner;
50  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
51  import org.codehaus.plexus.languages.java.jpms.LocationManager;
52  import org.codehaus.plexus.languages.java.jpms.ModuleNameSource;
53  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
54  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
55  
56  /**
57   * Compiles application sources
58   *
59   * @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
60   * @since 2.0
61   */
62  @Mojo(
63          name = "compile",
64          defaultPhase = LifecyclePhase.COMPILE,
65          threadSafe = true,
66          requiresDependencyResolution = ResolutionScope.COMPILE)
67  public class CompilerMojo extends AbstractCompilerMojo {
68      /**
69       * The source directories containing the sources to be compiled.
70       */
71      @Parameter(defaultValue = "${project.compileSourceRoots}", readonly = false, required = true)
72      private List<String> compileSourceRoots;
73  
74      /**
75       * The directory for compiled classes.
76       * <p>
77       * This parameter should only be modified in special cases. One example is creating
78       * a multi-release jar with a lower bytecode level (i.e. setting it to
79       * {@code ${project.build.outputDirectory}/META-INF/versions/21} or similar) in an additional
80       * execution.
81       * <p>
82       * When the required bytecode level is available though an installed JDK or toolchain,
83       * it is recommended to use the {@code <release>} property
84       * in conjunction with the ${multiReleaseOutput} parameter instead.
85       */
86      @Parameter(
87              property = "maven.compiler.outputDirectory",
88              defaultValue = "${project.build.outputDirectory}",
89              required = true,
90              readonly = false)
91      private File outputDirectory;
92  
93      /**
94       * Projects main artifact.
95       *
96       * @todo this is an export variable, really
97       */
98      @Parameter(defaultValue = "${project.artifact}", readonly = true, required = true)
99      private Artifact projectArtifact;
100 
101     /**
102      * A list of inclusion filters for the compiler.
103      */
104     @Parameter
105     private Set<String> includes = new HashSet<>();
106 
107     /**
108      * A list of exclusion filters for the compiler.
109      */
110     @Parameter
111     private Set<String> excludes = new HashSet<>();
112 
113     /**
114      * A list of exclusion filters for the incremental calculation.
115      * @since 3.11
116      */
117     @Parameter
118     private Set<String> incrementalExcludes = new HashSet<>();
119 
120     /**
121      * <p>
122      * Specify where to place generated source files created by annotation processing. Only applies to JDK 1.6+
123      * </p>
124      *
125      * @since 2.2
126      */
127     @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations")
128     private File generatedSourcesDirectory;
129 
130     /**
131      * Set this to 'true' to bypass compilation of main sources. Its use is NOT RECOMMENDED, but quite convenient on
132      * occasion.
133      */
134     @Parameter(property = "maven.main.skip")
135     private boolean skipMain;
136 
137     @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
138     private List<String> compilePath;
139 
140     /**
141      * <p>
142      * When set to {@code true}, the classes will be placed in <code>META-INF/versions/${release}</code> The release
143      * value must be set, otherwise the plugin will fail.
144      * </p>
145      * <strong>Note: </strong> A jar is only a multirelease jar if <code>META-INF/MANIFEST.MF</code> contains
146      * <code>Multi-Release: true</code>. You need to set this by configuring the <a href=
147      * "https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html">maven-jar-plugin</a>.
148      * This implies that you cannot test a multirelease jar using the outputDirectory.
149      *
150      * @since 3.7.1
151      */
152     @Parameter
153     private boolean multiReleaseOutput;
154 
155     /**
156      * when forking and debug activated the commandline used will be dumped in this file
157      * @since 3.10.0
158      */
159     @Parameter(defaultValue = "javac")
160     private String debugFileName;
161 
162     final LocationManager locationManager = new LocationManager();
163 
164     private List<String> classpathElements;
165 
166     private List<String> modulepathElements;
167 
168     private Map<String, JavaModuleDescriptor> pathElements;
169 
170     @Override
171     protected List<String> getCompileSourceRoots() {
172         return compileSourceRoots;
173     }
174 
175     @Override
176     protected List<String> getClasspathElements() {
177         return classpathElements;
178     }
179 
180     @Override
181     protected List<String> getModulepathElements() {
182         return modulepathElements;
183     }
184 
185     @Override
186     protected Map<String, JavaModuleDescriptor> getPathElements() {
187         return pathElements;
188     }
189 
190     @Override
191     protected File getOutputDirectory() {
192         File dir;
193         if (!multiReleaseOutput) {
194             dir = outputDirectory;
195         } else {
196             dir = new File(outputDirectory, "META-INF/versions/" + release);
197         }
198         return dir;
199     }
200 
201     @Override
202     public void execute() throws MojoExecutionException, CompilationFailureException {
203         if (skipMain) {
204             getLog().info("Not compiling main sources");
205             return;
206         }
207 
208         if (multiReleaseOutput && release == null) {
209             throw new MojoExecutionException("When using 'multiReleaseOutput' the release must be set");
210         }
211 
212         super.execute();
213 
214         if (outputDirectory.isDirectory()) {
215             File artifactFile = projectArtifact.getFile();
216             if (artifactFile != null && !Objects.equals(artifactFile, outputDirectory)) {
217                 getLog().warn("Overwriting artifact's file from " + artifactFile + " to " + outputDirectory);
218             }
219             projectArtifact.setFile(outputDirectory);
220         }
221     }
222 
223     @Override
224     protected Set<String> getIncludes() {
225         return includes;
226     }
227 
228     @Override
229     protected Set<String> getExcludes() {
230         return excludes;
231     }
232 
233     @Override
234     protected void preparePaths(Set<File> sourceFiles) {
235         // assert compilePath != null;
236 
237         Optional<Path> moduleDeclaration = getModuleDeclaration(sourceFiles);
238 
239         if (moduleDeclaration.isPresent()) {
240             // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
241             // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
242             // you cannot depend on this project and so it won't be distributed.
243 
244             modulepathElements = new ArrayList<>(compilePath.size());
245             classpathElements = new ArrayList<>(compilePath.size());
246             pathElements = new LinkedHashMap<>(compilePath.size());
247 
248             ResolvePathsResult<File> resolvePathsResult;
249             try {
250                 Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
251 
252                 ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts)
253                         .setIncludeStatic(true)
254                         .setMainModuleDescriptor(moduleDeclaration.get().toFile());
255 
256                 Toolchain toolchain = getToolchain();
257                 if (toolchain instanceof DefaultJavaToolChain) {
258                     request.setJdkHome(new File(((DefaultJavaToolChain) toolchain).getJavaHome()));
259                 }
260 
261                 resolvePathsResult = locationManager.resolvePaths(request);
262 
263                 for (Entry<File, Exception> pathException :
264                         resolvePathsResult.getPathExceptions().entrySet()) {
265                     Throwable cause = pathException.getValue();
266                     while (cause.getCause() != null) {
267                         cause = cause.getCause();
268                     }
269                     String fileName = pathException.getKey().getName();
270                     getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
271                 }
272 
273                 JavaModuleDescriptor moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
274 
275                 detectFilenameBasedAutomodules(resolvePathsResult, moduleDescriptor);
276 
277                 for (Map.Entry<File, JavaModuleDescriptor> entry :
278                         resolvePathsResult.getPathElements().entrySet()) {
279                     pathElements.put(entry.getKey().getPath(), entry.getValue());
280                 }
281 
282                 if (compilerArgs == null) {
283                     compilerArgs = new ArrayList<>();
284                 }
285 
286                 for (File file : resolvePathsResult.getClasspathElements()) {
287                     classpathElements.add(file.getPath());
288 
289                     if (multiReleaseOutput) {
290                         if (getOutputDirectory().toPath().startsWith(file.getPath())) {
291                             compilerArgs.add("--patch-module");
292                             compilerArgs.add(String.format("%s=%s", moduleDescriptor.name(), file.getPath()));
293                         }
294                     }
295                 }
296 
297                 for (File file : resolvePathsResult.getModulepathElements().keySet()) {
298                     modulepathElements.add(file.getPath());
299                 }
300 
301                 compilerArgs.add("--module-version");
302                 compilerArgs.add(getProject().getVersion());
303 
304             } catch (IOException e) {
305                 getLog().warn(e.getMessage());
306             }
307         } else {
308             classpathElements = new ArrayList<>();
309             for (File element : getCompileClasspathElements(getProject())) {
310                 classpathElements.add(element.getPath());
311             }
312             modulepathElements = Collections.emptyList();
313             pathElements = Collections.emptyMap();
314         }
315     }
316 
317     private void detectFilenameBasedAutomodules(
318             final ResolvePathsResult<File> resolvePathsResult, final JavaModuleDescriptor moduleDescriptor) {
319         List<String> automodulesDetected = new ArrayList<>();
320         for (Entry<File, ModuleNameSource> entry :
321                 resolvePathsResult.getModulepathElements().entrySet()) {
322             if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
323                 automodulesDetected.add(entry.getKey().getName());
324             }
325         }
326 
327         if (!automodulesDetected.isEmpty()) {
328             final String message = "Required filename-based automodules detected: "
329                     + automodulesDetected + ". "
330                     + "Please don't publish this project to a public artifact repository!";
331 
332             if (moduleDescriptor.exports().isEmpty()) {
333                 // application
334                 getLog().info(message);
335             } else {
336                 // library
337                 writeBoxedWarning(message);
338             }
339         }
340     }
341 
342     private List<File> getCompileClasspathElements(MavenProject project) {
343         // 3 is outputFolder + 2 preserved for multirelease
344         List<File> list = new ArrayList<>(project.getArtifacts().size() + 3);
345 
346         if (multiReleaseOutput) {
347             File versionsFolder = new File(project.getBuild().getOutputDirectory(), "META-INF/versions");
348 
349             // in reverse order
350             for (int version = Integer.parseInt(getRelease()) - 1; version >= 9; version--) {
351                 File versionSubFolder = new File(versionsFolder, String.valueOf(version));
352                 if (versionSubFolder.exists()) {
353                     list.add(versionSubFolder);
354                 }
355             }
356         }
357 
358         list.add(new File(project.getBuild().getOutputDirectory()));
359 
360         for (Artifact a : project.getArtifacts()) {
361             if (a.getArtifactHandler().isAddedToClasspath()) {
362                 list.add(a.getFile());
363             }
364         }
365         return list;
366     }
367 
368     @Override
369     protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
370         if (includes.isEmpty() && excludes.isEmpty() && incrementalExcludes.isEmpty()) {
371             return new StaleSourceScanner(staleMillis);
372         }
373 
374         if (includes.isEmpty()) {
375             includes.add("**/*.java");
376         }
377 
378         Set<String> excludesIncr = new HashSet<>(excludes);
379         excludesIncr.addAll(this.incrementalExcludes);
380         return new StaleSourceScanner(staleMillis, includes, excludesIncr);
381     }
382 
383     @Override
384     protected SourceInclusionScanner getSourceInclusionScanner(String inputFileEnding) {
385         // it's not defined if we get the ending with or without the dot '.'
386         String defaultIncludePattern = "**/*" + (inputFileEnding.startsWith(".") ? "" : ".") + inputFileEnding;
387 
388         if (includes.isEmpty()) {
389             includes.add(defaultIncludePattern);
390         }
391         Set<String> excludesIncr = new HashSet<>(excludes);
392         excludesIncr.addAll(excludesIncr);
393         return new SimpleSourceInclusionScanner(includes, excludesIncr);
394     }
395 
396     @Override
397     protected String getSource() {
398         return source;
399     }
400 
401     @Override
402     protected String getTarget() {
403         return target;
404     }
405 
406     @Override
407     protected String getRelease() {
408         return release;
409     }
410 
411     @Override
412     protected String getCompilerArgument() {
413         return compilerArgument;
414     }
415 
416     @Override
417     protected Map<String, String> getCompilerArguments() {
418         return compilerArguments;
419     }
420 
421     @Override
422     protected File getGeneratedSourcesDirectory() {
423         return generatedSourcesDirectory;
424     }
425 
426     @Override
427     protected String getDebugFileName() {
428         return debugFileName;
429     }
430 
431     private void writeBoxedWarning(String message) {
432         String line = StringUtils.repeat("*", message.length() + 4);
433         getLog().warn(line);
434         getLog().warn("* " + MessageUtils.buffer().strong(message) + " *");
435         getLog().warn(line);
436     }
437 }