Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3)

Side by Side Diff: tools/mb/mb.py

Issue 1342563002: Clean up logging in MB. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: catch gn gen failure Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tools/mb/mb_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2015 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """MB - the Meta-Build wrapper around GYP and GN 6 """MB - the Meta-Build wrapper around GYP and GN
7 7
8 MB is a wrapper script for GYP and GN that can be used to generate build files 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. 9 for sets of canned configurations and analyze them.
10 """ 10 """
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
56 help='configuration to analyze') 56 help='configuration to analyze')
57 subp.add_argument('-f', '--config-file', metavar='PATH', 57 subp.add_argument('-f', '--config-file', metavar='PATH',
58 default=self.default_config, 58 default=self.default_config,
59 help='path to config file ' 59 help='path to config file '
60 '(default is //tools/mb/mb_config.pyl)') 60 '(default is //tools/mb/mb_config.pyl)')
61 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'), 61 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
62 help='path to goma directory (default is %(default)s).') 62 help='path to goma directory (default is %(default)s).')
63 subp.add_argument('-n', '--dryrun', action='store_true', 63 subp.add_argument('-n', '--dryrun', action='store_true',
64 help='Do a dry run (i.e., do nothing, just print ' 64 help='Do a dry run (i.e., do nothing, just print '
65 'the commands that will run)') 65 'the commands that will run)')
66 subp.add_argument('-q', '--quiet', action='store_true', 66 subp.add_argument('-v', '--verbose', action='store_true',
67 help='Do not print anything on success, ' 67 help='verbose logging')
68 'just return an exit code.')
69 subp.add_argument('-v', '--verbose', action='count',
70 help='verbose logging (may specify multiple times).')
71 68
72 parser = argparse.ArgumentParser(prog='mb') 69 parser = argparse.ArgumentParser(prog='mb')
73 subps = parser.add_subparsers() 70 subps = parser.add_subparsers()
74 71
75 subp = subps.add_parser('analyze', 72 subp = subps.add_parser('analyze',
76 help='analyze whether changes to a set of files ' 73 help='analyze whether changes to a set of files '
77 'will cause a set of binaries to be rebuilt.') 74 'will cause a set of binaries to be rebuilt.')
78 AddCommonOptions(subp) 75 AddCommonOptions(subp)
79 subp.add_argument('--swarming-targets-file', 76 subp.add_argument('--swarming-targets-file',
80 help='save runtime dependencies for targets listed ' 77 help='save runtime dependencies for targets listed '
(...skipping 23 matching lines...) Expand all
104 'builder') 101 'builder')
105 AddCommonOptions(subp) 102 AddCommonOptions(subp)
106 subp.set_defaults(func=self.CmdLookup) 103 subp.set_defaults(func=self.CmdLookup)
107 104
108 subp = subps.add_parser('validate', 105 subp = subps.add_parser('validate',
109 help='validate the config file') 106 help='validate the config file')
110 subp.add_argument('-f', '--config-file', metavar='PATH', 107 subp.add_argument('-f', '--config-file', metavar='PATH',
111 default=self.default_config, 108 default=self.default_config,
112 help='path to config file ' 109 help='path to config file '
113 '(default is //tools/mb/mb_config.pyl)') 110 '(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) 111 subp.set_defaults(func=self.CmdValidate)
118 112
119 subp = subps.add_parser('help', 113 subp = subps.add_parser('help',
120 help='Get help on a subcommand.') 114 help='Get help on a subcommand.')
121 subp.add_argument(nargs='?', action='store', dest='subcommand', 115 subp.add_argument(nargs='?', action='store', dest='subcommand',
122 help='The command to get help for.') 116 help='The command to get help for.')
123 subp.set_defaults(func=self.CmdHelp) 117 subp.set_defaults(func=self.CmdHelp)
124 118
125 self.args = parser.parse_args(argv) 119 self.args = parser.parse_args(argv)
126 120
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after
225 219
226 # Check that every mixin defined is actually referenced somewhere. 220 # Check that every mixin defined is actually referenced somewhere.
227 for mixin in self.mixins: 221 for mixin in self.mixins:
228 if not mixin in referenced_mixins: 222 if not mixin in referenced_mixins:
229 errs.append('Unreferenced mixin "%s".' % mixin) 223 errs.append('Unreferenced mixin "%s".' % mixin)
230 224
231 if errs: 225 if errs:
232 raise MBErr(('mb config file %s has problems:' % self.args.config_file) + 226 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
233 '\n ' + '\n '.join(errs)) 227 '\n ' + '\n '.join(errs))
234 228
235 if not self.args.quiet: 229 self.Print('mb config file %s looks ok.' % self.args.config_file)
236 self.Print('mb config file %s looks ok.' % self.args.config_file)
237 return 0 230 return 0
238 231
239 def GetConfig(self): 232 def GetConfig(self):
240 self.ReadConfigFile() 233 self.ReadConfigFile()
241 config = self.ConfigFromArgs() 234 config = self.ConfigFromArgs()
242 if not config in self.configs: 235 if not config in self.configs:
243 raise MBErr('Config "%s" not found in %s' % 236 raise MBErr('Config "%s" not found in %s' %
244 (config, self.args.config_file)) 237 (config, self.args.config_file))
245 238
246 return self.FlattenConfig(config) 239 return self.FlattenConfig(config)
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
352 345
353 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps') 346 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
354 347
355 # Since GN hasn't run yet, the build directory may not even exist. 348 # Since GN hasn't run yet, the build directory may not even exist.
356 self.MaybeMakeDirectory(self.ToAbsPath(path)) 349 self.MaybeMakeDirectory(self.ToAbsPath(path))
357 350
358 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n') 351 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
359 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path) 352 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
360 353
361 ret, _, _ = self.Run(cmd) 354 ret, _, _ = self.Run(cmd)
355 if ret:
356 # If `gn gen` failed, we should exit early rather than trying to
357 # generate isolates. Run() will have already logged any error output.
358 self.Print('GN gen failed: %d' % ret)
359 return ret
362 360
363 for target in swarming_targets: 361 for target in swarming_targets:
364 if gn_isolate_map[target]['type'] == 'gpu_browser_test': 362 if gn_isolate_map[target]['type'] == 'gpu_browser_test':
365 runtime_deps_target = 'browser_tests' 363 runtime_deps_target = 'browser_tests'
366 elif gn_isolate_map[target]['type'] == 'script': 364 elif gn_isolate_map[target]['type'] == 'script':
367 # For script targets, the build target is usually a group, 365 # For script targets, the build target is usually a group,
368 # for which gn generates the runtime_deps next to the stamp file 366 # for which gn generates the runtime_deps next to the stamp file
369 # for the label, which lives under the obj/ directory. 367 # for the label, which lives under the obj/ directory.
370 label = gn_isolate_map[target]['label'] 368 label = gn_isolate_map[target]['label']
371 runtime_deps_target = 'obj/%s.stamp' % label.replace(':', '/') 369 runtime_deps_target = 'obj/%s.stamp' % label.replace(':', '/')
(...skipping 29 matching lines...) Expand all
401 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)), 399 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)),
402 '--isolate', 400 '--isolate',
403 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)), 401 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)),
404 ], 402 ],
405 'dir': self.chromium_src_dir, 403 'dir': self.chromium_src_dir,
406 'version': 1, 404 'version': 1,
407 }, 405 },
408 isolate_path + 'd.gen.json', 406 isolate_path + 'd.gen.json',
409 ) 407 )
410 408
411
412 return ret 409 return ret
413 410
414 def GNCmd(self, subcommand, path, gn_args=''): 411 def GNCmd(self, subcommand, path, gn_args=''):
415 if self.platform == 'linux2': 412 if self.platform == 'linux2':
416 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64', 413 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64',
417 'gn') 414 'gn')
418 elif self.platform == 'darwin': 415 elif self.platform == 'darwin':
419 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac', 416 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac',
420 'gn') 417 'gn')
421 else: 418 else:
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
466 ret, _, _ = self.Run(cmd) 463 ret, _, _ = self.Run(cmd)
467 if not ret and self.args.verbose: 464 if not ret and self.args.verbose:
468 outp = json.loads(self.ReadFile(self.args.output_path[0])) 465 outp = json.loads(self.ReadFile(self.args.output_path[0]))
469 self.Print() 466 self.Print()
470 self.Print('analyze output:') 467 self.Print('analyze output:')
471 self.PrintJSON(outp) 468 self.PrintJSON(outp)
472 self.Print() 469 self.Print()
473 470
474 return ret 471 return ret
475 472
476 def RunGNIsolate(self, vals):
477 build_path = self.args.path[0]
478 inp = self.ReadInputJSON(['targets'])
479 if self.args.verbose:
480 self.Print()
481 self.Print('isolate input:')
482 self.PrintJSON(inp)
483 self.Print()
484 output_path = self.args.output_path[0]
485
486 for target in inp['targets']:
487 runtime_deps_path = self.ToAbsPath(build_path, target + '.runtime_deps')
488
489 if not self.Exists(runtime_deps_path):
490 self.WriteFailureAndRaise('"%s" does not exist' % runtime_deps_path,
491 output_path)
492
493 command, extra_files = self.GetIsolateCommand(target, vals, None)
494
495 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
496
497
498 isolate_path = self.ToAbsPath(build_path, target + '.isolate')
499 self.WriteFile(isolate_path,
500 pprint.pformat({
501 'variables': {
502 'command': command,
503 'files': sorted(runtime_deps + extra_files),
504 }
505 }) + '\n')
506
507 self.WriteJSON(
508 {
509 'args': [
510 '--isolated',
511 self.ToSrcRelPath('%s/%s.isolated' % (build_path, target)),
512 '--isolate',
513 self.ToSrcRelPath('%s/%s.isolate' % (build_path, target)),
514 ],
515 'dir': self.chromium_src_dir,
516 'version': 1,
517 },
518 isolate_path + 'd.gen.json',
519 )
520
521 return 0
522
523 def GetIsolateCommand(self, target, vals, gn_isolate_map): 473 def GetIsolateCommand(self, target, vals, gn_isolate_map):
524 # This needs to mirror the settings in //build/config/ui.gni: 474 # This needs to mirror the settings in //build/config/ui.gni:
525 # use_x11 = is_linux && !use_ozone. 475 # use_x11 = is_linux && !use_ozone.
526 # TODO(dpranke): Figure out how to keep this in sync better. 476 # TODO(dpranke): Figure out how to keep this in sync better.
527 use_x11 = (sys.platform == 'linux2' and 477 use_x11 = (sys.platform == 'linux2' and
528 not 'target_os="android"' in vals['gn_args'] and 478 not 'target_os="android"' in vals['gn_args'] and
529 not 'use_ozone=true' in vals['gn_args']) 479 not 'use_ozone=true' in vals['gn_args'])
530 480
531 asan = 'is_asan=true' in vals['gn_args'] 481 asan = 'is_asan=true' in vals['gn_args']
532 msan = 'is_msan=true' in vals['gn_args'] 482 msan = 'is_msan=true' in vals['gn_args']
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after
676 626
677 ret = 0 627 ret = 0
678 response_file = self.TempFile() 628 response_file = self.TempFile()
679 response_file.write('\n'.join(inp['files']) + '\n') 629 response_file.write('\n'.join(inp['files']) + '\n')
680 response_file.close() 630 response_file.close()
681 631
682 matching_targets = [] 632 matching_targets = []
683 try: 633 try:
684 cmd = self.GNCmd('refs', self.args.path[0]) + [ 634 cmd = self.GNCmd('refs', self.args.path[0]) + [
685 '@%s' % response_file.name, '--all', '--as=output'] 635 '@%s' % response_file.name, '--all', '--as=output']
686 ret, out, _ = self.Run(cmd) 636 ret, out, _ = self.Run(cmd, force_verbose=False)
687 if ret and not 'The input matches no targets' in out: 637 if ret and not 'The input matches no targets' in out:
688 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), 638 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
689 output_path) 639 output_path)
690 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep 640 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep
691 for output in out.splitlines(): 641 for output in out.splitlines():
692 build_output = output.replace(build_dir, '') 642 build_output = output.replace(build_dir, '')
693 if build_output in inp['targets']: 643 if build_output in inp['targets']:
694 matching_targets.append(build_output) 644 matching_targets.append(build_output)
695 645
696 cmd = self.GNCmd('refs', self.args.path[0]) + [ 646 cmd = self.GNCmd('refs', self.args.path[0]) + [
697 '@%s' % response_file.name, '--all'] 647 '@%s' % response_file.name, '--all']
698 ret, out, _ = self.Run(cmd) 648 ret, out, _ = self.Run(cmd, force_verbose=False)
699 if ret and not 'The input matches no targets' in out: 649 if ret and not 'The input matches no targets' in out:
700 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), 650 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
701 output_path) 651 output_path)
702 for label in out.splitlines(): 652 for label in out.splitlines():
703 build_target = label[2:] 653 build_target = label[2:]
704 # We want to accept 'chrome/android:chrome_public_apk' and 654 # We want to accept 'chrome/android:chrome_public_apk' and
705 # just 'chrome_public_apk'. This may result in too many targets 655 # just 'chrome_public_apk'. This may result in too many targets
706 # getting built, but we can adjust that later if need be. 656 # getting built, but we can adjust that later if need be.
707 for input_target in inp['targets']: 657 for input_target in inp['targets']:
708 if (input_target == build_target or 658 if (input_target == build_target or
709 build_target.endswith(':' + input_target)): 659 build_target.endswith(':' + input_target)):
710 matching_targets.append(input_target) 660 matching_targets.append(input_target)
711 finally: 661 finally:
712 self.RemoveFile(response_file.name) 662 self.RemoveFile(response_file.name)
713 663
714 if matching_targets: 664 if matching_targets:
715 # TODO: it could be that a target X might depend on a target Y 665 # TODO: it could be that a target X might depend on a target Y
716 # and both would be listed in the input, but we would only need 666 # and both would be listed in the input, but we would only need
717 # to specify target X as a build_target (whereas both X and Y are 667 # to specify target X as a build_target (whereas both X and Y are
718 # targets). I'm not sure if that optimization is generally worth it. 668 # targets). I'm not sure if that optimization is generally worth it.
719 self.WriteJSON({'targets': sorted(matching_targets), 669 self.WriteJSON({'targets': sorted(set(matching_targets)),
720 'build_targets': sorted(matching_targets), 670 'build_targets': sorted(set(matching_targets)),
721 'status': 'Found dependency'}, output_path) 671 'status': 'Found dependency'}, output_path)
722 else: 672 else:
723 self.WriteJSON({'targets': [], 673 self.WriteJSON({'targets': [],
724 'build_targets': [], 674 'build_targets': [],
725 'status': 'No dependency'}, output_path) 675 'status': 'No dependency'}, output_path)
726 676
727 if not ret and self.args.verbose: 677 if self.args.verbose:
728 outp = json.loads(self.ReadFile(output_path)) 678 outp = json.loads(self.ReadFile(output_path))
729 self.Print() 679 self.Print()
730 self.Print('analyze output:') 680 self.Print('analyze output:')
731 self.PrintJSON(outp) 681 self.PrintJSON(outp)
732 self.Print() 682 self.Print()
733 683
734 return 0 684 return 0
735 685
736 def ReadInputJSON(self, required_keys): 686 def ReadInputJSON(self, required_keys):
737 path = self.args.input_path[0] 687 path = self.args.input_path[0]
738 output_path = self.args.output_path[0] 688 output_path = self.args.output_path[0]
739 if not self.Exists(path): 689 if not self.Exists(path):
740 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path) 690 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
741 691
742 try: 692 try:
743 inp = json.loads(self.ReadFile(path)) 693 inp = json.loads(self.ReadFile(path))
744 except Exception as e: 694 except Exception as e:
745 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' % 695 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
746 (path, e), output_path) 696 (path, e), output_path)
747 697
748 for k in required_keys: 698 for k in required_keys:
749 if not k in inp: 699 if not k in inp:
750 self.WriteFailureAndRaise('input file is missing a "%s" key' % k, 700 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
751 output_path) 701 output_path)
752 702
753 return inp 703 return inp
754 704
755 def WriteFailureAndRaise(self, msg, output_path): 705 def WriteFailureAndRaise(self, msg, output_path):
756 if output_path: 706 if output_path:
757 self.WriteJSON({'error': msg}, output_path) 707 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
758 raise MBErr(msg) 708 raise MBErr(msg)
759 709
760 def WriteJSON(self, obj, path): 710 def WriteJSON(self, obj, path, force_verbose=False):
761 try: 711 try:
762 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n') 712 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
713 force_verbose=force_verbose)
763 except Exception as e: 714 except Exception as e:
764 raise MBErr('Error %s writing to the output path "%s"' % 715 raise MBErr('Error %s writing to the output path "%s"' %
765 (e, path)) 716 (e, path))
766 717
767 def PrintCmd(self, cmd): 718 def PrintCmd(self, cmd):
768 if cmd[0] == sys.executable: 719 if cmd[0] == sys.executable:
769 cmd = ['python'] + cmd[1:] 720 cmd = ['python'] + cmd[1:]
770 self.Print(*[pipes.quote(c) for c in cmd]) 721 self.Print(*[pipes.quote(c) for c in cmd])
771 722
772 def PrintJSON(self, obj): 723 def PrintJSON(self, obj):
773 self.Print(json.dumps(obj, indent=2, sort_keys=True)) 724 self.Print(json.dumps(obj, indent=2, sort_keys=True))
774 725
775 def Print(self, *args, **kwargs): 726 def Print(self, *args, **kwargs):
776 # This function largely exists so it can be overridden for testing. 727 # This function largely exists so it can be overridden for testing.
777 print(*args, **kwargs) 728 print(*args, **kwargs)
778 729
779 def Run(self, cmd, env=None): 730 def Run(self, cmd, env=None, force_verbose=True):
780 # This function largely exists so it can be overridden for testing. 731 # This function largely exists so it can be overridden for testing.
781 if self.args.dryrun or self.args.verbose: 732 if self.args.dryrun or self.args.verbose or force_verbose:
782 self.PrintCmd(cmd) 733 self.PrintCmd(cmd)
783 if self.args.dryrun: 734 if self.args.dryrun:
784 return 0, '', '' 735 return 0, '', ''
736
785 ret, out, err = self.Call(cmd, env=env) 737 ret, out, err = self.Call(cmd, env=env)
786 if self.args.verbose: 738 if self.args.verbose or force_verbose:
787 if out: 739 if out:
788 self.Print(out, end='') 740 self.Print(out, end='')
789 if err: 741 if err:
790 self.Print(err, end='', file=sys.stderr) 742 self.Print(err, end='', file=sys.stderr)
791 return ret, out, err 743 return ret, out, err
792 744
793 def Call(self, cmd, env=None): 745 def Call(self, cmd, env=None):
794 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, 746 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
795 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 747 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
796 env=env) 748 env=env)
(...skipping 21 matching lines...) Expand all
818 return fp.read() 770 return fp.read()
819 771
820 def RemoveFile(self, path): 772 def RemoveFile(self, path):
821 # This function largely exists so it can be overriden for testing. 773 # This function largely exists so it can be overriden for testing.
822 os.remove(path) 774 os.remove(path)
823 775
824 def TempFile(self, mode='w'): 776 def TempFile(self, mode='w'):
825 # This function largely exists so it can be overriden for testing. 777 # This function largely exists so it can be overriden for testing.
826 return tempfile.NamedTemporaryFile(mode=mode, delete=False) 778 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
827 779
828 def WriteFile(self, path, contents): 780 def WriteFile(self, path, contents, force_verbose=False):
829 # This function largely exists so it can be overriden for testing. 781 # This function largely exists so it can be overriden for testing.
830 if self.args.dryrun or self.args.verbose: 782 if self.args.dryrun or self.args.verbose or force_verbose:
831 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path)) 783 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
832 with open(path, 'w') as fp: 784 with open(path, 'w') as fp:
833 return fp.write(contents) 785 return fp.write(contents)
834 786
835 787
836 class MBErr(Exception): 788 class MBErr(Exception):
837 pass 789 pass
838 790
839 791
840 if __name__ == '__main__': 792 if __name__ == '__main__':
841 try: 793 try:
842 sys.exit(main(sys.argv[1:])) 794 sys.exit(main(sys.argv[1:]))
843 except MBErr as e: 795 except MBErr as e:
844 print(e) 796 print(e)
845 sys.exit(1) 797 sys.exit(1)
846 except KeyboardInterrupt: 798 except KeyboardInterrupt:
847 print("interrupted, exiting", stream=sys.stderr) 799 print("interrupted, exiting", stream=sys.stderr)
848 sys.exit(130) 800 sys.exit(130)
OLDNEW
« no previous file with comments | « no previous file | tools/mb/mb_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698