1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 public class ToolExecutor {
72
73
74
75 private static final Locale LOCALE = null;
76
77
78
79
80
81
82 protected final Charset encoding;
83
84
85
86
87
88
89
90 final List<SourceDirectory> sourceDirectories;
91
92
93
94
95
96
97
98
99 protected final Set<Path> generatedSourceDirectories;
100
101
102
103
104
105
106
107
108
109 private List<SourceFile> sourceFiles;
110
111
112
113
114
115
116
117 protected final boolean hasModuleDeclaration;
118
119
120
121
122
123
124 final DependencyResolverResult dependencyResolution;
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 protected final Map<PathType, List<Path>> dependencies;
141
142
143
144
145
146
147
148
149 protected final Path outputDirectory;
150
151
152
153
154
155
156
157 private final EnumSet<IncrementalBuild.Aspect> incrementalBuildConfig;
158
159
160
161
162
163 private IncrementalBuild incrementalBuild;
164
165
166
167
168
169 private boolean isPartialBuild;
170
171
172
173
174
175 protected final DiagnosticListener<? super JavaFileObject> listener;
176
177
178
179
180
181
182
183
184
185
186
187 protected final Log logger;
188
189
190
191
192
193
194 final List<SourcesForRelease> sourcesForDebugFile;
195
196
197
198
199
200
201
202
203
204
205
206
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
226
227
228
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
248
249
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
262
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
274
275
276 private void copyDependencyValues() {
277 dependencies.entrySet().forEach((entry) -> entry.setValue(List.copyOf(entry.getValue())));
278 }
279
280
281
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
294
295
296
297
298
299
300
301
302
303
304
305
306
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
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()) {
334 logger.info(causeOfRebuild);
335 }
336 } else {
337 isPartialBuild = true;
338 sourceFiles = incrementalBuild.getModifiedSources();
339 if (IncrementalBuild.isEmptyOrIgnorable(sourceFiles)) {
340 incrementalBuildConfig.clear();
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');
349 }
350 logger.info(sb.append('.'));
351 }
352 }
353 if (!(checkSources | checkDepends | checkOptions)) {
354 incrementalBuild.deleteCache();
355 incrementalBuild = null;
356 }
357 }
358 incrementalBuildConfig.clear();
359 return true;
360 }
361
362
363
364
365
366
367
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);
377 copy.addAll(paths);
378 return copy;
379 }
380 });
381 }
382
383
384
385
386
387
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
397
398
399 Optional<JavaFileManager.Location> location = type.location();
400 if (location.isPresent()) {
401 var value = location.get();
402 if (value == StandardLocation.CLASS_PATH) {
403 if (isPartialBuild && !hasModuleDeclaration) {
404
405
406
407
408
409
410
411
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
424
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
445
446
447
448
449
450
451
452
453
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
463
464 private static SourceVersion nonNullOrLatest(SourceVersion release) {
465 return (release != null) ? release : SourceVersion.latest();
466 }
467
468
469
470
471
472
473
474
475
476
477 String inferModuleNameIfMissing(String moduleName) throws IOException {
478 return moduleName;
479 }
480
481
482
483
484
485
486
487
488
489 private Collection<SourcesForRelease> groupByReleaseAndModule() {
490 var result = new EnumMap<SourceVersion, SourcesForRelease>(SourceVersion.class);
491 for (SourceDirectory directory : sourceDirectories) {
492
493
494
495
496
497 SourcesForRelease unit = result.computeIfAbsent(
498 nonNullOrLatest(directory.release),
499 (release) -> new SourcesForRelease(directory.release));
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
514
515
516
517
518
519
520
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
532
533
534
535
536
537
538
539
540
541 @SuppressWarnings("checkstyle:MethodLength")
542 public boolean compile(final JavaCompiler compiler, final Options configuration, final Writer otherOutput)
543 throws IOException {
544
545
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
569
570
571
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
583
584
585
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()) {
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;
615 if (isVersioned) {
616 outputForRelease = Files.createDirectories(
617 SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release));
618 if (isClasspathProject) {
619
620
621
622
623 List<Path> classpath = prependDependency(JavaPathType.CLASSES, latestOutputDirectory);
624 fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
625 } else {
626
627
628
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
639
640
641
642
643
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
658
659
660
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
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;
689 }
690
691
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
708
709
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 }