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 shlex | 19 import shlex |
20 import shutil | 20 import shutil |
21 import sys | 21 import sys |
22 import subprocess | 22 import subprocess |
23 | 23 |
24 | 24 |
25 def main(args): | 25 def main(args): |
26 mb = MetaBuildWrapper() | 26 mbw = MetaBuildWrapper() |
27 mb.ParseArgs(args) | 27 mbw.ParseArgs(args) |
28 return mb.args.func() | 28 return mbw.args.func() |
29 | 29 |
30 | 30 |
31 class MetaBuildWrapper(object): | 31 class MetaBuildWrapper(object): |
32 def __init__(self): | 32 def __init__(self): |
33 p = os.path | 33 p = os.path |
34 d = os.path.dirname | 34 d = os.path.dirname |
35 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__))))) | 35 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__))))) |
36 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb', | 36 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb', |
37 'mb_config.pyl') | 37 'mb_config.pyl') |
38 self.args = argparse.Namespace() | 38 self.args = argparse.Namespace() |
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
277 } | 277 } |
278 | 278 |
279 visited = [] | 279 visited = [] |
280 self.FlattenMixins(mixins, vals, visited) | 280 self.FlattenMixins(mixins, vals, visited) |
281 return vals | 281 return vals |
282 | 282 |
283 def FlattenMixins(self, mixins, vals, visited): | 283 def FlattenMixins(self, mixins, vals, visited): |
284 for m in mixins: | 284 for m in mixins: |
285 if m not in self.mixins: | 285 if m not in self.mixins: |
286 raise MBErr('Unknown mixin "%s"' % m) | 286 raise MBErr('Unknown mixin "%s"' % m) |
287 if m in visited: | 287 |
288 raise MBErr('Cycle in mixins for "%s": %s' % (m, visited)) | 288 # TODO: check for cycles in mixins. |
Dirk Pranke
2015/04/08 21:37:31
This logic didn't actually work right; it would er
| |
289 | 289 |
290 visited.append(m) | 290 visited.append(m) |
291 | 291 |
292 mixin_vals = self.mixins[m] | 292 mixin_vals = self.mixins[m] |
293 if 'type' in mixin_vals: | 293 if 'type' in mixin_vals: |
294 vals['type'] = mixin_vals['type'] | 294 vals['type'] = mixin_vals['type'] |
295 if 'gn_args' in mixin_vals: | 295 if 'gn_args' in mixin_vals: |
296 if vals['gn_args']: | 296 if vals['gn_args']: |
297 vals['gn_args'] += ' ' + mixin_vals['gn_args'] | 297 vals['gn_args'] += ' ' + mixin_vals['gn_args'] |
298 else: | 298 else: |
(...skipping 10 matching lines...) Expand all Loading... | |
309 return vals | 309 return vals |
310 | 310 |
311 def RunGNGen(self, path, vals): | 311 def RunGNGen(self, path, vals): |
312 cmd = self.GNCmd(path, vals['gn_args']) | 312 cmd = self.GNCmd(path, vals['gn_args']) |
313 ret, _, _ = self.Run(cmd) | 313 ret, _, _ = self.Run(cmd) |
314 return ret | 314 return ret |
315 | 315 |
316 def GNCmd(self, path, gn_args): | 316 def GNCmd(self, path, gn_args): |
317 # TODO(dpranke): Find gn explicitly in the path ... | 317 # TODO(dpranke): Find gn explicitly in the path ... |
318 cmd = ['gn', 'gen', path] | 318 cmd = ['gn', 'gen', path] |
319 gn_args = gn_args.replace("$(goma_dir)", gn_args) | 319 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir) |
320 if gn_args: | 320 if gn_args: |
321 cmd.append('--args=%s' % gn_args) | 321 cmd.append('--args=%s' % gn_args) |
322 return cmd | 322 return cmd |
323 | 323 |
324 def RunGYPGen(self, path, vals): | 324 def RunGYPGen(self, path, vals): |
325 output_dir, gyp_config = self.ParseGYPConfigPath(path) | 325 output_dir, gyp_config = self.ParseGYPConfigPath(path) |
326 if gyp_config != vals['gyp_config']: | 326 if gyp_config != vals['gyp_config']: |
327 raise MBErr('The last component of the path (%s) must match the ' | 327 raise MBErr('The last component of the path (%s) must match the ' |
328 'GYP configuration specified in the config (%s), and ' | 328 'GYP configuration specified in the config (%s), and ' |
329 'it does not.' % (gyp_config, vals['gyp_config'])) | 329 'it does not.' % (gyp_config, vals['gyp_config'])) |
330 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config) | 330 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config) |
331 ret, _, _ = self.Run(cmd) | 331 ret, _, _ = self.Run(cmd) |
332 return ret | 332 return ret |
333 | 333 |
334 def RunGYPAnalyze(self, vals): | 334 def RunGYPAnalyze(self, vals): |
335 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0]) | 335 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0]) |
336 if gyp_config != vals['gyp_config']: | 336 if gyp_config != vals['gyp_config']: |
337 raise MBErr('The last component of the path (%s) must match the ' | 337 raise MBErr('The last component of the path (%s) must match the ' |
338 'GYP configuration specified in the config (%s), and ' | 338 'GYP configuration specified in the config (%s), and ' |
339 'it does not.' % (gyp_config, vals['gyp_config'])) | 339 'it does not.' % (gyp_config, vals['gyp_config'])) |
340 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config) | 340 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config) |
341 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0], | 341 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0], |
342 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]]) | 342 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]]) |
343 ret, _, _ = self.Run(cmd) | 343 ret, _, _ = self.Run(cmd) |
344 return ret | 344 return ret |
345 | 345 |
346 def ParseGYPOutputPath(self, path): | 346 def ToSrcRelPath(self, path): |
347 """Returns a relative path from the top of the repo.""" | |
348 # TODO: Support normal paths in addition to source-absolute paths. | |
347 assert(path.startswith('//')) | 349 assert(path.startswith('//')) |
348 return path[2:] | 350 return path[2:] |
349 | 351 |
350 def ParseGYPConfigPath(self, path): | 352 def ParseGYPConfigPath(self, path): |
351 assert(path.startswith('//')) | 353 rpath = self.ToSrcRelPath(path) |
352 output_dir, _, config = path[2:].rpartition('/') | 354 output_dir, _, config = rpath.rpartition('/') |
353 self.CheckGYPConfigIsSupported(config, path) | 355 self.CheckGYPConfigIsSupported(config, path) |
354 return output_dir, config | 356 return output_dir, config |
355 | 357 |
356 def CheckGYPConfigIsSupported(self, config, path): | 358 def CheckGYPConfigIsSupported(self, config, path): |
357 if config not in ('Debug', 'Release'): | 359 if config not in ('Debug', 'Release'): |
358 if (sys.platform in ('win32', 'cygwin') and | 360 if (sys.platform in ('win32', 'cygwin') and |
359 config not in ('Debug_x64', 'Release_x64')): | 361 config not in ('Debug_x64', 'Release_x64')): |
360 raise MBErr('Unknown or unsupported config type "%s" in "%s"' % | 362 raise MBErr('Unknown or unsupported config type "%s" in "%s"' % |
361 config, path) | 363 config, path) |
362 | 364 |
(...skipping 19 matching lines...) Expand all Loading... | |
382 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']): | 384 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']): |
383 self.WriteJSONOutput({'status': 'Found dependency (all)'}) | 385 self.WriteJSONOutput({'status': 'Found dependency (all)'}) |
384 return 0 | 386 return 0 |
385 | 387 |
386 # TODO: Break long lists of files that might exceed the max command line | 388 # TODO: Break long lists of files that might exceed the max command line |
387 # up into chunks so that we can return more accurate info. | 389 # up into chunks so that we can return more accurate info. |
388 if len(' '.join(inp['files'])) > 1024: | 390 if len(' '.join(inp['files'])) > 1024: |
389 self.WriteJSONOutput({'status': 'Found dependency (all)'}) | 391 self.WriteJSONOutput({'status': 'Found dependency (all)'}) |
390 return 0 | 392 return 0 |
391 | 393 |
392 cmd = (['gn', 'refs', self.args.path[0]] + inp['files'] + | 394 cmd = (['gn', 'refs', self.args.path[0] ] + |
395 ['//' + f for f in inp['files']] + | |
393 ['--type=executable', '--all', '--as=output']) | 396 ['--type=executable', '--all', '--as=output']) |
394 needed_targets = [] | 397 needed_targets = [] |
395 ret, out, _ = self.Run(cmd) | 398 ret, out, _ = self.Run(cmd) |
396 | |
397 if ret: | 399 if ret: |
398 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out)) | 400 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out)) |
399 | 401 |
400 rpath = os.path.relpath(self.args.path[0], self.chromium_src_dir) + os.sep | 402 rpath = self.ToSrcRelPath(self.args.path[0]) + os.sep |
401 needed_targets = [t.replace(rpath, '') for t in out.splitlines()] | 403 needed_targets = [t.replace(rpath, '') for t in out.splitlines()] |
402 needed_targets = [nt for nt in needed_targets if nt in inp['targets']] | 404 needed_targets = [nt for nt in needed_targets if nt in inp['targets']] |
403 | 405 |
404 for nt in needed_targets: | |
405 self.Print(nt) | |
406 | |
407 if needed_targets: | 406 if needed_targets: |
408 # TODO: it could be that a target X might depend on a target Y | 407 # TODO: it could be that a target X might depend on a target Y |
409 # and both would be listed in the input, but we would only need | 408 # and both would be listed in the input, but we would only need |
410 # to specify target X as a build_target (whereas both X and Y are | 409 # to specify target X as a build_target (whereas both X and Y are |
411 # targets). I'm not sure if that optimization is generally worth it. | 410 # targets). I'm not sure if that optimization is generally worth it. |
412 self.WriteJSON({'targets': needed_targets, | 411 self.WriteJSON({'targets': needed_targets, |
413 'build_targets': needed_targets, | 412 'build_targets': needed_targets, |
414 'status': 'Found dependency'}) | 413 'status': 'Found dependency'}) |
415 else: | 414 else: |
416 self.WriteJSON({'targets': [], | 415 self.WriteJSON({'targets': [], |
(...skipping 20 matching lines...) Expand all Loading... | |
437 return inp | 436 return inp |
438 | 437 |
439 def WriteFailureAndRaise(self, msg): | 438 def WriteFailureAndRaise(self, msg): |
440 self.WriteJSON({'error': msg}) | 439 self.WriteJSON({'error': msg}) |
441 raise MBErr(msg) | 440 raise MBErr(msg) |
442 | 441 |
443 def WriteJSON(self, obj): | 442 def WriteJSON(self, obj): |
444 output_path = self.args.output_path[0] | 443 output_path = self.args.output_path[0] |
445 if output_path: | 444 if output_path: |
446 try: | 445 try: |
447 self.WriteFile(output_path, json.dumps(obj)) | 446 self.WriteFile(output_path, json.dumps(obj, indent=2) + '\n') |
448 except Exception as e: | 447 except Exception as e: |
449 raise MBErr('Error %s writing to the output path "%s"' % | 448 raise MBErr('Error %s writing to the output path "%s"' % |
450 (e, output_path)) | 449 (e, output_path)) |
451 | 450 |
452 def PrintCmd(self, cmd): | 451 def PrintCmd(self, cmd): |
453 if cmd[0] == sys.executable: | 452 if cmd[0] == sys.executable: |
454 cmd = ['python'] + cmd[1:] | 453 cmd = ['python'] + cmd[1:] |
455 self.Print(*[pipes.quote(c) for c in cmd]) | 454 self.Print(*[pipes.quote(c) for c in cmd]) |
456 | 455 |
457 def Print(self, *args, **kwargs): | 456 def Print(self, *args, **kwargs): |
458 # This function largely exists so it can be overridden for testing. | 457 # This function largely exists so it can be overridden for testing. |
459 print(*args, **kwargs) | 458 print(*args, **kwargs) |
460 | 459 |
461 def Run(self, cmd): | 460 def Run(self, cmd): |
462 # This function largely exists so it can be overridden for testing. | 461 # This function largely exists so it can be overridden for testing. |
463 if self.args.dryrun or self.args.verbose: | 462 if self.args.dryrun or self.args.verbose: |
464 self.PrintCmd(cmd) | 463 self.PrintCmd(cmd) |
465 if self.args.dryrun: | 464 if self.args.dryrun: |
466 return 0, '', '' | 465 return 0, '', '' |
467 ret, out, err = self.Call(cmd) | 466 ret, out, err = self.Call(cmd) |
468 if self.args.verbose: | 467 if self.args.verbose: |
469 if out: | 468 if out: |
470 self.Print(out) | 469 self.Print(out, end='') |
471 if err: | 470 if err: |
472 self.Print(err, file=sys.stderr) | 471 self.Print(err, end='', file=sys.stderr) |
473 return ret, out, err | 472 return ret, out, err |
474 | 473 |
475 def Call(self, cmd): | 474 def Call(self, cmd): |
476 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, | 475 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir, |
477 stdout=subprocess.PIPE, stderr=subprocess.PIPE) | 476 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
478 out, err = p.communicate() | 477 out, err = p.communicate() |
479 return p.returncode, out, err | 478 return p.returncode, out, err |
480 | 479 |
481 def ExpandUser(self, path): | 480 def ExpandUser(self, path): |
482 # This function largely exists so it can be overridden for testing. | 481 # This function largely exists so it can be overridden for testing. |
(...skipping 19 matching lines...) Expand all Loading... | |
502 | 501 |
503 if __name__ == '__main__': | 502 if __name__ == '__main__': |
504 try: | 503 try: |
505 sys.exit(main(sys.argv[1:])) | 504 sys.exit(main(sys.argv[1:])) |
506 except MBErr as e: | 505 except MBErr as e: |
507 print(e) | 506 print(e) |
508 sys.exit(1) | 507 sys.exit(1) |
509 except KeyboardInterrupt: | 508 except KeyboardInterrupt: |
510 print("interrupted, exiting", stream=sys.stderr) | 509 print("interrupted, exiting", stream=sys.stderr) |
511 sys.exit(130) | 510 sys.exit(130) |
OLD | NEW |