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/README.md b/README.md index 7551491c..b9ae9e85 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Contributing to [Apache Maven Compiler Plugin](https://maven.apache.org/plugins/ [![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-compiler-plugin.svg?label=Maven%20Central&versionPrefix=3.)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-compiler-plugin) [![Maven Central](https://img.shields.io/maven-central/v/org.apache.maven.plugins/maven-compiler-plugin.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.apache.maven.plugins/maven-compiler-plugin) [![Reproducible Builds](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jvm-repo-rebuild/reproducible-central/master/content/org/apache/maven/plugins/maven-compiler-plugin/badge.json)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/org/apache/maven/plugins/maven-compiler-plugin/README.md) - @@ -37,20 +37,26 @@ Getting Started --------------- + Make sure you have a [GitHub account](https://github.com/signup/free). -+ If you're planning to implement a new feature, it makes sense to discuss your changes - on the [dev list][ml-list] first. - This way you can make sure you're not wasting your time on something that isn't ++ If you're planning to implement a new feature, it makes sense to discuss your changes + on the [dev list][ml-list] first. + This way you can make sure you're not wasting your time on something that isn't considered to be in Apache Maven's scope. + Submit a ticket for your issue, assuming one does not already exist. + Clearly describe the issue, including steps to reproduce when it is a bug. + Make sure you fill in the earliest version that you know has the issue. + Fork the repository on GitHub. +Build requirements +-------------- + +Building requires Maven 4. Executing the tests on Windows requires the developer mode. +This is enabled with _Settings_ > _Update & Security_ > _For Developers_. + Making and Submitting Changes -------------- We accept Pull Requests via GitHub. The [developer mailing list][ml-list] is the -main channel of communication for contributors. +main channel of communication for contributors. There are some guidelines which will make applying PRs easier for us: + Create a topic branch from where you want to base your work (this is usually the master branch). Push your changes to a topic branch in your fork of the repository. @@ -58,7 +64,7 @@ There are some guidelines which will make applying PRs easier for us: + Respect the original code style: by using the same [codestyle][code-style], patches should only highlight the actual difference, not being disturbed by any formatting issues: + Only use spaces for indentation. - + Create minimal diffs - disable on save actions like reformat source code or organize imports. + + Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted, create a separate PR for this change. + Check for unnecessary whitespace with `git diff --check` before committing. + Make sure you have added the necessary tests (JUnit/IT) for your changes. 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-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-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-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/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/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/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/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/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java b/src/it/modular-sources/src/java/org.bar/main/module-info.java similarity index 93% rename from src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java rename to src/it/modular-sources/src/java/org.bar/main/module-info.java index 36f00e07..cf8da2f2 100644 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/module-info.java +++ b/src/it/modular-sources/src/java/org.bar/main/module-info.java @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -module example.mrjar { - exports base; - exports mr; +module org.bar { + requires org.foo; } diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java b/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java similarity index 77% rename from src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java rename to src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java index 0e48eee6..21923691 100644 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/A.java +++ b/src/it/modular-sources/src/java/org.bar/test/bar/AppTest.java @@ -16,17 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package mr; +package bar; -import base.Base; +import org.junit.jupiter.api.Test; -public class A implements I { - public static String getString() { - return Base.get() + " -> 8"; - } - - @Override - public Class introducedClass() { - return java.time.LocalDateTime.class; +/** + * 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/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java b/src/it/modular-sources/src/java/org.foo/main/foo/App.java similarity index 87% rename from src/it/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java rename to src/it/modular-sources/src/java/org.foo/main/foo/App.java index 19ec7d8f..b9c24f5f 100644 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java/base/Base.java +++ b/src/it/modular-sources/src/java/org.foo/main/foo/App.java @@ -16,11 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package base; +package foo; -public class Base { - - public static String get() { - return "BASE"; +public class App { + public static void main(String[] args) { + System.out.println("Foo"); } } diff --git a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java b/src/it/modular-sources/src/java/org.foo/main/module-info.java similarity index 92% rename from src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java rename to src/it/modular-sources/src/java/org.foo/main/module-info.java index a0523266..27fa41cd 100644 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java/mr/I.java +++ b/src/it/modular-sources/src/java/org.foo/main/module-info.java @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -package mr; - -public interface I { - Class introducedClass(); +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-patterns/packaging-plugin/invoker.properties b/src/it/multirelease-on-classpath/invoker.properties similarity index 92% rename from src/it/multirelease-patterns/packaging-plugin/invoker.properties rename to src/it/multirelease-on-classpath/invoker.properties index 1992d544..d8beb8a8 100644 --- a/src/it/multirelease-patterns/packaging-plugin/invoker.properties +++ b/src/it/multirelease-on-classpath/invoker.properties @@ -15,5 +15,5 @@ # specific language governing permissions and limitations # under the License. -invoker.java.version = 9+ -invoker.goals = verify +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-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java b/src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java similarity index 68% rename from src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java rename to src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java index 5083e1ce..a0d1fdbe 100644 --- a/src/it/multirelease-patterns/packaging-plugin/src/main/java-mr/9/mr/A.java +++ b/src/it/multirelease-on-classpath/src/main/java_17/foo/YetAnotherFile.java @@ -16,19 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package mr; +package foo; -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; +/** + * 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 +} 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/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; -} - 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..f935c8c5 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -19,17 +19,17 @@ 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; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StreamTokenizer; @@ -37,15 +37,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; @@ -54,13 +50,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; @@ -74,6 +73,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; @@ -87,6 +87,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 @@ -104,13 +110,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 // ---------------------------------------------------------------------- @@ -139,7 +138,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); @@ -183,8 +182,23 @@ private 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 @@ -192,6 +206,12 @@ private 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. @@ -202,6 +222,18 @@ 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. + * + * @deprecated Replaced by the project-wide {@code } element. + */ + @Parameter + @Deprecated(since = "4.0.0") + 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, @@ -257,7 +289,7 @@ private 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 @@ -314,8 +346,8 @@ private 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") @@ -331,7 +363,12 @@ private 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; @@ -426,7 +463,7 @@ private 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 */ @@ -501,8 +538,8 @@ private 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. @@ -527,12 +564,6 @@ private 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 @@ -540,6 +571,18 @@ private 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.

@@ -548,13 +591,23 @@ private 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; /** @@ -563,13 +616,44 @@ private 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") @Parameter(property = "maven.compiler.useIncrementalCompilation") protected Boolean useIncrementalCompilation; + /** + * Returns the configuration of the incremental compilation. + * 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 (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); + } + } + /** * File extensions to check timestamp for incremental build. * Default contains only {@code class} and {@code jar}. @@ -796,6 +880,9 @@ private Charset charset() { /** * 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; @@ -817,26 +904,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)}. @@ -859,6 +940,16 @@ protected AbstractCompilerMojo(boolean isTestCompile) { */ 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. @@ -893,6 +984,33 @@ 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. + * 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()) { + Stream roots = getSourceRoots(compileScope.projectScope()); + return SourceDirectory.fromProject(roots, getRelease(), outputDirectory); + } else { + return SourceDirectory.fromPluginConfiguration(compileSourceRoots, getRelease(), outputDirectory); + } + } + /** * {@return the path where to place generated source files created by annotation processing}. */ @@ -904,6 +1022,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.

