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

Java tutorial

Introduction

Here is the source code for com.facebook.buck.jvm.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.jvm.java;

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

import com.facebook.buck.android.AndroidPackageable;
import com.facebook.buck.android.AndroidPackageableCollector;
import com.facebook.buck.io.BuckPaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.core.JavaPackageFinder;
import com.facebook.buck.jvm.core.SuggestBuildRules;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRuleWithResolver;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildOutputInitializer;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildTargetSourcePath;
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.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.SourcePaths;
import com.facebook.buck.rules.keys.SupportsDependencyFileRuleKey;
import com.facebook.buck.rules.keys.SupportsInputBasedRuleKey;
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.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
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.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.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

/**
 * Suppose this were a rule defined in <code>src/com/facebook/feed/BUCK</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 AbstractBuildRuleWithResolver
        implements JavaLibrary, HasClasspathEntries, ExportDependencies, InitializableFromDisk<JavaLibrary.Data>,
        AndroidPackageable, SupportsInputBasedRuleKey, SupportsDependencyFileRuleKey, JavaLibraryWithTests {

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

    @AddToRuleKey
    private final ImmutableSortedSet<SourcePath> srcs;
    @AddToRuleKey
    private final ImmutableSortedSet<SourcePath> resources;
    @AddToRuleKey(stringify = true)
    private final Optional<Path> resourcesRoot;
    @AddToRuleKey
    private final Optional<SourcePath> manifestFile;
    @AddToRuleKey
    private final Optional<String> mavenCoords;
    private final Optional<Path> outputJar;
    @AddToRuleKey
    private final Optional<SourcePath> proguardConfig;
    @AddToRuleKey
    private final ImmutableList<String> postprocessClassesCommands;
    private final ImmutableSortedSet<BuildRule> exportedDeps;
    private final ImmutableSortedSet<BuildRule> providedDeps;
    // Some classes need to override this when enhancing deps (see AndroidLibrary).
    private final ImmutableSet<Path> additionalClasspathEntries;
    private final Supplier<ImmutableSet<Path>> outputClasspathEntriesSupplier;
    private final Supplier<ImmutableSet<Path>> transitiveClasspathsSupplier;
    private final Supplier<ImmutableSet<JavaLibrary>> transitiveClasspathDepsSupplier;

    private final BuildTarget abiJar;
    private final boolean trackClassUsage;
    @AddToRuleKey
    @SuppressWarnings("PMD.UnusedPrivateField")
    private final JarArchiveDependencySupplier abiClasspath;
    private final ImmutableSortedSet<BuildRule> deps;
    @Nullable
    private Path depFileOutputPath;

    private final BuildOutputInitializer<Data> buildOutputInitializer;
    private final ImmutableSortedSet<BuildTarget> tests;
    private final Optional<Path> generatedSourceFolder;

    @SuppressWarnings("PMD.UnusedPrivateField")
    @AddToRuleKey
    private final ImmutableSet<Pattern> classesToRemoveFromJar;

    private final SourcePathRuleFinder ruleFinder;
    @AddToRuleKey
    private final CompileToJarStepFactory compileStepFactory;

    @Override
    public ImmutableSortedSet<BuildTarget> getTests() {
        return tests;
    }

    private static final SuggestBuildRules.JarResolver JAR_RESOLVER = classPath -> {
        ImmutableSet.Builder<String> topLevelSymbolsBuilder = ImmutableSet.builder();
        try {
            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();
    };

    public DefaultJavaLibrary(final BuildRuleParams params, SourcePathResolver resolver,
            SourcePathRuleFinder ruleFinder, Set<? extends SourcePath> srcs, Set<? extends SourcePath> resources,
            Optional<Path> generatedSourceFolder, Optional<SourcePath> proguardConfig,
            ImmutableList<String> postprocessClassesCommands, ImmutableSortedSet<BuildRule> exportedDeps,
            ImmutableSortedSet<BuildRule> providedDeps, BuildTarget abiJar,
            ImmutableSortedSet<SourcePath> abiInputs, boolean trackClassUsage,
            ImmutableSet<Path> additionalClasspathEntries, CompileToJarStepFactory compileStepFactory,
            Optional<Path> resourcesRoot, Optional<SourcePath> manifestFile, Optional<String> mavenCoords,
            ImmutableSortedSet<BuildTarget> tests, ImmutableSet<Pattern> classesToRemoveFromJar) {
        this(params, resolver, ruleFinder, srcs, resources, generatedSourceFolder, proguardConfig,
                postprocessClassesCommands, exportedDeps, providedDeps, abiJar, trackClassUsage,
                new JarArchiveDependencySupplier(abiInputs, params.getProjectFilesystem()),
                additionalClasspathEntries, compileStepFactory, resourcesRoot, manifestFile, mavenCoords, tests,
                classesToRemoveFromJar);
    }

    protected DefaultJavaLibrary(BuildRuleParams params, final SourcePathResolver resolver,
            SourcePathRuleFinder ruleFinder, Set<? extends SourcePath> srcs, Set<? extends SourcePath> resources,
            Optional<Path> generatedSourceFolder, Optional<SourcePath> proguardConfig,
            ImmutableList<String> postprocessClassesCommands, ImmutableSortedSet<BuildRule> exportedDeps,
            ImmutableSortedSet<BuildRule> providedDeps, BuildTarget abiJar, boolean trackClassUsage,
            final JarArchiveDependencySupplier abiClasspath, ImmutableSet<Path> additionalClasspathEntries,
            CompileToJarStepFactory compileStepFactory, Optional<Path> resourcesRoot,
            Optional<SourcePath> manifestFile, Optional<String> mavenCoords, ImmutableSortedSet<BuildTarget> tests,
            ImmutableSet<Pattern> classesToRemoveFromJar) {
        super(params.appendExtraDeps(() -> ruleFinder.filterBuildRuleInputs(abiClasspath.get())), resolver);
        this.ruleFinder = ruleFinder;
        this.compileStepFactory = compileStepFactory;

        // Exported deps are meant to be forwarded onto the CLASSPATH for dependents,
        // and so only make sense for java library types.
        for (BuildRule dep : exportedDeps) {
            if (!(dep instanceof JavaLibrary)) {
                throw new HumanReadableException(params.getBuildTarget() + ": exported dep " + dep.getBuildTarget()
                        + " (" + dep.getType() + ") " + "must be a type of java library.");
            }
        }

        this.srcs = ImmutableSortedSet.copyOf(srcs);
        this.resources = ImmutableSortedSet.copyOf(resources);
        this.proguardConfig = proguardConfig;
        this.postprocessClassesCommands = postprocessClassesCommands;
        this.exportedDeps = exportedDeps;
        this.providedDeps = providedDeps;
        this.additionalClasspathEntries = additionalClasspathEntries.stream().map(getProjectFilesystem()::resolve)
                .collect(MoreCollectors.toImmutableSet());
        this.resourcesRoot = resourcesRoot;
        this.manifestFile = manifestFile;
        this.mavenCoords = mavenCoords;
        this.tests = tests;

        this.abiJar = abiJar;
        this.trackClassUsage = trackClassUsage;
        this.abiClasspath = abiClasspath;
        this.deps = params.getDeps();
        if (!srcs.isEmpty() || !resources.isEmpty() || manifestFile.isPresent()) {
            this.outputJar = Optional.of(getOutputJarPath(getBuildTarget(), getProjectFilesystem()));
        } else {
            this.outputJar = Optional.empty();
        }

        this.outputClasspathEntriesSupplier = Suppliers.memoize(() -> JavaLibraryClasspathProvider
                .getOutputClasspathJars(DefaultJavaLibrary.this, getResolver(), sourcePathForOutputJar()));

        this.transitiveClasspathsSupplier = Suppliers.memoize(
                () -> JavaLibraryClasspathProvider.getClasspathsFromLibraries(getTransitiveClasspathDeps()));

        this.transitiveClasspathDepsSupplier = Suppliers
                .memoize(() -> JavaLibraryClasspathProvider.getTransitiveClasspathDeps(DefaultJavaLibrary.this));

        this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this);
        this.generatedSourceFolder = generatedSourceFolder;
        this.classesToRemoveFromJar = classesToRemoveFromJar;
    }

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

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

    private Optional<SourcePath> sourcePathForOutputJar() {
        return outputJar.map(SourcePaths.getToBuildTargetSourcePath(getBuildTarget())::apply);
    }

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

    static Path getUsedClassesFilePath(BuildTarget target, ProjectFilesystem filesystem) {
        return getOutputJarDirPath(target, filesystem).resolve("used-classes.json");
    }

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

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

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

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

    @Override
    public ImmutableSortedSet<SourcePath> getResources() {
        return resources;
    }

    @Override
    public ImmutableSortedSet<BuildRule> getDepsForTransitiveClasspathEntries() {
        return ImmutableSortedSet.copyOf(Sets.union(getDeclaredDeps(), exportedDeps));
    }

    @Override
    public ImmutableSet<Path> getTransitiveClasspaths() {
        return transitiveClasspathsSupplier.get();
    }

    @Override
    public ImmutableSet<JavaLibrary> getTransitiveClasspathDeps() {
        return transitiveClasspathDepsSupplier.get();
    }

    @Override
    public ImmutableSet<Path> getImmediateClasspaths() {
        ImmutableSet.Builder<Path> builder = ImmutableSet.builder();

        // Add any exported deps.
        for (BuildRule exported : getExportedDeps()) {
            if (exported instanceof JavaLibrary) {
                builder.addAll(((JavaLibrary) exported).getImmediateClasspaths());
            }
        }

        // Add ourselves to the classpath if there's a jar to be built.
        Optional<SourcePath> sourcePathForOutputJar = sourcePathForOutputJar();
        if (sourcePathForOutputJar.isPresent()) {
            builder.add(getResolver().getAbsolutePath(sourcePathForOutputJar.get()));
        }

        return builder.build();
    }

    @Override
    public ImmutableSet<Path> getOutputClasspaths() {
        return outputClasspathEntriesSupplier.get();
    }

    @Override
    public Optional<Path> getGeneratedSourcePath() {
        return generatedSourceFolder;
    }

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

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

        FluentIterable<JavaLibrary> declaredClasspathDeps = JavaLibraryClasspathProvider
                .getJavaLibraryDeps(getDepsForTransitiveClasspathEntries());

        // 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.
        BuildTarget target = getBuildTarget();
        Path outputDirectory = getClassesDir(target, getProjectFilesystem());
        steps.add(new MakeCleanDirectoryStep(getProjectFilesystem(), outputDirectory));

        SuggestBuildRules suggestBuildRule = DefaultSuggestBuildRules.createSuggestBuildFunction(JAR_RESOLVER,
                declaredClasspathDeps.toSet(),
                ImmutableSet.<JavaLibrary>builder().addAll(getTransitiveClasspathDeps()).add(this).build(),
                context.getActionGraph().getNodes());

        // We don't want to add these to the declared or transitive deps, since they're only used at
        // compile time.
        Collection<Path> provided = JavaLibraryClasspathProvider.getJavaLibraryDeps(providedDeps)
                .transformAndConcat(JavaLibrary::getOutputClasspaths).filter(Objects::nonNull).toSet();

        ProjectFilesystem projectFilesystem = getProjectFilesystem(); // NOPMD confused by lambda
        Iterable<Path> declaredClasspaths = declaredClasspathDeps
                .transformAndConcat(JavaLibrary::getOutputClasspaths).transform(projectFilesystem::resolve);
        // Only override the bootclasspath if this rule is supposed to compile Android code.
        ImmutableSortedSet<Path> declared = ImmutableSortedSet.<Path>naturalOrder().addAll(declaredClasspaths)
                .addAll(additionalClasspathEntries).addAll(provided).build();

        // Make sure that this directory exists because ABI information will be written here.
        Step mkdir = new MakeCleanDirectoryStep(getProjectFilesystem(), getPathToAbiOutputDir());
        steps.add(mkdir);

        // If there are resources, then link them to the appropriate place in the classes directory.
        JavaPackageFinder finder = context.getJavaPackageFinder();
        if (resourcesRoot.isPresent()) {
            finder = new ResourcesRootPackageFinder(resourcesRoot.get(), finder);
        }

        steps.add(new CopyResourcesStep(getProjectFilesystem(), getResolver(), ruleFinder, target, resources,
                outputDirectory, finder));

        steps.add(new MakeCleanDirectoryStep(getProjectFilesystem(),
                getOutputJarDirPath(target, getProjectFilesystem())));

        // Only run javac if there are .java files to compile or we need to shovel the manifest file
        // into the built jar.
        if (!getJavaSrcs().isEmpty()) {
            ClassUsageFileWriter usedClassesFileWriter;
            if (trackClassUsage) {
                final Path usedClassesFilePath = getUsedClassesFilePath(getBuildTarget(), getProjectFilesystem());
                depFileOutputPath = getProjectFilesystem().getPathForRelativePath(usedClassesFilePath);
                usedClassesFileWriter = new DefaultClassUsageFileWriter(usedClassesFilePath);

                buildableContext.recordArtifact(usedClassesFilePath);
            } else {
                usedClassesFileWriter = NoOpClassUsageFileWriter.instance();
            }

            // This adds the javac command, along with any supporting commands.
            Path pathToSrcsList = BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "__%s__srcs");
            steps.add(new MkdirStep(getProjectFilesystem(), pathToSrcsList.getParent()));

            Path scratchDir = BuildTargets.getGenPath(getProjectFilesystem(), target,
                    "lib__%s____working_directory");
            steps.add(new MakeCleanDirectoryStep(getProjectFilesystem(), scratchDir));
            Optional<Path> workingDirectory = Optional.of(scratchDir);

            ImmutableSortedSet<Path> javaSrcs = getJavaSrcs().stream().map(getResolver()::getRelativePath)
                    .collect(MoreCollectors.toImmutableSortedSet());

            compileStepFactory.createCompileToJarStep(context, javaSrcs, target, getResolver(), ruleFinder,
                    getProjectFilesystem(), declared, outputDirectory, workingDirectory, pathToSrcsList,
                    Optional.of(suggestBuildRule), postprocessClassesCommands,
                    ImmutableSortedSet.of(outputDirectory), /* mainClass */ Optional.empty(),
                    manifestFile.map(getResolver()::getAbsolutePath), outputJar.get(), usedClassesFileWriter,
                    /* output params */
                    steps, buildableContext, classesToRemoveFromJar);
        }

        if (outputJar.isPresent()) {
            Path output = outputJar.get();

            // No source files, only resources
            if (getJavaSrcs().isEmpty()) {
                steps.add(
                        new JarDirectoryStep(getProjectFilesystem(), output, ImmutableSortedSet.of(outputDirectory),
                                /* mainClass */ null, manifestFile.map(getResolver()::getAbsolutePath).orElse(null),
                                true, classesToRemoveFromJar));
            }
            buildableContext.recordArtifact(output);
        }

        JavaLibraryRules.addAccumulateClassNamesStep(this, buildableContext, steps);

        return steps.build();
    }

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

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

    @Override
    public Optional<BuildTarget> getAbiJar() {
        return outputJar.isPresent() ? Optional.of(abiJar) : Optional.empty();
    }

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

    @Override
    @Nullable
    public Path getPathToOutput() {
        return outputJar.orElse(null);
    }

    @Override
    public Iterable<AndroidPackageable> getRequiredPackageables() {
        return AndroidPackageableCollector.getPackageableRules(ImmutableSortedSet
                .copyOf(Sets.difference(Sets.union(getDeclaredDeps(), exportedDeps), providedDeps)));
    }

    @Override
    public Optional<String> getMavenCoords() {
        return mavenCoords;
    }

    @Override
    public void addToCollector(AndroidPackageableCollector collector) {
        if (outputJar.isPresent()) {
            collector.addClasspathEntry(this, new BuildTargetSourcePath(getBuildTarget(), outputJar.get()));
        }
        if (proguardConfig.isPresent()) {
            collector.addProguardConfig(getBuildTarget(), proguardConfig.get());
        }
    }

    @Override
    public boolean useDependencyFileRuleKeys() {
        return !getJavaSrcs().isEmpty() && trackClassUsage;
    }

    @Override
    public Optional<ImmutableSet<SourcePath>> getPossibleInputSourcePaths() {
        return Optional.of(abiClasspath.getArchiveMembers(getResolver()));
    }

    @Override
    public ImmutableList<SourcePath> getInputsAfterBuildingLocally() throws IOException {
        Preconditions.checkState(useDependencyFileRuleKeys());
        return DefaultClassUsageFileReader.loadFromFile(getProjectFilesystem(),
                Preconditions.checkNotNull(depFileOutputPath), deps);
    }

}