diff --git a/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy b/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy index baf07f7b..3bc4b84f 100644 --- a/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy +++ b/src/it/MCOMPILER-275_separate-moduleinfo/verify.groovy @@ -20,7 +20,7 @@ def log = new File( basedir, 'build.log').text assert log.count( "[INFO] Toolchain in maven-compiler-plugin: JDK" ) == 1 -assert log.count( "[INFO] Changes detected - recompiling the module!" ) == 3 +assert log.count( "[INFO] Changes detected - recompiling the module" ) == 3 // major_version: 52 = java8 -> execution id "base-compile" assert new File( basedir, 'target/classes/com/foo/MyClass.class' ).bytes[7] == 52 diff --git a/src/it/MCOMPILER-500-package-info-incr/verify.groovy b/src/it/MCOMPILER-500-package-info-incr/verify.groovy index ecba9c29..987364f8 100644 --- a/src/it/MCOMPILER-500-package-info-incr/verify.groovy +++ b/src/it/MCOMPILER-500-package-info-incr/verify.groovy @@ -21,7 +21,7 @@ def logFile = new File( basedir, 'build.log' ) assert logFile.exists() content = logFile.text -assert 1 == content.count( 'Changes detected - recompiling the module!' ) +assert 1 == content.count( 'Changes detected - recompiling the module' ) assert 1 == content.count( 'Nothing to compile - all classes are up to date' ) diff --git a/src/it/MCOMPILER-525/invoker.properties b/src/it/MCOMPILER-525/invoker.properties new file mode 100644 index 00000000..da0f0927 --- /dev/null +++ b/src/it/MCOMPILER-525/invoker.properties @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# NOTE: The first time, we run up to "integration-test" phase which includes the AntRun execution which saves the +# timestamp of the first JAR for comparison with the timestamp of the JAR from the final "package" invocation. +# Note: +invoker.goals = clean integration-test package -Dmaven.compiler.showCompilationChanges=true diff --git a/src/it/MCOMPILER-525/pom.xml b/src/it/MCOMPILER-525/pom.xml new file mode 100644 index 00000000..48de91e9 --- /dev/null +++ b/src/it/MCOMPILER-525/pom.xml @@ -0,0 +1,64 @@ + + + + 4.0.0 + org.apache.maven.plugins + MCOMPILER-525-no-recreation + MCOMPILER-525-no-recreation + jar + 1.0-SNAPSHOT + + + + + org.apache.maven.plugins + maven-compiler-plugin + @project.version@ + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + touch + integration-test + + run + + + + + + + + + + + + + diff --git a/src/it/MCOMPILER-525/src/main/java/myproject/HelloWorld.java b/src/it/MCOMPILER-525/src/main/java/myproject/HelloWorld.java new file mode 100644 index 00000000..05e16e02 --- /dev/null +++ b/src/it/MCOMPILER-525/src/main/java/myproject/HelloWorld.java @@ -0,0 +1,36 @@ +package myproject; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * The classic Hello World App. + */ +public class HelloWorld { + + /** + * Main method. + * + * @param args Not used + */ + public static void main( String[] args ) + { + System.out.println( "Hi!" ); + } +} \ No newline at end of file diff --git a/src/it/MCOMPILER-525/verify.groovy b/src/it/MCOMPILER-525/verify.groovy new file mode 100644 index 00000000..0e97225d --- /dev/null +++ b/src/it/MCOMPILER-525/verify.groovy @@ -0,0 +1,44 @@ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.nio.file.*; +import java.time.*; +import java.time.temporal.* +import java.util.*; + +File target = new File( basedir, "target" ); +assert target.isDirectory() + +File jarFile = new File( target, "MCOMPILER-525-no-recreation-1.0-SNAPSHOT.jar" ); +assert jarFile.isFile() + +File refFile = new File( target, "reference.jar" ); +assert refFile.isFile() + +Instant referenceTimestamp = Files.getLastModifiedTime( refFile.toPath() ) + .toInstant().truncatedTo( ChronoUnit.MILLIS ); +System.out.println( "Reference timestamp: " + referenceTimestamp ); + +Instant actualTimestamp = Files.getLastModifiedTime( jarFile.toPath() ) + .toInstant().truncatedTo( ChronoUnit.MILLIS ); +System.out.println( "Actual timestamp : " + actualTimestamp ); + +assert referenceTimestamp.equals(actualTimestamp), + "Timestamps don't match, JAR was recreated although contents has not changed" 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 03cd6af1..ad832c60 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -28,18 +28,24 @@ 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; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.apache.commons.io.FilenameUtils; import org.apache.maven.artifact.handler.ArtifactHandler; import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; import org.apache.maven.execution.MavenExecutionRequest; @@ -180,29 +186,33 @@ public abstract class AbstractCompilerMojo private boolean showWarnings; /** - *

