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