blob: 77c7fce9542eeaaa0942c1d907932bc126e02565 [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.apple;
import com.facebook.buck.apple.clang.HeaderMap;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.ImmutableFlavor;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.RuleKey.Builder;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.coercer.SourceWithFlags;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.util.VersionStringComparator;
import com.facebook.infer.annotation.SuppressFieldNotInitialized;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
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 com.google.common.collect.Ordering;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* Build rule that generates a <a href="http://clang.llvm.org/docs/JSONCompilationDatabase.html">
* clang compilation database</a> for an Apple target.
*
* TODO(mbolin): t5879160 Generate the compilation database based on a set of CxxCompile actions
* rather than a TargetSources object so the set of build flags is legit.
*/
public class CompilationDatabase extends AbstractBuildRule {
public static final Flavor COMPILATION_DATABASE = ImmutableFlavor.of("compilation-database");
private final AppleConfig appleConfig;
private final ImmutableSortedSet<SourceWithFlags> sourcesWithFlags;
private final ImmutableSortedSet<SourcePath> publicHeaders;
private final ImmutableSortedSet<SourcePath> privateHeaders;
private final Path outputJsonFile;
private final ImmutableSortedSet<String> frameworks;
private final ImmutableSet<Path> includePaths;
private final Optional<SourcePath> pchFile;
/**
* @param buildRuleParams As needed by superclass constructor.
* @param resolver As needed by superclass constructor.
* @param sourcesWithFlags Target's sources and their per-file flags.
* @param publicHeaders Target's headers that are visible by dependent targets.
* @param privateHeaders Target's headers that are only visible in the target itself.
* @param frameworks Paths to frameworks to link against. Each may start with {@code "$SDKROOT"},
* in which case the appropriate path will be substituted.
* @param includePaths Paths that should be passed as clang args with {@code -I}.
* @param pchFile If specified, including as a {@code -include} clang arg.
*/
CompilationDatabase(
BuildRuleParams buildRuleParams,
SourcePathResolver resolver,
AppleConfig appleConfig,
ImmutableSortedSet<SourceWithFlags> sourcesWithFlags,
ImmutableSortedSet<SourcePath> publicHeaders,
ImmutableSortedSet<SourcePath> privateHeaders,
ImmutableSortedSet<String> frameworks,
ImmutableSet<Path> includePaths,
Optional<SourcePath> pchFile) {
super(buildRuleParams, resolver);
this.appleConfig = appleConfig;
this.sourcesWithFlags = sourcesWithFlags;
this.publicHeaders = publicHeaders;
this.privateHeaders = privateHeaders;
this.outputJsonFile = BuildTargets.getGenPath(
buildRuleParams.getBuildTarget(),
"__%s_compilation_database.json");
this.frameworks = frameworks;
this.includePaths = includePaths;
this.pchFile = pchFile;
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context,
BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
// If set, this header map will be passed via -iquote to clang. In practice, this has been seen
// to be necessary when the .pch uses quoted imports for headers that exist in a subdirectory of
// the project, such as Categories.
final AtomicReference<Path> internalHeaderMap = new AtomicReference<>();
final Path headerMapPath = BuildTargets.getBinPath(getBuildTarget(), "__my_%s__.hmap");
steps.add(new MkdirStep(headerMapPath.getParent()));
steps.add(new AbstractExecutionStep("generate_internal_header_map") {
@Override
public int execute(ExecutionContext context) {
Iterable<SourcePath> allHeaderPaths = Iterables.concat(publicHeaders, privateHeaders);
if (Iterables.isEmpty(allHeaderPaths)) {
return 0;
}
HeaderMap.Builder builder = HeaderMap.builder();
ProjectFilesystem projectFilesystem = context.getProjectFilesystem();
for (SourcePath headerPath : allHeaderPaths) {
Path relativePath = getResolver().getPath(headerPath);
Path absolutePath = projectFilesystem.resolve(relativePath);
builder.add(relativePath.getFileName().toString(), absolutePath);
}
HeaderMap headerMap = builder.build();
try {
projectFilesystem.writeBytesToPath(
headerMap.getBytes(),
headerMapPath);
} catch (IOException e) {
context.logError(e, "Failed to write header map: %s.", headerMapPath);
return 1;
}
internalHeaderMap.set(headerMapPath);
return 0;
}
});
steps.add(new MkdirStep(getPathToOutputFile().getParent()));
steps.add(new GenerateCompilationCommandsJson(internalHeaderMap));
return steps.build();
}
@Override
public Path getPathToOutputFile() {
return outputJsonFile;
}
@Override
protected ImmutableCollection<Path> getInputsToCompareToOutput() {
return getResolver().filterInputsToCompareToOutput(
Iterables.concat(
Iterables.transform(
sourcesWithFlags,
SourceWithFlags.TO_SOURCE_PATH),
publicHeaders,
privateHeaders));
}
@Override
protected Builder appendDetailsToRuleKey(Builder builder) {
// TODO(mbolin): If this contains absolute paths, need to add information to the builder that
// makes it specific to the developer's machine and root directory.
// Also need to include all of the information from the other fields.
return builder;
}
class GenerateCompilationCommandsJson extends AbstractExecutionStep {
private final AtomicReference<Path> internalHeaderMap;
public GenerateCompilationCommandsJson(AtomicReference<Path> internalHeaderMap) {
super("generate compile_commands.json");
this.internalHeaderMap = internalHeaderMap;
}
@Override
public int execute(ExecutionContext context) {
Iterable<JsonSerializableDatabaseEntry> entries = createEntries(context);
return writeOutput(entries, context);
}
@VisibleForTesting
Iterable<JsonSerializableDatabaseEntry> createEntries(ExecutionContext context) {
List<JsonSerializableDatabaseEntry> entries = Lists.newArrayList();
for (SourceWithFlags sourceWithFlags : sourcesWithFlags) {
entries.add(
createEntry(
context,
sourceWithFlags.getSourcePath(),
sourceWithFlags.getFlags()));
}
Iterable<SourcePath> allHeaderPaths = Iterables.concat(
publicHeaders,
privateHeaders);
for (SourcePath headerPath : allHeaderPaths) {
entries.add(
createEntry(
context,
headerPath,
/* flags */ ImmutableList.<String>of()));
}
return entries;
}
private JsonSerializableDatabaseEntry createEntry(
ExecutionContext context,
SourcePath sourcePath,
List<String> perFileFlags) {
ProjectFilesystem projectFilesystem = context.getProjectFilesystem();
String fileToCompile = projectFilesystem.resolve(getResolver().getPath(sourcePath))
.toString();
String language;
String languageStandard;
if (fileToCompile.endsWith(".mm")) {
language = "objective-c++";
languageStandard = "-std=c++11";
} else {
language = "objective-c";
languageStandard = "-std=gnu99";
}
List<String> commandArgs = Lists.newArrayList(
"clang",
"-x",
language,
// TODO(mbolin): Simulator arguments should be configurable (and should likely be
// derived from the PlatformFlavor).
"-arch",
"i386",
"-mios-simulator-version-min=7.0",
"-fmessage-length=0",
"-fdiagnostics-show-note-include-stack",
"-fmacro-backtrace-limit=0",
languageStandard,
"-fpascal-strings",
"-fexceptions",
"-fasm-blocks",
"-fstrict-aliasing",
"-fobjc-abi-version=2",
"-fobjc-legacy-dispatch",
"-O0", // No optimizations.
// TODO(mbolin): Include all of the -W and -D flags.
// TODO(mbolin): Support -MMD, -MT, -MF. Requires -o to trigger it.
"-g", // Generate source level debug information.
"-MMD" // Write a depfile containing user headers.
);
// TODO(mbolin): Determine whether -fno-objc-arc should be used instead.
commandArgs.add("-fobjc-arc");
// Result of `xcode-select --print-path`.
ImmutableMap<AppleSdk, AppleSdkPaths> allAppleSdkPaths = appleConfig.getAppleSdkPaths(
context.getProcessExecutor());
AppleSdkPaths appleSdkPaths = selectNewestSimulatorSdk(allAppleSdkPaths);
// TODO(mbolin): Make the sysroot configurable.
commandArgs.add("-isysroot");
Path sysroot = appleSdkPaths.getSdkPath();
commandArgs.add(sysroot.toString());
String sdkRoot = appleSdkPaths.getSdkPath().toString();
for (String framework : frameworks) {
// TODO(mbolin): Other placeholders are possible, but do not appear to be used yet.
// Specifically, PBXReference.SourceTree#fromBuildSetting() seems to have more
// flexible parsing. We should figure out how to refactor that could so it can be used
// here.
framework = framework.replace("$SDKROOT", sdkRoot);
commandArgs.add("-F" + framework);
}
// Add -I and -iquote flags, as appropriate.
for (Path includePath : includePaths) {
commandArgs.add("-I" + projectFilesystem.resolve(includePath));
}
Path iquoteArg = internalHeaderMap.get();
if (iquoteArg != null) {
commandArgs.add("-iquote");
commandArgs.add(projectFilesystem.resolve(iquoteArg).toString());
}
if (pchFile.isPresent()) {
commandArgs.add("-include");
Path relativePathToPchFile = getResolver().getPath(pchFile.get());
commandArgs.add(projectFilesystem.resolve(relativePathToPchFile).toString());
}
commandArgs.add("-c");
commandArgs.add(fileToCompile);
commandArgs.addAll(perFileFlags);
String command = Joiner.on(' ').join(commandArgs);
return new JsonSerializableDatabaseEntry(
/* directory */ projectFilesystem.resolve(getBuildTarget().getBasePath()).toString(),
fileToCompile,
command);
}
private int writeOutput(
Iterable<JsonSerializableDatabaseEntry> entries,
ExecutionContext context) {
ObjectMapper mapper = new ObjectMapper();
try {
OutputStream outputStream = context.getProjectFilesystem().newFileOutputStream(
getPathToOutputFile());
mapper.writeValue(outputStream, entries);
} catch (IOException e) {
logError(e, context);
return 1;
}
return 0;
}
private void logError(Throwable throwable, ExecutionContext context) {
context.logError(
throwable,
"Failed writing to %s in %s.",
getPathToOutputFile(),
getBuildTarget());
}
}
// TODO(mbolin): This method should go away when the sdkName becomes a flavor.
static AppleSdkPaths selectNewestSimulatorSdk(
ImmutableMap<AppleSdk, AppleSdkPaths> allAppleSdkPaths) {
Ordering<AppleSdk> appleSdkVersionComparator =
Ordering
.from(new VersionStringComparator())
.onResultOf(new Function<AppleSdk, String>() {
@Override
public String apply(AppleSdk appleSdk) {
return appleSdk.getVersion();
}
});
ImmutableSortedSet<AppleSdk> sortedIphoneSimulatorSdks = FluentIterable
.from(allAppleSdkPaths.keySet())
.filter(new Predicate<AppleSdk>() {
@Override
public boolean apply(AppleSdk sdk) {
return sdk.getApplePlatform() == ApplePlatform.IPHONESIMULATOR;
}
})
.toSortedSet(appleSdkVersionComparator);
if (sortedIphoneSimulatorSdks.isEmpty()) {
throw new RuntimeException("No iphonesimulator found in: " + allAppleSdkPaths.keySet());
}
return Preconditions.checkNotNull(allAppleSdkPaths.get(sortedIphoneSimulatorSdks.last()));
}
@VisibleForTesting
@SuppressFieldNotInitialized
static class JsonSerializableDatabaseEntry {
public String directory;
public String file;
public String command;
/** Empty constructor will be used by Jackson. */
public JsonSerializableDatabaseEntry() {}
public JsonSerializableDatabaseEntry(String directory, String file, String command) {
this.directory = directory;
this.file = file;
this.command = command;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof JsonSerializableDatabaseEntry)) {
return false;
}
JsonSerializableDatabaseEntry that = (JsonSerializableDatabaseEntry) obj;
return Objects.equal(this.directory, that.directory) &&
Objects.equal(this.file, that.file) &&
Objects.equal(this.command, that.command);
}
@Override
public int hashCode() {
return Objects.hashCode(directory, file, command);
}
// Useful if CompilationDatabaseTest fails when comparing JsonSerializableDatabaseEntry objects.
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("directory", directory)
.add("file", file)
.add("command", command)
.toString();
}
}
}