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 DirectoryHierarchy.PACKAGE
271                     .outputDirectoryForReleases(outputDirectory)
272                     .resolve(release);
273         }
274         return outputDirectory;
275     }
276 
277     /**
278      * {@return the file where to dump the command-line when debug is activated or when the compilation failed}
279      *
280      * @see #debugFileName
281      */
282     @Nullable
283     @Override
284     protected String getDebugFileName() {
285         return debugFileName;
286     }
287 
288     /**
289      * Creates a new task for compiling the main classes.
290      *
291      * @param listener where to send compilation warnings, or {@code null} for the Maven logger
292      * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr>
293      * @return the task to execute for compiling the main code using the configuration in this <abbr>MOJO</abbr>
294      * @throws IOException if an error occurred while creating the output directory or scanning the source directories
295      */
296     @Override
297     public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException {
298         ToolExecutor executor = super.createExecutor(listener);
299         if (SUPPORT_LEGACY && multiReleaseOutput) {
300             addImplicitDependencies(executor);
301         }
302         return executor;
303     }
304 
305     /**
306      * {@return whether the project has at least one module-info file}
307      * If no such file is found in the code to be compiled by this <abbr>MOJO</abbr> execution,
308      * then this method searches in the multi-release codes compiled by previous executions.
309      *
310      * @param roots root directories of the sources to compile
311      * @throws IOException if this method needed to read a module descriptor and failed
312      *
313      * @deprecated For compatibility with the previous way to build multi-release JAR file.
314      *             May be removed after we drop support of the old way to do multi-release.
315      */
316     @Override
317     @Deprecated(since = "4.0.0")
318     final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
319         boolean hasModuleDeclaration = super.hasModuleDeclaration(roots);
320         if (SUPPORT_LEGACY && !hasModuleDeclaration && multiReleaseOutput) {
321             String type = project.getPackaging().type().id();
322             if (!Type.CLASSPATH_JAR.equals(type)) {
323                 for (Path p : getOutputDirectoryPerVersion().values()) {
324                     p = p.resolve(SourceDirectory.MODULE_INFO + SourceDirectory.CLASS_FILE_SUFFIX);
325                     if (Files.exists(p)) {
326                         return true;
327                     }
328                 }
329             }
330         }
331         return hasModuleDeclaration;
332     }
333 
334     /**
335      * {@return the output directory of each target Java version}
336      * By convention, {@link SourceVersion#RELEASE_0} stands for the base version.
337      *
338      * @throws IOException if this method needs to walk through directories and that operation failed
339      *
340      * @deprecated For compatibility with the previous way to build multi-release JAR file.
341      *             May be removed after we drop support of the old way to do multi-release.
342      */
343     @Deprecated(since = "4.0.0")
344     private TreeMap<SourceVersion, Path> getOutputDirectoryPerVersion() throws IOException {
345         final Path root = DirectoryHierarchy.PACKAGE.outputDirectoryForReleases(outputDirectory);
346         if (Files.notExists(root)) {
347             return null;
348         }
349         final var paths = new TreeMap<SourceVersion, Path>();
350         Files.walk(root, 1).forEach((path) -> {
351             SourceVersion version;
352             if (path.equals(root)) {
353                 path = outputDirectory;
354                 version = SourceVersion.RELEASE_0;
355             } else {
356                 try {
357                     version = SourceVersion.valueOf("RELEASE_" + path.getFileName());
358                 } catch (IllegalArgumentException e) {
359                     throw new CompilationFailureException("Invalid version number for " + path, e);
360                 }
361             }
362             if (paths.put(version, path) != null) {
363                 throw new CompilationFailureException("Duplicated version number for " + path);
364             }
365         });
366         return paths;
367     }
368 
369     /**
370      * Adds the compilation outputs of previous Java releases to the class-path of module-path.
371      * This method should be invoked only when compiling a multi-release <abbr>JAR</abbr> in the
372      * old deprecated way.
373      *
374      * <p>The {@code executor} argument may be {@code null} if the caller is only interested in the
375      * module name, with no executor to modify. The module name found by this method is specific to
376      * the way that projects are organized when {@link #multiReleaseOutput} is {@code true}.</p>
377      *
378      * @param  executor  the executor where to add implicit dependencies, or {@code null} if none
379      * @return the module name, or {@code null} if none
380      * @throws IOException if this method needs to walk through directories and that operation failed
381      *
382      * @deprecated For compatibility with the previous way to build multi-release JAR file.
383      *             May be removed after we drop support of the old way to do multi-release.
384      */
385     @Deprecated(since = "4.0.0")
386     private String addImplicitDependencies(final ToolExecutor executor) throws IOException {
387         final TreeMap<SourceVersion, Path> paths = getOutputDirectoryPerVersion();
388         /*
389          * Search for the module name. If many module-info classes are found,
390          * the most basic one (with lowest Java release number) is selected.
391          */
392         String moduleName = null;
393         for (Path path : paths.values()) {
394             path = path.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
395             if (Files.exists(path)) {
396                 try (InputStream in = Files.newInputStream(path)) {
397                     moduleName = ModuleDescriptor.read(in).name();
398                 }
399                 break;
400             }
401         }
402         /*
403          * If no module name was found in the classes compiled for previous Java releases,
404          * search in the source files for the Java release of the current compilation unit.
405          */
406         if (moduleName == null) {
407             final Stream<Path> sourceDirectories;
408             if (executor != null) {
409                 sourceDirectories = executor.sourceDirectories.stream().map(dir -> dir.root);
410             } else if (isAbsent(compileSourceRoots)) {
411                 sourceDirectories = getSourceRoots(compileScope.projectScope()).map(SourceRoot::directory);
412             } else {
413                 sourceDirectories = compileSourceRoots.stream().map(Path::of);
414             }
415             for (Path root : sourceDirectories.toList()) {
416                 moduleName = parseModuleInfoName(root.resolve(MODULE_INFO + JAVA_FILE_SUFFIX));
417                 if (moduleName != null) {
418                     break;
419                 }
420             }
421         }
422         if (executor != null) {
423             /*
424              * Add previous versions as dependencies on the class-path or module-path, depending on whether
425              * the project is modular. Each path should be on either the class-path or module-path, but not
426              * both. If a path for a modular project seems needed on the class-path, it may be a sign that
427              * other options are not used correctly (e.g., `--source-path` versus `--module-source-path`).
428              */
429             PathType type = JavaPathType.CLASSES;
430             if (moduleName != null) {
431                 type = JavaPathType.patchModule(moduleName);
432                 directoryLevelToRemove = ModuleDirectoryRemover.create(executor.outputDirectory, moduleName);
433             }
434             if (!paths.isEmpty()) {
435                 executor.dependencies(type).addAll(paths.descendingMap().values());
436             }
437         }
438         return moduleName;
439     }
440 
441     /**
442      * {@return the module name in a previous execution of the compiler plugin, or {@code null} if none}
443      *
444      * @deprecated For compatibility with the previous way to build multi-release JAR file.
445      *             May be removed after we drop support of the old way to do multi-release.
446      */
447     @Override
448     @Deprecated(since = "4.0.0")
449     final String moduleOfPreviousExecution() throws IOException {
450         if (SUPPORT_LEGACY && multiReleaseOutput) {
451             return addImplicitDependencies(null);
452         }
453         return super.moduleOfPreviousExecution();
454     }
455 }