"Fossies" - the Fresh Open Source Software Archive

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