View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.compiler;
20  
21  import javax.lang.model.SourceVersion;
22  import javax.tools.DiagnosticListener;
23  import javax.tools.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      * &lt;configuration&gt;
345      *   &lt;annotationProcessorPaths&gt;
346      *     &lt;path&gt;
347      *       &lt;groupId&gt;org.sample&lt;/groupId&gt;
348      *       &lt;artifactId&gt;sample-annotation-processor&lt;/artifactId&gt;
349      *       &lt;version&gt;1.2.3&lt;/version&gt; &lt;!-- Optional - taken from dependency management if not specified --&gt;
350      *       &lt;!-- Optionally exclude transitive dependencies --&gt;
351      *       &lt;exclusions&gt;
352      *         &lt;exclusion&gt;
353      *           &lt;groupId&gt;org.sample&lt;/groupId&gt;
354      *           &lt;artifactId&gt;sample-dependency&lt;/artifactId&gt;
355      *         &lt;/exclusion&gt;
356      *       &lt;/exclusions&gt;
357      *     &lt;/path&gt;
358      *     &lt;!-- ... more ... --&gt;
359      *   &lt;/annotationProcessorPaths&gt;
360      * &lt;/configuration&gt;
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      * &lt;configuration&gt;
727      *   &lt;jdkToolchain&gt;
728      *     &lt;version&gt;11&lt;/version&gt;
729      *   &lt;/jdkToolchain&gt;
730      *   ...
731      * &lt;/configuration&gt;
732      *
733      * &lt;configuration&gt;
734      *   &lt;jdkToolchain&gt;
735      *     &lt;version&gt;1.8&lt;/version&gt;
736      *     &lt;vendor&gt;zulu&lt;/vendor&gt;
737      *   &lt;/jdkToolchain&gt;
738      *   ...
739      * &lt;/configuration&gt;
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 }