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

Java tutorial

Introduction

Here is the source code for com.facebook.buck.apple.project_generator.NewNativeTargetProjectMutator.java

Source

/*
 * 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.project_generator;

import com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSString;
import com.facebook.buck.apple.AppleAssetCatalogDescription;
import com.facebook.buck.apple.AppleHeaderVisibilities;
import com.facebook.buck.apple.AppleResourceDescription;
import com.facebook.buck.apple.AppleWrapperResourceArg;
import com.facebook.buck.apple.GroupedSource;
import com.facebook.buck.apple.RuleUtils;
import com.facebook.buck.apple.XcodePostbuildScriptDescription;
import com.facebook.buck.apple.XcodePrebuildScriptDescription;
import com.facebook.buck.apple.XcodeScriptDescriptionArg;
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.PBXVariantGroup;
import com.facebook.buck.apple.xcode.xcodeproj.ProductType;
import com.facebook.buck.apple.xcode.xcodeproj.SourceTreePath;
import com.facebook.buck.cxx.CxxSource;
import com.facebook.buck.cxx.HeaderVisibility;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.js.IosReactNativeLibraryDescription;
import com.facebook.buck.js.ReactNativeBundle;
import com.facebook.buck.js.ReactNativeLibraryArgs;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourceWithFlags;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.rules.coercer.FrameworkPath;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.common.io.Resources;

import org.stringtemplate.v4.ST;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Configures a PBXProject by adding a PBXNativeTarget and its associated dependencies into a
 * PBXProject object graph.
 */
class NewNativeTargetProjectMutator {
    private static final Logger LOG = Logger.get(NewNativeTargetProjectMutator.class);
    private static final String REACT_NATIVE_PACKAGE_TEMPLATE = "rn-package.st";

    public static class Result {
        public final PBXNativeTarget target;
        public final Optional<PBXGroup> targetGroup;

        private Result(PBXNativeTarget target, Optional<PBXGroup> targetGroup) {
            this.target = target;
            this.targetGroup = targetGroup;
        }
    }

    private final PathRelativizer pathRelativizer;
    private final Function<SourcePath, Path> sourcePathResolver;

    private ProductType productType = ProductType.BUNDLE;
    private Path productOutputPath = Paths.get("");
    private String productName = "";
    private String targetName = "";
    private ImmutableMap<CxxSource.Type, ImmutableList<String>> langPreprocessorFlags = ImmutableMap.of();
    private ImmutableList<String> targetGroupPath = ImmutableList.of();
    private ImmutableSet<SourceWithFlags> sourcesWithFlags = ImmutableSet.of();
    private ImmutableSet<SourcePath> extraXcodeSources = ImmutableSet.of();
    private ImmutableSet<SourcePath> publicHeaders = ImmutableSet.of();
    private ImmutableSet<SourcePath> privateHeaders = ImmutableSet.of();
    private Optional<SourcePath> prefixHeader = Optional.empty();
    private Optional<SourcePath> infoPlist = Optional.empty();
    private Optional<SourcePath> bridgingHeader = Optional.empty();
    private ImmutableSet<FrameworkPath> frameworks = ImmutableSet.of();
    private ImmutableSet<PBXFileReference> archives = ImmutableSet.of();
    private ImmutableSet<AppleResourceDescription.Arg> recursiveResources = ImmutableSet.of();
    private ImmutableSet<AppleResourceDescription.Arg> directResources = ImmutableSet.of();
    private ImmutableSet<AppleAssetCatalogDescription.Arg> recursiveAssetCatalogs = ImmutableSet.of();
    private ImmutableSet<AppleAssetCatalogDescription.Arg> directAssetCatalogs = ImmutableSet.of();
    private ImmutableSet<AppleWrapperResourceArg> wrapperResources = ImmutableSet.of();
    private Iterable<PBXShellScriptBuildPhase> preBuildRunScriptPhases = ImmutableList.of();
    private Iterable<PBXBuildPhase> copyFilesPhases = ImmutableList.of();
    private Iterable<PBXShellScriptBuildPhase> postBuildRunScriptPhases = ImmutableList.of();

