blob: c8b9a345d3369e46d969a67cb821f718c8f15a70 [file] [log] [blame]
/*
* Copyright 2013-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.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSString;
import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildFile;
import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXFileReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXFrameworksBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXGroup;
import com.facebook.buck.apple.xcode.xcodeproj.PBXHeadersBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXNativeTarget;
import com.facebook.buck.apple.xcode.xcodeproj.PBXProject;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXResourcesBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXShellScriptBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXSourcesBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget;
import com.facebook.buck.apple.xcode.xcodeproj.PBXVariantGroup;
import com.facebook.buck.apple.xcode.xcodeproj.SourceTreePath;
import com.facebook.buck.log.Logger;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.rules.coercer.SourceWithFlags;
import com.facebook.buck.shell.GenruleDescription;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Configures a PBXProject by adding a PBXNativeTarget and its associated dependencies into a
* PBXProject object graph.
*/
public class NewNativeTargetProjectMutator {
private static final Logger LOG = Logger.get(NewNativeTargetProjectMutator.class);
public static class Result {
public final PBXNativeTarget target;
public final PBXGroup targetGroup;
private Result(PBXNativeTarget target, PBXGroup targetGroup) {
this.target = target;
this.targetGroup = targetGroup;
}
}
private final PathRelativizer pathRelativizer;
private final SourcePathResolver sourcePathResolver;
private PBXTarget.ProductType productType = PBXTarget.ProductType.BUNDLE;
private Path productOutputPath = Paths.get("");
private String productName = "";
private String targetName = "";
private ImmutableList<String> targetGroupPath = ImmutableList.of();
private Optional<String> gid = Optional.absent();
private ImmutableSet<SourceWithFlags> sourcesWithFlags = ImmutableSet.of();
private ImmutableSet<SourcePath> publicHeaders = ImmutableSet.of();
private ImmutableSet<SourcePath> privateHeaders = ImmutableSet.of();
private boolean shouldGenerateCopyHeadersPhase = true;
private ImmutableSet<FrameworkPath> frameworks = ImmutableSet.of();
private ImmutableSet<PBXFileReference> archives = ImmutableSet.of();
private ImmutableSet<AppleResourceDescription.Arg> resources = ImmutableSet.of();
private ImmutableSet<AppleAssetCatalogDescription.Arg> assetCatalogs = ImmutableSet.of();
private Path assetCatalogBuildScript = Paths.get("");
private Iterable<TargetNode<?>> preBuildRunScriptPhases = ImmutableList.of();
private Iterable<TargetNode<?>> postBuildRunScriptPhases = ImmutableList.of();
public NewNativeTargetProjectMutator(
PathRelativizer pathRelativizer,
SourcePathResolver sourcePathResolver) {
this.pathRelativizer = pathRelativizer;
this.sourcePathResolver = sourcePathResolver;
}
/**
* Set product related configuration.
*
* @param productType declared product type
* @param productName product display name
* @param productOutputPath build output relative product path.
*/
public NewNativeTargetProjectMutator setProduct(
PBXNativeTarget.ProductType productType,
String productName,
Path productOutputPath) {
this.productName = productName;
this.productType = productType;
this.productOutputPath = productOutputPath;
return this;
}
public NewNativeTargetProjectMutator setGid(Optional<String> gid) {
this.gid = gid;
return this;
}
public NewNativeTargetProjectMutator setTargetName(String targetName) {
this.targetName = targetName;
return this;
}
public NewNativeTargetProjectMutator setTargetGroupPath(ImmutableList<String> targetGroupPath) {
this.targetGroupPath = targetGroupPath;
return this;
}
public NewNativeTargetProjectMutator setSourcesWithFlags(
Iterable<SourceWithFlags> sourcesWithFlags) {
this.sourcesWithFlags = ImmutableSet.copyOf(sourcesWithFlags);
return this;
}
public NewNativeTargetProjectMutator setPublicHeaders(
Iterable<SourcePath> publicHeaders) {
this.publicHeaders = ImmutableSet.copyOf(publicHeaders);
return this;
}
public NewNativeTargetProjectMutator setPrivateHeaders(
Iterable<SourcePath> privateHeaders) {
this.privateHeaders = ImmutableSet.copyOf(privateHeaders);
return this;
}
public NewNativeTargetProjectMutator setShouldGenerateCopyHeadersPhase(boolean value) {
this.shouldGenerateCopyHeadersPhase = value;
return this;
}
public NewNativeTargetProjectMutator setFrameworks(Set<FrameworkPath> frameworks) {
this.frameworks = ImmutableSet.copyOf(frameworks);
return this;
}
public NewNativeTargetProjectMutator setArchives(Set<PBXFileReference> archives) {
this.archives = ImmutableSet.copyOf(archives);
return this;
}
public NewNativeTargetProjectMutator setResources(Set<AppleResourceDescription.Arg> resources) {
this.resources = ImmutableSet.copyOf(resources);
return this;
}
public NewNativeTargetProjectMutator setPreBuildRunScriptPhases(Iterable<TargetNode<?>> phases) {
preBuildRunScriptPhases = phases;
return this;
}
public NewNativeTargetProjectMutator setPostBuildRunScriptPhases(Iterable<TargetNode<?>> phases) {
postBuildRunScriptPhases = phases;
return this;
}
/**
* @param assetCatalogBuildScript Path of the asset catalog build script relative to repo root.
* @param assetCatalogs List of asset catalog targets.
*/
public NewNativeTargetProjectMutator setAssetCatalogs(
Path assetCatalogBuildScript,
Set<AppleAssetCatalogDescription.Arg> assetCatalogs) {
this.assetCatalogBuildScript = assetCatalogBuildScript;
this.assetCatalogs = ImmutableSet.copyOf(assetCatalogs);
return this;
}
public Result buildTargetAndAddToProject(PBXProject project)
throws NoSuchBuildTargetException {
PBXNativeTarget target = new PBXNativeTarget(targetName, productType);
PBXGroup targetGroup = project.getMainGroup();
for (String groupPathPart : targetGroupPath) {
targetGroup = targetGroup.getOrCreateChildGroupByName(groupPathPart);
}
targetGroup = targetGroup.getOrCreateChildGroupByName(targetName);
if (gid.isPresent()) {
target.setGlobalID(gid.get());
}
// Phases
addRunScriptBuildPhases(target, preBuildRunScriptPhases);
addPhasesAndGroupsForSources(target, targetGroup);
addFrameworksBuildPhase(project, target);
addResourcesBuildPhase(target, targetGroup);
addAssetCatalogBuildPhase(target, targetGroup);
addRunScriptBuildPhases(target, postBuildRunScriptPhases);
// Product
PBXGroup productsGroup = project.getMainGroup().getOrCreateChildGroupByName("Products");
PBXFileReference productReference = productsGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(PBXReference.SourceTree.BUILT_PRODUCTS_DIR, productOutputPath));
target.setProductName(productName);
target.setProductReference(productReference);
project.getTargets().add(target);
return new Result(target, targetGroup);
}
private void addPhasesAndGroupsForSources(PBXNativeTarget target, PBXGroup targetGroup) {
PBXGroup sourcesGroup = targetGroup.getOrCreateChildGroupByName("Sources");
// Sources groups stay in the order in which they're declared in the BUCK file.
sourcesGroup.setSortPolicy(PBXGroup.SortPolicy.UNSORTED);
PBXSourcesBuildPhase sourcesBuildPhase = new PBXSourcesBuildPhase();
PBXHeadersBuildPhase headersBuildPhase = new PBXHeadersBuildPhase();
traverseGroupsTreeAndHandleSources(
sourcesGroup,
sourcesBuildPhase,
// We still want to create groups for header files even if header build phases
// are replaced with header maps.
!shouldGenerateCopyHeadersPhase
? Optional.<PBXHeadersBuildPhase>absent()
: Optional.of(headersBuildPhase),
RuleUtils.createGroupsFromSourcePaths(
sourcePathResolver,
sourcesWithFlags,
publicHeaders,
privateHeaders));
if (!sourcesBuildPhase.getFiles().isEmpty()) {
target.getBuildPhases().add(sourcesBuildPhase);
}
if (!headersBuildPhase.getFiles().isEmpty()) {
target.getBuildPhases().add(headersBuildPhase);
}
}
private void traverseGroupsTreeAndHandleSources(
final PBXGroup sourcesGroup,
final PBXSourcesBuildPhase sourcesBuildPhase,
final Optional<PBXHeadersBuildPhase> headersBuildPhase,
Iterable<GroupedSource> groupedSources) {
GroupedSource.Visitor visitor = new GroupedSource.Visitor() {
@Override
public void visitSourceWithFlags(SourceWithFlags sourceWithFlags) {
addSourcePathToSourcesBuildPhase(
sourceWithFlags,
sourcesGroup,
sourcesBuildPhase);
}
@Override
public void visitPublicHeader(SourcePath publicHeader) {
addSourcePathToHeadersBuildPhase(
publicHeader,
sourcesGroup,
headersBuildPhase,
HeaderVisibility.PUBLIC);
}
@Override
public void visitPrivateHeader(SourcePath privateHeader) {
addSourcePathToHeadersBuildPhase(
privateHeader,
sourcesGroup,
headersBuildPhase,
HeaderVisibility.PROJECT);
}
@Override
public void visitSourceGroup(
String sourceGroupName, List<GroupedSource> sourceGroup) {
PBXGroup newSourceGroup = sourcesGroup.getOrCreateChildGroupByName(sourceGroupName);
// Sources groups stay in the order in which they're in the GroupedSource.
newSourceGroup.setSortPolicy(PBXGroup.SortPolicy.UNSORTED);
traverseGroupsTreeAndHandleSources(
newSourceGroup,
sourcesBuildPhase,
headersBuildPhase,
sourceGroup);
}
};
for (GroupedSource groupedSource : groupedSources) {
groupedSource.visit(visitor);
}
}
private void addSourcePathToSourcesBuildPhase(
SourceWithFlags sourceWithFlags,
PBXGroup sourcesGroup,
PBXSourcesBuildPhase sourcesBuildPhase) {
PBXFileReference fileReference = sourcesGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(
sourcePathResolver.getPath(sourceWithFlags.getSourcePath()))));
PBXBuildFile buildFile = new PBXBuildFile(fileReference);
sourcesBuildPhase.getFiles().add(buildFile);
List<String> customFlags = sourceWithFlags.getFlags();
if (!customFlags.isEmpty()) {
NSDictionary settings = new NSDictionary();
settings.put("COMPILER_FLAGS", Joiner.on(' ').join(customFlags));
buildFile.setSettings(Optional.of(settings));
}
LOG.verbose(
"Added source path %s to group %s, flags %s, PBXFileReference %s",
sourceWithFlags,
sourcesGroup.getName(),
customFlags,
fileReference);
}
private void addSourcePathToHeadersBuildPhase(
SourcePath headerPath,
PBXGroup headersGroup,
Optional<PBXHeadersBuildPhase> headersBuildPhase,
HeaderVisibility visibility) {
PBXFileReference fileReference = headersGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputPathToSourcePath(headerPath)));
PBXBuildFile buildFile = new PBXBuildFile(fileReference);
if (visibility != HeaderVisibility.PROJECT) {
NSDictionary settings = new NSDictionary();
settings.put(
"ATTRIBUTES",
new NSArray(new NSString(visibility.toXcodeAttribute())));
buildFile.setSettings(Optional.of(settings));
} else {
buildFile.setSettings(Optional.<NSDictionary>absent());
}
if (headersBuildPhase.isPresent()) {
headersBuildPhase.get().getFiles().add(buildFile);
LOG.verbose(
"Added header path %s to headers group %s, PBXFileReference %s",
headerPath,
headersGroup.getName(),
fileReference);
} else {
LOG.verbose(
"Skipped header path %s to headers group %s, PBXFileReference %s",
headerPath,
headersGroup.getName(),
fileReference);
}
}
private void addFrameworksBuildPhase(PBXProject project, PBXNativeTarget target) {
if (frameworks.isEmpty() && archives.isEmpty()) {
return;
}
PBXGroup sharedFrameworksGroup =
project.getMainGroup().getOrCreateChildGroupByName("Frameworks");
PBXFrameworksBuildPhase frameworksBuildPhase = new PBXFrameworksBuildPhase();
target.getBuildPhases().add(frameworksBuildPhase);
for (FrameworkPath framework : frameworks) {
SourceTreePath sourceTreePath;
if (framework.getSourceTreePath().isPresent()) {
sourceTreePath = framework.getSourceTreePath().get();
} else if (framework.getSourcePath().isPresent()) {
sourceTreePath = new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputPathToSourcePath(framework.getSourcePath().get()));
} else {
throw new RuntimeException();
}
PBXFileReference fileReference =
sharedFrameworksGroup.getOrCreateFileReferenceBySourceTreePath(sourceTreePath);
frameworksBuildPhase.getFiles().add(new PBXBuildFile(fileReference));
}
for (PBXFileReference archive : archives) {
frameworksBuildPhase.getFiles().add(new PBXBuildFile(archive));
}
}
private void addResourcesBuildPhase(PBXNativeTarget target, PBXGroup targetGroup) {
if (resources.isEmpty()) {
return;
}
PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources");
PBXBuildPhase phase = new PBXResourcesBuildPhase();
target.getBuildPhases().add(phase);
for (AppleResourceDescription.Arg resource : resources) {
Iterable<Path> paths = Iterables.concat(
sourcePathResolver.getAllPaths(resource.files),
resource.dirs);
for (Path path : paths) {
PBXFileReference fileReference = resourcesGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(path)));
PBXBuildFile buildFile = new PBXBuildFile(fileReference);
phase.getFiles().add(buildFile);
}
for (Map.Entry<String, Map<String, SourcePath>> virtualOutputEntry :
resource.variants.get().entrySet()) {
String variantName = Paths.get(virtualOutputEntry.getKey()).getFileName().toString();
PBXVariantGroup variantGroup =
resourcesGroup.getOrCreateChildVariantGroupByName(variantName);
PBXBuildFile buildFile = new PBXBuildFile(variantGroup);
phase.getFiles().add(buildFile);
for (Map.Entry<String, SourcePath> childVirtualNameEntry :
virtualOutputEntry.getValue().entrySet()) {
SourceTreePath sourceTreePath = new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputPathToSourcePath(childVirtualNameEntry.getValue()));
variantGroup.getOrCreateVariantFileReferenceByNameAndSourceTreePath(
childVirtualNameEntry.getKey(),
sourceTreePath);
}
}
}
LOG.debug("Added resources build phase %s", phase);
}
private void addAssetCatalogBuildPhase(PBXNativeTarget target, PBXGroup targetGroup) {
if (assetCatalogs.isEmpty()) {
return;
}
// Asset catalogs go in the resources group also.
PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources");
// Some asset catalogs should be copied to their sibling bundles, while others use the default
// output format (which may be to copy individual files to the root resource output path or to
// be archived in Assets.car if it is supported by the target platform version).
ImmutableList.Builder<String> commonAssetCatalogsBuilder = ImmutableList.builder();
ImmutableList.Builder<String> assetCatalogsToSplitIntoBundlesBuilder =
ImmutableList.builder();
for (AppleAssetCatalogDescription.Arg assetCatalog : assetCatalogs) {
for (Path dir : assetCatalog.dirs) {
Path pathRelativeToProjectRoot = pathRelativizer.outputDirToRootRelative(dir);
resourcesGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativeToProjectRoot));
LOG.debug("Resolved asset catalog path %s, result %s", dir, pathRelativeToProjectRoot);
String bundlePath = "$PROJECT_DIR/" + pathRelativeToProjectRoot.toString();
if (assetCatalog.getCopyToBundles()) {
assetCatalogsToSplitIntoBundlesBuilder.add(bundlePath);
} else {
commonAssetCatalogsBuilder.add(bundlePath);
}
}
}
ImmutableList<String> commonAssetCatalogs = commonAssetCatalogsBuilder.build();
ImmutableList<String> assetCatalogsToSplitIntoBundles =
assetCatalogsToSplitIntoBundlesBuilder.build();
// Map asset catalog paths to their shell script arguments relative to the project's root
Path buildScript = pathRelativizer.outputDirToRootRelative(assetCatalogBuildScript);
StringBuilder scriptBuilder = new StringBuilder("set -e\n");
if (commonAssetCatalogs.size() != 0) {
scriptBuilder
.append("\"${PROJECT_DIR}/\"")
.append(buildScript.toString())
.append(" ")
.append(Joiner.on(' ').join(commonAssetCatalogs))
.append("\n");
}
if (assetCatalogsToSplitIntoBundles.size() != 0) {
scriptBuilder
.append("\"${PROJECT_DIR}/\"")
.append(buildScript.toString())
.append(" -b ")
.append(Joiner.on(' ').join(assetCatalogsToSplitIntoBundles))
.append("\n");
}
PBXShellScriptBuildPhase phase = new PBXShellScriptBuildPhase();
target.getBuildPhases().add(phase);
phase.setShellScript(scriptBuilder.toString());
LOG.debug("Added asset catalog build phase %s", phase);
}
private void addRunScriptBuildPhases(
PBXNativeTarget target,
Iterable<TargetNode<?>> nodes) throws NoSuchBuildTargetException{
for (TargetNode<?> node : nodes) {
// TODO(user): Check and validate dependencies of the script. If it depends on libraries etc.
// we can't handle it currently.
PBXShellScriptBuildPhase shellScriptBuildPhase = new PBXShellScriptBuildPhase();
target.getBuildPhases().add(shellScriptBuildPhase);
if (GenruleDescription.TYPE.equals(node.getType())) {
GenruleDescription.Arg arg = (GenruleDescription.Arg) node.getConstructorArg();
for (Path path : sourcePathResolver.getAllPaths(arg.srcs.get())) {
shellScriptBuildPhase.getInputPaths().add(
pathRelativizer.outputDirToRootRelative(path).toString());
}
if (arg.cmd.isPresent() && !arg.cmd.get().isEmpty()) {
shellScriptBuildPhase.setShellScript(arg.cmd.get());
} else if (arg.bash.isPresent() || arg.cmdExe.isPresent()) {
throw new IllegalStateException("Shell script phase only supports cmd for genrule.");
}
if (!arg.out.isEmpty()) {
shellScriptBuildPhase.getOutputPaths().add(arg.out);
}
} else if (IosPostprocessResourcesDescription.TYPE.equals(node.getType())) {
IosPostprocessResourcesDescription.Arg arg =
(IosPostprocessResourcesDescription.Arg) node.getConstructorArg();
if (arg.cmd.isPresent()) {
shellScriptBuildPhase.setShellScript(arg.cmd.get());
}
} else {
// unreachable
throw new IllegalStateException("Invalid rule type for shell script build phase");
}
}
}
}