diff --git a/pom.xml b/pom.xml index 82767116..25c7d92c 100644 --- a/pom.xml +++ b/pom.xml @@ -80,16 +80,6 @@ under the License. org.apache.maven.plugins.compiler.its - - - - - com.thoughtworks.qdox - qdox - 2.0.3 - - - org.apache.maven.plugin-tools diff --git a/src/it/MCOMPILER-522-unresolvable-dependency/verify.groovy b/src/it/MCOMPILER-522-unresolvable-dependency/verify.groovy index b2198cd4..fcb0d735 100644 --- a/src/it/MCOMPILER-522-unresolvable-dependency/verify.groovy +++ b/src/it/MCOMPILER-522-unresolvable-dependency/verify.groovy @@ -24,4 +24,4 @@ def buildLog = logFile.getText('UTF-8') assert buildLog.contains( "Caused by: org.apache.maven.plugin.MojoExecutionException: " + "Resolution of annotationProcessorPath dependencies failed: " ) assert buildLog.contains( - "Could not find artifact org.apache.maven.plugins.compiler.it:annotation-processor-non-existing:jar:1.0-SNAPSHOT" ) + "The POM for org.apache.maven.plugins.compiler.it:annotation-processor-non-existing:jar:1.0-SNAPSHOT is missing, no dependency information available" ) diff --git a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java index 89754ee5..69baca66 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -27,10 +27,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; @@ -40,7 +41,9 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.maven.RepositoryUtils; import org.apache.maven.artifact.handler.ArtifactHandler; @@ -57,11 +60,7 @@ import org.apache.maven.project.MavenProject; import org.apache.maven.shared.incremental.IncrementalBuildHelper; import org.apache.maven.shared.incremental.IncrementalBuildHelperRequest; -import org.apache.maven.shared.utils.ReaderFactory; import org.apache.maven.shared.utils.StringUtils; -import org.apache.maven.shared.utils.io.DirectoryScanResult; -import org.apache.maven.shared.utils.io.DirectoryScanner; -import org.apache.maven.shared.utils.io.FileUtils; import org.apache.maven.shared.utils.logging.MessageBuilder; import org.apache.maven.shared.utils.logging.MessageUtils; import org.apache.maven.toolchain.Toolchain; @@ -558,12 +557,11 @@ public abstract class AbstractCompilerMojo extends AbstractMojo { /** * File extensions to check timestamp for incremental build. - * Default contains only class and jar. * * @since 3.1 */ - @Parameter - private List fileExtensions; + @Parameter(defaultValue = "class,jar") + private Set fileExtensions; /** *

to enable/disable incremental compilation feature.

@@ -876,37 +874,33 @@ public void execute() throws MojoExecutionException, CompilationFailureException incrementalBuildHelperRequest = new IncrementalBuildHelperRequest().inputFiles(sources); - DirectoryScanResult dsr = computeInputFileTreeChanges(incrementalBuildHelper, sources); - - boolean immutableOutputFile = compiler.getCompilerOutputStyle() - .equals(CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES) - && !canUpdateTarget; - boolean dependencyChanged = isDependencyChanged(); - boolean sourceChanged = isSourceChanged(compilerConfiguration, compiler); - boolean inputFileTreeChanged = hasInputFileTreeChanged(dsr); - // CHECKSTYLE_OFF: LineLength - if (immutableOutputFile || dependencyChanged || sourceChanged || inputFileTreeChanged) - // CHECKSTYLE_ON: LineLength - { - String cause = immutableOutputFile - ? "immutable single output file" - : (dependencyChanged - ? "changed dependency" - : (sourceChanged ? "changed source code" : "added or removed source files")); - getLog().info("Recompiling the module because of " + cause + "."); - if (showCompilationChanges) { - for (String fileAdded : dsr.getFilesAdded()) { - getLog().info("\t+ " + fileAdded); - } - for (String fileRemoved : dsr.getFilesRemoved()) { - getLog().info("\t- " + fileRemoved); - } - } - + // Strategies used to detect modifications. + Supplier immutableOutputFile = () -> (compiler.getCompilerOutputStyle() + .equals(CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES) + && !canUpdateTarget) + ? "immutable single output file" + : null; + Supplier dependencyChanged = () -> isDependencyChanged() ? "changed dependency" : null; + Supplier sourceChanged = + () -> isSourceChanged(compilerConfiguration, compiler) ? "changed source code" : null; + Supplier inputFileTreeChanged = + () -> hasInputFileTreeChanged(computeInputFileTreeChanges(incrementalBuildHelper, sources)) + ? "added or removed source files" + : null; + + // Lazy evaluation of the incremental compilation detection. + String cause = Stream.of(immutableOutputFile, dependencyChanged, sourceChanged, inputFileTreeChanged) + .map(Supplier::get) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (cause != null) { + getLog().info("Recompiling the module because of " + + MessageUtils.buffer().strong(cause) + "."); compilerConfiguration.setSourceFiles(sources); } else { getLog().info("Nothing to compile - all classes are up to date."); - return; } } catch (CompilerException e) { @@ -937,8 +931,7 @@ public void execute() throws MojoExecutionException, CompilationFailureException } if (staleSources.isEmpty()) { - getLog().info("Nothing to compile - all classes are up to date"); - + getLog().info("Nothing to compile - all classes are up to date."); return; } @@ -1145,7 +1138,8 @@ public void execute() throws MojoExecutionException, CompilationFailureException // ---------------------------------------------------------------------- if (StringUtils.isEmpty(compilerConfiguration.getSourceEncoding())) { - getLog().warn("File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING + getLog().warn("File encoding has not been set, using platform encoding " + + MessageUtils.buffer().strong(Charset.defaultCharset()) + ", i.e. build is platform dependent!"); } @@ -1372,20 +1366,21 @@ private Set getCompileSources(Compiler compiler, CompilerConfiguration com /** * @param compilerConfiguration * @param compiler - * @return true if at least a single source file is newer than it's class file + * @return {@code true} if at least a single source file is newer than it's class file */ - private boolean isSourceChanged(CompilerConfiguration compilerConfiguration, Compiler compiler) - throws CompilerException, MojoExecutionException { - Set staleSources = - computeStaleSources(compilerConfiguration, compiler, getSourceInclusionScanner(staleMillis)); + private boolean isSourceChanged(CompilerConfiguration compilerConfiguration, Compiler compiler) { + Set staleSources = Collections.emptySet(); + try { + staleSources = computeStaleSources(compilerConfiguration, compiler, getSourceInclusionScanner(staleMillis)); + } catch (MojoExecutionException | CompilerException ex) { + // we cannot detect Stale Sources, so don't do anything beside logging + getLog().warn("Cannot detect stale sources."); + return false; + } if (getLog().isDebugEnabled() || showCompilationChanges) { for (File f : staleSources) { - if (showCompilationChanges) { - getLog().info("Stale source detected: " + f.getAbsolutePath()); - } else { - getLog().debug("Stale source detected: " + f.getAbsolutePath()); - } + getLog().info("\tStale source detected: " + f.getAbsolutePath()); } } return !staleSources.isEmpty(); @@ -1400,10 +1395,11 @@ protected int getRequestThreadCount() { return session.getRequest().getDegreeOfConcurrency(); } - protected Date getBuildStartTime() { - MavenExecutionRequest request = session.getRequest(); - Date buildStartTime = request == null ? new Date() : request.getStartTime(); - return buildStartTime == null ? new Date() : buildStartTime; + private Optional getBuildStartTime() { + return Optional.ofNullable(session.getRequest()) + .map(MavenExecutionRequest::getStartTime) + .map(java.util.Date::toInstant) + .map(i -> i.truncatedTo(ChronoUnit.MILLIS)); } private String getMemoryValue(String setting) { @@ -1539,36 +1535,41 @@ private static List removeEmptyCompileSourceRoots(List compileSo * generated classes and if we got a file which is >= the build-started timestamp, then we caught a file which * got changed during this build. * - * @return true if at least one single dependency has changed. + * @return {@code true} if at least one single dependency has changed. */ - protected boolean isDependencyChanged() { - if (session == null) { + private boolean isDependencyChanged() { + final Instant buildStartTime = getBuildStartTime().orElse(null); + if (buildStartTime == null) { // we just cannot determine it, so don't do anything beside logging - getLog().info("Cannot determine build start date, skipping incremental build detection."); + getLog().debug("Cannot determine build start time, skipping incremental build detection."); return false; } if (fileExtensions == null || fileExtensions.isEmpty()) { - fileExtensions = Collections.unmodifiableList(Arrays.asList("class", "jar")); + fileExtensions = new HashSet<>(Arrays.asList("class", "jar")); } - Date buildStartTime = getBuildStartTime(); - List pathElements = new ArrayList<>(); pathElements.addAll(getClasspathElements()); pathElements.addAll(getModulepathElements()); for (String pathElement : pathElements) { - File artifactPath = new File(pathElement); - if (artifactPath.isDirectory() || artifactPath.isFile()) { - if (!artifactPath.equals(getOutputDirectory()) && hasNewFile(artifactPath, buildStartTime)) { - if (showCompilationChanges) { - getLog().info("New dependency detected: " + artifactPath.getAbsolutePath()); - } else { - getLog().debug("New dependency detected: " + artifactPath.getAbsolutePath()); + Path artifactPath = Paths.get(pathElement); + + // Search files only on dependencies (other modules), not on the current project, + if (Files.isDirectory(artifactPath) + && !artifactPath.equals(getOutputDirectory().toPath())) { + try (Stream walk = Files.walk(artifactPath)) { + if (walk.anyMatch(p -> hasNewFile(p, buildStartTime))) { + return true; } - return true; + } catch (IOException ex) { + // we just cannot determine it, so don't do anything beside logging + getLog().warn("I/O error walking the path: " + ex.getMessage()); + return false; } + } else if (hasNewFile(artifactPath, buildStartTime)) { + return true; } } @@ -1577,31 +1578,35 @@ protected boolean isDependencyChanged() { } /** - * @param classPathEntry entry to check + * @param file entry to check * @param buildStartTime time build start * @return if any changes occurred */ - private boolean hasNewFile(File classPathEntry, Date buildStartTime) { - if (!classPathEntry.exists()) { - return false; - } - - if (classPathEntry.isFile()) { - return classPathEntry.lastModified() >= buildStartTime.getTime() - && fileExtensions.contains(FileUtils.getExtension(classPathEntry.getName())); - } - - File[] children = classPathEntry.listFiles(); - - for (File child : children) { - if (hasNewFile(child, buildStartTime)) { - return true; + private boolean hasNewFile(Path file, Instant buildStartTime) { + if (Files.isRegularFile(file) && fileExtensions.contains(getFileExtension(file))) { + try { + Instant lastModifiedTime = + Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS); + boolean isChanged = lastModifiedTime.isAfter(buildStartTime); + if (isChanged && (getLog().isDebugEnabled() || showCompilationChanges)) { + getLog().info("\tNew dependency detected: " + file.toAbsolutePath()); + } + return isChanged; + } catch (IOException ex) { + // we just cannot determine it, so don't do anything beside logging + getLog().warn("I/O error reading the lastModifiedTime: " + ex.getMessage()); } } return false; } + private static String getFileExtension(Path path) { + String fileName = path.getFileName().toString(); + int dotIndex = fileName.lastIndexOf('.'); + return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1); + } + private List resolveProcessorPathEntries() throws MojoExecutionException { if (annotationProcessorPaths == null || annotationProcessorPaths.isEmpty()) { return null; @@ -1753,36 +1758,92 @@ private String getMavenCompilerPluginVersion() { return pomProperties.getProperty("version"); } - private DirectoryScanResult computeInputFileTreeChanges(IncrementalBuildHelper ibh, Set inputFiles) - throws MojoExecutionException { - File mojoConfigBase = ibh.getMojoStatusDirectory(); - File mojoConfigFile = new File(mojoConfigBase, INPUT_FILES_LST_FILENAME); - - String[] oldInputFiles = new String[0]; + private FilesDelta computeInputFileTreeChanges(IncrementalBuildHelper ibh, Set inputFiles) { + Path mojoConfigBase = null; + try { + mojoConfigBase = ibh.getMojoStatusDirectory().toPath(); + } catch (MojoExecutionException e) { + // we cannot get the mojo status dir, so don't do anything beside logging + getLog().warn("Error reading mojo status directory."); + return FilesDelta.EMPTY; + } + Path mojoConfigFile = mojoConfigBase.resolve(INPUT_FILES_LST_FILENAME); - if (mojoConfigFile.exists()) { + List oldInputFiles = Collections.emptyList(); + if (Files.isRegularFile(mojoConfigFile)) { try { - oldInputFiles = FileUtils.fileReadArray(mojoConfigFile); + oldInputFiles = Files.readAllLines(mojoConfigFile); } catch (IOException e) { - throw new MojoExecutionException("Error reading old mojo status " + mojoConfigFile, e); + // we cannot read the mojo config file, so don't do anything beside logging + getLog().warn("Error while reading old mojo status: " + mojoConfigFile); + return FilesDelta.EMPTY; } } - String[] inputFileNames = inputFiles.stream().map(File::getAbsolutePath).toArray(String[]::new); - - DirectoryScanResult dsr = DirectoryScanner.diffFiles(oldInputFiles, inputFileNames); + List newInputFiles = + inputFiles.stream().sorted().map(File::getAbsolutePath).collect(Collectors.toList()); try { - FileUtils.fileWriteArray(mojoConfigFile, inputFileNames); + Files.write(mojoConfigFile, newInputFiles); } catch (IOException e) { - throw new MojoExecutionException("Error while storing the mojo status", e); + // we cannot write the mojo config file, so don't do anything beside logging + getLog().warn("Error while writing new mojo status: " + mojoConfigFile); + return FilesDelta.EMPTY; } - return dsr; + FilesDelta inputTreeChanges = new FilesDelta(oldInputFiles, newInputFiles); + if (getLog().isDebugEnabled() || showCompilationChanges) { + for (String fileAdded : inputTreeChanges.getFilesAdded()) { + getLog().info("\tInput tree files (+): " + fileAdded); + } + for (String fileRemoved : inputTreeChanges.getFilesRemoved()) { + getLog().info("\tInput tree files (-): " + fileRemoved); + } + } + + return inputTreeChanges; } - private boolean hasInputFileTreeChanged(DirectoryScanResult dsr) { - return dsr.getFilesAdded().length > 0 || dsr.getFilesRemoved().length > 0; + private boolean hasInputFileTreeChanged(FilesDelta filesDelta) { + return !filesDelta.getFilesAdded().isEmpty() + || !filesDelta.getFilesRemoved().isEmpty(); + } + + private static class FilesDelta { + private final List oldList; + private final List newList; + private List fileAdded; + private List fileRemoved; + + static final FilesDelta EMPTY = new FilesDelta(); + + FilesDelta() { + this.oldList = Collections.emptyList(); + this.newList = Collections.emptyList(); + this.fileAdded = Collections.emptyList(); + this.fileRemoved = Collections.emptyList(); + } + + FilesDelta(List oldList, List newList) { + this.oldList = oldList; + this.newList = newList; + } + + List getFilesAdded() { + if (fileAdded == null) { + fileAdded = new ArrayList<>(newList); + fileAdded.removeAll(oldList); + } + return fileAdded; + } + + List getFilesRemoved() { + if (fileRemoved == null) { + fileRemoved = new ArrayList<>(oldList); + fileRemoved.removeAll(newList); + } + return fileRemoved; + } } public void setTarget(String target) {