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