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, 'chrome/tools/build/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, 'chrome/tools/build/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 _DoPDFKeys(plist, add_keys):
    136   """Adds PDF support to the document types list. If add_keys is True, it will
    137   add the type information dictionary. If it is False, it will remove it if
    138   present."""
    139 
    140   PDF_FILE_EXTENSION = 'pdf'
    141 
    142   def __AddPDFKeys(sub_plist):
    143     """Writes the keys into a sub-dictionary of the plist."""
    144     sub_plist['CFBundleTypeExtensions'] = [PDF_FILE_EXTENSION]
    145     sub_plist['CFBundleTypeIconFile'] = 'document.icns'
    146     sub_plist['CFBundleTypeMIMETypes'] = 'application/pdf'
    147     sub_plist['CFBundleTypeName'] = 'PDF Document'
    148     sub_plist['CFBundleTypeRole'] = 'Viewer'
    149 
    150   DOCUMENT_TYPES_KEY = 'CFBundleDocumentTypes'
    151 
    152   # First get the list of document types, creating it if necessary.
    153   try:
    154     extensions = plist[DOCUMENT_TYPES_KEY]
    155   except KeyError:
    156     # If this plist doesn't have a type dictionary, create one if set to add the
    157     # keys. If not, bail.
    158     if not add_keys:
    159       return
    160     extensions = plist[DOCUMENT_TYPES_KEY] = []
    161 
    162   # Loop over each entry in the list, looking for one that handles PDF types.
    163   for i, ext in enumerate(extensions):
    164     # If an entry for .pdf files is found...
    165     if 'CFBundleTypeExtensions' not in ext:
    166       continue
    167     if PDF_FILE_EXTENSION in ext['CFBundleTypeExtensions']:
    168       if add_keys:
    169         # Overwrite the existing keys with new ones.
    170         __AddPDFKeys(ext)
    171       else:
    172         # Otherwise, delete the entry entirely.
    173         del extensions[i]
    174       return
    175 
    176   # No PDF entry exists. If one needs to be added, do so now.
    177   if add_keys:
    178     pdf_entry = {}
    179     __AddPDFKeys(pdf_entry)
    180     extensions.append(pdf_entry)
    181 
    182 
    183 def _AddBreakpadKeys(plist, branding):
    184   """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and
    185   also requires the |branding| argument."""
    186   plist['BreakpadReportInterval'] = '3600'  # Deliberately a string.
    187   plist['BreakpadProduct'] = '%s_Mac' % branding
    188   plist['BreakpadProductDisplay'] = branding
    189   plist['BreakpadVersion'] = plist['CFBundleShortVersionString']
    190   # These are both deliberately strings and not boolean.
    191   plist['BreakpadSendAndExit'] = 'YES'
    192   plist['BreakpadSkipConfirm'] = 'YES'
    193 
    194 
    195 def _RemoveBreakpadKeys(plist):
    196   """Removes any set Breakpad keys."""
    197   _RemoveKeys(plist,
    198       'BreakpadURL',
    199       'BreakpadReportInterval',
    200       'BreakpadProduct',
    201       'BreakpadProductDisplay',
    202       'BreakpadVersion',
    203       'BreakpadSendAndExit',
    204       'BreakpadSkipConfirm')
    205 
    206 
    207 def _TagSuffixes():
    208   # Keep this list sorted in the order that tag suffix components are to
    209   # appear in a tag value. That is to say, it should be sorted per ASCII.
    210   components = ('32bit', 'full')
    211   assert tuple(sorted(components)) == components
    212 
    213   components_len = len(components)
    214   combinations = 1 << components_len
    215   tag_suffixes = []
    216   for combination in xrange(0, combinations):
    217     tag_suffix = ''
    218     for component_index in xrange(0, components_len):
    219       if combination & (1 << component_index):
    220         tag_suffix += '-' + components[component_index]
    221     tag_suffixes.append(tag_suffix)
    222   return tag_suffixes
    223 
    224 
    225 def _AddKeystoneKeys(plist, bundle_identifier):
    226   """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and
    227   also requires the |bundle_identifier| argument (com.example.product)."""
    228   plist['KSVersion'] = plist['CFBundleShortVersionString']
    229   plist['KSProductID'] = bundle_identifier
    230   plist['KSUpdateURL'] = 'https://tools.google.com/service/update2'
    231 
    232   _RemoveKeys(plist, 'KSChannelID')
    233   for tag_suffix in _TagSuffixes():
    234     if tag_suffix:
    235       plist['KSChannelID' + tag_suffix] = tag_suffix
    236 
    237 
    238 def _RemoveKeystoneKeys(plist):
    239   """Removes any set Keystone keys."""
    240   _RemoveKeys(plist,
    241       'KSVersion',
    242       'KSProductID',
    243       'KSUpdateURL')
    244 
    245   tag_keys = []
    246   for tag_suffix in _TagSuffixes():
    247     tag_keys.append('KSChannelID' + tag_suffix)
    248   _RemoveKeys(plist, *tag_keys)
    249 
    250 
    251 def Main(argv):
    252   parser = optparse.OptionParser('%prog [options]')
    253   parser.add_option('--breakpad', dest='use_breakpad', action='store',
    254       type='int', default=False, help='Enable Breakpad [1 or 0]')
    255   parser.add_option('--breakpad_uploads', dest='breakpad_uploads',
    256       action='store', type='int', default=False,
    257       help='Enable Breakpad\'s uploading of crash dumps [1 or 0]')
    258   parser.add_option('--keystone', dest='use_keystone', action='store',
    259       type='int', default=False, help='Enable Keystone [1 or 0]')
    260   parser.add_option('--scm', dest='add_scm_info', action='store', type='int',
    261       default=True, help='Add SCM metadata [1 or 0]')
    262   parser.add_option('--pdf', dest='add_pdf_support', action='store', type='int',
    263       default=False, help='Add PDF file handler support [1 or 0]')
    264   parser.add_option('--branding', dest='branding', action='store',
    265       type='string', default=None, help='The branding of the binary')
    266   parser.add_option('--bundle_id', dest='bundle_identifier',
    267       action='store', type='string', default=None,
    268       help='The bundle id of the binary')
    269   parser.add_option('--version', dest='version', action='store', type='string',
    270       default=None, help='The version string [major.minor.build.patch]')
    271   (options, args) = parser.parse_args(argv)
    272 
    273   if len(args) > 0:
    274     print >>sys.stderr, parser.get_usage()
    275     return 1
    276 
    277   # Read the plist into its parsed format.
    278   DEST_INFO_PLIST = os.path.join(env['TARGET_BUILD_DIR'], env['INFOPLIST_PATH'])
    279   plist = plistlib.readPlist(DEST_INFO_PLIST)
    280 
    281   # Insert the product version.
    282   if not _AddVersionKeys(plist, version=options.version):
    283     return 2
    284 
    285   # Add Breakpad if configured to do so.
    286   if options.use_breakpad:
    287     if options.branding is None:
    288       print >>sys.stderr, 'Use of Breakpad requires branding.'
    289       return 1
    290     _AddBreakpadKeys(plist, options.branding)
    291     if options.breakpad_uploads:
    292       plist['BreakpadURL'] = 'https://clients2.google.com/cr/report'
    293     else:
    294       # This allows crash dumping to a file without uploading the
    295       # dump, for testing purposes.  Breakpad does not recognise
    296       # "none" as a special value, but this does stop crash dump
    297       # uploading from happening.  We need to specify something
    298       # because if "BreakpadURL" is not present, Breakpad will not
    299       # register its crash handler and no crash dumping will occur.
    300       plist['BreakpadURL'] = 'none'
    301   else:
    302     _RemoveBreakpadKeys(plist)
    303 
    304   # Only add Keystone in Release builds.
    305   if options.use_keystone and env['CONFIGURATION'] == 'Release':
    306     if options.bundle_identifier is None:
    307       print >>sys.stderr, 'Use of Keystone requires the bundle id.'
    308       return 1
    309     _AddKeystoneKeys(plist, options.bundle_identifier)
    310   else:
    311     _RemoveKeystoneKeys(plist)
    312 
    313   # Adds or removes any SCM keys.
    314   if not _DoSCMKeys(plist, options.add_scm_info):
    315     return 3
    316 
    317   # Adds or removes the PDF file handler entry.
    318   _DoPDFKeys(plist, options.add_pdf_support)
    319 
    320   # Now that all keys have been mutated, rewrite the file.
    321   temp_info_plist = tempfile.NamedTemporaryFile()
    322   plistlib.writePlist(plist, temp_info_plist.name)
    323 
    324   # Info.plist will work perfectly well in any plist format, but traditionally
    325   # applications use xml1 for this, so convert it to ensure that it's valid.
    326   proc = subprocess.Popen(['plutil', '-convert', 'xml1', '-o', DEST_INFO_PLIST,
    327                            temp_info_plist.name])
    328   proc.wait()
    329   return proc.returncode
    330 
    331 
    332 if __name__ == '__main__':
    333   sys.exit(Main(sys.argv[1:]))
    334