diff options
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/Desugar.java')
-rw-r--r-- | java/com/google/devtools/build/android/desugar/Desugar.java | 471 |
1 files changed, 290 insertions, 181 deletions
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 22780ea..797cbc4 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.ISO_8859_1; @@ -20,12 +21,16 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParser.OptionUsageRestrictions; +import com.google.errorprone.annotations.MustBeClosed; import java.io.IOException; import java.io.InputStream; import java.nio.file.FileVisitResult; @@ -57,8 +62,8 @@ class Desugar { converter = ExistingPathConverter.class, abbrev = 'i', help = - "Input Jar or directory with classes to desugar (required, the n-th input is paired with" - + "the n-th output)." + "Input Jar or directory with classes to desugar (required, the n-th input is paired with" + + "the n-th output)." ) public List<Path> inputJars; @@ -68,7 +73,9 @@ class Desugar { defaultValue = "", category = "input", converter = ExistingPathConverter.class, - help = "Ordered classpath to resolve symbols in the --input Jar, like javac's -cp flag." + help = + "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like " + + "javac's -cp flag." ) public List<Path> classpath; @@ -78,15 +85,16 @@ class Desugar { defaultValue = "", category = "input", converter = ExistingPathConverter.class, - help = "Bootclasspath that was used to compile the --input Jar with, like javac's " - + "-bootclasspath flag (required)." + help = + "Bootclasspath that was used to compile the --input Jar with, like javac's " + + "-bootclasspath flag (required)." ) public List<Path> bootclasspath; @Option( name = "allow_empty_bootclasspath", defaultValue = "false", - category = "undocumented" + optionUsageRestrictions = OptionUsageRestrictions.UNDOCUMENTED ) public boolean allowEmptyBootclasspath; @@ -95,11 +103,19 @@ class Desugar { defaultValue = "false", help = "A temporary flag specifically for android lint, subject to removal anytime (DO NOT USE)", - category = "undocumented" + optionUsageRestrictions = OptionUsageRestrictions.UNDOCUMENTED ) public boolean onlyDesugarJavac9ForLint; @Option( + name = "rewrite_calls_to_long_compare", + defaultValue = "true", + help = "rewrite calls to Long.compare(long, long) to the JVM instruction lcmp", + category = "misc" + ) + public boolean enableRewritingOfLongCompare; + + @Option( name = "output", allowMultiple = true, defaultValue = "", @@ -140,26 +156,261 @@ class Desugar { @Option( name = "core_library", defaultValue = "false", - category = "undocumented", + optionUsageRestrictions = OptionUsageRestrictions.UNDOCUMENTED, implicitRequirements = "--allow_empty_bootclasspath", help = "Enables rewriting to desugar java.* classes." ) public boolean coreLibrary; } + private final Options options; + private final Path dumpDirectory; + private final CoreLibraryRewriter rewriter; + private final LambdaClassMaker lambdas; + private final boolean allowDefaultMethods; + private final boolean allowCallsToObjectsNonNull; + /** An instance of Desugar is expected to be used ONLY ONCE */ + private boolean used; + + private Desugar(Options options, Path dumpDirectory) { + this.options = options; + this.dumpDirectory = dumpDirectory; + this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); + this.lambdas = new LambdaClassMaker(dumpDirectory); + this.allowDefaultMethods = options.minSdkVersion >= 24; + this.allowCallsToObjectsNonNull = options.minSdkVersion >= 19; + this.used = false; + } + + private void desugar() throws Exception { + checkState(!this.used, "This Desugar instance has been used. Please create another one."); + this.used = true; + + try (Closer closer = Closer.create()) { + IndexedInputs indexedClasspath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); + // Use a classloader that as much as possible uses the provided bootclasspath instead of + // the tool's system classloader. Unfortunately we can't do that for java. classes. + ClassLoader bootclassloader = + options.bootclasspath.isEmpty() + ? new ThrowingClassLoader() + : new HeaderClassLoader( + new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)), + rewriter, + new ThrowingClassLoader()); + + // Process each input separately + for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) { + desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader); + } + } + } + + private void desugarOneInput( + InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, ClassLoader bootclassloader) + throws Exception { + Path inputPath = inputOutputPair.getInput(); + Path outputPath = inputOutputPair.getOutput(); + checkArgument( + Files.isDirectory(inputPath) || !Files.isDirectory(outputPath), + "Input jar file requires an output jar file"); + + try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath); + InputFileProvider inputFiles = toInputFileProvider(inputPath)) { + IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles)); + // Prepend classpath with input file itself so LambdaDesugaring can load classes with + // lambdas. + IndexedInputs indexedClasspathAndInputFiles = indexedClasspath.withParent(indexedInputFiles); + // Note that input file and classpath need to be in the same classloader because + // we typically get the header Jar for inputJar on the classpath and having the header + // Jar in a parent loader means the header version is preferred over the real thing. + ClassLoader loader = + new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader); + + ClassReaderFactory readerFactory = + new ClassReaderFactory( + (options.copyBridgesFromClasspath && !allowDefaultMethods) + ? indexedClasspathAndInputFiles + : indexedInputFiles, + rewriter); + + ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); + + desugarClassesInInput( + inputFiles, outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector); + + desugarAndWriteDumpedLambdaClassesToOutput( + outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector); + } + + ImmutableMap<Path, LambdaInfo> leftBehind = lambdas.drain(); + checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind); + } + + /** Desugar the classes that are in the inputs specified in the command line arguments. */ + private void desugarClassesInInput( + InputFileProvider inputFiles, + OutputFileProvider outputFileProvider, + ClassLoader loader, + ClassReaderFactory readerFactory, + Builder<String> interfaceLambdaMethodCollector) + throws IOException { + for (String filename : inputFiles) { + try (InputStream content = inputFiles.getInputStream(filename)) { + // We can write classes uncompressed since they need to be converted to .dex format + // for Android anyways. Resources are written as they were in the input jar to avoid + // any danger of accidentally uncompressed resources ending up in an .apk. + if (filename.endsWith(".class")) { + ClassReader reader = rewriter.reader(content); + UnprefixingClassWriter writer = + rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); + ClassVisitor visitor = + createClassVisitorsForClassesInInputs( + loader, readerFactory, interfaceLambdaMethodCollector, writer); + reader.accept(visitor, 0); + + outputFileProvider.write(filename, writer.toByteArray()); + } else { + outputFileProvider.copyFrom(filename, inputFiles); + } + } + } + } + + /** + * Desugar the classes that are generated on the fly when we are desugaring the classes in the + * specified inputs. + */ + private void desugarAndWriteDumpedLambdaClassesToOutput( + OutputFileProvider outputFileProvider, + ClassLoader loader, + ClassReaderFactory readerFactory, + Builder<String> interfaceLambdaMethodCollector) + throws IOException { + ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build(); + checkState( + !allowDefaultMethods || interfaceLambdaMethods.isEmpty(), + "Desugaring with default methods enabled moved interface lambdas"); + + // Write out the lambda classes we generated along the way + ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain(); + checkState( + !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(), + "There should be no lambda classes generated: %s", + lambdaClasses.keySet()); + + for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) { + try (InputStream bytecode = + Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) { + ClassReader reader = rewriter.reader(bytecode); + UnprefixingClassWriter writer = + rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); + ClassVisitor visitor = + createClassVisitorsForDumpedLambdaClasses( + loader, readerFactory, interfaceLambdaMethods, lambdaClass.getValue(), writer); + reader.accept(visitor, 0); + String filename = + rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class"; + outputFileProvider.write(filename, writer.toByteArray()); + } + } + } + + /** + * Create the class visitors for the lambda classes that are generated on the fly. If no new class + * visitors are not generated, then the passed-in {@code writer} will be returned. + */ + private ClassVisitor createClassVisitorsForDumpedLambdaClasses( + ClassLoader loader, + ClassReaderFactory readerFactory, + ImmutableSet<String> interfaceLambdaMethods, + LambdaInfo lambdaClass, + UnprefixingClassWriter writer) { + ClassVisitor visitor = writer; + + if (!allowDefaultMethods) { + // null ClassReaderFactory b/c we don't expect to need it for lambda classes + visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); + } + + visitor = + new LambdaClassFixer( + visitor, lambdaClass, readerFactory, interfaceLambdaMethods, allowDefaultMethods); + // Send lambda classes through desugaring to make sure there's no invokedynamic + // instructions in generated lambda classes (checkState below will fail) + visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods); + if (!allowCallsToObjectsNonNull) { + // Not sure whether there will be implicit null check emitted by javac, so we rerun + // the inliner again + visitor = new ObjectsRequireNonNullMethodRewriter(visitor); + } + if (options.enableRewritingOfLongCompare) { + visitor = new LongCompareMethodRewriter(visitor); + } + return visitor; + } + + /** + * Create the class visitors for the classes which are in the inputs. If new visitors are created, + * then all these visitors and the passed-in writer will be chained together. If no new visitor is + * created, then the passed-in {@code writer} will be returned. + */ + private ClassVisitor createClassVisitorsForClassesInInputs( + ClassLoader loader, + ClassReaderFactory readerFactory, + Builder<String> interfaceLambdaMethodCollector, + UnprefixingClassWriter writer) { + checkArgument(writer != null, "The class writer cannot be null"); + ClassVisitor visitor = writer; + + if (!options.onlyDesugarJavac9ForLint) { + if (!allowDefaultMethods) { + visitor = new Java7Compatibility(visitor, readerFactory); + } + + visitor = + new LambdaDesugaring( + visitor, loader, lambdas, interfaceLambdaMethodCollector, allowDefaultMethods); + } + + if (!allowCallsToObjectsNonNull) { + visitor = new ObjectsRequireNonNullMethodRewriter(visitor); + } + if (options.enableRewritingOfLongCompare) { + visitor = new LongCompareMethodRewriter(visitor); + } + return visitor; + } + + public static void main(String[] args) throws Exception { - // LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating - // the call to LambdaMetafactory that the JVM would make when encountering an invokedynamic. - // LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump") - // generated classes, which we take advantage of here. Set property before doing anything else - // since the property is read in the static initializer; if this breaks we can investigate - // setting the property when calling the tool. + // It is important that this method is called first. See its javadoc. + Path dumpDirectory = createAndRegisterLambdaDumpDirectory(); + Options options = parseCommandLineOptions(args); + if (options.verbose) { + System.out.printf("Lambda classes will be written under %s%n", dumpDirectory); + } + new Desugar(options, dumpDirectory).desugar(); + } + + /** + * LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating the + * call to LambdaMetafactory that the JVM would make when encountering an invokedynamic. + * LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump") + * generated classes, which we take advantage of here. Set property before doing anything else + * since the property is read in the static initializer; if this breaks we can investigate setting + * the property when calling the tool. + */ + private static Path createAndRegisterLambdaDumpDirectory() throws IOException { Path dumpDirectory = Files.createTempDirectory("lambdas"); System.setProperty( LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString()); deleteTreeOnExit(dumpDirectory); + return dumpDirectory; + } + private static Options parseCommandLineOptions(String[] args) throws IOException { if (args.length == 1 && args[0].startsWith("@")) { args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]); } @@ -167,155 +418,25 @@ class Desugar { OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class); optionsParser.setAllowResidue(false); optionsParser.parseAndExitUponError(args); + Options options = optionsParser.getOptions(Options.class); - checkState(!options.inputJars.isEmpty(), "--input is required"); - checkState( + checkArgument(!options.inputJars.isEmpty(), "--input is required"); + checkArgument( options.inputJars.size() == options.outputJars.size(), - "Desugar requires the same number of inputs and outputs to pair them"); - checkState(!options.bootclasspath.isEmpty() || options.allowEmptyBootclasspath, + "Desugar requires the same number of inputs and outputs to pair them. #input=%s,#output=%s", + options.inputJars.size(), + options.outputJars.size()); + checkArgument( + !options.bootclasspath.isEmpty() || options.allowEmptyBootclasspath, "At least one --bootclasspath_entry is required"); - for (Path path : options.classpath) { - checkState(!Files.isDirectory(path), "Classpath entry must be a jar file: %s", path); - } for (Path path : options.bootclasspath) { - checkState(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path); - } - if (options.verbose) { - System.out.printf("Lambda classes will be written under %s%n", dumpDirectory); - } - - CoreLibraryRewriter rewriter = - new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); - - boolean allowDefaultMethods = options.minSdkVersion >= 24; - boolean allowCallsToObjectsNonNull = options.minSdkVersion >= 19; - - LambdaClassMaker lambdas = new LambdaClassMaker(dumpDirectory); - - // Process each input separately - for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) { - Path inputPath = inputOutputPair.getInput(); - Path outputPath = inputOutputPair.getOutput(); - checkState( - Files.isDirectory(inputPath) || !Files.isDirectory(outputPath), - "Input jar file requires an output jar file"); - - try (Closer closer = Closer.create(); - OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath)) { - InputFileProvider appInputFiles = toInputFileProvider(closer, inputPath); - List<InputFileProvider> classpathInputFiles = - toInputFileProvider(closer, options.classpath); - IndexedInputs appIndexedInputs = new IndexedInputs(ImmutableList.of(appInputFiles)); - IndexedInputs appAndClasspathIndexedInputs = - new IndexedInputs(classpathInputFiles, appIndexedInputs); - ClassLoader loader = - createClassLoader( - rewriter, - toInputFileProvider(closer, options.bootclasspath), - appAndClasspathIndexedInputs); - - ClassReaderFactory readerFactory = - new ClassReaderFactory( - (options.copyBridgesFromClasspath && !allowDefaultMethods) - ? appAndClasspathIndexedInputs - : appIndexedInputs, - rewriter); - - ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); - - // Process inputs, desugaring as we go - for (String filename : appInputFiles) { - try (InputStream content = appInputFiles.getInputStream(filename)) { - // We can write classes uncompressed since they need to be converted to .dex format - // for Android anyways. Resources are written as they were in the input jar to avoid - // any danger of accidentally uncompressed resources ending up in an .apk. - if (filename.endsWith(".class")) { - ClassReader reader = rewriter.reader(content); - CoreLibraryRewriter.UnprefixingClassWriter writer = - rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); - ClassVisitor visitor = writer; - - if (!options.onlyDesugarJavac9ForLint) { - if (!allowDefaultMethods) { - visitor = new Java7Compatibility(visitor, readerFactory); - } - - visitor = - new LambdaDesugaring( - visitor, - loader, - lambdas, - interfaceLambdaMethodCollector, - allowDefaultMethods); - } - - if (!allowCallsToObjectsNonNull) { - visitor = new ObjectsRequireNonNullMethodInliner(visitor); - } - reader.accept(visitor, 0); - - outputFileProvider.write(filename, writer.toByteArray()); - } else { - outputFileProvider.copyFrom(filename, appInputFiles); - } - } - } - - ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build(); - checkState( - !allowDefaultMethods || interfaceLambdaMethods.isEmpty(), - "Desugaring with default methods enabled moved interface lambdas"); - - // Write out the lambda classes we generated along the way - ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain(); - checkState( - !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(), - "There should be no lambda classes generated: %s", - lambdaClasses.keySet()); - - for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) { - try (InputStream bytecode = - Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) { - ClassReader reader = rewriter.reader(bytecode); - CoreLibraryRewriter.UnprefixingClassWriter writer = - rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); - ClassVisitor visitor = writer; - - if (!allowDefaultMethods) { - // null ClassReaderFactory b/c we don't expect to need it for lambda classes - visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); - } - - visitor = - new LambdaClassFixer( - visitor, - lambdaClass.getValue(), - readerFactory, - interfaceLambdaMethods, - allowDefaultMethods); - // Send lambda classes through desugaring to make sure there's no invokedynamic - // instructions in generated lambda classes (checkState below will fail) - visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods); - if (!allowCallsToObjectsNonNull) { - // Not sure whether there will be implicit null check emitted by javac, so we rerun - // the inliner again - visitor = new ObjectsRequireNonNullMethodInliner(visitor); - } - reader.accept(visitor, 0); - String filename = - rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class"; - outputFileProvider.write(filename, writer.toByteArray()); - } - } - - Map<Path, LambdaInfo> leftBehind = lambdas.drain(); - checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind); - } + checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path); } + return options; } - private static List<InputOutputPair> toInputOutputPairs(Options options) { + private static ImmutableList<InputOutputPair> toInputOutputPairs(Options options) { final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder(); for (Iterator<Path> inputIt = options.inputJars.iterator(), outputIt = options.outputJars.iterator(); @@ -325,24 +446,6 @@ class Desugar { return ioPairListbuilder.build(); } - private static ClassLoader createClassLoader( - CoreLibraryRewriter rewriter, - List<InputFileProvider> bootclasspath, - IndexedInputs appAndClasspathIndexedInputs) - throws IOException { - // Use a classloader that as much as possible uses the provided bootclasspath instead of - // the tool's system classloader. Unfortunately we can't do that for java. classes. - ClassLoader parent = new ThrowingClassLoader(); - if (!bootclasspath.isEmpty()) { - parent = new HeaderClassLoader(new IndexedInputs(bootclasspath), rewriter, parent); - } - // Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas. - // Note that inputJar and classpath need to be in the same classloader because we typically get - // the header Jar for inputJar on the classpath and having the header Jar in a parent loader - // means the header version is preferred over the real thing. - return new HeaderClassLoader(appAndClasspathIndexedInputs, rewriter, parent); - } - private static class ThrowingClassLoader extends ClassLoader { @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { @@ -394,6 +497,7 @@ class Desugar { } /** Transform a Path to an {@link OutputFileProvider} */ + @MustBeClosed private static OutputFileProvider toOutputFileProvider(Path path) throws IOException { if (Files.isDirectory(path)) { @@ -403,26 +507,31 @@ class Desugar { } } - /** Transform a Path to an InputFileProvider and register it to close it at the end of desugar */ - private static InputFileProvider toInputFileProvider(Closer closer, Path path) + /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ + @MustBeClosed + private static InputFileProvider toInputFileProvider(Path path) throws IOException { if (Files.isDirectory(path)) { - return closer.register(new DirectoryInputFileProvider(path)); + return new DirectoryInputFileProvider(path); } else { - return closer.register(new ZipInputFileProvider(path)); + return new ZipInputFileProvider(path); } } - private static ImmutableList<InputFileProvider> toInputFileProvider( + /** + * Transform a list of Path to a list of InputFileProvider and register them with the given + * closer. + */ + @SuppressWarnings("MustBeClosedChecker") + private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider( Closer closer, List<Path> paths) throws IOException { ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>(); for (Path path : paths) { - checkState(!Files.isDirectory(path), "Directory is not supported: %s", path); - builder.add(closer.register(new ZipInputFileProvider(path))); + builder.add(closer.register(toInputFileProvider(path))); } return builder.build(); } - + /** * Pair input and output. */ |