Completed
Branch rebuild (e7a2d2)
by Glenn
08:26
created

MacTool   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 576
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 98
dl 0
loc 576
c 0
b 0
f 0
rs 1.5789

How to fix   Complexity   

Complex Class

Complex classes like MacTool often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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:]))
611