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/
[](https://search.maven.org/artifact/org.apache.maven.plugins/maven-compiler-plugin)
[](https://search.maven.org/artifact/org.apache.maven.plugins/maven-compiler-plugin)
[](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-39.7.16.0.05.17.0
- 4.0.0-beta-3
+ 4.0.0-beta-42.15.00.9.0.M23.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.pluginsmaven-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:
+ *
+ *
+ *
+ * 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:
+ *
+ *
+ *
Get a Java compiler by a call to {@link #compiler()}.
+ *
Get the options to give to the compiler by a call to {@link #parseParameters(OptionChecker)}.
+ *
Get an executor with {@link #createExecutor(DiagnosticListener)} with the default listener.
+ *
{@linkplain ToolExecutor#applyIncrementalBuild Apply the incremental build} if enabled.
+ *
{@linkplain ToolExecutor#compile Execute the compilation}.
+ *
Shows messages in the {@linkplain #logger}.
+ *
*
* @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 super JavaFileObject> 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 extends JavaFileObject> 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 super JavaFileObject> 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 extends JavaFileObject> 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 extends JavaFileObject> 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 extends Path> 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:
*
*
*
If {@link #moduleName} is non-null, then the module name is appended.
- *
If {@link #release} is non-null, then the next elements in the paths are
+ *
If {@link #isVersioned} is {@code true}, then the next elements in the paths are
* {@code "META-INF/versions/"} where {@code } is the release number.
*
+ *
+ * @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 extends Path> 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 super JavaFileObject> 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 super JavaFileObject> 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 super JavaFileObject> 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 extends JavaFileObject> 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 super JavaFileObject> 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")