From cb82437e15d91dd972dd4b82dc1c403cd1c25e20 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 15 Mar 2025 13:22:01 +0100 Subject: [PATCH 01/13] Remove the `multirelease-patterns/packaging-plugin` test because it was testing another plugin than this project. It was testing `pw.krejci:multi-release-jar-maven-plugin`, which fails with `NoSuchMethodError` with Maven 4-RC3. That external project may need to be updated for Maven 4. --- .../packaging-plugin/invoker.properties | 19 ----- .../packaging-plugin/pom.xml | 84 ------------------- .../src/main/java-mr/9/module-info.java | 22 ----- .../src/main/java-mr/9/mr/A.java | 34 -------- .../src/main/java/base/Base.java | 26 ------ .../packaging-plugin/src/main/java/mr/A.java | 32 ------- .../packaging-plugin/src/main/java/mr/I.java | 23 ----- .../src/test/java/mr/ATest.java | 48 ----------- .../packaging-plugin/verify.groovy | 73 ---------------- 9 files changed, 361 deletions(-) delete mode 100644 src/it/multirelease-patterns/packaging-plugin/invoker.properties delete mode 100644 src/it/multirelease-patterns/packaging-plugin/pom.xml delete mode 100644 src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java delete mode 100644 src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java delete mode 100644 src/it/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java delete mode 100644 src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java delete mode 100644 src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java delete mode 100644 src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java delete mode 100644 src/it/multirelease-patterns/packaging-plugin/verify.groovy diff --git a/src/it/multirelease-patterns/packaging-plugin/invoker.properties b/src/it/multirelease-patterns/packaging-plugin/invoker.properties deleted file mode 100644 index 1992d544..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/invoker.properties +++ /dev/null @@ -1,19 +0,0 @@ -# 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 = 9+ -invoker.goals = verify diff --git a/src/it/multirelease-patterns/packaging-plugin/pom.xml b/src/it/multirelease-patterns/packaging-plugin/pom.xml deleted file mode 100644 index 020c9e32..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/pom.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - 4.0.0 - multirelease - - multirelease - 1.0.0-SNAPSHOT - multi-release-jar - Packaging Plugin - - - - junit - junit - 4.13.1 - test - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - @version.maven-surefire@ - - - true - - - - - - - pw.krejci - multi-release-jar-maven-plugin - 0.1.5 - true - - 1.8 - 1.8 - - - - - org.apache.maven.plugins - maven-failsafe-plugin - @version.maven-surefire@ - - - **/*Test.java - - - - - - integration-test - verify - - - - - - - diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java b/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java deleted file mode 100644 index 36f00e07..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 example.mrjar { - exports base; - exports mr; -} diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java b/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java deleted file mode 100644 index 5083e1ce..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 mr; - -import java.util.Optional; - -import base.Base; - -public class A implements I { - public static String getString() { - return Base.get() + " -> " + Optional.of("9").get(); - } - - @Override - public Class introducedClass() { - return Module.class; - } -} diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java b/src/it/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java deleted file mode 100644 index 19ec7d8f..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 base; - -public class Base { - - public static String get() { - return "BASE"; - } -} diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java b/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java deleted file mode 100644 index 0e48eee6..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 mr; - -import base.Base; - -public class A implements I { - public static String getString() { - return Base.get() + " -> 8"; - } - - @Override - public Class introducedClass() { - return java.time.LocalDateTime.class; - } -} diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java b/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java deleted file mode 100644 index a0523266..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 mr; - -public interface I { - Class introducedClass(); -} diff --git a/src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java b/src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java deleted file mode 100644 index 510fb1c4..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 mr; - -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assume.assumeThat; - -public class ATest { - - private static final String javaVersion = System.getProperty("java.version"); - - @Test - public void testGet8() throws Exception { - assumeThat(javaVersion, is("8")); - - assertThat(A.getString(), is("BASE -> 8")); - - assertThat(new A().introducedClass().getName(), is("java.time.LocalDateTime")); - } - - @Test - public void testGet9() throws Exception { - assumeThat(javaVersion, is("9")); - - assertThat(A.getString(), is("BASE -> 9")); - - assertThat(new A().introducedClass().getName(), is("java.lang.Module")); - } -} diff --git a/src/it/multirelease-patterns/packaging-plugin/verify.groovy b/src/it/multirelease-patterns/packaging-plugin/verify.groovy deleted file mode 100644 index 6e97274b..00000000 --- a/src/it/multirelease-patterns/packaging-plugin/verify.groovy +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.util.jar.JarFile - -def mrjar = new JarFile(new File(basedir,'target/multirelease-1.0.0-SNAPSHOT.jar')) - -assert mrjar.manifest.mainAttributes.getValue('Multi-Release') == 'true' - -assert (je = mrjar.getEntry('base/Base.class')) != null -assert 52 == getMajor(mrjar.getInputStream(je)) -assert (je = mrjar.getEntry('mr/A.class')) != null -assert 52 == getMajor(mrjar.getInputStream(je)) -assert (je = mrjar.getEntry('mr/I.class')) != null -assert 52 == getMajor(mrjar.getInputStream(je)) - -assert (je = mrjar.getEntry('META-INF/versions/9/mr/A.class')) != null -assert 53 == getMajor(mrjar.getInputStream(je)) -assert (je = mrjar.getEntry('META-INF/versions/9/module-info.class')) != null -assert 53 == getMajor(mrjar.getInputStream(je)) - -/* - base - base/Base.class - mr - mr/A.class - mr/I.class - META-INF - META-INF/MANFEST.MF - META-INF/versions - META-INF/versions/9 - META-INF/versions/9/mr - META-INF/versions/9/mr/A.class - META-INF/versions/9/module-info.class - META-INF/maven - META-INF/maven/multirelease - META-INF/maven/multirelease/multirelease - META-INF/maven/multirelease/multirelease/pom.xml - META-INF/maven/multirelease/multirelease/pom.properties -*/ -assert mrjar.entries().size() == 17 - -int getMajor(InputStream is) -{ - def dis = new DataInputStream(is) - final String firstFourBytes = Integer.toHexString(dis.readUnsignedShort()) + Integer.toHexString(dis.readUnsignedShort()) - if (!firstFourBytes.equalsIgnoreCase("cafebabe")) - { - throw new IllegalArgumentException(dataSourceName + " is NOT a Java .class file.") - } - final int minorVersion = dis.readUnsignedShort() - final int majorVersion = dis.readUnsignedShort() - - is.close(); - return majorVersion; -} - From f97b68368d753be6125d3d18dfefc4c814e42a54 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Tue, 28 Jan 2025 12:55:47 +0100 Subject: [PATCH 02/13] Upgrade to Maven 4.0.0-rc-3. Update for changes in the `Project` interface. The fix leverages the new `SourceRoot` element. --- .github/workflows/maven-verify.yml | 2 +- pom.xml | 4 +- src/it/MCOMPILER-366/verify.groovy | 2 +- src/it/automodules-application/verify.groovy | 2 +- src/it/automodules-library/verify.groovy | 2 +- src/it/automodules-manifest/verify.groovy | 2 +- .../verify.groovy | 2 +- .../plugin/compiler/AbstractCompilerMojo.java | 82 +++++++++++-------- .../maven/plugin/compiler/CompilerMojo.java | 37 ++------- .../plugin/compiler/SourceDirectory.java | 31 ++++++- .../plugin/compiler/TestCompilerMojo.java | 40 +++------ .../plugin/compiler/CompilerMojoTestCase.java | 4 +- 12 files changed, 104 insertions(+), 106 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 9bffbd18..511df70f 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -28,4 +28,4 @@ jobs: with: jdk-distribution-matrix: '[ "temurin", "zulu", "microsoft", "adopt-openj9" ]' maven4-build: true - maven4-version: '4.0.0-rc-2' # the same as used in project + maven4-version: '4.0.0-rc-3' # the same as used in project diff --git a/pom.xml b/pom.xml index 78722190..b3d4533a 100644 --- a/pom.xml +++ b/pom.xml @@ -82,12 +82,12 @@ under the License. 17 - 4.0.0-rc-2 + 4.0.0-rc-3 9.7.1 6.0.0 5.17.0 - 4.0.0-beta-3 + 4.0.0-beta-4 2.15.0 0.9.0.M2 3.13.1 diff --git a/src/it/MCOMPILER-366/verify.groovy b/src/it/MCOMPILER-366/verify.groovy index a9afc615..00af22fa 100644 --- a/src/it/MCOMPILER-366/verify.groovy +++ b/src/it/MCOMPILER-366/verify.groovy @@ -19,6 +19,6 @@ buildLog = new File( basedir, 'build.log' ).text; -assert buildLog.contains("[WARNING] Filename-based automodules detected on the module-path:") +assert buildLog.contains("[WARNING] Filename-based automodules detected on the module path:") assert buildLog.contains(" - plexus-utils-3.0.24.jar") assert buildLog.contains(" - plexus-resources-1.1.0.jar") diff --git a/src/it/automodules-application/verify.groovy b/src/it/automodules-application/verify.groovy index 64aef812..f1782a2b 100644 --- a/src/it/automodules-application/verify.groovy +++ b/src/it/automodules-application/verify.groovy @@ -19,5 +19,5 @@ buildLog = new File( basedir, 'build.log' ).text; -assert buildLog.contains("[WARNING] Filename-based automodules detected on the module-path:") +assert buildLog.contains("[WARNING] Filename-based automodules detected on the module path:") assert buildLog.contains(" - plexus-utils-3.0.24.jar") diff --git a/src/it/automodules-library/verify.groovy b/src/it/automodules-library/verify.groovy index 64aef812..f1782a2b 100644 --- a/src/it/automodules-library/verify.groovy +++ b/src/it/automodules-library/verify.groovy @@ -19,5 +19,5 @@ buildLog = new File( basedir, 'build.log' ).text; -assert buildLog.contains("[WARNING] Filename-based automodules detected on the module-path:") +assert buildLog.contains("[WARNING] Filename-based automodules detected on the module path:") assert buildLog.contains(" - plexus-utils-3.0.24.jar") diff --git a/src/it/automodules-manifest/verify.groovy b/src/it/automodules-manifest/verify.groovy index 2c16519a..e8f92054 100644 --- a/src/it/automodules-manifest/verify.groovy +++ b/src/it/automodules-manifest/verify.groovy @@ -19,4 +19,4 @@ buildLog = new File( basedir, 'build.log' ).text; -assert !buildLog.contains("Filename-based automodules detected on the module-path") +assert !buildLog.contains("Filename-based automodules detected on the module path") diff --git a/src/it/automodules-transitive-module/verify.groovy b/src/it/automodules-transitive-module/verify.groovy index 64aef812..f1782a2b 100644 --- a/src/it/automodules-transitive-module/verify.groovy +++ b/src/it/automodules-transitive-module/verify.groovy @@ -19,5 +19,5 @@ buildLog = new File( basedir, 'build.log' ).text; -assert buildLog.contains("[WARNING] Filename-based automodules detected on the module-path:") +assert buildLog.contains("[WARNING] Filename-based automodules detected on the module path:") assert buildLog.contains(" - plexus-utils-3.0.24.jar") 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 a98f6e57..ba1d2dcd 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -54,13 +54,16 @@ import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; +import java.util.stream.Stream; import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.Language; import org.apache.maven.api.PathScope; import org.apache.maven.api.PathType; import org.apache.maven.api.Project; import org.apache.maven.api.ProjectScope; import org.apache.maven.api.Session; +import org.apache.maven.api.SourceRoot; import org.apache.maven.api.Toolchain; import org.apache.maven.api.Type; import org.apache.maven.api.annotations.Nonnull; @@ -202,6 +205,15 @@ private Charset charset() { @Parameter(property = "maven.compiler.enablePreview", defaultValue = "false") protected boolean enablePreview; + /** + * The root directories containing the source files to be compiled. If {@code null} or empty, + * the directories will be obtained from the {@code } elements declared in the project. + * If non-empty, the project {@code } elements are ignored. This configuration option + * should be used only when there is a need to override the project configuration. + */ + @Parameter + protected List compileSourceRoots; + /** * Additional arguments to be passed verbatim to the Java compiler. This parameter can be used when * the Maven compiler plugin does not provide a parameter for a Java compiler option. It may happen, @@ -817,26 +829,20 @@ private Charset charset() { private String tipForCommandLineCompilation; /** - * {@code true} if this MOJO is for compiling tests, or {@code false} if compiling the main code. + * {@code MAIN_COMPILE} if this MOJO is for compiling the main code, + * or {@code TEST_COMPILE} if compiling the tests. */ - final boolean isTestCompile; + final PathScope compileScope; /** * Creates a new MOJO. * - * @param isTestCompile {@code true} for compiling tests, or {@code false} for compiling the main code + * @param compileScope {@code MAIN_COMPILE} or {@code TEST_COMPILE} */ - protected AbstractCompilerMojo(boolean isTestCompile) { - this.isTestCompile = isTestCompile; + protected AbstractCompilerMojo(PathScope compileScope) { + this.compileScope = compileScope; } - /** - * {@return the root directories of Java source files to compile}. If the sources are organized according the - * Module Source Hierarchy, then the list shall enumerate the root source directory for each module. - */ - @Nonnull - protected abstract List getCompileSourceRoots(); - /** * {@return the inclusion filters for the compiler, or an empty list for all Java source files}. * The filter patterns are described in {@link java.nio.file.FileSystem#getPathMatcher(String)}. @@ -924,11 +930,13 @@ boolean hasModuleDeclaration(final List roots) throws IOExcepti * The typical case is the compilation of tests, which depends on the main compilation outputs. * The default implementation does nothing. * + * @param sourceDirectories the source directories * @param addTo where to add dependencies * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file * @throws IOException if this method needs to walk through directories and that operation failed */ - protected void addImplicitDependencies(Map> addTo, boolean hasModuleDeclaration) + void addImplicitDependencies( + List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) throws IOException { // Nothing to add in a standard build of main classes. } @@ -939,10 +947,10 @@ protected void addImplicitDependencies(Map> addTo, boolean * options may be used) or the test classes (in which case the {@code --patch-module} option may be used). * * @param addTo the collection of source paths to augment - * @param compileSourceRoots the source paths to eventually adds to the {@code toAdd} map + * @param sourceDirectories the source paths to eventually adds to the {@code toAdd} map * @throws IOException if this method needs to read a module descriptor and this operation failed */ - void addSourceDirectories(Map> addTo, List compileSourceRoots) + void addSourceDirectories(Map> addTo, List sourceDirectories) throws IOException { // No need to specify --source-path at this time, as it is for additional sources. } @@ -1100,7 +1108,7 @@ protected Options acceptParameters(final OptionChecker compiler) { compilerConfiguration.addIfNonBlank("--source", getSource()); targetOrReleaseSet = compilerConfiguration.addIfNonBlank("--target", getTarget()); targetOrReleaseSet |= compilerConfiguration.addIfNonBlank("--release", getRelease()); - if (!targetOrReleaseSet && !isTestCompile) { + if (!targetOrReleaseSet && ProjectScope.MAIN.equals(compileScope.projectScope())) { MessageBuilder mb = messageBuilderFactory .builder() .a("No explicit value set for --release or --target. " @@ -1186,11 +1194,17 @@ private void compile(final JavaCompiler compiler, final Options compilerConfigur */ List sourceFiles = List.of(); final Path outputDirectory = Files.createDirectories(getOutputDirectory()); - final List compileSourceRoots = - SourceDirectory.fromPaths(getCompileSourceRoots(), outputDirectory); + final List sourceDirectories; + if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { + ProjectScope scope = compileScope.projectScope(); + Stream roots = projectManager.getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY); + sourceDirectories = SourceDirectory.fromProject(roots, outputDirectory); + } else { + sourceDirectories = SourceDirectory.fromPluginConfiguration(compileSourceRoots, outputDirectory); + } final boolean hasModuleDeclaration; if (incAspects.contains(IncrementalBuild.Aspect.MODULES)) { - for (SourceDirectory root : compileSourceRoots) { + for (SourceDirectory root : sourceDirectories) { if (root.moduleName == null) { throw new CompilationFailureException("The value can be \"modules\" " + "only if all source directories are Java modules."); @@ -1205,7 +1219,7 @@ && getIncrementalExcludes().isEmpty())) { hasModuleDeclaration = true; } else { var filter = new PathFilter(getIncludes(), getExcludes(), getIncrementalExcludes()); - sourceFiles = filter.walkSourceFiles(compileSourceRoots); + sourceFiles = filter.walkSourceFiles(sourceDirectories); if (sourceFiles.isEmpty()) { String message = "No sources to compile."; try { @@ -1224,7 +1238,7 @@ && getIncrementalExcludes().isEmpty())) { hasModuleDeclaration = true; break; default: - hasModuleDeclaration = hasModuleDeclaration(compileSourceRoots); + hasModuleDeclaration = hasModuleDeclaration(sourceDirectories); break; } } @@ -1234,13 +1248,13 @@ && getIncrementalExcludes().isEmpty())) { * and this MOJO is compiling the main code, then a warning will be logged. * * NOTE: this method assumes that the map and the list values are modifiable. - * This is true with org.apache.maven.internal.impl.DefaultDependencyResolverResult, + * This is true with org.apache.maven.impl.DefaultDependencyResolverResult, * but may not be true in the general case. To be safe, we should perform a deep copy. * But it would be unnecessary copies in most cases. */ final Map> dependencies = resolveDependencies(compilerConfiguration, hasModuleDeclaration); resolveProcessorPathEntries(dependencies); - addImplicitDependencies(dependencies, hasModuleDeclaration); + addImplicitDependencies(sourceDirectories, dependencies, hasModuleDeclaration); /* * Verify if a dependency changed since the build started, or if a source file changed since the last build. * If there is no change, we can skip the build. If a dependency or the source tree has changed, we may @@ -1305,7 +1319,7 @@ && getIncrementalExcludes().isEmpty())) { * the `javax.tools.StandardLocation` API. */ if (hasModuleDeclaration) { - addSourceDirectories(dependencies, compileSourceRoots); + addSourceDirectories(dependencies, sourceDirectories); } /* * Create a `JavaFileManager`, configure all paths (dependencies and sources), then run the compiler. @@ -1450,7 +1464,7 @@ && getIncrementalExcludes().isEmpty())) { .append("Cannot compile ") .append(project.getId()) .append(' ') - .append(isTestCompile ? "test" : "main") + .append(compileScope.projectScope().id()) .append(" classes."); listener.firstError(failureCause).ifPresent((c) -> message.append(System.lineSeparator()) .append("The first error is: ") @@ -1571,7 +1585,7 @@ private Map> resolveDependencies(Options compilerConfigurat .session(session) .project(project) .requestType(DependencyResolverRequest.RequestType.RESOLVE) - .pathScope(isTestCompile ? PathScope.TEST_COMPILE : PathScope.MAIN_COMPILE) + .pathScope(compileScope) .pathTypeFilter(allowedTypes) .build()); /* @@ -1597,7 +1611,7 @@ private Map> resolveDependencies(Options compilerConfigurat throw (RuntimeException) exception; // A ClassCastException here would be a bug in above loop. } } - if (!isTestCompile) { + if (ProjectScope.MAIN.equals(compileScope.projectScope())) { String warning = dependencies.warningForFilenameBasedAutomodules().orElse(null); if (warning != null) { // Do not use Optional.ifPresent(…) for avoiding confusing source class name. logger.warn(warning); @@ -1619,7 +1633,7 @@ private Map> resolveDependencies(Options compilerConfigurat * to the {@link JavaPathType#PROCESSOR_CLASSES} entry of given map, which should be modifiable. * *

Implementation note

- * We rely on the fact that {@link org.apache.maven.internal.impl.DefaultDependencyResolverResult} creates + * We rely on the fact that {@link org.apache.maven.impl.DefaultDependencyResolverResult} creates * modifiable instances of map and lists. This is a fragile assumption, but this method is deprecated anyway * and may be removed in a future version. * @@ -1690,17 +1704,19 @@ private Set addGeneratedSourceDirectory(Path generatedSourcesDirectory) th // `createDirectories(Path)` does nothing if the directory already exists. generatedSourcesDirectory = Files.createDirectories(generatedSourcesDirectory); } - ProjectScope scope = isTestCompile ? ProjectScope.TEST : ProjectScope.MAIN; - projectManager.addCompileSourceRoot(project, scope, generatedSourcesDirectory.toAbsolutePath()); + ProjectScope scope = compileScope.projectScope(); + projectManager.addSourceRoot(project, scope, Language.JAVA_FAMILY, generatedSourcesDirectory.toAbsolutePath()); if (logger.isDebugEnabled()) { var sb = new StringBuilder("Adding \"") .append(generatedSourcesDirectory) .append("\" to ") .append(scope.id()) .append("-compile source roots. New roots are:"); - for (Path p : projectManager.getCompileSourceRoots(project, scope)) { - sb.append(System.lineSeparator()).append(" ").append(p); - } + projectManager + .getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY) + .forEach((p) -> { + sb.append(System.lineSeparator()).append(" ").append(p.directory()); + }); logger.debug(sb.toString()); } return Set.of(generatedSourcesDirectory); diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java index d89a6a9f..8f573fe3 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java @@ -25,7 +25,6 @@ import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -33,9 +32,9 @@ import java.util.TreeMap; import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.PathScope; import org.apache.maven.api.PathType; import org.apache.maven.api.ProducedArtifact; -import org.apache.maven.api.ProjectScope; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.plugin.MojoException; @@ -64,13 +63,6 @@ public class CompilerMojo extends AbstractCompilerMojo { @Parameter(property = "maven.main.skip") protected boolean skipMain; - /** - * The source directories containing the sources to be compiled. - * If {@code null} or empty, the directory will be obtained from the project manager. - */ - @Parameter - protected List compileSourceRoots; - /** * Specify where to place generated source files created by annotation processing. * @@ -148,7 +140,7 @@ public class CompilerMojo extends AbstractCompilerMojo { * Creates a new compiler MOJO. */ public CompilerMojo() { - super(false); + super(PathScope.MAIN_COMPILE); } /** @@ -185,23 +177,6 @@ protected Options acceptParameters(final OptionChecker compiler) { return compilerConfiguration; } - /** - * {@return the root directories of Java source files to compile}. - * It can be a parameter specified to the compiler plugin, - * or otherwise the value provided by the project manager. - */ - @Nonnull - @Override - protected List getCompileSourceRoots() { - List sources; - if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { - sources = projectManager.getCompileSourceRoots(project, ProjectScope.MAIN); - } else { - sources = compileSourceRoots.stream().map(Paths::get).toList(); - } - return sources; - } - /** * {@return the path where to place generated source files created by annotation processing on the main classes}. */ @@ -261,6 +236,7 @@ protected String getDebugFileName() { /** * If compiling a multi-release JAR in the old deprecated way, add the previous versions to the path. * + * @param sourceDirectories the source directories * @param addTo where to add dependencies * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file * @throws IOException if this method needs to walk through directories and that operation failed @@ -269,7 +245,8 @@ protected String getDebugFileName() { */ @Override @Deprecated(since = "4.0.0") - protected void addImplicitDependencies(Map> addTo, boolean hasModuleDeclaration) + protected void addImplicitDependencies( + List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) throws IOException { if (SUPPORT_LEGACY && multiReleaseOutput) { var paths = new TreeMap(); @@ -309,8 +286,8 @@ protected void addImplicitDependencies(Map> addTo, boolean * search in the source files for the Java release of the current compilation unit. */ if (moduleName == null) { - for (Path path : getCompileSourceRoots()) { - moduleName = parseModuleInfoName(path.resolve(MODULE_INFO + JAVA_FILE_SUFFIX)); + for (SourceDirectory dir : sourceDirectories) { + moduleName = parseModuleInfoName(dir.root.resolve(MODULE_INFO + JAVA_FILE_SUFFIX)); if (moduleName != null) { break; } diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java index 180f7218..25ac7926 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java @@ -28,6 +28,9 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; + +import org.apache.maven.api.SourceRoot; /** * A single root directory of source files, associated with module name and release version. @@ -146,19 +149,41 @@ private SourceDirectory( this.outputFileKind = outputFileKind; } + /** + * Gets the list of source directories from the project manager. + * The returned list includes only the directories that exist. + * + * @param compileSourceRoots the root paths to source files + * @param outputDirectory the directory where to store the compilation results + * @return the given list of paths wrapped as source directory objects + */ + static List fromProject(Stream compileSourceRoots, Path outputDirectory) { + var roots = new ArrayList(); + compileSourceRoots.forEach((SourceRoot r) -> { + Path p = r.directory(); + if (Files.exists(p)) { + // TODO: specify file kind, module name and release version. + roots.add(new SourceDirectory( + p, JavaFileObject.Kind.SOURCE, null, null, outputDirectory, JavaFileObject.Kind.CLASS)); + } + }); + return roots; + } + /** * Converts the given list of paths to a list of source directories. * The returned list includes only the directories that exist. + * Used only when the compiler plugin is configured with the {@code compileSourceRoots} option. * * @param compileSourceRoots the root paths to source files * @param outputDirectory the directory where to store the compilation results * @return the given list of paths wrapped as source directory objects */ - static List fromPaths(List compileSourceRoots, Path outputDirectory) { + static List fromPluginConfiguration(List compileSourceRoots, Path outputDirectory) { var roots = new ArrayList(compileSourceRoots.size()); - for (Path p : compileSourceRoots) { + for (String file : compileSourceRoots) { + Path p = Path.of(file); if (Files.exists(p)) { - // TODO: specify file kind, module name and release version. roots.add(new SourceDirectory( p, JavaFileObject.Kind.SOURCE, null, null, outputDirectory, JavaFileObject.Kind.CLASS)); } diff --git a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java index ed3b729e..4e6f29f1 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java @@ -26,7 +26,6 @@ import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -36,8 +35,8 @@ import org.apache.maven.api.Dependency; import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.PathScope; import org.apache.maven.api.PathType; -import org.apache.maven.api.ProjectScope; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.plugin.MojoException; @@ -70,14 +69,6 @@ public class TestCompilerMojo extends AbstractCompilerMojo { @Parameter(property = "maven.test.skip") protected boolean skip; - /** - * The source directories containing the test-source to be compiled. - * - * @see CompilerMojo#compileSourceRoots - */ - @Parameter - protected List compileSourceRoots; - /** * Specify where to place generated source files created by annotation processing. * @@ -192,7 +183,7 @@ public class TestCompilerMojo extends AbstractCompilerMojo { * Its value should be the same as {@link CompilerMojo#outputDirectory}. * * @see CompilerMojo#outputDirectory - * @see #addImplicitDependencies(Map, boolean) + * @see #addImplicitDependencies(List, Map, boolean) */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) protected Path mainOutputDirectory; @@ -249,7 +240,7 @@ public class TestCompilerMojo extends AbstractCompilerMojo { * Creates a new test compiler MOJO. */ public TestCompilerMojo() { - super(true); + super(PathScope.TEST_COMPILE); } /** @@ -287,19 +278,6 @@ protected Options acceptParameters(final OptionChecker compiler) { return compilerConfiguration; } - /** - * {@return the root directories of Java source files to compile for the tests}. - */ - @Nonnull - @Override - protected List getCompileSourceRoots() { - if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { - return projectManager.getCompileSourceRoots(project, ProjectScope.TEST); - } else { - return compileSourceRoots.stream().map(Paths::get).toList(); - } - } - /** * {@return the path where to place generated source files created by annotation processing on the test classes}. */ @@ -456,11 +434,13 @@ final boolean hasModuleDeclaration(final List roots) throws IOE /** * Adds the main compilation output directories as test dependencies. * + * @param sourceDirectories the source directories (ignored) * @param addTo where to add dependencies * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file */ @Override - protected void addImplicitDependencies(Map> addTo, boolean hasModuleDeclaration) { + void addImplicitDependencies( + List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) { var pathType = hasModuleDeclaration ? JavaPathType.MODULES : JavaPathType.CLASSES; if (Files.exists(mainOutputDirectory)) { addTo.computeIfAbsent(pathType, (key) -> new ArrayList<>()).add(mainOutputDirectory); @@ -474,13 +454,13 @@ protected void addImplicitDependencies(Map> addTo, boolean * classes (it may happen in other parts of the compiler plugin). * * @param addTo the collection of source paths to augment - * @param compileSourceRoots the source paths to eventually adds to the {@code toAdd} map + * @param sourceDirectories the source paths to eventually adds to the {@code toAdd} map * @throws IOException if this method needs to read a module descriptor and this operation failed */ @Override - final void addSourceDirectories(Map> addTo, List compileSourceRoots) + final void addSourceDirectories(Map> addTo, List sourceDirectories) throws IOException { - for (SourceDirectory dir : compileSourceRoots) { + for (SourceDirectory dir : sourceDirectories) { String moduleToPatch = dir.moduleName; if (moduleToPatch == null) { moduleToPatch = getMainModuleName(); @@ -488,7 +468,7 @@ final void addSourceDirectories(Map> addTo, List Date: Mon, 27 Jan 2025 10:58:10 +0100 Subject: [PATCH 03/13] Move the formatting of options in the `Options` class. --- .../plugin/compiler/AbstractCompilerMojo.java | 36 ++---------- .../apache/maven/plugin/compiler/Options.java | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 32 deletions(-) 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 ba1d2dcd..0f752010 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1451,7 +1451,7 @@ && getIncrementalExcludes().isEmpty())) { if (!success || logger.isDebugEnabled()) { IOException suppressed = null; try { - writeDebugFile(compilerConfiguration.options, dependencies, sourceFiles); + writeDebugFile(compilerConfiguration, dependencies, sourceFiles); if (success && tipForCommandLineCompilation != null) { logger.debug(tipForCommandLineCompilation); tipForCommandLineCompilation = null; @@ -1767,13 +1767,13 @@ private void writePlugin(MessageBuilder mb, String option, String value) { * If a file name contains embedded spaces, then the whole file name must be between double quotation marks. * The -J options are not supported. * - * @param options the compiler options + * @param compilerConfiguration options to provide to the compiler * @param dependencies the dependencies * @param sourceFiles all files to compile * @throws IOException if an error occurred while writing the debug file */ private void writeDebugFile( - List options, Map> dependencies, List sourceFiles) + Options compilerConfiguration, Map> dependencies, List sourceFiles) throws IOException { final Path path = getDebugFilePath(); if (path == null) { @@ -1784,36 +1784,8 @@ private void writeDebugFile( .append(System.lineSeparator()) .append(" ") .append(executable != null ? executable : compilerId); - boolean hasOptions = false; try (BufferedWriter out = Files.newBufferedWriter(path)) { - for (String option : options) { - if (option.isBlank()) { - continue; - } - if (option.startsWith("-J")) { - commandLine.append(' ').append(option); - continue; - } - if (hasOptions) { - if (option.charAt(0) == '-') { - out.newLine(); - } else { - out.write(' '); - } - } - boolean needsQuote = option.indexOf(' ') >= 0; - if (needsQuote) { - out.write('"'); - } - out.write(option); - if (needsQuote) { - out.write('"'); - } - hasOptions = true; - } - if (hasOptions) { - out.newLine(); - } + compilerConfiguration.format(commandLine, out); for (Map.Entry> entry : dependencies.entrySet()) { String separator = ""; for (String element : entry.getKey().option(entry.getValue())) { diff --git a/src/main/java/org/apache/maven/plugin/compiler/Options.java b/src/main/java/org/apache/maven/plugin/compiler/Options.java index c12ec8f8..e7e4e704 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/Options.java +++ b/src/main/java/org/apache/maven/plugin/compiler/Options.java @@ -20,6 +20,8 @@ import javax.tools.OptionChecker; +import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -337,4 +339,60 @@ void addUnchecked(String arguments) { addUnchecked(Arrays.asList(arguments.split(" "))); } } + + /** + * Formats the options for debugging purposes. + * + * @param commandLine the prefix where to put the {@code -J} options before all other options + * @param out where to put all options other than {@code -J} + * @throws IOException if an error occurred while writing an option + */ + void format(final StringBuilder commandLine, final Appendable out) throws IOException { + boolean hasOptions = false; + for (String option : options) { + if (option.isBlank()) { + continue; + } + if (option.startsWith("-J")) { + if (commandLine.length() != 0) { + commandLine.append(' '); + } + commandLine.append(option); + continue; + } + if (hasOptions) { + if (option.charAt(0) == '-') { + out.append(System.lineSeparator()); + } else { + out.append(' '); + } + } + boolean needsQuote = option.indexOf(' ') >= 0; + if (needsQuote) { + out.append('"'); + } + out.append(option); + if (needsQuote) { + out.append('"'); + } + hasOptions = true; + } + if (hasOptions) { + out.append(System.lineSeparator()); + } + } + + /** + * {@return a string representatation of the options for debugging purposes}. + */ + @Override + public String toString() { + var out = new StringBuilder(40); + try { + format(out, out); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return out.toString(); + } } From 5eaf0d1d216b33a0ec6da5ad9badd817f1530ba3 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 15 Mar 2025 15:01:13 +0100 Subject: [PATCH 04/13] Use relative paths when possible in the `javac.args` file that is generated. https://github.com/apache/maven-compiler-plugin/pull/193 --- .../plugin/compiler/AbstractCompilerMojo.java | 28 +++++++++++++++++-- .../plugin/compiler/SourceDirectory.java | 6 +++- 2 files changed, 30 insertions(+), 4 deletions(-) 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 0f752010..70477f07 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1787,8 +1787,10 @@ private void writeDebugFile( try (BufferedWriter out = Files.newBufferedWriter(path)) { compilerConfiguration.format(commandLine, out); for (Map.Entry> entry : dependencies.entrySet()) { + List files = entry.getValue(); + files = files.stream().map(this::relativize).toList(); String separator = ""; - for (String element : entry.getKey().option(entry.getValue())) { + for (String element : entry.getKey().option(files)) { out.write(separator); out.write(element); separator = " "; @@ -1796,16 +1798,36 @@ private void writeDebugFile( out.newLine(); } out.write("-d \""); - out.write(getOutputDirectory().toString()); + out.write(relativize(getOutputDirectory()).toString()); out.write('"'); out.newLine(); for (SourceFile sf : sourceFiles) { out.write('"'); - out.write(sf.file.toString()); + out.write(relativize(sf.file).toString()); out.write('"'); out.newLine(); } } tipForCommandLineCompilation = commandLine.append(" @").append(path).toString(); } + + /** + * Makes the given file relative to the base directory if the path is inside the project directory tree. + * The check for the project directory tree (starting from the root of all sub-projects) is for avoiding + * to relativize the paths to JAR files in the Maven local repository for example. + * + * @param file the path to make relative to the base directory + * @return the given path, potentially relative to the base directory + */ + private Path relativize(Path file) { + Path root = project.getRootDirectory(); + if (root != null && file.startsWith(root)) { + try { + file = basedir.relativize(file); + } catch (IllegalArgumentException e) { + // Ignore, keep the absolute path. + } + } + return file; + } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java index 25ac7926..55789c53 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java @@ -59,7 +59,11 @@ final class SourceDirectory { static final String CLASS_FILE_SUFFIX = ".class"; /** - * The root directory of all source files. + * The root directory of all source files. Whether the path is relative or absolute depends on the paths given to + * the {@link #fromProject fromProject(…)} or {@link #fromPluginConfiguration fromPluginConfiguration(…)} methods. + * This class preserves the relative/absolute characteristic of the user-specified directories in order to behave + * as intended by users in operations such as {@linkplain Path#relativize relativization}, especially in regard of + * symbolic links. In practice, this path is often an absolute path. */ final Path root; From 51947a8efaee808b6769e7df6c56fb26a8bdeb4b Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 16 Mar 2025 13:20:13 +0100 Subject: [PATCH 05/13] Take in account more information provided by the `` elements of the `pom.xml` file. With this commit, the include/exclude filters declared in `` are combined with the include/exclude filters declared in the puglin configuration. The module name and release information are also stored, but only partially supported yet. --- .../plugin/compiler/AbstractCompilerMojo.java | 2 +- .../maven/plugin/compiler/CompilerMojo.java | 6 +- .../maven/plugin/compiler/PathFilter.java | 101 +++++++-------- .../plugin/compiler/SourceDirectory.java | 117 ++++++++++++++---- .../maven/plugin/compiler/SourceFile.java | 2 +- .../plugin/compiler/SourcesForRelease.java | 24 +++- 6 files changed, 169 insertions(+), 83 deletions(-) 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 70477f07..b70b6aa6 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1372,7 +1372,7 @@ && getIncrementalExcludes().isEmpty())) { logger.warn(sb); } /* - * Configure all paths to source files. Each compilation unit has its own set of source. + * Configure all paths to source files. Each compilation unit has its own set of sources. * More than one compilation unit may exist in the case of a multi-releases project. * Units are compiled in the order of the release version, with base compiled first. */ diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java index 8f573fe3..0bef3795 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java @@ -219,7 +219,7 @@ protected Set getIncrementalExcludes() { @Override protected Path getOutputDirectory() { if (SUPPORT_LEGACY && multiReleaseOutput && release != null) { - return outputDirectory.resolve(Path.of("META-INF", "versions", release)); + return SourceDirectory.outputDirectoryForReleases(outputDirectory).resolve(release); } return outputDirectory; } @@ -245,12 +245,12 @@ protected String getDebugFileName() { */ @Override @Deprecated(since = "4.0.0") - protected void addImplicitDependencies( + void addImplicitDependencies( List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) throws IOException { if (SUPPORT_LEGACY && multiReleaseOutput) { var paths = new TreeMap(); - Path root = outputDirectory.resolve(Path.of("META-INF", "versions")); + Path root = SourceDirectory.outputDirectoryForReleases(outputDirectory); Files.walk(root, 1).forEach((path) -> { int version; if (path.equals(root)) { diff --git a/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java b/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java index d194b30e..a917e191 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java +++ b/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java @@ -18,8 +18,6 @@ */ package org.apache.maven.plugin.compiler; -import javax.tools.JavaFileObject; - import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.FileSystem; @@ -61,17 +59,22 @@ final class PathFilter extends SimpleFileVisitor implements Predicate implements Predicate implements Predicate implements Predicate includes, Collection excludes, Collection incrementalExcludes) { - defaultInclude = includes.isEmpty(); - if (defaultInclude) { - includes = List.of("**"); + useDefaultInclude = includes.isEmpty(); + if (useDefaultInclude) { + includes = List.of("**"); // Place-holder replaced by "**/*.java" in `test(…)`. } this.includes = includes.toArray(String[]::new); this.excludes = excludes.toArray(String[]::new); this.incrementalExcludes = incrementalExcludes.toArray(String[]::new); - includeMatchers = new PathMatcher[this.includes.length]; - excludeMatchers = new PathMatcher[this.excludes.length]; - incrementalExcludeMatchers = new PathMatcher[this.incrementalExcludes.length]; - needRelativize = needRelativize(this.includes) || needRelativize(this.excludes); } /** @@ -158,7 +157,7 @@ final class PathFilter extends SimpleFileVisitor implements PredicateThis method should be invoked only once, unless different paths are on different file systems.

