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