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 + +