OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 | 2 |
3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 # | 7 # |
8 # Xcode supports build variable substitutions and CPP; sadly, that doesn't work | 8 # Xcode supports build variable substitutions and CPP; sadly, that doesn't work |
9 # because: | 9 # because: |
10 # | 10 # |
(...skipping 13 matching lines...) Expand all Loading... |
24 import os | 24 import os |
25 import plistlib | 25 import plistlib |
26 import re | 26 import re |
27 import subprocess | 27 import subprocess |
28 import sys | 28 import sys |
29 import tempfile | 29 import tempfile |
30 | 30 |
31 TOP = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) | 31 TOP = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) |
32 | 32 |
33 | 33 |
| 34 def _ConvertPlist(source_plist, output_plist, fmt): |
| 35 """Convert |source_plist| to |fmt| and save as |output_plist|.""" |
| 36 return subprocess.call( |
| 37 ['plutil', '-convert', fmt, '-o', output_plist, source_plist]) |
| 38 |
| 39 |
34 def _GetOutput(args): | 40 def _GetOutput(args): |
35 """Runs a subprocess and waits for termination. Returns (stdout, returncode) | 41 """Runs a subprocess and waits for termination. Returns (stdout, returncode) |
36 of the process. stderr is attached to the parent.""" | 42 of the process. stderr is attached to the parent.""" |
37 proc = subprocess.Popen(args, stdout=subprocess.PIPE) | 43 proc = subprocess.Popen(args, stdout=subprocess.PIPE) |
38 (stdout, stderr) = proc.communicate() | 44 (stdout, stderr) = proc.communicate() |
39 return (stdout, proc.returncode) | 45 return (stdout, proc.returncode) |
40 | 46 |
41 | 47 |
42 def _GetOutputNoError(args): | 48 def _GetOutputNoError(args): |
43 """Similar to _GetOutput() but ignores stderr. If there's an error launching | 49 """Similar to _GetOutput() but ignores stderr. If there's an error launching |
(...skipping 10 matching lines...) Expand all Loading... |
54 | 60 |
55 def _RemoveKeys(plist, *keys): | 61 def _RemoveKeys(plist, *keys): |
56 """Removes a varargs of keys from the plist.""" | 62 """Removes a varargs of keys from the plist.""" |
57 for key in keys: | 63 for key in keys: |
58 try: | 64 try: |
59 del plist[key] | 65 del plist[key] |
60 except KeyError: | 66 except KeyError: |
61 pass | 67 pass |
62 | 68 |
63 | 69 |
64 def _AddVersionKeys(plist, version=None): | 70 def _ApplyVersionOverrides(version, keys, overrides, separator='.'): |
| 71 """Applies version overrides. |
| 72 |
| 73 Given a |version| string as "a.b.c.d" (assuming a default separator) with |
| 74 version components named by |keys| then overrides any value that is present |
| 75 in |overrides|. |
| 76 |
| 77 >>> _ApplyVersionOverrides('a.b', ['major', 'minor'], {'minor': 'd'}) |
| 78 'a.d' |
| 79 """ |
| 80 if not overrides: |
| 81 return version |
| 82 version_values = version.split(separator) |
| 83 for i, (key, value) in enumerate(zip(keys, version_values)): |
| 84 if key in overrides: |
| 85 version_values[i] = overrides[key] |
| 86 return separator.join(version_values) |
| 87 |
| 88 |
| 89 def _AddVersionKeys(plist, version=None, overrides=None): |
65 """Adds the product version number into the plist. Returns True on success and | 90 """Adds the product version number into the plist. Returns True on success and |
66 False on error. The error will be printed to stderr.""" | 91 False on error. The error will be printed to stderr.""" |
67 if version: | 92 if version: |
68 match = re.match('\d+\.\d+\.(\d+\.\d+)$', version) | 93 match = re.match('\d+\.\d+\.(\d+\.\d+)$', version) |
69 if not match: | 94 if not match: |
70 print >>sys.stderr, 'Invalid version string specified: "%s"' % version | 95 print >>sys.stderr, 'Invalid version string specified: "%s"' % version |
71 return False | 96 return False |
72 | 97 |
73 full_version = match.group(0) | 98 full_version = match.group(0) |
74 bundle_version = match.group(1) | 99 bundle_version = match.group(1) |
75 | 100 |
76 else: | 101 else: |
77 # Pull in the Chrome version number. | 102 # Pull in the Chrome version number. |
78 VERSION_TOOL = os.path.join(TOP, 'build/util/version.py') | 103 VERSION_TOOL = os.path.join(TOP, 'build/util/version.py') |
79 VERSION_FILE = os.path.join(TOP, 'chrome/VERSION') | 104 VERSION_FILE = os.path.join(TOP, 'chrome/VERSION') |
80 | 105 |
81 (stdout, retval1) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t', | 106 (stdout, retval1) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t', |
82 '@MAJOR@.@MINOR@.@BUILD@.@PATCH@']) | 107 '@MAJOR@.@MINOR@.@BUILD@.@PATCH@']) |
83 full_version = stdout.rstrip() | 108 full_version = _ApplyVersionOverrides( |
| 109 stdout.rstrip(), ('MAJOR', 'MINOR', 'BUILD', 'PATCH'), overrides) |
84 | 110 |
85 (stdout, retval2) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t', | 111 (stdout, retval2) = _GetOutput([VERSION_TOOL, '-f', VERSION_FILE, '-t', |
86 '@BUILD@.@PATCH@']) | 112 '@BUILD@.@PATCH@']) |
87 bundle_version = stdout.rstrip() | 113 bundle_version = _ApplyVersionOverrides( |
| 114 stdout.rstrip(), ('BUILD', 'PATCH'), overrides) |
88 | 115 |
89 # If either of the two version commands finished with non-zero returncode, | 116 # If either of the two version commands finished with non-zero returncode, |
90 # report the error up. | 117 # report the error up. |
91 if retval1 or retval2: | 118 if retval1 or retval2: |
92 return False | 119 return False |
93 | 120 |
94 # Add public version info so "Get Info" works. | 121 # Add public version info so "Get Info" works. |
95 plist['CFBundleShortVersionString'] = full_version | 122 plist['CFBundleShortVersionString'] = full_version |
96 | 123 |
97 # Honor the 429496.72.95 limit. The maximum comes from splitting 2^32 - 1 | 124 # Honor the 429496.72.95 limit. The maximum comes from splitting 2^32 - 1 |
(...skipping 26 matching lines...) Expand all Loading... |
124 # See if the operation failed. | 151 # See if the operation failed. |
125 _RemoveKeys(plist, 'SCMRevision') | 152 _RemoveKeys(plist, 'SCMRevision') |
126 if scm_revision != None: | 153 if scm_revision != None: |
127 plist['SCMRevision'] = scm_revision | 154 plist['SCMRevision'] = scm_revision |
128 elif add_keys: | 155 elif add_keys: |
129 print >>sys.stderr, 'Could not determine SCM revision. This may be OK.' | 156 print >>sys.stderr, 'Could not determine SCM revision. This may be OK.' |
130 | 157 |
131 return True | 158 return True |
132 | 159 |
133 | 160 |
134 def _AddBreakpadKeys(plist, branding): | 161 def _AddBreakpadKeys(plist, branding, platform): |
135 """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and | 162 """Adds the Breakpad keys. This must be called AFTER _AddVersionKeys() and |
136 also requires the |branding| argument.""" | 163 also requires the |branding| argument.""" |
137 plist['BreakpadReportInterval'] = '3600' # Deliberately a string. | 164 plist['BreakpadReportInterval'] = '3600' # Deliberately a string. |
138 plist['BreakpadProduct'] = '%s_Mac' % branding | 165 plist['BreakpadProduct'] = '%s_%s' % (branding, platform) |
139 plist['BreakpadProductDisplay'] = branding | 166 plist['BreakpadProductDisplay'] = branding |
140 plist['BreakpadVersion'] = plist['CFBundleShortVersionString'] | 167 plist['BreakpadVersion'] = plist['CFBundleShortVersionString'] |
141 # These are both deliberately strings and not boolean. | 168 # These are both deliberately strings and not boolean. |
142 plist['BreakpadSendAndExit'] = 'YES' | 169 plist['BreakpadSendAndExit'] = 'YES' |
143 plist['BreakpadSkipConfirm'] = 'YES' | 170 plist['BreakpadSkipConfirm'] = 'YES' |
144 | 171 |
145 | 172 |
146 def _RemoveBreakpadKeys(plist): | 173 def _RemoveBreakpadKeys(plist): |
147 """Removes any set Breakpad keys.""" | 174 """Removes any set Breakpad keys.""" |
148 _RemoveKeys(plist, | 175 _RemoveKeys(plist, |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
213 help='Enable Breakpad\'s uploading of crash dumps [1 or 0]') | 240 help='Enable Breakpad\'s uploading of crash dumps [1 or 0]') |
214 parser.add_option('--keystone', dest='use_keystone', action='store', | 241 parser.add_option('--keystone', dest='use_keystone', action='store', |
215 type='int', default=False, help='Enable Keystone [1 or 0]') | 242 type='int', default=False, help='Enable Keystone [1 or 0]') |
216 parser.add_option('--scm', dest='add_scm_info', action='store', type='int', | 243 parser.add_option('--scm', dest='add_scm_info', action='store', type='int', |
217 default=True, help='Add SCM metadata [1 or 0]') | 244 default=True, help='Add SCM metadata [1 or 0]') |
218 parser.add_option('--branding', dest='branding', action='store', | 245 parser.add_option('--branding', dest='branding', action='store', |
219 type='string', default=None, help='The branding of the binary') | 246 type='string', default=None, help='The branding of the binary') |
220 parser.add_option('--bundle_id', dest='bundle_identifier', | 247 parser.add_option('--bundle_id', dest='bundle_identifier', |
221 action='store', type='string', default=None, | 248 action='store', type='string', default=None, |
222 help='The bundle id of the binary') | 249 help='The bundle id of the binary') |
| 250 parser.add_option('--platform', choices=('ios', 'mac'), default='mac', |
| 251 help='The target platform of the bundle') |
| 252 parser.add_option('--version-overrides', 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', choices=('binary1', 'xml1', 'json'), |
| 257 default='xml1', help='Format to use when writing property list ' |
| 258 '(default: %(default)s)') |
223 parser.add_option('--version', dest='version', action='store', type='string', | 259 parser.add_option('--version', dest='version', action='store', type='string', |
224 default=None, help='The version string [major.minor.build.patch]') | 260 default=None, help='The version string [major.minor.build.patch]') |
225 (options, args) = parser.parse_args(argv) | 261 (options, args) = parser.parse_args(argv) |
226 | 262 |
227 if len(args) > 0: | 263 if len(args) > 0: |
228 print >>sys.stderr, parser.get_usage() | 264 print >>sys.stderr, parser.get_usage() |
229 return 1 | 265 return 1 |
230 | 266 |
231 if not options.plist_path: | 267 if not options.plist_path: |
232 print >>sys.stderr, 'No --plist specified.' | 268 print >>sys.stderr, 'No --plist specified.' |
233 return 1 | 269 return 1 |
234 | 270 |
235 # Read the plist into its parsed format. | 271 # Read the plist into its parsed format. Convert the file to 'xml1' as |
236 plist = plistlib.readPlist(options.plist_path) | 272 # plistlib only supports that format in Python 2.7. |
| 273 with tempfile.NamedTemporaryFile() as temp_info_plist: |
| 274 retcode = _ConvertPlist(options.plist_path, temp_info_plist.name, 'xml1') |
| 275 if retcode != 0: |
| 276 return retcode |
| 277 plist = plistlib.readPlist(temp_info_plist.name) |
| 278 |
| 279 # Convert overrides. |
| 280 overrides = {} |
| 281 if options.version_overrides: |
| 282 for pair in options.version_overrides: |
| 283 if not '=' in pair: |
| 284 print >>sys.stderr, 'Invalid value for --version-overrides:', pair |
| 285 return 1 |
| 286 key, value = pair.split('=', 1) |
| 287 overrides[key] = value |
| 288 if key not in ('MAJOR', 'MINOR', 'BUILD', 'PATCH'): |
| 289 print >>sys.stderr, 'Unsupported key for --version-overrides:', key |
| 290 return 1 |
237 | 291 |
238 # Insert the product version. | 292 # Insert the product version. |
239 if not _AddVersionKeys(plist, version=options.version): | 293 if not _AddVersionKeys(plist, version=options.version, overrides=overrides): |
240 return 2 | 294 return 2 |
241 | 295 |
242 # Add Breakpad if configured to do so. | 296 # Add Breakpad if configured to do so. |
243 if options.use_breakpad: | 297 if options.use_breakpad: |
244 if options.branding is None: | 298 if options.branding is None: |
245 print >>sys.stderr, 'Use of Breakpad requires branding.' | 299 print >>sys.stderr, 'Use of Breakpad requires branding.' |
246 return 1 | 300 return 1 |
247 _AddBreakpadKeys(plist, options.branding) | 301 # Map gyp "OS" / gn "target_os" passed via the --platform parameter to |
| 302 # the platform as known by breakpad. |
| 303 platform = {'mac': 'Mac', 'ios': 'iOS'}[options.platform] |
| 304 _AddBreakpadKeys(plist, options.branding, platform) |
248 if options.breakpad_uploads: | 305 if options.breakpad_uploads: |
249 plist['BreakpadURL'] = 'https://clients2.google.com/cr/report' | 306 plist['BreakpadURL'] = 'https://clients2.google.com/cr/report' |
250 else: | 307 else: |
251 # This allows crash dumping to a file without uploading the | 308 # This allows crash dumping to a file without uploading the |
252 # dump, for testing purposes. Breakpad does not recognise | 309 # dump, for testing purposes. Breakpad does not recognise |
253 # "none" as a special value, but this does stop crash dump | 310 # "none" as a special value, but this does stop crash dump |
254 # uploading from happening. We need to specify something | 311 # uploading from happening. We need to specify something |
255 # because if "BreakpadURL" is not present, Breakpad will not | 312 # because if "BreakpadURL" is not present, Breakpad will not |
256 # register its crash handler and no crash dumping will occur. | 313 # register its crash handler and no crash dumping will occur. |
257 plist['BreakpadURL'] = 'none' | 314 plist['BreakpadURL'] = 'none' |
258 else: | 315 else: |
259 _RemoveBreakpadKeys(plist) | 316 _RemoveBreakpadKeys(plist) |
260 | 317 |
261 # Add Keystone if configured to do so. | 318 # Add Keystone if configured to do so. |
262 if options.use_keystone: | 319 if options.use_keystone: |
263 if options.bundle_identifier is None: | 320 if options.bundle_identifier is None: |
264 print >>sys.stderr, 'Use of Keystone requires the bundle id.' | 321 print >>sys.stderr, 'Use of Keystone requires the bundle id.' |
265 return 1 | 322 return 1 |
266 _AddKeystoneKeys(plist, options.bundle_identifier) | 323 _AddKeystoneKeys(plist, options.bundle_identifier) |
267 else: | 324 else: |
268 _RemoveKeystoneKeys(plist) | 325 _RemoveKeystoneKeys(plist) |
269 | 326 |
270 # Adds or removes any SCM keys. | 327 # Adds or removes any SCM keys. |
271 if not _DoSCMKeys(plist, options.add_scm_info): | 328 if not _DoSCMKeys(plist, options.add_scm_info): |
272 return 3 | 329 return 3 |
273 | 330 |
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 | 331 output_path = options.plist_path |
281 if options.plist_output is not None: | 332 if options.plist_output is not None: |
282 output_path = options.plist_output | 333 output_path = options.plist_output |
283 proc = subprocess.Popen(['plutil', '-convert', 'xml1', | 334 |
284 '-o', output_path, | 335 # Now that all keys have been mutated, rewrite the file. |
285 temp_info_plist.name]) | 336 with tempfile.NamedTemporaryFile() as temp_info_plist: |
286 proc.wait() | 337 plistlib.writePlist(plist, temp_info_plist.name) |
287 return proc.returncode | 338 |
| 339 # Convert Info.plist to the format requested by the --format flag. Any |
| 340 # format would work on Mac but iOS requires specific format. |
| 341 return _ConvertPlist(temp_info_plist.name, output_path, options.format) |
288 | 342 |
289 | 343 |
290 if __name__ == '__main__': | 344 if __name__ == '__main__': |
291 sys.exit(Main(sys.argv[1:])) | 345 sys.exit(Main(sys.argv[1:])) |
OLD | NEW |