com.google.devtools.build.android.desugar.Desugar.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.android.desugar.Desugar.java

Source

// Copyright 2016 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// 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;

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.errorprone.annotations.MustBeClosed;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

/**
 * Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in
 * particular lambdas and method references.
 */
class Desugar {

    /** Commandline options for {@link Desugar}. */
    public static class Options extends OptionsBase {
        @Option(name = "input", allowMultiple = true, defaultValue = "", category = "input", 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).")
        public List<Path> inputJars;

        @Option(name = "classpath_entry", allowMultiple = true, defaultValue = "", category = "input", converter = ExistingPathConverter.class, help = "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like "
                + "javac's -cp flag.")
        public List<Path> classpath;

        @Option(name = "bootclasspath_entry", allowMultiple = true, defaultValue = "", category = "input", converter = ExistingPathConverter.class, 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")
        public boolean allowEmptyBootclasspath;

        @Option(name = "only_desugar_javac9_for_lint", defaultValue = "false", help = "A temporary flag specifically for android lint, subject to removal anytime (DO NOT USE)", category = "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 = "", category = "output", converter = PathConverter.class, abbrev = 'o', help = "Output Jar or directory to write desugared classes into (required, the n-th output is "
                + "paired with the n-th input, output must be a Jar if input is a Jar).")
        public List<Path> outputJars;

        @Option(name = "verbose", defaultValue = "false", category = "misc", abbrev = 'v', help = "Enables verbose debugging output.")
        public boolean verbose;

        @Option(name = "min_sdk_version", defaultValue = "1", category = "misc", help = "Minimum targeted sdk version.  If >= 24, enables default methods in interfaces.")
        public int minSdkVersion;

        @Option(name = "copy_bridges_from_classpath", defaultValue = "false", category = "misc", help = "Copy bridges from classpath to desugared classes.")
        public boolean copyBridgesFromClasspath;

        @Option(name = "core_library", defaultValue = "false", category = "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 {
        // 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]);
        }

        OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class);
        optionsParser.setAllowResidue(false);
        optionsParser.parseAndExitUponError(args);

        Options options = optionsParser.getOptions(Options.class);

        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. #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.bootclasspath) {
            checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path);
        }
        return 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(); inputIt.hasNext();) {
            ioPairListbuilder.add(InputOutputPair.create(inputIt.next(), outputIt.next()));
        }
        return ioPairListbuilder.build();
    }

    private static class ThrowingClassLoader extends ClassLoader {
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (name.startsWith("java.")) {
                // Use system class loader for java. classes, since ClassLoader.defineClass gets
                // grumpy when those don't come from the standard place.
                return super.loadClass(name, resolve);
            }
            throw new ClassNotFoundException();
        }
    }

    private static void deleteTreeOnExit(final Path directory) {
        Thread shutdownHook = new Thread() {
            @Override
            public void run() {
                try {
                    deleteTree(directory);
                } catch (IOException e) {
                    throw new RuntimeException("Failed to delete " + directory, e);
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    /** Recursively delete a directory. */
    private static void deleteTree(final Path directory) throws IOException {
        if (directory.toFile().exists()) {
            Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }

    /** Transform a Path to an {@link OutputFileProvider} */
    @MustBeClosed
    private static OutputFileProvider toOutputFileProvider(Path path) throws IOException {
        if (Files.isDirectory(path)) {
            return new DirectoryOutputFileProvider(path);
        } else {
            return new ZipOutputFileProvider(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 new DirectoryInputFileProvider(path);
        } else {
            return new ZipInputFileProvider(path);
        }
    }

    /**
     * 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) {
            builder.add(closer.register(toInputFileProvider(path)));
        }
        return builder.build();
    }

    /**
     * Pair input and output.
     */
    @AutoValue
    abstract static class InputOutputPair {

        static InputOutputPair create(Path input, Path output) {
            return new AutoValue_Desugar_InputOutputPair(input, output);
        }

        abstract Path getInput();

        abstract Path getOutput();
    }
}