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 102 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
113 subp.add_argument('path', nargs=1, | 113 subp.add_argument('path', nargs=1, |
114 help='path build was generated into.') | 114 help='path build was generated into.') |
115 subp.add_argument('input_path', nargs=1, | 115 subp.add_argument('input_path', nargs=1, |
116 help='path to a file containing the input arguments ' | 116 help='path to a file containing the input arguments ' |
117 'as a JSON object.') | 117 'as a JSON object.') |
118 subp.add_argument('output_path', nargs=1, | 118 subp.add_argument('output_path', nargs=1, |
119 help='path to a file containing the output arguments ' | 119 help='path to a file containing the output arguments ' |
120 'as a JSON object.') | 120 'as a JSON object.') |
121 subp.set_defaults(func=self.CmdAnalyze) | 121 subp.set_defaults(func=self.CmdAnalyze) |
122 | 122 |
| 123 subp = subps.add_parser('export', |
| 124 help='print out the expanded configuration for' |
| 125 'each builder as a JSON object') |
| 126 subp.add_argument('-f', '--config-file', metavar='PATH', |
| 127 default=self.default_config, |
| 128 help='path to config file ' |
| 129 '(default is //tools/mb/mb_config.pyl)') |
| 130 subp.add_argument('-g', '--goma-dir', |
| 131 help='path to goma directory') |
| 132 subp.set_defaults(func=self.CmdExport) |
| 133 |
123 subp = subps.add_parser('gen', | 134 subp = subps.add_parser('gen', |
124 help='generate a new set of build files') | 135 help='generate a new set of build files') |
125 AddCommonOptions(subp) | 136 AddCommonOptions(subp) |
126 subp.add_argument('--swarming-targets-file', | 137 subp.add_argument('--swarming-targets-file', |
127 help='save runtime dependencies for targets listed ' | 138 help='save runtime dependencies for targets listed ' |
128 'in file.') | 139 'in file.') |
129 subp.add_argument('path', nargs=1, | 140 subp.add_argument('path', nargs=1, |
130 help='path to generate build into') | 141 help='path to generate build into') |
131 subp.set_defaults(func=self.CmdGen) | 142 subp.set_defaults(func=self.CmdGen) |
132 | 143 |
(...skipping 109 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
242 '--swarming-targets-file', self.args.swarming_targets_file) | 253 '--swarming-targets-file', self.args.swarming_targets_file) |
243 | 254 |
244 def CmdAnalyze(self): | 255 def CmdAnalyze(self): |
245 vals = self.Lookup() | 256 vals = self.Lookup() |
246 self.ClobberIfNeeded(vals) | 257 self.ClobberIfNeeded(vals) |
247 if vals['type'] == 'gn': | 258 if vals['type'] == 'gn': |
248 return self.RunGNAnalyze(vals) | 259 return self.RunGNAnalyze(vals) |
249 else: | 260 else: |
250 return self.RunGYPAnalyze(vals) | 261 return self.RunGYPAnalyze(vals) |
251 | 262 |
| 263 def CmdExport(self): |
| 264 self.ReadConfigFile() |
| 265 obj = {} |
| 266 for master, builders in self.masters.items(): |
| 267 obj[master] = {} |
| 268 for builder in builders: |
| 269 config = self.masters[master][builder] |
| 270 if not config: |
| 271 continue |
| 272 |
| 273 if isinstance(config, list): |
| 274 args = [self.FlattenConfig(c)['gn_args'] for c in config] |
| 275 elif config.startswith('//'): |
| 276 args = config |
| 277 else: |
| 278 args = self.FlattenConfig(config)['gn_args'] |
| 279 if 'error' in args: |
| 280 continue |
| 281 |
| 282 obj[master][builder] = args |
| 283 |
| 284 # Dump object and trim trailing whitespace. |
| 285 s = '\n'.join(l.rstrip() for l in |
| 286 json.dumps(obj, sort_keys=True, indent=2).splitlines()) |
| 287 self.Print(s) |
| 288 return 0 |
| 289 |
252 def CmdGen(self): | 290 def CmdGen(self): |
253 vals = self.Lookup() | 291 vals = self.Lookup() |
254 self.ClobberIfNeeded(vals) | 292 self.ClobberIfNeeded(vals) |
255 if vals['type'] == 'gn': | 293 if vals['type'] == 'gn': |
256 return self.RunGNGen(vals) | 294 return self.RunGNGen(vals) |
257 else: | 295 else: |
258 return self.RunGYPGen(vals) | 296 return self.RunGYPGen(vals) |
259 | 297 |
260 def CmdHelp(self): | 298 def CmdHelp(self): |
261 if self.args.subcommand: | 299 if self.args.subcommand: |
(...skipping 257 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
519 self.Print(fmt.format('Totals', str(sum(int(v) for v in stats.values())))) | 557 self.Print(fmt.format('Totals', str(sum(int(v) for v in stats.values())))) |
520 self.Print(fmt.format('-' * 27, '----')) | 558 self.Print(fmt.format('-' * 27, '----')) |
521 for stat, count in stats.items(): | 559 for stat, count in stats.items(): |
522 self.Print(fmt.format(stat, str(count))) | 560 self.Print(fmt.format(stat, str(count))) |
523 | 561 |
524 return 0 | 562 return 0 |
525 | 563 |
526 def GetConfig(self): | 564 def GetConfig(self): |
527 build_dir = self.args.path[0] | 565 build_dir = self.args.path[0] |
528 | 566 |
529 vals = {} | 567 vals = self.DefaultVals() |
530 if self.args.builder or self.args.master or self.args.config: | 568 if self.args.builder or self.args.master or self.args.config: |
531 vals = self.Lookup() | 569 vals = self.Lookup() |
532 if vals['type'] == 'gn': | 570 if vals['type'] == 'gn': |
533 # Re-run gn gen in order to ensure the config is consistent with the | 571 # Re-run gn gen in order to ensure the config is consistent with the |
534 # build dir. | 572 # build dir. |
535 self.RunGNGen(vals) | 573 self.RunGNGen(vals) |
536 return vals | 574 return vals |
537 | 575 |
538 mb_type_path = self.PathJoin(self.ToAbsPath(build_dir), 'mb_type') | 576 mb_type_path = self.PathJoin(self.ToAbsPath(build_dir), 'mb_type') |
539 if not self.Exists(mb_type_path): | 577 if not self.Exists(mb_type_path): |
540 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir), | 578 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir), |
541 'toolchain.ninja') | 579 'toolchain.ninja') |
542 if not self.Exists(toolchain_path): | 580 if not self.Exists(toolchain_path): |
543 self.Print('Must either specify a path to an existing GN build dir ' | 581 self.Print('Must either specify a path to an existing GN build dir ' |
544 'or pass in a -m/-b pair or a -c flag to specify the ' | 582 'or pass in a -m/-b pair or a -c flag to specify the ' |
545 'configuration') | 583 'configuration') |
546 return {} | 584 return {} |
547 else: | 585 else: |
548 mb_type = 'gn' | 586 mb_type = 'gn' |
549 else: | 587 else: |
550 mb_type = self.ReadFile(mb_type_path).strip() | 588 mb_type = self.ReadFile(mb_type_path).strip() |
551 | 589 |
552 if mb_type == 'gn': | 590 if mb_type == 'gn': |
553 vals = self.GNValsFromDir(build_dir) | 591 vals['gn_args'] = self.GNArgsFromDir(build_dir) |
554 else: | |
555 vals = {} | |
556 vals['type'] = mb_type | 592 vals['type'] = mb_type |
557 | 593 |
558 return vals | 594 return vals |
559 | 595 |
560 def GNValsFromDir(self, build_dir): | 596 def GNArgsFromDir(self, build_dir): |
561 args_contents = "" | 597 args_contents = "" |
562 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn') | 598 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn') |
563 if self.Exists(gn_args_path): | 599 if self.Exists(gn_args_path): |
564 args_contents = self.ReadFile(gn_args_path) | 600 args_contents = self.ReadFile(gn_args_path) |
565 gn_args = [] | 601 gn_args = [] |
566 for l in args_contents.splitlines(): | 602 for l in args_contents.splitlines(): |
567 fields = l.split(' ') | 603 fields = l.split(' ') |
568 name = fields[0] | 604 name = fields[0] |
569 val = ' '.join(fields[2:]) | 605 val = ' '.join(fields[2:]) |
570 gn_args.append('%s=%s' % (name, val)) | 606 gn_args.append('%s=%s' % (name, val)) |
571 | 607 |
572 return { | 608 return ' '.join(gn_args) |
573 'gn_args': ' '.join(gn_args), | |
574 'type': 'gn', | |
575 } | |
576 | 609 |
577 def Lookup(self): | 610 def Lookup(self): |
578 vals = self.ReadBotConfig() | 611 vals = self.ReadIOSBotConfig() |
579 if not vals: | 612 if not vals: |
580 self.ReadConfigFile() | 613 self.ReadConfigFile() |
581 config = self.ConfigFromArgs() | 614 config = self.ConfigFromArgs() |
582 if config.startswith('//'): | 615 if config.startswith('//'): |
583 if not self.Exists(self.ToAbsPath(config)): | 616 if not self.Exists(self.ToAbsPath(config)): |
584 raise MBErr('args file "%s" not found' % config) | 617 raise MBErr('args file "%s" not found' % config) |
585 vals = { | 618 vals = self.DefaultVals() |
586 'args_file': config, | 619 vals['args_file'] = config |
587 'cros_passthrough': False, | |
588 'gn_args': '', | |
589 'gyp_crosscompile': False, | |
590 'gyp_defines': '', | |
591 'type': 'gn', | |
592 } | |
593 else: | 620 else: |
594 if not config in self.configs: | 621 if not config in self.configs: |
595 raise MBErr('Config "%s" not found in %s' % | 622 raise MBErr('Config "%s" not found in %s' % |
596 (config, self.args.config_file)) | 623 (config, self.args.config_file)) |
597 vals = self.FlattenConfig(config) | 624 vals = self.FlattenConfig(config) |
598 | 625 |
599 # Do some basic sanity checking on the config so that we | 626 # Do some basic sanity checking on the config so that we |
600 # don't have to do this in every caller. | 627 # don't have to do this in every caller. |
601 assert 'type' in vals, 'No meta-build type specified in the config' | 628 if 'type' not in vals: |
| 629 vals['type'] = 'gn' |
602 assert vals['type'] in ('gn', 'gyp'), ( | 630 assert vals['type'] in ('gn', 'gyp'), ( |
603 'Unknown meta-build type "%s"' % vals['gn_args']) | 631 'Unknown meta-build type "%s"' % vals['gn_args']) |
604 | 632 |
605 return vals | 633 return vals |
606 | 634 |
607 def ReadBotConfig(self): | 635 def ReadIOSBotConfig(self): |
608 if not self.args.master or not self.args.builder: | 636 if not self.args.master or not self.args.builder: |
609 return {} | 637 return {} |
610 path = self.PathJoin(self.chromium_src_dir, 'ios', 'build', 'bots', | 638 path = self.PathJoin(self.chromium_src_dir, 'ios', 'build', 'bots', |
611 self.args.master, self.args.builder + '.json') | 639 self.args.master, self.args.builder + '.json') |
612 if not self.Exists(path): | 640 if not self.Exists(path): |
613 return {} | 641 return {} |
614 | 642 |
615 contents = json.loads(self.ReadFile(path)) | 643 contents = json.loads(self.ReadFile(path)) |
616 gyp_vals = contents.get('GYP_DEFINES', {}) | 644 gyp_vals = contents.get('GYP_DEFINES', {}) |
617 if isinstance(gyp_vals, dict): | 645 if isinstance(gyp_vals, dict): |
618 gyp_defines = ' '.join('%s=%s' % (k, v) for k, v in gyp_vals.items()) | 646 gyp_defines = ' '.join('%s=%s' % (k, v) for k, v in gyp_vals.items()) |
619 else: | 647 else: |
620 gyp_defines = ' '.join(gyp_vals) | 648 gyp_defines = ' '.join(gyp_vals) |
621 gn_args = ' '.join(contents.get('gn_args', [])) | 649 gn_args = ' '.join(contents.get('gn_args', [])) |
622 | 650 |
623 return { | 651 vals = self.DefaultVals() |
624 'args_file': '', | 652 vals['gn_args'] = gn_args |
625 'cros_passthrough': False, | 653 vals['gyp_defines'] = gyp_defines |
626 'gn_args': gn_args, | 654 vals['type'] = contents.get('mb_type', 'gn') |
627 'gyp_crosscompile': False, | 655 return vals |
628 'gyp_defines': gyp_defines, | |
629 'type': contents.get('mb_type', ''), | |
630 } | |
631 | 656 |
632 def ReadConfigFile(self): | 657 def ReadConfigFile(self): |
633 if not self.Exists(self.args.config_file): | 658 if not self.Exists(self.args.config_file): |
634 raise MBErr('config file not found at %s' % self.args.config_file) | 659 raise MBErr('config file not found at %s' % self.args.config_file) |
635 | 660 |
636 try: | 661 try: |
637 contents = ast.literal_eval(self.ReadFile(self.args.config_file)) | 662 contents = ast.literal_eval(self.ReadFile(self.args.config_file)) |
638 except SyntaxError as e: | 663 except SyntaxError as e: |
639 raise MBErr('Failed to parse config file "%s": %s' % | 664 raise MBErr('Failed to parse config file "%s": %s' % |
640 (self.args.config_file, e)) | 665 (self.args.config_file, e)) |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
678 (phase, self.args.builder, self.args.master)) | 703 (phase, self.args.builder, self.args.master)) |
679 return config[phase-1] | 704 return config[phase-1] |
680 | 705 |
681 if self.args.phase is not None: | 706 if self.args.phase is not None: |
682 raise MBErr('Must not specify a build --phase for %s on %s' % | 707 raise MBErr('Must not specify a build --phase for %s on %s' % |
683 (self.args.builder, self.args.master)) | 708 (self.args.builder, self.args.master)) |
684 return config | 709 return config |
685 | 710 |
686 def FlattenConfig(self, config): | 711 def FlattenConfig(self, config): |
687 mixins = self.configs[config] | 712 mixins = self.configs[config] |
688 vals = { | 713 vals = self.DefaultVals() |
689 'args_file': '', | |
690 'cros_passthrough': False, | |
691 'gn_args': [], | |
692 'gyp_defines': '', | |
693 'gyp_crosscompile': False, | |
694 'type': None, | |
695 } | |
696 | 714 |
697 visited = [] | 715 visited = [] |
698 self.FlattenMixins(mixins, vals, visited) | 716 self.FlattenMixins(mixins, vals, visited) |
699 return vals | 717 return vals |
700 | 718 |
| 719 def DefaultVals(self): |
| 720 return { |
| 721 'args_file': '', |
| 722 'cros_passthrough': False, |
| 723 'gn_args': '', |
| 724 'gyp_defines': '', |
| 725 'gyp_crosscompile': False, |
| 726 'type': 'gn', |
| 727 } |
| 728 |
701 def FlattenMixins(self, mixins, vals, visited): | 729 def FlattenMixins(self, mixins, vals, visited): |
702 for m in mixins: | 730 for m in mixins: |
703 if m not in self.mixins: | 731 if m not in self.mixins: |
704 raise MBErr('Unknown mixin "%s"' % m) | 732 raise MBErr('Unknown mixin "%s"' % m) |
705 | 733 |
706 visited.append(m) | 734 visited.append(m) |
707 | 735 |
708 mixin_vals = self.mixins[m] | 736 mixin_vals = self.mixins[m] |
709 | 737 |
710 if 'cros_passthrough' in mixin_vals: | 738 if 'cros_passthrough' in mixin_vals: |
(...skipping 812 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1523 # Then check to see if the arg contains any metacharacters other than | 1551 # Then check to see if the arg contains any metacharacters other than |
1524 # double quotes; if it does, quote everything (including the double | 1552 # double quotes; if it does, quote everything (including the double |
1525 # quotes) for safety. | 1553 # quotes) for safety. |
1526 if any(a in UNSAFE_FOR_CMD for a in arg): | 1554 if any(a in UNSAFE_FOR_CMD for a in arg): |
1527 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg) | 1555 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg) |
1528 return arg | 1556 return arg |
1529 | 1557 |
1530 | 1558 |
1531 if __name__ == '__main__': | 1559 if __name__ == '__main__': |
1532 sys.exit(main(sys.argv[1:])) | 1560 sys.exit(main(sys.argv[1:])) |
OLD | NEW |