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 _AddKeystoneKeys(plist, bundle_identifier):
    208   """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and
    209   also requires the |bundle_identifier| argument (com.example.product)."""
    210   plist['KSVersion'] = plist['CFBundleShortVersionString']
    211   plist['KSProductID'] = bundle_identifier
    212   plist['KSUpdateURL'] = 'https://tools.google.com/service/update2'
    213 
    214 
    215 def _RemoveKeystoneKeys(plist):
    216   """Removes any set Keystone keys."""
    217   _RemoveKeys(plist,
    218       'KSVersion',
    219       'KSProductID',
    220       'KSUpdateURL')
    221 
    222 
    223 def Main(argv):
    224   parser = optparse.OptionParser('%prog [options]')
    225   parser.add_option('--breakpad', dest='use_breakpad', action='store',
    226       type='int', default=False, help='Enable Breakpad [1 or 0]')
    227   parser.add_option('--breakpad_uploads', dest='breakpad_uploads',
    228       action='store', type='int', default=False,
    229       help='Enable Breakpad\'s uploading of crash dumps [1 or 0]')
    230   parser.add_option('--keystone', dest='use_keystone', action='store',
    231       type='int', default=False, help='Enable Keystone [1 or 0]')
    232   parser.add_option('--scm', dest='add_scm_info', action='store', type='int',
    233       default=True, help='Add SCM metadata [1 or 0]')
    234   parser.add_option('--pdf', dest='add_pdf_support', action='store', type='int',
    235       default=False, help='Add PDF file handler support [1 or 0]')
    236   parser.add_option('--branding', dest='branding', action='store',
    237       type='string', default=None, help='The branding of the binary')
    238   parser.add_option('--bundle_id', dest='bundle_identifier',
    239       action='store', type='string', default=None,
    240       help='The bundle id of the binary')
    241   parser.add_option('--version', dest='version', action='store', type='string',
    242       default=None, help='The version string [major.minor.build.patch]')
    243   (options, args) = parser.parse_args(argv)
    244 
    245   if len(args) > 0:
    246     print >>sys.stderr, parser.get_usage()
    247     return 1
    248 
    249   # Read the plist into its parsed format.
    250   DEST_INFO_PLIST = os.path.join(env['TARGET_BUILD_DIR'], env['INFOPLIST_PATH'])
    251   plist = plistlib.readPlist(DEST_INFO_PLIST)
    252 
    253   # Insert the product version.
    254   if not _AddVersionKeys(plist, version=options.version):
    255     return 2
    256 
    257   # Add Breakpad if configured to do so.
    258   if options.use_breakpad:
    259     if options.branding is None:
    260       print >>sys.stderr, 'Use of Breakpad requires branding.'
    261       return 1
    262     _AddBreakpadKeys(plist, options.branding)
    263     if options.breakpad_uploads:
    264       plist['BreakpadURL'] = 'https://clients2.google.com/cr/report'
    265     else:
    266       # This allows crash dumping to a file without uploading the
    267       # dump, for testing purposes.  Breakpad does not recognise
    268       # "none" as a special value, but this does stop crash dump
    269       # uploading from happening.  We need to specify something
    270       # because if "BreakpadURL" is not present, Breakpad will not
    271       # register its crash handler and no crash dumping will occur.
    272       plist['BreakpadURL'] = 'none'
    273   else:
    274     _RemoveBreakpadKeys(plist)
    275 
    276   # Only add Keystone in Release builds.
    277   if options.use_keystone and env['CONFIGURATION'] == 'Release':
    278     if options.bundle_identifier is None:
    279       print >>sys.stderr, 'Use of Keystone requires the bundle id.'
    280       return 1
    281     _AddKeystoneKeys(plist, options.bundle_identifier)
    282   else:
    283     _RemoveKeystoneKeys(plist)
    284 
    285   # Adds or removes any SCM keys.
    286   if not _DoSCMKeys(plist, options.add_scm_info):
    287     return 3
    288 
    289   # Adds or removes the PDF file handler entry.
    290   _DoPDFKeys(plist, options.add_pdf_support)
    291 
    292   # Now that all keys have been mutated, rewrite the file.
    293   temp_info_plist = tempfile.NamedTemporaryFile()
    294   plistlib.writePlist(plist, temp_info_plist.name)
    295 
    296   # Info.plist will work perfectly well in any plist format, but traditionally
    297   # applications use xml1 for this, so convert it to ensure that it's valid.
    298   proc = subprocess.Popen(['plutil', '-convert', 'xml1', '-o', DEST_INFO_PLIST,
    299                            temp_info_plist.name])
    300   proc.wait()
    301   return proc.returncode
    302 
    303 
    304 if __name__ == '__main__':
    305   sys.exit(Main(sys.argv[1:]))
    306