• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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