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