1#!/usr/bin/env python3 2 3# Copyright 2012 The Chromium Authors 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 24import optparse 25import os 26import plistlib 27import re 28import subprocess 29import sys 30import tempfile 31 32TOP = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 33 34 35def _ConvertPlist(source_plist, output_plist, fmt): 36 """Convert |source_plist| to |fmt| and save as |output_plist|.""" 37 assert sys.version_info.major == 2, "Use plistlib directly in Python 3" 38 return subprocess.call( 39 ['plutil', '-convert', fmt, '-o', output_plist, source_plist]) 40 41 42def _GetOutput(args): 43 """Runs a subprocess and waits for termination. Returns (stdout, returncode) 44 of the process. stderr is attached to the parent.""" 45 proc = subprocess.Popen(args, stdout=subprocess.PIPE) 46 stdout, _ = proc.communicate() 47 return stdout.decode('UTF-8'), proc.returncode 48 49 50def _RemoveKeys(plist, *keys): 51 """Removes a varargs of keys from the plist.""" 52 for key in keys: 53 try: 54 del plist[key] 55 except KeyError: 56 pass 57 58 59def _ApplyVersionOverrides(version, keys, overrides, separator='.'): 60 """Applies version overrides. 61 62 Given a |version| string as "a.b.c.d" (assuming a default separator) with 63 version components named by |keys| then overrides any value that is present 64 in |overrides|. 65 66 >>> _ApplyVersionOverrides('a.b', ['major', 'minor'], {'minor': 'd'}) 67 'a.d' 68 """ 69 if not overrides: 70 return version 71 version_values = version.split(separator) 72 for i, (key, value) in enumerate(zip(keys, version_values)): 73 if key in overrides: 74 version_values[i] = overrides[key] 75 return separator.join(version_values) 76 77 78def _GetVersion(version_format, values, overrides=None): 79 """Generates a version number according to |version_format| using the values 80 from |values| or |overrides| if given.""" 81 result = version_format 82 for key in values: 83 if overrides and key in overrides: 84 value = overrides[key] 85 else: 86 value = values[key] 87 result = result.replace('@%s@' % key, value) 88 return result 89 90 91def _AddVersionKeys(plist, version_format_for_key, version=None, 92 overrides=None): 93 """Adds the product version number into the plist. Returns True on success and 94 False on error. The error will be printed to stderr.""" 95 if not version: 96 # Pull in the Chrome version number. 97 VERSION_TOOL = os.path.join(TOP, 'build/util/version.py') 98 VERSION_FILE = os.path.join(TOP, 'chrome/VERSION') 99 (stdout, retval) = _GetOutput([ 100 VERSION_TOOL, '-f', VERSION_FILE, '-t', 101 '@MAJOR@.@MINOR@.@BUILD@.@PATCH@' 102 ]) 103 104 # If the command finished with a non-zero return code, then report the 105 # error up. 106 if retval != 0: 107 return False 108 109 version = stdout.strip() 110 111 # Parse the given version number, that should be in MAJOR.MINOR.BUILD.PATCH 112 # format (where each value is a number). Note that str.isdigit() returns 113 # True if the string is composed only of digits (and thus match \d+ regexp). 114 groups = version.split('.') 115 if len(groups) != 4 or not all(element.isdigit() for element in groups): 116 print('Invalid version string specified: "%s"' % version, file=sys.stderr) 117 return False 118 values = dict(zip(('MAJOR', 'MINOR', 'BUILD', 'PATCH'), groups)) 119 120 for key in version_format_for_key: 121 plist[key] = _GetVersion(version_format_for_key[key], values, overrides) 122 123 # Return with no error. 124 return True 125 126 127def _DoSCMKeys(plist, add_keys): 128 """Adds the SCM information, visible in about:version, to property list. If 129 |add_keys| is True, it will insert the keys, otherwise it will remove them.""" 130 scm_revision = None 131 if add_keys: 132 # Pull in the Chrome revision number. 133 VERSION_TOOL = os.path.join(TOP, 'build/util/version.py') 134 LASTCHANGE_FILE = os.path.join(TOP, 'build/util/LASTCHANGE') 135 (stdout, retval) = _GetOutput( 136 [VERSION_TOOL, '-f', LASTCHANGE_FILE, '-t', '@LASTCHANGE@']) 137 if retval: 138 return False 139 scm_revision = stdout.rstrip() 140 141 # See if the operation failed. 142 _RemoveKeys(plist, 'SCMRevision') 143 if scm_revision != None: 144 plist['SCMRevision'] = scm_revision 145 elif add_keys: 146 print('Could not determine SCM revision. This may be OK.', file=sys.stderr) 147 148 return True 149 150 151def _AddBreakpadKeys(plist, branding, platform, staging): 152 """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and 153 also requires the |branding| argument.""" 154 plist['BreakpadReportInterval'] = '3600' # Deliberately a string. 155 plist['BreakpadProduct'] = '%s_%s' % (branding, platform) 156 plist['BreakpadProductDisplay'] = branding 157 if staging: 158 plist['BreakpadURL'] = 'https://clients2.google.com/cr/staging_report' 159 else: 160 plist['BreakpadURL'] = 'https://clients2.google.com/cr/report' 161 162 # These are both deliberately strings and not boolean. 163 plist['BreakpadSendAndExit'] = 'YES' 164 plist['BreakpadSkipConfirm'] = 'YES' 165 166 167def _RemoveBreakpadKeys(plist): 168 """Removes any set Breakpad keys.""" 169 _RemoveKeys(plist, 'BreakpadURL', 'BreakpadReportInterval', 'BreakpadProduct', 170 'BreakpadProductDisplay', 'BreakpadVersion', 171 'BreakpadSendAndExit', 'BreakpadSkipConfirm') 172 173 174def _TagSuffixes(): 175 # Keep this list sorted in the order that tag suffix components are to 176 # appear in a tag value. That is to say, it should be sorted per ASCII. 177 components = ('full', ) 178 assert tuple(sorted(components)) == components 179 180 components_len = len(components) 181 combinations = 1 << components_len 182 tag_suffixes = [] 183 for combination in range(0, combinations): 184 tag_suffix = '' 185 for component_index in range(0, components_len): 186 if combination & (1 << component_index): 187 tag_suffix += '-' + components[component_index] 188 tag_suffixes.append(tag_suffix) 189 return tag_suffixes 190 191 192def _AddKeystoneKeys(plist, bundle_identifier, base_tag): 193 """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and 194 also requires the |bundle_identifier| argument (com.example.product).""" 195 plist['KSVersion'] = plist['CFBundleShortVersionString'] 196 plist['KSProductID'] = bundle_identifier 197 plist['KSUpdateURL'] = 'https://tools.google.com/service/update2' 198 199 _RemoveKeys(plist, 'KSChannelID') 200 if base_tag != '': 201 plist['KSChannelID'] = base_tag 202 for tag_suffix in _TagSuffixes(): 203 if tag_suffix: 204 plist['KSChannelID' + tag_suffix] = base_tag + tag_suffix 205 206 207def _RemoveKeystoneKeys(plist): 208 """Removes any set Keystone keys.""" 209 _RemoveKeys(plist, 'KSVersion', 'KSProductID', 'KSUpdateURL') 210 211 tag_keys = ['KSChannelID'] 212 for tag_suffix in _TagSuffixes(): 213 tag_keys.append('KSChannelID' + tag_suffix) 214 _RemoveKeys(plist, *tag_keys) 215 216 217def _AddGTMKeys(plist, platform): 218 """Adds the GTM metadata keys. This must be called AFTER _AddVersionKeys().""" 219 plist['GTMUserAgentID'] = plist['CFBundleName'] 220 if platform == 'ios': 221 plist['GTMUserAgentVersion'] = plist['CFBundleVersion'] 222 else: 223 plist['GTMUserAgentVersion'] = plist['CFBundleShortVersionString'] 224 225 226def _RemoveGTMKeys(plist): 227 """Removes any set GTM metadata keys.""" 228 _RemoveKeys(plist, 'GTMUserAgentID', 'GTMUserAgentVersion') 229 230 231def _AddPrivilegedHelperId(plist, privileged_helper_id): 232 plist['SMPrivilegedExecutables'] = { 233 privileged_helper_id: 'identifier ' + privileged_helper_id 234 } 235 236 237def _RemovePrivilegedHelperId(plist): 238 _RemoveKeys(plist, 'SMPrivilegedExecutables') 239 240 241def Main(argv): 242 parser = optparse.OptionParser('%prog [options]') 243 parser.add_option('--plist', 244 dest='plist_path', 245 action='store', 246 type='string', 247 default=None, 248 help='The path of the plist to tweak.') 249 parser.add_option('--output', dest='plist_output', action='store', 250 type='string', default=None, help='If specified, the path to output ' + \ 251 'the tweaked plist, rather than overwriting the input.') 252 parser.add_option('--breakpad', 253 dest='use_breakpad', 254 action='store', 255 type='int', 256 default=False, 257 help='Enable Breakpad [1 or 0]') 258 parser.add_option( 259 '--breakpad_staging', 260 dest='use_breakpad_staging', 261 action='store_true', 262 default=False, 263 help='Use staging breakpad to upload reports. Ignored if --breakpad=0.') 264 parser.add_option('--keystone', 265 dest='use_keystone', 266 action='store', 267 type='int', 268 default=False, 269 help='Enable Keystone [1 or 0]') 270 parser.add_option('--keystone-base-tag', 271 default='', 272 help='Base Keystone tag to set') 273 parser.add_option('--scm', 274 dest='add_scm_info', 275 action='store', 276 type='int', 277 default=True, 278 help='Add SCM metadata [1 or 0]') 279 parser.add_option('--branding', 280 dest='branding', 281 action='store', 282 type='string', 283 default=None, 284 help='The branding of the binary') 285 parser.add_option('--bundle_id', 286 dest='bundle_identifier', 287 action='store', 288 type='string', 289 default=None, 290 help='The bundle id of the binary') 291 parser.add_option('--platform', 292 choices=('ios', 'mac'), 293 default='mac', 294 help='The target platform of the bundle') 295 parser.add_option('--add-gtm-metadata', 296 dest='add_gtm_info', 297 action='store', 298 type='int', 299 default=False, 300 help='Add GTM metadata [1 or 0]') 301 parser.add_option( 302 '--version-overrides', 303 action='append', 304 help='Key-value pair to override specific component of version ' 305 'like key=value (can be passed multiple time to configure ' 306 'more than one override)') 307 parser.add_option('--format', 308 choices=('binary1', 'xml1'), 309 default='xml1', 310 help='Format to use when writing property list ' 311 '(default: %(default)s)') 312 parser.add_option('--version', 313 dest='version', 314 action='store', 315 type='string', 316 default=None, 317 help='The version string [major.minor.build.patch]') 318 parser.add_option('--privileged_helper_id', 319 dest='privileged_helper_id', 320 action='store', 321 type='string', 322 default=None, 323 help='The id of the privileged helper executable.') 324 (options, args) = parser.parse_args(argv) 325 326 if len(args) > 0: 327 print(parser.get_usage(), file=sys.stderr) 328 return 1 329 330 if not options.plist_path: 331 print('No --plist specified.', file=sys.stderr) 332 return 1 333 334 # Read the plist into its parsed format. Convert the file to 'xml1' as 335 # plistlib only supports that format in Python 2.7. 336 with tempfile.NamedTemporaryFile() as temp_info_plist: 337 if sys.version_info.major == 2: 338 retcode = _ConvertPlist(options.plist_path, temp_info_plist.name, 'xml1') 339 if retcode != 0: 340 return retcode 341 plist = plistlib.readPlist(temp_info_plist.name) 342 else: 343 with open(options.plist_path, 'rb') as f: 344 plist = plistlib.load(f) 345 346 # Convert overrides. 347 overrides = {} 348 if options.version_overrides: 349 for pair in options.version_overrides: 350 if not '=' in pair: 351 print('Invalid value for --version-overrides:', pair, file=sys.stderr) 352 return 1 353 key, value = pair.split('=', 1) 354 overrides[key] = value 355 if key not in ('MAJOR', 'MINOR', 'BUILD', 'PATCH'): 356 print('Unsupported key for --version-overrides:', key, file=sys.stderr) 357 return 1 358 359 if options.platform == 'mac': 360 version_format_for_key = { 361 # Add public version info so "Get Info" works. 362 'CFBundleShortVersionString': '@MAJOR@.@MINOR@.@BUILD@.@PATCH@', 363 364 # Honor the 429496.72.95 limit. The maximum comes from splitting 365 # 2^32 - 1 into 6, 2, 2 digits. The limitation was present in Tiger, 366 # but it could have been fixed in later OS release, but hasn't been 367 # tested (it's easy enough to find out with "lsregister -dump). 368 # http://lists.apple.com/archives/carbon-dev/2006/Jun/msg00139.html 369 # BUILD will always be an increasing value, so BUILD_PATH gives us 370 # something unique that meetings what LS wants. 371 'CFBundleVersion': '@BUILD@.@PATCH@', 372 } 373 else: 374 version_format_for_key = { 375 'CFBundleShortVersionString': '@MAJOR@.@BUILD@.@PATCH@', 376 'CFBundleVersion': '@MAJOR@.@MINOR@.@BUILD@.@PATCH@' 377 } 378 379 if options.use_breakpad: 380 version_format_for_key['BreakpadVersion'] = \ 381 '@MAJOR@.@MINOR@.@BUILD@.@PATCH@' 382 383 # Insert the product version. 384 if not _AddVersionKeys(plist, 385 version_format_for_key, 386 version=options.version, 387 overrides=overrides): 388 return 2 389 390 # Add Breakpad if configured to do so. 391 if options.use_breakpad: 392 if options.branding is None: 393 print('Use of Breakpad requires branding.', file=sys.stderr) 394 return 1 395 # Map "target_os" passed from gn via the --platform parameter 396 # to the platform as known by breakpad. 397 platform = {'mac': 'Mac', 'ios': 'iOS'}[options.platform] 398 _AddBreakpadKeys(plist, options.branding, platform, 399 options.use_breakpad_staging) 400 else: 401 _RemoveBreakpadKeys(plist) 402 403 # Add Keystone if configured to do so. 404 if options.use_keystone: 405 if options.bundle_identifier is None: 406 print('Use of Keystone requires the bundle id.', file=sys.stderr) 407 return 1 408 _AddKeystoneKeys(plist, options.bundle_identifier, 409 options.keystone_base_tag) 410 else: 411 _RemoveKeystoneKeys(plist) 412 413 # Adds or removes any SCM keys. 414 if not _DoSCMKeys(plist, options.add_scm_info): 415 return 3 416 417 # Add GTM metadata keys. 418 if options.add_gtm_info: 419 _AddGTMKeys(plist, options.platform) 420 else: 421 _RemoveGTMKeys(plist) 422 423 # Add SMPrivilegedExecutables keys. 424 if options.privileged_helper_id: 425 _AddPrivilegedHelperId(plist, options.privileged_helper_id) 426 else: 427 _RemovePrivilegedHelperId(plist) 428 429 output_path = options.plist_path 430 if options.plist_output is not None: 431 output_path = options.plist_output 432 433 # Now that all keys have been mutated, rewrite the file. 434 # Convert Info.plist to the format requested by the --format flag. Any 435 # format would work on Mac but iOS requires specific format. 436 if sys.version_info.major == 2: 437 with tempfile.NamedTemporaryFile() as temp_info_plist: 438 plistlib.writePlist(plist, temp_info_plist.name) 439 return _ConvertPlist(temp_info_plist.name, output_path, options.format) 440 with open(output_path, 'wb') as f: 441 plist_format = {'binary1': plistlib.FMT_BINARY, 'xml1': plistlib.FMT_XML} 442 plistlib.dump(plist, f, fmt=plist_format[options.format]) 443 444 445if __name__ == '__main__': 446 # TODO(https://crbug.com/941669): Temporary workaround until all scripts use 447 # python3 by default. 448 if sys.version_info[0] < 3: 449 os.execvp('python3', ['python3'] + sys.argv) 450 sys.exit(Main(sys.argv[1:])) 451