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: