com.facebook.buck.features.apple.project.XCodeProjectCommandHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.features.apple.project.XCodeProjectCommandHelper.java

Source

/*
 * Copyright 2017-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.features.apple.project;

import com.facebook.buck.apple.AppleBinaryDescription;
import com.facebook.buck.apple.AppleBundleDescription;
import com.facebook.buck.apple.AppleConfig;
import com.facebook.buck.apple.AppleLibraryDescription;
import com.facebook.buck.apple.XCodeDescriptions;
import com.facebook.buck.apple.XCodeDescriptionsFactory;
import com.facebook.buck.cli.ProjectTestsMode;
import com.facebook.buck.command.config.BuildBuckConfig;
import com.facebook.buck.core.cell.Cell;
import com.facebook.buck.core.cell.CellProvider;
import com.facebook.buck.core.config.BuckConfig;
import com.facebook.buck.core.description.BaseDescription;
import com.facebook.buck.core.exceptions.HumanReadableException;
import com.facebook.buck.core.model.BuildTarget;
import com.facebook.buck.core.model.Flavor;
import com.facebook.buck.core.model.TargetConfiguration;
import com.facebook.buck.core.model.UnflavoredBuildTarget;
import com.facebook.buck.core.model.impl.ImmutableUnflavoredBuildTarget;
import com.facebook.buck.core.model.targetgraph.NoSuchTargetException;
import com.facebook.buck.core.model.targetgraph.TargetGraph;
import com.facebook.buck.core.model.targetgraph.TargetNode;
import com.facebook.buck.core.model.targetgraph.impl.TargetGraphAndTargets;
import com.facebook.buck.core.parser.buildtargetparser.UnconfiguredBuildTargetFactory;
import com.facebook.buck.core.rules.ActionGraphBuilder;
import com.facebook.buck.core.rules.resolver.impl.SingleThreadedActionGraphBuilder;
import com.facebook.buck.core.rules.transformer.impl.DefaultTargetNodeToBuildRuleTransformer;
import com.facebook.buck.core.util.graph.AcyclicDepthFirstPostOrderTraversal;
import com.facebook.buck.core.util.graph.AcyclicDepthFirstPostOrderTraversal.CycleException;
import com.facebook.buck.core.util.log.Logger;
import com.facebook.buck.cxx.toolchain.CxxBuckConfig;
import com.facebook.buck.cxx.toolchain.CxxPlatform;
import com.facebook.buck.cxx.toolchain.CxxPlatformsProvider;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.features.halide.HalideBuckConfig;
import com.facebook.buck.parser.BuildFileSpec;
import com.facebook.buck.parser.ImmutableTargetNodePredicateSpec;
import com.facebook.buck.parser.Parser;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.parser.ParsingContext;
import com.facebook.buck.parser.SpeculativeParsing;
import com.facebook.buck.parser.TargetNodeSpec;
import com.facebook.buck.parser.exceptions.BuildFileParseException;
import com.facebook.buck.parser.exceptions.NoSuchBuildTargetException;
import com.facebook.buck.rules.coercer.TypeCoercerFactory;
import com.facebook.buck.rules.keys.config.RuleKeyConfiguration;
import com.facebook.buck.swift.SwiftBuckConfig;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.ExitCode;
import com.facebook.buck.util.MoreExceptions;
import com.facebook.buck.util.ProcessManager;
import com.facebook.buck.util.RichStream;
import com.facebook.buck.util.config.Configs;
import com.facebook.buck.versions.InstrumentedVersionedTargetGraphCache;
import com.facebook.buck.versions.VersionException;
import com.facebook.buck.versions.VersionedTargetGraphAndTargets;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.concurrent.ThreadSafe;
import org.pf4j.PluginManager;

public class XCodeProjectCommandHelper {

    private static final Logger LOG = Logger.get(XCodeProjectCommandHelper.class);

    private static final String XCODE_PROCESS_NAME = "Xcode";

    private final BuckEventBus buckEventBus;
    private final PluginManager pluginManager;
    private final Parser parser;
    private final BuckConfig buckConfig;
    private final InstrumentedVersionedTargetGraphCache versionedTargetGraphCache;
    private final TypeCoercerFactory typeCoercerFactory;
    private final UnconfiguredBuildTargetFactory unconfiguredBuildTargetFactory;
    private final Cell cell;
    private final TargetConfiguration targetConfiguration;
    private final ImmutableSet<Flavor> appleCxxFlavors;
    private final RuleKeyConfiguration ruleKeyConfiguration;
    private final Console console;
    private final Optional<ProcessManager> processManager;
    private final ImmutableMap<String, String> environment;
    private final ListeningExecutorService executorService;
    private final List<String> arguments;
    private final boolean absoluteHeaderMapPaths;
    private final boolean sharedLibrariesInBundles;
    private final boolean withTests;
    private final boolean withoutTests;
    private final boolean withoutDependenciesTests;
    private final String modulesToFocusOn;
    private final boolean combinedProject;
    private final boolean createProjectSchemes;
    private final boolean dryRun;
    private final boolean readOnly;
    private final PathOutputPresenter outputPresenter;
    private final ParsingContext parsingContext;

    private final Function<Iterable<String>, ImmutableList<TargetNodeSpec>> argsParser;
    private final Function<ImmutableList<String>, ExitCode> buildRunner;

    public XCodeProjectCommandHelper(BuckEventBus buckEventBus, PluginManager pluginManager, Parser parser,
            BuckConfig buckConfig, InstrumentedVersionedTargetGraphCache versionedTargetGraphCache,
            TypeCoercerFactory typeCoercerFactory, UnconfiguredBuildTargetFactory unconfiguredBuildTargetFactory,
            Cell cell, RuleKeyConfiguration ruleKeyConfiguration, TargetConfiguration targetConfiguration,
            Console console, Optional<ProcessManager> processManager, ImmutableMap<String, String> environment,
            ListeningExecutorService executorService, ListeningExecutorService parsingExecutorService,
            List<String> arguments, ImmutableSet<Flavor> appleCxxFlavors, boolean absoluteHeaderMapPaths,
            boolean sharedLibrariesInBundles, boolean enableParserProfiling, boolean withTests,
            boolean withoutTests, boolean withoutDependenciesTests, String modulesToFocusOn,
            boolean combinedProject, boolean createProjectSchemes, boolean dryRun, boolean readOnly,
            PathOutputPresenter outputPresenter,
            Function<Iterable<String>, ImmutableList<TargetNodeSpec>> argsParser,
            Function<ImmutableList<String>, ExitCode> buildRunner) {
        this.buckEventBus = buckEventBus;
        this.pluginManager = pluginManager;
        this.parser = parser;
        this.buckConfig = buckConfig;
        this.versionedTargetGraphCache = versionedTargetGraphCache;
        this.typeCoercerFactory = typeCoercerFactory;
        this.unconfiguredBuildTargetFactory = unconfiguredBuildTargetFactory;
        this.cell = cell;
        this.targetConfiguration = targetConfiguration;
        this.appleCxxFlavors = appleCxxFlavors;
        this.ruleKeyConfiguration = ruleKeyConfiguration;
        this.console = console;
        this.processManager = processManager;
        this.environment = environment;
        this.executorService = executorService;
        this.arguments = arguments;
        this.absoluteHeaderMapPaths = absoluteHeaderMapPaths;
        this.sharedLibrariesInBundles = sharedLibrariesInBundles;
        this.withTests = withTests;
        this.withoutTests = withoutTests;
        this.withoutDependenciesTests = withoutDependenciesTests;
        this.modulesToFocusOn = modulesToFocusOn;
        this.combinedProject = combinedProject;
        this.createProjectSchemes = createProjectSchemes;
        this.dryRun = dryRun;
        this.readOnly = readOnly;
        this.outputPresenter = outputPresenter;
        this.argsParser = argsParser;
        this.buildRunner = buildRunner;
        this.parsingContext = ParsingContext.builder(cell, parsingExecutorService)
                .setProfilingEnabled(enableParserProfiling).setSpeculativeParsing(SpeculativeParsing.ENABLED)
                .setApplyDefaultFlavorsMode(buckConfig.getView(ParserConfig.class).getDefaultFlavorsMode()).build();
    }

    public ExitCode parseTargetsAndRunXCodeGenerator() throws IOException, InterruptedException {
        ImmutableSet<BuildTarget> passedInTargetsSet;
        TargetGraph projectGraph;

        LOG.debug("Xcode project generation: Getting the target graph");

        try {
            passedInTargetsSet = ImmutableSet.copyOf(Iterables.concat(
                    parser.resolveTargetSpecs(parsingContext, argsParser.apply(arguments), targetConfiguration)));
            projectGraph = getProjectGraphForIde(passedInTargetsSet);
        } catch (BuildFileParseException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return ExitCode.PARSE_ERROR;
        } catch (HumanReadableException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return ExitCode.BUILD_ERROR;
        }

        LOG.debug("Xcode project generation: Killing existing Xcode if needed");

        checkForAndKillXcodeIfRunning(getIDEForceKill(buckConfig));

        LOG.debug("Xcode project generation: Computing graph roots");

        ImmutableSet<BuildTarget> graphRoots;
        if (passedInTargetsSet.isEmpty()) {
            graphRoots = getRootsFromPredicate(projectGraph,
                    node -> node.getDescription() instanceof XcodeWorkspaceConfigDescription);
        } else {
            graphRoots = passedInTargetsSet;
        }

        LOG.debug("Xcode project generation: Getting more part of the target graph");

        TargetGraphAndTargets targetGraphAndTargets;
        try {
            targetGraphAndTargets = createTargetGraph(projectGraph, graphRoots, isWithTests(buckConfig),
                    isWithDependenciesTests(buckConfig), passedInTargetsSet.isEmpty());
        } catch (BuildFileParseException | NoSuchTargetException | VersionException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return ExitCode.PARSE_ERROR;
        } catch (HumanReadableException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return ExitCode.BUILD_ERROR;
        }

        Optional<ImmutableMap<BuildTarget, TargetNode<?>>> sharedLibraryToBundle = Optional.empty();

        if (sharedLibrariesInBundles) {
            sharedLibraryToBundle = Optional.of(ProjectGenerator.computeSharedLibrariesToBundles(
                    targetGraphAndTargets.getTargetGraph().getNodes(), targetGraphAndTargets));
        }

        if (dryRun) {
            for (TargetNode<?> targetNode : targetGraphAndTargets.getTargetGraph().getNodes()) {
                console.getStdOut().println(targetNode.toString());
            }

            return ExitCode.SUCCESS;
        }

        LOG.debug("Xcode project generation: Run the project generator");

        return runXcodeProjectGenerator(targetGraphAndTargets, passedInTargetsSet, sharedLibraryToBundle);
    }

    private static String getIDEForceKillSectionName() {
        return "project";
    }

    private static String getIDEForceKillFieldName() {
        return "ide_force_kill";
    }

    private IDEForceKill getIDEForceKill(BuckConfig buckConfig) {
        Optional<IDEForceKill> forceKill = buckConfig.getEnum(getIDEForceKillSectionName(),
                getIDEForceKillFieldName(), IDEForceKill.class);
        if (forceKill.isPresent()) {
            return forceKill.get();
        }

        // Support legacy config if new key is missing.
        Optional<Boolean> legacyPrompty = buckConfig.getBoolean("project", "ide_prompt");
        if (legacyPrompty.isPresent()) {
            return legacyPrompty.get().booleanValue() ? IDEForceKill.PROMPT : IDEForceKill.NEVER;
        }

        return IDEForceKill.PROMPT;
    }

    private ProjectTestsMode getXcodeProjectTestsMode(BuckConfig buckConfig) {
        return buckConfig.getEnum("project", "xcode_project_tests_mode", ProjectTestsMode.class)
                .orElse(ProjectTestsMode.WITH_TESTS);
    }

    private ProjectTestsMode testsMode(BuckConfig buckConfig) {
        ProjectTestsMode parameterMode = getXcodeProjectTestsMode(buckConfig);

        if (withoutTests) {
            parameterMode = ProjectTestsMode.WITHOUT_TESTS;
        } else if (withoutDependenciesTests) {
            parameterMode = ProjectTestsMode.WITHOUT_DEPENDENCIES_TESTS;
        } else if (withTests) {
            parameterMode = ProjectTestsMode.WITH_TESTS;
        }

        return parameterMode;
    }

    private boolean isWithTests(BuckConfig buckConfig) {
        return testsMode(buckConfig) != ProjectTestsMode.WITHOUT_TESTS;
    }

    private boolean isWithDependenciesTests(BuckConfig buckConfig) {
        return testsMode(buckConfig) == ProjectTestsMode.WITH_TESTS;
    }

    /** Run xcode specific project generation actions. */
    private ExitCode runXcodeProjectGenerator(TargetGraphAndTargets targetGraphAndTargets,
            ImmutableSet<BuildTarget> passedInTargetsSet,
            Optional<ImmutableMap<BuildTarget, TargetNode<?>>> sharedLibraryToBundle)
            throws IOException, InterruptedException {
        ExitCode exitCode = ExitCode.SUCCESS;
        AppleConfig appleConfig = buckConfig.getView(AppleConfig.class);
        ProjectGeneratorOptions options = ProjectGeneratorOptions.builder().setShouldGenerateReadOnlyFiles(readOnly)
                .setShouldIncludeTests(isWithTests(buckConfig))
                .setShouldIncludeDependenciesTests(isWithDependenciesTests(buckConfig))
                .setShouldUseHeaderMaps(appleConfig.shouldUseHeaderMapsInXcodeProject())
                .setShouldUseAbsoluteHeaderMapPaths(absoluteHeaderMapPaths)
                .setShouldMergeHeaderMaps(appleConfig.shouldMergeHeaderMapsInXcodeProject())
                .setShouldAddLinkedLibrariesAsFlags(appleConfig.shouldAddLinkedLibrariesAsFlags())
                .setShouldForceLoadLinkWholeLibraries(appleConfig.shouldAddLinkerFlagsForLinkWholeLibraries())
                .setShouldGenerateHeaderSymlinkTreesOnly(appleConfig.shouldGenerateHeaderSymlinkTreesOnly())
                .setShouldGenerateMissingUmbrellaHeader(appleConfig.shouldGenerateMissingUmbrellaHeaders())
                .setShouldUseShortNamesForTargets(true).setShouldCreateDirectoryStructure(combinedProject)
                .setShouldGenerateProjectSchemes(createProjectSchemes).build();

        LOG.debug("Xcode project generation: Generates workspaces for targets");

        ImmutableSet<BuildTarget> requiredBuildTargets = generateWorkspacesForTargets(buckEventBus, pluginManager,
                cell, buckConfig, ruleKeyConfiguration, executorService, targetGraphAndTargets, passedInTargetsSet,
                options, appleCxxFlavors, getFocusModules(), new HashMap<>(), combinedProject, outputPresenter,
                sharedLibraryToBundle);
        if (!requiredBuildTargets.isEmpty()) {
            ImmutableMultimap<Path, String> cellPathToCellName = cell.getCellPathResolver().getCellPaths()
                    .asMultimap().inverse();
            ImmutableList<String> arguments = RichStream.from(requiredBuildTargets).map(target -> {
                if (!target.getCellPath().equals(cell.getRoot())) {
                    Optional<String> cellName = cellPathToCellName.get(target.getCellPath()).stream().findAny();
                    if (cellName.isPresent()) {
                        return target.withUnflavoredBuildTarget(ImmutableUnflavoredBuildTarget
                                .of(target.getCellPath(), cellName, target.getBaseName(), target.getShortName()));
                    } else {
                        throw new IllegalStateException(
                                "Failed to find cell name for cell path while constructing parameters to "
                                        + "build dependencies for project generation. " + "Build target: " + target
                                        + " cell path: " + target.getCellPath());
                    }
                } else {
                    return target;
                }
            }).map(Object::toString).toImmutableList();
            exitCode = buildRunner.apply(arguments);
        }
        return exitCode;
    }

    @VisibleForTesting
    static ImmutableSet<BuildTarget> generateWorkspacesForTargets(BuckEventBus buckEventBus,
            PluginManager pluginManager, Cell cell, BuckConfig buckConfig,
            RuleKeyConfiguration ruleKeyConfiguration, ListeningExecutorService executorService,
            TargetGraphAndTargets targetGraphAndTargets, ImmutableSet<BuildTarget> passedInTargetsSet,
            ProjectGeneratorOptions options, ImmutableSet<Flavor> appleCxxFlavors,
            FocusedModuleTargetMatcher focusModules, Map<Path, ProjectGenerator> projectGenerators,
            boolean combinedProject, PathOutputPresenter presenter,
            Optional<ImmutableMap<BuildTarget, TargetNode<?>>> sharedLibraryToBundle)
            throws IOException, InterruptedException {
        ImmutableSet<BuildTarget> targets;
        if (passedInTargetsSet.isEmpty()) {
            targets = targetGraphAndTargets.getProjectRoots().stream().map(TargetNode::getBuildTarget)
                    .collect(ImmutableSet.toImmutableSet());
        } else {
            targets = passedInTargetsSet;
        }

        LazyActionGraph lazyActionGraph = new LazyActionGraph(targetGraphAndTargets.getTargetGraph(),
                cell.getCellProvider());

        XCodeDescriptions xcodeDescriptions = XCodeDescriptionsFactory.create(pluginManager);

        LOG.debug("Generating workspace for config targets %s", targets);
        ImmutableSet.Builder<BuildTarget> requiredBuildTargetsBuilder = ImmutableSet.builder();
        for (BuildTarget inputTarget : targets) {
            TargetNode<?> inputNode = targetGraphAndTargets.getTargetGraph().get(inputTarget);
            XcodeWorkspaceConfigDescriptionArg workspaceArgs;
            if (inputNode.getDescription() instanceof XcodeWorkspaceConfigDescription) {
                TargetNode<XcodeWorkspaceConfigDescriptionArg> castedWorkspaceNode = castToXcodeWorkspaceTargetNode(
                        inputNode);
                workspaceArgs = castedWorkspaceNode.getConstructorArg();
            } else if (canGenerateImplicitWorkspaceForDescription(inputNode.getDescription())) {
                workspaceArgs = createImplicitWorkspaceArgs(inputNode);
            } else {
                throw new HumanReadableException(
                        "%s must be a xcode_workspace_config, apple_binary, apple_bundle, or apple_library",
                        inputNode);
            }

            AppleConfig appleConfig = buckConfig.getView(AppleConfig.class);
            HalideBuckConfig halideBuckConfig = new HalideBuckConfig(buckConfig);
            CxxBuckConfig cxxBuckConfig = new CxxBuckConfig(buckConfig);
            SwiftBuckConfig swiftBuckConfig = new SwiftBuckConfig(buckConfig);

            CxxPlatformsProvider cxxPlatformsProvider = cell.getToolchainProvider()
                    .getByName(CxxPlatformsProvider.DEFAULT_NAME, CxxPlatformsProvider.class);

            CxxPlatform defaultCxxPlatform = cxxPlatformsProvider.getDefaultUnresolvedCxxPlatform()
                    .getLegacyTotallyUnsafe();
            Cell workspaceCell = cell.getCell(inputTarget);
            WorkspaceAndProjectGenerator generator = new WorkspaceAndProjectGenerator(xcodeDescriptions,
                    workspaceCell, targetGraphAndTargets.getTargetGraph(), workspaceArgs, inputTarget, options,
                    combinedProject, focusModules, !appleConfig.getXcodeDisableParallelizeBuild(),
                    defaultCxxPlatform, appleCxxFlavors, buckConfig.getView(ParserConfig.class).getBuildFileName(),
                    lazyActionGraph::getActionGraphBuilderWhileRequiringSubgraph, buckEventBus,
                    ruleKeyConfiguration, halideBuckConfig, cxxBuckConfig, appleConfig, swiftBuckConfig,
                    sharedLibraryToBundle);
            Objects.requireNonNull(executorService, "CommandRunnerParams does not have executor for PROJECT pool");
            Path outputPath = generator.generateWorkspaceAndDependentProjects(projectGenerators, executorService);

            ImmutableSet<BuildTarget> requiredBuildTargetsForWorkspace = generator.getRequiredBuildTargets();
            LOG.debug("Required build targets for workspace %s: %s", inputTarget, requiredBuildTargetsForWorkspace);
            requiredBuildTargetsBuilder.addAll(requiredBuildTargetsForWorkspace);

            Path absolutePath = workspaceCell.getFilesystem().resolve(outputPath);
            Path relativePath = cell.getFilesystem().relativize(absolutePath);
            presenter.present(inputTarget.getFullyQualifiedName(), relativePath);
        }

        return requiredBuildTargetsBuilder.build();
    }

    private FocusedModuleTargetMatcher getFocusModules() throws IOException, InterruptedException {
        if (modulesToFocusOn == null) {
            return FocusedModuleTargetMatcher.noFocus();
        }

        Iterable<String> patterns = Splitter.onPattern("\\s+").split(modulesToFocusOn);
        // Parse patterns with the following syntax:
        // https://buckbuild.com/concept/build_target_pattern.html
        ImmutableList<TargetNodeSpec> specs = argsParser.apply(patterns);

        // Resolve the list of targets matching the patterns.
        ImmutableSet<BuildTarget> passedInTargetsSet;
        try {
            passedInTargetsSet = parser
                    .resolveTargetSpecs(parsingContext.withSpeculativeParsing(SpeculativeParsing.DISABLED), specs,
                            targetConfiguration)
                    .stream().flatMap(Collection::stream).collect(ImmutableSet.toImmutableSet());
        } catch (HumanReadableException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return FocusedModuleTargetMatcher.noFocus();
        }
        LOG.debug("Selected targets: %s", passedInTargetsSet.toString());

        ImmutableSet<UnflavoredBuildTarget> passedInUnflavoredTargetsSet = RichStream.from(passedInTargetsSet)
                .map(BuildTarget::getUnflavoredBuildTarget).toImmutableSet();
        LOG.debug("Selected unflavored targets: %s", passedInUnflavoredTargetsSet.toString());
        return FocusedModuleTargetMatcher.focusedOn(passedInUnflavoredTargetsSet);
    }

    @SuppressWarnings(value = "unchecked")
    private static TargetNode<XcodeWorkspaceConfigDescriptionArg> castToXcodeWorkspaceTargetNode(
            TargetNode<?> targetNode) {
        Preconditions.checkArgument(targetNode.getDescription() instanceof XcodeWorkspaceConfigDescription);
        return (TargetNode<XcodeWorkspaceConfigDescriptionArg>) targetNode;
    }

    private void checkForAndKillXcodeIfRunning(IDEForceKill forceKill) throws InterruptedException, IOException {
        if (forceKill == IDEForceKill.NEVER) {
            // We don't even check if Xcode is running because pkill can hang.
            LOG.debug("Prompt to kill Xcode is disabled");
            return;
        }

        if (!processManager.isPresent()) {
            LOG.warn("Could not check if Xcode is running (no process manager)");
            return;
        }

        if (!processManager.get().isProcessRunning(XCODE_PROCESS_NAME)) {
            LOG.debug("Xcode is not running.");
            return;
        }

        switch (forceKill) {
        case PROMPT: {
            boolean canPromptResult = canPrompt(environment);
            if (canPromptResult) {
                if (prompt("Xcode is currently running. Buck will modify files Xcode currently has "
                        + "open, which can cause it to become unstable.\n\n" + "Kill Xcode and continue?")) {
                    processManager.get().killProcess(XCODE_PROCESS_NAME);
                } else {
                    console.getStdOut()
                            .println(console.getAnsi().asWarningText(
                                    "Xcode is running. Generated projects might be lost or corrupted if Xcode "
                                            + "currently has them open."));
                }
                console.getStdOut().format(
                        "To disable this prompt in the future, add the following to %s: \n\n" + "[%s]\n"
                                + "  %s = %s\n\n" + "If you would like to always kill Xcode, use '%s'.\n",
                        cell.getFilesystem().getRootPath().resolve(Configs.DEFAULT_BUCK_CONFIG_OVERRIDE_FILE_NAME)
                                .toAbsolutePath(),
                        getIDEForceKillSectionName(), getIDEForceKillFieldName(),
                        IDEForceKill.NEVER.toString().toLowerCase(), IDEForceKill.ALWAYS.toString().toLowerCase());
            } else {
                LOG.debug("Xcode is running, but cannot prompt to kill it (force kill %s, can prompt %s)",
                        forceKill.toString(), canPromptResult);
            }
            break;
        }
        case ALWAYS: {
            LOG.debug("Will try to force kill Xcode without prompting...");
            processManager.get().killProcess(XCODE_PROCESS_NAME);
            console.getStdOut().println(console.getAnsi().asWarningText("Xcode was force killed."));
            break;
        }
        case NEVER:
            break;
        }
    }

    private boolean canPrompt(ImmutableMap<String, String> environment) {
        String nailgunStdinTty = environment.get("NAILGUN_TTY_0");
        if (nailgunStdinTty != null) {
            return nailgunStdinTty.equals("1");
        } else {
            return System.console() != null;
        }
    }

    private boolean prompt(String prompt) throws IOException {
        Preconditions.checkState(canPrompt(environment));

        LOG.debug("Displaying prompt %s..", prompt);
        console.getStdOut().print(console.getAnsi().asWarningText(prompt + " [Y/n] "));

        // Do not close readers! Otherwise they close System.in in turn
        // Another design may be to provide reader in the context, to use instead of System.in
        BufferedReader bufferedStdinReader = new BufferedReader(new InputStreamReader(System.in, Charsets.UTF_8));
        Optional<String> result = Optional.ofNullable(bufferedStdinReader.readLine());

        LOG.debug("Result of prompt: [%s]", result.orElse(""));
        return result.isPresent()
                && (result.get().isEmpty() || result.get().toLowerCase(Locale.US).startsWith("y"));
    }

    @VisibleForTesting
    static ImmutableSet<BuildTarget> getRootsFromPredicate(TargetGraph projectGraph,
            Predicate<TargetNode<?>> rootsPredicate) {
        return projectGraph.getNodes().stream().filter(rootsPredicate).map(TargetNode::getBuildTarget)
                .collect(ImmutableSet.toImmutableSet());
    }

    private TargetGraph getProjectGraphForIde(ImmutableSet<BuildTarget> passedInTargets)
            throws InterruptedException, BuildFileParseException, IOException {

        if (passedInTargets.isEmpty()) {
            return parser.buildTargetGraphWithConfigurationTargets(parsingContext,
                    ImmutableList.of(ImmutableTargetNodePredicateSpec
                            .of(BuildFileSpec.fromRecursivePath(Paths.get(""), cell.getRoot()))),
                    targetConfiguration).getTargetGraph();
        }
        Preconditions.checkState(!passedInTargets.isEmpty());
        return parser.buildTargetGraph(parsingContext, passedInTargets);
    }

    private TargetGraphAndTargets createTargetGraph(TargetGraph projectGraph, ImmutableSet<BuildTarget> graphRoots,
            boolean isWithTests, boolean isWithDependenciesTests, boolean needsFullRecursiveParse)
            throws IOException, InterruptedException, BuildFileParseException, VersionException {

        ImmutableSet<BuildTarget> explicitTestTargets = ImmutableSet.of();
        ImmutableSet<BuildTarget> graphRootsOrSourceTargets = replaceWorkspacesWithSourceTargetsIfPossible(
                graphRoots, projectGraph);

        if (isWithTests) {
            FocusedModuleTargetMatcher focusedModules = getFocusModules();

            explicitTestTargets = getExplicitTestTargets(graphRootsOrSourceTargets, projectGraph,
                    isWithDependenciesTests, focusedModules);
            if (!needsFullRecursiveParse) {
                projectGraph = parser.buildTargetGraph(parsingContext, Sets.union(graphRoots, explicitTestTargets));
            } else {
                projectGraph = parser.buildTargetGraph(parsingContext,
                        Sets.union(projectGraph.getNodes().stream().map(TargetNode::getBuildTarget)
                                .collect(ImmutableSet.toImmutableSet()), explicitTestTargets));
            }
        }

        TargetGraphAndTargets targetGraphAndTargets = TargetGraphAndTargets.create(graphRoots, projectGraph,
                isWithTests, explicitTestTargets);
        if (buckConfig.getView(BuildBuckConfig.class).getBuildVersions()) {
            targetGraphAndTargets = VersionedTargetGraphAndTargets.toVersionedTargetGraphAndTargets(
                    targetGraphAndTargets, versionedTargetGraphCache, buckEventBus, buckConfig, typeCoercerFactory,
                    unconfiguredBuildTargetFactory, explicitTestTargets);
        }
        return targetGraphAndTargets;
    }

    @VisibleForTesting
    static ImmutableSet<BuildTarget> replaceWorkspacesWithSourceTargetsIfPossible(
            ImmutableSet<BuildTarget> buildTargets, TargetGraph projectGraph) {
        Iterable<TargetNode<?>> targetNodes = projectGraph.getAll(buildTargets);
        ImmutableSet.Builder<BuildTarget> resultBuilder = ImmutableSet.builder();
        for (TargetNode<?> node : targetNodes) {
            if (node.getDescription() instanceof XcodeWorkspaceConfigDescription) {
                TargetNode<XcodeWorkspaceConfigDescriptionArg> castedWorkspaceNode = castToXcodeWorkspaceTargetNode(
                        node);
                Optional<BuildTarget> srcTarget = castedWorkspaceNode.getConstructorArg().getSrcTarget();
                if (srcTarget.isPresent()) {
                    resultBuilder.add(srcTarget.get());
                } else {
                    resultBuilder.add(node.getBuildTarget());
                }
            } else {
                resultBuilder.add(node.getBuildTarget());
            }
        }
        return resultBuilder.build();
    }

    private static boolean canGenerateImplicitWorkspaceForDescription(BaseDescription<?> description) {
        // We weren't given a workspace target, but we may have been given something that could
        // still turn into a workspace (for example, a library or an actual app rule). If that's the
        // case we still want to generate a workspace.
        return description instanceof AppleBinaryDescription || description instanceof AppleBundleDescription
                || description instanceof AppleLibraryDescription;
    }

    /**
     * @param sourceTargetNode - The TargetNode which will act as our fake workspaces `src_target`
     * @return Workspace Args that describe a generic Xcode workspace containing `src_target` and its
     *     tests
     */
    private static XcodeWorkspaceConfigDescriptionArg createImplicitWorkspaceArgs(TargetNode<?> sourceTargetNode) {
        return XcodeWorkspaceConfigDescriptionArg.builder().setName("dummy")
                .setSrcTarget(sourceTargetNode.getBuildTarget()).build();
    }

    /**
     * @param buildTargets The set of targets for which we would like to find tests
     * @param projectGraph A TargetGraph containing all nodes and their tests.
     * @param shouldIncludeDependenciesTests Should or not include tests that test dependencies
     * @return A set of all test targets that test any of {@code buildTargets} or their dependencies.
     */
    @VisibleForTesting
    static ImmutableSet<BuildTarget> getExplicitTestTargets(ImmutableSet<BuildTarget> buildTargets,
            TargetGraph projectGraph, boolean shouldIncludeDependenciesTests,
            FocusedModuleTargetMatcher focusedModules) {
        Iterable<TargetNode<?>> projectRoots = projectGraph.getAll(buildTargets);
        Iterable<TargetNode<?>> nodes;
        if (shouldIncludeDependenciesTests) {
            nodes = projectGraph.getSubgraph(projectRoots).getNodes();
        } else {
            nodes = projectRoots;
        }

        return TargetGraphAndTargets.getExplicitTestTargets(RichStream.from(nodes)
                .filter(node -> focusedModules.isFocusedOn(node.getBuildTarget())).iterator());
    }

    /**
     * An action graph where subtrees are populated as needed.
     *
     * <p>This is useful when only select sub-graphs of the action graph needs to be generated, but
     * the subgraph is not known at this point in time. The synchronization and bottom-up traversal is
     * necessary as this will be accessed from multiple threads during project generation, and
     * BuildRuleResolver is not 100% thread safe when it comes to mutations.
     */
    @ThreadSafe
    private static class LazyActionGraph {
        private final TargetGraph targetGraph;
        private final ActionGraphBuilder graphBuilder;
        private final Set<BuildTarget> traversedTargets;

        public LazyActionGraph(TargetGraph targetGraph, CellProvider cellProvider) {
            this.targetGraph = targetGraph;
            this.graphBuilder = new SingleThreadedActionGraphBuilder(targetGraph,
                    new DefaultTargetNodeToBuildRuleTransformer(), cellProvider);
            this.traversedTargets = new HashSet<>();
        }

        public ActionGraphBuilder getActionGraphBuilderWhileRequiringSubgraph(TargetNode<?> root) {
            synchronized (this) {
                try {
                    List<BuildTarget> currentTargets = new ArrayList<>();
                    for (TargetNode<?> targetNode : new AcyclicDepthFirstPostOrderTraversal<TargetNode<?>>(
                            node -> traversedTargets.contains(node.getBuildTarget()) ? Collections.emptyIterator()
                                    : targetGraph.getOutgoingNodesFor(node).iterator())
                                            .traverse(ImmutableList.of(root))) {
                        if (!traversedTargets.contains(targetNode.getBuildTarget())) {
                            graphBuilder.requireRule(targetNode.getBuildTarget());
                            currentTargets.add(targetNode.getBuildTarget());
                        }
                    }
                    traversedTargets.addAll(currentTargets);
                } catch (NoSuchBuildTargetException e) {
                    throw new HumanReadableException(e);
                } catch (CycleException e) {
                    throw new RuntimeException(e);
                }
                return graphBuilder;
            }
        }
    }
}