+ * + * @param forDirectory the matchers declared in the {@code } element for the current {@link #sourceRoot} + * @param patterns the matterns declared in the compiler plugin configuration + * @param hasDefault whether the first element of {@code patterns} is the default pattern + * @param fs the file system + * @return all matchers from the source, followed by matchers from the given patterns */ - private static void createMatchers(String[] patterns, PathMatcher[] target, FileSystem fs) { - for (int i = 0; i < patterns.length; i++) { + private static PathMatcher[] createMatchers( + List forDirectory, String[] patterns, boolean hasDefault, FileSystem fs) { + final int base = forDirectory.size(); + final int skip = (hasDefault && base != 0) ? 1 : 0; + final var target = forDirectory.toArray(new PathMatcher[base + patterns.length - skip]); + for (int i = skip; i < patterns.length; i++) { String pattern = patterns[i]; if (pattern.indexOf(':') < 0) { pattern = "glob:" + pattern; } - target[i] = fs.getPathMatcher(pattern); + target[base + i] = fs.getPathMatcher(pattern); } + return target; } /** @@ -207,11 +202,19 @@ private static void createMatchers(String[] patterns, PathMatcher[] target, File */ @Override public boolean test(Path path) { + @SuppressWarnings("LocalVariableHidesMemberVariable") + final SourceDirectory sourceRoot = this.sourceRoot; // Protect from changes. FileSystem pfs = path.getFileSystem(); if (pfs != fs) { - createMatchers(includes, includeMatchers, pfs); - createMatchers(excludes, excludeMatchers, pfs); - createMatchers(incrementalExcludes, incrementalExcludeMatchers, pfs); + if (useDefaultInclude) { + includes[0] = "glob:**" + sourceRoot.fileKind.extension; + } + includeMatchers = createMatchers(sourceRoot.includes, includes, useDefaultInclude, pfs); + excludeMatchers = createMatchers(sourceRoot.excludes, excludes, false, pfs); + incrementalExcludeMatchers = createMatchers(List.of(), incrementalExcludes, false, pfs); + needRelativize = !(sourceRoot.includes.isEmpty() && sourceRoot.excludes.isEmpty()) + || needRelativize(includes) + || needRelativize(excludes); fs = pfs; } if (needRelativize) { @@ -291,8 +294,8 @@ public List walkSourceFiles(Iterable rootDirectorie sourceFiles = result; for (SourceDirectory directory : rootDirectories) { sourceRoot = directory; - updateDefaultInclude(directory.fileKind); Files.walkFileTree(directory.root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, this); + fs = null; // Will force a recalculation of matchers in next iteration. } } catch (UncheckedIOException e) { throw e.getCause(); diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java index 55789c53..603e3783 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java @@ -21,16 +21,18 @@ import javax.lang.model.SourceVersion; import javax.tools.JavaFileObject; -import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; +import org.apache.maven.api.Language; import org.apache.maven.api.SourceRoot; +import org.apache.maven.api.Version; /** * A single root directory of source files, associated with module name and release version. @@ -67,6 +69,26 @@ final class SourceDirectory { */ final Path root; + /** + * Filter for selecting files below the {@linkplain #root} directory, or an empty list for the default filter. + * For the Java language, the default filter is {@code "*.java"}. The filters are used by {@link PathFilter}. + * + *

This field differs from {@link PathFilter#includes} in that it is specified in the {@code } element, + * while the latter is specified in the plugin configuration. The filter specified here can be different for each + * source directory, while the plugin configuration applies to all source directories.

+ * + * @see PathFilter#includes + */ + final List includes; + + /** + * Filter for excluding files below the {@linkplain #root} directory, or an empty list for no exclusion. + * See {@link #includes} for the difference between this field and {@link PathFilter#excludes}. + * + * @see PathFilter#excludes + */ + final List excludes; + /** * Kind of source files in this directory. This is usually {@link JavaFileObject.Kind#SOURCE}. * This information is used for building a default include filter such as {@code "glob:*.java} @@ -124,35 +146,59 @@ final class SourceDirectory { * @param outputDirectory the directory where to store the compilation results * @param outputFileKind Kind of output files in the output directory (usually {@ codeCLASS}) */ + @SuppressWarnings("checkstyle:ParameterNumber") private SourceDirectory( Path root, + List includes, + List excludes, JavaFileObject.Kind fileKind, String moduleName, SourceVersion release, Path outputDirectory, JavaFileObject.Kind outputFileKind) { this.root = Objects.requireNonNull(root); + this.includes = Objects.requireNonNull(includes); + this.excludes = Objects.requireNonNull(excludes); this.fileKind = Objects.requireNonNull(fileKind); this.moduleName = moduleName; this.release = release; + if (moduleName != null) { + outputDirectory = outputDirectory.resolve(moduleName); + } if (release != null) { - String version = release.name(); + String version = release.name(); // TODO: replace by runtimeVersion() in Java 18. version = version.substring(version.lastIndexOf('_') + 1); - FileSystem fs = outputDirectory.getFileSystem(); - Path subdir; - if (moduleName != null) { - subdir = fs.getPath(moduleName, "META-INF", "versions", version); - } else { - subdir = fs.getPath("META-INF", "versions", version); - } - outputDirectory = outputDirectory.resolve(subdir); - } else if (moduleName != null) { - outputDirectory = outputDirectory.resolve(moduleName); + outputDirectory = outputDirectoryForReleases(outputDirectory).resolve(version); } this.outputDirectory = outputDirectory; this.outputFileKind = outputFileKind; } + /** + * Returns the directory where to write the compilation for a specific Java release. + * The caller shall add the version number to the returned path. + */ + static Path outputDirectoryForReleases(Path outputDirectory) { + // TODO: use Path.resolve(String, String...) with Java 22. + return outputDirectory.resolve("META-INF").resolve("versions"); + } + + /** + * Converts a version from Maven API to Java tools API. + * This method parses the version with {@link Runtime.Version#parse(String)}. + * Therefore, for Java 8, the version shall be "8", not "1.8". + */ + private static SourceVersion parse(final Version version) { + String text = version.asString(); + try { + var parsed = Runtime.Version.parse(text); + return SourceVersion.valueOf("RELEASE_" + parsed.feature()); + // TODO: Replace by return SourceVersion.valueOf(v) after upgrade to Java 18. + } catch (IllegalArgumentException e) { + throw new CompilationFailureException("Illegal version number: \"" + text + '"', e); + } + } + /** * Gets the list of source directories from the project manager. * The returned list includes only the directories that exist. @@ -163,12 +209,24 @@ private SourceDirectory( */ static List fromProject(Stream compileSourceRoots, Path outputDirectory) { var roots = new ArrayList(); - compileSourceRoots.forEach((SourceRoot r) -> { - Path p = r.directory(); - if (Files.exists(p)) { - // TODO: specify file kind, module name and release version. + compileSourceRoots.forEach((SourceRoot source) -> { + Path directory = source.directory(); + if (Files.exists(directory)) { + var fileKind = JavaFileObject.Kind.OTHER; + var outputFileKind = JavaFileObject.Kind.OTHER; + if (Language.JAVA_FAMILY.equals(source.language())) { + fileKind = JavaFileObject.Kind.SOURCE; + outputFileKind = JavaFileObject.Kind.CLASS; + } roots.add(new SourceDirectory( - p, JavaFileObject.Kind.SOURCE, null, null, outputDirectory, JavaFileObject.Kind.CLASS)); + directory, + source.includes(), + source.excludes(), + fileKind, + source.module().orElse(null), + source.targetVersion().map(SourceDirectory::parse).orElse(null), + outputDirectory, + outputFileKind)); } }); return roots; @@ -186,10 +244,17 @@ static List fromProject(Stream compileSourceRoots, static List fromPluginConfiguration(List compileSourceRoots, Path outputDirectory) { var roots = new ArrayList(compileSourceRoots.size()); for (String file : compileSourceRoots) { - Path p = Path.of(file); - if (Files.exists(p)) { + Path directory = Path.of(file); + if (Files.exists(directory)) { roots.add(new SourceDirectory( - p, JavaFileObject.Kind.SOURCE, null, null, outputDirectory, JavaFileObject.Kind.CLASS)); + directory, + List.of(), + List.of(), + JavaFileObject.Kind.SOURCE, + null, + null, + outputDirectory, + JavaFileObject.Kind.CLASS)); } } return roots; @@ -234,10 +299,14 @@ public Optional getModuleInfo() { @Override public boolean equals(Object obj) { if (obj instanceof SourceDirectory other) { - return release == other.release + return root.equals(other.root) + && includes.equals(other.includes) + && excludes.equals(other.excludes) + && fileKind == other.fileKind && Objects.equals(moduleName, other.moduleName) - && root.equals(other.root) - && outputDirectory.equals(other.outputDirectory); + && release == other.release + && outputDirectory.equals(other.outputDirectory) + && outputFileKind == other.outputFileKind; } return false; } @@ -247,7 +316,7 @@ public boolean equals(Object obj) { */ @Override public int hashCode() { - return root.hashCode() + 7 * Objects.hash(moduleName, release); + return Objects.hash(root, moduleName, release); } /** diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java b/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java index cae5baee..bf8331f5 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java @@ -91,7 +91,7 @@ final class SourceFile { /** * Returns the file resulting from the compilation of this source file. If the output file has been * {@linkplain javax.tools.JavaFileManager#getFileForOutput obtained from the compiler}, that value - * if returned. Otherwise if {@code infer} is {@code true}, then the output file is inferred using + * if returned. Otherwise, if {@code infer} is {@code true}, then the output file is inferred using * {@linkplain #toOutputFile heuristic rules}. * * @param infer whether to allow this method to infer a default path using heuristic rules diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java index 68c76086..6880f360 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java @@ -43,17 +43,24 @@ final class SourcesForRelease implements Closeable { /** * The release for this set of sources. For this class, the * {@link SourceVersion#RELEASE_0} value means "no version". + * + * @see SourceDirectory#release */ final SourceVersion release; /** - * All source files. + * All source files. This is the union of all {@link SourceFile#file} for this {@linkplain #release}. + * + * @see SourceFile#file */ final List files; /** - * The root directories for each module. Keys are module names. - * The empty string stands for no module. + * All source directories that are part of this compilation unit, grouped by module names. + * The keys in the map are the module names, with the empty string standing for no module. + * Values are the union of all {@link SourceDirectory#root} for this {@linkplain #release}. + * + * @see SourceDirectory#root */ final Map> roots; @@ -84,7 +91,7 @@ private SourcesForRelease(SourceVersion release) { /** * Adds the given source file to this collection of source files. - * The value of {@code source.directory.release} must be {@link #release}. + * The value of {@code source.directory.release}, if not null, must be equal to {@link #release}. * * @param source the source file to add. */ @@ -119,7 +126,6 @@ public static Collection groupByReleaseAndModule(List Date: Sun, 16 Mar 2025 18:12:37 +0100 Subject: [PATCH 06/13] Refactor `AbstractCompilerMojo` by moving part of the `compile` method body in a separated class. The intend is to reduce the complexity by splitting one big method into smaller methods. It may also help external applications (e.g. IDE) to use `ToolExecutor` in their environment. --- .../plugin/compiler/AbstractCompilerMojo.java | 537 ++++++------------ .../maven/plugin/compiler/CompilerMojo.java | 20 +- .../maven/plugin/compiler/PathFilter.java | 17 +- .../plugin/compiler/SourcesForRelease.java | 37 +- .../plugin/compiler/TestCompilerMojo.java | 241 ++------ .../maven/plugin/compiler/ToolExecutor.java | 447 +++++++++++++++ .../plugin/compiler/ToolExecutorForTest.java | 304 ++++++++++ 7 files changed, 1007 insertions(+), 596 deletions(-) create mode 100644 src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java create mode 100644 src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.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 b70b6aa6..fabb42bc 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -19,12 +19,11 @@ package org.apache.maven.plugin.compiler; import javax.lang.model.SourceVersion; +import javax.tools.DiagnosticListener; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.OptionChecker; -import javax.tools.StandardJavaFileManager; -import javax.tools.StandardLocation; import javax.tools.Tool; import javax.tools.ToolProvider; @@ -37,15 +36,11 @@ import java.io.UncheckedIOException; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; -import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; @@ -90,6 +85,12 @@ * This plugin uses the {@link JavaCompiler} interface from JDK 6+. * Each instance shall be used only once, then discarded. * + *

Thread-safety

+ * This class is not thread-safe. If this class is used in a multi-thread context, + * users are responsible for synchronizing all accesses to this MOJO instance. + * However, the executor returned by {@link #createExecutor(DiagnosticListener)} can safely + * launch the compilation in a background thread. + * * @author Trygve Laugstøl * @author Martin Desruisseaux * @since 2.0 @@ -107,13 +108,6 @@ public abstract class AbstractCompilerMojo implements Mojo { */ private static final String DEFAULT_EXECUTABLE = "javac"; - /** - * The locale for diagnostics, or {@code null} for the platform default. - * - * @see #encoding - */ - private static final Locale LOCALE = null; - // ---------------------------------------------------------------------- // Configurables // ---------------------------------------------------------------------- @@ -142,7 +136,7 @@ public abstract class AbstractCompilerMojo implements Mojo { * No warning is emitted in the latter case because as of Java 18, the default is UTF-8, * i.e. the encoding is no longer platform-dependent. */ - private Charset charset() { + final Charset charset() { if (encoding != null) { try { return Charset.forName(encoding); @@ -582,6 +576,25 @@ private Charset charset() { @Parameter(property = "maven.compiler.useIncrementalCompilation") protected Boolean useIncrementalCompilation; + /** + * Returns the configuration of the incremental compilation. + * This method may be removed in a future version if the deprecated parameter is removed. + * + * @throws MojoException if a value is not recognized, or if mutually exclusive values are specified + */ + final EnumSet incrementalCompilationConfiguration() { + if (useIncrementalCompilation != null) { + return useIncrementalCompilation + ? EnumSet.of( + IncrementalBuild.Aspect.SOURCES, + IncrementalBuild.Aspect.ADDITIONS, + IncrementalBuild.Aspect.DEPENDENCIES) + : EnumSet.of(IncrementalBuild.Aspect.CLASSES); + } else { + return IncrementalBuild.Aspect.parse(incrementalCompilation); + } + } + /** * File extensions to check timestamp for incremental build. * Default contains only {@code class} and {@code jar}. @@ -865,6 +878,16 @@ protected AbstractCompilerMojo(PathScope compileScope) { */ protected abstract Set getIncrementalExcludes(); + /** + * {@return whether all includes/excludes matchers specified in the plugin configuration are empty}. + * This method checks only the plugin configuration. It does not check the {@code } elements. + */ + final boolean hasNoFileMatchers() { + return getIncludes().isEmpty() + && getExcludes().isEmpty() + && getIncrementalExcludes().isEmpty(); + } + /** * {@return the destination directory (or class output directory) for class files}. * This directory will be given to the {@code -d} Java compiler option. @@ -899,6 +922,24 @@ protected String getRelease() { return release; } + /** + * {@return the root directories of the Java source files to compile, excluding empty directories}. + * The list needs to be modifiable for allowing the addition of generated source directories. + * This is determined from the {@link #compileSourceRoots} plugin configuration if non-empty, + * or from {@code } elements otherwise. + * + * @param outputDirectory the directory where to store the compilation results + */ + final List getSourceDirectories(final Path outputDirectory) { + if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { + ProjectScope scope = compileScope.projectScope(); + Stream roots = projectManager.getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY); + return SourceDirectory.fromProject(roots, outputDirectory); + } else { + return SourceDirectory.fromPluginConfiguration(compileSourceRoots, outputDirectory); + } + } + /** * {@return the path where to place generated source files created by annotation processing}. */ @@ -910,6 +951,9 @@ protected String getRelease() { * Note that the sources may contain more than one {@code module-info.java} file * if compiling a project with Module Source Hierarchy. * + *

If the user explicitly specified a modular or classpath project, then the + * {@code module-info.java} is assumed to exist or not without verification.

+ * *

The test compiler overrides this method for checking the existence of the * the {@code module-info.class} file in the main output directory instead.

* @@ -917,12 +961,19 @@ protected String getRelease() { * @throws IOException if this method needed to read a module descriptor and failed */ boolean hasModuleDeclaration(final List roots) throws IOException { - for (SourceDirectory root : roots) { - if (root.getModuleInfo().isPresent()) { + switch (project.getPackaging().type().id()) { + case Type.CLASSPATH_JAR: + return false; + case Type.MODULAR_JAR: return true; - } + default: + for (SourceDirectory root : roots) { + if (root.getModuleInfo().isPresent()) { + return true; + } + } + return false; } - return false; } /** @@ -941,32 +992,6 @@ void addImplicitDependencies( // Nothing to add in a standard build of main classes. } - /** - * Adds options for declaring the source directories. The way to declare those directories depends on whether - * we are compiling the main classes (in which case the {@code --source-path} or {@code --module-source-path} - * options may be used) or the test classes (in which case the {@code --patch-module} option may be used). - * - * @param addTo the collection of source paths to augment - * @param sourceDirectories the source paths to eventually adds to the {@code toAdd} map - * @throws IOException if this method needs to read a module descriptor and this operation failed - */ - void addSourceDirectories(Map> addTo, List sourceDirectories) - throws IOException { - // No need to specify --source-path at this time, as it is for additional sources. - } - - /** - * Generates options for handling the given dependencies. - * This method should do nothing when compiling the main classes, because the {@code module-info.java} file - * should contain all the required configuration. However, this method may need to add some {@code -add-reads} - * options when compiling the test classes. - * - * @param dependencies the project dependencies - * @param addTo where to add the options - * @throws IOException if the module information of a dependency cannot be read - */ - protected void addModuleOptions(DependencyResolverResult dependencies, Options addTo) throws IOException {} - /** * {@return the file where to dump the command-line when debug logging is enabled or when the compilation failed}. * For example, if the value is {@code "javac"}, then the Java compiler can be launched @@ -991,16 +1016,25 @@ final Path getDebugFilePath() { } /** - * Runs the Java compiler. + * Runs the Java compiler. This method performs the following steps: + * + *
    + *
  1. Get a Java compiler by a call to {@link #compiler()}.
  2. + *
  3. Get the options to give to the compiler by a call to {@link #parseParameters(OptionChecker)}.
  4. + *
  5. Get an executor with {@link #createExecutor(DiagnosticListener)} with the default listener.
  6. + *
  7. {@linkplain ToolExecutor#applyIncrementalBuild Apply the incremental build} if enabled.
  8. + *
  9. {@linkplain ToolExecutor#compile Execute the compilation}.
  10. + *
  11. Shows messages in the {@linkplain #logger}.
  12. + *
* * @throws MojoException if the compiler cannot be run */ @Override public void execute() throws MojoException { JavaCompiler compiler = compiler(); - Options compilerConfiguration = acceptParameters(compiler); + Options configuration = parseParameters(compiler); try { - compile(compiler, compilerConfiguration); + compile(compiler, configuration); } catch (RuntimeException e) { String message = e.getLocalizedMessage(); if (message == null) { @@ -1030,15 +1064,28 @@ public void execute() throws MojoException { } } + /** + * Creates a new task by taking a snapshot of the current configuration of this MOJO. + * This method creates the {@linkplain ToolExecutor#outputDirectory output directory} if it does not already exist. + * + * @param listener where to send compilation warnings, or {@code null} for the Maven logger + * @throws MojoException if this method identifies an invalid parameter in this MOJO + * @return the task to execute for compiling the project using the configuration in this MOJO + * @throws IOException if an error occurred while creating the output directory or scanning the source directories + */ + public ToolExecutor createExecutor(DiagnosticListener listener) throws IOException { + return new ToolExecutor(this, listener); + } + /** * {@return the compiler to use for compiling the code}. - * If {@link #fork} is {@code true}, the returned compiler will be a wrapper for the command line. - * Otherwise it will be the compiler identified by {@link #compilerId} if a value was supplied, + * If {@link #fork} is {@code true}, the returned compiler will be a wrapper for a command line. + * Otherwise, it will be the compiler identified by {@link #compilerId} if a value was supplied, * or the standard compiler provided with the Java platform otherwise. * * @throws MojoException if no compiler was found */ - private JavaCompiler compiler() throws MojoException { + public JavaCompiler compiler() throws MojoException { /* * Use the `compilerId` as identifier for toolchains. * I.e, we assume that `compilerId` is also the name of the executable binary. @@ -1090,12 +1137,12 @@ private JavaCompiler compiler() throws MojoException { } /** - * Parses the parameters declared in the MOJO. + * Parses the parameters declared in the MOJO. * * @param compiler the tools to use for verifying the validity of options * @return the options after validation */ - protected Options acceptParameters(final OptionChecker compiler) { + public Options parseParameters(final OptionChecker compiler) { /* * Options to provide to the compiler, excluding all kinds of path (source files, destination directory, * class-path, module-path, etc.). Some options are validated by Maven in addition of being validated by @@ -1104,10 +1151,10 @@ protected Options acceptParameters(final OptionChecker compiler) { * the fully formatted option (e.g. "-g:vars,lines") that we provided to it. */ boolean targetOrReleaseSet; - final var compilerConfiguration = new Options(compiler, logger); - compilerConfiguration.addIfNonBlank("--source", getSource()); - targetOrReleaseSet = compilerConfiguration.addIfNonBlank("--target", getTarget()); - targetOrReleaseSet |= compilerConfiguration.addIfNonBlank("--release", getRelease()); + final var configuration = new Options(compiler, logger); + configuration.addIfNonBlank("--source", getSource()); + targetOrReleaseSet = configuration.addIfNonBlank("--target", getTarget()); + targetOrReleaseSet |= configuration.addIfNonBlank("--release", getRelease()); if (!targetOrReleaseSet && ProjectScope.MAIN.equals(compileScope.projectScope())) { MessageBuilder mb = messageBuilderFactory .builder() @@ -1118,307 +1165,57 @@ protected Options acceptParameters(final OptionChecker compiler) { writePlugin(mb, "release", String.valueOf(Runtime.version().feature())); logger.warn(mb.build()); } - compilerConfiguration.addIfTrue("--enable-preview", enablePreview); - compilerConfiguration.addComaSeparated("-proc", proc, List.of("none", "only", "full"), null); + configuration.addIfTrue("--enable-preview", enablePreview); + configuration.addComaSeparated("-proc", proc, List.of("none", "only", "full"), null); if (annotationProcessors != null) { var list = new StringJoiner(","); for (String p : annotationProcessors) { list.add(p); } - compilerConfiguration.addIfNonBlank("-processor", list.toString()); + configuration.addIfNonBlank("-processor", list.toString()); } - compilerConfiguration.addComaSeparated("-implicit", implicit, List.of("none", "class"), null); - compilerConfiguration.addIfTrue("-parameters", parameters); - compilerConfiguration.addIfTrue("-Xpkginfo:always", createMissingPackageInfoClass); + configuration.addComaSeparated("-implicit", implicit, List.of("none", "class"), null); + configuration.addIfTrue("-parameters", parameters); + configuration.addIfTrue("-Xpkginfo:always", createMissingPackageInfoClass); if (debug) { - compilerConfiguration.addComaSeparated( + configuration.addComaSeparated( "-g", debuglevel, List.of("lines", "vars", "source", "all", "none"), (options) -> Arrays.asList(options).contains("all") ? new String[0] : options); } else { - compilerConfiguration.addIfTrue("-g:none", true); + configuration.addIfTrue("-g:none", true); } - compilerConfiguration.addIfNonBlank("--module-version", moduleVersion); - compilerConfiguration.addIfTrue("-deprecation", showDeprecation); - compilerConfiguration.addIfTrue("-nowarn", !showWarnings); - compilerConfiguration.addIfTrue("-Werror", failOnWarning); - compilerConfiguration.addIfTrue("-verbose", verbose); + configuration.addIfNonBlank("--module-version", moduleVersion); + configuration.addIfTrue("-deprecation", showDeprecation); + configuration.addIfTrue("-nowarn", !showWarnings); + configuration.addIfTrue("-Werror", failOnWarning); + configuration.addIfTrue("-verbose", verbose); if (fork) { - compilerConfiguration.addMemoryValue("-J-Xms", "meminitial", meminitial, SUPPORT_LEGACY); - compilerConfiguration.addMemoryValue("-J-Xmx", "maxmem", maxmem, SUPPORT_LEGACY); + configuration.addMemoryValue("-J-Xms", "meminitial", meminitial, SUPPORT_LEGACY); + configuration.addMemoryValue("-J-Xmx", "maxmem", maxmem, SUPPORT_LEGACY); } - return compilerConfiguration; - } - - /** - * Subdivides a compilation unit into one or more compilation tasks. A compilation unit may, for example, - * compile the source files for a specific Java release in a multi-release project. Normally, such unit maps - * to exactly one compilation task. However, it is sometime useful to split a compilation unit into smaller tasks, - * usually as a workaround for deprecated practices such as overwriting the main {@code module-info} in the tests. - * In the latter case, we need to compile the test {@code module-info} separately, before the other test classes. - */ - CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) { - return new CompilationTaskSources[] {new CompilationTaskSources(unit.files)}; + return configuration; } /** - * Runs the compiler. + * Runs the compiler, then shows the result in the Maven logger. * * @param compiler the compiler - * @param compilerConfiguration options to provide to the compiler + * @param configuration options to provide to the compiler * @throws IOException if an input file cannot be read * @throws MojoException if the compilation failed */ - @SuppressWarnings({"checkstyle:MethodLength", "checkstyle:AvoidNestedBlocks"}) - private void compile(final JavaCompiler compiler, final Options compilerConfiguration) throws IOException { - final EnumSet incAspects; - if (useIncrementalCompilation != null) { - incAspects = useIncrementalCompilation - ? EnumSet.of( - IncrementalBuild.Aspect.SOURCES, - IncrementalBuild.Aspect.ADDITIONS, - IncrementalBuild.Aspect.DEPENDENCIES) - : EnumSet.of(IncrementalBuild.Aspect.CLASSES); - } else { - incAspects = IncrementalBuild.Aspect.parse(incrementalCompilation); - } - /* - * Get the root directories of the Java source files to compile, excluding empty directories. - * The list needs to be modifiable for allowing the addition of generated source directories. - * Then get the list of all source files to compile. - * - * Note that we perform this step after processing compiler arguments, because this block may - * skip the build if there is no source code to compile. We want arguments to be verified first - * in order to warn about possible configuration problems. - */ - List sourceFiles = List.of(); - final Path outputDirectory = Files.createDirectories(getOutputDirectory()); - final List sourceDirectories; - if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { - ProjectScope scope = compileScope.projectScope(); - Stream roots = projectManager.getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY); - sourceDirectories = SourceDirectory.fromProject(roots, outputDirectory); - } else { - sourceDirectories = SourceDirectory.fromPluginConfiguration(compileSourceRoots, outputDirectory); - } - final boolean hasModuleDeclaration; - if (incAspects.contains(IncrementalBuild.Aspect.MODULES)) { - for (SourceDirectory root : sourceDirectories) { - if (root.moduleName == null) { - throw new CompilationFailureException("The value can be \"modules\" " - + "only if all source directories are Java modules."); - } - } - if (!(getIncludes().isEmpty() - && getExcludes().isEmpty() - && getIncrementalExcludes().isEmpty())) { - throw new CompilationFailureException("Include and exclude filters cannot be specified " - + "when is set to \"modules\"."); - } - hasModuleDeclaration = true; - } else { - var filter = new PathFilter(getIncludes(), getExcludes(), getIncrementalExcludes()); - sourceFiles = filter.walkSourceFiles(sourceDirectories); - if (sourceFiles.isEmpty()) { - String message = "No sources to compile."; - try { - Files.delete(outputDirectory); - } catch (DirectoryNotEmptyException e) { - message += " However, the output directory is not empty."; - } - logger.info(message); - return; - } - switch (project.getPackaging().type().id()) { - case Type.CLASSPATH_JAR: - hasModuleDeclaration = false; - break; - case Type.MODULAR_JAR: - hasModuleDeclaration = true; - break; - default: - hasModuleDeclaration = hasModuleDeclaration(sourceDirectories); - break; - } - } - final Set generatedSourceDirectories = addGeneratedSourceDirectory(getGeneratedSourcesDirectory()); - /* - * Get the dependencies. If the module-path contains any file-based dependency - * and this MOJO is compiling the main code, then a warning will be logged. - * - * NOTE: this method assumes that the map and the list values are modifiable. - * This is true with org.apache.maven.impl.DefaultDependencyResolverResult, - * but may not be true in the general case. To be safe, we should perform a deep copy. - * But it would be unnecessary copies in most cases. - */ - final Map> dependencies = resolveDependencies(compilerConfiguration, hasModuleDeclaration); - resolveProcessorPathEntries(dependencies); - addImplicitDependencies(sourceDirectories, dependencies, hasModuleDeclaration); - /* - * Verify if a dependency changed since the build started, or if a source file changed since the last build. - * If there is no change, we can skip the build. If a dependency or the source tree has changed, we may - * conservatively clean before rebuild. - */ - { // For reducing the scope of the Boolean flags. - final boolean checkSources = incAspects.contains(IncrementalBuild.Aspect.SOURCES); - final boolean checkClasses = incAspects.contains(IncrementalBuild.Aspect.CLASSES); - final boolean checkDepends = incAspects.contains(IncrementalBuild.Aspect.DEPENDENCIES); - final boolean checkOptions = incAspects.contains(IncrementalBuild.Aspect.OPTIONS); - final boolean rebuildOnAdd = incAspects.contains(IncrementalBuild.Aspect.ADDITIONS); - if (checkSources | checkClasses | checkDepends | checkOptions) { - final var incrementalBuild = new IncrementalBuild(this, sourceFiles); - String causeOfRebuild = null; - if (checkSources) { - // Should be first, because this method deletes output files of removed sources. - causeOfRebuild = incrementalBuild.inputFileTreeChanges(staleMillis, rebuildOnAdd); - } - if (checkClasses && causeOfRebuild == null) { - causeOfRebuild = incrementalBuild.markNewOrModifiedSources(staleMillis, rebuildOnAdd); - } - if (checkDepends && causeOfRebuild == null) { - if (fileExtensions == null || fileExtensions.isEmpty()) { - fileExtensions = List.of("class", "jar"); - } - causeOfRebuild = incrementalBuild.dependencyChanges(dependencies.values(), fileExtensions); - } - int optionsHash = 0; // Hash code collision may happen, this is a "best effort" only. - if (checkOptions) { - optionsHash = compilerConfiguration.options.hashCode(); - if (causeOfRebuild == null) { - causeOfRebuild = incrementalBuild.optionChanges(optionsHash); - } - } - if (causeOfRebuild != null) { - logger.info(causeOfRebuild); - } else { - sourceFiles = incrementalBuild.getModifiedSources(); - if (IncrementalBuild.isEmptyOrIgnorable(sourceFiles)) { - logger.info("Nothing to compile - all classes are up to date."); - return; - } - } - if (checkSources | checkDepends | checkOptions) { - incrementalBuild.writeCache(optionsHash, checkSources); - } - } - } - if (logger.isDebugEnabled()) { - int n = sourceFiles.size(); - @SuppressWarnings("checkstyle:MagicNumber") - final var sb = - new StringBuilder(n * 40).append("Compiling ").append(n).append(" source files:"); - for (SourceFile file : sourceFiles) { - sb.append(System.lineSeparator()).append(" ").append(file); - } - logger.debug(sb); - } - /* - * If we are compiling the test classes of a modular project, add the `--patch-modules` options. - * Note that those options are handled like dependencies, because they will need to be set using - * the `javax.tools.StandardLocation` API. - */ - if (hasModuleDeclaration) { - addSourceDirectories(dependencies, sourceDirectories); + private void compile(final JavaCompiler compiler, final Options configuration) throws IOException { + final var executor = createExecutor(null); + if (!executor.applyIncrementalBuild(this, configuration)) { + return; } - /* - * Create a `JavaFileManager`, configure all paths (dependencies and sources), then run the compiler. - * The Java file manager has a cache, so it needs to be disposed after the compilation is completed. - * The same `JavaFileManager` may be reused for many compilation units (e.g. multi-releases) before - * disposal in order to reuse its cache. - */ - boolean success = true; Exception failureCause = null; - final var unresolvedPaths = new ArrayList(); final var compilerOutput = new StringWriter(); - final var listener = new DiagnosticLogger(logger, messageBuilderFactory, LOCALE); - try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, charset())) { - /* - * Dispatch all dependencies on the kind of paths determined by `DependencyResolver`: - * class-path, module-path, annotation processor class-path/module-path, etc. - * This configuration will be unchanged for all compilation units. - */ - List patchedOptions = compilerConfiguration.options; // Workaround for JDK-TBD. - for (Map.Entry> entry : dependencies.entrySet()) { - List paths = entry.getValue(); - PathType key = entry.getKey(); // TODO: replace by pattern matching in Java 21. - if (key instanceof JavaPathType type) { - Optional location = type.location(); - if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException. - fileManager.setLocationFromPaths(location.get(), paths); - continue; - } - } else if (key instanceof JavaPathType.Modular type) { - Optional location = type.rawType().location(); - if (location.isPresent()) { - try { - fileManager.setLocationForModule(location.get(), type.moduleName(), paths); - } catch (UnsupportedOperationException e) { // Workaround forJDK-TBD. - if (patchedOptions == compilerConfiguration.options) { - patchedOptions = new ArrayList<>(patchedOptions); - } - patchedOptions.addAll(Arrays.asList(type.option(paths))); - } - continue; - } - } - unresolvedPaths.addAll(paths); - } - if (!unresolvedPaths.isEmpty()) { - var sb = new StringBuilder("Cannot determine where to place the following artifacts:"); - for (Path p : unresolvedPaths) { - sb.append(System.lineSeparator()).append(" - ").append(p); - } - logger.warn(sb); - } - /* - * Configure all paths to source files. Each compilation unit has its own set of sources. - * More than one compilation unit may exist in the case of a multi-releases project. - * Units are compiled in the order of the release version, with base compiled first. - */ - if (!generatedSourceDirectories.isEmpty()) { - fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, generatedSourceDirectories); - } - compile: - for (SourcesForRelease unit : SourcesForRelease.groupByReleaseAndModule(sourceFiles)) { - for (Map.Entry> root : unit.roots.entrySet()) { - String moduleName = root.getKey(); - if (moduleName.isBlank()) { - fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, root.getValue()); - } else { - fileManager.setLocationForModule( - StandardLocation.MODULE_SOURCE_PATH, moduleName, root.getValue()); - } - } - /* - * TODO: for all compilations after the base one, add the base to class-path or module-path. - * TODO: prepend META-INF/version/## to output directory if needed. - */ - fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputDirectory)); - /* - * Compile the source files now. The following loop should be executed exactly once. - * It may be executed twice when compiling test classes overwriting the `module-info`, - * in which case the `module-info` needs to be compiled separately from other classes. - * However, this is a deprecated practice. - */ - JavaCompiler.CompilationTask task; - for (CompilationTaskSources c : toCompilationTasks(unit)) { - Iterable sources = fileManager.getJavaFileObjectsFromPaths(c.files); - task = compiler.getTask(compilerOutput, fileManager, listener, patchedOptions, null, sources); - patchedOptions = compilerConfiguration.options; // Patched options shall be used only once. - success = c.compile(task); - if (!success) { - break compile; - } - } - } - /* - * Post-compilation. - */ - listener.logSummary(); - } catch (UncheckedIOException e) { - success = false; - failureCause = e.getCause(); + boolean success; + try { + success = executor.compile(compiler, configuration, compilerOutput); } catch (Exception e) { success = false; failureCause = e; @@ -1451,7 +1248,7 @@ && getIncrementalExcludes().isEmpty())) { if (!success || logger.isDebugEnabled()) { IOException suppressed = null; try { - writeDebugFile(compilerConfiguration, dependencies, sourceFiles); + writeDebugFile(configuration, executor.dependencies, executor.getSourceFiles()); if (success && tipForCommandLineCompilation != null) { logger.debug(tipForCommandLineCompilation); tipForCommandLineCompilation = null; @@ -1466,9 +1263,11 @@ && getIncrementalExcludes().isEmpty())) { .append(' ') .append(compileScope.projectScope().id()) .append(" classes."); - listener.firstError(failureCause).ifPresent((c) -> message.append(System.lineSeparator()) - .append("The first error is: ") - .append(c)); + if (executor.listener instanceof DiagnosticLogger diagnostic) { + diagnostic.firstError(failureCause).ifPresent((c) -> message.append(System.lineSeparator()) + .append("The first error is: ") + .append(c)); + } var failure = new CompilationFailureException(message.toString(), failureCause); if (suppressed != null) { failure.addSuppressed(suppressed); @@ -1485,16 +1284,12 @@ && getIncrementalExcludes().isEmpty())) { * has been removed because Reproducible Build are enabled by default in Maven now. */ if (!isVersionEqualOrNewer(compiler, "RELEASE_22")) { - Path moduleDescriptor = getOutputDirectory().resolve(MODULE_INFO + CLASS_FILE_SUFFIX); + Path moduleDescriptor = executor.outputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); if (Files.isRegularFile(moduleDescriptor)) { - try { - byte[] oridinal = Files.readAllBytes(moduleDescriptor); - byte[] modified = ByteCodeTransformer.patchJdkModuleVersion(oridinal, getRelease(), logger); - if (modified != null) { - Files.write(moduleDescriptor, modified); - } - } catch (IOException ex) { - throw new MojoException("Error reading or writing " + MODULE_INFO + CLASS_FILE_SUFFIX, ex); + byte[] oridinal = Files.readAllBytes(moduleDescriptor); + byte[] modified = ByteCodeTransformer.patchJdkModuleVersion(oridinal, getRelease(), logger); + if (modified != null) { + Files.write(moduleDescriptor, modified); } } } @@ -1564,17 +1359,15 @@ final String parseModuleInfoName(Path source) throws IOException { } /** - * {@return all dependencies organized by the path types where to place them}. If the module-path contains - * any file-based dependency and this MOJO is compiling the main code, then a warning will be logged. + * {@return all dependencies grouped by the path types where to place them}. If the module-path contains + * any filename-based dependency and this MOJO is compiling the main code, then a warning will be logged. * - * @param compilerConfiguration where to add {@code --add-reads} options when compiling test classes * @param hasModuleDeclaration whether to allow placement of dependencies on the module-path. */ - private Map> resolveDependencies(Options compilerConfiguration, boolean hasModuleDeclaration) - throws IOException { + final DependencyResolverResult resolveDependencies(boolean hasModuleDeclaration) throws IOException { DependencyResolver resolver = session.getService(DependencyResolver.class); if (resolver == null) { // Null value happen during tests, depending on the mock used. - return new LinkedHashMap<>(); // The caller needs a modifiable map. + return null; } var allowedTypes = EnumSet.of(JavaPathType.CLASSES, JavaPathType.PROCESSOR_CLASSES); if (hasModuleDeclaration) { @@ -1617,15 +1410,7 @@ private Map> resolveDependencies(Options compilerConfigurat logger.warn(warning); } } - /* - * Add `--add-reads` options when compiling the test classes. - * Nothing should be changed when compiling the main classes. - */ - if (hasModuleDeclaration) { - addModuleOptions(dependencies, compilerConfiguration); - } - // TODO: to be safe, we should perform a deep clone here. - return dependencies.getDispatchedPaths(); + return dependencies; } /** @@ -1644,7 +1429,7 @@ private Map> resolveDependencies(Options compilerConfigurat * set to {@code proc}, {@code classpath-proc} or {@code modular-proc}. */ @Deprecated(since = "4.0.0") - private void resolveProcessorPathEntries(Map> addTo) throws MojoException { + final void resolveProcessorPathEntries(Map> addTo) throws MojoException { List dependencies = annotationProcessorPaths; if (dependencies != null && !dependencies.isEmpty()) { try { @@ -1681,13 +1466,13 @@ private void resolveProcessorPathEntries(Map> addTo) throws /** * Ensures that the directory for generated sources exists, and adds it to the list of source directories * known to the project manager. This is used for adding the output of annotation processor. - * The given directory should be the result of {@link #getGeneratedSourcesDirectory()}. + * The returned set is either empty or a singleton. * - * @param generatedSourcesDirectory the directory to add, or {@code null} if none * @return the added directory in a singleton set, or an empty set if none * @throws IOException if the directory cannot be created */ - private Set addGeneratedSourceDirectory(Path generatedSourcesDirectory) throws IOException { + final Set addGeneratedSourceDirectory() throws IOException { + Path generatedSourcesDirectory = getGeneratedSourcesDirectory(); if (generatedSourcesDirectory == null) { return Set.of(); } @@ -1767,14 +1552,14 @@ private void writePlugin(MessageBuilder mb, String option, String value) { * If a file name contains embedded spaces, then the whole file name must be between double quotation marks. * The -J options are not supported. * - * @param compilerConfiguration options to provide to the compiler + * @param configuration options to provide to the compiler * @param dependencies the dependencies * @param sourceFiles all files to compile * @throws IOException if an error occurred while writing the debug file */ - private void writeDebugFile( - Options compilerConfiguration, Map> dependencies, List sourceFiles) + private void writeDebugFile(Options configuration, Map> dependencies, Stream sourceFiles) throws IOException { + final Path path = getDebugFilePath(); if (path == null) { logger.warn("The parameter should not be empty."); @@ -1785,7 +1570,7 @@ private void writeDebugFile( .append(" ") .append(executable != null ? executable : compilerId); try (BufferedWriter out = Files.newBufferedWriter(path)) { - compilerConfiguration.format(commandLine, out); + configuration.format(commandLine, out); for (Map.Entry> entry : dependencies.entrySet()) { List files = entry.getValue(); files = files.stream().map(this::relativize).toList(); @@ -1801,11 +1586,19 @@ private void writeDebugFile( out.write(relativize(getOutputDirectory()).toString()); out.write('"'); out.newLine(); - for (SourceFile sf : sourceFiles) { - out.write('"'); - out.write(relativize(sf.file).toString()); - out.write('"'); - out.newLine(); + try { + sourceFiles.forEach((file) -> { + try { + out.write('"'); + out.write(relativize(file).toString()); + out.write('"'); + out.newLine(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); } } tipForCommandLineCompilation = commandLine.append(" @").append(path).toString(); diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java index 0bef3795..597640bc 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java @@ -98,6 +98,8 @@ public class CompilerMojo extends AbstractCompilerMojo { /** * The directory for compiled classes. + * + * @see #getOutputDirectory() */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) protected Path outputDirectory; @@ -137,7 +139,7 @@ public class CompilerMojo extends AbstractCompilerMojo { protected String debugFileName; /** - * Creates a new compiler MOJO. + * Creates a new compiler MOJO for the main code. */ public CompilerMojo() { super(PathScope.MAIN_COMPILE); @@ -145,6 +147,8 @@ public CompilerMojo() { /** * Runs the Java compiler on the main source code. + * If {@link #skipMain} is {@code true}, then this method logs a message and does nothing else. + * Otherwise, this method executes the steps described in the method of the parent class. * * @throws MojoException if the compiler cannot be run. */ @@ -163,18 +167,18 @@ public void execute() throws MojoException { } /** - * Parses the parameters declared in the MOJO. + * Parses the parameters declared in the MOJO. * * @param compiler the tools to use for verifying the validity of options * @return the options after validation */ @Override @SuppressWarnings("deprecation") - protected Options acceptParameters(final OptionChecker compiler) { - Options compilerConfiguration = super.acceptParameters(compiler); - compilerConfiguration.addUnchecked(compilerArgs); - compilerConfiguration.addUnchecked(compilerArgument); - return compilerConfiguration; + public Options parseParameters(final OptionChecker compiler) { + Options configuration = super.parseParameters(compiler); + configuration.addUnchecked(compilerArgs); + configuration.addUnchecked(compilerArgument); + return configuration; } /** @@ -245,7 +249,7 @@ protected String getDebugFileName() { */ @Override @Deprecated(since = "4.0.0") - void addImplicitDependencies( + final void addImplicitDependencies( List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) throws IOException { if (SUPPORT_LEGACY && multiReleaseOutput) { diff --git a/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java b/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java index a917e191..cb2b437b 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java +++ b/src/main/java/org/apache/maven/plugin/compiler/PathFilter.java @@ -136,18 +136,17 @@ final class PathFilter extends SimpleFileVisitor implements PredicateMOJO from which to take the includes/excludes configuration */ - PathFilter(Collection includes, Collection excludes, Collection incrementalExcludes) { - useDefaultInclude = includes.isEmpty(); + PathFilter(AbstractCompilerMojo mojo) { + Collection specified = mojo.getIncludes(); + useDefaultInclude = specified.isEmpty(); if (useDefaultInclude) { - includes = List.of("**"); // Place-holder replaced by "**/*.java" in `test(…)`. + specified = List.of("**"); // Place-holder replaced by "**/*.java" in `test(…)`. } - this.includes = includes.toArray(String[]::new); - this.excludes = excludes.toArray(String[]::new); - this.incrementalExcludes = incrementalExcludes.toArray(String[]::new); + includes = specified.toArray(String[]::new); + excludes = mojo.getExcludes().toArray(String[]::new); + incrementalExcludes = mojo.getIncrementalExcludes().toArray(String[]::new); } /** diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java index 6880f360..25cd7331 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java @@ -41,8 +41,7 @@ */ final class SourcesForRelease implements Closeable { /** - * The release for this set of sources. For this class, the - * {@link SourceVersion#RELEASE_0} value means "no version". + * The release for this set of sources, or {@code null} if the user did not specified a release. * * @see SourceDirectory#release */ @@ -80,12 +79,12 @@ final class SourcesForRelease implements Closeable { /** * Creates an initially empty instance for the given Java release. * - * @param release the release for this set of sources, or {@link SourceVersion#RELEASE_0} for no version. + * @param release the release for this set of sources, or {@code null} if the user did not specified a release */ private SourcesForRelease(SourceVersion release) { this.release = release; - roots = new LinkedHashMap<>(); files = new ArrayList<>(256); + roots = new LinkedHashMap<>(); moduleInfos = new LinkedHashMap<>(); } @@ -103,12 +102,25 @@ private void add(SourceFile source) { if (moduleName == null) { moduleName = ""; } - roots.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<>()).add(directory.root); + add(roots, moduleName, directory.root); directory.getModuleInfo().ifPresent((path) -> moduleInfos.put(directory, null)); } files.add(source.file); } + /** + * Adds the given directory in the given map. + * + * @param target the map where to add the specified directory + * @param moduleName name of the module, or an empty string if none + * @param directory the directory to add, or {@code null} if none + */ + private static void add(Map> target, String moduleName, Path directory) { + if (directory != null) { + target.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<>()).add(directory); + } + } + /** * Groups all sources files first by Java release versions, then by module names. * The elements in the returned collection are sorted in the order of {@link SourceVersion} @@ -117,14 +129,14 @@ private void add(SourceFile source) { * @param sources the sources to group. * @return the given sources grouped by Java release versions and module names. */ - public static Collection groupByReleaseAndModule(List sources) { + static Collection groupByReleaseAndModule(List sources) { var result = new EnumMap(SourceVersion.class); for (SourceFile source : sources) { - SourceVersion release = source.directory.release; - if (release == null) { - release = SourceVersion.RELEASE_0; // No release sub-directory for the compiled classes. - } - result.computeIfAbsent(release, SourcesForRelease::new).add(source); + final SourceVersion release = source.directory.release; + result.computeIfAbsent( + (release != null) ? release : SourceVersion.latest(), + (key) -> new SourcesForRelease(release)) + .add(source); } return result.values(); } @@ -190,6 +202,7 @@ public void close() throws IOException { */ @Override public String toString() { - return getClass().getSimpleName() + '[' + release + ": " + files.size() + " files]"; + return getClass().getSimpleName() + '[' + (release != null ? release : "default") + ": " + files.size() + + " files]"; } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java index 4e6f29f1..7bd3720b 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java @@ -18,22 +18,18 @@ */ package org.apache.maven.plugin.compiler; -import javax.tools.JavaCompiler; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; import javax.tools.OptionChecker; import java.io.IOException; -import java.io.InputStream; -import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.StringJoiner; -import org.apache.maven.api.Dependency; import org.apache.maven.api.JavaPathType; import org.apache.maven.api.PathScope; import org.apache.maven.api.PathType; @@ -42,7 +38,6 @@ import org.apache.maven.api.plugin.MojoException; import org.apache.maven.api.plugin.annotations.Mojo; import org.apache.maven.api.plugin.annotations.Parameter; -import org.apache.maven.api.services.DependencyResolverResult; import org.apache.maven.api.services.MessageBuilder; import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX; @@ -173,6 +168,7 @@ public class TestCompilerMojo extends AbstractCompilerMojo { * See the {@link CompilerMojo#outputDirectory} for more information. * * @see CompilerMojo#outputDirectory + * @see #getOutputDirectory() */ @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) protected Path outputDirectory; @@ -183,7 +179,6 @@ public class TestCompilerMojo extends AbstractCompilerMojo { * Its value should be the same as {@link CompilerMojo#outputDirectory}. * * @see CompilerMojo#outputDirectory - * @see #addImplicitDependencies(List, Map, boolean) */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) protected Path mainOutputDirectory; @@ -201,28 +196,23 @@ public class TestCompilerMojo extends AbstractCompilerMojo { @Parameter(defaultValue = "true") protected boolean useModulePath = true; - /** - * Name of the main module to compile, or {@code null} if not yet determined. - * If the project is not modular, an empty string. - * - * TODO: use "*" as a sentinel value for modular source hierarchy. - * - * @see #getMainModuleName() - */ - private String moduleName; - /** * Whether a {@code module-info.java} file is defined in the test sources. * In such case, it has precedence over the {@code module-info.java} in main sources. * This is defined for compatibility with Maven 3, but not recommended. + * + *

This field exists in this class only for transferring this information + * to {@link ToolExecutorForTest#hasTestModuleInfo}, which is the class that + * needs this information.

*/ - private boolean hasTestModuleInfo; + transient boolean hasTestModuleInfo; /** - * Whether the {@code module-info} of the tests overwrites the main {@code module-info}. - * This is a deprecated practice, but is accepted if {@link #SUPPORT_LEGACY} is true. + * Path to the {@code module-info.class} file of the main code, or {@code null} if that file does not exist. + * This field exists only for transferring this information to {@link ToolExecutorForTest#hasTestModuleInfo}, + * and should be {@code null} the rest of the time. */ - private boolean overwriteMainModuleInfo; + transient Path mainModulePath; /** * The file where to dump the command-line when debug is activated or when the compilation failed. @@ -237,7 +227,7 @@ public class TestCompilerMojo extends AbstractCompilerMojo { protected String debugFileName; /** - * Creates a new test compiler MOJO. + * Creates a new compiler MOJO for the tests. */ public TestCompilerMojo() { super(PathScope.TEST_COMPILE); @@ -245,6 +235,8 @@ public TestCompilerMojo() { /** * Runs the Java compiler on the test source code. + * If {@link #skip} is {@code true}, then this method logs a message and does nothing else. + * Otherwise, this method executes the steps described in the method of the parent class. * * @throws MojoException if the compiler cannot be run. */ @@ -258,24 +250,24 @@ public void execute() throws MojoException { } /** - * Parses the parameters declared in the MOJO. + * Parses the parameters declared in the MOJO. * * @param compiler the tools to use for verifying the validity of options * @return the options after validation */ @Override @SuppressWarnings("deprecation") - protected Options acceptParameters(final OptionChecker compiler) { - Options compilerConfiguration = super.acceptParameters(compiler); - compilerConfiguration.addUnchecked( + public Options parseParameters(final OptionChecker compiler) { + Options configuration = super.parseParameters(compiler); + configuration.addUnchecked( testCompilerArgs == null || testCompilerArgs.isEmpty() ? compilerArgs : testCompilerArgs); if (testCompilerArguments != null) { for (Map.Entry entry : testCompilerArguments.entrySet()) { - compilerConfiguration.addUnchecked(List.of(entry.getKey(), entry.getValue())); + configuration.addUnchecked(List.of(entry.getKey(), entry.getValue())); } } - compilerConfiguration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument); - return compilerConfiguration; + configuration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument); + return configuration; } /** @@ -365,33 +357,13 @@ protected String getDebugFileName() { return debugFileName; } - /** - * {@return the module name of the main code, or an empty string if none}. - * This method reads the module descriptor when first needed and caches the result. - * - * @throws IOException if the module descriptor cannot be read. - */ - private String getMainModuleName() throws IOException { - if (moduleName == null) { - Path file = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); - if (Files.isRegularFile(file)) { - try (InputStream in = Files.newInputStream(file)) { - moduleName = ModuleDescriptor.read(in).name(); - } - } else { - moduleName = ""; - } - } - return moduleName; - } - /** * {@return the module name declared in the test sources}. We have to parse the source instead * of the {@code module-info.class} file because the classes may not have been compiled yet. * This is not very reliable, but putting a {@code module-info.java} file in the tests is * deprecated anyway. */ - private String getTestModuleName(List compileSourceRoots) throws IOException { + final String getTestModuleName(List compileSourceRoots) throws IOException { for (SourceDirectory directory : compileSourceRoots) { if (directory.moduleName != null) { return directory.moduleName; @@ -406,14 +378,18 @@ private String getTestModuleName(List compileSourceRoots) throw /** * {@return whether the project has at least one {@code module-info.class} file}. - * This method opportunistically fetches the module name. * * @param roots root directories of the sources to compile * @throws IOException if this method needed to read a module descriptor and failed */ @Override final boolean hasModuleDeclaration(final List roots) throws IOException { - hasTestModuleInfo = super.hasModuleDeclaration(roots); + for (SourceDirectory root : roots) { + if (root.getModuleInfo().isPresent()) { + hasTestModuleInfo = true; + break; + } + } if (hasTestModuleInfo) { MessageBuilder message = messageBuilderFactory.builder(); message.a("Overwriting the ") @@ -428,7 +404,7 @@ final boolean hasModuleDeclaration(final List roots) throws IOE return useModulePath; } } - return useModulePath && !getMainModuleName().isEmpty(); + return useModulePath && mainModulePath != null; } /** @@ -439,7 +415,7 @@ final boolean hasModuleDeclaration(final List roots) throws IOE * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file */ @Override - void addImplicitDependencies( + final void addImplicitDependencies( List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) { var pathType = hasModuleDeclaration ? JavaPathType.MODULES : JavaPathType.CLASSES; if (Files.exists(mainOutputDirectory)) { @@ -448,150 +424,25 @@ void addImplicitDependencies( } /** - * Adds {@code --patch-module} options for the given source directories. - * In this case, the option values are directory of source files. - * Not to be confused with cases where a module is patched with compiled - * classes (it may happen in other parts of the compiler plugin). - * - * @param addTo the collection of source paths to augment - * @param sourceDirectories the source paths to eventually adds to the {@code toAdd} map - * @throws IOException if this method needs to read a module descriptor and this operation failed - */ - @Override - final void addSourceDirectories(Map> addTo, List sourceDirectories) - throws IOException { - for (SourceDirectory dir : sourceDirectories) { - String moduleToPatch = dir.moduleName; - if (moduleToPatch == null) { - moduleToPatch = getMainModuleName(); - if (moduleToPatch.isEmpty()) { - continue; // No module-info found. - } - if (SUPPORT_LEGACY) { - String testModuleName = getTestModuleName(sourceDirectories); - if (testModuleName != null) { - overwriteMainModuleInfo = testModuleName.equals(getMainModuleName()); - if (!overwriteMainModuleInfo) { - continue; // The test classes are in their own module. - } - } - } - } - addTo.computeIfAbsent(JavaPathType.patchModule(moduleToPatch), (key) -> new ArrayList<>()) - .add(dir.root); - } - } - - /** - * Generates the {@code --add-modules} and {@code --add-reads} options for the dependencies that are not - * in the main compilation. This method is invoked only if {@code hasModuleDeclaration} is {@code true}. + * Creates a new task for compiling the test classes. * - * @param dependencies the project dependencies - * @param addTo where to add the options - * @throws IOException if the module information of a dependency cannot be read - */ - @Override - @SuppressWarnings({"checkstyle:MissingSwitchDefault", "fallthrough"}) - protected void addModuleOptions(DependencyResolverResult dependencies, Options addTo) throws IOException { - if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo) { - /* - * Do not add any `--add-reads` parameters. The developers should put - * everything needed in the `module-info`, including test dependencies. - */ - return; - } - final var done = new HashSet(); // Added modules and their dependencies. - final var addModules = new StringJoiner(","); - StringJoiner addReads = null; - boolean hasUnnamed = false; - for (Map.Entry entry : dependencies.getDependencies().entrySet()) { - boolean compile = false; - switch (entry.getKey().getScope()) { - case TEST: - case TEST_ONLY: - compile = true; - // Fall through - case TEST_RUNTIME: - if (compile) { - // Needs to be initialized even if `name` is null. - if (addReads == null) { - addReads = new StringJoiner(",", getMainModuleName() + "=", ""); - } - } - Path path = entry.getValue(); - String name = dependencies.getModuleName(path).orElse(null); - if (name == null) { - hasUnnamed = true; - } else if (done.add(name)) { - addModules.add(name); - if (compile) { - addReads.add(name); - } - /* - * For making the options simpler, we do not add `--add-modules` or `--add-reads` - * options for modules that are required by a module that we already added. This - * simplification is not necessary, but makes the command-line easier to read. - */ - dependencies.getModuleDescriptor(path).ifPresent((descriptor) -> { - for (ModuleDescriptor.Requires r : descriptor.requires()) { - done.add(r.name()); - } - }); - } - break; - } - } - if (!done.isEmpty()) { - addTo.addIfNonBlank("--add-modules", addModules.toString()); - } - if (addReads != null) { - if (hasUnnamed) { - addReads.add("ALL-UNNAMED"); - } - addTo.addIfNonBlank("--add-reads", addReads.toString()); - } - } - - /** - * Separates the compilation of {@code module-info} from other classes. This is needed when the - * {@code module-info} of the test classes overwrite the {@code module-info} of the main classes. - * In the latter case, we need to compile the test {@code module-info} first in order to substitute - * the main module-info by the test one before to compile the remaining test classes. + * @param listener where to send compilation warnings, or {@code null} for the Maven logger + * @throws MojoException if this method identifies an invalid parameter in this MOJO + * @return the task to execute for compiling the tests using the configuration in this MOJO + * @throws IOException if an error occurred while creating the output directory or scanning the source directories */ @Override - final CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) { - if (!(SUPPORT_LEGACY && useModulePath && hasTestModuleInfo && overwriteMainModuleInfo)) { - return super.toCompilationTasks(unit); - } - CompilationTaskSources moduleInfo = null; - final List files = unit.files; - for (int i = files.size(); --i >= 0; ) { - if (SourceDirectory.isModuleInfoSource(files.get(i))) { - moduleInfo = new CompilationTaskSources(List.of(files.remove(i))); - if (files.isEmpty()) { - return new CompilationTaskSources[] {moduleInfo}; - } - break; - } - } - var task = new CompilationTaskSources(files) { - /** - * Substitutes the main {@code module-info.class} by the test's one, compiles test classes, - * then restores the original {@code module-info.class}. The test {@code module-info.class} - * must have been compiled separately before this method is invoked. - */ - @Override - boolean compile(JavaCompiler.CompilationTask task) throws IOException { - try (unit) { - unit.substituteModuleInfos(mainOutputDirectory, outputDirectory); - return super.compile(task); - } + public ToolExecutor createExecutor(DiagnosticListener listener) throws IOException { + try { + Path file = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); + if (Files.isRegularFile(file)) { + mainModulePath = file; } - }; - if (moduleInfo != null) { - return new CompilationTaskSources[] {moduleInfo, task}; - } else { - return new CompilationTaskSources[] {task}; + return new ToolExecutorForTest(this, listener); + } finally { + // Reset the fields that were used only for transfering information to `ToolExecutorForTest`. + hasTestModuleInfo = false; + mainModulePath = null; } } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java new file mode 100644 index 00000000..4084cd90 --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -0,0 +1,447 @@ +/* + * 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 javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.PathType; +import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.services.DependencyResolverResult; + +/** + * A task which configures and executes a Java tool such as the Java compiler. + * This class takes a snapshot of the information provided in the MOJO. + * Then, it collects additional information such as the source files and the dependencies. + * The set of source files to compile can optionally be filtered for keeping only the files + * that changed since the last build with the {@linkplain #applyIncrementalBuild incremental build}. + * + *

Thread safety

+ * This class is not thread-safe. However, it is independent of the {@link AbstractCompilerMojo} instance + * given in argument to the constructor and to the {@linkplain #applyIncrementalBuild incremental build}. + * After all methods with an {@link AbstractCompilerMojo} argument have been invoked, {@code ToolExecutor} + * can safety be used in a background thread for launching the compilation (but must still be used by only + * only thread at a time). + * + * @author Martin Desruisseaux + */ +public class ToolExecutor { + /** + * The locale for diagnostics, or {@code null} for the platform default. + */ + private static final Locale LOCALE = null; + + /** + * The character encoding of source files, or {@code null} for the platform default encoding. + * + * @see AbstractCompilerMojo#encoding + */ + protected final Charset encoding; + + /** + * The root directories of the Java source files to compile, excluding empty directories. + * The list needs to be modifiable for allowing the addition of generated source directories. + * + * @see AbstractCompilerMojo#compileSourceRoots + */ + final List sourceDirectories; + + /** + * The directories where to write generated source files. + * This set is either empty or a singleton. + * + * @see AbstractCompilerMojo#proc + * @see StandardLocation#SOURCE_OUTPUT + */ + protected final Set generatedSourceDirectories; + + /** + * All source files to compile. May include files for many Java modules and many Java releases. + * When the compilation will be executed, those files will be grouped in compilation units where + * each unit will be the source files for one particular Java release. + * + * @see StandardLocation#SOURCE_PATH + * @see StandardLocation#MODULE_SOURCE_PATH + */ + private List sourceFiles; + + /** + * Whether the project contains or is assumed to contain a {@code module-info.java} file. + * If the user specified explicitly whether the project is a modular or a classpath JAR, + * then this flag is set to the user's specification without verification. + * Otherwise, this flag is determined by scanning the list of source files. + */ + protected final boolean hasModuleDeclaration; + + /** + * The result of resolving the dependencies, or {@code null} if not available or not needed. + * For example, this field may be null if the constructor found no file to compile, + * so there is no need to fetch dependencies. + */ + final DependencyResolverResult dependencyResolution; + + /** + * All dependencies grouped by the path types where to place them. + * The path type can be the class-path, module-path, annotation processor path, etc. + */ + protected final Map> dependencies; + + /** + * The destination directory (or class output directory) for class files. + * This directory will be given to the {@code -d} Java compiler option + * when compiling the classes for the base Java release. + * + * @see AbstractCompilerMojo#getOutputDirectory() + */ + protected final Path outputDirectory; + + /** + * Configuration of the incremental compilation. + * + * @see AbstractCompilerMojo#incrementalCompilation + * @see AbstractCompilerMojo#useIncrementalCompilation + */ + private final EnumSet incrementalCompilation; + + /** + * Where to send the compilation warning (never {@code null}). If a null value was specified + * to the constructor, then this listener sends the warnings to the Maven {@linkplain #logger}. + */ + protected final DiagnosticListener listener; + + /** + * The Maven logger for reporting information or warnings to the user. + * Used for messages emitted directly by the Maven compiler plugin. + * Not necessarily used for messages emitted by the Java compiler. + * + * @see AbstractCompilerMojo#logger + */ + protected final Log logger; + + /** + * Creates a new task by taking a snapshot of the current configuration of the given MOJO. + * This constructor creates the {@linkplain #outputDirectory output directory} if it does not already exist. + * + * @param mojo the MOJO from which to take a snapshot + * @param listener where to send compilation warnings, or {@code null} for the Maven logger + * @throws MojoException if this constructor identifies an invalid parameter in the MOJO + * @throws IOException if an error occurred while creating the output directory or scanning the source directories + * + * @see AbstractCompilerMojo#createExecutor(DiagnosticListener) + */ + @SuppressWarnings("deprecation") + protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener listener) + throws IOException { + + logger = mojo.logger; + if (listener == null) { + listener = new DiagnosticLogger(logger, mojo.messageBuilderFactory, LOCALE); + } + this.listener = listener; + encoding = mojo.charset(); + incrementalCompilation = mojo.incrementalCompilationConfiguration(); + outputDirectory = Files.createDirectories(mojo.getOutputDirectory()); + sourceDirectories = mojo.getSourceDirectories(outputDirectory); + /* + * Get the source files and whether they include or are assumed to include `module-info.java`. + * Note that we perform this step after processing compiler arguments, because this block may + * skip the build if there is no source code to compile. We want arguments to be verified first + * in order to warn about possible configuration problems. + */ + if (incrementalCompilation.contains(IncrementalBuild.Aspect.MODULES)) { + boolean hasNoFileMatchers = mojo.hasNoFileMatchers(); + for (SourceDirectory root : sourceDirectories) { + if (root.moduleName == null) { + throw new CompilationFailureException("The value can be \"modules\" " + + "only if all source directories are Java modules."); + } + hasNoFileMatchers &= root.includes.isEmpty() && root.excludes.isEmpty(); + } + if (!hasNoFileMatchers) { + throw new CompilationFailureException("Include and exclude filters cannot be specified " + + "when is set to \"modules\"."); + } + hasModuleDeclaration = true; + sourceFiles = List.of(); + } else { + // The order of the two next lines matter for initialization of `SourceDirectory.moduleInfo`. + sourceFiles = new PathFilter(mojo).walkSourceFiles(sourceDirectories); + hasModuleDeclaration = mojo.hasModuleDeclaration(sourceDirectories); + if (sourceFiles.isEmpty()) { + generatedSourceDirectories = Set.of(); + dependencyResolution = null; + dependencies = Map.of(); + return; + } + } + generatedSourceDirectories = mojo.addGeneratedSourceDirectory(); + /* + * Get the dependencies. If the module-path contains any automatic (filename-based) + * dependency and the MOJO is compiling the main code, then a warning will be logged. + * + * NOTE: this code assumes that the map and the list values are modifiable. + * This is true with `org.apache.maven.impl.DefaultDependencyResolverResult`, + * but may not be true in the general case. To be safe, we should perform a deep copy. + * But it would be unnecessary copies in most cases. + */ + dependencyResolution = mojo.resolveDependencies(hasModuleDeclaration); + dependencies = (dependencyResolution != null) + ? dependencyResolution.getDispatchedPaths() // TODO: deep clone here if we want to be safe. + : new LinkedHashMap<>(); + mojo.resolveProcessorPathEntries(dependencies); + mojo.addImplicitDependencies(sourceDirectories, dependencies, hasModuleDeclaration); + } + + /** + * {@return the source files to compile}. + */ + public Stream getSourceFiles() { + return sourceFiles.stream().map((s) -> s.file); + } + + /** + * Filters the source files to recompile, or cleans the output directory if everything should be rebuilt. + * If the directory structure of the source files has changed since the last build, + * or if a compiler option changed, or if a dependency changed, + * then this method keeps all source files and cleans the {@linkplain #outputDirectory output directory}. + * Otherwise, the source files that did not changed since the last build are removed from the list of sources + * to compile. If all source files have been removed, then this method returns {@code false} for notifying the + * caller that it can skip the build. + * + *

If this method is invoked many times, all invocations after this first one have no effect.

+ * + * @param mojo the MOJO from which to take the incremental build configuration + * @param configuration the options which should match the options used during the last build + * @throws IOException if an error occurred while accessing the cache file or walking through the directory tree + * @return whether there is at least one file to recompile + */ + public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Options configuration) + throws IOException { + final boolean checkSources = incrementalCompilation.contains(IncrementalBuild.Aspect.SOURCES); + final boolean checkClasses = incrementalCompilation.contains(IncrementalBuild.Aspect.CLASSES); + final boolean checkDepends = incrementalCompilation.contains(IncrementalBuild.Aspect.DEPENDENCIES); + final boolean checkOptions = incrementalCompilation.contains(IncrementalBuild.Aspect.OPTIONS); + final boolean rebuildOnAdd = incrementalCompilation.contains(IncrementalBuild.Aspect.ADDITIONS); + incrementalCompilation.clear(); // Prevent this method to be executed twice. + if (checkSources | checkClasses | checkDepends | checkOptions) { + final var incrementalBuild = new IncrementalBuild(mojo, sourceFiles); + String causeOfRebuild = null; + if (checkSources) { + // Should be first, because this method deletes output files of removed sources. + causeOfRebuild = incrementalBuild.inputFileTreeChanges(mojo.staleMillis, rebuildOnAdd); + } + if (checkClasses && causeOfRebuild == null) { + causeOfRebuild = incrementalBuild.markNewOrModifiedSources(mojo.staleMillis, rebuildOnAdd); + } + if (checkDepends && causeOfRebuild == null) { + List fileExtensions = mojo.fileExtensions; + if (fileExtensions == null || fileExtensions.isEmpty()) { + fileExtensions = List.of("class", "jar"); + } + causeOfRebuild = incrementalBuild.dependencyChanges(dependencies.values(), fileExtensions); + } + int optionsHash = 0; // Hash code collision may happen, this is a "best effort" only. + if (checkOptions) { + optionsHash = configuration.options.hashCode(); + if (causeOfRebuild == null) { + causeOfRebuild = incrementalBuild.optionChanges(optionsHash); + } + } + if (causeOfRebuild != null) { + logger.info(causeOfRebuild); + } else { + sourceFiles = incrementalBuild.getModifiedSources(); + if (IncrementalBuild.isEmptyOrIgnorable(sourceFiles)) { + logger.info("Nothing to compile - all classes are up to date."); + sourceFiles = List.of(); + return false; + } + } + if (checkSources | checkDepends | checkOptions) { + incrementalBuild.writeCache(optionsHash, checkSources); + } + } + return true; + } + + /** + * Runs the compilation task. + * + * @param compiler the compiler + * @param configuration the options to give to the Java compiler + * @param otherOutput where to write additional output from the compiler + * @return whether the compilation succeeded + * @throws IOException if an error occurred while reading or writing a file + * @throws MojoException if the compilation failed for a reason identified by this method + * @throws RuntimeException if any other kind of error occurred + */ + public boolean compile(final JavaCompiler compiler, final Options configuration, final Writer otherOutput) + throws IOException { + + if (sourceFiles.isEmpty()) { + String message = "No sources to compile."; + try { + Files.delete(outputDirectory); + } catch (DirectoryNotEmptyException e) { + message += " However, the output directory is not empty."; + } + logger.info(message); + return true; + } + if (logger.isDebugEnabled()) { + int n = sourceFiles.size(); + @SuppressWarnings("checkstyle:MagicNumber") + var sb = new StringBuilder(n * 40).append("Compiling ").append(n).append(" source files:"); + for (SourceFile file : sourceFiles) { + sb.append(System.lineSeparator()).append(" ").append(file); + } + logger.debug(sb); + } + /* + * Create a `JavaFileManager`, configure all paths (dependencies and sources), then run the compiler. + * The Java file manager has a cache, so it needs to be disposed after the compilation is completed. + * The same `JavaFileManager` may be reused for many compilation units (e.g. multi-releases) before + * disposal in order to reuse its cache. + */ + boolean success = true; + final var unresolvedPaths = new ArrayList(); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding)) { + /* + * Dispatch all dependencies on the kind of paths determined by `DependencyResolver`: + * class-path, module-path, annotation processor class-path/module-path, etc. + * This configuration will be unchanged for all compilation units. + */ + List patchedOptions = configuration.options; // Workaround for JDK-TBD. + for (Map.Entry> entry : dependencies.entrySet()) { + List paths = entry.getValue(); + PathType key = entry.getKey(); // TODO: replace by pattern matching in Java 21. + if (key instanceof JavaPathType type) { + Optional location = type.location(); + if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException. + fileManager.setLocationFromPaths(location.get(), paths); + continue; + } + } else if (key instanceof JavaPathType.Modular type) { + Optional location = type.rawType().location(); + if (location.isPresent()) { + try { + fileManager.setLocationForModule(location.get(), type.moduleName(), paths); + } catch (UnsupportedOperationException e) { // Workaround forJDK-TBD. + if (patchedOptions == configuration.options) { + patchedOptions = new ArrayList<>(patchedOptions); + } + patchedOptions.addAll(Arrays.asList(type.option(paths))); + } + continue; + } + } + unresolvedPaths.addAll(paths); + } + if (!unresolvedPaths.isEmpty()) { + var sb = new StringBuilder("Cannot determine where to place the following artifacts:"); + for (Path p : unresolvedPaths) { + sb.append(System.lineSeparator()).append(" - ").append(p); + } + logger.warn(sb); + } + if (!generatedSourceDirectories.isEmpty()) { + fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, generatedSourceDirectories); + } + /* + * More than one compilation unit may exist in the case of a multi-releases project. + * Units are compiled in the order of the release version, with base compiled first. + */ + compile: + for (SourcesForRelease unit : SourcesForRelease.groupByReleaseAndModule(sourceFiles)) { + for (Map.Entry> root : unit.roots.entrySet()) { + String moduleName = root.getKey(); + if (moduleName.isBlank()) { + fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, root.getValue()); + } else { + fileManager.setLocationForModule( + StandardLocation.MODULE_SOURCE_PATH, moduleName, root.getValue()); + } + } + /* + * TODO: for all compilations after the base one, add the base to class-path or module-path. + * TODO: prepend META-INF/version/## to output directory if needed. + */ + fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputDirectory)); + /* + * Compile the source files now. The following loop should be executed exactly once. + * It may be executed twice when compiling test classes overwriting the `module-info`, + * in which case the `module-info` needs to be compiled separately from other classes. + * However, this is a deprecated practice. + */ + JavaCompiler.CompilationTask task; + for (CompilationTaskSources c : toCompilationTasks(unit)) { + Iterable sources = fileManager.getJavaFileObjectsFromPaths(c.files); + task = compiler.getTask(otherOutput, fileManager, listener, patchedOptions, null, sources); + patchedOptions = configuration.options; // Patched options shall be used only once. + success = c.compile(task); + if (!success) { + break compile; + } + } + } + /* + * Post-compilation. + */ + if (listener instanceof DiagnosticLogger diagnostic) { + diagnostic.logSummary(); + } + } catch (UncheckedIOException e) { + throw e.getCause(); + } + return success; + } + + /** + * Subdivides a compilation unit into one or more compilation tasks. + * This is a workaround for deprecated practices such as overwriting the main {@code module-info} in the tests. + * In the latter case, we need to compile the test {@code module-info} separately, before the other test classes. + */ + CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) { + return new CompilationTaskSources[] {new CompilationTaskSources(unit.files)}; + } +} diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java new file mode 100644 index 00000000..8af22a78 --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java @@ -0,0 +1,304 @@ +/* + * 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 javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.lang.module.ModuleDescriptor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +import org.apache.maven.api.Dependency; +import org.apache.maven.api.JavaPathType; + +import static org.apache.maven.plugin.compiler.AbstractCompilerMojo.SUPPORT_LEGACY; + +/** + * A task which configures and executes the Java compiler for the test classes. + * This executor contains additional configurations compared to the base class. + * + * @author Martin Desruisseaux + */ +class ToolExecutorForTest extends ToolExecutor { + /** + * The output directory of the main classes. + * This directory will be added to the class-path or module-path. + * + * @see TestCompilerMojo#mainOutputDirectory + */ + protected final Path mainOutputDirectory; + + /** + * Path to the {@code module-info.class} file of the main code, or {@code null} if that file does not exist. + */ + private final Path mainModulePath; + + /** + * Whether to place the main classes on the module path when {@code module-info} is present. + * The default and recommended value is {@code true}. The user may force to {@code false}, + * in which case the main classes are placed on the class path, but this is deprecated. + * This flag may be removed in a future version if we remove support of this practice. + * + * @see TestCompilerMojo#useModulePath + */ + private final boolean useModulePath; + + /** + * Whether a {@code module-info.java} file is defined in the test sources. + * In such case, it has precedence over the {@code module-info.java} in main sources. + * This is defined for compatibility with Maven 3, but not recommended. + */ + private final boolean hasTestModuleInfo; + + /** + * Whether the {@code module-info} of the tests overwrites the main {@code module-info}. + * This is a deprecated practice, but is accepted if {@link #SUPPORT_LEGACY} is true. + */ + private boolean overwriteMainModuleInfo; + + /** + * Name of the main module to compile, or {@code null} if not yet determined. + * If the project is not modular, hen this field contains an empty string. + * + * TODO: use "*" as a sentinel value for modular source hierarchy. + * + * @see #getMainModuleName() + */ + private String moduleName; + + /** + * Whether {@link #addModuleOptions(Options)} has already been invoked. + * The options shall be completed only once, otherwise conflicts may occur. + */ + private boolean addedModuleOptions; + + /** + * Creates a new task by taking a snapshot of the current configuration of the given MOJO. + * This constructor creates the {@linkplain #outputDirectory output directory} if it does not already exist. + * + * @param mojo the MOJO from which to take a snapshot + * @param listener where to send compilation warnings, or {@code null} for the Maven logger + * @throws MojoException if this constructor identifies an invalid parameter in the MOJO + * @throws IOException if an error occurred while creating the output directory or scanning the source directories + */ + @SuppressWarnings("deprecation") + ToolExecutorForTest(TestCompilerMojo mojo, DiagnosticListener listener) throws IOException { + super(mojo, listener); + mainOutputDirectory = mojo.mainOutputDirectory; + mainModulePath = mojo.mainModulePath; + useModulePath = mojo.useModulePath; + hasTestModuleInfo = mojo.hasTestModuleInfo; + /* + * If we are compiling the test classes of a modular project, add the `--patch-modules` options. + * In this case, the option values are directory of source files, not to be confused with cases + * where a module is patched with compiled classes. + * + * Note that those options are handled like dependencies, + * because they will need to be set using the `javax.tools.StandardLocation` API. + */ + for (SourceDirectory dir : sourceDirectories) { + String moduleToPatch = dir.moduleName; + if (moduleToPatch == null) { + moduleToPatch = getMainModuleName(); + if (moduleToPatch.isEmpty()) { + continue; // No module-info found. + } + if (SUPPORT_LEGACY) { + String testModuleName = mojo.getTestModuleName(sourceDirectories); + if (testModuleName != null) { + overwriteMainModuleInfo = testModuleName.equals(getMainModuleName()); + if (!overwriteMainModuleInfo) { + continue; // The test classes are in their own module. + } + } + } + } + dependencies + .computeIfAbsent(JavaPathType.patchModule(moduleToPatch), (key) -> new ArrayList<>()) + .add(dir.root); + } + } + + /** + * {@return the module name of the main code, or an empty string if none}. + * This method reads the module descriptor when first needed and caches the result. + * + * @throws IOException if the module descriptor cannot be read. + */ + private String getMainModuleName() throws IOException { + if (moduleName == null) { + if (mainModulePath != null) { + try (InputStream in = Files.newInputStream(mainModulePath)) { + moduleName = ModuleDescriptor.read(in).name(); + } + } else { + moduleName = ""; + } + } + return moduleName; + } + + /** + * Generates the {@code --add-modules} and {@code --add-reads} options for the dependencies that are not + * in the main compilation. This method is invoked only if {@code hasModuleDeclaration} is {@code true}. + * + * @param dependencyResolution the project dependencies + * @param configuration where to add the options + * @throws IOException if the module information of a dependency cannot be read + */ + @SuppressWarnings({"checkstyle:MissingSwitchDefault", "fallthrough"}) + private void addModuleOptions(final Options configuration) throws IOException { + if (addedModuleOptions) { + return; + } + addedModuleOptions = true; + if (!hasModuleDeclaration || dependencyResolution == null) { + return; + } + if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo) { + /* + * Do not add any `--add-reads` parameters. The developers should put + * everything needed in the `module-info`, including test dependencies. + */ + return; + } + final var done = new HashSet(); // Added modules and their dependencies. + final var addModules = new StringJoiner(","); + StringJoiner addReads = null; + boolean hasUnnamed = false; + for (Map.Entry entry : + dependencyResolution.getDependencies().entrySet()) { + boolean compile = false; + switch (entry.getKey().getScope()) { + case TEST: + case TEST_ONLY: + compile = true; + // Fall through + case TEST_RUNTIME: + if (compile) { + // Needs to be initialized even if `name` is null. + if (addReads == null) { + addReads = new StringJoiner(",", getMainModuleName() + "=", ""); + } + } + Path path = entry.getValue(); + String name = dependencyResolution.getModuleName(path).orElse(null); + if (name == null) { + hasUnnamed = true; + } else if (done.add(name)) { + addModules.add(name); + if (compile) { + addReads.add(name); + } + /* + * For making the options simpler, we do not add `--add-modules` or `--add-reads` + * options for modules that are required by a module that we already added. This + * simplification is not necessary, but makes the command-line easier to read. + */ + dependencyResolution.getModuleDescriptor(path).ifPresent((descriptor) -> { + for (ModuleDescriptor.Requires r : descriptor.requires()) { + done.add(r.name()); + } + }); + } + break; + } + } + if (!done.isEmpty()) { + configuration.addIfNonBlank("--add-modules", addModules.toString()); + } + if (addReads != null) { + if (hasUnnamed) { + addReads.add("ALL-UNNAMED"); + } + configuration.addIfNonBlank("--add-reads", addReads.toString()); + } + } + + /** + * @hidden + */ + @Override + public boolean applyIncrementalBuild(AbstractCompilerMojo mojo, Options configuration) throws IOException { + addModuleOptions(configuration); // Effective only once. + return super.applyIncrementalBuild(mojo, configuration); + } + + /** + * @hidden + */ + @Override + public boolean compile(JavaCompiler compiler, Options configuration, Writer otherOutput) throws IOException { + addModuleOptions(configuration); // Effective only once. + return super.compile(compiler, configuration, otherOutput); + } + + /** + * Separates the compilation of {@code module-info} from other classes. This is needed when the + * {@code module-info} of the test classes overwrite the {@code module-info} of the main classes. + * In the latter case, we need to compile the test {@code module-info} first in order to substitute + * the main module-info by the test one before to compile the remaining test classes. + */ + @Override + final CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) { + if (!(SUPPORT_LEGACY && useModulePath && hasTestModuleInfo && overwriteMainModuleInfo)) { + return super.toCompilationTasks(unit); + } + CompilationTaskSources moduleInfo = null; + final List files = unit.files; + for (int i = files.size(); --i >= 0; ) { + if (SourceDirectory.isModuleInfoSource(files.get(i))) { + moduleInfo = new CompilationTaskSources(List.of(files.remove(i))); + if (files.isEmpty()) { + return new CompilationTaskSources[] {moduleInfo}; + } + break; + } + } + var task = new CompilationTaskSources(files) { + /** + * Substitutes the main {@code module-info.class} by the test's one, compiles test classes, + * then restores the original {@code module-info.class}. The test {@code module-info.class} + * must have been compiled separately before this method is invoked. + */ + @Override + boolean compile(JavaCompiler.CompilationTask task) throws IOException { + try (unit) { + unit.substituteModuleInfos(mainOutputDirectory, outputDirectory); + return super.compile(task); + } + } + }; + if (moduleInfo != null) { + return new CompilationTaskSources[] {moduleInfo, task}; + } else { + return new CompilationTaskSources[] {task}; + } + } +} From 5dae077c1f21829235b442cba2d05d3dbd882c5a Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Mon, 24 Mar 2025 01:08:07 +0100 Subject: [PATCH 07/13] Initial support of multi-releases project by using the `` element. Initial support of modular project (JPMS) by using the `` element. Currently support only one or the other, not yet both in same time. Contains fixes for incremental build: * The `package-info.java` source files may produce no output. * Provide an option to rebuild everything as soon as one file changed. The `additions` aspect has been renamed `rebuild-on-add` for clarity and for consistency with `rebuild-on-change` added in the last above point. --- src/it/MCOMPILER-192/verify.groovy | 2 +- src/it/mcompiler-21_methodname-change/pom.xml | 3 + .../plugin/compiler/AbstractCompilerMojo.java | 149 +++++--- .../maven/plugin/compiler/CompilerMojo.java | 21 +- .../plugin/compiler/DiagnosticLogger.java | 9 +- .../plugin/compiler/ForkedToolSources.java | 10 +- .../plugin/compiler/IncrementalBuild.java | 206 ++++++---- .../apache/maven/plugin/compiler/Options.java | 33 ++ .../plugin/compiler/SourceDirectory.java | 131 ++++++- .../maven/plugin/compiler/SourceFile.java | 36 +- .../maven/plugin/compiler/SourcePathType.java | 116 ++++++ .../plugin/compiler/SourcesForRelease.java | 67 ++-- .../plugin/compiler/TestCompilerMojo.java | 35 +- .../maven/plugin/compiler/ToolExecutor.java | 359 +++++++++++++----- .../plugin/compiler/ToolExecutorForTest.java | 145 ++++++- .../compiler/UnsupportedVersionException.java | 47 +++ .../maven/plugin/compiler/package-info.java | 30 ++ 17 files changed, 1065 insertions(+), 334 deletions(-) create mode 100644 src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java create mode 100644 src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java create mode 100644 src/main/java/org/apache/maven/plugin/compiler/package-info.java diff --git a/src/it/MCOMPILER-192/verify.groovy b/src/it/MCOMPILER-192/verify.groovy index a705918a..1e386a23 100644 --- a/src/it/MCOMPILER-192/verify.groovy +++ b/src/it/MCOMPILER-192/verify.groovy @@ -22,7 +22,7 @@ assert logFile.exists() def content = logFile.getText('UTF-8') def causedByExpected = content.contains ( 'Caused by: org.apache.maven.plugin.compiler.CompilationFailureException:' ) -def twoFilesBeingCompiled = content.contains ( 'Compiling 2 source files' ) +def twoFilesBeingCompiled = content.contains ( 'Compiling all files' ) def checkResult = content.contains ( 'BUILD FAILURE' ) def compilationFailure1 = content.contains( '[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:') def compilationFailure2 = content.contains( ':compile (default-compile) on project blah: Cannot compile') diff --git a/src/it/mcompiler-21_methodname-change/pom.xml b/src/it/mcompiler-21_methodname-change/pom.xml index 8b507ca1..e285040e 100644 --- a/src/it/mcompiler-21_methodname-change/pom.xml +++ b/src/it/mcompiler-21_methodname-change/pom.xml @@ -35,6 +35,9 @@ under the License. org.apache.maven.plugins maven-compiler-plugin @project.version@ + + sources,rebuild-on-change + 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 fabb42bc..6e7ee6b7 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -180,8 +180,23 @@ final Charset charset() { protected String target; /** - * The {@code --release} argument for the Java compiler. - * If omitted, then the compiler will generate bytecodes for the Java version running the compiler. + * The {@code --release} argument for the Java compiler when the sources do not declare this version. + * The suggested way to declare the target Java release is to specify it with the sources like below: + * + *
{@code
+     * 
+     *   
+     *     
+     *       src/main/java
+     *       17
+     *     
+     *   
+     * }
+ * + * If such {@code } element is found, it has precedence over this {@code release} property. + * If a source does not declare a target Java version, then the value of this {@code release} property is + * used as a fallback. + * If omitted, the compiler will generate bytecodes for the Java version running the compiler. * * @see javac --release * @since 3.6 @@ -189,6 +204,12 @@ final Charset charset() { @Parameter(property = "maven.compiler.release") protected String release; + /** + * Whether {@link #target} or {@link #release} has a non-blank value. + * Used for logging a warning if no target Java version was specified. + */ + private boolean targetOrReleaseSet; + /** * Whether to enable preview language features of the java compiler. * If {@code true}, then the {@code --enable-preview} option will be added to compiler arguments. @@ -204,8 +225,11 @@ final Charset charset() { * the directories will be obtained from the {@code } elements declared in the project. * If non-empty, the project {@code } elements are ignored. This configuration option * should be used only when there is a need to override the project configuration. + * + * @deprecated Replaced by the project-wide {@code } element. */ @Parameter + @Deprecated(since = "4.0.0") protected List compileSourceRoots; /** @@ -507,8 +531,8 @@ final Charset charset() { /** * The algorithm to use for selecting which files to compile. - * Values can be {@code dependencies}, {@code sources}, {@code classes}, {@code additions}, - * {@code modules} or {@code none}. + * Values can be {@code dependencies}, {@code sources}, {@code classes}, {@code rebuild-on-change}, + * {@code rebuild-on-add}, {@code modules} or {@code none}. * *

{@code options}: * recompile all source files if the compiler options changed. @@ -533,12 +557,6 @@ final Charset charset() { *

The {@code sources} and {@code classes} values are partially redundant, * doing the same work in different ways. It is usually not necessary to specify those two values.

* - *

{@code additions}: - * recompile all source files when the addition of a new file is detected. - * This aspect should be used together with {@code sources} or {@code classes}. - * When used with {@code classes}, it provides a way to detect class renaming - * (this is not needed with {@code sources}).

- * *

{@code modules}: * recompile modules and let the compiler decides which individual files to recompile. * The compiler plugin does not enumerate the source files to recompile (actually, it does not scan at all the @@ -546,6 +564,18 @@ final Charset charset() { * The Java compiler will scan the source directories itself and compile only those source files that are newer * than the corresponding files in the output directory.

* + *

{@code rebuild-on-add}: + * modifier for recompiling all source files when the addition of a new file is detected. + * This flag is effective only when used together with {@code sources} or {@code classes}. + * When used with {@code classes}, it provides a way to detect class renaming + * (this is not needed with {@code sources} for detecting renaming).

+ * + *

{@code rebuild-on-change}: + * modifier for recompiling all source files when a change is detected in at least one source file. + * This flag is effective only when used together with {@code sources} or {@code classes}. + * It does not rebuild when a new source file is added without change in other files, + * unless {@code rebuild-on-add} is also specified.

+ * *

{@code none}: * the compiler plugin unconditionally specifies all sources to the Java compiler. * This option is mutually exclusive with all other incremental compilation options.

@@ -569,7 +599,7 @@ final Charset charset() { * @since 3.1 * * @deprecated Replaced by {@link #incrementalCompilation}. - * A value of {@code true} in this old property is equivalent to {@code "dependencies,sources,additions"} + * A value of {@code true} in this old property is equivalent to {@code "dependencies,sources,rebuild-on-add"} * in the new property, and a value of {@code false} is equivalent to {@code "classes"}. */ @Deprecated(since = "4.0.0") @@ -586,9 +616,9 @@ final EnumSet incrementalCompilationConfiguration() { if (useIncrementalCompilation != null) { return useIncrementalCompilation ? EnumSet.of( + IncrementalBuild.Aspect.DEPENDENCIES, IncrementalBuild.Aspect.SOURCES, - IncrementalBuild.Aspect.ADDITIONS, - IncrementalBuild.Aspect.DEPENDENCIES) + IncrementalBuild.Aspect.REBUILD_ON_ADD) : EnumSet.of(IncrementalBuild.Aspect.CLASSES); } else { return IncrementalBuild.Aspect.parse(incrementalCompilation); @@ -821,6 +851,9 @@ final EnumSet incrementalCompilationConfiguration() { /** * The logger for reporting information or warnings to the user. * Currently, this is also used for console output. + * + *

Thread safety

+ * This logger should be thread-safe if the {@link ToolExecutor} is executed in a background thread. */ @Inject protected Log logger; @@ -922,6 +955,16 @@ protected String getRelease() { return release; } + /** + * {@return the root directories of Java source code for the given scope}. + * This method ignores the deprecated {@link #compileSourceRoots} element. + * + * @param scope whether to get the directories for main code or for the test code + */ + final Stream getSourceRoots(ProjectScope scope) { + return projectManager.getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY); + } + /** * {@return the root directories of the Java source files to compile, excluding empty directories}. * The list needs to be modifiable for allowing the addition of generated source directories. @@ -932,11 +975,10 @@ protected String getRelease() { */ final List getSourceDirectories(final Path outputDirectory) { if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { - ProjectScope scope = compileScope.projectScope(); - Stream roots = projectManager.getEnabledSourceRoots(project, scope, Language.JAVA_FAMILY); - return SourceDirectory.fromProject(roots, outputDirectory); + Stream roots = getSourceRoots(compileScope.projectScope()); + return SourceDirectory.fromProject(roots, getRelease(), outputDirectory); } else { - return SourceDirectory.fromPluginConfiguration(compileSourceRoots, outputDirectory); + return SourceDirectory.fromPluginConfiguration(compileSourceRoots, getRelease(), outputDirectory); } } @@ -976,22 +1018,6 @@ boolean hasModuleDeclaration(final List roots) throws IOExcepti } } - /** - * Adds dependencies others than the ones declared in POM file. - * The typical case is the compilation of tests, which depends on the main compilation outputs. - * The default implementation does nothing. - * - * @param sourceDirectories the source directories - * @param addTo where to add dependencies - * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file - * @throws IOException if this method needs to walk through directories and that operation failed - */ - void addImplicitDependencies( - List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) - throws IOException { - // Nothing to add in a standard build of main classes. - } - /** * {@return the file where to dump the command-line when debug logging is enabled or when the compilation failed}. * For example, if the value is {@code "javac"}, then the Java compiler can be launched @@ -1068,13 +1094,31 @@ public void execute() throws MojoException { * Creates a new task by taking a snapshot of the current configuration of this MOJO. * This method creates the {@linkplain ToolExecutor#outputDirectory output directory} if it does not already exist. * + *

Multi-threading

+ * This method and the returned objects are not thread-safe. + * However, this method takes a snapshot of the configuration of this MOJO. + * Changes in this MOJO after this method call will not affect the returned executor. + * Therefore, the executor can safely be executed in a background thread, + * provided that the {@link #logger} is thread-safe. + * * @param listener where to send compilation warnings, or {@code null} for the Maven logger * @throws MojoException if this method identifies an invalid parameter in this MOJO * @return the task to execute for compiling the project using the configuration in this MOJO * @throws IOException if an error occurred while creating the output directory or scanning the source directories */ public ToolExecutor createExecutor(DiagnosticListener listener) throws IOException { - return new ToolExecutor(this, listener); + var executor = new ToolExecutor(this, listener); + if (!(targetOrReleaseSet || executor.isReleaseSpecifiedForAll())) { + MessageBuilder mb = messageBuilderFactory + .builder() + .a("No explicit value set for --release or --target. " + + "To ensure the same result in different environments, please add") + .newline() + .newline(); + writePlugin(mb, "release", String.valueOf(Runtime.version().feature())); + logger.warn(mb.build()); + } + return executor; } /** @@ -1138,6 +1182,8 @@ public JavaCompiler compiler() throws MojoException { /** * Parses the parameters declared in the MOJO. + * The {@link #release} parameter is excluded because it is handled in a special way + * in order to support the compilation of multi-version projects. * * @param compiler the tools to use for verifying the validity of options * @return the options after validation @@ -1150,21 +1196,10 @@ public Options parseParameters(final OptionChecker compiler) { * For example, Maven will check for illegal values in the "-g" option only if the compiler rejected * the fully formatted option (e.g. "-g:vars,lines") that we provided to it. */ - boolean targetOrReleaseSet; final var configuration = new Options(compiler, logger); configuration.addIfNonBlank("--source", getSource()); targetOrReleaseSet = configuration.addIfNonBlank("--target", getTarget()); - targetOrReleaseSet |= configuration.addIfNonBlank("--release", getRelease()); - if (!targetOrReleaseSet && ProjectScope.MAIN.equals(compileScope.projectScope())) { - MessageBuilder mb = messageBuilderFactory - .builder() - .a("No explicit value set for --release or --target. " - + "To ensure the same result in different environments, please add") - .newline() - .newline(); - writePlugin(mb, "release", String.valueOf(Runtime.version().feature())); - logger.warn(mb.build()); - } + targetOrReleaseSet |= configuration.setRelease(getRelease()); configuration.addIfTrue("--enable-preview", enablePreview); configuration.addComaSeparated("-proc", proc, List.of("none", "only", "full"), null); if (annotationProcessors != null) { @@ -1207,7 +1242,7 @@ public Options parseParameters(final OptionChecker compiler) { * @throws MojoException if the compilation failed */ private void compile(final JavaCompiler compiler, final Options configuration) throws IOException { - final var executor = createExecutor(null); + final ToolExecutor executor = createExecutor(null); if (!executor.applyIncrementalBuild(this, configuration)) { return; } @@ -1245,10 +1280,10 @@ private void compile(final JavaCompiler compiler, final Options configuration) t * In case of failure, or if debugging is enabled, dump the options to a file. * By default, the file will have the ".args" extension. */ - if (!success || logger.isDebugEnabled()) { + if (!success || verbose || logger.isDebugEnabled()) { IOException suppressed = null; try { - writeDebugFile(configuration, executor.dependencies, executor.getSourceFiles()); + writeDebugFile(executor, configuration); if (success && tipForCommandLineCompilation != null) { logger.debug(tipForCommandLineCompilation); tipForCommandLineCompilation = null; @@ -1552,14 +1587,11 @@ private void writePlugin(MessageBuilder mb, String option, String value) { * If a file name contains embedded spaces, then the whole file name must be between double quotation marks. * The -J options are not supported. * - * @param configuration options to provide to the compiler - * @param dependencies the dependencies - * @param sourceFiles all files to compile + * @param executor the executor that compiled the classes + * @param configuration options provided to the compiler * @throws IOException if an error occurred while writing the debug file */ - private void writeDebugFile(Options configuration, Map> dependencies, Stream sourceFiles) - throws IOException { - + private void writeDebugFile(final ToolExecutor executor, final Options configuration) throws IOException { final Path path = getDebugFilePath(); if (path == null) { logger.warn("The parameter should not be empty."); @@ -1571,7 +1603,7 @@ private void writeDebugFile(Options configuration, Map> dep .append(executable != null ? executable : compilerId); try (BufferedWriter out = Files.newBufferedWriter(path)) { configuration.format(commandLine, out); - for (Map.Entry> entry : dependencies.entrySet()) { + for (Map.Entry> entry : executor.dependencies.entrySet()) { List files = entry.getValue(); files = files.stream().map(this::relativize).toList(); String separator = ""; @@ -1587,7 +1619,7 @@ private void writeDebugFile(Options configuration, Map> dep out.write('"'); out.newLine(); try { - sourceFiles.forEach((file) -> { + executor.getSourceFiles().forEach((file) -> { try { out.write('"'); out.write(relativize(file).toString()); @@ -1601,7 +1633,8 @@ private void writeDebugFile(Options configuration, Map> dep throw e.getCause(); } } - tipForCommandLineCompilation = commandLine.append(" @").append(path).toString(); + tipForCommandLineCompilation = + commandLine.append(" @").append(relativize(path)).toString(); } /** diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java index 597640bc..f058fdf8 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java @@ -18,6 +18,8 @@ */ package org.apache.maven.plugin.compiler; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; import javax.tools.OptionChecker; import java.io.IOException; @@ -237,6 +239,21 @@ protected String getDebugFileName() { return debugFileName; } + /** + * Creates a new task for compiling the main classes. + * + * @param listener where to send compilation warnings, or {@code null} for the Maven logger + * @throws MojoException if this method identifies an invalid parameter in this MOJO + * @return the task to execute for compiling the main code using the configuration in this MOJO + * @throws IOException if an error occurred while creating the output directory or scanning the source directories + */ + @Override + public ToolExecutor createExecutor(DiagnosticListener listener) throws IOException { + ToolExecutor executor = super.createExecutor(listener); + addImplicitDependencies(executor.sourceDirectories, executor.dependencies); + return executor; + } + /** * If compiling a multi-release JAR in the old deprecated way, add the previous versions to the path. * @@ -247,10 +264,8 @@ protected String getDebugFileName() { * * @deprecated For compatibility with the previous way to build multi-releases JAR file. */ - @Override @Deprecated(since = "4.0.0") - final void addImplicitDependencies( - List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) + private void addImplicitDependencies(List sourceDirectories, Map> addTo) throws IOException { if (SUPPORT_LEGACY && multiReleaseOutput) { var paths = new TreeMap(); diff --git a/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java index 2c720a08..584d7d31 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java +++ b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java @@ -89,8 +89,11 @@ final class DiagnosticLogger implements DiagnosticListener { */ @Override public void report(Diagnostic diagnostic) { - MessageBuilder record = messageBuilderFactory.builder(); String message = diagnostic.getMessage(locale); + if (message == null || message.isBlank()) { + return; + } + MessageBuilder record = messageBuilderFactory.builder(); record.a(message); Diagnostic.Kind kind = diagnostic.getKind(); String style; @@ -176,7 +179,7 @@ void logSummary() { patternForCount = patternForCount(Math.max(numWarnings, numErrors)); } if ((numWarnings | numErrors) != 0) { - message.strong("Total:").newline(); + message.strong("Total:"); } if (numWarnings != 0) { writeCount(message, patternForCount, numWarnings, "warning"); @@ -200,10 +203,10 @@ private static String patternForCount(int n) { * Appends the count of warnings or errors, making them plural if needed. */ private static void writeCount(MessageBuilder message, String patternForCount, int count, String name) { + message.newline(); message.format(patternForCount, count, name); if (count > 1) { message.append('s'); } - message.newline(); } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java b/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java index 88d41b2c..c34bd358 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ForkedToolSources.java @@ -63,7 +63,7 @@ final class ForkedToolSources implements StandardJavaFileManager { * Option for source files. These options are not declared in * {@link JavaPathType} because they are not about dependencies. */ - private enum SourcePathType implements PathType { + private enum OtherPathType implements PathType { /** * The option for the directory of source files. */ @@ -84,7 +84,7 @@ private enum SourcePathType implements PathType { */ private final String option; - SourcePathType(String option) { + OtherPathType(String option) { this.option = option; } @@ -411,11 +411,11 @@ public void setLocationFromPaths(Location location, Collection p PathType type = JavaPathType.valueOf(location).orElse(null); if (type == null) { if (location == StandardLocation.SOURCE_OUTPUT) { - type = SourcePathType.GENERATED_SOURCES; + type = OtherPathType.GENERATED_SOURCES; } else if (location == StandardLocation.SOURCE_PATH) { - type = SourcePathType.SOURCES; + type = OtherPathType.SOURCES; } else if (location == StandardLocation.CLASS_OUTPUT) { - type = SourcePathType.OUTPUT; + type = OtherPathType.OUTPUT; } else { throw new IllegalArgumentException("Unsupported location: " + location); } diff --git a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java index 773a1be0..898bfc8f 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java +++ b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java @@ -98,14 +98,6 @@ enum Aspect { */ CLASSES(Set.of()), - /** - * Recompile all source files when the addition of a new file is detected. - * This aspect should be used together with {@link #SOURCES} or {@link #CLASSES}. - * When used with {@link #CLASSES}, it provides a way to detect class renaming - * (this is not needed with {@link #SOURCES}). - */ - ADDITIONS(Set.of()), - /** * Recompile modules and let the compiler decides which individual files to recompile. * The compiler plugin does not enumerate the source files to recompile (actually, it does not scan at all the @@ -116,17 +108,34 @@ enum Aspect { *

This option is available only at the following conditions:

*
    *
  • All sources of the project to compile are modules in the Java sense.
  • - *
  • {@link #SOURCES}, {@link #CLASSES} and {@link #ADDITIONS} aspects are not used.
  • + *
  • {@link #SOURCES}, {@link #CLASSES}, {@link #REBUILD_ON_ADD} and {@link #REBUILD_ON_CHANGE} + * aspects are not used.
  • *
  • There is no include/exclude filter.
  • *
*/ - MODULES(Set.of(SOURCES, CLASSES, ADDITIONS)), + MODULES(Set.of(SOURCES, CLASSES)), + + /** + * Modifier for recompiling all source files when the addition of a new file is detected. + * This flag is effective only when used together with {@link #SOURCES} or {@link #CLASSES}. + * When used with {@link #CLASSES}, it provides a way to detect class renaming + * (this is not needed with {@link #SOURCES} for detecting renaming). + */ + REBUILD_ON_ADD(Set.of(MODULES)), + + /** + * Modifier for recompiling all source files when a change is detected in at least one source file. + * This flag is effective only when used together with {@link #SOURCES} or {@link #CLASSES}. + * It does not rebuild when a new source file is added without change in other files, + * unless {@link #REBUILD_ON_ADD} is also specified. + */ + REBUILD_ON_CHANGE(REBUILD_ON_ADD.excludes), /** * The compiler plugin unconditionally specifies all sources to the Java compiler. * This aspect is mutually exclusive with all other aspects. */ - NONE(Set.of(OPTIONS, DEPENDENCIES, SOURCES, CLASSES, ADDITIONS, MODULES)); + NONE(Set.of(OPTIONS, DEPENDENCIES, SOURCES, CLASSES, REBUILD_ON_ADD, REBUILD_ON_CHANGE, MODULES)); /** * If this aspect is mutually exclusive with other aspects, the excluded aspects. @@ -162,7 +171,7 @@ static EnumSet parse(final String values) { for (String value : values.split(",")) { value = value.trim(); try { - aspects.add(valueOf(value.toUpperCase(Locale.US))); + aspects.add(valueOf(value.toUpperCase(Locale.US).replace('-', '_'))); } catch (IllegalArgumentException e) { var sb = new StringBuilder(256) .append("Illegal incremental build setting: \"") @@ -217,11 +226,22 @@ static EnumSet parse(final String values) { * than the one inferred by heuristic rules. For performance reason, we store the output * files explicitly only when it cannot be inferred. * - * @see SourceInfo#toOutputFile(Path, Path, Path) * @see javax.tools.JavaFileManager#getFileForOutput */ private static final byte EXPLICIT_OUTPUT_FILE = 4; + /** + * Flag in the binary output file telling that the output file has been omitted. + * This is the case of {@code package-info.class} files when the result is empty. + */ + private static final byte OMITTED_OUTPUT_FILE = 8; + + /** + * Bitmask of all flags that are allowed in a cache file. + */ + private static final byte ALL_FLAGS = + NEW_SOURCE_DIRECTORY | NEW_TARGET_DIRECTORY | EXPLICIT_OUTPUT_FILE | OMITTED_OUTPUT_FILE; + /** * Name of the file where to store the list of source files and the list of files created by the compiler. * This is a binary format used for detecting changes. The file is stored in the {@code target} directory. @@ -257,12 +277,43 @@ static EnumSet parse(final String values) { */ private long previousBuildTime; + /** + * The granularity in milliseconds to use for comparing modification times. + * + * @see AbstractCompilerMojo#staleMillis + */ + private final long staleMillis; + /** * Hash code value of the compiler options during the previous build. * This value is initialized by {@link #loadCache()}. */ private int previousOptionsHash; + /** + * Hash code value of the current {@link Options#options} list. + */ + private final int optionsHash; + + /** + * Whether to save the list of source files. + */ + private final boolean saveSourceList; + + /** + * Whether to recompile all source files if a file addition is detected. + * + * @see Aspect#REBUILD_ON_ADD + */ + private final boolean rebuildOnAdd; + + /** + * Whether to recompile all source files if at least one source changed. + * + * @see Aspect#REBUILD_ON_CHANGE + */ + private final boolean rebuildOnChange; + /** * Whether to provide more details about why a module is rebuilt. */ @@ -273,15 +324,38 @@ static EnumSet parse(final String values) { * * @param mojo the MOJO which is compiling source code * @param sourceFiles all source files + * @param saveSourceList whether to save the list of source files in the cache + * @param options the compiler options + * @param aspects result of {@link Aspect#parse(String)} * @throws IOException if the parent directory cannot be created */ - IncrementalBuild(AbstractCompilerMojo mojo, List sourceFiles) throws IOException { + IncrementalBuild( + AbstractCompilerMojo mojo, + List sourceFiles, + boolean saveSourceList, + Options configuration, + EnumSet aspects) + throws IOException { this.sourceFiles = sourceFiles; + this.saveSourceList = saveSourceList; Path file = mojo.mojoStatusPath; cacheFile = Files.createDirectories(file.getParent()).resolve(file.getFileName()); showCompilationChanges = mojo.showCompilationChanges; buildTime = System.currentTimeMillis(); previousBuildTime = buildTime; + staleMillis = mojo.staleMillis; + rebuildOnAdd = aspects.contains(Aspect.REBUILD_ON_ADD); + rebuildOnChange = aspects.contains(Aspect.REBUILD_ON_CHANGE); + optionsHash = configuration.options.hashCode(); + } + + /** + * Deletes the cache if it exists. + * + * @throws IOException if an error occurred while deleting the file + */ + public void deleteCache() throws IOException { + Files.deleteIfExists(cacheFile); } /** @@ -298,20 +372,20 @@ static EnumSet parse(final String values) { *
  • If {@link #NEW_SOURCE_DIRECTORY} is set, the new root directory of source files.
  • *
  • If {@link #NEW_TARGET_DIRECTORY} is set, the new root directory of output files.
  • *
  • If {@link #EXPLICIT_OUTPUT_FILE} is set, the output file.
  • - *
  • The file path relative to the parent of the previous file.
  • + *
  • The file path as a sibling of the previous file, unless a new root directory has been specified.
  • *
  • Last modification time of the source file, in milliseconds since January 1st.
  • * * * - * The "is sibling" Boolean is for avoiding to repeat the parent directory. If that flag is {@code true}, - * then only the filename is stored and the parent is the same as the previous file. + * The "new source directory" flag is for avoiding to repeat the parent directory. + * If that flag is {@code false}, then only the filename is stored and the parent + * is the same as the previous file. * - * @param optionsHash hash code value of the {@link Options#options} list * @param sources whether to save also the list of source files * @throws IOException if an error occurred while writing the cache file */ @SuppressWarnings({"checkstyle:InnerAssignment", "checkstyle:NeedBraces"}) - public void writeCache(final int optionsHash, final boolean sources) throws IOException { + public void writeCache() throws IOException { try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream( cacheFile, StandardOpenOption.WRITE, @@ -320,22 +394,22 @@ public void writeCache(final int optionsHash, final boolean sources) throws IOEx out.writeLong(MAGIC_NUMBER); out.writeLong(buildTime); out.writeInt(optionsHash); - out.writeInt(sources ? sourceFiles.size() : 0); - if (sources) { + out.writeInt(saveSourceList ? sourceFiles.size() : 0); + if (saveSourceList) { Path srcDir = null; Path tgtDir = null; Path previousParent = null; for (SourceFile source : sourceFiles) { final Path sourceFile = source.file; - final Path outputFile = source.getOutputFile(false); + final Path outputFile = source.getOutputFile(); boolean sameSrcDir = Objects.equals(srcDir, srcDir = source.directory.root); - boolean sameTgtDir = Objects.equals(tgtDir, tgtDir = source.directory.outputDirectory); - boolean sameOutput = (outputFile == null) - || outputFile.equals(SourceInfo.toOutputFile(srcDir, tgtDir, sourceFile)); - + boolean sameTgtDir = Objects.equals(tgtDir, tgtDir = source.directory.getOutputDirectory()); + boolean sameOutput = source.isStandardOutputFile(); + boolean omitted = Files.notExists(outputFile); out.writeByte((sameSrcDir ? 0 : NEW_SOURCE_DIRECTORY) | (sameTgtDir ? 0 : NEW_TARGET_DIRECTORY) - | (sameOutput ? 0 : EXPLICIT_OUTPUT_FILE)); + | (sameOutput ? 0 : EXPLICIT_OUTPUT_FILE) + | (omitted ? OMITTED_OUTPUT_FILE : 0)); if (!sameSrcDir) out.writeUTF((previousParent = srcDir).toString()); if (!sameTgtDir) out.writeUTF(tgtDir.toString()); @@ -373,12 +447,13 @@ private Map loadCache() throws IOException { Path srcFile = null; while (--remaining >= 0) { final byte flags = in.readByte(); - if ((flags & ~(NEW_SOURCE_DIRECTORY | NEW_TARGET_DIRECTORY | EXPLICIT_OUTPUT_FILE)) != 0) { + if ((flags & ~ALL_FLAGS) != 0) { throw new IOException("Invalid cache file."); } boolean newSrcDir = (flags & NEW_SOURCE_DIRECTORY) != 0; boolean newTgtDir = (flags & NEW_TARGET_DIRECTORY) != 0; boolean newOutput = (flags & EXPLICIT_OUTPUT_FILE) != 0; + boolean omitted = (flags & OMITTED_OUTPUT_FILE) != 0; Path output = null; if (newSrcDir) srcDir = Path.of(in.readUTF()); if (newTgtDir) tgtDir = Path.of(in.readUTF()); @@ -386,7 +461,8 @@ private Map loadCache() throws IOException { String path = in.readUTF(); srcFile = newSrcDir ? srcDir.resolve(path) : srcFile.resolveSibling(path); srcFile = srcFile.normalize(); - if (previousBuild.put(srcFile, new SourceInfo(srcDir, tgtDir, output, in.readLong())) != null) { + var info = new SourceInfo(srcDir, tgtDir, output, omitted, in.readLong()); + if (previousBuild.put(srcFile, info) != null) { throw new IOException("Duplicated source file declared in the cache: " + srcFile); } } @@ -401,32 +477,13 @@ private Map loadCache() throws IOException { * @param sourceDirectory root directory of the source file * @param outputDirectory output directory of the compiled file * @param outputFile the output file if it was explicitly specified, or {@code null} if it can be inferred + * @param omitted whether the output file has not be generated by the compiler (e.g. {@code package-info.class}) * @param lastModified last modification times of the source file during the previous build */ - private static record SourceInfo(Path sourceDirectory, Path outputDirectory, Path outputFile, long lastModified) { - /** - * The default output extension used in heuristic rules. It is okay if the actual output file does not use - * this extension, because the heuristic rules should be applied only when we have detected that they apply. - */ - private static final String OUTPUT_EXTENSION = SourceDirectory.CLASS_FILE_SUFFIX; - - /** - * Infers the path to the output file using heuristic rules. This method is used for saving space in the - * common space where the heuristic rules work. If the heuristic rules do not work, the full output path - * will be stored in the {@link #cacheFile}. - * - * @param sourceDirectory root directory of the source file - * @param outputDirectory output directory of the compiled file - * @param sourceFile path to the source file - * @return path to the target file - */ - static Path toOutputFile(Path sourceDirectory, Path outputDirectory, Path sourceFile) { - return SourceFile.toOutputFile( - sourceDirectory, outputDirectory, sourceFile, SourceDirectory.JAVA_FILE_SUFFIX, OUTPUT_EXTENSION); - } - + private static record SourceInfo( + Path sourceDirectory, Path outputDirectory, Path outputFile, boolean omitted, long lastModified) { /** - * Delete all output files associated to the given source file. If the output file is a {@code .class} file, + * Deletes all output files associated to the given source file. If the output file is a {@code .class} file, * then this method deletes also the output files for all inner classes (e.g. {@code "Foo$0.class"}). * * @param sourceFile the source file for which to delete output files @@ -435,17 +492,22 @@ static Path toOutputFile(Path sourceDirectory, Path outputDirectory, Path source void deleteClassFiles(final Path sourceFile) throws IOException { Path output = outputFile; if (output == null) { - output = toOutputFile(sourceDirectory, outputDirectory, sourceFile); + output = SourceFile.toOutputFile( + sourceDirectory, + outputDirectory, + sourceFile, + SourceDirectory.JAVA_FILE_SUFFIX, + SourceDirectory.CLASS_FILE_SUFFIX); } String filename = output.getFileName().toString(); - if (filename.endsWith(OUTPUT_EXTENSION)) { - String prefix = filename.substring(0, filename.length() - OUTPUT_EXTENSION.length()); + if (filename.endsWith(SourceDirectory.CLASS_FILE_SUFFIX)) { + String prefix = filename.substring(0, filename.length() - SourceDirectory.CLASS_FILE_SUFFIX.length()); List outputs; try (Stream files = Files.walk(output.getParent(), 1)) { outputs = files.filter((f) -> { String name = f.getFileName().toString(); return name.startsWith(prefix) - && name.endsWith(OUTPUT_EXTENSION) + && name.endsWith(SourceDirectory.CLASS_FILE_SUFFIX) && (name.equals(filename) || name.charAt(prefix.length()) == '$'); }) .toList(); @@ -462,22 +524,20 @@ void deleteClassFiles(final Path sourceFile) throws IOException { /** * Detects whether the list of detected files has changed since the last build. * This method loads the list of files of the previous build from a status file - * and compare it with the new list. If the file cannot be read, then this method - * conservatively assumes that the file tree changed. + * and compares it with the new list file. If the list file cannot be read, + * then this method conservatively assumes that the file tree changed. * *

    If this method returns {@code null}, the caller can check the {@link SourceFile#isNewOrModified} flag * for deciding which files to recompile. If this method returns non-null value, then the {@code isModified} * flag should be ignored and all files recompiled unconditionally. The returned non-null value is a message * saying why the project needs to be rebuilt.

    * - * @param staleMillis the granularity in milliseconds to use for comparing modification times - * @param rebuildOnAdd whether to recompile all source files if a file addition is detected * @return {@code null} if the project does not need to be rebuilt, otherwise a message saying why to rebuild * @throws IOException if an error occurred while deleting output files of the previous build * * @see Aspect#SOURCES */ - String inputFileTreeChanges(final long staleMillis, final boolean rebuildOnAdd) throws IOException { + String inputFileTreeChanges() throws IOException { final Map previousBuild; try { previousBuild = loadCache(); @@ -502,10 +562,16 @@ String inputFileTreeChanges(final long staleMillis, final boolean rebuildOnAdd) * of another class. */ allChanged = false; - Path output = source.getOutputFile(true); + if (previous.omitted) { + continue; + } + Path output = source.getOutputFile(); if (Files.exists(output, LINK_OPTIONS)) { continue; // Source file has not been modified and output file exists. } + } else if (rebuildOnChange) { + return causeOfRebuild("at least one source file changed", false) + .toString(); } } else if (!source.ignoreModification) { if (showCompilationChanges) { @@ -571,7 +637,7 @@ String dependencyChanges(Iterable> dependencies, Collection f loadCache(); } final FileTime changeTime = FileTime.fromMillis(previousBuildTime); - List updated = new ArrayList<>(); + final var updated = new ArrayList(); for (List roots : dependencies) { for (Path root : roots) { try (Stream files = Files.walk(root)) { @@ -607,16 +673,15 @@ String dependencyChanges(Iterable> dependencies, Collection f } /** - * Returns whether the compilar options have changed. + * Returns whether the compiler options have changed. * This method should be invoked only after {@link #inputFileTreeChanges} returned {@code null}. * - * @param optionsHash hash code value of the {@link Options#options} list * @return {@code null} if the project does not need to be rebuilt, otherwise a message saying why to rebuild * @throws IOException if an error occurred while loading the cache file * * @see Aspect#OPTIONS */ - String optionChanges(int optionsHash) throws IOException { + String optionChanges() throws IOException { if (!cacheLoaded) { loadCache(); } @@ -646,22 +711,23 @@ private static StringBuilder causeOfRebuild(String cause, boolean colon) { * The files identified as in need to be recompiled have their {@link SourceFile#isNewOrModified} * flag set to {@code true}. This method does not use the cache file. * - * @param staleMillis the granularity in milliseconds to use for comparing modification times - * @param rebuildOnAdd whether to recompile all source files if a file addition is detected * @return {@code null} if the project does not need to be rebuilt, otherwise a message saying why to rebuild * @throws IOException if an error occurred while reading the time stamp of an output file * * @see Aspect#CLASSES */ - String markNewOrModifiedSources(long staleMillis, boolean rebuildOnAdd) throws IOException { + String markNewOrModifiedSources() throws IOException { for (SourceFile source : sourceFiles) { if (!source.isNewOrModified) { // Check even if `source.ignoreModification` is true. - Path output = source.getOutputFile(true); + Path output = source.getOutputFile(); if (Files.exists(output, LINK_OPTIONS)) { FileTime t = Files.getLastModifiedTime(output, LINK_OPTIONS); if (source.lastModified - t.toMillis() <= staleMillis) { continue; + } else if (rebuildOnChange) { + return causeOfRebuild("at least one source file changed", false) + .toString(); } } else if (rebuildOnAdd) { StringBuilder causeOfRebuild = causeOfRebuild("of added source files", showCompilationChanges); diff --git a/src/main/java/org/apache/maven/plugin/compiler/Options.java b/src/main/java/org/apache/maven/plugin/compiler/Options.java index e7e4e704..47b5b61a 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/Options.java +++ b/src/main/java/org/apache/maven/plugin/compiler/Options.java @@ -45,6 +45,13 @@ public final class Options { */ final List options; + /** + * Index of the value of the {@code --release} parameter, or 0 if not present. + * + * @see #setRelease(String) + */ + private int indexOfReleaseValue; + /** * The tools to use for checking whether an option is supported. * It can be the Java compiler or the Javadoc generator. @@ -90,6 +97,32 @@ private static String strip(String value) { return value; } + /** + * Adds or sets the value of the {@code --release} option. If this option was not present, it is added. + * If this option has already been specified, then its value is changed to the given value if non-null + * and non-blank, or removed otherwise. + * + * @param value value of the {@code --release} option, or {@code null} or empty if none + * @return whether the option has been added or defined + */ + public boolean setRelease(String value) { + if (indexOfReleaseValue == 0) { + boolean added = addIfNonBlank("--release", value); + if (added) { + indexOfReleaseValue = options.size() - 1; + } + return added; + } + value = strip(value); + if (value != null) { + options.set(indexOfReleaseValue, value); + return true; + } + options.subList(indexOfReleaseValue - 1, indexOfReleaseValue + 1).clear(); + indexOfReleaseValue = 0; + return false; + } + /** * Adds the given option if the given value is true and the option is supported. * If the option is unsupported, then a warning is logged and the option is not added. diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java index 603e3783..7a04e5b0 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java @@ -101,7 +101,9 @@ final class SourceDirectory { * Name of the module for which source directories are provided, or {@code null} if none. * This name is supplied to the constructor instead of parsed from {@code module-info.java} * file because the latter may not exist in this directory. For example, in a multi-release - * project the module-info may be declared in another directory for the base version. + * project, the module-info may be declared in another directory for the base version. + * + * @see #getModuleInfo() */ final String moduleName; @@ -109,26 +111,44 @@ final class SourceDirectory { * Path to the {@code module-info} file, or {@code null} if none. This flag is set when * walking through the directory content. This is related, but not strictly equivalent, * to whether the {@link #moduleName} is non-null. + * + * @see #getModuleInfo() */ private Path moduleInfo; /** * The Java release for which source directories are provided, or {@code null} for the default release. - * This is used for multi-versions JAR files. + * This is used for multi-versions JAR files. Note that a non-null value does not mean that the classes + * will be put in a {@code META-INF/versions/} subdirectory, because this version may be the base version. + * + * @see #getSpecificVersion() */ final SourceVersion release; + /** + * Whether the {@linkplain #release} is a version other than the base version. + * This flag is initially unknown (conservatively assumed false) and is set after the base version is known. + * Note that a null {@linkplain #release} is considered more recent than all non-null releases (because null + * stands for the default, which is usually the runtime version), and therefore is considered versioned if + * some non-null releases exist. + * + * @see #completeIfVersioned(SourceVersion) + */ + private boolean isVersioned; + /** * The directory where to store the compilation results. * This is the MOJO output directory with sub-directories appended according the following rules, in that order: * *
      *
    1. If {@link #moduleName} is non-null, then the module name is appended.
    2. - *
    3. If {@link #release} is non-null, then the next elements in the paths are + *
    4. If {@link #isVersioned} is {@code true}, then the next elements in the paths are * {@code "META-INF/versions/"} where {@code } is the release number.
    5. *
    + * + * @see #getOutputDirectory() */ - final Path outputDirectory; + private Path outputDirectory; /** * Kind of output files in the output directory. @@ -165,15 +185,43 @@ private SourceDirectory( if (moduleName != null) { outputDirectory = outputDirectory.resolve(moduleName); } - if (release != null) { - String version = release.name(); // TODO: replace by runtimeVersion() in Java 18. - version = version.substring(version.lastIndexOf('_') + 1); - outputDirectory = outputDirectoryForReleases(outputDirectory).resolve(version); - } this.outputDirectory = outputDirectory; this.outputFileKind = outputFileKind; } + /** + * Potentially adds the {@code META-INF/versions/} part of the path to the output directory. + * This method can be invoked only after the base version has been determined, which happens + * after all other source directories have been built. + */ + private void completeIfVersioned(SourceVersion baseVersion) { + @SuppressWarnings("LocalVariableHidesMemberVariable") + SourceVersion release = this.release; + isVersioned = (release != baseVersion); + if (isVersioned) { + if (release == null) { + release = SourceVersion.latestSupported(); + // `this.release` intentionally left to null. + } + outputDirectory = outputDirectoryForReleases(outputDirectory, release); + } + } + + /** + * Returns the directory where to write the compilation for a specific Java release. + * + * @param outputDirectory usually the value of {@link #outputDirectory} + * @param release the release, or {@code null} for the default release + */ + static Path outputDirectoryForReleases(Path outputDirectory, SourceVersion release) { + if (release == null) { + release = SourceVersion.latestSupported(); + } + String version = release.name(); // TODO: replace by runtimeVersion() in Java 18. + version = version.substring(version.lastIndexOf('_') + 1); + return outputDirectoryForReleases(outputDirectory).resolve(version); + } + /** * Returns the directory where to write the compilation for a specific Java release. * The caller shall add the version number to the returned path. @@ -184,18 +232,34 @@ static Path outputDirectoryForReleases(Path outputDirectory) { } /** - * Converts a version from Maven API to Java tools API. + * {@return the target version as an object from the Java tools API}. + * + * @param root the source directory for which to get the target version + * @throws UnsupportedVersionException if the version string cannot be parsed + */ + static Optional targetVersion(final SourceRoot root) { + return root.targetVersion().map(Version::asString).map(SourceDirectory::parse); + } + + /** + * Parses the given version string. * This method parses the version with {@link Runtime.Version#parse(String)}. * Therefore, for Java 8, the version shall be "8", not "1.8". + * + * @param version the version to parse, or null or empty if none + * @return the parsed version, or {@code null} if the given string was null or empty + * @throws UnsupportedVersionException if the version string cannot be parsed */ - private static SourceVersion parse(final Version version) { - String text = version.asString(); + private static SourceVersion parse(final String version) { + if (version == null || version.isBlank()) { + return null; + } try { - var parsed = Runtime.Version.parse(text); + var parsed = Runtime.Version.parse(version); return SourceVersion.valueOf("RELEASE_" + parsed.feature()); // TODO: Replace by return SourceVersion.valueOf(v) after upgrade to Java 18. } catch (IllegalArgumentException e) { - throw new CompilationFailureException("Illegal version number: \"" + text + '"', e); + throw new UnsupportedVersionException("Illegal version number: \"" + version + '"', e); } } @@ -204,10 +268,13 @@ private static SourceVersion parse(final Version version) { * The returned list includes only the directories that exist. * * @param compileSourceRoots the root paths to source files + * @param defaultRelease the release to use if the {@code } element provides none, or {@code null} * @param outputDirectory the directory where to store the compilation results * @return the given list of paths wrapped as source directory objects */ - static List fromProject(Stream compileSourceRoots, Path outputDirectory) { + static List fromProject( + Stream compileSourceRoots, String defaultRelease, Path outputDirectory) { + var release = parse(defaultRelease); // May be null. var roots = new ArrayList(); compileSourceRoots.forEach((SourceRoot source) -> { Path directory = source.directory(); @@ -224,11 +291,16 @@ static List fromProject(Stream compileSourceRoots, source.excludes(), fileKind, source.module().orElse(null), - source.targetVersion().map(SourceDirectory::parse).orElse(null), + targetVersion(source).orElse(release), outputDirectory, outputFileKind)); } }); + roots.stream() + .map((dir) -> dir.release) + .filter(Objects::nonNull) + .min(SourceVersion::compareTo) + .ifPresent((baseVersion) -> roots.forEach((dir) -> dir.completeIfVersioned(baseVersion))); return roots; } @@ -238,10 +310,13 @@ static List fromProject(Stream compileSourceRoots, * Used only when the compiler plugin is configured with the {@code compileSourceRoots} option. * * @param compileSourceRoots the root paths to source files + * @param defaultRelease the release to use, or {@code null} of unspecified * @param outputDirectory the directory where to store the compilation results * @return the given list of paths wrapped as source directory objects */ - static List fromPluginConfiguration(List compileSourceRoots, Path outputDirectory) { + static List fromPluginConfiguration( + List compileSourceRoots, String defaultRelease, Path outputDirectory) { + var release = parse(defaultRelease); // May be null. var roots = new ArrayList(compileSourceRoots.size()); for (String file : compileSourceRoots) { Path directory = Path.of(file); @@ -252,7 +327,7 @@ static List fromPluginConfiguration(List compileSourceR List.of(), JavaFileObject.Kind.SOURCE, null, - null, + release, outputDirectory, JavaFileObject.Kind.CLASS)); } @@ -290,6 +365,26 @@ public Optional getModuleInfo() { return Optional.ofNullable(moduleInfo); } + /** + * {@return the Java version of the sources in this directory if different than the base version}. + * The value returned by this method is related to the {@code META-INF/versions/} subdirectory in + * the path returned by {@link #getOutputDirectory()}. If this method returns an empty value, then + * there is no such subdirectory (which doesn't mean that the user did not specified a Java version). + * If non-empty, the returned value is the value of n in {@code META-INF/versions/n}. + */ + public Optional getSpecificVersion() { + return Optional.ofNullable(isVersioned ? release : null); + } + + /** + * {@return the directory where to store the compilation results}. + * This is the MOJO output directory potentially completed with + * sub-directories for module name and {@code META-INF/versions} versioning. + */ + public Path getOutputDirectory() { + return outputDirectory; + } + /** * Compares the given object with this source directory for equality. * diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java b/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java index bf8331f5..87a4ee26 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceFile.java @@ -68,7 +68,7 @@ final class SourceFile { /** * The path of the {@code .class} file, created when first requested. * - * @see #getOutputFile(boolean) + * @see #getOutputFile() */ private Path outputFile; @@ -88,30 +88,38 @@ final class SourceFile { directory.visit(file); } + /** + * {@return whether the output file is the same as the one that we would infer from heuristic rules}. + * + *

    TODO: this is not yet implemented. We need to clarify how to get the output file information + * from the compiler, maybe via the {@link javax.tools.JavaFileManager#getFileForOutput} method. + * Then, {@link #getOutputFile} should compare that value with the inferred one and set a flag.

    + */ + boolean isStandardOutputFile() { + // The constants below must match the ones in `IncrementalBuild.SourceInfo`. + return SourceDirectory.JAVA_FILE_SUFFIX.equals(directory.fileKind.extension) + && SourceDirectory.CLASS_FILE_SUFFIX.equals(directory.outputFileKind.extension); + } + /** * Returns the file resulting from the compilation of this source file. If the output file has been * {@linkplain javax.tools.JavaFileManager#getFileForOutput obtained from the compiler}, that value - * if returned. Otherwise, if {@code infer} is {@code true}, then the output file is inferred using - * {@linkplain #toOutputFile heuristic rules}. + * if returned. Otherwise, output file is inferred using {@linkplain #toOutputFile heuristic rules}. * - * @param infer whether to allow this method to infer a default path using heuristic rules - * @return path to the output file, or {@code null} if unspecified and {@code infer} is {@code false} + * @return path to the output file */ - Path getOutputFile(boolean infer) { - if (!infer) { - /* - * TODO: add a `setOutputFile(Path)` method after we clarified how to get this information from the compiler. - * It may be from javax.tools.JavaFileManager.getFileForOutput(...). - */ - return null; - } + Path getOutputFile() { if (outputFile == null) { outputFile = toOutputFile( directory.root, - directory.outputDirectory, + directory.getOutputDirectory(), file, directory.fileKind.extension, directory.outputFileKind.extension); + /* + * TODO: compare with the file given by the compiler (if we can get that information) + * and set a `isStandardOutputFile` flag with the comparison result. + */ } return outputFile; } diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java b/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java new file mode 100644 index 00000000..1e986a5f --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java @@ -0,0 +1,116 @@ +/* + * 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.util.Optional; +import java.util.StringJoiner; + +import org.apache.maven.api.PathType; + +/** + * Key for declaring source files as an implementation convenience. + * This type is not declared in {@link JavaPathType} because it is not about dependencies. + * + * @author Martin Desruisseaux + */ +final class SourcePathType implements PathType { + /** + * The singleton instance for non-modular source paths. + */ + private static final SourcePathType SOURCE_PATH = new SourcePathType(null); + + /** + * The name of the module, or {@code null} if none. + */ + private final String moduleName; + + /** + * Creates a new path type for the given module. + * + * @param moduleName the name of the module, or {@code null} if none + */ + private SourcePathType(String moduleName) { + this.moduleName = moduleName; + } + + /** + * Returns the source path type for the given module name. + * + * @param moduleName the name of the module, or {@code null} if none + * @return the source path type + */ + static SourcePathType valueOf(String moduleName) { + return (moduleName == null || moduleName.isBlank()) ? SOURCE_PATH : new SourcePathType(moduleName); + } + + /** + * Returns the unique name of this path type, including the module to patch if any. + * + * @return the name of this path type, including module name + */ + @Override + public String id() { + String id = name(); + if (moduleName != null) { + id = id + ':' + moduleName; + } + return id; + } + + /** + * Returns the programmatic name of this path type, without module name. + * + * @return the programmatic name of this path type + */ + @Override + public String name() { + return (moduleName == null) ? "SOURCE_PATH" : "MODULE_SOURCE_PATH"; + } + + /** + * Returns the name of the tool option for this path. + * It does not include the module name. + */ + @Override + public Optional option() { + return Optional.of((moduleName == null) ? "--source-path" : "--module-source-path"); + } + + /** + * {@return the option followed by a string representation of the given path elements}. + * + * @param paths the path elements to format + */ + @Override + public String[] option(Iterable paths) { + var joiner = new StringJoiner(File.pathSeparator, (moduleName != null) ? moduleName + "=\"" : "\"", "\""); + paths.forEach((path) -> joiner.add(path.toString())); + return new String[] {option().get(), joiner.toString()}; + } + + /** + * {@return a string representation for debugging purposes}. + */ + @Override + public String toString() { + return getClass().getSimpleName() + '[' + id() + ']'; + } +} diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java index 25cd7331..017cb959 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java @@ -25,10 +25,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collection; -import java.util.EnumMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -43,6 +40,7 @@ final class SourcesForRelease implements Closeable { /** * The release for this set of sources, or {@code null} if the user did not specified a release. * + * @see #getReleaseString() * @see SourceDirectory#release */ final SourceVersion release; @@ -81,66 +79,46 @@ final class SourcesForRelease implements Closeable { * * @param release the release for this set of sources, or {@code null} if the user did not specified a release */ - private SourcesForRelease(SourceVersion release) { + SourcesForRelease(SourceVersion release) { this.release = release; - files = new ArrayList<>(256); + files = new ArrayList<>(); roots = new LinkedHashMap<>(); moduleInfos = new LinkedHashMap<>(); } + /** + * Returns the release as a string suitable for the {@code --release} compiler option. + * + * @return the release number as a string, or {@code null} if none + */ + String getReleaseString() { + if (release == null) { + return null; + } + var version = release.name(); + return version.substring(version.lastIndexOf('_') + 1); + } + /** * Adds the given source file to this collection of source files. * The value of {@code source.directory.release}, if not null, must be equal to {@link #release}. * * @param source the source file to add. */ - private void add(SourceFile source) { + void add(SourceFile source) { var directory = source.directory; if (lastDirectoryAdded != directory) { lastDirectoryAdded = directory; String moduleName = directory.moduleName; - if (moduleName == null) { + if (moduleName == null || moduleName.isBlank()) { moduleName = ""; } - add(roots, moduleName, directory.root); + roots.get(moduleName).add(directory.root); directory.getModuleInfo().ifPresent((path) -> moduleInfos.put(directory, null)); } files.add(source.file); } - /** - * Adds the given directory in the given map. - * - * @param target the map where to add the specified directory - * @param moduleName name of the module, or an empty string if none - * @param directory the directory to add, or {@code null} if none - */ - private static void add(Map> target, String moduleName, Path directory) { - if (directory != null) { - target.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<>()).add(directory); - } - } - - /** - * Groups all sources files first by Java release versions, then by module names. - * The elements in the returned collection are sorted in the order of {@link SourceVersion} - * enumeration values. It should match the increasing order of Java releases. - * - * @param sources the sources to group. - * @return the given sources grouped by Java release versions and module names. - */ - static Collection groupByReleaseAndModule(List sources) { - var result = new EnumMap(SourceVersion.class); - for (SourceFile source : sources) { - final SourceVersion release = source.directory.release; - result.computeIfAbsent( - (release != null) ? release : SourceVersion.latest(), - (key) -> new SourcesForRelease(release)) - .add(source); - } - return result.values(); - } - /** * If there is any {@code module-info.class} in the main classes that are overwritten by this set of sources, * temporarily replace the main files by the test files. The {@link #close()} method must be invoked after @@ -202,7 +180,10 @@ public void close() throws IOException { */ @Override public String toString() { - return getClass().getSimpleName() + '[' + (release != null ? release : "default") + ": " + files.size() - + " files]"; + var sb = new StringBuilder(getClass().getSimpleName()).append('['); + if (release != null) { + sb.append(release).append(": "); + } + return sb.append(files.size()).append(" files]").toString(); } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java index 7bd3720b..fa5ac6cf 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java @@ -25,14 +25,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; -import org.apache.maven.api.JavaPathType; import org.apache.maven.api.PathScope; -import org.apache.maven.api.PathType; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.plugin.MojoException; @@ -207,9 +204,14 @@ public class TestCompilerMojo extends AbstractCompilerMojo { */ transient boolean hasTestModuleInfo; + /** + * Whether a {@code module-info.java} file is defined in the main sources. + */ + private transient boolean hasMainModuleInfo; + /** * Path to the {@code module-info.class} file of the main code, or {@code null} if that file does not exist. - * This field exists only for transferring this information to {@link ToolExecutorForTest#hasTestModuleInfo}, + * This field exists only for transferring this information to {@link ToolExecutorForTest#mainModulePath}, * and should be {@code null} the rest of the time. */ transient Path mainModulePath; @@ -385,8 +387,9 @@ final String getTestModuleName(List compileSourceRoots) throws @Override final boolean hasModuleDeclaration(final List roots) throws IOException { for (SourceDirectory root : roots) { - if (root.getModuleInfo().isPresent()) { - hasTestModuleInfo = true; + hasMainModuleInfo |= root.moduleName != null; + hasTestModuleInfo |= root.getModuleInfo().isPresent(); + if (hasMainModuleInfo & hasTestModuleInfo) { break; } } @@ -404,23 +407,7 @@ final boolean hasModuleDeclaration(final List roots) throws IOE return useModulePath; } } - return useModulePath && mainModulePath != null; - } - - /** - * Adds the main compilation output directories as test dependencies. - * - * @param sourceDirectories the source directories (ignored) - * @param addTo where to add dependencies - * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file - */ - @Override - final void addImplicitDependencies( - List sourceDirectories, Map> addTo, boolean hasModuleDeclaration) { - var pathType = hasModuleDeclaration ? JavaPathType.MODULES : JavaPathType.CLASSES; - if (Files.exists(mainOutputDirectory)) { - addTo.computeIfAbsent(pathType, (key) -> new ArrayList<>()).add(mainOutputDirectory); - } + return useModulePath && hasMainModuleInfo; } /** @@ -437,11 +424,13 @@ public ToolExecutor createExecutor(DiagnosticListener li Path file = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); if (Files.isRegularFile(file)) { mainModulePath = file; + hasMainModuleInfo = true; } return new ToolExecutorForTest(this, listener); } finally { // Reset the fields that were used only for transfering information to `ToolExecutorForTest`. hasTestModuleInfo = false; + hasMainModuleInfo = false; mainModulePath = null; } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java index 4084cd90..099bd7d8 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -18,6 +18,7 @@ */ package org.apache.maven.plugin.compiler; +import javax.lang.model.SourceVersion; import javax.tools.DiagnosticListener; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; @@ -34,8 +35,11 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.EnumMap; import java.util.EnumSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -121,11 +125,19 @@ public class ToolExecutor { final DependencyResolverResult dependencyResolution; /** - * All dependencies grouped by the path types where to place them. - * The path type can be the class-path, module-path, annotation processor path, etc. + * All dependencies grouped by the path types where to place them, together with the modules to patch. + * The path type can be the class-path, module-path, annotation processor path, patched path, etc. + * Some path types include a module name. */ protected final Map> dependencies; + /** + * The classpath given to the compiler. Stored for making possible to prepend the paths + * of the compilation results of previous versions in a multi-version JAR file. + * This list needs to be modifiable. + */ + private List classpath; + /** * The destination directory (or class output directory) for class files. * This directory will be given to the {@code -d} Java compiler option @@ -141,7 +153,19 @@ public class ToolExecutor { * @see AbstractCompilerMojo#incrementalCompilation * @see AbstractCompilerMojo#useIncrementalCompilation */ - private final EnumSet incrementalCompilation; + private final EnumSet incrementalBuildConfig; + + /** + * The incremental build to save if the build succeed. + * In case of failure, the cached information will be unchanged. + */ + private IncrementalBuild incrementalBuild; + + /** + * Whether only a subset of the files will be compiled. This flag can be {@code true} only when + * incremental build is enabled and detected that some files do not need to be recompiled. + */ + private boolean isPartialBuild; /** * Where to send the compilation warning (never {@code null}). If a null value was specified @@ -154,6 +178,9 @@ public class ToolExecutor { * Used for messages emitted directly by the Maven compiler plugin. * Not necessarily used for messages emitted by the Java compiler. * + *

    Thread safety

    + * This logger should be thread-safe if this {@code ToolExecutor} is executed in a background thread. + * * @see AbstractCompilerMojo#logger */ protected final Log logger; @@ -179,16 +206,17 @@ protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener(); /* * Get the source files and whether they include or are assumed to include `module-info.java`. * Note that we perform this step after processing compiler arguments, because this block may * skip the build if there is no source code to compile. We want arguments to be verified first * in order to warn about possible configuration problems. */ - if (incrementalCompilation.contains(IncrementalBuild.Aspect.MODULES)) { + if (incrementalBuildConfig.contains(IncrementalBuild.Aspect.MODULES)) { boolean hasNoFileMatchers = mojo.hasNoFileMatchers(); for (SourceDirectory root : sourceDirectories) { if (root.moduleName == null) { @@ -210,7 +238,6 @@ protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener(); + if (dependencyResolution != null) { + dependencies.putAll(dependencyResolution.getDispatchedPaths()); + dependencies.entrySet().forEach((e) -> e.setValue(new ArrayList<>(e.getValue()))); + } mojo.resolveProcessorPathEntries(dependencies); - mojo.addImplicitDependencies(sourceDirectories, dependencies, hasModuleDeclaration); } /** @@ -239,6 +267,18 @@ public Stream getSourceFiles() { return sourceFiles.stream().map((s) -> s.file); } + /** + * {@return whether a release version is specified for all sources}. + */ + final boolean isReleaseSpecifiedForAll() { + for (SourceDirectory source : sourceDirectories) { + if (source.release == null) { + return false; + } + } + return true; + } + /** * Filters the source files to recompile, or cleans the output directory if everything should be rebuilt. * If the directory structure of the source files has changed since the last build, @@ -257,21 +297,20 @@ public Stream getSourceFiles() { */ public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Options configuration) throws IOException { - final boolean checkSources = incrementalCompilation.contains(IncrementalBuild.Aspect.SOURCES); - final boolean checkClasses = incrementalCompilation.contains(IncrementalBuild.Aspect.CLASSES); - final boolean checkDepends = incrementalCompilation.contains(IncrementalBuild.Aspect.DEPENDENCIES); - final boolean checkOptions = incrementalCompilation.contains(IncrementalBuild.Aspect.OPTIONS); - final boolean rebuildOnAdd = incrementalCompilation.contains(IncrementalBuild.Aspect.ADDITIONS); - incrementalCompilation.clear(); // Prevent this method to be executed twice. + final boolean checkSources = incrementalBuildConfig.contains(IncrementalBuild.Aspect.SOURCES); + final boolean checkClasses = incrementalBuildConfig.contains(IncrementalBuild.Aspect.CLASSES); + final boolean checkDepends = incrementalBuildConfig.contains(IncrementalBuild.Aspect.DEPENDENCIES); + final boolean checkOptions = incrementalBuildConfig.contains(IncrementalBuild.Aspect.OPTIONS); if (checkSources | checkClasses | checkDepends | checkOptions) { - final var incrementalBuild = new IncrementalBuild(mojo, sourceFiles); + incrementalBuild = + new IncrementalBuild(mojo, sourceFiles, checkSources, configuration, incrementalBuildConfig); String causeOfRebuild = null; if (checkSources) { // Should be first, because this method deletes output files of removed sources. - causeOfRebuild = incrementalBuild.inputFileTreeChanges(mojo.staleMillis, rebuildOnAdd); + causeOfRebuild = incrementalBuild.inputFileTreeChanges(); } if (checkClasses && causeOfRebuild == null) { - causeOfRebuild = incrementalBuild.markNewOrModifiedSources(mojo.staleMillis, rebuildOnAdd); + causeOfRebuild = incrementalBuild.markNewOrModifiedSources(); } if (checkDepends && causeOfRebuild == null) { List fileExtensions = mojo.fileExtensions; @@ -280,30 +319,160 @@ public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Opti } causeOfRebuild = incrementalBuild.dependencyChanges(dependencies.values(), fileExtensions); } - int optionsHash = 0; // Hash code collision may happen, this is a "best effort" only. - if (checkOptions) { - optionsHash = configuration.options.hashCode(); - if (causeOfRebuild == null) { - causeOfRebuild = incrementalBuild.optionChanges(optionsHash); - } + if (checkOptions && causeOfRebuild == null) { + causeOfRebuild = incrementalBuild.optionChanges(); } if (causeOfRebuild != null) { - logger.info(causeOfRebuild); + if (!sourceFiles.isEmpty()) { // Avoid misleading message such as "all sources changed". + logger.info(causeOfRebuild); + } } else { + isPartialBuild = true; sourceFiles = incrementalBuild.getModifiedSources(); if (IncrementalBuild.isEmptyOrIgnorable(sourceFiles)) { + incrementalBuildConfig.clear(); // Prevent this method to be executed twice. logger.info("Nothing to compile - all classes are up to date."); sourceFiles = List.of(); return false; + } else { + int n = sourceFiles.size(); + var sb = new StringBuilder("Compiling ").append(n).append(" modified source file"); + if (n > 1) { + sb.append('s'); // Make plural. + } + logger.info(sb.append('.')); } } - if (checkSources | checkDepends | checkOptions) { - incrementalBuild.writeCache(optionsHash, checkSources); + if (!(checkSources | checkDepends | checkOptions)) { + incrementalBuild.deleteCache(); + incrementalBuild = null; } } + incrementalBuildConfig.clear(); // Prevent this method to be executed twice. return true; } + /** + * Dispatches sources and dependencies on the kind of paths determined by {@code DependencyResolver}. + * The targets may be class-path, module-path, annotation processor class-path/module-path, etc. + * + * @param fileManager the file manager where to set the dependency paths + */ + private void setDependencyPaths(final StandardJavaFileManager fileManager) throws IOException { + final var unresolvedPaths = new ArrayList(); + for (Map.Entry> entry : dependencies.entrySet()) { + List paths = entry.getValue(); + PathType key = entry.getKey(); + if (key instanceof JavaPathType type) { + /* + * Dependency to a JAR file (usually). + * Placed on: --class-path, --module-path. + */ + Optional location = type.location(); + if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException. + var value = location.get(); + if (value == StandardLocation.CLASS_PATH) { + classpath = new ArrayList<>(paths); // Need a modifiable list. + paths = classpath; + if (isPartialBuild && !hasModuleDeclaration) { + /* + * From https://docs.oracle.com/en/java/javase/24/docs/specs/man/javac.html: + * "When compiling code for one or more modules, the class output directory will + * automatically be checked when searching for previously compiled classes. + * When not compiling for modules, for backwards compatibility, the directory is not + * automatically checked for previously compiled classes, and so it is recommended to + * specify the class output directory as one of the locations on the user class path, + * using the --class-path option or one of its alternate forms." + */ + paths.add(outputDirectory); + } + } + fileManager.setLocationFromPaths(value, paths); + continue; + } + } else if (key instanceof JavaPathType.Modular type) { + /* + * Source code of test classes, handled as a "dependency". + * Placed on: --patch-module-path. + */ + Optional location = type.rawType().location(); + if (location.isPresent()) { + try { + fileManager.setLocationForModule(location.get(), type.moduleName(), paths); + } catch (UnsupportedOperationException e) { // Happen with `PATCH_MODULE_PATH`. + var it = Arrays.asList(type.option(paths)).iterator(); + if (!fileManager.handleOption(it.next(), it) || it.hasNext()) { + throw new CompilationFailureException("Cannot handle " + type, e); + } + } + continue; + } + } + unresolvedPaths.addAll(paths); + } + if (!unresolvedPaths.isEmpty()) { + var sb = new StringBuilder("Cannot determine where to place the following artifacts:"); + for (Path p : unresolvedPaths) { + sb.append(System.lineSeparator()).append(" - ").append(p); + } + logger.warn(sb); + } + } + + /** + * Ensures that the given value is non-null, replacing null values by the latest version. + */ + private static SourceVersion nonNull(SourceVersion release) { + return (release != null) ? release : SourceVersion.latest(); + } + + /** + * Ensures that the given value is non-null, replacing null or blank values by an empty string. + */ + private static String nonNull(String moduleName) { + return (moduleName == null || moduleName.isBlank()) ? "" : moduleName; + } + + /** + * If the given module name is empty, tries to infer a default module name. A module name is inferred + * (tentatively) when the POM file does not contain an explicit {@code } element. + * This method exists only for compatibility with the Maven 3 way to do a modular project. + * + * @param moduleName the module name, or an empty string if not explicitly specified + * @return the specified module name, or an inferred module name if available, or an empty string + * @throws IOException if the module descriptor cannot be read. + */ + String inferModuleNameIfMissing(String moduleName) throws IOException { + return moduleName; + } + + /** + * Groups all sources files first by Java release versions, then by module names. + * The elements are sorted in the order of {@link SourceVersion} enumeration values, + * with null version sorted last on the assumption that they will be for the latest + * version supported by the runtime environment. + * + * @return the given sources grouped by Java release versions and module names + */ + private Collection groupByReleaseAndModule() { + var result = new EnumMap(SourceVersion.class); + for (SourceDirectory directory : sourceDirectories) { + /* + * We need an entry for every versions even if there is no source to compile for a version. + * This is needed for configuring the classpath in a consistent way, for example with the + * output directory of previous version even if we skipped the compilation of that version. + */ + SourcesForRelease unit = result.computeIfAbsent( + nonNull(directory.release), + (release) -> new SourcesForRelease(directory.release)); // Intentionally ignore the key. + unit.roots.computeIfAbsent(nonNull(directory.moduleName), (moduleName) -> new LinkedHashSet()); + } + for (SourceFile source : sourceFiles) { + result.get(nonNull(source.directory.release)).add(source); + } + return result.values(); + } + /** * Runs the compilation task. * @@ -315,9 +484,12 @@ public boolean applyIncrementalBuild(final AbstractCompilerMojo mojo, final Opti * @throws MojoException if the compilation failed for a reason identified by this method * @throws RuntimeException if any other kind of error occurred */ + @SuppressWarnings("checkstyle:MagicNumber") public boolean compile(final JavaCompiler compiler, final Options configuration, final Writer otherOutput) throws IOException { - + /* + * Announce what the compiler is about to do. + */ if (sourceFiles.isEmpty()) { String message = "No sources to compile."; try { @@ -331,7 +503,7 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, if (logger.isDebugEnabled()) { int n = sourceFiles.size(); @SuppressWarnings("checkstyle:MagicNumber") - var sb = new StringBuilder(n * 40).append("Compiling ").append(n).append(" source files:"); + var sb = new StringBuilder(n * 40).append("The source files to compile are:"); for (SourceFile file : sourceFiles) { sb.append(System.lineSeparator()).append(" ").append(file); } @@ -344,69 +516,77 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, * disposal in order to reuse its cache. */ boolean success = true; - final var unresolvedPaths = new ArrayList(); try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding)) { - /* - * Dispatch all dependencies on the kind of paths determined by `DependencyResolver`: - * class-path, module-path, annotation processor class-path/module-path, etc. - * This configuration will be unchanged for all compilation units. - */ - List patchedOptions = configuration.options; // Workaround for JDK-TBD. - for (Map.Entry> entry : dependencies.entrySet()) { - List paths = entry.getValue(); - PathType key = entry.getKey(); // TODO: replace by pattern matching in Java 21. - if (key instanceof JavaPathType type) { - Optional location = type.location(); - if (location.isPresent()) { // Cannot use `Optional.ifPresent(…)` because of checked IOException. - fileManager.setLocationFromPaths(location.get(), paths); - continue; - } - } else if (key instanceof JavaPathType.Modular type) { - Optional location = type.rawType().location(); - if (location.isPresent()) { - try { - fileManager.setLocationForModule(location.get(), type.moduleName(), paths); - } catch (UnsupportedOperationException e) { // Workaround forJDK-TBD. - if (patchedOptions == configuration.options) { - patchedOptions = new ArrayList<>(patchedOptions); - } - patchedOptions.addAll(Arrays.asList(type.option(paths))); - } - continue; - } - } - unresolvedPaths.addAll(paths); - } - if (!unresolvedPaths.isEmpty()) { - var sb = new StringBuilder("Cannot determine where to place the following artifacts:"); - for (Path p : unresolvedPaths) { - sb.append(System.lineSeparator()).append(" - ").append(p); - } - logger.warn(sb); - } + setDependencyPaths(fileManager); if (!generatedSourceDirectories.isEmpty()) { fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, generatedSourceDirectories); } + boolean isVersioned = false; + Path latestOutputDirectory = null; /* * More than one compilation unit may exist in the case of a multi-releases project. * Units are compiled in the order of the release version, with base compiled first. + * At the beginning of each new iteration, `latestOutputDirectory` is the path to + * the compiled classes of the previous version. */ compile: - for (SourcesForRelease unit : SourcesForRelease.groupByReleaseAndModule(sourceFiles)) { - for (Map.Entry> root : unit.roots.entrySet()) { - String moduleName = root.getKey(); - if (moduleName.isBlank()) { - fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, root.getValue()); + for (final SourcesForRelease unit : groupByReleaseAndModule()) { + Path outputForRelease = null; + boolean isClasspathProject = false; + boolean isModularProject = false; + configuration.setRelease(unit.getReleaseString()); + for (final Map.Entry> root : unit.roots.entrySet()) { + final String moduleName = inferModuleNameIfMissing(root.getKey()); + if (moduleName.isEmpty()) { + isClasspathProject = true; + } else { + isModularProject = true; + } + if (isClasspathProject & isModularProject) { + throw new CompilationFailureException("Mix of modular and non-modular sources."); + } + final Set sourcePaths = root.getValue(); + if (isClasspathProject) { + fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourcePaths); } else { - fileManager.setLocationForModule( - StandardLocation.MODULE_SOURCE_PATH, moduleName, root.getValue()); + fileManager.setLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName, sourcePaths); + } + outputForRelease = outputDirectory; // Modified below if compiling a non-base release. + if (isVersioned) { + if (isClasspathProject) { + /* + * For a non-modular project, this block is executed at most once par compilation unit. + * Add the paths to the classes compiled for previous versions. + */ + if (classpath == null) { + classpath = new ArrayList<>(); + } + classpath.add(0, latestOutputDirectory); + fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath); + outputForRelease = Files.createDirectories( + SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release)); + } else { + /* + * For a modular project, this block can be executed an arbitrary number of times + * (once per module). + * TODO: need to provide --patch-module. Difficulty is that we can specify only once. + */ + throw new UnsupportedOperationException( + "Multi-versions of a modular project is not yet implemented."); + } + } else { + /* + * This addition is for allowing AbstractCompilerMojo.writeDebugFile(…) to show those paths. + * It has no effect on the compilation performed in this method, because the dependencies + * have already been set by the call to `setDependencyPaths(fileManager)`. + */ + if (!sourcePaths.isEmpty()) { + dependencies.put(SourcePathType.valueOf(moduleName), List.copyOf(sourcePaths)); + } } } - /* - * TODO: for all compilations after the base one, add the base to class-path or module-path. - * TODO: prepend META-INF/version/## to output directory if needed. - */ - fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputDirectory)); + fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputForRelease)); + latestOutputDirectory = outputForRelease; /* * Compile the source files now. The following loop should be executed exactly once. * It may be executed twice when compiling test classes overwriting the `module-info`, @@ -416,13 +596,13 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, JavaCompiler.CompilationTask task; for (CompilationTaskSources c : toCompilationTasks(unit)) { Iterable sources = fileManager.getJavaFileObjectsFromPaths(c.files); - task = compiler.getTask(otherOutput, fileManager, listener, patchedOptions, null, sources); - patchedOptions = configuration.options; // Patched options shall be used only once. + task = compiler.getTask(otherOutput, fileManager, listener, configuration.options, null, sources); success = c.compile(task); if (!success) { break compile; } } + isVersioned = true; // Any further iteration is for a version after the base version. } /* * Post-compilation. @@ -433,6 +613,10 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, } catch (UncheckedIOException e) { throw e.getCause(); } + if (success && incrementalBuild != null) { + incrementalBuild.writeCache(); + incrementalBuild = null; + } return success; } @@ -442,6 +626,9 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, * In the latter case, we need to compile the test {@code module-info} separately, before the other test classes. */ CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) { + if (unit.files.isEmpty()) { + return new CompilationTaskSources[0]; + } return new CompilationTaskSources[] {new CompilationTaskSources(unit.files)}; } } diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java index 8af22a78..3191809c 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java @@ -24,18 +24,28 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.io.Writer; import java.lang.module.ModuleDescriptor; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; +import java.util.stream.Stream; import org.apache.maven.api.Dependency; import org.apache.maven.api.JavaPathType; +import org.apache.maven.api.PathType; +import org.apache.maven.api.ProjectScope; import static org.apache.maven.plugin.compiler.AbstractCompilerMojo.SUPPORT_LEGACY; @@ -84,7 +94,7 @@ class ToolExecutorForTest extends ToolExecutor { /** * Name of the main module to compile, or {@code null} if not yet determined. - * If the project is not modular, hen this field contains an empty string. + * If the project is not modular, then this field contains an empty string. * * TODO: use "*" as a sentinel value for modular source hierarchy. * @@ -98,6 +108,19 @@ class ToolExecutorForTest extends ToolExecutor { */ private boolean addedModuleOptions; + /** + * If non-null, the {@code module} part to remove in {@code target/test-classes/module/package}. + * This {@code module} directory is generated by {@code javac} for some compiler options. + * We keep it when the project is configured with the new {@code } element, but + * have to remove it for compatibility reason if the project is compiled in the old way. + * + * @deprecated Exists only for compatibility with the Maven 3 way to do a modular project. + * Is likely to cause confusion, for example with incremental builds. + * New projects should use the {@code } elements instead. + */ + @Deprecated(since = "4.0.0") + private Path directoryLevelToRemove; + /** * Creates a new task by taking a snapshot of the current configuration of the given MOJO. * This constructor creates the {@linkplain #outputDirectory output directory} if it does not already exist. @@ -116,12 +139,9 @@ class ToolExecutorForTest extends ToolExecutor { hasTestModuleInfo = mojo.hasTestModuleInfo; /* * If we are compiling the test classes of a modular project, add the `--patch-modules` options. - * In this case, the option values are directory of source files, not to be confused with cases - * where a module is patched with compiled classes. - * - * Note that those options are handled like dependencies, - * because they will need to be set using the `javax.tools.StandardLocation` API. + * In this case, the option values are directories of main class files of the patched module. */ + final var patchedModules = new LinkedHashMap>(); for (SourceDirectory dir : sourceDirectories) { String moduleToPatch = dir.moduleName; if (moduleToPatch == null) { @@ -138,16 +158,53 @@ class ToolExecutorForTest extends ToolExecutor { } } } + directoryLevelToRemove = outputDirectory.resolve(moduleToPatch); } + patchedModules.put(moduleToPatch, new LinkedHashSet<>()); // Signal that this module exists in the test. + } + /* + * The values of `patchedModules` are empty lists. Now, add the real paths to + * main class for each module that exists in both the main code and the test. + */ + mojo.getSourceRoots(ProjectScope.MAIN).forEach((root) -> { + root.module().ifPresent((moduleToPatch) -> { + Set paths = patchedModules.get(moduleToPatch); + if (paths != null) { + Path path = root.targetPath().orElseGet(() -> Path.of(moduleToPatch)); + path = mainOutputDirectory.resolve(path); + paths.add(path); + } + }); + }); + patchedModules.values().removeIf(Set::isEmpty); + patchedModules.forEach((moduleToPatch, paths) -> { dependencies .computeIfAbsent(JavaPathType.patchModule(moduleToPatch), (key) -> new ArrayList<>()) - .add(dir.root); + .addAll(paths); + }); + /* + * If there is no module to patch, we probably have a non-modular project. + * In such case, we need to put the main output directory on the classpath. + * It may also be a modular project not declared in the `` element. + */ + if (patchedModules.isEmpty() && Files.exists(mainOutputDirectory)) { + PathType pathType = JavaPathType.CLASSES; + if (hasModuleDeclaration) { + pathType = JavaPathType.MODULES; + String moduleToPatch = getMainModuleName(); + if (!moduleToPatch.isEmpty()) { + pathType = JavaPathType.patchModule(moduleToPatch); + directoryLevelToRemove = outputDirectory.resolve(moduleToPatch); + } + } + dependencies.computeIfAbsent(pathType, (key) -> new ArrayList<>()).add(0, mainOutputDirectory); } } /** * {@return the module name of the main code, or an empty string if none}. * This method reads the module descriptor when first needed and caches the result. + * This used if the user did not specified an explicit {@code } element in the sources. * * @throws IOException if the module descriptor cannot be read. */ @@ -164,6 +221,14 @@ private String getMainModuleName() throws IOException { return moduleName; } + /** + * If the given module name is empty, tries to infer a default module name. + */ + @Override + final String inferModuleNameIfMissing(String moduleName) throws IOException { + return moduleName.isEmpty() ? getMainModuleName() : moduleName; + } + /** * Generates the {@code --add-modules} and {@code --add-reads} options for the dependencies that are not * in the main compilation. This method is invoked only if {@code hasModuleDeclaration} is {@code true}. @@ -204,7 +269,7 @@ private void addModuleOptions(final Options configuration) throws IOException { if (compile) { // Needs to be initialized even if `name` is null. if (addReads == null) { - addReads = new StringJoiner(",", getMainModuleName() + "=", ""); + addReads = new StringJoiner(","); } } Path path = entry.getValue(); @@ -237,7 +302,20 @@ private void addModuleOptions(final Options configuration) throws IOException { if (hasUnnamed) { addReads.add("ALL-UNNAMED"); } - configuration.addIfNonBlank("--add-reads", addReads.toString()); + String reads = addReads.toString(); + addReads(configuration, getMainModuleName(), reads); + for (SourceDirectory root : sourceDirectories) { + addReads(configuration, root.moduleName, reads); + } + } + } + + /** + * Adds an {@code --add-reads} compiler option if the given module name is non-null and non-blank. + */ + private static void addReads(Options configuration, String moduleName, String reads) { + if (moduleName != null && !moduleName.isBlank()) { + configuration.addIfNonBlank("--add-reads", moduleName + '=' + reads); } } @@ -256,7 +334,51 @@ public boolean applyIncrementalBuild(AbstractCompilerMojo mojo, Options configur @Override public boolean compile(JavaCompiler compiler, Options configuration, Writer otherOutput) throws IOException { addModuleOptions(configuration); // Effective only once. - return super.compile(compiler, configuration, otherOutput); + try { + return super.compile(compiler, configuration, otherOutput); + } finally { + if (directoryLevelToRemove != null && Files.exists(directoryLevelToRemove)) { + Path target = directoryLevelToRemove.getParent(); + try (Stream files = Files.list(directoryLevelToRemove)) { + files.forEach((path) -> { + try { + Files.move(path, deleteRecursively(target.resolve(path.getFileName()))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + Files.delete(directoryLevelToRemove); + } + } + } + + /** + * Deletes the specified directory recursively. + * + * @param delete the directory to delete + * @return the given directory + */ + private static Path deleteRecursively(Path delete) throws IOException { + if (Files.notExists(delete)) { + return delete; + } + return Files.walkFileTree(delete, new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException error) throws IOException { + var result = super.postVisitDirectory(dir, error); + Files.delete(dir); + return result; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + Files.delete(file); + return super.visitFile(file, attributes); + } + }); } /** @@ -281,6 +403,9 @@ final CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) break; } } + if (files.isEmpty()) { + return new CompilationTaskSources[0]; + } var task = new CompilationTaskSources(files) { /** * Substitutes the main {@code module-info.class} by the test's one, compiles test classes, diff --git a/src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java b/src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java new file mode 100644 index 00000000..02558a97 --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * Thrown when the source cannot be compiled because it required a Java version + * higher than the current runtime. + * + * @author Martin Desruisseaux + */ +@SuppressWarnings("serial") +public class UnsupportedVersionException extends CompilationFailureException { + /** + * Creates a new exception with the given message. + * + * @param message the short message + */ + public UnsupportedVersionException(String message) { + super(message); + } + + /** + * Creates a new exception with the given message and cause. + * + * @param message the short message + * @param cause the cause of the failure, or {@code null} if none + */ + public UnsupportedVersionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/apache/maven/plugin/compiler/package-info.java b/src/main/java/org/apache/maven/plugin/compiler/package-info.java new file mode 100644 index 00000000..6851c46b --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/package-info.java @@ -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. + */ + +/** + * Maven Compiler Plugin MOJO. + * The {@link org.apache.maven.plugin.compiler.CompilerMojo} + * and {@link org.apache.maven.plugin.compiler.TestCompilerMojo} + * classes contains the configuration for compiling the main source code and the tests respectively. + * These classes are mutable as they can be extended and have their properties modified in subclasses. + * However, the actual compilation is performed by {@link org.apache.maven.plugin.compiler.ToolExecutor}, + * which takes a snapshot of the MOJO at construction time. After the test executor has been + * created, it can be executed safely in a background thread even if the MOJO is modified concurrently. + */ +package org.apache.maven.plugin.compiler; From c484c8278aa86a5ca22ce8c24e05bc5136336802 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 30 Mar 2025 18:10:02 +0200 Subject: [PATCH 08/13] Replace the convolved hack (which was moving files around) by symbolic link. This hack is needed only when we want to simulate the Maven 3 behaviour for compatibility reasons. This hack is fragile to the case where a module name is a single name (without dot) and that name is the same as the package name. This ambiguity breaks two tests. For allowing the tests to pass, this commit renames "foo" module as "foo.bar". --- .../src/main/java/module-info.java | 2 +- .../src/test/java/bar/BarTests.java | 2 +- .../src/test/java/module-info.java | 4 +- .../src/main/java/module-info.java | 2 +- .../src/test/java/foo/FooTests.java | 2 +- .../src/test/java/module-info.java | 2 +- .../plugin/compiler/ToolExecutorForTest.java | 71 ++++++------------- 7 files changed, 28 insertions(+), 57 deletions(-) diff --git a/src/it/jpms_compile-main-foo-test-bar/src/main/java/module-info.java b/src/it/jpms_compile-main-foo-test-bar/src/main/java/module-info.java index fd24faa4..d3fe28e5 100644 --- a/src/it/jpms_compile-main-foo-test-bar/src/main/java/module-info.java +++ b/src/it/jpms_compile-main-foo-test-bar/src/main/java/module-info.java @@ -17,7 +17,7 @@ * under the License. */ -module foo { +module foo.bar { requires org.apache.commons.lang3; exports foo; diff --git a/src/it/jpms_compile-main-foo-test-bar/src/test/java/bar/BarTests.java b/src/it/jpms_compile-main-foo-test-bar/src/test/java/bar/BarTests.java index 60ba2856..f00c5236 100644 --- a/src/it/jpms_compile-main-foo-test-bar/src/test/java/bar/BarTests.java +++ b/src/it/jpms_compile-main-foo-test-bar/src/test/java/bar/BarTests.java @@ -30,6 +30,6 @@ void constructor() { @Test void moduleNameIsFoo() { Assertions.assertTrue(Foo.class.getModule().isNamed(), "Foo resides in a named module"); - Assertions.assertEquals("foo", Foo.class.getModule().getName()); + Assertions.assertEquals("foo.bar", Foo.class.getModule().getName()); } } diff --git a/src/it/jpms_compile-main-foo-test-bar/src/test/java/module-info.java b/src/it/jpms_compile-main-foo-test-bar/src/test/java/module-info.java index 9bd6f9c1..80f536d1 100644 --- a/src/it/jpms_compile-main-foo-test-bar/src/test/java/module-info.java +++ b/src/it/jpms_compile-main-foo-test-bar/src/test/java/module-info.java @@ -17,8 +17,8 @@ * under the License. */ -open module bar { - requires foo; +open module bar.biz { + requires foo.bar; requires java.scripting; requires org.junit.jupiter.api; } diff --git a/src/it/jpms_compile-main-foo-test-foo/src/main/java/module-info.java b/src/it/jpms_compile-main-foo-test-foo/src/main/java/module-info.java index 84db0b19..95fc83ab 100644 --- a/src/it/jpms_compile-main-foo-test-foo/src/main/java/module-info.java +++ b/src/it/jpms_compile-main-foo-test-foo/src/main/java/module-info.java @@ -17,6 +17,6 @@ * under the License. */ -module foo { +module foo.bar { requires org.apache.commons.lang3; } diff --git a/src/it/jpms_compile-main-foo-test-foo/src/test/java/foo/FooTests.java b/src/it/jpms_compile-main-foo-test-foo/src/test/java/foo/FooTests.java index 98fb088f..8b6d9b2f 100644 --- a/src/it/jpms_compile-main-foo-test-foo/src/test/java/foo/FooTests.java +++ b/src/it/jpms_compile-main-foo-test-foo/src/test/java/foo/FooTests.java @@ -29,6 +29,6 @@ void constructor() { @Test void moduleNameIsFoo() { Assertions.assertTrue(Foo.class.getModule().isNamed(), "Foo resides in a named module"); - Assertions.assertEquals("foo", Foo.class.getModule().getName()); + Assertions.assertEquals("foo.bar", Foo.class.getModule().getName()); } } diff --git a/src/it/jpms_compile-main-foo-test-foo/src/test/java/module-info.java b/src/it/jpms_compile-main-foo-test-foo/src/test/java/module-info.java index 38ecd13f..63bba5d2 100644 --- a/src/it/jpms_compile-main-foo-test-foo/src/test/java/module-info.java +++ b/src/it/jpms_compile-main-foo-test-foo/src/test/java/module-info.java @@ -17,7 +17,7 @@ * under the License. */ -open module foo { +open module foo.bar { // main requires org.apache.commons.lang3; diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java index 3191809c..1ecf24c4 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java @@ -24,14 +24,10 @@ import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.io.Writer; import java.lang.module.ModuleDescriptor; -import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; @@ -40,7 +36,6 @@ import java.util.Map; import java.util.Set; import java.util.StringJoiner; -import java.util.stream.Stream; import org.apache.maven.api.Dependency; import org.apache.maven.api.JavaPathType; @@ -86,6 +81,13 @@ class ToolExecutorForTest extends ToolExecutor { */ private final boolean hasTestModuleInfo; + /** + * Whether the tests are declared in their own module. If {@code true}, + * then the {@code module-info.java} file of the test declares a name + * different than the {@code module-info.java} file of the main code. + */ + private boolean testInItsOwnModule; + /** * Whether the {@code module-info} of the tests overwrites the main {@code module-info}. * This is a deprecated practice, but is accepted if {@link #SUPPORT_LEGACY} is true. @@ -154,6 +156,7 @@ class ToolExecutorForTest extends ToolExecutor { if (testModuleName != null) { overwriteMainModuleInfo = testModuleName.equals(getMainModuleName()); if (!overwriteMainModuleInfo) { + testInItsOwnModule = true; continue; // The test classes are in their own module. } } @@ -191,10 +194,12 @@ class ToolExecutorForTest extends ToolExecutor { PathType pathType = JavaPathType.CLASSES; if (hasModuleDeclaration) { pathType = JavaPathType.MODULES; - String moduleToPatch = getMainModuleName(); - if (!moduleToPatch.isEmpty()) { - pathType = JavaPathType.patchModule(moduleToPatch); - directoryLevelToRemove = outputDirectory.resolve(moduleToPatch); + if (!testInItsOwnModule) { + String moduleToPatch = getMainModuleName(); + if (!moduleToPatch.isEmpty()) { + pathType = JavaPathType.patchModule(moduleToPatch); + directoryLevelToRemove = outputDirectory.resolve(moduleToPatch); + } } } dependencies.computeIfAbsent(pathType, (key) -> new ArrayList<>()).add(0, mainOutputDirectory); @@ -226,7 +231,7 @@ private String getMainModuleName() throws IOException { */ @Override final String inferModuleNameIfMissing(String moduleName) throws IOException { - return moduleName.isEmpty() ? getMainModuleName() : moduleName; + return (!testInItsOwnModule && moduleName.isEmpty()) ? getMainModuleName() : moduleName; } /** @@ -334,53 +339,19 @@ public boolean applyIncrementalBuild(AbstractCompilerMojo mojo, Options configur @Override public boolean compile(JavaCompiler compiler, Options configuration, Writer otherOutput) throws IOException { addModuleOptions(configuration); // Effective only once. + Path delete = null; try { + if (directoryLevelToRemove != null) { + delete = Files.createSymbolicLink(directoryLevelToRemove, directoryLevelToRemove.getParent()); + } return super.compile(compiler, configuration, otherOutput); } finally { - if (directoryLevelToRemove != null && Files.exists(directoryLevelToRemove)) { - Path target = directoryLevelToRemove.getParent(); - try (Stream files = Files.list(directoryLevelToRemove)) { - files.forEach((path) -> { - try { - Files.move(path, deleteRecursively(target.resolve(path.getFileName()))); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } catch (UncheckedIOException e) { - throw e.getCause(); - } - Files.delete(directoryLevelToRemove); + if (delete != null) { + Files.delete(delete); } } } - /** - * Deletes the specified directory recursively. - * - * @param delete the directory to delete - * @return the given directory - */ - private static Path deleteRecursively(Path delete) throws IOException { - if (Files.notExists(delete)) { - return delete; - } - return Files.walkFileTree(delete, new SimpleFileVisitor() { - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException error) throws IOException { - var result = super.postVisitDirectory(dir, error); - Files.delete(dir); - return result; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { - Files.delete(file); - return super.visitFile(file, attributes); - } - }); - } - /** * Separates the compilation of {@code module-info} from other classes. This is needed when the * {@code module-info} of the test classes overwrite the {@code module-info} of the main classes. From 195a80949b19aca24834b2be22d27c529537d10e Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 5 Apr 2025 14:55:49 +0200 Subject: [PATCH 09/13] Add two integration tests: - Multi-release project defined with the new `` element. - Modular project defined with the new `` element. --- src/it/modular-sources/invoker.properties | 19 +++++ src/it/modular-sources/pom.xml | 72 +++++++++++++++++++ .../src/java/org.bar/main/bar/App.java | 26 +++++++ .../src/java/org.bar/main/module-info.java | 21 ++++++ .../src/java/org.bar/test/bar/AppTest.java | 31 ++++++++ .../src/java/org.foo/main/foo/App.java | 25 +++++++ .../src/java/org.foo/main/module-info.java | 21 ++++++ .../src/java/org.foo/test/foo/AppTest.java | 31 ++++++++ src/it/modular-sources/verify.groovy | 28 ++++++++ .../invoker.properties | 19 +++++ src/it/multirelease-on-classpath/pom.xml | 57 +++++++++++++++ .../src/main/java/foo/MainFile.java | 29 ++++++++ .../src/main/java/foo/OtherFile.java | 29 ++++++++ .../src/main/java_16/foo/OtherFile.java | 34 +++++++++ .../src/main/java_17/foo/YetAnotherFile.java | 31 ++++++++ .../multirelease-on-classpath/verify.groovy | 45 ++++++++++++ 16 files changed, 518 insertions(+) create mode 100644 src/it/modular-sources/invoker.properties create mode 100644 src/it/modular-sources/pom.xml create mode 100644 src/it/modular-sources/src/java/org.bar/main/bar/App.java create mode 100644 src/it/modular-sources/src/java/org.bar/main/module-info.java create mode 100644 src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java create mode 100644 src/it/modular-sources/src/java/org.foo/main/foo/App.java create mode 100644 src/it/modular-sources/src/java/org.foo/main/module-info.java create mode 100644 src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java create mode 100644 src/it/modular-sources/verify.groovy create mode 100644 src/it/multirelease-on-classpath/invoker.properties create mode 100644 src/it/multirelease-on-classpath/pom.xml create mode 100644 src/it/multirelease-on-classpath/src/main/java/foo/MainFile.java create mode 100644 src/it/multirelease-on-classpath/src/main/java/foo/OtherFile.java create mode 100644 src/it/multirelease-on-classpath/src/main/java_16/foo/OtherFile.java create mode 100644 src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java create mode 100644 src/it/multirelease-on-classpath/verify.groovy diff --git a/src/it/modular-sources/invoker.properties b/src/it/modular-sources/invoker.properties new file mode 100644 index 00000000..e1b3c9b2 --- /dev/null +++ b/src/it/modular-sources/invoker.properties @@ -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. + +invoker.goals = clean compile test-compile +invoker.buildResult = success diff --git a/src/it/modular-sources/pom.xml b/src/it/modular-sources/pom.xml new file mode 100644 index 00000000..59b24526 --- /dev/null +++ b/src/it/modular-sources/pom.xml @@ -0,0 +1,72 @@ + + + + 4.1.0 + org.apache.maven.plugins + modular-sources + 1.0-SNAPSHOT + jar + Modular project in Maven 4 + + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @project.version@ + + + + + + + + + + + org.foo + src/java/org.foo/main + + + org.foo + src/java/org.foo/test + test + + + org.bar + src/java/org.bar/main + + + org.bar + src/java/org.bar/test + test + + + + diff --git a/src/it/modular-sources/src/java/org.bar/main/bar/App.java b/src/it/modular-sources/src/java/org.bar/main/bar/App.java new file mode 100644 index 00000000..c2521c8a --- /dev/null +++ b/src/it/modular-sources/src/java/org.bar/main/bar/App.java @@ -0,0 +1,26 @@ +/* + * 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 bar; + +public class App { + public static void main(String[] args) { + foo.App.main(args); + System.out.println("Bar"); + } +} diff --git a/src/it/modular-sources/src/java/org.bar/main/module-info.java b/src/it/modular-sources/src/java/org.bar/main/module-info.java new file mode 100644 index 00000000..cf8da2f2 --- /dev/null +++ b/src/it/modular-sources/src/java/org.bar/main/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.bar { + requires org.foo; +} diff --git a/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java b/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java new file mode 100644 index 00000000..21923691 --- /dev/null +++ b/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java @@ -0,0 +1,31 @@ +/* + * 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 bar; + +import org.junit.jupiter.api.Test; + +/** + * Verifies that the compiler has access to JUnit and the main code. + */ +public class AppTest { + @Test + public void testMain() { + App.main(null); + } +} diff --git a/src/it/modular-sources/src/java/org.foo/main/foo/App.java b/src/it/modular-sources/src/java/org.foo/main/foo/App.java new file mode 100644 index 00000000..b9c24f5f --- /dev/null +++ b/src/it/modular-sources/src/java/org.foo/main/foo/App.java @@ -0,0 +1,25 @@ +/* + * 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 foo; + +public class App { + public static void main(String[] args) { + System.out.println("Foo"); + } +} diff --git a/src/it/modular-sources/src/java/org.foo/main/module-info.java b/src/it/modular-sources/src/java/org.foo/main/module-info.java new file mode 100644 index 00000000..27fa41cd --- /dev/null +++ b/src/it/modular-sources/src/java/org.foo/main/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.foo { + exports foo; +} diff --git a/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java b/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java new file mode 100644 index 00000000..db54dc71 --- /dev/null +++ b/src/it/modular-sources/src/java/org.foo/test/foo/AppTest.java @@ -0,0 +1,31 @@ +/* + * 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 foo; + +import org.junit.jupiter.api.Test; + +/** + * Verifies that the compiler has access to JUnit and the main code. + */ +public class AppTest { + @Test + public void testMain() { + App.main(null); + } +} diff --git a/src/it/modular-sources/verify.groovy b/src/it/modular-sources/verify.groovy new file mode 100644 index 00000000..211c143b --- /dev/null +++ b/src/it/modular-sources/verify.groovy @@ -0,0 +1,28 @@ +/* + * 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.util.jar.JarFile + +assert new File( basedir, "target/classes/org.foo/module-info.class").exists() +assert new File( basedir, "target/classes/org.foo/foo/App.class").exists() +assert new File( basedir, "target/test-classes/org.foo/foo/AppTest.class").exists() + +assert new File( basedir, "target/classes/org.bar/module-info.class").exists() +assert new File( basedir, "target/classes/org.bar/bar/App.class").exists() +assert new File( basedir, "target/test-classes/org.bar/bar/AppTest.class").exists() diff --git a/src/it/multirelease-on-classpath/invoker.properties b/src/it/multirelease-on-classpath/invoker.properties new file mode 100644 index 00000000..d8beb8a8 --- /dev/null +++ b/src/it/multirelease-on-classpath/invoker.properties @@ -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. + +invoker.goals = clean compile +invoker.buildResult = success diff --git a/src/it/multirelease-on-classpath/pom.xml b/src/it/multirelease-on-classpath/pom.xml new file mode 100644 index 00000000..3a47eb97 --- /dev/null +++ b/src/it/multirelease-on-classpath/pom.xml @@ -0,0 +1,57 @@ + + + + 4.1.0 + org.apache.maven.plugins + multirelease-on-classpath + 1.0-SNAPSHOT + jar + Mulirelease in Maven 4 + + + + + org.apache.maven.plugins + maven-compiler-plugin + @project.version@ + + + + + + + + + + + src/main/java + 15 + + + src/main/java_16 + 16 + + + src/main/java_17 + 17 + + + + diff --git a/src/it/multirelease-on-classpath/src/main/java/foo/MainFile.java b/src/it/multirelease-on-classpath/src/main/java/foo/MainFile.java new file mode 100644 index 00000000..502f2780 --- /dev/null +++ b/src/it/multirelease-on-classpath/src/main/java/foo/MainFile.java @@ -0,0 +1,29 @@ +/* + * 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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class MainFile { + public static void main(String[] args) { + System.out.println("MainFile"); + } +} diff --git a/src/it/multirelease-on-classpath/src/main/java/foo/OtherFile.java b/src/it/multirelease-on-classpath/src/main/java/foo/OtherFile.java new file mode 100644 index 00000000..472210e1 --- /dev/null +++ b/src/it/multirelease-on-classpath/src/main/java/foo/OtherFile.java @@ -0,0 +1,29 @@ +/* + * 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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class OtherFile { + public static void main(String[] args) { + System.out.println("OtherFile"); + } +} diff --git a/src/it/multirelease-on-classpath/src/main/java_16/foo/OtherFile.java b/src/it/multirelease-on-classpath/src/main/java_16/foo/OtherFile.java new file mode 100644 index 00000000..cbfa0b98 --- /dev/null +++ b/src/it/multirelease-on-classpath/src/main/java_16/foo/OtherFile.java @@ -0,0 +1,34 @@ +/* + * 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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +public class OtherFile { + public static void main(String[] args) { + System.out.println("OtherFile on Java 16"); + MainFile.main(args); // Verify that we have access to the base version. + } + + static void requireJava16() { + System.out.println("Method available only on Java 16+"); + } +} diff --git a/src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java b/src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java new file mode 100644 index 00000000..a0d1fdbe --- /dev/null +++ b/src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java @@ -0,0 +1,31 @@ +/* + * 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 foo; + +/** + * Test {@code <Source>}. + * Another {@code <Source>}. + */ +class YetAnotherFile { + static void main(String[] args) { + System.out.println("YetAnotherFile on Java 17"); + MainFile.main(args); // Verify that we have access to the base version. + OtherFile.requireJava16(); // Verify that we have access to the Java 16 version. + } +} diff --git a/src/it/multirelease-on-classpath/verify.groovy b/src/it/multirelease-on-classpath/verify.groovy new file mode 100644 index 00000000..f19c7e86 --- /dev/null +++ b/src/it/multirelease-on-classpath/verify.groovy @@ -0,0 +1,45 @@ +/* + * 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.util.jar.JarFile + +def baseVersion = 59; // Java 15 +def nextVersion = 60; // Java 16 +def lastVersion = 61; // Java 17 + +assert baseVersion == getMajor(new File( basedir, "target/classes/foo/MainFile.class")) +assert baseVersion == getMajor(new File( basedir, "target/classes/foo/OtherFile.class")) +assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions/16/foo/OtherFile.class")) +assert lastVersion == getMajor(new File( basedir, "target/classes/META-INF/versions/17/foo/YetAnotherFile.class")) + +int getMajor(File file) +{ + assert file.exists() + def dis = new DataInputStream(new FileInputStream(file)) + final String firstFourBytes = Integer.toHexString(dis.readUnsignedShort()) + Integer.toHexString(dis.readUnsignedShort()) + if (!firstFourBytes.equalsIgnoreCase("cafebabe")) + { + throw new IllegalArgumentException(dataSourceName + " is not a Java .class file.") + } + final int minorVersion = dis.readUnsignedShort() + final int majorVersion = dis.readUnsignedShort() + + dis.close() + return majorVersion +} From 5d4d7f3f79e593bf4a7ac32f0aaae4d8c5b73779 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 6 Apr 2025 12:57:58 +0200 Subject: [PATCH 10/13] Documentation fixes and more straightforward policy for showing or not the stack trace. https://github.com/apache/maven-compiler-plugin/issues/323 --- .../plugin/compiler/AbstractCompilerMojo.java | 17 ++++++++++------- .../maven/plugin/compiler/ToolExecutor.java | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) 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 6e7ee6b7..8f7d0404 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -72,6 +72,7 @@ import org.apache.maven.api.services.DependencyResolver; import org.apache.maven.api.services.DependencyResolverRequest; import org.apache.maven.api.services.DependencyResolverResult; +import org.apache.maven.api.services.MavenException; import org.apache.maven.api.services.MessageBuilder; import org.apache.maven.api.services.MessageBuilderFactory; import org.apache.maven.api.services.ProjectManager; @@ -344,8 +345,8 @@ final Charset charset() { * @see javac Annotation Processing * @since 3.5 * - * @deprecated Replaced by ordinary dependencies with {@code } element - * set to {@code proc}, {@code classpath-proc} or {@code modular-proc}. + * @deprecated Replaced by ordinary dependencies with {@code } element set to + * {@code processor}, {@code classpath-processor} or {@code modular-processor}. */ @Parameter @Deprecated(since = "4.0.0") @@ -1075,8 +1076,7 @@ public void execute() throws MojoException { .builder() .strong("COMPILATION ERROR: ") .a(message); - // Do not log stack trace for `CompilationFailureException` because they are not unexpected. - logger.error(mb.toString(), e instanceof CompilationFailureException ? null : e); + logger.error(mb.toString(), verbose ? e : null); if (tipForCommandLineCompilation != null) { logger.info(tipForCommandLineCompilation); tipForCommandLineCompilation = null; @@ -1102,9 +1102,10 @@ public void execute() throws MojoException { * provided that the {@link #logger} is thread-safe. * * @param listener where to send compilation warnings, or {@code null} for the Maven logger - * @throws MojoException if this method identifies an invalid parameter in this MOJO * @return the task to execute for compiling the project using the configuration in this MOJO + * @throws MojoException if this method identifies an invalid parameter in this MOJO * @throws IOException if an error occurred while creating the output directory or scanning the source directories + * @throws MavenException if an error occurred while fetching dependencies */ public ToolExecutor createExecutor(DiagnosticListener listener) throws IOException { var executor = new ToolExecutor(this, listener); @@ -1398,6 +1399,8 @@ final String parseModuleInfoName(Path source) throws IOException { * any filename-based dependency and this MOJO is compiling the main code, then a warning will be logged. * * @param hasModuleDeclaration whether to allow placement of dependencies on the module-path. + * @throws IOException if an I/O error occurred while fetching dependencies + * @throws MavenException if an error occurred while fetching dependencies for a reason other than I/O. */ final DependencyResolverResult resolveDependencies(boolean hasModuleDeclaration) throws IOException { DependencyResolver resolver = session.getService(DependencyResolver.class); @@ -1460,8 +1463,8 @@ final DependencyResolverResult resolveDependencies(boolean hasModuleDeclaration) * @param addTo the modifiable map and lists where to append more paths to annotation processor dependencies * @throws MojoException if an error occurred while resolving the dependencies * - * @deprecated Replaced by ordinary dependencies with {@code } element - * set to {@code proc}, {@code classpath-proc} or {@code modular-proc}. + * @deprecated Replaced by ordinary dependencies with {@code } element set to + * {@code processor}, {@code classpath-processor} or {@code modular-processor}. */ @Deprecated(since = "4.0.0") final void resolveProcessorPathEntries(Map> addTo) throws MojoException { diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java index 099bd7d8..d067d563 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -52,6 +52,7 @@ import org.apache.maven.api.plugin.Log; import org.apache.maven.api.plugin.MojoException; import org.apache.maven.api.services.DependencyResolverResult; +import org.apache.maven.api.services.MavenException; /** * A task which configures and executes a Java tool such as the Java compiler. @@ -193,6 +194,7 @@ public class ToolExecutor { * @param listener where to send compilation warnings, or {@code null} for the Maven logger * @throws MojoException if this constructor identifies an invalid parameter in the MOJO * @throws IOException if an error occurred while creating the output directory or scanning the source directories + * @throws MavenException if an error occurred while fetching dependencies * * @see AbstractCompilerMojo#createExecutor(DiagnosticListener) */ From 1271c85a7ab88fe4d866cf508073ba14a728dc59 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 13 Apr 2025 13:04:04 +0200 Subject: [PATCH 11/13] Use relative paths in compiler error and warning messages, for shorter and more readable output. Give a hint when a change of directory is needed for using the relative paths. --- .../plugin/compiler/AbstractCompilerMojo.java | 17 +++++++--- .../plugin/compiler/DiagnosticLogger.java | 32 +++++++++++++++++-- .../maven/plugin/compiler/ToolExecutor.java | 3 +- 3 files changed, 44 insertions(+), 8 deletions(-) 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 8f7d0404..62030677 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -29,6 +29,7 @@ import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StreamTokenizer; @@ -1600,10 +1601,18 @@ private void writeDebugFile(final ToolExecutor executor, final Options configura logger.warn("The parameter should not be empty."); return; } - final var commandLine = new StringBuilder("For trying to compile from the command-line, use:") - .append(System.lineSeparator()) - .append(" ") - .append(executable != null ? executable : compilerId); + final var commandLine = new StringBuilder("For trying to compile from the command-line, use:"); + final var chdir = + Path.of(System.getProperty("user.dir")).relativize(basedir).toString(); + if (!chdir.isEmpty()) { + boolean isWindows = (File.separatorChar == '\\'); + commandLine + .append(System.lineSeparator()) + .append(" ") + .append(isWindows ? "chdir " : "cd ") + .append(chdir); + } + commandLine.append(System.lineSeparator()).append(" ").append(executable != null ? executable : compilerId); try (BufferedWriter out = Files.newBufferedWriter(path)) { configuration.format(commandLine, out); for (Map.Entry> entry : executor.dependencies.entrySet()) { diff --git a/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java index 584d7d31..9af2e3fb 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java +++ b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java @@ -22,6 +22,7 @@ import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; +import java.nio.file.Path; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Locale; @@ -53,6 +54,11 @@ final class DiagnosticLogger implements DiagnosticListener { */ private final Locale locale; + /** + * The base directory with which to relativize the paths to source files. + */ + private final Path directory; + /** * Number of errors or warnings. */ @@ -74,14 +80,31 @@ final class DiagnosticLogger implements DiagnosticListener { * @param logger the logger where to send diagnostics * @param messageBuilderFactory the factory for creating message builders * @param locale the locale for compiler message + * @param directory the base directory with which to relativize the paths to source files */ - DiagnosticLogger(Log logger, MessageBuilderFactory messageBuilderFactory, Locale locale) { + DiagnosticLogger(Log logger, MessageBuilderFactory messageBuilderFactory, Locale locale, Path directory) { this.logger = logger; this.messageBuilderFactory = messageBuilderFactory; this.locale = locale; + this.directory = directory; codeCount = new LinkedHashMap<>(); } + /** + * Makes the given file relative to the base directory. + * + * @param file the path to make relative to the base directory + * @return the given path, potentially relative to the base directory + */ + private String relativize(String file) { + try { + return directory.relativize(Path.of(file)).toString(); + } catch (IllegalArgumentException e) { + // Ignore, keep the absolute path. + return file; + } + } + /** * Invoked when the compiler emitted a warning. * @@ -95,6 +118,7 @@ public void report(Diagnostic diagnostic) { } MessageBuilder record = messageBuilderFactory.builder(); record.a(message); + JavaFileObject source = diagnostic.getSource(); Diagnostic.Kind kind = diagnostic.getKind(); String style; switch (kind) { @@ -107,11 +131,13 @@ public void report(Diagnostic diagnostic) { break; default: style = ".info:-bold,f:blue"; + if (diagnostic.getLineNumber() == Diagnostic.NOPOS) { + source = null; // Some messages are generic, e.g. "Recompile with -Xlint:deprecation". + } break; } - JavaFileObject source = diagnostic.getSource(); if (source != null) { - record.newline().a(" at ").a(source.getName()); + record.newline().a(" at ").a(relativize(source.getName())); long line = diagnostic.getLineNumber(); long column = diagnostic.getColumnNumber(); if (line != Diagnostic.NOPOS || column != Diagnostic.NOPOS) { diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java index d067d563..92700e59 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -204,7 +204,8 @@ protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener Date: Sat, 26 Apr 2025 12:32:18 +0200 Subject: [PATCH 12/13] Use the native encoding for reading the file written by a native process (forked compiler). Avoid too verbatim verifications of locale-sensitive compiler messages. https://github.com/apache/maven-compiler-plugin/issues/327 --- src/it/MCOMPILER-346/verify.groovy | 13 +++++++++++-- src/it/mcompiler-120/verify.groovy | 7 ++++++- src/it/mcompiler-179/verify.groovy | 10 +++++++++- .../apache/maven/plugin/compiler/ForkedTool.java | 8 +++++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/it/MCOMPILER-346/verify.groovy b/src/it/MCOMPILER-346/verify.groovy index 837bdd79..9c4e0f10 100644 --- a/src/it/MCOMPILER-346/verify.groovy +++ b/src/it/MCOMPILER-346/verify.groovy @@ -21,5 +21,14 @@ def logFile = new File( basedir, 'build.log' ) assert logFile.exists() content = logFile.text -assert content.contains( 'package org.jenkinsci.test.acceptance.controller does not exist' ) -assert content.contains( 'package org.jenkinsci.test.acceptance.log does not exist' ) +/* + * The messages expected by this test were: + * + * - package org.jenkinsci.test.acceptance.controller does not exist + * - package org.jenkinsci.test.acceptance.log does not exist + * + * But we cannot test the full messages as shown above because they may be localized. + * Test only the package name on the assumption that they will be present in all locales. + */ +assert content.contains( 'org.jenkinsci.test.acceptance.controller' ) +assert content.contains( 'org.jenkinsci.test.acceptance.log' ) diff --git a/src/it/mcompiler-120/verify.groovy b/src/it/mcompiler-120/verify.groovy index c540498e..7b780c6d 100644 --- a/src/it/mcompiler-120/verify.groovy +++ b/src/it/mcompiler-120/verify.groovy @@ -20,7 +20,12 @@ def logFile = new File( basedir, 'build.log' ) assert logFile.exists() content = logFile.text +/* + * The message expected by this test was "unchecked call to add(E) as a member of the raw type List". + * But we cannot test that message because it is locale-dependent. Check only a few keywords instead. + */ +assert content.contains( 'add(E)' ) +assert content.contains( 'List' ) // May be `List` or `java.util.List`. assert content.contains( 'COMPILATION ERROR:' ) assert content.contains( 'CompilationFailureException' ) // In debug level logs. assert !content.contains( 'invalid flag' ) -assert content.contains( 'unchecked call to add(E) as a member of the raw type ' ) // List or java.util.List diff --git a/src/it/mcompiler-179/verify.groovy b/src/it/mcompiler-179/verify.groovy index 5f0672b1..6a5d0d1a 100644 --- a/src/it/mcompiler-179/verify.groovy +++ b/src/it/mcompiler-179/verify.groovy @@ -21,6 +21,14 @@ def logFile = new File( basedir, 'build.log' ) assert logFile.exists() content = logFile.text -assert content.contains( '[WARNING] unchecked call' ) +/* + * The message expected by this test was "[WARNING] unchecked call" (the full message + * is actually "unchecked call to add(E) as a member of the raw type java.util.List"). + * But we cannot test that message because it is locale-dependent. + * Check only a few keywords instead. + */ +assert content.contains( '[WARNING]' ) +assert content.contains( 'add(E)' ) +assert content.contains( 'List' ) // May be `List` or `java.util.List`. assert content.contains( 'COMPILATION ERROR:' ) assert content.contains( 'CompilationFailureException' ) // In debug level logs. diff --git a/src/main/java/org/apache/maven/plugin/compiler/ForkedTool.java b/src/main/java/org/apache/maven/plugin/compiler/ForkedTool.java index a9cc44c1..1a774a59 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ForkedTool.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ForkedTool.java @@ -164,7 +164,13 @@ final boolean run( builder.redirectOutput(dest); return start(builder, out) == 0; } finally { - out.append(Files.readString(output.toPath())); + /* + * Need to use the native encoding because it is the encoding used by the native process. + * This is not necessarily the default encoding of the JVM, which is "file.encoding". + * This property is available since Java 17. + */ + String cs = System.getProperty("native.encoding"); + out.append(Files.readString(output.toPath(), Charset.forName(cs))); output.delete(); } } From 3f01ff8b26e3c9520c6396cdd1bc2c24437b112d Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sat, 26 Apr 2025 14:11:18 +0200 Subject: [PATCH 13/13] More conservative default value of the `` parameter if an annotation processor is present. --- .../plugin/compiler/AbstractCompilerMojo.java | 87 ++++++++++++++++--- .../plugin/compiler/IncrementalBuild.java | 2 +- 2 files changed, 77 insertions(+), 12 deletions(-) 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 62030677..f935c8c5 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -289,7 +289,7 @@ final Charset charset() { * * * Prior Java 21, {@code full} was the default. - * Starting with JDK 21, this option must be set explicitly. + * Starting with Java 21, the default is {@code none} unless another processor option is used. * * @see #annotationProcessors * @see javac -proc @@ -363,7 +363,12 @@ final Charset charset() { *

    * * @since 3.12.0 + * + * @deprecated This flag is ignored. + * Replaced by ordinary dependencies with {@code } element set to + * {@code processor}, {@code classpath-processor} or {@code modular-processor}. */ + @Deprecated(since = "4.0.0") @Parameter(defaultValue = "false") protected boolean annotationProcessorPathsUseDepMgmt; @@ -458,7 +463,7 @@ final Charset charset() { /** * Whether to provide more details about why a module is rebuilt. - * This is used only if {@link #incrementalCompilation} is {@code "inputTreeChanges"}. + * This is used only if {@link #incrementalCompilation} is set to something else than {@code "none"}. * * @see #incrementalCompilation */ @@ -586,13 +591,23 @@ final Charset charset() { * In all cases, the current compiler-plugin does not detect structural changes other than file addition or removal. * For example, the plugin does not detect whether a method has been removed in a class. * + *

    Default value

    + * The default value depends on the context. + * If there is no annotation processor, then the default is {@code "options,dependencies,sources"}. + * It means that a full rebuild will be done if the compiler options or the dependencies changed, + * or if a source file has been deleted. Otherwise, only the modified source files will be recompiled. + * + *

    If an annotation processor is present (e.g., {@link #proc} set to a value other than {@code "none"}), + * then the default value is same as above with the addition of {@code "rebuild-on-add,rebuild-on-change"}. + * It means that a full rebuild will be done if any kind of change is detected.

    + * * @see #staleMillis * @see #fileExtensions * @see #showCompilationChanges * @see #createMissingPackageInfoClass * @since 4.0.0 */ - @Parameter(defaultValue = "options,dependencies,sources") + @Parameter // The default values are implemented in `incrementalCompilationConfiguration()`. protected String incrementalCompilation; /** @@ -610,18 +625,30 @@ final Charset charset() { /** * Returns the configuration of the incremental compilation. - * This method may be removed in a future version if the deprecated parameter is removed. + * If the argument is null or blank, then this method applies + * the default values documented in {@link #incrementalCompilation} javadoc. * * @throws MojoException if a value is not recognized, or if mutually exclusive values are specified */ final EnumSet incrementalCompilationConfiguration() { - if (useIncrementalCompilation != null) { - return useIncrementalCompilation - ? EnumSet.of( - IncrementalBuild.Aspect.DEPENDENCIES, - IncrementalBuild.Aspect.SOURCES, - IncrementalBuild.Aspect.REBUILD_ON_ADD) - : EnumSet.of(IncrementalBuild.Aspect.CLASSES); + if (incrementalCompilation == null || incrementalCompilation.isBlank()) { + if (useIncrementalCompilation != null) { + return useIncrementalCompilation + ? EnumSet.of( + IncrementalBuild.Aspect.DEPENDENCIES, + IncrementalBuild.Aspect.SOURCES, + IncrementalBuild.Aspect.REBUILD_ON_ADD) + : EnumSet.of(IncrementalBuild.Aspect.CLASSES); + } + var aspects = EnumSet.of( + IncrementalBuild.Aspect.OPTIONS, + IncrementalBuild.Aspect.DEPENDENCIES, + IncrementalBuild.Aspect.SOURCES); + if (hasAnnotationProcessor()) { + aspects.add(IncrementalBuild.Aspect.REBUILD_ON_ADD); + aspects.add(IncrementalBuild.Aspect.REBUILD_ON_CHANGE); + } + return aspects; } else { return IncrementalBuild.Aspect.parse(incrementalCompilation); } @@ -1502,6 +1529,44 @@ final void resolveProcessorPathEntries(Map> addTo) throws M } } + /** + * {@return whether an annotation processor seems to be present}. + * This method is invoked if the user did not specified explicit incremental compilation options. + * + * @see #incrementalCompilation + */ + private boolean hasAnnotationProcessor() { + if ("none".equalsIgnoreCase(proc)) { + return false; + } + if (proc == null || proc.isBlank()) { + /* + * If the `proc` parameter was not specified, its default value depends on the Java version. + * It was "full" prior Java 21 and become "none if no other processor option" since Java 21. + * Since even the full" case may do nothing, always check if a processor is declared. + */ + if (annotationProcessors == null || annotationProcessors.length == 0) { + if (annotationProcessorPaths == null || annotationProcessorPaths.isEmpty()) { + DependencyResolver resolver = session.getService(DependencyResolver.class); + if (resolver == null) { // Null value happen during tests, depending on the mock used. + return false; + } + var allowedTypes = EnumSet.of(JavaPathType.PROCESSOR_CLASSES, JavaPathType.PROCESSOR_MODULES); + DependencyResolverResult dependencies = resolver.resolve(DependencyResolverRequest.builder() + .session(session) + .project(project) + .requestType(DependencyResolverRequest.RequestType.COLLECT) + .pathScope(compileScope) + .pathTypeFilter(allowedTypes) + .build()); + + return !dependencies.getDependencies().isEmpty(); + } + } + } + return true; + } + /** * Ensures that the directory for generated sources exists, and adds it to the list of source directories * known to the project manager. This is used for adding the output of annotation processor. diff --git a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java index 898bfc8f..5e3083ba 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java +++ b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java @@ -163,7 +163,7 @@ public String toString() { * Parses a comma-separated list of aspects. * * @param values the plugin parameter to parse as a comma-separated list - * @return the aspect + * @return the aspects which, when modified, should cause a partial or full rebuild * @throws MojoException if a value is not recognized, or if mutually exclusive values are specified */ static EnumSet parse(final String values) {