• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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