diff options
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/scan')
3 files changed, 759 insertions, 0 deletions
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java new file mode 100644 index 0000000..bae3f38 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java @@ -0,0 +1,51 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed 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. +package com.google.devtools.build.android.desugar.scan; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; + +@AutoValue +@Immutable +abstract class KeepReference { + public static KeepReference classReference(String internalName) { + checkArgument(!internalName.isEmpty()); + return new AutoValue_KeepReference(internalName, "", ""); + } + + public static KeepReference memberReference(String internalName, String name, String desc) { + checkArgument(!internalName.isEmpty()); + checkArgument(!name.isEmpty()); + checkArgument(!desc.isEmpty()); + return new AutoValue_KeepReference(internalName, name, desc); + } + + public final boolean isMemberReference() { + return !name().isEmpty(); + } + + public final boolean isMethodReference() { + return desc().startsWith("("); + } + + public final boolean isFieldReference() { + return isMemberReference() && !isMethodReference(); + } + + public abstract String internalName(); + public abstract String name(); + public abstract String desc(); +} diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java new file mode 100644 index 0000000..4924f7c --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java @@ -0,0 +1,303 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed 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. +package com.google.devtools.build.android.desugar.scan; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.util.Comparator.comparing; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; +import com.google.devtools.build.android.Converters.ExistingPathConverter; +import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionDocumentationCategory; +import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; + +class KeepScanner { + + public static class KeepScannerOptions extends OptionsBase { + @Option( + name = "input", + defaultValue = "null", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + converter = ExistingPathConverter.class, + abbrev = 'i', + help = "Input Jar with classes to scan." + ) + public Path inputJars; + + @Option( + name = "classpath_entry", + allowMultiple = true, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + converter = ExistingPathConverter.class, + help = + "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like " + + "javac's -cp flag." + ) + public List<Path> classpath; + + @Option( + name = "bootclasspath_entry", + allowMultiple = true, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + converter = ExistingPathConverter.class, + help = + "Bootclasspath that was used to compile the --input Jar with, like javac's " + + "-bootclasspath flag (required)." + ) + public List<Path> bootclasspath; + + @Option( + name = "keep_file", + defaultValue = "null", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + converter = PathConverter.class, + help = "Where to write keep rules to." + ) + public Path keepDest; + + @Option( + name = "prefix", + defaultValue = "j$/", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + help = "type to scan for." + ) + public String prefix; + } + + public static void main(String... args) throws Exception { + OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class); + parser.setAllowResidue(false); + parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); + parser.parseAndExitUponError(args); + KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class); + + Map<String, ImmutableSet<KeepReference>> seeds; + try (Closer closer = Closer.create()) { + // TODO(kmb): Try to share more of this code with Desugar binary + IndexedInputs classpath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); + IndexedInputs bootclasspath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)); + + // Construct classloader from classpath. Since we're assuming the prefix we're looking for + // isn't part of the input itself we shouldn't need to include the input in the classloader. + CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter(""); + ClassLoader classloader = + new HeaderClassLoader(classpath, noopRewriter, + new HeaderClassLoader(bootclasspath, noopRewriter, + new ThrowingClassLoader())); + seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader); + } + + try (PrintStream out = + new PrintStream( + Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) { + writeKeepDirectives(out, seeds); + } + } + + /** + * Writes a -keep rule for each class listing any members to keep. We sort classes and members + * so the output is deterministic. + */ + private static void writeKeepDirectives( + PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds) { + seeds + .entrySet() + .stream() + .sorted(comparing(Map.Entry::getKey)) + .forEachOrdered( + type -> { + out.printf("-keep class %s {%n", type.getKey().replace('/', '.')); + type.getValue() + .stream() + .filter(KeepReference::isMemberReference) + .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc)) + .map(ref -> toKeepDescriptor(ref)) + .distinct() // drop duplicates due to method descriptors with different returns + .forEachOrdered(line -> out.append(" ").append(line).append(";").println()); + out.printf("}%n"); + }); + } + + /** Scans for and returns references with owners matching the given prefix grouped by owner. */ + private static Map<String, ImmutableSet<KeepReference>> scan( + Path jarFile, String prefix, ClassLoader classpath) throws IOException { + // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each + // class in parallel. + try (ZipFile zip = new ZipFile(jarFile.toFile())) { + return zip.stream() + .filter(entry -> entry.getName().endsWith(".class")) + .map(entry -> readFully(zip, entry)) + .parallel() + .flatMap( + content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream()) + .distinct() // so we don't process the same reference multiple times next + .map(ref -> nearestDeclaration(ref, classpath)) + .collect( + Collectors.groupingByConcurrent( + KeepReference::internalName, ImmutableSet.toImmutableSet())); + } + } + + private static byte[] readFully(ZipFile zip, ZipEntry entry) { + byte[] result = new byte[(int) entry.getSize()]; + try (InputStream content = zip.getInputStream(entry)) { + ByteStreams.readFully(content, result); + return result; + } catch (IOException e) { + throw new IOError(e); + } + } + + /** + * Find the nearest definition of the given reference in the class hierarchy and return the + * modified reference. This is needed b/c bytecode sometimes refers to a method or field using + * an owner type that inherits the method or field instead of defining the member itself. + * In that case we need to find and keep the inherited definition. + */ + private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) { + if (!ref.isMemberReference() || "<init>".equals(ref.name())) { + return ref; // class and constructor references don't need any further work + } + + Class<?> clazz; + try { + clazz = classpath.loadClass(ref.internalName().replace('/', '.')); + } catch (ClassNotFoundException e) { + throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e); + } + + Class<?> owner = findDeclaringClass(clazz, ref); + if (owner == clazz) { + return ref; + } + String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/'); + return KeepReference.memberReference(parent, ref.name(), ref.desc()); + } + + private static Class<?> findDeclaringClass(Class<?> clazz, KeepReference ref) { + if (ref.isFieldReference()) { + try { + return clazz.getField(ref.name()).getDeclaringClass(); + } catch (NoSuchFieldException e) { + // field must be non-public, so search class hierarchy + do { + try { + return clazz.getDeclaredField(ref.name()).getDeclaringClass(); + } catch (NoSuchFieldException ignored) { + // fall through for clarity + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + } else { + checkState(ref.isMethodReference()); + Type descriptor = Type.getMethodType(ref.desc()); + for (Method m : clazz.getMethods()) { + if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) { + return m.getDeclaringClass(); + } + } + do { + // Method must be non-public, so search class hierarchy + for (Method m : clazz.getDeclaredMethods()) { + if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) { + return m.getDeclaringClass(); + } + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + return null; + } + + private static CharSequence toKeepDescriptor(KeepReference member) { + StringBuilder result = new StringBuilder(); + if (member.isMethodReference()) { + if (!"<init>".equals(member.name())) { + result.append("*** "); + } + result.append(member.name()).append("("); + // Ignore return type as it's unique in the source language + boolean first = true; + for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) { + if (first) { + first = false; + } else { + result.append(", "); + } + result.append(param.getClassName()); + } + result.append(")"); + } else { + checkArgument(member.isFieldReference()); + result.append("*** ").append(member.name()); // field names are unique so ignore descriptor + } + return result; + } + + /** + * Transform a list of Path to a list of InputFileProvider and register them with the given + * closer. + */ + @SuppressWarnings("MustBeClosedChecker") + private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider( + Closer closer, List<Path> paths) throws IOException { + ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>(); + for (Path path : paths) { + builder.add(closer.register(InputFileProvider.open(path))); + } + return builder.build(); + } + + private KeepScanner() {} +} diff --git a/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java new file mode 100644 index 0000000..b899ccc --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java @@ -0,0 +1,405 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed 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. +package com.google.devtools.build.android.desugar.scan; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableSet; +import javax.annotation.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; + +/** {@link ClassVisitor} that records references to classes starting with a given prefix. */ +class PrefixReferenceScanner extends ClassVisitor { + + /** + * Returns references with the given prefix in the given class. + * + * @param prefix an internal name prefix, typically a package such as {@code com/google/} + */ + public static ImmutableSet<KeepReference> scan(ClassReader reader, String prefix) { + PrefixReferenceScanner scanner = new PrefixReferenceScanner(prefix); + // Frames irrelevant for Android so skip them. Don't skip debug info in case the class we're + // visiting has local variable tables (typically it doesn't anyways). + reader.accept(scanner, ClassReader.SKIP_FRAMES); + return scanner.roots.build(); + } + + private final ImmutableSet.Builder<KeepReference> roots = ImmutableSet.builder(); + private final PrefixReferenceMethodVisitor mv = new PrefixReferenceMethodVisitor(); + private final PrefixReferenceFieldVisitor fv = new PrefixReferenceFieldVisitor(); + private final PrefixReferenceAnnotationVisitor av = new PrefixReferenceAnnotationVisitor(); + + private final String prefix; + + public PrefixReferenceScanner(String prefix) { + super(Opcodes.ASM6); + this.prefix = prefix; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkArgument(!name.startsWith(prefix)); + if (superName != null) { + classReference(superName); + } + classReferences(interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + classReference(owner); + if (desc != null) { + typeReference(Type.getMethodType(desc)); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + classReference(name); + if (outerName != null) { + classReference(outerName); + } + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + typeReference(desc); + return fv; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + typeReference(Type.getMethodType(desc)); + classReferences(exceptions); + return mv; + } + + private void classReferences(@Nullable String[] internalNames) { + if (internalNames != null) { + for (String itf : internalNames) { + classReference(itf); + } + } + } + + // The following methods are package-private so they don't incur bridge methods when called from + // inner classes below. + + void classReference(String internalName) { + checkArgument(internalName.charAt(0) != '[' && internalName.charAt(0) != '(', internalName); + checkArgument(!internalName.endsWith(";"), internalName); + if (internalName.startsWith(prefix)) { + roots.add(KeepReference.classReference(internalName)); + } + } + + void objectReference(String internalName) { + // don't call this for method types, convert to Type instead + checkArgument(internalName.charAt(0) != '(', internalName); + if (internalName.charAt(0) == '[') { + typeReference(internalName); + } else { + classReference(internalName); + } + } + + void typeReference(String typeDesc) { + // don't call this for method types, convert to Type instead + checkArgument(typeDesc.charAt(0) != '(', typeDesc); + + int lpos = typeDesc.lastIndexOf('[') + 1; + if (typeDesc.charAt(lpos) == 'L') { + checkArgument(typeDesc.endsWith(";"), typeDesc); + classReference(typeDesc.substring(lpos, typeDesc.length() - 1)); + } else { + // else primitive or primitive array + checkArgument(typeDesc.length() == lpos + 1, typeDesc); + switch (typeDesc.charAt(lpos)) { + case 'B': + case 'C': + case 'S': + case 'I': + case 'J': + case 'D': + case 'F': + case 'Z': + break; + default: + throw new AssertionError("Unexpected type descriptor: " + typeDesc); + } + } + } + + void typeReference(Type type) { + switch (type.getSort()) { + case Type.ARRAY: + typeReference(type.getElementType()); + break; + case Type.OBJECT: + classReference(type.getInternalName()); + break; + + case Type.METHOD: + for (Type param : type.getArgumentTypes()) { + typeReference(param); + } + typeReference(type.getReturnType()); + break; + + default: + break; + } + } + + void fieldReference(String owner, String name, String desc) { + objectReference(owner); + typeReference(desc); + if (owner.startsWith(prefix)) { + roots.add(KeepReference.memberReference(owner, name, desc)); + } + } + + void methodReference(String owner, String name, String desc) { + checkArgument(desc.charAt(0) == '(', desc); + objectReference(owner); + typeReference(Type.getMethodType(desc)); + if (owner.startsWith(prefix)) { + roots.add(KeepReference.memberReference(owner, name, desc)); + } + } + + void handleReference(Handle handle) { + switch (handle.getTag()) { + case Opcodes.H_GETFIELD: + case Opcodes.H_GETSTATIC: + case Opcodes.H_PUTFIELD: + case Opcodes.H_PUTSTATIC: + fieldReference(handle.getOwner(), handle.getName(), handle.getDesc()); + break; + + default: + methodReference(handle.getOwner(), handle.getName(), handle.getDesc()); + break; + } + } + + private class PrefixReferenceMethodVisitor extends MethodVisitor { + + public PrefixReferenceMethodVisitor() { + super(Opcodes.ASM6); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return av; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitTypeInsn(int opcode, String type) { + objectReference(type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + fieldReference(owner, name, desc); + } + + @Override + @SuppressWarnings("deprecation") // Implementing deprecated method to be sure + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + visitMethodInsn(opcode, owner, name, desc, opcode == Opcodes.INVOKEINTERFACE); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + methodReference(owner, name, desc); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { + typeReference(Type.getMethodType(desc)); + handleReference(bsm); + for (Object bsmArg : bsmArgs) { + visitConstant(bsmArg); + } + } + + @Override + public void visitLdcInsn(Object cst) { + visitConstant(cst); + } + + private void visitConstant(Object cst) { + if (cst instanceof Type) { + typeReference((Type) cst); + } else if (cst instanceof Handle) { + handleReference((Handle) cst); + } else { + // Check for other expected types as javadoc recommends + checkArgument( + cst instanceof String + || cst instanceof Integer + || cst instanceof Long + || cst instanceof Float + || cst instanceof Double, + "Unexpected constant: ", cst); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + typeReference(desc); + } + + @Override + public AnnotationVisitor visitInsnAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + classReference(type); + } + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitLocalVariable( + String name, String desc, String signature, Label start, Label end, int index) { + typeReference(desc); + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation( + int typeRef, + TypePath typePath, + Label[] start, + Label[] end, + int[] index, + String desc, + boolean visible) { + typeReference(desc); + return av; + } + } + + private class PrefixReferenceFieldVisitor extends FieldVisitor { + + public PrefixReferenceFieldVisitor() { + super(Opcodes.ASM6); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + } + + private class PrefixReferenceAnnotationVisitor extends AnnotationVisitor { + + public PrefixReferenceAnnotationVisitor() { + super(Opcodes.ASM6); + } + + @Override + public void visit(String name, Object value) { + if (value instanceof Type) { + typeReference((Type) value); + } + } + + @Override + public void visitEnum(String name, String desc, String value) { + fieldReference(desc.substring(1, desc.length() - 1), value, desc); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitArray(String name) { + return av; + } + } +} |