#!/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:]))