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