"Fossies" - the Fresh Open Source Software Archive

Member "Atom/resources/app/apm/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py" (7 Feb 2017, 23354 Bytes) of archive /windows/misc/atom-windows.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file.

    1 #!/usr/bin/env python
    2 # Copyright (c) 2012 Google Inc. All rights reserved.
    3 # Use of this source code is governed by a BSD-style license that can be
    4 # found in the LICENSE file.
    5 
    6 """Utility functions to perform Xcode-style build steps.
    7 
    8 These functions are executed via gyp-mac-tool when using the Makefile generator.
    9 """
   10 
   11 import fcntl
   12 import fnmatch
   13 import glob
   14 import json
   15 import os
   16 import plistlib
   17 import re
   18 import shutil
   19 import string
   20 import subprocess
   21 import sys
   22 import tempfile
   23 
   24 
   25 def main(args):
   26   executor = MacTool()
   27   exit_code = executor.Dispatch(args)
   28   if exit_code is not None:
   29     sys.exit(exit_code)
   30 
   31 
   32 class MacTool(object):
   33   """This class performs all the Mac tooling steps. The methods can either be
   34   executed directly, or dispatched from an argument list."""
   35 
   36   def Dispatch(self, args):
   37     """Dispatches a string command to a method."""
   38     if len(args) < 1:
   39       raise Exception("Not enough arguments")
   40 
   41     method = "Exec%s" % self._CommandifyName(args[0])
   42     return getattr(self, method)(*args[1:])
   43 
   44   def _CommandifyName(self, name_string):
   45     """Transforms a tool name like copy-info-plist to CopyInfoPlist"""
   46     return name_string.title().replace('-', '')
   47 
   48   def ExecCopyBundleResource(self, source, dest, convert_to_binary):
   49     """Copies a resource file to the bundle/Resources directory, performing any
   50     necessary compilation on each resource."""
   51     extension = os.path.splitext(source)[1].lower()
   52     if os.path.isdir(source):
   53       # Copy tree.
   54       # TODO(thakis): This copies file attributes like mtime, while the
   55       # single-file branch below doesn't. This should probably be changed to
   56       # be consistent with the single-file branch.
   57       if os.path.exists(dest):
   58         shutil.rmtree(dest)
   59       shutil.copytree(source, dest)
   60     elif extension == '.xib':
   61       return self._CopyXIBFile(source, dest)
   62     elif extension == '.storyboard':
   63       return self._CopyXIBFile(source, dest)
   64     elif extension == '.strings':
   65       self._CopyStringsFile(source, dest, convert_to_binary)
   66     else:
   67       shutil.copy(source, dest)
   68 
   69   def _CopyXIBFile(self, source, dest):
   70     """Compiles a XIB file with ibtool into a binary plist in the bundle."""
   71 
   72     # ibtool sometimes crashes with relative paths. See crbug.com/314728.
   73     base = os.path.dirname(os.path.realpath(__file__))
   74     if os.path.relpath(source):
   75       source = os.path.join(base, source)
   76     if os.path.relpath(dest):
   77       dest = os.path.join(base, dest)
   78 
   79     args = ['xcrun', 'ibtool', '--errors', '--warnings', '--notices',
   80         '--output-format', 'human-readable-text', '--compile', dest, source]
   81     ibtool_section_re = re.compile(r'/\*.*\*/')
   82     ibtool_re = re.compile(r'.*note:.*is clipping its content')
   83     ibtoolout = subprocess.Popen(args, stdout=subprocess.PIPE)
   84     current_section_header = None
   85     for line in ibtoolout.stdout:
   86       if ibtool_section_re.match(line):
   87         current_section_header = line
   88       elif not ibtool_re.match(line):
   89         if current_section_header:
   90           sys.stdout.write(current_section_header)
   91           current_section_header = None
   92         sys.stdout.write(line)
   93     return ibtoolout.returncode
   94 
   95   def _ConvertToBinary(self, dest):
   96     subprocess.check_call([
   97         'xcrun', 'plutil', '-convert', 'binary1', '-o', dest, dest])
   98 
   99   def _CopyStringsFile(self, source, dest, convert_to_binary):
  100     """Copies a .strings file using iconv to reconvert the input into UTF-16."""
  101     input_code = self._DetectInputEncoding(source) or "UTF-8"
  102 
  103     # Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call
  104     # CFPropertyListCreateFromXMLData() behind the scenes; at least it prints
  105     #     CFPropertyListCreateFromXMLData(): Old-style plist parser: missing
  106     #     semicolon in dictionary.
  107     # on invalid files. Do the same kind of validation.
  108     import CoreFoundation
  109     s = open(source, 'rb').read()
  110     d = CoreFoundation.CFDataCreate(None, s, len(s))
  111     _, error = CoreFoundation.CFPropertyListCreateFromXMLData(None, d, 0, None)
  112     if error:
  113       return
  114 
  115     fp = open(dest, 'wb')
  116     fp.write(s.decode(input_code).encode('UTF-16'))
  117     fp.close()
  118 
  119     if convert_to_binary == 'True':
  120       self._ConvertToBinary(dest)
  121 
  122   def _DetectInputEncoding(self, file_name):
  123     """Reads the first few bytes from file_name and tries to guess the text
  124     encoding. Returns None as a guess if it can't detect it."""
  125     fp = open(file_name, 'rb')
  126     try:
  127       header = fp.read(3)
  128     except e:
  129       fp.close()
  130       return None
  131     fp.close()
  132     if header.startswith("\xFE\xFF"):
  133       return "UTF-16"
  134     elif header.startswith("\xFF\xFE"):
  135       return "UTF-16"
  136     elif header.startswith("\xEF\xBB\xBF"):
  137       return "UTF-8"
  138     else:
  139       return None
  140 
  141   def ExecCopyInfoPlist(self, source, dest, convert_to_binary, *keys):
  142     """Copies the |source| Info.plist to the destination directory |dest|."""
  143     # Read the source Info.plist into memory.
  144     fd = open(source, 'r')
  145     lines = fd.read()
  146     fd.close()
  147 
  148     # Insert synthesized key/value pairs (e.g. BuildMachineOSBuild).
  149     plist = plistlib.readPlistFromString(lines)
  150     if keys:
  151       plist = dict(plist.items() + json.loads(keys[0]).items())
  152     lines = plistlib.writePlistToString(plist)
  153 
  154     # Go through all the environment variables and replace them as variables in
  155     # the file.
  156     IDENT_RE = re.compile(r'[/\s]')
  157     for key in os.environ:
  158       if key.startswith('_'):
  159         continue
  160       evar = '${%s}' % key
  161       evalue = os.environ[key]
  162       lines = string.replace(lines, evar, evalue)
  163 
  164       # Xcode supports various suffices on environment variables, which are
  165       # all undocumented. :rfc1034identifier is used in the standard project
  166       # template these days, and :identifier was used earlier. They are used to
  167       # convert non-url characters into things that look like valid urls --
  168       # except that the replacement character for :identifier, '_' isn't valid
  169       # in a URL either -- oops, hence :rfc1034identifier was born.
  170       evar = '${%s:identifier}' % key
  171       evalue = IDENT_RE.sub('_', os.environ[key])
  172       lines = string.replace(lines, evar, evalue)
  173 
  174       evar = '${%s:rfc1034identifier}' % key
  175       evalue = IDENT_RE.sub('-', os.environ[key])
  176       lines = string.replace(lines, evar, evalue)
  177 
  178     # Remove any keys with values that haven't been replaced.
  179     lines = lines.split('\n')
  180     for i in range(len(lines)):
  181       if lines[i].strip().startswith("<string>${"):
  182         lines[i] = None
  183         lines[i - 1] = None
  184     lines = '\n'.join(filter(lambda x: x is not None, lines))
  185 
  186     # Write out the file with variables replaced.
  187     fd = open(dest, 'w')
  188     fd.write(lines)
  189     fd.close()
  190 
  191     # Now write out PkgInfo file now that the Info.plist file has been
  192     # "compiled".
  193     self._WritePkgInfo(dest)
  194 
  195     if convert_to_binary == 'True':
  196       self._ConvertToBinary(dest)
  197 
  198   def _WritePkgInfo(self, info_plist):
  199     """This writes the PkgInfo file from the data stored in Info.plist."""
  200     plist = plistlib.readPlist(info_plist)
  201     if not plist:
  202       return
  203 
  204     # Only create PkgInfo for executable types.
  205     package_type = plist['CFBundlePackageType']
  206     if package_type != 'APPL':
  207       return
  208 
  209     # The format of PkgInfo is eight characters, representing the bundle type
  210     # and bundle signature, each four characters. If that is missing, four
  211     # '?' characters are used instead.
  212     signature_code = plist.get('CFBundleSignature', '????')
  213     if len(signature_code) != 4:  # Wrong length resets everything, too.
  214       signature_code = '?' * 4
  215 
  216     dest = os.path.join(os.path.dirname(info_plist), 'PkgInfo')
  217     fp = open(dest, 'w')
  218     fp.write('%s%s' % (package_type, signature_code))
  219     fp.close()
  220 
  221   def ExecFlock(self, lockfile, *cmd_list):
  222     """Emulates the most basic behavior of Linux's flock(1)."""
  223     # Rely on exception handling to report errors.
  224     fd = os.open(lockfile, os.O_RDONLY|os.O_NOCTTY|os.O_CREAT, 0o666)
  225     fcntl.flock(fd, fcntl.LOCK_EX)
  226     return subprocess.call(cmd_list)
  227 
  228   def ExecFilterLibtool(self, *cmd_list):
  229     """Calls libtool and filters out '/path/to/libtool: file: foo.o has no
  230     symbols'."""
  231     libtool_re = re.compile(r'^.*libtool: file: .* has no symbols$')
  232     libtool_re5 = re.compile(
  233         r'^.*libtool: warning for library: ' +
  234         r'.* the table of contents is empty ' +
  235         r'\(no object file members in the library define global symbols\)$')
  236     env = os.environ.copy()
  237     # Ref:
  238     # http://www.opensource.apple.com/source/cctools/cctools-809/misc/libtool.c
  239     # The problem with this flag is that it resets the file mtime on the file to
  240     # epoch=0, e.g. 1970-1-1 or 1969-12-31 depending on timezone.
  241     env['ZERO_AR_DATE'] = '1'
  242     libtoolout = subprocess.Popen(cmd_list, stderr=subprocess.PIPE, env=env)
  243     _, err = libtoolout.communicate()
  244     for line in err.splitlines():
  245       if not libtool_re.match(line) and not libtool_re5.match(line):
  246         print >>sys.stderr, line
  247     # Unconditionally touch the output .a file on the command line if present
  248     # and the command succeeded. A bit hacky.
  249     if not libtoolout.returncode:
  250       for i in range(len(cmd_list) - 1):
  251         if cmd_list[i] == "-o" and cmd_list[i+1].endswith('.a'):
  252           os.utime(cmd_list[i+1], None)
  253           break
  254     return libtoolout.returncode
  255 
  256   def ExecPackageFramework(self, framework, version):
  257     """Takes a path to Something.framework and the Current version of that and
  258     sets up all the symlinks."""
  259     # Find the name of the binary based on the part before the ".framework".
  260     binary = os.path.basename(framework).split('.')[0]
  261 
  262     CURRENT = 'Current'
  263     RESOURCES = 'Resources'
  264     VERSIONS = 'Versions'
  265 
  266     if not os.path.exists(os.path.join(framework, VERSIONS, version, binary)):
  267       # Binary-less frameworks don't seem to contain symlinks (see e.g.
  268       # chromium's out/Debug/org.chromium.Chromium.manifest/ bundle).
  269       return
  270 
  271     # Move into the framework directory to set the symlinks correctly.
  272     pwd = os.getcwd()
  273     os.chdir(framework)
  274 
  275     # Set up the Current version.
  276     self._Relink(version, os.path.join(VERSIONS, CURRENT))
  277 
  278     # Set up the root symlinks.
  279     self._Relink(os.path.join(VERSIONS, CURRENT, binary), binary)
  280     self._Relink(os.path.join(VERSIONS, CURRENT, RESOURCES), RESOURCES)
  281 
  282     # Back to where we were before!
  283     os.chdir(pwd)
  284 
  285   def _Relink(self, dest, link):
  286     """Creates a symlink to |dest| named |link|. If |link| already exists,
  287     it is overwritten."""
  288     if os.path.lexists(link):
  289       os.remove(link)
  290     os.symlink(dest, link)
  291 
  292   def ExecCompileXcassets(self, keys, *inputs):
  293     """Compiles multiple .xcassets files into a single .car file.
  294 
  295     This invokes 'actool' to compile all the inputs .xcassets files. The
  296     |keys| arguments is a json-encoded dictionary of extra arguments to
  297     pass to 'actool' when the asset catalogs contains an application icon
  298     or a launch image.
  299 
  300     Note that 'actool' does not create the Assets.car file if the asset
  301     catalogs does not contains imageset.
  302     """
  303     command_line = [
  304       'xcrun', 'actool', '--output-format', 'human-readable-text',
  305       '--compress-pngs', '--notices', '--warnings', '--errors',
  306     ]
  307     is_iphone_target = 'IPHONEOS_DEPLOYMENT_TARGET' in os.environ
  308     if is_iphone_target:
  309       platform = os.environ['CONFIGURATION'].split('-')[-1]
  310       if platform not in ('iphoneos', 'iphonesimulator'):
  311         platform = 'iphonesimulator'
  312       command_line.extend([
  313           '--platform', platform, '--target-device', 'iphone',
  314           '--target-device', 'ipad', '--minimum-deployment-target',
  315           os.environ['IPHONEOS_DEPLOYMENT_TARGET'], '--compile',
  316           os.path.abspath(os.environ['CONTENTS_FOLDER_PATH']),
  317       ])
  318     else:
  319       command_line.extend([
  320           '--platform', 'macosx', '--target-device', 'mac',
  321           '--minimum-deployment-target', os.environ['MACOSX_DEPLOYMENT_TARGET'],
  322           '--compile',
  323           os.path.abspath(os.environ['UNLOCALIZED_RESOURCES_FOLDER_PATH']),
  324       ])
  325     if keys:
  326       keys = json.loads(keys)
  327       for key, value in keys.iteritems():
  328         arg_name = '--' + key
  329         if isinstance(value, bool):
  330           if value:
  331             command_line.append(arg_name)
  332         elif isinstance(value, list):
  333           for v in value:
  334             command_line.append(arg_name)
  335             command_line.append(str(v))
  336         else:
  337           command_line.append(arg_name)
  338           command_line.append(str(value))
  339     # Note: actool crashes if inputs path are relative, so use os.path.abspath
  340     # to get absolute path name for inputs.
  341     command_line.extend(map(os.path.abspath, inputs))
  342     subprocess.check_call(command_line)
  343 
  344   def ExecMergeInfoPlist(self, output, *inputs):
  345     """Merge multiple .plist files into a single .plist file."""
  346     merged_plist = {}
  347     for path in inputs:
  348       plist = self._LoadPlistMaybeBinary(path)
  349       self._MergePlist(merged_plist, plist)
  350     plistlib.writePlist(merged_plist, output)
  351 
  352   def ExecCodeSignBundle(self, key, resource_rules, entitlements, provisioning):
  353     """Code sign a bundle.
  354 
  355     This function tries to code sign an iOS bundle, following the same
  356     algorithm as Xcode:
  357       1. copy ResourceRules.plist from the user or the SDK into the bundle,
  358       2. pick the provisioning profile that best match the bundle identifier,
  359          and copy it into the bundle as embedded.mobileprovision,
  360       3. copy Entitlements.plist from user or SDK next to the bundle,
  361       4. code sign the bundle.
  362     """
  363     resource_rules_path = self._InstallResourceRules(resource_rules)
  364     substitutions, overrides = self._InstallProvisioningProfile(
  365         provisioning, self._GetCFBundleIdentifier())
  366     entitlements_path = self._InstallEntitlements(
  367         entitlements, substitutions, overrides)
  368     subprocess.check_call([
  369         'codesign', '--force', '--sign', key, '--resource-rules',
  370         resource_rules_path, '--entitlements', entitlements_path,
  371         os.path.join(
  372             os.environ['TARGET_BUILD_DIR'],
  373             os.environ['FULL_PRODUCT_NAME'])])
  374 
  375   def _InstallResourceRules(self, resource_rules):
  376     """Installs ResourceRules.plist from user or SDK into the bundle.
  377 
  378     Args:
  379       resource_rules: string, optional, path to the ResourceRules.plist file
  380         to use, default to "${SDKROOT}/ResourceRules.plist"
  381 
  382     Returns:
  383       Path to the copy of ResourceRules.plist into the bundle.
  384     """
  385     source_path = resource_rules
  386     target_path = os.path.join(
  387         os.environ['BUILT_PRODUCTS_DIR'],
  388         os.environ['CONTENTS_FOLDER_PATH'],
  389         'ResourceRules.plist')
  390     if not source_path:
  391       source_path = os.path.join(
  392           os.environ['SDKROOT'], 'ResourceRules.plist')
  393     shutil.copy2(source_path, target_path)
  394     return target_path
  395 
  396   def _InstallProvisioningProfile(self, profile, bundle_identifier):
  397     """Installs embedded.mobileprovision into the bundle.
  398 
  399     Args:
  400       profile: string, optional, short name of the .mobileprovision file
  401         to use, if empty or the file is missing, the best file installed
  402         will be used
  403       bundle_identifier: string, value of CFBundleIdentifier from Info.plist
  404 
  405     Returns:
  406       A tuple containing two dictionary: variables substitutions and values
  407       to overrides when generating the entitlements file.
  408     """
  409     source_path, provisioning_data, team_id = self._FindProvisioningProfile(
  410         profile, bundle_identifier)
  411     target_path = os.path.join(
  412         os.environ['BUILT_PRODUCTS_DIR'],
  413         os.environ['CONTENTS_FOLDER_PATH'],
  414         'embedded.mobileprovision')
  415     shutil.copy2(source_path, target_path)
  416     substitutions = self._GetSubstitutions(bundle_identifier, team_id + '.')
  417     return substitutions, provisioning_data['Entitlements']
  418 
  419   def _FindProvisioningProfile(self, profile, bundle_identifier):
  420     """Finds the .mobileprovision file to use for signing the bundle.
  421 
  422     Checks all the installed provisioning profiles (or if the user specified
  423     the PROVISIONING_PROFILE variable, only consult it) and select the most
  424     specific that correspond to the bundle identifier.
  425 
  426     Args:
  427       profile: string, optional, short name of the .mobileprovision file
  428         to use, if empty or the file is missing, the best file installed
  429         will be used
  430       bundle_identifier: string, value of CFBundleIdentifier from Info.plist
  431 
  432     Returns:
  433       A tuple of the path to the selected provisioning profile, the data of
  434       the embedded plist in the provisioning profile and the team identifier
  435       to use for code signing.
  436 
  437     Raises:
  438       SystemExit: if no .mobileprovision can be used to sign the bundle.
  439     """
  440     profiles_dir = os.path.join(
  441         os.environ['HOME'], 'Library', 'MobileDevice', 'Provisioning Profiles')
  442     if not os.path.isdir(profiles_dir):
  443       print >>sys.stderr, (
  444           'cannot find mobile provisioning for %s' % bundle_identifier)
  445       sys.exit(1)
  446     provisioning_profiles = None
  447     if profile:
  448       profile_path = os.path.join(profiles_dir, profile + '.mobileprovision')
  449       if os.path.exists(profile_path):
  450         provisioning_profiles = [profile_path]
  451     if not provisioning_profiles:
  452       provisioning_profiles = glob.glob(
  453           os.path.join(profiles_dir, '*.mobileprovision'))
  454     valid_provisioning_profiles = {}
  455     for profile_path in provisioning_profiles:
  456       profile_data = self._LoadProvisioningProfile(profile_path)
  457       app_id_pattern = profile_data.get(
  458           'Entitlements', {}).get('application-identifier', '')
  459       for team_identifier in profile_data.get('TeamIdentifier', []):
  460         app_id = '%s.%s' % (team_identifier, bundle_identifier)
  461         if fnmatch.fnmatch(app_id, app_id_pattern):
  462           valid_provisioning_profiles[app_id_pattern] = (
  463               profile_path, profile_data, team_identifier)
  464     if not valid_provisioning_profiles:
  465       print >>sys.stderr, (
  466           'cannot find mobile provisioning for %s' % bundle_identifier)
  467       sys.exit(1)
  468     # If the user has multiple provisioning profiles installed that can be
  469     # used for ${bundle_identifier}, pick the most specific one (ie. the
  470     # provisioning profile whose pattern is the longest).
  471     selected_key = max(valid_provisioning_profiles, key=lambda v: len(v))
  472     return valid_provisioning_profiles[selected_key]
  473 
  474   def _LoadProvisioningProfile(self, profile_path):
  475     """Extracts the plist embedded in a provisioning profile.
  476 
  477     Args:
  478       profile_path: string, path to the .mobileprovision file
  479 
  480     Returns:
  481       Content of the plist embedded in the provisioning profile as a dictionary.
  482     """
  483     with tempfile.NamedTemporaryFile() as temp:
  484       subprocess.check_call([
  485           'security', 'cms', '-D', '-i', profile_path, '-o', temp.name])
  486       return self._LoadPlistMaybeBinary(temp.name)
  487 
  488   def _MergePlist(self, merged_plist, plist):
  489     """Merge |plist| into |merged_plist|."""
  490     for key, value in plist.iteritems():
  491       if isinstance(value, dict):
  492         merged_value = merged_plist.get(key, {})
  493         if isinstance(merged_value, dict):
  494           self._MergePlist(merged_value, value)
  495           merged_plist[key] = merged_value
  496         else:
  497           merged_plist[key] = value
  498       else:
  499         merged_plist[key] = value
  500 
  501   def _LoadPlistMaybeBinary(self, plist_path):
  502     """Loads into a memory a plist possibly encoded in binary format.
  503 
  504     This is a wrapper around plistlib.readPlist that tries to convert the
  505     plist to the XML format if it can't be parsed (assuming that it is in
  506     the binary format).
  507 
  508     Args:
  509       plist_path: string, path to a plist file, in XML or binary format
  510 
  511     Returns:
  512       Content of the plist as a dictionary.
  513     """
  514     try:
  515       # First, try to read the file using plistlib that only supports XML,
  516       # and if an exception is raised, convert a temporary copy to XML and
  517       # load that copy.
  518       return plistlib.readPlist(plist_path)
  519     except:
  520       pass
  521     with tempfile.NamedTemporaryFile() as temp:
  522       shutil.copy2(plist_path, temp.name)
  523       subprocess.check_call(['plutil', '-convert', 'xml1', temp.name])
  524       return plistlib.readPlist(temp.name)
  525 
  526   def _GetSubstitutions(self, bundle_identifier, app_identifier_prefix):
  527     """Constructs a dictionary of variable substitutions for Entitlements.plist.
  528 
  529     Args:
  530       bundle_identifier: string, value of CFBundleIdentifier from Info.plist
  531       app_identifier_prefix: string, value for AppIdentifierPrefix
  532 
  533     Returns:
  534       Dictionary of substitutions to apply when generating Entitlements.plist.
  535     """
  536     return {
  537       'CFBundleIdentifier': bundle_identifier,
  538       'AppIdentifierPrefix': app_identifier_prefix,
  539     }
  540 
  541   def _GetCFBundleIdentifier(self):
  542     """Extracts CFBundleIdentifier value from Info.plist in the bundle.
  543 
  544     Returns:
  545       Value of CFBundleIdentifier in the Info.plist located in the bundle.
  546     """
  547     info_plist_path = os.path.join(
  548         os.environ['TARGET_BUILD_DIR'],
  549         os.environ['INFOPLIST_PATH'])
  550     info_plist_data = self._LoadPlistMaybeBinary(info_plist_path)
  551     return info_plist_data['CFBundleIdentifier']
  552 
  553   def _InstallEntitlements(self, entitlements, substitutions, overrides):
  554     """Generates and install the ${BundleName}.xcent entitlements file.
  555 
  556     Expands variables "$(variable)" pattern in the source entitlements file,
  557     add extra entitlements defined in the .mobileprovision file and the copy
  558     the generated plist to "${BundlePath}.xcent".
  559 
  560     Args:
  561       entitlements: string, optional, path to the Entitlements.plist template
  562         to use, defaults to "${SDKROOT}/Entitlements.plist"
  563       substitutions: dictionary, variable substitutions
  564       overrides: dictionary, values to add to the entitlements
  565 
  566     Returns:
  567       Path to the generated entitlements file.
  568     """
  569     source_path = entitlements
  570     target_path = os.path.join(
  571         os.environ['BUILT_PRODUCTS_DIR'],
  572         os.environ['PRODUCT_NAME'] + '.xcent')
  573     if not source_path:
  574       source_path = os.path.join(
  575           os.environ['SDKROOT'],
  576           'Entitlements.plist')
  577     shutil.copy2(source_path, target_path)
  578     data = self._LoadPlistMaybeBinary(target_path)
  579     data = self._ExpandVariables(data, substitutions)
  580     if overrides:
  581       for key in overrides:
  582         if key not in data:
  583           data[key] = overrides[key]
  584     plistlib.writePlist(data, target_path)
  585     return target_path
  586 
  587   def _ExpandVariables(self, data, substitutions):
  588     """Expands variables "$(variable)" in data.
  589 
  590     Args:
  591       data: object, can be either string, list or dictionary
  592       substitutions: dictionary, variable substitutions to perform
  593 
  594     Returns:
  595       Copy of data where each references to "$(variable)" has been replaced
  596       by the corresponding value found in substitutions, or left intact if
  597       the key was not found.
  598     """
  599     if isinstance(data, str):
  600       for key, value in substitutions.iteritems():
  601         data = data.replace('$(%s)' % key, value)
  602       return data
  603     if isinstance(data, list):
  604       return [self._ExpandVariables(v, substitutions) for v in data]
  605     if isinstance(data, dict):
  606       return {k: self._ExpandVariables(data[k], substitutions) for k in data}
  607     return data
  608 
  609 if __name__ == '__main__':
  610   sys.exit(main(sys.argv[1:]))