/*
* Copyright 2016-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.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.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.RichStream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.UncheckedExecutionException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
/**
* Rule to generate a precompiled header from an existing header.
*
* <p>Precompiled headers are only useful for compilation style where preprocessing and compiling
* are both done in the same process. If a preprocessed output needs to be serialized and later read
* back in, the entire rationale of using a precompiled header, to avoid parsing excess headers, is
* obviated.
*
* <p>PCH files are not very portable, and so they are not cached.
*
* <ul>
* <li>The compiler verifies that header mtime identical to that recorded in the PCH file.
* <li>PCH file records absolute paths, limited support for "relocatable" pch exists in Clang, but
* it is not very flexible.
* </ul>
*
* <p>While the problems are not impossible to overcome, PCH generation is fast enough that it isn't
* a significant problem. The PCH file is only generated when a source file needs to be compiled,
* anyway.
*
* <p>Additionally, since PCH files contain information like timestamps, absolute paths, and
* (effectively) random unique IDs, they are not amenable to the InputBasedRuleKey optimization when
* used to compile another file.
*/
public class CxxPrecompiledHeader extends AbstractBuildRule
implements SupportsDependencyFileRuleKey, SupportsInputBasedRuleKey {
// Fields that are added to rule key as is.
@AddToRuleKey private final PreprocessorDelegate preprocessorDelegate;
@AddToRuleKey private final CompilerDelegate compilerDelegate;
@AddToRuleKey private final SourcePath input;
@AddToRuleKey private final CxxSource.Type inputType;
// Fields that added to the rule key with some processing.
private final CxxToolFlags compilerFlags;
// Fields that are not added to the rule key.
private final DebugPathSanitizer compilerSanitizer;
private final Path output;
/**
* Cache the loading and processing of the depfile. This data can always be reloaded from disk, so
* only cache it weakly.
*/
private final Cache<BuildContext, ImmutableList<Path>> depFileCache =
CacheBuilder.newBuilder().weakKeys().weakValues().build();
public CxxPrecompiledHeader(
BuildRuleParams buildRuleParams,
Path output,
PreprocessorDelegate preprocessorDelegate,
CompilerDelegate compilerDelegate,
CxxToolFlags compilerFlags,
SourcePath input,
CxxSource.Type inputType,
DebugPathSanitizer compilerSanitizer) {
super(buildRuleParams);
Preconditions.checkArgument(
!inputType.isAssembly(), "Asm files do not use precompiled headers.");
this.preprocessorDelegate = preprocessorDelegate;
this.compilerDelegate = compilerDelegate;
this.compilerFlags = compilerFlags;
this.output = output;
this.input = input;
this.inputType = inputType;
this.compilerSanitizer = compilerSanitizer;
}
@Override
public void appendToRuleKey(RuleKeyObjectSink sink) {
sink.setReflectively("compilationDirectory", compilerSanitizer.getCompilationDirectory());
sink.setReflectively(
"compilerFlagsPlatform", compilerSanitizer.sanitizeFlags(compilerFlags.getPlatformFlags()));
sink.setReflectively(
"compilerFlagsRule", compilerSanitizer.sanitizeFlags(compilerFlags.getRuleFlags()));
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
Path scratchDir =
BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), "%s_tmp");
return new ImmutableList.Builder<Step>()
.add(MkdirStep.of(getProjectFilesystem(), output.getParent()))
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), scratchDir))
.add(makeMainStep(context.getSourcePathResolver(), scratchDir))
.build();
}
public SourcePath getInput() {
return input;
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), output);
}
private Path getSuffixedOutput(SourcePathResolver pathResolver, String suffix) {
return Paths.get(pathResolver.getRelativePath(getSourcePathToOutput()).toString() + suffix);
}
public CxxIncludePaths getCxxIncludePaths() {
return CxxIncludePaths.concat(
RichStream.from(this.getBuildDeps())
.filter(CxxPreprocessAndCompile.class)
.map(CxxPreprocessAndCompile::getPreprocessorDelegate)
.filter(Optional::isPresent)
.map(ppDelegate -> ppDelegate.get().getCxxIncludePaths())
.iterator());
}
@Override
public boolean useDependencyFileRuleKeys() {
return true;
}
@Override
public Predicate<SourcePath> getCoveredByDepFilePredicate() {
return preprocessorDelegate.getCoveredByDepFilePredicate();
}
@Override
public Predicate<SourcePath> getExistenceOfInterestPredicate() {
return (SourcePath path) -> false;
}
@Override
public ImmutableList<SourcePath> getInputsAfterBuildingLocally(BuildContext context)
throws IOException {
try {
return ImmutableList.<SourcePath>builder()
.addAll(preprocessorDelegate.getInputsAfterBuildingLocally(readDepFileLines(context)))
.add(input)
.build();
} catch (Depfiles.HeaderVerificationException e) {
throw new HumanReadableException(e);
}
}
@Override
public boolean isCacheable() {
return false;
}
private Path getDepFilePath(SourcePathResolver pathResolver) {
return getSuffixedOutput(pathResolver, ".dep");
}
public ImmutableList<Path> readDepFileLines(BuildContext context)
throws IOException, Depfiles.HeaderVerificationException {
try {
return depFileCache.get(
context,
() ->
Depfiles.parseAndOutputBuckCompatibleDepfile(
context.getEventBus(),
getProjectFilesystem(),
preprocessorDelegate.getHeaderPathNormalizer(),
preprocessorDelegate.getHeaderVerification(),
getDepFilePath(context.getSourcePathResolver()),
// TODO(10194465): This uses relative path so as to get relative paths in the dep file
context.getSourcePathResolver().getRelativePath(input),
output));
} catch (ExecutionException e) {
// Unwrap and re-throw the loader's Exception.
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
Throwables.throwIfInstanceOf(e.getCause(), Depfiles.HeaderVerificationException.class);
throw new IllegalStateException("Unexpected cause for ExecutionException: ", e);
} catch (UncheckedExecutionException e) {
Throwables.throwIfUnchecked(e.getCause());
throw e;
}
}
@VisibleForTesting
CxxPreprocessAndCompileStep makeMainStep(SourcePathResolver resolver, Path scratchDir) {
return new CxxPreprocessAndCompileStep(
getBuildTarget(),
getProjectFilesystem(),
CxxPreprocessAndCompileStep.Operation.GENERATE_PCH,
resolver.getRelativePath(getSourcePathToOutput()),
Optional.of(getDepFilePath(resolver)),
// TODO(10194465): This uses relative path so as to get relative paths in the dep file
resolver.getRelativePath(input),
inputType,
new CxxPreprocessAndCompileStep.ToolCommand(
preprocessorDelegate.getCommandPrefix(),
ImmutableList.copyOf(
CxxToolFlags.explicitBuilder()
.addAllRuleFlags(
getCxxIncludePaths()
.getFlags(resolver, preprocessorDelegate.getPreprocessor()))
.addAllRuleFlags(
preprocessorDelegate.getArguments(
compilerFlags, /* no pch */ Optional.empty()))
.build()
.getAllFlags()),
preprocessorDelegate.getEnvironment()),
preprocessorDelegate.getHeaderPathNormalizer(),
compilerSanitizer,
scratchDir,
/* useArgFile*/ true,
compilerDelegate.getCompiler());
}
/**
* Helper method for dealing with compiler flags in a precompiled header build.
*
* <p>
*
* <p>Triage the given list of compiler flags, and divert {@code -I} flags' arguments to {@code
* iDirsBuilder}, do similar for {@code -isystem} flags and {@code iSystemDirsBuilder}, and
* finally output other non-include-path related stuff to {@code nonIncludeFlagsBuilder}.
*
* <p>
*
* <p>Note that while Buck doesn't tend to produce {@code -I} and {@code -isystem} flags without a
* space between the flag and its argument, though historically compilers have accepted that.
* We'll accept that here as well, inserting a break between the flag and its parameter.
*
* @param iDirsBuilder a builder which will receive a list of directories provided with the {@code
* -I} option (the flag itself will not be added to this builder)
* @param iSystemDirsBuilder a builder which will receive a list of directories provided with the
* {@code -isystem} option (the flag itself will not be added to this builder)
* @param nonIncludeFlagsBuilder builder that receives all the stuff not outputted to the above.
*/
public static void separateIncludePathArgs(
ImmutableList<String> flags,
ImmutableList.Builder<String> iDirsBuilder,
ImmutableList.Builder<String> iSystemDirsBuilder,
ImmutableList.Builder<String> nonIncludeFlagsBuilder) {
// TODO(steveo): unused?
Iterator<String> it = flags.iterator();
while (it.hasNext()) {
String flag = it.next();
if (flag.equals("-I") && it.hasNext()) {
iDirsBuilder.add(it.next());
} else if (flag.startsWith("-I")) {
iDirsBuilder.add(flag.substring("-I".length()));
} else if (flag.equals("-isystem") && it.hasNext()) {
iSystemDirsBuilder.add(it.next());
} else if (flag.startsWith("-isystem")) {
iSystemDirsBuilder.add(flag.substring("-isystem".length()));
} else {
nonIncludeFlagsBuilder.add(flag);
}
}
}
}