summaryrefslogtreecommitdiffstats
path: root/chromium/build/mac/tweak_info_plist.py
blob: 0f65e4aed2016118fe1bb1f288a5dcbd98bfe353 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/env python

# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

#
# Xcode supports build variable substitutions and CPP; sadly, that doesn't work
# because:
#
# 1. Xcode wants to do the Info.plist work before it runs any build phases,
#    this means if we were to generate a .h file for INFOPLIST_PREFIX_HEADER
#    we'd have to put it in another target so it runs in time.
# 2. Xcode also doesn't check to see if the header being used as a prefix for
#    the Info.plist has changed.  So even if we updated it, it's only looking
#    at the modtime of the info.plist to see if that's changed.
#
# So, we work around all of this by making a script build phase that will run
# during the app build, and simply update the info.plist in place.  This way
# by the time the app target is done, the info.plist is correct.
#

import optparse
import os
from os import environ as env
import plistlib
import re
import subprocess
import sys
import tempfile

TOP = os.path.join(env['SRCROOT'], '..')


def _GetOutput(args):
  """Runs a subprocess and waits for termination. Returns (stdout, returncode)
  of the process. stderr is attached to the parent."""
  proc = subprocess.Popen(args, stdout=subprocess.PIPE)
  (stdout, stderr) = proc.communicate()
  return (stdout, proc.returncode)


def _GetOutputNoError(args):
  """Similar to _GetOutput() but ignores stderr. If there's an error launching
  the child (like file not found), the exception will be caught and (None, 1)
  will be returned to mimic quiet failure."""
  try:
    proc = subprocess.Popen(args, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
  except OSError:
    return (None, 1)
  (stdout, stderr) = proc.communicate()
  return (stdout, proc.returncode)


def _RemoveKeys(plist, *keys):
  """Removes a varargs of keys from the plist."""
  for key in keys:
    try:
      del plist[key]
    except KeyError:
      pass


def _AddVersionKeys(plist, version=None):
  """Adds the product version number into the plist. Returns True on success and
  False on error. The error will be printed to stderr."""
  if version:
    match = re.match('\d+\.\d+\.(\d+\.\d+)$', version)
    if not match:
      print >>sys.stderr, 'Invalid version string specified: "%s"' % version
      return False

    full_version = match.group(0)
    bundle_version = match.group(1)

  else:
    # Pull in the Chrome version number.
    VERSION_TOOL = os.path.join(TOP, 'chrome/tools/build/version.py')
    VERSION_FILE = os.path.join(TOP, 'chrome/VERSION')

    (stdout, retval1) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t',
                                    '@MAJOR@.@MINOR@.@BUILD@.@PATCH@'])
    full_version = stdout.rstrip()

    (stdout, retval2) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t',
                                    '@BUILD@.@PATCH@'])
    bundle_version = stdout.rstrip()

    # If either of the two version commands finished with non-zero returncode,
    # report the error up.
    if retval1 or retval2:
      return False

  # Add public version info so "Get Info" works.
  plist['CFBundleShortVersionString'] = full_version

  # Honor the 429496.72.95 limit.  The maximum comes from splitting 2^32 - 1
  # into  6, 2, 2 digits.  The limitation was present in Tiger, but it could
  # have been fixed in later OS release, but hasn't been tested (it's easy
  # enough to find out with "lsregister -dump).
  # http://lists.apple.com/archives/carbon-dev/2006/Jun/msg00139.html
  # BUILD will always be an increasing value, so BUILD_PATH gives us something
  # unique that meetings what LS wants.
  plist['CFBundleVersion'] = bundle_version

  # Return with no error.
  return True


def _DoSCMKeys(plist, add_keys):
  """Adds the SCM information, visible in about:version, to property list. If
  |add_keys| is True, it will insert the keys, otherwise it will remove them."""
  scm_revision = None
  if add_keys:
    # Pull in the Chrome revision number.
    VERSION_TOOL = os.path.join(TOP, 'chrome/tools/build/version.py')
    LASTCHANGE_FILE = os.path.join(TOP, 'build/util/LASTCHANGE')
    (stdout, retval) = _GetOutput([VERSION_TOOL, '-f', LASTCHANGE_FILE, '-t',
                                  '@LASTCHANGE@'])
    if retval:
      return False
    scm_revision = stdout.rstrip()

  # See if the operation failed.
  _RemoveKeys(plist, 'SCMRevision')
  if scm_revision != None:
    plist['SCMRevision'] = scm_revision
  elif add_keys:
    print >>sys.stderr, 'Could not determine SCM revision.  This may be OK.'

  return True


