Fix waterfall bots to not use fastbuild and add presubmit validation.
[chromium-blink-merge.git] / tools / mb / mb.py
blobcaad637a90ecc52684bb66c7c5a9b368f1fec049
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
27 def main(args):
28 mbw = MetaBuildWrapper()
29 mbw.ParseArgs(args)
30 return mbw.args.func()
33 class MetaBuildWrapper(object):
34 def __init__(self):
35 p = os.path
36 d = os.path.dirname
37 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
38 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
39 'mb_config.pyl')
40 self.platform = sys.platform
41 self.args = argparse.Namespace()
42 self.configs = {}
43 self.masters = {}
44 self.mixins = {}
45 self.private_configs = []
46 self.common_dev_configs = []
47 self.unsupported_configs = []
49 def ParseArgs(self, argv):
50 def AddCommonOptions(subp):
51 subp.add_argument('-b', '--builder',
52 help='builder name to look up config from')
53 subp.add_argument('-m', '--master',
54 help='master name to look up config from')
55 subp.add_argument('-c', '--config',
56 help='configuration to analyze')
57 subp.add_argument('-f', '--config-file', metavar='PATH',
58 default=self.default_config,
59 help='path to config file '
60 '(default is //tools/mb/mb_config.pyl)')
61 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
62 help='path to goma directory (default is %(default)s).')
63 subp.add_argument('-n', '--dryrun', action='store_true',
64 help='Do a dry run (i.e., do nothing, just print '
65 'the commands that will run)')
66 subp.add_argument('-q', '--quiet', action='store_true',
67 help='Do not print anything on success, '
68 'just return an exit code.')
69 subp.add_argument('-v', '--verbose', action='count',
70 help='verbose logging (may specify multiple times).')
72 parser = argparse.ArgumentParser(prog='mb')
73 subps = parser.add_subparsers()
75 subp = subps.add_parser('analyze',
76 help='analyze whether changes to a set of files '
77 'will cause a set of binaries to be rebuilt.')
78 AddCommonOptions(subp)
79 subp.add_argument('--swarming-targets-file',
80 help='save runtime dependencies for targets listed '
81 'in file.')
82 subp.add_argument('path', nargs=1,
83 help='path build was generated into.')
84 subp.add_argument('input_path', nargs=1,
85 help='path to a file containing the input arguments '
86 'as a JSON object.')
87 subp.add_argument('output_path', nargs=1,
88 help='path to a file containing the output arguments '
89 'as a JSON object.')
90 subp.set_defaults(func=self.CmdAnalyze)
92 subp = subps.add_parser('gen',
93 help='generate a new set of build files')
94 AddCommonOptions(subp)
95 subp.add_argument('--swarming-targets-file',
96 help='save runtime dependencies for targets listed '
97 'in file.')
98 subp.add_argument('path', nargs=1,
99 help='path to generate build into')
100 subp.set_defaults(func=self.CmdGen)
102 subp = subps.add_parser('lookup',
103 help='look up the command for a given config or '
104 'builder')
105 AddCommonOptions(subp)
106 subp.set_defaults(func=self.CmdLookup)
108 subp = subps.add_parser('validate',
109 help='validate the config file')
110 subp.add_argument('-f', '--config-file', metavar='PATH',
111 default=self.default_config,
112 help='path to config file '
113 '(default is //tools/mb/mb_config.pyl)')
114 subp.add_argument('-q', '--quiet', action='store_true',
115 help='Do not print anything on success, '
116 'just return an exit code.')
117 subp.set_defaults(func=self.CmdValidate)
119 subp = subps.add_parser('help',
120 help='Get help on a subcommand.')
121 subp.add_argument(nargs='?', action='store', dest='subcommand',
122 help='The command to get help for.')
123 subp.set_defaults(func=self.CmdHelp)
125 self.args = parser.parse_args(argv)
127 def CmdAnalyze(self):
128 vals = self.GetConfig()
129 if vals['type'] == 'gn':
130 return self.RunGNAnalyze(vals)
131 elif vals['type'] == 'gyp':
132 return self.RunGYPAnalyze(vals)
133 else:
134 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
136 def CmdGen(self):
137 vals = self.GetConfig()
138 if vals['type'] == 'gn':
139 return self.RunGNGen(vals)
140 if vals['type'] == 'gyp':
141 return self.RunGYPGen(vals)
143 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
145 def CmdLookup(self):
146 vals = self.GetConfig()
147 if vals['type'] == 'gn':
148 cmd = self.GNCmd('gen', '<path>', vals['gn_args'])
149 elif vals['type'] == 'gyp':
150 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config'])
151 else:
152 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
154 self.PrintCmd(cmd)
155 return 0
157 def CmdHelp(self):
158 if self.args.subcommand:
159 self.ParseArgs([self.args.subcommand, '--help'])
160 else:
161 self.ParseArgs(['--help'])
163 def CmdValidate(self):
164 errs = []
166 # Read the file to make sure it parses.
167 self.ReadConfigFile()
169 # Figure out the whole list of configs and ensure that no config is
170 # listed in more than one category.
171 all_configs = {}
172 for config in self.common_dev_configs:
173 all_configs[config] = 'common_dev_configs'
174 for config in self.private_configs:
175 if config in all_configs:
176 errs.append('config "%s" listed in "private_configs" also '
177 'listed in "%s"' % (config, all_configs['config']))
178 else:
179 all_configs[config] = 'private_configs'
180 for config in self.unsupported_configs:
181 if config in all_configs:
182 errs.append('config "%s" listed in "unsupported_configs" also '
183 'listed in "%s"' % (config, all_configs['config']))
184 else:
185 all_configs[config] = 'unsupported_configs'
187 for master in self.masters:
188 for builder in self.masters[master]:
189 config = self.masters[master][builder]
190 if config in all_configs and all_configs[config] not in self.masters:
191 errs.append('Config "%s" used by a bot is also listed in "%s".' %
192 (config, all_configs[config]))
193 else:
194 all_configs[config] = master
196 # Check that every referenced config actually exists.
197 for config, loc in all_configs.items():
198 if not config in self.configs:
199 errs.append('Unknown config "%s" referenced from "%s".' %
200 (config, loc))
202 # Check that every actual config is actually referenced.
203 for config in self.configs:
204 if not config in all_configs:
205 errs.append('Unused config "%s".' % config)
207 # Figure out the whole list of mixins, and check that every mixin
208 # listed by a config or another mixin actually exists.
209 referenced_mixins = set()
210 for config, mixins in self.configs.items():
211 for mixin in mixins:
212 if not mixin in self.mixins:
213 errs.append('Unknown mixin "%s" referenced by config "%s".' %
214 (mixin, config))
215 referenced_mixins.add(mixin)
217 for mixin in self.mixins:
218 for sub_mixin in self.mixins[mixin].get('mixins', []):
219 if not sub_mixin in self.mixins:
220 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
221 (sub_mixin, mixin))
222 referenced_mixins.add(sub_mixin)
224 # Check that every mixin defined is actually referenced somewhere.
225 for mixin in self.mixins:
226 if not mixin in referenced_mixins:
227 errs.append('Unreferenced mixin "%s".' % mixin)
229 if errs:
230 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
231 '\n ' + '\n '.join(errs))
233 if not self.args.quiet:
234 self.Print('mb config file %s looks ok.' % self.args.config_file)
235 return 0
237 def GetConfig(self):
238 self.ReadConfigFile()
239 config = self.ConfigFromArgs()
240 if not config in self.configs:
241 raise MBErr('Config "%s" not found in %s' %
242 (config, self.args.config_file))
244 return self.FlattenConfig(config)
246 def ReadConfigFile(self):
247 if not self.Exists(self.args.config_file):
248 raise MBErr('config file not found at %s' % self.args.config_file)
250 try:
251 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
252 except SyntaxError as e:
253 raise MBErr('Failed to parse config file "%s": %s' %
254 (self.args.config_file, e))
256 self.common_dev_configs = contents['common_dev_configs']
257 self.configs = contents['configs']
258 self.masters = contents['masters']
259 self.mixins = contents['mixins']
260 self.private_configs = contents['private_configs']
261 self.unsupported_configs = contents['unsupported_configs']
263 def ConfigFromArgs(self):
264 if self.args.config:
265 if self.args.master or self.args.builder:
266 raise MBErr('Can not specific both -c/--config and -m/--master or '
267 '-b/--builder')
269 return self.args.config
271 if not self.args.master or not self.args.builder:
272 raise MBErr('Must specify either -c/--config or '
273 '(-m/--master and -b/--builder)')
275 if not self.args.master in self.masters:
276 raise MBErr('Master name "%s" not found in "%s"' %
277 (self.args.master, self.args.config_file))
279 if not self.args.builder in self.masters[self.args.master]:
280 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
281 (self.args.builder, self.args.master, self.args.config_file))
283 return self.masters[self.args.master][self.args.builder]
285 def FlattenConfig(self, config):
286 mixins = self.configs[config]
287 vals = {
288 'type': None,
289 'gn_args': [],
290 'gyp_config': [],
291 'gyp_defines': [],
294 visited = []
295 self.FlattenMixins(mixins, vals, visited)
296 return vals
298 def FlattenMixins(self, mixins, vals, visited):
299 for m in mixins:
300 if m not in self.mixins:
301 raise MBErr('Unknown mixin "%s"' % m)
303 # TODO: check for cycles in mixins.
305 visited.append(m)
307 mixin_vals = self.mixins[m]
308 if 'type' in mixin_vals:
309 vals['type'] = mixin_vals['type']
310 if 'gn_args' in mixin_vals:
311 if vals['gn_args']:
312 vals['gn_args'] += ' ' + mixin_vals['gn_args']
313 else:
314 vals['gn_args'] = mixin_vals['gn_args']
315 if 'gyp_config' in mixin_vals:
316 vals['gyp_config'] = mixin_vals['gyp_config']
317 if 'gyp_defines' in mixin_vals:
318 if vals['gyp_defines']:
319 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
320 else:
321 vals['gyp_defines'] = mixin_vals['gyp_defines']
322 if 'mixins' in mixin_vals:
323 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
324 return vals
326 def RunGNGen(self, vals):
327 path = self.args.path[0]
329 cmd = self.GNCmd('gen', path, vals['gn_args'])
331 swarming_targets = []
332 if self.args.swarming_targets_file:
333 # We need GN to generate the list of runtime dependencies for
334 # the compile targets listed (one per line) in the file so
335 # we can run them via swarming. We use ninja_to_gn.pyl to convert
336 # the compile targets to the matching GN labels.
337 contents = self.ReadFile(self.args.swarming_targets_file)
338 swarming_targets = contents.splitlines()
339 gn_isolate_map = ast.literal_eval(self.ReadFile(os.path.join(
340 self.chromium_src_dir, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
341 gn_labels = []
342 for target in swarming_targets:
343 if not target in gn_isolate_map:
344 raise MBErr('test target "%s" not found in %s' %
345 (target, '//testing/buildbot/gn_isolate_map.pyl'))
346 gn_labels.append(gn_isolate_map[target]['label'])
348 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
350 # Since GN hasn't run yet, the build directory may not even exist.
351 self.MaybeMakeDirectory(self.ToAbsPath(path))
353 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
354 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
356 ret, _, _ = self.Run(cmd)
358 for target in swarming_targets:
359 if sys.platform == 'win32':
360 deps_path = self.ToAbsPath(path, target + '.exe.runtime_deps')
361 else:
362 deps_path = self.ToAbsPath(path, target + '.runtime_deps')
363 if not self.Exists(deps_path):
364 raise MBErr('did not generate %s' % deps_path)
366 command, extra_files = self.GetIsolateCommand(target, vals,
367 gn_isolate_map)
369 runtime_deps = self.ReadFile(deps_path).splitlines()
371 isolate_path = self.ToAbsPath(path, target + '.isolate')
372 self.WriteFile(isolate_path,
373 pprint.pformat({
374 'variables': {
375 'command': command,
376 'files': sorted(runtime_deps + extra_files),
378 }) + '\n')
380 self.WriteJSON(
382 'args': [
383 '--isolated',
384 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)),
385 '--isolate',
386 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)),
388 'dir': self.chromium_src_dir,
389 'version': 1,
391 isolate_path + 'd.gen.json',
395 return ret
397 def GNCmd(self, subcommand, path, gn_args=''):
398 if self.platform == 'linux2':
399 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64',
400 'gn')
401 elif self.platform == 'darwin':
402 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac',
403 'gn')
404 else:
405 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'win',
406 'gn.exe')
408 cmd = [gn_path, subcommand, path]
409 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir)
410 if gn_args:
411 cmd.append('--args=%s' % gn_args)
412 return cmd
414 def RunGYPGen(self, vals):
415 path = self.args.path[0]
417 output_dir, gyp_config = self.ParseGYPConfigPath(path)
418 if gyp_config != vals['gyp_config']:
419 raise MBErr('The last component of the path (%s) must match the '
420 'GYP configuration specified in the config (%s), and '
421 'it does not.' % (gyp_config, vals['gyp_config']))
422 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
423 ret, _, _ = self.Run(cmd)
424 return ret
426 def RunGYPAnalyze(self, vals):
427 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0])
428 if gyp_config != vals['gyp_config']:
429 raise MBErr('The last component of the path (%s) must match the '
430 'GYP configuration specified in the config (%s), and '
431 'it does not.' % (gyp_config, vals['gyp_config']))
432 if self.args.verbose:
433 inp = self.ReadInputJSON(['files', 'targets'])
434 self.Print()
435 self.Print('analyze input:')
436 self.PrintJSON(inp)
437 self.Print()
439 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
440 cmd.extend(['-f', 'analyzer',
441 '-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)