/*
* 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.cxx;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.RuleKeyObjectSink;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SymlinkTree;
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.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Predicate;
/** A build rule which preprocesses and/or compiles a C/C++ source in a single step. */
public class CxxPreprocessAndCompile extends AbstractBuildRule
implements SupportsInputBasedRuleKey, SupportsDependencyFileRuleKey {
/** The presence or absence of this field denotes whether the input needs to be preprocessed. */
@AddToRuleKey private final Optional<PreprocessorDelegate> preprocessDelegate;
@AddToRuleKey private final CompilerDelegate compilerDelegate;
@AddToRuleKey(stringify = true)
private final Path output;
@AddToRuleKey private final SourcePath input;
private final Optional<CxxPrecompiledHeader> precompiledHeaderRule;
private final CxxSource.Type inputType;
private final DebugPathSanitizer sanitizer;
private final Optional<SymlinkTree> sandboxTree;
private CxxPreprocessAndCompile(
BuildRuleParams params,
Optional<PreprocessorDelegate> preprocessDelegate,
CompilerDelegate compilerDelegate,
Path output,
SourcePath input,
CxxSource.Type inputType,
Optional<CxxPrecompiledHeader> precompiledHeaderRule,
DebugPathSanitizer sanitizer,
Optional<SymlinkTree> sandboxTree) {
super(params);
this.sandboxTree = sandboxTree;
if (precompiledHeaderRule.isPresent()) {
Preconditions.checkState(
preprocessDelegate.isPresent(),
"Precompiled headers are only used when compilation includes preprocessing.");
}
this.preprocessDelegate = preprocessDelegate;
this.compilerDelegate = compilerDelegate;
this.output = output;
this.input = input;
this.inputType = inputType;
this.precompiledHeaderRule = precompiledHeaderRule;
this.sanitizer = sanitizer;
Preconditions.checkArgument(
!params.getBuildTarget().getFlavors().contains(CxxStrip.RULE_FLAVOR)
|| !StripStyle.FLAVOR_DOMAIN.containsAnyOf(params.getBuildTarget().getFlavors()),
"CxxPreprocessAndCompile should not be created with CxxStrip flavors");
Preconditions.checkArgument(
!LinkerMapMode.FLAVOR_DOMAIN.containsAnyOf(params.getBuildTarget().getFlavors()),
"CxxPreprocessAndCompile %s should not be created with LinkerMapMode flavor (%s)",
this,
LinkerMapMode.FLAVOR_DOMAIN);
}
/** @return a {@link CxxPreprocessAndCompile} step that compiles the given preprocessed source. */
public static CxxPreprocessAndCompile compile(
BuildRuleParams params,
CompilerDelegate compilerDelegate,
Path output,
SourcePath input,
CxxSource.Type inputType,
DebugPathSanitizer sanitizer,
Optional<SymlinkTree> sandboxTree) {
return new CxxPreprocessAndCompile(
params,
Optional.empty(),
compilerDelegate,
output,
input,
inputType,
Optional.empty(),
sanitizer,
sandboxTree);
}
/**
* @return a {@link CxxPreprocessAndCompile} step that preprocesses and compiles the given source.
*/
public static CxxPreprocessAndCompile preprocessAndCompile(
BuildRuleParams params,
PreprocessorDelegate preprocessorDelegate,
CompilerDelegate compilerDelegate,
Path output,
SourcePath input,
CxxSource.Type inputType,
Optional<CxxPrecompiledHeader> precompiledHeaderRule,
DebugPathSanitizer sanitizer,
Optional<SymlinkTree> sandboxTree) {
return new CxxPreprocessAndCompile(
params,
Optional.of(preprocessorDelegate),
compilerDelegate,
output,
input,
inputType,
precompiledHeaderRule,
sanitizer,
sandboxTree);
}
@Override
public void appendToRuleKey(RuleKeyObjectSink sink) {
// If a sanitizer is being used for compilation, we need to record the working directory in
// the rule key, as changing this changes the generated object file.
if (preprocessDelegate.isPresent()) {
sink.setReflectively("compilationDirectory", sanitizer.getCompilationDirectory());
}
if (sandboxTree.isPresent()) {
ImmutableMap<Path, SourcePath> links = sandboxTree.get().getLinks();
for (Path path : ImmutableSortedSet.copyOf(links.keySet())) {
SourcePath source = links.get(path);
sink.setReflectively("sandbox(" + path.toString() + ")", source);
}
}
precompiledHeaderRule.ifPresent(
cxxPrecompiledHeader ->
sink.setReflectively("precompiledHeaderRuleInput", cxxPrecompiledHeader.getInput()));
}
private Path getDepFilePath() {
return output.getFileSystem().getPath(output.toString() + ".dep");
}
@VisibleForTesting
CxxPreprocessAndCompileStep makeMainStep(
SourcePathResolver resolver, Path scratchDir, boolean useArgfile) {
// If we're compiling, this will just be empty.
HeaderPathNormalizer headerPathNormalizer =
preprocessDelegate
.map(PreprocessorDelegate::getHeaderPathNormalizer)
.orElseGet(() -> HeaderPathNormalizer.empty(resolver));
ImmutableList<String> arguments =
compilerDelegate.getArguments(
preprocessDelegate
.map(delegate -> delegate.getFlagsWithSearchPaths(precompiledHeaderRule))
.orElseGet(CxxToolFlags::of),
getBuildTarget().getCellPath());
return new CxxPreprocessAndCompileStep(
getBuildTarget(),
getProjectFilesystem(),
preprocessDelegate.isPresent()
? CxxPreprocessAndCompileStep.Operation.PREPROCESS_AND_COMPILE
: CxxPreprocessAndCompileStep.Operation.COMPILE,
output,
// Use a depfile if there's a preprocessing stage, this logic should be kept in sync with
// getInputsAfterBuildingLocally.
preprocessDelegate.isPresent() ? Optional.of(getDepFilePath()) : Optional.empty(),
getRelativeInputPath(resolver),
inputType,
new CxxPreprocessAndCompileStep.ToolCommand(
compilerDelegate.getCommandPrefix(), arguments, compilerDelegate.getEnvironment()),
headerPathNormalizer,
sanitizer,
scratchDir,
useArgfile,
compilerDelegate.getCompiler());
}
public Path getRelativeInputPath(SourcePathResolver resolver) {
// For caching purposes, the path passed to the compiler is relativized by the absolute path by
// the current cell root, so that file references emitted by the compiler would not change if
// the repo is checked out into different places on disk.
return getProjectFilesystem().getRootPath().relativize(resolver.getAbsolutePath(input));
}
@Override
public String getType() {
return "cxx_preprocess_compile";
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
buildableContext.recordArtifact(output);
for (String flag : compilerDelegate.getCompilerFlags().getAllFlags()) {
if (flag.equals("-ftest-coverage")) {
buildableContext.recordArtifact(getGcnoPath(output));
break;
}
}
return new ImmutableList.Builder<Step>()
.add(MkdirStep.of(getProjectFilesystem(), output.getParent()))
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), getScratchPath()))
.add(
makeMainStep(
context.getSourcePathResolver(),
getScratchPath(),
compilerDelegate.isArgFileSupported()))
.build();
}
@VisibleForTesting
static Path getGcnoPath(Path output) {
String basename = MorePaths.getNameWithoutExtension(output);
return output.getParent().resolve(basename + ".gcno");
}
private Path getScratchPath() {
return BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), "%s-tmp");
}
@VisibleForTesting
Optional<PreprocessorDelegate> getPreprocessorDelegate() {
return preprocessDelegate;
}
// Used for compdb
public ImmutableList<String> getCommand(SourcePathResolver pathResolver) {
return makeMainStep(pathResolver, getScratchPath(), false).getCommand();
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), output);
}
public SourcePath getInput() {
return input;
}
@Override
public boolean useDependencyFileRuleKeys() {
return compilerDelegate.isDependencyFileSupported();
}
@Override
public Predicate<SourcePath> getCoveredByDepFilePredicate() {
if (preprocessDelegate.isPresent()) {
return preprocessDelegate.get().getCoveredByDepFilePredicate();
}
return (SourcePath path) -> true;
}
@Override
public Predicate<SourcePath> getExistenceOfInterestPredicate() {
return (SourcePath path) -> false;
}
@Override
public ImmutableList<SourcePath> getInputsAfterBuildingLocally(BuildContext context)
throws IOException {
ImmutableList.Builder<SourcePath> inputs = ImmutableList.builder();
// If present, include all inputs coming from the preprocessor tool.
if (preprocessDelegate.isPresent()) {
Iterable<Path> depFileLines;
try {
depFileLines =
Depfiles.parseAndOutputBuckCompatibleDepfile(
context.getEventBus(),
getProjectFilesystem(),
preprocessDelegate.get().getHeaderPathNormalizer(),
preprocessDelegate.get().getHeaderVerification(),
getDepFilePath(),
getRelativeInputPath(context.getSourcePathResolver()),
output);
} catch (Depfiles.HeaderVerificationException e) {
throw new HumanReadableException(e);
}
inputs.addAll(preprocessDelegate.get().getInputsAfterBuildingLocally(depFileLines));
}
// If present, include all inputs coming from the compiler tool.
inputs.addAll(compilerDelegate.getInputsAfterBuildingLocally());
if (precompiledHeaderRule.isPresent()) {
CxxPrecompiledHeader pch = precompiledHeaderRule.get();
inputs.addAll(pch.getInputsAfterBuildingLocally(context));
}
// Add the input.
inputs.add(input);
return inputs.build();
}
}