com.facebook.buck.java.DefaultJavaLibrary.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.java.DefaultJavaLibrary.java

Source

/*
 * Copyright 2012-present Facebook, Inc.
 *
 * 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.facebook.buck.java;

import static com.facebook.buck.rules.BuildableProperties.Kind.ANDROID;
import static com.facebook.buck.rules.BuildableProperties.Kind.LIBRARY;

import com.facebook.buck.graph.TopologicalSort;
import com.facebook.buck.graph.TraversableGraph;
import com.facebook.buck.java.abi.AbiWriterProtocol;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbiRule;
import com.facebook.buck.rules.AbstractBuildable;
import com.facebook.buck.rules.AnnotationProcessingData;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildDependencies;
import com.facebook.buck.rules.BuildOutputInitializer;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.Buildable;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.BuildableProperties;
import com.facebook.buck.rules.ExportDependencies;
import com.facebook.buck.rules.InitializableFromDisk;
import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.Sha1HashCode;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePaths;
import com.facebook.buck.shell.BashStep;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.util.BuckConstant;
import com.facebook.buck.util.Optionals;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.reflect.ClassPath;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;

import javax.annotation.Nullable;

/**
 * Suppose this were a rule defined in <code>src/com/facebook/feed/BUILD</code>:
 * <pre>
 * java_library(
 *   name = 'feed',
 *   srcs = [
 *     'FeedStoryRenderer.java',
 *   ],
 *   deps = [
 *     '//src/com/facebook/feed/model:model',
 *     '//third-party/java/guava:guava',
 *   ],
 * )
 * </pre>
 * Then this would compile {@code FeedStoryRenderer.java} against Guava and the classes generated
 * from the {@code //src/com/facebook/feed/model:model} rule.
 */
