aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/desugar
diff options
context:
space:
mode:
authorGravatar kmb <kmb@google.com>2018-02-20 15:30:22 -0800
committerGravatar Copybara-Service <copybara-piper@google.com>2018-02-20 15:32:30 -0800
commitc1042f2adc55d040495a1159100146fad607d32a (patch)
tree69b2e5505ba3063c6a2966c414da1067b61782b7 /src/tools/android/java/com/google/devtools/build/android/desugar
parent2dd46e6178a7e99e38c56a0fddc1a6e533b53bf0 (diff)
Tool that scans a given Jar for references to select classes and outputs corresponding Proguard-style -keep rules
RELNOTES: None. PiperOrigin-RevId: 186372769
Diffstat (limited to 'src/tools/android/java/com/google/devtools/build/android/desugar')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepReference.java51
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java174
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java405
3 files changed, 630 insertions, 0 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
new file mode 100644
index 0000000000..bae3f38db8
--- /dev/null
+++ b/src/tools/android/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/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
new file mode 100644
index 0000000000..5892bf5df8
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
@@ -0,0 +1,174 @@
+// 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.ImmutableSet;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+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.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+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 = "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 =
+ scan(checkNotNull(options.inputJars), options.prefix);
+
+ 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)
+ 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())
+ .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)) {
+ checkState(content.read(result) == result.length);
+ checkState(content.read() == -1);
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return result;
+ }
+
+ private static CharSequence toKeepDescriptor(KeepReference member) {
+ StringBuilder result = new StringBuilder();
+ if (member.isMethodReference()) {
+ result.append("*** ").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;
+ }
+
+ private KeepScanner() {}
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
new file mode 100644
index 0000000000..b899ccc41d
--- /dev/null
+++ b/src/tools/android/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;
+ }
+ }
+}