• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3# Copyright 2012 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7#
8# Xcode supports build variable substitutions and CPP; sadly, that doesn't work
9# because:
10#
11# 1. Xcode wants to do the Info.plist work before it runs any build phases,
12#    this means if we were to generate a .h file for INFOPLIST_PREFIX_HEADER
13#    we'd have to put it in another target so it runs in time.
14# 2. Xcode also doesn't check to see if the header being used as a prefix for
15#    the Info.plist has changed.  So even if we updated it, it's only looking
16#    at the modtime of the info.plist to see if that's changed.
17#
18# So, we work around all of this by making a script build phase that will run
19# during the app build, and simply update the info.plist in place.  This way
20# by the time the app target is done, the info.plist is correct.
21#
22
23
24import optparse
25import os
26import plistlib
27import re
28import subprocess
29import sys
30import tempfile
31
32TOP = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
33
34
35def _ConvertPlist(source_plist, output_plist, fmt):
36  """Convert |source_plist| to |fmt| and save as |output_plist|."""
37  assert sys.version_info.major == 2, "Use plistlib directly in Python 3"
38  return subprocess.call(
39      ['plutil', '-convert', fmt, '-o', output_plist, source_plist])
40
41
42def _GetOutput(args):
43  """Runs a subprocess and waits for termination. Returns (stdout, returncode)
44  of the process. stderr is attached to the parent."""
45  proc = subprocess.Popen(args, stdout=subprocess.PIPE)
46  stdout, _ = proc.communicate()
47  return stdout.decode('UTF-8'), proc.returncode
48
49
50def _RemoveKeys(plist, *keys):
51  """Removes a varargs of keys from the plist."""
52  for key in keys:
53    try:
54      del plist[key]
55    except KeyError:
56      pass
57
58
59def _ApplyVersionOverrides(version, keys, overrides, separator='.'):
60  """Applies version overrides.
61
62  Given a |version| string as "a.b.c.d" (assuming a default separator) with
63  version components named by |keys| then overrides any value that is present
64  in |overrides|.
65
66  >>> _ApplyVersionOverrides('a.b', ['major', 'minor'], {'minor': 'd'})
67  'a.d'
68  """
69  if not overrides:
70    return version
71  version_values = version.split(separator)
72  for i, (key, value) in enumerate(zip(keys, version_values)):
73    if key in overrides:
74      version_values[i] = overrides[key]
75  return separator.join(version_values)
76
77
78def _GetVersion(version_format, values, overrides=None):
79  """Generates a version number according to |version_format| using the values
80  from |values| or |overrides| if given."""
81  result = version_format
82  for key in values:
83    if overrides and key in overrides:
84      value = overrides[key]
85    else:
86      value = values[key]
87    result = result.replace('@%s@' % key, value)
88  return result
89
90
91def _AddVersionKeys(plist, version_format_for_key, version=None,
92                    overrides=None):
93  """Adds the product version number into the plist. Returns True on success and
94  False on error. The error will be printed to stderr."""
95  if not version:
96    # Pull in the Chrome version number.
97    VERSION_TOOL = os.path.join(TOP, 'build/util/version.py')
98    VERSION_FILE = os.path.join(TOP, 'chrome/VERSION')
99    (stdout, retval) = _GetOutput([
100        VERSION_TOOL, '-f', VERSION_FILE, '-t',
101        '@MAJOR@.@MINOR@.@BUILD@.@PATCH@'
102    ])
103
104    # If the command finished with a non-zero return code, then report the
105    # error up.
106    if retval != 0:
107      return False
108
109    version = stdout.strip()
110
111  # Parse the given version number, that should be in MAJOR.MINOR.BUILD.PATCH
112  # format (where each value is a number). Note that str.isdigit() returns
113  # True if the string is composed only of digits (and thus match \d+ regexp).
114  groups = version.split('.')
115  if len(groups) != 4 or not all(element.isdigit() for element in groups):
116    print('Invalid version string specified: "%s"' % version, file=sys.stderr)
117    return False
118  values = dict(zip(('MAJOR', 'MINOR', 'BUILD', 'PATCH'), groups))
119
120  for key in version_format_for_key:
121    plist[key] = _GetVersion(version_format_for_key[key], values, overrides)
122
123  # Return with no error.
124  return True
125
126
127def _DoSCMKeys(plist, add_keys):
128  """Adds the SCM information, visible in about:version, to property list. If
129  |add_keys| is True, it will insert the keys, otherwise it will remove them."""
130  scm_revision = None
131  if add_keys:
132    # Pull in the Chrome revision number.
133    VERSION_TOOL = os.path.join(TOP, 'build/util/version.py')
134    LASTCHANGE_FILE = os.path.join(TOP, 'build/util/LASTCHANGE')
135    (stdout, retval) = _GetOutput(
136        [VERSION_TOOL, '-f', LASTCHANGE_FILE, '-t', '@LASTCHANGE@'])
137    if retval:
138      return False
139    scm_revision = stdout.rstrip()
140
141  # See if the operation failed.
142  _RemoveKeys(plist, 'SCMRevision')
143  if scm_revision != None:
144    plist['SCMRevision'] = scm_revision
145  elif add_keys:
146    print('Could not determine SCM revision.  This may be OK.', file=sys.stderr)
147
148  return True
149
150
151def _AddBreakpadKeys(plist, branding, platform, staging):
152  """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and
153  also requires the |branding| argument."""
154  plist['BreakpadReportInterval'] = '3600'  # Deliberately a string.
155  plist['BreakpadProduct'] = '%s_%s' % (branding, platform)
156  plist['BreakpadProductDisplay'] = branding
157  if staging:
158    plist['BreakpadURL'] = 'https://clients2.google.com/cr/staging_report'
159  else:
160    plist['BreakpadURL'] = 'https://clients2.google.com/cr/report'
161
162  # These are both deliberately strings and not boolean.
163  plist['BreakpadSendAndExit'] = 'YES'
164  plist['BreakpadSkipConfirm'] = 'YES'
165
166
167def _RemoveBreakpadKeys(plist):
168  """Removes any set Breakpad keys."""
169  _RemoveKeys(plist, 'BreakpadURL', 'BreakpadReportInterval', 'BreakpadProduct',
170              'BreakpadProductDisplay', 'BreakpadVersion',
171              'BreakpadSendAndExit', 'BreakpadSkipConfirm')
172
173
174def _TagSuffixes():
175  # Keep this list sorted in the order that tag suffix components are to
176  # appear in a tag value. That is to say, it should be sorted per ASCII.
177  components = ('full', )
178  assert tuple(sorted(components)) == components
179
180  components_len = len(components)
181  combinations = 1 << components_len
182  tag_suffixes = []
183  for combination in range(0, combinations):
184    tag_suffix = ''
185    for component_index in range(0, components_len):
186      if combination & (1 << component_index):
187        tag_suffix += '-' + components[component_index]
188    tag_suffixes.append(tag_suffix)
189  return tag_suffixes
190
191
192def _AddKeystoneKeys(plist, bundle_identifier, base_tag):
193  """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and
194  also requires the |bundle_identifier| argument (com.example.product)."""
195  plist['KSVersion'] = plist['CFBundleShortVersionString']
196  plist['KSProductID'] = bundle_identifier
197  plist['KSUpdateURL'] = 'https://tools.google.com/service/update2'
198
199  _RemoveKeys(plist, 'KSChannelID')
200  if base_tag != '':
201    plist['KSChannelID'] = base_tag
202  for tag_suffix in _TagSuffixes():
203    if tag_suffix:
204      plist['KSChannelID' + tag_suffix] = base_tag + tag_suffix
205
206
207def _RemoveKeystoneKeys(plist):
208  """Removes any set Keystone keys."""
209  _RemoveKeys(plist, 'KSVersion', 'KSProductID', 'KSUpdateURL')
210
211  tag_keys = ['KSChannelID']
212  for tag_suffix in _TagSuffixes():
213    tag_keys.append('KSChannelID' + tag_suffix)
214  _RemoveKeys(plist, *tag_keys)
215
216
217def _AddGTMKeys(plist, platform):
218  """Adds the GTM metadata keys. This must be called AFTER _AddVersionKeys()."""
219  plist['GTMUserAgentID'] = plist['CFBundleName']
220  if platform == 'ios':
221    plist['GTMUserAgentVersion'] = plist['CFBundleVersion']
222  else:
223    plist['GTMUserAgentVersion'] = plist['CFBundleShortVersionString']
224
225
226def _RemoveGTMKeys(plist):
227  """Removes any set GTM metadata keys."""
228  _RemoveKeys(plist, 'GTMUserAgentID', 'GTMUserAgentVersion')
229
230
231def _AddPrivilegedHelperId(plist, privileged_helper_id):
232  plist['SMPrivilegedExecutables'] = {
233      privileged_helper_id: 'identifier ' + privileged_helper_id
234  }
235
236
237def _RemovePrivilegedHelperId(plist):
238  _RemoveKeys(plist, 'SMPrivilegedExecutables')
239
240
241def Main(argv):
242  parser = optparse.OptionParser('%prog [options]')
243  parser.add_option('--plist',
244                    dest='plist_path',
245                    action='store',
246                    type='string',
247                    default=None,
248                    help='The path of the plist to tweak.')
249  parser.add_option('--output', dest='plist_output', action='store',
250      type='string', default=None, help='If specified, the path to output ' + \
251      'the tweaked plist, rather than overwriting the input.')
252  parser.add_option('--breakpad',
253                    dest='use_breakpad',
254                    action='store',
255                    type='int',
256                    default=False,
257                    help='Enable Breakpad [1 or 0]')
258  parser.add_option(
259      '--breakpad_staging',
260      dest='use_breakpad_staging',
261      action='store_true',
262      default=False,
263      help='Use staging breakpad to upload reports. Ignored if --breakpad=0.')
264  parser.add_option('--keystone',
265                    dest='use_keystone',
266                    action='store',
267                    type='int',
268                    default=False,
269                    help='Enable Keystone [1 or 0]')
270  parser.add_option('--keystone-base-tag',
271                    default='',
272                    help='Base Keystone tag to set')
273  parser.add_option('--scm',
274                    dest='add_scm_info',
275                    action='store',
276                    type='int',
277                    default=True,
278                    help='Add SCM metadata [1 or 0]')
279  parser.add_option('--branding',
280                    dest='branding',
281                    action='store',
282                    type='string',
283                    default=None,
284                    help='The branding of the binary')
285  parser.add_option('--bundle_id',
286                    dest='bundle_identifier',
287                    action='store',
288                    type='string',
289                    default=None,
290                    help='The bundle id of the binary')
291  parser.add_option('--platform',
292                    choices=('ios', 'mac'),
293                    default='mac',
294                    help='The target platform of the bundle')
295  parser.add_option('--add-gtm-metadata',
296                    dest='add_gtm_info',
297                    action='store',
298                    type='int',
299                    default=False,
300                    help='Add GTM metadata [1 or 0]')
301  parser.add_option(
302      '--version-overrides',
303      action='append',
304      help='Key-value pair to override specific component of version '
305      'like key=value (can be passed multiple time to configure '
306      'more than one override)')
307  parser.add_option('--format',
308                    choices=('binary1', 'xml1'),
309                    default='xml1',
310                    help='Format to use when writing property list '
311                    '(default: %(default)s)')
312  parser.add_option('--version',
313                    dest='version',
314                    action='store',
315                    type='string',
316                    default=None,
317                    help='The version string [major.minor.build.patch]')
318  parser.add_option('--privileged_helper_id',
319                    dest='privileged_helper_id',
320                    action='store',
321                    type='string',
322                    default=None,
323                    help='The id of the privileged helper executable.')
324  (options, args) = parser.parse_args(argv)
325
326  if len(args) > 0:
327    print(parser.get_usage(), file=sys.stderr)
328    return 1
329
330  if not options.plist_path:
331    print('No --plist specified.', file=sys.stderr)
332    return 1
333
334  # Read the plist into its parsed format. Convert the file to 'xml1' as
335  # plistlib only supports that format in Python 2.7.
336  with tempfile.NamedTemporaryFile() as temp_info_plist:
337    if sys.version_info.major == 2:
338      retcode = _ConvertPlist(options.plist_path, temp_info_plist.name, 'xml1')
339      if retcode != 0:
340        return retcode
341      plist = plistlib.readPlist(temp_info_plist.name)
342    else:
343      with open(options.plist_path, 'rb') as f:
344        plist = plistlib.load(f)
345
346  # Convert overrides.
347  overrides = {}
348  if options.version_overrides:
349    for pair in options.version_overrides:
350      if not '=' in pair:
351        print('Invalid value for --version-overrides:', pair, file=sys.stderr)
352        return 1
353      key, value = pair.split('=', 1)
354      overrides[key] = value
355      if key not in ('MAJOR', 'MINOR', 'BUILD', 'PATCH'):
356        print('Unsupported key for --version-overrides:', key, file=sys.stderr)
357        return 1
358
359  if options.platform == 'mac':
360    version_format_for_key = {
361        # Add public version info so "Get Info" works.
362        'CFBundleShortVersionString': '@MAJOR@.@MINOR@.@BUILD@.@PATCH@',
363
364        # Honor the 429496.72.95 limit.  The maximum comes from splitting
365        # 2^32 - 1 into  6, 2, 2 digits.  The limitation was present in Tiger,
366        # but it could have been fixed in later OS release, but hasn't been
367        # tested (it's easy enough to find out with "lsregister -dump).
368        # http://lists.apple.com/archives/carbon-dev/2006/Jun/msg00139.html
369        # BUILD will always be an increasing value, so BUILD_PATH gives us
370        # something unique that meetings what LS wants.
371        'CFBundleVersion': '@BUILD@.@PATCH@',
372    }
373  else:
374    version_format_for_key = {
375        'CFBundleShortVersionString': '@MAJOR@.@BUILD@.@PATCH@',
376        'CFBundleVersion': '@MAJOR@.@MINOR@.@BUILD@.@PATCH@'
377    }
378
379  if options.use_breakpad:
380    version_format_for_key['BreakpadVersion'] = \
381        '@MAJOR@.@MINOR@.@BUILD@.@PATCH@'
382
383  # Insert the product version.
384  if not _AddVersionKeys(plist,
385                         version_format_for_key,
386                         version=options.version,
387                         overrides=overrides):
388    return 2
389
390  # Add Breakpad if configured to do so.
391  if options.use_breakpad:
392    if options.branding is None:
393      print('Use of Breakpad requires branding.', file=sys.stderr)
394      return 1
395    # Map "target_os" passed from gn via the --platform parameter
396    # to the platform as known by breakpad.
397    platform = {'mac': 'Mac', 'ios': 'iOS'}[options.platform]
398    _AddBreakpadKeys(plist, options.branding, platform,
399                     options.use_breakpad_staging)
400  else:
401    _RemoveBreakpadKeys(plist)
402
403  # Add Keystone if configured to do so.
404  if options.use_keystone:
405    if options.bundle_identifier is None:
406      print('Use of Keystone requires the bundle id.', file=sys.stderr)
407      return 1
408    _AddKeystoneKeys(plist, options.bundle_identifier,
409                     options.keystone_base_tag)
410  else:
411    _RemoveKeystoneKeys(plist)
412
413  # Adds or removes any SCM keys.
414  if not _DoSCMKeys(plist, options.add_scm_info):
415    return 3
416
417  # Add GTM metadata keys.
418  if options.add_gtm_info:
419    _AddGTMKeys(plist, options.platform)
420  else:
421    _RemoveGTMKeys(plist)
422
423  # Add SMPrivilegedExecutables keys.
424  if options.privileged_helper_id:
425    _AddPrivilegedHelperId(plist, options.privileged_helper_id)
426  else:
427    _RemovePrivilegedHelperId(plist)
428
429  output_path = options.plist_path
430  if options.plist_output is not None:
431    output_path = options.plist_output
432
433  # Now that all keys have been mutated, rewrite the file.
434  # Convert Info.plist to the format requested by the --format flag. Any
435  # format would work on Mac but iOS requires specific format.
436  if sys.version_info.major == 2:
437    with tempfile.NamedTemporaryFile() as temp_info_plist:
438      plistlib.writePlist(plist, temp_info_plist.name)
439      return _ConvertPlist(temp_info_plist.name, output_path, options.format)
440  with open(output_path, 'wb') as f:
441    plist_format = {'binary1': plistlib.FMT_BINARY, 'xml1': plistlib.FMT_XML}
442    plistlib.dump(plist, f, fmt=plist_format[options.format])
443
444
445if __name__ == '__main__':
446  # TODO(https://crbug.com/941669): Temporary workaround until all scripts use
447  # python3 by default.
448  if sys.version_info[0] < 3:
449    os.execvp('python3', ['python3'] + sys.argv)
450  sys.exit(Main(sys.argv[1:]))
451