diff --git a/src/it/MCOMPILER-578/invoker.properties b/src/it/MCOMPILER-578/invoker.properties
new file mode 100644
index 000000000..b5bbe7de9
--- /dev/null
+++ b/src/it/MCOMPILER-578/invoker.properties
@@ -0,0 +1,20 @@
+# 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.
+
+invoker.java.version = 17+
+invoker.goals = clean compile
+invoker.goals.2 = compile
diff --git a/src/it/MCOMPILER-578/pom.xml b/src/it/MCOMPILER-578/pom.xml
new file mode 100644
index 000000000..b3db4fc5d
--- /dev/null
+++ b/src/it/MCOMPILER-578/pom.xml
@@ -0,0 +1,78 @@
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.compiler.it
+ mcompiler-578
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ @project.version@
+
+ false
+
+
+
+ default-compile
+
+ ${java.vm.specification.version}
+
+
+
+ base-modules-compile
+
+ compile
+
+
+ 11
+
+ module-info.java
+
+
+
+
+ base-compile
+
+ compile
+
+
+ 8
+
+ module-info.java
+
+
+
+
+
+
+
+
diff --git a/src/it/MCOMPILER-578/src/main/java/module-info.java b/src/it/MCOMPILER-578/src/main/java/module-info.java
new file mode 100644
index 000000000..1e1d899b6
--- /dev/null
+++ b/src/it/MCOMPILER-578/src/main/java/module-info.java
@@ -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.
+ */
+module org.example {
+ exports org.example;
+}
diff --git a/src/it/MCOMPILER-578/src/main/java/org/example/Example.java b/src/it/MCOMPILER-578/src/main/java/org/example/Example.java
new file mode 100644
index 000000000..c520a4fe5
--- /dev/null
+++ b/src/it/MCOMPILER-578/src/main/java/org/example/Example.java
@@ -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.
+ */
+package org.example;
+
+public class Example {}
diff --git a/src/it/MCOMPILER-578/src/main/java/org/example/package-info.java b/src/it/MCOMPILER-578/src/main/java/org/example/package-info.java
new file mode 100644
index 000000000..9517db210
--- /dev/null
+++ b/src/it/MCOMPILER-578/src/main/java/org/example/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+package org.example;
diff --git a/src/it/MCOMPILER-578/verify.groovy b/src/it/MCOMPILER-578/verify.groovy
new file mode 100644
index 000000000..279dd9ccb
--- /dev/null
+++ b/src/it/MCOMPILER-578/verify.groovy
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+def majorVersion = { File classFile ->
+ assert classFile.isFile()
+ byte[] bytes = classFile.bytes
+ assert bytes.length >= 8
+ ((bytes[6] & 0xFF) << 8) | (bytes[7] & 0xFF)
+}
+
+def exampleClass = new File( basedir, 'target/classes/org/example/Example.class' )
+def exampleMajorVersion = majorVersion( exampleClass )
+// major_version: 52 = Java 8, from the base-compile execution.
+assert exampleMajorVersion == 52
+
+def moduleInfoClass = new File( basedir, 'target/classes/module-info.class' )
+def moduleInfoMajorVersion = majorVersion( moduleInfoClass )
+// major_version: 55 = Java 11, from the base-modules-compile execution.
+assert moduleInfoMajorVersion == 55
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 edb9ad7ed..15c04ca7d 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
@@ -930,6 +930,9 @@ private void executeReal() throws MojoExecutionException, CompilationFailureExce
final Set sources;
+ Set outputs = Collections.emptySet();
+ String compilerExecution = mojoExecution.getGoal() + '@' + mojoExecution.getExecutionId();
+
IncrementalBuildHelperRequest incrementalBuildHelperRequest = null;
if (useIncrementalCompilation) {
@@ -941,6 +944,9 @@ private void executeReal() throws MojoExecutionException, CompilationFailureExce
preparePaths(sources);
+ // Expected output paths let us detect overwrites that IncrementalBuildHelper cannot see.
+ outputs = getOutputPaths(compilerConfiguration, compiler, sources);
+
incrementalBuildHelperRequest = new IncrementalBuildHelperRequest().inputFiles(sources);
// Strategies used to detect modifications.
@@ -954,9 +960,18 @@ private void executeReal() throws MojoExecutionException, CompilationFailureExce
String inputFileTreeChanged = hasInputFileTreeChanged(incrementalBuildHelper, sources)
? "added or removed source files"
: null;
+ // A different execution may have overwritten an otherwise up-to-date output.
+ String outputChanged = hasPreviouslyCompiledOutput(compilerExecution, outputs)
+ ? "output from another compiler execution"
+ : null;
// Get the first cause for the rebuild compilation detection.
- String cause = Stream.of(immutableOutputFile, dependencyChanged, sourceChanged, inputFileTreeChanged)
+ String cause = Stream.of(
+ immutableOutputFile,
+ dependencyChanged,
+ sourceChanged,
+ inputFileTreeChanged,
+ outputChanged)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
@@ -1267,6 +1282,8 @@ private void executeReal() throws MojoExecutionException, CompilationFailureExce
getLog().debug(
"skip incrementalBuildHelper#afterRebuildExecution as the output directory doesn't exist");
}
+ // Make these outputs visible to later compiler executions in this Maven session.
+ recordCompiledOutputs(compilerExecution, outputs);
}
List warnings = new ArrayList<>();
@@ -1569,6 +1586,56 @@ private Set computeStaleSources(
return staleSources;
}
+ /**
+ * Maps selected sources to their expected compiler outputs.
+ *
+ * @param compilerConfiguration the compiler configuration
+ * @param compiler the selected compiler
+ * @param sources sources selected by the current execution
+ * @return normalized absolute output paths
+ */
+ private Set getOutputPaths(CompilerConfiguration compilerConfiguration, Compiler compiler, Set sources)
+ throws CompilerException, MojoExecutionException {
+ SourceMapping mapping = getSourceMapping(compilerConfiguration, compiler);
+
+ File outputDirectory =
+ compiler.getCompilerOutputStyle() == CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES
+ ? buildDirectory
+ : getOutputDirectory();
+ try {
+ return CompilationOutputRegistry.mapOutputs(mapping, outputDirectory, getCompileSourceRoots(), sources);
+ } catch (InclusionScanException e) {
+ throw new MojoExecutionException("Error mapping sources to their outputs.", e);
+ }
+ }
+
+ /**
+ * Checks whether a different compiler execution last processed an expected output.
+ *
+ * @param compilerExecution the current execution
+ * @param outputs expected outputs of the current execution
+ * @return whether an output overlaps with another execution
+ */
+ private boolean hasPreviouslyCompiledOutput(String compilerExecution, Set outputs) {
+ Optional output = CompilationOutputRegistry.find(getPluginContext(), compilerExecution, outputs);
+ if (output.isPresent() && showCompilationChanges) {
+ getLog().info("\tOutput from another compiler execution: " + output.get());
+ } else if (output.isPresent()) {
+ getLog().debug("\tOutput from another compiler execution: " + output.get());
+ }
+ return output.isPresent();
+ }
+
+ /**
+ * Registers the current execution as the last processor of its expected outputs.
+ *
+ * @param compilerExecution the current execution
+ * @param outputs expected outputs of the current execution
+ */
+ private void recordCompiledOutputs(String compilerExecution, Set outputs) {
+ CompilationOutputRegistry.register(getPluginContext(), compilerExecution, outputs);
+ }
+
private SourceMapping getSourceMapping(CompilerConfiguration compilerConfiguration, Compiler compiler)
throws CompilerException, MojoExecutionException {
CompilerOutputStyle outputStyle = compiler.getCompilerOutputStyle();
diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java b/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java
new file mode 100644
index 000000000..110ad9f5c
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugin.compiler;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
+import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping;
+
+/**
+ * Tracks output files processed by compiler executions for the current project and Maven session.
+ *
+ *
Multiple compiler executions may share an output directory while using different compiler options.
+ * {@link org.apache.maven.shared.incremental.IncrementalBuildHelper} compares output file names before and after
+ * compilation, so it cannot detect an existing output overwritten by another execution. This registry lets a later
+ * execution detect that overlap without relying on file timestamps, whose precision varies between file systems.
+ *
+ *
The registry is stored in Maven's plugin context, which is shared by all executions of this plugin for one
+ * project and discarded after the Maven session.
+ */
+final class CompilationOutputRegistry {
+ private static final String KEY = CompilationOutputRegistry.class.getName() + ".compiledOutputs";
+
+ /**
+ * Prevents instantiation.
+ */
+ private CompilationOutputRegistry() {}
+
+ /**
+ * Maps sources to normalized absolute output paths.
+ *
+ * @param mapping mapping from source paths to output paths
+ * @param outputDirectory compiler output directory
+ * @param sourceRoots configured source roots
+ * @param sources sources selected by the compiler execution
+ * @return expected output paths
+ * @throws InclusionScanException if a source cannot be mapped
+ */
+ static Set mapOutputs(
+ SourceMapping mapping, File outputDirectory, List sourceRoots, Set sources)
+ throws InclusionScanException {
+ Set outputs = new HashSet<>();
+ List roots = new ArrayList<>(sourceRoots.size());
+ for (String sourceRoot : sourceRoots) {
+ roots.add(Paths.get(sourceRoot).toAbsolutePath().normalize());
+ }
+ for (File source : sources) {
+ Path path = source.toPath().toAbsolutePath().normalize();
+ Path matchingRoot = null;
+ for (Path root : roots) {
+ if (path.startsWith(root)
+ && (matchingRoot == null || root.getNameCount() > matchingRoot.getNameCount())) {
+ matchingRoot = root;
+ }
+ }
+ if (matchingRoot != null) {
+ for (File output : mapping.getTargetFiles(
+ outputDirectory, matchingRoot.relativize(path).toString())) {
+ outputs.add(output.toPath().toAbsolutePath().normalize());
+ }
+ }
+ }
+ return outputs;
+ }
+
+ /**
+ * Finds an output most recently processed by a different compiler execution.
+ *
+ * @param pluginContext Maven's context for this plugin and project
+ * @param compilerExecution the current compiler execution, formatted as {@code goal@executionId}
+ * @param outputs outputs mapped from the current execution's sources
+ * @return an overlapping output, or an empty value if there is none
+ */
+ static Optional find(Map, ?> pluginContext, String compilerExecution, Set outputs) {
+ Map compiledOutputs = get(pluginContext, false);
+ return compiledOutputs != null
+ ? outputs.stream()
+ .filter(output -> {
+ String previousExecution = compiledOutputs.get(output);
+ return previousExecution != null && !previousExecution.equals(compilerExecution);
+ })
+ .findFirst()
+ : Optional.empty();
+ }
+
+ /**
+ * Associates outputs with the compiler execution that most recently processed them.
+ *
+ * @param pluginContext Maven's context for this plugin and project
+ * @param compilerExecution the compiler execution, formatted as {@code goal@executionId}
+ * @param outputs outputs mapped from the execution's sources
+ */
+ static void register(Map, ?> pluginContext, String compilerExecution, Set outputs) {
+ Map compiledOutputs = get(pluginContext, true);
+ if (compiledOutputs != null) {
+ for (Path output : outputs) {
+ compiledOutputs.put(output, compilerExecution);
+ }
+ }
+ }
+
+ /**
+ * Returns the output registry from the plugin context.
+ *
+ * @param pluginContext Maven's context for this plugin and project
+ * @param create whether to create the registry when absent
+ * @return the registry, or {@code null} if the context or registry is absent
+ */
+ @SuppressWarnings("unchecked")
+ private static Map get(Map, ?> pluginContext, boolean create) {
+ if (pluginContext == null) {
+ return null;
+ }
+ Object value = pluginContext.get(KEY);
+ if (value == null && create) {
+ Map outputs = new ConcurrentHashMap<>();
+ Map