From 5921eb2a141f0dcc83c6a5d7dcd5035a30c5edfc Mon Sep 17 00:00:00 2001 From: Eric Bruneton Date: Sun, 31 Jul 2022 12:21:41 +0000 Subject: [PATCH] Generate the module info classes without Bnd. Delete the Bnd plugin. --- .../asm/tools/ModuleInfoBndPlugin.java | 99 ------ .../org/objectweb/asm/tools/Retrofitter.java | 336 +++++++++++++----- 4 files changed, 267 insertions(+), 206 deletions(-) delete mode 100644 tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java diff --git a/tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java b/tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java deleted file mode 100644 index ee91bdc1..00000000 --- a/tools/bnd-module-plugin/src/main/java/org/objectweb/asm/tools/ModuleInfoBndPlugin.java +++ /dev/null @@ -1,99 +0,0 @@ -// ASM: a very small and fast Java bytecode manipulation framework -// Copyright (c) 2000-2011 INRIA, France Telecom -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// 3. Neither the name of the copyright holders nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -package org.objectweb.asm.tools; - -import aQute.bnd.header.Attrs; -import aQute.bnd.header.Parameters; -import aQute.bnd.osgi.Analyzer; -import aQute.bnd.osgi.Constants; -import aQute.bnd.osgi.EmbeddedResource; -import aQute.bnd.service.AnalyzerPlugin; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.ModuleVisitor; -import org.objectweb.asm.Opcodes; - -/** - * An biz.aQute.bnd plugin to generate a module-info class from the name, version, requires and - * export properties of the bundle. - * - * @author Remi Forax - */ -public class ModuleInfoBndPlugin implements AnalyzerPlugin { - private static final String MODULE_NAME = "Module-Name"; - private static final String MODULE_VERSION = "Module-Version"; - private static final String MODULE_REQUIRES = "Module-Requires"; - private static final String MODULE_EXPORTS = "Module-Exports"; - - @Override - public boolean analyzeJar(final Analyzer analyzer) throws Exception { - ClassWriter classWriter = new ClassWriter(0); - classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); - String moduleName = - analyzer.getProperty(MODULE_NAME, analyzer.getProperty(Constants.BUNDLE_SYMBOLICNAME)); - String moduleVersion = - analyzer.getProperty(MODULE_VERSION, analyzer.getProperty(Constants.BUNDLE_VERSION)); - ModuleVisitor moduleVisitor = - classWriter.visitModule(moduleName, Opcodes.ACC_OPEN, moduleVersion); - - String requireModules = analyzer.getProperty(MODULE_REQUIRES); - if (requireModules != null) { - Parameters requireParams = analyzer.parseHeader(requireModules); - for (String requireName : requireParams.keySet()) { - Attrs attrs = requireParams.get(requireName); - boolean isTransitive = attrs.containsKey("transitive"); - boolean isStatic = attrs.containsKey("static"); - moduleVisitor.visitRequire( - requireName, - (isTransitive ? Opcodes.ACC_TRANSITIVE : 0) | (isStatic ? Opcodes.ACC_STATIC_PHASE : 0), - null); - } - } - moduleVisitor.visitRequire("java.base", Opcodes.ACC_MANDATED, null); - - String exportPackages = - analyzer.getProperty(MODULE_EXPORTS, analyzer.getProperty(Constants.EXPORT_PACKAGE)); - if (exportPackages != null) { - Parameters exportParams = analyzer.parseHeader(exportPackages); - for (String packageName : exportParams.keySet()) { - if (packageName.endsWith("*")) { - throw new IllegalStateException("Unsupported wildcard packages " + packageName); - } - moduleVisitor.visitExport(packageName.replace('.', '/'), 0); - } - } - moduleVisitor.visitEnd(); - classWriter.visitEnd(); - - analyzer - .getJar() - .putResource( - "module-info.class", - new EmbeddedResource(classWriter.toByteArray(), System.currentTimeMillis())); - return false; - } -} diff --git a/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java b/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java index 2d017ed2..b3d51e63 100644 --- a/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java +++ b/tools/retrofitter/src/main/java/org/objectweb/asm/tools/Retrofitter.java @@ -27,22 +27,33 @@ // THE POSSIBILITY OF SUCH DAMAGE. package org.objectweb.asm.tools; +import static java.lang.String.format; +import static java.util.stream.Collectors.toSet; + import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; -import java.io.OutputStream; +import java.lang.module.ModuleDescriptor; import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.ModuleVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -57,6 +68,12 @@ import org.objectweb.asm.Type; */ public class Retrofitter { + /** The name of the module-info file. */ + private static final String MODULE_INFO = "module-info.class"; + + /** The name of the java.base module. */ + private static final String JAVA_BASE_MODULE = "java.base"; + /** * The fields and methods of the JDK 1.5 API. Each string has the form * "<owner><name><descriptor>". @@ -68,12 +85,158 @@ public class Retrofitter { */ private final HashMap jdkHierarchy = new HashMap<>(); + /** The internal names of the packages exported by the retrofitted classes. */ + private final HashSet exports = new HashSet<>(); + + /** The internal names of the packages imported by the retrofitted classes. */ + private final HashSet imports = new HashSet<>(); + + /** + * Transforms the class files in the given directory, in place, in order to make them compatible + * with the JDK 1.5. Also generates a module-info class in this directory, with the given module + * version. + * + * @param args a directory containing compiled classes and the ASM release version. + * @throws IOException if a file can't be read or written. + */ + public static void main(final String[] args) throws IOException { + if (args.length == 2) { + new Retrofitter().retrofit(new File(args[0]), args[1]); + } else { + System.err.println("Usage: Retrofitter "); // NOPMD + } + } + + /** + * Transforms the class files in the given directory, in place, in order to make them compatible + * with the JDK 1.5. Also generates a module-info class in this directory, with the given module + * version. + * + * @param classesDir a directory containing compiled classes. + * @param version the module-info version. + * @throws IOException if a file can't be read or written. + */ + public void retrofit(final File classesDir, final String version) throws IOException { + for (File classFile : getAllClasses(classesDir, new ArrayList())) { + ClassReader classReader = new ClassReader(Files.newInputStream(classFile.toPath())); + ClassWriter classWriter = new ClassWriter(0); + classReader.accept(new ClassRetrofitter(classWriter), ClassReader.SKIP_FRAMES); + Files.write(classFile.toPath(), classWriter.toByteArray()); + } + generateModuleInfoClass(classesDir, version); + } + /** - * Constructs a new {@link Retrofitter}. + * Verify that the class files in the given directory only use JDK 1.5 APIs, and that a + * module-info class is present with the expected content. * - * @throws IOException if the JDK API description file can't be read. + * @param classesDir a directory containing compiled classes. + * @param expectedVersion the expected module-info version. + * @param expectedExports the expected module-info exported packages. + * @param expectedRequires the expected module-info required modules. + * @throws IOException if a file can't be read. + * @throws IllegalArgumentException if the module-info class does not have the expected content. */ - public Retrofitter() throws IOException { + public void verify( + final File classesDir, + final String expectedVersion, + final List expectedExports, + final List expectedRequires) + throws IOException { + if (jdkApi.isEmpty()) { + readJdkApi(); + } + for (File classFile : getAllClasses(classesDir, new ArrayList())) { + if (!classFile.getName().equals(MODULE_INFO)) { + new ClassReader(Files.newInputStream(classFile.toPath())).accept(new ClassVerifier(), 0); + } + } + verifyModuleInfoClass( + classesDir, + expectedVersion, + new HashSet(expectedExports), + Stream.concat(expectedRequires.stream(), Stream.of(JAVA_BASE_MODULE)).collect(toSet())); + } + + private List getAllClasses(final File file, final List allClasses) + throws IOException { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children == null) { + throw new IOException("Unable to read files of " + file); + } + for (File child : children) { + getAllClasses(child, allClasses); + } + } else if (file.getName().endsWith(".class")) { + allClasses.add(file); + } + return allClasses; + } + + private void generateModuleInfoClass(final File dstDir, final String version) throws IOException { + ClassWriter classWriter = new ClassWriter(0); + classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); + ArrayList moduleNames = new ArrayList<>(); + for (String exportName : exports) { + if (isAsmModule(exportName)) { + moduleNames.add(exportName); + } + } + if (moduleNames.size() != 1) { + throw new IllegalArgumentException("Module name can't be infered from classes"); + } + ModuleVisitor moduleVisitor = + classWriter.visitModule(moduleNames.get(0), Opcodes.ACC_OPEN, version); + + for (String importName : imports) { + if (isAsmModule(importName) && !exports.contains(importName)) { + moduleVisitor.visitRequire(importName.replace('/', '.'), Opcodes.ACC_TRANSITIVE, null); + } + } + moduleVisitor.visitRequire(JAVA_BASE_MODULE, Opcodes.ACC_MANDATED, null); + + for (String exportName : exports) { + moduleVisitor.visitExport(exportName, 0); + } + moduleVisitor.visitEnd(); + classWriter.visitEnd(); + Files.write(Path.of(dstDir.getAbsolutePath(), MODULE_INFO), classWriter.toByteArray()); + } + + private void verifyModuleInfoClass( + final File dstDir, + final String expectedVersion, + final Set expectedExports, + final Set expectedRequires) + throws IOException { + ModuleDescriptor module = + ModuleDescriptor.read(Files.newInputStream(Path.of(dstDir.getAbsolutePath(), MODULE_INFO))); + String version = module.version().map(ModuleDescriptor.Version::toString).orElse(""); + if (!version.equals(expectedVersion)) { + throw new IllegalArgumentException( + format("Wrong module-info version '%s' (expected '%s')", version, expectedVersion)); + } + Set exports = + module.exports().stream().map(ModuleDescriptor.Exports::source).collect(toSet()); + if (!exports.equals(expectedExports)) { + throw new IllegalArgumentException( + format("Wrong module-info exports %s (expected %s)", exports, expectedExports)); + } + Set requires = + module.requires().stream().map(ModuleDescriptor.Requires::name).collect(toSet()); + if (!requires.equals(expectedRequires)) { + throw new IllegalArgumentException( + format("Wrong module-info requires %s (expected %s)", requires, expectedRequires)); + } + } + + private static boolean isAsmModule(final String packageName) { + return packageName.startsWith("org/objectweb/asm") + && !packageName.equals("org/objectweb/asm/signature"); + } + + private void readJdkApi() throws IOException { try (InputStream inputStream = new GZIPInputStream( Retrofitter.class.getClassLoader().getResourceAsStream("jdk1.5.0.12.txt.gz")); @@ -97,56 +260,8 @@ public class Retrofitter { } } - /** - * Transforms the source class file, or if it is a directory, its files (recursively), in place, - * in order to make them compatible with the JDK 1.5. - * - * @param src source file or directory. - * @throws IOException if the source files can't be read or written. - */ - public void retrofit(final File src) throws IOException { - retrofit(src, null); - } - - /** - * Transforms the source class file, or if it is a directory, its files (recursively), either in - * place or into the destination file or directory, in order to make them compatible with the JDK - * 1.5. - * - * @param src source file or directory. - * @param dst optional destination file or directory. - * @throws IOException if the source or destination file can't be read or written. - */ - public void retrofit(final File src, final File dst) throws IOException { - if (src.isDirectory()) { - File[] files = src.listFiles(); - if (files == null) { - throw new IOException("Unable to read files of " + src); - } - for (File file : files) { - retrofit(file, dst == null ? null : new File(dst, file.getName())); - } - } else if (src.getName().endsWith(".class")) { - if (dst == null || !dst.exists() || dst.lastModified() < src.lastModified()) { - ClassReader classReader = new ClassReader(Files.newInputStream(src.toPath())); - ClassWriter classWriter = new ClassWriter(0); - ClassVerifier classVerifier = new ClassVerifier(classWriter); - ClassRetrofitter classRetrofitter = new ClassRetrofitter(classVerifier); - classReader.accept(classRetrofitter, ClassReader.SKIP_FRAMES); - - if (dst != null && !dst.getParentFile().exists() && !dst.getParentFile().mkdirs()) { - throw new IOException("Cannot create directory " + dst.getParentFile()); - } - try (OutputStream outputStream = - Files.newOutputStream((dst == null ? src : dst).toPath())) { - outputStream.write(classWriter.toByteArray()); - } - } - } - } - /** A ClassVisitor that retrofits classes to 1.5 version. */ - static class ClassRetrofitter extends ClassVisitor { + class ClassRetrofitter extends ClassVisitor { public ClassRetrofitter(final ClassVisitor classVisitor) { super(/* latest api =*/ Opcodes.ASM8, classVisitor); @@ -160,9 +275,21 @@ public class Retrofitter { final String signature, final String superName, final String[] interfaces) { + addPackageReferences(Type.getObjectType(name), /* export = */ true); super.visit(Opcodes.V1_5, access, name, signature, superName, interfaces); } + @Override + public FieldVisitor visitField( + final int access, + final String name, + final String descriptor, + final String signature, + final Object value) { + addPackageReferences(Type.getType(descriptor), /* export = */ false); + return super.visitField(access, name, descriptor, signature, value); + } + @Override public MethodVisitor visitMethod( final int access, @@ -170,9 +297,17 @@ public class Retrofitter { final String descriptor, final String signature, final String[] exceptions) { + addPackageReferences(Type.getType(descriptor), /* export = */ false); return new MethodVisitor( api, super.visitMethod(access, name, descriptor, signature, exceptions)) { + @Override + public void visitFieldInsn( + final int opcode, final String owner, final String name, final String descriptor) { + addPackageReferences(Type.getType(descriptor), /* export = */ false); + super.visitFieldInsn(opcode, owner, name, descriptor); + } + @Override public void visitMethodInsn( final int opcode, @@ -180,6 +315,7 @@ public class Retrofitter { final String name, final String descriptor, final boolean isInterface) { + addPackageReferences(Type.getType(descriptor), /* export = */ false); // Remove the addSuppressed() method calls generated for try-with-resources statements. // This method is not defined in JDK1.5. if (owner.equals("java/lang/Throwable") @@ -190,8 +326,52 @@ public class Retrofitter { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } } + + @Override + public void visitTypeInsn(final int opcode, final String type) { + addPackageReferences(Type.getObjectType(type), /* export = */ false); + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) { + addPackageReferences(Type.getType(descriptor), /* export = */ false); + super.visitMultiANewArrayInsn(descriptor, numDimensions); + } + + @Override + public void visitTryCatchBlock( + final Label start, final Label end, final Label handler, final String type) { + if (type != null) { + addPackageReferences(Type.getObjectType(type), /* export = */ false); + } + super.visitTryCatchBlock(start, end, handler, type); + } }; } + + private void addPackageReferences(final Type type, final boolean export) { + switch (type.getSort()) { + case Type.ARRAY: + addPackageReferences(type.getElementType(), export); + break; + case Type.METHOD: + for (Type argumentType : type.getArgumentTypes()) { + addPackageReferences(argumentType, export); + } + addPackageReferences(type.getReturnType(), export); + break; + case Type.OBJECT: + String internalName = type.getInternalName(); + int lastSlashIndex = internalName.lastIndexOf('/'); + if (lastSlashIndex != -1) { + (export ? exports : imports).add(internalName.substring(0, lastSlashIndex)); + } + break; + default: + break; + } + } } /** @@ -199,18 +379,18 @@ public class Retrofitter { */ class ClassVerifier extends ClassVisitor { - /** The name of the visited class. */ + /** The internal name of the visited class. */ String className; /** The name of the currently visited method. */ String currentMethodName; - public ClassVerifier(final ClassVisitor classVisitor) { + public ClassVerifier() { // Make sure use we don't use Java 9 or higher classfile features. // We also want to make sure we don't use Java 6, 7 or 8 classfile // features (invokedynamic), but this can't be done in the same way. // Instead, we use manual checks below. - super(Opcodes.ASM4, classVisitor); + super(Opcodes.ASM4, null); } @Override @@ -222,10 +402,9 @@ public class Retrofitter { final String superName, final String[] interfaces) { if ((version & 0xFFFF) > Opcodes.V1_5) { - throw new IllegalArgumentException("ERROR: " + name + " version is newer than 1.5"); + throw new IllegalArgumentException(format("ERROR: %d version is newer than 1.5", version)); } className = name; - super.visit(version, access, name, signature, superName, interfaces); } @Override @@ -243,7 +422,6 @@ public class Retrofitter { public void visitFieldInsn( final int opcode, final String owner, final String name, final String descriptor) { check(owner, name); - super.visitFieldInsn(opcode, owner, name, descriptor); } @Override @@ -254,7 +432,6 @@ public class Retrofitter { final String descriptor, final boolean isInterface) { check(owner, name + descriptor); - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } @Override @@ -263,21 +440,16 @@ public class Retrofitter { int sort = ((Type) value).getSort(); if (sort == Type.METHOD) { throw new IllegalArgumentException( - "ERROR: ldc with a MethodType called in " - + className - + ' ' - + currentMethodName - + " is not available in JDK 1.5"); + format( + "ERROR: ldc with a MethodType called in %s %s is not available in JDK 1.5", + className, currentMethodName)); } } else if (value instanceof Handle) { throw new IllegalArgumentException( - "ERROR: ldc with a MethodHandle called in " - + className - + ' ' - + currentMethodName - + " is not available in JDK 1.5"); + format( + "ERROR: ldc with a MethodHandle called in %s %s is not available in JDK 1.5", + className, currentMethodName)); } - super.visitLdcInsn(value); } @Override @@ -287,11 +459,9 @@ public class Retrofitter { final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments) { throw new IllegalArgumentException( - "ERROR: invokedynamic called in " - + className - + ' ' - + currentMethodName - + " is not available in JDK 1.5"); + format( + "ERROR: invokedynamic called in %s %s is not available in JDK 1.5", + className, currentMethodName)); } }; } @@ -302,7 +472,7 @@ public class Retrofitter { * @param owner A class name. * @param member A field name or a method name and descriptor. */ - void check(final String owner, final String member) { + private void check(final String owner, final String member) { if (owner.startsWith("java/")) { String currentOwner = owner; while (currentOwner != null) { @@ -312,15 +482,9 @@ public class Retrofitter { currentOwner = jdkHierarchy.get(currentOwner); } throw new IllegalArgumentException( - "ERROR: " - + owner - + ' ' - + member - + " called in " - + className - + ' ' - + currentMethodName - + " is not defined in the JDK 1.5 API"); + format( + "ERROR: %s %s called in %s %s is not defined in the JDK 1.5 API", + owner, member, className, currentMethodName)); } } } -- 2.37.1