| #!/usr/bin/env python | 
 |  | 
 | # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | # | 
 | # Xcode supports build variable substitutions and CPP; sadly, that doesn't work | 
 | # because: | 
 | # | 
 | # 1. Xcode wants to do the Info.plist work before it runs any build phases, | 
 | #    this means if we were to generate a .h file for INFOPLIST_PREFIX_HEADER | 
 | #    we'd have to put it in another target so it runs in time. | 
 | # 2. Xcode also doesn't check to see if the header being used as a prefix for | 
 | #    the Info.plist has changed.  So even if we updated it, it's only looking | 
 | #    at the modtime of the info.plist to see if that's changed. | 
 | # | 
 | # So, we work around all of this by making a script build phase that will run | 
 | # during the app build, and simply update the info.plist in place.  This way | 
 | # by the time the app target is done, the info.plist is correct. | 
 | # | 
 |  | 
 | import optparse | 
 | import os | 
 | import plistlib | 
 | import re | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 |  | 
 | TOP = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) | 
 |  | 
 |  | 
 | def _ConvertPlist(source_plist, output_plist, fmt): | 
 |   """Convert |source_plist| to |fmt| and save as |output_plist|.""" | 
 |   return subprocess.call( | 
 |       ['plutil', '-convert', fmt, '-o', output_plist, source_plist]) | 
 |  | 
 |  | 
 | def _GetOutput(args): | 
 |   """Runs a subprocess and waits for termination. Returns (stdout, returncode) | 
 |   of the process. stderr is attached to the parent.""" | 
 |   proc = subprocess.Popen(args, stdout=subprocess.PIPE) | 
 |   (stdout, stderr) = proc.communicate() | 
 |   return (stdout, proc.returncode) | 
 |  | 
 |  | 
 | def _GetOutputNoError(args): | 
 |   """Similar to _GetOutput() but ignores stderr. If there's an error launching | 
 |   the child (like file not found), the exception will be caught and (None, 1) | 
 |   will be returned to mimic quiet failure.""" | 
 |   try: | 
 |     proc = subprocess.Popen(args, stdout=subprocess.PIPE, | 
 |                             stderr=subprocess.PIPE) | 
 |   except OSError: | 
 |     return (None, 1) | 
 |   (stdout, stderr) = proc.communicate() | 
 |   return (stdout, proc.returncode) | 
 |  | 
 |  | 
 | def _RemoveKeys(plist, *keys): | 
 |   """Removes a varargs of keys from the plist.""" | 
 |   for key in keys: | 
 |     try: | 
 |       del plist[key] | 
 |     except KeyError: | 
 |       pass | 
 |  | 
 |  | 
 | def _ApplyVersionOverrides(version, keys, overrides, separator='.'): | 
 |   """Applies version overrides. | 
 |  | 
 |   Given a |version| string as "a.b.c.d" (assuming a default separator) with | 
 |   version components named by |keys| then overrides any value that is present | 
 |   in |overrides|. | 
 |  | 
 |   >>> _ApplyVersionOverrides('a.b', ['major', 'minor'], {'minor': 'd'}) | 
 |   'a.d' | 
 |   """ | 
 |   if not overrides: | 
 |     return version | 
 |   version_values = version.split(separator) | 
 |   for i, (key, value) in enumerate(zip(keys, version_values)): | 
 |     if key in overrides: | 
 |       version_values[i] = overrides[key] | 
 |   return separator.join(version_values) | 
 |  | 
 |  | 
 | def _GetVersion(version_format, values, overrides=None): | 
 |   """Generates a version number according to |version_format| using the values | 
 |   from |values| or |overrides| if given.""" | 
 |   result = version_format | 
 |   for key in values: | 
 |     if overrides and key in overrides: | 
 |       value = overrides[key] | 
 |     else: | 
 |       value = values[key] | 
 |     result = result.replace('@%s@' % key, value) | 
 |   return result | 
 |  | 
 |  | 
 | def _AddVersionKeys( | 
 |     plist, version_format_for_key, version=None, overrides=None): | 
 |   """Adds the product version number into the plist. Returns True on success and | 
 |   False on error. The error will be printed to stderr.""" | 
 |   if not version: | 
 |     # Pull in the Chrome version number. | 
 |     VERSION_TOOL = os.path.join(TOP, 'build/util/version.py') | 
 |     VERSION_FILE = os.path.join(TOP, 'chrome/VERSION') | 
 |     (stdout, retval) = _GetOutput([ | 
 |         VERSION_TOOL, '-f', VERSION_FILE, | 
 |         '-t', '@MAJOR@.@MINOR@.@BUILD@.@PATCH@']) | 
 |  | 
 |     # If the command finished with a non-zero return code, then report the | 
 |     # error up. | 
 |     if retval != 0: | 
 |       return False | 
 |  | 
 |     version = stdout.strip() | 
 |  | 
 |   # Parse the given version number, that should be in MAJOR.MINOR.BUILD.PATCH | 
 |   # format (where each value is a number). Note that str.isdigit() returns | 
 |   # True if the string is composed only of digits (and thus match \d+ regexp). | 
 |   groups = version.split('.') | 
 |   if len(groups) != 4 or not all(element.isdigit() for element in groups): | 
 |     print >>sys.stderr, 'Invalid version string specified: "%s"' % version | 
 |     return False | 
 |   values = dict(zip(('MAJOR', 'MINOR', 'BUILD', 'PATCH'), groups)) | 
 |  | 
 |   for key in version_format_for_key: | 
 |     plist[key] = _GetVersion(version_format_for_key[key], values, overrides) | 
 |  | 
 |   # Return with no error. | 
 |   return True | 
 |  | 
 |  | 
 | def _DoSCMKeys(plist, add_keys): | 
 |   """Adds the SCM information, visible in about:version, to property list. If | 
 |   |add_keys| is True, it will insert the keys, otherwise it will remove them.""" | 
 |   scm_revision = None | 
 |   if add_keys: | 
 |     # Pull in the Chrome revision number. | 
 |     VERSION_TOOL = os.path.join(TOP, 'build/util/version.py') | 
 |     LASTCHANGE_FILE = os.path.join(TOP, 'build/util/LASTCHANGE') | 
 |     (stdout, retval) = _GetOutput([VERSION_TOOL, '-f', LASTCHANGE_FILE, '-t', | 
 |                                   '@LASTCHANGE@']) | 
 |     if retval: | 
 |       return False | 
 |     scm_revision = stdout.rstrip() | 
 |  | 
 |   # See if the operation failed. | 
 |   _RemoveKeys(plist, 'SCMRevision') | 
 |   if scm_revision != None: | 
 |     plist['SCMRevision'] = scm_revision | 
 |   elif add_keys: | 
 |     print >>sys.stderr, 'Could not determine SCM revision.  This may be OK.' | 
 |  | 
 |   return True | 
 |  | 
 |  | 
 | def _AddBreakpadKeys(plist, branding, platform, staging): | 
 |   """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and | 
 |   also requires the |branding| argument.""" | 
 |   plist['BreakpadReportInterval'] = '3600'  # Deliberately a string. | 
 |   plist['BreakpadProduct'] = '%s_%s' % (branding, platform) | 
 |   plist['BreakpadProductDisplay'] = branding | 
 |   if staging: | 
 |     plist['BreakpadURL'] = 'https://clients2.google.com/cr/staging_report' | 
 |   else: | 
 |     plist['BreakpadURL'] = 'https://clients2.google.com/cr/report' | 
 |  | 
 |   # These are both deliberately strings and not boolean. | 
 |   plist['BreakpadSendAndExit'] = 'YES' | 
 |   plist['BreakpadSkipConfirm'] = 'YES' | 
 |  | 
 |  | 
 | def _RemoveBreakpadKeys(plist): | 
 |   """Removes any set Breakpad keys.""" | 
 |   _RemoveKeys(plist, | 
 |       'BreakpadURL', | 
 |       'BreakpadReportInterval', | 
 |       'BreakpadProduct', | 
 |       'BreakpadProductDisplay', | 
 |       'BreakpadVersion', | 
 |       'BreakpadSendAndExit', | 
 |       'BreakpadSkipConfirm') | 
 |  | 
 |  | 
 | def _TagSuffixes(): | 
 |   # Keep this list sorted in the order that tag suffix components are to | 
 |   # appear in a tag value. That is to say, it should be sorted per ASCII. | 
 |   components = ('full',) | 
 |   assert tuple(sorted(components)) == components | 
 |  | 
 |   components_len = len(components) | 
 |   combinations = 1 << components_len | 
 |   tag_suffixes = [] | 
 |   for combination in xrange(0, combinations): | 
 |     tag_suffix = '' | 
 |     for component_index in xrange(0, components_len): | 
 |       if combination & (1 << component_index): | 
 |         tag_suffix += '-' + components[component_index] | 
 |     tag_suffixes.append(tag_suffix) | 
 |   return tag_suffixes | 
 |  | 
 |  | 
 | def _AddKeystoneKeys(plist, bundle_identifier): | 
 |   """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and | 
 |   also requires the |bundle_identifier| argument (com.example.product).""" | 
 |   plist['KSVersion'] = plist['CFBundleShortVersionString'] | 
 |   plist['KSProductID'] = bundle_identifier | 
 |   plist['KSUpdateURL'] = 'https://tools.google.com/service/update2' | 
 |  | 
 |   _RemoveKeys(plist, 'KSChannelID') | 
 |   for tag_suffix in _TagSuffixes(): | 
 |     if tag_suffix: | 
 |       plist['KSChannelID' + tag_suffix] = tag_suffix | 
 |  | 
 |  | 
 | def _RemoveKeystoneKeys(plist): | 
 |   """Removes any set Keystone keys.""" | 
 |   _RemoveKeys(plist, | 
 |       'KSVersion', | 
 |       'KSProductID', | 
 |       'KSUpdateURL') | 
 |  | 
 |   tag_keys = [] | 
 |   for tag_suffix in _TagSuffixes(): | 
 |     tag_keys.append('KSChannelID' + tag_suffix) | 
 |   _RemoveKeys(plist, *tag_keys) | 
 |  | 
 |  | 
 | def Main(argv): | 
 |   parser = optparse.OptionParser('%prog [options]') | 
 |   parser.add_option('--plist', dest='plist_path', action='store', | 
 |       type='string', default=None, help='The path of the plist to tweak.') | 
 |   parser.add_option('--output', dest='plist_output', action='store', | 
 |       type='string', default=None, help='If specified, the path to output ' + \ | 
 |       'the tweaked plist, rather than overwriting the input.') | 
 |   parser.add_option('--breakpad', dest='use_breakpad', action='store', | 
 |       type='int', default=False, help='Enable Breakpad [1 or 0]') | 
 |   parser.add_option('--breakpad_staging', dest='use_breakpad_staging', | 
 |       action='store_true', default=False, | 
 |       help='Use staging breakpad to upload reports. Ignored if --breakpad=0.') | 
 |   parser.add_option('--keystone', dest='use_keystone', action='store', | 
 |       type='int', default=False, help='Enable Keystone [1 or 0]') | 
 |   parser.add_option('--scm', dest='add_scm_info', action='store', type='int', | 
 |       default=True, help='Add SCM metadata [1 or 0]') | 
 |   parser.add_option('--branding', dest='branding', action='store', | 
 |       type='string', default=None, help='The branding of the binary') | 
 |   parser.add_option('--bundle_id', dest='bundle_identifier', | 
 |       action='store', type='string', default=None, | 
 |       help='The bundle id of the binary') | 
 |   parser.add_option('--platform', choices=('ios', 'mac'), default='mac', | 
 |       help='The target platform of the bundle') | 
 |   parser.add_option('--version-overrides', action='append', | 
 |       help='Key-value pair to override specific component of version ' | 
 |            'like key=value (can be passed multiple time to configure ' | 
 |            'more than one override)') | 
 |   parser.add_option('--format', choices=('binary1', 'xml1', 'json'), | 
 |       default='xml1', help='Format to use when writing property list ' | 
 |           '(default: %(default)s)') | 
 |   parser.add_option('--version', dest='version', action='store', type='string', | 
 |       default=None, help='The version string [major.minor.build.patch]') | 
 |   (options, args) = parser.parse_args(argv) | 
 |  | 
 |   if len(args) > 0: | 
 |     print >>sys.stderr, parser.get_usage() | 
 |     return 1 | 
 |  | 
 |   if not options.plist_path: | 
 |     print >>sys.stderr, 'No --plist specified.' | 
 |     return 1 | 
 |  | 
 |   # Read the plist into its parsed format. Convert the file to 'xml1' as | 
 |   # plistlib only supports that format in Python 2.7. | 
 |   with tempfile.NamedTemporaryFile() as temp_info_plist: | 
 |     retcode = _ConvertPlist(options.plist_path, temp_info_plist.name, 'xml1') | 
 |     if retcode != 0: | 
 |       return retcode | 
 |     plist = plistlib.readPlist(temp_info_plist.name) | 
 |  | 
 |   # Convert overrides. | 
 |   overrides = {} | 
 |   if options.version_overrides: | 
 |     for pair in options.version_overrides: | 
 |       if not '=' in pair: | 
 |         print >>sys.stderr, 'Invalid value for --version-overrides:', pair | 
 |         return 1 | 
 |       key, value = pair.split('=', 1) | 
 |       overrides[key] = value | 
 |       if key not in ('MAJOR', 'MINOR', 'BUILD', 'PATCH'): | 
 |         print >>sys.stderr, 'Unsupported key for --version-overrides:', key | 
 |         return 1 | 
 |  | 
 |   if options.platform == 'mac': | 
 |     version_format_for_key = { | 
 |       # Add public version info so "Get Info" works. | 
 |       'CFBundleShortVersionString': '@MAJOR@.@MINOR@.@BUILD@.@PATCH@', | 
 |  | 
 |       # Honor the 429496.72.95 limit.  The maximum comes from splitting 2^32 - 1 | 
 |       # into  6, 2, 2 digits.  The limitation was present in Tiger, but it could | 
 |       # have been fixed in later OS release, but hasn't been tested (it's easy | 
 |       # enough to find out with "lsregister -dump). | 
 |       # http://lists.apple.com/archives/carbon-dev/2006/Jun/msg00139.html | 
 |       # BUILD will always be an increasing value, so BUILD_PATH gives us | 
 |       # something unique that meetings what LS wants. | 
 |       'CFBundleVersion': '@BUILD@.@PATCH@', | 
 |     } | 
 |   else: | 
 |     version_format_for_key = { | 
 |       'CFBundleShortVersionString': '@MAJOR@.@BUILD@.@PATCH@', | 
 |       'CFBundleVersion': '@MAJOR@.@MINOR@.@BUILD@.@PATCH@' | 
 |     } | 
 |  | 
 |   if options.use_breakpad: | 
 |     version_format_for_key['BreakpadVersion'] = \ | 
 |         '@MAJOR@.@MINOR@.@BUILD@.@PATCH@' | 
 |  | 
 |   # Insert the product version. | 
 |   if not _AddVersionKeys( | 
 |       plist, version_format_for_key, version=options.version, | 
 |       overrides=overrides): | 
 |     return 2 | 
 |  | 
 |   # Add Breakpad if configured to do so. | 
 |   if options.use_breakpad: | 
 |     if options.branding is None: | 
 |       print >>sys.stderr, 'Use of Breakpad requires branding.' | 
 |       return 1 | 
 |     # Map "target_os" passed from gn via the --platform parameter | 
 |     # to the platform as known by breakpad. | 
 |     platform = {'mac': 'Mac', 'ios': 'iOS'}[options.platform] | 
 |     _AddBreakpadKeys(plist, options.branding, platform, | 
 |         options.use_breakpad_staging) | 
 |   else: | 
 |     _RemoveBreakpadKeys(plist) | 
 |  | 
 |   # Add Keystone if configured to do so. | 
 |   if options.use_keystone: | 
 |     if options.bundle_identifier is None: | 
 |       print >>sys.stderr, 'Use of Keystone requires the bundle id.' | 
 |       return 1 | 
 |     _AddKeystoneKeys(plist, options.bundle_identifier) | 
 |   else: | 
 |     _RemoveKeystoneKeys(plist) | 
 |  | 
 |   # Adds or removes any SCM keys. | 
 |   if not _DoSCMKeys(plist, options.add_scm_info): | 
 |     return 3 | 
 |  | 
 |   output_path = options.plist_path | 
 |   if options.plist_output is not None: | 
 |     output_path = options.plist_output | 
 |  | 
 |   # Now that all keys have been mutated, rewrite the file. | 
 |   with tempfile.NamedTemporaryFile() as temp_info_plist: | 
 |     plistlib.writePlist(plist, temp_info_plist.name) | 
 |  | 
 |     # Convert Info.plist to the format requested by the --format flag. Any | 
 |     # format would work on Mac but iOS requires specific format. | 
 |     return _ConvertPlist(temp_info_plist.name, output_path, options.format) | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   sys.exit(Main(sys.argv[1:])) |