| /* |
| * 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"); |
| } |
| } |
| } |
| } |