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