From d458a26c13eb600f5956f091849367c38fa3727b Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 16 Nov 2025 18:52:00 +0100 Subject: [PATCH] When building a project which is both multi-module multi-release, rename the `META-INF/versions` subdirectory as `versions-modular`. Therefore, the full pattern of the path to the class files become `META-INF/versions-modular/${release}/${module}. Rational: the "${module}" directory in above-cited path is inserted by `javac` when module hierarchy is used. We have no control on that. However, there is no multi-module JAR file format as of November 2025. Therefore, dispatching the files for different modules in each JAR file would be the work of the Maven JAR plugin. A prototype is already ready, but experience with it shows that it is difficult to guess whether the `META-INF/versions/`${release}` directory is expected to be followed by a `${module}` subdirectory. Renaming `versions` to anything else would avoid that ambiguity. We propose "versions-modular" as a shortcut of "versions for a modular project". The name is not very important because this directory will not appear in any JAR or any other file to be distributed. Only its content is dispatched in JAR files. --- .../multirelease-with-modules/verify.groovy | 4 +-- .../maven/plugin/compiler/CompilerMojo.java | 7 ++-- .../plugin/compiler/SourceDirectory.java | 33 ++++++++++++++----- .../maven/plugin/compiler/ToolExecutor.java | 4 +-- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/it/multirelease-with-modules/verify.groovy b/src/it/multirelease-with-modules/verify.groovy index a4d068ae5..146fa96ac 100644 --- a/src/it/multirelease-with-modules/verify.groovy +++ b/src/it/multirelease-with-modules/verify.groovy @@ -27,8 +27,8 @@ assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar/foo/Ot assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar/foo/YetAnotherFile.class")) assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar.more/more/MainFile.class")) assert baseVersion == getMajor(new File( basedir, "target/classes/foo.bar.more/more/OtherFile.class")) -assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions/16/foo.bar/foo/OtherFile.class")) -assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions/16/foo.bar.more/more/OtherFile.class")) +assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions-modular/16/foo.bar/foo/OtherFile.class")) +assert nextVersion == getMajor(new File( basedir, "target/classes/META-INF/versions-modular/16/foo.bar.more/more/OtherFile.class")) int getMajor(File 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 a62e2ec80..373f07319 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/CompilerMojo.java @@ -267,7 +267,8 @@ protected Set getIncrementalExcludes() { @Override protected Path getOutputDirectory() { if (SUPPORT_LEGACY && multiReleaseOutput && release != null) { - return SourceDirectory.outputDirectoryForReleases(outputDirectory).resolve(release); + return SourceDirectory.outputDirectoryForReleases(false, outputDirectory) + .resolve(release); } return outputDirectory; } @@ -340,7 +341,7 @@ final boolean hasModuleDeclaration(final List roots) throws IOE */ @Deprecated(since = "4.0.0") private TreeMap getOutputDirectoryPerVersion() throws IOException { - final Path root = SourceDirectory.outputDirectoryForReleases(outputDirectory); + final Path root = SourceDirectory.outputDirectoryForReleases(false, outputDirectory); if (Files.notExists(root)) { return null; } @@ -365,7 +366,7 @@ private TreeMap getOutputDirectoryPerVersion() throws IOExc } /** - * Adds the compilation outputs of previous Java releases to the class-path ot module-path. + * Adds the compilation outputs of previous Java releases to the class-path of module-path. * This method should be invoked only when compiling a multi-release JAR in the * old deprecated way. * 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 45f05058e..bb4ee92a0 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java +++ b/src/main/java/org/apache/maven/plugin/compiler/SourceDirectory.java @@ -140,9 +140,9 @@ final class SourceDirectory { * This is the MOJO output directory with sub-directories appended according the following rules, in that order: * *
    - *
  1. If {@link #moduleName} is non-null, then the module name is appended.
  2. - *
  3. If {@link #isVersioned} is {@code true}, then the next elements in the paths are + *
  4. If {@link #isVersioned} is {@code true}, then the relative part of the path starts with * {@code "META-INF/versions/"} where {@code } is the release number.
  5. + *
  6. If {@link #moduleName} is non-null, then the module name is appended.
  7. *
* * @see #getOutputDirectory() @@ -194,6 +194,8 @@ private SourceDirectory( * Potentially adds the {@code META-INF/versions/} part of the path to the output directory. * This method can be invoked only after the base version has been determined, which happens * after all other source directories have been built. + * + * @param baseVersion the Java release target by the non-versioned classes */ private void completeIfVersioned(SourceVersion baseVersion) { @SuppressWarnings("LocalVariableHidesMemberVariable") @@ -204,32 +206,45 @@ private void completeIfVersioned(SourceVersion baseVersion) { release = SourceVersion.latestSupported(); // `this.release` intentionally left to null. } - outputDirectory = outputDirectoryForReleases(outputDirectory, release); + outputDirectory = outputDirectoryForReleases(moduleName != null, outputDirectory, release); } } /** - * Returns the directory where to write the compilation for a specific Java release. + * Returns the directory where to write the compiled class files for a specific Java release. + * The standard path is {@code META-INF/versions/${release}} where {@code ${release}} is the + * numerical value of the {@code release} argument. However if {@code modular} is {@code true}, + * then the returned path is rather {@code META-INF/versions-modular/${release}}. The latter is + * non-standard because there is no standard multi-module JAR formats as of 2025. + * The use of {@code "versions-modular"} is for allowing other plugins such as Maven JAR plugin + * to avoid confusion with the standard case. * + * @param modular whether each version directory contains module names * @param outputDirectory usually the value of {@link #outputDirectory} * @param release the release, or {@code null} for the default release + * @return the directory for the classes of the specified version */ - static Path outputDirectoryForReleases(Path outputDirectory, SourceVersion release) { + static Path outputDirectoryForReleases(boolean modular, Path outputDirectory, SourceVersion release) { if (release == null) { release = SourceVersion.latestSupported(); } String version = release.name(); // TODO: replace by runtimeVersion() in Java 18. version = version.substring(version.lastIndexOf('_') + 1); - return outputDirectoryForReleases(outputDirectory).resolve(version); + return outputDirectoryForReleases(modular, outputDirectory).resolve(version); } /** - * Returns the directory where to write the compilation for a specific Java release. + * Returns the directory where to write the compiled class files for all Java releases. + * The standard path (when {@code modular} is {@code false}) is {@code META-INF/versions}. * The caller shall add the version number to the returned path. + * + * @param modular whether each version directory contains module names + * @param outputDirectory usually the value of {@link #outputDirectory} + * @return the directory for all versions */ - static Path outputDirectoryForReleases(Path outputDirectory) { + static Path outputDirectoryForReleases(boolean modular, Path outputDirectory) { // TODO: use Path.resolve(String, String...) with Java 22. - return outputDirectory.resolve("META-INF").resolve("versions"); + return outputDirectory.resolve("META-INF").resolve(modular ? "versions-modular" : "versions"); } /** 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 c32d7bfea..1a9b1f29c 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java +++ b/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java @@ -614,8 +614,8 @@ public boolean compile(final JavaCompiler compiler, final Options configuration, } outputForRelease = outputDirectory; // Modified below if compiling a non-base release. if (isVersioned) { - outputForRelease = Files.createDirectories( - SourceDirectory.outputDirectoryForReleases(outputForRelease, unit.release)); + outputForRelease = Files.createDirectories(SourceDirectory.outputDirectoryForReleases( + isModularProject, outputForRelease, unit.release)); if (isClasspathProject) { /* * For a non-modular project, this block is executed at most once par compilation unit.