// Copyright 2015 Google Inc. 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.xcode.common.TargetDeviceFamily.UI_DEVICE_FAMILY_VALUES;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.Artifact;
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.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.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.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraActoolArgs;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.xcode.common.InvalidFamilyNameException;
import com.google.devtools.build.xcode.common.Platform;
import com.google.devtools.build.xcode.common.RepeatedFamilyNameException;
import com.google.devtools.build.xcode.common.TargetDeviceFamily;
import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting;
import java.util.List;
import java.util.Set;
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");
/**
* Transition that when applied to a target generates a configured target for each value in
* {@code --ios_multi_cpus}, such that {@code --ios_cpu} is set to a different one of those values
* in the configured targets.
*/
public static final SplitTransition SPLIT_ARCH_TRANSITION =
new SplitTransition() {
@Override
public List split(BuildOptions buildOptions) {
List iosMultiCpus = buildOptions.get(ObjcCommandLineOptions.class).iosMultiCpus;
if (iosMultiCpus.isEmpty()) {
return ImmutableList.of();
}
ImmutableList.Builder splitBuildOptions = ImmutableList.builder();
for (String iosCpu : iosMultiCpus) {
BuildOptions splitOptions = buildOptions.clone();
splitOptions.get(ObjcCommandLineOptions.class).iosSplitCpu = iosCpu;
splitBuildOptions.add(splitOptions);
}
return splitBuildOptions.build();
}
@Override
public boolean defaultsToSelf() {
return true;
}
};
@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";
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 ImmutableSet families;
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 optionsProvider provider containing options and plist settings for this rule and its
* dependencies
* @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"})
*/
ReleaseBundlingSupport(
RuleContext ruleContext, ObjcProvider objcProvider, OptionsProvider optionsProvider,
LinkedBinary linkedBinary, String bundleDirFormat) {
this.linkedBinary = linkedBinary;
this.attributes = new Attributes(ruleContext);
this.ruleContext = ruleContext;
this.objcProvider = objcProvider;
this.families = ImmutableSet.copyOf(attributes.families());
this.intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext);
bundling = bundling(ruleContext, objcProvider, optionsProvider, bundleDirFormat);
bundleSupport = new BundleSupport(ruleContext, families, bundling, extraActoolArgs());
}
/**
* 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 (families.isEmpty()) {
ruleContext.attributeError("families", INVALID_FAMILIES_ERROR);
}
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
*/
ReleaseBundlingSupport registerActions() {
bundleSupport.registerActions(objcProvider);
registerCombineArchitecturesAction();
ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
Artifact ipaOutput = ruleContext.getImplicitOutputArtifact(IPA);
Artifact maybeSignedIpa;
if (objcConfiguration.getPlatform() == Platform.SIMULATOR) {
maybeSignedIpa = ipaOutput;
} else if (attributes.provisioningProfile() == null) {
throw new IllegalStateException(DEVICE_NO_PROVISIONING_PROFILE);
} else {
maybeSignedIpa = registerBundleSigningActions(ipaOutput);
}
BundleMergeControlBytes bundleMergeControlBytes = new BundleMergeControlBytes(
bundling, maybeSignedIpa, objcConfiguration, families);
registerBundleMergeActions(
maybeSignedIpa, bundling.getBundleContentArtifacts(), bundleMergeControlBytes);
return this;
}
private Artifact registerBundleSigningActions(Artifact ipaOutput) {
PathFragment entitlementsDirectory = ruleContext.getUniqueDirectory("entitlements");
Artifact teamPrefixFile = ruleContext.getRelatedArtifact(
entitlementsDirectory, ".team_prefix_file");
registerExtractTeamPrefixAction(teamPrefixFile);
Artifact entitlementsNeedingSubstitution = attributes.entitlements();
if (entitlementsNeedingSubstitution == null) {
entitlementsNeedingSubstitution = ruleContext.getRelatedArtifact(
entitlementsDirectory, ".entitlements_with_variables");
registerExtractEntitlementsAction(entitlementsNeedingSubstitution);
}
Artifact entitlements = ruleContext.getRelatedArtifact(
entitlementsDirectory, ".entitlements");
registerEntitlementsVariableSubstitutionAction(
entitlementsNeedingSubstitution, entitlements, teamPrefixFile);
Artifact ipaUnsigned = ObjcRuleClasses.artifactByAppendingToRootRelativePath(
ruleContext, ipaOutput.getExecPath(), ".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);
xcodeProviderBuilder.addXcodeprojBuildSettings(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
*/
ReleaseBundlingSupport addFilesToBuild(NestedSetBuilder filesToBuild) {
NestedSetBuilder debugSymbolBuilder = NestedSetBuilder.stableOrder()
.addTransitive(objcProvider.get(ObjcProvider.DEBUG_SYMBOLS));
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}.
*/
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.
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.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.singleArchitectureBinary(),
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);
ImmutableList substitutions = ImmutableList.of(
Substitution.of("%app_name%", ruleContext.getLabel().getName()),
Substitution.of("%ipa_file%", ipaInput.getRootRelativePath().getPathString()),
Substitution.of("%sim_device%", objcConfiguration.getIosSimulatorDevice()),
Substitution.of("%sdk_version%", objcConfiguration.getIosSimulatorVersion()),
Substitution.of("%iossim%", attributes.iossim().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.
*/
RunfilesSupport runfilesSupport(Artifact runnerScript) {
Artifact ipaFile = ruleContext.getImplicitOutputArtifact(ReleaseBundlingSupport.IPA);
Runfiles runfiles = new Runfiles.Builder()
.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 static Bundling bundling(
RuleContext ruleContext, ObjcProvider objcProvider, OptionsProvider optionsProvider,
String bundleDirFormat) {
ImmutableList extraBundleFiles;
ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
if (objcConfiguration.getPlatform() == Platform.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(ruleContext.getLabel().getName())
.setBundleDirFormat(bundleDirFormat)
.setExtraBundleFiles(extraBundleFiles)
.setObjcProvider(objcProvider)
.setInfoplistMerging(
BundleSupport.infoPlistMerging(ruleContext, objcProvider, optionsProvider))
.setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext))
.setPrimaryBundleId(primaryBundleId)
.setFallbackBundleId(fallbackBundleId)
.build();
}
private void registerCombineArchitecturesAction() {
Artifact resultingLinkedBinary = intermediateArtifacts.combinedArchitectureBinary();
NestedSet linkedBinaries = linkedBinaries();
ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("ObjcCombiningArchitectures")
.addTransitiveInputs(linkedBinaries)
.addOutput(resultingLinkedBinary)
.setExecutable(ObjcActionsBuilder.LIPO)
.setCommandLine(CustomCommandLine.builder()
.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.singleArchitectureBinary());
}
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.
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
ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("IosSignBundle")
.setProgressMessage("Signing iOS bundle: " + ruleContext.getLabel())
.setExecutable(new PathFragment("/bin/bash"))
.addArgument("-c")
// TODO(bazel-team): Support --resource-rules for resources
.addArgument("set -e && "
+ "t=$(mktemp -d -t signing_intermediate) && "
// Get an absolute path since we need to cd into the temp directory for zip.
+ "signed_ipa=${PWD}/" + ipaOutput.getExecPathString() + " && "
+ "unzip -qq " + ipaUnsigned.getExecPathString() + " -d ${t} && "
+ codesignCommand(
attributes.provisioningProfile(),
entitlements,
"${t}/" + bundling.getBundleDir())
// Using zip since we need to preserve permissions
+ " && cd \"${t}\" && /usr/bin/zip -q -r \"${signed_ipa}\" .")
.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));
}
private void registerExtractTeamPrefixAction(Artifact teamPrefixFile) {
ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("ExtractIosTeamPrefix")
.setExecutable(new PathFragment("/bin/bash"))
.addArgument("-c")
.addArgument("set -e &&"
+ " PLIST=$(" + extractPlistCommand(attributes.provisioningProfile()) + ") && "
// We think PlistBuddy uses PRead internally to seek through the file. Or possibly
// mmaps the file. Or something similar.
//
// Pipe FDs do not support PRead or mmap, though.
//
// <<< however does something magical like write to a temporary file or something
// like that internally, which means that this Just Works.
+ " PREFIX=$(/usr/libexec/PlistBuddy -c 'Print ApplicationIdentifierPrefix:0'"
+ " /dev/stdin <<< \"${PLIST}\") && "
+ " echo ${PREFIX} > " + teamPrefixFile.getExecPathString())
.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.
ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("ExtractIosEntitlements")
.setProgressMessage("Extracting entitlements: " + ruleContext.getLabel())
.setExecutable(new PathFragment("/bin/bash"))
.addArgument("-c")
.addArgument("set -e && "
+ "PLIST=$("
+ extractPlistCommand(attributes.provisioningProfile()) + ") && "
// We think PlistBuddy uses PRead internally to seek through the file. Or possibly
// mmaps the file. Or something similar.
//
// Pipe FDs do not support PRead or mmap, though.
//
// <<< however does something magical like write to a temporary file or something
// like that internally, which means that this Just Works.
+ "/usr/libexec/PlistBuddy -x -c 'Print Entitlements' /dev/stdin <<< \"${PLIST}\" "
+ "> " + entitlements.getExecPathString())
.addInput(attributes.provisioningProfile())
.addOutput(entitlements)
.build(ruleContext));
return this;
}
private void registerEntitlementsVariableSubstitutionAction(Artifact in, Artifact out,
Artifact prefix) {
String escapedBundleId = ShellUtils.shellEscape(attributes.bundleId());
ruleContext.registerAction(new SpawnAction.Builder()
.setMnemonic("SubstituteIosEntitlements")
.setExecutable(new PathFragment("/bin/bash"))
.addArgument("-c")
.addArgument("set -e && "
+ "PREFIX=\"$(cat " + prefix.getExecPathString() + ")\" && "
+ "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.getExecPathString() + " "
+ "> " + out.getExecPathString())
.addInput(in)
.addInput(prefix)
.addOutput(out)
.build(ruleContext));
}
private String extractPlistCommand(Artifact provisioningProfile) {
return "security cms -D -i " + ShellUtils.shellEscape(provisioningProfile.getExecPathString());
}
private String codesignCommand(
Artifact provisioningProfile, Artifact entitlements, String appDir) {
String fingerprintCommand =
"/usr/libexec/PlistBuddy -c 'Print DeveloperCertificates:0' /dev/stdin <<< "
+ "$(" + extractPlistCommand(provisioningProfile) + ") | "
+ "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",
fingerprintCommand,
entitlements.getExecPathString(),
appDir);
}
/**
* 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() {
return ruleContext.getPrerequisiteArtifact("provisioning_profile", Mode.TARGET);
}
/**
* Returns the value of the {@code families} attribute in a form that is more useful than a list
* of strings. Returns an empty set for any invalid {@code families} attribute value, including
* an empty list.
*/
Set families() {
List rawFamilies = ruleContext.attributes().get("families", Type.STRING_LIST);
try {
return TargetDeviceFamily.fromNamesInRule(rawFamilies);
} catch (InvalidFamilyNameException | RepeatedFamilyNameException e) {
return ImmutableSet.of();
}
}
@Nullable
Artifact entitlements() {
return ruleContext.getPrerequisiteArtifact("entitlements", Mode.TARGET);
}
NestedSet extends Artifact> 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 runnerScriptTemplate() {
return checkNotNull(
ruleContext.getPrerequisiteArtifact("$runner_script_template", Mode.HOST));
}
String bundleId() {
return checkNotNull(stringAttribute("bundle_id"));
}
@Nullable
private String stringAttribute(String attribute) {
String value = ruleContext.attributes().get(attribute, Type.STRING);
return value.isEmpty() ? null : value;
}
}
}