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.StandardJavaFileManager;
27  import javax.tools.StandardLocation;
28  
29  import java.io.IOException;
30  import java.io.UncheckedIOException;
31  import java.io.Writer;
32  import java.nio.charset.Charset;
33  import java.nio.file.DirectoryNotEmptyException;
34  import java.nio.file.Files;
35  import java.nio.file.Path;
36  import java.util.ArrayDeque;
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Deque;
40  import java.util.EnumMap;
41  import java.util.EnumSet;
42  import java.util.HashMap;
43  import java.util.LinkedHashMap;
44  import java.util.LinkedHashSet;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.Optional;
49  import java.util.Set;
50  
51  import org.apache.maven.api.JavaPathType;
52  import org.apache.maven.api.PathType;
53  import org.apache.maven.api.plugin.Log;
54  import org.apache.maven.api.plugin.MojoException;
55  import org.apache.maven.api.services.DependencyResolverResult;
56  import org.apache.maven.api.services.MavenException;
57  
58  /**
59   * A task which configures and executes a Java tool such as the Java compiler.
60   * This class takes a snapshot of the information provided in the <abbr>MOJO</abbr>.
61   * Then, it collects additional information such as the source files and the dependencies.
62   * The set of source files to compile can optionally be filtered for keeping only the files
63   * that changed since the last build with the {@linkplain #applyIncrementalBuild incremental build}.
64   *
65   * <h2>Thread safety</h2>
66   * This class is not thread-safe. However, it is independent of the {@link AbstractCompilerMojo} instance
67   * given in argument to the constructor and to the {@linkplain #applyIncrementalBuild incremental build}.
68   * After all methods with an {@link AbstractCompilerMojo} argument have been invoked, {@code ToolExecutor}
69   * can safety be used in a background thread for launching the compilation (but must still be used by only
70   * only thread at a time).
71   *
72   * @author Martin Desruisseaux
73   */
74  public class ToolExecutor {
75      /**
76       * The locale for diagnostics, or {@code null} for the platform default.
77       */
78      private static final Locale LOCALE = null;
79  
80      /**
81       * The character encoding of source files, or {@code null} for the platform default encoding.
82       *
83       * @see AbstractCompilerMojo#encoding
84       */
85      protected final Charset encoding;
86  
87      /**
88       * The root directories of the Java source files to compile, excluding empty directories.
89       * The list needs to be modifiable for allowing the addition of generated source directories.
90       *
91       * @see AbstractCompilerMojo#compileSourceRoots
92       */
93      final List<SourceDirectory> sourceDirectories;
94  
95      /**
96       * The directories where to write generated source files.
97       * This set is either empty or a singleton.
98       *
99       * @see AbstractCompilerMojo#proc
100      * @see StandardLocation#SOURCE_OUTPUT
101      */
102     protected final Set<Path> generatedSourceDirectories;
103 
104     /**
105      * All source files to compile. May include files for many Java modules and many Java releases.
106      * When the compilation will be executed, those files will be grouped in compilation units where
107      * each unit will be the source files for one particular Java release.
108      *
109      * @see StandardLocation#SOURCE_PATH
110      * @see StandardLocation#MODULE_SOURCE_PATH
111      */
112     private List<SourceFile> sourceFiles;
113 
114     /**
115      * Whether the project contains or is assumed to contain a {@code module-info.java} file.
116      * If the user specified explicitly whether the project is a modular or a classpath JAR,
117      * then this flag is set to the user's specification without verification.
118      * Otherwise, this flag is determined by scanning the list of source files.
119      */
120     protected final boolean hasModuleDeclaration;
121 
122     /**
123      * How the source code of the project is organized, or {@code null} if not yet determined.
124      * <a href="https://docs.oracle.com/en/java/javase/25/docs/specs/man/javac.html#directory-hierarchies">Directory
125      * hierarchies</a> are <i>package hierarchy</i>, <i>module hierarchy</i> and <i>module source hierarchy</i>, but
126      * for the purpose of the compiler plugin we do not distinguish between the two latter.
127      *
128      * @see #determineDirectoryHierarchy(Collection)
129      */
130     private DirectoryHierarchy directoryHierarchy;
131 
132     /**
133      * The result of resolving the dependencies, or {@code null} if not available or not needed.
134      * For example, this field may be null if the constructor found no file to compile,
135      * so there is no need to fetch dependencies.
136      */
137     final DependencyResolverResult dependencyResolution;
138 
139     /**
140      * All dependencies grouped by the path types where to place them, together with the modules to patch.
141      * The path type can be the class-path, module-path, annotation processor path, patched path, <i>etc.</i>
142      * Some path types include a module name.
143      *
144      * <h4>Modifications during the build of multi-release project</h4>
145      * When building a multi-release project, values associated to {@code --class-path}, {@code --module-path}
146      * or {@code --patch-module} options are modified every time that {@code ToolExecutor} compiles for a new
147      * Java release. The output directories for the previous Java releases are inserted as the first elements
148      * of their lists, or new entries are created if no list existed previously for an option.
149      *
150      * @see #dependencies(PathType)
151      * @see #prependDependency(PathType, Path)
152      */
153     private final Map<PathType, Collection<Path>> dependencies;
154 
155     /**
156      * The destination directory (or class output directory) for class files.
157      * This directory will be given to the {@code -d} Java compiler option
158      * when compiling the classes for the base Java release.
159      *
160      * @see AbstractCompilerMojo#getOutputDirectory()
161      */
162     protected final Path outputDirectory;
163 
164     /**
165      * Configuration of the incremental compilation.
166      *
167      * @see AbstractCompilerMojo#incrementalCompilation
168      * @see AbstractCompilerMojo#useIncrementalCompilation
169      */
170     private final EnumSet<IncrementalBuild.Aspect> incrementalBuildConfig;
171 
172     /**
173      * The incremental build to save if the build succeed.
174      * In case of failure, the cached information will be unchanged.
175      */
176     private IncrementalBuild incrementalBuild;
177 
178     /**
179      * Whether only a subset of the files will be compiled. This flag can be {@code true} only when
180      * incremental build is enabled and detected that some files do not need to be recompiled.
181      */
182     private boolean isPartialBuild;
183 
184     /**
185      * Where to send the compilation warning (never {@code null}). If a null value was specified
186      * to the constructor, then this listener sends the warnings to the Maven {@linkplain #logger}.
187      */
188     protected final DiagnosticListener<? super JavaFileObject> listener;
189 
190     /**
191      * The Maven logger for reporting information or warnings to the user.
192      * Used for messages emitted directly by the Maven compiler plugin.
193      * Not necessarily used for messages emitted by the Java compiler.
194      *
195      * <h4>Thread safety</h4>
196      * This logger should be thread-safe if this {@code ToolExecutor} is executed in a background thread.
197      *
198      * @see AbstractCompilerMojo#logger
199      */
200     protected final Log logger;
201 
202     /**
203      * The sources to write in the {@code target/javac.args} debug files.
204      * This list contains only the sources for which the compiler has been executed, successfully or not.
205      * If a compilation error occurred, the last element in the list contains the sources where the error occurred.
206      */
207     final List<SourcesForRelease> sourcesForDebugFile;
208 
209     /**
210      * Creates a new task by taking a snapshot of the current configuration of the given <abbr>MOJO</abbr>.
211      * This constructor creates the {@linkplain #outputDirectory output directory} if it does not already exist.
212      *
213      * @param mojo the <abbr>MOJO</abbr> from which to take a snapshot
214      * @param listener where to send compilation warnings, or {@code null} for the Maven logger
215      * @throws MojoException if this constructor identifies an invalid parameter in the <abbr>MOJO</abbr>
216      * @throws IOException if an error occurred while creating the output directory or scanning the source directories
217      * @throws MavenException if an error occurred while fetching dependencies
218      *
219      * @see AbstractCompilerMojo#createExecutor(DiagnosticListener)
220      */
221     @SuppressWarnings("deprecation")
222     protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener<? super JavaFileObject> listener)
223             throws IOException {
224 
225         logger = mojo.logger;
226         if (listener == null) {
227             Path root = mojo.project.getRootDirectory();
228             listener = new DiagnosticLogger(logger, mojo.messageBuilderFactory, LOCALE, root);
229         }
230         this.listener = listener;
231         encoding = mojo.charset();
232         incrementalBuildConfig = mojo.incrementalCompilationConfiguration();
233         outputDirectory = Files.createDirectories(mojo.getOutputDirectory());
234         sourceDirectories = mojo.getSourceDirectories(outputDirectory);
235         dependencies = new LinkedHashMap<>();
236         sourcesForDebugFile = new ArrayList<>();
237         /*
238          * Get the source files and whether they include or are assumed to include `module-info.java`.
239          * Note that we perform this step after processing compiler arguments, because this block may
240          * skip the build if there is no source code to compile. We want arguments to be verified first
241          * in order to warn about possible configuration problems.
242          */
243         if (incrementalBuildConfig.contains(IncrementalBuild.Aspect.MODULES)) {
244             boolean hasNoFileMatchers = mojo.hasNoFileMatchers();
245             for (SourceDirectory root : sourceDirectories) {
246                 if (root.moduleName == null) {
247                     throw new CompilationFailureException("The <incrementalCompilation> value can be \"modules\" "
248                             + "only if all source directories are Java modules.");
249                 }
250                 hasNoFileMatchers &= root.includes.isEmpty() && root.excludes.isEmpty();
251             }
252             if (!hasNoFileMatchers) {
253                 throw new CompilationFailureException("Include and exclude filters cannot be specified "
254                         + "when <incrementalCompilation> is set to \"modules\".");
255             }
256             hasModuleDeclaration = true;
257             sourceFiles = List.of();
258         } else {
259             /*
260              * The order of the two next lines matter for initialization of `SourceDirectory.moduleInfo`.
261              * This initialization is done indirectly when the walk invokes the `SourceFile` constructor,
262              * which in turn invokes `SourceDirectory.visit(Path)`.
263              */
264             sourceFiles = new PathFilter(mojo).walkSourceFiles(sourceDirectories);
265             hasModuleDeclaration = mojo.hasModuleDeclaration(sourceDirectories);
266             if (sourceFiles.isEmpty()) {
267                 generatedSourceDirectories = Set.of();
268                 dependencyResolution = null;
269                 return;
270             }
271         }
272         /*
273          * Get the dependencies. If the module-path contains any automatic (filename-based)
274          * dependency and the MOJO is compiling the main code, then a warning will be logged.
275          */
276         dependencyResolution = mojo.resolveDependencies(hasModuleDeclaration);
277         if (dependencyResolution != null) {
278             dependencies.putAll(dependencyResolution.getDispatchedPaths());
279         }
280         mojo.resolveProcessorPathEntries(dependencies);
281         mojo.amendincrementalCompilation(incrementalBuildConfig, dependencies.keySet());
282         generatedSourceDirectories = mojo.addGeneratedSourceDirectory(dependencies.keySet());
283         copyDependencyValues();
284     }
285 
286     /**
287      * Copies all values of the dependency map in unmodifiable lists.
288      * This is used for creating a snapshot of the current state of the dependency map.
289      */
290     private void copyDependencyValues() {
291         dependencies.entrySet().forEach((entry) -> entry.setValue(List.copyOf(entry.getValue())));
292     }
293 
294     /**
295      * Returns the output directory of the main classes if they were compiled in a previous Maven phase.
296      * This method shall always return {@code null} when compiling to main code. The return value can be
297      * non-null only when compiling the test classes, in which case the returned path is the directory to
298      * prepend to the class-path or module-path before to compile the classes managed by this executor.
299      *
300      * @return the directory to prepend to the class-path or module-path, or {@code null} if none
301      */
302     Path getOutputDirectoryOfPreviousPhase() {
303         return null;
304     }
305 
306     /**
307      * Returns the directory of the classes compiled for the specified module.
308      * If the project is multi-release, this method returns the directory for the base version.
309      *
310      * <p>This is normally a sub-directory of the same name as the module name.
311      * However, when building tests for a project which is both multi-release and multi-module,
312      * the directory may exist only for a target Java version higher than the base version.</p>
313      *
314      * @param outputDirectory the output directory which is the root of modules
315      * @param moduleName the name of the module for which the class directory is desired
316      * @return directories of classes for the given module
317      */
318     Path resolveModuleOutputDirectory(Path outputDirectory, String moduleName) {
319         return outputDirectory.resolve(moduleName);
320     }
321 
322     /**
323      * Name of the module when using package hierarchy, or {@code null} if not applicable.
324      * This is used for setting {@code --patch-module} option during compilation of tests.
325      * This field is null in a class-path project or in a multi-module project.
326      *
327      * <p>This information is used for compatibility with the Maven 3 way to build a modular project.
328      * It is recommended to use the {@code <sources>} element instead. We may remove this method in a
329      * future version if we abandon compatibility with the Maven 3 way to build modular projects.</p>
330      *
331      * @deprecated Declare modules in {@code <source>} elements instead.
332      */
333     @Deprecated(since = "4.0.0")
334     String moduleNameFromPackageHierarchy() {
335         return null;
336     }
337 
338     /**
339      * {@return whether a release version is specified for all sources}
340      */
341     final boolean isReleaseSpecifiedForAll() {
342         for (SourceDirectory source : sourceDirectories) {
343             if (source.release == null) {
344                 return false;
345             }
346         }
347         return true;
348     }
349 
350     /**
351      * Filters the source files to recompile, or cleans the output directory if everything should be rebuilt.
352      * If the directory structure of the source files has changed since the last build,
353      * or if a compiler option changed, or if a dependency changed,
354      * then this method keeps all source files and cleans the {@linkplain #outputDirectory output directory}.
355      * Otherwise, the source files that did not changed since the last build are removed from the list of sources
356      * to compile. If all source files have been removed, then this method returns {@code false} for notifying the
357      * caller that it can skip the build.
358      *
359      * <p>If this method is invoked many times, all invocations after this first one have no effect.</p>
360      *
361      * @param mojo the <abbr>MOJO</abbr> from which to take the incremental build configuration
362      * @param configuration the options which should match the options used during the last build
363      * @throws IOException if an error occurred while accessing the cache file or walking through the directory tree
364      * @return whether there is at least one file to recompile
365      */
366     public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Options configuration)
367             throws IOException {
368         final boolean checkSources = incrementalBuildConfig.contains(IncrementalBuild.Aspect.SOURCES);
369         final boolean checkClasses = incrementalBuildConfig.contains(IncrementalBuild.Aspect.CLASSES);
370         final boolean checkDepends = incrementalBuildConfig.contains(IncrementalBuild.Aspect.DEPENDENCIES);
371         final boolean checkOptions = incrementalBuildConfig.contains(IncrementalBuild.Aspect.OPTIONS);
372         if (checkSources | checkClasses | checkDepends | checkOptions) {
373             incrementalBuild =
374                     new IncrementalBuild(mojo, sourceFiles, checkSources, configuration, incrementalBuildConfig);
375             String causeOfRebuild = null;
376             if (checkSources) {
377                 // Should be first, because this method deletes output files of removed sources.
378                 causeOfRebuild = incrementalBuild.inputFileTreeChanges();
379             }
380             if (checkClasses && causeOfRebuild == null) {
381                 causeOfRebuild = incrementalBuild.markNewOrModifiedSources();
382             }
383             if (checkDepends && causeOfRebuild == null) {
384                 List<String> fileExtensions = mojo.fileExtensions;
385                 causeOfRebuild = incrementalBuild.dependencyChanges(dependencies.values(), fileExtensions);
386             }
387             if (checkOptions && causeOfRebuild == null) {
388                 causeOfRebuild = incrementalBuild.optionChanges();
389             }
390             if (causeOfRebuild != null) {
391                 if (!sourceFiles.isEmpty()) { // Avoid misleading message such as "all sources changed".
392                     logger.info(causeOfRebuild);
393                 }
394             } else {
395                 isPartialBuild = true;
396                 sourceFiles = incrementalBuild.getModifiedSources();
397                 if (IncrementalBuild.isEmptyOrIgnorable(sourceFiles)) {
398                     incrementalBuildConfig.clear(); // Prevent this method to be executed twice.
399                     logger.info("Nothing to compile - all classes are up to date.");
400                     sourceFiles = List.of();
401                     return false;
402                 } else {
403                     int n = sourceFiles.size();
404                     var sb = new StringBuilder("Compiling ").append(n).append(" modified source file");
405                     if (n > 1) {
406                         sb.append('s'); // Make plural.
407                     }
408                     logger.info(sb.append('.'));
409                 }
410             }
411             if (!(checkSources | checkDepends | checkOptions)) {
412                 incrementalBuild.deleteCache();
413                 incrementalBuild = null;
414             }
415         }
416         incrementalBuildConfig.clear(); // Prevent this method to be executed twice.
417         return true;
418     }
419 
420     /**
421      * Writes the incremental build cache into the {@code target/maven-status/maven-compiler-plugin/} directory.
422      * This method should be invoked only once. Next invocations after the first one have no effect.
423      *
424      * @throws IOException if an error occurred while writing the cache
425      */
426     private void saveIncrementalBuild() throws IOException {
427         if (incrementalBuild != null) {
428             incrementalBuild.writeCache();
429             incrementalBuild = null;
430         }
431     }
432 
433     /**
434      * {@return a modifiable collection of paths to all dependencies of the given type}
435      * The returned collection is intentionally live: elements can be added or removed
436      * from the collection for changing the state of this executor.
437      *
438      * @param  pathType  type of path for which to get the dependencies
439      */
440     protected Deque<Path> dependencies(PathType pathType) {
441         return (Deque<Path>) dependencies.compute(pathType, (key, paths) -> {
442             if (paths == null) {
443                 return new ArrayDeque<>();
444             } else if (paths instanceof ArrayDeque<Path> deque) {
445                 return deque;
446             } else {
447                 var copy = new ArrayDeque<Path>(paths.size() + 4); // Anticipate the addition of new elements.
448                 copy.addAll(paths);
449                 return copy;
450             }
451         });
452     }
453 
454     /**
455      * Dispatches sources and dependencies on the kind of paths determined by {@code DependencyResolver}.
456      * The targets may be class-path, module-path, annotation processor class-path/module-path, <i>etc</i>.
457      *
458      * @param fileManager the file manager where to set the dependency paths
459      */
460     private void setDependencyPaths(final StandardJavaFileManager fileManager) throws IOException {
461         final var unresolvedPaths = new ArrayList<Path>();
462         for (Map.Entry<PathType, Collection<Path>> entry : dependencies.entrySet()) {
463             Collection<Path> paths = entry.getValue();
464             PathType key = entry.getKey();
465             if (key instanceof JavaPathType type) {
466                 /*
467                  * Dependency to a JAR file (usually).
468                  * Placed on: --class-path, --module-path.
469                  */
470                 Optional<JavaFileManager.Location> location = type.location();
471                 if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException.
472                     var value = location.get();
473                     if (value == StandardLocation.CLASS_PATH) {
474                         if (isPartialBuild && !hasModuleDeclaration) {
475                             /*
476                              * From https://docs.oracle.com/en/java/javase/24/docs/specs/man/javac.html:
477                              * "When compiling code for one or more modules, the class output directory will
478                              * automatically be checked when searching for previously compiled classes.
479                              * When not compiling for modules, for backwards compatibility, the directory is not
480                              * automatically checked for previously compiled classes, and so it is recommended to
481                              * specify the class output directory as one of the locations on the user class path,
482                              * using the --class-path option or one of its alternate forms."
483                              */
484                             paths = new ArrayDeque<>(paths);
485                             paths.add(outputDirectory);
486                             entry.setValue(paths);
487                         }
488                     }
489                     fileManager.setLocationFromPaths(value, paths);
490                     continue;
491                 }
492             } else if (key instanceof JavaPathType.Modular type) {
493                 /*
494                  * Main code to be tested by the test classes. This is handled as a "dependency".
495                  * Placed on: --patch-module-path.
496                  */
497                 Optional<JavaFileManager.Location> location = type.rawType().location();
498                 if (location.isPresent()) {
499                     fileManager.setLocationForModule(location.get(), type.moduleName(), paths);
500                     continue;
501                 }
502             }
503             unresolvedPaths.addAll(paths);
504         }
505         if (!unresolvedPaths.isEmpty()) {
506             var sb = new StringBuilder("Cannot determine where to place the following artifacts:");
507             for (Path p : unresolvedPaths) {
508                 sb.append(System.lineSeparator()).append(" - ").append(p);
509             }
510             logger.warn(sb);
511         }
512     }
513 
514     /**
515      * Inserts the given path as the first element of the list of paths of the given type.
516      * The main purpose of this method is during the build of a multi-release project,
517      * for adding the output directory of the code targeting the previous Java release
518      * before to compile the code targeting the next Java release. In this context,
519      * the {@code type} argument usually identifies a {@code --class-path},
520      * {@code --module-path} or {@code --patch-module} option.
521      *
522      * @param  pathType type of path for which to add an element
523      * @param  first the path to put first
524      * @return the new paths for the given type, as a modifiable list
525      */
526     protected Deque<Path> prependDependency(final PathType pathType, final Path first) {
527         Deque<Path> paths = dependencies(pathType);
528         paths.addFirst(first);
529         return paths;
530     }
531 
532     /**
533      * Ensures that the given value is non-null, replacing null values by the latest version.
534      */
535     private static SourceVersion nonNullOrLatest(SourceVersion release) {
536         return (release != null) ? release : SourceVersion.latest();
537     }
538 
539     /**
540      * Groups all sources files first by Java release versions, then by module names.
541      * The elements are sorted in the order of {@link SourceVersion} enumeration values,
542      * with null version sorted last on the assumption that they will be for the latest
543      * version supported by the runtime environment.
544      *
545      * @return the given sources grouped by Java release versions and module names
546      */
547     private Collection<SourcesForRelease> groupByReleaseAndModule() {
548         var result = new EnumMap<SourceVersion, SourcesForRelease>(SourceVersion.class);
549         for (SourceDirectory directory : sourceDirectories) {
550             /*
551              * We need an entry for every versions even if there is no source to compile for a version.
552              * This is needed for configuring the classpath in a consistent way, for example with the
553              * output directory of previous version even if we skipped the compilation of that version.
554              */
555             SourcesForRelease unit = result.computeIfAbsent(
556                     nonNullOrLatest(directory.release),
557                     (release) -> new SourcesForRelease(directory.release)); // Intentionally ignore the key.
558             String moduleName = directory.moduleName;
559             if (moduleName == null || moduleName.isBlank()) {
560                 moduleName = "";
561             }
562             unit.roots.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<Path>());
563         }
564         for (SourceFile source : sourceFiles) {
565             result.get(nonNullOrLatest(source.directory.release)).add(source);
566         }
567         return result.values();
568     }
569 
570     /**
571      * Checks if there are no sources to compile and handles that case.
572      * When there are no sources, this method cleans up the output directory and logs a message.
573      *
574      * @return {@code true} if there are no sources to compile, {@code false} if there are sources
575      * @throws IOException if an error occurred while deleting the empty output directory
576      */
577     private boolean noSourcesToCompile() throws IOException {
578         sourcesForDebugFile.clear();
579         if (sourceFiles.isEmpty()) {
580             String message = "No sources to compile.";
581             try {
582                 // The directory must exist since it was created in the constructor.
583                 Files.delete(outputDirectory);
584             } catch (DirectoryNotEmptyException e) {
585                 message += " However, the output directory is not empty.";
586             }
587             logger.info(message);
588             return true;
589         }
590         if (logger.isDebugEnabled()) {
591             int n = sourceFiles.size();
592             @SuppressWarnings("checkstyle:MagicNumber")
593             var sb = new StringBuilder(n * 40).append("The source files to compile are:");
594             for (SourceFile file : sourceFiles) {
595                 sb.append(System.lineSeparator()).append("    ").append(file);
596             }
597             logger.debug(sb);
598         }
599         return false;
600     }
601 
602     /**
603      * Determines the directory hierarchy by scanning all compilation units.
604      * Also validates that there are no conflicting directory hierarchies
605      * and performs the necessary remapping for Maven 3 compatibility.
606      * This should be called once before processing any units.
607      *
608      * @param units all compilation units to scan
609      * @throws CompilationFailureException if both explicit and detected module names are present
610      */
611     private void determineDirectoryHierarchy(final Collection<SourcesForRelease> units) {
612         final String moduleNameFromPackageHierarchy = moduleNameFromPackageHierarchy();
613         for (SourcesForRelease unit : units) {
614             for (String moduleName : unit.roots.keySet()) {
615                 DirectoryHierarchy detected;
616                 if (moduleName.isEmpty()) {
617                     if (moduleNameFromPackageHierarchy == null) {
618                         detected = DirectoryHierarchy.PACKAGE;
619                     } else {
620                         detected = DirectoryHierarchy.PACKAGE_WITH_MODULE;
621                     }
622                 } else {
623                     if (moduleNameFromPackageHierarchy == null) {
624                         detected = DirectoryHierarchy.MODULE_SOURCE;
625                     } else {
626                         // Mix of package hierarchy and module source hierarchy.
627                         throw new CompilationFailureException(
628                                 "The \"%s\" module must be declared in a <module> element of <sources>."
629                                         .formatted(moduleNameFromPackageHierarchy));
630                     }
631                 }
632                 if (directoryHierarchy == null) {
633                     directoryHierarchy = detected;
634                 } else if (directoryHierarchy != detected) {
635                     throw new CompilationFailureException(
636                             "Mix of %s and %s hierarchies.".formatted(directoryHierarchy, detected));
637                 }
638             }
639         }
640         /*
641          * The following adjustment is for the case when the project is a Java module, but nevertheless organized
642          * in a package hierarchy instead of a module source hierarchy. Update the `unit.roots` map for compiling
643          * the module as if module source hiearchy was used. It will require moving the output directory after
644          * compilation, which is done by `ModuleDirectoryRemover`.
645          */
646         if (moduleNameFromPackageHierarchy != null) {
647             for (SourcesForRelease unit : units) {
648                 Set<Path> paths = unit.roots.remove("");
649                 if (paths != null) {
650                     unit.roots.put(moduleNameFromPackageHierarchy, paths);
651                 }
652             }
653         }
654     }
655 
656     /**
657      * Manager of class-path or module-paths specified to a {@link StandardJavaFileManager}.
658      * This base class assumes {@link DirectoryHierarchy#PACKAGE}, and a subclass is defined
659      * for the {@link DirectoryHierarchy#MODULE_SOURCE} case.
660      */
661     private class PathManager {
662         /**
663          * The file manager to configure for class-path or module-paths.
664          */
665         protected final StandardJavaFileManager fileManager;
666 
667         /**
668          * The output directory of the previous compilation phase or version.
669          * For test compilation, this is the main output directory.
670          * For multi-release, this is the output of the previous Java version.
671          */
672         protected Path latestOutputDirectory;
673 
674         /**
675          * Whether we are compiling a version after the base version.
676          *
677          * @see #markVersioned()
678          */
679         private boolean isVersioned;
680 
681         /**
682          * Creates a new path manager for the given file manager.
683          *
684          * @param fileManager the file manager to configure for class-path or module-paths
685          */
686         protected PathManager(StandardJavaFileManager fileManager) {
687             this.fileManager = fileManager;
688             latestOutputDirectory = getOutputDirectoryOfPreviousPhase();
689         }
690 
691         /**
692          * Merges all the given sets into a single set. We use our own loop instead of streams
693          * because the given collection should always contain exactly one {@code Set<Path>},
694          * so we can return that set directly without copying its content in a new set.
695          * The merge is a paranoiac safety as we could also throw an exception instead.
696          */
697         private static Set<Path> merge(final Collection<Set<Path>> directories) {
698             Set<Path> allSources = Set.of();
699             for (Set<Path> more : directories) {
700                 if (allSources.isEmpty()) {
701                     allSources = more;
702                 } else {
703                     // Should never happen, but merge anyway by safety.
704                     allSources = new LinkedHashSet<>(allSources);
705                     allSources.addAll(more);
706                 }
707             }
708             return allSources;
709         }
710 
711         /**
712          * Configures source directories for all roots in a compilation unit.
713          * Also configures the class-path or module-paths with the output directories
714          * of previous compilation units (if any).
715          *
716          * <h4>Default implementation</h4>
717          * The default implementation configures source directories and class-path for package hierarchy
718          * without {@code module-info}. Sub-classes need to override this method if the project is modular.
719          *
720          * @param roots map of module names to source paths
721          * @throws IOException if an error occurred while setting locations
722          */
723         protected void configureSourcePaths(final Map<String, Set<Path>> roots) throws IOException {
724             fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, merge(roots.values()));
725 
726             // For multi-release builds, add previous version's output to class-path.
727             if (latestOutputDirectory != null) {
728                 Deque<Path> paths = prependDependency(JavaPathType.CLASSES, latestOutputDirectory);
729                 fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, paths);
730             }
731         }
732 
733         /**
734          * Sets up the output directory for a compilation unit.
735          *
736          * @param unit the compilation unit
737          * @throws IOException if an error occurred while creating directories or setting locations
738          */
739         final void setupOutputDirectory(final SourcesForRelease unit) throws IOException {
740             Path outputForRelease = outputDirectory;
741             if (isVersioned) {
742                 outputForRelease = Files.createDirectories(
743                         directoryHierarchy.outputDirectoryForReleases(outputForRelease, unit.release));
744             }
745             fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputForRelease));
746             // Records that a compilation unit completed, updating the baseline for the next phase.
747             latestOutputDirectory = outputForRelease;
748             unit.outputForRelease = outputForRelease;
749             sourcesForDebugFile.add(unit);
750         }
751 
752         /**
753          * Marks that subsequent iterations are for versions after the base version.
754          */
755         final void markVersioned() {
756             isVersioned = true;
757         }
758     }
759 
760     /**
761      * Manager of module-paths specified to a {@link StandardJavaFileManager}.
762      * This subclass handles the {@link DirectoryHierarchy#MODULE_SOURCE} case.
763      *
764      * <h2>Implementation details</h2>
765      * The fields in this class are used for patching, i.e. when compiling test classes or a non-base version
766      * of a multi-release project. The output directory of the previous Java version needs to be added to the
767      * class-path or module-path. However, in the case of a modular project, we can add to the module path only
768      * once and all other additions must be done as patches.
769      */
770     private final class ModulePathManager extends PathManager {
771         /**
772          * Whether we can add output directories to the module-path.
773          * For modular projects, we can only add to module-path once.
774          * Subsequent additions must use {@code --patch-module}.
775          */
776         private boolean canAddOutputToModulePath;
777 
778         /**
779          * Tracks modules from previous versions that may not be present in the current version.
780          * Keys are module names, values indicate whether cleanup is needed.
781          */
782         private final Map<String, Boolean> modulesNotPresentInNewVersion;
783 
784         /**
785          * Tracks how many source directories were added as patches per module.
786          * Keys are module names, values are the count of source directories.
787          * Used to remove these source entries and replace them with compiled output.
788          *
789          * <h4>Purpose</h4>
790          * When patching a module, the source directories of the compilation unit are declared as a patch applied
791          * over the output directories of previous compilation units. But after the compilation, if there are more
792          * units to compile, we will need to replace the sources in {@code --patch-module} by the compilation output
793          * before to declare the source directories of the next compilation unit.
794          */
795         private final Map<String, Integer> modulesWithSourcesAsPatches;
796 
797         /**
798          * Creates a new path manager for the given file manager.
799          *
800          * @param fileManager  the  file manager to configure for class-path or module-paths
801          */
802         ModulePathManager(StandardJavaFileManager fileManager) {
803             super(fileManager);
804             canAddOutputToModulePath = true;
805             modulesNotPresentInNewVersion = new LinkedHashMap<>();
806             modulesWithSourcesAsPatches = new HashMap<>();
807         }
808 
809         /**
810          * Configures module source paths for all roots in a compilation unit.
811          * If the project uses package hierarchy with a {@code module-info} file,
812          * the module names in the keys of the {@code roots} map must have been resolved by
813          * {@link #determineDirectoryHierarchy(Collection)} before to invoke this method.</p>
814          *
815          * <p>Configures also the {@code --patch-module} options for a module being compiled for
816          * a newer Java version. The patch consists of (in order, highest priority first):</p>
817          * <ol>
818          *   <li>Current source paths (so the compiler sees the new version's sources).</li>
819          *   <li>Output from previous Java version (compiled classes to inherit).</li>
820          *   <li>Existing patch-module dependencies.</li>
821          * </ol>
822          *
823          * @param roots map of module names to source paths
824          * @throws IOException if an error occurred while setting locations
825          */
826         @Override
827         protected void configureSourcePaths(final Map<String, Set<Path>> roots) throws IOException {
828             for (var entry : roots.entrySet()) {
829                 final String moduleName = entry.getKey();
830                 final Set<Path> sourcePaths = entry.getValue();
831                 fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName, sourcePaths);
832                 modulesNotPresentInNewVersion.put(moduleName, Boolean.FALSE);
833                 /*
834                  * When compiling for the base Java version, the configuration for current module is finished.
835                  * The remaining of this loop is executed only for target Java versions after the base version.
836                  * In those cases, we need to add the paths to the classes compiled for the previous version.
837                  * A non-modular project would always add the paths to the class-path. For a modular project,
838                  * add the paths to the module-path only the first time. After, we need to use patch-module.
839                  */
840                 if (latestOutputDirectory != null) {
841                     if (canAddOutputToModulePath) {
842                         canAddOutputToModulePath = false;
843                         Deque<Path> paths = prependDependency(JavaPathType.MODULES, latestOutputDirectory);
844                         fileManager.setLocationFromPaths(StandardLocation.MODULE_PATH, paths);
845                     }
846                     /*
847                      * For a modular project, following block can be executed an arbitrary number of times
848                      * We need to declare that the sources that we are compiling are for patching a module.
849                      * But we also need to remember that these sources will need to be removed in the next
850                      * iteration, because they will be replaced by the compiled classes (the above block).
851                      */
852                     final Deque<Path> paths = dependencies(JavaPathType.patchModule(moduleName));
853                     removeFirsts(paths, modulesWithSourcesAsPatches.put(moduleName, sourcePaths.size()));
854                     Path latestOutput = resolveModuleOutputDirectory(latestOutputDirectory, moduleName);
855                     if (Files.exists(latestOutput)) {
856                         paths.addFirst(latestOutput);
857                     }
858                     sourcePaths.forEach(paths::addFirst);
859                     fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths);
860                 }
861             }
862             omitSourcelessModulesInNewVersion();
863         }
864 
865         /**
866          * Removes from compilation the modules that were present in previous version but not in the current version.
867          * This clears the source paths and updates patch-module for leftover modules.
868          * This method has no effect when compiling for the base Java version.
869          *
870          * @throws IOException if an error occurred while setting locations
871          */
872         private void omitSourcelessModulesInNewVersion() throws IOException {
873             for (var iterator = modulesNotPresentInNewVersion.entrySet().iterator(); iterator.hasNext(); ) {
874                 Map.Entry<String, Boolean> entry = iterator.next();
875                 if (entry.getValue()) {
876                     String moduleName = entry.getKey();
877                     Deque<Path> paths = dependencies(JavaPathType.patchModule(moduleName));
878                     if (removeFirsts(paths, modulesWithSourcesAsPatches.remove(moduleName))) {
879                         paths.addFirst(latestOutputDirectory.resolve(moduleName));
880                     } else if (paths.isEmpty()) {
881                         // Not sure why the following is needed, but it has been observed in real projects.
882                         paths.add(outputDirectory.resolve(moduleName));
883                     }
884                     fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths);
885                     fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName, Set.of());
886                     iterator.remove();
887                 } else {
888                     entry.setValue(Boolean.TRUE); // For compilation of next target version (if any).
889                 }
890             }
891         }
892 
893         /**
894          * Removes the first <var>n</var> elements of the given collection.
895          * This is used for removing {@code --patch-module} items that were added as source directories.
896          * The callers should replace the removed items by the output directory of these source files.
897          *
898          * @param paths  the paths from which to remove the first elements
899          * @param count  number of elements to remove, or {@code null} if none
900          * @return whether at least one item has been removed
901          */
902         private static boolean removeFirsts(Deque<Path> paths, Integer count) {
903             boolean changed = false;
904             if (count != null) {
905                 for (int i = count; --i >= 0; ) {
906                     changed |= (paths.removeFirst() != null);
907                 }
908             }
909             return changed;
910         }
911     }
912 
913     /**
914      * Runs the compilation task.
915      *
916      * @param compiler the compiler
917      * @param configuration the options to give to the Java compiler
918      * @param otherOutput where to write additional output from the compiler
919      * @return whether the compilation succeeded
920      * @throws IOException if an error occurred while reading or writing a file
921      * @throws MojoException if the compilation failed for a reason identified by this method
922      * @throws RuntimeException if any other kind of  error occurred
923      */
924     public boolean compile(JavaCompiler compiler, final Options configuration, final Writer otherOutput)
925             throws IOException {
926 
927         if (noSourcesToCompile()) {
928             return true;
929         }
930 
931         // Determine project type once from all units before processing.
932         final Collection<SourcesForRelease> units = groupByReleaseAndModule();
933         determineDirectoryHierarchy(units);
934 
935         // Workaround for a `javax.tools` method which seems not yet supported on all compilers.
936         if (WorkaroundForPatchModule.ENABLED && hasModuleDeclaration && !(compiler instanceof ForkedTool)) {
937             compiler = new WorkaroundForPatchModule(compiler);
938         }
939         boolean success = true;
940         try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding)) {
941             setDependencyPaths(fileManager);
942             if (!generatedSourceDirectories.isEmpty()) {
943                 fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, generatedSourceDirectories);
944             }
945             final PathManager pathManager =
946                     switch (directoryHierarchy) {
947                         case PACKAGE -> new PathManager(fileManager);
948                         case PACKAGE_WITH_MODULE, MODULE_SOURCE -> new ModulePathManager(fileManager);
949                     };
950 
951             // Compile each release version in order (base version first for multi-release projects).
952             for (final SourcesForRelease unit : units) {
953                 configuration.setRelease(unit.getReleaseString());
954                 pathManager.configureSourcePaths(unit.roots);
955 
956                 // Snapshot dependencies for debug file
957                 copyDependencyValues();
958                 unit.dependencySnapshot = new LinkedHashMap<>(dependencies);
959 
960                 // Set up output directory and compile (only if there are files).
961                 pathManager.setupOutputDirectory(unit);
962 
963                 // Compile the source files now.
964                 if (!unit.files.isEmpty()) {
965                     Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjectsFromPaths(unit.files);
966                     JavaCompiler.CompilationTask task;
967                     task = compiler.getTask(otherOutput, fileManager, listener, configuration.options, null, sources);
968                     success = task.call();
969                     if (!success) {
970                         break;
971                     }
972                 }
973                 pathManager.markVersioned();
974             }
975         } catch (UncheckedIOException e) {
976             throw e.getCause();
977         }
978 
979         // Performs post-compilation tasks such as logging and writing incremental build cache.
980         if (listener instanceof DiagnosticLogger diagnostic) {
981             diagnostic.logSummary();
982         }
983         if (success) {
984             saveIncrementalBuild();
985         }
986         return success;
987     }
988 }