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     final LocationManager locationManager = new LocationManager();
176 
177     private List<String> classpathElements;
178 
179     private List<String> modulepathElements;
180 
181     private Map<String, JavaModuleDescriptor> pathElements;
182 
183     @Override
184     protected List<String> getCompileSourceRoots() {
185         return compileSourceRoots;
186     }
187 
188     @Override
189     protected List<String> getClasspathElements() {
190         return classpathElements;
191     }
192 
193     @Override
194     protected List<String> getModulepathElements() {
195         return modulepathElements;
196     }
197 
198     @Override
199     protected Map<String, JavaModuleDescriptor> getPathElements() {
200         return pathElements;
201     }
202 
203     @Override
204     protected File getOutputDirectory() {
205         File dir;
206         if (!multiReleaseOutput) {
207             dir = outputDirectory;
208         } else {
209             dir = new File(outputDirectory, "META-INF/versions/" + release);
210         }
211         return dir;
212     }
213 
214     @Override
215     public void execute() throws MojoExecutionException, CompilationFailureException {
216         if (skipMain) {
217             getLog().info("Not compiling main sources");
218             return;
219         }
220 
221         if (multiReleaseOutput && release == null) {
222             throw new MojoExecutionException("When using 'multiReleaseOutput' the release must be set");
223         }
224 
225         super.execute();
226 
227         if (outputDirectory.isDirectory()) {
228             File artifactFile = projectArtifact.getFile();
229             if (artifactFile != null && !Objects.equals(artifactFile, outputDirectory)) {
230                 getLog().warn("Overwriting artifact's file from " + artifactFile + " to " + outputDirectory);
231             }
232             projectArtifact.setFile(outputDirectory);
233         }
234     }
235 
236     @Override
237     protected Set<String> getIncludes() {
238         return includes;
239     }
240 
241     @Override
242     protected Set<String> getExcludes() {
243         return excludes;
244     }
245 
246     @Override
247     protected void preparePaths(Set<File> sourceFiles) {
248         // assert compilePath != null;
249 
250         Optional<Path> moduleDeclaration = getModuleDeclaration(sourceFiles);
251 
252         if (moduleDeclaration.isPresent()) {
253             // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
254             // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
255             // you cannot depend on this project and so it won't be distributed.
256 
257             modulepathElements = new ArrayList<>(compilePath.size());
258             classpathElements = new ArrayList<>(compilePath.size());
259             pathElements = new LinkedHashMap<>(compilePath.size());
260 
261             ResolvePathsResult<File> resolvePathsResult;
262             try {
263                 Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
264 
265                 ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts)
266                         .setIncludeStatic(true)
267                         .setMainModuleDescriptor(moduleDeclaration.get().toFile());
268 
269                 Toolchain toolchain = getToolchain();
270                 if (toolchain instanceof DefaultJavaToolChain) {
271                     request.setJdkHome(new File(((DefaultJavaToolChain) toolchain).getJavaHome()));
272                 }
273 
274                 resolvePathsResult = locationManager.resolvePaths(request);
275 
276                 for (Entry<File, Exception> pathException :
277                         resolvePathsResult.getPathExceptions().entrySet()) {
278                     Throwable cause = pathException.getValue();
279                     while (cause.getCause() != null) {
280                         cause = cause.getCause();
281                     }
282                     String fileName = pathException.getKey().getName();
283                     getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
284                 }
285 
286                 JavaModuleDescriptor moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
287 
288                 detectFilenameBasedAutomodules(resolvePathsResult, moduleDescriptor);
289 
290                 for (Map.Entry<File, JavaModuleDescriptor> entry :
291                         resolvePathsResult.getPathElements().entrySet()) {
292                     pathElements.put(entry.getKey().getPath(), entry.getValue());
293                 }
294 
295                 if (compilerArgs == null) {
296                     compilerArgs = new ArrayList<>();
297                 }
298 
299                 for (File file : resolvePathsResult.getClasspathElements()) {
300                     classpathElements.add(file.getPath());
301 
302                     if (multiReleaseOutput) {
303                         if (getOutputDirectory().toPath().startsWith(file.getPath())) {
304                             compilerArgs.add("--patch-module");
305                             compilerArgs.add(String.format("%s=%s", moduleDescriptor.name(), file.getPath()));
306                         }
307                     }
308                 }
309 
310                 for (File file : resolvePathsResult.getModulepathElements().keySet()) {
311                     modulepathElements.add(file.getPath());
312                 }
313 
314                 compilerArgs.add("--module-version");
315                 compilerArgs.add(moduleVersion);
316 
317             } catch (IOException e) {
318                 getLog().warn(e.getMessage());
319             }
320         } else {
321             classpathElements = new ArrayList<>();
322             for (File element : getCompileClasspathElements(getProject())) {
323                 classpathElements.add(element.getPath());
324             }
325             modulepathElements = Collections.emptyList();
326             pathElements = Collections.emptyMap();
327         }
328     }
329 
330     private void detectFilenameBasedAutomodules(
331             final ResolvePathsResult<File> resolvePathsResult, final JavaModuleDescriptor moduleDescriptor) {
332         List<String> automodulesDetected = new ArrayList<>();
333         for (Entry<File, ModuleNameSource> entry :
334                 resolvePathsResult.getModulepathElements().entrySet()) {
335             if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
336                 automodulesDetected.add(entry.getKey().getName());
337             }
338         }
339 
340         if (!automodulesDetected.isEmpty()) {
341             final String message = "Required filename-based automodules detected: "
342                     + automodulesDetected + ". "
343                     + "Please don't publish this project to a public artifact repository!";
344 
345             if (moduleDescriptor.exports().isEmpty()) {
346                 // application
347                 getLog().info(message);
348             } else {
349                 // library
350                 writeBoxedWarning(message);
351             }
352         }
353     }
354 
355     private List<File> getCompileClasspathElements(MavenProject project) {
356         // 3 is outputFolder + 2 preserved for multirelease
357         List<File> list = new ArrayList<>(project.getArtifacts().size() + 3);
358 
359         if (multiReleaseOutput) {
360             File versionsFolder = new File(project.getBuild().getOutputDirectory(), "META-INF/versions");
361 
362             // in reverse order
363             for (int version = Integer.parseInt(getRelease()) - 1; version >= 9; version--) {
364                 File versionSubFolder = new File(versionsFolder, String.valueOf(version));
365                 if (versionSubFolder.exists()) {
366                     list.add(versionSubFolder);
367                 }
368             }
369         }
370 
371         list.add(new File(project.getBuild().getOutputDirectory()));
372 
373         for (Artifact a : project.getArtifacts()) {
374             if (a.getArtifactHandler().isAddedToClasspath()) {
375                 list.add(a.getFile());
376             }
377         }
378         return list;
379     }
380 
381     @Override
382     protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
383         if (includes.isEmpty() && excludes.isEmpty() && incrementalExcludes.isEmpty()) {
384             return new StaleSourceScanner(staleMillis);
385         }
386 
387         if (includes.isEmpty()) {
388             includes.add("**/*.java");
389         }
390 
391         Set<String> excludesIncr = new HashSet<>(excludes);
392         excludesIncr.addAll(this.incrementalExcludes);
393         return new StaleSourceScanner(staleMillis, includes, excludesIncr);
394     }
395 
396     @Override
397     protected SourceInclusionScanner getSourceInclusionScanner(String inputFileEnding) {
398         // it's not defined if we get the ending with or without the dot '.'
399         String defaultIncludePattern = "**/*" + (inputFileEnding.startsWith(".") ? "" : ".") + inputFileEnding;
400 
401         if (includes.isEmpty()) {
402             includes.add(defaultIncludePattern);
403         }
404         Set<String> excludesIncr = new HashSet<>(excludes);
405         excludesIncr.addAll(excludesIncr);
406         return new SimpleSourceInclusionScanner(includes, excludesIncr);
407     }
408 
409     @Override
410     protected String getSource() {
411         return source;
412     }
413 
414     @Override
415     protected String getTarget() {
416         return target;
417     }
418 
419     @Override
420     protected String getRelease() {
421         return release;
422     }
423 
424     @Override
425     protected String getCompilerArgument() {
426         return compilerArgument;
427     }
428 
429     @Override
430     protected Map<String, String> getCompilerArguments() {
431         return compilerArguments;
432     }
433 
434     @Override
435     protected File getGeneratedSourcesDirectory() {
436         return generatedSourcesDirectory;
437     }
438 
439     @Override
440     protected String getDebugFileName() {
441         return debugFileName;
442     }
443 
444     private void writeBoxedWarning(String message) {
445         String line = StringUtils.repeat("*", message.length() + 4);
446         getLog().warn(line);
447         getLog().warn("* " + MessageUtils.buffer().strong(message) + " *");
448         getLog().warn(line);
449     }
450 }