com.facebook.buck.apple.CompilationDatabase.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.apple.CompilationDatabase.java

Source

/*
 * 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.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Flavor;
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.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.base.Strings;
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.io.Files;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Collections;
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.
 */
public class CompilationDatabase extends AbstractBuildRule {

    public static final Flavor COMPILATION_DATABASE = new Flavor("compilation-database");

    private final AppleConfig appleConfig;
    private final TargetSources targetSources;
    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 targetSources The {@link TargetSources#headerPaths} and {@link TargetSources#srcPaths}
     *     will be the entries in the generated compilation database.
     * @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,
            TargetSources targetSources, ImmutableSortedSet<String> frameworks, ImmutableSet<Path> includePaths,
            Optional<SourcePath> pchFile) {
        super(buildRuleParams, resolver);
        this.appleConfig = appleConfig;
        this.targetSources = targetSources;
        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) {
                if (targetSources.headerPaths.isEmpty()) {
                    return 0;
                }

                HeaderMap.Builder builder = HeaderMap.builder();
                ProjectFilesystem projectFilesystem = context.getProjectFilesystem();
                for (SourcePath headerPath : targetSources.headerPaths) {
                    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(targetSources.headerPaths, targetSources.srcPaths));
    }

    @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) {
            BuildTarget target = getBuildTarget();
            List<JsonSerializableDatabaseEntry> entries = Lists.newArrayList();
            Iterable<SourcePath> allSources = Iterables.concat(targetSources.srcPaths, targetSources.headerPaths);
            ProjectFilesystem projectFilesystem = context.getProjectFilesystem();
            for (SourcePath srcPath : allSources) {
                List<String> commandArgs = Lists.newArrayList("clang", "-x", "objective-c",

                        // 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",
                        "-std=gnu99", "-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<String, AppleSdkPaths> allAppleSdkPaths = appleConfig
                        .getAppleSdkPaths(context.getConsole());
                AppleSdkPaths appleSdkPaths = selectNewestSimulatorSdk(allAppleSdkPaths);

                // TODO(mbolin): Make the sysroot configurable.
                commandArgs.add("-isysroot");
                Path sysroot = appleSdkPaths.sdkPath();
                commandArgs.add(sysroot.toString());

                String sdkRoot = appleSdkPaths.sdkPath().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");
                String fileToCompile = projectFilesystem.resolve(getResolver().getPath(srcPath)).toString();
                commandArgs.add(fileToCompile);

                // Currently, perFileFlags is a single string rather than a list, so we concatenate it
                // to the end of the command string without escaping or splitting.
                String perFileFlags = Strings.nullToEmpty(targetSources.perFileFlags.get(srcPath));
                if (!perFileFlags.isEmpty()
                        && FileExtensions.CLANG_SOURCES.contains(Files.getFileExtension(fileToCompile))) {
                    commandArgs.add(perFileFlags);
                }

                String command = Joiner.on(' ').join(commandArgs);
                entries.add(new JsonSerializableDatabaseEntry(
                        /* directory */ projectFilesystem.resolve(target.getBasePath()).toString(), fileToCompile,
                        command));
            }

            return entries;
        }

        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<String, AppleSdkPaths> allAppleSdkPaths) {
        final String prefix = "iphonesimulator";
        List<String> iphoneSimulatorVersions = Lists
                .newArrayList(FluentIterable.from(allAppleSdkPaths.keySet()).filter(new Predicate<String>() {
                    @Override
                    public boolean apply(String sdkName) {
                        return sdkName.startsWith(prefix);
                    }
                }).transform(new Function<String, String>() {
                    @Override
                    public String apply(String sdkName) {
                        return sdkName.substring(prefix.length());
                    }
                }).toSet());
        if (iphoneSimulatorVersions.isEmpty()) {
            throw new RuntimeException("No iphonesimulator found in: " + allAppleSdkPaths.keySet());
        }

        Collections.sort(iphoneSimulatorVersions, new VersionStringComparator());
        String version = iphoneSimulatorVersions.get(iphoneSimulatorVersions.size() - 1);
        return Preconditions.checkNotNull(allAppleSdkPaths.get(prefix + version));
    }

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