Add missing 'gpu_tests' config to MB, fix typo in MB.
[chromium-blink-merge.git] / tools / mb / mb.py
blob86ffa6e8de255926617f96a6b82c4bb0004b7e08
1 #!/usr/bin/env python
2 # Copyright 2015 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.
6 """MB - the Meta-Build wrapper around GYP and GN
8 MB is a wrapper script for GYP and GN that can be used to generate build files
9 for sets of canned configurations and analyze them.
10 """
12 from __future__ import print_function
14 import argparse
15 import ast
16 import errno
17 import json
18 import os
19 import pipes
20 import pprint
21 import shlex
22 import shutil
23 import sys
24 import subprocess
25 import tempfile
28 def main(args):
29 mbw = MetaBuildWrapper()
30 mbw.ParseArgs(args)
31 return mbw.args.func()
34 class MetaBuildWrapper(object):
35 def __init__(self):
36 p = os.path
37 d = os.path.dirname
38 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
39 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
40 'mb_config.pyl')
41 self.platform = sys.platform
42 self.args = argparse.Namespace()
43 self.configs = {}
44 self.masters = {}
45 self.mixins = {}
46 self.private_configs = []
47 self.common_dev_configs = []
48 self.unsupported_configs = []
50 def ParseArgs(self, argv):
51 def AddCommonOptions(subp):
52 subp.add_argument('-b', '--builder',
53 help='builder name to look up config from')
54 subp.add_argument('-m', '--master',
55 help='master name to look up config from')
56 subp.add_argument('-c', '--config',
57 help='configuration to analyze')
58 subp.add_argument('-f', '--config-file', metavar='PATH',
59 default=self.default_config,
60 help='path to config file '
61 '(default is //tools/mb/mb_config.pyl)')
62 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
63 help='path to goma directory (default is %(default)s).')
64 subp.add_argument('-n', '--dryrun', action='store_true',
65 help='Do a dry run (i.e., do nothing, just print '
66 'the commands that will run)')
67 subp.add_argument('-q', '--quiet', action='store_true',
68 help='Do not print anything on success, '
69 'just return an exit code.')
70 subp.add_argument('-v', '--verbose', action='count',
71 help='verbose logging (may specify multiple times).')
73 parser = argparse.ArgumentParser(prog='mb')
74 subps = parser.add_subparsers()
76 subp = subps.add_parser('analyze',
77 help='analyze whether changes to a set of files '
78 'will cause a set of binaries to be rebuilt.')
79 AddCommonOptions(subp)
80 subp.add_argument('--swarming-targets-file',
81 help='save runtime dependencies for targets listed '
82 'in file.')
83 subp.add_argument('path', nargs=1,
84 help='path build was generated into.')
85 subp.add_argument('input_path', nargs=1,
86 help='path to a file containing the input arguments '
87 'as a JSON object.')
88 subp.add_argument('output_path', nargs=1,
89 help='path to a file containing the output arguments '
90 'as a JSON object.')
91 subp.set_defaults(func=self.CmdAnalyze)
93 subp = subps.add_parser('gen',
94 help='generate a new set of build files')
95 AddCommonOptions(subp)
96 subp.add_argument('--swarming-targets-file',
97 help='save runtime dependencies for targets listed '
98 'in file.')
99 subp.add_argument('path', nargs=1,
100 help='path to generate build into')
101 subp.set_defaults(func=self.CmdGen)
103 subp = subps.add_parser('lookup',
104 help='look up the command for a given config or '
105 'builder')
106 AddCommonOptions(subp)
107 subp.set_defaults(func=self.CmdLookup)
109 subp = subps.add_parser('validate',
110 help='validate the config file')
111 subp.add_argument('-f', '--config-file', metavar='PATH',
112 default=self.default_config,
113 help='path to config file '
114 '(default is //tools/mb/mb_config.pyl)')
115 subp.add_argument('-q', '--quiet', action='store_true',
116 help='Do not print anything on success, '
117 'just return an exit code.')
118 subp.set_defaults(func=self.CmdValidate)
120 subp = subps.add_parser('help',
121 help='Get help on a subcommand.')
122 subp.add_argument(nargs='?', action='store', dest='subcommand',
123 help='The command to get help for.')
124 subp.set_defaults(func=self.CmdHelp)
126 self.args = parser.parse_args(argv)
128 def CmdAnalyze(self):
129 vals = self.GetConfig()
130 if vals['type'] == 'gn':
131 return self.RunGNAnalyze(vals)
132 elif vals['type'] == 'gyp':
133 return self.RunGYPAnalyze(vals)
134 else:
135 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
137 def CmdGen(self):
138 vals = self.GetConfig()
139 if vals['type'] == 'gn':
140 return self.RunGNGen(vals)
141 if vals['type'] == 'gyp':
142 return self.RunGYPGen(vals)
144 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
146 def CmdLookup(self):
147 vals = self.GetConfig()
148 if vals['type'] == 'gn':
149 cmd = self.GNCmd('gen', '<path>', vals['gn_args'])
150 elif vals['type'] == 'gyp':
151 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config'])
152 else:
153 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
155 self.PrintCmd(cmd)
156 return 0
158 def CmdHelp(self):
159 if self.args.subcommand:
160 self.ParseArgs([self.args.subcommand, '--help'])
161 else:
162 self.ParseArgs(['--help'])
164 def CmdValidate(self):
165 errs = []
167 # Read the file to make sure it parses.
168 self.ReadConfigFile()
170 # Figure out the whole list of configs and ensure that no config is
171 # listed in more than one category.
172 all_configs = {}
173 for config in self.common_dev_configs:
174 all_configs[config] = 'common_dev_configs'
175 for config in self.private_configs:
176 if config in all_configs:
177 errs.append('config "%s" listed in "private_configs" also '
178 'listed in "%s"' % (config, all_configs['config']))
179 else:
180 all_configs[config] = 'private_configs'
181 for config in self.unsupported_configs:
182 if config in all_configs:
183 errs.append('config "%s" listed in "unsupported_configs" also '
184 'listed in "%s"' % (config, all_configs['config']))
185 else:
186 all_configs[config] = 'unsupported_configs'
188 for master in self.masters:
189 for builder in self.masters[master]:
190 config = self.masters[master][builder]
191 if config in all_configs and all_configs[config] not in self.masters:
192 errs.append('Config "%s" used by a bot is also listed in "%s".' %
193 (config, all_configs[config]))
194 else:
195 all_configs[config] = master
197 # Check that every referenced config actually exists.
198 for config, loc in all_configs.items():
199 if not config in self.configs:
200 errs.append('Unknown config "%s" referenced from "%s".' %
201 (config, loc))
203 # Check that every actual config is actually referenced.
204 for config in self.configs:
205 if not config in all_configs:
206 errs.append('Unused config "%s".' % config)
208 # Figure out the whole list of mixins, and check that every mixin
209 # listed by a config or another mixin actually exists.
210 referenced_mixins = set()
211 for config, mixins in self.configs.items():
212 for mixin in mixins:
213 if not mixin in self.mixins:
214 errs.append('Unknown mixin "%s" referenced by config "%s".' %
215 (mixin, config))
216 referenced_mixins.add(mixin)
218 for mixin in self.mixins:
219 for sub_mixin in self.mixins[mixin].get('mixins', []):
220 if not sub_mixin in self.mixins:
221 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
222 (sub_mixin, mixin))
223 referenced_mixins.add(sub_mixin)
225 # Check that every mixin defined is actually referenced somewhere.
226 for mixin in self.mixins:
227 if not mixin in referenced_mixins:
228 errs.append('Unreferenced mixin "%s".' % mixin)
230 if errs:
231 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
232 + '\n ' + '\n '.join(errs))
234 if not self.args.quiet:
235 self.Print('mb config file %s looks ok.' % self.args.config_file)
236 return 0
238 def GetConfig(self):
239 self.ReadConfigFile()
240 config = self.ConfigFromArgs()
241 if not config in self.configs:
242 raise MBErr('Config "%s" not found in %s' %
243 (config, self.args.config_file))
245 return self.FlattenConfig(config)
247 def ReadConfigFile(self):
248 if not self.Exists(self.args.config_file):
249 raise MBErr('config file not found at %s' % self.args.config_file)
251 try:
252 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
253 except SyntaxError as e:
254 raise MBErr('Failed to parse config file "%s": %s' %
255 (self.args.config_file, e))
257 self.common_dev_configs = contents['common_dev_configs']
258 self.configs = contents['configs']
259 self.masters = contents['masters']
260 self.mixins = contents['mixins']
261 self.private_configs = contents['private_configs']
262 self.unsupported_configs = contents['unsupported_configs']
264 def ConfigFromArgs(self):
265 if self.args.config:
266 if self.args.master or self.args.builder:
267 raise MBErr('Can not specific both -c/--config and -m/--master or '
268 '-b/--builder')
270 return self.args.config
272 if not self.args.master or not self.args.builder:
273 raise MBErr('Must specify either -c/--config or '
274 '(-m/--master and -b/--builder)')
276 if not self.args.master in self.masters:
277 raise MBErr('Master name "%s" not found in "%s"' %
278 (self.args.master, self.args.config_file))
280 if not self.args.builder in self.masters[self.args.master]:
281 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
282 (self.args.builder, self.args.master, self.args.config_file))
284 return self.masters[self.args.master][self.args.builder]
286 def FlattenConfig(self, config):
287 mixins = self.configs[config]
288 vals = {
289 'type': None,
290 'gn_args': [],
291 'gyp_config': [],
292 'gyp_defines': [],
295 visited = []
296 self.FlattenMixins(mixins, vals, visited)
297 return vals
299 def FlattenMixins(self, mixins, vals, visited):
300 for m in mixins:
301 if m not in self.mixins:
302 raise MBErr('Unknown mixin "%s"' % m)
304 # TODO: check for cycles in mixins.
306 visited.append(m)
308 mixin_vals = self.mixins[m]
309 if 'type' in mixin_vals:
310 vals['type'] = mixin_vals['type']
311 if 'gn_args' in mixin_vals:
312 if vals['gn_args']:
313 vals['gn_args'] += ' ' + mixin_vals['gn_args']
314 else:
315 vals['gn_args'] = mixin_vals['gn_args']
316 if 'gyp_config' in mixin_vals:
317 vals['gyp_config'] = mixin_vals['gyp_config']
318 if 'gyp_defines' in mixin_vals:
319 if vals['gyp_defines']:
320 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
321 else:
322 vals['gyp_defines'] = mixin_vals['gyp_defines']
323 if 'mixins' in mixin_vals:
324 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
325 return vals
327 def RunGNGen(self, vals):
328 path = self.args.path[0]
330 cmd = self.GNCmd('gen', path, vals['gn_args'])
332 swarming_targets = []
333 if self.args.swarming_targets_file:
334 # We need GN to generate the list of runtime dependencies for
335 # the compile targets listed (one per line) in the file so
336 # we can run them via swarming. We use ninja_to_gn.pyl to convert
337 # the compile targets to the matching GN labels.
338 contents = self.ReadFile(self.args.swarming_targets_file)
339 swarming_targets = contents.splitlines()
340 gn_isolate_map = ast.literal_eval(self.ReadFile(os.path.join(
341 self.chromium_src_dir, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
342 gn_labels = []
343 for target in swarming_targets:
344 if not target in gn_isolate_map:
345 raise MBErr('test target "%s" not found in %s' %
346 (target, '//testing/buildbot/gn_isolate_map.pyl'))
347 gn_labels.append(gn_isolate_map[target]['label'])
349 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
351 # Since GN hasn't run yet, the build directory may not even exist.
352 self.MaybeMakeDirectory(self.ToAbsPath(path))
354 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
355 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
357 ret, _, _ = self.Run(cmd)
359 for target in swarming_targets:
360 if sys.platform == 'win32':
361 deps_path = self.ToAbsPath(path, target + '.exe.runtime_deps')
362 else:
363 deps_path = self.ToAbsPath(path, target + '.runtime_deps')
364 if not self.Exists(deps_path):
365 raise MBErr('did not generate %s' % deps_path)
367 command, extra_files = self.GetIsolateCommand(target, vals,
368 gn_isolate_map)
370 runtime_deps = self.ReadFile(deps_path).splitlines()
372 isolate_path = self.ToAbsPath(path, target + '.isolate')
373 self.WriteFile(isolate_path,
374 pprint.pformat({
375 'variables': {
376 'command': command,
377 'files': sorted(runtime_deps + extra_files),
379 }) + '\n')
381 self.WriteJSON(
383 'args': [
384 '--isolated',
385 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)),
386 '--isolate',
387 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)),
389 'dir': self.chromium_src_dir,
390 'version': 1,
392 isolate_path + 'd.gen.json',
396 return ret
398 def GNCmd(self, subcommand, path, gn_args=''):
399 if self.platform == 'linux2':
400 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64',
401 'gn')
402 elif self.platform == 'darwin':
403 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac',
404 'gn')
405 else:
406 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'win',
407 'gn.exe')
409 cmd = [gn_path, subcommand, path]
410 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir)
411 if gn_args:
412 cmd.append('--args=%s' % gn_args)
413 return cmd
415 def RunGYPGen(self, vals):
416 path = self.args.path[0]
418 output_dir, gyp_config = self.ParseGYPConfigPath(path)
419 if gyp_config != vals['gyp_config']:
420 raise MBErr('The last component of the path (%s) must match the '
421 'GYP configuration specified in the config (%s), and '
422 'it does not.' % (gyp_config, vals['gyp_config']))
423 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
424 ret, _, _ = self.Run(cmd)
425 return ret
427 def RunGYPAnalyze(self, vals):
428 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0])
429 if gyp_config != vals['gyp_config']:
430 raise MBErr('The last component of the path (%s) must match the '
431 'GYP configuration specified in the config (%s), and '
432 'it does not.' % (gyp_config, vals['gyp_config']))
433 if self.args.verbose:
434 inp = self.GetAnalyzeInput()
435 self.Print()
436 self.Print('analyze input:')
437 self.PrintJSON(inp)
438 self.Print()
440 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
441 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0],
442 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
443 ret, _, _ = self.Run(cmd)
444 if not ret and self.args.verbose:
445 outp = json.loads(self.ReadFile(self.args.output_path[0]))
446 self.Print()
447 self.Print('analyze output:')
448 self.PrintJSON(outp)
449 self.Print()
451 return ret
453 def RunGNIsolate(self, vals):
454 build_path = self.args.path[0]
455 inp = self.ReadInputJSON(['targets'])
456 if self.args.verbose:
457 self.Print()
458 self.Print('isolate input:')
459 self.PrintJSON(inp)
460 self.Print()
461 output_path = self.args.output_path[0]
463 for target in inp['targets']:
464 runtime_deps_path = self.ToAbsPath(build_path, target + '.runtime_deps')
466 if not self.Exists(runtime_deps_path):
467 self.WriteFailureAndRaise('"%s" does not exist' % runtime_deps_path,
468 output_path)
470 command, extra_files = self.GetIsolateCommand(target, vals, None)
472 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
475 isolate_path = self.ToAbsPath(build_path, target + '.isolate')
476 self.WriteFile(isolate_path,
477 pprint.pformat({
478 'variables': {
479 'command': command,
480 'files': sorted(runtime_deps + extra_files),
482 }) + '\n')
484 self.WriteJSON(
486 'args': [
487 '--isolated',
488 self.ToSrcRelPath('%s/%s.isolated' % (build_path, target)),
489 '--isolate',
490 self.ToSrcRelPath('%s/%s.isolate' % (build_path, target)),
492 'dir': self.chromium_src_dir,
493 'version': 1,
495 isolate_path + 'd.gen.json',
498 return 0
500 def GetIsolateCommand(self, target, vals, gn_isolate_map):
501 # This needs to mirror the settings in //build/config/ui.gni:
502 # use_x11 = is_linux && !use_ozone.
503 # TODO(dpranke): Figure out how to keep this in sync better.
504 use_x11 = (sys.platform == 'linux2' and
505 not 'target_os="android"' in vals['gn_args'] and
506 not 'use_ozone=true' in vals['gn_args'])
508 asan = 'is_asan=true' in vals['gn_args']
509 msan = 'is_msan=true' in vals['gn_args']
510 tsan = 'is_tsan=true' in vals['gn_args']
512 executable_suffix = '.exe' if sys.platform == 'win32' else ''
514 test_type = gn_isolate_map[target]['type']
515 cmdline = []
516 extra_files = []
518 if use_x11 and test_type == 'windowed_test_launcher':
519 extra_files = [
520 'xdisplaycheck',
521 '../../testing/test_env.py',
522 '../../testing/xvfb.py',
524 cmdline = [
525 '../../testing/xvfb.py',
526 '.',
527 './' + str(target),
528 '--brave-new-test-launcher',
529 '--test-launcher-bot-mode',
530 '--asan=%d' % asan,
531 '--msan=%d' % msan,
532 '--tsan=%d' % tsan,
534 elif test_type in ('windowed_test_launcher', 'console_test_launcher'):
535 extra_files = [
536 '../../testing/test_env.py'
538 cmdline = [
539 '../../testing/test_env.py',
540 './' + str(target) + executable_suffix,
541 '--brave-new-test-launcher',
542 '--test-launcher-bot-mode',
543 '--asan=%d' % asan,
544 '--msan=%d' % msan,
545 '--tsan=%d' % tsan,
547 elif test_type in ('raw'):
548 extra_files = []
549 cmdline = [
550 './' + str(target) + executable_suffix,
551 ] + gn_isolate_map[target].get('args')
553 else:
554 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
555 % (target, test_type), output_path=None)
557 return cmdline, extra_files
559 def ToAbsPath(self, build_path, *comps):
560 return os.path.join(self.chromium_src_dir,
561 self.ToSrcRelPath(build_path),
562 *comps)
564 def ToSrcRelPath(self, path):
565 """Returns a relative path from the top of the repo."""
566 # TODO: Support normal paths in addition to source-absolute paths.
567 assert(path.startswith('//'))
568 return path[2:].replace('/', os.sep)
570 def ParseGYPConfigPath(self, path):
571 rpath = self.ToSrcRelPath(path)
572 output_dir, _, config = rpath.rpartition('/')
573 self.CheckGYPConfigIsSupported(config, path)
574 return output_dir, config
576 def CheckGYPConfigIsSupported(self, config, path):
577 if config not in ('Debug', 'Release'):
578 if (sys.platform in ('win32', 'cygwin') and
579 config not in ('Debug_x64', 'Release_x64')):
580 raise MBErr('Unknown or unsupported config type "%s" in "%s"' %
581 config, path)
583 def GYPCmd(self, output_dir, gyp_defines, config):
584 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir)
585 cmd = [
586 sys.executable,
587 os.path.join('build', 'gyp_chromium'),
588 '-G',
589 'output_dir=' + output_dir,
590 '-G',
591 'config=' + config,
593 for d in shlex.split(gyp_defines):
594 cmd += ['-D', d]
595 return cmd
597 def RunGNAnalyze(self, vals):
598 # analyze runs before 'gn gen' now, so we need to run gn gen
599 # in order to ensure that we have a build directory.
600 ret = self.RunGNGen(vals)
601 if ret:
602 return ret
604 inp = self.ReadInputJSON(['files', 'targets'])
605 if self.args.verbose:
606 self.Print()
607 self.Print('analyze input:')
608 self.PrintJSON(inp)
609 self.Print()
611 output_path = self.args.output_path[0]
613 # Bail out early if a GN file was modified, since 'gn refs' won't know
614 # what to do about it.
615 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']):
616 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
617 return 0
619 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it.
620 if 'all' in inp['targets']:
621 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
622 return 0
624 # This shouldn't normally happen, but could due to unusual race conditions,
625 # like a try job that gets scheduled before a patch lands but runs after
626 # the patch has landed.
627 if not inp['files']:
628 self.Print('Warning: No files modified in patch, bailing out early.')
629 self.WriteJSON({'targets': [],
630 'build_targets': [],
631 'status': 'No dependency'}, output_path)
632 return 0
634 ret = 0
635 response_file = self.TempFile()
636 response_file.write('\n'.join(inp['files']) + '\n')
637 response_file.close()
639 matching_targets = []
640 try:
641 cmd = self.GNCmd('refs', self.args.path[0]) + [
642 '@%s' % response_file.name, '--all', '--as=output']
643 ret, out, _ = self.Run(cmd)
644 if ret and not 'The input matches no targets' in out:
645 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
646 output_path)
647 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep
648 for output in out.splitlines():
649 build_output = output.replace(build_dir, '')
650 if build_output in inp['targets']:
651 matching_targets.append(build_output)
653 cmd = self.GNCmd('refs', self.args.path[0]) + [
654 '@%s' % response_file.name, '--all']
655 ret, out, _ = self.Run(cmd)
656 if ret and not 'The input matches no targets' in out:
657 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
658 output_path)
659 for label in out.splitlines():
660 build_target = label[2:]
661 # We want to accept 'chrome/android:chrome_shell_apk' and
662 # just 'chrome_shell_apk'. This may result in too many targets
663 # getting built, but we can adjust that later if need be.
664 for input_target in inp['targets']:
665 if (input_target == build_target or
666 build_target.endswith(':' + input_target)):
667 matching_targets.append(input_target)
668 finally:
669 self.RemoveFile(response_file.name)
671 if matching_targets:
672 # TODO: it could be that a target X might depend on a target Y
673 # and both would be listed in the input, but we would only need
674 # to specify target X as a build_target (whereas both X and Y are
675 # targets). I'm not sure if that optimization is generally worth it.
676 self.WriteJSON({'targets': sorted(matching_targets),
677 'build_targets': sorted(matching_targets),
678 'status': 'Found dependency'}, output_path)
679 else:
680 self.WriteJSON({'targets': [],
681 'build_targets': [],
682 'status': 'No dependency'}, output_path)
684 if not ret and self.args.verbose:
685 outp = json.loads(self.ReadFile(output_path))
686 self.Print()
687 self.Print('analyze output:')
688 self.PrintJSON(outp)
689 self.Print()
691 return 0
693 def ReadInputJSON(self, required_keys):
694 path = self.args.input_path[0]
695 output_path = self.args.output_path[0]
696 if not self.Exists(path):
697 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
699 try:
700 inp = json.loads(self.ReadFile(path))
701 except Exception as e:
702 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
703 (path, e), output_path)
705 for k in required_keys:
706 if not k in inp:
707 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
708 output_path)
710 return inp
712 def WriteFailureAndRaise(self, msg, output_path):
713 if output_path:
714 self.WriteJSON({'error': msg}, output_path)
715 raise MBErr(msg)
717 def WriteJSON(self, obj, path):
718 try:
719 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n')
720 except Exception as e:
721 raise MBErr('Error %s writing to the output path "%s"' %
722 (e, path))
724 def PrintCmd(self, cmd):
725 if cmd[0] == sys.executable:
726 cmd = ['python'] + cmd[1:]
727 self.Print(*[pipes.quote(c) for c in cmd])
729 def PrintJSON(self, obj):
730 self.Print(json.dumps(obj, indent=2, sort_keys=True))
732 def Print(self, *args, **kwargs):
733 # This function largely exists so it can be overridden for testing.
734 print(*args, **kwargs)
736 def Run(self, cmd):
737 # This function largely exists so it can be overridden for testing.
738 if self.args.dryrun or self.args.verbose:
739 self.PrintCmd(cmd)
740 if self.args.dryrun:
741 return 0, '', ''
742 ret, out, err = self.Call(cmd)
743 if self.args.verbose:
744 if out:
745 self.Print(out, end='')
746 if err:
747 self.Print(err, end='', file=sys.stderr)
748 return ret, out, err
750 def Call(self, cmd):
751 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
752 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
753 out, err = p.communicate()
754 return p.returncode, out, err
756 def ExpandUser(self, path):
757 # This function largely exists so it can be overridden for testing.
758 return os.path.expanduser(path)
760 def Exists(self, path):
761 # This function largely exists so it can be overridden for testing.
762 return os.path.exists(path)
764 def MaybeMakeDirectory(self, path):
765 try:
766 os.makedirs(path)
767 except OSError, e:
768 if e.errno != errno.EEXIST:
769 raise
771 def ReadFile(self, path):
772 # This function largely exists so it can be overriden for testing.
773 with open(path) as fp:
774 return fp.read()
776 def RemoveFile(self, path):
777 # This function largely exists so it can be overriden for testing.
778 os.remove(path)
780 def TempFile(self, mode='w'):
781 # This function largely exists so it can be overriden for testing.
782 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
784 def WriteFile(self, path, contents):
785 # This function largely exists so it can be overriden for testing.
786 if self.args.dryrun or self.args.verbose:
787 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
788 with open(path, 'w') as fp:
789 return fp.write(contents)
792 class MBErr(Exception):
793 pass
796 if __name__ == '__main__':
797 try:
798 sys.exit(main(sys.argv[1:]))
799 except MBErr as e:
800 print(e)
801 sys.exit(1)
802 except KeyboardInterrupt:
803 print("interrupted, exiting", stream=sys.stderr)
804 sys.exit(130)