This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-compiler-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new 2197c22 Build projects that are both multi-module and multi-release
(#959)
2197c22 is described below
commit 2197c222a60f519a1b85bfb0a76b1f8bb990f75b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Oct 14 18:37:24 2025 +0200
Build projects that are both multi-module and multi-release (#959)
Build projects that are both multi-module and multi-release.
Before this commit, a project could be either multi-module or
multi-release, but not both in same time.
As a side effect of the debugging work, this commit also fixes the content
of `target/javac.args` file
(before this commit, the source files for all versions were mixed in the
same `target/javac.args` file).
---
src/it/multirelease-on-classpath/pom.xml | 2 +-
.../multirelease-with-modules/invoker.properties | 19 ++
.../pom.xml | 20 +-
.../src/foo.bar.more/main/java/module-info.java | 19 ++
.../src/foo.bar.more/main/java/more/MainFile.java | 29 +++
.../src/foo.bar.more/main/java/more/OtherFile.java | 29 +++
.../foo.bar.more/main/java_16/more/OtherFile.java | 30 +++
.../src/foo.bar/main/java/foo/MainFile.java | 29 +++
.../src/foo.bar/main/java/foo/OtherFile.java | 29 +++
.../src/foo.bar/main/java/foo/YetAnotherFile.java | 29 +++
.../src/foo.bar/main/java/module-info.java | 19 ++
.../src/foo.bar/main/java_16/foo/OtherFile.java | 34 +++
src/it/multirelease-with-modules/verify.groovy | 47 ++++
.../plugin/compiler/AbstractCompilerMojo.java | 108 +++++++---
.../maven/plugin/compiler/IncrementalBuild.java | 3 +-
.../org/apache/maven/plugin/compiler/Options.java | 8 +-
.../maven/plugin/compiler/SourcePathType.java | 17 ++
.../maven/plugin/compiler/SourcesForRelease.java | 15 ++
.../apache/maven/plugin/compiler/ToolExecutor.java | 179 +++++++++++-----
.../maven/plugin/compiler/ToolExecutorForTest.java | 2 +-
.../plugin/compiler/WorkaroundForPatchModule.java | 237 +++++++++++++++++++++
21 files changed, 803 insertions(+), 101 deletions(-)
diff --git a/src/it/multirelease-on-classpath/pom.xml
b/src/it/multirelease-on-classpath/pom.xml
index 3a47eb9..2ca77ca 100644
--- a/src/it/multirelease-on-classpath/pom.xml
+++ b/src/it/multirelease-on-classpath/pom.xml
@@ -23,7 +23,7 @@
<artifactId>multirelease-on-classpath</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
- <name>Mulirelease in Maven 4</name>
+ <name>Multirelease in Maven 4</name>
<build>
<plugins>
diff --git a/src/it/multirelease-with-modules/invoker.properties
b/src/it/multirelease-with-modules/invoker.properties
new file mode 100644
index 0000000..d8beb8a
--- /dev/null
+++ b/src/it/multirelease-with-modules/invoker.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.goals = clean compile
+invoker.buildResult = success
diff --git a/src/it/multirelease-on-classpath/pom.xml
b/src/it/multirelease-with-modules/pom.xml
similarity index 76%
copy from src/it/multirelease-on-classpath/pom.xml
copy to src/it/multirelease-with-modules/pom.xml
index 3a47eb9..a33f0a4 100644
--- a/src/it/multirelease-on-classpath/pom.xml
+++ b/src/it/multirelease-with-modules/pom.xml
@@ -20,10 +20,10 @@
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0
http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>org.apache.maven.plugins</groupId>
- <artifactId>multirelease-on-classpath</artifactId>
+ <artifactId>multirelease-with-modules</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
- <name>Mulirelease in Maven 4</name>
+ <name>Multirelease with modules</name>
<build>
<plugins>
@@ -41,16 +41,24 @@
<sources>
<source>
- <directory>src/main/java</directory>
+ <module>foo.bar</module>
+ <directory>src/foo.bar/main/java</directory>
<targetVersion>15</targetVersion>
</source>
<source>
- <directory>src/main/java_16</directory>
+ <module>foo.bar</module>
+ <directory>src/foo.bar/main/java_16</directory>
<targetVersion>16</targetVersion>
</source>
<source>
- <directory>src/main/java_17</directory>
- <targetVersion>17</targetVersion>
+ <module>foo.bar.more</module>
+ <directory>src/foo.bar.more/main/java</directory>
+ <targetVersion>15</targetVersion>
+ </source>
+ <source>
+ <module>foo.bar.more</module>
+ <directory>src/foo.bar.more/main/java_16</directory>
+ <targetVersion>16</targetVersion>
</source>
</sources>
</build>
diff --git
a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 0000000..778a3a4
--- /dev/null
+++
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar.more {}
diff --git
a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 0000000..d64f30a
--- /dev/null
+++
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/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 more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile of more");
+ }
+}
diff --git
a/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java
new file mode 100644
index 0000000..54e29b3
--- /dev/null
+++
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/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 more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more");
+ }
+}
diff --git
a/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java
new file mode 100644
index 0000000..4b21485
--- /dev/null
+++
b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.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.
+ */
+package more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+}
diff --git
a/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java
b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 0000000..502f278
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/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-with-modules/src/foo.bar/main/java/foo/OtherFile.java
b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java
new file mode 100644
index 0000000..472210e
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/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-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java
b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java
new file mode 100644
index 0000000..ab5f900
--- /dev/null
+++
b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.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 YetAnotherFile {
+ public static void main(String[] args) {
+ System.out.println("YetAnotherFile");
+ }
+}
diff --git
a/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
new file mode 100644
index 0000000..38f61c0
--- /dev/null
+++ b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar {}
diff --git
a/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java
b/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java
new file mode 100644
index 0000000..cbfa0b9
--- /dev/null
+++
b/src/it/multirelease-with-modules/src/foo.bar/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-with-modules/verify.groovy
b/src/it/multirelease-with-modules/verify.groovy
new file mode 100644
index 0000000..a4d068a
--- /dev/null
+++ b/src/it/multirelease-with-modules/verify.groovy
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.JarFile
+
+def baseVersion = 59; // Java 15
+def nextVersion = 60; // Java 16
+
+assert baseVersion == getMajor(new File( basedir,
"target/classes/foo.bar/foo/MainFile.class"))
+assert baseVersion == getMajor(new File( basedir,
"target/classes/foo.bar/foo/OtherFile.class"))
+assert baseVersion == getMajor(new File( basedir,
"target/classes/foo.bar/foo/YetAnotherFile.class"))
+assert baseVersion == getMajor(new File( basedir,
"target/classes/foo.bar.more/more/MainFile.class"))
+assert baseVersion == getMajor(new File( basedir,
"target/classes/foo.bar.more/more/OtherFile.class"))
+assert nextVersion == getMajor(new File( basedir,
"target/classes/META-INF/versions/16/foo.bar/foo/OtherFile.class"))
+assert nextVersion == getMajor(new File( basedir,
"target/classes/META-INF/versions/16/foo.bar.more/more/OtherFile.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/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
index c2f7a50..6663659 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
@@ -40,6 +40,7 @@ import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
+import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@@ -1311,6 +1312,7 @@ public abstract class AbstractCompilerMojo implements
Mojo {
* @throws IOException if an input file cannot be read
* @throws MojoException if the compilation failed
*/
+ @SuppressWarnings("UseSpecificCatch")
private void compile(final JavaCompiler compiler, final Options
configuration) throws IOException {
final ToolExecutor executor = createExecutor(null);
if (!executor.applyIncrementalBuild(this, configuration)) {
@@ -1353,7 +1355,7 @@ public abstract class AbstractCompilerMojo implements
Mojo {
if (!success || verbose || logger.isDebugEnabled()) {
IOException suppressed = null;
try {
- writeDebugFile(executor, configuration);
+ writeDebugFile(executor, configuration, success);
if (success && tipForCommandLineCompilation != null) {
logger.debug(tipForCommandLineCompilation);
tipForCommandLineCompilation = null;
@@ -1536,6 +1538,7 @@ public abstract class AbstractCompilerMojo implements
Mojo {
* {@code processor}, {@code classpath-processor} or {@code
modular-processor}.
*/
@Deprecated(since = "4.0.0")
+ @SuppressWarnings("UseSpecificCatch")
final void resolveProcessorPathEntries(Map<PathType, List<Path>> addTo)
throws MojoException {
List<DependencyCoordinate> dependencies = annotationProcessorPaths;
if (dependencies != null && !dependencies.isEmpty()) {
@@ -1699,11 +1702,13 @@ public abstract class AbstractCompilerMojo implements
Mojo {
*
* @param executor the executor that compiled the classes
* @param configuration options provided to the compiler
+ * @param showBaseVersion whether the tip shown to user suggests the base
Java release instead of the last one
* @throws IOException if an error occurred while writing the debug file
*/
- private void writeDebugFile(final ToolExecutor executor, final Options
configuration) throws IOException {
- final Path path = getDebugFilePath();
- if (path == null) {
+ private void writeDebugFile(final ToolExecutor executor, final Options
configuration, final boolean showBaseVersion)
+ throws IOException {
+ final Path debugFilePath = getDebugFilePath();
+ if (debugFilePath == null) {
logger.warn("The <debugFileName> parameter should not be empty.");
return;
}
@@ -1719,40 +1724,77 @@ public abstract class AbstractCompilerMojo implements
Mojo {
.append(chdir);
}
commandLine.append(System.lineSeparator()).append("
").append(executable != null ? executable : compilerId);
- try (BufferedWriter out = Files.newBufferedWriter(path)) {
- configuration.format(commandLine, out);
- for (Map.Entry<PathType, List<Path>> entry :
executor.dependencies.entrySet()) {
- List<Path> files = entry.getValue();
- files = files.stream().map(this::relativize).toList();
- String separator = "";
- for (String element : entry.getKey().option(files)) {
- out.write(separator);
- out.write(element);
- separator = " ";
+ Path pathForRelease = debugFilePath;
+ /*
+ * The following loop will iterate over all groups of source files
compiled for the same Java release,
+ * starting with the base release. If the project is not a
multi-release project, it iterates only once.
+ * If the compilation failed, the loop will stop after the first Java
release for which an error occurred.
+ */
+ final int count = executor.sourcesForDebugFile.size();
+ final int indexToShow = showBaseVersion ? 0 : count - 1;
+ for (int i = 0; i < count; i++) {
+ final SourcesForRelease sources =
executor.sourcesForDebugFile.get(i);
+ if (i != 0) {
+ String version =
sources.outputForRelease.getFileName().toString();
+ String filename = debugFilePath.getFileName().toString();
+ int s = filename.lastIndexOf('.');
+ if (s >= 0) {
+ filename = filename.substring(0, s) + '-' + version +
filename.substring(s);
+ } else {
+ filename = filename + '-' + version;
+ }
+ pathForRelease = debugFilePath.resolveSibling(filename);
+ }
+ /*
+ * Write the `javac.args` or `javac-<version>.args` file where
`<version>` is the targeted Java release.
+ * The `-J` options need to be on the command line rather than in
the file, and therefore can be written
+ * only once.
+ */
+ try (BufferedWriter out = Files.newBufferedWriter(pathForRelease))
{
+ configuration.setRelease(sources.getReleaseString());
+ configuration.format((i == indexToShow) ? commandLine : null,
out);
+ for (Map.Entry<PathType, List<Path>> entry :
sources.dependencySnapshot.entrySet()) {
+ writeOption(out, entry.getKey(), entry.getValue());
+ }
+ for (Map.Entry<String, Set<Path>> root :
sources.roots.entrySet()) {
+ String moduleName = root.getKey();
+ writeOption(out, SourcePathType.valueOf(moduleName),
root.getValue());
}
+ out.write("-d \"");
+ out.write(relativize(sources.outputForRelease).toString());
+ out.write('"');
out.newLine();
+ for (final Path file : sources.files) {
+ out.write('"');
+ out.write(relativize(file).toString());
+ out.write('"');
+ out.newLine();
+ }
}
- out.write("-d \"");
- out.write(relativize(getOutputDirectory()).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();
+ }
+ Path path = relativize(showBaseVersion ? debugFilePath :
pathForRelease);
+ tipForCommandLineCompilation = commandLine.append("
@").append(path).toString();
+ }
+
+ /**
+ * Writes the paths for the given Java compiler option.
+ *
+ * @param out where to write
+ * @param type the type of path to write as a compiler option
+ * @param files the paths associated to the specified option
+ * @throws IOException in an error occurred while writing to the output
+ */
+ private void writeOption(BufferedWriter out, PathType type,
Collection<Path> files) throws IOException {
+ if (!files.isEmpty()) {
+ files = files.stream().map(this::relativize).toList();
+ String separator = "";
+ for (String element : type.option(files)) {
+ out.write(separator);
+ out.write(element);
+ separator = " ";
}
+ out.newLine();
}
- tipForCommandLineCompilation =
- commandLine.append(" @").append(relativize(path)).toString();
}
/**
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 5e3083b..e0a8ee5 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/IncrementalBuild.java
@@ -624,9 +624,8 @@ final class IncrementalBuild {
* Each given root can be either a regular file (typically a JAR file) or
a directory.
* Directories are scanned recursively.
*
- * @param directories files or directories to scan
+ * @param dependencies files or directories to scan
* @param fileExtensions extensions of the file to check (usually "jar"
and "class")
- * @param changeTime the time at which a file is considered as changed
* @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 scanning the directories
*
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 47b5b61..58b778c 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/Options.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/Options.java
@@ -387,10 +387,12 @@ public final class Options {
continue;
}
if (option.startsWith("-J")) {
- if (commandLine.length() != 0) {
- commandLine.append(' ');
+ if (commandLine != null) {
+ if (commandLine.length() != 0) {
+ commandLine.append(' ');
+ }
+ commandLine.append(option);
}
- commandLine.append(option);
continue;
}
if (hasOptions) {
diff --git a/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java
b/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java
index 1e986a5..547920f 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/SourcePathType.java
@@ -20,6 +20,7 @@ package org.apache.maven.plugin.compiler;
import java.io.File;
import java.nio.file.Path;
+import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
@@ -106,6 +107,22 @@ final class SourcePathType implements PathType {
return new String[] {option().get(), joiner.toString()};
}
+ /**
+ * {@return a hash code value based on the module name}.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(moduleName) + 17;
+ }
+
+ /**
+ * {@return whether the given object represents the same source path as
this object}.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return (obj instanceof SourcePathType) && Objects.equals(moduleName,
((SourcePathType) obj).moduleName);
+ }
+
/**
* {@return a string representation for debugging purposes}.
*/
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 017cb95..0778381 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/SourcesForRelease.java
@@ -30,6 +30,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.apache.maven.api.PathType;
+
/**
* Source files for a specific Java release. Instances of {@code
SourcesForRelease} are created from
* a list of {@link SourceFile} after the sources have been filtered according
include and exclude filters.
@@ -74,6 +76,19 @@ final class SourcesForRelease implements Closeable {
*/
private SourceDirectory lastDirectoryAdded;
+ /**
+ * Snapshot of {@link ToolExecutor#dependencies}.
+ * This information is saved in case a {@code target/javac.args} debug
file needs to be written.
+ */
+ Map<PathType, List<Path>> dependencySnapshot;
+
+ /**
+ * The output directory for the release. This is either the base output
directory or a sub-directory
+ * in {@code META-INF/versions/}. This field is not used by this class,
but made available for making
+ * easier to write the {@code target/javac.args} debug file.
+ */
+ Path outputForRelease;
+
/**
* Creates an initially empty instance for the given Java release.
*
diff --git a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
index 4177459..62f40dc 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
@@ -34,7 +34,6 @@ 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;
@@ -45,7 +44,6 @@ 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;
@@ -130,17 +128,17 @@ public class ToolExecutor {
* The path type can be the class-path, module-path, annotation processor
path, patched path, <i>etc.</i>
* Some path types include a module name.
*
+ * <h4>Modifications during the build of multi-release project</h4>
+ * When building a multi-release project, values associated to {@code
--class-path}, {@code --module-path}
+ * or {@code --patch-module} options are modified every time that {@code
ToolExecutor} compiles for a new
+ * Java release. The output directories for the previous Java releases are
inserted as the first elements
+ * of their lists, or new entries are created if no list existed
previously for an option.
+ *
* @see #dependencies(PathType)
+ * @see #prependDependency(PathType, Path)
*/
protected final Map<PathType, List<Path>> 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<Path> classpath;
-
/**
* The destination directory (or class output directory) for class files.
* This directory will be given to the {@code -d} Java compiler option
@@ -188,6 +186,13 @@ public class ToolExecutor {
*/
protected final Log logger;
+ /**
+ * The sources to write in the {@code target/javac.args} debug files.
+ * This list contains only the sources for which the compiler has been
executed, successfully or not.
+ * If a compilation error occurred, the last element in the list contains
the sources where the error occurred.
+ */
+ final List<SourcesForRelease> sourcesForDebugFile;
+
/**
* Creates a new task by taking a snapshot of the current configuration of
the given <abbr>MOJO</abbr>.
* This constructor creates the {@linkplain #outputDirectory output
directory} if it does not already exist.
@@ -206,8 +211,8 @@ public class ToolExecutor {
logger = mojo.logger;
if (listener == null) {
- listener =
- new DiagnosticLogger(logger, mojo.messageBuilderFactory,
LOCALE, mojo.project.getRootDirectory());
+ Path root = mojo.project.getRootDirectory();
+ listener = new DiagnosticLogger(logger,
mojo.messageBuilderFactory, LOCALE, root);
}
this.listener = listener;
encoding = mojo.charset();
@@ -215,6 +220,7 @@ public class ToolExecutor {
outputDirectory = Files.createDirectories(mojo.getOutputDirectory());
sourceDirectories = mojo.getSourceDirectories(outputDirectory);
dependencies = new LinkedHashMap<>();
+ sourcesForDebugFile = new ArrayList<>();
/*
* 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
@@ -254,26 +260,21 @@ public class ToolExecutor {
/*
* 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())));
+ copyDependencyValues();
}
mojo.resolveProcessorPathEntries(dependencies);
}
/**
- * {@return the source files to compile}.
+ * Copies all values of the dependency map in unmodifiable lists.
+ * This is used for creating a snapshot of the current state of the
dependency map.
*/
- public Stream<Path> getSourceFiles() {
- return sourceFiles.stream().map((s) -> s.file);
+ private void copyDependencyValues() {
+ dependencies.entrySet().forEach((entry) ->
entry.setValue(List.copyOf(entry.getValue())));
}
/**
@@ -366,7 +367,17 @@ public class ToolExecutor {
* @param pathType type of path for which to get the dependencies
*/
protected List<Path> dependencies(PathType pathType) {
- return dependencies.computeIfAbsent(pathType, (key) -> new
ArrayList<>());
+ return dependencies.compute(pathType, (key, paths) -> {
+ if (paths == null) {
+ return new ArrayList<>();
+ } else if (paths instanceof ArrayList<?>) {
+ return paths;
+ } else {
+ var copy = new ArrayList<Path>(paths.size() + 4); //
Anticipate the addition of new elements.
+ copy.addAll(paths);
+ return copy;
+ }
+ });
}
/**
@@ -389,8 +400,6 @@ public class ToolExecutor {
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:
@@ -399,9 +408,11 @@ public class ToolExecutor {
* 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."
+ * using the --class-path option or one of its
alternate forms."
*/
+ paths = new ArrayList<>(paths);
paths.add(outputDirectory);
+ entry.setValue(paths);
}
}
fileManager.setLocationFromPaths(value, paths);
@@ -414,14 +425,7 @@ public class ToolExecutor {
*/
Optional<JavaFileManager.Location> 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);
- }
- }
+ fileManager.setLocationForModule(location.get(),
type.moduleName(), paths);
continue;
}
}
@@ -436,6 +440,24 @@ public class ToolExecutor {
}
}
+ /**
+ * Inserts the given path as the first element of the list of paths of the
given type.
+ * The main purpose of this method is during the build of a multi-release
project,
+ * for adding the output directory of the code targeting the previous Java
release
+ * before to compile the code targeting the next Java release. In this
context,
+ * the {@code type} argument usually identifies a {@code --class-path},
+ * {@code --module-path} or {@code --patch-module} option.
+ *
+ * @param pathType type of path for which to add an element
+ * @param first the path to put first
+ * @return the new paths for the given type, as a modifiable list
+ */
+ protected List<Path> prependDependency(final PathType pathType, final Path
first) {
+ List<Path> paths = dependencies(pathType);
+ paths.add(0, first);
+ return paths;
+ }
+
/**
* Ensures that the given value is non-null, replacing null values by the
latest version.
*/
@@ -487,6 +509,24 @@ public class ToolExecutor {
return result.values();
}
+ /**
+ * Creates the file manager which will be used by the compiler.
+ * This method does not configure the locations (sources, dependencies,
<i>etc.</i>).
+ * Locations will be set by {@link #compile(JavaCompiler, Options,
Writer)} on the
+ * file manager returned by this method.
+ *
+ * @param compiler the compiler
+ * @param workaround whether to apply {@link WorkaroundForPatchModule}
+ * @return the file manager to use
+ */
+ private StandardJavaFileManager createFileManager(JavaCompiler compiler,
boolean workaround) {
+ StandardJavaFileManager fileManager =
compiler.getStandardFileManager(listener, LOCALE, encoding);
+ if (workaround && WorkaroundForPatchModule.ENABLED) {
+ fileManager = new WorkaroundForPatchModule(fileManager);
+ }
+ return fileManager;
+ }
+
/**
* Runs the compilation task.
*
@@ -498,12 +538,13 @@ public class ToolExecutor {
* @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")
+ @SuppressWarnings("checkstyle:MethodLength")
public boolean compile(final JavaCompiler compiler, final Options
configuration, final Writer otherOutput)
throws IOException {
/*
* Announce what the compiler is about to do.
*/
+ sourcesForDebugFile.clear();
if (sourceFiles.isEmpty()) {
String message = "No sources to compile.";
try {
@@ -530,7 +571,7 @@ public class ToolExecutor {
* disposal in order to reuse its cache.
*/
boolean success = true;
- try (StandardJavaFileManager fileManager =
compiler.getStandardFileManager(listener, LOCALE, encoding)) {
+ try (StandardJavaFileManager fileManager = createFileManager(compiler,
hasModuleDeclaration)) {
setDependencyPaths(fileManager);
if (!generatedSourceDirectories.isEmpty()) {
fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT,
generatedSourceDirectories);
@@ -548,13 +589,18 @@ public class ToolExecutor {
Path outputForRelease = null;
boolean isClasspathProject = false;
boolean isModularProject = false;
+ String defaultModuleName = null;
configuration.setRelease(unit.getReleaseString());
for (final Map.Entry<String, Set<Path>> root :
unit.roots.entrySet()) {
- final String moduleName =
inferModuleNameIfMissing(root.getKey());
+ final String declaredModuleName = root.getKey();
+ final String moduleName =
inferModuleNameIfMissing(declaredModuleName);
if (moduleName.isEmpty()) {
isClasspathProject = true;
} else {
isModularProject = true;
+ if (declaredModuleName.isEmpty()) { // Modular project
using package source hierarchy.
+ defaultModuleName = moduleName;
+ }
}
if (isClasspathProject & isModularProject) {
throw new CompilationFailureException("Mix of modular
and non-modular sources.");
@@ -567,40 +613,46 @@ public class ToolExecutor {
}
outputForRelease = outputDirectory; // Modified below if
compiling a non-base release.
if (isVersioned) {
+ outputForRelease = Files.createDirectories(
+
SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release));
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);
+ List<Path> classpath =
prependDependency(JavaPathType.CLASSES, 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));
+ Path latestOutputForModule =
latestOutputDirectory.resolve(moduleName);
+ JavaPathType.Modular pathType =
JavaPathType.patchModule(moduleName);
+ List<Path> paths = prependDependency(pathType,
latestOutputForModule);
+
fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH,
moduleName, paths);
}
}
}
+ /*
+ * At this point, we finished to set the source paths. We have
also modified the class-path or
+ * patched the modules with the output directories of codes
compiled for lower Java releases.
+ * The `defaultModuleName` is an adjustment done when the
project is a Java module, but still
+ * organized in a package source hierarchy instead of a module
source hierarchy. Updating the
+ * `unit.roots` map is not needed for this class, but done in
case a `target/javac.args` file
+ * will be written after the compilation.
+ */
+ if (defaultModuleName != null) {
+ Set<Path> paths = unit.roots.remove("");
+ if (paths != null) {
+ unit.roots.put(defaultModuleName, paths);
+ }
+ }
+ copyDependencyValues();
+ unit.dependencySnapshot = new LinkedHashMap<>(dependencies);
fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT,
Set.of(outputForRelease));
latestOutputDirectory = outputForRelease;
+ unit.outputForRelease = 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`,
@@ -610,8 +662,25 @@ public class ToolExecutor {
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);
+ StandardJavaFileManager workaround = fileManager;
+ boolean workaroundNeedsClose = false;
+ // Check flag separately to clearly indicate this entire
block is a workaround hack.
+ if (WorkaroundForPatchModule.ENABLED) {
+ if (workaround instanceof WorkaroundForPatchModule wp)
{
+ workaround = wp.getFileManagerIfUsable();
+ if (workaround == null) {
+ workaround = createFileManager(compiler,
false);
+ wp.copyTo(workaround);
+ workaroundNeedsClose = true;
+ }
+ }
+ }
+ task = compiler.getTask(otherOutput, workaround, listener,
configuration.options, null, sources);
success = c.compile(task);
+ if (workaroundNeedsClose) {
+ workaround.close();
+ }
+ sourcesForDebugFile.add(unit);
if (!success) {
break compile;
}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
index 6be8b69..8e4ba65 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
@@ -199,7 +199,7 @@ class ToolExecutorForTest extends ToolExecutor {
}
}
}
- dependencies(pathType).add(0, mainOutputDirectory);
+ prependDependency(pathType, mainOutputDirectory);
}
}
diff --git
a/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
b/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
new file mode 100644
index 0000000..e659a84
--- /dev/null
+++
b/src/main/java/org/apache/maven/plugin/compiler/WorkaroundForPatchModule.java
@@ -0,0 +1,237 @@
+/*
+ * 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.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.api.JavaPathType;
+
+/**
+ * Workaround for a {@code javax.tools} method which seems not yet supported
on all compilers.
+ * At least with OpenJDK 24, an {@link UnsupportedOperationException} may
occur during the call to
+ * {@code fileManager.setLocationForModule(StandardLocation.PATCH_MODULE_PATH,
moduleName, paths)}.
+ * The workaround is to format the paths in a {@code --patch-module} option
instead.
+ * The problem is that we can specify this option only once per file manager
instance.
+ *
+ * <p>We may remove this workaround in a future version of the Maven Compiler
Plugin
+ * if the {@code UnsupportedOperationException} is fixed in a future Java
release.
+ * For checking if this workaround is still necessary, set {@link #ENABLED} to
{@code false}
+ * and run the JUnit tests.</p>
+ *
+ * @author Martin Desruisseaux
+ */
+final class WorkaroundForPatchModule extends
ForwardingJavaFileManager<StandardJavaFileManager>
+ implements StandardJavaFileManager {
+ /**
+ * Set this flag to {@code false} for testing if this workaround is still
necessary.
+ */
+ static final boolean ENABLED = true;
+
+ /**
+ * All locations that have been successfully specified to the file manager
through programmatic API.
+ * This set excludes the {@code PATCH_MODULE_PATH} locations which were
defined using the workaround
+ * described in class Javadoc.
+ */
+ private final Set<JavaFileManager.Location> definedLocations;
+
+ /**
+ * The locations that we had to define by formatting a {@code
--patch-module} option.
+ * Keys are module names and values are the paths for the associated
module.
+ */
+ private final Map<String, Collection<? extends Path>> patchesAsOption;
+
+ /**
+ * Whether the caller needs to create a new file manager.
+ * It happens when we have been unable to set a {@code --patch-module}
option on the current file manager.
+ */
+ private boolean needsNewFileManager;
+
+ /**
+ * Creates a new workaround for the given file manager.
+ */
+ WorkaroundForPatchModule(final StandardJavaFileManager fileManager) {
+ super(fileManager);
+ definedLocations = new HashSet<>();
+ patchesAsOption = new HashMap<>();
+ }
+
+ /**
+ * {@return the original file manager, or {@code null} if the caller needs
to create a new one}.
+ * The returned value is {@code null} when we have been unable to set a
{@code --patch-module}
+ * option on the current file manager. In such case, the caller should
create a new file manager
+ * and configure it with {@link #copyTo(StandardJavaFileManager)}.
+ */
+ StandardJavaFileManager getFileManagerIfUsable() {
+ return needsNewFileManager ? null : fileManager;
+ }
+
+ /**
+ * Copies the locations defined in this file manager to the given file
manager.
+ *
+ * @param target where to copy the locations
+ * @throws IOException if a location cannot be set on the target file
manager
+ */
+ void copyTo(final StandardJavaFileManager target) throws IOException {
+ for (JavaFileManager.Location location : definedLocations) {
+ target.setLocation(location, fileManager.getLocation(location));
+ }
+ for (Map.Entry<String, Collection<? extends Path>> entry :
patchesAsOption.entrySet()) {
+ Collection<? extends Path> paths = entry.getValue();
+ String moduleName = entry.getKey();
+ try {
+
target.setLocationForModule(StandardLocation.PATCH_MODULE_PATH, moduleName,
paths);
+ } catch (UnsupportedOperationException e) {
+ specifyAsOption(target, JavaPathType.patchModule(moduleName),
paths, e);
+ }
+ }
+ }
+
+ /**
+ * Sets a module path by asking the file manager to parse an option
formatted by this method.
+ * Invoked when a module path cannot be specified through the API
+ * This is the workaround described in class Javadoc.
+ *
+ * @param fileManager the file manager on which an attempt to set the
location has been made and failed
+ * @param type the type of path together with the module name
+ * @param paths the paths to set
+ * @param cause the exception that occurred when invoking the standard API
+ * @throws CompilationFailureException if this workaround doesn't work
neither
+ */
+ private static void specifyAsOption(
+ StandardJavaFileManager fileManager,
+ JavaPathType.Modular type,
+ Collection<? extends Path> paths,
+ UnsupportedOperationException cause)
+ throws IOException {
+
+ var it = Arrays.asList(type.option(paths)).iterator();
+ if (!fileManager.handleOption(it.next(), it) || it.hasNext()) {
+ throw new CompilationFailureException("Cannot handle " + type,
cause);
+ }
+ }
+
+ /**
+ * Adds the given module path to the file manager.
+ * If we cannot do that using the programmatic API, formats as a
command-line option.
+ */
+ @Override
+ public void setLocationForModule(
+ JavaFileManager.Location location, String moduleName, Collection<?
extends Path> paths) throws IOException {
+
+ if (paths.isEmpty()) {
+ return;
+ }
+ final boolean isPatch = (location ==
StandardLocation.PATCH_MODULE_PATH);
+ if (isPatch && patchesAsOption.replace(moduleName, paths) != null) {
+ /*
+ * The patch was already specified by formatting the
`--patch-module` option.
+ * We cannot do that again, because that option can appear only
once per module.
+ */
+ needsNewFileManager = true;
+ return;
+ }
+ try {
+ fileManager.setLocationForModule(location, moduleName, paths);
+ } catch (UnsupportedOperationException e) {
+ if (isPatch) {
+ specifyAsOption(fileManager,
JavaPathType.patchModule(moduleName), paths, e);
+ patchesAsOption.put(moduleName, paths);
+ return;
+ }
+ throw e;
+ }
+ definedLocations.add(fileManager.getLocationForModule(location,
moduleName));
+ }
+
+ /**
+ * Adds the given path to the file manager.
+ */
+ @Override
+ public void setLocationFromPaths(JavaFileManager.Location location,
Collection<? extends Path> paths)
+ throws IOException {
+ fileManager.setLocationFromPaths(location, paths);
+ definedLocations.add(location);
+ }
+
+ @Override
+ public void setLocation(Location location, Iterable<? extends File> files)
throws IOException {
+ fileManager.setLocation(location, files);
+ definedLocations.add(location);
+ }
+
+ @Override
+ public Iterable<? extends File> getLocation(Location location) {
+ return fileManager.getLocation(location);
+ }
+
+ @Override
+ public Iterable<? extends Path> getLocationAsPaths(Location location) {
+ return fileManager.getLocationAsPaths(location);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject> getJavaFileObjects(String...
names) {
+ return fileManager.getJavaFileObjects(names);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject> getJavaFileObjects(File...
files) {
+ return fileManager.getJavaFileObjects(files);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject> getJavaFileObjects(Path...
paths) {
+ return fileManager.getJavaFileObjects(paths);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromStrings(Iterable<String> names) {
+ return fileManager.getJavaFileObjectsFromStrings(names);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromFiles(Iterable<? extends File> files) {
+ return fileManager.getJavaFileObjectsFromFiles(files);
+ }
+
+ @Override
+ public Iterable<? extends JavaFileObject>
getJavaFileObjectsFromPaths(Collection<? extends Path> paths) {
+ return fileManager.getJavaFileObjectsFromPaths(paths);
+ }
+
+ @Override
+ public Path asPath(FileObject file) {
+ return fileManager.asPath(file);
+ }
+}