com.facebook.buck.apple.project_generator.XCodeProjectCommandHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.apple.project_generator.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.apple.project_generator;

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.XcodeWorkspaceConfigDescription;
import com.facebook.buck.apple.XcodeWorkspaceConfigDescriptionArg;
import com.facebook.buck.cli.BuckConfig;
import com.facebook.buck.cli.ProjectTestsMode;
import com.facebook.buck.cxx.CxxBuckConfig;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.graph.AbstractBottomUpTraversal;
import com.facebook.buck.halide.HalideBuckConfig;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetException;
import com.facebook.buck.model.UnflavoredBuildTarget;
import com.facebook.buck.parser.BuildFileSpec;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.parser.Parser;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.parser.SpeculativeParsing;
import com.facebook.buck.parser.TargetNodePredicateSpec;
import com.facebook.buck.parser.TargetNodeSpec;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.Cell;
import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetGraphAndTargets;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.swift.SwiftBuckConfig;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.MoreExceptions;
import com.facebook.buck.util.ProcessManager;
import com.facebook.buck.util.RichStream;
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.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.concurrent.ThreadSafe;

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 Parser parser;
    private final BuckConfig buckConfig;
    private final Cell cell;
    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 enableParserProfiling;
    private final boolean withTests;
    private final boolean withoutTests;
    private final boolean withoutDependenciesTests;
    private final String modulesToFocusOn;
    private final boolean combinedProject;
    private final boolean dryRun;
    private final boolean readOnly;
    private final Function<Iterable<String>, ImmutableList<TargetNodeSpec>> argsParser;
    private final Function<ImmutableList<String>, Integer> buildRunner;

    public XCodeProjectCommandHelper(BuckEventBus buckEventBus, Parser parser, BuckConfig buckConfig, Cell cell,
            Console console, Optional<ProcessManager> processManager, ImmutableMap<String, String> environment,
            ListeningExecutorService executorService, List<String> arguments, boolean enableParserProfiling,
            boolean withTests, boolean withoutTests, boolean withoutDependenciesTests, String modulesToFocusOn,
            boolean combinedProject, boolean dryRun, boolean readOnly,
            Function<Iterable<String>, ImmutableList<TargetNodeSpec>> argsParser,
            Function<ImmutableList<String>, Integer> buildRunner) {
        this.buckEventBus = buckEventBus;
        this.parser = parser;
        this.buckConfig = buckConfig;
        this.cell = cell;
        this.console = console;
        this.processManager = processManager;
        this.environment = environment;
        this.executorService = executorService;
        this.arguments = arguments;
        this.enableParserProfiling = enableParserProfiling;
        this.withTests = withTests;
        this.withoutTests = withoutTests;
        this.withoutDependenciesTests = withoutDependenciesTests;
        this.modulesToFocusOn = modulesToFocusOn;
        this.combinedProject = combinedProject;
        this.dryRun = dryRun;
        this.readOnly = readOnly;
        this.argsParser = argsParser;
        this.buildRunner = buildRunner;
    }

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

        try {
            ParserConfig parserConfig = buckConfig.getView(ParserConfig.class);
            passedInTargetsSet = ImmutableSet.copyOf(Iterables.concat(parser.resolveTargetSpecs(buckEventBus, cell,
                    enableParserProfiling, executor, argsParser.apply(arguments), SpeculativeParsing.of(true),
                    parserConfig.getDefaultFlavorsMode())));
            projectGraph = getProjectGraphForIde(executor, passedInTargetsSet);
        } catch (BuildTargetException | BuildFileParseException | HumanReadableException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return 1;
        }

        checkForAndKillXcodeIfRunning(getIdePrompt(buckConfig));

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

        TargetGraphAndTargets targetGraphAndTargets;
        try {
            targetGraphAndTargets = createTargetGraph(projectGraph, graphRoots, isWithTests(buckConfig),
                    isWithDependenciesTests(buckConfig), passedInTargetsSet.isEmpty(), executor);
        } catch (BuildFileParseException | TargetGraph.NoSuchNodeException | BuildTargetException
                | HumanReadableException e) {
            buckEventBus.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
            return 1;
        }

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

            return 0;
        }

        return runXcodeProjectGenerator(executor, targetGraphAndTargets, passedInTargetsSet);
    }

    /**
     * Returns true if Buck should prompt to kill a running IDE before changing its files, false
     * otherwise.
     */
    private boolean getIdePrompt(BuckConfig buckConfig) {
        return buckConfig.getBooleanValue("project", "ide_prompt", true);
    }

    private ProjectTestsMode testsMode(BuckConfig buckConfig) {
        ProjectTestsMode parameterMode = buckConfig.xcodeProjectTestsMode();

        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 int runXcodeProjectGenerator(ListeningExecutorService executor,
            final TargetGraphAndTargets targetGraphAndTargets, ImmutableSet<BuildTarget> passedInTargetsSet)
            throws IOException, InterruptedException {
        int exitCode = 0;
        AppleConfig appleConfig = buckConfig.getView(AppleConfig.class);
        ImmutableSet<ProjectGenerator.Option> options = buildWorkspaceGeneratorOptions(readOnly,
                isWithTests(buckConfig), isWithDependenciesTests(buckConfig), combinedProject,
                appleConfig.shouldUseHeaderMapsInXcodeProject(), appleConfig.shouldMergeHeaderMapsInXcodeProject(),
                appleConfig.shouldGenerateHeaderSymlinkTreesOnly());

        ImmutableSet<BuildTarget> requiredBuildTargets = generateWorkspacesForTargets(buckEventBus, cell,
                buckConfig, executorService, targetGraphAndTargets, passedInTargetsSet, options,
                getFocusModules(executor), new HashMap<>(), combinedProject);
        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(UnflavoredBuildTarget.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, Cell cell,
            BuckConfig buckConfig, ListeningExecutorService executorService,
            final TargetGraphAndTargets targetGraphAndTargets, ImmutableSet<BuildTarget> passedInTargetsSet,
            ImmutableSet<ProjectGenerator.Option> options, FocusedModuleTargetMatcher focusModules,
            Map<Path, ProjectGenerator> projectGenerators, boolean combinedProject)
            throws IOException, InterruptedException {
        ImmutableSet<BuildTarget> targets;
        if (passedInTargetsSet.isEmpty()) {
            targets = targetGraphAndTargets.getProjectRoots().stream().map(TargetNode::getBuildTarget)
                    .collect(MoreCollectors.toImmutableSet());
        } else {
            targets = passedInTargetsSet;
        }

        LazyActionGraph lazyActionGraph = new LazyActionGraph(targetGraphAndTargets.getTargetGraph(), buckEventBus);

        LOG.debug("Generating workspace for config targets %s", targets);
        ImmutableSet.Builder<BuildTarget> requiredBuildTargetsBuilder = ImmutableSet.builder();
        for (final 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);

            CxxPlatform defaultCxxPlatform = cell.getKnownBuildRuleTypes().getDefaultCxxPlatforms();
            WorkspaceAndProjectGenerator generator = new WorkspaceAndProjectGenerator(cell,
                    targetGraphAndTargets.getTargetGraph(), workspaceArgs, inputTarget, options, combinedProject,
                    focusModules, !appleConfig.getXcodeDisableParallelizeBuild(), defaultCxxPlatform,
                    buckConfig.getView(ParserConfig.class).getBuildFileName(),
                    lazyActionGraph::getBuildRuleResolverWhileRequiringSubgraph, buckEventBus, halideBuckConfig,
                    cxxBuckConfig, swiftBuckConfig);
            Preconditions.checkNotNull(executorService,
                    "CommandRunnerParams does not have executor for PROJECT pool");
            generator.generateWorkspaceAndDependentProjects(projectGenerators, executorService);
            ImmutableSet<BuildTarget> requiredBuildTargetsForWorkspace = generator.getRequiredBuildTargets();
            LOG.debug("Required build targets for workspace %s: %s", inputTarget, requiredBuildTargetsForWorkspace);
            requiredBuildTargetsBuilder.addAll(requiredBuildTargetsForWorkspace);
        }

        return requiredBuildTargetsBuilder.build();
    }

    private FocusedModuleTargetMatcher getFocusModules(ListeningExecutorService executor)
            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;
        ParserConfig parserConfig = buckConfig.getView(ParserConfig.class);
        try {
            passedInTargetsSet = parser
                    .resolveTargetSpecs(buckEventBus, cell, enableParserProfiling, executor, specs,
                            SpeculativeParsing.of(false), parserConfig.getDefaultFlavorsMode())
                    .stream().flatMap(Collection::stream).map(target -> target.withoutCell())
                    .collect(MoreCollectors.toImmutableSet());
        } catch (BuildTargetException | BuildFileParseException | 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);
    }

    @VisibleForTesting
    static ImmutableSet<ProjectGenerator.Option> buildWorkspaceGeneratorOptions(boolean isReadonly,
            boolean isWithTests, boolean isWithDependenciesTests, boolean isProjectsCombined,
            boolean shouldUseHeaderMaps, boolean shouldMergeHeaderMaps,
            boolean shouldGenerateHeaderSymlinkTreesOnly) {
        ImmutableSet.Builder<ProjectGenerator.Option> optionsBuilder = ImmutableSet.builder();
        if (isReadonly) {
            optionsBuilder.add(ProjectGenerator.Option.GENERATE_READ_ONLY_FILES);
        }
        if (isWithTests) {
            optionsBuilder.add(ProjectGenerator.Option.INCLUDE_TESTS);
        }
        if (isWithDependenciesTests) {
            optionsBuilder.add(ProjectGenerator.Option.INCLUDE_DEPENDENCIES_TESTS);
        }
        if (isProjectsCombined) {
            optionsBuilder.addAll(ProjectGenerator.COMBINED_PROJECT_OPTIONS);
        } else {
            optionsBuilder.addAll(ProjectGenerator.SEPARATED_PROJECT_OPTIONS);
        }
        if (!shouldUseHeaderMaps) {
            optionsBuilder.add(ProjectGenerator.Option.DISABLE_HEADER_MAPS);
        }
        if (shouldMergeHeaderMaps) {
            optionsBuilder.add(ProjectGenerator.Option.MERGE_HEADER_MAPS);
        }
        if (shouldGenerateHeaderSymlinkTreesOnly) {
            optionsBuilder.add(ProjectGenerator.Option.GENERATE_HEADERS_SYMLINK_TREES_ONLY);
        }
        return optionsBuilder.build();
    }

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

    private void checkForAndKillXcodeIfRunning(boolean enablePrompt) throws InterruptedException, IOException {
        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;
        }

        boolean canPromptResult = canPrompt(environment);
        if (enablePrompt && 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" + "[project]\n"
                            + "  ide_prompt = false\n\n",
                            cell.getFilesystem().getRootPath().resolve(BuckConfig.BUCK_CONFIG_OVERRIDE_FILE_NAME)
                                    .toAbsolutePath());
        } else {
            LOG.debug("Xcode is running, but cannot prompt to kill it (enabled %s, can prompt %s)", enablePrompt,
                    canPromptResult);
        }
    }

    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] "));

        Optional<String> result;
        try (InputStreamReader stdinReader = new InputStreamReader(System.in, Charsets.UTF_8);
                BufferedReader bufferedStdinReader = new BufferedReader(stdinReader)) {
            result = Optional.ofNullable(bufferedStdinReader.readLine());
        }
        LOG.debug("Result of prompt: [%s]", result);
        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(MoreCollectors.toImmutableSet());
    }

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

        if (passedInTargets.isEmpty()) {
            return parser
                    .buildTargetGraphForTargetNodeSpecs(buckEventBus, cell, enableParserProfiling, executor,
                            ImmutableList.of(TargetNodePredicateSpec.of(x -> true,
                                    BuildFileSpec.fromRecursivePath(Paths.get(""), cell.getRoot()))))
                    .getTargetGraph();
        }
        Preconditions.checkState(!passedInTargets.isEmpty());
        return parser.buildTargetGraph(buckEventBus, cell, enableParserProfiling, executor, passedInTargets);
    }

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

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

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

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

        return TargetGraphAndTargets.create(graphRoots, projectGraph, isWithTests, explicitTestTargets);
    }

    @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(Description<?> 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 BuildRuleResolver resolver;

        public LazyActionGraph(TargetGraph targetGraph, BuckEventBus buckEventBus) {
            this.targetGraph = targetGraph;
            this.resolver = new BuildRuleResolver(targetGraph, new DefaultTargetNodeToBuildRuleTransformer(),
                    buckEventBus);
        }

        public BuildRuleResolver getBuildRuleResolverWhileRequiringSubgraph(TargetNode<?, ?> root) {
            TargetGraph subgraph = targetGraph.getSubgraph(ImmutableList.of(root));

            try {
                synchronized (this) {
                    new AbstractBottomUpTraversal<TargetNode<?, ?>, NoSuchBuildTargetException>(subgraph) {
                        @Override
                        public void visit(TargetNode<?, ?> node) throws NoSuchBuildTargetException {
                            resolver.requireRule(node.getBuildTarget());
                        }
                    }.traverse();
                }
            } catch (NoSuchBuildTargetException e) {
                throw new HumanReadableException(e);
            }
            return resolver;
        }
    }
}