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 javax.lang.model.SourceVersion;
22  import javax.tools.DiagnosticListener;
23  import javax.tools.JavaFileObject;
24  import javax.tools.OptionChecker;
25  
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.lang.module.ModuleDescriptor;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.util.List;
32  import java.util.Set;
33  import java.util.TreeMap;
34  import java.util.stream.Stream;
35  
36  import org.apache.maven.api.JavaPathType;
37  import org.apache.maven.api.PathScope;
38  import org.apache.maven.api.PathType;
39  import org.apache.maven.api.ProducedArtifact;
40  import org.apache.maven.api.SourceRoot;
41  import org.apache.maven.api.Type;
42  import org.apache.maven.api.annotations.Nonnull;
43  import org.apache.maven.api.annotations.Nullable;
44  import org.apache.maven.api.plugin.MojoException;
45  import org.apache.maven.api.plugin.annotations.Mojo;
46  import org.apache.maven.api.plugin.annotations.Parameter;
47  
48  import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
49  import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX;
50  import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
51  
52  /**
53   * Compiles application sources.
54   * Each instance shall be used only once, then discarded.
55   *
56   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
57   * @author Martin Desruisseaux
58   * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
59   * @since 2.0
60   */
61  @Mojo(name = "compile", defaultPhase = "compile")
62  public class CompilerMojo extends AbstractCompilerMojo {
63      /**
64       * Set this to {@code true} to bypass compilation of main sources.
65       * Its use is not recommended, but quite convenient on occasion.
66       */
67      @Parameter(property = "maven.main.skip")
68      protected boolean skipMain;
69  
70      /**
71       * Specify where to place generated source files created by annotation processing.
72       *
73       * @since 2.2
74       */
75      @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations")
76      protected Path generatedSourcesDirectory;
77  
78      /**
79       * A set of inclusion filters for the compiler.
80       */
81      @Parameter
82      protected Set<String> includes;
83  
84      /**
85       * A set of exclusion filters for the compiler.
86       */
87      @Parameter
88      protected Set<String> excludes;
89  
90      /**
91       * A set of exclusion filters for the incremental calculation.
92       * Updated source files, if excluded by this filter, will not cause the project to be rebuilt.
93       *
94       * <h4>Limitation</h4>
95       * In the current implementation, those exclusion filters are applied for added or removed files,
96       * but not yet for removed files.
97       *
98       * @since 3.11
99       */
100     @Parameter
101     protected Set<String> incrementalExcludes;
102 
103     /**
104      * The directory for compiled classes.
105      *
106      * @see #getOutputDirectory()
107      */
108     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
109     protected Path outputDirectory;
110 
111     /**
112      * Projects main artifact.
113      */
114     @Parameter(defaultValue = "${project.mainArtifact}", readonly = true, required = true)
115     protected ProducedArtifact projectArtifact;
116 
117     /**
118      * When set to {@code true}, the classes will be placed in {@code META-INF/versions/${release}}.
119      * <p>
120      * <strong>Note:</strong> A jar is only a multi-release jar if {@code META-INF/MANIFEST.MF} contains
121      * {@code Multi-Release: true}. You need to set this by configuring the <a href=
122      * "https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html">maven-jar-plugin</a>.
123      * This implies that you cannot test a multi-release jar using the {@link #outputDirectory}.
124      * </p>
125      *
126      * @since 3.7.1
127      *
128      * @deprecated Replaced by specifying the {@code <targetVersion>} value inside a {@code <source>} element.
129      */
130     @Parameter
131     @Deprecated(since = "4.0.0")
132     protected boolean multiReleaseOutput;
133 
134     /**
135      * The file where to dump the command-line when debug is activated or when the compilation failed.
136      * For example, if the value is {@code "javac"}, then the Java compiler can be launched from the
137      * command-line by typing {@code javac @target/javac.args}.
138      * The debug file will contain the compiler options together with the list of source files to compile.
139      *
140      * <p>By default, this debug file is written only if the compilation of main code failed.
141      * The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true}
142      * or by specifying the {@code --verbose} option to Maven on the command-line.</p>
143      *
144      * @since 3.10.0
145      */
146     @Parameter(defaultValue = "javac.args")
147     protected String debugFileName;
148 
149     /**
150      * Target directory that have been temporarily created as symbolic link before compilation.
151      * This is used as a workaround for the fact that, when compiling a modular project with
152      * all the module-related compiler options, the classes are written in a directory with
153      * the module name. It does not fit in the {@code META-INF/versions/<release>} pattern.
154      * Temporary symbolic link is a workaround for this problem.
155      *
156      * <h4>Example</h4>
157      * When compiling the {@code my.app} module for Java 17, the desired output directory is:
158      *
159      * <blockquote>{@code target/classes/META-INF/versions/17}</blockquote>
160      *
161      * But {@code javac}, when used with the {@code --module-source-path} option,
162      * will write the classes in the following directory:
163      *
164      * <blockquote>{@code target/classes/META-INF/versions/17/my.app}</blockquote>
165      *
166      * We workaround this problem with a symbolic link which redirects {@code 17/my.app} to {@code 17}.
167      * We need to do this only when compiling multi-release project in the old deprecated way.
168      * When using the recommended {@code <sources>} approach, the plugins are designed to work
169      * with the directory layout produced by {@code javac} instead of fighting against it.
170      *
171      * @deprecated For compatibility with the previous way to build multi-release JAR file.
172      *             May be removed after we drop support of the old way to do multi-release.
173      */
174     @Deprecated(since = "4.0.0")
175     private ModuleDirectoryRemover directoryLevelToRemove;
176 
177     /**
178      * Creates a new compiler <abbr>MOJO</abbr> for the main code.
179      */
180     public CompilerMojo() {
181         super(PathScope.MAIN_COMPILE);
182     }
183 
184     /**
185      * Runs the Java compiler on the main source code.
186      * If {@link #skipMain} is {@code true}, then this method logs a message and does nothing else.
187      * Otherwise, this method executes the steps described in the method of the parent class.
188      *
189      * @throws MojoException if the compiler cannot be run.
190      */
191     @Override
192     public void execute() throws MojoException {
193         if (skipMain) {
194             logger.info("Not compiling main sources");
195             return;
196         }
197         try {
198             super.execute();
199         } finally {
200             try (ModuleDirectoryRemover r = directoryLevelToRemove) {
201                 // Implicit call to directoryLevelToRemove.close().
202             } catch (IOException e) {
203                 throw new CompilationFailureException("I/O error while organizing multi-release classes.", e);
204             }
205         }
206         @SuppressWarnings("LocalVariableHidesMemberVariable")
207         Path outputDirectory = getOutputDirectory();
208         if (Files.isDirectory(outputDirectory) && projectArtifact != null) {
209             artifactManager.setPath(projectArtifact, outputDirectory);
210         }
211     }
212 
213     /**
214      * Parses the parameters declared in the <abbr>MOJO</abbr>.
215      *
216      * @param  compiler  the tools to use for verifying the validity of options
217      * @return the options after validation
218      */
219     @Override
220     @SuppressWarnings("deprecation")
221     public Options parseParameters(final OptionChecker compiler) {
222         Options configuration = super.parseParameters(compiler);
223         configuration.addUnchecked(compilerArgs);
224         configuration.addUnchecked(compilerArgument);
225         return configuration;
226     }
227 
228     /**
229      * {@return the path where to place generated source files created by annotation processing on the main classes}
230      */
231     @Nullable
232     @Override
233     protected Path getGeneratedSourcesDirectory() {
234         return generatedSourcesDirectory;
235     }
236 
237     /**
238      * {@return the inclusion filters for the compiler, or an empty set for all Java source files}
239      */
240     @Override
241     protected Set<String> getIncludes() {
242         return (includes != null) ? includes : Set.of();
243     }
244 
245     /**
246      * {@return the exclusion filters for the compiler, or an empty set if none}
247      */
248     @Override
249     protected Set<String> getExcludes() {
250         return (excludes != null) ? excludes : Set.of();
251     }
252 
253     /**
254      * {@return the exclusion filters for the incremental calculation, or an empty set if none}
255      */
256     @Override
257     protected Set<String> getIncrementalExcludes() {
258         return (incrementalExcludes != null) ? incrementalExcludes : Set.of();
259     }
260 
261     /**
262      * {@return the destination directory for main class files}
263      * If {@link #multiReleaseOutput} is true <em>(deprecated)</em>,
264      * the output will be in a {@code META-INF/versions} subdirectory.
265      */
266     @Nonnull
267     @Override
268     protected Path getOutputDirectory() {
269         if (SUPPORT_LEGACY && multiReleaseOutput && release != null) {
270             return SourceDirectory.outputDirectoryForReleases(outputDirectory).resolve(release);
271         }
272         return outputDirectory;
273     }
274 
275     /**
276      * {@return the file where to dump the command-line when debug is activated or when the compilation failed}
277      *
278      * @see #debugFileName
279      */
280     @Nullable
281     @Override
282     protected String getDebugFileName() {
283         return debugFileName;
284     }
285 
286     /**
287      * Creates a new task for compiling the main classes.
288      *
289      * @param listener where to send compilation warnings, or {@code null} for the Maven logger
290      * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr>
291      * @return the task to execute for compiling the main code using the configuration in this <abbr>MOJO</abbr>
292      * @throws IOException if an error occurred while creating the output directory or scanning the source directories
293      */
294     @Override
295     public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException {
296         ToolExecutor executor = super.createExecutor(listener);
297         if (SUPPORT_LEGACY && multiReleaseOutput) {
298             addImplicitDependencies(executor);
299         }
300         return executor;
301     }
302 
303     /**
304      * {@return whether the project has at least one module-info file}
305      * If no such file is found in the code to be compiled by this <abbr>MOJO</abbr> execution,
306      * then this method searches in the multi-release codes compiled by previous executions.
307      *
308      * @param roots root directories of the sources to compile
309      * @throws IOException if this method needed to read a module descriptor and failed
310      *
311      * @deprecated For compatibility with the previous way to build multi-release JAR file.
312      *             May be removed after we drop support of the old way to do multi-release.
313      */
314     @Override
315     @Deprecated(since = "4.0.0")
316     final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
317         boolean hasModuleDeclaration = super.hasModuleDeclaration(roots);
318         if (SUPPORT_LEGACY && !hasModuleDeclaration && multiReleaseOutput) {
319             String type = project.getPackaging().type().id();
320             if (!Type.CLASSPATH_JAR.equals(type)) {
321                 for (Path p : getOutputDirectoryPerVersion().values()) {
322                     p = p.resolve(SourceDirectory.MODULE_INFO + SourceDirectory.CLASS_FILE_SUFFIX);
323                     if (Files.exists(p)) {
324                         return true;
325                     }
326                 }
327             }
328         }
329         return hasModuleDeclaration;
330     }
331 
332     /**
333      * {@return the output directory of each target Java version}
334      * By convention, {@link SourceVersion#RELEASE_0} stands for the base version.
335      *
336      * @throws IOException if this method needs to walk through directories and that operation failed
337      *
338      * @deprecated For compatibility with the previous way to build multi-release JAR file.
339      *             May be removed after we drop support of the old way to do multi-release.
340      */
341     @Deprecated(since = "4.0.0")
342     private TreeMap<SourceVersion, Path> getOutputDirectoryPerVersion() throws IOException {
343         final Path root = SourceDirectory.outputDirectoryForReleases(outputDirectory);
344         if (Files.notExists(root)) {
345             return null;
346         }
347         final var paths = new TreeMap<SourceVersion, Path>();
348         Files.walk(root, 1).forEach((path) -> {
349             SourceVersion version;
350             if (path.equals(root)) {
351                 path = outputDirectory;
352                 version = SourceVersion.RELEASE_0;
353             } else {
354                 try {
355                     version = SourceVersion.valueOf("RELEASE_" + path.getFileName());
356                 } catch (IllegalArgumentException e) {
357                     throw new CompilationFailureException("Invalid version number for " + path, e);
358                 }
359             }
360             if (paths.put(version, path) != null) {
361                 throw new CompilationFailureException("Duplicated version number for " + path);
362             }
363         });
364         return paths;
365     }
366 
367     /**
368      * Adds the compilation outputs of previous Java releases to the class-path ot module-path.
369      * This method should be invoked only when compiling a multi-release <abbr>JAR</abbr> in the
370      * old deprecated way.
371      *
372      * <p>The {@code executor} argument may be {@code null} if the caller is only interested in the
373      * module name, with no executor to modify. The module name found by this method is specific to
374      * the way that projects are organized when {@link #multiReleaseOutput} is {@code true}.</p>
375      *
376      * @param  executor  the executor where to add implicit dependencies, or {@code null} if none
377      * @return the module name, or {@code null} if none
378      * @throws IOException if this method needs to walk through directories and that operation failed
379      *
380      * @deprecated For compatibility with the previous way to build multi-release JAR file.
381      *             May be removed after we drop support of the old way to do multi-release.
382      */
383     @Deprecated(since = "4.0.0")
384     private String addImplicitDependencies(final ToolExecutor executor) throws IOException {
385         final TreeMap<SourceVersion, Path> paths = getOutputDirectoryPerVersion();
386         /*
387          * Search for the module name. If many module-info classes are found,
388          * the most basic one (with lowest Java release number) is selected.
389          */
390         String moduleName = null;
391         for (Path path : paths.values()) {
392             path = path.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
393             if (Files.exists(path)) {
394                 try (InputStream in = Files.newInputStream(path)) {
395                     moduleName = ModuleDescriptor.read(in).name();
396                 }
397                 break;
398             }
399         }
400         /*
401          * If no module name was found in the classes compiled for previous Java releases,
402          * search in the source files for the Java release of the current compilation unit.
403          */
404         if (moduleName == null) {
405             final Stream<Path> sourceDirectories;
406             if (executor != null) {
407                 sourceDirectories = executor.sourceDirectories.stream().map(dir -> dir.root);
408             } else if (compileSourceRoots == null || compileSourceRoots.isEmpty()) {
409                 sourceDirectories = getSourceRoots(compileScope.projectScope()).map(SourceRoot::directory);
410             } else {
411                 sourceDirectories = compileSourceRoots.stream().map(Path::of);
412             }
413             for (Path root : sourceDirectories.toList()) {
414                 moduleName = parseModuleInfoName(root.resolve(MODULE_INFO + JAVA_FILE_SUFFIX));
415                 if (moduleName != null) {
416                     break;
417                 }
418             }
419         }
420         if (executor != null) {
421             /*
422              * Add previous versions as dependencies on the class-path or module-path, depending on whether
423              * the project is modular. Each path should be on either the class-path or module-path, but not
424              * both. If a path for a modular project seems needed on the class-path, it may be a sign that
425              * other options are not used correctly (e.g., `--source-path` versus `--module-source-path`).
426              */
427             PathType type = JavaPathType.CLASSES;
428             if (moduleName != null) {
429                 type = JavaPathType.patchModule(moduleName);
430                 directoryLevelToRemove = ModuleDirectoryRemover.create(executor.outputDirectory, moduleName);
431             }
432             if (!paths.isEmpty()) {
433                 executor.dependencies(type).addAll(paths.descendingMap().values());
434             }
435         }
436         return moduleName;
437     }
438 
439     /**
440      * {@return the module name in a previous execution of the compiler plugin, or {@code null} if none}
441      *
442      * @deprecated For compatibility with the previous way to build multi-release JAR file.
443      *             May be removed after we drop support of the old way to do multi-release.
444      */
445     @Override
446     @Deprecated(since = "4.0.0")
447     final String moduleOfPreviousExecution() throws IOException {
448         if (SUPPORT_LEGACY && multiReleaseOutput) {
449             return addImplicitDependencies(null);
450         }
451         return super.moduleOfPreviousExecution();
452     }
453 }