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