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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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 &lt;Source&gt;}.
+ * Another {@code &lt;Source&gt;}.
+ */
+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);
+    }
+}

Reply via email to