• 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 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