gradle/generation/extract-jdk-apis/ExtractJdkApis.java (156 lines of code) (raw):

/* * 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.IOException; import java.lang.classfile.AccessFlags; import java.lang.classfile.Attributes; import java.lang.classfile.ClassFile; import java.lang.classfile.ClassFileBuilder; import java.lang.classfile.ClassFileElement; import java.lang.classfile.ClassFileVersion; import java.lang.classfile.ClassModel; import java.lang.classfile.ClassTransform; import java.lang.classfile.CodeModel; import java.lang.classfile.FieldModel; import java.lang.classfile.MethodModel; import java.lang.classfile.MethodTransform; import java.lang.classfile.attribute.InnerClassesAttribute; import java.lang.classfile.attribute.RuntimeInvisibleAnnotationsAttribute; import java.lang.classfile.attribute.SourceFileAttribute; import java.lang.classfile.constantpool.ClassEntry; import java.lang.constant.ClassDesc; import java.lang.invoke.MethodHandles; import java.lang.reflect.AccessFlag; import java.lang.reflect.ClassFileFormatVersion; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public final class ExtractJdkApis { private static final FileTime FIXED_FILEDATE = FileTime.from(Instant.parse("2025-05-05T00:00:00Z")); private static final String PATTERN_VECTOR_INCUBATOR = "jdk.incubator.vector/jdk/incubator/vector/*"; private static final String PATTERN_VECTOR_VM_INTERNALS = "java.base/jdk/internal/vm/vector/VectorSupport{,$Vector,$VectorMask,$VectorPayload,$VectorShuffle}"; static final Map<Integer,List<String>> CLASSFILE_PATTERNS = Map.of( 24, List.of(PATTERN_VECTOR_VM_INTERNALS, PATTERN_VECTOR_INCUBATOR) ); private static final ClassDesc CD_PreviewFeature = ClassDesc.ofInternalName("jdk/internal/javac/PreviewFeature"); public static void main(String... args) throws IOException { if (args.length != 3) { throw new IllegalArgumentException("Need two parameters: target java version, extract java version, output file"); } int targetJdk = Integer.parseInt(args[0]); Integer extractJdk = Integer.valueOf(args[1]); int runtimeJdk = Runtime.version().feature(); if (extractJdk.intValue() != runtimeJdk) { throw new IllegalStateException("Incorrect runtime java version: " + runtimeJdk); } if (extractJdk.intValue() < targetJdk) { throw new IllegalStateException("extract java version " + extractJdk + " < target java version " + targetJdk); } if (!CLASSFILE_PATTERNS.containsKey(extractJdk)) { throw new IllegalArgumentException("No support to extract stubs from java version: " + extractJdk); } var outputPath = Paths.get(args[2]); // the output class files need to be compatible with the targetJdk of our compilation, so we need to adapt them: var classFileVersion = ClassFileVersion.of(ClassFileFormatVersion.valueOf("RELEASE_" + targetJdk).major(), 0); // create JRT filesystem and build a combined FileMatcher: var jrtPath = Paths.get(URI.create("jrt:/")).toRealPath(); var patterns = CLASSFILE_PATTERNS.get(extractJdk).stream() .map(pattern -> jrtPath.getFileSystem().getPathMatcher("glob:" + pattern + ".class")) .toArray(PathMatcher[]::new); PathMatcher pattern = p -> Arrays.stream(patterns).anyMatch(matcher -> matcher.matches(p)); // Collect all files to process: final List<Path> filesToExtract; try (var stream = Files.walk(jrtPath)) { filesToExtract = stream.filter(p -> pattern.matches(jrtPath.relativize(p))).toList(); } // Process all class files: try (var out = new ZipOutputStream(Files.newOutputStream(outputPath))) { process(filesToExtract, out, classFileVersion); } } private static void process(List<Path> filesToExtract, ZipOutputStream out, ClassFileVersion classFileVersion) throws IOException { System.out.println("Loading and analyzing " + filesToExtract.size() + " class files..."); var classesToInclude = new HashSet<String>(); var toProcess = new TreeMap<String, ClassModel>(); var cc = ClassFile.of(ClassFile.ConstantPoolSharingOption.NEW_POOL, ClassFile.DebugElementsOption.DROP_DEBUG, ClassFile.LineNumbersOption.DROP_LINE_NUMBERS, ClassFile.StackMapsOption.DROP_STACK_MAPS); for (Path p : filesToExtract) { ClassModel parsed = cc.parse(p); String internalName = parsed.thisClass().asInternalName(); toProcess.put(internalName, parsed); if (isVisible(parsed.flags())) { classesToInclude.add(internalName); } } // recursively add all superclasses / interfaces / outer classes of visible classes to classesToInclude: for (Set<String> a = classesToInclude; !a.isEmpty();) { classesToInclude.addAll(a = a.stream().map(toProcess::get).filter(Objects::nonNull).flatMap(ExtractJdkApis::getReferences).collect(Collectors.toSet())); } // remove all non-visible or not referenced classes: toProcess.keySet().removeIf(Predicate.not(classesToInclude::contains)); // transformation of class files: System.out.println("Writing " + toProcess.size() + " visible classes..."); for (var parsed : toProcess.values()) { String internalName = parsed.thisClass().asInternalName(); System.out.println("Writing stub for class: " + internalName); ClassTransform ct = ClassTransform.dropping(ce -> switch (ce) { case MethodModel e -> !isVisible(e.flags()); case FieldModel e -> !isVisible(e.flags()); case SourceFileAttribute _ -> true; default -> false; }).andThen((builder, ce) -> { switch (ce) { case ClassFileVersion _ -> builder.with(classFileVersion); // the PreviewFeature annotation may refer to its own inner classes and therefore we must get rid of the inner class entry: case InnerClassesAttribute a -> builder.with(InnerClassesAttribute.of(a.classes().stream() .filter(c -> !CD_PreviewFeature.equals(c.outerClass().map(ClassEntry::asSymbol).orElse(null))) .toList())); default -> builder.with(ce); } }).andThen(ExtractJdkApis::dropPreview) .andThen(ClassTransform.transformingMethods(MethodTransform.dropping(CodeModel.class::isInstance).andThen(ExtractJdkApis::dropPreview)) .andThen(ClassTransform.transformingFields(ExtractJdkApis::dropPreview))); out.putNextEntry(new ZipEntry(internalName.concat(".class")).setLastModifiedTime(FIXED_FILEDATE)); out.write(cc.transformClass(parsed, ct)); out.closeEntry(); } // make sure that no classes are left over except those which are in java.base module classesToInclude.removeIf(toProcess.keySet()::contains); var missingClasses = classesToInclude.stream().filter(internalName -> { try { return ClassDesc.ofInternalName(internalName).resolveConstantDesc(MethodHandles.publicLookup()).getModule() != Object.class.getModule(); } catch (ReflectiveOperationException _) { return true; } }).sorted().toList(); if (!missingClasses.isEmpty()) { throw new IllegalStateException("Some referenced classes are not publicly available in java.base module: " + missingClasses); } } /** returns all superclasses, interfaces, and outer classes of the parsed class as stream of internal names */ private static Stream<String> getReferences(ClassModel parsed) { var parents = Stream.concat(parsed.superclass().stream(), parsed.interfaces().stream()) .map(ClassEntry::asInternalName).collect(Collectors.toSet()); var outerClasses = parsed.findAttributes(Attributes.innerClasses()).stream() .flatMap(a -> a.classes().stream()) .filter(i -> parents.contains(i.innerClass().asInternalName())) .flatMap(i -> i.outerClass().stream()) .map(ClassEntry::asInternalName); return Stream.concat(parents.stream(), outerClasses); } @SuppressWarnings("unchecked") // no idea how to get generics correct!?! private static <E extends ClassFileElement, B extends ClassFileBuilder<E, B>> void dropPreview(ClassFileBuilder<E, B> builder, E ele) { switch (ele) { case RuntimeInvisibleAnnotationsAttribute att -> builder.with((E) RuntimeInvisibleAnnotationsAttribute.of(att.annotations().stream() .filter(ann -> !CD_PreviewFeature.equals(ann.classSymbol())) .toList())); default -> builder.with(ele); } } private static boolean isVisible(AccessFlags access) { return access.has(AccessFlag.PUBLIC) || access.has(AccessFlag.PROTECTED); } }