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