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.JavaCompiler;
24 import javax.tools.JavaFileManager;
25 import javax.tools.JavaFileObject;
26 import javax.tools.OptionChecker;
27 import javax.tools.ToolProvider;
28
29 import java.io.BufferedReader;
30 import java.io.BufferedWriter;
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.StreamTokenizer;
35 import java.io.StringWriter;
36 import java.io.UncheckedIOException;
37 import java.nio.charset.Charset;
38 import java.nio.charset.UnsupportedCharsetException;
39 import java.nio.file.Files;
40 import java.nio.file.Path;
41 import java.util.Arrays;
42 import java.util.Collection;
43 import java.util.EnumSet;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Optional;
47 import java.util.ServiceLoader;
48 import java.util.Set;
49 import java.util.StringJoiner;
50 import java.util.jar.Attributes;
51 import java.util.jar.JarFile;
52 import java.util.jar.Manifest;
53 import java.util.stream.Stream;
54
55 import org.apache.maven.api.JavaPathType;
56 import org.apache.maven.api.Language;
57 import org.apache.maven.api.PathScope;
58 import org.apache.maven.api.PathType;
59 import org.apache.maven.api.Project;
60 import org.apache.maven.api.ProjectScope;
61 import org.apache.maven.api.Session;
62 import org.apache.maven.api.SourceRoot;
63 import org.apache.maven.api.Toolchain;
64 import org.apache.maven.api.Type;
65 import org.apache.maven.api.annotations.Nonnull;
66 import org.apache.maven.api.annotations.Nullable;
67 import org.apache.maven.api.di.Inject;
68 import org.apache.maven.api.plugin.Log;
69 import org.apache.maven.api.plugin.Mojo;
70 import org.apache.maven.api.plugin.MojoException;
71 import org.apache.maven.api.plugin.annotations.Parameter;
72 import org.apache.maven.api.services.ArtifactManager;
73 import org.apache.maven.api.services.DependencyResolver;
74 import org.apache.maven.api.services.DependencyResolverRequest;
75 import org.apache.maven.api.services.DependencyResolverResult;
76 import org.apache.maven.api.services.MavenException;
77 import org.apache.maven.api.services.MessageBuilder;
78 import org.apache.maven.api.services.MessageBuilderFactory;
79 import org.apache.maven.api.services.ProjectManager;
80 import org.apache.maven.api.services.ToolchainManager;
81
82 import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
83 import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
84
85 /**
86 * Base class of Mojos compiling Java source code.
87 * This plugin uses the {@link JavaCompiler} interface from JDK 6+.
88 * Each instance shall be used only once, then discarded.
89 *
90 * <h2>Thread-safety</h2>
91 * This class is not thread-safe. If this class is used in a multi-thread context,
92 * users are responsible for synchronizing all accesses to this <abbr>MOJO</abbr> instance.
93 * However, the executor returned by {@link #createExecutor(DiagnosticListener)} can safely
94 * launch the compilation in a background thread.
95 *
96 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl</a>
97 * @author Martin Desruisseaux
98 * @since 2.0
99 */
100 public abstract class AbstractCompilerMojo implements Mojo {
101 /**
102 * Whether to support legacy (and often deprecated) behavior.
103 * This is currently hard-coded to {@code true} for compatibility reason.
104 * TODO: consider making configurable.
105 */
106 static final boolean SUPPORT_LEGACY = true;
107
108 /**
109 * The executable to use by default if nine is specified.
110 */
111 private static final String DEFAULT_EXECUTABLE = "javac";
112
113 // ----------------------------------------------------------------------
114 // Configurables
115 // ----------------------------------------------------------------------
116
117 /**
118 * The {@code --module-version} argument for the Java compiler.
119 * This is ignored if not applicable, e.g., in non-modular projects.
120 *
121 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-module-version">javac --module-version</a>
122 * @since 4.0.0
123 */
124 @Parameter(property = "maven.compiler.moduleVersion", defaultValue = "${project.version}")
125 protected String moduleVersion;
126
127 /**
128 * The {@code -encoding} argument for the Java compiler.
129 *
130 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-encoding">javac -encoding</a>
131 * @since 2.1
132 */
133 @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
134 protected String encoding;
135
136 /**
137 * {@return the character set used for decoding bytes, or null for the platform default}
138 * No warning is emitted in the latter case because as of Java 18, the default is UTF-8,
139 * i.e. the encoding is no longer platform-dependent.
140 */
141 final Charset charset() {
142 if (encoding != null) {
143 try {
144 return Charset.forName(encoding);
145 } catch (UnsupportedCharsetException e) {
146 throw new CompilationFailureException("Invalid 'encoding' option: " + encoding, e);
147 }
148 }
149 return null;
150 }
151
152 /**
153 * The {@code --source} argument for the Java compiler.
154 * <p><b>Notes:</b></p>
155 * <ul>
156 * <li>Since 3.8.0 the default value has changed from 1.5 to 1.6.</li>
157 * <li>Since 3.9.0 the default value has changed from 1.6 to 1.7.</li>
158 * <li>Since 3.11.0 the default value has changed from 1.7 to 1.8.</li>
159 * <li>Since 4.0.0-beta-2 the default value has been removed.
160 * As of Java 9, the {@link #release} parameter is preferred.</li>
161 * </ul>
162 *
163 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-source">javac --source</a>
164 */
165 @Parameter(property = "maven.compiler.source")
166 protected String source;
167
168 /**
169 * The {@code --target} argument for the Java compiler.
170 * <p><b>Notes:</b></p>
171 * <ul>
172 * <li>Since 3.8.0 the default value has changed from 1.5 to 1.6.</li>
173 * <li>Since 3.9.0 the default value has changed from 1.6 to 1.7.</li>
174 * <li>Since 3.11.0 the default value has changed from 1.7 to 1.8.</li>
175 * <li>Since 4.0.0-beta-2 the default value has been removed.
176 * As of Java 9, the {@link #release} parameter is preferred.</li>
177 * </ul>
178 *
179 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-target">javac --target</a>
180 */
181 @Parameter(property = "maven.compiler.target")
182 protected String target;
183
184 /**
185 * The {@code --release} argument for the Java compiler when the sources do not declare this version.
186 * The suggested way to declare the target Java release is to specify it with the sources like below:
187 *
188 * <pre>{@code
189 * <build>
190 * <sources>
191 * <source>
192 * <directory>src/main/java</directory>
193 * <targetVersion>17</targetVersion>
194 * </source>
195 * </sources>
196 * </build>}</pre>
197 *
198 * If such {@code <targetVersion>} element is found, it has precedence over this {@code release} property.
199 * If a source does not declare a target Java version, then the value of this {@code release} property is
200 * used as a fallback.
201 * If omitted, the compiler will generate bytecodes for the Java version running the compiler.
202 *
203 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-release">javac --release</a>
204 * @since 3.6
205 */
206 @Parameter(property = "maven.compiler.release")
207 protected String release;
208
209 /**
210 * Whether {@link #target} or {@link #release} has a non-blank value.
211 * Used for logging a warning if no target Java version was specified.
212 */
213 private boolean targetOrReleaseSet;
214
215 /**
216 * The highest version supported by the compiler, or {@code null} if not yet determined.
217 *
218 * @see #isVersionEqualOrNewer(String)
219 */
220 private SourceVersion supportedVersion;
221
222 /**
223 * Whether to enable preview language features of the java compiler.
224 * If {@code true}, then the {@code --enable-preview} option will be added to compiler arguments.
225 *
226 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-enable-preview">javac --enable-preview</a>
227 * @since 3.10.1
228 */
229 @Parameter(property = "maven.compiler.enablePreview", defaultValue = "false")
230 protected boolean enablePreview;
231
232 /**
233 * The root directories containing the source files to be compiled. If {@code null} or empty,
234 * the directories will be obtained from the {@code <Source>} elements declared in the project.
235 * If non-empty, the project {@code <Source>} elements are ignored. This configuration option
236 * should be used only when there is a need to override the project configuration.
237 *
238 * @deprecated Replaced by the project-wide {@code <sources>} element.
239 */
240 @Parameter
241 @Deprecated(since = "4.0.0")
242 protected List<String> compileSourceRoots;
243
244 /**
245 * Additional arguments to be passed verbatim to the Java compiler. This parameter can be used when
246 * the Maven compiler plugin does not provide a parameter for a Java compiler option. It may happen,
247 * for example, for new or preview Java features which are not yet handled by this compiler plugin.
248 *
249 * <p>If an option has a value, the option and the value shall be specified in two separated {@code <arg>}
250 * elements. For example, the {@code -Xmaxerrs 1000} option (for setting the maximal number of errors to
251 * 1000) can be specified as below (together with other options):</p>
252 *
253 * <pre>{@code
254 * <compilerArgs>
255 * <arg>-Xlint</arg>
256 * <arg>-Xmaxerrs</arg>
257 * <arg>1000</arg>
258 * <arg>J-Duser.language=en_us</arg>
259 * </compilerArgs>}</pre>
260 *
261 * Note that {@code -J} options should be specified only if {@link #fork} is set to {@code true}.
262 * Other options can be specified regardless the {@link #fork} value.
263 * The compiler plugin does not verify whether the arguments given through this parameter are valid.
264 * For this reason, the other parameters provided by the compiler plugin should be preferred when
265 * they exist, because the plugin checks whether the corresponding options are supported.
266 *
267 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-J">javac -J</a>
268 * @since 3.1
269 */
270 @Parameter
271 protected List<String> compilerArgs;
272
273 /**
274 * The single argument string to be passed to the compiler. To pass multiple arguments such as
275 * {@code -Xmaxerrs 1000} (which are actually two arguments), {@link #compilerArgs} is preferred.
276 *
277 * <p>Note that {@code -J} options should be specified only if {@link #fork} is set to {@code true}.</p>
278 *
279 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-J">javac -J</a>
280 *
281 * @deprecated Use {@link #compilerArgs} instead.
282 */
283 @Parameter
284 @Deprecated(since = "4.0.0")
285 protected String compilerArgument;
286
287 /**
288 * Configures if annotation processing and/or compilation are performed by the compiler.
289 * If set, the value will be appended to the {@code -proc:} compiler option.
290 *
291 * Possible values are:
292 * <ul>
293 * <li>{@code none} – no annotation processing is performed, only compilation is done.</li>
294 * <li>{@code only} – only annotation processing is done, no compilation.</li>
295 * <li>{@code full} – annotation processing followed by compilation is done.</li>
296 * </ul>
297 *
298 * The default value depends on the JDK used for the build.
299 * Prior to Java 23, the default was {@code full},
300 * so annotation processing and compilation were executed without explicit configuration.
301 *
302 * For security reasons, starting with Java 23 no annotation processing is done if neither
303 * any {@code -processor}, {@code -processor path} or {@code -processor module} are set,
304 * or either {@code only} or {@code full} is set.
305 * So literally the default is {@code none}.
306 * It is recommended to always list the annotation processors you want to execute
307 * instead of using the {@code proc} configuration,
308 * to ensure that only desired processors are executed and not any "hidden" (and maybe malicious).
309 *
310 * @see #annotationProcessors
311 * @see <a href="https://inside.java/2024/06/18/quality-heads-up/">Inside Java 2024-06-18 Quality Heads up</a>
312 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-proc">javac -proc</a>
313 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#annotation-processing">javac Annotation Processing</a>
314 * @since 2.2
315 */
316 @Parameter(property = "maven.compiler.proc")
317 protected String proc;
318 // Reminder: if above list of legal values is modified, update also addComaSeparated("-proc", …)
319
320 /**
321 * Class names of annotation processors to run.
322 * If not set, the default annotation processors discovery process applies.
323 * If set, the value will be appended to the {@code -processor} compiler option.
324 *
325 * @see #proc
326 * @since 2.2
327 */
328 @Parameter
329 protected String[] annotationProcessors;
330
331 /**
332 * Classpath elements to supply as annotation processor path. If specified, the compiler will detect annotation
333 * processors only in those classpath elements. If omitted (and {@code proc} is set to {@code only} or {@code full}), the default classpath is used to detect annotation
334 * processors. The detection itself depends on the configuration of {@link #annotationProcessors}.
335 * Since JDK 23 by default no annotation processing is performed as long as no processors is listed for security reasons.
336 * Therefore, you should always list the desired processors using this configuration element or {@code annotationProcessorPaths}.
337 *
338 * <p>
339 * Each classpath element is specified using their Maven coordinates (groupId, artifactId, version, classifier,
340 * type). Transitive dependencies are added automatically. Exclusions are supported as well. Example:
341 * </p>
342 *
343 * <pre>
344 * <configuration>
345 * <annotationProcessorPaths>
346 * <path>
347 * <groupId>org.sample</groupId>
348 * <artifactId>sample-annotation-processor</artifactId>
349 * <version>1.2.3</version> <!-- Optional - taken from dependency management if not specified -->
350 * <!-- Optionally exclude transitive dependencies -->
351 * <exclusions>
352 * <exclusion>
353 * <groupId>org.sample</groupId>
354 * <artifactId>sample-dependency</artifactId>
355 * </exclusion>
356 * </exclusions>
357 * </path>
358 * <!-- ... more ... -->
359 * </annotationProcessorPaths>
360 * </configuration>
361 * </pre>
362 *
363 * <b>Note:</b> Exclusions are supported from version 3.11.0.
364 *
365 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-processor-path">javac -processorpath</a>
366 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#annotation-processing">javac Annotation Processing</a>
367 * @since 3.5
368 *
369 * @deprecated Replaced by ordinary dependencies with {@code <type>} element set to
370 * {@code processor}, {@code classpath-processor} or {@code modular-processor}.
371 */
372 @Parameter
373 @Deprecated(since = "4.0.0")
374 protected List<DependencyCoordinate> annotationProcessorPaths;
375
376 /**
377 * Whether to use the Maven dependency management section when resolving transitive dependencies of annotation
378 * processor paths.
379 * <p>
380 * This flag does not enable / disable the ability to resolve the version of annotation processor paths
381 * from dependency management section. It only influences the resolution of transitive dependencies of those
382 * top-level paths.
383 * </p>
384 *
385 * @since 3.12.0
386 *
387 * @deprecated This flag is ignored.
388 * Replaced by ordinary dependencies with {@code <type>} element set to
389 * {@code processor}, {@code classpath-processor} or {@code modular-processor}.
390 */
391 @Deprecated(since = "4.0.0")
392 @Parameter(defaultValue = "false")
393 protected boolean annotationProcessorPathsUseDepMgmt;
394
395 /**
396 * Whether to generate {@code package-info.class} even when empty.
397 * By default, package info source files that only contain javadoc and no annotation
398 * on the package can lead to no class file being generated by the compiler.
399 * It may cause a file miss on build systems that check for file existence in order to decide what to recompile.
400 *
401 * <p>If {@code true}, the {@code -Xpkginfo:always} compiler option is added if the compiler supports that
402 * extra option. If the extra option is not supported, then a warning is logged and no option is added to
403 * the compiler arguments.</p>
404 *
405 * @see #incrementalCompilation
406 * @since 3.10
407 */
408 @Parameter(property = "maven.compiler.createMissingPackageInfoClass", defaultValue = "false")
409 protected boolean createMissingPackageInfoClass;
410
411 /**
412 * Whether to generate class files for implicitly referenced files.
413 * If set, the value will be appended to the {@code -implicit:} compiler option.
414 * Standard values are:
415 * <ul>
416 * <li>{@code class} – automatically generates class files.</li>
417 * <li>{@code none} – suppresses class file generation.</li>
418 * </ul>
419 *
420 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-implicit">javac -implicit</a>
421 * @since 3.10.2
422 */
423 @Parameter(property = "maven.compiler.implicit")
424 protected String implicit;
425 // Reminder: if above list of legal values is modified, update also addComaSeparated("-implicit", …)
426
427 /**
428 * Whether to generate metadata for reflection on method parameters.
429 * If {@code true}, the {@code -parameters} option will be added to compiler arguments.
430 *
431 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-parameters">javac -parameters</a>
432 * @since 3.6.2
433 */
434 @Parameter(property = "maven.compiler.parameters", defaultValue = "false")
435 protected boolean parameters;
436
437 /**
438 * Whether to include debugging information in the compiled class files.
439 * The amount of debugging information to include is specified by the {@link #debuglevel} parameter.
440 * If this {@code debug} flag is {@code true}, then the {@code -g} option may be added to compiler arguments
441 * with a value determined by the {@link #debuglevel} argument. If this {@code debug} flag is {@code false},
442 * then the {@code -g:none} option will be added to the compiler arguments.
443 *
444 * @see #debuglevel
445 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-g">javac -g</a>
446 *
447 * @deprecated Setting this flag to {@code false} is replaced by {@code <debuglevel>none</debuglevel>}.
448 */
449 @Deprecated(since = "4.0.0")
450 @Parameter(property = "maven.compiler.debug", defaultValue = "true")
451 protected boolean debug = true;
452
453 /**
454 * Kinds of debugging information to include in the compiled class files.
455 * Legal values are {@code lines}, {@code vars}, {@code source}, {@code all} and {@code none}.
456 * Values other than {@code all} and {@code none} can be combined in a comma-separated list.
457 *
458 * <p>If debug level is not specified, then the {@code -g} option will <em>not</em> be added,
459 * which means that the default debugging information will be generated
460 * (typically {@code lines} and {@code source} but not {@code vars}).</p>
461 *
462 * <p>If debug level is {@code all}, then only the {@code -g} option is added,
463 * which means that all debugging information will be generated.
464 * If debug level is anything else, then the comma-separated list of keywords
465 * is appended to the {@code -g} command-line switch.</p>
466 *
467 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-g-custom">javac -g:[lines,vars,source]</a>
468 * @since 2.1
469 */
470 @Parameter(property = "maven.compiler.debuglevel")
471 protected String debuglevel;
472 // Reminder: if above list of legal values is modified, update also addComaSeparated("-g", …)
473
474 /**
475 * Whether to optimize the compiled code using the compiler's optimization methods.
476 * @deprecated This property is ignored.
477 */
478 @Deprecated(forRemoval = true)
479 @Parameter(property = "maven.compiler.optimize")
480 protected Boolean optimize;
481
482 /**
483 * Whether to show messages about what the compiler is doing.
484 * If {@code true}, then the {@code -verbose} option will be added to compiler arguments.
485 * In addition, files such as {@code target/javac.args} will be generated even on successful compilation.
486 *
487 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html#option-verbose">javac -verbose</a>
488 */
489 @Parameter(property = "maven.compiler.verbose", defaultValue = "false")
490 protected boolean verbose;
491
492 /**
493 * Whether to provide more details about why a module is rebuilt.
494 * This is used only if {@link #incrementalCompilation} is set to something else than {@code "none"}.
495 *
496 * @see #incrementalCompilation
497 */
498 @Parameter(property = "maven.compiler.showCompilationChanges", defaultValue = "false")
499 protected boolean showCompilationChanges;
500
501 /**
502 * Whether to show source locations where deprecated APIs are used.
503 * If {@code true}, then the {@code -deprecation} option will be added to compiler arguments.
504 * That option is itself a shorthand for {@code -Xlint:deprecation}.
505 *
506 * @see #showWarnings
507 * @see #failOnWarning
508 */
509 @Parameter(property = "maven.compiler.showDeprecation", defaultValue = "false")
510 protected boolean showDeprecation;
511
512 /**
513 * Whether to show compilation warnings.
514 * If {@code false}, then the {@code -nowarn} option will be added to compiler arguments.
515 * That option is itself a shorthand for {@code -Xlint:none}.
516 *
517 * @see #showDeprecation
518 * @see #failOnWarning
519 */
520 @Parameter(property = "maven.compiler.showWarnings", defaultValue = "true")
521 protected boolean showWarnings = true;
522
523 /**
524 * Whether the build will stop if there are compilation warnings.
525 * If {@code true}, then the {@code -Werror} option will be added to compiler arguments.
526 *
527 * @see #showWarnings
528 * @see #showDeprecation
529 * @since 3.6
530 */
531 @Parameter(property = "maven.compiler.failOnWarning", defaultValue = "false")
532 protected boolean failOnWarning;
533
534 /**
535 * Whether the build will stop if there are compilation errors.
536 *
537 * @see #failOnWarning
538 * @since 2.0.2
539 */
540 @Parameter(property = "maven.compiler.failOnError", defaultValue = "true")
541 protected boolean failOnError = true;
542
543 /**
544 * Sets the name of the output file when compiling a set of sources to a single file.
545 *
546 * <p>expression="${project.build.finalName}"</p>
547 *
548 * @deprecated Bundling many class files into a single file should be done by other plugins.
549 */
550 @Parameter
551 @Deprecated(since = "4.0.0", forRemoval = true)
552 protected String outputFileName;
553
554 /**
555 * Timestamp for reproducible output archive entries. It can be either formatted as ISO 8601
556 * {@code yyyy-MM-dd'T'HH:mm:ssXXX} or as an int representing seconds since the epoch (like
557 * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
558 *
559 * @since 3.12.0
560 *
561 * @deprecated Not used by the compiler plugin since it does not generate archive.
562 */
563 @Deprecated(since = "4.0.0", forRemoval = true)
564 @Parameter(defaultValue = "${project.build.outputTimestamp}")
565 protected String outputTimestamp;
566
567 /**
568 * The algorithm to use for selecting which files to compile.
569 * Values can be {@code dependencies}, {@code sources}, {@code classes}, {@code rebuild-on-change},
570 * {@code rebuild-on-add}, {@code modules} or {@code none}.
571 *
572 * <p><b>{@code options}:</b>
573 * recompile all source files if the compiler options changed.
574 * Changes are detected on a <i>best-effort</i> basis only.</p>
575 *
576 * <p><b>{@code dependencies}:</b>
577 * recompile all source files if at least one dependency (JAR file) changed since the last build.
578 * This check is based on the last modification times of JAR files.</p>
579 *
580 * <p><b>{@code sources}:</b>
581 * recompile source files modified since the last build.
582 * In addition, if a source file has been deleted, then all source files are recompiled.
583 * This check is based on the modification times of source files
584 * rather than the modification times of the {@code *.class} files.</p>
585 *
586 * <p><b>{@code classes}:</b>
587 * recompile source files ({@code *.java}) associated to no output file ({@code *.class})
588 * or associated to an output file older than the source. This algorithm does not check
589 * if a source file has been removed, potentially leaving non-recompiled classes with
590 * references to classes that no longer exist.</p>
591 *
592 * <p>The {@code sources} and {@code classes} values are partially redundant,
593 * doing the same work in different ways. It is usually not necessary to specify those two values.</p>
594 *
595 * <p><b>{@code modules}:</b>
596 * recompile modules and let the compiler decides which individual files to recompile.
597 * The compiler plugin does not enumerate the source files to recompile (actually, it does not scan at all the
598 * source directories). Instead, it only specifies the module to recompile using the {@code --module} option.
599 * The Java compiler will scan the source directories itself and compile only those source files that are newer
600 * than the corresponding files in the output directory.</p>
601 *
602 * <p><b>{@code rebuild-on-add}:</b>
603 * modifier for recompiling all source files when the addition of a new file is detected.
604 * This flag is effective only when used together with {@code sources} or {@code classes}.
605 * When used with {@code classes}, it provides a way to detect class renaming
606 * (this is not needed with {@code sources} for detecting renaming).</p>
607 *
608 * <p><b>{@code rebuild-on-change}:</b>
609 * modifier for recompiling all source files when a change is detected in at least one source file.
610 * This flag is effective only when used together with {@code sources} or {@code classes}.
611 * It does not rebuild when a new source file is added without change in other files,
612 * unless {@code rebuild-on-add} is also specified.</p>
613 *
614 * <p><b>{@code none}:</b>
615 * the compiler plugin unconditionally specifies all sources to the Java compiler.
616 * This option is mutually exclusive with all other incremental compilation options.</p>
617 *
618 * <h4>Limitations</h4>
619 * In all cases, the current compiler-plugin does not detect structural changes other than file addition or removal.
620 * For example, the plugin does not detect whether a method has been removed in a class.
621 *
622 * <h4>Default value</h4>
623 * The default value depends on the context.
624 * If there is no annotation processor, then the default is {@code "options,dependencies,sources"}.
625 * It means that a full rebuild will be done if the compiler options or the dependencies changed,
626 * or if a source file has been deleted. Otherwise, only the modified source files will be recompiled.
627 *
628 * <p>If an annotation processor is present (e.g., {@link #proc} set to a value other than {@code "none"}),
629 * then the default value is same as above with the addition of {@code "rebuild-on-add,rebuild-on-change"}.
630 * It means that a full rebuild will be done if any kind of change is detected.</p>
631 *
632 * @see #staleMillis
633 * @see #fileExtensions
634 * @see #showCompilationChanges
635 * @see #createMissingPackageInfoClass
636 * @since 4.0.0
637 */
638 @Parameter(property = "maven.compiler.incrementalCompilation")
639 protected String incrementalCompilation;
640
641 /**
642 * Whether to enable/disable incremental compilation feature.
643 *
644 * @since 3.1
645 *
646 * @deprecated Replaced by {@link #incrementalCompilation}.
647 * A value of {@code true} in this old property is equivalent to {@code "dependencies,sources,rebuild-on-add"}
648 * in the new property, and a value of {@code false} is equivalent to {@code "classes"}.
649 */
650 @Deprecated(since = "4.0.0")
651 @Parameter(property = "maven.compiler.useIncrementalCompilation")
652 protected Boolean useIncrementalCompilation;
653
654 /**
655 * Returns the configuration of the incremental compilation.
656 * If the argument is null or blank, then this method applies
657 * the default values documented in {@link #incrementalCompilation} javadoc.
658 *
659 * @throws MojoException if a value is not recognized, or if mutually exclusive values are specified
660 */
661 final EnumSet<IncrementalBuild.Aspect> incrementalCompilationConfiguration() {
662 if (incrementalCompilation == null || incrementalCompilation.isBlank()) {
663 if (useIncrementalCompilation != null) {
664 return useIncrementalCompilation
665 ? EnumSet.of(
666 IncrementalBuild.Aspect.DEPENDENCIES,
667 IncrementalBuild.Aspect.SOURCES,
668 IncrementalBuild.Aspect.REBUILD_ON_ADD)
669 : EnumSet.of(IncrementalBuild.Aspect.CLASSES);
670 }
671 var aspects = EnumSet.of(
672 IncrementalBuild.Aspect.OPTIONS,
673 IncrementalBuild.Aspect.DEPENDENCIES,
674 IncrementalBuild.Aspect.SOURCES);
675 if (hasAnnotationProcessor(false)) {
676 aspects.add(IncrementalBuild.Aspect.REBUILD_ON_ADD);
677 aspects.add(IncrementalBuild.Aspect.REBUILD_ON_CHANGE);
678 }
679 return aspects;
680 } else {
681 return IncrementalBuild.Aspect.parse(incrementalCompilation);
682 }
683 }
684
685 /**
686 * File extensions to check timestamp for incremental build.
687 * Default contains only {@code class} and {@code jar}.
688 *
689 * TODO: Rename with a name making clearer that this parameter is about incremental build.
690 *
691 * @see #incrementalCompilation
692 * @since 3.1
693 */
694 @Parameter(defaultValue = "class,jar")
695 protected List<String> fileExtensions;
696
697 /**
698 * The granularity in milliseconds of the last modification
699 * date for testing whether a source needs recompilation.
700 *
701 * @see #incrementalCompilation
702 */
703 @Parameter(property = "lastModGranularityMs", defaultValue = "0")
704 protected int staleMillis;
705
706 /**
707 * Allows running the compiler in a separate process.
708 * If {@code false}, the plugin uses the built-in compiler, while if {@code true} it will use an executable.
709 *
710 * @see #executable
711 * @see #compilerId
712 * @see #meminitial
713 * @see #maxmem
714 */
715 @Parameter(property = "maven.compiler.fork", defaultValue = "false")
716 protected boolean fork;
717
718 /**
719 * Requirements for this JDK toolchain for using a different {@code javac} than the one of the JDK used by Maven.
720 * This overrules the toolchain selected by the
721 * <a href="https://maven.apache.org/plugins/maven-toolchains-plugin/">maven-toolchain-plugin</a>.
722 * See <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html"> Guide to Toolchains</a>
723 * for more info.
724 *
725 * <pre>
726 * <configuration>
727 * <jdkToolchain>
728 * <version>11</version>
729 * </jdkToolchain>
730 * ...
731 * </configuration>
732 *
733 * <configuration>
734 * <jdkToolchain>
735 * <version>1.8</version>
736 * <vendor>zulu</vendor>
737 * </jdkToolchain>
738 * ...
739 * </configuration>
740 * </pre>
741 *
742 * @see #fork
743 * @see #executable
744 * @since 3.6
745 */
746 @Parameter
747 protected Map<String, String> jdkToolchain;
748
749 /**
750 * Identifier of the compiler to use. This identifier shall match the identifier of a compiler known
751 * to the {@linkplain #jdkToolchain JDK tool chain}, or the {@linkplain JavaCompiler#name() name} of
752 * a {@link JavaCompiler} instance registered as a service findable by {@link ServiceLoader}.
753 * See this <a href="non-javac-compilers.html">guide</a> for more information.
754 * If unspecified, then the {@linkplain ToolProvider#getSystemJavaCompiler() system Java compiler} is used.
755 * The identifier of the system Java compiler is usually {@code javac}.
756 *
757 * @see #fork
758 * @see #executable
759 * @see JavaCompiler#name()
760 */
761 @Parameter(property = "maven.compiler.compilerId")
762 protected String compilerId;
763
764 /**
765 * Version of the compiler to use if {@link #fork} is set to {@code true}.
766 * Examples! "1.3", "1.5".
767 *
768 * @deprecated This parameter is no longer used by the underlying compilers.
769 *
770 * @see #fork
771 */
772 @Deprecated(since = "4.0.0", forRemoval = true)
773 @Parameter(property = "maven.compiler.compilerVersion")
774 protected String compilerVersion;
775
776 /**
777 * Whether to use the legacy {@code com.sun.tools.javac} API instead of {@code javax.tools} API.
778 *
779 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.compiler/javax/tools/package-summary.html">New API</a>
780 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/tools/javac/package-summary.html">Legacy API</a>
781 * @since 3.13
782 *
783 * @deprecated Ignored because the compiler plugin now always use the {@code javax.tools} API.
784 */
785 @Deprecated(since = "4.0.0", forRemoval = true)
786 @Parameter(property = "maven.compiler.forceLegacyJavacApi")
787 protected Boolean forceLegacyJavacApi;
788
789 /**
790 * Whether to use legacy compiler API.
791 *
792 * @since 3.0
793 *
794 * @deprecated Ignored because {@code java.lang.Compiler} has been deprecated and removed from the JDK.
795 */
796 @Deprecated(since = "4.0.0", forRemoval = true)
797 @Parameter(property = "maven.compiler.forceJavacCompilerUse")
798 protected Boolean forceJavacCompilerUse;
799
800 /**
801 * Strategy to re use {@code javacc} class created. Legal values are:
802 * <ul>
803 * <li>{@code reuseCreated} (default) – will reuse already created but in case of multi-threaded builds,
804 * each thread will have its own instance.</li>
805 * <li>{@code reuseSame} – the same Javacc class will be used for each compilation even
806 * for multi-threaded build.</li>
807 * <li>{@code alwaysNew} – a new Javacc class will be created for each compilation.</li>
808 * </ul>
809 * Note this parameter value depends on the OS/JDK you are using, but the default value should work on most of env.
810 *
811 * @since 2.5
812 *
813 * @deprecated Not supported anymore. The reuse of {@link JavaFileManager} instance is plugin implementation details.
814 */
815 @Deprecated(since = "4.0.0", forRemoval = true)
816 @Parameter(property = "maven.compiler.compilerReuseStrategy")
817 protected String compilerReuseStrategy;
818
819 /**
820 * @since 2.5
821 *
822 * @deprecated Deprecated as a consequence of {@link #compilerReuseStrategy} deprecation.
823 */
824 @Deprecated(since = "4.0.0", forRemoval = true)
825 @Parameter(property = "maven.compiler.skipMultiThreadWarning")
826 protected Boolean skipMultiThreadWarning;
827
828 /**
829 * Executable of the compiler to use when {@link #fork} is {@code true}.
830 * If this parameter is specified, then the {@link #jdkToolchain} is ignored.
831 *
832 * @see #jdkToolchain
833 * @see #fork
834 * @see #compilerId
835 */
836 @Parameter(property = "maven.compiler.executable")
837 protected String executable;
838
839 /**
840 * Initial size, in megabytes, of the memory allocation pool if {@link #fork} is set to {@code true}.
841 * Examples: "64", "64M". Suffixes "k" (for kilobytes) and "G" (for gigabytes) are also accepted.
842 * If no suffix is provided, "M" is assumed.
843 *
844 * @see #fork
845 * @since 2.0.1
846 */
847 @Parameter(property = "maven.compiler.meminitial")
848 protected String meminitial;
849
850 /**
851 * Maximum size, in megabytes, of the memory allocation pool if {@link #fork} is set to {@code true}.
852 * Examples: "128", "128M". Suffixes "k" (for kilobytes) and "G" (for gigabytes) are also accepted.
853 * If no suffix is provided, "M" is assumed.
854 *
855 * @see #fork
856 * @since 2.0.1
857 */
858 @Parameter(property = "maven.compiler.maxmem")
859 protected String maxmem;
860
861 // ----------------------------------------------------------------------
862 // Read-only parameters
863 // ----------------------------------------------------------------------
864
865 /**
866 * The directory to run the compiler from if fork is true.
867 */
868 @Parameter(defaultValue = "${project.basedir}", required = true, readonly = true)
869 protected Path basedir;
870
871 /**
872 * Path to a file where to cache information about the last incremental build.
873 * This is used when "incremental" builds are enabled for detecting additions
874 * or removals of source files, or changes in plugin configuration.
875 * This file should be in the output directory and can be deleted at any time
876 */
877 @Parameter(
878 defaultValue =
879 "${project.build.directory}/maven-status/${mojo.plugin.descriptor.artifactId}/${mojo.executionId}.cache",
880 required = true,
881 readonly = true)
882 protected Path mojoStatusPath;
883
884 /**
885 * The current build session instance.
886 */
887 @Inject
888 protected Session session;
889
890 /**
891 * The current project instance.
892 */
893 @Inject
894 protected Project project;
895
896 @Inject
897 protected ProjectManager projectManager;
898
899 @Inject
900 protected ArtifactManager artifactManager;
901
902 @Inject
903 protected ToolchainManager toolchainManager;
904
905 @Inject
906 protected MessageBuilderFactory messageBuilderFactory;
907
908 /**
909 * The logger for reporting information or warnings to the user.
910 * Currently, this is also used for console output.
911 *
912 * <h4>Thread safety</h4>
913 * This logger should be thread-safe if the {@link ToolExecutor} is executed in a background thread.
914 */
915 @Inject
916 protected Log logger;
917
918 /**
919 * Cached value for writing replacement proposal when a deprecated option is used.
920 * This is set to a non-null value when first needed. An empty string means that
921 * this information couldn't be fetched.
922 *
923 * @see #writePlugin(MessageBuilder, String, String)
924 */
925 private String mavenCompilerPluginVersion;
926
927 /**
928 * A tip about how to launch the Java compiler from the command-line.
929 * The command-line may have {@code -J} options before the argument file.
930 * This is non-null if the compilation failed or if Maven is executed in debug mode.
931 */
932 private String tipForCommandLineCompilation;
933
934 /**
935 * {@code MAIN_COMPILE} if this MOJO is for compiling the main code,
936 * or {@code TEST_COMPILE} if compiling the tests.
937 */
938 final PathScope compileScope;
939
940 /**
941 * Creates a new MOJO.
942 *
943 * @param compileScope {@code MAIN_COMPILE} or {@code TEST_COMPILE}
944 */
945 protected AbstractCompilerMojo(PathScope compileScope) {
946 this.compileScope = compileScope;
947 }
948
949 /**
950 * {@return the inclusion filters for the compiler, or an empty list for all Java source files}
951 * The filter patterns are described in {@link java.nio.file.FileSystem#getPathMatcher(String)}.
952 * If no syntax is specified, the default syntax is a derivative of "glob" compatible with the
953 * behavior of Maven 3.
954 */
955 protected abstract Set<String> getIncludes();
956
957 /**
958 * {@return the exclusion filters for the compiler, or an empty list if none}
959 * The filter patterns are described in {@link java.nio.file.FileSystem#getPathMatcher(String)}.
960 * If no syntax is specified, the default syntax is a derivative of "glob" compatible with the
961 * behavior of Maven 3.
962 */
963 protected abstract Set<String> getExcludes();
964
965 /**
966 * {@return the exclusion filters for the incremental calculation}
967 * Updated source files, if excluded by this filter, will not cause the project to be rebuilt.
968 *
969 * @see SourceFile#ignoreModification
970 */
971 protected abstract Set<String> getIncrementalExcludes();
972
973 /**
974 * {@return whether all includes/excludes matchers specified in the plugin configuration are empty}
975 * This method checks only the plugin configuration. It does not check the {@code <source>} elements.
976 */
977 final boolean hasNoFileMatchers() {
978 return getIncludes().isEmpty()
979 && getExcludes().isEmpty()
980 && getIncrementalExcludes().isEmpty();
981 }
982
983 /**
984 * {@return the destination directory (or class output directory) for class files}
985 * This directory will be given to the {@code -d} Java compiler option.
986 */
987 @Nonnull
988 protected abstract Path getOutputDirectory();
989
990 /**
991 * {@return the {@code --source} argument for the Java compiler}
992 * The default implementation returns the {@link #source} value.
993 */
994 @Nullable
995 protected String getSource() {
996 return source;
997 }
998
999 /**
1000 * {@return the {@code --target} argument for the Java compiler}
1001 * The default implementation returns the {@link #target} value.
1002 */
1003 @Nullable
1004 protected String getTarget() {
1005 return target;
1006 }
1007
1008 /**
1009 * {@return the {@code --release} argument for the Java compiler}
1010 * The default implementation returns the {@link #release} value.
1011 */
1012 @Nullable
1013 protected String getRelease() {
1014 return release;
1015 }
1016
1017 /**
1018 * {@return the root directories of Java source code for the given scope}
1019 * This method ignores the deprecated {@link #compileSourceRoots} element.
1020 *
1021 * @param scope whether to get the directories for main code or for the test code
1022 */
1023 final Stream<SourceRoot> getSourceRoots(ProjectScope scope) {
1024 return projectManager.getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY);
1025 }
1026
1027 /**
1028 * {@return the root directories of the Java source files to compile, excluding empty directories}
1029 * The list needs to be modifiable for allowing the addition of generated source directories.
1030 * This is determined from the {@link #compileSourceRoots} plugin configuration if non-empty,
1031 * or from {@code <source>} elements otherwise.
1032 *
1033 * @param outputDirectory the directory where to store the compilation results
1034 * @throws IOException if this method needs to walk through directories and that operation failed
1035 */
1036 final List<SourceDirectory> getSourceDirectories(final Path outputDirectory) throws IOException {
1037 if (compileSourceRoots == null || compileSourceRoots.isEmpty()) {
1038 Stream<SourceRoot> roots = getSourceRoots(compileScope.projectScope());
1039 return SourceDirectory.fromProject(roots, getRelease(), outputDirectory);
1040 } else {
1041 return SourceDirectory.fromPluginConfiguration(
1042 compileSourceRoots, moduleOfPreviousExecution(), getRelease(), outputDirectory);
1043 }
1044 }
1045
1046 /**
1047 * {@return the path where to place generated source files created by annotation processing}
1048 */
1049 @Nullable
1050 protected abstract Path getGeneratedSourcesDirectory();
1051
1052 /**
1053 * Returns the module which is being patched in a multi-release project, or {@code null} if none.
1054 * This is used when the {@link CompilerMojo#multiReleaseOutput} deprecated flag is {@code true}.
1055 * This module name is handled in a special way because, contrarily to the case where the project
1056 * uses the recommended {@code <sources>} elements (in which case all target releases are compiled
1057 * in a single Maven Compiler Plugin execution), the Maven Compiler Plugin does not know what have
1058 * been compiled for the other releases, because each target release is compiled with an execution
1059 * of {@link CompilerMojo} separated from other executions.
1060 *
1061 * @return the module name in a previous execution of the compiler plugin, or {@code null} if none
1062 * @throws IOException if this method needs to walk through directories and that operation failed
1063 *
1064 * @see CompilerMojo#addImplicitDependencies(ToolExecutor)
1065 *
1066 * @deprecated For compatibility with the previous way to build multi-release JAR file.
1067 * May be removed after we drop support of the old way to do multi-release.
1068 */
1069 @Deprecated(since = "4.0.0")
1070 String moduleOfPreviousExecution() throws IOException {
1071 return null;
1072 }
1073
1074 /**
1075 * {@return whether the sources contain at least one {@code module-info.java} file}
1076 * Note that the sources may contain more than one {@code module-info.java} file
1077 * if compiling a project with Module Source Hierarchy.
1078 *
1079 * <p>If the user explicitly specified a modular or classpath project, then the
1080 * {@code module-info.java} is assumed to exist or not without verification.</p>
1081 *
1082 * <p>The test compiler overrides this method for checking the existence of the
1083 * the {@code module-info.class} file in the main output directory instead.</p>
1084 *
1085 * @param roots root directories of the sources to compile
1086 * @throws IOException if this method needed to read a module descriptor and failed
1087 */
1088 boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
1089 return switch (project.getPackaging().type().id()) {
1090 case Type.CLASSPATH_JAR -> false;
1091 case Type.MODULAR_JAR -> true;
1092 default -> {
1093 for (SourceDirectory root : roots) {
1094 if (root.getModuleInfo().isPresent()) {
1095 yield true;
1096 }
1097 }
1098 yield false;
1099 }
1100 };
1101 }
1102
1103 /**
1104 * {@return the file where to dump the command-line when debug logging is enabled or when the compilation failed}
1105 * For example, if the value is {@code "javac"}, then the Java compiler can be launched
1106 * from the command-line by typing {@code javac @target/javac.args}.
1107 * The debug file will contain the compiler options together with the list of source files to compile.
1108 *
1109 * <p>Note: debug logging should not be confused with the {@link #debug} flag.</p>
1110 *
1111 * @see CompilerMojo#debugFileName
1112 * @see TestCompilerMojo#debugFileName
1113 */
1114 @Nullable
1115 protected abstract String getDebugFileName();
1116
1117 /**
1118 * {@return the debug file name with its path, or null if none}
1119 * This method does not check if the debug file will be written, as the compilation result is not yet known.
1120 */
1121 final Path getDebugFilePath() {
1122 String filename = getDebugFileName();
1123 if (filename == null || filename.isBlank()) {
1124 return null;
1125 }
1126 // Do not use `this.getOutputDirectory()` because it may be deeper in `classes/META-INF/versions/`.
1127 return Path.of(project.getBuild().getOutputDirectory()).resolveSibling(filename);
1128 }
1129
1130 /**
1131 * Returns whether the debug file should be written after a successful build.
1132 * By default, debug files are written only if the build failed.
1133 * However, some options can change this behavior.
1134 */
1135 final boolean shouldWriteDebugFile() {
1136 return verbose || logger.isDebugEnabled();
1137 }
1138
1139 /**
1140 * Runs the Java compiler. This method performs the following steps:
1141 *
1142 * <ol>
1143 * <li>Get a Java compiler by a call to {@link #compiler()}.</li>
1144 * <li>Get the options to give to the compiler by a call to {@link #parseParameters(OptionChecker)}.</li>
1145 * <li>Get an executor with {@link #createExecutor(DiagnosticListener)} with the default listener.</li>
1146 * <li>{@linkplain ToolExecutor#applyIncrementalBuild Apply the incremental build} if enabled.</li>
1147 * <li>{@linkplain ToolExecutor#compile Execute the compilation}.</li>
1148 * <li>Shows messages in the {@linkplain #logger}.</li>
1149 * </ol>
1150 *
1151 * @throws MojoException if the compiler cannot be run
1152 */
1153 @Override
1154 public void execute() throws MojoException {
1155 JavaCompiler compiler = compiler();
1156 for (SourceVersion version : compiler.getSourceVersions()) {
1157 if (supportedVersion == null || version.compareTo(supportedVersion) >= 0) {
1158 supportedVersion = version;
1159 }
1160 }
1161 Options configuration = parseParameters(compiler);
1162 try {
1163 compile(compiler, configuration);
1164 } catch (RuntimeException e) {
1165 String message = e.getLocalizedMessage();
1166 if (message == null) {
1167 message = e.getClass().getSimpleName();
1168 } else if (e instanceof MojoException) {
1169 int s = message.indexOf(System.lineSeparator());
1170 if (s >= 0) {
1171 message = message.substring(0, s); // Log only the first line.
1172 }
1173 }
1174 MessageBuilder mb = messageBuilderFactory
1175 .builder()
1176 .strong("COMPILATION ERROR: ")
1177 .a(message);
1178 logger.error(mb.toString(), verbose ? e : null);
1179 if (tipForCommandLineCompilation != null) {
1180 logger.info(tipForCommandLineCompilation);
1181 tipForCommandLineCompilation = null;
1182 }
1183 if (failOnError) {
1184 throw e;
1185 }
1186 } catch (IOException e) {
1187 logger.error("I/O error while compiling the project.", e);
1188 throw new CompilationFailureException("I/O error while compiling the project.", e);
1189 }
1190 }
1191
1192 /**
1193 * Creates a new task by taking a snapshot of the current configuration of this <abbr>MOJO</abbr>.
1194 * This method creates the {@linkplain ToolExecutor#outputDirectory output directory} if it does not already exist.
1195 *
1196 * <h4>Multi-threading</h4>
1197 * This method and the returned objects are not thread-safe.
1198 * However, this method takes a snapshot of the configuration of this <abbr>MOJO</abbr>.
1199 * Changes in this <abbr>MOJO</abbr> after this method call will not affect the returned executor.
1200 * Therefore, the executor can safely be executed in a background thread,
1201 * provided that the {@link #logger} is thread-safe.
1202 *
1203 * @param listener where to send compilation warnings, or {@code null} for the Maven logger
1204 * @return the task to execute for compiling the project using the configuration in this <abbr>MOJO</abbr>
1205 * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr>
1206 * @throws IOException if an error occurred while creating the output directory or scanning the source directories
1207 * @throws MavenException if an error occurred while fetching dependencies
1208 */
1209 public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException {
1210 var executor = new ToolExecutor(this, listener);
1211 if (!(targetOrReleaseSet || executor.isReleaseSpecifiedForAll())) {
1212 MessageBuilder mb = messageBuilderFactory
1213 .builder()
1214 .a("No explicit value set for --release or --target. "
1215 + "To ensure the same result in different environments, please add")
1216 .newline()
1217 .newline();
1218 writePlugin(mb, "release", String.valueOf(Runtime.version().feature()));
1219 logger.warn(mb.build());
1220 }
1221 return executor;
1222 }
1223
1224 /**
1225 * {@return the compiler to use for compiling the code}
1226 * If {@link #fork} is {@code true}, the returned compiler will be a wrapper for a command line.
1227 * Otherwise, it will be the compiler identified by {@link #compilerId} if a value was supplied,
1228 * or the standard compiler provided with the Java platform otherwise.
1229 *
1230 * @throws MojoException if no compiler was found
1231 */
1232 public JavaCompiler compiler() throws MojoException {
1233 /*
1234 * Use the `compilerId` as identifier for toolchains.
1235 * I.e, we assume that `compilerId` is also the name of the executable binary.
1236 */
1237 getToolchain().ifPresent((tc) -> {
1238 logger.info("Toolchain in maven-compiler-plugin is \"" + tc + "\".");
1239 if (executable != null) {
1240 logger.warn(
1241 "Toolchains are ignored because the 'executable' parameter is set to \"" + executable + "\".");
1242 } else {
1243 fork = true;
1244 if (compilerId == null) {
1245 compilerId = DEFAULT_EXECUTABLE;
1246 }
1247 // TODO somehow shaky dependency between compilerId and tool executable.
1248 executable = tc.findTool(compilerId);
1249 }
1250 });
1251 if (fork) {
1252 if (executable == null) {
1253 executable = DEFAULT_EXECUTABLE;
1254 }
1255 return new ForkedCompiler(this);
1256 }
1257 /*
1258 * Search a `javax.tools.JavaCompiler` having a name matching the specified `compilerId`.
1259 * This is done before other code that can cause the mojo to return before the lookup is
1260 * done, possibly resulting in misconfigured POMs still building. If no `compilerId` was
1261 * specified, then the Java compiler bundled with the JDK is used (it may be absent).
1262 */
1263 if (logger.isDebugEnabled()) {
1264 logger.debug(
1265 "Using " + (compilerId != null ? ("compiler \"" + compilerId + '"') : "system compiler") + '.');
1266 }
1267 if (compilerId == null) {
1268 compilerId = DEFAULT_EXECUTABLE;
1269 final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
1270 if (compiler != null) {
1271 return compiler;
1272 }
1273 } else {
1274 for (JavaCompiler t : ServiceLoader.load(JavaCompiler.class)) {
1275 if (compilerId.equals(t.name())) {
1276 return t;
1277 }
1278 }
1279 }
1280 throw new CompilationFailureException("No such \"" + compilerId + "\" compiler.");
1281 }
1282
1283 /**
1284 * Parses the parameters declared in the <abbr>MOJO</abbr>.
1285 * The {@link #release} parameter is excluded because it is handled in a special way
1286 * in order to support the compilation of multi-version projects.
1287 *
1288 * @param compiler the tools to use for verifying the validity of options
1289 * @return the options after validation
1290 */
1291 public Options parseParameters(final OptionChecker compiler) {
1292 /*
1293 * Options to provide to the compiler, excluding all kinds of path (source files, destination directory,
1294 * class-path, module-path, etc.). Some options are validated by Maven in addition of being validated by
1295 * the compiler. In those cases, the validation by the compiler is done before the validation by Maven.
1296 * For example, Maven will check for illegal values in the "-g" option only if the compiler rejected
1297 * the fully formatted option (e.g. "-g:vars,lines") that we provided to it.
1298 */
1299 final var configuration = new Options(compiler, logger);
1300 configuration.addIfNonBlank("--source", getSource());
1301 targetOrReleaseSet = configuration.addIfNonBlank("--target", getTarget());
1302 targetOrReleaseSet |= configuration.setRelease(getRelease());
1303 configuration.addIfTrue("--enable-preview", enablePreview);
1304 configuration.addComaSeparated("-proc", proc, List.of("none", "only", "full"), null);
1305 if (annotationProcessors != null) {
1306 var list = new StringJoiner(",");
1307 for (String p : annotationProcessors) {
1308 list.add(p);
1309 }
1310 configuration.addIfNonBlank("-processor", list.toString());
1311 }
1312 configuration.addComaSeparated("-implicit", implicit, List.of("none", "class"), null);
1313 configuration.addIfTrue("-parameters", parameters);
1314 configuration.addIfTrue("-Xpkginfo:always", createMissingPackageInfoClass);
1315 if (debug) {
1316 configuration.addComaSeparated(
1317 "-g",
1318 debuglevel,
1319 List.of("lines", "vars", "source", "all", "none"),
1320 (options) -> Arrays.asList(options).contains("all") ? new String[0] : options);
1321 } else {
1322 configuration.addIfTrue("-g:none", true);
1323 }
1324 configuration.addIfNonBlank("--module-version", moduleVersion);
1325 configuration.addIfTrue("-deprecation", showDeprecation);
1326 configuration.addIfTrue("-nowarn", !showWarnings);
1327 configuration.addIfTrue("-Werror", failOnWarning);
1328 configuration.addIfTrue("-verbose", verbose);
1329 if (fork) {
1330 configuration.addMemoryValue("-J-Xms", "meminitial", meminitial, SUPPORT_LEGACY);
1331 configuration.addMemoryValue("-J-Xmx", "maxmem", maxmem, SUPPORT_LEGACY);
1332 }
1333 return configuration;
1334 }
1335
1336 /**
1337 * Runs the compiler, then shows the result in the Maven logger.
1338 *
1339 * @param compiler the compiler
1340 * @param configuration options to provide to the compiler
1341 * @throws IOException if an input file cannot be read
1342 * @throws MojoException if the compilation failed
1343 */
1344 @SuppressWarnings("UseSpecificCatch")
1345 private void compile(final JavaCompiler compiler, final Options configuration) throws IOException {
1346 final ToolExecutor executor = createExecutor(null);
1347 if (!executor.applyIncrementalBuild(this, configuration)) {
1348 return;
1349 }
1350 Exception failureCause = null;
1351 final var compilerOutput = new StringWriter();
1352 boolean success;
1353 try {
1354 success = executor.compile(compiler, configuration, compilerOutput);
1355 } catch (Exception e) {
1356 success = false;
1357 failureCause = e;
1358 }
1359 /*
1360 * The compilation errors or warnings should have already been reported by `DiagnosticLogger`.
1361 * However, the compiler may have other messages not associated to a particular source file.
1362 * For example, `ForkedCompiler` uses this writer if the compilation has been interrupted.
1363 */
1364 String additionalMessage = compilerOutput.toString();
1365 if (!additionalMessage.isBlank()) {
1366 if (success || failureCause != null) { // Keep the error level for the exception message.
1367 logger.warn(additionalMessage);
1368 } else {
1369 logger.error(additionalMessage);
1370 }
1371 }
1372 if (failureCause != null) {
1373 String message = failureCause.getMessage();
1374 if (message != null) {
1375 logger.error(message);
1376 } else {
1377 logger.error(failureCause);
1378 }
1379 }
1380 /*
1381 * In case of failure, or if debugging is enabled, dump the options to a file.
1382 * By default, the file will have the ".args" extension.
1383 */
1384 if (!success || shouldWriteDebugFile()) {
1385 IOException suppressed = null;
1386 try {
1387 writeDebugFile(executor, configuration, success);
1388 if (success && tipForCommandLineCompilation != null) {
1389 logger.debug(tipForCommandLineCompilation);
1390 tipForCommandLineCompilation = null;
1391 }
1392 } catch (IOException e) {
1393 suppressed = e;
1394 }
1395 if (!success) {
1396 var message = new StringBuilder(100)
1397 .append("Cannot compile ")
1398 .append(project.getId())
1399 .append(' ')
1400 .append(compileScope.projectScope().id())
1401 .append(" classes.");
1402 if (executor.listener instanceof DiagnosticLogger diagnostic) {
1403 diagnostic.firstError(failureCause).ifPresent((c) -> message.append(System.lineSeparator())
1404 .append("The first error is: ")
1405 .append(c));
1406 }
1407 var failure = new CompilationFailureException(message.toString(), failureCause);
1408 if (suppressed != null) {
1409 failure.addSuppressed(suppressed);
1410 }
1411 throw failure;
1412 }
1413 if (suppressed != null) {
1414 throw suppressed;
1415 }
1416 }
1417 /*
1418 * Workaround for MCOMPILER-542, needed only if a modular project is compiled with a JDK older than Java 22.
1419 * Note: a previous version used as an heuristic way to detect if Reproducible Build was enabled. This check
1420 * has been removed because Reproducible Build are enabled by default in Maven now.
1421 */
1422 if (!isVersionEqualOrNewer("RELEASE_22")) {
1423 Path moduleDescriptor = executor.outputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
1424 if (Files.isRegularFile(moduleDescriptor)) {
1425 byte[] oridinal = Files.readAllBytes(moduleDescriptor);
1426 byte[] modified = ByteCodeTransformer.patchJdkModuleVersion(oridinal, getRelease(), logger);
1427 if (modified != null) {
1428 Files.write(moduleDescriptor, modified);
1429 }
1430 }
1431 }
1432 }
1433
1434 /**
1435 * Returns whether the compiler supports the given source version or newer versions.
1436 * The specified source version shall be the name of one of the {@link SourceVersion} enumeration values.
1437 * Note that a return value of {@code true} does not mean that the compiler supports that exact version,
1438 * as it may supports only newer versions.
1439 */
1440 private boolean isVersionEqualOrNewer(String sourceVersion) {
1441 final SourceVersion requested;
1442 try {
1443 requested = SourceVersion.valueOf(sourceVersion);
1444 } catch (IllegalArgumentException e) {
1445 // The current tool is from a JDK older than the one for the requested source release.
1446 return false;
1447 }
1448 if (supportedVersion == null) {
1449 supportedVersion = SourceVersion.latestSupported();
1450 }
1451 return supportedVersion.compareTo(requested) >= 0;
1452 }
1453
1454 /**
1455 * {@return the tool chain specified by the user in plugin parameters}
1456 */
1457 private Optional<Toolchain> getToolchain() {
1458 if (jdkToolchain != null) {
1459 List<Toolchain> tcs = toolchainManager.getToolchains(session, "jdk", jdkToolchain);
1460 if (tcs != null && !tcs.isEmpty()) {
1461 return Optional.of(tcs.get(0));
1462 }
1463 }
1464 return toolchainManager.getToolchainFromBuildContext(session, "jdk");
1465 }
1466
1467 /**
1468 * Returns the module name as declared in the given {@code module-info.java} source file.
1469 * This approach is less reliable than reading the compiled {@code module-info.class} file,
1470 * but is sometime needed when the compiled file is not yet available.
1471 *
1472 * @param source the source file to parse (may be null or not exist)
1473 * @return the module name, or {@code null} if not found
1474 */
1475 final String parseModuleInfoName(Path source) throws IOException {
1476 if (source != null && Files.exists(source)) {
1477 Charset charset = charset();
1478 try (BufferedReader in =
1479 (charset != null) ? Files.newBufferedReader(source, charset) : Files.newBufferedReader(source)) {
1480 var tokenizer = new StreamTokenizer(in);
1481 tokenizer.slashSlashComments(true);
1482 tokenizer.slashStarComments(true);
1483 int t;
1484 while ((t = tokenizer.nextToken()) != StreamTokenizer.TT_EOF) {
1485 if (t == StreamTokenizer.TT_WORD && "module".equals(tokenizer.sval)) {
1486 do {
1487 t = tokenizer.nextToken();
1488 } while (t == StreamTokenizer.TT_EOL);
1489 if (t == StreamTokenizer.TT_WORD) {
1490 return tokenizer.sval;
1491 }
1492 break; // Found a "module" keyword followed by something that we didn't recognized.
1493 }
1494 }
1495 }
1496 }
1497 return null;
1498 }
1499
1500 /**
1501 * {@return all dependencies grouped by the path types where to place them}
1502 * If the module-path contains any filename-based dependency and this <abbr>MOJO</abbr>
1503 * is compiling the main code, then a warning will be logged.
1504 *
1505 * @param hasModuleDeclaration whether to allow placement of dependencies on the module-path.
1506 * @throws IOException if an I/O error occurred while fetching dependencies
1507 * @throws MavenException if an error occurred while fetching dependencies for a reason other than I/O.
1508 */
1509 final DependencyResolverResult resolveDependencies(boolean hasModuleDeclaration) throws IOException {
1510 DependencyResolver resolver = session.getService(DependencyResolver.class);
1511 if (resolver == null) { // Null value happen during tests, depending on the mock used.
1512 return null;
1513 }
1514 var allowedTypes = EnumSet.of(JavaPathType.CLASSES, JavaPathType.PROCESSOR_CLASSES);
1515 if (hasModuleDeclaration) {
1516 allowedTypes.add(JavaPathType.MODULES);
1517 allowedTypes.add(JavaPathType.PROCESSOR_MODULES);
1518 }
1519 DependencyResolverResult dependencies = resolver.resolve(DependencyResolverRequest.builder()
1520 .session(session)
1521 .project(project)
1522 .requestType(DependencyResolverRequest.RequestType.RESOLVE)
1523 .pathScope(compileScope)
1524 .pathTypeFilter(allowedTypes)
1525 .build());
1526 /*
1527 * Report errors or warnings. If possible, we rethrow the first exception directly without
1528 * wrapping in a `MojoException` for making the stack-trace a little bit easier to analyze.
1529 */
1530 Exception exception = null;
1531 for (Exception cause : dependencies.getExceptions()) {
1532 if (exception != null) {
1533 exception.addSuppressed(cause);
1534 } else if (cause instanceof UncheckedIOException e) {
1535 exception = e.getCause();
1536 } else if (cause instanceof RuntimeException || cause instanceof IOException) {
1537 exception = cause;
1538 } else {
1539 exception = new CompilationFailureException("Cannot collect the compile-time dependencies.", cause);
1540 }
1541 }
1542 if (exception != null) {
1543 if (exception instanceof IOException e) {
1544 throw e;
1545 } else {
1546 throw (RuntimeException) exception; // A ClassCastException here would be a bug in above loop.
1547 }
1548 }
1549 if (ProjectScope.MAIN.equals(compileScope.projectScope())) {
1550 String warning = dependencies.warningForFilenameBasedAutomodules().orElse(null);
1551 if (warning != null) { // Do not use Optional.ifPresent(…) for avoiding confusing source class name.
1552 logger.warn(warning);
1553 }
1554 }
1555 return dependencies;
1556 }
1557
1558 /**
1559 * Adds paths to the annotation processor dependencies. Paths are added to the list associated
1560 * to the {@link JavaPathType#PROCESSOR_CLASSES} entry of given map, which should be modifiable.
1561 *
1562 * <h4>Implementation note</h4>
1563 * We rely on the fact that {@link org.apache.maven.impl.DefaultDependencyResolverResult} creates
1564 * modifiable instances of map and lists. This is a fragile assumption, but this method is deprecated anyway
1565 * and may be removed in a future version.
1566 *
1567 * @param addTo the modifiable map and lists where to append more paths to annotation processor dependencies
1568 * @throws MojoException if an error occurred while resolving the dependencies
1569 *
1570 * @deprecated Replaced by ordinary dependencies with {@code <type>} element set to
1571 * {@code processor}, {@code classpath-processor} or {@code modular-processor}.
1572 */
1573 @Deprecated(since = "4.0.0")
1574 @SuppressWarnings("UseSpecificCatch")
1575 final void resolveProcessorPathEntries(Map<PathType, List<Path>> addTo) throws MojoException {
1576 List<DependencyCoordinate> dependencies = annotationProcessorPaths;
1577 if (dependencies != null && !dependencies.isEmpty()) {
1578 try {
1579 List<org.apache.maven.api.DependencyCoordinates> coords = dependencies.stream()
1580 .map((coord) -> coord.toCoordinate(project, session))
1581 .toList();
1582 Session sessionWithRepo =
1583 session.withRemoteRepositories(projectManager.getRemoteProjectRepositories(project));
1584 addTo.merge(
1585 JavaPathType.PROCESSOR_CLASSES,
1586 sessionWithRepo
1587 .getService(DependencyResolver.class)
1588 .resolve(DependencyResolverRequest.builder()
1589 .session(sessionWithRepo)
1590 .dependencies(coords)
1591 .managedDependencies(project.getManagedDependencies())
1592 .requestType(DependencyResolverRequest.RequestType.RESOLVE)
1593 .pathScope(PathScope.MAIN_RUNTIME)
1594 .build())
1595 .getPaths(),
1596 (oldPaths, newPaths) -> {
1597 oldPaths.addAll(newPaths);
1598 return oldPaths;
1599 });
1600 } catch (MojoException e) {
1601 throw e;
1602 } catch (Exception e) {
1603 throw new CompilationFailureException(
1604 "Resolution of annotationProcessorPath dependencies failed: " + e.getMessage(), e);
1605 }
1606 }
1607 }
1608
1609 /**
1610 * {@return whether an annotation processor seems to be present}
1611 * This method is invoked if the user did not specified explicit incremental compilation options.
1612 *
1613 * @param strict whether to be conservative if the current Java version is older than 23
1614 *
1615 * @see #incrementalCompilation
1616 */
1617 private boolean hasAnnotationProcessor(final boolean strict) {
1618 if ("none".equalsIgnoreCase(proc)) {
1619 return false;
1620 }
1621 if (proc == null || proc.isBlank()) {
1622 if (strict && !isVersionEqualOrNewer("RELEASE_23")) {
1623 return true; // Before Java 23, default value of `-proc` was `full`.
1624 }
1625 /*
1626 * If the `proc` parameter was not specified, its default value depends on the Java version.
1627 * It was "full" prior Java 23 and become "none if no other processor option" since Java 23.
1628 */
1629 if (annotationProcessors == null || annotationProcessors.length == 0) {
1630 if (annotationProcessorPaths == null || annotationProcessorPaths.isEmpty()) {
1631 DependencyResolver resolver = session.getService(DependencyResolver.class);
1632 if (resolver == null) { // Null value happen during tests, depending on the mock used.
1633 return false;
1634 }
1635 var allowedTypes = EnumSet.of(JavaPathType.PROCESSOR_CLASSES, JavaPathType.PROCESSOR_MODULES);
1636 DependencyResolverResult dependencies = resolver.resolve(DependencyResolverRequest.builder()
1637 .session(session)
1638 .project(project)
1639 .requestType(DependencyResolverRequest.RequestType.COLLECT)
1640 .pathScope(compileScope)
1641 .pathTypeFilter(allowedTypes)
1642 .build());
1643
1644 return !dependencies.getDependencies().isEmpty();
1645 }
1646 }
1647 }
1648 return true;
1649 }
1650
1651 /**
1652 * Ensures that the directory for generated sources exists, and adds it to the list of source directories
1653 * known to the project manager. This is used for adding the output of annotation processor.
1654 * The returned set is either empty or a singleton.
1655 *
1656 * @return the added directory in a singleton set, or an empty set if none
1657 * @throws IOException if the directory cannot be created
1658 */
1659 final Set<Path> addGeneratedSourceDirectory() throws IOException {
1660 Path generatedSourcesDirectory = getGeneratedSourcesDirectory();
1661 if (generatedSourcesDirectory == null) {
1662 return Set.of();
1663 }
1664 /*
1665 * Do not create an empty directory if this plugin is not going to generate new source files.
1666 * However, if a directory already exists, use it because maybe its content was generated by
1667 * another plugin executed before the compiler plugin.
1668 */
1669 if (hasAnnotationProcessor(true)) {
1670 // `createDirectories(Path)` does nothing if the directory already exists.
1671 generatedSourcesDirectory = Files.createDirectories(generatedSourcesDirectory);
1672 } else if (Files.notExists(generatedSourcesDirectory)) {
1673 return Set.of();
1674 }
1675 ProjectScope scope = compileScope.projectScope();
1676 projectManager.addSourceRoot(project, scope, Language.JAVA_FAMILY, generatedSourcesDirectory.toAbsolutePath());
1677 if (logger.isDebugEnabled()) {
1678 var sb = new StringBuilder("Adding \"")
1679 .append(generatedSourcesDirectory)
1680 .append("\" to ")
1681 .append(scope.id())
1682 .append("-compile source roots. New roots are:");
1683 projectManager
1684 .getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY)
1685 .forEach((p) -> {
1686 sb.append(System.lineSeparator()).append(" ").append(p.directory());
1687 });
1688 logger.debug(sb.toString());
1689 }
1690 return Set.of(generatedSourcesDirectory);
1691 }
1692
1693 /**
1694 * Formats the {@code <plugin>} block of code for configuring this plugin with the given option.
1695 *
1696 * @param mb the message builder where to format the block of code
1697 * @param option name of the XML sub-element of {@code <configuration>} for the option
1698 * @param value the option value, or {@code null} if none
1699 */
1700 private void writePlugin(MessageBuilder mb, String option, String value) {
1701 if (mavenCompilerPluginVersion == null) {
1702 try (InputStream is = AbstractCompilerMojo.class.getResourceAsStream("/" + JarFile.MANIFEST_NAME)) {
1703 if (is != null) {
1704 mavenCompilerPluginVersion =
1705 new Manifest(is).getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
1706 }
1707 } catch (IOException e) {
1708 // noop
1709 }
1710 if (mavenCompilerPluginVersion == null) {
1711 mavenCompilerPluginVersion = "";
1712 }
1713 }
1714 mb.a(" <plugin>").newline();
1715 mb.a(" <groupId>org.apache.maven.plugins</groupId>").newline();
1716 mb.a(" <artifactId>maven-compiler-plugin</artifactId>").newline();
1717 if (mavenCompilerPluginVersion != null && !mavenCompilerPluginVersion.isBlank()) {
1718 mb.a(" <version>")
1719 .a(mavenCompilerPluginVersion)
1720 .a("</version>")
1721 .newline();
1722 }
1723 mb.a(" <configuration>").newline();
1724 mb.a(" <").a(option).a('>').a(value).a("</").a(option).a('>').newline();
1725 mb.a(" </configuration>").newline();
1726 mb.a(" </plugin>").newline();
1727 }
1728
1729 /**
1730 * Dumps the compiler options together with the list of source files into a debug file.
1731 * This is invoked in case of compilation failure, or if debug is enabled.
1732 *
1733 * <h4>Syntax</h4>
1734 * The arguments within a file can be separated by spaces or new line characters.
1735 * If a file name contains embedded spaces, then the whole file name must be between double quotation marks.
1736 * The -J options are not supported.
1737 *
1738 * @param executor the executor that compiled the classes
1739 * @param configuration options provided to the compiler
1740 * @param showBaseVersion whether the tip shown to user suggests the base Java release instead of the last one
1741 * @throws IOException if an error occurred while writing the debug file
1742 */
1743 private void writeDebugFile(final ToolExecutor executor, final Options configuration, final boolean showBaseVersion)
1744 throws IOException {
1745 final Path debugFilePath = getDebugFilePath();
1746 if (debugFilePath == null) {
1747 logger.warn("The <debugFileName> parameter should not be empty.");
1748 return;
1749 }
1750 final var commandLine = new StringBuilder("For trying to compile from the command-line, use:");
1751 Path dir = basedir;
1752 if (dir != null) { // Should never be null, but it has been observed with some Maven versions.
1753 dir = Path.of(System.getProperty("user.dir")).relativize(dir);
1754 String chdir = dir.toString();
1755 if (!chdir.isEmpty()) {
1756 boolean isWindows = (File.separatorChar == '\\');
1757 commandLine
1758 .append(System.lineSeparator())
1759 .append(" ")
1760 .append(isWindows ? "chdir " : "cd ")
1761 .append(chdir);
1762 }
1763 }
1764 commandLine.append(System.lineSeparator()).append(" ").append(executable != null ? executable : compilerId);
1765 Path pathForRelease = debugFilePath;
1766 /*
1767 * The following loop will iterate over all groups of source files compiled for the same Java release,
1768 * starting with the base release. If the project is not a multi-release project, it iterates only once.
1769 * If the compilation failed, the loop will stop after the first Java release for which an error occurred.
1770 */
1771 final int count = executor.sourcesForDebugFile.size();
1772 final int indexToShow = showBaseVersion ? 0 : count - 1;
1773 for (int i = 0; i < count; i++) {
1774 final SourcesForRelease sources = executor.sourcesForDebugFile.get(i);
1775 if (i != 0) {
1776 String version = sources.outputForRelease.getFileName().toString();
1777 String filename = debugFilePath.getFileName().toString();
1778 int s = filename.lastIndexOf('.');
1779 if (s >= 0) {
1780 filename = filename.substring(0, s) + '-' + version + filename.substring(s);
1781 } else {
1782 filename = filename + '-' + version;
1783 }
1784 pathForRelease = debugFilePath.resolveSibling(filename);
1785 }
1786 /*
1787 * Write the `javac.args` or `javac-<version>.args` file where `<version>` is the targeted Java release.
1788 * The `-J` options need to be on the command line rather than in the file, and therefore can be written
1789 * only once.
1790 */
1791 try (BufferedWriter out = Files.newBufferedWriter(pathForRelease)) {
1792 configuration.setRelease(sources.getReleaseString());
1793 configuration.format((i == indexToShow) ? commandLine : null, out);
1794 for (Map.Entry<PathType, List<Path>> entry : sources.dependencySnapshot.entrySet()) {
1795 writeOption(out, entry.getKey(), entry.getValue());
1796 }
1797 for (Map.Entry<String, Set<Path>> root : sources.roots.entrySet()) {
1798 String moduleName = root.getKey();
1799 writeOption(out, SourcePathType.valueOf(moduleName), root.getValue());
1800 }
1801 out.write("-d \"");
1802 out.write(relativize(sources.outputForRelease).toString());
1803 out.write('"');
1804 out.newLine();
1805 for (final Path file : sources.files) {
1806 out.write('"');
1807 out.write(relativize(file).toString());
1808 out.write('"');
1809 out.newLine();
1810 }
1811 }
1812 }
1813 Path path = relativize(showBaseVersion ? debugFilePath : pathForRelease);
1814 tipForCommandLineCompilation = commandLine.append(" @").append(path).toString();
1815 }
1816
1817 /**
1818 * Writes the paths for the given Java compiler option.
1819 *
1820 * @param out where to write
1821 * @param type the type of path to write as a compiler option
1822 * @param files the paths associated to the specified option
1823 * @throws IOException in an error occurred while writing to the output
1824 */
1825 private void writeOption(BufferedWriter out, PathType type, Collection<Path> files) throws IOException {
1826 if (!files.isEmpty()) {
1827 files = files.stream().map(this::relativize).toList();
1828 String separator = "";
1829 for (String element : type.option(files)) {
1830 out.write(separator);
1831 out.write(element);
1832 separator = " ";
1833 }
1834 out.newLine();
1835 }
1836 }
1837
1838 /**
1839 * Makes the given file relative to the base directory if the path is inside the project directory tree.
1840 * The check for the project directory tree (starting from the root of all sub-projects) is for avoiding
1841 * to relativize the paths to JAR files in the Maven local repository for example.
1842 *
1843 * @param file the path to make relative to the base directory
1844 * @return the given path, potentially relative to the base directory
1845 */
1846 private Path relativize(Path file) {
1847 final Path dir = basedir;
1848 if (dir != null) { // Should never be null, but it has been observed with some Maven versions.
1849 Path root = project.getRootDirectory();
1850 if (root != null && file.startsWith(root)) {
1851 try {
1852 file = dir.relativize(file);
1853 } catch (IllegalArgumentException e) {
1854 // Ignore, keep the absolute path.
1855 }
1856 }
1857 }
1858 return file;
1859 }
1860 }