Index: tools/mb/mb.py |
diff --git a/tools/mb/mb.py b/tools/mb/mb.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..d573092297fc95d1dd3fb1cf369418d21a856325 |
--- /dev/null |
+++ b/tools/mb/mb.py |
@@ -0,0 +1,320 @@ |
+#!/usr/bin/env python |
+# Copyright 2015 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""MB - the Meta-Build wrapper around GYP and GN |
+ |
+MB is a wrapper script for GYP and GN that can be used to generate build files |
+for sets of canned configurations and analyze them. |
+""" |
+ |
+from __future__ import print_function |
+ |
+import argparse |
+import ast |
+import json |
+import os |
+import pipes |
+import shutil |
+import sys |
+import subprocess |
+import tempfile |
+ |
+ |
+DEFAULT_BUILDS = { |
+ '//out': 'gyp' |
+} |
+ |
+ |
+def main(argv): |
+ mb = MetaBuildWrapper() |
+ mb.ParseArgs(argv) |
+ return mb.args.func() |
+ |
+ |
+class MetaBuildWrapper(object): |
+ def __init__(self): |
+ p = os.path |
+ d = os.path.dirname |
+ self.chromium_src_dir = p.normpath(p.abspath(d(d(d(__file__))))) |
+ self.mb_conf_pyl = p.join(self.chromium_src_dir, 'build', 'mb_conf.pyl') |
+ self.builds_pyl = p.normpath(p.join(self.chromium_src_dir, '..', |
+ 'builds.pyl')) |
+ self.build_configs = {} |
+ self.mixins = {} |
+ |
+ def ParseArgs(self, argv): |
+ parser = argparse.ArgumentParser(prog='mb') |
+ subps = parser.add_subparsers() |
+ |
+ subp = subps.add_parser('analyze', |
+ help='analyze whether changes to a set of files ' |
+ 'will cause a set of binaries to be rebuilt.') |
+ subp.add_argument('-i', '--json-input', action='store', |
+ help='path to a file containing the input arguments ' |
+ 'as a JSON object.') |
+ subp.add_argument('-o', '--json-output', action='store', |
+ help='path to a file containing the output arguments ' |
+ 'as a JSON object.') |
+ subp.add_argument('-n', '--dryrun', action='store_true', |
+ help='Do a dry run (i.e., do nothing, just print ' |
+ 'the commands') |
+ subp.add_argument('-v', '--verbose', action='count', |
+ help='verbose logging (may specify multiple times.') |
+ |
+ subp.add_argument('build_config', type=str, nargs='?', |
+ help='configuration to analyze') |
+ subp.add_argument('paths', type=str, nargs='*', |
+ help='list of files or targets to analyze') |
+ subp.set_defaults(func=self.CmdAnalyze) |
+ |
+ subp = subps.add_parser('gen', |
+ help='generate a new set of build files') |
+ subp.add_argument('-n', '--dryrun', action='store_true', |
+ help='Do a dry run (i.e., do nothing, just print ' |
+ 'the commands') |
+ subp.add_argument('-v', '--verbose', action='count', |
+ help='verbose logging (may specify multiple times.') |
+ subp.add_argument('path', type=str, nargs='?', |
+ help='path to generate build into') |
+ subp.add_argument('build_config', type=str, nargs='?', |
+ help='build configuration to generate') |
+ subp.set_defaults(func=self.CmdGen) |
+ |
+ subp = subps.add_parser('help', |
+ help='Get help on a subcommand.') |
+ subp.add_argument(nargs='?', action='store', dest='subcommand', |
+ help='The command to get help for.') |
+ subp.set_defaults(func=self.CmdHelp) |
+ |
+ self.args = parser.parse_args(argv) |
+ |
+ def ReadConf(self): |
+ if not self.PathExists(self.mwb_conf_pyl): |
+ raise _Err('mwb conf file not found at %s' % self.mwb_conf_pyl) |
+ contents = self.ReadPyl(self.mwb_conf_pyl, 'mwb conf file') |
+ self.build_configs = contents['build_configs'] |
+ self.mixins = contents['mixins'] |
+ |
+ def PathExists(self, path): |
+ return os.path.exists(path) |
+ |
+ def ReadPyl(self, path, desc): |
+ try: |
+ return ast.literal_eval(open(path).read()) |
+ except SyntaxError as e: |
+ raise _Err('Failed to parse %s at "%s": %s' % (desc, path, e)) |
+ |
+ def Call(self, cmd): |
+ if self.args.dryrun or self.args.verbose: |
+ if cmd[0] == sys.executable: |
+ cmd = ['python'] + cmd[1:] |
+ print(*[pipes.quote(c) for c in cmd]) |
+ |
+ if self.args.dryrun: |
+ return 0, '', '' |
+ p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, |
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
+ out, err = p.communicate() |
+ if self.args.verbose: |
+ if out: |
+ print(out) |
+ if err: |
+ print(err, stream=sys.stderr) |
+ return p.returncode, out, err |
+ |
+ def CmdHelp(self): |
+ if self.args.subcommand: |
+ self.ParseArgs([self.args.subcommand, '--help']) |
+ else: |
+ self.ParseArgs(['--help']) |
+ |
+ def CmdGen(self): |
+ self.ReadConf() |
+ |
+ if self.args.build_config and self.args.path: |
+ builds = self.BuildsFromArgs() |
+ elif os.path.exists(self.builds_pyl): |
+ builds = self.BuildsFromPyl() |
+ else: |
+ build = DEFAULT_BUILDS |
+ |
+ for path, build_config in builds.items(): |
+ vals = self.FlattenBuildConfig(build_config) |
+ if vals['type'] == 'gn': |
+ self.RunGN(path, vals['gn_args']) |
+ elif vals['type'] == 'gyp': |
+ self.RunGYP(path, vals['gyp_defines']) |
+ elif vals['type'] == 'gyp_one_config': |
+ self.RunGYPOneConfig(path, vals['gyp_defines']) |
+ else: |
+ raise _Err('Unknown meta-build type "%s"' % vals['type']) |
+ return 0 |
+ |
+ def BuildsFromArgs(self): |
+ builds = {} |
+ builds[self.args.path] = self.args.build_config |
+ return builds |
+ |
+ def BuildsFromPyl(self): |
+ return self.ReadPyl(self.builds_pyl, "build file") |
+ |
+ def RunGN(self, path, gn_args=''): |
+ cmd = self.GNCmd(path, gn_args) |
+ ret, _, _ = self.Call(cmd) |
+ return ret |
+ |
+ def FlattenBuildConfig(self, build_config): |
+ if build_config not in self.build_configs: |
+ raise _Err('Unknown build config "%s"' % build_config) |
+ if self.build_configs[build_config] is None: |
+ mixins = build_config.split('_') |
+ else: |
+ mixins = self.build_configs[build_config] |
+ |
+ vals = { |
+ 'type': None, |
+ 'gn_args': [], |
+ 'gyp_config': [], |
+ 'gyp_defines': [], |
+ } |
+ |
+ visited = [] |
+ self.FlattenMixins(mixins, vals, visited) |
+ return vals |
+ |
+ def FlattenMixins(self, mixins, vals, visited): |
+ for m in mixins: |
+ if m not in self.mixins: |
+ raise _Err('Unknown mixin "%s"' % m) |
+ if m in visited: |
+ raise _Err('Cycle in build configs for "%s": %s' % (config, visited)) |
+ |
+ visited.append(m) |
+ |
+ mixin_vals = self.mixins[m] |
+ if 'type' in mixin_vals: |
+ vals['type'] = mixin_vals['type'] |
+ if 'gn_args' in mixin_vals: |
+ vals['gn_args'].extend(mixin_vals['gn_args']) |
+ if 'gyp_config' in mixin_vals: |
+ vals['gyp_config'] = mixin_vals['gyp_config'] |
+ if 'gyp_defines' in mixin_vals: |
+ vals['gyp_defines'].extend(mixin_vals['gyp_defines']) |
+ if 'mixins' in mixin_vals: |
+ self.FlattenMixins(mixin_vals['mixins'], vals, visited) |
+ return vals |
+ |
+ def GNCmd(self, path, gn_args): |
+ # TODO(dpranke): Find gn explicitly in the path ... |
+ cmd = ['gn', 'gen', path] |
+ if gn_args: |
+ cmd.append('--args=%s' % ' '.join(gn_args)) |
+ return cmd |
+ |
+ def RunGYP(self, path, gyp_defines): |
+ output_dir = self.ParseGYPOutputPath(path) |
+ cmd = self.GYPCmd(output_dir, gyp_defines, config=None) |
+ ret, _, _ = self.Call(cmd) |
+ return ret |
+ |
+ def RunGYPConfig(self, path, gyp_defines): |
+ output_dir, gyp_config = self.ParseGYPConfigPath(path) |
+ cmd = self.GYPCmd(output_dir, gyp_defines, config=gyp_config) |
+ ret, _, _ = self.Call(cmd) |
+ return ret |
+ |
+ def ParseGYPOutputPath(self, path): |
+ # Do we need to handle absolute paths or relative paths? |
+ assert(path.startswith('//')) |
+ return path[2:] |
+ |
+ def ParseGYPConfigPath(self, path): |
+ # Do we need to handle absolute paths or relative paths? |
+ assert(path.startswith('//')) |
+ output_dir, _, config = path[2:].rpartition('/') |
+ self.CheckGYPConfigIsSupported(config, path) |
+ return output_dir, config |
+ |
+ def CheckGYPConfigIsSupported(self, config, path): |
+ if config not in ('Debug', 'Release'): |
+ if (sys.platform in ('win32', 'cygwin') and |
+ config not in ('Debug_x64', 'Release_x64')): |
+ raise _Err('Unknown or unsupported config type "%s" in "%s"' % |
+ config, path) |
+ |
+ def GYPCmd(self, output_dir, gyp_defines, config): |
+ cmd = [ |
+ sys.executable, |
+ os.path.join('build', 'gyp_chromium'), |
+ '-G', |
+ 'output_dir=' + output_dir |
+ ] |
+ if config: |
+ cmd += ['-G', 'config=' + config] |
+ for d in gyp_defines: |
+ cmd += ['-D', d] |
+ return cmd |
+ |
+ def CmdAnalyze(self): |
+ self.ReadConf() |
+ vals = self.FlattenBuildConfig(self.args.build_config) |
+ if vals['type'] == 'gn': |
+ self.RunGNAnalyze(vals) |
+ elif vals['type'] in ('gyp', 'gyp_one_config'): |
+ self.RunGypAnalyze(vals) |
+ else: |
+ raise _Err('Unknown meta-build type "%s"' % vals['type']) |
+ |
+ def RunGNAnalyze(self, vals): |
+ if self.args.dryrun: |
+ tmpdir = '//out/$tmpdir' |
+ else: |
+ tmpdir = tempfile.mkdtemp(prefix='analyze', suffix='tmp', |
+ dir=os.path.join(self.chromium_src_dir, 'out')) |
+ |
+ files = self.args.paths |
+ try: |
+ idx = files.index('-') |
+ targets = files[idx + 1:] |
+ files = files[:idx] |
+ except ValueError: |
+ targets = [] |
+ |
+ self.RunGN(tmpdir, vals['gn_args']) |
+ |
+ cmd = ['gn', 'refs', tmpdir] + files + ['--type=executable', |
+ '--all', '--as=output'] |
+ needed_targets = [] |
+ ret, out, _ = self.Call(cmd) |
+ |
+ # TODO: handle failures from 'gn refs' |
+ |
+ if self.args.dryrun or self.args.verbose: |
+ print('rm -fr', tmpdir) |
+ if not self.args.dryrun: |
+ shutil.rmtree(tmpdir, ignore_errors=True) |
+ |
+ rpath = os.path.relpath(tmpdir, self.chromium_src_dir) + os.sep |
+ needed_targets = [t.replace(rpath, '') for t in out.splitlines()] |
+ if targets: |
+ needed_targets = [nt for nt in needed_targets if nt in targets] |
+ |
+ for nt in needed_targets: |
+ print(nt) |
+ |
+ |
+class _Err(Exception): |
+ pass |
+ |
+ |
+if __name__ == '__main__': |
+ try: |
+ sys.exit(main(sys.argv[1:])) |
+ except _Err as e: |
+ print(e) |
+ sys.exit(1) |
+ except KeyboardInterrupt: |
+ print("interrupted, exiting", stream=sys.stderr) |
+ sys.exit(130) |