The -source argument for the Java compiler.

+ * The {@code -source} argument for the Java compiler. * - *

NOTE:

- *

Since 3.8.0 the default value has changed from 1.5 to 1.6

- *

Since 3.9.0 the default value has changed from 1.6 to 1.7

- *

Since 3.11.0 the default value has changed from 1.7 to 1.8

+ *

NOTE:

+ * */ @Parameter( property = "maven.compiler.source", defaultValue = DEFAULT_SOURCE ) protected String source; /** - *

The -target argument for the Java compiler.

+ * The {@code -target} argument for the Java compiler. * - *

NOTE:

- *

Since 3.8.0 the default value has changed from 1.5 to 1.6

- *

Since 3.9.0 the default value has changed from 1.6 to 1.7

- *

Since 3.11.0 the default value has changed from 1.7 to 1.8

+ *

NOTE:

+ * */ @Parameter( property = "maven.compiler.target", defaultValue = DEFAULT_TARGET ) protected String target; /** - * The -release argument for the Java compiler, supported since Java9 + * The {@code -release} argument for the Java compiler, supported since Java 9. * * @since 3.6 */ @@ -210,7 +220,7 @@ public abstract class AbstractCompilerMojo protected String release; /** - * The -encoding argument for the Java compiler. + * The {@code -encoding} argument for the Java compiler. * * @since 2.1 */ @@ -414,7 +424,7 @@ public abstract class AbstractCompilerMojo /** * Keyword to be appended to the -implicit: command-line switch. * - * @since 3.10.2 + * @since 3.11.0 */ @Parameter( property = "maven.compiler.implicit" ) private String implicit; @@ -531,12 +541,11 @@ public abstract class AbstractCompilerMojo /** * 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.

@@ -886,30 +895,38 @@ else if ( CompilerConfiguration.CompilerReuseStrategy.ReuseSame.getStrategy().eq incrementalBuildHelperRequest = new IncrementalBuildHelperRequest().inputFiles( sources ); - DirectoryScanResult dsr = computeInputFileTreeChanges( incrementalBuildHelper, sources ); + Supplier dsr = + () -> computeInputFileTreeChanges( incrementalBuildHelper, sources ); + + Supplier idk = () -> ( compiler.getCompilerOutputStyle() + .equals( CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES ) && !canUpdateTarget ) + ? "idk" : null; + Supplier dependencyChanged = () -> isDependencyChanged() + ? "dependency" : null; + Supplier sourceChanged = () -> isSourceChanged( compilerConfiguration, compiler ) + ? "source" : null; + Supplier inputFileTreeChanged = () -> hasInputFileTreeChanged( dsr.get() ) + ? "input tree" : null; + + // Lazy evaluation of the incremental compilation detection. + String cause = Stream.of( idk, dependencyChanged, sourceChanged, inputFileTreeChanged ) + .map( Supplier::get ) + .filter( Objects::nonNull ) + .findFirst().orElse( null ); - boolean idk = 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 ( idk - || dependencyChanged - || sourceChanged - || inputFileTreeChanged ) + if ( cause != null ) // CHECKSTYLE_ON: LineLength { - String cause = idk ? "idk" : ( dependencyChanged ? "dependency" - : ( sourceChanged ? "source" : "input tree" ) ); - getLog().info( "Changes detected - recompiling the module! :" + cause ); + getLog().info( "Changes detected - recompiling the module due to: " + cause ); if ( showCompilationChanges ) { - for ( String fileAdded : dsr.getFilesAdded() ) + DirectoryScanResult inputTreeChanges = dsr.get(); + for ( String fileAdded : inputTreeChanges.getFilesAdded() ) { getLog().info( "\t+ " + fileAdded ); } - for ( String fileRemoved : dsr.getFilesRemoved() ) + for ( String fileRemoved : inputTreeChanges.getFilesRemoved() ) { getLog().info( "\t- " + fileRemoved ); } @@ -1274,7 +1291,8 @@ else if ( !values[0].equals( descriptor.name() ) ) if ( useIncrementalCompilation ) { - if ( incrementalBuildHelperRequest.getOutputDirectory().exists() ) + Path outputDirectory = incrementalBuildHelperRequest.getOutputDirectory().toPath(); + if ( Files.exists( outputDirectory ) ) { getLog().debug( "incrementalBuildHelper#afterRebuildExecution" ); // now scan the same directory again and create a diff @@ -1513,10 +1531,19 @@ private Set getCompileSources( Compiler compiler, CompilerConfiguration co * @return 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 ) ); + Set staleSources = Collections.emptySet(); + try + { + staleSources = computeStaleSources( compilerConfiguration, compiler, + getSourceInclusionScanner( staleMillis ) ); + } + catch ( MojoExecutionException | CompilerException ex ) + { + // we cannot get the mojo status dir, so don't do anything beside logging + getLog().warn( "Cannot detect stale sources." ); + return false; + } if ( getLog().isDebugEnabled() || showCompilationChanges ) { @@ -1546,11 +1573,12 @@ protected int getRequestThreadCount() return session.getRequest().getDegreeOfConcurrency(); } - protected Date getBuildStartTime() + private Optional getBuildStartTime() { - MavenExecutionRequest request = session.getRequest(); - Date buildStartTime = request == null ? new Date() : request.getStartTime(); - return buildStartTime == null ? new Date() : buildStartTime; + return Optional.ofNullable( session.getRequest() ) + .map( MavenExecutionRequest::getStartTime ) + .map( java.util.Date::toInstant ) + .map( i -> i.truncatedTo( ChronoUnit.MILLIS ) ); } @@ -1717,45 +1745,51 @@ private static List removeEmptyCompileSourceRoots( List compileS * 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() + private boolean isDependencyChanged() { - if ( session == null ) + 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().info( "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() ) + 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() ) ) { - if ( hasNewFile( artifactPath, buildStartTime ) ) + try ( Stream walk = Files.walk( artifactPath ) ) { - if ( showCompilationChanges ) - { - getLog().info( "New dependency detected: " + artifactPath.getAbsolutePath() ); - } - else + if ( walk.anyMatch( p -> hasNewFile( p, buildStartTime ) ) ) { - getLog().debug( "New dependency detected: " + artifactPath.getAbsolutePath() ); + 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; } } @@ -1767,27 +1801,35 @@ protected boolean isDependencyChanged() * @param classPathEntry entry to check * @param buildStartTime time build start * @return if any changes occurred + * @throws IOException */ - private boolean hasNewFile( File classPathEntry, Date buildStartTime ) + private boolean hasNewFile( Path classPathEntry, Instant 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 ( Files.isRegularFile( classPathEntry ) + && fileExtensions.contains( FilenameUtils.getExtension( classPathEntry.toString() ) ) ) { - if ( hasNewFile( child, buildStartTime ) ) + try { - return true; + Instant lastModifiedTime = Files.getLastModifiedTime( classPathEntry ).toInstant() + .truncatedTo( ChronoUnit.MILLIS ); + boolean isChanged = lastModifiedTime.isAfter( buildStartTime ); + if ( isChanged ) + { + if ( showCompilationChanges ) + { + getLog().info( "New dependency detected: " + classPathEntry.toAbsolutePath() ); + } + else + { + getLog().debug( "New dependency detected: " + classPathEntry.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() ); } } @@ -1902,9 +1944,18 @@ private String getMavenCompilerPluginVersion() } private DirectoryScanResult computeInputFileTreeChanges( IncrementalBuildHelper ibh, Set inputFiles ) - throws MojoExecutionException { - File mojoConfigBase = ibh.getMojoStatusDirectory(); + File mojoConfigBase = null; + try + { + mojoConfigBase = ibh.getMojoStatusDirectory(); + } + 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 new DirectoryScanResult( new String[0], new String[0] ); + } File mojoConfigFile = new File( mojoConfigBase, INPUT_FILES_LST_FILENAME ); String[] oldInputFiles = new String[0]; @@ -1917,7 +1968,9 @@ private DirectoryScanResult computeInputFileTreeChanges( IncrementalBuildHelper } 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 new DirectoryScanResult( new String[0], new String[0] ); } } @@ -1931,7 +1984,9 @@ private DirectoryScanResult computeInputFileTreeChanges( IncrementalBuildHelper } 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 storing old mojo status: " + mojoConfigFile ); + return new DirectoryScanResult( new String[0], new String[0] ); } return dsr;