From 45958f3e9c1094a80e00df93b51eea6942765605 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Tue, 28 Jul 2026 18:41:12 +0200 Subject: [PATCH 1/3] [MCOMPILER-578] Add regression test for repeated compiler executions Run clean compile followed by compile and verify that Java 8 classes and the Java 11 module descriptor retain their configured class-file versions. The test exposes the incremental compilation failure before the production fix. --- src/it/MCOMPILER-578/invoker.properties | 20 +++++ src/it/MCOMPILER-578/pom.xml | 78 +++++++++++++++++++ .../src/main/java/module-info.java | 21 +++++ .../src/main/java/org/example/Example.java | 21 +++++ .../main/java/org/example/package-info.java | 19 +++++ src/it/MCOMPILER-578/verify.groovy | 30 +++++++ 6 files changed, 189 insertions(+) create mode 100644 src/it/MCOMPILER-578/invoker.properties create mode 100644 src/it/MCOMPILER-578/pom.xml create mode 100644 src/it/MCOMPILER-578/src/main/java/module-info.java create mode 100644 src/it/MCOMPILER-578/src/main/java/org/example/Example.java create mode 100644 src/it/MCOMPILER-578/src/main/java/org/example/package-info.java create mode 100644 src/it/MCOMPILER-578/verify.groovy 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..485fa660f --- /dev/null +++ b/src/it/MCOMPILER-578/verify.groovy @@ -0,0 +1,30 @@ +/* + * 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 exampleClass = new File( basedir, 'target/classes/org/example/Example.class' ) +assert exampleClass.isFile() +def exampleMajorVersion = exampleClass.bytes[7] & 0xFF +// major_version: 52 = Java 8, from the base-compile execution. +assert exampleMajorVersion == 52 + +def moduleInfoClass = new File( basedir, 'target/classes/module-info.class' ) +assert moduleInfoClass.isFile() +def moduleInfoMajorVersion = moduleInfoClass.bytes[7] & 0xFF +// major_version: 55 = Java 11, from the base-modules-compile execution. +assert moduleInfoMajorVersion == 55 From 193fbe63e294b220386f32d63975e66166bf0fc0 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Tue, 28 Jul 2026 19:44:07 +0200 Subject: [PATCH 2/3] [MCOMPILER-578] Track outputs across compiler executions Record the last compiler execution that processed each mapped output in the shared plugin context. Recompile when a different execution overlaps that output while allowing repeated invocations of the same execution to remain up to date. Fixes #788 --- .../plugin/compiler/AbstractCompilerMojo.java | 67 ++++++++- .../compiler/CompilationOutputRegistry.java | 141 ++++++++++++++++++ .../CompilationOutputRegistryTest.java | 84 +++++++++++ 3 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java create mode 100644 src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java 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..c264b210e 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,54 @@ 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() && (getLog().isDebugEnabled() || showCompilationChanges)) { + getLog().info("\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..97375e716 --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java @@ -0,0 +1,141 @@ +/* + * 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.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<>(); + for (String sourceRoot : sourceRoots) { + Path root = Paths.get(sourceRoot).toAbsolutePath().normalize(); + for (File source : sources) { + Path path = source.toPath().toAbsolutePath().normalize(); + if (path.startsWith(root)) { + for (File output : mapping.getTargetFiles( + outputDirectory, root.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 context = (Map) pluginContext; + value = context.putIfAbsent(KEY, outputs); + if (value == null) { + return outputs; + } + } + return (Map) value; + } +} diff --git a/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java b/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java new file mode 100644 index 000000000..45c575dd9 --- /dev/null +++ b/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java @@ -0,0 +1,84 @@ +/* + * 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.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class CompilationOutputRegistryTest { + private static final String FIRST_EXECUTION = "compile@first"; + + private static final String SECOND_EXECUTION = "compile@second"; + + private static final Path OUTPUT = Paths.get("target/classes/Example.class"); + + private static final Path OTHER_OUTPUT = Paths.get("target/classes/Other.class"); + + private final Map pluginContext = new HashMap<>(); + + private final Set outputs = Collections.singleton(OUTPUT); + + @Test + void shouldIgnoreOutputsFromTheSameExecution() { + CompilationOutputRegistry.register(pluginContext, FIRST_EXECUTION, outputs); + + assertFalse(CompilationOutputRegistry.find(pluginContext, FIRST_EXECUTION, outputs) + .isPresent()); + } + + @Test + void shouldFindOutputsFromAnotherExecution() { + CompilationOutputRegistry.register(pluginContext, FIRST_EXECUTION, outputs); + + assertEquals( + OUTPUT, + CompilationOutputRegistry.find(pluginContext, SECOND_EXECUTION, outputs) + .orElse(null)); + } + + @Test + void shouldIgnoreDisjointOutputs() { + CompilationOutputRegistry.register(pluginContext, FIRST_EXECUTION, outputs); + + assertFalse(CompilationOutputRegistry.find(pluginContext, SECOND_EXECUTION, Collections.singleton(OTHER_OUTPUT)) + .isPresent()); + } + + @Test + void shouldTrackTheLastExecution() { + CompilationOutputRegistry.register(pluginContext, FIRST_EXECUTION, outputs); + CompilationOutputRegistry.register(pluginContext, SECOND_EXECUTION, outputs); + + assertEquals( + OUTPUT, + CompilationOutputRegistry.find(pluginContext, FIRST_EXECUTION, outputs) + .orElse(null)); + assertFalse(CompilationOutputRegistry.find(pluginContext, SECOND_EXECUTION, outputs) + .isPresent()); + } +} From 192de3ff9590baf9b6d51e5f49e7534a2255866f Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Tue, 28 Jul 2026 23:14:37 +0200 Subject: [PATCH 3/3] [MCOMPILER-578] Address review comments --- src/it/MCOMPILER-578/verify.groovy | 13 +++++++--- .../plugin/compiler/AbstractCompilerMojo.java | 4 ++- .../compiler/CompilationOutputRegistry.java | 25 +++++++++++++------ .../CompilationOutputRegistryTest.java | 25 +++++++++++++++++-- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/it/MCOMPILER-578/verify.groovy b/src/it/MCOMPILER-578/verify.groovy index 485fa660f..279dd9ccb 100644 --- a/src/it/MCOMPILER-578/verify.groovy +++ b/src/it/MCOMPILER-578/verify.groovy @@ -17,14 +17,19 @@ * 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' ) -assert exampleClass.isFile() -def exampleMajorVersion = exampleClass.bytes[7] & 0xFF +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' ) -assert moduleInfoClass.isFile() -def moduleInfoMajorVersion = moduleInfoClass.bytes[7] & 0xFF +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 c264b210e..15c04ca7d 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1618,8 +1618,10 @@ private Set getOutputPaths(CompilerConfiguration compilerConfiguration, Co */ private boolean hasPreviouslyCompiledOutput(String compilerExecution, Set outputs) { Optional output = CompilationOutputRegistry.find(getPluginContext(), compilerExecution, outputs); - if (output.isPresent() && (getLog().isDebugEnabled() || showCompilationChanges)) { + 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(); } diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java b/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java index 97375e716..110ad9f5c 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilationOutputRegistry.java @@ -21,6 +21,7 @@ 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; @@ -64,15 +65,23 @@ 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) { - Path root = Paths.get(sourceRoot).toAbsolutePath().normalize(); - for (File source : sources) { - Path path = source.toPath().toAbsolutePath().normalize(); - if (path.startsWith(root)) { - for (File output : mapping.getTargetFiles( - outputDirectory, root.relativize(path).toString())) { - outputs.add(output.toPath().toAbsolutePath().normalize()); - } + 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()); } } } diff --git a/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java b/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java index 45c575dd9..a76ab2d28 100644 --- a/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java +++ b/src/test/java/org/apache/maven/plugin/compiler/CompilationOutputRegistryTest.java @@ -20,11 +20,14 @@ import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import org.codehaus.plexus.compiler.util.scan.InclusionScanException; +import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -35,14 +38,32 @@ class CompilationOutputRegistryTest { private static final String SECOND_EXECUTION = "compile@second"; - private static final Path OUTPUT = Paths.get("target/classes/Example.class"); + private static final Path OUTPUT = + Paths.get("target/classes/Example.class").toAbsolutePath().normalize(); - private static final Path OTHER_OUTPUT = Paths.get("target/classes/Other.class"); + private static final Path OTHER_OUTPUT = + Paths.get("target/classes/Other.class").toAbsolutePath().normalize(); private final Map pluginContext = new HashMap<>(); private final Set outputs = Collections.singleton(OUTPUT); + @Test + void shouldMapSourceAgainstMostSpecificRoot() throws InclusionScanException { + Path sourceRoot = Paths.get("src").toAbsolutePath().normalize(); + Path nestedSourceRoot = sourceRoot.resolve("main/java"); + Path outputDirectory = Paths.get("target/classes").toAbsolutePath().normalize(); + Path source = nestedSourceRoot.resolve("org/example/Example.java"); + + assertEquals( + Collections.singleton(outputDirectory.resolve("org/example/Example.class")), + CompilationOutputRegistry.mapOutputs( + new SuffixMapping(".java", ".class"), + outputDirectory.toFile(), + Arrays.asList(sourceRoot.toString(), nestedSourceRoot.toString()), + Collections.singleton(source.toFile()))); + } + @Test void shouldIgnoreOutputsFromTheSameExecution() { CompilationOutputRegistry.register(pluginContext, FIRST_EXECUTION, outputs);