blob: eade823fa4ab6efeb4c808a2d2c1ebebf5b008c4 [file] [log] [blame]
/*
* Copyright 2014-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.apple;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget;
import com.facebook.buck.graph.TopologicalSort;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.HasBuildTarget;
import com.facebook.buck.model.HasTests;
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
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.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
public class WorkspaceAndProjectGenerator {
private static final Logger LOG = Logger.get(WorkspaceAndProjectGenerator.class);
private final ProjectFilesystem projectFilesystem;
private final TargetGraph projectGraph;
private final TargetNode<XcodeWorkspaceConfigDescription.Arg> workspaceTargetNode;
private final ImmutableSet<ProjectGenerator.Option> projectGeneratorOptions;
private final ImmutableSet<TargetNode<AppleTestDescription.Arg>> extraTestBundleTargetNodes;
private final boolean combinedProject;
private ImmutableSet<TargetNode<AppleTestDescription.Arg>> groupableTests = ImmutableSet.of();
private Optional<ProjectGenerator> combinedProjectGenerator;
private Optional<ProjectGenerator> combinedTestsProjectGenerator = Optional.absent();
private Optional<SchemeGenerator> schemeGenerator = Optional.absent();
private String buildFileName;
public WorkspaceAndProjectGenerator(
ProjectFilesystem projectFilesystem,
TargetGraph projectGraph,
TargetNode<XcodeWorkspaceConfigDescription.Arg> workspaceTargetNode,
Set<ProjectGenerator.Option> projectGeneratorOptions,
boolean combinedProject,
String buildFileName) {
this.projectFilesystem = projectFilesystem;
this.projectGraph = projectGraph;
this.workspaceTargetNode = workspaceTargetNode;
this.projectGeneratorOptions = ImmutableSet.copyOf(projectGeneratorOptions);
this.combinedProject = combinedProject;
this.buildFileName = buildFileName;
this.combinedProjectGenerator = Optional.absent();
extraTestBundleTargetNodes = getExtraTestTargetNodes(
projectGraph, workspaceTargetNode.getConstructorArg().extraTests.get());
}
@VisibleForTesting
Optional<ProjectGenerator> getCombinedProjectGenerator() {
return combinedProjectGenerator;
}
@VisibleForTesting
Optional<SchemeGenerator> getSchemeGenerator() {
return schemeGenerator;
}
/**
* Return the project generator to generate the combined test bundles.
* This is only set when generating separated projects.
*/
@VisibleForTesting
Optional<ProjectGenerator> getCombinedTestsProjectGenerator() {
return combinedTestsProjectGenerator;
}
/**
* Set the tests that can be grouped. These tests will always be generated as static libraries,
* and linked into synthetic test targets.
*
* While it may seem unnecessary to do this for tests which may not be able to share a bundle with
* any other test, note that WorkspaceAndProjectGenerator only has a limited view of all the tests
* that exists, but the generated projects are shared amongst all Workspaces.
*/
public WorkspaceAndProjectGenerator setGroupableTests(
Set<TargetNode<AppleTestDescription.Arg>> tests) {
groupableTests = ImmutableSet.copyOf(tests);
return this;
}
public Path generateWorkspaceAndDependentProjects(
Map<Path, ProjectGenerator> projectGenerators)
throws IOException {
LOG.debug("Generating workspace for target %s", workspaceTargetNode);
String workspaceName = XcodeWorkspaceConfigDescription.getWorkspaceNameFromArg(
workspaceTargetNode.getConstructorArg());
Path outputDirectory;
if (combinedProject) {
workspaceName += "-Combined";
outputDirectory =
BuildTargets.getGenPath(workspaceTargetNode.getBuildTarget(), "%s").getParent();
} else {
outputDirectory = workspaceTargetNode.getBuildTarget().getBasePath();
}
WorkspaceGenerator workspaceGenerator = new WorkspaceGenerator(
projectFilesystem,
workspaceName,
outputDirectory);
ImmutableSet<TargetNode<?>> orderedTargetNodes;
if (workspaceTargetNode.getConstructorArg().srcTarget.isPresent()) {
orderedTargetNodes = AppleBuildRules.getSchemeBuildableTargetNodes(
projectGraph,
Preconditions.checkNotNull(
projectGraph.get(
workspaceTargetNode.getConstructorArg().srcTarget.get().getBuildTarget())));
} else {
orderedTargetNodes = ImmutableSet.of();
}
ImmutableSet<TargetNode<AppleTestDescription.Arg>> selectedTests = getOrderedTestNodes(
projectGraph,
orderedTargetNodes,
extraTestBundleTargetNodes);
ImmutableList<TargetNode<?>> buildForTestNodes =
TopologicalSort.sort(
projectGraph,
Predicates.in(getTransitiveDepsAndInputs(selectedTests, orderedTargetNodes)));
GroupedTestResults groupedTestResults = groupTests(selectedTests);
Iterable<PBXTarget> synthesizedCombinedTestTargets = ImmutableList.of();
ImmutableSet<BuildTarget> targetsInRequiredProjects = FluentIterable
.from(orderedTargetNodes)
.append(buildForTestNodes)
.transform(HasBuildTarget.TO_TARGET)
.toSet();
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMapBuilder =
ImmutableMap.builder();
ImmutableMap.Builder<PBXTarget, Path> targetToProjectPathMapBuilder =
ImmutableMap.builder();
if (combinedProject) {
LOG.debug("Generating a combined project");
ProjectGenerator generator = new ProjectGenerator(
projectGraph,
targetsInRequiredProjects,
projectFilesystem,
outputDirectory,
workspaceName,
buildFileName,
projectGeneratorOptions)
.setAdditionalCombinedTestTargets(groupedTestResults.groupedTests)
.setTestsToGenerateAsStaticLibraries(groupableTests);
combinedProjectGenerator = Optional.of(generator);
generator.createXcodeProjects();
workspaceGenerator.addFilePath(generator.getProjectPath(), Optional.<Path>absent());
buildTargetToPbxTargetMapBuilder.putAll(generator.getBuildTargetToGeneratedTargetMap());
for (PBXTarget target : generator.getBuildTargetToGeneratedTargetMap().values()) {
targetToProjectPathMapBuilder.put(target, generator.getProjectPath());
}
synthesizedCombinedTestTargets = generator.getBuildableCombinedTestTargets();
for (PBXTarget target : synthesizedCombinedTestTargets) {
targetToProjectPathMapBuilder.put(target, generator.getProjectPath());
}
} else {
ImmutableMultimap.Builder<Path, BuildTarget> projectDirectoryToBuildTargetsBuilder =
ImmutableMultimap.builder();
for (TargetNode<?> targetNode : projectGraph.getNodes()) {
BuildTarget buildTarget = targetNode.getBuildTarget();
projectDirectoryToBuildTargetsBuilder.put(buildTarget.getBasePath(), buildTarget);
}
ImmutableMultimap<Path, BuildTarget> projectDirectoryToBuildTargets =
projectDirectoryToBuildTargetsBuilder.build();
for (Path projectDirectory : projectDirectoryToBuildTargets.keySet()) {
ImmutableSet<BuildTarget> rules = filterRulesForProjectDirectory(
projectGraph,
ImmutableSet.copyOf(projectDirectoryToBuildTargets.get(projectDirectory)));
if (Sets.intersection(targetsInRequiredProjects, rules).isEmpty()) {
continue;
}
ProjectGenerator generator = projectGenerators.get(projectDirectory);
if (generator == null) {
LOG.debug("Generating project for directory %s with targets %s", projectDirectory, rules);
String projectName;
if (projectDirectory.getNameCount() == 0) {
// 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,
rules,
projectFilesystem,
projectDirectory,
projectName,
buildFileName,
projectGeneratorOptions)
.setTestsToGenerateAsStaticLibraries(groupableTests);
generator.createXcodeProjects();
projectGenerators.put(projectDirectory, generator);
} else {
LOG.debug("Already generated project for target %s, skipping", projectDirectory);
}
workspaceGenerator.addFilePath(generator.getProjectPath());
buildTargetToPbxTargetMapBuilder.putAll(generator.getBuildTargetToGeneratedTargetMap());
for (PBXTarget target : generator.getBuildTargetToGeneratedTargetMap().values()) {
targetToProjectPathMapBuilder.put(target, generator.getProjectPath());
}
}
if (!groupedTestResults.groupedTests.isEmpty()) {
ProjectGenerator combinedTestsProjectGenerator = new ProjectGenerator(
projectGraph,
ImmutableSortedSet.<BuildTarget>of(),
projectFilesystem,
BuildTargets.getGenPath(workspaceTargetNode.getBuildTarget(), "%s-CombinedTestBundles"),
"_CombinedTestBundles",
buildFileName,
projectGeneratorOptions);
combinedTestsProjectGenerator
.setAdditionalCombinedTestTargets(groupedTestResults.groupedTests)
.createXcodeProjects();
workspaceGenerator.addFilePath(combinedTestsProjectGenerator.getProjectPath());
for (PBXTarget target :
combinedTestsProjectGenerator.getBuildTargetToGeneratedTargetMap().values()) {
targetToProjectPathMapBuilder.put(target, combinedTestsProjectGenerator.getProjectPath());
}
synthesizedCombinedTestTargets =
combinedTestsProjectGenerator.getBuildableCombinedTestTargets();
for (PBXTarget target : synthesizedCombinedTestTargets) {
targetToProjectPathMapBuilder.put(target, combinedTestsProjectGenerator.getProjectPath());
}
this.combinedTestsProjectGenerator = Optional.of(combinedTestsProjectGenerator);
}
}
Path workspacePath = workspaceGenerator.writeWorkspace();
final Map<BuildTarget, PBXTarget> buildTargetToTarget =
buildTargetToPbxTargetMapBuilder.build();
Function<TargetNode<?>, PBXTarget> targetNodeToPBXTargetTransformer =
new Function<TargetNode<?>, PBXTarget>() {
@Override
public PBXTarget apply(TargetNode<?> input) {
return Preconditions.checkNotNull(buildTargetToTarget.get(input.getBuildTarget()));
}
};
SchemeGenerator schemeGenerator = new SchemeGenerator(
projectFilesystem,
workspaceTargetNode.getConstructorArg().srcTarget.transform(
Functions.forMap(buildTargetToTarget)),
Iterables.transform(orderedTargetNodes, targetNodeToPBXTargetTransformer),
FluentIterable
.from(buildForTestNodes)
.transform(targetNodeToPBXTargetTransformer)
.append(synthesizedCombinedTestTargets),
FluentIterable
.from(groupedTestResults.ungroupedTests)
.transform(targetNodeToPBXTargetTransformer)
.append(synthesizedCombinedTestTargets),
workspaceName,
outputDirectory.resolve(workspaceName + ".xcworkspace"),
XcodeWorkspaceConfigDescription.getActionConfigNamesFromArg(
workspaceTargetNode.getConstructorArg()),
targetToProjectPathMapBuilder.build());
schemeGenerator.writeScheme();
this.schemeGenerator = Optional.of(schemeGenerator);
return workspacePath;
}
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.getType() == AppleBundleDescription.TYPE) {
AppleBundleDescription.Arg appleBundleDescriptionArg =
(AppleBundleDescription.Arg) projectTargetNode.getConstructorArg();
// We don't support apple_bundle rules referring to apple_binary rules
// outside their current directory.
Preconditions.checkState(
appleBundleDescriptionArg.binary.getBasePath().equals(
projectTargetNode.getBuildTarget().getBasePath()),
"apple_bundle target %s contains reference to binary %s outside base path %s",
projectTargetNode.getBuildTarget(),
appleBundleDescriptionArg.binary,
projectTargetNode.getBuildTarget().getBasePath());
binaryTargetsInsideBundlesBuilder.add(appleBundleDescriptionArg.binary);
}
}
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 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<AppleTestDescription.Arg>> getOrderedTestNodes(
TargetGraph targetGraph,
ImmutableSet<TargetNode<?>> orderedTargetNodes,
ImmutableSet<TargetNode<AppleTestDescription.Arg>> extraTestBundleTargets) {
LOG.debug("Getting ordered test target nodes for %s", orderedTargetNodes);
ImmutableSet.Builder<TargetNode<AppleTestDescription.Arg>> testsBuilder =
ImmutableSet.builder();
if (projectGeneratorOptions.contains(ProjectGenerator.Option.INCLUDE_TESTS)) {
for (TargetNode<?> node : orderedTargetNodes) {
if (!(node.getConstructorArg() instanceof HasTests)) {
continue;
}
for (BuildTarget explicitTestTarget : ((HasTests) node.getConstructorArg()).getTests()) {
TargetNode<?> explicitTestNode = targetGraph.get(explicitTestTarget);
if (explicitTestNode != null) {
Optional<TargetNode<AppleTestDescription.Arg>> castedNode =
explicitTestNode.castArg(AppleTestDescription.Arg.class);
if (castedNode.isPresent()) {
testsBuilder.add(castedNode.get());
} else {
throw new HumanReadableException(
"Test target specified in '%s' is not a test: '%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<AppleTestDescription.Arg> extraTestTarget : extraTestBundleTargets) {
testsBuilder.add(extraTestTarget);
}
return testsBuilder.build();
}
/**
* Find transitive dependencies of inputs for building.
*
* @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 ImmutableSet<TargetNode<?>> getTransitiveDepsAndInputs(
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,
AppleBuildRules.RecursiveDependenciesMode.BUILDING,
input,
Optional.<ImmutableSet<BuildRuleType>>absent());
}
})
.append(nodes)
.filter(
new Predicate<TargetNode<?>>() {
@Override
public boolean apply(TargetNode<?> input) {
return !excludes.contains(input) &&
AppleBuildRules.isXcodeTargetBuildRuleType(input.getType());
}
})
.toSet();
}
private static ImmutableSet<TargetNode<AppleTestDescription.Arg>> getExtraTestTargetNodes(
TargetGraph graph,
Iterable<BuildTarget> targets) {
ImmutableSet.Builder<TargetNode<AppleTestDescription.Arg>> builder = ImmutableSet.builder();
for (TargetNode<?> node : graph.getAll(targets)) {
Optional<TargetNode<AppleTestDescription.Arg>> castedNode =
node.castArg(AppleTestDescription.Arg.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 GroupedTestResults groupTests(ImmutableSet<TargetNode<AppleTestDescription.Arg>> tests) {
// Put tests in groups.
ImmutableMultimap.Builder<AppleTestBundleParamsKey, TargetNode<AppleTestDescription.Arg>>
groupsBuilder = ImmutableMultimap.builder();
ImmutableSet.Builder<TargetNode<AppleTestDescription.Arg>> ungroupedTestsBuilder =
ImmutableSet.builder();
for (TargetNode<AppleTestDescription.Arg> test : tests) {
if (groupableTests.contains(test)) {
Preconditions.checkState(
test.getConstructorArg().canGroup(),
"Groupable test should actually be groupable.");
groupsBuilder.put(
AppleTestBundleParamsKey.fromAppleTestDescriptionArg(test.getConstructorArg()),
test);
} else {
ungroupedTestsBuilder.add(test);
}
}
return new GroupedTestResults(groupsBuilder.build(), ungroupedTestsBuilder.build());
}
private static class GroupedTestResults {
public final ImmutableMultimap<AppleTestBundleParamsKey, TargetNode<AppleTestDescription.Arg>>
groupedTests;
public final ImmutableSet<TargetNode<AppleTestDescription.Arg>> ungroupedTests;
protected GroupedTestResults(
ImmutableMultimap<AppleTestBundleParamsKey, TargetNode<AppleTestDescription.Arg>>
groupedTests,
ImmutableSet<TargetNode<AppleTestDescription.Arg>> ungroupedTests) {
this.groupedTests = groupedTests;
this.ungroupedTests = ungroupedTests;
}
}
}