summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/config/ios/codesign.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/build/config/ios/codesign.py')
-rw-r--r--third_party/libwebrtc/build/config/ios/codesign.py691
1 files changed, 691 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/config/ios/codesign.py b/third_party/libwebrtc/build/config/ios/codesign.py
new file mode 100644
index 0000000000..15d25a78df
--- /dev/null
+++ b/third_party/libwebrtc/build/config/ios/codesign.py
@@ -0,0 +1,691 @@
+# 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.
+
+from __future__ import print_function
+
+import argparse
+import codecs
+import datetime
+import fnmatch
+import glob
+import json
+import os
+import plistlib
+import shutil
+import subprocess
+import sys
+import tempfile
+
+if sys.version_info.major < 3:
+ basestring_compat = basestring
+else:
+ basestring_compat = str
+
+
+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 ReadPlistFromString(plist_bytes):
+ """Parse property list from given |plist_bytes|.
+
+ Args:
+ plist_bytes: contents of property list to load. Must be bytes in python 3.
+
+ Returns:
+ The contents of property list as a python object.
+ """
+ if sys.version_info.major == 2:
+ return plistlib.readPlistFromString(plist_bytes)
+ else:
+ return plistlib.loads(plist_bytes)
+
+
+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.
+ """
+ if sys.version_info.major == 2:
+ return plistlib.readPlistFromString(
+ subprocess.check_output(
+ ['xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path]))
+ else:
+ with open(plist_path, 'rb') as fp:
+ return plistlib.load(fp)
+
+
+def CreateSymlink(value, location):
+ """Creates symlink with value at location if the target exists."""
+ target = os.path.join(os.path.dirname(location), value)
+ if os.path.exists(location):
+ os.unlink(location)
+ os.symlink(value, location)
+
+
+class Bundle(object):
+ """Wraps a bundle."""
+
+ def __init__(self, bundle_path, platform):
+ """Initializes the Bundle object with data from bundle Info.plist file."""
+ self._path = bundle_path
+ self._kind = Bundle.Kind(platform, os.path.splitext(bundle_path)[-1])
+ self._data = None
+
+ def Load(self):
+ self._data = LoadPlistFile(self.info_plist_path)
+
+ @staticmethod
+ def Kind(platform, extension):
+ if platform == 'iphonesimulator' or platform == 'iphoneos':
+ return 'ios'
+ if platform == 'macosx':
+ if extension == '.framework':
+ return 'mac_framework'
+ return 'mac'
+ raise ValueError('unknown bundle type %s for %s' % (extension, platform))
+
+ @property
+ def kind(self):
+ return self._kind
+
+ @property
+ def path(self):
+ return self._path
+
+ @property
+ def contents_dir(self):
+ if self._kind == 'mac':
+ return os.path.join(self.path, 'Contents')
+ if self._kind == 'mac_framework':
+ return os.path.join(self.path, 'Versions/A')
+ return self.path
+
+ @property
+ def executable_dir(self):
+ if self._kind == 'mac':
+ return os.path.join(self.contents_dir, 'MacOS')
+ return self.contents_dir
+
+ @property
+ def resources_dir(self):
+ if self._kind == 'mac' or self._kind == 'mac_framework':
+ return os.path.join(self.contents_dir, 'Resources')
+ return self.path
+
+ @property
+ def info_plist_path(self):
+ if self._kind == 'mac_framework':
+ return os.path.join(self.resources_dir, 'Info.plist')
+ return os.path.join(self.contents_dir, 'Info.plist')
+
+ @property
+ def signature_dir(self):
+ return os.path.join(self.contents_dir, '_CodeSignature')
+
+ @property
+ def identifier(self):
+ return self._data['CFBundleIdentifier']
+
+ @property
+ def binary_name(self):
+ return self._data['CFBundleExecutable']
+
+ @property
+ def binary_path(self):
+ return os.path.join(self.executable_dir, self.binary_name)
+
+ 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.items():
+ 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 = ReadPlistFromString(
+ subprocess.check_output([
+ 'xcrun', 'security', 'cms', '-D', '-u', 'certUsageAnyCA', '-i',
+ provisioning_profile_path
+ ]))
+
+ @property
+ def path(self):
+ return self._path
+
+ @property
+ def team_identifier(self):
+ return self._data.get('TeamIdentifier', [''])[0]
+
+ @property
+ def name(self):
+ return self._data.get('Name', '')
+
+ @property
+ def application_identifier_pattern(self):
+ return self._data.get('Entitlements', {}).get('application-identifier', '')
+
+ @property
+ def application_identifier_prefix(self):
+ return self._data.get('ApplicationIdentifierPrefix', [''])[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.application_identifier_prefix, 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, basestring_compat):
+ for key, substitution in substitutions.items():
+ data = data.replace('$(%s)' % (key,), substitution)
+ return data
+
+ if isinstance(data, dict):
+ for key, value in data.items():
+ 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.items():
+ if key not in self._data:
+ self._data[key] = value
+
+ def WriteTo(self, target_path):
+ with open(target_path, 'wb') as fp:
+ if sys.version_info.major == 2:
+ plistlib.writePlist(self._data, fp)
+ else:
+ plistlib.dump(self._data, fp)
+
+
+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,
+ universal_newlines=True)
+ _, 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.isfile(framework_path):
+ shutil.copy(framework_path, installed_framework_path)
+ elif os.path.isdir(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,flags'])
+
+
+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.application_identifier_prefix + '.'
+ else:
+ app_identifier_prefix = '*.'
+ entitlements.ExpandVariables({
+ 'CFBundleIdentifier': bundle_identifier,
+ 'AppIdentifierPrefix': app_identifier_prefix,
+ })
+ return entitlements
+
+
+def GenerateBundleInfoPlist(bundle, plist_compiler, partial_plist):
+ """Generates the bundle Info.plist for a list of partial .plist files.
+
+ Args:
+ bundle: a Bundle instance
+ 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 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',
+ bundle.info_plist_path,
+ ] + 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 = '-'
+
+ bundle = Bundle(args.path, args.platform)
+
+ if args.partial_info_plist:
+ GenerateBundleInfoPlist(bundle, args.plist_compiler_path,
+ args.partial_info_plist)
+
+ # The bundle Info.plist may have been updated by GenerateBundleInfoPlist()
+ # above. Load the bundle information from Info.plist after the modification
+ # have been written to disk.
+ bundle.Load()
+
+ # 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.
+ if os.path.exists(bundle.signature_dir):
+ shutil.rmtree(bundle.signature_dir)
+
+ # Install system frameworks if requested.
+ for framework_path in args.frameworks:
+ InstallSystemFramework(framework_path, args.path, args)
+
+ # Copy main binary into bundle.
+ if not os.path.isdir(bundle.executable_dir):
+ os.makedirs(bundle.executable_dir)
+ shutil.copy(args.binary, bundle.binary_path)
+
+ if bundle.kind == 'mac_framework':
+ # Create Versions/Current -> Versions/A symlink
+ CreateSymlink('A', os.path.join(bundle.path, 'Versions/Current'))
+
+ # Create $binary_name -> Versions/Current/$binary_name symlink
+ CreateSymlink(os.path.join('Versions/Current', bundle.binary_name),
+ os.path.join(bundle.path, bundle.binary_name))
+
+ # Create optional symlinks.
+ for name in ('Headers', 'Resources', 'Modules'):
+ target = os.path.join(bundle.path, 'Versions/A', name)
+ if os.path.exists(target):
+ CreateSymlink(os.path.join('Versions/Current', name),
+ os.path.join(bundle.path, name))
+ else:
+ obsolete_path = os.path.join(bundle.path, name)
+ if os.path.exists(obsolete_path):
+ os.unlink(obsolete_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)
+
+
+class FindProvisioningProfileAction(Action):
+ """Class implementing the find-codesign-identity action."""
+
+ name = 'find-provisioning-profile'
+ help = 'find provisioning profile for use by Xcode project generator'
+
+ @staticmethod
+ def _Register(parser):
+ parser.add_argument('--bundle-id',
+ '-b',
+ required=True,
+ help='bundle identifier')
+
+ @staticmethod
+ def _Execute(args):
+ provisioning_profile_info = {}
+ provisioning_profile = FindProvisioningProfile(args.bundle_id, False)
+ for key in ('team_identifier', 'name'):
+ if provisioning_profile:
+ provisioning_profile_info[key] = getattr(provisioning_profile, key)
+ else:
+ provisioning_profile_info[key] = ''
+ print(json.dumps(provisioning_profile_info))
+
+
+def Main():
+ # Cache this codec so that plistlib can find it. See
+ # https://crbug.com/999461#c12 for more details.
+ codecs.lookup('utf-8')
+
+ parser = argparse.ArgumentParser('codesign iOS bundles')
+ subparsers = parser.add_subparsers()
+
+ actions = [
+ CodeSignBundleAction,
+ CodeSignFileAction,
+ GenerateEntitlementsAction,
+ FindProvisioningProfileAction,
+ ]
+
+ for action in actions:
+ action.Register(subparsers)
+
+ args = parser.parse_args()
+ args.func(args)
+
+
+if __name__ == '__main__':
+ sys.exit(Main())