com.facebook.buck.thrift.ThriftLibraryDescription.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.thrift.ThriftLibraryDescription.java

Source

/*
 * Copyright 2014-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.thrift;

import com.facebook.buck.cxx.HeaderSymlinkTree;
import com.facebook.buck.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.FlavorDomain;
import com.facebook.buck.model.Flavored;
import com.facebook.buck.model.HasBuildTarget;
import com.facebook.buck.model.ImmutableFlavor;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.BuildRules;
import com.facebook.buck.rules.CellPathResolver;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.ImplicitDepsInferringDescription;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.Tool;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.OptionalCompat;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class ThriftLibraryDescription implements Description<ThriftConstructorArg>, Flavored,
        ImplicitDepsInferringDescription<ThriftConstructorArg> {

    private static final Flavor INCLUDE_SYMLINK_TREE_FLAVOR = ImmutableFlavor.of("include_symlink_tree");

    private final ThriftBuckConfig thriftBuckConfig;
    private final FlavorDomain<ThriftLanguageSpecificEnhancer> enhancers;

    public ThriftLibraryDescription(ThriftBuckConfig thriftBuckConfig,
            ImmutableList<ThriftLanguageSpecificEnhancer> enhancers) {

        this.thriftBuckConfig = thriftBuckConfig;

        // Now build up a map indexing them by their flavor.
        this.enhancers = FlavorDomain.from("language", enhancers);
    }

    /**
     * Return the path to use for the symlink tree we setup for the thrift files to be
     * included by other rules.
     */
    @VisibleForTesting
    protected Path getIncludeRoot(BuildTarget target, ProjectFilesystem filesystem) {
        return BuildTargets.getScratchPath(filesystem, target, "%s/include-symlink-tree");
    }

    @VisibleForTesting
    protected BuildTarget createThriftIncludeSymlinkTreeTarget(BuildTarget target) {
        return BuildTarget.builder(target).addFlavors(INCLUDE_SYMLINK_TREE_FLAVOR).build();
    }

    /**
     * Create a unique build target to represent the compile rule for this thrift source for
     * the given language.
     */
    private static BuildTarget createThriftCompilerBuildTarget(BuildTarget target, String name) {
        Preconditions.checkArgument(target.isFlavored());
        return BuildTarget.builder(target).addFlavors(ImmutableFlavor.of(String.format("thrift-compile-%s",
                name.replace('/', '-').replace('.', '-').replace('+', '-').replace(' ', '-')))).build();
    }

    /**
     * Get the output directory for the generated sources used when compiling the given
     * thrift source for the given language.
     */
    @VisibleForTesting
    protected Path getThriftCompilerOutputDir(ProjectFilesystem filesystem, BuildTarget target, String name) {
        Preconditions.checkArgument(target.isFlavored());
        BuildTarget flavoredTarget = createThriftCompilerBuildTarget(target, name);
        return BuildTargets.getGenPath(filesystem, flavoredTarget, "%s/sources");
    }

    // Find all transitive thrift library dependencies of this rule.
    private ImmutableSortedSet<ThriftLibrary> getTransitiveThriftLibraryDeps(Iterable<ThriftLibrary> inputs) {

        final ImmutableSortedSet.Builder<ThriftLibrary> depsBuilder = ImmutableSortedSet.naturalOrder();

        // Build up a graph of the inputs and their transitive dependencies.
        new AbstractBreadthFirstTraversal<BuildRule>(inputs) {
            @Override
            public ImmutableSet<BuildRule> visit(BuildRule rule) {
                ThriftLibrary thriftRule = (ThriftLibrary) rule;
                depsBuilder.add(thriftRule);
                return ImmutableSet.copyOf(thriftRule.getThriftDeps());
            }
        }.start();

        return depsBuilder.build();
    }

    /**
     * Create the build rules which compile the input thrift sources into their respective
     * language specific sources.
     */
    @VisibleForTesting
    protected ImmutableMap<String, ThriftCompiler> createThriftCompilerBuildRules(BuildRuleParams params,
            BuildRuleResolver resolver, CompilerType compilerType, ImmutableList<String> flags, String language,
            ImmutableSet<String> options, ImmutableMap<String, SourcePath> srcs,
            ImmutableSortedSet<ThriftLibrary> deps,
            ImmutableMap<String, ImmutableSortedSet<String>> generatedSources) {

        SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
        Tool compiler = thriftBuckConfig.getCompiler(compilerType, resolver);

        // Build up the include roots to find thrift file deps and also the build rules that
        // generate them.
        ImmutableMap.Builder<Path, SourcePath> includesBuilder = ImmutableMap.builder();
        ImmutableSortedSet.Builder<HeaderSymlinkTree> includeTreeRulesBuilder = ImmutableSortedSet.naturalOrder();
        ImmutableList.Builder<Path> includeRootsBuilder = ImmutableList.builder();
        ImmutableSet.Builder<Path> headerMapsBuilder = ImmutableSet.builder();
        for (ThriftLibrary dep : deps) {
            includesBuilder.putAll(dep.getIncludes());
            includeTreeRulesBuilder.add(dep.getIncludeTreeRule());
            includeRootsBuilder.add(dep.getIncludeTreeRule().getIncludePath());
            headerMapsBuilder.addAll(OptionalCompat.asSet(dep.getIncludeTreeRule().getHeaderMap()));
        }
        ImmutableMap<Path, SourcePath> includes = includesBuilder.build();
        ImmutableSortedSet<HeaderSymlinkTree> includeTreeRules = includeTreeRulesBuilder.build();
        ImmutableList<Path> includeRoots = includeRootsBuilder.build();
        ImmutableSet<Path> headerMaps = headerMapsBuilder.build();

        // For each thrift source, add a thrift compile rule to generate it's sources.
        ImmutableMap.Builder<String, ThriftCompiler> compileRules = ImmutableMap.builder();
        for (ImmutableMap.Entry<String, SourcePath> ent : srcs.entrySet()) {
            String name = ent.getKey();
            SourcePath source = ent.getValue();
            ImmutableSortedSet<String> genSrcs = Preconditions.checkNotNull(generatedSources.get(name));

            BuildTarget target = createThriftCompilerBuildTarget(params.getBuildTarget(), name);
            Path outputDir = getThriftCompilerOutputDir(params.getProjectFilesystem(), params.getBuildTarget(),
                    name);

            compileRules.put(name, new ThriftCompiler(
                    params.copyWithChanges(target, Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>naturalOrder()
                            .addAll(compiler.getDeps(ruleFinder))
                            .addAll(ruleFinder.filterBuildRuleInputs(ImmutableList.<SourcePath>builder().add(source)
                                    .addAll(includes.values()).build()))
                            .addAll(includeTreeRules).build()), Suppliers.ofInstance(ImmutableSortedSet.of())),
                    compiler, flags, outputDir, source, language, options, includeRoots, headerMaps, includes,
                    genSrcs));
        }

        return compileRules.build();
    }

    /**
     * Downcast the given deps to {@link ThriftLibrary} rules, throwing an error if we see an
     * unexpected type.
     */
    private static ImmutableSortedSet<ThriftLibrary> resolveThriftDeps(BuildTarget target,
            Iterable<BuildRule> deps) {

        ImmutableSortedSet.Builder<ThriftLibrary> libDepsBuilder = ImmutableSortedSet.naturalOrder();
        for (BuildRule dep : deps) {
            if (!(dep instanceof ThriftLibrary)) {
                throw new HumanReadableException("%s: parameter \"deps\": \"%s\" (%s) is not a thrift_library",
                        target, dep.getBuildTarget(), dep.getType());
            }
            libDepsBuilder.add((ThriftLibrary) dep);
        }

        return libDepsBuilder.build();
    }

    @Override
    public <A extends ThriftConstructorArg> BuildRule createBuildRule(TargetGraph targetGraph,
            BuildRuleParams params, BuildRuleResolver resolver, A args) throws NoSuchBuildTargetException {

        BuildTarget target = params.getBuildTarget();

        // Extract the thrift language we're using from our build target.
        Optional<Map.Entry<Flavor, ThriftLanguageSpecificEnhancer>> enhancerFlavor = enhancers
                .getFlavorAndValue(target);
        SourcePathResolver pathResolver = new SourcePathResolver(new SourcePathRuleFinder(resolver));
        ImmutableMap<String, SourcePath> namedSources = pathResolver.getSourcePathNames(target, "srcs",
                args.srcs.keySet());

        // The dependencies listed in "deps", which should all be of type "ThriftLibrary".
        ImmutableSortedSet<ThriftLibrary> thriftDeps = resolveThriftDeps(target, resolver.getAllRules(args.deps));

        // The unflavored version of this rule is responsible for setting up the the various
        // build rules to facilitate dependents including it's thrift sources.
        if (!enhancerFlavor.isPresent()) {

            // Namespace the thrift files using our target's base path.
            ImmutableMap.Builder<Path, SourcePath> includesBuilder = ImmutableMap.builder();
            for (ImmutableMap.Entry<String, SourcePath> entry : namedSources.entrySet()) {
                includesBuilder.put(target.getBasePath().resolve(entry.getKey()), entry.getValue());
            }
            ImmutableMap<Path, SourcePath> includes = includesBuilder.build();

            // Create the symlink tree build rule and add it to the resolver.
            Path includeRoot = getIncludeRoot(target, params.getProjectFilesystem());
            BuildTarget symlinkTreeTarget = createThriftIncludeSymlinkTreeTarget(target);
            HeaderSymlinkTree symlinkTree = new HeaderSymlinkTree(params.copyWithChanges(symlinkTreeTarget,
                    Suppliers.ofInstance(ImmutableSortedSet.of()), Suppliers.ofInstance(ImmutableSortedSet.of())),
                    pathResolver, includeRoot, includes);
            resolver.addToIndex(symlinkTree);

            // Create a dummy rule that dependents can use to grab the information they need
            // about this rule from the action graph.
            return new ThriftLibrary(params, pathResolver, thriftDeps, symlinkTree, includes);
        }

        ThriftLanguageSpecificEnhancer enhancer = enhancerFlavor.get().getValue();
        String language = enhancer.getLanguage();
        ImmutableSet<String> options = enhancer.getOptions(target, args);
        ImmutableSet<BuildTarget> implicitDeps = enhancer.getImplicitDepsForTargetFromConstructorArg(target, args);

        // Lookup the thrift library corresponding to this rule.  We add an implicit dep onto
        // this rule in the findImplicitDepsFromParams method, so this should always exist by
        // the time we get here.
        ThriftLibrary thriftLibrary = (ThriftLibrary) resolver
                .getRule(BuildTarget.of(target.getUnflavoredBuildTarget()));

        // We implicitly pass the language-specific flavors of your thrift lib dependencies as
        // language specific deps to the language specific enhancer.
        ImmutableSortedSet<BuildRule> languageSpecificDeps = BuildRules.toBuildRulesFor(target, resolver,
                Iterables.concat(
                        BuildTargets.propagateFlavorDomains(target, ImmutableList.of(enhancers),
                                FluentIterable.from(thriftDeps).transform(HasBuildTarget::getBuildTarget)),
                        implicitDeps));

        // Form the set of generated sources, so that compiler rules know what output paths to record.
        ImmutableMap.Builder<String, ImmutableSortedSet<String>> generatedSourcesBuilder = ImmutableMap.builder();
        for (ImmutableMap.Entry<String, SourcePath> ent : namedSources.entrySet()) {
            String thriftName = ent.getKey();
            ImmutableList<String> services = Preconditions.checkNotNull(args.srcs.get(ent.getValue()));
            generatedSourcesBuilder.put(thriftName,
                    enhancer.getGeneratedSources(target, args, thriftName, services));
        }
        ImmutableMap<String, ImmutableSortedSet<String>> generatedSources = generatedSourcesBuilder.build();

        // Create a a build rule for thrift source file, to compile the language specific sources.
        // They keys in this map are the logical names of the thrift files (e.g as specific in a BUCK
        // file, such as "test.thrift").
        ImmutableMap<String, ThriftCompiler> compilerRules = createThriftCompilerBuildRules(params, resolver,
                enhancer.getCompilerType(), args.flags, language, options, namedSources,
                ImmutableSortedSet.<ThriftLibrary>naturalOrder().add(thriftLibrary)
                        .addAll(getTransitiveThriftLibraryDeps(thriftDeps)).build(),
                generatedSources);
        resolver.addAllToIndex(compilerRules.values());

        // Build up the map of {@link ThriftSource} objects to pass the language specific enhancer.
        // They keys in this map are the logical names of the thrift files (e.g as specific in a BUCK
        // file, such as "test.thrift").
        ImmutableMap.Builder<String, ThriftSource> thriftSourceBuilder = ImmutableMap.builder();
        for (ImmutableMap.Entry<String, SourcePath> ent : namedSources.entrySet()) {
            ImmutableList<String> services = Preconditions.checkNotNull(args.srcs.get(ent.getValue()));
            ThriftCompiler compilerRule = Preconditions.checkNotNull(compilerRules.get(ent.getKey()));
            thriftSourceBuilder.put(ent.getKey(), new ThriftSource(compilerRule, services,
                    getThriftCompilerOutputDir(params.getProjectFilesystem(), target, ent.getKey())));
        }
        ImmutableMap<String, ThriftSource> thriftSources = thriftSourceBuilder.build();

        // Generate language specific rules.
        return enhancer.createBuildRule(targetGraph, params, resolver, args, thriftSources, languageSpecificDeps);
    }

    @Override
    public ThriftConstructorArg createUnpopulatedConstructorArg() {
        return new ThriftConstructorArg();
    }

    @Override
    public boolean hasFlavors(ImmutableSet<Flavor> flavors) {
        return enhancers.containsAnyOf(flavors) || flavors.isEmpty();
    }

    /**
     * Collect implicit deps for the thrift compiler and language specific enhancers.
     */
    @Override
    public Iterable<BuildTarget> findDepsForTargetFromConstructorArgs(BuildTarget buildTarget,
            CellPathResolver cellRoots, ThriftConstructorArg arg) {
        Optional<Map.Entry<Flavor, ThriftLanguageSpecificEnhancer>> enhancerFlavor = enhancers
                .getFlavorAndValue(buildTarget);
        // The unflavored target represents the actual thrift library, which doesn't need
        // any implicit deps.
        if (!enhancerFlavor.isPresent()) {
            return ImmutableList.of();
        }

        List<BuildTarget> deps = Lists.newArrayList();

        // The flavored versions of this rule must always implicitly depend on the non-flavored
        // version, as it sets up the include rules for dependents.
        deps.add(BuildTarget.of(buildTarget.getUnflavoredBuildTarget()));

        // Convert all the thrift library deps into their flavored counterparts and
        // add them to our list of deps, to make sure they get included in the target graph.
        deps.addAll(BuildTargets.propagateFlavorDomains(buildTarget, ImmutableList.of(enhancers), arg.deps));

        // Add the compiler target, if there is one.
        deps.addAll(OptionalCompat
                .asSet(thriftBuckConfig.getCompilerTarget(enhancerFlavor.get().getValue().getCompilerType())));

        // Grab the language specific implicit dependencies and add their raw target representations
        // to our list.
        ThriftLanguageSpecificEnhancer enhancer = enhancerFlavor.get().getValue();
        ImmutableSet<BuildTarget> implicitDeps = enhancer.getImplicitDepsForTargetFromConstructorArg(buildTarget,
                arg);
        deps.addAll(implicitDeps);

        return deps;
    }

    // The version of thrift compiler to use.
    public enum CompilerType {
        THRIFT, THRIFT2
    }

}