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 """ |
11 | 11 |
12 from __future__ import print_function | 12 from __future__ import print_function |
13 | 13 |
14 import argparse | 14 import argparse |
15 import ast | 15 import ast |
16 import json | 16 import json |
17 import os | 17 import os |
18 import pipes | 18 import pipes |
| 19 import pprint |
19 import shlex | 20 import shlex |
20 import shutil | 21 import shutil |
21 import sys | 22 import sys |
22 import subprocess | 23 import subprocess |
23 import tempfile | 24 import tempfile |
24 | 25 |
25 | 26 |
26 def main(args): | 27 def main(args): |
27 mbw = MetaBuildWrapper() | 28 mbw = MetaBuildWrapper() |
28 mbw.ParseArgs(args) | 29 mbw.ParseArgs(args) |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
68 subp.add_argument('-v', '--verbose', action='count', | 69 subp.add_argument('-v', '--verbose', action='count', |
69 help='verbose logging (may specify multiple times).') | 70 help='verbose logging (may specify multiple times).') |
70 | 71 |
71 parser = argparse.ArgumentParser(prog='mb') | 72 parser = argparse.ArgumentParser(prog='mb') |
72 subps = parser.add_subparsers() | 73 subps = parser.add_subparsers() |
73 | 74 |
74 subp = subps.add_parser('analyze', | 75 subp = subps.add_parser('analyze', |
75 help='analyze whether changes to a set of files ' | 76 help='analyze whether changes to a set of files ' |
76 'will cause a set of binaries to be rebuilt.') | 77 'will cause a set of binaries to be rebuilt.') |
77 AddCommonOptions(subp) | 78 AddCommonOptions(subp) |
78 subp.add_argument('path', type=str, nargs=1, | 79 subp.add_argument('path', nargs=1, |
79 help='path build was generated into.') | 80 help='path build was generated into.') |
80 subp.add_argument('input_path', nargs=1, | 81 subp.add_argument('input_path', nargs=1, |
81 help='path to a file containing the input arguments ' | 82 help='path to a file containing the input arguments ' |
82 'as a JSON object.') | 83 'as a JSON object.') |
83 subp.add_argument('output_path', nargs=1, | 84 subp.add_argument('output_path', nargs=1, |
84 help='path to a file containing the output arguments ' | 85 help='path to a file containing the output arguments ' |
85 'as a JSON object.') | 86 'as a JSON object.') |
86 subp.set_defaults(func=self.CmdAnalyze) | 87 subp.set_defaults(func=self.CmdAnalyze) |
87 | 88 |
88 subp = subps.add_parser('gen', | 89 subp = subps.add_parser('gen', |
89 help='generate a new set of build files') | 90 help='generate a new set of build files') |
90 AddCommonOptions(subp) | 91 AddCommonOptions(subp) |
91 subp.add_argument('path', type=str, nargs=1, | 92 subp.add_argument('path', nargs=1, |
92 help='path to generate build into') | 93 help='path to generate build into') |
93 subp.set_defaults(func=self.CmdGen) | 94 subp.set_defaults(func=self.CmdGen) |
94 | 95 |
| 96 subp = subps.add_parser('isolate', |
| 97 help='build isolates') |
| 98 AddCommonOptions(subp) |
| 99 subp.add_argument('path', nargs=1, |
| 100 help='path build was generated into.') |
| 101 subp.add_argument('input_path', nargs=1, |
| 102 help='path to a file containing the input arguments ' |
| 103 'as a JSON object.') |
| 104 subp.add_argument('output_path', nargs=1, |
| 105 help='path to a file containing the output arguments ' |
| 106 'as a JSON object.') |
| 107 subp.set_defaults(func=self.CmdIsolate) |
| 108 |
95 subp = subps.add_parser('lookup', | 109 subp = subps.add_parser('lookup', |
96 help='look up the command for a given config or ' | 110 help='look up the command for a given config or ' |
97 'builder') | 111 'builder') |
98 AddCommonOptions(subp) | 112 AddCommonOptions(subp) |
99 subp.set_defaults(func=self.CmdLookup) | 113 subp.set_defaults(func=self.CmdLookup) |
100 | 114 |
101 subp = subps.add_parser('validate', | 115 subp = subps.add_parser('validate', |
102 help='validate the config file') | 116 help='validate the config file') |
103 AddCommonOptions(subp) | 117 AddCommonOptions(subp) |
104 subp.set_defaults(func=self.CmdValidate) | 118 subp.set_defaults(func=self.CmdValidate) |
(...skipping 17 matching lines...) Expand all Loading... |
122 | 136 |
123 def CmdGen(self): | 137 def CmdGen(self): |
124 vals = self.GetConfig() | 138 vals = self.GetConfig() |
125 if vals['type'] == 'gn': | 139 if vals['type'] == 'gn': |
126 return self.RunGNGen(self.args.path[0], vals) | 140 return self.RunGNGen(self.args.path[0], vals) |
127 if vals['type'] == 'gyp': | 141 if vals['type'] == 'gyp': |
128 return self.RunGYPGen(self.args.path[0], vals) | 142 return self.RunGYPGen(self.args.path[0], vals) |
129 | 143 |
130 raise MBErr('Unknown meta-build type "%s"' % vals['type']) | 144 raise MBErr('Unknown meta-build type "%s"' % vals['type']) |
131 | 145 |
| 146 def CmdIsolate(self): |
| 147 vals = self.GetConfig() |
| 148 if vals['type'] == 'gn': |
| 149 return self.RunGNIsolate(vals) |
| 150 if vals['type'] == 'gyp': |
| 151 # For GYP builds the .isolate files are checked in and the |
| 152 # .isolate.gen.json files are generated during the compile, |
| 153 # so there is no work to do here. |
| 154 return 0 |
| 155 raise MBErr('Unknown meta-build type "%s"' % vals['type']) |
| 156 |
132 def CmdLookup(self): | 157 def CmdLookup(self): |
133 vals = self.GetConfig() | 158 vals = self.GetConfig() |
134 if vals['type'] == 'gn': | 159 if vals['type'] == 'gn': |
135 cmd = self.GNCmd('gen', '<path>', vals['gn_args']) | 160 cmd = self.GNCmd('gen', '<path>', vals['gn_args']) |
136 elif vals['type'] == 'gyp': | 161 elif vals['type'] == 'gyp': |
137 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config']) | 162 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config']) |
138 else: | 163 else: |
139 raise MBErr('Unknown meta-build type "%s"' % vals['type']) | 164 raise MBErr('Unknown meta-build type "%s"' % vals['type']) |
140 | 165 |
141 self.PrintCmd(cmd) | 166 self.PrintCmd(cmd) |
(...skipping 218 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
360 ret, _, _ = self.Run(cmd) | 385 ret, _, _ = self.Run(cmd) |
361 if not ret and self.args.verbose: | 386 if not ret and self.args.verbose: |
362 outp = json.loads(self.ReadFile(self.args.output_path[0])) | 387 outp = json.loads(self.ReadFile(self.args.output_path[0])) |
363 self.Print() | 388 self.Print() |
364 self.Print('analyze output:') | 389 self.Print('analyze output:') |
365 self.PrintJSON(inp) | 390 self.PrintJSON(inp) |
366 self.Print() | 391 self.Print() |
367 | 392 |
368 return ret | 393 return ret |
369 | 394 |
| 395 def RunGNIsolate(self, vals): |
| 396 build_path = self.args.path[0] |
| 397 inp = self.ReadInputJSON(['targets']) |
| 398 if self.args.verbose: |
| 399 self.Print() |
| 400 self.Print('isolate input:') |
| 401 self.PrintJSON(inp) |
| 402 self.Print() |
| 403 output_path = self.args.output_path[0] |
| 404 |
| 405 for target in inp['targets']: |
| 406 runtime_deps_path = self.ToAbsPath(build_path, target + '.runtime_deps') |
| 407 |
| 408 if not self.Exists(runtime_deps_path): |
| 409 self.WriteFailureAndRaise('"%s" does not exist' % runtime_deps_path, |
| 410 output_path) |
| 411 |
| 412 command, extra_files = self.GetIsolateCommand(target, vals) |
| 413 |
| 414 runtime_deps = self.ReadFile(runtime_deps_path).splitlines() |
| 415 |
| 416 |
| 417 isolate_path = self.ToAbsPath(build_path, target + '.isolate') |
| 418 self.WriteFile(isolate_path, |
| 419 pprint.pformat({ |
| 420 'variables': { |
| 421 'command': command, |
| 422 'files': sorted(runtime_deps + extra_files), |
| 423 'read_only': 1, |
| 424 } |
| 425 }) + '\n') |
| 426 |
| 427 self.WriteJSON( |
| 428 { |
| 429 'args': [ |
| 430 '--isolated', |
| 431 self.ToSrcRelPath('%s/%s.isolated' % (build_path, target)), |
| 432 '--isolate', |
| 433 self.ToSrcRelPath('%s/%s.isolate' % (build_path, target)), |
| 434 ], |
| 435 'dir': self.chromium_src_dir, |
| 436 'version': 1, |
| 437 }, |
| 438 isolate_path + '.gen.json', |
| 439 ) |
| 440 |
| 441 return 0 |
| 442 |
| 443 def GetIsolateCommand(self, target, vals): |
| 444 output_path = self.args.output_path[0] |
| 445 |
| 446 extra_files = [] |
| 447 |
| 448 # TODO(dpranke): We should probably pull this from |
| 449 # the test list info in //testing/buildbot/*.json, |
| 450 # and assert that the test has can_use_on_swarming_builders: True, |
| 451 # but we hardcode it here for now. |
| 452 test_type = {}.get(target, 'gtest_test') |
| 453 |
| 454 # This needs to mirror the settings in //build/config/ui.gni: |
| 455 # use_x11 = is_linux && !use_ozone. |
| 456 # TODO(dpranke): Figure out how to keep this in sync better. |
| 457 use_x11 = (sys.platform == 'linux2' and |
| 458 not 'target_os="android"' in vals['gn_args'] and |
| 459 not 'use_ozone=true' in vals['gn_args']) |
| 460 |
| 461 asan = 'is_asan=true' in vals['gn_args'] |
| 462 msan = 'is_msan=true' in vals['gn_args'] |
| 463 tsan = 'is_tsan=true' in vals['gn_args'] |
| 464 |
| 465 executable_suffix = '.exe' if sys.platform == 'win32' else '' |
| 466 |
| 467 if test_type == 'gtest_test': |
| 468 extra_files.append('../../testing/test_env.py') |
| 469 |
| 470 if use_x11: |
| 471 # TODO(dpranke): Figure out some way to figure out which |
| 472 # test steps really need xvfb. |
| 473 extra_files.append('xdisplaycheck') |
| 474 extra_files.append('../../testing/xvfb.py') |
| 475 |
| 476 cmdline = [ |
| 477 '../../testing/xvfb.py', |
| 478 '.', |
| 479 './' + str(target), |
| 480 '--brave-new-test-launcher', |
| 481 '--test-launcher-bot-mode', |
| 482 '--asan=%d' % asan, |
| 483 '--msan=%d' % msan, |
| 484 '--tsan=%d' % tsan, |
| 485 ] |
| 486 else: |
| 487 cmdline = [ |
| 488 '../../testing/test_env.py', |
| 489 '.', |
| 490 './' + str(target) + executable_suffix, |
| 491 '--brave-new-test-launcher', |
| 492 '--test-launcher-bot-mode', |
| 493 '--asan=%d' % asan, |
| 494 '--msan=%d' % msan, |
| 495 '--tsan=%d' % tsan, |
| 496 ] |
| 497 else: |
| 498 # TODO(dpranke): Handle script_tests and other types of |
| 499 # swarmed tests. |
| 500 self.WriteFailureAndRaise('unknown test type "%s" for %s' % |
| 501 (test_type, target), |
| 502 output_path) |
| 503 |
| 504 |
| 505 return cmdline, extra_files |
| 506 |
| 507 def ToAbsPath(self, build_path, relpath): |
| 508 return os.path.join(self.chromium_src_dir, |
| 509 self.ToSrcRelPath(build_path), |
| 510 relpath) |
| 511 |
370 def ToSrcRelPath(self, path): | 512 def ToSrcRelPath(self, path): |
371 """Returns a relative path from the top of the repo.""" | 513 """Returns a relative path from the top of the repo.""" |
372 # TODO: Support normal paths in addition to source-absolute paths. | 514 # TODO: Support normal paths in addition to source-absolute paths. |
373 assert(path.startswith('//')) | 515 assert(path.startswith('//')) |
374 return path[2:] | 516 return path[2:] |
375 | 517 |
376 def ParseGYPConfigPath(self, path): | 518 def ParseGYPConfigPath(self, path): |
377 rpath = self.ToSrcRelPath(path) | 519 rpath = self.ToSrcRelPath(path) |
378 output_dir, _, config = rpath.rpartition('/') | 520 output_dir, _, config = rpath.rpartition('/') |
379 self.CheckGYPConfigIsSupported(config, path) | 521 self.CheckGYPConfigIsSupported(config, path) |
(...skipping 14 matching lines...) Expand all Loading... |
394 '-G', | 536 '-G', |
395 'output_dir=' + output_dir, | 537 'output_dir=' + output_dir, |
396 '-G', | 538 '-G', |
397 'config=' + config, | 539 'config=' + config, |
398 ] | 540 ] |
399 for d in shlex.split(gyp_defines): | 541 for d in shlex.split(gyp_defines): |
400 cmd += ['-D', d] | 542 cmd += ['-D', d] |
401 return cmd | 543 return cmd |
402 | 544 |
403 def RunGNAnalyze(self, _vals): | 545 def RunGNAnalyze(self, _vals): |
404 inp = self.GetAnalyzeInput() | 546 inp = self.ReadInputJSON(['files', 'targets']) |
405 if self.args.verbose: | 547 if self.args.verbose: |
406 self.Print() | 548 self.Print() |
407 self.Print('analyze input:') | 549 self.Print('analyze input:') |
408 self.PrintJSON(inp) | 550 self.PrintJSON(inp) |
409 self.Print() | 551 self.Print() |
410 | 552 |
411 output_path = self.args.output_path[0] | 553 output_path = self.args.output_path[0] |
412 | 554 |
413 # Bail out early if a GN file was modified, since 'gn refs' won't know | 555 # Bail out early if a GN file was modified, since 'gn refs' won't know |
414 # what to do about it. | 556 # what to do about it. |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
473 | 615 |
474 if not ret and self.args.verbose: | 616 if not ret and self.args.verbose: |
475 outp = json.loads(self.ReadFile(output_path)) | 617 outp = json.loads(self.ReadFile(output_path)) |
476 self.Print() | 618 self.Print() |
477 self.Print('analyze output:') | 619 self.Print('analyze output:') |
478 self.PrintJSON(outp) | 620 self.PrintJSON(outp) |
479 self.Print() | 621 self.Print() |
480 | 622 |
481 return 0 | 623 return 0 |
482 | 624 |
483 def GetAnalyzeInput(self): | 625 def ReadInputJSON(self, required_keys): |
484 path = self.args.input_path[0] | 626 path = self.args.input_path[0] |
485 output_path = self.args.output_path[0] | 627 output_path = self.args.output_path[0] |
486 if not self.Exists(path): | 628 if not self.Exists(path): |
487 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path) | 629 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path) |
488 | 630 |
489 try: | 631 try: |
490 inp = json.loads(self.ReadFile(path)) | 632 inp = json.loads(self.ReadFile(path)) |
491 except Exception as e: | 633 except Exception as e: |
492 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' % | 634 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' % |
493 (path, e), output_path) | 635 (path, e), output_path) |
494 if not 'files' in inp: | 636 |
495 self.WriteFailureAndRaise('input file is missing a "files" key', | 637 for k in required_keys: |
496 output_path) | 638 if not k in inp: |
497 if not 'targets' in inp: | 639 self.WriteFailureAndRaise('input file is missing a "%s" key' % k, |
498 self.WriteFailureAndRaise('input file is missing a "targets" key', | 640 output_path) |
499 output_path) | |
500 | 641 |
501 return inp | 642 return inp |
502 | 643 |
503 def WriteFailureAndRaise(self, msg, path): | 644 def WriteFailureAndRaise(self, msg, path): |
504 self.WriteJSON({'error': msg}, path) | 645 self.WriteJSON({'error': msg}, path) |
505 raise MBErr(msg) | 646 raise MBErr(msg) |
506 | 647 |
507 def WriteJSON(self, obj, path): | 648 def WriteJSON(self, obj, path): |
508 try: | 649 try: |
509 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n') | 650 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n') |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
576 | 717 |
577 if __name__ == '__main__': | 718 if __name__ == '__main__': |
578 try: | 719 try: |
579 sys.exit(main(sys.argv[1:])) | 720 sys.exit(main(sys.argv[1:])) |
580 except MBErr as e: | 721 except MBErr as e: |
581 print(e) | 722 print(e) |
582 sys.exit(1) | 723 sys.exit(1) |
583 except KeyboardInterrupt: | 724 except KeyboardInterrupt: |
584 print("interrupted, exiting", stream=sys.stderr) | 725 print("interrupted, exiting", stream=sys.stderr) |
585 sys.exit(130) | 726 sys.exit(130) |
OLD | NEW |