| /* |
| * 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(); |
| } |
| } |
| } |