com.google.devtools.build.lib.rules.objc.ReleaseBundlingSupport.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.rules.objc.ReleaseBundlingSupport.java

Source

// Copyright 2015 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.rules.objc;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag.USES_SWIFT;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ReleaseBundlingRule.APP_ICON_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ReleaseBundlingRule.DEBUG_ENTITLEMENTS_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ReleaseBundlingRule.EXTRA_ENTITLEMENTS_ATTR;
import static com.google.devtools.build.lib.rules.objc.TargetDeviceFamily.UI_DEVICE_FAMILY_VALUES;

import com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSObject;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
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.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.BuildInfo;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
import com.google.devtools.build.lib.rules.apple.AppleCommandLineOptions;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration.ConfigurationDistinguisher;
import com.google.devtools.build.lib.rules.apple.DottedVersion;
import com.google.devtools.build.lib.rules.apple.Platform;
import com.google.devtools.build.lib.rules.apple.Platform.PlatformType;
import com.google.devtools.build.lib.rules.objc.BundleSupport.ExtraActoolArgs;
import com.google.devtools.build.lib.rules.objc.Bundling.Builder;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting;
import java.util.List;
import java.util.Map.Entry;
import javax.annotation.Nullable;

/**
 * Support for released bundles, such as an application or extension. Such a bundle is generally
 * composed of a top-level {@link BundleSupport bundle}, potentially signed, as well as some debug
 * information, if {@link ObjcConfiguration#generateDsym() requested}.
 *
 * <p>Contains actions, validation logic and provider value generation.
 *
 * <p>Methods on this class can be called in any order without impacting the result.
 */
public final class ReleaseBundlingSupport {

    /**
     * Template for the containing application folder.
     */
    public static final SafeImplicitOutputsFunction IPA = fromTemplates("%{name}.ipa");

    @VisibleForTesting
    static final String NO_ASSET_CATALOG_ERROR_FORMAT = "a value was specified (%s), but this app does not have any asset catalogs";
    @VisibleForTesting
    static final String DEVICE_NO_PROVISIONING_PROFILE = "Provisioning profile must be set for device build";

    @VisibleForTesting
    static final String PROVISIONING_PROFILE_BUNDLE_FILE = "embedded.mobileprovision";
    @VisibleForTesting
    static final String APP_BUNDLE_DIR_FORMAT = "Payload/%s.app";
    @VisibleForTesting
    static final String XCTEST_BUNDLE_DIR_FORMAT = "Payload/%s.xctest";
    @VisibleForTesting
    static final String EXTENSION_BUNDLE_DIR_FORMAT = "PlugIns/%s.appex";
    @VisibleForTesting
    static final String FRAMEWORK_BUNDLE_DIR_FORMAT = "Frameworks/%s.framework";

    /**
     * Command string for "sed" that tries to extract the application version number from a larger
     * string. For example, from "foo_1.2.3_RC00" this would extract "1.2.3". This regex looks for
     * versions of the format "x.y" or "x.y.z", which may be preceded and/or followed by other text,
     * such as a project name or release candidate number.
     *
     * <p>This command also preserves double quotes around the string, if any.
     */
    private static final String EXTRACT_VERSION_NUMBER_SED_COMMAND = "s#\\(\"\\)\\{0,1\\}\\(.*_\\)\\{0,1\\}\\([0-9][0-9]*\\(\\.[0-9][0-9]*\\)\\{1,2\\}\\)"
            + "\\(_[^\"]*\\)\\{0,1\\}\\(\"\\)\\{0,1\\}#\\1\\3\\6#";

    private final Attributes attributes;
    private final BundleSupport bundleSupport;
    private final RuleContext ruleContext;
    private final Bundling bundling;
    private final ObjcProvider objcProvider;
    private final LinkedBinary linkedBinary;
    private final IntermediateArtifacts intermediateArtifacts;
    private final ReleaseBundling releaseBundling;
    private final Platform platform;

    /**
     * Indicator as to whether this rule generates a binary directly or whether only dependencies
     * should be considered.
     */
    enum LinkedBinary {
        /**
         * This rule generates its own binary which should be included as well as dependency-generated
         * binaries.
         */
        LOCAL_AND_DEPENDENCIES,

        /**
         * This rule does not generate its own binary, only consider binaries from dependencies.
         */
        DEPENDENCIES_ONLY
    }

    /**
     * Creates a new release bundling support within the given rule context.
     *
     * @param ruleContext context for the application-generating rule
     * @param objcProvider provider containing all dependencies' information as well as some of this
     *    rule's
     * @param linkedBinary whether to look for a linked binary from this rule and dependencies or just
     *    the latter
     * @param bundleDirFormat format string representing the bundle's directory with a single
     *    placeholder for the target name (e.g. {@code "Payload/%s.app"})
     * @param bundleName name of the bundle, used with bundleDirFormat
     * @param bundleMinimumOsVersion the minimum OS version this bundle's plist should be generated
     *    for (<b>not</b> the minimum OS version its binary is compiled with, that needs to be set
     *    through the configuration)
     * @param releaseBundling the {@link ReleaseBundling} containing information for creating a
     *    releaseable bundle.
     * @param platform the platform that bundles will be created for using this support
     */
    ReleaseBundlingSupport(RuleContext ruleContext, ObjcProvider objcProvider, LinkedBinary linkedBinary,
            String bundleDirFormat, String bundleName, DottedVersion bundleMinimumOsVersion,
            ReleaseBundling releaseBundling, Platform platform) {
        this.platform = platform;
        this.linkedBinary = linkedBinary;
        this.attributes = new Attributes(ruleContext);
        this.ruleContext = ruleContext;
        this.objcProvider = objcProvider;
        this.releaseBundling = releaseBundling;
        this.intermediateArtifacts = releaseBundling.getIntermediateArtifacts();
        this.bundling = bundling(ruleContext, objcProvider, bundleDirFormat, bundleName, bundleMinimumOsVersion);
        // TODO(cparsons): Take the rule configuration as a param instead of inferring.
        bundleSupport = new BundleSupport(ruleContext, ruleContext.getFragment(AppleConfiguration.class), platform,
                bundling, extraActoolArgs());
    }

    /**
     * Creates a new application support within the given rule context.
     *
     * @param ruleContext context for the application-generating rule
     * @param objcProvider provider containing all dependencies' information as well as some of this
     *    rule's
     * @param linkedBinary whether to look for a linked binary from this rule and dependencies or just
     *    the latter
     * @param bundleDirFormat format string representing the bundle's directory with a single
     *    placeholder for the target name (e.g. {@code "Payload/%s.app"})
     * @param bundleName name of the bundle, used with bundleDirFormat
     * @param platform the platform that bundles will be created for using this support
     */
    ReleaseBundlingSupport(RuleContext ruleContext, ObjcProvider objcProvider, LinkedBinary linkedBinary,
            String bundleDirFormat, String bundleName, DottedVersion bundleMinimumOsVersion, Platform platform)
            throws InterruptedException {
        this(ruleContext, objcProvider, linkedBinary, bundleDirFormat, bundleName, bundleMinimumOsVersion,
                ReleaseBundling.releaseBundling(ruleContext), platform);
    }

