tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

codesign.py (24589B)


      1 # Copyright 2016 The Chromium Authors
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 
      6 import argparse
      7 import codecs
      8 import datetime
      9 import fnmatch
     10 import glob
     11 import json
     12 import os
     13 import plistlib
     14 import shutil
     15 import subprocess
     16 import stat
     17 import sys
     18 import tempfile
     19 
     20 # Keys that should not be copied from mobileprovision
     21 BANNED_KEYS = [
     22    "com.apple.developer.cs.allow-jit",
     23    "com.apple.developer.memory.transfer-send",
     24    "com.apple.developer.web-browser",
     25    "com.apple.developer.web-browser-engine.host",
     26    "com.apple.developer.web-browser-engine.networking",
     27    "com.apple.developer.web-browser-engine.rendering",
     28    "com.apple.developer.web-browser-engine.webcontent",
     29 ]
     30 
     31 if sys.version_info.major < 3:
     32  basestring_compat = basestring
     33 else:
     34  basestring_compat = str
     35 
     36 
     37 def GetProvisioningProfilesDir():
     38  """Returns the location of the installed mobile provisioning profiles.
     39 
     40  Returns:
     41    The path to the directory containing the installed mobile provisioning
     42    profiles as a string.
     43  """
     44  return os.path.join(
     45      os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles')
     46 
     47 
     48 def ReadPlistFromString(plist_bytes):
     49  """Parse property list from given |plist_bytes|.
     50 
     51    Args:
     52      plist_bytes: contents of property list to load. Must be bytes in python 3.
     53 
     54    Returns:
     55      The contents of property list as a python object.
     56    """
     57  if sys.version_info.major == 2:
     58    return plistlib.readPlistFromString(plist_bytes)
     59  else:
     60    return plistlib.loads(plist_bytes)
     61 
     62 
     63 def LoadPlistFile(plist_path):
     64  """Loads property list file at |plist_path|.
     65 
     66  Args:
     67    plist_path: path to the property list file to load.
     68 
     69  Returns:
     70    The content of the property list file as a python object.
     71  """
     72  if sys.version_info.major == 2:
     73    return plistlib.readPlistFromString(
     74        subprocess.check_output(
     75            ['xcrun', 'plutil', '-convert', 'xml1', '-o', '-', plist_path]))
     76  else:
     77    with open(plist_path, 'rb') as fp:
     78      return plistlib.load(fp)
     79 
     80 
     81 def CreateSymlink(value, location):
     82  """Creates symlink with value at location if the target exists."""
     83  target = os.path.join(os.path.dirname(location), value)
     84  if os.path.exists(location):
     85    os.unlink(location)
     86  os.symlink(value, location)
     87 
     88 
     89 class Bundle(object):
     90  """Wraps a bundle."""
     91 
     92  def __init__(self, bundle_path, platform):
     93    """Initializes the Bundle object with data from bundle Info.plist file."""
     94    self._path = bundle_path
     95    self._kind = Bundle.Kind(platform, os.path.splitext(bundle_path)[-1])
     96    self._data = None
     97 
     98  def Load(self):
     99    self._data = LoadPlistFile(self.info_plist_path)
    100 
    101  @staticmethod
    102  def Kind(platform, extension):
    103    if platform in ('iphoneos', 'iphonesimulator'):
    104      return 'ios'
    105    if platform == 'macosx':
    106      if extension == '.framework':
    107        return 'mac_framework'
    108      return 'mac'
    109    if platform in ('watchos', 'watchsimulator'):
    110      return 'watchos'
    111    if platform in ('appletvos', 'appletvsimulator'):
    112      return 'tvos'
    113    raise ValueError('unknown bundle type %s for %s' % (extension, platform))
    114 
    115  @property
    116  def kind(self):
    117    return self._kind
    118 
    119  @property
    120  def path(self):
    121    return self._path
    122 
    123  @property
    124  def contents_dir(self):
    125    if self._kind == 'mac':
    126      return os.path.join(self.path, 'Contents')
    127    if self._kind == 'mac_framework':
    128      return os.path.join(self.path, 'Versions/A')
    129    return self.path
    130 
    131  @property
    132  def executable_dir(self):
    133    if self._kind == 'mac':
    134      return os.path.join(self.contents_dir, 'MacOS')
    135    return self.contents_dir
    136 
    137  @property
    138  def resources_dir(self):
    139    if self._kind == 'mac' or self._kind == 'mac_framework':
    140      return os.path.join(self.contents_dir, 'Resources')
    141    return self.path
    142 
    143  @property
    144  def info_plist_path(self):
    145    if self._kind == 'mac_framework':
    146      return os.path.join(self.resources_dir, 'Info.plist')
    147    return os.path.join(self.contents_dir, 'Info.plist')
    148 
    149  @property
    150  def signature_dir(self):
    151    return os.path.join(self.contents_dir, '_CodeSignature')
    152 
    153  @property
    154  def identifier(self):
    155    return self._data['CFBundleIdentifier']
    156 
    157  @property
    158  def binary_name(self):
    159    return self._data['CFBundleExecutable']
    160 
    161  @property
    162  def binary_path(self):
    163    return os.path.join(self.executable_dir, self.binary_name)
    164 
    165  def Validate(self, expected_mappings):
    166    """Checks that keys in the bundle have the expected value.
    167 
    168    Args:
    169      expected_mappings: a dictionary of string to object, each mapping will
    170      be looked up in the bundle data to check it has the same value (missing
    171      values will be ignored)
    172 
    173    Returns:
    174      A dictionary of the key with a different value between expected_mappings
    175      and the content of the bundle (i.e. errors) so that caller can format the
    176      error message. The dictionary will be empty if there are no errors.
    177    """
    178    errors = {}
    179    for key, expected_value in expected_mappings.items():
    180      if key in self._data:
    181        value = self._data[key]
    182        if value != expected_value:
    183          errors[key] = (value, expected_value)
    184    return errors
    185 
    186 
    187 class ProvisioningProfile(object):
    188  """Wraps a mobile provisioning profile file."""
    189 
    190  def __init__(self, provisioning_profile_path):
    191    """Initializes the ProvisioningProfile with data from profile file."""
    192    self._path = provisioning_profile_path
    193    self._data = ReadPlistFromString(
    194        subprocess.check_output([
    195            'xcrun', 'security', 'cms', '-D', '-u', 'certUsageAnyCA', '-i',
    196            provisioning_profile_path
    197        ]))
    198 
    199  @property
    200  def path(self):
    201    return self._path
    202 
    203  @property
    204  def team_identifier(self):
    205    return self._data.get('TeamIdentifier', [''])[0]
    206 
    207  @property
    208  def name(self):
    209    return self._data.get('Name', '')
    210 
    211  @property
    212  def application_identifier_pattern(self):
    213    return self._data.get('Entitlements', {}).get('application-identifier', '')
    214 
    215  @property
    216  def application_identifier_prefix(self):
    217    return self._data.get('ApplicationIdentifierPrefix', [''])[0]
    218 
    219  @property
    220  def entitlements(self):
    221    return self._data.get('Entitlements', {})
    222 
    223  @property
    224  def expiration_date(self):
    225    return self._data.get('ExpirationDate', datetime.datetime.now())
    226 
    227  def ValidToSignBundle(self, bundle_identifier):
    228    """Checks whether the provisioning profile can sign bundle_identifier.
    229 
    230    Args:
    231      bundle_identifier: the identifier of the bundle that needs to be signed.
    232 
    233    Returns:
    234      True if the mobile provisioning profile can be used to sign a bundle
    235      with the corresponding bundle_identifier, False otherwise.
    236    """
    237    return fnmatch.fnmatch(
    238        '%s.%s' % (self.application_identifier_prefix, bundle_identifier),
    239        self.application_identifier_pattern)
    240 
    241  def Install(self, installation_path):
    242    """Copies mobile provisioning profile info to |installation_path|."""
    243    shutil.copy2(self.path, installation_path)
    244    st = os.stat(installation_path)
    245    os.chmod(installation_path, st.st_mode | stat.S_IWUSR)
    246 
    247 
    248 class Entitlements(object):
    249  """Wraps an Entitlement plist file."""
    250 
    251  def __init__(self, entitlements_path):
    252    """Initializes Entitlements object from entitlement file."""
    253    self._path = entitlements_path
    254    self._data = LoadPlistFile(self._path)
    255 
    256  @property
    257  def path(self):
    258    return self._path
    259 
    260  def ExpandVariables(self, substitutions):
    261    self._data = self._ExpandVariables(self._data, substitutions)
    262 
    263  def _ExpandVariables(self, data, substitutions):
    264    if isinstance(data, basestring_compat):
    265      for key, substitution in substitutions.items():
    266        data = data.replace('$(%s)' % (key,), substitution)
    267      return data
    268 
    269    if isinstance(data, dict):
    270      for key, value in data.items():
    271        data[key] = self._ExpandVariables(value, substitutions)
    272      return data
    273 
    274    if isinstance(data, list):
    275      for i, value in enumerate(data):
    276        data[i] = self._ExpandVariables(value, substitutions)
    277 
    278    return data
    279 
    280  def LoadDefaults(self, defaults):
    281    for key, value in defaults.items():
    282      if key not in self._data and key not in BANNED_KEYS:
    283        self._data[key] = value
    284 
    285  def WriteTo(self, target_path):
    286    with open(target_path, 'wb') as fp:
    287      if sys.version_info.major == 2:
    288        plistlib.writePlist(self._data, fp)
    289      else:
    290        plistlib.dump(self._data, fp)
    291 
    292 
    293 def FindProvisioningProfile(provisioning_profile_paths, bundle_identifier,
    294                            required):
    295  """Finds mobile provisioning profile to use to sign bundle.
    296 
    297  Args:
    298    bundle_identifier: the identifier of the bundle to sign.
    299 
    300  Returns:
    301    The ProvisioningProfile object that can be used to sign the Bundle
    302    object or None if no matching provisioning profile was found.
    303  """
    304  if not provisioning_profile_paths:
    305    provisioning_profile_paths = glob.glob(
    306        os.path.join(GetProvisioningProfilesDir(), '*.mobileprovision'))
    307 
    308  # Iterate over all installed mobile provisioning profiles and filter those
    309  # that can be used to sign the bundle, ignoring expired ones.
    310  now = datetime.datetime.now()
    311  valid_provisioning_profiles = []
    312  one_hour = datetime.timedelta(0, 3600)
    313  for provisioning_profile_path in provisioning_profile_paths:
    314    provisioning_profile = ProvisioningProfile(provisioning_profile_path)
    315    if provisioning_profile.expiration_date - now < one_hour:
    316      sys.stderr.write(
    317          'Warning: ignoring expired provisioning profile: %s.\n' %
    318          provisioning_profile_path)
    319      continue
    320    if provisioning_profile.ValidToSignBundle(bundle_identifier):
    321      valid_provisioning_profiles.append(provisioning_profile)
    322 
    323  if not valid_provisioning_profiles:
    324    if required:
    325      sys.stderr.write(
    326          'Error: no mobile provisioning profile found for "%s" in %s.\n' %
    327          (bundle_identifier, provisioning_profile_paths))
    328      sys.exit(1)
    329    return None
    330 
    331  # Select the most specific mobile provisioning profile, i.e. the one with
    332  # the longest application identifier pattern (prefer the one with the latest
    333  # expiration date as a secondary criteria).
    334  selected_provisioning_profile = max(
    335      valid_provisioning_profiles,
    336      key=lambda p: (len(p.application_identifier_pattern), p.expiration_date))
    337 
    338  one_week = datetime.timedelta(7)
    339  if selected_provisioning_profile.expiration_date - now < 2 * one_week:
    340    sys.stderr.write(
    341        'Warning: selected provisioning profile will expire soon: %s' %
    342        selected_provisioning_profile.path)
    343  return selected_provisioning_profile
    344 
    345 
    346 def CodeSignBundle(bundle_path, identity, extra_args):
    347  process = subprocess.Popen(
    348      ['xcrun', 'codesign', '--force', '--sign', identity, '--timestamp=none'] +
    349      list(extra_args) + [bundle_path],
    350      stderr=subprocess.PIPE,
    351      universal_newlines=True)
    352  _, stderr = process.communicate()
    353  if process.returncode:
    354    sys.stderr.write(stderr)
    355    sys.exit(process.returncode)
    356  for line in stderr.splitlines():
    357    if line.endswith(': replacing existing signature'):
    358      # Ignore warning about replacing existing signature as this should only
    359      # happen when re-signing system frameworks (and then it is expected).
    360      continue
    361    sys.stderr.write(line)
    362    sys.stderr.write('\n')
    363 
    364 
    365 def InstallSystemFramework(framework_path, bundle_path, args):
    366  """Install framework from |framework_path| to |bundle| and code-re-sign it."""
    367  installed_framework_path = os.path.join(
    368      bundle_path, 'Frameworks', os.path.basename(framework_path))
    369 
    370  if os.path.isfile(framework_path):
    371    shutil.copy(framework_path, installed_framework_path)
    372  elif os.path.isdir(framework_path):
    373    if os.path.exists(installed_framework_path):
    374      shutil.rmtree(installed_framework_path)
    375    shutil.copytree(framework_path, installed_framework_path)
    376 
    377  CodeSignBundle(installed_framework_path, args.identity,
    378      ['--deep', '--preserve-metadata=identifier,entitlements,flags'])
    379 
    380 
    381 def GenerateEntitlements(path, provisioning_profile, bundle_identifier):
    382  """Generates an entitlements file.
    383 
    384  Args:
    385    path: path to the entitlements template file
    386    provisioning_profile: ProvisioningProfile object to use, may be None
    387    bundle_identifier: identifier of the bundle to sign.
    388  """
    389  entitlements = Entitlements(path)
    390  if provisioning_profile:
    391    entitlements.LoadDefaults(provisioning_profile.entitlements)
    392    app_identifier_prefix = \
    393      provisioning_profile.application_identifier_prefix + '.'
    394  else:
    395    app_identifier_prefix = '*.'
    396  entitlements.ExpandVariables({
    397      'CFBundleIdentifier': bundle_identifier,
    398      'AppIdentifierPrefix': app_identifier_prefix,
    399  })
    400  return entitlements
    401 
    402 
    403 def GenerateBundleInfoPlist(bundle, plist_compiler, partial_plist):
    404  """Generates the bundle Info.plist for a list of partial .plist files.
    405 
    406  Args:
    407    bundle: a Bundle instance
    408    plist_compiler: string, path to the Info.plist compiler
    409    partial_plist: list of path to partial .plist files to merge
    410  """
    411 
    412  # Filter empty partial .plist files (this happens if an application
    413  # does not compile any asset catalog, in which case the partial .plist
    414  # file from the asset catalog compilation step is just a stamp file).
    415  filtered_partial_plist = []
    416  for plist in partial_plist:
    417    plist_size = os.stat(plist).st_size
    418    if plist_size:
    419      filtered_partial_plist.append(plist)
    420 
    421  # Invoke the plist_compiler script. It needs to be a python script.
    422  subprocess.check_call([
    423      'python3',
    424      plist_compiler,
    425      'merge',
    426      '-f',
    427      'binary1',
    428      '-o',
    429      bundle.info_plist_path,
    430  ] + filtered_partial_plist)
    431 
    432 
    433 class Action(object):
    434  """Class implementing one action supported by the script."""
    435 
    436  @classmethod
    437  def Register(cls, subparsers):
    438    parser = subparsers.add_parser(cls.name, help=cls.help)
    439    parser.set_defaults(func=cls._Execute)
    440    cls._Register(parser)
    441 
    442 
    443 class CodeSignBundleAction(Action):
    444  """Class implementing the code-sign-bundle action."""
    445 
    446  name = 'code-sign-bundle'
    447  help = 'perform code signature for a bundle'
    448 
    449  @staticmethod
    450  def _Register(parser):
    451    parser.add_argument(
    452        '--entitlements', '-e', dest='entitlements_path',
    453        help='path to the entitlements file to use')
    454    parser.add_argument(
    455        'path', help='path to the iOS bundle to codesign')
    456    parser.add_argument(
    457        '--identity', '-i', required=True,
    458        help='identity to use to codesign')
    459    parser.add_argument(
    460        '--binary', '-b', required=True,
    461        help='path to the iOS bundle binary')
    462    parser.add_argument(
    463        '--framework', '-F', action='append', default=[], dest='frameworks',
    464        help='install and resign system framework')
    465    parser.add_argument(
    466        '--disable-code-signature', action='store_true', dest='no_signature',
    467        help='disable code signature')
    468    parser.add_argument(
    469        '--disable-embedded-mobileprovision', action='store_false',
    470        default=True, dest='embedded_mobileprovision',
    471        help='disable finding and embedding mobileprovision')
    472    parser.add_argument(
    473        '--platform', '-t', required=True,
    474        help='platform the signed bundle is targeting')
    475    parser.add_argument(
    476        '--partial-info-plist', '-p', action='append', default=[],
    477        help='path to partial Info.plist to merge to create bundle Info.plist')
    478    parser.add_argument(
    479        '--plist-compiler-path', '-P', action='store',
    480        help='path to the plist compiler script (for --partial-info-plist)')
    481    parser.add_argument(
    482        '--mobileprovision',
    483        '-m',
    484        action='append',
    485        default=[],
    486        dest='mobileprovision_files',
    487        help='list of mobileprovision files to use. If empty, uses the files ' +
    488        'in $HOME/Library/MobileDevice/Provisioning Profiles')
    489    parser.set_defaults(no_signature=False)
    490 
    491  @staticmethod
    492  def _Execute(args):
    493    if not args.identity:
    494      args.identity = '-'
    495 
    496    bundle = Bundle(args.path, args.platform)
    497 
    498    if args.partial_info_plist:
    499      GenerateBundleInfoPlist(bundle, args.plist_compiler_path,
    500                              args.partial_info_plist)
    501 
    502    # The bundle Info.plist may have been updated by GenerateBundleInfoPlist()
    503    # above. Load the bundle information from Info.plist after the modification
    504    # have been written to disk.
    505    bundle.Load()
    506 
    507    # According to Apple documentation, the application binary must be the same
    508    # as the bundle name without the .app suffix. See crbug.com/740476 for more
    509    # information on what problem this can cause.
    510    #
    511    # To prevent this class of error, fail with an error if the binary name is
    512    # incorrect in the Info.plist as it is not possible to update the value in
    513    # Info.plist at this point (the file has been copied by a different target
    514    # and ninja would consider the build dirty if it was updated).
    515    #
    516    # Also checks that the name of the bundle is correct too (does not cause the
    517    # build to be considered dirty, but still terminate the script in case of an
    518    # incorrect bundle name).
    519    #
    520    # Apple documentation is available at:
    521    # https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html
    522    bundle_name = os.path.splitext(os.path.basename(bundle.path))[0]
    523    errors = bundle.Validate({
    524        'CFBundleName': bundle_name,
    525        'CFBundleExecutable': bundle_name,
    526    })
    527    if errors:
    528      for key in sorted(errors):
    529        value, expected_value = errors[key]
    530        sys.stderr.write('%s: error: %s value incorrect: %s != %s\n' % (
    531            bundle.path, key, value, expected_value))
    532      sys.stderr.flush()
    533      sys.exit(1)
    534 
    535    # Delete existing embedded mobile provisioning.
    536    embedded_provisioning_profile = os.path.join(
    537        bundle.path, 'embedded.mobileprovision')
    538    if os.path.isfile(embedded_provisioning_profile):
    539      os.unlink(embedded_provisioning_profile)
    540 
    541    # Delete existing code signature.
    542    if os.path.exists(bundle.signature_dir):
    543      shutil.rmtree(bundle.signature_dir)
    544 
    545    # Install system frameworks if requested.
    546    for framework_path in args.frameworks:
    547      InstallSystemFramework(framework_path, args.path, args)
    548 
    549    # Copy main binary into bundle.
    550    if not os.path.isdir(bundle.executable_dir):
    551      os.makedirs(bundle.executable_dir)
    552    shutil.copy(args.binary, bundle.binary_path)
    553 
    554    if bundle.kind == 'mac_framework':
    555      # Create Versions/Current -> Versions/A symlink
    556      CreateSymlink('A', os.path.join(bundle.path, 'Versions/Current'))
    557 
    558      # Create $binary_name -> Versions/Current/$binary_name symlink
    559      CreateSymlink(os.path.join('Versions/Current', bundle.binary_name),
    560                    os.path.join(bundle.path, bundle.binary_name))
    561 
    562      # Create optional symlinks.
    563      for name in ('Headers', 'Resources', 'Modules'):
    564        target = os.path.join(bundle.path, 'Versions/A', name)
    565        if os.path.exists(target):
    566          CreateSymlink(os.path.join('Versions/Current', name),
    567                        os.path.join(bundle.path, name))
    568        else:
    569          obsolete_path = os.path.join(bundle.path, name)
    570          if os.path.exists(obsolete_path):
    571            os.unlink(obsolete_path)
    572 
    573    if args.no_signature:
    574      return
    575 
    576    codesign_extra_args = []
    577 
    578    if args.embedded_mobileprovision:
    579      # Find mobile provisioning profile and embeds it into the bundle (if a
    580      # code signing identify has been provided, fails if no valid mobile
    581      # provisioning is found).
    582      provisioning_profile_required = args.identity != '-'
    583      provisioning_profile = FindProvisioningProfile(
    584          args.mobileprovision_files, bundle.identifier,
    585          provisioning_profile_required)
    586      if provisioning_profile and not args.platform.endswith('simulator'):
    587        provisioning_profile.Install(embedded_provisioning_profile)
    588 
    589        if args.entitlements_path is not None:
    590          temporary_entitlements_file = \
    591              tempfile.NamedTemporaryFile(suffix='.xcent')
    592          codesign_extra_args.extend(
    593              ['--entitlements', temporary_entitlements_file.name])
    594 
    595          entitlements = GenerateEntitlements(
    596              args.entitlements_path, provisioning_profile, bundle.identifier)
    597          entitlements.WriteTo(temporary_entitlements_file.name)
    598 
    599    CodeSignBundle(bundle.path, args.identity, codesign_extra_args)
    600 
    601 
    602 class CodeSignFileAction(Action):
    603  """Class implementing code signature for a single file."""
    604 
    605  name = 'code-sign-file'
    606  help = 'code-sign a single file'
    607 
    608  @staticmethod
    609  def _Register(parser):
    610    parser.add_argument(
    611        'path', help='path to the file to codesign')
    612    parser.add_argument(
    613        '--identity', '-i', required=True,
    614        help='identity to use to codesign')
    615    parser.add_argument(
    616        '--output', '-o',
    617        help='if specified copy the file to that location before signing it')
    618    parser.set_defaults(sign=True)
    619 
    620  @staticmethod
    621  def _Execute(args):
    622    if not args.identity:
    623      args.identity = '-'
    624 
    625    install_path = args.path
    626    if args.output:
    627 
    628      if os.path.isfile(args.output):
    629        os.unlink(args.output)
    630      elif os.path.isdir(args.output):
    631        shutil.rmtree(args.output)
    632 
    633      if os.path.isfile(args.path):
    634        shutil.copy(args.path, args.output)
    635      elif os.path.isdir(args.path):
    636        shutil.copytree(args.path, args.output)
    637 
    638      install_path = args.output
    639 
    640    CodeSignBundle(install_path, args.identity,
    641      ['--deep', '--preserve-metadata=identifier,entitlements'])
    642 
    643 
    644 class GenerateEntitlementsAction(Action):
    645  """Class implementing the generate-entitlements action."""
    646 
    647  name = 'generate-entitlements'
    648  help = 'generate entitlements file'
    649 
    650  @staticmethod
    651  def _Register(parser):
    652    parser.add_argument(
    653        '--entitlements', '-e', dest='entitlements_path',
    654        help='path to the entitlements file to use')
    655    parser.add_argument(
    656        'path', help='path to the entitlements file to generate')
    657    parser.add_argument(
    658        '--info-plist', '-p', required=True,
    659        help='path to the bundle Info.plist')
    660    parser.add_argument(
    661        '--mobileprovision',
    662        '-m',
    663        action='append',
    664        default=[],
    665        dest='mobileprovision_files',
    666        help='set of mobileprovision files to use. If empty, uses the files ' +
    667        'in $HOME/Library/MobileDevice/Provisioning Profiles')
    668 
    669  @staticmethod
    670  def _Execute(args):
    671    info_plist = LoadPlistFile(args.info_plist)
    672    bundle_identifier = info_plist['CFBundleIdentifier']
    673    provisioning_profile = FindProvisioningProfile(args.mobileprovision_files,
    674                                                   bundle_identifier, False)
    675    entitlements = GenerateEntitlements(
    676        args.entitlements_path, provisioning_profile, bundle_identifier)
    677    entitlements.WriteTo(args.path)
    678 
    679 
    680 class FindProvisioningProfileAction(Action):
    681  """Class implementing the find-codesign-identity action."""
    682 
    683  name = 'find-provisioning-profile'
    684  help = 'find provisioning profile for use by Xcode project generator'
    685 
    686  @staticmethod
    687  def _Register(parser):
    688    parser.add_argument('--bundle-id',
    689                        '-b',
    690                        required=True,
    691                        help='bundle identifier')
    692    parser.add_argument(
    693        '--mobileprovision',
    694        '-m',
    695        action='append',
    696        default=[],
    697        dest='mobileprovision_files',
    698        help='set of mobileprovision files to use. If empty, uses the files ' +
    699        'in $HOME/Library/MobileDevice/Provisioning Profiles')
    700 
    701  @staticmethod
    702  def _Execute(args):
    703    provisioning_profile_info = {}
    704    provisioning_profile = FindProvisioningProfile(args.mobileprovision_files,
    705                                                   args.bundle_id, False)
    706    for key in ('team_identifier', 'name', 'path'):
    707      if provisioning_profile:
    708        provisioning_profile_info[key] = getattr(provisioning_profile, key)
    709      else:
    710        provisioning_profile_info[key] = ''
    711    print(json.dumps(provisioning_profile_info))
    712 
    713 
    714 def Main():
    715  # Cache this codec so that plistlib can find it. See
    716  # https://crbug.com/999461#c12 for more details.
    717  codecs.lookup('utf-8')
    718 
    719  parser = argparse.ArgumentParser('codesign iOS bundles')
    720  subparsers = parser.add_subparsers()
    721 
    722  actions = [
    723      CodeSignBundleAction,
    724      CodeSignFileAction,
    725      GenerateEntitlementsAction,
    726      FindProvisioningProfileAction,
    727  ]
    728 
    729  for action in actions:
    730    action.Register(subparsers)
    731 
    732  args = parser.parse_args()
    733  args.func(args)
    734 
    735 
    736 if __name__ == '__main__':
    737  sys.exit(Main())