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 }