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.ArrayList;
37  import java.util.Collection;
38  import java.util.EnumMap;
39  import java.util.EnumSet;
40  import java.util.LinkedHashMap;
41  import java.util.LinkedHashSet;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.Map;
45  import java.util.Optional;
46  import java.util.Set;
47  
48  import org.apache.maven.api.JavaPathType;
49  import org.apache.maven.api.PathType;
50  import org.apache.maven.api.plugin.Log;
51  import org.apache.maven.api.plugin.MojoException;
52  import org.apache.maven.api.services.DependencyResolverResult;
53  import org.apache.maven.api.services.MavenException;
54  
55  /**
56   * A task which configures and executes a Java tool such as the Java compiler.
57   * This class takes a snapshot of the information provided in the <abbr>MOJO</abbr>.
58   * Then, it collects additional information such as the source files and the dependencies.
59   * The set of source files to compile can optionally be filtered for keeping only the files
60   * that changed since the last build with the {@linkplain #applyIncrementalBuild incremental build}.
61   *
62   * <h2>Thread safety</h2>
63   * This class is not thread-safe. However, it is independent of the {@link AbstractCompilerMojo} instance
64   * given in argument to the constructor and to the {@linkplain #applyIncrementalBuild incremental build}.
65   * After all methods with an {@link AbstractCompilerMojo} argument have been invoked, {@code ToolExecutor}
66   * can safety be used in a background thread for launching the compilation (but must still be used by only
67   * only thread at a time).
68   *
69   * @author Martin Desruisseaux
70   */
71  public class ToolExecutor {
72      /**
73       * The locale for diagnostics, or {@code null} for the platform default.
74       */
75      private static final Locale LOCALE = null;
76  
77      /**
78       * The character encoding of source files, or {@code null} for the platform default encoding.
79       *
80       * @see AbstractCompilerMojo#encoding
81       */
82      protected final Charset encoding;
83  
84      /**
85       * The root directories of the Java source files to compile, excluding empty directories.
86       * The list needs to be modifiable for allowing the addition of generated source directories.
87       *
88       * @see AbstractCompilerMojo#compileSourceRoots
89       */
90      final List<SourceDirectory> sourceDirectories;
91  
92      /**
93       * The directories where to write generated source files.
94       * This set is either empty or a singleton.
95       *
96       * @see AbstractCompilerMojo#proc
97       * @see StandardLocation#SOURCE_OUTPUT
98       */
99      protected final Set<Path> generatedSourceDirectories;
100 
101     /**
102      * All source files to compile. May include files for many Java modules and many Java releases.
103      * When the compilation will be executed, those files will be grouped in compilation units where
104      * each unit will be the source files for one particular Java release.
105      *
106      * @see StandardLocation#SOURCE_PATH
107      * @see StandardLocation#MODULE_SOURCE_PATH
108      */
109     private List<SourceFile> sourceFiles;
110 
111     /**
112      * Whether the project contains or is assumed to contain a {@code module-info.java} file.
113      * If the user specified explicitly whether the project is a modular or a classpath JAR,
114      * then this flag is set to the user's specification without verification.
115      * Otherwise, this flag is determined by scanning the list of source files.
116      */
117     protected final boolean hasModuleDeclaration;
118 
119     /**
120      * The result of resolving the dependencies, or {@code null} if not available or not needed.
121      * For example, this field may be null if the constructor found no file to compile,
122      * so there is no need to fetch dependencies.
123      */
124     final DependencyResolverResult dependencyResolution;
125 
126     /**
127      * All dependencies grouped by the path types where to place them, together with the modules to patch.
128      * The path type can be the class-path, module-path, annotation processor path, patched path, <i>etc.</i>
129      * Some path types include a module name.
130      *
131      * <h4>Modifications during the build of multi-release project</h4>
132      * When building a multi-release project, values associated to {@code --class-path}, {@code --module-path}
133      * or {@code --patch-module} options are modified every time that {@code ToolExecutor} compiles for a new
134      * Java release. The output directories for the previous Java releases are inserted as the first elements
135      * of their lists, or new entries are created if no list existed previously for an option.
136      *
137      * @see #dependencies(PathType)
138      * @see #prependDependency(PathType, Path)
139      */
140     protected final Map<PathType, List<Path>> dependencies;
141 
142     /**
143      * The destination directory (or class output directory) for class files.
144      * This directory will be given to the {@code -d} Java compiler option
145      * when compiling the classes for the base Java release.
146      *
147      * @see AbstractCompilerMojo#getOutputDirectory()
148      */
149     protected final Path outputDirectory;
150 
151     /**
152      * Configuration of the incremental compilation.
153      *
154      * @see AbstractCompilerMojo#incrementalCompilation
155      * @see AbstractCompilerMojo#useIncrementalCompilation
156      */
157     private final EnumSet<IncrementalBuild.Aspect> incrementalBuildConfig;
158 
159     /**
160      * The incremental build to save if the build succeed.
161      * In case of failure, the cached information will be unchanged.
162      */
163     private IncrementalBuild incrementalBuild;
164 
165     /**
166      * Whether only a subset of the files will be compiled. This flag can be {@code true} only when
167      * incremental build is enabled and detected that some files do not need to be recompiled.
168      */
169     private boolean isPartialBuild;
170 
171     /**
172      * Where to send the compilation warning (never {@code null}). If a null value was specified
173      * to the constructor, then this listener sends the warnings to the Maven {@linkplain #logger}.
174      */
175     protected final DiagnosticListener<? super JavaFileObject> listener;
176 
177     /**
178      * The Maven logger for reporting information or warnings to the user.
179      * Used for messages emitted directly by the Maven compiler plugin.
180      * Not necessarily used for messages emitted by the Java compiler.
181      *
182      * <h4>Thread safety</h4>
183      * This logger should be thread-safe if this {@code ToolExecutor} is executed in a background thread.
184      *
185      * @see AbstractCompilerMojo#logger
186      */
187     protected final Log logger;
188 
189     /**
190      * The sources to write in the {@code target/javac.args} debug files.
191      * This list contains only the sources for which the compiler has been executed, successfully or not.
192      * If a compilation error occurred, the last element in the list contains the sources where the error occurred.
193      */
194     final List<SourcesForRelease> sourcesForDebugFile;
195 
196     /**
197      * Creates a new task by taking a snapshot of the current configuration of the given <abbr>MOJO</abbr>.
198      * This constructor creates the {@linkplain #outputDirectory output directory} if it does not already exist.
199      *
200      * @param mojo the <abbr>MOJO</abbr> from which to take a snapshot
201      * @param listener where to send compilation warnings, or {@code null} for the Maven logger
202      * @throws MojoException if this constructor identifies an invalid parameter in the <abbr>MOJO</abbr>
203      * @throws IOException if an error occurred while creating the output directory or scanning the source directories
204      * @throws MavenException if an error occurred while fetching dependencies
205      *
206      * @see AbstractCompilerMojo#createExecutor(DiagnosticListener)
207      */
208     @SuppressWarnings("deprecation")
209     protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener<? super JavaFileObject> listener)
210             throws IOException {
211 
212         logger = mojo.logger;
213         if (listener == null) {
214             Path root = mojo.project.getRootDirectory();
215             listener = new DiagnosticLogger(logger, mojo.messageBuilderFactory, LOCALE, root);
216         }
217         this.listener = listener;
218         encoding = mojo.charset();
219         incrementalBuildConfig = mojo.incrementalCompilationConfiguration();
220         outputDirectory = Files.createDirectories(mojo.getOutputDirectory());
221         sourceDirectories = mojo.getSourceDirectories(outputDirectory);
222         dependencies = new LinkedHashMap<>();
223         sourcesForDebugFile = new ArrayList<>();
224         /*
225          * Get the source files and whether they include or are assumed to include `module-info.java`.
226          * Note that we perform this step after processing compiler arguments, because this block may
227          * skip the build if there is no source code to compile. We want arguments to be verified first
228          * in order to warn about possible configuration problems.
229          */
230         if (incrementalBuildConfig.contains(IncrementalBuild.Aspect.MODULES)) {
231             boolean hasNoFileMatchers = mojo.hasNoFileMatchers();
232             for (SourceDirectory root : sourceDirectories) {
233                 if (root.moduleName == null) {
234                     throw new CompilationFailureException("The <incrementalCompilation> value can be \"modules\" "
235                             + "only if all source directories are Java modules.");
236                 }
237                 hasNoFileMatchers &= root.includes.isEmpty() && root.excludes.isEmpty();
238             }
239             if (!hasNoFileMatchers) {
240                 throw new CompilationFailureException("Include and exclude filters cannot be specified "
241                         + "when <incrementalCompilation> is set to \"modules\".");
242             }
243             hasModuleDeclaration = true;
244             sourceFiles = List.of();
245         } else {
246             /*
247              * The order of the two next lines matter for initialization of `SourceDirectory.moduleInfo`.
248              * This initialization is done indirectly when the walk invokes the `SourceFile` constructor,
249              * which in turn invokes `SourceDirectory.visit(Path)`.
250              */
251             sourceFiles = new PathFilter(mojo).walkSourceFiles(sourceDirectories);
252             hasModuleDeclaration = mojo.hasModuleDeclaration(sourceDirectories);
253             if (sourceFiles.isEmpty()) {
254                 generatedSourceDirectories = Set.of();
255                 dependencyResolution = null;
256                 return;
257             }
258         }
259         generatedSourceDirectories = mojo.addGeneratedSourceDirectory();
260         /*
261          * Get the dependencies. If the module-path contains any automatic (filename-based)
262          * dependency and the MOJO is compiling the main code, then a warning will be logged.
263          */
264         dependencyResolution = mojo.resolveDependencies(hasModuleDeclaration);
265         if (dependencyResolution != null) {
266             dependencies.putAll(dependencyResolution.getDispatchedPaths());
267             copyDependencyValues();
268         }
269         mojo.resolveProcessorPathEntries(dependencies);
270     }
271 
272     /**
273      * Copies all values of the dependency map in unmodifiable lists.
274      * This is used for creating a snapshot of the current state of the dependency map.
275      */
276     private void copyDependencyValues() {
277         dependencies.entrySet().forEach((entry) -> entry.setValue(List.copyOf(entry.getValue())));
278     }
279 
280     /**
281      * {@return whether a release version is specified for all sources}
282      */
283     final boolean isReleaseSpecifiedForAll() {
284         for (SourceDirectory source : sourceDirectories) {
285             if (source.release == null) {
286                 return false;
287             }
288         }
289         return true;
290     }
291 
292     /**
293      * Filters the source files to recompile, or cleans the output directory if everything should be rebuilt.
294      * If the directory structure of the source files has changed since the last build,
295      * or if a compiler option changed, or if a dependency changed,
296      * then this method keeps all source files and cleans the {@linkplain #outputDirectory output directory}.
297      * Otherwise, the source files that did not changed since the last build are removed from the list of sources
298      * to compile. If all source files have been removed, then this method returns {@code false} for notifying the
299      * caller that it can skip the build.
300      *
301      * <p>If this method is invoked many times, all invocations after this first one have no effect.</p>
302      *
303      * @param mojo the <abbr>MOJO</abbr> from which to take the incremental build configuration
304      * @param configuration the options which should match the options used during the last build
305      * @throws IOException if an error occurred while accessing the cache file or walking through the directory tree
306      * @return whether there is at least one file to recompile
307      */
308     public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Options configuration)
309             throws IOException {
310         final boolean checkSources = incrementalBuildConfig.contains(IncrementalBuild.Aspect.SOURCES);
311         final boolean checkClasses = incrementalBuildConfig.contains(IncrementalBuild.Aspect.CLASSES);
312         final boolean checkDepends = incrementalBuildConfig.contains(IncrementalBuild.Aspect.DEPENDENCIES);
313         final boolean checkOptions = incrementalBuildConfig.contains(IncrementalBuild.Aspect.OPTIONS);
314         if (checkSources | checkClasses | checkDepends | checkOptions) {
315             incrementalBuild =
316                     new IncrementalBuild(mojo, sourceFiles, checkSources, configuration, incrementalBuildConfig);
317             String causeOfRebuild = null;
318             if (checkSources) {
319                 // Should be first, because this method deletes output files of removed sources.
320                 causeOfRebuild = incrementalBuild.inputFileTreeChanges();
321             }
322             if (checkClasses && causeOfRebuild == null) {
323                 causeOfRebuild = incrementalBuild.markNewOrModifiedSources();
324             }
325             if (checkDepends && causeOfRebuild == null) {
326                 List<String> fileExtensions = mojo.fileExtensions;
327                 causeOfRebuild = incrementalBuild.dependencyChanges(dependencies.values(), fileExtensions);
328             }
329             if (checkOptions && causeOfRebuild == null) {
330                 causeOfRebuild = incrementalBuild.optionChanges();
331             }
332             if (causeOfRebuild != null) {
333                 if (!sourceFiles.isEmpty()) { // Avoid misleading message such as "all sources changed".
334                     logger.info(causeOfRebuild);
335                 }
336             } else {
337                 isPartialBuild = true;
338                 sourceFiles = incrementalBuild.getModifiedSources();
339                 if (IncrementalBuild.isEmptyOrIgnorable(sourceFiles)) {
340                     incrementalBuildConfig.clear(); // Prevent this method to be executed twice.
341                     logger.info("Nothing to compile - all classes are up to date.");
342                     sourceFiles = List.of();
343                     return false;
344                 } else {
345                     int n = sourceFiles.size();
346                     var sb = new StringBuilder("Compiling ").append(n).append(" modified source file");
347                     if (n > 1) {
348                         sb.append('s'); // Make plural.
349                     }
350                     logger.info(sb.append('.'));
351                 }
352             }
353             if (!(checkSources | checkDepends | checkOptions)) {
354                 incrementalBuild.deleteCache();
355                 incrementalBuild = null;
356             }
357         }
358         incrementalBuildConfig.clear(); // Prevent this method to be executed twice.
359         return true;
360     }
361 
362     /**
363      * {@return a modifiable list of paths to all dependencies of the given type}
364      * The returned list is intentionally live: elements can be added or removed
365      * from the list for changing the state of this executor.
366      *
367      * @param  pathType  type of path for which to get the dependencies
368      */
369     protected List<Path> dependencies(PathType pathType) {
370         return dependencies.compute(pathType, (key, paths) -> {
371             if (paths == null) {
372                 return new ArrayList<>();
373             } else if (paths instanceof ArrayList<?>) {
374                 return paths;
375             } else {
376                 var copy = new ArrayList<Path>(paths.size() + 4); // Anticipate the addition of new elements.
377                 copy.addAll(paths);
378                 return copy;
379             }
380         });
381     }
382 
383     /**
384      * Dispatches sources and dependencies on the kind of paths determined by {@code DependencyResolver}.
385      * The targets may be class-path, module-path, annotation processor class-path/module-path, <i>etc</i>.
386      *
387      * @param fileManager the file manager where to set the dependency paths
388      */
389     private void setDependencyPaths(final StandardJavaFileManager fileManager) throws IOException {
390         final var unresolvedPaths = new ArrayList<Path>();
391         for (Map.Entry<PathType, List<Path>> entry : dependencies.entrySet()) {
392             List<Path> paths = entry.getValue();
393             PathType key = entry.getKey();
394             if (key instanceof JavaPathType type) {
395                 /*
396                  * Dependency to a JAR file (usually).
397                  * Placed on: --class-path, --module-path.
398                  */
399                 Optional<JavaFileManager.Location> location = type.location();
400                 if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException.
401                     var value = location.get();
402                     if (value == StandardLocation.CLASS_PATH) {
403                         if (isPartialBuild && !hasModuleDeclaration) {
404                             /*
405                              * From https://docs.oracle.com/en/java/javase/24/docs/specs/man/javac.html:
406                              * "When compiling code for one or more modules, the class output directory will
407                              * automatically be checked when searching for previously compiled classes.
408                              * When not compiling for modules, for backwards compatibility, the directory is not
409                              * automatically checked for previously compiled classes, and so it is recommended to
410                              * specify the class output directory as one of the locations on the user class path,
411                              * using the --class-path option or one of its alternate forms."
412                              */
413                             paths = new ArrayList<>(paths);
414                             paths.add(outputDirectory);
415                             entry.setValue(paths);
416                         }
417                     }
418                     fileManager.setLocationFromPaths(value, paths);
419                     continue;
420                 }
421             } else if (key instanceof JavaPathType.Modular type) {
422                 /*
423                  * Main code to be tested by the test classes. This is handled as a "dependency".
424                  * Placed on: --patch-module-path.
425                  */
426                 Optional<JavaFileManager.Location> location = type.rawType().location();
427                 if (location.isPresent()) {
428                     fileManager.setLocationForModule(location.get(), type.moduleName(), paths);
429                     continue;
430                 }
431             }
432             unresolvedPaths.addAll(paths);
433         }
434         if (!unresolvedPaths.isEmpty()) {
435             var sb = new StringBuilder("Cannot determine where to place the following artifacts:");
436             for (Path p : unresolvedPaths) {
437                 sb.append(System.lineSeparator()).append(" - ").append(p);
438             }
439             logger.warn(sb);
440         }
441     }
442 
443     /**
444      * Inserts the given path as the first element of the list of paths of the given type.
445      * The main purpose of this method is during the build of a multi-release project,
446      * for adding the output directory of the code targeting the previous Java release
447      * before to compile the code targeting the next Java release. In this context,
448      * the {@code type} argument usually identifies a {@code --class-path},
449      * {@code --module-path} or {@code --patch-module} option.
450      *
451      * @param  pathType type of path for which to add an element
452      * @param  first the path to put first
453      * @return the new paths for the given type, as a modifiable list
454      */
455     protected List<Path> prependDependency(final PathType pathType, final Path first) {
456         List<Path> paths = dependencies(pathType);
457         paths.add(0, first);
458         return paths;
459     }
460 
461     /**
462      * Ensures that the given value is non-null, replacing null values by the latest version.
463      */
464     private static SourceVersion nonNullOrLatest(SourceVersion release) {
465         return (release != null) ? release : SourceVersion.latest();
466     }
467 
468     /**
469      * If the given module name is empty, tries to infer a default module name. A module name is inferred
470      * (tentatively) when the <abbr>POM</abbr> file does not contain an explicit {@code <module>} element.
471      * This method exists only for compatibility with the Maven 3 way to do a modular project.
472      *
473      * @param moduleName the module name, or an empty string if not explicitly specified
474      * @return the specified module name, or an inferred module name if available, or an empty string
475      * @throws IOException if the module descriptor cannot be read.
476      */
477     String inferModuleNameIfMissing(String moduleName) throws IOException {
478         return moduleName;
479     }
480 
481     /**
482      * Groups all sources files first by Java release versions, then by module names.
483      * The elements are sorted in the order of {@link SourceVersion} enumeration values,
484      * with null version sorted last on the assumption that they will be for the latest
485      * version supported by the runtime environment.
486      *
487      * @return the given sources grouped by Java release versions and module names
488      */
489     private Collection<SourcesForRelease> groupByReleaseAndModule() {
490         var result = new EnumMap<SourceVersion, SourcesForRelease>(SourceVersion.class);
491         for (SourceDirectory directory : sourceDirectories) {
492             /*
493              * We need an entry for every versions even if there is no source to compile for a version.
494              * This is needed for configuring the classpath in a consistent way, for example with the
495              * output directory of previous version even if we skipped the compilation of that version.
496              */
497             SourcesForRelease unit = result.computeIfAbsent(
498                     nonNullOrLatest(directory.release),
499                     (release) -> new SourcesForRelease(directory.release)); // Intentionally ignore the key.
500             String moduleName = directory.moduleName;
501             if (moduleName == null || moduleName.isBlank()) {
502                 moduleName = "";
503             }
504             unit.roots.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<Path>());
505         }
506         for (SourceFile source : sourceFiles) {
507             result.get(nonNullOrLatest(source.directory.release)).add(source);
508         }
509         return result.values();
510     }
511 
512     /**
513      * Creates the file manager which will be used by the compiler.
514      * This method does not configure the locations (sources, dependencies, <i>etc.</i>).
515      * Locations will be set by {@link #compile(JavaCompiler, Options, Writer)} on the
516      * file manager returned by this method.
517      *
518      * @param compiler the compiler
519      * @param workaround whether to apply {@link WorkaroundForPatchModule}
520      * @return the file manager to use
521      */
522     private StandardJavaFileManager createFileManager(JavaCompiler compiler, boolean workaround) {
523         StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding);
524         if (WorkaroundForPatchModule.ENABLED && workaround && !(compiler instanceof ForkedTool)) {
525             fileManager = new WorkaroundForPatchModule(fileManager);
526         }
527         return fileManager;
528     }
529 
530     /**
531      * Runs the compilation task.
532      *
533      * @param compiler the compiler
534      * @param configuration the options to give to the Java compiler
535      * @param otherOutput where to write additional output from the compiler
536      * @return whether the compilation succeeded
537      * @throws IOException if an error occurred while reading or writing a file
538      * @throws MojoException if the compilation failed for a reason identified by this method
539      * @throws RuntimeException if any other kind of  error occurred
540      */
541     @SuppressWarnings("checkstyle:MethodLength")
542     public boolean compile(final JavaCompiler compiler, final Options configuration, final Writer otherOutput)
543             throws IOException {
544         /*
545          * Announce what the compiler is about to do.
546          */
547         sourcesForDebugFile.clear();
548         if (sourceFiles.isEmpty()) {
549             String message = "No sources to compile.";
550             try {
551                 Files.delete(outputDirectory);
552             } catch (DirectoryNotEmptyException e) {
553                 message += " However, the output directory is not empty.";
554             }
555             logger.info(message);
556             return true;
557         }
558         if (logger.isDebugEnabled()) {
559             int n = sourceFiles.size();
560             @SuppressWarnings("checkstyle:MagicNumber")
561             var sb = new StringBuilder(n * 40).append("The source files to compile are:");
562             for (SourceFile file : sourceFiles) {
563                 sb.append(System.lineSeparator()).append("    ").append(file);
564             }
565             logger.debug(sb);
566         }
567         /*
568          * Create a `JavaFileManager`, configure all paths (dependencies and sources), then run the compiler.
569          * The Java file manager has a cache, so it needs to be disposed after the compilation is completed.
570          * The same `JavaFileManager` may be reused for many compilation units (e.g. multi-release) before
571          * disposal in order to reuse its cache.
572          */
573         boolean success = true;
574         try (StandardJavaFileManager fileManager = createFileManager(compiler, hasModuleDeclaration)) {
575             setDependencyPaths(fileManager);
576             if (!generatedSourceDirectories.isEmpty()) {
577                 fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, generatedSourceDirectories);
578             }
579             boolean isVersioned = false;
580             Path latestOutputDirectory = null;
581             /*
582              * More than one compilation unit may exist in the case of a multi-release project.
583              * Units are compiled in the order of the release version, with base compiled first.
584              * At the beginning of each new iteration, `latestOutputDirectory` is the path to
585              * the compiled classes of the previous version.
586              */
587             compile:
588             for (final SourcesForRelease unit : groupByReleaseAndModule()) {
589                 Path outputForRelease = null;
590                 boolean isClasspathProject = false;
591                 boolean isModularProject = false;
592                 String defaultModuleName = null;
593                 configuration.setRelease(unit.getReleaseString());
594                 for (final Map.Entry<String, Set<Path>> root : unit.roots.entrySet()) {
595                     final String declaredModuleName = root.getKey();
596                     final String moduleName = inferModuleNameIfMissing(declaredModuleName);
597                     if (moduleName.isEmpty()) {
598                         isClasspathProject = true;
599                     } else {
600                         isModularProject = true;
601                         if (declaredModuleName.isEmpty()) { // Modular project using package source hierarchy.
602                             defaultModuleName = moduleName;
603                         }
604                     }
605                     if (isClasspathProject & isModularProject) {
606                         throw new CompilationFailureException("Mix of modular and non-modular sources.");
607                     }
608                     final Set<Path> sourcePaths = root.getValue();
609                     if (isClasspathProject) {
610                         fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourcePaths);
611                     } else {
612                         fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName, sourcePaths);
613                     }
614                     outputForRelease = outputDirectory; // Modified below if compiling a non-base release.
615                     if (isVersioned) {
616                         outputForRelease = Files.createDirectories(
617                                 SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release));
618                         if (isClasspathProject) {
619                             /*
620                              * For a non-modular project, this block is executed at most once par compilation unit.
621                              * Add the paths to the classes compiled for previous versions.
622                              */
623                             List<Path> classpath = prependDependency(JavaPathType.CLASSES, latestOutputDirectory);
624                             fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
625                         } else {
626                             /*
627                              * For a modular project, this block can be executed an arbitrary number of times
628                              * (once per module).
629                              */
630                             Path latestOutputForModule = latestOutputDirectory.resolve(moduleName);
631                             JavaPathType.Modular pathType = JavaPathType.patchModule(moduleName);
632                             List<Path> paths = prependDependency(pathType, latestOutputForModule);
633                             fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName, paths);
634                         }
635                     }
636                 }
637                 /*
638                  * At this point, we finished to set the source paths. We have also modified the class-path or
639                  * patched the modules with the output directories of codes compiled for lower Java releases.
640                  * The `defaultModuleName` is an adjustment done when the project is a Java module, but still
641                  * organized in a package source hierarchy instead of a module source hierarchy. Updating the
642                  * `unit.roots` map is not needed for this class, but done in case a `target/javac.args` file
643                  * will be written after the compilation.
644                  */
645                 if (defaultModuleName != null) {
646                     Set<Path> paths = unit.roots.remove("");
647                     if (paths != null) {
648                         unit.roots.put(defaultModuleName, paths);
649                     }
650                 }
651                 copyDependencyValues();
652                 unit.dependencySnapshot = new LinkedHashMap<>(dependencies);
653                 fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputForRelease));
654                 latestOutputDirectory = outputForRelease;
655                 unit.outputForRelease = outputForRelease;
656                 /*
657                  * Compile the source files now. The following loop should be executed exactly once.
658                  * It may be executed twice when compiling test classes overwriting the `module-info`,
659                  * in which case the `module-info` needs to be compiled separately from other classes.
660                  * However, this is a deprecated practice.
661                  */
662                 JavaCompiler.CompilationTask task;
663                 for (CompilationTaskSources c : toCompilationTasks(unit)) {
664                     Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjectsFromPaths(c.files);
665                     StandardJavaFileManager workaround = fileManager;
666                     boolean workaroundNeedsClose = false;
667                     // Check flag separately to clearly indicate this entire block is a workaround hack.
668                     if (WorkaroundForPatchModule.ENABLED) {
669                         if (workaround instanceof WorkaroundForPatchModule wp) {
670                             workaround = wp.getFileManagerIfUsable();
671                             if (workaround == null) {
672                                 workaround = createFileManager(compiler, false);
673                                 wp.copyTo(workaround);
674                                 workaroundNeedsClose = true;
675                             }
676                         }
677                     }
678                     task = compiler.getTask(otherOutput, workaround, listener, configuration.options, null, sources);
679                     success = c.compile(task);
680                     if (workaroundNeedsClose) {
681                         workaround.close();
682                     }
683                     sourcesForDebugFile.add(unit);
684                     if (!success) {
685                         break compile;
686                     }
687                 }
688                 isVersioned = true; // Any further iteration is for a version after the base version.
689             }
690             /*
691              * Post-compilation.
692              */
693             if (listener instanceof DiagnosticLogger diagnostic) {
694                 diagnostic.logSummary();
695             }
696         } catch (UncheckedIOException e) {
697             throw e.getCause();
698         }
699         if (success && incrementalBuild != null) {
700             incrementalBuild.writeCache();
701             incrementalBuild = null;
702         }
703         return success;
704     }
705 
706     /**
707      * Subdivides a compilation unit into one or more compilation tasks.
708      * This is a workaround for deprecated practices such as overwriting the main {@code module-info} in the tests.
709      * In the latter case, we need to compile the test {@code module-info} separately, before the other test classes.
710      */
711     CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) {
712         if (unit.files.isEmpty()) {
713             return new CompilationTaskSources[0];
714         }
715         return new CompilationTaskSources[] {new CompilationTaskSources(unit.files)};
716     }
717 }