// 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.TargetDeviceFamily.UI_DEVICE_FAMILY_VALUES; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Strings; 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.SpawnAction; 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.BuildConfiguration; 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.AppleToolchain; import com.google.devtools.build.lib.rules.apple.Platform; import com.google.devtools.build.lib.rules.objc.BundleSupport.ExtraActoolArgs; import com.google.devtools.build.lib.shell.ShellUtils; import com.google.devtools.build.lib.syntax.Type; 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#generateDebugSymbols() requested}. * *

Contains actions, validation logic and provider value generation. * *

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"); // This is not an actual implicit output. This function is used to compute the name of an // artifact. public static final SafeImplicitOutputsFunction IPA_UNSIGNED = fromTemplates("%{name}.ipa.unsigned"); @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 INVALID_FAMILIES_ERROR = "Expected one or two strings from the list 'iphone', 'ipad'"; @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 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. * *

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; /** * 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 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 bundleMinimumOsVersion the minimum OS version this bundle's plist should be generated * for (not the minimum OS version its binary is compiled with, that needs to be set * through the configuration) */ ReleaseBundlingSupport(RuleContext ruleContext, ObjcProvider objcProvider, LinkedBinary linkedBinary, String bundleDirFormat, String bundleName, String bundleMinimumOsVersion) { this.linkedBinary = linkedBinary; this.attributes = new Attributes(ruleContext); this.ruleContext = ruleContext; this.objcProvider = objcProvider; this.intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); bundling = bundling(ruleContext, objcProvider, bundleDirFormat, bundleName, bundleMinimumOsVersion); bundleSupport = new BundleSupport(ruleContext, bundling, extraActoolArgs()); } /** * 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 (not the minimum OS version its binary is compiled with, that needs to be set * through the configuration) */ ReleaseBundlingSupport(RuleContext ruleContext, ObjcProvider objcProvider, LinkedBinary linkedBinary, String bundleDirFormat, String bundleMinimumOsVersion) { this(ruleContext, objcProvider, linkedBinary, bundleDirFormat, ruleContext.getLabel().getName(), bundleMinimumOsVersion); } /** * 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 (attributes.appIcon() != null) { ruleContext.attributeError("app_icon", String.format(NO_ASSET_CATALOG_ERROR_FORMAT, attributes.appIcon())); } if (attributes.launchImage() != null) { ruleContext.attributeError("launch_image", String.format(NO_ASSET_CATALOG_ERROR_FORMAT, attributes.launchImage())); } } if (bundleSupport.targetDeviceFamilies().isEmpty()) { ruleContext.attributeError("families", INVALID_FAMILIES_ERROR); } 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. * * @return this application support * @throws InterruptedException */ ReleaseBundlingSupport registerActions() throws InterruptedException { bundleSupport.registerActions(objcProvider); registerCombineArchitecturesAction(); registerTransformAndCopyBreakpadFilesAction(); registerSwiftStdlibActionsIfNecessary(); AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class); Artifact ipaOutput = ruleContext.getImplicitOutputArtifact(IPA); Artifact maybeSignedIpa; if (appleConfiguration.getBundlingPlatform() == Platform.IOS_SIMULATOR) { maybeSignedIpa = ipaOutput; } else if (attributes.provisioningProfile() == null) { throw new IllegalStateException(DEVICE_NO_PROVISIONING_PROFILE); } else { maybeSignedIpa = registerBundleSigningActions(ipaOutput); } registerEmbedLabelPlistAction(); registerEnvironmentPlistAction(); BundleMergeControlBytes bundleMergeControlBytes = new BundleMergeControlBytes( bundling, maybeSignedIpa, appleConfiguration, bundleSupport.targetDeviceFamilies()); registerBundleMergeActions( maybeSignedIpa, bundling.getBundleContentArtifacts(), bundleMergeControlBytes); return this; } private void registerEmbedLabelPlistAction() { 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 + " <\n" + "\n" + "\n" + "\n" + "EOF\n" + "if [[ -n \"${VERSION}\" ]]; then\n" + " for KEY in CFBundleVersion CFBundleShortVersionString; do\n" + " echo \" ${KEY}\n\" >> " + generatedVersionPlistPath + "\n" + " echo \" ${VERSION}\n\" >> " + generatedVersionPlistPath + "\n" + " done\n" + "fi\n" + "cat >>" + generatedVersionPlistPath + " <\n" + "\n" + "EOF\n"; ruleContext.registerAction(new SpawnAction.Builder() .setMnemonic("ObjcVersionPlist") .setShellCommand(shellCommand) .addInput(buildInfo) .addOutput(getGeneratedVersionPlist()) .build(ruleContext)); } 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", configuration.getBundlingPlatform().getLowerCaseNameInPlist(), Strings.nullToEmpty(configuration.getIosSdkVersion())); ruleContext.registerAction( ObjcRuleClasses.spawnOnDarwinActionBuilder(ruleContext) .setMnemonic("EnvironmentPlist") .addInput(attributes.environmentPlistScript()) .setExecutable(attributes.environmentPlistScript()) .addArguments("--platform", platformWithVersion) .addArguments("--output", getGeneratedEnvironmentPlist().getExecPathString()) .addOutput(getGeneratedEnvironmentPlist()) .build(ruleContext)); } private Artifact registerBundleSigningActions(Artifact ipaOutput) throws InterruptedException { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); Artifact teamPrefixFile = intermediateArtifacts.appendExtensionForEntitlementArtifact(".team_prefix_file"); registerExtractTeamPrefixAction(teamPrefixFile); Artifact entitlementsNeedingSubstitution = attributes.entitlements(); if (entitlementsNeedingSubstitution == null) { entitlementsNeedingSubstitution = intermediateArtifacts.appendExtensionForEntitlementArtifact( ".entitlements_with_variables"); registerExtractEntitlementsAction(entitlementsNeedingSubstitution); } Artifact entitlements = intermediateArtifacts.appendExtensionForEntitlementArtifact(".entitlements"); registerEntitlementsVariableSubstitutionAction( entitlementsNeedingSubstitution, entitlements, teamPrefixFile); Artifact ipaUnsigned = ruleContext.getImplicitOutputArtifact(IPA_UNSIGNED); registerSignBundleAction(entitlements, ipaOutput, ipaUnsigned); return ipaUnsigned; } /** * 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. * * @return this application support * @throws InterruptedException */ ReleaseBundlingSupport addFilesToBuild(NestedSetBuilder filesToBuild) throws InterruptedException { NestedSetBuilder debugSymbolBuilder = NestedSetBuilder.stableOrder().addTransitive( objcProvider.get(ObjcProvider.DEBUG_SYMBOLS)); for (Artifact breakpadFile : getBreakpadFiles().values()) { filesToBuild.add(breakpadFile); } if (linkedBinary == LinkedBinary.LOCAL_AND_DEPENDENCIES && ObjcRuleClasses.objcConfiguration(ruleContext).generateDebugSymbols()) { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); debugSymbolBuilder.add(intermediateArtifacts.dsymPlist()) .add(intermediateArtifacts.dsymSymbol()) .add(intermediateArtifacts.breakpadSym()); } filesToBuild.add(ruleContext.getImplicitOutputArtifact(ReleaseBundlingSupport.IPA)) // TODO(bazel-team): Fat binaries may require some merging of these file rather than just // making them available. .addTransitive(debugSymbolBuilder.build()); return this; } /** * Creates the {@link XcTestAppProvider} that can be used if this application is used as an * {@code xctest_app}. * @throws InterruptedException */ XcTestAppProvider xcTestAppProvider() throws InterruptedException { // 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. ObjcProvider partialObjcProvider = new ObjcProvider.Builder() .addTransitiveAndPropagate(ObjcProvider.HEADER, objcProvider) .addTransitiveAndPropagate(ObjcProvider.INCLUDE, objcProvider) .addTransitiveAndPropagate(ObjcProvider.SDK_DYLIB, objcProvider) .addTransitiveAndPropagate(ObjcProvider.SDK_FRAMEWORK, objcProvider) .addTransitiveAndPropagate(ObjcProvider.SOURCE, objcProvider) .addTransitiveAndPropagate(ObjcProvider.WEAK_SDK_FRAMEWORK, objcProvider) .addTransitiveAndPropagate(ObjcProvider.FRAMEWORK_DIR, objcProvider) .addTransitiveAndPropagate(ObjcProvider.FRAMEWORK_FILE, objcProvider) .build(); // 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. return new XcTestAppProvider(intermediateArtifacts.combinedArchitectureBinary(), ruleContext.getImplicitOutputArtifact(IPA), 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()); ImmutableList substitutions = ImmutableList.of( Substitution.of("%app_name%", ruleContext.getLabel().getName()), Substitution.of("%ipa_file%", ipaInput.getRootRelativePath().getPathString()), Substitution.of("%sim_device%", escapedSimDevice), Substitution.of("%sdk_version%", escapedSdkVersion), Substitution.of("%iossim%", attributes.iossim().getRootRelativePath().getPathString()), Substitution.of("%std_redirect_dylib_path%", attributes.stdRedirectDylib().getRootRelativePath().getPathString())); 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. * @throws InterruptedException */ RunfilesSupport runfilesSupport(Artifact runnerScript) throws InterruptedException { Artifact ipaFile = ruleContext.getImplicitOutputArtifact(ReleaseBundlingSupport.IPA); Runfiles runfiles = new Runfiles.Builder(ruleContext.getWorkspaceName()) .addArtifact(ipaFile) .addArtifact(runnerScript) .addArtifact(attributes.iossim()) .build(); return RunfilesSupport.withExecutable(ruleContext, runfiles, runnerScript); } private ExtraActoolArgs extraActoolArgs() { ImmutableList.Builder extraArgs = ImmutableList.builder(); if (attributes.appIcon() != null) { extraArgs.add("--app-icon", attributes.appIcon()); } if (attributes.launchImage() != null) { extraArgs.add("--launch-image", attributes.launchImage()); } return new ExtraActoolArgs(extraArgs.build()); } private Bundling bundling(RuleContext ruleContext, ObjcProvider objcProvider, String bundleDirFormat, String bundleName, String minimumOsVersion) { ImmutableList extraBundleFiles; AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class); if (appleConfiguration.getBundlingPlatform() == Platform.IOS_DEVICE) { extraBundleFiles = ImmutableList.of(new BundleableFile( new Attributes(ruleContext).provisioningProfile(), PROVISIONING_PROFILE_BUNDLE_FILE)); } else { extraBundleFiles = ImmutableList.of(); } String primaryBundleId = null; String fallbackBundleId = null; if (ruleContext.attributes().isAttributeValueExplicitlySpecified("bundle_id")) { primaryBundleId = ruleContext.attributes().get("bundle_id", Type.STRING); } else { fallbackBundleId = ruleContext.attributes().get("bundle_id", Type.STRING); } return new Bundling.Builder() .setName(bundleName) // Architecture that determines which nested bundles are kept. .setArchitecture(appleConfiguration.getDependencySingleArchitecture()) .setBundleDirFormat(bundleDirFormat) .addExtraBundleFiles(extraBundleFiles) .setObjcProvider(objcProvider) .addInfoplistInputFromRule(ruleContext) .addInfoplistInput(getGeneratedVersionPlist()) .addInfoplistInput(getGeneratedEnvironmentPlist()) .setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext)) .setPrimaryBundleId(primaryBundleId) .setFallbackBundleId(fallbackBundleId) .setMinimumOsVersion(minimumOsVersion) .build(); } private void registerCombineArchitecturesAction() { Artifact resultingLinkedBinary = intermediateArtifacts.combinedArchitectureBinary(); NestedSet linkedBinaries = linkedBinaries(); ruleContext.registerAction(ObjcRuleClasses.spawnOnDarwinActionBuilder(ruleContext) .setMnemonic("ObjcCombiningArchitectures") .addTransitiveInputs(linkedBinaries) .addOutput(resultingLinkedBinary) .setExecutable(CompilationSupport.xcrunwrapper(ruleContext)) .setCommandLine(CustomCommandLine.builder() .add(ObjcRuleClasses.LIPO) .addExecPaths("-create", linkedBinaries) .addExecPath("-o", resultingLinkedBinary) .build()) .build(ruleContext)); } private NestedSet linkedBinaries() { NestedSetBuilder linkedBinariesBuilder = NestedSetBuilder.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 buildSettings() { ImmutableList.Builder buildSettings = new ImmutableList.Builder<>(); if (attributes.appIcon() != null) { buildSettings.add(XcodeprojBuildSetting.newBuilder() .setName("ASSETCATALOG_COMPILER_APPICON_NAME") .setValue(attributes.appIcon()) .build()); } if (attributes.launchImage() != null) { buildSettings.add(XcodeprojBuildSetting.newBuilder() .setName("ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME") .setValue(attributes.launchImage()) .build()); } // Convert names to a sequence containing "1" and/or "2" for iPhone and iPad, respectively. ImmutableSet families = bundleSupport.targetDeviceFamilies(); Iterable familyIndexes = families.isEmpty() ? ImmutableList.of() : UI_DEVICE_FAMILY_VALUES.get(families); buildSettings.add(XcodeprojBuildSetting.newBuilder() .setName("TARGETED_DEVICE_FAMILY") .setValue(Joiner.on(',').join(familyIndexes)) .build()); Artifact entitlements = attributes.entitlements(); if (entitlements != null) { buildSettings.add(XcodeprojBuildSetting.newBuilder() .setName("CODE_SIGN_ENTITLEMENTS") .setValue("$(WORKSPACE_ROOT)/" + entitlements.getExecPathString()) .build()); } return buildSettings.build(); } private ReleaseBundlingSupport registerSignBundleAction( Artifact entitlements, Artifact ipaOutput, Artifact ipaUnsigned) { // TODO(bazel-team): Support variable substitution ImmutableList.Builder dirsToSign = new ImmutableList.Builder<>(); // Explicitly sign Swift dylibs. Unfortunately --deep option on codesign doesn't do this // automatically. // The order here is important. The innermost code must singed first. String bundleDir = ShellUtils.shellEscape(bundling.getBundleDir()); if (objcProvider.is(USES_SWIFT)) { dirsToSign.add(bundleDir + "/Frameworks/*"); } dirsToSign.add(bundleDir); StringBuilder codesignCommandLineBuilder = new StringBuilder(); for (String dir : dirsToSign.build()) { codesignCommandLineBuilder .append(codesignCommand(entitlements, "${t}/" + dir)) .append(" && "); } // TODO(bazel-team): Support nested code signing. String shellCommand = "set -e && " + "t=$(mktemp -d -t signing_intermediate) && " + "trap \"rm -rf ${t}\" EXIT && " // Get an absolute path since we need to cd into the temp directory for zip. + "signed_ipa=${PWD}/" + ipaOutput.getShellEscapedExecPathString() + " && " + "/usr/bin/unzip -qq " + ipaUnsigned.getShellEscapedExecPathString() + " -d ${t} && " + codesignCommandLineBuilder.toString() // Using zip since we need to preserve permissions + "cd ${t} && /usr/bin/zip -q -r \"${signed_ipa}\" ."; ruleContext.registerAction( ObjcRuleClasses.spawnBashOnDarwinActionBuilder(ruleContext, shellCommand) .setMnemonic("IosSignBundle") .setProgressMessage("Signing iOS bundle: " + ruleContext.getLabel()) .addInput(ipaUnsigned) .addInput(attributes.provisioningProfile()) .addInput(entitlements) .addOutput(ipaOutput) .build(ruleContext)); return this; } private void registerBundleMergeActions(Artifact ipaUnsigned, NestedSet bundleContentArtifacts, BundleMergeControlBytes controlBytes) { Artifact bundleMergeControlArtifact = ObjcRuleClasses.artifactByAppendingToBaseName(ruleContext, ".ipa-control"); 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(bundleContentArtifacts) .addOutput(ipaUnsigned) .build(ruleContext)); } /** * Registers the actions that transform and copy the breakpad files from the CPU-specific binaries * that are part of this application. There are two steps involved: 1) The breakpad files have to * be renamed to include their corresponding CPU architecture as a suffix. 2) The first line of * the breakpad file has to be rewritten, as it has to include the name of the application instead * of the name of the binary artifact. * *

Example:
* The ios_application "PrenotCalculator" is specified to use "PrenotCalculatorBinary" as its * binary. Assuming that the application is built for armv7 and arm64 CPUs, in the build process * two binaries with a corresponding breakpad file each will be built: * *

blaze-out/xyz-crosstool-ios-arm64/.../PrenotCalculatorBinary_bin
   * blaze-out/xyz-crosstool-ios-arm64/.../PrenotCalculatorBinary.breakpad
   * blaze-out/xyz-crosstool-ios-armv7/.../PrenotCalculatorBinary_bin
   * blaze-out/xyz-crosstool-ios-armv7/.../PrenotCalculatorBinary.breakpad
* *

The first line of the breakpad files will look like this: *

MODULE mac arm64 8A7A2DDD28E83E27B339E63631ADBEF30 PrenotCalculatorBinary_bin
* *

For our application, we have to transform & copy these breakpad files like this: *

$ head -n1 blaze-bin/.../PrenotCalculator_arm64.breakpad
   * MODULE mac arm64 8A7A2DDD28E83E27B339E63631ADBEF30 PrenotCalculator
*/ private void registerTransformAndCopyBreakpadFilesAction() { for (Entry breakpadFiles : getBreakpadFiles().entrySet()) { ruleContext.registerAction( new SpawnAction.Builder().setMnemonic("CopyBreakpadFile") .setShellCommand(String.format( // This sed command replaces the last word of the first line with the application // name. "sed \"1 s/^\\(MODULE \\w* \\w* \\w*\\).*$/\\1 %s/\" < %s > %s", ruleContext.getLabel().getName(), breakpadFiles.getKey().getExecPathString(), breakpadFiles.getValue().getExecPathString())) .addInput(breakpadFiles.getKey()) .addOutput(breakpadFiles.getValue()) .build(ruleContext)); } } /** * Returns a map of input breakpad artifacts from the CPU-specific binaries built for this * ios_application to the new output breakpad artifacts. */ private ImmutableMap getBreakpadFiles() { ImmutableMap.Builder results = ImmutableMap.builder(); for (Entry breakpadFile : attributes.cpuSpecificBreakpadFiles().entrySet()) { Artifact destBreakpad = intermediateArtifacts.breakpadSym(breakpadFile.getKey()); results.put(breakpadFile.getValue(), destBreakpad); } return results.build(); } private void registerExtractTeamPrefixAction(Artifact teamPrefixFile) { String shellCommand = "set -e && " + "PLIST=$(mktemp -t teamprefix.plist) && trap \"rm ${PLIST}\" EXIT && " + extractPlistCommand(attributes.provisioningProfile()) + " > ${PLIST} && " + "/usr/libexec/PlistBuddy -c 'Print ApplicationIdentifierPrefix:0' ${PLIST} > " + teamPrefixFile.getShellEscapedExecPathString(); ruleContext.registerAction( ObjcRuleClasses.spawnBashOnDarwinActionBuilder(ruleContext, shellCommand) .setMnemonic("ExtractIosTeamPrefix") .addInput(attributes.provisioningProfile()) .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(attributes.provisioningProfile()) + " > ${PLIST} && " + "/usr/libexec/PlistBuddy -x -c 'Print Entitlements' ${PLIST} > " + entitlements.getShellEscapedExecPathString(); ruleContext.registerAction( ObjcRuleClasses.spawnBashOnDarwinActionBuilder(ruleContext, shellCommand) .setMnemonic("ExtractIosEntitlements") .setProgressMessage("Extracting entitlements: " + ruleContext.getLabel()) .addInput(attributes.provisioningProfile()) .addOutput(entitlements) .build(ruleContext)); return this; } private void registerEntitlementsVariableSubstitutionAction(Artifact in, Artifact out, Artifact prefix) { String escapedBundleId = ShellUtils.shellEscape(attributes.bundleId()); 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\" " + in.getShellEscapedExecPathString() + " " + "> " + out.getShellEscapedExecPathString(); ruleContext.registerAction(new SpawnAction.Builder() .setMnemonic("SubstituteIosEntitlements") .setShellCommand(shellCommand) .addInput(in) .addInput(prefix) .addOutput(out) .build(ruleContext)); } /** Registers an action to copy Swift standard library dylibs into app bundle. */ private void registerSwiftStdlibActionsIfNecessary() { if (!objcProvider.is(USES_SWIFT)) { return; } AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class); CustomCommandLine.Builder commandLine = CustomCommandLine.builder() .addPath(intermediateArtifacts.swiftFrameworksFileZip().getExecPath()) .add("--platform").add(AppleToolchain.swiftPlatform(appleConfiguration)) .addExecPath("--scan-executable", intermediateArtifacts.combinedArchitectureBinary()); ruleContext.registerAction( ObjcRuleClasses.spawnOnDarwinActionBuilder(ruleContext) .setMnemonic("SwiftStdlibCopy") .setExecutable(attributes.swiftStdlibToolWrapper()) .setCommandLine(commandLine.build()) .addOutput(intermediateArtifacts.swiftFrameworksFileZip()) .addInput(intermediateArtifacts.combinedArchitectureBinary()) // TODO(dmaclach): Adding realpath and xcrunwrapper should not be required once // https://github.com/google/bazel/issues/285 is fixed. .addInput(attributes.realpath()) .addInput(CompilationSupport.xcrunwrapper(ruleContext).getExecutable()) .build(ruleContext)); } private String extractPlistCommand(Artifact provisioningProfile) { return "security cms -D -i " + ShellUtils.shellEscape(provisioningProfile.getExecPathString()); } private String codesignCommand(Artifact entitlements, String appDir) { String signingCertName = ObjcRuleClasses.objcConfiguration(ruleContext).getSigningCertName(); 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(attributes.provisioningProfile()) + " > ${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"), "-version.plist"); } private Artifact getGeneratedEnvironmentPlist() { return ruleContext.getRelatedArtifact( ruleContext.getUniqueDirectory("plists"), "-environment.plist"); } /** * Logic to access attributes 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; } @Nullable String appIcon() { return stringAttribute("app_icon"); } @Nullable String launchImage() { return stringAttribute("launch_image"); } @Nullable Artifact provisioningProfile() { Artifact explicitProvisioningProfile = ruleContext.getPrerequisiteArtifact("provisioning_profile", Mode.TARGET); if (explicitProvisioningProfile != null) { return explicitProvisioningProfile; } return ruleContext.getPrerequisiteArtifact(":default_provisioning_profile", Mode.TARGET); } @Nullable Artifact entitlements() { return ruleContext.getPrerequisiteArtifact("entitlements", Mode.TARGET); } NestedSet dependentLinkedBinaries() { if (ruleContext.attributes().getAttributeDefinition("binary") == null) { return NestedSetBuilder.emptySet(Order.STABLE_ORDER); } NestedSetBuilder 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)); } Artifact iossim() { return checkNotNull(ruleContext.getPrerequisiteArtifact("$iossim", 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 realpath tool. * TODO(dmaclach): Should not be required once https://github.com/google/bazel/issues/285 * is fixed. */ Artifact realpath() { return ruleContext.getPrerequisiteArtifact("$realpath", Mode.HOST); } /** * Returns the location of the environment_plist.sh. */ public Artifact environmentPlistScript() { return checkNotNull( ruleContext.getPrerequisiteArtifact("$environment_plist_sh", Mode.HOST)); } String bundleId() { return checkNotNull(stringAttribute("bundle_id")); } ImmutableMap cpuSpecificBreakpadFiles() { ImmutableMap.Builder 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 sourceBreakpad = Iterables.getOnlyElement( prerequisiteProvider.get(ObjcProvider.BREAKPAD_FILE), null); if (sourceBreakpad != null) { String cpu = prerequisite.getConfiguration().getFragment(AppleConfiguration.class).getIosCpu(); results.put(cpu, sourceBreakpad); } } } } return results.build(); } @Nullable private String stringAttribute(String attribute) { String value = ruleContext.attributes().get(attribute, Type.STRING); return value.isEmpty() ? null : value; } } /** * Transition that results in one configured target per architecture set in {@code * --ios_multi_cpus}. */ protected static class SplitArchTransition implements SplitTransition { @Override public final List split(BuildOptions buildOptions) { List iosMultiCpus = buildOptions.get(AppleCommandLineOptions.class).iosMultiCpus; if (iosMultiCpus.isEmpty()) { return defaultOptions(buildOptions); } ImmutableList.Builder splitBuildOptions = ImmutableList.builder(); for (String iosCpu : iosMultiCpus) { BuildOptions splitOptions = buildOptions.clone(); setArchitectureOptions(splitOptions, iosCpu); setAdditionalOptions(splitOptions, buildOptions); splitOptions.get(ObjcCommandLineOptions.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 defaultOptions(BuildOptions originalOptions) { return ImmutableList.of(); } /** * Sets or overwrites flags on the given split options. * *

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 void setArchitectureOptions(BuildOptions splitOptions, String iosCpu) { splitOptions.get(ObjcCommandLineOptions.class).iosSplitCpu = 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. splitOptions.get(BuildConfiguration.Options.class).cpu = "ios_" + iosCpu; } } @Override public boolean defaultsToSelf() { return true; } /** Returns the configuration distinguisher for this transition instance. */ protected ConfigurationDistinguisher getConfigurationDistinguisher() { return ConfigurationDistinguisher.APPLICATION; } /** * Value used to avoid multiple configurations from conflicting. No two instances of this * transition may exist with the same value in a single Bazel invocation. */ enum ConfigurationDistinguisher { EXTENSION, APPLICATION, FRAMEWORK, UNKNOWN } } }