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 1ed03b8  Fix multi-release support when module-info is not in the base 
class (#948)
1ed03b8 is described below

commit 1ed03b8a710af42942666b0625447f699afb18d3
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Jul 16 14:33:34 2025 +0200

    Fix multi-release support when module-info is not in the base class (#948)
    
    The plugin needs to be better at detecting whether to put the result 
targeting previous Java releases on the class-path or module-path.
    This change requires better detection of `module-info.class` or 
`module-info.java` files which may be in non-straightforward directories.
    More appropriate use of module-path also implies the use of 
`--module-source-path` instead of `--source-path` when patching a Java module,
    even if Oracle's documentation of `javac` said that the use of 
`--module-source-path` option is not mandatory when there is only one module.
    The plugin uses `--module-source-path` because `javac` seems to not always 
find `module-info` when it is located in a non-obvious directory.
    
    `--module-source-path` causes `javac` to create an extra directory with the 
module name,
    which is not the directory layout that Maven 3 users and other Maven 
plugins are used to.
    The Maven Compiler Plugin was redirecting the `javac` output using a 
symbolic link,
    but it appeared to be sometime a source of confusion because of files in 
two places.
    This commit replaces the symbolic link by temporary renaming of directories.
    
    Note: all this complexity is an effort to mimic the Maven 3 behaviour for 
compatibility reasons.
    This complexity does not apply when the new `<sources>` element of Maven 4 
is used,
    because we accept that `javac` produces a slightly different dirctory 
layout in such case.
---
 .../singleproject-modular/invoker.properties       |  17 ++
 .../singleproject-modular/pom.xml                  | 156 ++++++++++++++
 .../src/main/java/base/Base.java                   |  26 +++
 .../singleproject-modular/src/main/java/mr/A.java  |  32 +++
 .../singleproject-modular/src/main/java/mr/I.java  |  23 ++
 .../src/main/java17/mr/A.java                      |  34 +++
 .../src/main/java9/module-info.java                |  22 ++
 .../singleproject-modular/src/main/java9/mr/A.java |  34 +++
 .../src/test/java/mr/ATest.java                    |  48 +++++
 .../singleproject-modular/verify.groovy            |  83 ++++++++
 .../plugin/compiler/AbstractCompilerMojo.java      |  28 ++-
 .../apache/maven/plugin/compiler/CompilerMojo.java | 231 ++++++++++++++++-----
 .../plugin/compiler/ModuleDirectoryRemover.java    |  93 +++++++++
 .../maven/plugin/compiler/SourceDirectory.java     |   5 +-
 .../apache/maven/plugin/compiler/ToolExecutor.java |  42 ++--
 .../maven/plugin/compiler/ToolExecutorForTest.java |  27 +--
 src/site/markdown/examples/set-compiler-release.md |   2 +-
 src/site/markdown/multirelease.md                  |   6 +-
 18 files changed, 816 insertions(+), 93 deletions(-)

diff --git 
a/src/it/multirelease-patterns/singleproject-modular/invoker.properties 
b/src/it/multirelease-patterns/singleproject-modular/invoker.properties
new file mode 100644
index 0000000..8e4e5be
--- /dev/null
+++ b/src/it/multirelease-patterns/singleproject-modular/invoker.properties
@@ -0,0 +1,17 @@
+# 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 = verify
diff --git a/src/it/multirelease-patterns/singleproject-modular/pom.xml 
b/src/it/multirelease-patterns/singleproject-modular/pom.xml
new file mode 100644
index 0000000..7aeed2a
--- /dev/null
+++ b/src/it/multirelease-patterns/singleproject-modular/pom.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0                           
    http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>multirelease</groupId>
+
+  <artifactId>multirelease</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <name>Single Project :: Runtime</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13.1</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>@project.version@</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>@version.maven-jar-plugin@</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-failsafe-plugin</artifactId>
+          <version>@version.maven-surefire@</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>@version.maven-surefire@</version>
+          <configuration>
+            <!-- this shows that the Java 9 code isn't tested -->
+            <testFailureIgnore>true</testFailureIgnore>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <!-- TODO: remove source and target after we identified where Maven 
inherits those values. -->
+          <source />
+          <target />
+          <release>8</release>
+        </configuration>
+        <executions>
+          <execution>
+            <id>jdk9</id>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+            <configuration>
+              <release>9</release>
+              <compileSourceRoots>
+                
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
+              </compileSourceRoots>
+              <multiReleaseOutput>true</multiReleaseOutput>
+            </configuration>
+          </execution>
+          <execution>
+            <id>jdk17</id>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+            <configuration>
+              <release>17</release>
+              <compileSourceRoots>
+                
<compileSourceRoot>${project.basedir}/src/main/java17</compileSourceRoot>
+              </compileSourceRoots>
+              <multiReleaseOutput>true</multiReleaseOutput>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- Rerun unittests with the multirelease jar, cannot be done with 
exploded directory of classes  -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <configuration>
+          <includes>
+            <include>**/*Test.java</include>
+          </includes>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>jdk9</id>
+      <activation>
+        <jdk>[9,)</jdk>
+      </activation>
+      <build>
+        <pluginManagement>
+          <plugins>
+            <plugin>
+              <groupId>org.apache.maven.plugins</groupId>
+              <artifactId>maven-jar-plugin</artifactId>
+              <executions>
+                <execution>
+                  <id>default-jar</id>
+                  <configuration>
+                    <archive>
+                      <manifestEntries>
+                        <Multi-Release>true</Multi-Release>
+                      </manifestEntries>
+                    </archive>
+                  </configuration>
+                </execution>
+              </executions>
+            </plugin>
+          </plugins>
+        </pluginManagement>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/main/java/base/Base.java
 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java/base/Base.java
new file mode 100644
index 0000000..19ec7d8
--- /dev/null
+++ 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java/base/Base.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package base;
+
+public class Base {
+
+    public static String get() {
+        return "BASE";
+    }
+}
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/main/java/mr/A.java 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java/mr/A.java
new file mode 100644
index 0000000..0e48eee
--- /dev/null
+++ b/src/it/multirelease-patterns/singleproject-modular/src/main/java/mr/A.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package mr;
+
+import base.Base;
+
+public class A implements I {
+    public static String getString() {
+        return Base.get() + " -> 8";
+    }
+
+    @Override
+    public Class<?> introducedClass() {
+        return java.time.LocalDateTime.class;
+    }
+}
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/main/java/mr/I.java 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java/mr/I.java
new file mode 100644
index 0000000..a052326
--- /dev/null
+++ b/src/it/multirelease-patterns/singleproject-modular/src/main/java/mr/I.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package mr;
+
+public interface I {
+    Class<?> introducedClass();
+}
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/main/java17/mr/A.java 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java17/mr/A.java
new file mode 100644
index 0000000..c1dc066
--- /dev/null
+++ 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java17/mr/A.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 mr;
+
+import java.util.Optional;
+
+import base.Base;
+
+public class A implements I {
+    public static String getString() {
+        return Base.get() + " -> " + Optional.of("17").get();
+    }
+
+    @Override
+    public Class<?> introducedClass() {
+        return Module.class;
+    }
+}
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/main/java9/module-info.java
 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java9/module-info.java
new file mode 100644
index 0000000..36f00e0
--- /dev/null
+++ 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java9/module-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module example.mrjar {
+    exports base;
+    exports mr;
+}
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/main/java9/mr/A.java 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java9/mr/A.java
new file mode 100644
index 0000000..5083e1c
--- /dev/null
+++ 
b/src/it/multirelease-patterns/singleproject-modular/src/main/java9/mr/A.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 mr;
+
+import java.util.Optional;
+
+import base.Base;
+
+public class A implements I {
+    public static String getString() {
+        return Base.get() + " -> " + Optional.of("9").get();
+    }
+
+    @Override
+    public Class<?> introducedClass() {
+        return Module.class;
+    }
+}
diff --git 
a/src/it/multirelease-patterns/singleproject-modular/src/test/java/mr/ATest.java
 
b/src/it/multirelease-patterns/singleproject-modular/src/test/java/mr/ATest.java
new file mode 100644
index 0000000..510fb1c
--- /dev/null
+++ 
b/src/it/multirelease-patterns/singleproject-modular/src/test/java/mr/ATest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package mr;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assume.assumeThat;
+
+public class ATest {
+
+    private static final String javaVersion = 
System.getProperty("java.version");
+
+    @Test
+    public void testGet8() throws Exception {
+        assumeThat(javaVersion, is("8"));
+
+        assertThat(A.getString(), is("BASE -> 8"));
+
+        assertThat(new A().introducedClass().getName(), 
is("java.time.LocalDateTime"));
+    }
+
+    @Test
+    public void testGet9() throws Exception {
+        assumeThat(javaVersion, is("9"));
+
+        assertThat(A.getString(), is("BASE -> 9"));
+
+        assertThat(new A().introducedClass().getName(), 
is("java.lang.Module"));
+    }
+}
diff --git a/src/it/multirelease-patterns/singleproject-modular/verify.groovy 
b/src/it/multirelease-patterns/singleproject-modular/verify.groovy
new file mode 100644
index 0000000..947c560
--- /dev/null
+++ b/src/it/multirelease-patterns/singleproject-modular/verify.groovy
@@ -0,0 +1,83 @@
+/*
+ * 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 = 52 // Java 8
+def mr1Version = 53; // Java 9
+def mr2Version = 61; // Java 17
+
+def mrjar = new JarFile(new 
File(basedir,'target/multirelease-1.0.0-SNAPSHOT.jar'))
+
+assert (je = mrjar.getEntry('base/Base.class')) != null
+assert baseVersion == getMajor(mrjar.getInputStream(je))
+assert (je = mrjar.getEntry('mr/A.class')) != null
+assert baseVersion == getMajor(mrjar.getInputStream(je))
+assert (je = mrjar.getEntry('mr/I.class')) != null
+assert baseVersion == getMajor(mrjar.getInputStream(je))
+
+assert mrjar.manifest.mainAttributes.getValue('Multi-Release') == 'true'
+
+assert (je = mrjar.getEntry('META-INF/versions/9/mr/A.class')) != null
+assert mr1Version == getMajor(mrjar.getInputStream(je))
+assert (je = mrjar.getEntry('META-INF/versions/9/module-info.class')) != null
+assert mr1Version == getMajor(mrjar.getInputStream(je))
+
+assert (je = mrjar.getEntry('META-INF/versions/17/mr/A.class')) != null
+assert mr2Version == getMajor(mrjar.getInputStream(je))
+
+/*
+  base
+  base/Base.class
+  mr
+  mr/A.class
+  mr/I.class
+  META-INF
+  META-INF/MANFEST.MF
+  META-INF/versions
+  META-INF/versions/9
+  META-INF/versions/9/mr
+  META-INF/versions/9/mr/A.class
+  META-INF/versions/9/module-info.class
+  META-INF/versions/17
+  META-INF/versions/17/mr
+  META-INF/versions/17/mr/A.class
+  META-INF/maven
+  META-INF/maven/multirelease
+  META-INF/maven/multirelease/multirelease
+  META-INF/maven/multirelease/multirelease/pom.xml
+  META-INF/maven/multirelease/multirelease/pom.properties
+*/
+assert mrjar.entries().size() == 20
+
+int getMajor(InputStream is)
+{
+  def dis = new DataInputStream(is)
+  final String firstFourBytes = Integer.toHexString(dis.readUnsignedShort()) + 
Integer.toHexString(dis.readUnsignedShort())
+  if (!firstFourBytes.equalsIgnoreCase("cafebabe"))
+  {
+    throw new IllegalArgumentException(dataSourceName + " is NOT a Java .class 
file.")
+  }
+  final int minorVersion = dis.readUnsignedShort()
+  final int majorVersion = dis.readUnsignedShort()
+
+  is.close();
+  return majorVersion;
+}
+
diff --git 
a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java 
b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
index 893f121..08eeeb2 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java
@@ -1012,13 +1012,15 @@ public abstract class AbstractCompilerMojo implements 
Mojo {
      * or from {@code <source>} elements otherwise.
      *
      * @param outputDirectory the directory where to store the compilation 
results
+     * @throws IOException if this method needs to walk through directories 
and that operation failed
      */
-    final List<SourceDirectory> getSourceDirectories(final Path 
outputDirectory) {
+    final List<SourceDirectory> getSourceDirectories(final Path 
outputDirectory) throws IOException {
         if (compileSourceRoots == null || compileSourceRoots.isEmpty()) {
             Stream<SourceRoot> roots = 
getSourceRoots(compileScope.projectScope());
             return SourceDirectory.fromProject(roots, getRelease(), 
outputDirectory);
         } else {
-            return SourceDirectory.fromPluginConfiguration(compileSourceRoots, 
getRelease(), outputDirectory);
+            return SourceDirectory.fromPluginConfiguration(
+                    compileSourceRoots, moduleOfPreviousExecution(), 
getRelease(), outputDirectory);
         }
     }
 
@@ -1028,6 +1030,28 @@ public abstract class AbstractCompilerMojo implements 
Mojo {
     @Nullable
     protected abstract Path getGeneratedSourcesDirectory();
 
+    /**
+     * Returns the module which is being patched in a multi-release project, 
or {@code null} if none.
+     * This is used when the {@link CompilerMojo#multiReleaseOutput} 
deprecated flag is {@code true}.
+     * This module name is handled in a special way because, contrarily to the 
case where the project
+     * uses the recommended {@code <sources>} elements (in which case all 
target releases are compiled
+     * in a single Maven Compiler Plugin execution), the Maven Compiler Plugin 
does not know what have
+     * been compiled for the other releases, because each target release is 
compiled with an execution
+     * of {@link CompilerMojo} separated from other executions.
+     *
+     * @return the module name in a previous execution of the compiler plugin, 
or {@code null} if none
+     * @throws IOException if this method needs to walk through directories 
and that operation failed
+     *
+     * @see CompilerMojo#addImplicitDependencies(ToolExecutor)
+     *
+     * @deprecated For compatibility with the previous way to build 
multi-release JAR file.
+     *             May be removed after we drop support of the old way to do 
multi-release.
+     */
+    @Deprecated(since = "4.0.0")
+    String moduleOfPreviousExecution() throws IOException {
+        return null;
+    }
+
     /**
      * {@return whether the sources contain at least one {@code 
module-info.java} file}.
      * Note that the sources may contain more than one {@code 
module-info.java} file
diff --git a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java 
b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
index f058fdf..0127d86 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java
@@ -18,6 +18,7 @@
  */
 package org.apache.maven.plugin.compiler;
 
+import javax.lang.model.SourceVersion;
 import javax.tools.DiagnosticListener;
 import javax.tools.JavaFileObject;
 import javax.tools.OptionChecker;
@@ -27,16 +28,17 @@ import java.io.InputStream;
 import java.lang.module.ModuleDescriptor;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.stream.Stream;
 
 import org.apache.maven.api.JavaPathType;
 import org.apache.maven.api.PathScope;
 import org.apache.maven.api.PathType;
 import org.apache.maven.api.ProducedArtifact;
+import org.apache.maven.api.SourceRoot;
+import org.apache.maven.api.Type;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.plugin.MojoException;
@@ -123,7 +125,7 @@ public class CompilerMojo extends AbstractCompilerMojo {
      *
      * @since 3.7.1
      *
-     * @deprecated Replaced by specifying the release version together with 
the source directory.
+     * @deprecated Replaced by specifying the {@code <targetVersion>} value 
inside a {@code <source>} element.
      */
     @Parameter
     @Deprecated(since = "4.0.0")
@@ -140,6 +142,34 @@ public class CompilerMojo extends AbstractCompilerMojo {
     @Parameter(defaultValue = "javac.args")
     protected String debugFileName;
 
+    /**
+     * Target directory that have been temporarily created as symbolic link 
before compilation.
+     * This is used as a workaround for the fact that, when compiling a 
modular project with
+     * all the module-related compiler options, the classes are written in a 
directory with
+     * the module name. It does not fit in the {@code 
META-INF/versions/<release>} pattern.
+     * Temporary symbolic link is a workaround for this problem.
+     *
+     * <h4>Example</h4>
+     * When compiling the {@code my.app} module for Java 17, the desired 
output directory is:
+     *
+     * <blockquote>{@code target/classes/META-INF/versions/17}</blockquote>
+     *
+     * But {@code javac}, when used with the {@code --module-source-path} 
option,
+     * will write the classes in the following directory:
+     *
+     * <blockquote>{@code 
target/classes/META-INF/versions/17/my.app}</blockquote>
+     *
+     * We workaround this problem with a symbolic link which redirects {@code 
17/my.app} to {@code 17}.
+     * We need to do this only when compiling multi-release project in the old 
deprecated way.
+     * When using the recommended {@code <sources>} approach, the plugins are 
designed to work
+     * with the directory layout produced by {@code javac} instead of fighting 
against it.
+     *
+     * @deprecated For compatibility with the previous way to build 
multi-release JAR file.
+     *             May be removed after we drop support of the old way to do 
multi-release.
+     */
+    @Deprecated(since = "4.0.0")
+    private ModuleDirectoryRemover directoryLevelToRemove;
+
     /**
      * Creates a new compiler <abbr>MOJO</abbr> for the main code.
      */
@@ -160,7 +190,15 @@ public class CompilerMojo extends AbstractCompilerMojo {
             logger.info("Not compiling main sources");
             return;
         }
-        super.execute();
+        try {
+            super.execute();
+        } finally {
+            try (ModuleDirectoryRemover r = directoryLevelToRemove) {
+                // Implicit call to directoryLevelToRemove.close().
+            } catch (IOException e) {
+                throw new CompilationFailureException("I/O error while 
organizing multi-release classes.", e);
+            }
+        }
         @SuppressWarnings("LocalVariableHidesMemberVariable")
         Path outputDirectory = getOutputDirectory();
         if (Files.isDirectory(outputDirectory) && projectArtifact != null) {
@@ -250,71 +288,160 @@ public class CompilerMojo extends AbstractCompilerMojo {
     @Override
     public ToolExecutor createExecutor(DiagnosticListener<? super 
JavaFileObject> listener) throws IOException {
         ToolExecutor executor = super.createExecutor(listener);
-        addImplicitDependencies(executor.sourceDirectories, 
executor.dependencies);
+        if (SUPPORT_LEGACY && multiReleaseOutput) {
+            addImplicitDependencies(executor);
+        }
         return executor;
     }
 
     /**
-     * If compiling a multi-release JAR in the old deprecated way, add the 
previous versions to the path.
+     * {@return whether the project has at least one module-info file}.
+     * If no such file is found in the code to be compiled by this 
<abbr>MOJO</abbr> execution,
+     * then this method searches in the multi-release codes compiled by 
previous executions.
      *
-     * @param sourceDirectories the source directories
-     * @param addTo where to add dependencies
-     * @param hasModuleDeclaration whether the main sources have or should 
have a {@code module-info} file
-     * @throws IOException if this method needs to walk through directories 
and that operation failed
+     * @param roots root directories of the sources to compile
+     * @throws IOException if this method needed to read a module descriptor 
and failed
      *
-     * @deprecated For compatibility with the previous way to build 
multi-releases JAR file.
+     * @deprecated For compatibility with the previous way to build 
multi-release JAR file.
+     *             May be removed after we drop support of the old way to do 
multi-release.
      */
+    @Override
     @Deprecated(since = "4.0.0")
-    private void addImplicitDependencies(List<SourceDirectory> 
sourceDirectories, Map<PathType, List<Path>> addTo)
-            throws IOException {
-        if (SUPPORT_LEGACY && multiReleaseOutput) {
-            var paths = new TreeMap<Integer, Path>();
-            Path root = 
SourceDirectory.outputDirectoryForReleases(outputDirectory);
-            Files.walk(root, 1).forEach((path) -> {
-                int version;
-                if (path.equals(root)) {
-                    path = outputDirectory;
-                    version = 0;
-                } else {
-                    try {
-                        version = 
Integer.parseInt(path.getFileName().toString());
-                    } catch (NumberFormatException e) {
-                        throw new CompilationFailureException("Invalid version 
number for " + path, e);
+    final boolean hasModuleDeclaration(final List<SourceDirectory> roots) 
throws IOException {
+        boolean hasModuleDeclaration = super.hasModuleDeclaration(roots);
+        if (SUPPORT_LEGACY && !hasModuleDeclaration && multiReleaseOutput) {
+            String type = project.getPackaging().type().id();
+            if (!Type.CLASSPATH_JAR.equals(type)) {
+                for (Path p : getOutputDirectoryPerVersion().values()) {
+                    p = p.resolve(SourceDirectory.MODULE_INFO + 
SourceDirectory.CLASS_FILE_SUFFIX);
+                    if (Files.exists(p)) {
+                        return true;
                     }
                 }
-                if (paths.put(version, path) != null) {
-                    throw new CompilationFailureException("Duplicated version 
number for " + path);
+            }
+        }
+        return hasModuleDeclaration;
+    }
+
+    /**
+     * {@return the output directory of each target Java version}.
+     * By convention, {@link SourceVersion#RELEASE_0} stands for the base 
version.
+     *
+     * @throws IOException if this method needs to walk through directories 
and that operation failed
+     *
+     * @deprecated For compatibility with the previous way to build 
multi-release JAR file.
+     *             May be removed after we drop support of the old way to do 
multi-release.
+     */
+    @Deprecated(since = "4.0.0")
+    private TreeMap<SourceVersion, Path> getOutputDirectoryPerVersion() throws 
IOException {
+        final Path root = 
SourceDirectory.outputDirectoryForReleases(outputDirectory);
+        if (Files.notExists(root)) {
+            return null;
+        }
+        final var paths = new TreeMap<SourceVersion, Path>();
+        Files.walk(root, 1).forEach((path) -> {
+            SourceVersion version;
+            if (path.equals(root)) {
+                path = outputDirectory;
+                version = SourceVersion.RELEASE_0;
+            } else {
+                try {
+                    version = SourceVersion.valueOf("RELEASE_" + 
path.getFileName());
+                } catch (IllegalArgumentException e) {
+                    throw new CompilationFailureException("Invalid version 
number for " + path, e);
                 }
-            });
-            /*
-             * Find the module name. If many module-info classes are found,
-             * the most basic one (with lowest Java release number) is taken.
-             */
-            String moduleName = null;
-            for (Path path : paths.values()) {
-                path = path.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
-                if (Files.exists(path)) {
-                    try (InputStream in = Files.newInputStream(path)) {
-                        moduleName = ModuleDescriptor.read(in).name();
-                    }
+            }
+            if (paths.put(version, path) != null) {
+                throw new CompilationFailureException("Duplicated version 
number for " + path);
+            }
+        });
+        return paths;
+    }
+
+    /**
+     * Adds the compilation outputs of previous Java releases to the 
class-path ot module-path.
+     * This method should be invoked only when compiling a multi-release 
<abbr>JAR</abbr> in the
+     * old deprecated way.
+     *
+     * <p>The {@code executor} argument may be {@code null} if the caller is 
only interested in the
+     * module name, with no executor to modify. The module name found by this 
method is specific to
+     * the way that projects are organized when {@link #multiReleaseOutput} is 
{@code true}.</p>
+     *
+     * @param  executor  the executor where to add implicit dependencies, or 
{@code null} if none
+     * @return the module name, or {@code null} if none
+     * @throws IOException if this method needs to walk through directories 
and that operation failed
+     *
+     * @deprecated For compatibility with the previous way to build 
multi-release JAR file.
+     *             May be removed after we drop support of the old way to do 
multi-release.
+     */
+    @Deprecated(since = "4.0.0")
+    private String addImplicitDependencies(final ToolExecutor executor) throws 
IOException {
+        final TreeMap<SourceVersion, Path> paths = 
getOutputDirectoryPerVersion();
+        /*
+         * Search for the module name. If many module-info classes are found,
+         * the most basic one (with lowest Java release number) is selected.
+         */
+        String moduleName = null;
+        for (Path path : paths.values()) {
+            path = path.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
+            if (Files.exists(path)) {
+                try (InputStream in = Files.newInputStream(path)) {
+                    moduleName = ModuleDescriptor.read(in).name();
+                }
+                break;
+            }
+        }
+        /*
+         * If no module name was found in the classes compiled for previous 
Java releases,
+         * search in the source files for the Java release of the current 
compilation unit.
+         */
+        if (moduleName == null) {
+            final Stream<Path> sourceDirectories;
+            if (executor != null) {
+                sourceDirectories = 
executor.sourceDirectories.stream().map(dir -> dir.root);
+            } else if (compileSourceRoots == null || 
compileSourceRoots.isEmpty()) {
+                sourceDirectories = 
getSourceRoots(compileScope.projectScope()).map(SourceRoot::directory);
+            } else {
+                sourceDirectories = compileSourceRoots.stream().map(Path::of);
+            }
+            for (Path root : sourceDirectories.toList()) {
+                moduleName = parseModuleInfoName(root.resolve(MODULE_INFO + 
JAVA_FILE_SUFFIX));
+                if (moduleName != null) {
                     break;
                 }
             }
+        }
+        if (executor != null) {
             /*
-             * If no module name was found in the classes compiled for 
previous Java releases,
-             * search in the source files for the Java release of the current 
compilation unit.
+             * Add previous versions as dependencies on the class-path or 
module-path, depending on whether
+             * the project is modular. Each path should be on either the 
class-path or module-path, but not
+             * both. If a path for a modular project seems needed on the 
class-path, it may be a sign that
+             * other options are not used correctly (e.g., `--source-path` 
versus `--module-source-path`).
              */
-            if (moduleName == null) {
-                for (SourceDirectory dir : sourceDirectories) {
-                    moduleName = 
parseModuleInfoName(dir.root.resolve(MODULE_INFO + JAVA_FILE_SUFFIX));
-                    if (moduleName != null) {
-                        break;
-                    }
-                }
+            PathType type = JavaPathType.CLASSES;
+            if (moduleName != null) {
+                type = JavaPathType.patchModule(moduleName);
+                directoryLevelToRemove = 
ModuleDirectoryRemover.create(executor.outputDirectory, moduleName);
             }
-            var pathType = (moduleName != null) ? 
JavaPathType.patchModule(moduleName) : JavaPathType.CLASSES;
-            addTo.computeIfAbsent(pathType, (key) -> new ArrayList<>())
-                    .addAll(paths.descendingMap().values());
+            if (!paths.isEmpty()) {
+                
executor.dependencies(type).addAll(paths.descendingMap().values());
+            }
+        }
+        return moduleName;
+    }
+
+    /**
+     * {@return the module name in a previous execution of the compiler 
plugin, or {@code null} if none}.
+     *
+     * @deprecated For compatibility with the previous way to build 
multi-release JAR file.
+     *             May be removed after we drop support of the old way to do 
multi-release.
+     */
+    @Override
+    @Deprecated(since = "4.0.0")
+    final String moduleOfPreviousExecution() throws IOException {
+        if (SUPPORT_LEGACY && multiReleaseOutput) {
+            return addImplicitDependencies(null);
         }
+        return super.moduleOfPreviousExecution();
     }
 }
diff --git 
a/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java 
b/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java
new file mode 100644
index 0000000..878b603
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugin/compiler/ModuleDirectoryRemover.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugin.compiler;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Helper class for removing the directory having a module name.
+ * This hack is used when the {@code --module-source-path} compiler option is 
used
+ * but we still want to reproduce the directory layout of Maven 3.
+ * This hack is not used when the new {@code <source>} elements is used 
instead.
+ *
+ * <p>The code in this class is useful only when {@link 
AbstractCompilerMojo#SUPPORT_LEGACY} is true.
+ * This class can be fully deleted if a future version permanently set 
above-cited flag to false.</p>
+ */
+final class ModuleDirectoryRemover implements Closeable {
+    /**
+     * The output directory expected by Maven 3.
+     * Example: {@code target/classes/META-INF/versions/21}.
+     */
+    private final Path mavenTarget;
+
+    /**
+     * The output directory where {@code javac} will write the classes.
+     * Example: {@code target/classes/META-INF/versions/21/org.foo.bar}.
+     */
+    private final Path javacTarget;
+
+    /**
+     * A temporary directory used as an intermediate step for avoiding name 
clash.
+     * Example: {@code target/classes/META-INF/versions/org.foo.bar}.
+     */
+    private final Path interTarget;
+
+    /**
+     * Temporarily renames the given output directory for matching the layout 
of {@code javac} output.
+     *
+     * @param  outputDirectory  the output directory (must exist)
+     * @param  moduleName       the name of the module
+     * @throws IOException if an error occurred while renaming the output 
directory
+     */
+    private ModuleDirectoryRemover(Path outputDirectory, String moduleName) 
throws IOException {
+        mavenTarget = outputDirectory;
+        interTarget = Files.move(outputDirectory, 
outputDirectory.resolveSibling(moduleName));
+        javacTarget = 
Files.createDirectory(outputDirectory).resolve(moduleName);
+        Files.move(interTarget, javacTarget);
+    }
+
+    /**
+     * Temporarily renames the given output directory for matching the layout 
of {@code javac} output.
+     *
+     * @param  outputDirectory  the output directory (must exist)
+     * @param  moduleName       the name of the module, or {@code null} if none
+     * @return a handler for restoring the directory to its original name, or 
{@code null} if there is no renaming
+     * @throws IOException if an error occurred while renaming the output 
directory
+     */
+    static ModuleDirectoryRemover create(Path outputDirectory, String 
moduleName) throws IOException {
+        return (moduleName != null) ? new 
ModuleDirectoryRemover(outputDirectory, moduleName) : null;
+    }
+
+    /**
+     * Restores the output directory to its original name.
+     * Note: contrarily to {@link Closeable} contract, this method is not 
idempotent:
+     * it cannot be executed twice. However, this is okay for the usage in 
this package.
+     *
+     * @throws IOException if an error occurred while renaming the output 
directory
+     */
+    @Override
+    public void close() throws IOException {
+        Files.move(javacTarget, interTarget);
+        Files.delete(mavenTarget);
+        Files.move(interTarget, mavenTarget);
+    }
+}
diff --git 
a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java 
b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
index 920981b..e89c01f 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java
@@ -311,12 +311,13 @@ final class SourceDirectory {
      * Used only when the compiler plugin is configured with the {@code 
compileSourceRoots} option.
      *
      * @param compileSourceRoots the root paths to source files
+     * @param moduleName name of the module for which source directories are 
provided, or {@code null} if none
      * @param defaultRelease the release to use, or {@code null} of unspecified
      * @param outputDirectory the directory where to store the compilation 
results
      * @return the given list of paths wrapped as source directory objects
      */
     static List<SourceDirectory> fromPluginConfiguration(
-            List<String> compileSourceRoots, String defaultRelease, Path 
outputDirectory) {
+            List<String> compileSourceRoots, String moduleName, String 
defaultRelease, Path outputDirectory) {
         var release = parse(defaultRelease); // May be null.
         var roots = new ArrayList<SourceDirectory>(compileSourceRoots.size());
         for (String file : compileSourceRoots) {
@@ -327,7 +328,7 @@ final class SourceDirectory {
                         List.of(),
                         List.of(),
                         JavaFileObject.Kind.SOURCE,
-                        null,
+                        moduleName,
                         release,
                         outputDirectory,
                         JavaFileObject.Kind.CLASS));
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 92700e5..de7c3fb 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java
@@ -129,6 +129,8 @@ public class ToolExecutor {
      * All dependencies grouped by the path types where to place them, 
together with the modules to patch.
      * The path type can be the class-path, module-path, annotation processor 
path, patched path, <i>etc.</i>
      * Some path types include a module name.
+     *
+     * @see #dependencies(PathType)
      */
     protected final Map<PathType, List<Path>> dependencies;
 
@@ -235,7 +237,11 @@ public class ToolExecutor {
             hasModuleDeclaration = true;
             sourceFiles = List.of();
         } else {
-            // The order of the two next lines matter for initialization of 
`SourceDirectory.moduleInfo`.
+            /*
+             * The order of the two next lines matter for initialization of 
`SourceDirectory.moduleInfo`.
+             * This initialization is done indirectly when the walk invokes 
the `SourceFile` constructor,
+             * which in turn invokes `SourceDirectory.visit(Path)`.
+             */
             sourceFiles = new 
PathFilter(mojo).walkSourceFiles(sourceDirectories);
             hasModuleDeclaration = 
mojo.hasModuleDeclaration(sourceDirectories);
             if (sourceFiles.isEmpty()) {
@@ -355,6 +361,17 @@ public class ToolExecutor {
         return true;
     }
 
+    /**
+     * {@return a modifiable list of paths to all dependencies of the given 
type}.
+     * The returned list is intentionally live: elements can be added or 
removed
+     * from the list for changing the state of this executor.
+     *
+     * @param  pathType  type of path for which to get the dependencies
+     */
+    protected List<Path> dependencies(PathType pathType) {
+        return dependencies.computeIfAbsent(pathType, (key) -> new 
ArrayList<>());
+    }
+
     /**
      * Dispatches sources and dependencies on the kind of paths determined by 
{@code DependencyResolver}.
      * The targets may be class-path, module-path, annotation processor 
class-path/module-path, <i>etc</i>.
@@ -425,17 +442,10 @@ public class ToolExecutor {
     /**
      * Ensures that the given value is non-null, replacing null values by the 
latest version.
      */
-    private static SourceVersion nonNull(SourceVersion release) {
+    private static SourceVersion nonNullOrLatest(SourceVersion release) {
         return (release != null) ? release : SourceVersion.latest();
     }
 
-    /**
-     * Ensures that the given value is non-null, replacing null or blank 
values by an empty string.
-     */
-    private static String nonNull(String moduleName) {
-        return (moduleName == null || moduleName.isBlank()) ? "" : moduleName;
-    }
-
     /**
      * If the given module name is empty, tries to infer a default module 
name. A module name is inferred
      * (tentatively) when the <abbr>POM</abbr> file does not contain an 
explicit {@code <module>} element.
@@ -466,12 +476,16 @@ public class ToolExecutor {
              * output directory of previous version even if we skipped the 
compilation of that version.
              */
             SourcesForRelease unit = result.computeIfAbsent(
-                    nonNull(directory.release),
+                    nonNullOrLatest(directory.release),
                     (release) -> new SourcesForRelease(directory.release)); // 
Intentionally ignore the key.
-            unit.roots.computeIfAbsent(nonNull(directory.moduleName), 
(moduleName) -> new LinkedHashSet<Path>());
+            String moduleName = directory.moduleName;
+            if (moduleName == null || moduleName.isBlank()) {
+                moduleName = "";
+            }
+            unit.roots.computeIfAbsent(moduleName, (key) -> new 
LinkedHashSet<Path>());
         }
         for (SourceFile source : sourceFiles) {
-            result.get(nonNull(source.directory.release)).add(source);
+            result.get(nonNullOrLatest(source.directory.release)).add(source);
         }
         return result.values();
     }
@@ -515,7 +529,7 @@ public class ToolExecutor {
         /*
          * Create a `JavaFileManager`, configure all paths (dependencies and 
sources), then run the compiler.
          * The Java file manager has a cache, so it needs to be disposed after 
the compilation is completed.
-         * The same `JavaFileManager` may be reused for many compilation units 
(e.g. multi-releases) before
+         * The same `JavaFileManager` may be reused for many compilation units 
(e.g. multi-release) before
          * disposal in order to reuse its cache.
          */
         boolean success = true;
@@ -527,7 +541,7 @@ public class ToolExecutor {
             boolean isVersioned = false;
             Path latestOutputDirectory = null;
             /*
-             * More than one compilation unit may exist in the case of a 
multi-releases project.
+             * More than one compilation unit may exist in the case of a 
multi-release project.
              * Units are compiled in the order of the release version, with 
base compiled first.
              * At the beginning of each new iteration, `latestOutputDirectory` 
is the path to
              * the compiled classes of the previous version.
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 1ecf24c..6be8b69 100644
--- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
+++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutorForTest.java
@@ -28,7 +28,6 @@ import java.io.Writer;
 import java.lang.module.ModuleDescriptor;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -113,15 +112,15 @@ class ToolExecutorForTest extends ToolExecutor {
     /**
      * If non-null, the {@code module} part to remove in {@code 
target/test-classes/module/package}.
      * This {@code module} directory is generated by {@code javac} for some 
compiler options.
-     * We keep it when the project is configured with the new {@code <source>} 
element, but
-     * have to remove it for compatibility reason if the project is compiled 
in the old way.
+     * We keep that directory when the project is configured with the new 
{@code <source>} element,
+     * but have to remove it for compatibility reason if the project is 
compiled in the old way.
      *
      * @deprecated Exists only for compatibility with the Maven 3 way to do a 
modular project.
      * Is likely to cause confusion, for example with incremental builds.
      * New projects should use the {@code <source>} elements instead.
      */
     @Deprecated(since = "4.0.0")
-    private Path directoryLevelToRemove;
+    private String directoryLevelToRemove;
 
     /**
      * Creates a new task by taking a snapshot of the current configuration of 
the given <abbr>MOJO</abbr>.
@@ -161,7 +160,7 @@ class ToolExecutorForTest extends ToolExecutor {
                         }
                     }
                 }
-                directoryLevelToRemove = 
outputDirectory.resolve(moduleToPatch);
+                directoryLevelToRemove = moduleToPatch;
             }
             patchedModules.put(moduleToPatch, new LinkedHashSet<>()); // 
Signal that this module exists in the test.
         }
@@ -181,9 +180,7 @@ class ToolExecutorForTest extends ToolExecutor {
         });
         patchedModules.values().removeIf(Set::isEmpty);
         patchedModules.forEach((moduleToPatch, paths) -> {
-            dependencies
-                    .computeIfAbsent(JavaPathType.patchModule(moduleToPatch), 
(key) -> new ArrayList<>())
-                    .addAll(paths);
+            
dependencies(JavaPathType.patchModule(moduleToPatch)).addAll(paths);
         });
         /*
          * If there is no module to patch, we probably have a non-modular 
project.
@@ -198,11 +195,11 @@ class ToolExecutorForTest extends ToolExecutor {
                     String moduleToPatch = getMainModuleName();
                     if (!moduleToPatch.isEmpty()) {
                         pathType = JavaPathType.patchModule(moduleToPatch);
-                        directoryLevelToRemove = 
outputDirectory.resolve(moduleToPatch);
+                        directoryLevelToRemove = moduleToPatch;
                     }
                 }
             }
-            dependencies.computeIfAbsent(pathType, (key) -> new 
ArrayList<>()).add(0, mainOutputDirectory);
+            dependencies(pathType).add(0, mainOutputDirectory);
         }
     }
 
@@ -339,16 +336,8 @@ class ToolExecutorForTest extends ToolExecutor {
     @Override
     public boolean compile(JavaCompiler compiler, Options configuration, 
Writer otherOutput) throws IOException {
         addModuleOptions(configuration); // Effective only once.
-        Path delete = null;
-        try {
-            if (directoryLevelToRemove != null) {
-                delete = Files.createSymbolicLink(directoryLevelToRemove, 
directoryLevelToRemove.getParent());
-            }
+        try (var r = ModuleDirectoryRemover.create(outputDirectory, 
directoryLevelToRemove)) {
             return super.compile(compiler, configuration, otherOutput);
-        } finally {
-            if (delete != null) {
-                Files.delete(delete);
-            }
         }
     }
 
diff --git a/src/site/markdown/examples/set-compiler-release.md 
b/src/site/markdown/examples/set-compiler-release.md
index 500d6d3..cc8fa39 100644
--- a/src/site/markdown/examples/set-compiler-release.md
+++ b/src/site/markdown/examples/set-compiler-release.md
@@ -71,7 +71,7 @@ or by configuring the plugin directly:
 Since version 4 of the compiler plugin, which requires Maven 4,
 the preferred way to specify the release is together with the source 
declaration.
 This is the recommended way because it makes the creation of
-[multi-releases](../multirelease.html) projects easier.
+[multi-release](../multirelease.html) projects easier.
 
 ```xml
 <project>
diff --git a/src/site/markdown/multirelease.md 
b/src/site/markdown/multirelease.md
index 1ce0a55..29529d0 100644
--- a/src/site/markdown/multirelease.md
+++ b/src/site/markdown/multirelease.md
@@ -22,7 +22,7 @@ under the License.
 With [JEP-238](http://openjdk.java.net/jeps/238) the support of multirelease 
JARs was introduced.
 This means that you can have Java version dependent classes inside one JAR.
 Based on the runtime, it will pick up the best matching version of a class.
-The files of a multi-releases project are organized like below:
+The files of a multi-release project are organized like below:
 
 ```
 .
@@ -68,7 +68,7 @@ There are a couple of important facts one should know when 
creating Multi Releas
 
 ## Maven 3
 
-Maven 3 proposed many different patterns for building multi-releases project.
+Maven 3 proposed many different patterns for building multi-release project.
 One pattern is to create a sub-project for each version.
 The project needs to be build with the highest required version of the JDK,
 and a `--release` option is specified in each sub-project.
@@ -93,7 +93,7 @@ for examples of small projects using the following patterns:
 
 ## Maven 4
 
-Building a multi-releases project is much easier with version 4 of the Maven 
Compiler Plugin.
+Building a multi-release project is much easier with version 4 of the Maven 
Compiler Plugin.
 The source code for all versions are placed in different directories of the 
same Maven project.
 These directories are declared together with the Java release like below:
 

Reply via email to