blob: 751c23d1d44741f638c1055d45843c09c77b0ac0 [file] [log] [blame]
/*
* 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.graph.AbstractBreadthFirstTraversal;
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.FlavorDomainException;
import com.facebook.buck.model.Flavored;
import com.facebook.buck.model.HasBuildTarget;
import com.facebook.buck.model.ImmutableFlavor;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.BuildRules;
import com.facebook.buck.rules.BuildTargetSourcePath;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.ImmutableBuildRuleType;
import com.facebook.buck.rules.ImplicitDepsInferringDescription;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SymlinkTree;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
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;
public class ThriftLibraryDescription
implements
Description<ThriftConstructorArg>,
Flavored,
ImplicitDepsInferringDescription<ThriftConstructorArg> {
public static final BuildRuleType TYPE = ImmutableBuildRuleType.of("thrift_library");
private static final Flavor INCLUDE_SYMLINK_TREE_FLAVOR =
ImmutableFlavor.of("include_symlink_tree");
private static final BuildRuleType INCLUDE_SYMLINK_TREE_TYPE =
ImmutableBuildRuleType.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.
ImmutableMap.Builder<Flavor, ThriftLanguageSpecificEnhancer> enhancerMapBuilder =
ImmutableMap.builder();
for (ThriftLanguageSpecificEnhancer enhancer : enhancers) {
enhancerMapBuilder.put(enhancer.getFlavor(), enhancer);
}
this.enhancers = new FlavorDomain<>("language", enhancerMapBuilder.build());
}
/**
* 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) {
return BuildTargets.getBinPath(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(BuildTarget target, String name) {
Preconditions.checkArgument(target.isFlavored());
BuildTarget flavoredTarget = createThriftCompilerBuildTarget(target, name);
return BuildTargets.getGenPath(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.<BuildRule>copyOf(thriftRule.getThriftDeps());
}
}.start();
return depsBuilder.build();
}
/**
* Build rule type to use for the rule which generates the language sources.
*/
private static final BuildRuleType THRIFT_COMPILE_TYPE =
ImmutableBuildRuleType.of("thrift_compile");
/**
* 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,
ImmutableList<String> flags,
String language,
ImmutableSet<String> options,
ImmutableMap<String, SourcePath> srcs,
ImmutableSortedSet<ThriftLibrary> deps) {
SourcePath compiler = thriftBuckConfig.getCompiler();
// 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<SymlinkTree> includeTreeRulesBuilder =
ImmutableSortedSet.naturalOrder();
ImmutableList.Builder<Path> includeRootsBuilder = ImmutableList.builder();
for (ThriftLibrary dep : deps) {
includesBuilder.putAll(dep.getIncludes());
includeTreeRulesBuilder.add(dep.getIncludeTreeRule());
includeRootsBuilder.add(dep.getIncludeTreeRule().getRoot());
}
ImmutableMap<Path, SourcePath> includes = includesBuilder.build();
ImmutableSortedSet<SymlinkTree> includeTreeRules = includeTreeRulesBuilder.build();
ImmutableList<Path> includeRoots = includeRootsBuilder.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();
BuildTarget target = createThriftCompilerBuildTarget(params.getBuildTarget(), name);
Path outputDir = getThriftCompilerOutputDir(params.getBuildTarget(), name);
compileRules.put(
name,
new ThriftCompiler(
params.copyWithChanges(
THRIFT_COMPILE_TYPE,
target,
Suppliers.ofInstance(
ImmutableSortedSet.<BuildRule>naturalOrder()
.addAll(
new SourcePathResolver(resolver).filterBuildRuleInputs(
ImmutableList.<SourcePath>builder()
.add(compiler)
.add(source)
.addAll(includes.values())
.build()))
.addAll(includeTreeRules)
.build()),
Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>of())),
new SourcePathResolver(resolver),
compiler,
flags,
outputDir,
source,
language,
options,
includeRoots,
includes));
}
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(
BuildRuleParams params,
BuildRuleResolver resolver,
A args) {
BuildTarget target = params.getBuildTarget();
// Extract the thrift language we're using from our build target.
Optional<Map.Entry<Flavor, ThriftLanguageSpecificEnhancer>> enhancerFlavor;
try {
enhancerFlavor = enhancers.getFlavorAndValue(ImmutableSet.copyOf(target.getFlavors()));
} catch (FlavorDomainException e) {
throw new HumanReadableException("%s: %s", target, e.getMessage());
}
SourcePathResolver pathResolver = new SourcePathResolver(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.or(ImmutableSortedSet.<BuildTarget>of())));
// 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);
BuildTarget symlinkTreeTarget = createThriftIncludeSymlinkTreeTarget(target);
SymlinkTree symlinkTree = new SymlinkTree(
params.copyWithChanges(
INCLUDE_SYMLINK_TREE_TYPE,
symlinkTreeTarget,
Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>of()),
Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>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.<FlavorDomain<?>>of(enhancers),
FluentIterable.from(thriftDeps)
.transform(HasBuildTarget.TO_TARGET)),
implicitDeps),
false);
// 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,
args.flags.or(ImmutableList.<String>of()),
language,
options,
namedSources,
ImmutableSortedSet.<ThriftLibrary>naturalOrder()
.add(thriftLibrary)
.addAll(getTransitiveThriftLibraryDeps(thriftDeps))
.build());
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()) {
thriftSourceBuilder.put(
ent.getKey(),
new ThriftSource(
Preconditions.checkNotNull(compilerRules.get(ent.getKey())),
Preconditions.checkNotNull(args.srcs.get(ent.getValue())),
getThriftCompilerOutputDir(target, ent.getKey())));
}
ImmutableMap<String, ThriftSource> thriftSources = thriftSourceBuilder.build();
// Generate language specific rules.
return enhancer.createBuildRule(
params,
resolver,
args,
thriftSources,
languageSpecificDeps);
}
@Override
public ThriftConstructorArg createUnpopulatedConstructorArg() {
return new ThriftConstructorArg();
}
@Override
public BuildRuleType getBuildRuleType() {
return TYPE;
}
@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,
ThriftConstructorArg arg) {
Optional<Map.Entry<Flavor, ThriftLanguageSpecificEnhancer>> enhancerFlavor;
try {
enhancerFlavor = enhancers.getFlavorAndValue(ImmutableSet.copyOf(buildTarget.getFlavors()));
} catch (FlavorDomainException e) {
throw new HumanReadableException("%s: %s", buildTarget, e.getMessage());
}
// 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.<FlavorDomain<?>>of(enhancers),
arg.deps.get()));
// Add the compiler target, if there is one.
SourcePath compiler = thriftBuckConfig.getCompiler();
if (compiler instanceof BuildTargetSourcePath) {
deps.add(((BuildTargetSourcePath) compiler).getTarget());
}
// 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;
}
}