diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index faf344f..af403ad 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -23,6 +23,13 @@ jobs:
build:
name: Build it
uses: codehaus-plexus/.github/.github/workflows/maven.yml@master
+ with:
+ matrix-include: >
+ [
+ { "os": "ubuntu-latest", "jdk": "26", "distribution": "zulu" },
+ { "os": "ubuntu-latest", "jdk": "27", "distribution": "zulu" },
+ { "os": "ubuntu-latest", "jdk": "28-ea", "distribution": "temurin" }
+ ]
# deploy:
# name: Deploy
diff --git a/plexus-java/pom.xml b/plexus-java/pom.xml
index 6ca4feb..8fcafcc 100644
--- a/plexus-java/pom.xml
+++ b/plexus-java/pom.xml
@@ -128,6 +128,41 @@
+
+
+ jdk24
+
+ [24,)
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ jdk24
+
+ compile
+
+
+ 24
+ true
+
+ ${project.basedir}/src/main/java24
+
+
+
+
+
+
+
+
+
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 ac0b710..feb2001 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
@@ -109,4 +109,24 @@ static Path findVersionedModuleInfo(Path directory, JavaVersion jdkVersion) {
}
abstract JavaModuleDescriptor parse(InputStream in) throws IOException;
+
+ /**
+ * Whether this parser reads the {@code Module} attribute with the runtime's own
+ * {@code java.lang.classfile} API (JEP 484, final since Java 24) rather than ASM or
+ * {@code java.lang.module.ModuleDescriptor}.
+ *
+ * {@link BinaryModuleInfoParser} is a multi-release class: depending on the JDK actually
+ * running the build, {@code new BinaryModuleInfoParser()} resolves to the Java 8 (ASM-backed),
+ * Java 9 ({@code java.lang.module}-backed) or Java 24 ({@code java.lang.classfile}-backed)
+ * implementation. {@link LocationManager#getBinaryModuleInfoParser(java.nio.file.Path)} cannot
+ * reference {@code java.lang.classfile} types directly — that package doesn't exist
+ * before Java 24, and {@code LocationManager} is compiled once at the Java 8 baseline —
+ * so it asks the resolved instance about its own capability instead.
+ *
+ * @return {@code true} for the {@code src/main/java24} implementation, {@code false} otherwise
+ * @since 1.7.0
+ */
+ boolean isClassFileApiBased() {
+ return false;
+ }
}
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 714df3d..b9f16fb 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
@@ -279,19 +279,21 @@ public ResolvePathsResult resolvePaths(final ResolvePathsRequest reque
/**
* If the jdkHome is specified, its version it considered higher than the runtime java version.
- * In that case ASM must be used to read the module descriptor
+ * In that case ASM must be used to read the module descriptor, unless this runtime's own
+ * {@link BinaryModuleInfoParser} is the Java 24 {@code java.lang.classfile}-based one: like the
+ * ASM parser (since codehaus-plexus/plexus-languages#239) it clamps a class file major version
+ * newer than it supports down to the newest one it knows, so a toolchain pointing at a
+ * different JDK than this process is running on is not a reason to prefer ASM over it.
*
* @param jdkHome
* @return
*/
ModuleInfoParser getBinaryModuleInfoParser(final Path jdkHome) {
- final ModuleInfoParser binaryParser;
- if (jdkHome == null) {
- binaryParser = new BinaryModuleInfoParser();
- } else {
- binaryParser = new AsmModuleInfoParser();
+ BinaryModuleInfoParser runtimeParser = new BinaryModuleInfoParser();
+ if (jdkHome == null || runtimeParser.isClassFileApiBased()) {
+ return runtimeParser;
}
- return binaryParser;
+ return new AsmModuleInfoParser();
}
private JavaModuleDescriptor getMainModuleDescriptor(
diff --git a/plexus-java/src/main/java24/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java b/plexus-java/src/main/java24/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java
new file mode 100644
index 0000000..97a9f7e
--- /dev/null
+++ b/plexus-java/src/main/java24/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java
@@ -0,0 +1,142 @@
+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.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.classfile.Attributes;
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.ClassModel;
+import java.lang.classfile.attribute.ModuleAttribute;
+import java.lang.classfile.attribute.ModuleExportInfo;
+import java.lang.classfile.attribute.ModuleProvideInfo;
+import java.lang.classfile.attribute.ModuleRequireInfo;
+import java.lang.classfile.constantpool.ClassEntry;
+import java.lang.classfile.constantpool.ModuleEntry;
+import java.lang.reflect.AccessFlag;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.Builder;
+
+/**
+ * Extract information from a module descriptor with the {@code java.lang.classfile} API
+ * (JEP 484), final since Java 24. Multi-release counterpart of the {@code src/main/java9}
+ * {@code BinaryModuleInfoParser}, which reads the same information with
+ * {@code java.lang.module.ModuleDescriptor}.
+ *
+ * @see JEP 484: Class-File API
+ * @see plexus-languages#165
+ * @since 1.7.0
+ */
+class BinaryModuleInfoParser extends AbstractBinaryModuleInfoParser {
+
+ // The highest class file major version this JDK's Class-File API knows how to read. A
+ // module-info.class produced by a newer toolchain than the running JDK (major version above
+ // this) makes ClassFile.of().parse(...) throw IllegalArgumentException, exactly like
+ // java.lang.module.ModuleDescriptor.read(...) and, before it was patched, ASM's ClassReader
+ // (see AsmModuleInfoParser and codehaus-plexus/plexus-languages#165: "Unsupported class file
+ // major version 72" on a JDK that only knows up to 71). The Module attribute format hasn't
+ // changed since Java 9, so it's safe to clamp the major version down to the newest one this
+ // runtime supports before parsing.
+ private static final int NEWEST_MAJOR_VERSION_SUPPORTED = ClassFile.latestMajorVersion();
+
+ @Override
+ JavaModuleDescriptor parse(InputStream in) throws IOException {
+ byte[] classBytes = toByteArray(in);
+
+ // bytes 6-7 are the big-endian major version
+ int majorVersion = ((classBytes[6] & 0xFF) << 8) | (classBytes[7] & 0xFF);
+ if (majorVersion > NEWEST_MAJOR_VERSION_SUPPORTED) {
+ classBytes[6] = (byte) (NEWEST_MAJOR_VERSION_SUPPORTED >>> 8);
+ classBytes[7] = (byte) NEWEST_MAJOR_VERSION_SUPPORTED;
+ }
+
+ ClassModel classModel = ClassFile.of().parse(classBytes);
+ ModuleAttribute moduleAttribute = classModel
+ .findAttribute(Attributes.module())
+ .orElseThrow(() -> new IOException("Not a module-info.class: no Module attribute found"));
+
+ Builder builder = JavaModuleDescriptor.newModule(
+ moduleAttribute.moduleName().name().stringValue());
+
+ for (ModuleRequireInfo requires : moduleAttribute.requires()) {
+ Set requiresFlags = requires.requiresFlags();
+ if (requiresFlags.contains(AccessFlag.STATIC_PHASE) || requiresFlags.contains(AccessFlag.TRANSITIVE)) {
+ Set modifiers = new LinkedHashSet<>();
+ if (requiresFlags.contains(AccessFlag.STATIC_PHASE)) {
+ modifiers.add(JavaModuleDescriptor.JavaRequires.JavaModifier.STATIC);
+ }
+ if (requiresFlags.contains(AccessFlag.TRANSITIVE)) {
+ modifiers.add(JavaModuleDescriptor.JavaRequires.JavaModifier.TRANSITIVE);
+ }
+ builder.requires(modifiers, requires.requires().name().stringValue());
+ } else {
+ builder.requires(requires.requires().name().stringValue());
+ }
+ }
+
+ for (ModuleExportInfo exports : moduleAttribute.exports()) {
+ String packageName = exports.exportedPackage().name().stringValue().replace('/', '.');
+ if (exports.exportsTo().isEmpty()) {
+ builder.exports(packageName);
+ } else {
+ Set targets = exports.exportsTo().stream()
+ .map(ModuleEntry::name)
+ .map(name -> name.stringValue())
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ builder.exports(packageName, targets);
+ }
+ }
+
+ for (ClassEntry uses : moduleAttribute.uses()) {
+ builder.uses(uses.name().stringValue().replace('/', '.'));
+ }
+
+ for (ModuleProvideInfo provides : moduleAttribute.provides()) {
+ String service = provides.provides().name().stringValue().replace('/', '.');
+ List providers = provides.providesWith().stream()
+ .map(ClassEntry::name)
+ .map(name -> name.stringValue().replace('/', '.'))
+ .collect(Collectors.toList());
+ builder.provides(service, providers);
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ boolean isClassFileApiBased() {
+ return true;
+ }
+
+ private static byte[] toByteArray(InputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ return out.toByteArray();
+ }
+}
diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/ClassFileModuleInfoParserIT.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/ClassFileModuleInfoParserIT.java
new file mode 100644
index 0000000..90cf655
--- /dev/null
+++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/ClassFileModuleInfoParserIT.java
@@ -0,0 +1,120 @@
+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.io.ByteArrayInputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.JavaRequires;
+import org.codehaus.plexus.languages.java.version.JavaVersion;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Exercises the {@code java.lang.classfile}-based module descriptor parser added under
+ * {@code src/main/java24} to prototype "option C" for
+ * #165.
+ *
+ * This class deliberately never mentions a {@code java.lang.classfile} type: it is compiled at
+ * this module's Java 8 baseline like every other test, and it only proves anything once run
+ * against the packaged multi-release jar. Directory classpaths (the {@code test} phase) are never
+ * multi-release aware, so {@code new BinaryModuleInfoParser()} there always resolves to whichever
+ * variant this module's own compilation produced last, regardless of the running JDK; only the
+ * jar's {@code Multi-Release: true} manifest entry, honoured by {@code JarFile}/{@code
+ * URLClassLoader} once failsafe reruns the suite against {@code target/*.jar} (see this module's
+ * pom.xml and {@link LocationManagerIT}), makes the JVM pick the {@code META-INF/versions/24}
+ * class on a Java 24+ runtime.
+ */
+@EnabledForJreRange(min = JRE.JAVA_24, disabledReason = "src/main/java24 is only in the jar's MR layer on a 24+ build")
+class ClassFileModuleInfoParserIT {
+
+ private final LocationManager locationManager = new LocationManager();
+
+ private final Path moduleInfo = Paths.get("src/test/test-data/dir.descriptor.requires/out/module-info.class");
+
+ @Test
+ void parsesAnOrdinaryModuleDescriptor() throws Exception {
+ JavaModuleDescriptor descriptor =
+ locationManager.getBinaryModuleInfoParser(null).getModuleDescriptor(moduleInfo.getParent());
+
+ assertNotNull(descriptor);
+ assertThat(descriptor.requires()).hasSize(5);
+ assertEquals(expectedRequires(), descriptor.requires());
+ }
+
+ @Test
+ void clampsAModuleDescriptorNewerThanTheRuntimeSupports() throws Exception {
+ byte[] classBytes = Files.readAllBytes(moduleInfo);
+
+ // Bytes 6-7 are the big-endian class file major version. This JDK's classfile major
+ // version is its feature version + 44 (Java 9 -> 53, ..., Java 24 -> 68, see
+ // java.lang.classfile.ClassFile.JAVA_9_VERSION..JAVA_24_VERSION); one past that is a
+ // version no released JDK can parse yet, e.g. a toolchain one release ahead of this one.
+ int unsupportedMajorVersion = JavaVersion.JAVA_SPECIFICATION_VERSION.getMajorVersion() + 44 + 1;
+ classBytes[6] = (byte) (unsupportedMajorVersion >>> 8);
+ classBytes[7] = (byte) unsupportedMajorVersion;
+
+ ModuleInfoParser parser = locationManager.getBinaryModuleInfoParser(null);
+ JavaModuleDescriptor descriptor;
+ try (ByteArrayInputStream in = new ByteArrayInputStream(classBytes)) {
+ descriptor = ((AbstractBinaryModuleInfoParser) parser).parse(in);
+ }
+
+ assertNotNull(descriptor);
+ assertThat(descriptor.requires()).hasSize(5);
+ assertEquals(expectedRequires(), descriptor.requires());
+ }
+
+ @Test
+ void isPreferredOverAsmForAToolchainOnA24PlusRuntime() {
+ // #165: a toolchain (jdkHome) used to always fall back to AsmModuleInfoParser, on the
+ // assumption that only ASM could tolerate a class file version newer than this process
+ // understands. The java.lang.classfile parser clamps just like ASM does (see
+ // codehaus-plexus/plexus-languages#239), so on a 24+ runtime it should be used here too.
+ ModuleInfoParser parser = locationManager.getBinaryModuleInfoParser(Paths.get("some/jdk/home"));
+
+ assertThat(parser).isNotInstanceOf(AsmModuleInfoParser.class);
+ }
+
+ private static Set expectedRequires() {
+ return JavaModuleDescriptor.newAutomaticModule("_")
+ .requires("java.base")
+ .requires("mod_r")
+ .requires(Collections.singleton(JavaRequires.JavaModifier.STATIC), "mod_r_s")
+ .requires(Collections.singleton(JavaRequires.JavaModifier.TRANSITIVE), "mod_r_t")
+ .requires(
+ new HashSet<>(
+ Arrays.asList(JavaRequires.JavaModifier.STATIC, JavaRequires.JavaModifier.TRANSITIVE)),
+ "mod_r_s_t")
+ .build()
+ .requires();
+ }
+}
diff --git a/pom.xml b/pom.xml
index c0621f3..c6ddb64 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,7 @@
src/main/java/**/*.java
src/main/java9/**/*.java
+ src/main/java24/**/*.java
src/test/java/**/*.java
@@ -110,7 +111,8 @@
- 9
+ 24
+ Releases must be built on JDK 24+, otherwise the jar silently lacks the META-INF/versions/24 layer.
@@ -120,5 +122,16 @@
+
+
+ jdk27-skip-spotless
+
+ [27,)
+
+
+ true
+ true
+
+