From 558256d6c835a170a32a31f10cbc389b50d3ff47 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 26 Sep 2026 01:39:59 +0200 Subject: [PATCH] Choose the versioned module descriptor by the target release AbstractBinaryModuleInfoParser and LocationManager.resolvePath picked the versioned module-info.class of a multi-release jar or output directory by counting down from the running JDK's version. Add ResolvePathsRequest. setTargetRelease(JavaVersion) so callers such as maven-compiler-plugin can pass the release the code is actually compiled for, and thread it through resolvePaths to both the jar/directory lookup and the modulepath routing check. Defaults to the running JDK, so behaviour is unchanged unless a caller sets it. Fixes #237 --- .../jpms/AbstractBinaryModuleInfoParser.java | 5 +- .../languages/java/jpms/LocationManager.java | 20 ++-- .../java/jpms/ResolvePathRequest.java | 30 +++++ .../java/jpms/ResolvePathsRequest.java | 30 +++++ .../languages/java/version/JavaVersion.java | 17 +++ .../LocationManagerTargetReleaseTest.java | 103 ++++++++++++++++++ .../java/jpms/LocationManagerTest.java | 44 +++++++- .../java/version/JavaVersionTest.java | 8 ++ .../META-INF/versions/11/module-info.class | Bin 0 -> 168 bytes .../META-INF/versions/17/module-info.class | Bin 0 -> 168 bytes 10 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTargetReleaseTest.java create mode 100644 plexus-java/src/test/test-data/dir.mr.target.release/out/META-INF/versions/11/module-info.class create mode 100644 plexus-java/src/test/test-data/dir.mr.target.release/out/META-INF/versions/17/module-info.class diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/AbstractBinaryModuleInfoParser.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/AbstractBinaryModuleInfoParser.java index b39bc2d0..ac0b710f 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/AbstractBinaryModuleInfoParser.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/AbstractBinaryModuleInfoParser.java @@ -64,8 +64,7 @@ public JavaModuleDescriptor getModuleDescriptor(Path modulePath, JavaVersion jdk && "true" .equalsIgnoreCase( manifest.getMainAttributes().getValue("Multi-Release"))) { - int javaVersion = - Integer.parseInt(jdkVersion.asMajor().getValue(1)); + int javaVersion = jdkVersion.getMajorVersion(); for (int version = javaVersion; version >= 9; version--) { String resource = "META-INF/versions/" + version + "/module-info.class"; @@ -98,7 +97,7 @@ public JavaModuleDescriptor getModuleDescriptor(Path modulePath, JavaVersion jdk * @return the path of the highest applicable versioned descriptor, or {@code null} if there is none */ static Path findVersionedModuleInfo(Path directory, JavaVersion jdkVersion) { - int javaVersion = Integer.parseInt(jdkVersion.asMajor().getValue(1)); + int javaVersion = jdkVersion.getMajorVersion(); for (int version = javaVersion; version >= 9; version--) { Path moduleInfo = directory.resolve("META-INF/versions/" + version + "/module-info.class"); diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java index 632a9e3e..714df3d1 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java @@ -122,7 +122,8 @@ public String extract(Path file) throws IOException { return resolvePath( request.toPath(request.getPathElement()), filenameExtractor, - getBinaryModuleInfoParser(request.getJdkHome())); + getBinaryModuleInfoParser(request.getJdkHome()), + request.getTargetRelease()); } /** @@ -173,7 +174,8 @@ public ResolvePathsResult resolvePaths(final ResolvePathsRequest reque }; try { - ResolvePathResult resolvedPath = resolvePath(request.toPath(t), nameExtractor, binaryParser); + ResolvePathResult resolvedPath = + resolvePath(request.toPath(t), nameExtractor, binaryParser, request.getTargetRelease()); moduleDescriptor = resolvedPath.getModuleDescriptor(); @@ -302,7 +304,8 @@ private JavaModuleDescriptor getMainModuleDescriptor( if (descriptorPath.endsWith("module-info.java")) { mainModuleDescriptor = sourceParser.fromSourcePath(descriptorPath); } else if (descriptorPath.endsWith("module-info.class")) { - mainModuleDescriptor = binaryParser.getModuleDescriptor(descriptorPath.getParent()); + mainModuleDescriptor = + binaryParser.getModuleDescriptor(descriptorPath.getParent(), request.getTargetRelease()); } else { throw new IOException("Invalid path to module descriptor: " + descriptorPath); } @@ -313,7 +316,11 @@ private JavaModuleDescriptor getMainModuleDescriptor( } private ResolvePathResult resolvePath( - Path path, ModuleNameExtractor fileModulenameExtractor, ModuleInfoParser binaryParser) throws IOException { + Path path, + ModuleNameExtractor fileModulenameExtractor, + ModuleInfoParser binaryParser, + JavaVersion targetRelease) + throws IOException { ResolvePathResult result = new ResolvePathResult(); JavaModuleDescriptor moduleDescriptor = null; @@ -326,9 +333,8 @@ private ResolvePathResult resolvePath( if (Files.isRegularFile(path) || Files.exists(path.resolve("module-info.class")) - || AbstractBinaryModuleInfoParser.findVersionedModuleInfo(path, JavaVersion.JAVA_SPECIFICATION_VERSION) - != null) { - moduleDescriptor = binaryParser.getModuleDescriptor(path); + || AbstractBinaryModuleInfoParser.findVersionedModuleInfo(path, targetRelease) != null) { + moduleDescriptor = binaryParser.getModuleDescriptor(path, targetRelease); } if (moduleDescriptor != null) { diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathRequest.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathRequest.java index afd321f1..8eb04c9e 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathRequest.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathRequest.java @@ -22,6 +22,9 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; + +import org.codehaus.plexus.languages.java.version.JavaVersion; /** * @@ -33,6 +36,8 @@ public abstract class ResolvePathRequest { private T path; + private JavaVersion targetRelease = JavaVersion.JAVA_SPECIFICATION_VERSION; + private ResolvePathRequest() {} public static ResolvePathRequest ofFile(File file) { @@ -88,4 +93,29 @@ public ResolvePathRequest setJdkHome(T jdkHome) { public Path getJdkHome() { return jdkHome; } + + /** + * The Java release the module descriptor must be resolved for - the {@code javac --release} value, or + * {@code -target} when {@code --release} is unset. Accepts {@code "1.8"}-style values as well as bare + * major versions. This picks the applicable versioned {@code module-info.class} of a multi-release jar or + * output directory, i.e. the highest {@code META-INF/versions/} whose {@code N} does not exceed this + * release. Defaults to the version of the running JDK, so callers that compile for a different release + * should set this explicitly. + * + * @param targetRelease the target release, never {@code null} + * @return this request + * @since 1.6.1 + */ + public ResolvePathRequest setTargetRelease(JavaVersion targetRelease) { + this.targetRelease = Objects.requireNonNull(targetRelease, "targetRelease"); + return this; + } + + /** + * @return the Java release the module descriptor is resolved for, defaults to the running JDK's version + * @since 1.6.1 + */ + public JavaVersion getTargetRelease() { + return targetRelease; + } } diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java index 60b9f781..fdfe676b 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java @@ -25,6 +25,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Objects; + +import org.codehaus.plexus.languages.java.version.JavaVersion; /** * Contains all information required to analyze the project @@ -47,6 +50,8 @@ public abstract class ResolvePathsRequest { private boolean includeStatic; + private JavaVersion targetRelease = JavaVersion.JAVA_SPECIFICATION_VERSION; + private ResolvePathsRequest() {} /** @@ -226,4 +231,29 @@ public ResolvePathsRequest setIncludeStatic(boolean includeStatic) { this.includeStatic = includeStatic; return this; } + + /** + * The Java release the module descriptors must be resolved for - the {@code javac --release} value, or + * {@code -target} when {@code --release} is unset. Accepts {@code "1.8"}-style values as well as bare + * major versions. This picks the applicable versioned {@code module-info.class} of a multi-release jar or + * output directory, i.e. the highest {@code META-INF/versions/} whose {@code N} does not exceed this + * release. Defaults to the version of the running JDK, so callers that compile for a different release + * should set this explicitly. + * + * @param targetRelease the target release, never {@code null} + * @return this request + * @since 1.6.1 + */ + public ResolvePathsRequest setTargetRelease(JavaVersion targetRelease) { + this.targetRelease = Objects.requireNonNull(targetRelease, "targetRelease"); + return this; + } + + /** + * @return the Java release module descriptors are resolved for, defaults to the running JDK's version + * @since 1.6.1 + */ + public JavaVersion getTargetRelease() { + return targetRelease; + } } diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java index 245527ea..f0d74b90 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/version/JavaVersion.java @@ -219,6 +219,23 @@ public String getValue(int groups) { return value.toString(); } + /** + * The major version as an integer, parsed from the leading digits of {@link #asMajor()}'s value, so a + * pre-release suffix such as {@code "-ea"} does not prevent parsing, e.g. both {@code "21"} and + * {@code "21-ea"} return {@code 21}, and {@code "1.8"} returns {@code 8}. + * + * @return the major version number + * @throws NumberFormatException if the version does not start with a digit + * @since 1.6.1 + */ + public int getMajorVersion() { + Matcher matcher = startingDigits.matcher(asMajor().getValue(1)); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + throw new NumberFormatException("Cannot parse a major version from '" + rawVersion + "'"); + } + @Override public String toString() { return rawVersion; diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTargetReleaseTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTargetReleaseTest.java new file mode 100644 index 00000000..e1d17dfe --- /dev/null +++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTargetReleaseTest.java @@ -0,0 +1,103 @@ +package org.codehaus.plexus.languages.java.jpms; + +/* + * 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.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; + +import org.codehaus.plexus.languages.java.version.JavaVersion; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * End-to-end coverage of {@link ResolvePathsRequest#setTargetRelease(JavaVersion)}, using the real + * {@link BinaryModuleInfoParser} against a directory that has a versioned module descriptor for both Java 11 + * and Java 17. Runs on every JDK, including Java 8, since the whole point of the target release setting is to + * pick the applicable versioned descriptor without relying on the version of the JDK that runs the build. + */ +class LocationManagerTargetReleaseTest { + private final LocationManager locationManager = new LocationManager(); + + private final Path dir = Paths.get("src/test/test-data/dir.mr.target.release/out"); + + @Test + void targetReleaseSelectsItsOwnVersionedDescriptor() throws Exception { + ResolvePathsRequest request = + ResolvePathsRequest.ofPaths(Collections.singletonList(dir)).setTargetRelease(JavaVersion.parse("11")); + + ResolvePathsResult result = locationManager.resolvePaths(request); + + assertThat(result.getPathElements().get(dir).name()).isEqualTo("dir.mr.target.release.v11"); + } + + @Test + void targetReleaseWorksIndependentlyOfTheRunningJdk() throws Exception { + // On a Java 8 runtime compiling through a Java 9+ toolchain, the running JDK's version must not be used + // to pick the versioned descriptor - this is the scenario from #237 (Maven 3.9 on JDK 8, --release 17). + ResolvePathsRequest request = + ResolvePathsRequest.ofPaths(Collections.singletonList(dir)).setTargetRelease(JavaVersion.parse("17")); + + ResolvePathsResult result = locationManager.resolvePaths(request); + + assertThat(result.getPathElements().get(dir).name()).isEqualTo("dir.mr.target.release.v17"); + } + + @Test + void targetReleaseBelowEveryVersionedDescriptorSelectsNone() throws Exception { + ResolvePathsRequest request = + ResolvePathsRequest.ofPaths(Collections.singletonList(dir)).setTargetRelease(JavaVersion.parse("9")); + + ResolvePathsResult result = locationManager.resolvePaths(request); + + assertThat(result.getPathElements().get(dir)).isNull(); + } + + @Test + void preReleaseTargetRelease() throws Exception { + ResolvePathsRequest request = ResolvePathsRequest.ofPaths(Collections.singletonList(dir)) + .setTargetRelease(JavaVersion.parse("11-ea")); + + ResolvePathsResult result = locationManager.resolvePaths(request); + + assertThat(result.getPathExceptions()).isEmpty(); + assertThat(result.getPathElements().get(dir).name()).isEqualTo("dir.mr.target.release.v11"); + } + + @Test + void singlePathTargetRelease() throws Exception { + ResolvePathRequest request = ResolvePathRequest.ofPath(dir).setTargetRelease(JavaVersion.parse("11")); + + ResolvePathResult result = locationManager.resolvePath(request); + + assertThat(result.getModuleDescriptor().name()).isEqualTo("dir.mr.target.release.v11"); + } + + @Test + void nullTargetReleaseIsRejected() { + assertThatThrownBy(() -> ResolvePathsRequest.ofPaths(Collections.singletonList(dir)) + .setTargetRelease(null)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> ResolvePathRequest.ofPath(dir).setTargetRelease(null)) + .isInstanceOf(NullPointerException.class); + } +} diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java index 69df2f1d..4996714a 100644 --- a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java +++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java @@ -27,6 +27,7 @@ import java.util.HashSet; import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.JavaRequires.JavaModifier; +import org.codehaus.plexus.languages.java.version.JavaVersion; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnJre; @@ -34,7 +35,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class LocationManagerTest { @@ -47,7 +50,7 @@ class LocationManagerTest { final Path mockModuleInfoJava = Paths.get("src/test/test-data/mock/module-info.java"); @BeforeEach - void onSetup() { + void onSetup() throws Exception { asmParser = mock(BinaryModuleInfoParser.class); sourceParser = mock(SourceModuleInfoParser.class); locationManager = new LocationManager(sourceParser) { @@ -56,6 +59,13 @@ ModuleInfoParser getBinaryModuleInfoParser(Path jdkHome) { return asmParser; } }; + + // LocationManager always calls the 2-arg overload now; delegate it to the 1-arg overload so every + // existing when(asmParser.getModuleDescriptor(x)) stub below keeps working regardless of which + // JavaVersion production code passes. Tests that care about the JavaVersion argument itself stub the + // 2-arg overload explicitly, which takes precedence over this catch-all. + when(asmParser.getModuleDescriptor(any(Path.class), any(JavaVersion.class))) + .thenAnswer(invocation -> asmParser.getModuleDescriptor(invocation.getArgument(0, Path.class))); } @Test @@ -314,6 +324,38 @@ void resolvePath() throws Exception { assertThat(result.getModuleNameSource()).isEqualTo(ModuleNameSource.MODULEDESCRIPTOR); } + @Test + void unsetTargetReleaseDefaultsToRunningJdk() throws Exception { + Path p = Paths.get("src/test/test-data/mock/jar0.jar"); + ResolvePathsRequest request = ResolvePathsRequest.ofPaths(Collections.singletonList(p)); + + assertThat(request.getTargetRelease()).isEqualTo(JavaVersion.JAVA_SPECIFICATION_VERSION); + + doReturn(JavaModuleDescriptor.newModule("org.objectweb.asm").build()) + .when(asmParser) + .getModuleDescriptor(p, JavaVersion.JAVA_SPECIFICATION_VERSION); + + locationManager.resolvePaths(request); + + verify(asmParser).getModuleDescriptor(p, JavaVersion.JAVA_SPECIFICATION_VERSION); + } + + @Test + void targetReleaseIsPassedToBinaryParser() throws Exception { + Path p = Paths.get("src/test/test-data/mock/jar0.jar"); + JavaVersion targetRelease = JavaVersion.parse("11"); + ResolvePathsRequest request = + ResolvePathsRequest.ofPaths(Collections.singletonList(p)).setTargetRelease(targetRelease); + + doReturn(JavaModuleDescriptor.newModule("org.objectweb.asm").build()) + .when(asmParser) + .getModuleDescriptor(p, targetRelease); + + locationManager.resolvePaths(request); + + verify(asmParser).getModuleDescriptor(p, targetRelease); + } + @Test void noMatchingProviders() throws Exception { Path abc = Paths.get("src/test/test-data/mock/module-info.java"); // some file called module-info.java diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaVersionTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaVersionTest.java index a45a1325..33362138 100644 --- a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaVersionTest.java +++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/version/JavaVersionTest.java @@ -166,4 +166,12 @@ void valueWithGroups() { assertThat(JavaVersion.parse("3.2.1").getValue(2)).isEqualTo("3.2"); assertThat(JavaVersion.parse("3.2.1").getValue(3)).isEqualTo("3.2.1"); } + + @Test + void getMajorVersion() { + assertEquals(8, JavaVersion.parse("1.8").getMajorVersion()); + assertEquals(11, JavaVersion.parse("11.0.2").getMajorVersion()); + assertEquals(21, JavaVersion.parse("21-ea").getMajorVersion()); + assertEquals(25, JavaVersion.parse("25").getMajorVersion()); + } } diff --git a/plexus-java/src/test/test-data/dir.mr.target.release/out/META-INF/versions/11/module-info.class b/plexus-java/src/test/test-data/dir.mr.target.release/out/META-INF/versions/11/module-info.class new file mode 100644 index 0000000000000000000000000000000000000000..82860a8f7ace906e6c40f9f2fccdcbb42152282e GIT binary patch literal 168 zcmX^0Z`VEs1_o0GZgvJHMh5QO{FKt1RNc(Hw0uSeuHgLAqU2P!%$!t41_2}~y{yEt zL`DWSU$AOn26jdU$&}0@z1$+blEk9))Dpd-)ST4B;#9pdLqlN(PDTb!kPf{hpb#Si oqh$jF10&E@2w-Ah1~Ql!SQuD=v={>$SeTK4qk(~e3(N)*0LnWe^Z)<= literal 0 HcmV?d00001 diff --git a/plexus-java/src/test/test-data/dir.mr.target.release/out/META-INF/versions/17/module-info.class b/plexus-java/src/test/test-data/dir.mr.target.release/out/META-INF/versions/17/module-info.class new file mode 100644 index 0000000000000000000000000000000000000000..b8d4d8fd1d52a6c42e3f218b684252c13e66d715 GIT binary patch literal 168 zcmX^0Z`VEs1_o0GZgvJHMh5QO{FKt1RNc(Hw0uSeuHgLAqU2P!%$!t41_2}~y{yEt zL`DWSU$AOn26jdU$&}0@z1$+blEk9))Dpd-)ST4B;#9pdLvvvUPDTb!kPf{hpb#Si oqh$jF10&E@2w-Ah1~Ql!SQuD=v={>$SeTK4qk(~e3(N)*0L$MZ`Tzg` literal 0 HcmV?d00001