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 }