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