/*
* 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.project_generator;
import com.facebook.buck.apple.AppleBuildRules;
import com.facebook.buck.apple.AppleBundleDescription;
import com.facebook.buck.apple.AppleBundleDescriptionArg;
import com.facebook.buck.apple.AppleDependenciesCache;
import com.facebook.buck.apple.AppleTestDescriptionArg;
import com.facebook.buck.apple.XcodeWorkspaceConfigDescription;
import com.facebook.buck.apple.XcodeWorkspaceConfigDescriptionArg;
import com.facebook.buck.apple.xcode.XCScheme;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget;
import com.facebook.buck.cxx.CxxBuckConfig;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.graph.TopologicalSort;
import com.facebook.buck.halide.HalideBuckConfig;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.UnflavoredBuildTarget;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.Cell;
import com.facebook.buck.rules.HasTests;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.swift.SwiftBuckConfig;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.Optionals;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
// import com.facebook.buck.io.ProjectFilesystem;
public class WorkspaceAndProjectGenerator {
private static final Logger LOG = Logger.get(WorkspaceAndProjectGenerator.class);
private final Cell rootCell;
private final TargetGraph projectGraph;
private final AppleDependenciesCache dependenciesCache;
private final XcodeWorkspaceConfigDescriptionArg workspaceArguments;
private final BuildTarget workspaceBuildTarget;
private final FocusedModuleTargetMatcher focusModules;
private final ImmutableSet<ProjectGenerator.Option> projectGeneratorOptions;
private final boolean combinedProject;
private final boolean parallelizeBuild;
private final CxxPlatform defaultCxxPlatform;
private Optional<ProjectGenerator> combinedProjectGenerator;
private final Map<String, SchemeGenerator> schemeGenerators = new HashMap<>();
private final String buildFileName;
private final Function<TargetNode<?, ?>, BuildRuleResolver> buildRuleResolverForNode;
private final BuckEventBus buckEventBus;
private final ImmutableSet.Builder<BuildTarget> requiredBuildTargetsBuilder =
ImmutableSet.builder();
private final HalideBuckConfig halideBuckConfig;
private final CxxBuckConfig cxxBuckConfig;
private final SwiftBuckConfig swiftBuckConfig;
public WorkspaceAndProjectGenerator(
Cell cell,
TargetGraph projectGraph,
XcodeWorkspaceConfigDescriptionArg workspaceArguments,
BuildTarget workspaceBuildTarget,
Set<ProjectGenerator.Option> projectGeneratorOptions,
boolean combinedProject,
FocusedModuleTargetMatcher focusModules,
boolean parallelizeBuild,
CxxPlatform defaultCxxPlatform,
String buildFileName,
Function<TargetNode<?, ?>, BuildRuleResolver> buildRuleResolverForNode,
BuckEventBus buckEventBus,
HalideBuckConfig halideBuckConfig,
CxxBuckConfig cxxBuckConfig,
SwiftBuckConfig swiftBuckConfig) {
this.rootCell = cell;
this.projectGraph = projectGraph;
this.dependenciesCache = new AppleDependenciesCache(projectGraph);
this.workspaceArguments = workspaceArguments;
this.workspaceBuildTarget = workspaceBuildTarget;
this.projectGeneratorOptions = ImmutableSet.copyOf(projectGeneratorOptions);
this.combinedProject = combinedProject;
this.parallelizeBuild = parallelizeBuild;
this.defaultCxxPlatform = defaultCxxPlatform;
this.buildFileName = buildFileName;
this.buildRuleResolverForNode = buildRuleResolverForNode;
this.buckEventBus = buckEventBus;
this.swiftBuckConfig = swiftBuckConfig;
this.combinedProjectGenerator = Optional.empty();
this.halideBuckConfig = halideBuckConfig;
this.cxxBuckConfig = cxxBuckConfig;
this.focusModules =
focusModules.map(
inputs ->
// Update the focused modules list (if present) to contain srcTarget (if present).
workspaceArguments
.getSrcTarget()
.map(
srcTarget ->
ImmutableSet.<UnflavoredBuildTarget>builder()
.addAll(inputs)
.add(srcTarget.getUnflavoredBuildTarget())
.build())
.orElse(inputs));
}
@VisibleForTesting
Optional<ProjectGenerator> getCombinedProjectGenerator() {
return combinedProjectGenerator;
}
@VisibleForTesting
Map<String, SchemeGenerator> getSchemeGenerators() {
return schemeGenerators;
}
public ImmutableSet<BuildTarget> getRequiredBuildTargets() {
return requiredBuildTargetsBuilder.build();
}
public Path generateWorkspaceAndDependentProjects(
Map<Path, ProjectGenerator> projectGenerators,
ListeningExecutorService listeningExecutorService)
throws IOException, InterruptedException {
LOG.debug("Generating workspace for target %s", workspaceBuildTarget);
String workspaceName =
XcodeWorkspaceConfigDescription.getWorkspaceNameFromArg(workspaceArguments);
Path outputDirectory;
if (combinedProject) {
workspaceName += "-Combined";
outputDirectory =
BuildTargets.getGenPath(rootCell.getFilesystem(), workspaceBuildTarget, "%s")
.getParent()
.resolve(workspaceName + ".xcodeproj");
} else {
outputDirectory = workspaceBuildTarget.getBasePath();
}
WorkspaceGenerator workspaceGenerator =
new WorkspaceGenerator(
rootCell.getFilesystem(), combinedProject ? "project" : workspaceName, outputDirectory);
ImmutableMap.Builder<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigsBuilder =
ImmutableMap.builder();
ImmutableSetMultimap.Builder<String, Optional<TargetNode<?, ?>>>
schemeNameToSrcTargetNodeBuilder = ImmutableSetMultimap.builder();
ImmutableSetMultimap.Builder<String, TargetNode<?, ?>> buildForTestNodesBuilder =
ImmutableSetMultimap.builder();
ImmutableSetMultimap.Builder<String, TargetNode<AppleTestDescriptionArg, ?>> testsBuilder =
ImmutableSetMultimap.builder();
buildWorkspaceSchemes(
projectGraph,
projectGeneratorOptions.contains(ProjectGenerator.Option.INCLUDE_TESTS),
projectGeneratorOptions.contains(ProjectGenerator.Option.INCLUDE_DEPENDENCIES_TESTS),
workspaceName,
workspaceArguments,
schemeConfigsBuilder,
schemeNameToSrcTargetNodeBuilder,
buildForTestNodesBuilder,
testsBuilder);
ImmutableMap<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigs =
schemeConfigsBuilder.build();
ImmutableSetMultimap<String, Optional<TargetNode<?, ?>>> schemeNameToSrcTargetNode =
schemeNameToSrcTargetNodeBuilder.build();
ImmutableSetMultimap<String, TargetNode<?, ?>> buildForTestNodes =
buildForTestNodesBuilder.build();
ImmutableSetMultimap<String, TargetNode<AppleTestDescriptionArg, ?>> tests =
testsBuilder.build();
ImmutableSet<BuildTarget> targetsInRequiredProjects =
Stream.concat(
schemeNameToSrcTargetNode.values().stream().flatMap(Optionals::toStream),
buildForTestNodes.values().stream())
.map(TargetNode::getBuildTarget)
.collect(MoreCollectors.toImmutableSet());
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMapBuilder =
ImmutableMap.builder();
ImmutableMap.Builder<PBXTarget, Path> targetToProjectPathMapBuilder = ImmutableMap.builder();
generateProjects(
projectGenerators,
listeningExecutorService,
workspaceName,
outputDirectory,
workspaceGenerator,
targetsInRequiredProjects,
buildTargetToPbxTargetMapBuilder,
targetToProjectPathMapBuilder);
if (projectGeneratorOptions.contains(
ProjectGenerator.Option.GENERATE_HEADERS_SYMLINK_TREES_ONLY)) {
return workspaceGenerator.getWorkspaceDir();
} else {
final ImmutableMap<BuildTarget, PBXTarget> buildTargetToTarget =
buildTargetToPbxTargetMapBuilder.build();
writeWorkspaceSchemes(
workspaceName,
outputDirectory,
schemeConfigs,
schemeNameToSrcTargetNode,
buildForTestNodes,
tests,
targetToProjectPathMapBuilder.build(),
buildTargetToTarget);
return workspaceGenerator.writeWorkspace();
}
}
private void generateProjects(
Map<Path, ProjectGenerator> projectGenerators,
ListeningExecutorService listeningExecutorService,
String workspaceName,
Path outputDirectory,
WorkspaceGenerator workspaceGenerator,
ImmutableSet<BuildTarget> targetsInRequiredProjects,
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMapBuilder,
ImmutableMap.Builder<PBXTarget, Path> targetToProjectPathMapBuilder)
throws IOException, InterruptedException {
if (combinedProject) {
generateCombinedProject(
workspaceName,
outputDirectory,
workspaceGenerator,
targetsInRequiredProjects,
buildTargetToPbxTargetMapBuilder,
targetToProjectPathMapBuilder);
} else {
generateProject(
projectGenerators,
listeningExecutorService,
workspaceGenerator,
targetsInRequiredProjects,
buildTargetToPbxTargetMapBuilder,
targetToProjectPathMapBuilder);
}
}
private void generateProject(
final Map<Path, ProjectGenerator> projectGenerators,
ListeningExecutorService listeningExecutorService,
WorkspaceGenerator workspaceGenerator,
ImmutableSet<BuildTarget> targetsInRequiredProjects,
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMapBuilder,
ImmutableMap.Builder<PBXTarget, Path> targetToProjectPathMapBuilder)
throws IOException, InterruptedException {
ImmutableMultimap.Builder<Cell, BuildTarget> projectCellToBuildTargetsBuilder =
ImmutableMultimap.builder();
for (TargetNode<?, ?> targetNode : projectGraph.getNodes()) {
BuildTarget buildTarget = targetNode.getBuildTarget();
projectCellToBuildTargetsBuilder.put(rootCell.getCell(buildTarget), buildTarget);
}
ImmutableMultimap<Cell, BuildTarget> projectCellToBuildTargets =
projectCellToBuildTargetsBuilder.build();
List<ListenableFuture<GenerationResult>> projectGeneratorFutures = new ArrayList<>();
for (final Cell projectCell : projectCellToBuildTargets.keySet()) {
ImmutableMultimap.Builder<Path, BuildTarget> projectDirectoryToBuildTargetsBuilder =
ImmutableMultimap.builder();
final ImmutableSet<BuildTarget> cellRules =
ImmutableSet.copyOf(projectCellToBuildTargets.get(projectCell));
for (BuildTarget buildTarget : cellRules) {
projectDirectoryToBuildTargetsBuilder.put(buildTarget.getBasePath(), buildTarget);
}
ImmutableMultimap<Path, BuildTarget> projectDirectoryToBuildTargets =
projectDirectoryToBuildTargetsBuilder.build();
final Path relativeTargetCell = rootCell.getRoot().relativize(projectCell.getRoot());
for (final Path projectDirectory : projectDirectoryToBuildTargets.keySet()) {
final ImmutableSet<BuildTarget> rules =
filterRulesForProjectDirectory(
projectGraph,
ImmutableSet.copyOf(projectDirectoryToBuildTargets.get(projectDirectory)));
if (Sets.intersection(targetsInRequiredProjects, rules).isEmpty()) {
continue;
}
final boolean isMainProject =
workspaceArguments.getSrcTarget().isPresent()
&& rules.contains(workspaceArguments.getSrcTarget().get());
projectGeneratorFutures.add(
listeningExecutorService.submit(
() -> {
GenerationResult result =
generateProjectForDirectory(
projectGenerators,
projectCell,
projectDirectory,
rules,
isMainProject,
targetsInRequiredProjects);
// convert the projectPath to relative to the target cell here
result =
GenerationResult.of(
relativeTargetCell.resolve(result.getProjectPath()),
result.isProjectGenerated(),
result.getRequiredBuildTargets(),
result.getBuildTargetToGeneratedTargetMap());
return result;
}));
}
}
List<GenerationResult> generationResults;
try {
generationResults = Futures.allAsList(projectGeneratorFutures).get();
} catch (ExecutionException e) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
Throwables.throwIfUnchecked(e.getCause());
throw new IllegalStateException("Unexpected exception: ", e);
}
for (GenerationResult result : generationResults) {
if (!result.isProjectGenerated()) {
continue;
}
workspaceGenerator.addFilePath(result.getProjectPath());
processGenerationResult(
buildTargetToPbxTargetMapBuilder, targetToProjectPathMapBuilder, result);
}
}
private void processGenerationResult(
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMapBuilder,
ImmutableMap.Builder<PBXTarget, Path> targetToProjectPathMapBuilder,
GenerationResult result) {
requiredBuildTargetsBuilder.addAll(result.getRequiredBuildTargets());
buildTargetToPbxTargetMapBuilder.putAll(result.getBuildTargetToGeneratedTargetMap());
for (PBXTarget target : result.getBuildTargetToGeneratedTargetMap().values()) {
targetToProjectPathMapBuilder.put(target, result.getProjectPath());
}
}
private GenerationResult generateProjectForDirectory(
Map<Path, ProjectGenerator> projectGenerators,
Cell projectCell,
Path projectDirectory,
final ImmutableSet<BuildTarget> rules,
boolean isMainProject,
ImmutableSet<BuildTarget> targetsInRequiredProjects)
throws IOException {
boolean shouldGenerateProjects = false;
ProjectGenerator generator;
synchronized (projectGenerators) {
generator = projectGenerators.get(projectDirectory);
if (generator != null) {
LOG.debug("Already generated project for target %s, skipping", projectDirectory);
} else {
LOG.debug("Generating project for directory %s with targets %s", projectDirectory, rules);
String projectName;
if (projectDirectory.getFileName().toString().equals("")) {
// If we're generating a project in the root directory, use a generic name.
projectName = "Project";
} else {
// Otherwise, name the project the same thing as the directory we're in.
projectName = projectDirectory.getFileName().toString();
}
generator =
new ProjectGenerator(
projectGraph,
dependenciesCache,
rules,
projectCell,
projectDirectory,
projectName,
buildFileName,
projectGeneratorOptions,
isMainProject,
workspaceArguments.getSrcTarget(),
targetsInRequiredProjects,
focusModules,
defaultCxxPlatform,
buildRuleResolverForNode,
buckEventBus,
halideBuckConfig,
cxxBuckConfig,
swiftBuckConfig);
projectGenerators.put(projectDirectory, generator);
shouldGenerateProjects = true;
}
}
ImmutableSet<BuildTarget> requiredBuildTargets = ImmutableSet.of();
ImmutableMap<BuildTarget, PBXTarget> buildTargetToGeneratedTargetMap = ImmutableMap.of();
if (shouldGenerateProjects) {
generator.createXcodeProjects();
}
if (generator.isProjectGenerated()) {
requiredBuildTargets = generator.getRequiredBuildTargets();
buildTargetToGeneratedTargetMap = generator.getBuildTargetToGeneratedTargetMap();
}
return GenerationResult.of(
generator.getProjectPath(),
generator.isProjectGenerated(),
requiredBuildTargets,
buildTargetToGeneratedTargetMap);
}
private void generateCombinedProject(
String workspaceName,
Path outputDirectory,
WorkspaceGenerator workspaceGenerator,
ImmutableSet<BuildTarget> targetsInRequiredProjects,
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMapBuilder,
ImmutableMap.Builder<PBXTarget, Path> targetToProjectPathMapBuilder)
throws IOException {
LOG.debug("Generating a combined project");
ProjectGenerator generator =
new ProjectGenerator(
projectGraph,
dependenciesCache,
targetsInRequiredProjects,
rootCell,
outputDirectory.getParent(),
workspaceName,
buildFileName,
projectGeneratorOptions,
true,
workspaceArguments.getSrcTarget(),
targetsInRequiredProjects,
focusModules,
defaultCxxPlatform,
buildRuleResolverForNode,
buckEventBus,
halideBuckConfig,
cxxBuckConfig,
swiftBuckConfig);
combinedProjectGenerator = Optional.of(generator);
generator.createXcodeProjects();
GenerationResult result =
GenerationResult.of(
generator.getProjectPath(),
generator.isProjectGenerated(),
generator.getRequiredBuildTargets(),
generator.getBuildTargetToGeneratedTargetMap());
workspaceGenerator.addFilePath(result.getProjectPath(), Optional.empty());
processGenerationResult(
buildTargetToPbxTargetMapBuilder, targetToProjectPathMapBuilder, result);
}
private void buildWorkspaceSchemes(
TargetGraph projectGraph,
boolean includeProjectTests,
boolean includeDependenciesTests,
String workspaceName,
XcodeWorkspaceConfigDescriptionArg workspaceArguments,
ImmutableMap.Builder<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigsBuilder,
ImmutableSetMultimap.Builder<String, Optional<TargetNode<?, ?>>>
schemeNameToSrcTargetNodeBuilder,
ImmutableSetMultimap.Builder<String, TargetNode<?, ?>> buildForTestNodesBuilder,
ImmutableSetMultimap.Builder<String, TargetNode<AppleTestDescriptionArg, ?>> testsBuilder) {
ImmutableSetMultimap.Builder<String, TargetNode<AppleTestDescriptionArg, ?>>
extraTestNodesBuilder = ImmutableSetMultimap.builder();
addWorkspaceScheme(
projectGraph,
dependenciesCache,
workspaceName,
workspaceArguments,
schemeConfigsBuilder,
schemeNameToSrcTargetNodeBuilder,
extraTestNodesBuilder);
addExtraWorkspaceSchemes(
projectGraph,
dependenciesCache,
workspaceArguments.getExtraSchemes(),
schemeConfigsBuilder,
schemeNameToSrcTargetNodeBuilder,
extraTestNodesBuilder);
ImmutableSetMultimap<String, Optional<TargetNode<?, ?>>> schemeNameToSrcTargetNode =
schemeNameToSrcTargetNodeBuilder.build();
ImmutableSetMultimap<String, TargetNode<AppleTestDescriptionArg, ?>> extraTestNodes =
extraTestNodesBuilder.build();
buildWorkspaceSchemeTests(
workspaceArguments.getSrcTarget(),
projectGraph,
includeProjectTests,
includeDependenciesTests,
schemeNameToSrcTargetNode,
extraTestNodes,
testsBuilder,
buildForTestNodesBuilder);
}
private static void addWorkspaceScheme(
TargetGraph projectGraph,
AppleDependenciesCache dependenciesCache,
String schemeName,
XcodeWorkspaceConfigDescriptionArg schemeArguments,
ImmutableMap.Builder<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigsBuilder,
ImmutableSetMultimap.Builder<String, Optional<TargetNode<?, ?>>>
schemeNameToSrcTargetNodeBuilder,
ImmutableSetMultimap.Builder<String, TargetNode<AppleTestDescriptionArg, ?>>
extraTestNodesBuilder) {
LOG.debug("Adding scheme %s", schemeName);
schemeConfigsBuilder.put(schemeName, schemeArguments);
if (schemeArguments.getSrcTarget().isPresent()) {
schemeNameToSrcTargetNodeBuilder.putAll(
schemeName,
Iterables.transform(
AppleBuildRules.getSchemeBuildableTargetNodes(
projectGraph,
Optional.of(dependenciesCache),
projectGraph.get(schemeArguments.getSrcTarget().get())),
Optional::of));
} else {
schemeNameToSrcTargetNodeBuilder.put(
XcodeWorkspaceConfigDescription.getWorkspaceNameFromArg(schemeArguments),
Optional.empty());
}
for (BuildTarget extraTarget : schemeArguments.getExtraTargets()) {
schemeNameToSrcTargetNodeBuilder.putAll(
schemeName,
Iterables.transform(
AppleBuildRules.getSchemeBuildableTargetNodes(
projectGraph,
Optional.of(dependenciesCache),
Preconditions.checkNotNull(projectGraph.get(extraTarget))),
Optional::of));
}
extraTestNodesBuilder.putAll(
schemeName, getExtraTestTargetNodes(projectGraph, schemeArguments.getExtraTests()));
}
private static void addExtraWorkspaceSchemes(
TargetGraph projectGraph,
AppleDependenciesCache dependenciesCache,
ImmutableSortedMap<String, BuildTarget> extraSchemes,
ImmutableMap.Builder<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigsBuilder,
ImmutableSetMultimap.Builder<String, Optional<TargetNode<?, ?>>>
schemeNameToSrcTargetNodeBuilder,
ImmutableSetMultimap.Builder<String, TargetNode<AppleTestDescriptionArg, ?>>
extraTestNodesBuilder) {
for (Map.Entry<String, BuildTarget> extraSchemeEntry : extraSchemes.entrySet()) {
BuildTarget extraSchemeTarget = extraSchemeEntry.getValue();
Optional<TargetNode<?, ?>> extraSchemeNode = projectGraph.getOptional(extraSchemeTarget);
if (!extraSchemeNode.isPresent()
|| !(extraSchemeNode.get().getDescription() instanceof XcodeWorkspaceConfigDescription)) {
throw new HumanReadableException(
"Extra scheme target '%s' should be of type 'xcode_workspace_config'",
extraSchemeTarget);
}
XcodeWorkspaceConfigDescriptionArg extraSchemeArg =
(XcodeWorkspaceConfigDescriptionArg) extraSchemeNode.get().getConstructorArg();
String schemeName = extraSchemeEntry.getKey();
addWorkspaceScheme(
projectGraph,
dependenciesCache,
schemeName,
extraSchemeArg,
schemeConfigsBuilder,
schemeNameToSrcTargetNodeBuilder,
extraTestNodesBuilder);
}
}
private static ImmutableSet<BuildTarget> filterRulesForProjectDirectory(
TargetGraph projectGraph, ImmutableSet<BuildTarget> projectBuildTargets) {
// ProjectGenerator implicitly generates targets for all apple_binary rules which
// are referred to by apple_bundle rules' 'binary' field.
//
// We used to support an explicit xcode_project_config() which
// listed all dependencies explicitly, but now that we synthesize
// one, we need to ensure we continue to only pass apple_binary
// targets which do not belong to apple_bundle rules.
ImmutableSet.Builder<BuildTarget> binaryTargetsInsideBundlesBuilder = ImmutableSet.builder();
for (TargetNode<?, ?> projectTargetNode : projectGraph.getAll(projectBuildTargets)) {
if (projectTargetNode.getDescription() instanceof AppleBundleDescription) {
AppleBundleDescriptionArg appleBundleDescriptionArg =
(AppleBundleDescriptionArg) projectTargetNode.getConstructorArg();
// We don't support apple_bundle rules referring to apple_binary rules
// outside their current directory.
Preconditions.checkState(
appleBundleDescriptionArg
.getBinary()
.getBasePath()
.equals(projectTargetNode.getBuildTarget().getBasePath()),
"apple_bundle target %s contains reference to binary %s outside base path %s",
projectTargetNode.getBuildTarget(),
appleBundleDescriptionArg.getBinary(),
projectTargetNode.getBuildTarget().getBasePath());
binaryTargetsInsideBundlesBuilder.add(appleBundleDescriptionArg.getBinary());
}
}
ImmutableSet<BuildTarget> binaryTargetsInsideBundles =
binaryTargetsInsideBundlesBuilder.build();
// Remove all apple_binary targets which are inside bundles from
// the rest of the build targets in the project.
return ImmutableSet.copyOf(Sets.difference(projectBuildTargets, binaryTargetsInsideBundles));
}
/**
* Find tests to run.
*
* @param targetGraph input target graph
* @param includeProjectTests whether to include tests of nodes in the project
* @param orderedTargetNodes target nodes for which to fetch tests for
* @param extraTestBundleTargets extra tests to include
* @return test targets that should be run.
*/
private ImmutableSet<TargetNode<AppleTestDescriptionArg, ?>> getOrderedTestNodes(
Optional<BuildTarget> mainTarget,
TargetGraph targetGraph,
boolean includeProjectTests,
boolean includeDependenciesTests,
ImmutableSet<TargetNode<?, ?>> orderedTargetNodes,
ImmutableSet<TargetNode<AppleTestDescriptionArg, ?>> extraTestBundleTargets) {
LOG.debug("Getting ordered test target nodes for %s", orderedTargetNodes);
ImmutableSet.Builder<TargetNode<AppleTestDescriptionArg, ?>> testsBuilder =
ImmutableSet.builder();
if (includeProjectTests) {
Optional<TargetNode<?, ?>> mainTargetNode = Optional.empty();
if (mainTarget.isPresent()) {
mainTargetNode = targetGraph.getOptional(mainTarget.get());
}
for (TargetNode<?, ?> node : orderedTargetNodes) {
if (includeDependenciesTests
|| (mainTargetNode.isPresent() && node.equals(mainTargetNode.get()))) {
if (!(node.getConstructorArg() instanceof HasTests)) {
continue;
}
for (BuildTarget explicitTestTarget : ((HasTests) node.getConstructorArg()).getTests()) {
if (!focusModules.isFocusedOn(explicitTestTarget)) {
continue;
}
Optional<TargetNode<?, ?>> explicitTestNode =
targetGraph.getOptional(explicitTestTarget);
if (explicitTestNode.isPresent()) {
Optional<TargetNode<AppleTestDescriptionArg, ?>> castedNode =
explicitTestNode.get().castArg(AppleTestDescriptionArg.class);
if (castedNode.isPresent()) {
testsBuilder.add(castedNode.get());
} else {
LOG.debug(
"Test target specified in '%s' is not a apple_test;"
+ " not including in project: '%s'",
node.getBuildTarget(), explicitTestTarget);
}
} else {
throw new HumanReadableException(
"Test target specified in '%s' is not in the target graph: '%s'",
node.getBuildTarget(), explicitTestTarget);
}
}
}
}
}
for (TargetNode<AppleTestDescriptionArg, ?> extraTestTarget : extraTestBundleTargets) {
testsBuilder.add(extraTestTarget);
}
return testsBuilder.build();
}
/**
* Find transitive dependencies of inputs for building.
*
* @param projectGraph {@link TargetGraph} containing nodes
* @param nodes Nodes to fetch dependencies for.
* @param excludes Nodes to exclude from dependencies list.
* @return targets and their dependencies that should be build.
*/
private static ImmutableSet<TargetNode<?, ?>> getTransitiveDepsAndInputs(
final TargetGraph projectGraph,
final AppleDependenciesCache dependenciesCache,
Iterable<? extends TargetNode<?, ?>> nodes,
final ImmutableSet<TargetNode<?, ?>> excludes) {
return FluentIterable.from(nodes)
.transformAndConcat(
new Function<TargetNode<?, ?>, Iterable<TargetNode<?, ?>>>() {
@Override
public Iterable<TargetNode<?, ?>> apply(TargetNode<?, ?> input) {
return AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
projectGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.BUILDING,
input,
Optional.empty());
}
})
.append(nodes)
.filter(
input ->
!excludes.contains(input)
&& AppleBuildRules.isXcodeTargetDescription(input.getDescription()))
.toSet();
}
private static ImmutableSet<TargetNode<AppleTestDescriptionArg, ?>> getExtraTestTargetNodes(
TargetGraph graph, Iterable<BuildTarget> targets) {
ImmutableSet.Builder<TargetNode<AppleTestDescriptionArg, ?>> builder = ImmutableSet.builder();
for (TargetNode<?, ?> node : graph.getAll(targets)) {
Optional<TargetNode<AppleTestDescriptionArg, ?>> castedNode =
node.castArg(AppleTestDescriptionArg.class);
if (castedNode.isPresent()) {
builder.add(castedNode.get());
} else {
throw new HumanReadableException(
"Extra test target is not a test: '%s'", node.getBuildTarget());
}
}
return builder.build();
}
private void buildWorkspaceSchemeTests(
Optional<BuildTarget> mainTarget,
TargetGraph projectGraph,
boolean includeProjectTests,
boolean includeDependenciesTests,
ImmutableSetMultimap<String, Optional<TargetNode<?, ?>>> schemeNameToSrcTargetNode,
ImmutableSetMultimap<String, TargetNode<AppleTestDescriptionArg, ?>> extraTestNodes,
ImmutableSetMultimap.Builder<String, TargetNode<AppleTestDescriptionArg, ?>>
selectedTestsBuilder,
ImmutableSetMultimap.Builder<String, TargetNode<?, ?>> buildForTestNodesBuilder) {
for (String schemeName : schemeNameToSrcTargetNode.keySet()) {
ImmutableSet<TargetNode<?, ?>> targetNodes =
schemeNameToSrcTargetNode
.get(schemeName)
.stream()
.flatMap(Optionals::toStream)
.collect(MoreCollectors.toImmutableSet());
ImmutableSet<TargetNode<AppleTestDescriptionArg, ?>> testNodes =
getOrderedTestNodes(
mainTarget,
projectGraph,
includeProjectTests,
includeDependenciesTests,
targetNodes,
extraTestNodes.get(schemeName));
selectedTestsBuilder.putAll(schemeName, testNodes);
buildForTestNodesBuilder.putAll(
schemeName,
Iterables.filter(
TopologicalSort.sort(projectGraph),
getTransitiveDepsAndInputs(projectGraph, dependenciesCache, testNodes, targetNodes)
::contains));
}
}
private void writeWorkspaceSchemes(
String workspaceName,
Path outputDirectory,
ImmutableMap<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigs,
ImmutableSetMultimap<String, Optional<TargetNode<?, ?>>> schemeNameToSrcTargetNode,
ImmutableSetMultimap<String, TargetNode<?, ?>> buildForTestNodes,
ImmutableSetMultimap<String, TargetNode<AppleTestDescriptionArg, ?>> ungroupedTests,
ImmutableMap<PBXTarget, Path> targetToProjectPathMap,
ImmutableMap<BuildTarget, PBXTarget> buildTargetToPBXTarget)
throws IOException {
for (Map.Entry<String, XcodeWorkspaceConfigDescriptionArg> schemeConfigEntry :
schemeConfigs.entrySet()) {
String schemeName = schemeConfigEntry.getKey();
XcodeWorkspaceConfigDescriptionArg schemeConfigArg = schemeConfigEntry.getValue();
if (schemeConfigArg.getSrcTarget().isPresent()
&& !focusModules.isFocusedOn(schemeConfigArg.getSrcTarget().get())) {
continue;
}
ImmutableSet<PBXTarget> orderedBuildTargets =
schemeNameToSrcTargetNode
.get(schemeName)
.stream()
.distinct()
.flatMap(Optionals::toStream)
.map(TargetNode::getBuildTarget)
.map(buildTargetToPBXTarget::get)
.filter(Objects::nonNull)
.collect(MoreCollectors.toImmutableSet());
ImmutableSet<PBXTarget> orderedBuildTestTargets =
buildForTestNodes
.get(schemeName)
.stream()
.map(TargetNode::getBuildTarget)
.map(buildTargetToPBXTarget::get)
.filter(Objects::nonNull)
.collect(MoreCollectors.toImmutableSet());
ImmutableSet<PBXTarget> orderedRunTestTargets =
ungroupedTests
.get(schemeName)
.stream()
.map(TargetNode::getBuildTarget)
.map(buildTargetToPBXTarget::get)
.filter(Objects::nonNull)
.collect(MoreCollectors.toImmutableSet());
Optional<String> runnablePath = schemeConfigArg.getExplicitRunnablePath();
Optional<String> remoteRunnablePath;
if (schemeConfigArg.getIsRemoteRunnable().orElse(false)) {
// XXX TODO(beng): Figure out the actual name of the binary to launch
remoteRunnablePath = Optional.of("/" + workspaceName);
} else {
remoteRunnablePath = Optional.empty();
}
SchemeGenerator schemeGenerator =
new SchemeGenerator(
rootCell.getFilesystem(),
schemeConfigArg.getSrcTarget().map(buildTargetToPBXTarget::get),
orderedBuildTargets,
orderedBuildTestTargets,
orderedRunTestTargets,
schemeName,
combinedProject
? outputDirectory
: outputDirectory.resolve(workspaceName + ".xcworkspace"),
parallelizeBuild,
runnablePath,
remoteRunnablePath,
XcodeWorkspaceConfigDescription.getActionConfigNamesFromArg(workspaceArguments),
targetToProjectPathMap,
schemeConfigArg.getLaunchStyle().orElse(XCScheme.LaunchAction.LaunchStyle.AUTO));
schemeGenerator.writeScheme();
schemeGenerators.put(schemeName, schemeGenerator);
}
}
}