def _DoPDFKeys(plist, add_keys):
  """Adds PDF support to the document types list. If add_keys is True, it will
  add the type information dictionary. If it is False, it will remove it if
  present."""

  PDF_FILE_EXTENSION = 'pdf'

  def __AddPDFKeys(sub_plist):
    """Writes the keys into a sub-dictionary of the plist."""
    sub_plist['CFBundleTypeExtensions'] = [PDF_FILE_EXTENSION]
    sub_plist['CFBundleTypeIconFile'] = 'document.icns'
    sub_plist['CFBundleTypeMIMETypes'] = 'application/pdf'
    sub_plist['CFBundleTypeName'] = 'PDF Document'
    sub_plist['CFBundleTypeRole'] = 'Viewer'

  DOCUMENT_TYPES_KEY = 'CFBundleDocumentTypes'

  # First get the list of document types, creating it if necessary.
  try:
    extensions = plist[DOCUMENT_TYPES_KEY]
  except KeyError:
    # If this plist doesn't have a type dictionary, create one if set to add the
    # keys. If not, bail.
    if not add_keys:
      return
    extensions = plist[DOCUMENT_TYPES_KEY] = []

  # Loop over each entry in the list, looking for one that handles PDF types.
  for i, ext in enumerate(extensions):
    # If an entry for .pdf files is found...
    if 'CFBundleTypeExtensions' not in ext:
      continue
    if PDF_FILE_EXTENSION in ext['CFBundleTypeExtensions']:
      if add_keys:
        # Overwrite the existing keys with new ones.
        __AddPDFKeys(ext)
      else:
        # Otherwise, delete the entry entirely.
        del extensions[i]
      return

  # No PDF entry exists. If one needs to be added, do so now.
  if add_keys:
    pdf_entry = {}
    __AddPDFKeys(pdf_entry)
    extensions.append(pdf_entry)


def _AddBreakpadKeys(plist, branding):
  """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and
  also requires the |branding| argument."""
  plist['BreakpadReportInterval'] = '3600'  # Deliberately a string.
  plist['BreakpadProduct'] = '%s_Mac' % branding
  plist['BreakpadProductDisplay'] = branding
  plist['BreakpadVersion'] = plist['CFBundleShortVersionString']
  # These are both deliberately strings and not boolean.
  plist['BreakpadSendAndExit'] = 'YES'
  plist['BreakpadSkipConfirm'] = 'YES'


def _RemoveBreakpadKeys(plist):
  """Removes any set Breakpad keys."""
  _RemoveKeys(plist,
      'BreakpadURL',
      'BreakpadReportInterval',
      'BreakpadProduct',
      'BreakpadProductDisplay',
      'BreakpadVersion',
      'BreakpadSendAndExit',
      'BreakpadSkipConfirm')


def _TagSuffixes():
  # Keep this list sorted in the order that tag suffix components are to
  # appear in a tag value. That is to say, it should be sorted per ASCII.
  components = ('32bit', 'full')
  assert tuple(sorted(components)) == components

  components_len = len(components)
  combinations = 1 << components_len
  tag_suffixes = []
  for combination in xrange(0, combinations):
    tag_suffix = ''
    for component_index in xrange(0, components_len):
      if combination & (1 << component_index):
        tag_suffix += '-' + components[component_index]
    tag_suffixes.append(tag_suffix)
  return tag_suffixes


def _AddKeystoneKeys(plist, bundle_identifier):
  """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and
  also requires the |bundle_identifier| argument (com.example.product)."""
  plist['KSVersion'] = plist['CFBundleShortVersionString']
  plist['KSProductID'] = bundle_identifier
  plist['KSUpdateURL'] = 'https://tools.google.com/service/update2'

  _RemoveKeys(plist, 'KSChannelID')
  for tag_suffix in _TagSuffixes():
    if tag_suffix:
      plist['KSChannelID' + tag_suffix] = tag_suffix


