1 // Copyright 2016 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; 15 16 import static com.google.common.base.Preconditions.checkState; 17 import static java.nio.charset.StandardCharsets.ISO_8859_1; 18 19 import com.google.auto.value.AutoValue; 20 import com.google.common.collect.ImmutableList; 21 import com.google.common.collect.ImmutableMap; 22 import com.google.common.collect.ImmutableSet; 23 import com.google.common.io.Closer; 24 import com.google.devtools.build.android.Converters.ExistingPathConverter; 25 import com.google.devtools.build.android.Converters.PathConverter; 26 import com.google.devtools.common.options.Option; 27 import com.google.devtools.common.options.OptionsBase; 28 import com.google.devtools.common.options.OptionsParser; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.nio.file.FileVisitResult; 32 import java.nio.file.Files; 33 import java.nio.file.Path; 34 import java.nio.file.Paths; 35 import java.nio.file.SimpleFileVisitor; 36 import java.nio.file.attribute.BasicFileAttributes; 37 import java.util.Iterator; 38 import java.util.List; 39 import java.util.Map; 40 import org.objectweb.asm.ClassReader; 41 import org.objectweb.asm.ClassVisitor; 42 import org.objectweb.asm.ClassWriter; 43 44 /** 45 * Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in 46 * particular lambdas and method references. 47 */ 48 class Desugar { 49 50 /** Commandline options for {@link Desugar}. */ 51 public static class Options extends OptionsBase { 52 @Option( 53 name = "input", 54 allowMultiple = true, 55 defaultValue = "", 56 category = "input", 57 converter = ExistingPathConverter.class, 58 abbrev = 'i', 59 help = 60 "Input Jar or directory with classes to desugar (required, the n-th input is paired with" 61 + "the n-th output)." 62 ) 63 public List<Path> inputJars; 64 65 @Option( 66 name = "classpath_entry", 67 allowMultiple = true, 68 defaultValue = "", 69 category = "input", 70 converter = ExistingPathConverter.class, 71 help = "Ordered classpath to resolve symbols in the --input Jar, like javac's -cp flag." 72 ) 73 public List<Path> classpath; 74 75 @Option( 76 name = "bootclasspath_entry", 77 allowMultiple = true, 78 defaultValue = "", 79 category = "input", 80 converter = ExistingPathConverter.class, 81 help = "Bootclasspath that was used to compile the --input Jar with, like javac's " 82 + "-bootclasspath flag (required)." 83 ) 84 public List<Path> bootclasspath; 85 86 @Option( 87 name = "allow_empty_bootclasspath", 88 defaultValue = "false", 89 category = "undocumented" 90 ) 91 public boolean allowEmptyBootclasspath; 92 93 @Option( 94 name = "only_desugar_javac9_for_lint", 95 defaultValue = "false", 96 help = 97 "A temporary flag specifically for android lint, subject to removal anytime (DO NOT USE)", 98 category = "undocumented" 99 ) 100 public boolean onlyDesugarJavac9ForLint; 101 102 @Option( 103 name = "output", 104 allowMultiple = true, 105 defaultValue = "", 106 category = "output", 107 converter = PathConverter.class, 108 abbrev = 'o', 109 help = 110 "Output Jar or directory to write desugared classes into (required, the n-th output is " 111 + "paired with the n-th input, output must be a Jar if input is a Jar)." 112 ) 113 public List<Path> outputJars; 114 115 @Option( 116 name = "verbose", 117 defaultValue = "false", 118 category = "misc", 119 abbrev = 'v', 120 help = "Enables verbose debugging output." 121 ) 122 public boolean verbose; 123 124 @Option( 125 name = "min_sdk_version", 126 defaultValue = "1", 127 category = "misc", 128 help = "Minimum targeted sdk version. If >= 24, enables default methods in interfaces." 129 ) 130 public int minSdkVersion; 131 132 @Option( 133 name = "copy_bridges_from_classpath", 134 defaultValue = "false", 135 category = "misc", 136 help = "Copy bridges from classpath to desugared classes." 137 ) 138 public boolean copyBridgesFromClasspath; 139 140 @Option( 141 name = "core_library", 142 defaultValue = "false", 143 category = "undocumented", 144 implicitRequirements = "--allow_empty_bootclasspath", 145 help = "Enables rewriting to desugar java.* classes." 146 ) 147 public boolean coreLibrary; 148 } 149 main(String[] args)150 public static void main(String[] args) throws Exception { 151 // LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating 152 // the call to LambdaMetafactory that the JVM would make when encountering an invokedynamic. 153 // LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump") 154 // generated classes, which we take advantage of here. Set property before doing anything else 155 // since the property is read in the static initializer; if this breaks we can investigate 156 // setting the property when calling the tool. 157 Path dumpDirectory = Files.createTempDirectory("lambdas"); 158 System.setProperty( 159 LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString()); 160 161 deleteTreeOnExit(dumpDirectory); 162 163 if (args.length == 1 && args[0].startsWith("@")) { 164 args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]); 165 } 166 167 OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class); 168 optionsParser.setAllowResidue(false); 169 optionsParser.parseAndExitUponError(args); 170 Options options = optionsParser.getOptions(Options.class); 171 172 checkState(!options.inputJars.isEmpty(), "--input is required"); 173 checkState( 174 options.inputJars.size() == options.outputJars.size(), 175 "Desugar requires the same number of inputs and outputs to pair them"); 176 checkState(!options.bootclasspath.isEmpty() || options.allowEmptyBootclasspath, 177 "At least one --bootclasspath_entry is required"); 178 for (Path path : options.classpath) { 179 checkState(!Files.isDirectory(path), "Classpath entry must be a jar file: %s", path); 180 } 181 for (Path path : options.bootclasspath) { 182 checkState(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path); 183 } 184 if (options.verbose) { 185 System.out.printf("Lambda classes will be written under %s%n", dumpDirectory); 186 } 187 188 CoreLibraryRewriter rewriter = 189 new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); 190 191 boolean allowDefaultMethods = options.minSdkVersion >= 24; 192 boolean allowCallsToObjectsNonNull = options.minSdkVersion >= 19; 193 194 LambdaClassMaker lambdas = new LambdaClassMaker(dumpDirectory); 195 196 // Process each input separately 197 for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) { 198 Path inputPath = inputOutputPair.getInput(); 199 Path outputPath = inputOutputPair.getOutput(); 200 checkState( 201 Files.isDirectory(inputPath) || !Files.isDirectory(outputPath), 202 "Input jar file requires an output jar file"); 203 204 try (Closer closer = Closer.create(); 205 OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath)) { 206 InputFileProvider appInputFiles = toInputFileProvider(closer, inputPath); 207 List<InputFileProvider> classpathInputFiles = 208 toInputFileProvider(closer, options.classpath); 209 IndexedInputs appIndexedInputs = new IndexedInputs(ImmutableList.of(appInputFiles)); 210 IndexedInputs appAndClasspathIndexedInputs = 211 new IndexedInputs(classpathInputFiles, appIndexedInputs); 212 ClassLoader loader = 213 createClassLoader( 214 rewriter, 215 toInputFileProvider(closer, options.bootclasspath), 216 appAndClasspathIndexedInputs); 217 218 ClassReaderFactory readerFactory = 219 new ClassReaderFactory( 220 (options.copyBridgesFromClasspath && !allowDefaultMethods) 221 ? appAndClasspathIndexedInputs 222 : appIndexedInputs, 223 rewriter); 224 225 ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); 226 227 // Process inputs, desugaring as we go 228 for (String filename : appInputFiles) { 229 try (InputStream content = appInputFiles.getInputStream(filename)) { 230 // We can write classes uncompressed since they need to be converted to .dex format 231 // for Android anyways. Resources are written as they were in the input jar to avoid 232 // any danger of accidentally uncompressed resources ending up in an .apk. 233 if (filename.endsWith(".class")) { 234 ClassReader reader = rewriter.reader(content); 235 CoreLibraryRewriter.UnprefixingClassWriter writer = 236 rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); 237 ClassVisitor visitor = writer; 238 239 if (!options.onlyDesugarJavac9ForLint) { 240 if (!allowDefaultMethods) { 241 visitor = new Java7Compatibility(visitor, readerFactory); 242 } 243 244 visitor = 245 new LambdaDesugaring( 246 visitor, 247 loader, 248 lambdas, 249 interfaceLambdaMethodCollector, 250 allowDefaultMethods); 251 } 252 253 if (!allowCallsToObjectsNonNull) { 254 visitor = new ObjectsRequireNonNullMethodInliner(visitor); 255 } 256 reader.accept(visitor, 0); 257 258 outputFileProvider.write(filename, writer.toByteArray()); 259 } else { 260 outputFileProvider.copyFrom(filename, appInputFiles); 261 } 262 } 263 } 264 265 ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build(); 266 checkState( 267 !allowDefaultMethods || interfaceLambdaMethods.isEmpty(), 268 "Desugaring with default methods enabled moved interface lambdas"); 269 270 // Write out the lambda classes we generated along the way 271 ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain(); 272 checkState( 273 !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(), 274 "There should be no lambda classes generated: %s", 275 lambdaClasses.keySet()); 276 277 for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) { 278 try (InputStream bytecode = 279 Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) { 280 ClassReader reader = rewriter.reader(bytecode); 281 CoreLibraryRewriter.UnprefixingClassWriter writer = 282 rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); 283 ClassVisitor visitor = writer; 284 285 if (!allowDefaultMethods) { 286 // null ClassReaderFactory b/c we don't expect to need it for lambda classes 287 visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); 288 } 289 290 visitor = 291 new LambdaClassFixer( 292 visitor, 293 lambdaClass.getValue(), 294 readerFactory, 295 interfaceLambdaMethods, 296 allowDefaultMethods); 297 // Send lambda classes through desugaring to make sure there's no invokedynamic 298 // instructions in generated lambda classes (checkState below will fail) 299 visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods); 300 if (!allowCallsToObjectsNonNull) { 301 // Not sure whether there will be implicit null check emitted by javac, so we rerun 302 // the inliner again 303 visitor = new ObjectsRequireNonNullMethodInliner(visitor); 304 } 305 reader.accept(visitor, 0); 306 String filename = 307 rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class"; 308 outputFileProvider.write(filename, writer.toByteArray()); 309 } 310 } 311 312 Map<Path, LambdaInfo> leftBehind = lambdas.drain(); 313 checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind); 314 } 315 } 316 } 317 toInputOutputPairs(Options options)318 private static List<InputOutputPair> toInputOutputPairs(Options options) { 319 final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder(); 320 for (Iterator<Path> inputIt = options.inputJars.iterator(), 321 outputIt = options.outputJars.iterator(); 322 inputIt.hasNext();) { 323 ioPairListbuilder.add(InputOutputPair.create(inputIt.next(), outputIt.next())); 324 } 325 return ioPairListbuilder.build(); 326 } 327 createClassLoader( CoreLibraryRewriter rewriter, List<InputFileProvider> bootclasspath, IndexedInputs appAndClasspathIndexedInputs)328 private static ClassLoader createClassLoader( 329 CoreLibraryRewriter rewriter, 330 List<InputFileProvider> bootclasspath, 331 IndexedInputs appAndClasspathIndexedInputs) 332 throws IOException { 333 // Use a classloader that as much as possible uses the provided bootclasspath instead of 334 // the tool's system classloader. Unfortunately we can't do that for java. classes. 335 ClassLoader parent = new ThrowingClassLoader(); 336 if (!bootclasspath.isEmpty()) { 337 parent = new HeaderClassLoader(new IndexedInputs(bootclasspath), rewriter, parent); 338 } 339 // Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas. 340 // Note that inputJar and classpath need to be in the same classloader because we typically get 341 // the header Jar for inputJar on the classpath and having the header Jar in a parent loader 342 // means the header version is preferred over the real thing. 343 return new HeaderClassLoader(appAndClasspathIndexedInputs, rewriter, parent); 344 } 345 346 private static class ThrowingClassLoader extends ClassLoader { 347 @Override loadClass(String name, boolean resolve)348 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 349 if (name.startsWith("java.")) { 350 // Use system class loader for java. classes, since ClassLoader.defineClass gets 351 // grumpy when those don't come from the standard place. 352 return super.loadClass(name, resolve); 353 } 354 throw new ClassNotFoundException(); 355 } 356 } 357 deleteTreeOnExit(final Path directory)358 private static void deleteTreeOnExit(final Path directory) { 359 Thread shutdownHook = 360 new Thread() { 361 @Override 362 public void run() { 363 try { 364 deleteTree(directory); 365 } catch (IOException e) { 366 throw new RuntimeException("Failed to delete " + directory, e); 367 } 368 } 369 }; 370 Runtime.getRuntime().addShutdownHook(shutdownHook); 371 } 372 373 /** Recursively delete a directory. */ deleteTree(final Path directory)374 private static void deleteTree(final Path directory) throws IOException { 375 if (directory.toFile().exists()) { 376 Files.walkFileTree( 377 directory, 378 new SimpleFileVisitor<Path>() { 379 @Override 380 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 381 throws IOException { 382 Files.delete(file); 383 return FileVisitResult.CONTINUE; 384 } 385 386 @Override 387 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 388 throws IOException { 389 Files.delete(dir); 390 return FileVisitResult.CONTINUE; 391 } 392 }); 393 } 394 } 395 396 /** Transform a Path to an {@link OutputFileProvider} */ toOutputFileProvider(Path path)397 private static OutputFileProvider toOutputFileProvider(Path path) 398 throws IOException { 399 if (Files.isDirectory(path)) { 400 return new DirectoryOutputFileProvider(path); 401 } else { 402 return new ZipOutputFileProvider(path); 403 } 404 } 405 406 /** Transform a Path to an InputFileProvider and register it to close it at the end of desugar */ toInputFileProvider(Closer closer, Path path)407 private static InputFileProvider toInputFileProvider(Closer closer, Path path) 408 throws IOException { 409 if (Files.isDirectory(path)) { 410 return closer.register(new DirectoryInputFileProvider(path)); 411 } else { 412 return closer.register(new ZipInputFileProvider(path)); 413 } 414 } 415 toInputFileProvider( Closer closer, List<Path> paths)416 private static ImmutableList<InputFileProvider> toInputFileProvider( 417 Closer closer, List<Path> paths) throws IOException { 418 ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>(); 419 for (Path path : paths) { 420 checkState(!Files.isDirectory(path), "Directory is not supported: %s", path); 421 builder.add(closer.register(new ZipInputFileProvider(path))); 422 } 423 return builder.build(); 424 } 425 426 /** 427 * Pair input and output. 428 */ 429 @AutoValue 430 abstract static class InputOutputPair { 431 create(Path input, Path output)432 static InputOutputPair create(Path input, Path output) { 433 return new AutoValue_Desugar_InputOutputPair(input, output); 434 } 435 getInput()436 abstract Path getInput(); 437 getOutput()438 abstract Path getOutput(); 439 } 440 } 441