michael@0: #!/usr/bin/env python michael@0: michael@0: # Copyright (c) 2012 The Chromium Authors. All rights reserved. michael@0: # Use of this source code is governed by a BSD-style license that can be michael@0: # found in the LICENSE file. michael@0: michael@0: # michael@0: # Xcode supports build variable substitutions and CPP; sadly, that doesn't work michael@0: # because: michael@0: # michael@0: # 1. Xcode wants to do the Info.plist work before it runs any build phases, michael@0: # this means if we were to generate a .h file for INFOPLIST_PREFIX_HEADER michael@0: # we'd have to put it in another target so it runs in time. michael@0: # 2. Xcode also doesn't check to see if the header being used as a prefix for michael@0: # the Info.plist has changed. So even if we updated it, it's only looking michael@0: # at the modtime of the info.plist to see if that's changed. michael@0: # michael@0: # So, we work around all of this by making a script build phase that will run michael@0: # during the app build, and simply update the info.plist in place. This way michael@0: # by the time the app target is done, the info.plist is correct. michael@0: # michael@0: michael@0: import optparse michael@0: import os michael@0: from os import environ as env michael@0: import plistlib michael@0: import re michael@0: import subprocess michael@0: import sys michael@0: import tempfile michael@0: michael@0: TOP = os.path.join(env['SRCROOT'], '..') michael@0: michael@0: sys.path.insert(0, os.path.join(TOP, "build/util")) michael@0: import lastchange michael@0: michael@0: michael@0: def _GetOutput(args): michael@0: """Runs a subprocess and waits for termination. Returns (stdout, returncode) michael@0: of the process. stderr is attached to the parent.""" michael@0: proc = subprocess.Popen(args, stdout=subprocess.PIPE) michael@0: (stdout, stderr) = proc.communicate() michael@0: return (stdout, proc.returncode) michael@0: michael@0: michael@0: def _GetOutputNoError(args): michael@0: """Similar to _GetOutput() but ignores stderr. If there's an error launching michael@0: the child (like file not found), the exception will be caught and (None, 1) michael@0: will be returned to mimic quiet failure.""" michael@0: try: michael@0: proc = subprocess.Popen(args, stdout=subprocess.PIPE, michael@0: stderr=subprocess.PIPE) michael@0: except OSError: michael@0: return (None, 1) michael@0: (stdout, stderr) = proc.communicate() michael@0: return (stdout, proc.returncode) michael@0: michael@0: michael@0: def _RemoveKeys(plist, *keys): michael@0: """Removes a varargs of keys from the plist.""" michael@0: for key in keys: michael@0: try: michael@0: del plist[key] michael@0: except KeyError: michael@0: pass michael@0: michael@0: michael@0: def _AddVersionKeys(plist): michael@0: """Adds the product version number into the plist. Returns True on success and michael@0: False on error. The error will be printed to stderr.""" michael@0: # Pull in the Chrome version number. michael@0: VERSION_TOOL = os.path.join(TOP, 'chrome/tools/build/version.py') michael@0: VERSION_FILE = os.path.join(TOP, 'chrome/VERSION') michael@0: michael@0: (stdout, retval1) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t', michael@0: '@MAJOR@.@MINOR@.@BUILD@.@PATCH@']) michael@0: full_version = stdout.rstrip() michael@0: michael@0: (stdout, retval2) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t', michael@0: '@BUILD@.@PATCH@']) michael@0: bundle_version = stdout.rstrip() michael@0: michael@0: # If either of the two version commands finished with non-zero returncode, michael@0: # report the error up. michael@0: if retval1 or retval2: michael@0: return False michael@0: michael@0: # Add public version info so "Get Info" works. michael@0: plist['CFBundleShortVersionString'] = full_version michael@0: michael@0: # Honor the 429496.72.95 limit. The maximum comes from splitting 2^32 - 1 michael@0: # into 6, 2, 2 digits. The limitation was present in Tiger, but it could michael@0: # have been fixed in later OS release, but hasn't been tested (it's easy michael@0: # enough to find out with "lsregister -dump). michael@0: # http://lists.apple.com/archives/carbon-dev/2006/Jun/msg00139.html michael@0: # BUILD will always be an increasing value, so BUILD_PATH gives us something michael@0: # unique that meetings what LS wants. michael@0: plist['CFBundleVersion'] = bundle_version michael@0: michael@0: # Return with no error. michael@0: return True michael@0: michael@0: michael@0: def _DoSCMKeys(plist, add_keys): michael@0: """Adds the SCM information, visible in about:version, to property list. If michael@0: |add_keys| is True, it will insert the keys, otherwise it will remove them.""" michael@0: scm_path, scm_revision = None, None michael@0: if add_keys: michael@0: version_info = lastchange.FetchVersionInfo( michael@0: default_lastchange=None, directory=TOP) michael@0: scm_path, scm_revision = version_info.url, version_info.revision michael@0: michael@0: # See if the operation failed. michael@0: _RemoveKeys(plist, 'SCMRevision') michael@0: if scm_revision != None: michael@0: plist['SCMRevision'] = scm_revision michael@0: elif add_keys: michael@0: print >>sys.stderr, 'Could not determine SCM revision. This may be OK.' michael@0: michael@0: if scm_path != None: michael@0: plist['SCMPath'] = scm_path michael@0: else: michael@0: _RemoveKeys(plist, 'SCMPath') michael@0: michael@0: michael@0: def _DoPDFKeys(plist, add_keys): michael@0: """Adds PDF support to the document types list. If add_keys is True, it will michael@0: add the type information dictionary. If it is False, it will remove it if michael@0: present.""" michael@0: michael@0: PDF_FILE_EXTENSION = 'pdf' michael@0: michael@0: def __AddPDFKeys(sub_plist): michael@0: """Writes the keys into a sub-dictionary of the plist.""" michael@0: sub_plist['CFBundleTypeExtensions'] = [PDF_FILE_EXTENSION] michael@0: sub_plist['CFBundleTypeIconFile'] = 'document.icns' michael@0: sub_plist['CFBundleTypeMIMETypes'] = 'application/pdf' michael@0: sub_plist['CFBundleTypeName'] = 'PDF Document' michael@0: sub_plist['CFBundleTypeRole'] = 'Viewer' michael@0: michael@0: DOCUMENT_TYPES_KEY = 'CFBundleDocumentTypes' michael@0: michael@0: # First get the list of document types, creating it if necessary. michael@0: try: michael@0: extensions = plist[DOCUMENT_TYPES_KEY] michael@0: except KeyError: michael@0: # If this plist doesn't have a type dictionary, create one if set to add the michael@0: # keys. If not, bail. michael@0: if not add_keys: michael@0: return michael@0: extensions = plist[DOCUMENT_TYPES_KEY] = [] michael@0: michael@0: # Loop over each entry in the list, looking for one that handles PDF types. michael@0: for i, ext in enumerate(extensions): michael@0: # If an entry for .pdf files is found... michael@0: if 'CFBundleTypeExtensions' not in ext: michael@0: continue michael@0: if PDF_FILE_EXTENSION in ext['CFBundleTypeExtensions']: michael@0: if add_keys: michael@0: # Overwrite the existing keys with new ones. michael@0: __AddPDFKeys(ext) michael@0: else: michael@0: # Otherwise, delete the entry entirely. michael@0: del extensions[i] michael@0: return michael@0: michael@0: # No PDF entry exists. If one needs to be added, do so now. michael@0: if add_keys: michael@0: pdf_entry = {} michael@0: __AddPDFKeys(pdf_entry) michael@0: extensions.append(pdf_entry) michael@0: michael@0: michael@0: def _AddBreakpadKeys(plist, branding): michael@0: """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and michael@0: also requires the |branding| argument.""" michael@0: plist['BreakpadReportInterval'] = '3600' # Deliberately a string. michael@0: plist['BreakpadProduct'] = '%s_Mac' % branding michael@0: plist['BreakpadProductDisplay'] = branding michael@0: plist['BreakpadVersion'] = plist['CFBundleShortVersionString'] michael@0: # These are both deliberately strings and not boolean. michael@0: plist['BreakpadSendAndExit'] = 'YES' michael@0: plist['BreakpadSkipConfirm'] = 'YES' michael@0: michael@0: michael@0: def _RemoveBreakpadKeys(plist): michael@0: """Removes any set Breakpad keys.""" michael@0: _RemoveKeys(plist, michael@0: 'BreakpadURL', michael@0: 'BreakpadReportInterval', michael@0: 'BreakpadProduct', michael@0: 'BreakpadProductDisplay', michael@0: 'BreakpadVersion', michael@0: 'BreakpadSendAndExit', michael@0: 'BreakpadSkipConfirm') michael@0: michael@0: michael@0: def _AddKeystoneKeys(plist, bundle_identifier): michael@0: """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and michael@0: also requires the |bundle_identifier| argument (com.example.product).""" michael@0: plist['KSVersion'] = plist['CFBundleShortVersionString'] michael@0: plist['KSProductID'] = bundle_identifier michael@0: plist['KSUpdateURL'] = 'https://tools.google.com/service/update2' michael@0: michael@0: michael@0: def _RemoveKeystoneKeys(plist): michael@0: """Removes any set Keystone keys.""" michael@0: _RemoveKeys(plist, michael@0: 'KSVersion', michael@0: 'KSProductID', michael@0: 'KSUpdateURL') michael@0: michael@0: michael@0: def Main(argv): michael@0: parser = optparse.OptionParser('%prog [options]') michael@0: parser.add_option('--breakpad', dest='use_breakpad', action='store', michael@0: type='int', default=False, help='Enable Breakpad [1 or 0]') michael@0: parser.add_option('--breakpad_uploads', dest='breakpad_uploads', michael@0: action='store', type='int', default=False, michael@0: help='Enable Breakpad\'s uploading of crash dumps [1 or 0]') michael@0: parser.add_option('--keystone', dest='use_keystone', action='store', michael@0: type='int', default=False, help='Enable Keystone [1 or 0]') michael@0: parser.add_option('--scm', dest='add_scm_info', action='store', type='int', michael@0: default=True, help='Add SCM metadata [1 or 0]') michael@0: parser.add_option('--pdf', dest='add_pdf_support', action='store', type='int', michael@0: default=False, help='Add PDF file handler support [1 or 0]') michael@0: parser.add_option('--branding', dest='branding', action='store', michael@0: type='string', default=None, help='The branding of the binary') michael@0: parser.add_option('--bundle_id', dest='bundle_identifier', michael@0: action='store', type='string', default=None, michael@0: help='The bundle id of the binary') michael@0: (options, args) = parser.parse_args(argv) michael@0: michael@0: if len(args) > 0: michael@0: print >>sys.stderr, parser.get_usage() michael@0: return 1 michael@0: michael@0: # Read the plist into its parsed format. michael@0: DEST_INFO_PLIST = os.path.join(env['TARGET_BUILD_DIR'], env['INFOPLIST_PATH']) michael@0: plist = plistlib.readPlist(DEST_INFO_PLIST) michael@0: michael@0: # Insert the product version. michael@0: if not _AddVersionKeys(plist): michael@0: return 2 michael@0: michael@0: # Add Breakpad if configured to do so. michael@0: if options.use_breakpad: michael@0: if options.branding is None: michael@0: print >>sys.stderr, 'Use of Breakpad requires branding.' michael@0: return 1 michael@0: _AddBreakpadKeys(plist, options.branding) michael@0: if options.breakpad_uploads: michael@0: plist['BreakpadURL'] = 'https://clients2.google.com/cr/report' michael@0: else: michael@0: # This allows crash dumping to a file without uploading the michael@0: # dump, for testing purposes. Breakpad does not recognise michael@0: # "none" as a special value, but this does stop crash dump michael@0: # uploading from happening. We need to specify something michael@0: # because if "BreakpadURL" is not present, Breakpad will not michael@0: # register its crash handler and no crash dumping will occur. michael@0: plist['BreakpadURL'] = 'none' michael@0: else: michael@0: _RemoveBreakpadKeys(plist) michael@0: michael@0: # Only add Keystone in Release builds. michael@0: if options.use_keystone and env['CONFIGURATION'] == 'Release': michael@0: if options.bundle_identifier is None: michael@0: print >>sys.stderr, 'Use of Keystone requires the bundle id.' michael@0: return 1 michael@0: _AddKeystoneKeys(plist, options.bundle_identifier) michael@0: else: michael@0: _RemoveKeystoneKeys(plist) michael@0: michael@0: # Adds or removes any SCM keys. michael@0: _DoSCMKeys(plist, options.add_scm_info) michael@0: michael@0: # Adds or removes the PDF file handler entry. michael@0: _DoPDFKeys(plist, options.add_pdf_support) michael@0: michael@0: # Now that all keys have been mutated, rewrite the file. michael@0: temp_info_plist = tempfile.NamedTemporaryFile() michael@0: plistlib.writePlist(plist, temp_info_plist.name) michael@0: michael@0: # Info.plist will work perfectly well in any plist format, but traditionally michael@0: # applications use xml1 for this, so convert it to ensure that it's valid. michael@0: proc = subprocess.Popen(['plutil', '-convert', 'xml1', '-o', DEST_INFO_PLIST, michael@0: temp_info_plist.name]) michael@0: proc.wait() michael@0: return proc.returncode michael@0: michael@0: michael@0: if __name__ == '__main__': michael@0: sys.exit(Main(sys.argv[1:]))