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 shlex |
| 20 import shutil |
| 21 import sys |
| 22 import subprocess |
| 23 import tempfile |
| 24 |
| 25 |
| 26 def main(args): |
| 27 mb = MetaBuildWrapper() |
| 28 mb.ParseArgs(args) |
| 29 return mb.args.func() |
| 30 |
| 31 |
| 32 class MetaBuildWrapper(object): |
| 33 def __init__(self): |
| 34 p = os.path |
| 35 d = os.path.dirname |
| 36 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__))))) |
| 37 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb', |
| 38 'mb_config.pyl') |
| 39 self.configs = {} |
| 40 self.masters = {} |
| 41 self.mixins = {} |
| 42 |
| 43 def ParseArgs(self, argv): |
| 44 def AddCommonOptions(subp): |
| 45 subp.add_argument('-b', '--builder', |
| 46 help='builder name to look up config from') |
| 47 subp.add_argument('-m', '--master', |
| 48 help='master name to look up config from'), |
| 49 subp.add_argument('-c', '--config', |
| 50 help='configuration to analyze') |
| 51 subp.add_argument('-f', '--config-file', metavar='PATH', |
| 52 default=self.default_config, |
| 53 help='path to config file ' |
| 54 '(default is //tools/mb/mb_config.pyl)') |
| 55 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'), |
| 56 help='path to goma directory (default is %(default)s).') |
| 57 subp.add_argument('-n', '--dryrun', action='store_true', |
| 58 help='Do a dry run (i.e., do nothing, just print ' |
| 59 'the commands that will run)') |
| 60 subp.add_argument('-q', '--quiet', action='store_true', |
| 61 help='Do not print anything, just return an exit ' |
| 62 'code.') |
| 63 subp.add_argument('-v', '--verbose', action='count', |
| 64 help='verbose logging (may specify multiple times).') |
| 65 |
| 66 parser = argparse.ArgumentParser(prog='mb') |
| 67 subps = parser.add_subparsers() |
| 68 |
| 69 subp = subps.add_parser('analyze', |
| 70 help='analyze whether changes to a set of files ' |
| 71 'will cause a set of binaries to be rebuilt.') |
| 72 AddCommonOptions(subp) |
| 73 subp.add_argument('input-path', nargs=1, |
| 74 help='path to a file containing the input arguments ' |
| 75 'as a JSON object.') |
| 76 subp.add_argument('output-path', nargs=1, |
| 77 help='path to a file containing the output arguments ' |
| 78 'as a JSON object.') |
| 79 subp.set_defaults(func=self.CmdAnalyze) |
| 80 |
| 81 subp = subps.add_parser('gen', |
| 82 help='generate a new set of build files') |
| 83 AddCommonOptions(subp) |
| 84 subp.add_argument('path', type=str, nargs=1, |
| 85 help='path to generate build into') |
| 86 subp.set_defaults(func=self.CmdGen) |
| 87 |
| 88 subp = subps.add_parser('lookup', |
| 89 help='look up the command for a given config or ' |
| 90 'builder') |
| 91 AddCommonOptions(subp) |
| 92 subp.set_defaults(func=self.CmdLookup) |
| 93 |
| 94 subp = subps.add_parser('validate', |
| 95 help='validate the config file') |
| 96 AddCommonOptions(subp) |
| 97 subp.set_defaults(func=self.CmdValidate) |
| 98 |
| 99 subp = subps.add_parser('help', |
| 100 help='Get help on a subcommand.') |
| 101 subp.add_argument(nargs='?', action='store', dest='subcommand', |
| 102 help='The command to get help for.') |
| 103 subp.set_defaults(func=self.CmdHelp) |
| 104 |
| 105 self.args = parser.parse_args(argv) |
| 106 |
| 107 def CmdAnalyze(self): |
| 108 vals = self.GetConfig() |
| 109 if vals['type'] == 'gn': |
| 110 self.RunGNAnalyze(vals) |
| 111 elif vals['type'] in ('gyp', 'gyp_one_config'): |
| 112 self.RunGypAnalyze(vals) |
| 113 else: |
| 114 raise MBErr('Unknown meta-build type "%s"' % vals['type']) |
| 115 |
| 116 def CmdGen(self): |
| 117 vals = self.GetConfig() |
| 118 if vals['type'] == 'gn': |
| 119 self.RunGNGen(self.args.path[0], vals['gn_args']) |
| 120 elif vals['type'] == 'gyp': |
| 121 self.RunGYPGen(self.args.path[0], vals['gyp_defines']) |
| 122 else: |
| 123 raise MBErr('Unknown meta-build type "%s"' % vals['type']) |
| 124 return 0 |
| 125 |
| 126 def CmdLookup(self): |
| 127 vals = self.GetConfig() |
| 128 if vals['type'] == 'gn': |
| 129 cmd = self.GNCmd('<path>', vals['gn_args']) |
| 130 elif vals['type'] == 'gyp': |
| 131 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config']) |
| 132 else: |
| 133 raise MBErr('Unknown meta-build type "%s"' % vals['type']) |
| 134 |
| 135 self.PrintCmd(cmd) |
| 136 return 0 |
| 137 |
| 138 def CmdHelp(self): |
| 139 if self.args.subcommand: |
| 140 self.ParseArgs([self.args.subcommand, '--help']) |
| 141 else: |
| 142 self.ParseArgs(['--help']) |
| 143 |
| 144 def CmdValidate(self): |
| 145 errs = [] |
| 146 |
| 147 # Read the file to make sure it parses. |
| 148 self.ReadConfigFile() |
| 149 |
| 150 # Figure out the whole list of configs and ensure that no config is |
| 151 # listed in more than one category. |
| 152 all_configs = {} |
| 153 for config in self.common_dev_configs: |
| 154 all_configs[config] = 'common_dev_configs' |
| 155 for config in self.private_configs: |
| 156 if config in all_configs: |
| 157 errs.append('config "%s" listed in "private_configs" also ' |
| 158 'listed in "%s"' % (config, all_configs['config'])) |
| 159 else: |
| 160 all_configs[config] = 'private_configs' |
| 161 for config in self.unsupported_configs: |
| 162 if config in all_configs: |
| 163 errs.append('config "%s" listed in "unsupported_configs" also ' |
| 164 'listed in "%s"' % (config, all_configs['config'])) |
| 165 else: |
| 166 all_configs[config] = 'unsupported_configs' |
| 167 |
| 168 for master in self.masters: |
| 169 for builder in self.masters[master]: |
| 170 config = self.masters[master][builder] |
| 171 if config in all_configs and all_configs[config] not in self.masters: |
| 172 errs.append('Config "%s" used by a bot is also listed in "%s".' % |
| 173 (config, all_configs[config])) |
| 174 else: |
| 175 all_configs[config] = master |
| 176 |
| 177 # Check that every referenced config actually exists. |
| 178 for config, loc in all_configs.items(): |
| 179 if not config in self.configs: |
| 180 errs.append('Unknown config "%s" referenced from "%s".' % |
| 181 (config, loc)) |
| 182 |
| 183 # Check that every actual config is actually referenced. |
| 184 for config in self.configs: |
| 185 if not config in all_configs: |
| 186 errs.append('Unused config "%s".' % config) |
| 187 |
| 188 # Figure out the whole list of mixins, and check that every mixin |
| 189 # listed by a config or another mixin actually exists. |
| 190 referenced_mixins = set() |
| 191 for config, mixins in self.configs.items(): |
| 192 for mixin in mixins: |
| 193 if not mixin in self.mixins: |
| 194 errs.append('Unknown mixin "%s" referenced by config "%s".' % |
| 195 (mixin, config)) |
| 196 referenced_mixins.add(mixin) |
| 197 |
| 198 for mixin in self.mixins: |
| 199 for sub_mixin in self.mixins[mixin].get('mixins', []): |
| 200 if not sub_mixin in self.mixins: |
| 201 errs.append('Unknown mixin "%s" referenced by mixin "%s".' % |
| 202 (sub_mixin, mixin)) |
| 203 referenced_mixins.add(sub_mixin) |
| 204 |
| 205 # Check that every mixin defined is actually referenced somewhere. |
| 206 for mixin in self.mixins: |
| 207 if not mixin in referenced_mixins: |
| 208 errs.append('Unreferenced mixin "%s".' % mixin) |
| 209 |
| 210 if errs: |
| 211 raise MBErr('mb config file %s has problems:\n ' + '\n '.join(errs)) |
| 212 |
| 213 if not self.args.quiet: |
| 214 self.Print('mb config file %s looks ok.' % self.args.config_file) |
| 215 return 0 |
| 216 |
| 217 def GetConfig(self): |
| 218 self.ReadConfigFile() |
| 219 config = self.ConfigFromArgs() |
| 220 if not config in self.configs: |
| 221 raise MBErr('Config "%s" not found in %s' % |
| 222 (config, self.args.config_file)) |
| 223 |
| 224 return self.FlattenConfig(config) |
| 225 |
| 226 def ReadConfigFile(self): |
| 227 if not self.Exists(self.args.config_file): |
| 228 raise MBErr('config file not found at %s' % self.args.config_file) |
| 229 |
| 230 try: |
| 231 contents = ast.literal_eval(self.ReadFile(self.args.config_file)) |
| 232 except SyntaxError as e: |
| 233 raise MBErr('Failed to parse config file "%s": %s' % |
| 234 (self.args.config_file, e)) |
| 235 |
| 236 self.common_dev_configs = contents['common_dev_configs'] |
| 237 self.configs = contents['configs'] |
| 238 self.masters = contents['masters'] |
| 239 self.mixins = contents['mixins'] |
| 240 self.private_configs = contents['private_configs'] |
| 241 self.unsupported_configs = contents['unsupported_configs'] |
| 242 |
| 243 def ConfigFromArgs(self): |
| 244 if self.args.config: |
| 245 if self.args.master or self.args.builder: |
| 246 raise MBErr('Can not specific both -c/--config and -m/--master or ' |
| 247 '-b/--builder') |
| 248 |
| 249 return self.args.config |
| 250 |
| 251 if not self.args.master or not self.args.builder: |
| 252 raise MBErr('Must specify either -c/--config or ' |
| 253 '(-m/--master and -b/--builder)') |
| 254 |
| 255 if not self.args.master in self.masters: |
| 256 raise MBErr('Master name "%s" not found in "%s"' % |
| 257 (self.args.master, self.args.config_file)) |
| 258 |
| 259 if not self.args.builder in self.masters[self.args.master]: |
| 260 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' % |
| 261 (self.args.builder, self.args.master, self.args.config_file)) |
| 262 |
| 263 return self.masters[self.args.master][self.args.builder] |
| 264 |
| 265 def FlattenConfig(self, config): |
| 266 mixins = self.configs[config] |
| 267 vals = { |
| 268 'type': None, |
| 269 'gn_args': [], |
| 270 'gyp_config': [], |
| 271 'gyp_defines': [], |
| 272 } |
| 273 |
| 274 visited = [] |
| 275 self.FlattenMixins(mixins, vals, visited) |
| 276 return vals |
| 277 |
| 278 def FlattenMixins(self, mixins, vals, visited): |
| 279 for m in mixins: |
| 280 if m not in self.mixins: |
| 281 raise MBErr('Unknown mixin "%s"' % m) |
| 282 if m in visited: |
| 283 raise MBErr('Cycle in build configs for "%s": %s' % (config, visited)) |
| 284 |
| 285 visited.append(m) |
| 286 |
| 287 mixin_vals = self.mixins[m] |
| 288 if 'type' in mixin_vals: |
| 289 vals['type'] = mixin_vals['type'] |
| 290 if 'gn_args' in mixin_vals: |
| 291 if vals['gn_args']: |
| 292 vals['gn_args'] += ' ' + mixin_vals['gn_args'] |
| 293 else: |
| 294 vals['gn_args'] = mixin_vals['gn_args'] |
| 295 if 'gyp_config' in mixin_vals: |
| 296 vals['gyp_config'] = mixin_vals['gyp_config'] |
| 297 if 'gyp_defines' in mixin_vals: |
| 298 if vals['gyp_defines']: |
| 299 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines'] |
| 300 else: |
| 301 vals['gyp_defines'] = mixin_vals['gyp_defines'] |
| 302 if 'mixins' in mixin_vals: |
| 303 self.FlattenMixins(mixin_vals['mixins'], vals, visited) |
| 304 return vals |
| 305 |
| 306 def RunGNGen(self, path, gn_args): |
| 307 cmd = self.GNCmd(path, gn_args) |
| 308 ret, _, _ = self.Run(cmd) |
| 309 return ret |
| 310 |
| 311 def GNCmd(self, path, gn_args): |
| 312 # TODO(dpranke): Find gn explicitly in the path ... |
| 313 cmd = ['gn', 'gen', path] |
| 314 gn_args = gn_args.replace("$(goma_dir)", gn_args) |
| 315 if gn_args: |
| 316 cmd.append('--args=%s' % ' '.join(gn_args)) |
| 317 return cmd |
| 318 |
| 319 def RunGYPGen(self, path, gyp_defines): |
| 320 output_dir, gyp_config = self.ParseGYPConfigPath(path) |
| 321 if gyp_config != vals['gyp_config']: |
| 322 raise MBErr('The last component of the path (%s) must match the ' |
| 323 'GYP configuration specified in the config (%s), and ' |
| 324 'it does not.' % (gyp_config, vals['gyp_config'])) |
| 325 cmd = self.GYPCmd(output_dir, gyp_defines, config=gyp_config) |
| 326 ret, _, _ = self.Run(cmd) |
| 327 return ret |
| 328 |
| 329 def ParseGYPOutputPath(self, path): |
| 330 assert(path.startswith('//')) |
| 331 return path[2:] |
| 332 |
| 333 def ParseGYPConfigPath(self, path): |
| 334 assert(path.startswith('//')) |
| 335 output_dir, _, config = path[2:].rpartition('/') |
| 336 self.CheckGYPConfigIsSupported(config, path) |
| 337 return output_dir, config |
| 338 |
| 339 def CheckGYPConfigIsSupported(self, config, path): |
| 340 if config not in ('Debug', 'Release'): |
| 341 if (sys.platform in ('win32', 'cygwin') and |
| 342 config not in ('Debug_x64', 'Release_x64')): |
| 343 raise MBErr('Unknown or unsupported config type "%s" in "%s"' % |
| 344 config, path) |
| 345 |
| 346 def GYPCmd(self, output_dir, gyp_defines, config): |
| 347 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir) |
| 348 cmd = [ |
| 349 sys.executable, |
| 350 os.path.join('build', 'gyp_chromium'), |
| 351 '-G', |
| 352 'output_dir=' + output_dir, |
| 353 '-G', |
| 354 'config=' + config, |
| 355 ] |
| 356 for d in shlex.split(gyp_defines): |
| 357 cmd += ['-D', d] |
| 358 return cmd |
| 359 |
| 360 def RunGNAnalyze(self, vals): |
| 361 # TODO: Skip this if a valid gn build already exists, which it should on |
| 362 # the bots. |
| 363 if self.args.dryrun: |
| 364 tmpdir = '//out/$tmpdir' |
| 365 else: |
| 366 tmpdir = tempfile.mkdtemp(prefix='analyze', suffix='tmp', |
| 367 dir=os.path.join(self.chromium_src_dir, 'out')) |
| 368 |
| 369 files = self.args.paths |
| 370 try: |
| 371 idx = files.index('-') |
| 372 targets = files[idx + 1:] |
| 373 files = files[:idx] |
| 374 except ValueError: |
| 375 targets = [] |
| 376 |
| 377 self.RunGN(tmpdir, vals['gn_args']) |
| 378 |
| 379 cmd = ['gn', 'refs', tmpdir] + files + ['--type=executable', |
| 380 '--all', '--as=output'] |
| 381 needed_targets = [] |
| 382 ret, out, _ = self.Run(cmd) |
| 383 |
| 384 # TODO: handle failures from 'gn refs' |
| 385 |
| 386 if self.args.dryrun or self.args.verbose: |
| 387 self.Print('rm -fr', tmpdir) |
| 388 if not self.args.dryrun: |
| 389 shutil.rmtree(tmpdir, ignore_errors=True) |
| 390 |
| 391 rpath = os.path.relpath(tmpdir, self.chromium_src_dir) + os.sep |
| 392 needed_targets = [t.replace(rpath, '') for t in out.splitlines()] |
| 393 if targets: |
| 394 needed_targets = [nt for nt in needed_targets if nt in targets] |
| 395 |
| 396 for nt in needed_targets: |
| 397 self.Print(nt) |
| 398 |
| 399 def PrintCmd(self, cmd): |
| 400 if cmd[0] == sys.executable: |
| 401 cmd = ['python'] + cmd[1:] |
| 402 self.Print(*[pipes.quote(c) for c in cmd]) |
| 403 |
| 404 def Print(self, *args, **kwargs): |
| 405 # This function largely exists so it can be overridden for testing. |
| 406 print(*args, **kwargs) |
| 407 |
| 408 def Run(self, cmd): |
| 409 # This function largely exists so it can be overridden for testing. |
| 410 if self.args.dryrun or self.args.verbose: |
| 411 self.PrintCmd(cmd) |
| 412 if self.args.dryrun: |
| 413 return 0, '', '' |
| 414 if self.args.verbose: |
| 415 if out: |
| 416 self.Print(out) |
| 417 if err: |
| 418 self.Print(err, file=sys.stderr) |
| 419 return self.Call(cmd) |
| 420 |
| 421 def Call(self, cmd): |
| 422 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, |
| 423 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 424 out, err = p.communicate() |
| 425 return p.returncode, out, err |
| 426 |
| 427 def ExpandUser(self, path): |
| 428 # This function largely exists so it can be overridden for testing. |
| 429 return os.path.expanduser(path) |
| 430 |
| 431 def Exists(self, path): |
| 432 # This function largely exists so it can be overridden for testing. |
| 433 return os.path.exists(path) |
| 434 |
| 435 def ReadFile(self, path): |
| 436 # This function largely exists so it can be overriden for testing. |
| 437 with open(path) as fp: |
| 438 return fp.read() |
| 439 |
| 440 |
| 441 class MBErr(Exception): |
| 442 pass |
| 443 |
| 444 |
| 445 if __name__ == '__main__': |
| 446 try: |
| 447 sys.exit(main(sys.argv[1:])) |
| 448 except Err as e: |
| 449 print(e) |
| 450 sys.exit(1) |
| 451 except KeyboardInterrupt: |
| 452 print("interrupted, exiting", stream=sys.stderr) |
| 453 sys.exit(130) |
OLD | NEW |