| 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 |