/*
* Copyright 2017-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.rust;
import com.facebook.buck.cxx.CxxDescriptionEnhancer;
import com.facebook.buck.cxx.CxxGenruleDescription;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.cxx.Linker;
import com.facebook.buck.cxx.Linkers;
import com.facebook.buck.cxx.NativeLinkable;
import com.facebook.buck.cxx.NativeLinkables;
import com.facebook.buck.graph.AbstractBreadthFirstThrowingTraversal;
import com.facebook.buck.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.FlavorDomain;
import com.facebook.buck.model.Pair;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.BinaryWrapperRule;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.CommandTool;
import com.facebook.buck.rules.ForwardingBuildTargetSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.SymlinkTree;
import com.facebook.buck.rules.Tool;
import com.facebook.buck.rules.args.Arg;
import com.facebook.buck.rules.args.SourcePathArg;
import com.facebook.buck.rules.args.StringArg;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
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 java.nio.file.Path;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
/** Utilities to generate various kinds of Rust compilation. */
public class RustCompileUtils {
private RustCompileUtils() {}
protected static BuildTarget getCompileBuildTarget(
BuildTarget target, CxxPlatform cxxPlatform, CrateType crateType) {
return target.withFlavors(cxxPlatform.getFlavor(), crateType.getFlavor());
}
// Construct a RustCompileRule with:
// - all sources
// - rustc
// - linker
// - rustc optim / feature / cfg / user-specified flags
// - linker args
// - `--extern <crate>=<rlibpath>` for direct dependencies
// - `-L dependency=<dir>` for transitive dependencies
// - `-C relocation-model=pic/static/default/dynamic-no-pic` according to flavor
// - `--emit metadata` if flavor is "check"
// - `--crate-type lib/rlib/dylib/cdylib/staticlib` according to flavor
protected static RustCompileRule createBuild(
BuildTarget target,
String crateName,
BuildRuleParams params,
BuildRuleResolver resolver,
SourcePathRuleFinder ruleFinder,
CxxPlatform cxxPlatform,
RustBuckConfig rustConfig,
ImmutableList<String> extraFlags,
ImmutableList<String> extraLinkerFlags,
Iterable<Arg> linkerInputs,
CrateType crateType,
Linker.LinkableDepType depType,
boolean rpath,
ImmutableSortedSet<SourcePath> sources,
SourcePath rootModule)
throws NoSuchBuildTargetException {
ImmutableSortedSet<BuildRule> ruledeps = params.getBuildDeps();
ImmutableList.Builder<Arg> linkerArgs = ImmutableList.builder();
Stream.concat(rustConfig.getLinkerArgs(cxxPlatform).stream(), extraLinkerFlags.stream())
.map(StringArg::of)
.forEach(linkerArgs::add);
linkerArgs.addAll(linkerInputs);
ImmutableList.Builder<Arg> args = ImmutableList.builder();
String relocModel;
if (crateType.isPic()) {
relocModel = "pic";
} else {
relocModel = "static";
}
Stream<String> checkArgs;
if (crateType.isCheck()) {
args.add(StringArg.of("--emit=metadata"));
checkArgs = rustConfig.getRustCheckFlags().stream();
} else {
checkArgs = Stream.of();
}
Stream.of(
Stream.of(
String.format("--crate-name=%s", crateName),
String.format("--crate-type=%s", crateType),
String.format("-Crelocation-model=%s", relocModel)),
extraFlags.stream(),
checkArgs)
.flatMap(x -> x)
.map(StringArg::of)
.forEach(args::add);
// Find direct and transitive Rust deps. We do this in two passes, since a dependency that's
// both direct and transitive needs to be listed on the command line in each form.
//
// This could end up with a lot of redundant parameters (lots of rlibs in one directory),
// but Arg isn't comparable, so we can't put it in a Set.
// First pass - direct deps
ruledeps
.stream()
.filter(RustLinkable.class::isInstance)
.map(
rule ->
((RustLinkable) rule).getLinkerArg(true, crateType.isCheck(), cxxPlatform, depType))
.forEach(args::add);
// Second pass - indirect deps
new AbstractBreadthFirstTraversal<BuildRule>(
ruledeps
.stream()
.flatMap(r -> r.getBuildDeps().stream())
.collect(MoreCollectors.toImmutableList())) {
private final ImmutableSet<BuildRule> empty = ImmutableSet.of();
@Override
public Iterable<BuildRule> visit(BuildRule rule) {
ImmutableSet<BuildRule> deps = empty;
if (rule instanceof RustLinkable) {
deps = rule.getBuildDeps();
Arg arg =
((RustLinkable) rule).getLinkerArg(false, crateType.isCheck(), cxxPlatform, depType);
args.add(arg);
}
return deps;
}
}.start();
// A native crate output is no longer intended for consumption by the Rust toolchain;
// it's either an executable, or a native library that C/C++ can link with. Rust DYLIBs
// also need all dependencies available.
if (crateType.needAllDeps()) {
ImmutableList<Arg> nativeArgs =
NativeLinkables.getTransitiveNativeLinkableInput(
cxxPlatform,
ruledeps,
depType,
RustLinkable.class::isInstance,
RustLinkable.class::isInstance)
.getArgs();
// Add necessary rpaths if we're dynamically linking with things
if (rpath && depType == Linker.LinkableDepType.SHARED) {
args.add(StringArg.of("-Crpath"));
}
linkerArgs.addAll(nativeArgs);
}
// If we want shared deps or are building a dynamic rlib, make sure we prefer
// dynamic dependencies (esp to get dynamic dependency on standard libs)
if (depType == Linker.LinkableDepType.SHARED || crateType == CrateType.DYLIB) {
args.add(StringArg.of("-Cprefer-dynamic"));
}
String filename = crateType.filenameFor(crateName, cxxPlatform);
return resolver.addToIndex(
RustCompileRule.from(
ruleFinder,
params.withBuildTarget(target),
filename,
rustConfig.getRustCompiler().resolve(resolver),
rustConfig
.getLinkerProvider(cxxPlatform, cxxPlatform.getLd().getType())
.resolve(resolver),
args.build(),
linkerArgs.build(),
CxxGenruleDescription.fixupSourcePaths(resolver, ruleFinder, cxxPlatform, sources),
CxxGenruleDescription.fixupSourcePath(resolver, ruleFinder, cxxPlatform, rootModule),
crateType.hasOutput()));
}
public static RustCompileRule requireBuild(
BuildRuleParams params,
BuildRuleResolver resolver,
SourcePathRuleFinder ruleFinder,
CxxPlatform cxxPlatform,
RustBuckConfig rustConfig,
ImmutableList<String> extraFlags,
ImmutableList<String> extraLinkerFlags,
Iterable<Arg> linkerInputs,
String crateName,
CrateType crateType,
Linker.LinkableDepType depType,
ImmutableSortedSet<SourcePath> sources,
SourcePath rootModule)
throws NoSuchBuildTargetException {
BuildTarget target = getCompileBuildTarget(params.getBuildTarget(), cxxPlatform, crateType);
// If this rule has already been generated, return it.
Optional<RustCompileRule> existing =
resolver.getRuleOptionalWithType(target, RustCompileRule.class);
if (existing.isPresent()) {
return existing.get();
}
return createBuild(
target,
crateName,
params,
resolver,
ruleFinder,
cxxPlatform,
rustConfig,
extraFlags,
extraLinkerFlags,
linkerInputs,
crateType,
depType,
true,
sources,
rootModule);
}
public static Linker.LinkableDepType getLinkStyle(
BuildTarget target, Optional<Linker.LinkableDepType> linkStyle) {
Optional<RustBinaryDescription.Type> type = RustBinaryDescription.BINARY_TYPE.getValue(target);
Linker.LinkableDepType ret;
if (type.isPresent()) {
ret = type.get().getLinkStyle();
} else if (linkStyle.isPresent()) {
ret = linkStyle.get();
} else {
ret = Linker.LinkableDepType.STATIC;
}
// XXX rustc always links executables with "-pie", which requires all objects to be built
// with a PIC relocation model (-fPIC or -Crelocation-model=pic). Rust code does this by
// default, but we need to make sure any C/C++ dependencies are also PIC.
// So for now, remap STATIC -> STATIC_PIC, until we can control rustc's use of -pie.
if (ret == Linker.LinkableDepType.STATIC) {
ret = Linker.LinkableDepType.STATIC_PIC;
}
return ret;
}
public static BinaryWrapperRule createBinaryBuildRule(
BuildRuleParams params,
BuildRuleResolver resolver,
RustBuckConfig rustBuckConfig,
FlavorDomain<CxxPlatform> cxxPlatforms,
CxxPlatform defaultCxxPlatform,
Optional<String> crateName,
ImmutableSortedSet<String> features,
Iterator<String> rustcFlags,
Iterator<String> linkerFlags,
Linker.LinkableDepType linkStyle,
boolean rpath,
ImmutableSortedSet<SourcePath> srcs,
Optional<SourcePath> crateRoot,
ImmutableSet<String> defaultRoots,
boolean isCheck)
throws NoSuchBuildTargetException {
final BuildTarget buildTarget = params.getBuildTarget();
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
ImmutableList.Builder<String> rustcArgs = ImmutableList.builder();
RustCompileUtils.addFeatures(buildTarget, features, rustcArgs);
rustcArgs.addAll(rustcFlags);
ImmutableList.Builder<String> linkerArgs = ImmutableList.builder();
linkerArgs.addAll(linkerFlags);
String crate = crateName.orElse(ruleToCrateName(buildTarget.getShortName()));
CxxPlatform cxxPlatform =
cxxPlatforms.getValue(params.getBuildTarget()).orElse(defaultCxxPlatform);
Pair<SourcePath, ImmutableSortedSet<SourcePath>> rootModuleAndSources =
getRootModuleAndSources(
params.getBuildTarget(),
resolver,
pathResolver,
ruleFinder,
cxxPlatform,
crate,
crateRoot,
defaultRoots,
srcs);
// The target to use for the link rule.
BuildTarget binaryTarget =
params
.getBuildTarget()
.withAppendedFlavors(
isCheck ? RustDescriptionEnhancer.RFCHECK : RustDescriptionEnhancer.RFBIN);
if (!rustBuckConfig.getUnflavoredBinaries()) {
binaryTarget = binaryTarget.withAppendedFlavors(cxxPlatform.getFlavor());
}
CommandTool.Builder executableBuilder = new CommandTool.Builder();
// Special handling for dynamically linked binaries.
if (linkStyle == Linker.LinkableDepType.SHARED) {
// Create a symlink tree with for all native shared (NativeLinkable) libraries
// needed by this binary.
SymlinkTree sharedLibraries =
resolver.addToIndex(
CxxDescriptionEnhancer.createSharedLibrarySymlinkTree(
ruleFinder,
params.getBuildTarget(),
params.getProjectFilesystem(),
cxxPlatform,
params.getBuildDeps(),
RustLinkable.class::isInstance,
RustLinkable.class::isInstance));
// Embed a origin-relative library path into the binary so it can find the shared libraries.
// The shared libraries root is absolute. Also need an absolute path to the linkOutput
Path absBinaryDir =
params
.getBuildTarget()
.getCellPath()
.resolve(RustCompileRule.getOutputDir(binaryTarget, params.getProjectFilesystem()));
linkerArgs.addAll(
Linkers.iXlinker(
"-rpath",
String.format(
"%s/%s",
cxxPlatform.getLd().resolve(resolver).origin(),
absBinaryDir.relativize(sharedLibraries.getRoot()).toString())));
// Add all the shared libraries and the symlink tree as inputs to the tool that represents
// this binary, so that users can attach the proper deps.
executableBuilder.addDep(sharedLibraries);
executableBuilder.addInputs(sharedLibraries.getLinks().values());
// Also add Rust shared libraries as runtime deps. We don't need these in the symlink tree
// because rustc will include their dirs in rpath by default.
Map<String, SourcePath> rustSharedLibraries =
getTransitiveRustSharedLibraries(cxxPlatform, params.getBuildDeps());
executableBuilder.addInputs(rustSharedLibraries.values());
}
final RustCompileRule buildRule =
RustCompileUtils.createBuild(
binaryTarget,
crate,
params,
resolver,
ruleFinder,
cxxPlatform,
rustBuckConfig,
rustcArgs.build(),
linkerArgs.build(),
/* linkerInputs */ ImmutableList.of(),
isCheck ? CrateType.CHECKBIN : CrateType.BIN,
linkStyle,
rpath,
rootModuleAndSources.getSecond(),
rootModuleAndSources.getFirst());
// Add the binary as the first argument.
executableBuilder.addArg(SourcePathArg.of(buildRule.getSourcePathToOutput()));
final CommandTool executable = executableBuilder.build();
return new BinaryWrapperRule(params.copyAppendingExtraDeps(buildRule), ruleFinder) {
@Override
public Tool getExecutableCommand() {
return executable;
}
@Override
public SourcePath getSourcePathToOutput() {
return new ForwardingBuildTargetSourcePath(
getBuildTarget(), buildRule.getSourcePathToOutput());
}
};
}
/**
* Given a list of sources, return the one which is the root based on the defaults and user
* parameters.
*
* @param resolver Source path resolver for rule
* @param crate Name of crate
* @param defaults Default names for this rule (library, binary, etc)
* @param sources List of sources
* @return The matching source
*/
public static Optional<SourcePath> getCrateRoot(
SourcePathResolver resolver,
String crate,
ImmutableSet<String> defaults,
Stream<SourcePath> sources) {
String crateName = String.format("%s.rs", crate);
ImmutableList<SourcePath> res =
sources
.filter(
src -> {
String name = resolver.getRelativePath(src).getFileName().toString();
return defaults.contains(name) || name.equals(crateName);
})
.collect(MoreCollectors.toImmutableList());
if (res.size() == 1) {
return Optional.of(res.get(0));
} else {
return Optional.empty();
}
}
public static void addFeatures(
BuildTarget buildTarget, Iterable<String> features, ImmutableList.Builder<String> args) {
for (String feature : features) {
if (feature.contains("\"")) {
throw new HumanReadableException(
"%s contains an invalid feature name %s", buildTarget.getFullyQualifiedName(), feature);
}
args.add("--cfg", String.format("feature=\"%s\"", feature));
}
}
public static String ruleToCrateName(String rulename) {
return rulename.replace('-', '_');
}
/**
* Collect all the shared libraries generated by {@link RustLinkable}s found by transitively
* traversing all unbroken dependency chains of {@link com.facebook.buck.rust.RustLinkable}
* objects found via the passed in {@link com.facebook.buck.rules.BuildRule} roots.
*
* @return a mapping of library name to the library {@link SourcePath}.
*/
public static Map<String, SourcePath> getTransitiveRustSharedLibraries(
CxxPlatform cxxPlatform, Iterable<? extends BuildRule> inputs)
throws NoSuchBuildTargetException {
ImmutableSortedMap.Builder<String, SourcePath> libs = ImmutableSortedMap.naturalOrder();
new AbstractBreadthFirstThrowingTraversal<BuildRule, NoSuchBuildTargetException>(inputs) {
private final ImmutableSet<BuildRule> empty = ImmutableSet.of();
@Override
public Iterable<BuildRule> visit(BuildRule rule) throws NoSuchBuildTargetException {
ImmutableSet<BuildRule> deps = empty;
if (rule instanceof RustLinkable) {
deps = rule.getBuildDeps();
RustLinkable rustLinkable = (RustLinkable) rule;
if (rustLinkable.getPreferredLinkage() != NativeLinkable.Linkage.STATIC) {
libs.putAll(rustLinkable.getRustSharedLibraries(cxxPlatform));
}
}
return deps;
}
}.start();
return libs.build();
}
static Pair<SourcePath, ImmutableSortedSet<SourcePath>> getRootModuleAndSources(
BuildTarget target,
BuildRuleResolver resolver,
SourcePathResolver pathResolver,
SourcePathRuleFinder ruleFinder,
CxxPlatform cxxPlatform,
String crate,
Optional<SourcePath> crateRoot,
ImmutableSet<String> defaultRoots,
ImmutableSortedSet<SourcePath> srcs)
throws NoSuchBuildTargetException {
ImmutableSortedSet<SourcePath> fixedSrcs =
CxxGenruleDescription.fixupSourcePaths(resolver, ruleFinder, cxxPlatform, srcs);
Optional<SourcePath> rootModule =
crateRoot
.map(Optional::of)
.orElse(getCrateRoot(pathResolver, crate, defaultRoots, fixedSrcs.stream()));
return new Pair<>(
rootModule.orElseThrow(
() ->
new HumanReadableException(
"Can't find suitable top-level source file for %s: %s",
target.getFullyQualifiedName(), fixedSrcs)),
fixedSrcs);
}
}