Home | History | Annotate | Download | only in mac
      1 #!/usr/bin/env python
      2 
      3 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      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 import optparse
     24 import os
     25 from os import environ as env
     26 import plistlib
     27 import re
     28 import subprocess
     29 import sys
     30 import tempfile
     31 
     32 TOP = os.path.join(env['SRCROOT'], '..')
     33 
     34 
     35 def _GetOutput(args):
     36   """Runs a subprocess and waits for termination. Returns (stdout, returncode)
     37   of the process. stderr is attached to the parent."""
     38   proc = subprocess.Popen(args, stdout=subprocess.PIPE)
     39   (stdout, stderr) = proc.communicate()
     40   return (stdout, proc.returncode)
     41 
     42 
     43 def _GetOutputNoError(args):
     44   """Similar to _GetOutput() but ignores stderr. If there's an error launching
     45   the child (like file not found), the exception will be caught and (None, 1)
     46   will be returned to mimic quiet failure."""
     47   try:
     48     proc = subprocess.Popen(args, stdout=subprocess.PIPE,
     49                             stderr=subprocess.PIPE)
     50   except OSError:
     51     return (None, 1)
     52   (stdout, stderr) = proc.communicate()
     53   return (stdout, proc.returncode)
     54 
     55 
     56 def _RemoveKeys(plist, *keys):
     57   """Removes a varargs of keys from the plist."""
     58   for key in keys:
     59     try:
     60       del plist[key]
     61     except KeyError:
     62       pass
     63 
     64 
     65 def _AddVersionKeys(plist, version=None):
     66   """Adds the product version number into the plist. Returns True on success and
     67   False on error. The error will be printed to stderr."""
     68   if version:
     69     match = re.match('\d+\.\d+\.(\d+\.\d+)$', version)
     70     if not match:
     71       print >>sys.stderr, 'Invalid version string specified: "%s"' % version
     72       return False
     73 
     74     full_version = match.group(0)
     75     bundle_version = match.group(1)
     76 
     77   else:
     78     # Pull in the Chrome version number.
     79     VERSION_TOOL = os.path.join(TOP, 'build/util/version.py')
     80     VERSION_FILE = os.path.join(TOP, 'chrome/VERSION')
     81 
     82     (stdout, retval1) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t',
     83                                     '@MAJOR@.@MINOR@.@BUILD@.@PATCH@'])
     84     full_version = stdout.rstrip()
     85 
     86     (stdout, retval2) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t',
     87                                     '@BUILD@.@PATCH@'])
     88     bundle_version = stdout.rstrip()
     89 
     90     # If either of the two version commands finished with non-zero returncode,
     91     # report the error up.
     92     if retval1 or retval2:
     93       return False
     94 
     95   # Add public version info so "Get Info" works.
     96   plist['CFBundleShortVersionString'] = full_version
     97 
     98   # Honor the 429496.72.95 limit.  The maximum comes from splitting 2^32 - 1
     99   # into  6, 2, 2 digits.  The limitation was present in Tiger, but it could
    100   # have been fixed in later OS release, but hasn't been tested (it's easy
    101   # enough to find out with "lsregister -dump).
    102   # http://lists.apple.com/archives/carbon-dev/2006/Jun/msg00139.html
    103   # BUILD will always be an increasing value, so BUILD_PATH gives us something
    104   # unique that meetings what LS wants.
    105   plist['CFBundleVersion'] = bundle_version
    106 
    107   # Return with no error.
    108   return True
    109 
    110 
    111 def _DoSCMKeys(plist, add_keys):
    112   """Adds the SCM information, visible in about:version, to property list. If
    113   |add_keys| is True, it will insert the keys, otherwise it will remove them."""
    114   scm_revision = None
    115   if add_keys:
    116     # Pull in the Chrome revision number.
    117     VERSION_TOOL = os.path.join(TOP, 'build/util/version.py')
    118     LASTCHANGE_FILE = os.path.join(TOP, 'build/util/LASTCHANGE')
    119     (stdout, retval) = _GetOutput([VERSION_TOOL, '-f', LASTCHANGE_FILE, '-t',
    120                                   '@LASTCHANGE@'])
    121     if retval:
    122       return False
    123     scm_revision = stdout.rstrip()
    124 
    125   # See if the operation failed.
    126   _RemoveKeys(plist, 'SCMRevision')
    127   if scm_revision != None:
    128     plist['SCMRevision'] = scm_revision
    129   elif add_keys:
    130     print >>sys.stderr, 'Could not determine SCM revision.  This may be OK.'
    131 
    132   return True
    133 
    134 
    135 def _AddBreakpadKeys(plist, branding):
    136   """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and
    137   also requires the |branding| argument."""
    138   plist['BreakpadReportInterval'] = '3600'  # Deliberately a string.
    139   plist['BreakpadProduct'] = '%s_Mac' % branding
    140   plist['BreakpadProductDisplay'] = branding
    141   plist['BreakpadVersion'] = plist['CFBundleShortVersionString']
    142   # These are both deliberately strings and not boolean.
    143   plist['BreakpadSendAndExit'] = 'YES'
    144   plist['BreakpadSkipConfirm'] = 'YES'
    145 
    146 
    147 def _RemoveBreakpadKeys(plist):
    148   """Removes any set Breakpad keys."""
    149   _RemoveKeys(plist,
    150       'BreakpadURL',
    151       'BreakpadReportInterval',
    152       'BreakpadProduct',
    153       'BreakpadProductDisplay',
    154       'BreakpadVersion',
    155       'BreakpadSendAndExit',
    156       'BreakpadSkipConfirm')
    157 
    158 
    159 def _TagSuffixes():
    160   # Keep this list sorted in the order that tag suffix components are to
    161   # appear in a tag value. That is to say, it should be sorted per ASCII.
    162   components = ('32bit', 'full')
    163   assert tuple(sorted(components)) == components
    164 
    165   components_len = len(components)
    166   combinations = 1 << components_len
    167   tag_suffixes = []
    168   for combination in xrange(0, combinations):
    169     tag_suffix = ''
    170     for component_index in xrange(0, components_len):
    171       if combination & (1 << component_index):
    172         tag_suffix += '-' + components[component_index]
    173     tag_suffixes.append(tag_suffix)
    174   return tag_suffixes
    175 
    176 
    177 def _AddKeystoneKeys(plist, bundle_identifier):
    178   """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and
    179   also requires the |bundle_identifier| argument (com.example.product)."""
    180   plist['KSVersion'] = plist['CFBundleShortVersionString']
    181   plist['KSProductID'] = bundle_identifier
    182   plist['KSUpdateURL'] = 'https://tools.google.com/service/update2'
    183 
    184   _RemoveKeys(plist, 'KSChannelID')
    185   for tag_suffix in _TagSuffixes():
    186     if tag_suffix:
    187       plist['KSChannelID' + tag_suffix] = tag_suffix
    188 
    189 
    190 def _RemoveKeystoneKeys(plist):
    191   """Removes any set Keystone keys."""
    192   _RemoveKeys(plist,
    193       'KSVersion',
    194       'KSProductID',
    195       'KSUpdateURL')
    196 
    197   tag_keys = []
    198   for tag_suffix in _TagSuffixes():
    199     tag_keys.append('KSChannelID' + tag_suffix)
    200   _RemoveKeys(plist, *tag_keys)
    201 
    202 
    203 def Main(argv):
    204   parser = optparse.OptionParser('%prog [options]')
    205   parser.add_option('--breakpad', dest='use_breakpad', action='store',
    206       type='int', default=False, help='Enable Breakpad [1 or 0]')
    207   parser.add_option('--breakpad_uploads', dest='breakpad_uploads',
    208       action='store', type='int', default=False,
    209       help='Enable Breakpad\'s uploading of crash dumps [1 or 0]')
    210   parser.add_option('--keystone', dest='use_keystone', action='store',
    211       type='int', default=False, help='Enable Keystone [1 or 0]')
    212   parser.add_option('--scm', dest='add_scm_info', action='store', type='int',
    213       default=True, help='Add SCM metadata [1 or 0]')
    214   parser.add_option('--branding', dest='branding', action='store',
    215       type='string', default=None, help='The branding of the binary')
    216   parser.add_option('--bundle_id', dest='bundle_identifier',
    217       action='store', type='string', default=None,
    218       help='The bundle id of the binary')
    219   parser.add_option('--version', dest='version', action='store', type='string',
    220       default=None, help='The version string [major.minor.build.patch]')
    221   (options, args) = parser.parse_args(argv)
    222 
    223   if len(args) > 0:
    224     print >>sys.stderr, parser.get_usage()
    225     return 1
    226 
    227   # Read the plist into its parsed format.
    228   DEST_INFO_PLIST = os.path.join(env['TARGET_BUILD_DIR'], env['INFOPLIST_PATH'])
    229   plist = plistlib.readPlist(DEST_INFO_PLIST)
    230 
    231   # Insert the product version.
    232   if not _AddVersionKeys(plist, version=options.version):
    233     return 2
    234 
    235   # Add Breakpad if configured to do so.
    236   if options.use_breakpad:
    237     if options.branding is None:
    238       print >>sys.stderr, 'Use of Breakpad requires branding.'
    239       return 1
    240     _AddBreakpadKeys(plist, options.branding)
    241     if options.breakpad_uploads:
    242       plist['BreakpadURL'] = 'https://clients2.google.com/cr/report'
    243     else:
    244       # This allows crash dumping to a file without uploading the
    245       # dump, for testing purposes.  Breakpad does not recognise
    246       # "none" as a special value, but this does stop crash dump
    247       # uploading from happening.  We need to specify something
    248       # because if "BreakpadURL" is not present, Breakpad will not
    249       # register its crash handler and no crash dumping will occur.
    250       plist['BreakpadURL'] = 'none'
    251   else:
    252     _RemoveBreakpadKeys(plist)
    253 
    254   # Only add Keystone in Release builds.
    255   if options.use_keystone and env['CONFIGURATION'] == 'Release':
    256     if options.bundle_identifier is None:
    257       print >>sys.stderr, 'Use of Keystone requires the bundle id.'
    258       return 1
    259     _AddKeystoneKeys(plist, options.bundle_identifier)
    260   else:
    261     _RemoveKeystoneKeys(plist)
    262 
    263   # Adds or removes any SCM keys.
    264   if not _DoSCMKeys(plist, options.add_scm_info):
    265     return 3
    266 
    267   # Now that all keys have been mutated, rewrite the file.
    268   temp_info_plist = tempfile.NamedTemporaryFile()
    269   plistlib.writePlist(plist, temp_info_plist.name)
    270 
    271   # Info.plist will work perfectly well in any plist format, but traditionally
    272   # applications use xml1 for this, so convert it to ensure that it's valid.
    273   proc = subprocess.Popen(['plutil', '-convert', 'xml1', '-o', DEST_INFO_PLIST,
    274                            temp_info_plist.name])
    275   proc.wait()
    276   return proc.returncode
    277 
    278 
    279 if __name__ == '__main__':
    280   sys.exit(Main(sys.argv[1:]))
    281