    /**
     * Creates a new application support within the given rule context.
     *
     * {@code bundleName} defaults to label name
     *
     * @param ruleContext context for the application-generating rule
     * @param objcProvider provider containing all dependencies' information as well as some of this
     *    rule's
     * @param linkedBinary whether to look for a linked binary from this rule and dependencies or just
     *    the latter
     * @param bundleDirFormat format string representing the bundle's directory with a single
     *    placeholder for the target name (e.g. {@code "Payload/%s.app"})
     * @param bundleMinimumOsVersion the minimum OS version this bundle's plist should be generated
     *    for (<b>not</b> the minimum OS version its binary is compiled with, that needs to be set
     *    through the configuration)
     * @param platform the platform that bundles will be created for using this support
     * @throws InterruptedException
     */
    ReleaseBundlingSupport(RuleContext ruleContext, ObjcProvider objcProvider, LinkedBinary linkedBinary,
            String bundleDirFormat, DottedVersion bundleMinimumOsVersion, Platform platform)
            throws InterruptedException {
        this(ruleContext, objcProvider, linkedBinary, bundleDirFormat, ruleContext.getLabel().getName(),
                bundleMinimumOsVersion, platform);
    }

    /**
     * Validates application-related attributes set on this rule and registers any errors with the
     * rule context.
     *
     * @return this application support
     */
    ReleaseBundlingSupport validateAttributes() {
        // No asset catalogs. That means you cannot specify app_icon or
        // launch_image attributes, since they must not exist. However, we don't
        // run actool in this case, which means it does not do validity checks,
        // and we MUST raise our own error somehow...
        if (!objcProvider.hasAssetCatalogs()) {
            if (releaseBundling.getAppIcon() != null) {
                ruleContext.attributeError(APP_ICON_ATTR,
                        String.format(NO_ASSET_CATALOG_ERROR_FORMAT, releaseBundling.getAppIcon()));
            }
            if (releaseBundling.getLaunchImage() != null) {
                ruleContext.attributeError("launch_image",
                        String.format(NO_ASSET_CATALOG_ERROR_FORMAT, releaseBundling.getLaunchImage()));
            }
        }

        if (releaseBundling.getProvisioningProfile() == null && platform.isDevice()) {
            ruleContext.attributeError(releaseBundling.getProvisioningProfileAttrName(),
                    DEVICE_NO_PROVISIONING_PROFILE);
        }

        return this;
    }

    /**
     * Validates that resources defined in this rule and its dependencies and written to this bundle
     * are legal.
     *
     * @return this release bundling support
     */
    ReleaseBundlingSupport validateResources() {
        bundleSupport.validate(objcProvider);
        return this;
    }

    /**
     * Registers actions required to build an application. This includes any
     * {@link BundleSupport#registerActions(ObjcProvider) bundle} and bundle merge actions, signing
     * this application if appropriate and combining several single-architecture binaries into one
     * multi-architecture binary.
     *
     * @param dsymOutputType the file type of the dSYM bundle to be generated
     *
     * @return this application support
     */
    ReleaseBundlingSupport registerActions(DsymOutputType dsymOutputType) throws InterruptedException {
        bundleSupport.registerActions(objcProvider);

        Artifact combinedArchBinary = prepareCombinedArchitecturesArtifact();
        registerCopyDsymFilesAction(dsymOutputType);
        registerCopyDsymPlistAction(dsymOutputType);
        registerCopyLinkmapFilesAction();
        registerSwiftStdlibActionsIfNecessary(combinedArchBinary);
        registerSwiftSupportActionsIfNecessary(combinedArchBinary);

        registerEmbedLabelPlistAction();
        registerEnvironmentPlistAction();
        registerAutomaticPlistAction();

        if (releaseBundling.getLaunchStoryboard() != null) {
            registerLaunchStoryboardPlistAction();
        }

        registerBundleMergeActions();
        registerPostProcessAndSigningActions();

        return this;
    }

    private void registerEmbedLabelPlistAction() throws InterruptedException {
        Artifact buildInfo = Iterables.getOnlyElement(ruleContext.getBuildInfo(ObjcBuildInfoFactory.KEY));
        String generatedVersionPlistPath = getGeneratedVersionPlist().getShellEscapedExecPathString();
        String shellCommand = "VERSION=\"$(" + "grep \"^" + BuildInfo.BUILD_EMBED_LABEL + "\" "
                + buildInfo.getShellEscapedExecPathString() + " | cut -d' ' -f2- | sed -e '"
                + EXTRACT_VERSION_NUMBER_SED_COMMAND + "' | " + "sed -e 's#\"#\\\"#g')\" && " + "cat >"
                + generatedVersionPlistPath + " <<EOF\n" + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                + "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
                + "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" + "<plist version=\"1.0\">\n" + "<dict>\n"
                + "EOF\n"

                + "if [[ -n \"${VERSION}\" ]]; then\n"
                + "  for KEY in CFBundleVersion CFBundleShortVersionString; do\n"
                + "    echo \"  <key>${KEY}</key>\n\" >> " + generatedVersionPlistPath + "\n"
                + "    echo \"  <string>${VERSION}</string>\n\" >> " + generatedVersionPlistPath + "\n" + "  done\n"
                + "fi\n"

                + "cat >>" + generatedVersionPlistPath + " <<EOF\n" + "</dict>\n" + "</plist>\n" + "EOF\n";
        ruleContext.registerAction(
                new SpawnAction.Builder().setMnemonic("ObjcVersionPlist").setShellCommand(shellCommand)
                        .addInput(buildInfo).addOutput(getGeneratedVersionPlist()).build(ruleContext));
    }

    private void registerLaunchStoryboardPlistAction() {
        String launchStoryboard = releaseBundling.getLaunchStoryboard().getFilename();
        String launchStoryboardName = launchStoryboard.substring(0, launchStoryboard.lastIndexOf('.'));
        NSDictionary result = new NSDictionary();
        result.put("UILaunchStoryboardName", launchStoryboardName);
        String contents = result.toGnuStepASCIIPropertyList();
        ruleContext
                .registerAction(FileWriteAction.create(ruleContext, getLaunchStoryboardPlist(), contents, false));
    }

    private void registerEnvironmentPlistAction() {
        AppleConfiguration configuration = ruleContext.getFragment(AppleConfiguration.class);
        // Generates a .plist that contains environment values (such as the SDK used to build, the Xcode
        // version, etc), which are parsed from various .plist files of the OS, namely Xcodes' and
        // Platforms' plists.
        // The resulting file is meant to be merged with the final bundle.
        String platformWithVersion = String.format("%s%s", platform.getLowerCaseNameInPlist(),
                configuration.getSdkVersionForPlatform(platform));
        ruleContext.registerAction(ObjcRuleClasses.spawnAppleEnvActionBuilder(configuration, platform)
                .setMnemonic("EnvironmentPlist").setExecutable(attributes.environmentPlist())
                .addArguments("--platform", platformWithVersion)
                .addArguments("--output", getGeneratedEnvironmentPlist().getExecPathString())
                .addOutput(getGeneratedEnvironmentPlist()).build(ruleContext));
    }

