• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2018 The Bazel Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //    http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 package com.google.devtools.build.android.desugar.scan;
15 
16 import static com.google.common.base.Preconditions.checkArgument;
17 import static com.google.common.base.Preconditions.checkNotNull;
18 import static com.google.common.base.Preconditions.checkState;
19 import static java.nio.file.StandardOpenOption.CREATE;
20 import static java.util.Comparator.comparing;
21 
22 import com.google.common.collect.ImmutableList;
23 import com.google.common.collect.ImmutableSet;
24 import com.google.common.io.ByteStreams;
25 import com.google.common.io.Closer;
26 import com.google.devtools.build.android.Converters.ExistingPathConverter;
27 import com.google.devtools.build.android.Converters.PathConverter;
28 import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
29 import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
30 import com.google.devtools.build.android.desugar.io.IndexedInputs;
31 import com.google.devtools.build.android.desugar.io.InputFileProvider;
32 import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
33 import com.google.devtools.common.options.Option;
34 import com.google.devtools.common.options.OptionDocumentationCategory;
35 import com.google.devtools.common.options.OptionEffectTag;
36 import com.google.devtools.common.options.OptionsBase;
37 import com.google.devtools.common.options.OptionsParser;
38 import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
39 import java.io.IOError;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.PrintStream;
43 import java.lang.reflect.Method;
44 import java.nio.file.FileSystems;
45 import java.nio.file.Files;
46 import java.nio.file.Path;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.stream.Collectors;
50 import java.util.zip.ZipEntry;
51 import java.util.zip.ZipFile;
52 import org.objectweb.asm.ClassReader;
53 import org.objectweb.asm.Type;
54 
55 class KeepScanner {
56 
57   public static class KeepScannerOptions extends OptionsBase {
58     @Option(
59       name = "input",
60       defaultValue = "null",
61       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
62       effectTags = OptionEffectTag.UNKNOWN,
63       converter = ExistingPathConverter.class,
64       abbrev = 'i',
65       help = "Input Jar with classes to scan."
66     )
67     public Path inputJars;
68 
69     @Option(
70       name = "classpath_entry",
71       allowMultiple = true,
72       defaultValue = "",
73       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
74       effectTags = {OptionEffectTag.UNKNOWN},
75       converter = ExistingPathConverter.class,
76       help =
77           "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like "
78               + "javac's -cp flag."
79     )
80     public List<Path> classpath;
81 
82     @Option(
83       name = "bootclasspath_entry",
84       allowMultiple = true,
85       defaultValue = "",
86       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
87       effectTags = {OptionEffectTag.UNKNOWN},
88       converter = ExistingPathConverter.class,
89       help =
90           "Bootclasspath that was used to compile the --input Jar with, like javac's "
91               + "-bootclasspath flag (required)."
92     )
93     public List<Path> bootclasspath;
94 
95     @Option(
96       name = "keep_file",
97       defaultValue = "null",
98       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
99       effectTags = OptionEffectTag.UNKNOWN,
100       converter = PathConverter.class,
101       help = "Where to write keep rules to."
102     )
103     public Path keepDest;
104 
105     @Option(
106       name = "prefix",
107       defaultValue = "j$/",
108       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
109       effectTags = OptionEffectTag.UNKNOWN,
110       help = "type to scan for."
111     )
112     public String prefix;
113   }
114 
main(String... args)115   public static void main(String... args) throws Exception {
116     OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class);
117     parser.setAllowResidue(false);
118     parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
119     parser.parseAndExitUponError(args);
120     KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class);
121 
122     Map<String, ImmutableSet<KeepReference>> seeds;
123     try (Closer closer = Closer.create()) {
124       // TODO(kmb): Try to share more of this code with Desugar binary
125       IndexedInputs classpath =
126           new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
127       IndexedInputs bootclasspath =
128           new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath));
129 
130       // Construct classloader from classpath.  Since we're assuming the prefix we're looking for
131       // isn't part of the input itself we shouldn't need to include the input in the classloader.
132       CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter("");
133       ClassLoader classloader =
134           new HeaderClassLoader(classpath, noopRewriter,
135               new HeaderClassLoader(bootclasspath, noopRewriter,
136                   new ThrowingClassLoader()));
137       seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader);
138     }
139 
140     try (PrintStream out =
141         new PrintStream(
142             Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) {
143       writeKeepDirectives(out, seeds);
144     }
145   }
146 
147   /**
148    * Writes a -keep rule for each class listing any members to keep.  We sort classes and members
149    * so the output is deterministic.
150    */
writeKeepDirectives( PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds)151   private static void writeKeepDirectives(
152       PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds) {
153     seeds
154         .entrySet()
155         .stream()
156         .sorted(comparing(Map.Entry::getKey))
157         .forEachOrdered(
158             type -> {
159               out.printf("-keep class %s {%n", type.getKey().replace('/', '.'));
160               type.getValue()
161                   .stream()
162                   .filter(KeepReference::isMemberReference)
163                   .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc))
164                   .map(ref -> toKeepDescriptor(ref))
165                   .distinct() // drop duplicates due to method descriptors with different returns
166                   .forEachOrdered(line -> out.append("  ").append(line).append(";").println());
167               out.printf("}%n");
168             });
169   }
170 
171   /** Scans for and returns references with owners matching the given prefix grouped by owner. */
scan( Path jarFile, String prefix, ClassLoader classpath)172   private static Map<String, ImmutableSet<KeepReference>> scan(
173       Path jarFile, String prefix, ClassLoader classpath) throws IOException {
174     // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each
175     // class in parallel.
176     try (ZipFile zip = new ZipFile(jarFile.toFile())) {
177       return zip.stream()
178           .filter(entry -> entry.getName().endsWith(".class"))
179           .map(entry -> readFully(zip, entry))
180           .parallel()
181           .flatMap(
182               content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream())
183           .distinct() // so we don't process the same reference multiple times next
184           .map(ref -> nearestDeclaration(ref, classpath))
185           .collect(
186               Collectors.groupingByConcurrent(
187                   KeepReference::internalName, ImmutableSet.toImmutableSet()));
188     }
189   }
190 
readFully(ZipFile zip, ZipEntry entry)191   private static byte[] readFully(ZipFile zip, ZipEntry entry) {
192     byte[] result = new byte[(int) entry.getSize()];
193     try (InputStream content = zip.getInputStream(entry)) {
194       ByteStreams.readFully(content, result);
195       return result;
196     } catch (IOException e) {
197       throw new IOError(e);
198     }
199   }
200 
201   /**
202    * Find the nearest definition of the given reference in the class hierarchy and return the
203    * modified reference.  This is needed b/c bytecode sometimes refers to a method or field using
204    * an owner type that inherits the method or field instead of defining the member itself.
205    * In that case we need to find and keep the inherited definition.
206    */
nearestDeclaration(KeepReference ref, ClassLoader classpath)207   private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) {
208     if (!ref.isMemberReference() || "<init>".equals(ref.name())) {
209       return ref; // class and constructor references don't need any further work
210     }
211 
212     Class<?> clazz;
213     try {
214       clazz = classpath.loadClass(ref.internalName().replace('/', '.'));
215     } catch (ClassNotFoundException e) {
216       throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e);
217     }
218 
219     Class<?> owner = findDeclaringClass(clazz, ref);
220     if (owner == clazz) {
221       return ref;
222     }
223     String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/');
224     return KeepReference.memberReference(parent, ref.name(), ref.desc());
225   }
226 
findDeclaringClass(Class<?> clazz, KeepReference ref)227   private static Class<?> findDeclaringClass(Class<?> clazz, KeepReference ref) {
228     if (ref.isFieldReference()) {
229       try {
230         return clazz.getField(ref.name()).getDeclaringClass();
231       } catch (NoSuchFieldException e) {
232         // field must be non-public, so search class hierarchy
233         do {
234           try {
235             return clazz.getDeclaredField(ref.name()).getDeclaringClass();
236           } catch (NoSuchFieldException ignored) {
237             // fall through for clarity
238           }
239           clazz = clazz.getSuperclass();
240         } while (clazz != null);
241       }
242     } else {
243       checkState(ref.isMethodReference());
244       Type descriptor = Type.getMethodType(ref.desc());
245       for (Method m : clazz.getMethods()) {
246         if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
247           return m.getDeclaringClass();
248         }
249       }
250       do {
251         // Method must be non-public, so search class hierarchy
252         for (Method m : clazz.getDeclaredMethods()) {
253           if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
254             return m.getDeclaringClass();
255           }
256         }
257         clazz = clazz.getSuperclass();
258       } while (clazz != null);
259     }
260     return null;
261   }
262 
toKeepDescriptor(KeepReference member)263   private static CharSequence toKeepDescriptor(KeepReference member) {
264     StringBuilder result = new StringBuilder();
265     if (member.isMethodReference()) {
266       if (!"<init>".equals(member.name())) {
267         result.append("*** ");
268       }
269       result.append(member.name()).append("(");
270       // Ignore return type as it's unique in the source language
271       boolean first = true;
272       for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) {
273         if (first) {
274           first = false;
275         } else {
276           result.append(", ");
277         }
278         result.append(param.getClassName());
279       }
280       result.append(")");
281     } else {
282       checkArgument(member.isFieldReference());
283       result.append("*** ").append(member.name()); // field names are unique so ignore descriptor
284     }
285     return result;
286   }
287 
288   /**
289    * Transform a list of Path to a list of InputFileProvider and register them with the given
290    * closer.
291    */
292   @SuppressWarnings("MustBeClosedChecker")
toRegisteredInputFileProvider( Closer closer, List<Path> paths)293   private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider(
294       Closer closer, List<Path> paths) throws IOException {
295     ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
296     for (Path path : paths) {
297       builder.add(closer.register(InputFileProvider.open(path)));
298     }
299     return builder.build();
300   }
301 
KeepScanner()302   private KeepScanner() {}
303 }
304