/* * Copyright 2015-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.apple; import com.dd.plist.NSDictionary; import com.dd.plist.NSObject; import com.dd.plist.PropertyListParser; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.io.ProjectFilesystem.CopySourceMode; import com.facebook.buck.log.Logger; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.step.fs.WriteFileStep; import com.facebook.buck.util.HumanReadableException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; /** * Class to handle: 1. Identifying the best {@code .mobileprovision} file to use based on the bundle * ID and expiration date. 2. Copying that file to the bundle root. 3. Merging the entitlements * specified by the app in its {@code Entitlements.plist} with those provided in the {@code * .mobileprovision} file and writing out a new temporary file used for code-signing. */ class ProvisioningProfileCopyStep implements Step { private static final String KEYCHAIN_ACCESS_GROUPS = "keychain-access-groups"; private static final String APPLICATION_IDENTIFIER = "application-identifier"; private static final String BUNDLE_ID = "bundle-id"; private static final String PROFILE_UUID = "provisioning-profile-uuid"; private static final String PROFILE_FILENAME = "provisioning-profile-file"; private static final String TEAM_IDENTIFIER = "team-identifier"; private static final String ENTITLEMENTS = "entitlements"; private final ProjectFilesystem filesystem; private final ApplePlatform platform; private final Optional<Path> entitlementsPlist; private final Optional<String> provisioningProfileUUID; private final Path provisioningProfileDestination; private final Path signingEntitlementsTempPath; private final ProvisioningProfileStore provisioningProfileStore; private final CodeSignIdentityStore codeSignIdentityStore; private final Path infoPlist; private final SettableFuture<Optional<ProvisioningProfileMetadata>> selectedProvisioningProfileFuture = SettableFuture.create(); private Optional<Path> dryRunResultsPath; private static final Logger LOG = Logger.get(ProvisioningProfileCopyStep.class); /** * @param infoPlist Bundle relative path of the bundle's {@code Info.plist} file. * @param provisioningProfileUUID Optional. If specified, override the {@code .mobileprovision} * auto-detect and attempt to use {@code UUID.mobileprovision}. * @param entitlementsPlist Optional. If specified, use the metadata in this {@code * Entitlements.plist} file to determine app prefix. * @param provisioningProfileStore Known provisioning profiles to choose from. * @param provisioningProfileDestination Where to copy the {@code .mobileprovision} file, normally * the bundle root. * @param signingEntitlementsTempPath Where to copy the code signing entitlements file, normally a * scratch directory. * @param dryRunResultsPath If set, will output a plist into this path with the results of this * step. * <p>If a suitable profile was found, this will contain metadata on the provisioning profile * selected. * <p>If no suitable profile was found, this will contain the bundle ID and entitlements * needed in a profile (in lieu of throwing an exception.) */ public ProvisioningProfileCopyStep( ProjectFilesystem filesystem, Path infoPlist, ApplePlatform platform, Optional<String> provisioningProfileUUID, Optional<Path> entitlementsPlist, ProvisioningProfileStore provisioningProfileStore, Path provisioningProfileDestination, Path signingEntitlementsTempPath, CodeSignIdentityStore codeSignIdentityStore, Optional<Path> dryRunResultsPath) { this.filesystem = filesystem; this.provisioningProfileDestination = provisioningProfileDestination; this.infoPlist = infoPlist; this.platform = platform; this.provisioningProfileUUID = provisioningProfileUUID; this.entitlementsPlist = entitlementsPlist; this.provisioningProfileStore = provisioningProfileStore; this.codeSignIdentityStore = codeSignIdentityStore; this.signingEntitlementsTempPath = signingEntitlementsTempPath; this.dryRunResultsPath = dryRunResultsPath; } @Override public StepExecutionResult execute(ExecutionContext context) throws InterruptedException { final String bundleID; try { bundleID = AppleInfoPlistParsing.getBundleIdFromPlistStream( filesystem.getInputStreamForRelativePath(infoPlist)) .get(); } catch (IOException e) { throw new HumanReadableException("Unable to get bundle ID from info.plist: " + infoPlist); } final Optional<ImmutableMap<String, NSObject>> entitlements; final String prefix; if (entitlementsPlist.isPresent()) { try { NSDictionary entitlementsPlistDict = (NSDictionary) PropertyListParser.parse(entitlementsPlist.get().toFile()); entitlements = Optional.of(ImmutableMap.copyOf(entitlementsPlistDict.getHashMap())); prefix = ProvisioningProfileMetadata.prefixFromEntitlements(entitlements.get()).orElse("*"); } catch (IOException e) { throw new HumanReadableException( "Unable to find entitlement .plist: " + entitlementsPlist.get()); } catch (Exception e) { throw new HumanReadableException( "Malformed entitlement .plist: " + entitlementsPlist.get()); } } else { entitlements = ProvisioningProfileStore.MATCH_ANY_ENTITLEMENT; prefix = "*"; } final Optional<ImmutableList<CodeSignIdentity>> identities; if (!codeSignIdentityStore.getIdentities().isEmpty()) { identities = Optional.of(codeSignIdentityStore.getIdentities()); } else { identities = ProvisioningProfileStore.MATCH_ANY_IDENTITY; } Optional<ProvisioningProfileMetadata> bestProfile = provisioningProfileUUID.isPresent() ? provisioningProfileStore.getProvisioningProfileByUUID(provisioningProfileUUID.get()) : provisioningProfileStore.getBestProvisioningProfile( bundleID, platform, entitlements, identities); if (dryRunResultsPath.isPresent()) { try { NSDictionary dryRunResult = new NSDictionary(); dryRunResult.put(BUNDLE_ID, bundleID); dryRunResult.put(ENTITLEMENTS, entitlements.orElse(ImmutableMap.of())); if (bestProfile.isPresent()) { dryRunResult.put(PROFILE_UUID, bestProfile.get().getUUID()); dryRunResult.put( PROFILE_FILENAME, bestProfile.get().getProfilePath().getFileName().toString()); dryRunResult.put( TEAM_IDENTIFIER, bestProfile.get().getEntitlements().get("com.apple.developer.team-identifier")); } filesystem.writeContentsToPath(dryRunResult.toXMLPropertyList(), dryRunResultsPath.get()); } catch (IOException e) { context.logError( e, "Failed when trying to write dry run results: %s", getDescription(context)); return StepExecutionResult.ERROR; } } selectedProvisioningProfileFuture.set(bestProfile); if (!bestProfile.isPresent()) { String message = "No valid non-expired provisioning profiles match for " + prefix + "." + bundleID; if (dryRunResultsPath.isPresent()) { LOG.warn(message); return StepExecutionResult.SUCCESS; } else { throw new HumanReadableException(message); } } Path provisioningProfileSource = bestProfile.get().getProfilePath(); // Copy the actual .mobileprovision. try { filesystem.copy( provisioningProfileSource, provisioningProfileDestination, CopySourceMode.FILE); } catch (IOException e) { context.logError(e, "Failed when trying to copy: %s", getDescription(context)); return StepExecutionResult.ERROR; } // Merge the entitlements with the profile, and write out. if (entitlementsPlist.isPresent()) { return (new PlistProcessStep( filesystem, entitlementsPlist.get(), Optional.empty(), signingEntitlementsTempPath, bestProfile.get().getMergeableEntitlements(), ImmutableMap.of(), PlistProcessStep.OutputFormat.XML)) .execute(context); } else { // No entitlements.plist explicitly specified; write out the minimal entitlements needed. String appID = bestProfile.get().getAppID().getFirst() + "." + bundleID; NSDictionary entitlementsPlist = new NSDictionary(); entitlementsPlist.putAll(bestProfile.get().getMergeableEntitlements()); entitlementsPlist.put(APPLICATION_IDENTIFIER, appID); entitlementsPlist.put(KEYCHAIN_ACCESS_GROUPS, new String[] {appID}); return (new WriteFileStep( filesystem, entitlementsPlist.toXMLPropertyList(), signingEntitlementsTempPath, /* executable */ false)) .execute(context); } } @Override public String getShortName() { return "provisioning-profile-copy"; } @Override public String getDescription(ExecutionContext context) { return String.format("provisioning-profile-copy %s", provisioningProfileDestination); } /** Returns a future that's populated once the rule is executed. */ public ListenableFuture<Optional<ProvisioningProfileMetadata>> getSelectedProvisioningProfileFuture() { return selectedProvisioningProfileFuture; } }