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