    public NewNativeTargetProjectMutator(PathRelativizer pathRelativizer,
            Function<SourcePath, Path> 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(ProductType productType, String productName,
            Path productOutputPath) {
        this.productName = productName;
        this.productType = productType;
        this.productOutputPath = productOutputPath;
        return this;
    }

    public NewNativeTargetProjectMutator setTargetName(String targetName) {
        this.targetName = targetName;
        return this;
    }

    public NewNativeTargetProjectMutator setLangPreprocessorFlags(
            ImmutableMap<CxxSource.Type, ImmutableList<String>> langPreprocessorFlags) {
        this.langPreprocessorFlags = langPreprocessorFlags;
        return this;
    }

    public NewNativeTargetProjectMutator setTargetGroupPath(ImmutableList<String> targetGroupPath) {
        this.targetGroupPath = targetGroupPath;
        return this;
    }

    public NewNativeTargetProjectMutator setSourcesWithFlags(Set<SourceWithFlags> sourcesWithFlags) {
        this.sourcesWithFlags = ImmutableSet.copyOf(sourcesWithFlags);
        return this;
    }

    public NewNativeTargetProjectMutator setExtraXcodeSources(Set<SourcePath> extraXcodeSources) {
        this.extraXcodeSources = ImmutableSet.copyOf(extraXcodeSources);
        return this;
    }

    public NewNativeTargetProjectMutator setPublicHeaders(Set<SourcePath> publicHeaders) {
        this.publicHeaders = ImmutableSet.copyOf(publicHeaders);
        return this;
    }

    public NewNativeTargetProjectMutator setPrivateHeaders(Set<SourcePath> privateHeaders) {
        this.privateHeaders = ImmutableSet.copyOf(privateHeaders);
        return this;
    }

    public NewNativeTargetProjectMutator setPrefixHeader(Optional<SourcePath> prefixHeader) {
        this.prefixHeader = prefixHeader;
        return this;
    }

    public NewNativeTargetProjectMutator setInfoPlist(Optional<SourcePath> infoPlist) {
        this.infoPlist = infoPlist;
        return this;
    }

    public NewNativeTargetProjectMutator setBridgingHeader(Optional<SourcePath> bridgingHeader) {
        this.bridgingHeader = bridgingHeader;
        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 setRecursiveResources(
            Set<AppleResourceDescription.Arg> recursiveResources) {
        this.recursiveResources = ImmutableSet.copyOf(recursiveResources);
        return this;
    }

    public NewNativeTargetProjectMutator setDirectResources(
            ImmutableSet<AppleResourceDescription.Arg> directResources) {
        this.directResources = directResources;
        return this;
    }

    public NewNativeTargetProjectMutator setWrapperResources(
            ImmutableSet<AppleWrapperResourceArg> wrapperResources) {
        this.wrapperResources = wrapperResources;
        return this;
    }

    public NewNativeTargetProjectMutator setPreBuildRunScriptPhasesFromTargetNodes(
            Iterable<TargetNode<?, ?>> nodes) {
        preBuildRunScriptPhases = createScriptsForTargetNodes(nodes);
        return this;
    }

    public NewNativeTargetProjectMutator setPreBuildRunScriptPhases(Iterable<PBXShellScriptBuildPhase> phases) {
        preBuildRunScriptPhases = phases;
        return this;
    }

    public NewNativeTargetProjectMutator setCopyFilesPhases(Iterable<PBXBuildPhase> phases) {
        copyFilesPhases = phases;
        return this;
    }

    public NewNativeTargetProjectMutator setPostBuildRunScriptPhasesFromTargetNodes(
            Iterable<TargetNode<?, ?>> nodes) {
        postBuildRunScriptPhases = createScriptsForTargetNodes(nodes);
        return this;
    }

    /**
     * @param recursiveAssetCatalogs List of asset catalog targets of targetNode and dependencies of
     *                               targetNode.
     */
    public NewNativeTargetProjectMutator setRecursiveAssetCatalogs(
            Set<AppleAssetCatalogDescription.Arg> recursiveAssetCatalogs) {
        this.recursiveAssetCatalogs = ImmutableSet.copyOf(recursiveAssetCatalogs);
        return this;
    }

    /**
     * @param directAssetCatalogs List of asset catalog targets targetNode directly depends on
     */
    public NewNativeTargetProjectMutator setDirectAssetCatalogs(
            Set<AppleAssetCatalogDescription.Arg> directAssetCatalogs) {
        this.directAssetCatalogs = ImmutableSet.copyOf(directAssetCatalogs);
        return this;
    }

    public Result buildTargetAndAddToProject(PBXProject project, boolean addBuildPhases) {
        PBXNativeTarget target = new PBXNativeTarget(targetName);

        Optional<PBXGroup> optTargetGroup;
        if (addBuildPhases) {
            PBXGroup targetGroup = project.getMainGroup().getOrCreateDescendantGroupByPath(targetGroupPath);
            targetGroup = targetGroup.getOrCreateChildGroupByName(targetName);

            // Phases
            addRunScriptBuildPhases(target, preBuildRunScriptPhases);
            addPhasesAndGroupsForSources(target, targetGroup);
            addFrameworksBuildPhase(project, target);
            addResourcesFileReference(targetGroup);
            addResourcesBuildPhase(target, targetGroup);
            target.getBuildPhases().addAll((Collection<? extends PBXBuildPhase>) copyFilesPhases);
            addRunScriptBuildPhases(target, postBuildRunScriptPhases);

            optTargetGroup = Optional.of(targetGroup);
        } else {
            optTargetGroup = Optional.empty();
        }

        // Product

        PBXGroup productsGroup = project.getMainGroup().getOrCreateChildGroupByName("Products");
        PBXFileReference productReference = productsGroup.getOrCreateFileReferenceBySourceTreePath(
                new SourceTreePath(PBXReference.SourceTree.BUILT_PRODUCTS_DIR, productOutputPath,
                        Optional.empty()));
        target.setProductName(productName);
        target.setProductReference(productReference);
        target.setProductType(productType);

        project.getTargets().add(target);
        return new Result(target, optTargetGroup);
    }

    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,
                RuleUtils.createGroupsFromSourcePaths(pathRelativizer::outputPathToSourcePath, sourcesWithFlags,
                        extraXcodeSources, publicHeaders, privateHeaders));

        if (prefixHeader.isPresent()) {
            SourceTreePath prefixHeaderSourceTreePath = new SourceTreePath(PBXReference.SourceTree.GROUP,
                    pathRelativizer.outputPathToSourcePath(prefixHeader.get()), Optional.empty());
            sourcesGroup.getOrCreateFileReferenceBySourceTreePath(prefixHeaderSourceTreePath);
        }

        if (infoPlist.isPresent()) {
            SourceTreePath infoPlistSourceTreePath = new SourceTreePath(PBXReference.SourceTree.GROUP,
                    pathRelativizer.outputPathToSourcePath(infoPlist.get()), Optional.empty());
            sourcesGroup.getOrCreateFileReferenceBySourceTreePath(infoPlistSourceTreePath);
        }

        if (bridgingHeader.isPresent()) {
            SourceTreePath bridgingHeaderSourceTreePath = new SourceTreePath(PBXReference.SourceTree.GROUP,
                    pathRelativizer.outputPathToSourcePath(bridgingHeader.get()), Optional.empty());
            sourcesGroup.getOrCreateFileReferenceBySourceTreePath(bridgingHeaderSourceTreePath);
        }

        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, 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, HeaderVisibility.PUBLIC);
            }

            @Override
            public void visitPrivateHeader(SourcePath privateHeader) {
                addSourcePathToHeadersBuildPhase(privateHeader, sourcesGroup, HeaderVisibility.PRIVATE);
            }

            @Override
            public void visitSourceGroup(String sourceGroupName, Path sourceGroupPathRelativeToTarget,
                    List<GroupedSource> sourceGroup) {
                PBXGroup newSourceGroup = sourcesGroup.getOrCreateChildGroupByName(sourceGroupName);
                newSourceGroup.setSourceTree(PBXReference.SourceTree.SOURCE_ROOT);
                newSourceGroup.setPath(sourceGroupPathRelativeToTarget.toString());
                // Sources groups stay in the order in which they're in the GroupedSource.
                newSourceGroup.setSortPolicy(PBXGroup.SortPolicy.UNSORTED);
                traverseGroupsTreeAndHandleSources(newSourceGroup, sourcesBuildPhase, sourceGroup);
            }
        };
        for (GroupedSource groupedSource : groupedSources) {
            groupedSource.visit(visitor);
        }
    }

    private void addSourcePathToSourcesBuildPhase(SourceWithFlags sourceWithFlags, PBXGroup sourcesGroup,
            PBXSourcesBuildPhase sourcesBuildPhase) {
        SourceTreePath sourceTreePath = new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT,
                pathRelativizer.outputDirToRootRelative(sourcePathResolver.apply(sourceWithFlags.getSourcePath())),
                Optional.empty());
        PBXFileReference fileReference = sourcesGroup.getOrCreateFileReferenceBySourceTreePath(sourceTreePath);
        PBXBuildFile buildFile = new PBXBuildFile(fileReference);
        sourcesBuildPhase.getFiles().add(buildFile);

        ImmutableList<String> customLangPreprocessorFlags = ImmutableList.of();
        Optional<CxxSource.Type> sourceType = CxxSource.Type
                .fromExtension(Files.getFileExtension(sourceTreePath.toString()));
        if (sourceType.isPresent() && langPreprocessorFlags.containsKey(sourceType.get())) {
            customLangPreprocessorFlags = langPreprocessorFlags.get(sourceType.get());
        }

        ImmutableList<String> customFlags = ImmutableList
                .copyOf(Iterables.concat(customLangPreprocessorFlags, 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,
            HeaderVisibility visibility) {
        PBXFileReference fileReference = headersGroup
                .getOrCreateFileReferenceBySourceTreePath(new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT,
                        pathRelativizer.outputPathToSourcePath(headerPath), Optional.empty()));
        PBXBuildFile buildFile = new PBXBuildFile(fileReference);
        if (visibility != HeaderVisibility.PRIVATE) {
            NSDictionary settings = new NSDictionary();
            settings.put("ATTRIBUTES",
                    new NSArray(new NSString(AppleHeaderVisibilities.toXcodeAttribute(visibility))));
            buildFile.setSettings(Optional.of(settings));
        } else {
            buildFile.setSettings(Optional.empty());
        }
    }

    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()), Optional.empty());
            } 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 addResourcesFileReference(PBXGroup targetGroup) {
        ImmutableSet.Builder<Path> resourceFiles = ImmutableSet.builder();
        ImmutableSet.Builder<Path> resourceDirs = ImmutableSet.builder();
        ImmutableSet.Builder<Path> variantResourceFiles = ImmutableSet.builder();

        collectResourcePathsFromConstructorArgs(directResources, directAssetCatalogs, ImmutableSet.of(),
                resourceFiles, resourceDirs, variantResourceFiles);

        addResourcesFileReference(targetGroup, resourceFiles.build(), resourceDirs.build(),
                variantResourceFiles.build(), ignored -> {
                }, ignored -> {
                });
    }

    private PBXBuildPhase addResourcesBuildPhase(PBXNativeTarget target, PBXGroup targetGroup) {
        ImmutableSet.Builder<Path> resourceFiles = ImmutableSet.builder();
        ImmutableSet.Builder<Path> resourceDirs = ImmutableSet.builder();
        ImmutableSet.Builder<Path> variantResourceFiles = ImmutableSet.builder();

        collectResourcePathsFromConstructorArgs(recursiveResources, recursiveAssetCatalogs, wrapperResources,
                resourceFiles, resourceDirs, variantResourceFiles);

        final PBXBuildPhase phase = new PBXResourcesBuildPhase();
        addResourcesFileReference(targetGroup, resourceFiles.build(), resourceDirs.build(),
                variantResourceFiles.build(), input -> {
                    PBXBuildFile buildFile = new PBXBuildFile(input);
                    phase.getFiles().add(buildFile);
                }, input -> {
                    PBXBuildFile buildFile = new PBXBuildFile(input);
                    phase.getFiles().add(buildFile);
                });
        if (!phase.getFiles().isEmpty()) {
            target.getBuildPhases().add(phase);
            LOG.debug("Added resources build phase %s", phase);
        }
        return phase;
    }

    private void collectResourcePathsFromConstructorArgs(Set<AppleResourceDescription.Arg> resourceArgs,
            Set<AppleAssetCatalogDescription.Arg> assetCatalogArgs, Set<AppleWrapperResourceArg> resourcePathArgs,
            ImmutableSet.Builder<Path> resourceFilesBuilder, ImmutableSet.Builder<Path> resourceDirsBuilder,
            ImmutableSet.Builder<Path> variantResourceFilesBuilder) {
        for (AppleResourceDescription.Arg arg : resourceArgs) {
            resourceFilesBuilder.addAll(Iterables.transform(arg.files, sourcePathResolver));
            resourceDirsBuilder.addAll(Iterables.transform(arg.dirs, sourcePathResolver));
            variantResourceFilesBuilder.addAll(Iterables.transform(arg.variants, sourcePathResolver));
        }

        for (AppleAssetCatalogDescription.Arg arg : assetCatalogArgs) {
            resourceDirsBuilder.addAll(Iterables.transform(arg.dirs, sourcePathResolver));
        }

        for (AppleWrapperResourceArg arg : resourcePathArgs) {
            resourceDirsBuilder.add(arg.path);
        }
    }

    private void addResourcesFileReference(PBXGroup targetGroup, ImmutableSet<Path> resourceFiles,
            ImmutableSet<Path> resourceDirs, ImmutableSet<Path> variantResourceFiles,
            Consumer<? super PBXFileReference> resourceCallback,
            Consumer<? super PBXVariantGroup> variantGroupCallback) {
        if (resourceFiles.isEmpty() && resourceDirs.isEmpty() && variantResourceFiles.isEmpty()) {
            return;
        }

        PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources");
        for (Path path : resourceFiles) {
            PBXFileReference fileReference = resourcesGroup.getOrCreateFileReferenceBySourceTreePath(
                    new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT,
                            pathRelativizer.outputDirToRootRelative(path), Optional.empty()));
            resourceCallback.accept(fileReference);
        }
        for (Path path : resourceDirs) {
            PBXFileReference fileReference = resourcesGroup.getOrCreateFileReferenceBySourceTreePath(
                    new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT,
                            pathRelativizer.outputDirToRootRelative(path), Optional.of("folder")));
            resourceCallback.accept(fileReference);
        }

        Map<String, PBXVariantGroup> variantGroups = Maps.newHashMap();
        for (Path variantFilePath : variantResourceFiles) {
            String lprojSuffix = ".lproj";
            Path variantDirectory = variantFilePath.getParent();
            if (variantDirectory == null || !variantDirectory.toString().endsWith(lprojSuffix)) {
                throw new HumanReadableException(
                        "Variant files have to be in a directory with name ending in '.lproj', "
                                + "but '%s' is not.",
                        variantFilePath);
            }
            String variantDirectoryName = variantDirectory.getFileName().toString();
            String variantLocalization = variantDirectoryName.substring(0,
                    variantDirectoryName.length() - lprojSuffix.length());
            String variantFileName = variantFilePath.getFileName().toString();
            PBXVariantGroup variantGroup = variantGroups.get(variantFileName);
            if (variantGroup == null) {
                variantGroup = resourcesGroup.getOrCreateChildVariantGroupByName(variantFileName);
                variantGroupCallback.accept(variantGroup);
                variantGroups.put(variantFileName, variantGroup);
            }
            SourceTreePath sourceTreePath = new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT,
                    pathRelativizer.outputDirToRootRelative(variantFilePath), Optional.empty());
            variantGroup.getOrCreateVariantFileReferenceByNameAndSourceTreePath(variantLocalization,
                    sourceTreePath);
        }
    }

    private ImmutableList<PBXShellScriptBuildPhase> createScriptsForTargetNodes(Iterable<TargetNode<?, ?>> nodes)
            throws IllegalStateException {
        ImmutableList.Builder<PBXShellScriptBuildPhase> builder = ImmutableList.builder();
        for (TargetNode<?, ?> node : nodes) {
            PBXShellScriptBuildPhase shellScriptBuildPhase = new PBXShellScriptBuildPhase();
            boolean nodeIsPrebuildScript = node.getDescription() instanceof XcodePrebuildScriptDescription;
            boolean nodeIsPostbuildScript = node.getDescription() instanceof XcodePostbuildScriptDescription;
            if (nodeIsPrebuildScript || nodeIsPostbuildScript) {
                XcodeScriptDescriptionArg arg = (XcodeScriptDescriptionArg) node.getConstructorArg();
                shellScriptBuildPhase.getInputPaths()
                        .addAll(FluentIterable.from(arg.srcs).transform(sourcePathResolver)
                                .transform(pathRelativizer::outputDirToRootRelative).transform(Object::toString)
                                .toSet());
                shellScriptBuildPhase.getOutputPaths().addAll(arg.outputs);
                shellScriptBuildPhase.setShellScript(arg.cmd);
            } else if (node.getDescription() instanceof IosReactNativeLibraryDescription) {
                shellScriptBuildPhase.setShellScript(generateXcodeShellScript(node));
            } else {
                // unreachable
                throw new IllegalStateException("Invalid rule type for shell script build phase");
            }
            builder.add(shellScriptBuildPhase);
        }
        return builder.build();
    }

    private void addRunScriptBuildPhases(PBXNativeTarget target, Iterable<PBXShellScriptBuildPhase> phases) {
        for (PBXShellScriptBuildPhase phase : phases) {
            target.getBuildPhases().add(phase);
        }
    }

    private String generateXcodeShellScript(TargetNode<?, ?> targetNode) {
        Preconditions.checkArgument(targetNode.getConstructorArg() instanceof ReactNativeLibraryArgs);

        ST template;
        try {
            template = new ST(Resources.toString(
                    Resources.getResource(NewNativeTargetProjectMutator.class, REACT_NATIVE_PACKAGE_TEMPLATE),
                    Charsets.UTF_8));
        } catch (IOException e) {
            throw new RuntimeException("There was an error loading 'rn_package.st' template", e);
        }

        ReactNativeLibraryArgs args = (ReactNativeLibraryArgs) targetNode.getConstructorArg();

        template.add("bundle_name", args.bundleName);

        ProjectFilesystem filesystem = targetNode.getFilesystem();
        BuildTarget buildTarget = targetNode.getBuildTarget();
        Path jsOutput = ReactNativeBundle.getPathToJSBundleDir(buildTarget, filesystem).resolve(args.bundleName);
        template.add("built_bundle_path", filesystem.resolve(jsOutput));

        Path resourceOutput = ReactNativeBundle.getPathToResources(buildTarget, filesystem);
        template.add("built_resources_path", filesystem.resolve(resourceOutput));

        Path sourceMap = ReactNativeBundle.getPathToSourceMap(buildTarget, filesystem);
        template.add("built_source_map_path", filesystem.resolve(sourceMap));

        return template.render();
    }
}