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