OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 219 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
466 ret, _, _ = self.Run(cmd) | 459 ret, _, _ = self.Run(cmd) |
467 if not ret and self.args.verbose: | 460 if not ret and self.args.verbose: |
468 outp = json.loads(self.ReadFile(self.args.output_path[0])) | 461 outp = json.loads(self.ReadFile(self.args.output_path[0])) |
469 self.Print() | 462 self.Print() |
470 self.Print('analyze output:') | 463 self.Print('analyze output:') |
471 self.PrintJSON(outp) | 464 self.PrintJSON(outp) |
472 self.Print() | 465 self.Print() |
473 | 466 |
474 return ret | 467 return ret |
475 | 468 |
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): | 469 def GetIsolateCommand(self, target, vals, gn_isolate_map): |
524 # This needs to mirror the settings in //build/config/ui.gni: | 470 # This needs to mirror the settings in //build/config/ui.gni: |
525 # use_x11 = is_linux && !use_ozone. | 471 # use_x11 = is_linux && !use_ozone. |
526 # TODO(dpranke): Figure out how to keep this in sync better. | 472 # TODO(dpranke): Figure out how to keep this in sync better. |
527 use_x11 = (sys.platform == 'linux2' and | 473 use_x11 = (sys.platform == 'linux2' and |
528 not 'target_os="android"' in vals['gn_args'] and | 474 not 'target_os="android"' in vals['gn_args'] and |
529 not 'use_ozone=true' in vals['gn_args']) | 475 not 'use_ozone=true' in vals['gn_args']) |
530 | 476 |
531 asan = 'is_asan=true' in vals['gn_args'] | 477 asan = 'is_asan=true' in vals['gn_args'] |
532 msan = 'is_msan=true' in vals['gn_args'] | 478 msan = 'is_msan=true' in vals['gn_args'] |
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
676 | 622 |
677 ret = 0 | 623 ret = 0 |
678 response_file = self.TempFile() | 624 response_file = self.TempFile() |
679 response_file.write('\n'.join(inp['files']) + '\n') | 625 response_file.write('\n'.join(inp['files']) + '\n') |
680 response_file.close() | 626 response_file.close() |
681 | 627 |
682 matching_targets = [] | 628 matching_targets = [] |
683 try: | 629 try: |
684 cmd = self.GNCmd('refs', self.args.path[0]) + [ | 630 cmd = self.GNCmd('refs', self.args.path[0]) + [ |
685 '@%s' % response_file.name, '--all', '--as=output'] | 631 '@%s' % response_file.name, '--all', '--as=output'] |
686 ret, out, _ = self.Run(cmd) | 632 ret, out, _ = self.Run(cmd, verbose_only=True) |
687 if ret and not 'The input matches no targets' in out: | 633 if ret and not 'The input matches no targets' in out: |
688 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), | 634 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), |
689 output_path) | 635 output_path) |
690 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep | 636 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep |
691 for output in out.splitlines(): | 637 for output in out.splitlines(): |
692 build_output = output.replace(build_dir, '') | 638 build_output = output.replace(build_dir, '') |
693 if build_output in inp['targets']: | 639 if build_output in inp['targets']: |
694 matching_targets.append(build_output) | 640 matching_targets.append(build_output) |
695 | 641 |
696 cmd = self.GNCmd('refs', self.args.path[0]) + [ | 642 cmd = self.GNCmd('refs', self.args.path[0]) + [ |
697 '@%s' % response_file.name, '--all'] | 643 '@%s' % response_file.name, '--all'] |
698 ret, out, _ = self.Run(cmd) | 644 ret, out, _ = self.Run(cmd, verbose_only=True) |
699 if ret and not 'The input matches no targets' in out: | 645 if ret and not 'The input matches no targets' in out: |
700 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), | 646 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out), |
701 output_path) | 647 output_path) |
702 for label in out.splitlines(): | 648 for label in out.splitlines(): |
703 build_target = label[2:] | 649 build_target = label[2:] |
704 # We want to accept 'chrome/android:chrome_public_apk' and | 650 # We want to accept 'chrome/android:chrome_public_apk' and |
705 # just 'chrome_public_apk'. This may result in too many targets | 651 # just 'chrome_public_apk'. This may result in too many targets |
706 # getting built, but we can adjust that later if need be. | 652 # getting built, but we can adjust that later if need be. |
707 for input_target in inp['targets']: | 653 for input_target in inp['targets']: |
708 if (input_target == build_target or | 654 if (input_target == build_target or |
709 build_target.endswith(':' + input_target)): | 655 build_target.endswith(':' + input_target)): |
710 matching_targets.append(input_target) | 656 matching_targets.append(input_target) |
711 finally: | 657 finally: |
712 self.RemoveFile(response_file.name) | 658 self.RemoveFile(response_file.name) |
713 | 659 |
714 if matching_targets: | 660 if matching_targets: |
715 # TODO: it could be that a target X might depend on a target Y | 661 # 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 | 662 # 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 | 663 # 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. | 664 # targets). I'm not sure if that optimization is generally worth it. |
719 self.WriteJSON({'targets': sorted(matching_targets), | 665 self.WriteJSON({'targets': sorted(set(matching_targets)), |
720 'build_targets': sorted(matching_targets), | 666 'build_targets': sorted(set(matching_targets)), |
721 'status': 'Found dependency'}, output_path) | 667 'status': 'Found dependency'}, output_path) |
722 else: | 668 else: |
723 self.WriteJSON({'targets': [], | 669 self.WriteJSON({'targets': [], |
724 'build_targets': [], | 670 'build_targets': [], |
725 'status': 'No dependency'}, output_path) | 671 'status': 'No dependency'}, output_path) |
726 | 672 |
727 if not ret and self.args.verbose: | 673 if self.args.verbose: |
728 outp = json.loads(self.ReadFile(output_path)) | 674 outp = json.loads(self.ReadFile(output_path)) |
729 self.Print() | 675 self.Print() |
730 self.Print('analyze output:') | 676 self.Print('analyze output:') |
731 self.PrintJSON(outp) | 677 self.PrintJSON(outp) |
732 self.Print() | 678 self.Print() |
733 | 679 |
734 return 0 | 680 return 0 |
735 | 681 |
736 def ReadInputJSON(self, required_keys): | 682 def ReadInputJSON(self, required_keys): |
737 path = self.args.input_path[0] | 683 path = self.args.input_path[0] |
738 output_path = self.args.output_path[0] | 684 output_path = self.args.output_path[0] |
739 if not self.Exists(path): | 685 if not self.Exists(path): |
740 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path) | 686 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path) |
741 | 687 |
742 try: | 688 try: |
743 inp = json.loads(self.ReadFile(path)) | 689 inp = json.loads(self.ReadFile(path)) |
744 except Exception as e: | 690 except Exception as e: |
745 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' % | 691 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' % |
746 (path, e), output_path) | 692 (path, e), output_path) |
747 | 693 |
748 for k in required_keys: | 694 for k in required_keys: |
749 if not k in inp: | 695 if not k in inp: |
750 self.WriteFailureAndRaise('input file is missing a "%s" key' % k, | 696 self.WriteFailureAndRaise('input file is missing a "%s" key' % k, |
751 output_path) | 697 output_path) |
752 | 698 |
753 return inp | 699 return inp |
754 | 700 |
755 def WriteFailureAndRaise(self, msg, output_path): | 701 def WriteFailureAndRaise(self, msg, output_path): |
756 if output_path: | 702 if output_path: |
757 self.WriteJSON({'error': msg}, output_path) | 703 self.WriteJSON({'error': msg}, output_path, verbose_only=False) |
758 raise MBErr(msg) | 704 raise MBErr(msg) |
759 | 705 |
760 def WriteJSON(self, obj, path): | 706 def WriteJSON(self, obj, path, verbose_only=True): |
761 try: | 707 try: |
762 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n') | 708 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n', |
709 verbose_only=verbose_only) | |
763 except Exception as e: | 710 except Exception as e: |
764 raise MBErr('Error %s writing to the output path "%s"' % | 711 raise MBErr('Error %s writing to the output path "%s"' % |
765 (e, path)) | 712 (e, path)) |
766 | 713 |
767 def PrintCmd(self, cmd): | 714 def PrintCmd(self, cmd): |
768 if cmd[0] == sys.executable: | 715 if cmd[0] == sys.executable: |
769 cmd = ['python'] + cmd[1:] | 716 cmd = ['python'] + cmd[1:] |
770 self.Print(*[pipes.quote(c) for c in cmd]) | 717 self.Print(*[pipes.quote(c) for c in cmd]) |
771 | 718 |
772 def PrintJSON(self, obj): | 719 def PrintJSON(self, obj): |
773 self.Print(json.dumps(obj, indent=2, sort_keys=True)) | 720 self.Print(json.dumps(obj, indent=2, sort_keys=True)) |
774 | 721 |
775 def Print(self, *args, **kwargs): | 722 def Print(self, *args, **kwargs): |
776 # This function largely exists so it can be overridden for testing. | 723 # This function largely exists so it can be overridden for testing. |
777 print(*args, **kwargs) | 724 print(*args, **kwargs) |
778 | 725 |
779 def Run(self, cmd, env=None): | 726 def Run(self, cmd, env=None, verbose_only=False): |
Nico
2015/09/12 00:09:40
nit: I think it reads a bit better if this is call
| |
780 # This function largely exists so it can be overridden for testing. | 727 # This function largely exists so it can be overridden for testing. |
781 if self.args.dryrun or self.args.verbose: | 728 if self.args.dryrun or self.args.verbose or not verbose_only: |
782 self.PrintCmd(cmd) | 729 self.PrintCmd(cmd) |
783 if self.args.dryrun: | 730 if self.args.dryrun: |
784 return 0, '', '' | 731 return 0, '', '' |
732 | |
785 ret, out, err = self.Call(cmd, env=env) | 733 ret, out, err = self.Call(cmd, env=env) |
786 if self.args.verbose: | 734 if self.args.verbose or not verbose_only: |
787 if out: | 735 if out: |
788 self.Print(out, end='') | 736 self.Print(out, end='') |
789 if err: | 737 if err: |
790 self.Print(err, end='', file=sys.stderr) | 738 self.Print(err, end='', file=sys.stderr) |
791 return ret, out, err | 739 return ret, out, err |
792 | 740 |
793 def Call(self, cmd, env=None): | 741 def Call(self, cmd, env=None): |
794 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, | 742 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, |
795 stdout=subprocess.PIPE, stderr=subprocess.PIPE, | 743 stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
796 env=env) | 744 env=env) |
(...skipping 21 matching lines...) Expand all Loading... | |
818 return fp.read() | 766 return fp.read() |
819 | 767 |
820 def RemoveFile(self, path): | 768 def RemoveFile(self, path): |
821 # This function largely exists so it can be overriden for testing. | 769 # This function largely exists so it can be overriden for testing. |
822 os.remove(path) | 770 os.remove(path) |
823 | 771 |
824 def TempFile(self, mode='w'): | 772 def TempFile(self, mode='w'): |
825 # This function largely exists so it can be overriden for testing. | 773 # This function largely exists so it can be overriden for testing. |
826 return tempfile.NamedTemporaryFile(mode=mode, delete=False) | 774 return tempfile.NamedTemporaryFile(mode=mode, delete=False) |
827 | 775 |
828 def WriteFile(self, path, contents): | 776 def WriteFile(self, path, contents, verbose_only=True): |
829 # This function largely exists so it can be overriden for testing. | 777 # This function largely exists so it can be overriden for testing. |
830 if self.args.dryrun or self.args.verbose: | 778 if self.args.dryrun or self.args.verbose or not verbose_only: |
831 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path)) | 779 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path)) |
832 with open(path, 'w') as fp: | 780 with open(path, 'w') as fp: |
833 return fp.write(contents) | 781 return fp.write(contents) |
834 | 782 |
835 | 783 |
836 class MBErr(Exception): | 784 class MBErr(Exception): |
837 pass | 785 pass |
838 | 786 |
839 | 787 |
840 if __name__ == '__main__': | 788 if __name__ == '__main__': |
841 try: | 789 try: |
842 sys.exit(main(sys.argv[1:])) | 790 sys.exit(main(sys.argv[1:])) |
843 except MBErr as e: | 791 except MBErr as e: |
844 print(e) | 792 print(e) |
845 sys.exit(1) | 793 sys.exit(1) |
846 except KeyboardInterrupt: | 794 except KeyboardInterrupt: |
847 print("interrupted, exiting", stream=sys.stderr) | 795 print("interrupted, exiting", stream=sys.stderr) |
848 sys.exit(130) | 796 sys.exit(130) |
OLD | NEW |