public class DefaultJavaLibrary extends AbstractBuildable implements JavaLibrary, AbiRule, HasClasspathEntries,
        ExportDependencies, InitializableFromDisk<JavaLibrary.Data> {

    private static final BuildableProperties OUTPUT_TYPE = new BuildableProperties(LIBRARY);

    private final BuildTarget target;
    protected ImmutableSortedSet<BuildRule> deps;
    private final ImmutableSortedSet<SourcePath> srcs;
    private final ImmutableSortedSet<SourcePath> resources;
    private final Optional<Path> outputJar;
    private final Optional<Path> proguardConfig;
    private final ImmutableList<String> postprocessClassesCommands;
    private final ImmutableSortedSet<BuildRule> exportedDeps;
    // Some classes need to override this when enhancing deps (see AndroidLibrary).
    protected ImmutableSet<Path> additionalClasspathEntries;
    private final Supplier<ImmutableSetMultimap<JavaLibrary, Path>> outputClasspathEntriesSupplier;
    private final Supplier<ImmutableSetMultimap<JavaLibrary, Path>> transitiveClasspathEntriesSupplier;
    private final Supplier<ImmutableSetMultimap<JavaLibrary, Path>> declaredClasspathEntriesSupplier;
    private final BuildOutputInitializer<Data> buildOutputInitializer;

    // TODO(jacko): This really should be final, but we need to refactor how we get the
    // AndroidPlatformTarget first before it can be.
    private JavacOptions javacOptions;

    /**
     * Function for opening a JAR and returning all symbols that can be referenced from inside of that
     * jar.
     */
    @VisibleForTesting
    static interface JarResolver {
        public ImmutableSet<String> resolve(ProjectFilesystem filesystem, Path relativeClassPath);
    }

    private static final JarResolver JAR_RESOLVER = new JarResolver() {
        @Override
        public ImmutableSet<String> resolve(ProjectFilesystem filesystem, Path relativeClassPath) {
            ImmutableSet.Builder<String> topLevelSymbolsBuilder = ImmutableSet.builder();
            try {
                Path classPath = filesystem.getFileForRelativePath(relativeClassPath).toPath();
                ClassLoader loader = URLClassLoader.newInstance(new URL[] { classPath.toUri().toURL() },
                        /* parent */ null);

                // For every class contained in that jar, check to see if the package name
                // (e.g. com.facebook.foo), the simple name (e.g. ImmutableSet) or the name
                // (e.g com.google.common.collect.ImmutableSet) is one of the missing symbols.
                for (ClassPath.ClassInfo classInfo : ClassPath.from(loader).getTopLevelClasses()) {
                    topLevelSymbolsBuilder.add(classInfo.getPackageName(), classInfo.getSimpleName(),
                            classInfo.getName());
                }
            } catch (IOException e) {
                // Since this simply is a heuristic, return an empty set if we fail to load a jar.
                return topLevelSymbolsBuilder.build();
            }
            return topLevelSymbolsBuilder.build();
        }
    };

    protected DefaultJavaLibrary(BuildRuleParams buildRuleParams, Set<? extends SourcePath> srcs,
            Set<? extends SourcePath> resources, Optional<Path> proguardConfig,
            ImmutableList<String> postprocessClassesCommands, Set<BuildRule> exportedDeps,
            JavacOptions javacOptions) {
        this.target = buildRuleParams.getBuildTarget();
        this.deps = ImmutableSortedSet.<BuildRule>naturalOrder().addAll(buildRuleParams.getDeps())
                .addAll(exportedDeps).build();
        this.srcs = ImmutableSortedSet.copyOf(srcs);
        this.resources = ImmutableSortedSet.copyOf(resources);
        this.proguardConfig = Preconditions.checkNotNull(proguardConfig);
        this.postprocessClassesCommands = Preconditions.checkNotNull(postprocessClassesCommands);
        this.exportedDeps = ImmutableSortedSet.copyOf(exportedDeps);
        this.additionalClasspathEntries = ImmutableSet.of();
        this.javacOptions = Preconditions.checkNotNull(javacOptions);

        if (!srcs.isEmpty() || !resources.isEmpty()) {
            this.outputJar = Optional.of(getOutputJarPath(getBuildTarget()));
        } else {
            this.outputJar = Optional.absent();
        }

        this.outputClasspathEntriesSupplier = Suppliers
                .memoize(new Supplier<ImmutableSetMultimap<JavaLibrary, Path>>() {
                    @Override
                    public ImmutableSetMultimap<JavaLibrary, Path> get() {
                        return JavaLibraryClasspathProvider.getOutputClasspathEntries(DefaultJavaLibrary.this,
                                outputJar);
                    }
                });

        this.transitiveClasspathEntriesSupplier = Suppliers
                .memoize(new Supplier<ImmutableSetMultimap<JavaLibrary, Path>>() {
                    @Override
                    public ImmutableSetMultimap<JavaLibrary, Path> get() {
                        return JavaLibraryClasspathProvider.getTransitiveClasspathEntries(DefaultJavaLibrary.this,
                                outputJar);
                    }
                });

        this.declaredClasspathEntriesSupplier = Suppliers
                .memoize(new Supplier<ImmutableSetMultimap<JavaLibrary, Path>>() {
                    @Override
                    public ImmutableSetMultimap<JavaLibrary, Path> get() {
                        return JavaLibraryClasspathProvider.getDeclaredClasspathEntries(DefaultJavaLibrary.this);
                    }
                });

        this.buildOutputInitializer = new BuildOutputInitializer<>(buildRuleParams.getBuildTarget(), this);
    }

    @Override
    public BuildTarget getBuildTarget() {
        return target;
    }

    public ImmutableSortedSet<BuildRule> getDeps() {
        return deps;
    }

    /**
     * @param outputDirectory Directory to write class files to
     * @param transitiveClasspathEntries Classpaths of all transitive dependencies.
     * @param declaredClasspathEntries Classpaths of all declared dependencies.
     * @param javacOptions javac configuration.
     * @param suggestBuildRules Function to convert from missing symbols to the suggested rules.
     * @param commands List of steps to add to.
     * @return a {@link Supplier} that will return the ABI for this rule after javac is executed.
     */
    @VisibleForTesting
    Supplier<Sha1HashCode> createCommandsForJavac(Path outputDirectory,
            ImmutableSet<Path> transitiveClasspathEntries, ImmutableSet<Path> declaredClasspathEntries,
            JavacOptions javacOptions, BuildDependencies buildDependencies,
            Optional<JavacInMemoryStep.SuggestBuildRules> suggestBuildRules, ImmutableList.Builder<Step> commands,
            BuildTarget target) {
        // Make sure that this directory exists because ABI information will be written here.
        Step mkdir = new MakeCleanDirectoryStep(getPathToAbiOutputDir());
        commands.add(mkdir);

        // Only run javac if there are .java files to compile.
        if (!getJavaSrcs().isEmpty()) {
            Path pathToSrcsList = Paths.get(BuckConstant.GEN_DIR, getBuildTarget().getBasePath(),
                    "__" + getBuildTarget().getShortName() + "__srcs");
            commands.add(new MkdirStep(pathToSrcsList.getParent()));

            final JavacStep javacStep;
            if (javacOptions.getJavaCompilerEnvironment().getJavacPath().isPresent()) {
                Path workingDirectory = BuildTargets.getGenPath(target, "lib__%s____working_directory");
                commands.add(new MakeCleanDirectoryStep(workingDirectory));
                javacStep = new ExternalJavacStep(outputDirectory, getJavaSrcs(), transitiveClasspathEntries,
                        declaredClasspathEntries, javacOptions, Optional.of(getPathToAbiOutputFile()),
                        Optional.of(target.getFullyQualifiedName()), buildDependencies, suggestBuildRules,
                        Optional.of(pathToSrcsList), target, Optional.of(workingDirectory));
            } else {
                javacStep = new JavacInMemoryStep(outputDirectory, getJavaSrcs(), transitiveClasspathEntries,
                        declaredClasspathEntries, javacOptions, Optional.of(getPathToAbiOutputFile()),
                        Optional.of(target.getFullyQualifiedName()), buildDependencies, suggestBuildRules,
                        Optional.of(pathToSrcsList));
            }
            commands.add(javacStep);

            // Create a supplier that extracts the ABI key from javac after it executes.
            return Suppliers.memoize(new Supplier<Sha1HashCode>() {
                @Override
                public Sha1HashCode get() {
                    return createTotalAbiKey(javacStep.getAbiKey());
                }
            });
        } else {
            // When there are no .java files to compile, the ABI key should be a constant.
            return Suppliers.ofInstance(createTotalAbiKey(new Sha1HashCode(AbiWriterProtocol.EMPTY_ABI_KEY)));
        }
    }

    /**
     * Creates the total ABI key for this rule. If export_deps is true, the total key is computed by
     * hashing the ABI keys of the dependencies together with the ABI key of this rule. If export_deps
     * is false, the standalone ABI key for this rule is used as the total key.
     * @param abiKey the standalone ABI key for this rule.
     * @return total ABI key containing also the ABI keys of the dependencies.
     */
    protected Sha1HashCode createTotalAbiKey(Sha1HashCode abiKey) {
        if (getExportedDeps().isEmpty()) {
            return abiKey;
        }

        SortedSet<HasJavaAbi> depsForAbiKey = getDepsForAbiKey();

        // Hash the ABI keys of all dependencies together with ABI key for the current rule.
        Hasher hasher = createHasherWithAbiKeyForDeps(depsForAbiKey);
        hasher.putUnencodedChars(abiKey.getHash());
        return new Sha1HashCode(hasher.hash().toString());
    }

    private Path getPathToAbiOutputDir() {
        return BuildTargets.getGenPath(getBuildTarget(), "lib__%s__abi");
    }

    private Path getPathToAbiOutputFile() {
        return getPathToAbiOutputDir().resolve("abi");
    }

    private static Path getOutputJarDirPath(BuildTarget target) {
        return BuildTargets.getGenPath(target, "lib__%s__output");
    }

    private static Path getOutputJarPath(BuildTarget target) {
        return Paths.get(String.format("%s/%s.jar", getOutputJarDirPath(target), target.getShortName()));
    }

    /**
     * @return directory path relative to the project root where .class files will be generated.
     *     The return value does not end with a slash.
     */
    private static Path getClassesDir(BuildTarget target) {
        return BuildTargets.getBinPath(target, "lib__%s__classes");
    }

    /**
     * Finds all deps that implement JavaLibraryRule and hash their ABI keys together.
     */
    @Override
    public Sha1HashCode getAbiKeyForDeps() {
        return new Sha1HashCode(createHasherWithAbiKeyForDeps(getDepsForAbiKey()).hash().toString());
    }

    /**
     * Returns a sorted set containing the dependencies which will be hashed in the final ABI key.
     * @return the dependencies to be hashed in the final ABI key.
     */
    private SortedSet<HasJavaAbi> getDepsForAbiKey() {
        SortedSet<HasJavaAbi> rulesWithAbiToConsider = Sets.newTreeSet(BUILD_TARGET_COMPARATOR);
        for (BuildRule dep : deps) {
            // This looks odd. DummyJavaAbiRule contains a Buildable that isn't a JavaAbiRule.
            if (dep.getBuildable() instanceof HasJavaAbi) {
                if (dep.getBuildable() instanceof JavaLibrary) {
                    JavaLibrary javaRule = (JavaLibrary) dep.getBuildable();
                    rulesWithAbiToConsider.addAll(javaRule.getOutputClasspathEntries().keys());
                } else {
                    rulesWithAbiToConsider.add((HasJavaAbi) dep.getBuildable());
                }
            }
        }
        return rulesWithAbiToConsider;
    }

    /**
     * Creates a Hasher containing the ABI keys of the dependencies.
     * @param rulesWithAbiToConsider a sorted set containing the dependencies whose ABI key will be
     *     added to the hasher.
     * @return a Hasher containing the ABI keys of the dependencies.
     */
    private Hasher createHasherWithAbiKeyForDeps(SortedSet<HasJavaAbi> rulesWithAbiToConsider) {
        Hasher hasher = Hashing.sha1().newHasher();
        for (HasJavaAbi ruleWithAbiToConsider : rulesWithAbiToConsider) {
            if (ruleWithAbiToConsider == this) {
                continue;
            }

            Sha1HashCode abiKey = ruleWithAbiToConsider.getAbiKey();
            hasher.putUnencodedChars(abiKey.getHash());
        }

        return hasher;
    }

    @Override
    public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
        builder.setReflectively("postprocessClassesCommands", postprocessClassesCommands)
                .setReflectively("resources", resources);
        return javacOptions.appendToRuleKey(builder);
    }

    @Override
    public BuildableProperties getProperties() {
        return OUTPUT_TYPE;
    }

    @Override
    public ImmutableSortedSet<SourcePath> getJavaSrcs() {
        return srcs;
    }

    @Override
    public ImmutableSetMultimap<JavaLibrary, Path> getTransitiveClasspathEntries() {
        return transitiveClasspathEntriesSupplier.get();
    }

    @Override
    public ImmutableSetMultimap<JavaLibrary, Path> getDeclaredClasspathEntries() {
        return declaredClasspathEntriesSupplier.get();
    }

    @Override
    public ImmutableSetMultimap<JavaLibrary, Path> getOutputClasspathEntries() {
        return outputClasspathEntriesSupplier.get();
    }

    @Override
    public AnnotationProcessingData getAnnotationProcessingData() {
        return javacOptions.getAnnotationProcessingData();
    }

    public Optional<Path> getProguardConfig() {
        return proguardConfig;
    }

    @Override
    public Collection<Path> getInputsToCompareToOutput() {
        ImmutableList.Builder<Path> builder = ImmutableList.builder();
        builder.addAll(SourcePaths.filterInputsToCompareToOutput(this.srcs));
        builder.addAll(SourcePaths.filterInputsToCompareToOutput(this.resources));
        Optionals.addIfPresent(this.proguardConfig, builder);
        return builder.build();
    }

    @Override
    public ImmutableSortedSet<BuildRule> getExportedDeps() {
        return exportedDeps;
    }

    public JavacOptions getJavacOptions() {
        return javacOptions;
    }

    /**
     * Building a java_library() rule entails compiling the .java files specified in the srcs
     * attribute. They are compiled into a directory under {@link BuckConstant#BIN_DIR}.
     */
    @Override
    public final List<Step> getBuildSteps(BuildContext context, BuildableContext buildableContext) {
        ImmutableList.Builder<Step> steps = ImmutableList.builder();

        // Only override the bootclasspath if this rule is supposed to compile Android code.
        if (getProperties().is(ANDROID)) {
            this.javacOptions = JavacOptions.builder(javacOptions)
                    .setBootclasspath(context.getAndroidBootclasspathSupplier().get()).build();
        }

        ImmutableSetMultimap<JavaLibrary, Path> transitiveClasspathEntries = ImmutableSetMultimap
                .<JavaLibrary, Path>builder().putAll(getTransitiveClasspathEntries())
                .putAll(this, additionalClasspathEntries).build();

        ImmutableSetMultimap<JavaLibrary, Path> declaredClasspathEntries = ImmutableSetMultimap
                .<JavaLibrary, Path>builder().putAll(getDeclaredClasspathEntries())
                .putAll(this, additionalClasspathEntries).build();

        // Javac requires that the root directory for generated sources already exist.
        Path annotationGenFolder = javacOptions.getAnnotationProcessingData().getGeneratedSourceFolderName();
        if (annotationGenFolder != null) {
            MakeCleanDirectoryStep mkdirGeneratedSources = new MakeCleanDirectoryStep(annotationGenFolder);
            steps.add(mkdirGeneratedSources);
            buildableContext.recordArtifactsInDirectory(annotationGenFolder);
        }

        // Always create the output directory, even if there are no .java files to compile because there
        // might be resources that need to be copied there.
        Path outputDirectory = getClassesDir(getBuildTarget());
        steps.add(new MakeCleanDirectoryStep(outputDirectory));

        Optional<JavacInMemoryStep.SuggestBuildRules> suggestBuildRule = createSuggestBuildFunction(context,
                transitiveClasspathEntries, declaredClasspathEntries, JAR_RESOLVER);

        // This adds the javac command, along with any supporting commands.
        Supplier<Sha1HashCode> abiKeySupplier = createCommandsForJavac(outputDirectory,
                ImmutableSet.copyOf(transitiveClasspathEntries.values()),
                ImmutableSet.copyOf(declaredClasspathEntries.values()), javacOptions,
                context.getBuildDependencies(), suggestBuildRule, steps, getBuildTarget());

        addPostprocessClassesCommands(steps, postprocessClassesCommands, outputDirectory);

        // If there are resources, then link them to the appropriate place in the classes directory.
        steps.add(new CopyResourcesStep(getBuildTarget(), resources, outputDirectory,
                context.getJavaPackageFinder()));

        if (outputJar.isPresent()) {
            steps.add(new MakeCleanDirectoryStep(getOutputJarDirPath(getBuildTarget())));
            steps.add(new JarDirectoryStep(outputJar.get(), Collections.singleton(outputDirectory),
                    /* mainClass */ null, /* manifestFile */ null));
            buildableContext.recordArtifact(outputJar.get());
        }

        Preconditions.checkNotNull(abiKeySupplier, "abiKeySupplier must be set so that getAbiKey() will "
                + "return a non-null value if this rule builds successfully.");

        addStepsToRecordAbiToDisk(steps, abiKeySupplier, buildableContext);

        JavaLibraryRules.addAccumulateClassNamesStep(this, buildableContext, steps);

        return steps.build();
    }

    /**
     * Assuming the build has completed successfully, the ABI should have been computed, and it should
     * be stored for subsequent builds.
     */
    private void addStepsToRecordAbiToDisk(ImmutableList.Builder<Step> commands,
            final Supplier<Sha1HashCode> abiKeySupplier, final BuildableContext buildableContext) {
        // Note that the parent directories for all of the files written by these steps should already
        // have been created by a previous step. Therefore, there is no reason to add a MkdirStep here.
        commands.add(new AbstractExecutionStep("recording ABI metadata") {
            @Override
            public int execute(ExecutionContext context) {
                Sha1HashCode abiKey = abiKeySupplier.get();
                buildableContext.addMetadata(ABI_KEY_ON_DISK_METADATA, abiKey.getHash());
                return 0;
            }
        });
    }

    /**
     *  @param transitiveNotDeclaredRule A {@link BuildRule} that is contained in the transitive
     *      dependency list but is not declared as a dependency.
     *  @param failedImports A Set of remaining failed imports.  This function will mutate this set
     *      and remove any imports satisfied by {@code transitiveNotDeclaredDep}.
     *  @return whether or not adding {@code transitiveNotDeclaredDep} as a dependency to this build
     *      rule would have satisfied one of the {@code failedImports}.
     */
    private boolean isMissingBuildRule(ProjectFilesystem filesystem, BuildRule transitiveNotDeclaredRule,
            Set<String> failedImports, JarResolver jarResolver) {
        Buildable transitiveNotDeclaredDep = transitiveNotDeclaredRule.getBuildable();
        if (!(transitiveNotDeclaredDep instanceof JavaLibrary)) {
            return false;
        }

        ImmutableSet<Path> classPaths = ImmutableSet
                .copyOf(((JavaLibrary) transitiveNotDeclaredDep).getOutputClasspathEntries().values());
        boolean containsMissingBuildRule = false;
        // Open the output jar for every jar contained as the output of transitiveNotDeclaredDep.  With
        // the exception of rules that export their dependencies, this will result in a single
        // classpath.
        for (Path classPath : classPaths) {
            ImmutableSet<String> topLevelSymbols = jarResolver.resolve(filesystem, classPath);

            for (String symbolName : topLevelSymbols) {
                if (failedImports.contains(symbolName)) {
                    failedImports.remove(symbolName);
                    containsMissingBuildRule = true;

                    // If we've found all of the missing imports, bail out early.
                    if (failedImports.isEmpty()) {
                        return true;
                    }
                }
            }
        }
        return containsMissingBuildRule;
    }

    /**
     * @return A function that takes a list of failed imports from a javac invocation and returns a
     *    set of rules to suggest that the developer import to satisfy those imports.
     */
    @VisibleForTesting
    Optional<JavacInMemoryStep.SuggestBuildRules> createSuggestBuildFunction(BuildContext context,
            ImmutableSetMultimap<JavaLibrary, Path> transitiveClasspathEntries,
            ImmutableSetMultimap<JavaLibrary, Path> declaredClasspathEntries, final JarResolver jarResolver) {
        if (context.getBuildDependencies() != BuildDependencies.WARN_ON_TRANSITIVE) {
            return Optional.absent();
        }
        final Set<JavaLibrary> transitiveNotDeclaredDeps = Sets.difference(transitiveClasspathEntries.keySet(),
                Sets.union(ImmutableSet.of(this), declaredClasspathEntries.keySet()));

        TraversableGraph<BuildRule> graph = context.getDependencyGraph();
        final ImmutableList<BuildRule> sortedTransitiveNotDeclaredDeps = ImmutableList
                .copyOf(TopologicalSort.sort(graph, new Predicate<BuildRule>() {
                    @Override
                    public boolean apply(BuildRule input) {
                        return transitiveNotDeclaredDeps.contains(input.getBuildable());
                    }
                })).reverse();

        JavacInMemoryStep.SuggestBuildRules suggestBuildRuleFn = new JavacInMemoryStep.SuggestBuildRules() {
            @Override
            public ImmutableSet<String> suggest(ProjectFilesystem filesystem, ImmutableSet<String> failedImports) {
                ImmutableSet.Builder<String> suggestedDeps = ImmutableSet.builder();

                Set<String> remainingImports = Sets.newHashSet(failedImports);

                for (BuildRule transitiveNotDeclaredDep : sortedTransitiveNotDeclaredDeps) {
                    boolean ruleCanSeeDep = transitiveNotDeclaredDep
                            .isVisibleTo(DefaultJavaLibrary.this.getBuildTarget());
                    if (ruleCanSeeDep && isMissingBuildRule(filesystem, transitiveNotDeclaredDep, remainingImports,
                            jarResolver)) {
                        suggestedDeps.add(transitiveNotDeclaredDep.getFullyQualifiedName());
                    }
                    // If we've wiped out all remaining imports, break the loop looking for them.
                    if (remainingImports.isEmpty()) {
                        break;
                    }
                }
                return suggestedDeps.build();
            }
        };
        return Optional.of(suggestBuildRuleFn);
    }

    /**
     * Instructs this rule to report the ABI it has on disk as its current ABI.
     */
    @Override
    public JavaLibrary.Data initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
        return JavaLibraryRules.initializeFromDisk(getBuildTarget(), onDiskBuildInfo);
    }

    @Override
    public BuildOutputInitializer<Data> getBuildOutputInitializer() {
        return buildOutputInitializer;
    }

    @Override
    public Sha1HashCode getAbiKey() {
        return buildOutputInitializer.getBuildOutput().getAbiKey();
    }

    @Override
    public ImmutableSortedMap<String, HashCode> getClassNamesToHashes() {
        return buildOutputInitializer.getBuildOutput().getClassNamesToHashes();
    }

    /**
     * Adds a BashStep for each postprocessClasses command that runs the command followed by the
     * outputDirectory of javac outputs.
     *
     * The expectation is that the command will inspect and update the directory by
     * modifying, adding, and deleting the .class files in the directory.
     *
     * The outputDirectory should be a valid java root.  I.e., if outputDirectory
     * is buck-out/bin/java/abc/lib__abc__classes/, then a contained class abc.AbcModule
     * should be at buck-out/bin/java/abc/lib__abc__classes/abc/AbcModule.class
     *
     * @param commands the list of Steps we are building.
     * @param postprocessClassesCommands the list of commands to post-process .class files.
     * @param outputDirectory the directory that will contain all the javac output.
     */
    @VisibleForTesting
    static void addPostprocessClassesCommands(ImmutableList.Builder<Step> commands,
            List<String> postprocessClassesCommands, Path outputDirectory) {
        for (final String postprocessClassesCommand : postprocessClassesCommands) {
            BashStep bashStep = new BashStep(postprocessClassesCommand + " " + outputDirectory);
            commands.add(bashStep);
        }
    }

    @VisibleForTesting
    public Optional<Path> getJavac() {
        return javacOptions.getJavaCompilerEnvironment().getJavacPath();
    }

    @VisibleForTesting
    public Optional<JavacVersion> getJavacVersion() {
        return javacOptions.getJavaCompilerEnvironment().getJavacVersion();
    }

    @Override
    @Nullable
    public Path getPathToOutputFile() {
        return outputJar.orNull();
    }
}