OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 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 |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """MB - the Meta-Build wrapper around GYP and GN |
| 7 |
| 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. |
| 10 """ |
| 11 |
| 12 from __future__ import print_function |
| 13 |
| 14 import argparse |
| 15 import ast |
| 16 import json |
| 17 import os |
| 18 import pipes |
| 19 import shutil |
| 20 import sys |
| 21 import subprocess |
| 22 import tempfile |
| 23 |
| 24 |
| 25 DEFAULT_BUILDS = { |
| 26 '//out': 'gyp' |
| 27 } |
| 28 |
| 29 |
| 30 def main(argv): |
| 31 mb = MetaBuildWrapper() |
| 32 mb.ParseArgs(argv) |
| 33 return mb.args.func() |
| 34 |
| 35 |
| 36 class MetaBuildWrapper(object): |
| 37 def __init__(self): |
| 38 p = os.path |
| 39 d = os.path.dirname |
| 40 self.chromium_src_dir = p.normpath(p.abspath(d(d(d(__file__))))) |
| 41 self.mb_conf_pyl = p.join(self.chromium_src_dir, 'build', 'mb_conf.pyl') |
| 42 self.builds_pyl = p.normpath(p.join(self.chromium_src_dir, '..', |
| 43 'builds.pyl')) |
| 44 self.build_configs = {} |
| 45 self.mixins = {} |
| 46 |
| 47 def ParseArgs(self, argv): |
| 48 parser = argparse.ArgumentParser(prog='mb') |
| 49 subps = parser.add_subparsers() |
| 50 |
| 51 subp = subps.add_parser('analyze', |
| 52 help='analyze whether changes to a set of files ' |
| 53 'will cause a set of binaries to be rebuilt.') |
| 54 subp.add_argument('-i', '--json-input', action='store', |
| 55 help='path to a file containing the input arguments ' |
| 56 'as a JSON object.') |
| 57 subp.add_argument('-o', '--json-output', action='store', |
| 58 help='path to a file containing the output arguments ' |
| 59 'as a JSON object.') |
| 60 subp.add_argument('-n', '--dryrun', action='store_true', |
| 61 help='Do a dry run (i.e., do nothing, just print ' |
| 62 'the commands') |
| 63 subp.add_argument('-v', '--verbose', action='count', |
| 64 help='verbose logging (may specify multiple times.') |
| 65 |
| 66 subp.add_argument('build_config', type=str, nargs='?', |
| 67 help='configuration to analyze') |
| 68 subp.add_argument('paths', type=str, nargs='*', |
| 69 help='list of files or targets to analyze') |
| 70 subp.set_defaults(func=self.CmdAnalyze) |
| 71 |
| 72 subp = subps.add_parser('gen', |
| 73 help='generate a new set of build files') |
| 74 subp.add_argument('-n', '--dryrun', action='store_true', |
| 75 help='Do a dry run (i.e., do nothing, just print ' |
| 76 'the commands') |
| 77 subp.add_argument('-v', '--verbose', action='count', |
| 78 help='verbose logging (may specify multiple times.') |
| 79 subp.add_argument('path', type=str, nargs='?', |
| 80 help='path to generate build into') |
| 81 subp.add_argument('build_config', type=str, nargs='?', |
| 82 help='build configuration to generate') |
| 83 subp.set_defaults(func=self.CmdGen) |
| 84 |
| 85 subp = subps.add_parser('help', |
| 86 help='Get help on a subcommand.') |
| 87 subp.add_argument(nargs='?', action='store', dest='subcommand', |
| 88 help='The command to get help for.') |
| 89 subp.set_defaults(func=self.CmdHelp) |
| 90 |
| 91 self.args = parser.parse_args(argv) |
| 92 |
| 93 def ReadConf(self): |
| 94 if not self.PathExists(self.mwb_conf_pyl): |
| 95 raise _Err('mwb conf file not found at %s' % self.mwb_conf_pyl) |
| 96 contents = self.ReadPyl(self.mwb_conf_pyl, 'mwb conf file') |
| 97 self.build_configs = contents['build_configs'] |
| 98 self.mixins = contents['mixins'] |
| 99 |
| 100 def PathExists(self, path): |
| 101 return os.path.exists(path) |
| 102 |
| 103 def ReadPyl(self, path, desc): |
| 104 try: |
| 105 return ast.literal_eval(open(path).read()) |
| 106 except SyntaxError as e: |
| 107 raise _Err('Failed to parse %s at "%s": %s' % (desc, path, e)) |
| 108 |
| 109 def Call(self, cmd): |
| 110 if self.args.dryrun or self.args.verbose: |
| 111 if cmd[0] == sys.executable: |
| 112 cmd = ['python'] + cmd[1:] |
| 113 print(*[pipes.quote(c) for c in cmd]) |
| 114 |
| 115 if self.args.dryrun: |
| 116 return 0, '', '' |
| 117 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, |
| 118 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 119 out, err = p.communicate() |
| 120 if self.args.verbose: |
| 121 if out: |
| 122 print(out) |
| 123 if err: |
| 124 print(err, stream=sys.stderr) |
| 125 return p.returncode, out, err |
| 126 |
| 127 def CmdHelp(self): |
| 128 if self.args.subcommand: |
| 129 self.ParseArgs([self.args.subcommand, '--help']) |
| 130 else: |
| 131 self.ParseArgs(['--help']) |
| 132 |
| 133 def CmdGen(self): |
| 134 self.ReadConf() |
| 135 |
| 136 if self.args.build_config and self.args.path: |
| 137 builds = self.BuildsFromArgs() |
| 138 elif os.path.exists(self.builds_pyl): |
| 139 builds = self.BuildsFromPyl() |
| 140 else: |
| 141 build = DEFAULT_BUILDS |
| 142 |
| 143 for path, build_config in builds.items(): |
| 144 vals = self.FlattenBuildConfig(build_config) |
| 145 if vals['type'] == 'gn': |
| 146 self.RunGN(path, vals['gn_args']) |
| 147 elif vals['type'] == 'gyp': |
| 148 self.RunGYP(path, vals['gyp_defines']) |
| 149 elif vals['type'] == 'gyp_one_config': |
| 150 self.RunGYPOneConfig(path, vals['gyp_defines']) |
| 151 else: |
| 152 raise _Err('Unknown meta-build type "%s"' % vals['type']) |
| 153 return 0 |
| 154 |
| 155 def BuildsFromArgs(self): |
| 156 builds = {} |
| 157 builds[self.args.path] = self.args.build_config |
| 158 return builds |
| 159 |
| 160 def BuildsFromPyl(self): |
| 161 return self.ReadPyl(self.builds_pyl, "build file") |
| 162 |
| 163 def RunGN(self, path, gn_args=''): |
| 164 cmd = self.GNCmd(path, gn_args) |
| 165 ret, _, _ = self.Call(cmd) |
| 166 return ret |
| 167 |
| 168 def FlattenBuildConfig(self, build_config): |
| 169 if build_config not in self.build_configs: |
| 170 raise _Err('Unknown build config "%s"' % build_config) |
| 171 if self.build_configs[build_config] is None: |
| 172 mixins = build_config.split('_') |
| 173 else: |
| 174 mixins = self.build_configs[build_config] |
| 175 |
| 176 vals = { |
| 177 'type': None, |
| 178 'gn_args': [], |
| 179 'gyp_config': [], |
| 180 'gyp_defines': [], |
| 181 } |
| 182 |
| 183 visited = [] |
| 184 self.FlattenMixins(mixins, vals, visited) |
| 185 return vals |
| 186 |
| 187 def FlattenMixins(self, mixins, vals, visited): |
| 188 for m in mixins: |
| 189 if m not in self.mixins: |
| 190 raise _Err('Unknown mixin "%s"' % m) |
| 191 if m in visited: |
| 192 raise _Err('Cycle in build configs for "%s": %s' % (config, visited)) |
| 193 |
| 194 visited.append(m) |
| 195 |
| 196 mixin_vals = self.mixins[m] |
| 197 if 'type' in mixin_vals: |
| 198 vals['type'] = mixin_vals['type'] |
| 199 if 'gn_args' in mixin_vals: |
| 200 vals['gn_args'].extend(mixin_vals['gn_args']) |
| 201 if 'gyp_config' in mixin_vals: |
| 202 vals['gyp_config'] = mixin_vals['gyp_config'] |
| 203 if 'gyp_defines' in mixin_vals: |
| 204 vals['gyp_defines'].extend(mixin_vals['gyp_defines']) |
| 205 if 'mixins' in mixin_vals: |
| 206 self.FlattenMixins(mixin_vals['mixins'], vals, visited) |
| 207 return vals |
| 208 |
| 209 def GNCmd(self, path, gn_args): |
| 210 # TODO(dpranke): Find gn explicitly in the path ... |
| 211 cmd = ['gn', 'gen', path] |
| 212 if gn_args: |
| 213 cmd.append('--args=%s' % ' '.join(gn_args)) |
| 214 return cmd |
| 215 |
| 216 def RunGYP(self, path, gyp_defines): |
| 217 output_dir = self.ParseGYPOutputPath(path) |
| 218 cmd = self.GYPCmd(output_dir, gyp_defines, config=None) |
| 219 ret, _, _ = self.Call(cmd) |
| 220 return ret |
| 221 |
| 222 def RunGYPConfig(self, path, gyp_defines): |
| 223 output_dir, gyp_config = self.ParseGYPConfigPath(path) |
| 224 cmd = self.GYPCmd(output_dir, gyp_defines, config=gyp_config) |
| 225 ret, _, _ = self.Call(cmd) |
| 226 return ret |
| 227 |
| 228 def ParseGYPOutputPath(self, path): |
| 229 # Do we need to handle absolute paths or relative paths? |
| 230 assert(path.startswith('//')) |
| 231 return path[2:] |
| 232 |
| 233 def ParseGYPConfigPath(self, path): |
| 234 # Do we need to handle absolute paths or relative paths? |
| 235 assert(path.startswith('//')) |
| 236 output_dir, _, config = path[2:].rpartition('/') |
| 237 self.CheckGYPConfigIsSupported(config, path) |
| 238 return output_dir, config |
| 239 |
| 240 def CheckGYPConfigIsSupported(self, config, path): |
| 241 if config not in ('Debug', 'Release'): |
| 242 if (sys.platform in ('win32', 'cygwin') and |
| 243 config not in ('Debug_x64', 'Release_x64')): |
| 244 raise _Err('Unknown or unsupported config type "%s" in "%s"' % |
| 245 config, path) |
| 246 |
| 247 def GYPCmd(self, output_dir, gyp_defines, config): |
| 248 cmd = [ |
| 249 sys.executable, |
| 250 os.path.join('build', 'gyp_chromium'), |
| 251 '-G', |
| 252 'output_dir=' + output_dir |
| 253 ] |
| 254 if config: |
| 255 cmd += ['-G', 'config=' + config] |
| 256 for d in gyp_defines: |
| 257 cmd += ['-D', d] |
| 258 return cmd |
| 259 |
| 260 def CmdAnalyze(self): |
| 261 self.ReadConf() |
| 262 vals = self.FlattenBuildConfig(self.args.build_config) |
| 263 if vals['type'] == 'gn': |
| 264 self.RunGNAnalyze(vals) |
| 265 elif vals['type'] in ('gyp', 'gyp_one_config'): |
| 266 self.RunGypAnalyze(vals) |
| 267 else: |
| 268 raise _Err('Unknown meta-build type "%s"' % vals['type']) |
| 269 |
| 270 def RunGNAnalyze(self, vals): |
| 271 if self.args.dryrun: |
| 272 tmpdir = '//out/$tmpdir' |
| 273 else: |
| 274 tmpdir = tempfile.mkdtemp(prefix='analyze', suffix='tmp', |
| 275 dir=os.path.join(self.chromium_src_dir, 'out')) |
| 276 |
| 277 files = self.args.paths |
| 278 try: |
| 279 idx = files.index('-') |
| 280 targets = files[idx + 1:] |
| 281 files = files[:idx] |
| 282 except ValueError: |
| 283 targets = [] |
| 284 |
| 285 self.RunGN(tmpdir, vals['gn_args']) |
| 286 |
| 287 cmd = ['gn', 'refs', tmpdir] + files + ['--type=executable', |
| 288 '--all', '--as=output'] |
| 289 needed_targets = [] |
| 290 ret, out, _ = self.Call(cmd) |
| 291 |
| 292 # TODO: handle failures from 'gn refs' |
| 293 |
| 294 if self.args.dryrun or self.args.verbose: |
| 295 print('rm -fr', tmpdir) |
| 296 if not self.args.dryrun: |
| 297 shutil.rmtree(tmpdir, ignore_errors=True) |
| 298 |
| 299 rpath = os.path.relpath(tmpdir, self.chromium_src_dir) + os.sep |
| 300 needed_targets = [t.replace(rpath, '') for t in out.splitlines()] |
| 301 if targets: |
| 302 needed_targets = [nt for nt in needed_targets if nt in targets] |
| 303 |
| 304 for nt in needed_targets: |
| 305 print(nt) |
| 306 |
| 307 |
| 308 class _Err(Exception): |
| 309 pass |
| 310 |
| 311 |
| 312 if __name__ == '__main__': |
| 313 try: |
| 314 sys.exit(main(sys.argv[1:])) |
| 315 except _Err as e: |
| 316 print(e) |
| 317 sys.exit(1) |
| 318 except KeyboardInterrupt: |
| 319 print("interrupted, exiting", stream=sys.stderr) |
| 320 sys.exit(130) |
OLD | NEW |