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.checkArgument; 17 import static com.google.common.base.Preconditions.checkNotNull; 18 import static com.google.common.base.Preconditions.checkState; 19 import static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY; 20 import static java.nio.charset.StandardCharsets.ISO_8859_1; 21 22 import com.google.auto.value.AutoValue; 23 import com.google.common.annotations.VisibleForTesting; 24 import com.google.common.collect.ImmutableList; 25 import com.google.common.collect.ImmutableMap; 26 import com.google.common.collect.ImmutableSet; 27 import com.google.common.io.ByteStreams; 28 import com.google.common.io.Closer; 29 import com.google.devtools.build.android.Converters.ExistingPathConverter; 30 import com.google.devtools.build.android.Converters.PathConverter; 31 import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter; 32 import com.google.devtools.common.options.Option; 33 import com.google.devtools.common.options.OptionDocumentationCategory; 34 import com.google.devtools.common.options.OptionEffectTag; 35 import com.google.devtools.common.options.Options; 36 import com.google.devtools.common.options.OptionsBase; 37 import com.google.errorprone.annotations.MustBeClosed; 38 import java.io.IOError; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.lang.reflect.Field; 42 import java.nio.file.FileVisitResult; 43 import java.nio.file.Files; 44 import java.nio.file.Path; 45 import java.nio.file.Paths; 46 import java.nio.file.SimpleFileVisitor; 47 import java.nio.file.attribute.BasicFileAttributes; 48 import java.util.HashSet; 49 import java.util.Iterator; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 import java.util.concurrent.atomic.AtomicInteger; 54 import javax.annotation.Nullable; 55 import org.objectweb.asm.ClassReader; 56 import org.objectweb.asm.ClassVisitor; 57 import org.objectweb.asm.ClassWriter; 58 import org.objectweb.asm.tree.ClassNode; 59 60 /** 61 * Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in 62 * particular lambdas and method references. 63 */ 64 class Desugar { 65 66 /** Commandline options for {@link Desugar}. */ 67 public static class DesugarOptions extends OptionsBase { 68 @Option( 69 name = "input", 70 allowMultiple = true, 71 defaultValue = "", 72 category = "input", 73 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 74 effectTags = {OptionEffectTag.UNKNOWN}, 75 converter = ExistingPathConverter.class, 76 abbrev = 'i', 77 help = 78 "Input Jar or directory with classes to desugar (required, the n-th input is paired with" 79 + "the n-th output)." 80 ) 81 public List<Path> inputJars; 82 83 @Option( 84 name = "classpath_entry", 85 allowMultiple = true, 86 defaultValue = "", 87 category = "input", 88 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 89 effectTags = {OptionEffectTag.UNKNOWN}, 90 converter = ExistingPathConverter.class, 91 help = 92 "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like " 93 + "javac's -cp flag." 94 ) 95 public List<Path> classpath; 96 97 @Option( 98 name = "bootclasspath_entry", 99 allowMultiple = true, 100 defaultValue = "", 101 category = "input", 102 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 103 effectTags = {OptionEffectTag.UNKNOWN}, 104 converter = ExistingPathConverter.class, 105 help = 106 "Bootclasspath that was used to compile the --input Jar with, like javac's " 107 + "-bootclasspath flag (required)." 108 ) 109 public List<Path> bootclasspath; 110 111 @Option( 112 name = "allow_empty_bootclasspath", 113 defaultValue = "false", 114 documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, 115 effectTags = {OptionEffectTag.UNKNOWN} 116 ) 117 public boolean allowEmptyBootclasspath; 118 119 @Option( 120 name = "only_desugar_javac9_for_lint", 121 defaultValue = "false", 122 documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, 123 effectTags = {OptionEffectTag.UNKNOWN}, 124 help = 125 "A temporary flag specifically for android lint, subject to removal anytime (DO NOT USE)" 126 ) 127 public boolean onlyDesugarJavac9ForLint; 128 129 @Option( 130 name = "rewrite_calls_to_long_compare", 131 defaultValue = "false", 132 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 133 effectTags = {OptionEffectTag.UNKNOWN}, 134 help = 135 "Rewrite calls to Long.compare(long, long) to the JVM instruction lcmp " 136 + "regardless of --min_sdk_version.", 137 category = "misc" 138 ) 139 public boolean alwaysRewriteLongCompare; 140 141 @Option( 142 name = "output", 143 allowMultiple = true, 144 defaultValue = "", 145 category = "output", 146 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 147 effectTags = {OptionEffectTag.UNKNOWN}, 148 converter = PathConverter.class, 149 abbrev = 'o', 150 help = 151 "Output Jar or directory to write desugared classes into (required, the n-th output is " 152 + "paired with the n-th input, output must be a Jar if input is a Jar)." 153 ) 154 public List<Path> outputJars; 155 156 @Option( 157 name = "verbose", 158 defaultValue = "false", 159 category = "misc", 160 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 161 effectTags = {OptionEffectTag.UNKNOWN}, 162 abbrev = 'v', 163 help = "Enables verbose debugging output." 164 ) 165 public boolean verbose; 166 167 @Option( 168 name = "min_sdk_version", 169 defaultValue = "1", 170 category = "misc", 171 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 172 effectTags = {OptionEffectTag.UNKNOWN}, 173 help = "Minimum targeted sdk version. If >= 24, enables default methods in interfaces." 174 ) 175 public int minSdkVersion; 176 177 @Option( 178 name = "emit_dependency_metadata_as_needed", 179 defaultValue = "false", 180 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 181 effectTags = {OptionEffectTag.UNKNOWN}, 182 help = "Whether to emit META-INF/desugar_deps as needed for later consistency checking." 183 ) 184 public boolean emitDependencyMetadata; 185 186 @Option( 187 name = "best_effort_tolerate_missing_deps", 188 defaultValue = "true", 189 category = "misc", 190 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 191 effectTags = {OptionEffectTag.UNKNOWN}, 192 help = "Whether to tolerate missing dependencies on the classpath in some cases. You should " 193 + "strive to set this flag to false." 194 ) 195 public boolean tolerateMissingDependencies; 196 197 @Option( 198 name = "desugar_interface_method_bodies_if_needed", 199 defaultValue = "true", 200 category = "misc", 201 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 202 effectTags = {OptionEffectTag.UNKNOWN}, 203 help = 204 "Rewrites default and static methods in interfaces if --min_sdk_version < 24. This " 205 + "only works correctly if subclasses of rewritten interfaces as well as uses of " 206 + "static interface methods are run through this tool as well." 207 ) 208 public boolean desugarInterfaceMethodBodiesIfNeeded; 209 210 @Option( 211 name = "desugar_try_with_resources_if_needed", 212 defaultValue = "true", 213 category = "misc", 214 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 215 effectTags = {OptionEffectTag.UNKNOWN}, 216 help = "Rewrites try-with-resources statements if --min_sdk_version < 19." 217 ) 218 public boolean desugarTryWithResourcesIfNeeded; 219 220 @Option( 221 name = "desugar_try_with_resources_omit_runtime_classes", 222 defaultValue = "false", 223 category = "misc", 224 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 225 effectTags = {OptionEffectTag.UNKNOWN}, 226 help = 227 "Omits the runtime classes necessary to support try-with-resources from the output. " 228 + "This property has effect only if --desugar_try_with_resources_if_needed is used." 229 ) 230 public boolean desugarTryWithResourcesOmitRuntimeClasses; 231 232 @Option( 233 name = "copy_bridges_from_classpath", 234 defaultValue = "false", 235 category = "misc", 236 documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, 237 effectTags = {OptionEffectTag.UNKNOWN}, 238 help = "Copy bridges from classpath to desugared classes." 239 ) 240 public boolean copyBridgesFromClasspath; 241 242 @Option( 243 name = "core_library", 244 defaultValue = "false", 245 documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, 246 effectTags = {OptionEffectTag.UNKNOWN}, 247 help = "Enables rewriting to desugar java.* classes." 248 ) 249 public boolean coreLibrary; 250 251 /** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */ 252 // TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789) 253 @Option( 254 name = "legacy_jacoco_fix", 255 defaultValue = "false", 256 documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, 257 effectTags = {OptionEffectTag.UNKNOWN}, 258 help = "Consider setting this flag if you're using JaCoCo versions prior to 0.7.9 to work " 259 + "around issues with coverage instrumentation in default and static interface methods. " 260 + "This flag may be removed when no longer needed." 261 ) 262 public boolean legacyJacocoFix; 263 } 264 265 private final DesugarOptions options; 266 private final CoreLibraryRewriter rewriter; 267 private final LambdaClassMaker lambdas; 268 private final GeneratedClassStore store; 269 private final Set<String> visitedExceptionTypes = new HashSet<>(); 270 /** The counter to record the times of try-with-resources desugaring is invoked. */ 271 private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); 272 273 private final boolean outputJava7; 274 private final boolean allowDefaultMethods; 275 private final boolean allowTryWithResources; 276 private final boolean allowCallsToObjectsNonNull; 277 private final boolean allowCallsToLongCompare; 278 /** An instance of Desugar is expected to be used ONLY ONCE */ 279 private boolean used; 280 Desugar(DesugarOptions options, Path dumpDirectory)281 private Desugar(DesugarOptions options, Path dumpDirectory) { 282 this.options = options; 283 this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); 284 this.lambdas = new LambdaClassMaker(dumpDirectory); 285 this.store = new GeneratedClassStore(); 286 this.outputJava7 = options.minSdkVersion < 24; 287 this.allowDefaultMethods = 288 options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24; 289 this.allowTryWithResources = 290 !options.desugarTryWithResourcesIfNeeded || options.minSdkVersion >= 19; 291 this.allowCallsToObjectsNonNull = options.minSdkVersion >= 19; 292 this.allowCallsToLongCompare = options.minSdkVersion >= 19 && !options.alwaysRewriteLongCompare; 293 this.used = false; 294 } 295 desugar()296 private void desugar() throws Exception { 297 checkState(!this.used, "This Desugar instance has been used. Please create another one."); 298 this.used = true; 299 300 try (Closer closer = Closer.create()) { 301 IndexedInputs indexedBootclasspath = 302 new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)); 303 // Use a classloader that as much as possible uses the provided bootclasspath instead of 304 // the tool's system classloader. Unfortunately we can't do that for java. classes. 305 ClassLoader bootclassloader = 306 options.bootclasspath.isEmpty() 307 ? new ThrowingClassLoader() 308 : new HeaderClassLoader(indexedBootclasspath, rewriter, new ThrowingClassLoader()); 309 IndexedInputs indexedClasspath = 310 new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); 311 312 // Process each input separately 313 for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) { 314 desugarOneInput( 315 inputOutputPair, 316 indexedClasspath, 317 bootclassloader, 318 new ClassReaderFactory(indexedBootclasspath, rewriter)); 319 } 320 } 321 } 322 desugarOneInput( InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, ClassLoader bootclassloader, ClassReaderFactory bootclasspathReader)323 private void desugarOneInput( 324 InputOutputPair inputOutputPair, 325 IndexedInputs indexedClasspath, 326 ClassLoader bootclassloader, 327 ClassReaderFactory bootclasspathReader) 328 throws Exception { 329 Path inputPath = inputOutputPair.getInput(); 330 Path outputPath = inputOutputPair.getOutput(); 331 checkArgument( 332 Files.isDirectory(inputPath) || !Files.isDirectory(outputPath), 333 "Input jar file requires an output jar file"); 334 335 try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath); 336 InputFileProvider inputFiles = toInputFileProvider(inputPath)) { 337 DependencyCollector depsCollector = createDepsCollector(); 338 IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles)); 339 // Prepend classpath with input file itself so LambdaDesugaring can load classes with 340 // lambdas. 341 IndexedInputs indexedClasspathAndInputFiles = indexedClasspath.withParent(indexedInputFiles); 342 // Note that input file and classpath need to be in the same classloader because 343 // we typically get the header Jar for inputJar on the classpath and having the header 344 // Jar in a parent loader means the header version is preferred over the real thing. 345 ClassLoader loader = 346 new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader); 347 348 ClassReaderFactory classpathReader = null; 349 ClassReaderFactory bridgeMethodReader = null; 350 if (outputJava7) { 351 classpathReader = new ClassReaderFactory(indexedClasspathAndInputFiles, rewriter); 352 if (options.copyBridgesFromClasspath) { 353 bridgeMethodReader = classpathReader; 354 } else { 355 bridgeMethodReader = new ClassReaderFactory(indexedInputFiles, rewriter); 356 } 357 } 358 359 ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); 360 ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader); 361 desugarClassesInInput( 362 inputFiles, 363 outputFileProvider, 364 loader, 365 classpathReader, 366 depsCollector, 367 bootclasspathReader, 368 interfaceCache, 369 interfaceLambdaMethodCollector); 370 371 desugarAndWriteDumpedLambdaClassesToOutput( 372 outputFileProvider, 373 loader, 374 classpathReader, 375 depsCollector, 376 bootclasspathReader, 377 interfaceCache, 378 interfaceLambdaMethodCollector.build(), 379 bridgeMethodReader); 380 381 desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader); 382 copyThrowableExtensionClass(outputFileProvider); 383 384 byte[] depsInfo = depsCollector.toByteArray(); 385 if (depsInfo != null) { 386 outputFileProvider.write(OutputFileProvider.DESUGAR_DEPS_FILENAME, depsInfo); 387 } 388 } 389 390 ImmutableMap<Path, LambdaInfo> lambdasLeftBehind = lambdas.drain(); 391 checkState(lambdasLeftBehind.isEmpty(), "Didn't process %s", lambdasLeftBehind); 392 ImmutableMap<String, ClassNode> generatedLeftBehind = store.drain(); 393 checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet()); 394 } 395 396 /** 397 * Returns a dependency collector for use with a single input Jar. If 398 * {@link DesugarOptions#emitDependencyMetadata} is set, this method instantiates the collector 399 * reflectively to allow compiling and using the desugar tool without this mechanism. 400 */ createDepsCollector()401 private DependencyCollector createDepsCollector() { 402 if (options.emitDependencyMetadata) { 403 try { 404 return (DependencyCollector) 405 Thread.currentThread() 406 .getContextClassLoader() 407 .loadClass( 408 "com.google.devtools.build.android.desugar.dependencies.MetadataCollector") 409 .getConstructor(Boolean.TYPE) 410 .newInstance(options.tolerateMissingDependencies); 411 } catch (ReflectiveOperationException | SecurityException e) { 412 throw new IllegalStateException("Can't emit desugaring metadata as requested"); 413 } 414 } else if (options.tolerateMissingDependencies) { 415 return DependencyCollector.NoWriteCollectors.NOOP; 416 } else { 417 return DependencyCollector.NoWriteCollectors.FAIL_ON_MISSING; 418 } 419 } 420 copyThrowableExtensionClass(OutputFileProvider outputFileProvider)421 private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) { 422 if (allowTryWithResources || options.desugarTryWithResourcesOmitRuntimeClasses) { 423 // try-with-resources statements are okay in the output jar. 424 return; 425 } 426 if (this.numOfTryWithResourcesInvoked.get() <= 0) { 427 // the try-with-resources desugaring pass does nothing, so no need to copy these class files. 428 return; 429 } 430 for (String className : 431 TryWithResourcesRewriter.THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT) { 432 try (InputStream stream = Desugar.class.getClassLoader().getResourceAsStream(className)) { 433 outputFileProvider.write(className, ByteStreams.toByteArray(stream)); 434 } catch (IOException e) { 435 throw new IOError(e); 436 } 437 } 438 } 439 440 /** Desugar the classes that are in the inputs specified in the command line arguments. */ desugarClassesInInput( InputFileProvider inputFiles, OutputFileProvider outputFileProvider, ClassLoader loader, @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, ClassVsInterface interfaceCache, ImmutableSet.Builder<String> interfaceLambdaMethodCollector)441 private void desugarClassesInInput( 442 InputFileProvider inputFiles, 443 OutputFileProvider outputFileProvider, 444 ClassLoader loader, 445 @Nullable ClassReaderFactory classpathReader, 446 DependencyCollector depsCollector, 447 ClassReaderFactory bootclasspathReader, 448 ClassVsInterface interfaceCache, 449 ImmutableSet.Builder<String> interfaceLambdaMethodCollector) 450 throws IOException { 451 for (String filename : inputFiles) { 452 if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(filename)) { 453 // TODO(kmb): rule out that this happens or merge input file with what's in depsCollector 454 continue; // skip as we're writing a new file like this at the end or don't want it 455 } 456 try (InputStream content = inputFiles.getInputStream(filename)) { 457 // We can write classes uncompressed since they need to be converted to .dex format 458 // for Android anyways. Resources are written as they were in the input jar to avoid 459 // any danger of accidentally uncompressed resources ending up in an .apk. 460 if (filename.endsWith(".class")) { 461 ClassReader reader = rewriter.reader(content); 462 UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); 463 ClassVisitor visitor = 464 createClassVisitorsForClassesInInputs( 465 loader, 466 classpathReader, 467 depsCollector, 468 bootclasspathReader, 469 interfaceCache, 470 interfaceLambdaMethodCollector, 471 writer, 472 reader); 473 if (writer == visitor) { 474 // Just copy the input if there are no rewritings 475 outputFileProvider.write(filename, reader.b); 476 } else { 477 reader.accept(visitor, 0); 478 outputFileProvider.write(filename, writer.toByteArray()); 479 } 480 } else { 481 outputFileProvider.copyFrom(filename, inputFiles); 482 } 483 } 484 } 485 } 486 487 /** 488 * Desugar the classes that are generated on the fly when we are desugaring the classes in the 489 * specified inputs. 490 */ desugarAndWriteDumpedLambdaClassesToOutput( OutputFileProvider outputFileProvider, ClassLoader loader, @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, ClassVsInterface interfaceCache, ImmutableSet<String> interfaceLambdaMethods, @Nullable ClassReaderFactory bridgeMethodReader)491 private void desugarAndWriteDumpedLambdaClassesToOutput( 492 OutputFileProvider outputFileProvider, 493 ClassLoader loader, 494 @Nullable ClassReaderFactory classpathReader, 495 DependencyCollector depsCollector, 496 ClassReaderFactory bootclasspathReader, 497 ClassVsInterface interfaceCache, 498 ImmutableSet<String> interfaceLambdaMethods, 499 @Nullable ClassReaderFactory bridgeMethodReader) 500 throws IOException { 501 checkState( 502 !allowDefaultMethods || interfaceLambdaMethods.isEmpty(), 503 "Desugaring with default methods enabled moved interface lambdas"); 504 505 // Write out the lambda classes we generated along the way 506 ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain(); 507 checkState( 508 !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(), 509 "There should be no lambda classes generated: %s", 510 lambdaClasses.keySet()); 511 512 for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) { 513 try (InputStream bytecode = Files.newInputStream(lambdaClass.getKey())) { 514 ClassReader reader = rewriter.reader(bytecode); 515 InvokeDynamicLambdaMethodCollector collector = new InvokeDynamicLambdaMethodCollector(); 516 reader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); 517 ImmutableSet<MethodInfo> lambdaMethods = collector.getLambdaMethodsUsedInInvokeDynamics(); 518 checkState(lambdaMethods.isEmpty(), 519 "Didn't expect to find lambda methods but found %s", lambdaMethods); 520 UnprefixingClassWriter writer = 521 rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); 522 ClassVisitor visitor = 523 createClassVisitorsForDumpedLambdaClasses( 524 loader, 525 classpathReader, 526 depsCollector, 527 bootclasspathReader, 528 interfaceCache, 529 interfaceLambdaMethods, 530 bridgeMethodReader, 531 lambdaClass.getValue(), 532 writer, 533 reader); 534 reader.accept(visitor, 0); 535 String filename = 536 rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class"; 537 outputFileProvider.write(filename, writer.toByteArray()); 538 } 539 } 540 } 541 desugarAndWriteGeneratedClasses( OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader)542 private void desugarAndWriteGeneratedClasses( 543 OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader) 544 throws IOException { 545 // Write out any classes we generated along the way 546 ImmutableMap<String, ClassNode> generatedClasses = store.drain(); 547 checkState( 548 generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7), 549 "Didn't expect generated classes but got %s", 550 generatedClasses.keySet()); 551 for (Map.Entry<String, ClassNode> generated : generatedClasses.entrySet()) { 552 UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); 553 // checkState above implies that we want Java 7 .class files, so send through that visitor. 554 // Don't need a ClassReaderFactory b/c static interface methods should've been moved. 555 ClassVisitor visitor = 556 new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader); 557 generated.getValue().accept(visitor); 558 String filename = rewriter.unprefix(generated.getKey()) + ".class"; 559 outputFileProvider.write(filename, writer.toByteArray()); 560 } 561 } 562 563 /** 564 * Create the class visitors for the lambda classes that are generated on the fly. If no new class 565 * visitors are not generated, then the passed-in {@code writer} will be returned. 566 */ createClassVisitorsForDumpedLambdaClasses( ClassLoader loader, @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, ClassVsInterface interfaceCache, ImmutableSet<String> interfaceLambdaMethods, @Nullable ClassReaderFactory bridgeMethodReader, LambdaInfo lambdaClass, UnprefixingClassWriter writer, ClassReader input)567 private ClassVisitor createClassVisitorsForDumpedLambdaClasses( 568 ClassLoader loader, 569 @Nullable ClassReaderFactory classpathReader, 570 DependencyCollector depsCollector, 571 ClassReaderFactory bootclasspathReader, 572 ClassVsInterface interfaceCache, 573 ImmutableSet<String> interfaceLambdaMethods, 574 @Nullable ClassReaderFactory bridgeMethodReader, 575 LambdaInfo lambdaClass, 576 UnprefixingClassWriter writer, 577 ClassReader input) { 578 ClassVisitor visitor = checkNotNull(writer); 579 if (!allowTryWithResources) { 580 CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); 581 input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG); 582 visitor = 583 new TryWithResourcesRewriter( 584 visitor, 585 loader, 586 visitedExceptionTypes, 587 numOfTryWithResourcesInvoked, 588 closeResourceMethodScanner.hasCloseResourceMethod()); 589 } 590 if (!allowCallsToObjectsNonNull) { 591 // Not sure whether there will be implicit null check emitted by javac, so we rerun 592 // the inliner again 593 visitor = new ObjectsRequireNonNullMethodRewriter(visitor); 594 } 595 if (!allowCallsToLongCompare) { 596 visitor = new LongCompareMethodRewriter(visitor); 597 } 598 if (outputJava7) { 599 // null ClassReaderFactory b/c we don't expect to need it for lambda classes 600 visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader); 601 if (options.desugarInterfaceMethodBodiesIfNeeded) { 602 visitor = 603 new DefaultMethodClassFixer( 604 visitor, classpathReader, depsCollector, bootclasspathReader, loader); 605 visitor = 606 new InterfaceDesugaring( 607 visitor, 608 interfaceCache, 609 depsCollector, 610 bootclasspathReader, 611 store, 612 options.legacyJacocoFix); 613 } 614 } 615 visitor = 616 new LambdaClassFixer( 617 visitor, 618 lambdaClass, 619 bridgeMethodReader, 620 loader, 621 interfaceLambdaMethods, 622 allowDefaultMethods, 623 outputJava7); 624 // Send lambda classes through desugaring to make sure there's no invokedynamic 625 // instructions in generated lambda classes (checkState below will fail) 626 visitor = 627 new LambdaDesugaring( 628 visitor, loader, lambdas, null, ImmutableSet.of(), allowDefaultMethods); 629 return visitor; 630 } 631 632 /** 633 * Create the class visitors for the classes which are in the inputs. If new visitors are created, 634 * then all these visitors and the passed-in writer will be chained together. If no new visitor is 635 * created, then the passed-in {@code writer} will be returned. 636 */ createClassVisitorsForClassesInInputs( ClassLoader loader, @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, ClassVsInterface interfaceCache, ImmutableSet.Builder<String> interfaceLambdaMethodCollector, UnprefixingClassWriter writer, ClassReader input)637 private ClassVisitor createClassVisitorsForClassesInInputs( 638 ClassLoader loader, 639 @Nullable ClassReaderFactory classpathReader, 640 DependencyCollector depsCollector, 641 ClassReaderFactory bootclasspathReader, 642 ClassVsInterface interfaceCache, 643 ImmutableSet.Builder<String> interfaceLambdaMethodCollector, 644 UnprefixingClassWriter writer, 645 ClassReader input) { 646 ClassVisitor visitor = checkNotNull(writer); 647 if (!allowTryWithResources) { 648 CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); 649 input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG); 650 visitor = 651 new TryWithResourcesRewriter( 652 visitor, 653 loader, 654 visitedExceptionTypes, 655 numOfTryWithResourcesInvoked, 656 closeResourceMethodScanner.hasCloseResourceMethod()); 657 } 658 if (!allowCallsToObjectsNonNull) { 659 visitor = new ObjectsRequireNonNullMethodRewriter(visitor); 660 } 661 if (!allowCallsToLongCompare) { 662 visitor = new LongCompareMethodRewriter(visitor); 663 } 664 if (!options.onlyDesugarJavac9ForLint) { 665 if (outputJava7) { 666 visitor = new Java7Compatibility(visitor, classpathReader, bootclasspathReader); 667 if (options.desugarInterfaceMethodBodiesIfNeeded) { 668 visitor = 669 new DefaultMethodClassFixer( 670 visitor, classpathReader, depsCollector, bootclasspathReader, loader); 671 visitor = 672 new InterfaceDesugaring( 673 visitor, 674 interfaceCache, 675 depsCollector, 676 bootclasspathReader, 677 store, 678 options.legacyJacocoFix); 679 } 680 } 681 // LambdaDesugaring is relatively expensive, so check first whether we need it. Additionally, 682 // we need to collect lambda methods referenced by invokedynamic instructions up-front anyway. 683 // TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc. 684 InvokeDynamicLambdaMethodCollector collector = new InvokeDynamicLambdaMethodCollector(); 685 input.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); 686 ImmutableSet<MethodInfo> methodsUsedInInvokeDynamics = 687 collector.getLambdaMethodsUsedInInvokeDynamics(); 688 if (!methodsUsedInInvokeDynamics.isEmpty() || collector.needOuterClassRewrite()) { 689 visitor = 690 new LambdaDesugaring( 691 visitor, 692 loader, 693 lambdas, 694 interfaceLambdaMethodCollector, 695 methodsUsedInInvokeDynamics, 696 allowDefaultMethods); 697 } 698 } 699 return visitor; 700 } 701 main(String[] args)702 public static void main(String[] args) throws Exception { 703 // It is important that this method is called first. See its javadoc. 704 Path dumpDirectory = createAndRegisterLambdaDumpDirectory(); 705 verifyLambdaDumpDirectoryRegistered(dumpDirectory); 706 707 DesugarOptions options = parseCommandLineOptions(args); 708 if (options.verbose) { 709 System.out.printf("Lambda classes will be written under %s%n", dumpDirectory); 710 } 711 new Desugar(options, dumpDirectory).desugar(); 712 } 713 verifyLambdaDumpDirectoryRegistered(Path dumpDirectory)714 static void verifyLambdaDumpDirectoryRegistered(Path dumpDirectory) throws IOException { 715 try { 716 Class<?> klass = Class.forName("java.lang.invoke.InnerClassLambdaMetafactory"); 717 Field dumperField = klass.getDeclaredField("dumper"); 718 dumperField.setAccessible(true); 719 Object dumperValue = dumperField.get(null); 720 checkNotNull(dumperValue, "Failed to register lambda dump directory '%s'", dumpDirectory); 721 722 Field dumperPathField = dumperValue.getClass().getDeclaredField("dumpDir"); 723 dumperPathField.setAccessible(true); 724 Object dumperPath = dumperPathField.get(dumperValue); 725 checkState( 726 dumperPath instanceof Path && Files.isSameFile(dumpDirectory, (Path) dumperPath), 727 "Inconsistent lambda dump directories. real='%s', expected='%s'", 728 dumperPath, 729 dumpDirectory); 730 } catch (ReflectiveOperationException e) { 731 // We do not want to crash Desugar, if we cannot load or access these classes or fields. 732 // We aim to provide better diagnostics. If we cannot, just let it go. 733 e.printStackTrace(); 734 } 735 } 736 737 /** 738 * LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating the 739 * call to LambdaMetafactory that the JVM would make when encountering an invokedynamic. 740 * LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump") 741 * generated classes, which we take advantage of here. This property can be set externally, and in 742 * that case the specified directory is used as a temporary dir. Otherwise, it will be set here, 743 * before doing anything else since the property is read in the static initializer. 744 */ createAndRegisterLambdaDumpDirectory()745 static Path createAndRegisterLambdaDumpDirectory() throws IOException { 746 String propertyValue = System.getProperty(LAMBDA_METAFACTORY_DUMPER_PROPERTY); 747 if (propertyValue != null) { 748 Path path = Paths.get(propertyValue); 749 checkState(Files.isDirectory(path), "The path '%s' is not a directory.", path); 750 // It is not necessary to check whether 'path' is an empty directory. It is possible that 751 // LambdaMetafactory is loaded before this class, and there are already lambda classes dumped 752 // into the 'path' folder. 753 // TODO(kmb): Maybe we can empty the folder here. 754 return path; 755 } 756 757 Path dumpDirectory = Files.createTempDirectory("lambdas"); 758 System.setProperty(LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString()); 759 deleteTreeOnExit(dumpDirectory); 760 return dumpDirectory; 761 } 762 parseCommandLineOptions(String[] args)763 private static DesugarOptions parseCommandLineOptions(String[] args) throws IOException { 764 if (args.length == 1 && args[0].startsWith("@")) { 765 args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]); 766 } 767 DesugarOptions options = 768 Options.parseAndExitUponError(DesugarOptions.class, /*allowResidue=*/ false, args) 769 .getOptions(); 770 771 checkArgument(!options.inputJars.isEmpty(), "--input is required"); 772 checkArgument( 773 options.inputJars.size() == options.outputJars.size(), 774 "Desugar requires the same number of inputs and outputs to pair them. #input=%s,#output=%s", 775 options.inputJars.size(), 776 options.outputJars.size()); 777 checkArgument( 778 !options.bootclasspath.isEmpty() || options.allowEmptyBootclasspath, 779 "At least one --bootclasspath_entry is required"); 780 for (Path path : options.bootclasspath) { 781 checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path); 782 } 783 return options; 784 } 785 toInputOutputPairs(DesugarOptions options)786 private static ImmutableList<InputOutputPair> toInputOutputPairs(DesugarOptions options) { 787 final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder(); 788 for (Iterator<Path> inputIt = options.inputJars.iterator(), 789 outputIt = options.outputJars.iterator(); 790 inputIt.hasNext(); ) { 791 ioPairListbuilder.add(InputOutputPair.create(inputIt.next(), outputIt.next())); 792 } 793 return ioPairListbuilder.build(); 794 } 795 796 @VisibleForTesting 797 static class ThrowingClassLoader extends ClassLoader { 798 @Override loadClass(String name, boolean resolve)799 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 800 if (name.startsWith("java.")) { 801 // Use system class loader for java. classes, since ClassLoader.defineClass gets 802 // grumpy when those don't come from the standard place. 803 return super.loadClass(name, resolve); 804 } 805 throw new ClassNotFoundException(); 806 } 807 } 808 deleteTreeOnExit(final Path directory)809 private static void deleteTreeOnExit(final Path directory) { 810 Thread shutdownHook = 811 new Thread() { 812 @Override 813 public void run() { 814 try { 815 deleteTree(directory); 816 } catch (IOException e) { 817 throw new RuntimeException("Failed to delete " + directory, e); 818 } 819 } 820 }; 821 Runtime.getRuntime().addShutdownHook(shutdownHook); 822 } 823 824 /** Recursively delete a directory. */ deleteTree(final Path directory)825 private static void deleteTree(final Path directory) throws IOException { 826 if (directory.toFile().exists()) { 827 Files.walkFileTree( 828 directory, 829 new SimpleFileVisitor<Path>() { 830 @Override 831 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 832 throws IOException { 833 Files.delete(file); 834 return FileVisitResult.CONTINUE; 835 } 836 837 @Override 838 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 839 throws IOException { 840 Files.delete(dir); 841 return FileVisitResult.CONTINUE; 842 } 843 }); 844 } 845 } 846 847 /** Transform a Path to an {@link OutputFileProvider} */ 848 @MustBeClosed toOutputFileProvider(Path path)849 private static OutputFileProvider toOutputFileProvider(Path path) throws IOException { 850 if (Files.isDirectory(path)) { 851 return new DirectoryOutputFileProvider(path); 852 } else { 853 return new ZipOutputFileProvider(path); 854 } 855 } 856 857 /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ 858 @MustBeClosed toInputFileProvider(Path path)859 private static InputFileProvider toInputFileProvider(Path path) throws IOException { 860 if (Files.isDirectory(path)) { 861 return new DirectoryInputFileProvider(path); 862 } else { 863 return new ZipInputFileProvider(path); 864 } 865 } 866 867 /** 868 * Transform a list of Path to a list of InputFileProvider and register them with the given 869 * closer. 870 */ 871 @SuppressWarnings("MustBeClosedChecker") 872 @VisibleForTesting toRegisteredInputFileProvider( Closer closer, List<Path> paths)873 static ImmutableList<InputFileProvider> toRegisteredInputFileProvider( 874 Closer closer, List<Path> paths) throws IOException { 875 ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>(); 876 for (Path path : paths) { 877 builder.add(closer.register(toInputFileProvider(path))); 878 } 879 return builder.build(); 880 } 881 882 /** Pair input and output. */ 883 @AutoValue 884 abstract static class InputOutputPair { 885 create(Path input, Path output)886 static InputOutputPair create(Path input, Path output) { 887 return new AutoValue_Desugar_InputOutputPair(input, output); 888 } 889 getInput()890 abstract Path getInput(); 891 getOutput()892 abstract Path getOutput(); 893 } 894 } 895