    private void registerAutomaticPlistAction() {
        ruleContext.registerAction(FileWriteAction.create(ruleContext, getGeneratedAutomaticPlist(),
                automaticEntries().toGnuStepASCIIPropertyList(), /*makeExecutable=*/ false));
    }

    /**
     * Returns a map containing entries that should be added to the merged plist. These are usually
     * generated by Xcode automatically during the build process.
     */
    private NSDictionary automaticEntries() {
        List<Integer> uiDeviceFamily = TargetDeviceFamily.UI_DEVICE_FAMILY_VALUES
                .get(bundleSupport.targetDeviceFamilies());
        AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class);

        NSDictionary result = new NSDictionary();

        if (uiDeviceFamily != null) {
            result.put("UIDeviceFamily", uiDeviceFamily.toArray());
        }
        result.put("DTPlatformName", platform.getLowerCaseNameInPlist());
        result.put("DTSDKName",
                platform.getLowerCaseNameInPlist() + appleConfiguration.getSdkVersionForPlatform(platform));
        result.put("CFBundleSupportedPlatforms", new NSArray(NSObject.wrap(platform.getNameInPlist())));
        result.put("MinimumOSVersion", bundling.getMinimumOsVersion().toString());

        return result;
    }

    /**
     * Registers all actions necessary to create a processed and signed IPA from the initial merged
     * IPA.
     *
     * <p>Includes user-provided actions to process IPA contents (via {@code ipa_post_processor}),
     * and signing actions if the IPA is being built for device architectures. If signing is necessary
     * also includes entitlements generation and processing actions.
     *
     * <p>Note that multiple "actions" on the IPA contents may be run in a single blaze action to
     * avoid excessive zipping/unzipping of IPA contents.
     */
    private void registerPostProcessAndSigningActions() {
        Artifact processedIpa = releaseBundling.getIpaArtifact();
        Artifact unprocessedIpa = intermediateArtifacts.unprocessedIpa();

        NestedSetBuilder<Artifact> inputs = NestedSetBuilder.<Artifact>stableOrder().add(unprocessedIpa);

        String actionCommandLine = "set -e && "
                + "t=$(mktemp -d \"${TMPDIR:-/tmp}/signing_intermediate.XXXXXX\") && "
                + "trap \"rm -rf ${t}\" EXIT && "
                // Get an absolute path since we need to cd into the temp directory for zip.
                + "signed_ipa=${PWD}/" + processedIpa.getShellEscapedExecPathString() + " && "
                + "/usr/bin/unzip -qq " + unprocessedIpa.getShellEscapedExecPathString() + " -d ${t} && ";

        FilesToRunProvider processor = attributes.ipaPostProcessor();
        if (processor != null) {
            actionCommandLine += processor.getExecutable().getShellEscapedExecPathString() + " ${t} && ";
        }

        if (platform.isDevice()) {
            actionCommandLine += deviceSigningCommandLine();

            registerEntitlementsActions();
            inputs.add(releaseBundling.getProvisioningProfile()).add(intermediateArtifacts.entitlements());
        } else {
            actionCommandLine += simulatorSigningCommandLine();
        }

        actionCommandLine += "cd ${t} && /usr/bin/zip -q -r \"${signed_ipa}\" .";

        AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class);
        SpawnAction.Builder processAction = ObjcRuleClasses.spawnBashOnDarwinActionBuilder(actionCommandLine)
                .setEnvironment(ObjcRuleClasses.appleToolchainEnvironment(appleConfiguration, platform))
                .setMnemonic("ObjcProcessIpa").setProgressMessage("Processing iOS IPA: " + ruleContext.getLabel())
                .disableSandboxing().addTransitiveInputs(inputs.build()).addOutput(processedIpa);

        if (processor != null) {
            processAction.addTool(processor);
        }

        ruleContext.registerAction(processAction.build(ruleContext));
    }

    private String deviceSigningCommandLine() {
        StringBuilder codesignCommandLineBuilder = new StringBuilder();
        for (String dir : getDirsToSign()) {
            codesignCommandLineBuilder.append(deviceCodesignCommand("${t}/" + dir)).append(" && ");
        }
        return codesignCommandLineBuilder.toString();
    }

    private String simulatorSigningCommandLine() {
        StringBuilder codesignCommandLineBuilder = new StringBuilder();
        for (String dir : getDirsToSign()) {
            codesignCommandLineBuilder.append("/usr/bin/codesign --force --sign \"-\" ${t}/").append(dir)
                    .append(" && ");
        }
        return codesignCommandLineBuilder.toString();
    }

    private ImmutableList<String> getDirsToSign() {
        // The order here is important. The innermost code must singed first.
        ImmutableList.Builder<String> dirsToSign = new ImmutableList.Builder<>();
        String bundleDir = ShellUtils.shellEscape(bundling.getBundleDir());

        // Explicitly sign the frameworks (raw .dylib files and .framework directories in Frameworks/).
        // Unfortunately the --deep option on codesign doesn't do this automatically.
        if (objcProvider.is(USES_SWIFT) || !objcProvider.get(ObjcProvider.DYNAMIC_FRAMEWORK_FILE).isEmpty()) {
            dirsToSign.add(bundleDir + "/Frameworks/*");
        }
        dirsToSign.add(bundleDir);

        return dirsToSign.build();
    }

    /**
     * Creates entitlement actions such that an entitlements file is generated in
     * {@link IntermediateArtifacts#entitlements()} which can be used for signing in this bundle.
     *
     * <p>Entitlements are generated based on a plist-format entitlements file passed to this bundle's
     * {@code entitlements} attribute or, if that is not set, entitlements extracted from the provided
     * mobile provisioning profile. The team prefix is extracted from the provisioning profile and
     * the following substitutions performed (assuming the prefix extracted was {@code PREFIX}):
     * <ol>
     *   <li>"PREFIX.*" -> "PREFIX.BUNDLE_ID" (where BUNDLE_ID is this bundle's id)
     *   <li>"$(AppIdentifierPrefix)" -> "PREFIX."
     *   <li>"$(CFBundleIdentifier)" -> "BUNDLE_ID" (where BUNDLE_ID is this bundle's id)
     * </ol>
     *
     * <p>Finally, if an entitlements file was provided via {@code --extra_entitlements} it is merged
     * into the substituted entitlements.
     */
    private void registerEntitlementsActions() {
        Artifact teamPrefixFile = intermediateArtifacts.appendExtensionForEntitlementArtifact(".team_prefix_file");
        registerExtractTeamPrefixAction(teamPrefixFile);

        Artifact entitlementsNeedingSubstitution = releaseBundling.getEntitlements();
        if (entitlementsNeedingSubstitution == null) {
            entitlementsNeedingSubstitution = intermediateArtifacts
                    .appendExtensionForEntitlementArtifact(".entitlements_with_variables");
            registerExtractEntitlementsAction(entitlementsNeedingSubstitution);
        }

        Artifact substitutedEntitlements = intermediateArtifacts.entitlements();
        if (attributes.extraEntitlements() != null || includeDebugEntitlements()) {
            substitutedEntitlements = intermediateArtifacts.appendExtensionForEntitlementArtifact(".substituted");

            NestedSetBuilder<Artifact> entitlements = NestedSetBuilder.<Artifact>stableOrder()
                    .add(substitutedEntitlements);
            if (attributes.extraEntitlements() != null) {
                entitlements.add(attributes.extraEntitlements());
            }
            if (includeDebugEntitlements()) {
                entitlements.add(attributes.deviceDebugEntitlements());
            }

            registerMergeEntitlementsAction(entitlements.build());
        }

        registerEntitlementsVariableSubstitutionAction(entitlementsNeedingSubstitution, teamPrefixFile,
                substitutedEntitlements);
    }

    private boolean includeDebugEntitlements() {
        return attributes.deviceDebugEntitlements() != null
                && ObjcRuleClasses.objcConfiguration(ruleContext).useDeviceDebugEntitlements();
    }

    private void registerMergeEntitlementsAction(NestedSet<Artifact> entitlements) {
        PlMergeControlBytes controlBytes = PlMergeControlBytes.fromPlists(entitlements,
                intermediateArtifacts.entitlements(), PlMergeControlBytes.OutputFormat.XML);

        Artifact plMergeControlArtifact = ObjcRuleClasses.artifactByAppendingToBaseName(ruleContext,
                artifactName(".merge-entitlements-control"));

        ruleContext.registerAction(new BinaryFileWriteAction(ruleContext.getActionOwner(), plMergeControlArtifact,
                controlBytes, /*makeExecutable=*/ false));

        ruleContext.registerAction(new SpawnAction.Builder().setMnemonic("MergeEntitlementsFiles")
                .setExecutable(attributes.plmerge()).addArgument("--control")
                .addInputArgument(plMergeControlArtifact).addTransitiveInputs(entitlements)
                .addOutput(intermediateArtifacts.entitlements()).build(ruleContext));
    }

    /**
     * Adds bundle- and application-related settings to the given Xcode provider builder.
     *
     * @return this application support
     */
    ReleaseBundlingSupport addXcodeSettings(XcodeProvider.Builder xcodeProviderBuilder) {
        bundleSupport.addXcodeSettings(xcodeProviderBuilder);
        // Add application-related Xcode build settings to the main target only. The companion library
        // target does not need them.
        xcodeProviderBuilder.addMainTargetXcodeprojBuildSettings(buildSettings());

        return this;
    }

    /**
     * Adds any files to the given nested set builder that should be built if this application is the
     * top level target in a blaze invocation.
     *
     * @param filesToBuild a collection of files to be built, where new artifacts to be built are
     *     going to be placed
     * @param dsymOutputType the file type of the dSYM bundle to be built, or absent if no
     *     dSYM should be built for this bundle. A dSYM bundle will only be created if both this
     *     is present and the configuration values dictate dSYM is enabled
     *
     * @return this application support
     */
    ReleaseBundlingSupport addFilesToBuild(NestedSetBuilder<Artifact> filesToBuild,
            Optional<DsymOutputType> dsymOutputType) {
        NestedSetBuilder<Artifact> debugSymbolBuilder = NestedSetBuilder.<Artifact>stableOrder();

        for (Artifact linkmapFile : getLinkmapFiles().values()) {
            filesToBuild.add(linkmapFile);
        }

        if (ObjcRuleClasses.objcConfiguration(ruleContext).generateDsym() && dsymOutputType.isPresent()) {
            filesToBuild.addAll(getDsymFiles(dsymOutputType.get()).values());

            // TODO(bazel-team): Remove the 'if' when the objc_binary rule does not generate a bundle any
            // more. The reason this 'if' is here is because the plist is obtained from the ObjcProvider.
            // Since objc_binary is the rule that adds this file to the provider, and not before, when
            // running this the provider does not have the plist yet. This gets called again when running
            // the *_application targets, and since they depend on objc_binaries, the provider has the
            // files configured. When objc_binary stops bundling ipas as output, the bundling methods will
            // only get called by *_application rules, with the plist configured in the provider.
            Artifact cpuPlist = getAnyCpuSpecificDsymPlist();
            if (cpuPlist != null) {
                filesToBuild.add(intermediateArtifacts.dsymPlist(dsymOutputType.get()));
            }

            if (linkedBinary == LinkedBinary.LOCAL_AND_DEPENDENCIES) {
                debugSymbolBuilder.add(intermediateArtifacts.dsymPlist(dsymOutputType.get()))
                        .add(intermediateArtifacts.dsymSymbol(dsymOutputType.get()));
            }
        }

        filesToBuild.add(releaseBundling.getIpaArtifact()).addTransitive(debugSymbolBuilder.build())
                .addTransitive(objcProvider.get(ObjcProvider.EXPORTED_DEBUG_ARTIFACTS));

        return this;
    }

    /**
     * Adds dSYM artifacts (plist, arch-speficic binaries) to the {@link ObjcProvider} for export.
     */
    public void addExportedDebugArtifacts(ObjcProvider.Builder objcBuilder, DsymOutputType dsymOutputType) {
        if (ObjcRuleClasses.objcConfiguration(ruleContext).generateDsym()) {
            objcBuilder.addAll(ObjcProvider.EXPORTED_DEBUG_ARTIFACTS, getDsymFiles(dsymOutputType).values())
                    .add(ObjcProvider.EXPORTED_DEBUG_ARTIFACTS, intermediateArtifacts.dsymPlist(dsymOutputType));
        }
    }

    /**
     * Creates the {@link XcTestAppProvider} that can be used if this application is used as an
     * {@code xctest_app}.
     */
    XcTestAppProvider xcTestAppProvider() {
        // We want access to #import-able things from our test rig's dependency graph, but we don't
        // want to link anything since that stuff is shared automatically by way of the
        // -bundle_loader linker flag.
        // TODO(bazel-team): Handle the FRAMEWORK_DIR key properly. We probably want to add it to
        // framework search paths, but not actually link it with the -framework flag.
        ObjcProvider partialObjcProvider = new ObjcProvider.Builder()
                .addTransitiveAndPropagate(ObjcProvider.HEADER, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.INCLUDE, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.DEFINE, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.SDK_DYLIB, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.SDK_FRAMEWORK, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.SOURCE, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.WEAK_SDK_FRAMEWORK, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.STATIC_FRAMEWORK_FILE, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.DYNAMIC_FRAMEWORK_FILE, objcProvider)
                .addTransitiveAndPropagate(ObjcProvider.FRAMEWORK_SEARCH_PATH_ONLY,
                        objcProvider.get(ObjcProvider.STATIC_FRAMEWORK_DIR))
                .addTransitiveAndPropagate(ObjcProvider.FRAMEWORK_SEARCH_PATH_ONLY,
                        objcProvider.get(ObjcProvider.DYNAMIC_FRAMEWORK_DIR))
                .build();
        return new XcTestAppProvider(intermediateArtifacts.combinedArchitectureBinary(),
                releaseBundling.getIpaArtifact(), partialObjcProvider);
    }

    /**
     * Registers an action to generate a runner script based on a template.
     */
    ReleaseBundlingSupport registerGenerateRunnerScriptAction(Artifact runnerScript, Artifact ipaInput) {
        ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
        String escapedSimDevice = ShellUtils.shellEscape(objcConfiguration.getIosSimulatorDevice());
        String escapedSdkVersion = ShellUtils.shellEscape(objcConfiguration.getIosSimulatorVersion().toString());
        ImmutableList<Substitution> substitutions = ImmutableList.of(
                Substitution.of("%app_name%", ruleContext.getLabel().getName()),
                Substitution.of("%ipa_file%", ipaInput.getRunfilesPathString()),
                Substitution.of("%sim_device%", escapedSimDevice),
                Substitution.of("%sdk_version%", escapedSdkVersion), Substitution.of("%std_redirect_dylib_path%",
                        attributes.stdRedirectDylib().getRunfilesPathString()));

        ruleContext.registerAction(new TemplateExpansionAction(ruleContext.getActionOwner(),
                attributes.runnerScriptTemplate(), runnerScript, substitutions, true));
        return this;
    }

    /**
     * Returns a {@link RunfilesSupport} that uses the provided runner script as the executable.
     */
    RunfilesSupport runfilesSupport(Artifact runnerScript) {
        Runfiles runfiles = new Runfiles.Builder(ruleContext.getWorkspaceName(),
                ruleContext.getConfiguration().legacyExternalRunfiles())
                        .addArtifact(releaseBundling.getIpaArtifact()).addArtifact(runnerScript)
                        .addArtifact(attributes.stdRedirectDylib()).build();
        return RunfilesSupport.withExecutable(ruleContext, runfiles, runnerScript);
    }

    private ExtraActoolArgs extraActoolArgs() {
        ImmutableList.Builder<String> extraArgs = ImmutableList.builder();
        if (releaseBundling.getAppIcon() != null) {
            extraArgs.add("--app-icon", releaseBundling.getAppIcon());
        }
        if (releaseBundling.getLaunchImage() != null) {
            extraArgs.add("--launch-image", releaseBundling.getLaunchImage());
        }
        return new ExtraActoolArgs(extraArgs.build());
    }

    private Bundling bundling(RuleContext ruleContext, ObjcProvider objcProvider, String bundleDirFormat,
            String bundleName, DottedVersion minimumOsVersion) {
        ImmutableList<BundleableFile> extraBundleFiles;
        AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class);
        if (platform.isDevice()) {
            extraBundleFiles = ImmutableList.of(
                    new BundleableFile(releaseBundling.getProvisioningProfile(), PROVISIONING_PROFILE_BUNDLE_FILE));
        } else {
            extraBundleFiles = ImmutableList.of();
        }

        Bundling.Builder bundling = new Builder().setName(bundleName).setExecutableName(bundleName)
                // Architecture that determines which nested bundles are kept.
                .setArchitecture(appleConfiguration.getDependencySingleArchitecture())
                .setBundleDirFormat(bundleDirFormat).addExtraBundleFiles(extraBundleFiles)
                .setObjcProvider(objcProvider).setIntermediateArtifacts(intermediateArtifacts)
                .setPrimaryBundleId(releaseBundling.getPrimaryBundleId())
                .setFallbackBundleId(releaseBundling.getFallbackBundleId()).setMinimumOsVersion(minimumOsVersion)
                .setArtifactPrefix(releaseBundling.getArtifactPrefix())
                .setTargetDeviceFamilies(releaseBundling.getTargetDeviceFamilies());

        // Add plists from rule first.
        if (releaseBundling.getInfoPlistsFromRule() != null) {
            bundling.addInfoplistInputs(releaseBundling.getInfoPlistsFromRule());
        } else {
            bundling.addInfoplistInputFromRule(ruleContext);
        }

        // Add generated plists next so that generated values can override the default values in the
        // plists from rule.
        bundling.setAutomaticEntriesInfoplistInput(getGeneratedAutomaticPlist())
                .addInfoplistInput(getGeneratedVersionPlist()).addInfoplistInput(getGeneratedEnvironmentPlist())
                .addInfoplistInputs(releaseBundling.getInfoplistInputs());

        if (releaseBundling.getLaunchStoryboard() != null) {
            bundling.addInfoplistInput(getLaunchStoryboardPlist());
        }

        return bundling.build();
    }

    private Artifact prepareCombinedArchitecturesArtifact() {
        Artifact dependentMultiArchBinary = attributes.dependentMultiArchBinary();
        if (dependentMultiArchBinary != null) {
            return dependentMultiArchBinary;
        }

        Artifact resultingLinkedBinary = intermediateArtifacts.combinedArchitectureBinary();

        new LipoSupport(ruleContext).registerCombineArchitecturesAction(linkedBinaries(), resultingLinkedBinary,
                platform);
        return resultingLinkedBinary;
    }

    private NestedSet<Artifact> linkedBinaries() {
        NestedSetBuilder<Artifact> linkedBinariesBuilder = NestedSetBuilder.<Artifact>stableOrder()
                .addTransitive(attributes.dependentLinkedBinaries());
        if (linkedBinary == LinkedBinary.LOCAL_AND_DEPENDENCIES) {
            linkedBinariesBuilder.add(intermediateArtifacts.strippedSingleArchitectureBinary());
        }
        return linkedBinariesBuilder.build();
    }

    /** Returns this target's Xcode build settings. */
    private Iterable<XcodeprojBuildSetting> buildSettings() {
        ImmutableList.Builder<XcodeprojBuildSetting> buildSettings = new ImmutableList.Builder<>();
        if (releaseBundling.getAppIcon() != null) {
            buildSettings.add(XcodeprojBuildSetting.newBuilder().setName("ASSETCATALOG_COMPILER_APPICON_NAME")
                    .setValue(releaseBundling.getAppIcon()).build());
        }
        if (releaseBundling.getLaunchImage() != null) {
            buildSettings.add(XcodeprojBuildSetting.newBuilder().setName("ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME")
                    .setValue(releaseBundling.getLaunchImage()).build());
        }

        // Convert names to a sequence containing "1" and/or "2" for iPhone and iPad, respectively.
        ImmutableSet<TargetDeviceFamily> families;
        if (bundleSupport.isBuildingForWatch()) {
            families = ImmutableSet.of(TargetDeviceFamily.WATCH);
        } else {
            families = bundleSupport.targetDeviceFamilies();
        }
        Iterable<Integer> familyIndexes = families.isEmpty() ? ImmutableList.<Integer>of()
                : UI_DEVICE_FAMILY_VALUES.get(families);
        buildSettings.add(XcodeprojBuildSetting.newBuilder().setName("TARGETED_DEVICE_FAMILY")
                .setValue(Joiner.on(',').join(familyIndexes)).build());

        Artifact entitlements = releaseBundling.getEntitlements();
        if (entitlements != null) {
            buildSettings.add(XcodeprojBuildSetting.newBuilder().setName("CODE_SIGN_ENTITLEMENTS")
                    .setValue("$(WORKSPACE_ROOT)/" + entitlements.getExecPathString()).build());
        }

        return buildSettings.build();
    }

    private void registerBundleMergeActions() {
        Artifact bundleMergeControlArtifact = ObjcRuleClasses.artifactByAppendingToBaseName(ruleContext,
                artifactName(".ipa-control"));

        BundleMergeControlBytes controlBytes = new BundleMergeControlBytes(bundling,
                intermediateArtifacts.unprocessedIpa(), ruleContext.getFragment(AppleConfiguration.class));

        ruleContext.registerAction(new BinaryFileWriteAction(ruleContext.getActionOwner(),
                bundleMergeControlArtifact, controlBytes, /*makeExecutable=*/false));

        ruleContext.registerAction(new SpawnAction.Builder().setMnemonic("IosBundle")
                .setProgressMessage("Bundling iOS application: " + ruleContext.getLabel())
                .setExecutable(attributes.bundleMergeExecutable()).addInputArgument(bundleMergeControlArtifact)
                .addTransitiveInputs(bundling.getBundleContentArtifacts())
                .addOutput(intermediateArtifacts.unprocessedIpa()).build(ruleContext));
    }

    private void registerCopyLinkmapFilesAction() {
        for (Entry<Artifact, Artifact> linkmapFile : getLinkmapFiles().entrySet()) {
            ruleContext.registerAction(
                    new SymlinkAction(ruleContext.getActionOwner(), linkmapFile.getKey(), linkmapFile.getValue(),
                            String.format("Copying Linkmap %s", linkmapFile.getValue().prettyPrint())));
        }
    }

    /**
     * Registers the actions that copy the debug symbol files from the CPU-specific binaries that are
     * part of this application. The only one step executed is that he dsym files have to be renamed
     * to include their corresponding CPU architecture as a suffix.
     *
     * @param dsymOutputType the file type of the dSYM bundle to be copied
     */
    private void registerCopyDsymFilesAction(DsymOutputType dsymOutputType) {
        for (Entry<Artifact, Artifact> dsymFiles : getDsymFiles(dsymOutputType).entrySet()) {
            ruleContext.registerAction(new SymlinkAction(ruleContext.getActionOwner(), dsymFiles.getKey(),
                    dsymFiles.getValue(), "Symlinking dSYM files"));
        }
    }

    /**
     * Registers the action that copies the debug symbol plist from the binary.
     *
     * @param dsymOutputType the file type of the dSYM bundle to be copied
     */
    private void registerCopyDsymPlistAction(DsymOutputType dsymOutputType) {
        Artifact dsymPlist = getAnyCpuSpecificDsymPlist();
        if (dsymPlist != null) {
            ruleContext.registerAction(new SymlinkAction(ruleContext.getActionOwner(), dsymPlist,
                    intermediateArtifacts.dsymPlist(dsymOutputType), "Symlinking dSYM plist"));
        }
    }

    /**
     * Returns a map of input dsym artifacts from the CPU-specific binaries built for this
     * ios_application to the new output dsym artifacts.
     *
     * @param dsymOutputType the file type of the dSYM bundle to be generated
     */
    private ImmutableMap<Artifact, Artifact> getDsymFiles(DsymOutputType dsymOutputType) {
        ImmutableMap.Builder<Artifact, Artifact> results = ImmutableMap.builder();
        for (Entry<String, Artifact> dsymFile : attributes.cpuSpecificDsymFiles().entrySet()) {
            Artifact destDsym = intermediateArtifacts.dsymSymbol(dsymOutputType, dsymFile.getKey());
            results.put(dsymFile.getValue(), destDsym);
        }
        return results.build();
    }

    /**
     * Returns any available CPU specific dSYM plist file.
     */
    @Nullable
    private Artifact getAnyCpuSpecificDsymPlist() {
        for (Artifact dsymPlist : attributes.cpuSpecificDsymPlists().values()) {
            // The plist files generated by the dsym tool are all equal, and don't really have any
            // useful information. For now, just retrieving any one is OK, but ideally all of them should
            // be merged.
            return dsymPlist;
        }
        return null;
    }

    /**
     * Returns a map of input linkmap artifacts from the CPU-specific binaries built for this
     * ios_application to the new output linkmap artifacts.
     */
    private ImmutableMap<Artifact, Artifact> getLinkmapFiles() {
        ImmutableMap.Builder<Artifact, Artifact> results = ImmutableMap.builder();
        for (Entry<String, Artifact> linkmapFile : attributes.cpuSpecificLinkmapFiles().entrySet()) {
            Artifact destLinkMap = intermediateArtifacts.linkmap(linkmapFile.getKey());
            results.put(linkmapFile.getValue(), destLinkMap);
        }
        return results.build();
    }

    private void registerExtractTeamPrefixAction(Artifact teamPrefixFile) {
        String shellCommand = "set -e && " + "PLIST=$(mktemp -t teamprefix.plist) && trap \"rm ${PLIST}\" EXIT && "
                + extractPlistCommand(releaseBundling.getProvisioningProfile()) + " > ${PLIST} && "
                + "/usr/libexec/PlistBuddy -c 'Print ApplicationIdentifierPrefix:0' ${PLIST} > "
                + teamPrefixFile.getShellEscapedExecPathString();
        ruleContext.registerAction(ObjcRuleClasses.spawnBashOnDarwinActionBuilder(shellCommand)
                .setMnemonic("ExtractIosTeamPrefix").disableSandboxing()
                .addInput(releaseBundling.getProvisioningProfile()).addOutput(teamPrefixFile).build(ruleContext));
    }

    private ReleaseBundlingSupport registerExtractEntitlementsAction(Artifact entitlements) {
        // See Apple Glossary (http://goo.gl/EkhXOb)
        // An Application Identifier is constructed as: TeamID.BundleID
        // TeamID is extracted from the provisioning profile.
        // BundleID consists of a reverse-DNS string to identify the app, where the last component
        // is the application name, and is specified as an attribute.
        String shellCommand = "set -e && "
                + "PLIST=$(mktemp -t entitlements.plist) && trap \"rm ${PLIST}\" EXIT && "
                + extractPlistCommand(releaseBundling.getProvisioningProfile()) + " > ${PLIST} && "
                + "/usr/libexec/PlistBuddy -x -c 'Print Entitlements' ${PLIST} > "
                + entitlements.getShellEscapedExecPathString();
        ruleContext.registerAction(ObjcRuleClasses.spawnBashOnDarwinActionBuilder(shellCommand)
                .setMnemonic("ExtractIosEntitlements").disableSandboxing()
                .setProgressMessage("Extracting entitlements: " + ruleContext.getLabel())
                .addInput(releaseBundling.getProvisioningProfile()).addOutput(entitlements).build(ruleContext));

        return this;
    }

    private void registerEntitlementsVariableSubstitutionAction(Artifact inputEntitlements, Artifact prefix,
            Artifact substitutedEntitlements) {
        String escapedBundleId = ShellUtils.shellEscape(releaseBundling.getBundleId());
        String shellCommand = "set -e && " + "PREFIX=\"$(cat " + prefix.getShellEscapedExecPathString() + ")\" && "
                + "sed "
                // Replace .* from default entitlements file with bundle ID where suitable.
                + "-e \"s#${PREFIX}\\.\\*#${PREFIX}." + escapedBundleId + "#g\" "

                // Replace some variables that people put in their own entitlements files
                + "-e \"s#\\$(AppIdentifierPrefix)#${PREFIX}.#g\" " + "-e \"s#\\$(CFBundleIdentifier)#"
                + escapedBundleId + "#g\" " + inputEntitlements.getShellEscapedExecPathString() + " > "
                + substitutedEntitlements.getShellEscapedExecPathString();
        ruleContext.registerAction(new SpawnAction.Builder().setMnemonic("SubstituteIosEntitlements")
                .setShellCommand(shellCommand).addInput(inputEntitlements).addInput(prefix)
                .addOutput(substitutedEntitlements).build(ruleContext));
    }

    /** Registers an action to copy Swift standard library dylibs into app bundle. */
    private void registerSwiftStdlibActionsIfNecessary(Artifact combinedArchBinary) {
        if (!objcProvider.is(USES_SWIFT)) {
            return;
        }

        AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class);

        CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
        if (appleConfiguration.getXcodeToolchain() != null) {
            commandLine.add("--toolchain").add(appleConfiguration.getXcodeToolchain());
        }

        commandLine.add("--output_zip_path").addPath(intermediateArtifacts.swiftFrameworksFileZip().getExecPath())
                .add("--bundle_path").add("Frameworks").add("--platform").add(platform.getLowerCaseNameInPlist())
                .addExecPath("--scan-executable", combinedArchBinary);

        ruleContext.registerAction(ObjcRuleClasses.spawnAppleEnvActionBuilder(appleConfiguration, platform)
                .setMnemonic("SwiftStdlibCopy").setExecutable(attributes.swiftStdlibToolWrapper())
                .setCommandLine(commandLine.build()).addOutput(intermediateArtifacts.swiftFrameworksFileZip())
                .addInput(combinedArchBinary).build(ruleContext));
    }

    /** Registers an action to copy Swift standard library dylibs into SwiftSupport root directory. */
    private void registerSwiftSupportActionsIfNecessary(Artifact combinedArchBinary) {
        if (!objcProvider.is(USES_SWIFT)) {
            return;
        }

        AppleConfiguration configuration = ruleContext.getFragment(AppleConfiguration.class);

        CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
        if (configuration.getXcodeToolchain() != null) {
            commandLine.add("--toolchain").add(configuration.getXcodeToolchain());
        }

        commandLine.add("--output_zip_path").addPath(intermediateArtifacts.swiftSupportZip().getExecPath())
                .add("--bundle_path").add("SwiftSupport/" + platform.getLowerCaseNameInPlist()).add("--platform")
                .add(platform.getLowerCaseNameInPlist()).addExecPath("--scan-executable", combinedArchBinary);

        ruleContext.registerAction(ObjcRuleClasses.spawnAppleEnvActionBuilder(configuration, platform)
                .setMnemonic("SwiftCopySwiftSupport").setExecutable(attributes.swiftStdlibToolWrapper())
                .setCommandLine(commandLine.build()).addOutput(intermediateArtifacts.swiftSupportZip())
                .addInput(combinedArchBinary).build(ruleContext));
    }

    private String extractPlistCommand(Artifact provisioningProfile) {
        return "security cms -D -i " + ShellUtils.shellEscape(provisioningProfile.getExecPathString());
    }

    private String deviceCodesignCommand(String appDir) {
        String signingCertName = ObjcRuleClasses.objcConfiguration(ruleContext).getSigningCertName();
        Artifact entitlements = intermediateArtifacts.entitlements();

        final String identity;
        if (signingCertName != null) {
            identity = '"' + signingCertName + '"';
        } else {
            // Extracts an identity hash from the configured provisioning profile. Note that this will use
            // the first certificate identity in the profile, regardless of how many identities are
            // configured in it (DeveloperCertificates:0).
            identity = "$(PLIST=$(mktemp -t cert.plist) && trap \"rm ${PLIST}\" EXIT && "
                    + extractPlistCommand(releaseBundling.getProvisioningProfile()) + " > ${PLIST} && "
                    + "/usr/libexec/PlistBuddy -c 'Print DeveloperCertificates:0' ${PLIST} | "
                    + "openssl x509 -inform DER -noout -fingerprint | " + "cut -d= -f2 | sed -e 's#:##g')";
        }

        return String.format("/usr/bin/codesign --force --sign %s --entitlements %s %s", identity,
                entitlements.getShellEscapedExecPathString(), appDir);
    }

    private Artifact getGeneratedVersionPlist() {
        return ruleContext.getRelatedArtifact(ruleContext.getUniqueDirectory("plists"),
                artifactName("-version.plist"));
    }

    private Artifact getGeneratedEnvironmentPlist() {
        return ruleContext.getRelatedArtifact(ruleContext.getUniqueDirectory("plists"),
                artifactName("-environment.plist"));
    }

    private Artifact getGeneratedAutomaticPlist() {
        return ruleContext.getRelatedArtifact(ruleContext.getUniqueDirectory("plists"),
                artifactName("-automatic.plist"));
    }

    private Artifact getLaunchStoryboardPlist() {
        return ruleContext.getRelatedArtifact(ruleContext.getUniqueDirectory("plists"),
                artifactName("-launchstoryboard.plist"));
    }

    /**
     * Returns artifact name prefixed with prefix given in {@link ReleaseBundling} if available.
     * This helps in creating unique artifact name when multiple bundles are created with a different
     * name than the target name.
     */
    private String artifactName(String artifactName) {
        if (releaseBundling.getArtifactPrefix() != null) {
            return String.format("-%s%s", releaseBundling.getArtifactPrefix(), artifactName);
        }
        return artifactName;
    }

    /**
     * Logic to access attributes to access tools required by application support.
     * Attributes are required and guaranteed to return a value or throw unless they are annotated
     * with {@link Nullable} in which case they can return {@code null} if no value is defined.
     */
    private static class Attributes {
        private final RuleContext ruleContext;

        private Attributes(RuleContext ruleContext) {
            this.ruleContext = ruleContext;
        }

        /**
         * Returns this target's user-specified {@code ipa_post_processor} or null if not present.
         */
        @Nullable
        FilesToRunProvider ipaPostProcessor() {
            if (!ruleContext.attributes().has("ipa_post_processor", BuildType.LABEL)) {
                return null;
            }
            return ruleContext.getExecutablePrerequisite("ipa_post_processor", Mode.TARGET);
        }

        /**
         * Returns the multi-arch binary provided by the label under the "binary" attribute of the
         * current rule, or null if there is no such binary available.
         */
        @Nullable
        Artifact dependentMultiArchBinary() {
            if (ruleContext.attributes().getAttributeDefinition("binary") == null) {
                return null;
            }

            for (ObjcProvider provider : ruleContext.getPrerequisites("binary", Mode.DONT_CHECK,
                    ObjcProvider.class)) {
                if (!provider.get(ObjcProvider.MULTI_ARCH_LINKED_BINARIES).isEmpty()) {
                    return Iterables.getOnlyElement(provider.get(ObjcProvider.MULTI_ARCH_LINKED_BINARIES));
                }
            }
            return null;
        }

        NestedSet<? extends Artifact> dependentLinkedBinaries() {
            if (ruleContext.attributes().getAttributeDefinition("binary") == null) {
                return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
            }

            NestedSetBuilder<Artifact> linkedBinaries = NestedSetBuilder.stableOrder();
            for (ObjcProvider provider : ruleContext.getPrerequisites("binary", Mode.DONT_CHECK,
                    ObjcProvider.class)) {
                linkedBinaries.addTransitive(provider.get(ObjcProvider.LINKED_BINARY));
            }

            return linkedBinaries.build();
        }

        FilesToRunProvider bundleMergeExecutable() {
            return checkNotNull(ruleContext.getExecutablePrerequisite("$bundlemerge", Mode.HOST));
        }

        /**
         * Returns a reference to the plmerge executable.
         */
        FilesToRunProvider plmerge() {
            return ruleContext.getExecutablePrerequisite("$plmerge", Mode.HOST);
        }

        Artifact stdRedirectDylib() {
            return checkNotNull(ruleContext.getPrerequisiteArtifact("$std_redirect_dylib", Mode.HOST));
        }

        Artifact runnerScriptTemplate() {
            return checkNotNull(ruleContext.getPrerequisiteArtifact("$runner_script_template", Mode.HOST));
        }

        /** Returns the location of the swiftstdlibtoolwrapper. */
        FilesToRunProvider swiftStdlibToolWrapper() {
            return ruleContext.getExecutablePrerequisite("$swiftstdlibtoolwrapper", Mode.HOST);
        }

        /**
         * Returns the location of the environment_plist.
         */
        FilesToRunProvider environmentPlist() {
            return ruleContext.getExecutablePrerequisite("$environment_plist", Mode.HOST);
        }

        /**
         * Returns a plist specified by the user via {@code --extra_entitlements} or {@code null}.
         */
        @Nullable
        Artifact extraEntitlements() {
            if (ruleContext.attributes().getAttributeDefinition(EXTRA_ENTITLEMENTS_ATTR) == null) {
                return null;
            }
            return ruleContext.getPrerequisiteArtifact(EXTRA_ENTITLEMENTS_ATTR, Mode.HOST);
        }

        /**
         * Returns a plist containing entitlements that allow the signed IPA to be debugged.
         */
        @Nullable
        Artifact deviceDebugEntitlements() {
            if (ruleContext.attributes().getAttributeDefinition(DEBUG_ENTITLEMENTS_ATTR) == null) {
                return null;
            }
            return ruleContext.getPrerequisiteArtifact(DEBUG_ENTITLEMENTS_ATTR, Mode.HOST);
        }

        ImmutableMap<String, Artifact> cpuSpecificDsymFiles() {
            return cpuSpecificArtifacts(ObjcProvider.DEBUG_SYMBOLS);
        }

        ImmutableMap<String, Artifact> cpuSpecificDsymPlists() {
            return cpuSpecificArtifacts(ObjcProvider.DEBUG_SYMBOLS_PLIST);
        }

        ImmutableMap<String, Artifact> cpuSpecificLinkmapFiles() {
            return cpuSpecificArtifacts(ObjcProvider.LINKMAP_FILE);
        }

        ImmutableMap<String, Artifact> cpuSpecificArtifacts(ObjcProvider.Key<Artifact> key) {
            ImmutableMap.Builder<String, Artifact> results = ImmutableMap.builder();
            if (ruleContext.attributes().has("binary", BuildType.LABEL)) {
                for (TransitiveInfoCollection prerequisite : ruleContext.getPrerequisites("binary",
                        Mode.DONT_CHECK)) {
                    ObjcProvider prerequisiteProvider = prerequisite.getProvider(ObjcProvider.class);
                    if (prerequisiteProvider != null) {
                        Artifact sourceArtifact = Iterables.getOnlyElement(prerequisiteProvider.get(key), null);
                        if (sourceArtifact != null) {
                            String cpu = prerequisite.getConfiguration().getFragment(AppleConfiguration.class)
                                    .getIosCpu();
                            results.put(cpu, sourceArtifact);
                        }
                    }
                }
            }
            return results.build();
        }
    }

    /**
     * Transition that results in one configured target per architecture set in {@code
     * --ios_multi_cpus}.
     */
    protected static class SplitArchTransition implements SplitTransition<BuildOptions> {

        @Override
        public final List<BuildOptions> split(BuildOptions buildOptions) {
            List<String> iosMultiCpus = buildOptions.get(AppleCommandLineOptions.class).iosMultiCpus;
            if (iosMultiCpus.isEmpty()) {
                return defaultOptions(buildOptions);
            }

            ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
            for (String iosCpu : iosMultiCpus) {
                BuildOptions splitOptions = buildOptions.clone();
                setArchitectureOptions(splitOptions, buildOptions, iosCpu);
                setAdditionalOptions(splitOptions, buildOptions);
                splitOptions.get(
                        AppleCommandLineOptions.class).configurationDistinguisher = getConfigurationDistinguisher();
                splitBuildOptions.add(splitOptions);
            }
            return splitBuildOptions.build();
        }

        /**
         * Returns the default options to use if no split architectures are specified.
         *
         * @param originalOptions original options before this transition
         */
        protected ImmutableList<BuildOptions> defaultOptions(BuildOptions originalOptions) {
            return ImmutableList.of();
        }

        /**
         * Sets or overwrites flags on the given split options.
         *
         * <p>Invoked once for each configuration produced by this transition.
         *
         * @param splitOptions options to use after this transition
         * @param originalOptions original options before this transition
         */
        protected void setAdditionalOptions(BuildOptions splitOptions, BuildOptions originalOptions) {
        }

        private static void setArchitectureOptions(BuildOptions splitOptions, BuildOptions originalOptions,
                String iosCpu) {
            splitOptions.get(AppleCommandLineOptions.class).applePlatformType = PlatformType.IOS;
            splitOptions.get(AppleCommandLineOptions.class).appleSplitCpu = iosCpu;
            splitOptions.get(AppleCommandLineOptions.class).iosCpu = iosCpu;
            if (splitOptions.get(ObjcCommandLineOptions.class).enableCcDeps) {
                // Only set the (CC-compilation) CPU for dependencies if explicitly required by the user.
                // This helps users of the iOS rules who do not depend on CC rules as these CPU values
                // require additional flags to work (e.g. a custom crosstool) which now only need to be set
                // if this feature is explicitly requested.
                AppleCrosstoolTransition.setAppleCrosstoolTransitionConfiguration(originalOptions, splitOptions,
                        "ios_" + iosCpu);
            }
        }

        @Override
        public boolean defaultsToSelf() {
            return true;
        }

        /** Returns the configuration distinguisher for this transition instance. */
        protected ConfigurationDistinguisher getConfigurationDistinguisher() {
            return ConfigurationDistinguisher.IOS_APPLICATION;
        }
    }
}