def _RemoveKeystoneKeys(plist):
  """Removes any set Keystone keys."""
  _RemoveKeys(plist,
      'KSVersion',
      'KSProductID',
      'KSUpdateURL')

  tag_keys = []
  for tag_suffix in _TagSuffixes():
    tag_keys.append('KSChannelID' + tag_suffix)
  _RemoveKeys(plist, *tag_keys)


def Main(argv):
  parser = optparse.OptionParser('%prog [options]')
  parser.add_option('--breakpad', dest='use_breakpad', action='store',
      type='int', default=False, help='Enable Breakpad [1 or 0]')
  parser.add_option('--breakpad_uploads', dest='breakpad_uploads',
      action='store', type='int', default=False,
      help='Enable Breakpad\'s uploading of crash dumps [1 or 0]')
  parser.add_option('--keystone', dest='use_keystone', action='store',
      type='int', default=False, help='Enable Keystone [1 or 0]')
  parser.add_option('--scm', dest='add_scm_info', action='store', type='int',
      default=True, help='Add SCM metadata [1 or 0]')
  parser.add_option('--pdf', dest='add_pdf_support', action='store', type='int',
      default=False, help='Add PDF file handler support [1 or 0]')
  parser.add_option('--branding', dest='branding', action='store',
      type='string', default=None, help='The branding of the binary')
  parser.add_option('--bundle_id', dest='bundle_identifier',
      action='store', type='string', default=None,
      help='The bundle id of the binary')
  parser.add_option('--version', dest='version', action='store', type='string',
      default=None, help='The version string [major.minor.build.patch]')
  (options, args) = parser.parse_args(argv)

  if len(args) > 0:
    print >>sys.stderr, parser.get_usage()
    return 1

  # Read the plist into its parsed format.
  DEST_INFO_PLIST = os.path.join(env['TARGET_BUILD_DIR'], env['INFOPLIST_PATH'])
  plist = plistlib.readPlist(DEST_INFO_PLIST)

  # Insert the product version.
  if not _AddVersionKeys(plist, version=options.version):
    return 2

  # Add Breakpad if configured to do so.
  if options.use_breakpad:
    if options.branding is None:
      print >>sys.stderr, 'Use of Breakpad requires branding.'
      return 1
    _AddBreakpadKeys(plist, options.branding)
    if options.breakpad_uploads:
      plist['BreakpadURL'] = 'https://clients2.google.com/cr/report'
    else:
      # This allows crash dumping to a file without uploading the
      # dump, for testing purposes.  Breakpad does not recognise
      # "none" as a special value, but this does stop crash dump
      # uploading from happening.  We need to specify something
      # because if "BreakpadURL" is not present, Breakpad will not
      # register its crash handler and no crash dumping will occur.
      plist['BreakpadURL'] = 'none'
  else:
    _RemoveBreakpadKeys(plist)

  # Only add Keystone in Release builds.
  if options.use_keystone and env['CONFIGURATION'] == 'Release':
    if options.bundle_identifier is None:
      print >>sys.stderr, 'Use of Keystone requires the bundle id.'
      return 1
    _AddKeystoneKeys(plist, options.bundle_identifier)
  else:
    _RemoveKeystoneKeys(plist)

  # Adds or removes any SCM keys.
  if not _DoSCMKeys(plist, options.add_scm_info):
    return 3

  # Adds or removes the PDF file handler entry.
  _DoPDFKeys(plist, options.add_pdf_support)

  # Now that all keys have been mutated, rewrite the file.
  temp_info_plist = tempfile.NamedTemporaryFile()
  plistlib.writePlist(plist, temp_info_plist.name)

  # Info.plist will work perfectly well in any plist format, but traditionally
  # applications use xml1 for this, so convert it to ensure that it's valid.
  proc = subprocess.Popen(['plutil', '-convert', 'xml1', '-o', DEST_INFO_PLIST,
                           temp_info_plist.name])
  proc.wait()
  return proc.returncode


if __name__ == '__main__':
  sys.exit(Main(sys.argv[1:]))