naiveproxy/build/config/ios/codesign.py
2018-02-02 05:49:39 -05:00

532 lines
18 KiB
Python

# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import datetime
import fnmatch
import glob
import os
import plistlib
import shutil
import subprocess
import sys
import tempfile
def GetProvisioningProfilesDir():
"""Returns the location of the installed mobile provisioning profiles.
Returns:
The path to the directory containing the installed mobile provisioning
profiles as a string.
"""
return os.path.join(
os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles')
def LoadPlistFile(plist_path):
"""Loads property list file at |plist_path|.
Args:
plist_path: path to the property list file to load.
Returns:
The content of the property list file as a python object.
"""
return plistlib.readPlistFromString(subprocess.check_output([
'xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path]))
class Bundle(object):
"""Wraps a bundle."""
def __init__(self, bundle_path):
"""Initializes the Bundle object with data from bundle Info.plist file."""
self._path = bundle_path
self._data = LoadPlistFile(os.path.join(self._path, 'Info.plist'))
@property
def path(self):
return self._path
@property
def identifier(self):
return self._data['CFBundleIdentifier']
@property
def binary_path(self):
return os.path.join(self._path, self._data['CFBundleExecutable'])
def Validate(self, expected_mappings):
"""Checks that keys in the bundle have the expected value.
Args:
expected_mappings: a dictionary of string to object, each mapping will
be looked up in the bundle data to check it has the same value (missing
values will be ignored)
Returns:
A dictionary of the key with a different value between expected_mappings
and the content of the bundle (i.e. errors) so that caller can format the
error message. The dictionary will be empty if there are no errors.
"""
errors = {}
for key, expected_value in expected_mappings.iteritems():
if key in self._data:
value = self._data[key]
if value != expected_value:
errors[key] = (value, expected_value)
return errors
class ProvisioningProfile(object):
"""Wraps a mobile provisioning profile file."""
def __init__(self, provisioning_profile_path):
"""Initializes the ProvisioningProfile with data from profile file."""
self._path = provisioning_profile_path
self._data = plistlib.readPlistFromString(subprocess.check_output([
'xcrun', 'security', 'cms', '-D', '-u', 'certUsageAnyCA',
'-i', provisioning_profile_path]))
@property
def path(self):
return self._path
@property
def application_identifier_pattern(self):
return self._data.get('Entitlements', {}).get('application-identifier', '')
@property
def team_identifier(self):
return self._data.get('TeamIdentifier', [''])[0]
@property
def entitlements(self):
return self._data.get('Entitlements', {})
@property
def expiration_date(self):
return self._data.get('ExpirationDate', datetime.datetime.now())
def ValidToSignBundle(self, bundle_identifier):
"""Checks whether the provisioning profile can sign bundle_identifier.
Args:
bundle_identifier: the identifier of the bundle that needs to be signed.
Returns:
True if the mobile provisioning profile can be used to sign a bundle
with the corresponding bundle_identifier, False otherwise.
"""
return fnmatch.fnmatch(
'%s.%s' % (self.team_identifier, bundle_identifier),
self.application_identifier_pattern)
def Install(self, installation_path):
"""Copies mobile provisioning profile info to |installation_path|."""
shutil.copy2(self.path, installation_path)
class Entitlements(object):
"""Wraps an Entitlement plist file."""
def __init__(self, entitlements_path):
"""Initializes Entitlements object from entitlement file."""
self._path = entitlements_path
self._data = LoadPlistFile(self._path)
@property
def path(self):
return self._path
def ExpandVariables(self, substitutions):
self._data = self._ExpandVariables(self._data, substitutions)
def _ExpandVariables(self, data, substitutions):
if isinstance(data, str):
for key, substitution in substitutions.iteritems():
data = data.replace('$(%s)' % (key,), substitution)
return data
if isinstance(data, dict):
for key, value in data.iteritems():
data[key] = self._ExpandVariables(value, substitutions)
return data
if isinstance(data, list):
for i, value in enumerate(data):
data[i] = self._ExpandVariables(value, substitutions)
return data
def LoadDefaults(self, defaults):
for key, value in defaults.iteritems():
if key not in self._data:
self._data[key] = value
def WriteTo(self, target_path):
plistlib.writePlist(self._data, target_path)
def FindProvisioningProfile(bundle_identifier, required):
"""Finds mobile provisioning profile to use to sign bundle.
Args:
bundle_identifier: the identifier of the bundle to sign.
Returns:
The ProvisioningProfile object that can be used to sign the Bundle
object or None if no matching provisioning profile was found.
"""
provisioning_profile_paths = glob.glob(
os.path.join(GetProvisioningProfilesDir(), '*.mobileprovision'))
# Iterate over all installed mobile provisioning profiles and filter those
# that can be used to sign the bundle, ignoring expired ones.
now = datetime.datetime.now()
valid_provisioning_profiles = []
one_hour = datetime.timedelta(0, 3600)
for provisioning_profile_path in provisioning_profile_paths:
provisioning_profile = ProvisioningProfile(provisioning_profile_path)
if provisioning_profile.expiration_date - now < one_hour:
sys.stderr.write(
'Warning: ignoring expired provisioning profile: %s.\n' %
provisioning_profile_path)
continue
if provisioning_profile.ValidToSignBundle(bundle_identifier):
valid_provisioning_profiles.append(provisioning_profile)
if not valid_provisioning_profiles:
if required:
sys.stderr.write(
'Error: no mobile provisioning profile found for "%s".\n' %
bundle_identifier)
sys.exit(1)
return None
# Select the most specific mobile provisioning profile, i.e. the one with
# the longest application identifier pattern (prefer the one with the latest
# expiration date as a secondary criteria).
selected_provisioning_profile = max(
valid_provisioning_profiles,
key=lambda p: (len(p.application_identifier_pattern), p.expiration_date))
one_week = datetime.timedelta(7)
if selected_provisioning_profile.expiration_date - now < 2 * one_week:
sys.stderr.write(
'Warning: selected provisioning profile will expire soon: %s' %
selected_provisioning_profile.path)
return selected_provisioning_profile
def CodeSignBundle(bundle_path, identity, extra_args):
process = subprocess.Popen(['xcrun', 'codesign', '--force', '--sign',
identity, '--timestamp=none'] + list(extra_args) + [bundle_path],
stderr=subprocess.PIPE)
_, stderr = process.communicate()
if process.returncode:
sys.stderr.write(stderr)
sys.exit(process.returncode)
for line in stderr.splitlines():
if line.endswith(': replacing existing signature'):
# Ignore warning about replacing existing signature as this should only
# happen when re-signing system frameworks (and then it is expected).
continue
sys.stderr.write(line)
sys.stderr.write('\n')
def InstallSystemFramework(framework_path, bundle_path, args):
"""Install framework from |framework_path| to |bundle| and code-re-sign it."""
installed_framework_path = os.path.join(
bundle_path, 'Frameworks', os.path.basename(framework_path))
if os.path.exists(installed_framework_path):
shutil.rmtree(installed_framework_path)
shutil.copytree(framework_path, installed_framework_path)
CodeSignBundle(installed_framework_path, args.identity,
['--deep', '--preserve-metadata=identifier,entitlements'])
def GenerateEntitlements(path, provisioning_profile, bundle_identifier):
"""Generates an entitlements file.
Args:
path: path to the entitlements template file
provisioning_profile: ProvisioningProfile object to use, may be None
bundle_identifier: identifier of the bundle to sign.
"""
entitlements = Entitlements(path)
if provisioning_profile:
entitlements.LoadDefaults(provisioning_profile.entitlements)
app_identifier_prefix = provisioning_profile.team_identifier + '.'
else:
app_identifier_prefix = '*.'
entitlements.ExpandVariables({
'CFBundleIdentifier': bundle_identifier,
'AppIdentifierPrefix': app_identifier_prefix,
})
return entitlements
def GenerateBundleInfoPlist(bundle_path, plist_compiler, partial_plist):
"""Generates the bundle Info.plist for a list of partial .plist files.
Args:
bundle_path: path to the bundle
plist_compiler: string, path to the Info.plist compiler
partial_plist: list of path to partial .plist files to merge
"""
# Filter empty partial .plist files (this happens if an application
# does not include need to compile any asset catalog, in which case
# the partial .plist file from the asset catalog compilation step is
# just a stamp file).
filtered_partial_plist = []
for plist in partial_plist:
plist_size = os.stat(plist).st_size
if plist_size:
filtered_partial_plist.append(plist)
# Invoke the plist_compiler script. It needs to be a python script.
subprocess.check_call([
'python', plist_compiler, 'merge', '-f', 'binary1',
'-o', os.path.join(bundle_path, 'Info.plist'),
] + filtered_partial_plist)
class Action(object):
"""Class implementing one action supported by the script."""
@classmethod
def Register(cls, subparsers):
parser = subparsers.add_parser(cls.name, help=cls.help)
parser.set_defaults(func=cls._Execute)
cls._Register(parser)
class CodeSignBundleAction(Action):
"""Class implementing the code-sign-bundle action."""
name = 'code-sign-bundle'
help = 'perform code signature for a bundle'
@staticmethod
def _Register(parser):
parser.add_argument(
'--entitlements', '-e', dest='entitlements_path',
help='path to the entitlements file to use')
parser.add_argument(
'path', help='path to the iOS bundle to codesign')
parser.add_argument(
'--identity', '-i', required=True,
help='identity to use to codesign')
parser.add_argument(
'--binary', '-b', required=True,
help='path to the iOS bundle binary')
parser.add_argument(
'--framework', '-F', action='append', default=[], dest='frameworks',
help='install and resign system framework')
parser.add_argument(
'--disable-code-signature', action='store_true', dest='no_signature',
help='disable code signature')
parser.add_argument(
'--disable-embedded-mobileprovision', action='store_false',
default=True, dest='embedded_mobileprovision',
help='disable finding and embedding mobileprovision')
parser.add_argument(
'--platform', '-t', required=True,
help='platform the signed bundle is targeting')
parser.add_argument(
'--partial-info-plist', '-p', action='append', default=[],
help='path to partial Info.plist to merge to create bundle Info.plist')
parser.add_argument(
'--plist-compiler-path', '-P', action='store',
help='path to the plist compiler script (for --partial-info-plist)')
parser.set_defaults(no_signature=False)
@staticmethod
def _Execute(args):
if not args.identity:
args.identity = '-'
if args.partial_info_plist:
GenerateBundleInfoPlist(
args.path,
args.plist_compiler_path,
args.partial_info_plist)
bundle = Bundle(args.path)
# According to Apple documentation, the application binary must be the same
# as the bundle name without the .app suffix. See crbug.com/740476 for more
# information on what problem this can cause.
#
# To prevent this class of error, fail with an error if the binary name is
# incorrect in the Info.plist as it is not possible to update the value in
# Info.plist at this point (the file has been copied by a different target
# and ninja would consider the build dirty if it was updated).
#
# Also checks that the name of the bundle is correct too (does not cause the
# build to be considered dirty, but still terminate the script in case of an
# incorrect bundle name).
#
# Apple documentation is available at:
# https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html
bundle_name = os.path.splitext(os.path.basename(bundle.path))[0]
errors = bundle.Validate({
'CFBundleName': bundle_name,
'CFBundleExecutable': bundle_name,
})
if errors:
for key in sorted(errors):
value, expected_value = errors[key]
sys.stderr.write('%s: error: %s value incorrect: %s != %s\n' % (
bundle.path, key, value, expected_value))
sys.stderr.flush()
sys.exit(1)
# Delete existing embedded mobile provisioning.
embedded_provisioning_profile = os.path.join(
bundle.path, 'embedded.mobileprovision')
if os.path.isfile(embedded_provisioning_profile):
os.unlink(embedded_provisioning_profile)
# Delete existing code signature.
signature_file = os.path.join(args.path, '_CodeSignature', 'CodeResources')
if os.path.isfile(signature_file):
shutil.rmtree(os.path.dirname(signature_file))
# Install system frameworks if requested.
for framework_path in args.frameworks:
InstallSystemFramework(framework_path, args.path, args)
# Copy main binary into bundle.
if os.path.isfile(bundle.binary_path):
os.unlink(bundle.binary_path)
shutil.copy(args.binary, bundle.binary_path)
if args.no_signature:
return
codesign_extra_args = []
if args.embedded_mobileprovision:
# Find mobile provisioning profile and embeds it into the bundle (if a
# code signing identify has been provided, fails if no valid mobile
# provisioning is found).
provisioning_profile_required = args.identity != '-'
provisioning_profile = FindProvisioningProfile(
bundle.identifier, provisioning_profile_required)
if provisioning_profile and args.platform != 'iphonesimulator':
provisioning_profile.Install(embedded_provisioning_profile)
if args.entitlements_path is not None:
temporary_entitlements_file = \
tempfile.NamedTemporaryFile(suffix='.xcent')
codesign_extra_args.extend(
['--entitlements', temporary_entitlements_file.name])
entitlements = GenerateEntitlements(
args.entitlements_path, provisioning_profile, bundle.identifier)
entitlements.WriteTo(temporary_entitlements_file.name)
CodeSignBundle(bundle.path, args.identity, codesign_extra_args)
class CodeSignFileAction(Action):
"""Class implementing code signature for a single file."""
name = 'code-sign-file'
help = 'code-sign a single file'
@staticmethod
def _Register(parser):
parser.add_argument(
'path', help='path to the file to codesign')
parser.add_argument(
'--identity', '-i', required=True,
help='identity to use to codesign')
parser.add_argument(
'--output', '-o',
help='if specified copy the file to that location before signing it')
parser.set_defaults(sign=True)
@staticmethod
def _Execute(args):
if not args.identity:
args.identity = '-'
install_path = args.path
if args.output:
if os.path.isfile(args.output):
os.unlink(args.output)
elif os.path.isdir(args.output):
shutil.rmtree(args.output)
if os.path.isfile(args.path):
shutil.copy(args.path, args.output)
elif os.path.isdir(args.path):
shutil.copytree(args.path, args.output)
install_path = args.output
CodeSignBundle(install_path, args.identity,
['--deep', '--preserve-metadata=identifier,entitlements'])
class GenerateEntitlementsAction(Action):
"""Class implementing the generate-entitlements action."""
name = 'generate-entitlements'
help = 'generate entitlements file'
@staticmethod
def _Register(parser):
parser.add_argument(
'--entitlements', '-e', dest='entitlements_path',
help='path to the entitlements file to use')
parser.add_argument(
'path', help='path to the entitlements file to generate')
parser.add_argument(
'--info-plist', '-p', required=True,
help='path to the bundle Info.plist')
@staticmethod
def _Execute(args):
info_plist = LoadPlistFile(args.info_plist)
bundle_identifier = info_plist['CFBundleIdentifier']
provisioning_profile = FindProvisioningProfile(bundle_identifier, False)
entitlements = GenerateEntitlements(
args.entitlements_path, provisioning_profile, bundle_identifier)
entitlements.WriteTo(args.path)
def Main():
parser = argparse.ArgumentParser('codesign iOS bundles')
parser.add_argument('--developer_dir', required=False,
help='Path to Xcode.')
subparsers = parser.add_subparsers()
actions = [
CodeSignBundleAction,
CodeSignFileAction,
GenerateEntitlementsAction,
]
for action in actions:
action.Register(subparsers)
args = parser.parse_args()
if args.developer_dir:
os.environ['DEVELOPER_DIR'] = args.developer_dir
args.func(args)
if __name__ == '__main__':
sys.exit(Main())