tweak_info_plist.py 16 KB

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