* @@ -911,54 +1032,21 @@ 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; - } - - /** - * 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 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) - throws IOException { - // 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 compileSourceRoots 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) - 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 @@ -983,16 +1071,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) { @@ -1007,8 +1104,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; @@ -1022,15 +1118,47 @@ 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 + * @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); + 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; + } + /** * {@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. @@ -1082,12 +1210,14 @@ private JavaCompiler compiler() throws MojoException { } /** - * Parses the parameters declared in the MOJO. + * 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 */ - 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 @@ -1095,316 +1225,61 @@ protected Options acceptParameters(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 compilerConfiguration = new Options(compiler, logger); - compilerConfiguration.addIfNonBlank("--source", getSource()); - targetOrReleaseSet = compilerConfiguration.addIfNonBlank("--target", getTarget()); - targetOrReleaseSet |= compilerConfiguration.addIfNonBlank("--release", getRelease()); - if (!targetOrReleaseSet && !isTestCompile) { - 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()); - } - compilerConfiguration.addIfTrue("--enable-preview", enablePreview); - compilerConfiguration.addComaSeparated("-proc", proc, List.of("none", "only", "full"), null); + final var configuration = new Options(compiler, logger); + configuration.addIfNonBlank("--source", getSource()); + targetOrReleaseSet = configuration.addIfNonBlank("--target", getTarget()); + targetOrReleaseSet |= configuration.setRelease(getRelease()); + 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 compileSourceRoots = - SourceDirectory.fromPaths(getCompileSourceRoots(), outputDirectory); - final boolean hasModuleDeclaration; - if (incAspects.contains(IncrementalBuild.Aspect.MODULES)) { - for (SourceDirectory root : compileSourceRoots) { - 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(compileSourceRoots); - 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(compileSourceRoots); - 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.internal.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); - /* - * 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, compileSourceRoots); + private void compile(final JavaCompiler compiler, final Options configuration) throws IOException { + final ToolExecutor 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 source. - * 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; @@ -1434,10 +1309,10 @@ && getIncrementalExcludes().isEmpty())) { * 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(compilerConfiguration.options, dependencies, sourceFiles); + writeDebugFile(executor, configuration); if (success && tipForCommandLineCompilation != null) { logger.debug(tipForCommandLineCompilation); tipForCommandLineCompilation = null; @@ -1450,11 +1325,13 @@ && 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: ") - .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); @@ -1471,16 +1348,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); } } } @@ -1550,17 +1423,17 @@ 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. + * @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. */ - 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) { @@ -1571,7 +1444,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,21 +1470,13 @@ 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); } } - /* - * 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; } /** @@ -1619,18 +1484,18 @@ 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. * * @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") - private void resolveProcessorPathEntries(Map> addTo) throws MojoException { + final void resolveProcessorPathEntries(Map> addTo) throws MojoException { List dependencies = annotationProcessorPaths; if (dependencies != null && !dependencies.isEmpty()) { try { @@ -1664,16 +1529,54 @@ private void resolveProcessorPathEntries(Map> addTo) throws } } + /** + * {@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. - * 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(); } @@ -1690,17 +1593,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); @@ -1751,56 +1656,35 @@ 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 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( - List options, Map> dependencies, List 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."); return; } - final var commandLine = new StringBuilder("For trying to compile from the command-line, use:") - .append(System.lineSeparator()) - .append(" ") - .append(executable != null ? executable : compilerId); - boolean hasOptions = false; + 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)) { - 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(); - } - for (Map.Entry> entry : dependencies.entrySet()) { + configuration.format(commandLine, out); + for (Map.Entry> entry : executor.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 = " "; @@ -1808,16 +1692,45 @@ 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('"'); - out.newLine(); + try { + executor.getSourceFiles().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(relativize(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. } } - tipForCommandLineCompilation = commandLine.append(" @").append(path).toString(); + return file; } } 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..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; @@ -25,7 +27,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 +34,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 +65,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. * @@ -106,6 +100,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; @@ -145,14 +141,16 @@ 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(false); + super(PathScope.MAIN_COMPILE); } /** * 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. */ @@ -171,35 +169,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; - } - - /** - * {@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; + public Options parseParameters(final OptionChecker compiler) { + Options configuration = super.parseParameters(compiler); + configuration.addUnchecked(compilerArgs); + configuration.addUnchecked(compilerArgument); + return configuration; } /** @@ -244,7 +225,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; } @@ -258,22 +239,37 @@ 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. * + * @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 * * @deprecated For compatibility with the previous way to build multi-releases JAR file. */ - @Override @Deprecated(since = "4.0.0") - protected void addImplicitDependencies(Map> addTo, boolean hasModuleDeclaration) + private void addImplicitDependencies(List sourceDirectories, Map> addTo) 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)) { @@ -309,8 +305,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/DiagnosticLogger.java b/src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java index 2c720a08..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. * @@ -89,9 +112,13 @@ 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); + JavaFileObject source = diagnostic.getSource(); Diagnostic.Kind kind = diagnostic.getKind(); String style; switch (kind) { @@ -104,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) { @@ -176,7 +205,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 +229,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/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(); } } 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..5e3083ba 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. @@ -154,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) { @@ -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 c12ec8f8..47b5b61a 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; @@ -43,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. @@ -88,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. @@ -337,4 +372,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(); + } } 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..cb2b437b 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 PredicateMOJO from which to take the includes/excludes configuration */ - PathFilter(Collection includes, Collection excludes, Collection incrementalExcludes) { - defaultInclude = includes.isEmpty(); - if (defaultInclude) { - includes = List.of("**"); + PathFilter(AbstractCompilerMojo mojo) { + Collection specified = mojo.getIncludes(); + useDefaultInclude = specified.isEmpty(); + if (useDefaultInclude) { + 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); - 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); + includes = specified.toArray(String[]::new); + excludes = mojo.getExcludes().toArray(String[]::new); + incrementalExcludes = mojo.getIncrementalExcludes().toArray(String[]::new); } /** @@ -158,7 +156,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 +201,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 +293,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 180f7218..7a04e5b0 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java @@ -21,13 +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. @@ -56,10 +61,34 @@ 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; + /** + * 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} @@ -72,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; @@ -80,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. @@ -117,50 +166,170 @@ 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 (release != null) { - String version = release.name(); - 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) { + if (moduleName != null) { outputDirectory = outputDirectory.resolve(moduleName); } 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. + */ + static Path outputDirectoryForReleases(Path outputDirectory) { + // TODO: use Path.resolve(String, String...) with Java 22. + return outputDirectory.resolve("META-INF").resolve("versions"); + } + + /** + * {@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 String version) { + if (version == null || version.isBlank()) { + return null; + } + try { + 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 UnsupportedVersionException("Illegal version number: \"" + version + '"', e); + } + } + + /** + * 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 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, String defaultRelease, Path outputDirectory) { + var release = parse(defaultRelease); // May be null. + var roots = new ArrayList(); + 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( + directory, + source.includes(), + source.excludes(), + fileKind, + source.module().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; + } + /** * 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 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 fromPaths(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 (Path p : compileSourceRoots) { - if (Files.exists(p)) { - // TODO: specify file kind, module name and release version. + for (String file : compileSourceRoots) { + 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, + release, + outputDirectory, + JavaFileObject.Kind.CLASS)); } } return roots; @@ -196,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. * @@ -205,10 +394,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; } @@ -218,7 +411,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..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 68c76086..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; @@ -41,19 +38,26 @@ */ 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 #getReleaseString() + * @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; @@ -73,56 +77,48 @@ 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) { + SourcesForRelease(SourceVersion release) { this.release = release; + files = new ArrayList<>(); roots = new LinkedHashMap<>(); - files = new ArrayList<>(256); 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} 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. */ - 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 = ""; } - roots.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<>()).add(directory.root); + roots.get(moduleName).add(directory.root); directory.getModuleInfo().ifPresent((path) -> moduleInfos.put(directory, null)); } files.add(source.file); } - /** - * 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. - */ - public 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); - } - // TODO: add empty set for all modules present in a release but not in the next release. - 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 @@ -178,4 +174,16 @@ public void close() throws IOException { throw error; } } + + /** + * {@return a string representation for debugging purposes}. + */ + @Override + public String toString() { + 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 ed3b729e..fa5ac6cf 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/TestCompilerMojo.java @@ -18,32 +18,23 @@ */ 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.nio.file.Paths; -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.PathType; -import org.apache.maven.api.ProjectScope; +import org.apache.maven.api.PathScope; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; 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; @@ -70,14 +61,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. * @@ -182,6 +165,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; @@ -192,7 +176,6 @@ public class TestCompilerMojo extends AbstractCompilerMojo { * Its value should be the same as {@link CompilerMojo#outputDirectory}. * * @see CompilerMojo#outputDirectory - * @see #addImplicitDependencies(Map, boolean) */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) protected Path mainOutputDirectory; @@ -211,27 +194,27 @@ public class TestCompilerMojo extends AbstractCompilerMojo { 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. + * 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. * - * @see #getMainModuleName() + *

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

    */ - private String moduleName; + transient boolean hasTestModuleInfo; /** - * 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. + * Whether a {@code module-info.java} file is defined in the main sources. */ - private boolean hasTestModuleInfo; + private transient boolean hasMainModuleInfo; /** - * 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#mainModulePath}, + * 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. @@ -246,14 +229,16 @@ 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(true); + super(PathScope.TEST_COMPILE); } /** * 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. */ @@ -267,37 +252,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; - } - - /** - * {@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(); - } + configuration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument); + return configuration; } /** @@ -387,33 +359,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; @@ -428,14 +380,19 @@ 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) { + hasMainModuleInfo |= root.moduleName != null; + hasTestModuleInfo |= root.getModuleInfo().isPresent(); + if (hasMainModuleInfo & hasTestModuleInfo) { + break; + } + } if (hasTestModuleInfo) { MessageBuilder message = messageBuilderFactory.builder(); message.a("Overwriting the ") @@ -450,168 +407,31 @@ final boolean hasModuleDeclaration(final List roots) throws IOE return useModulePath; } } - return useModulePath && !getMainModuleName().isEmpty(); - } - - /** - * Adds the main compilation output directories as test dependencies. - * - * @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) { - var pathType = hasModuleDeclaration ? JavaPathType.MODULES : JavaPathType.CLASSES; - if (Files.exists(mainOutputDirectory)) { - addTo.computeIfAbsent(pathType, (key) -> new ArrayList<>()).add(mainOutputDirectory); - } - } - - /** - * 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 compileSourceRoots 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) - throws IOException { - for (SourceDirectory dir : compileSourceRoots) { - String moduleToPatch = dir.moduleName; - if (moduleToPatch == null) { - moduleToPatch = getMainModuleName(); - if (moduleToPatch.isEmpty()) { - continue; // No module-info found. - } - if (SUPPORT_LEGACY) { - String testModuleName = getTestModuleName(compileSourceRoots); - 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); - } + return useModulePath && hasMainModuleInfo; } /** - * 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; + hasMainModuleInfo = true; } - }; - 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; + 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 new file mode 100644 index 00000000..92700e59 --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -0,0 +1,637 @@ +/* + * 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.lang.model.SourceVersion; +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.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; +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; +import org.apache.maven.api.services.MavenException; + +/** + * 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, 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 + * 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 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 + * 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. + * + *

    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; + + /** + * 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 + * @throws MavenException if an error occurred while fetching dependencies + * + * @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, mojo.project.getRootDirectory()); + } + this.listener = listener; + encoding = mojo.charset(); + incrementalBuildConfig = mojo.incrementalCompilationConfiguration(); + outputDirectory = Files.createDirectories(mojo.getOutputDirectory()); + sourceDirectories = mojo.getSourceDirectories(outputDirectory); + dependencies = new LinkedHashMap<>(); + /* + * 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 (incrementalBuildConfig.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; + 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 code performs a deep copies for safety. They are unnecessary copies when + * the implementation is `org.apache.maven.impl.DefaultDependencyResolverResult`, + * but we assume for now that it is not worth an optimization. The copies also + * protect `dependencyResolution` from changes in `dependencies`. + */ + dependencyResolution = mojo.resolveDependencies(hasModuleDeclaration); + if (dependencyResolution != null) { + dependencies.putAll(dependencyResolution.getDispatchedPaths()); + dependencies.entrySet().forEach((e) -> e.setValue(new ArrayList<>(e.getValue()))); + } + mojo.resolveProcessorPathEntries(dependencies); + } + + /** + * {@return the source files to compile}. + */ + 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, + * 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 = 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) { + 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(); + } + if (checkClasses && causeOfRebuild == null) { + causeOfRebuild = incrementalBuild.markNewOrModifiedSources(); + } + if (checkDepends && causeOfRebuild == null) { + List fileExtensions = mojo.fileExtensions; + if (fileExtensions == null || fileExtensions.isEmpty()) { + fileExtensions = List.of("class", "jar"); + } + causeOfRebuild = incrementalBuild.dependencyChanges(dependencies.values(), fileExtensions); + } + if (checkOptions && causeOfRebuild == null) { + causeOfRebuild = incrementalBuild.optionChanges(); + } + if (causeOfRebuild != null) { + 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.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. + * + * @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 + */ + @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 { + 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("The source files to compile are:"); + 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; + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, LOCALE, encoding)) { + 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 (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, 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)); + } + } + } + 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`, + * 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, 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. + */ + if (listener instanceof DiagnosticLogger diagnostic) { + diagnostic.logSummary(); + } + } catch (UncheckedIOException e) { + throw e.getCause(); + } + if (success && incrementalBuild != null) { + incrementalBuild.writeCache(); + incrementalBuild = null; + } + 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) { + 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 new file mode 100644 index 00000000..1ecf24c4 --- /dev/null +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java @@ -0,0 +1,400 @@ +/* + * 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.LinkedHashMap; +import java.util.LinkedHashSet; +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.PathType; +import org.apache.maven.api.ProjectScope; + +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 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. + */ + private boolean overwriteMainModuleInfo; + + /** + * Name of the main module to compile, or {@code null} if not yet determined. + * If the project is not modular, then 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; + + /** + * 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. + * + * @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 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) { + 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) { + testInItsOwnModule = true; + continue; // The test classes are in their own module. + } + } + } + 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<>()) + .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; + 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); + } + } + + /** + * {@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. + */ + 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; + } + + /** + * If the given module name is empty, tries to infer a default module name. + */ + @Override + final String inferModuleNameIfMissing(String moduleName) throws IOException { + return (!testInItsOwnModule && 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}. + * + * @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(","); + } + } + 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"); + } + 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); + } + } + + /** + * @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. + Path delete = null; + try { + if (directoryLevelToRemove != null) { + delete = Files.createSymbolicLink(directoryLevelToRemove, directoryLevelToRemove.getParent()); + } + return super.compile(compiler, configuration, otherOutput); + } finally { + if (delete != null) { + Files.delete(delete); + } + } + } + + /** + * 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; + } + } + 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, + * 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}; + } + } +} diff --git a/src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java b/src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java similarity index 50% rename from src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java rename to src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java index 510fb1c4..02558a97 100644 --- a/src/it/multirelease-patterns/packaging-plugin/src/test/java/mr/ATest.java +++ b/src/main/java/org/apache/maven/plugin/compiler/UnsupportedVersionException.java @@ -16,33 +16,32 @@ * specific language governing permissions and limitations * under the License. */ -package mr; +package org.apache.maven.plugin.compiler; -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")); +/** + * 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); } - @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")); + /** + * 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..936a8565 --- /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 contain 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; diff --git a/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTestCase.java b/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTestCase.java index a53c51cc..62e260c9 100644 --- a/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTestCase.java +++ b/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTestCase.java @@ -50,8 +50,8 @@ import org.apache.maven.api.services.MessageBuilderFactory; import org.apache.maven.api.services.ProjectManager; import org.apache.maven.api.services.ToolchainManager; -import org.apache.maven.internal.impl.DefaultMessageBuilderFactory; -import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.impl.DefaultMessageBuilderFactory; +import org.apache.maven.impl.InternalSession; import org.apache.maven.plugin.compiler.stubs.CompilerStub; import org.junit.jupiter.api.Test; @@ -291,6 +291,10 @@ public void testImplicitFlagNotSet( /** * Tests the compilation of a project having a {@code module-info.java} file, together with its tests. * The compilation of tests requires a {@code --patch-module} option, otherwise compilation will fail. + * + *

    Requirements on Windows

    + * Executing the tests on Windows requires the developer mode. + * This is enabled with {@literal Settings > Update & Security > For Developers}. */ @Test @Basedir("${basedir}/target/test-classes/unit/compiler-modular-project")