Remove the 'gyp_config' concept from MB.
[chromium-blink-merge.git] / tools / mb / mb.py
blob507fba05496e067c9702cf7bd88581993bf6662a
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.
6 """MB - the Meta-Build wrapper around GYP and GN
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 """
12 from __future__ import print_function
14 import argparse
15 import ast
16 import errno
17 import json
18 import os
19 import pipes
20 import pprint
21 import shlex
22 import shutil
23 import sys
24 import subprocess
25 import tempfile
27 def main(args):
28 mbw = MetaBuildWrapper()
29 mbw.ParseArgs(args)
30 return mbw.args.func()
33 class MetaBuildWrapper(object):
34 def __init__(self):
35 p = os.path
36 d = os.path.dirname
37 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
38 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
39 'mb_config.pyl')
40 self.platform = sys.platform
41 self.args = argparse.Namespace()
42 self.configs = {}
43 self.masters = {}
44 self.mixins = {}
45 self.private_configs = []
46 self.common_dev_configs = []
47 self.unsupported_configs = []
49 def ParseArgs(self, argv):
50 def AddCommonOptions(subp):
51 subp.add_argument('-b', '--builder',
52 help='builder name to look up config from')
53 subp.add_argument('-m', '--master',
54 help='master name to look up config from')
55 subp.add_argument('-c', '--config',
56 help='configuration to analyze')
57 subp.add_argument('-f', '--config-file', metavar='PATH',
58 default=self.default_config,
59 help='path to config file '
60 '(default is //tools/mb/mb_config.pyl)')
61 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
62 help='path to goma directory (default is %(default)s).')
63 subp.add_argument('-n', '--dryrun', action='store_true',
64 help='Do a dry run (i.e., do nothing, just print '
65 'the commands that will run)')
66 subp.add_argument('-v', '--verbose', action='store_true',
67 help='verbose logging')
69 parser = argparse.ArgumentParser(prog='mb')
70 subps = parser.add_subparsers()
72 subp = subps.add_parser('analyze',
73 help='analyze whether changes to a set of files '
74 'will cause a set of binaries to be rebuilt.')
75 AddCommonOptions(subp)
76 subp.add_argument('--swarming-targets-file',
77 help='save runtime dependencies for targets listed '
78 'in file.')
79 subp.add_argument('path', nargs=1,
80 help='path build was generated into.')
81 subp.add_argument('input_path', nargs=1,
82 help='path to a file containing the input arguments '
83 'as a JSON object.')
84 subp.add_argument('output_path', nargs=1,
85 help='path to a file containing the output arguments '
86 'as a JSON object.')
87 subp.set_defaults(func=self.CmdAnalyze)
89 subp = subps.add_parser('gen',
90 help='generate a new set of build files')
91 AddCommonOptions(subp)
92 subp.add_argument('--swarming-targets-file',
93 help='save runtime dependencies for targets listed '
94 'in file.')
95 subp.add_argument('path', nargs=1,
96 help='path to generate build into')
97 subp.set_defaults(func=self.CmdGen)
99 subp = subps.add_parser('lookup',
100 help='look up the command for a given config or '
101 'builder')
102 AddCommonOptions(subp)
103 subp.set_defaults(func=self.CmdLookup)
105 subp = subps.add_parser('validate',
106 help='validate the config file')
107 subp.add_argument('-f', '--config-file', metavar='PATH',
108 default=self.default_config,
109 help='path to config file '
110 '(default is //tools/mb/mb_config.pyl)')
111 subp.set_defaults(func=self.CmdValidate)
113 subp = subps.add_parser('help',
114 help='Get help on a subcommand.')
115 subp.add_argument(nargs='?', action='store', dest='subcommand',
116 help='The command to get help for.')
117 subp.set_defaults(func=self.CmdHelp)
119 self.args = parser.parse_args(argv)
121 def CmdAnalyze(self):
122 vals = self.GetConfig()
123 if vals['type'] == 'gn':
124 return self.RunGNAnalyze(vals)
125 elif vals['type'] == 'gyp':
126 return self.RunGYPAnalyze(vals)
127 else:
128 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
130 def CmdGen(self):
131 vals = self.GetConfig()
133 self.ClobberIfNeeded(vals)
135 if vals['type'] == 'gn':
136 return self.RunGNGen(vals)
137 if vals['type'] == 'gyp':
138 return self.RunGYPGen(vals)
140 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
142 def CmdLookup(self):
143 vals = self.GetConfig()
144 if vals['type'] == 'gn':
145 cmd = self.GNCmd('gen', '<path>', vals['gn_args'])
146 elif vals['type'] == 'gyp':
147 if vals['gyp_crosscompile']:
148 self.Print('GYP_CROSSCOMPILE=1')
149 cmd = self.GYPCmd('<path>', vals['gyp_defines'])
150 else:
151 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
153 self.PrintCmd(cmd)
154 return 0
156 def CmdHelp(self):
157 if self.args.subcommand:
158 self.ParseArgs([self.args.subcommand, '--help'])
159 else:
160 self.ParseArgs(['--help'])
162 def CmdValidate(self):
163 errs = []
165 # Read the file to make sure it parses.
166 self.ReadConfigFile()
168 # Figure out the whole list of configs and ensure that no config is
169 # listed in more than one category.
170 all_configs = {}
171 for config in self.common_dev_configs:
172 all_configs[config] = 'common_dev_configs'
173 for config in self.private_configs:
174 if config in all_configs:
175 errs.append('config "%s" listed in "private_configs" also '
176 'listed in "%s"' % (config, all_configs['config']))
177 else:
178 all_configs[config] = 'private_configs'
179 for config in self.unsupported_configs:
180 if config in all_configs:
181 errs.append('config "%s" listed in "unsupported_configs" also '
182 'listed in "%s"' % (config, all_configs['config']))
183 else:
184 all_configs[config] = 'unsupported_configs'
186 for master in self.masters:
187 for builder in self.masters[master]:
188 config = self.masters[master][builder]
189 if config in all_configs and all_configs[config] not in self.masters:
190 errs.append('Config "%s" used by a bot is also listed in "%s".' %
191 (config, all_configs[config]))
192 else:
193 all_configs[config] = master
195 # Check that every referenced config actually exists.
196 for config, loc in all_configs.items():
197 if not config in self.configs:
198 errs.append('Unknown config "%s" referenced from "%s".' %
199 (config, loc))
201 # Check that every actual config is actually referenced.
202 for config in self.configs:
203 if not config in all_configs:
204 errs.append('Unused config "%s".' % config)
206 # Figure out the whole list of mixins, and check that every mixin
207 # listed by a config or another mixin actually exists.
208 referenced_mixins = set()
209 for config, mixins in self.configs.items():
210 for mixin in mixins:
211 if not mixin in self.mixins:
212 errs.append('Unknown mixin "%s" referenced by config "%s".' %
213 (mixin, config))
214 referenced_mixins.add(mixin)
216 for mixin in self.mixins:
217 for sub_mixin in self.mixins[mixin].get('mixins', []):
218 if not sub_mixin in self.mixins:
219 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
220 (sub_mixin, mixin))
221 referenced_mixins.add(sub_mixin)
223 # Check that every mixin defined is actually referenced somewhere.
224 for mixin in self.mixins:
225 if not mixin in referenced_mixins:
226 errs.append('Unreferenced mixin "%s".' % mixin)
228 if errs:
229 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
230 '\n ' + '\n '.join(errs))
232 self.Print('mb config file %s looks ok.' % self.args.config_file)
233 return 0
235 def GetConfig(self):
236 self.ReadConfigFile()
237 config = self.ConfigFromArgs()
238 if not config in self.configs:
239 raise MBErr('Config "%s" not found in %s' %
240 (config, self.args.config_file))
242 return self.FlattenConfig(config)
244 def ReadConfigFile(self):
245 if not self.Exists(self.args.config_file):
246 raise MBErr('config file not found at %s' % self.args.config_file)
248 try:
249 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
250 except SyntaxError as e:
251 raise MBErr('Failed to parse config file "%s": %s' %
252 (self.args.config_file, e))
254 self.common_dev_configs = contents['common_dev_configs']
255 self.configs = contents['configs']
256 self.masters = contents['masters']
257 self.mixins = contents['mixins']
258 self.private_configs = contents['private_configs']
259 self.unsupported_configs = contents['unsupported_configs']
261 def ConfigFromArgs(self):
262 if self.args.config:
263 if self.args.master or self.args.builder:
264 raise MBErr('Can not specific both -c/--config and -m/--master or '
265 '-b/--builder')
267 return self.args.config
269 if not self.args.master or not self.args.builder:
270 raise MBErr('Must specify either -c/--config or '
271 '(-m/--master and -b/--builder)')
273 if not self.args.master in self.masters:
274 raise MBErr('Master name "%s" not found in "%s"' %
275 (self.args.master, self.args.config_file))
277 if not self.args.builder in self.masters[self.args.master]:
278 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
279 (self.args.builder, self.args.master, self.args.config_file))
281 return self.masters[self.args.master][self.args.builder]
283 def FlattenConfig(self, config):
284 mixins = self.configs[config]
285 vals = {
286 'type': None,
287 'gn_args': [],
288 'gyp_defines': '',
289 'gyp_crosscompile': False,
292 visited = []
293 self.FlattenMixins(mixins, vals, visited)
294 return vals
296 def FlattenMixins(self, mixins, vals, visited):
297 for m in mixins:
298 if m not in self.mixins:
299 raise MBErr('Unknown mixin "%s"' % m)
301 # TODO: check for cycles in mixins.
303 visited.append(m)
305 mixin_vals = self.mixins[m]
306 if 'type' in mixin_vals:
307 vals['type'] = mixin_vals['type']
308 if 'gn_args' in mixin_vals:
309 if vals['gn_args']:
310 vals['gn_args'] += ' ' + mixin_vals['gn_args']
311 else:
312 vals['gn_args'] = mixin_vals['gn_args']
313 if 'gyp_crosscompile' in mixin_vals:
314 vals['gyp_crosscompile'] = mixin_vals['gyp_crosscompile']
315 if 'gyp_defines' in mixin_vals:
316 if vals['gyp_defines']:
317 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
318 else:
319 vals['gyp_defines'] = mixin_vals['gyp_defines']
320 if 'mixins' in mixin_vals:
321 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
322 return vals
324 def ClobberIfNeeded(self, vals):
325 path = self.args.path[0]
326 build_dir = self.ToAbsPath(path)
327 mb_type_path = os.path.join(build_dir, 'mb_type')
328 needs_clobber = False
329 new_mb_type = vals['type']
330 if self.Exists(build_dir):
331 if self.Exists(mb_type_path):
332 old_mb_type = self.ReadFile(mb_type_path)
333 if old_mb_type != new_mb_type:
334 self.Print("Build type mismatch: was %s, will be %s, clobbering %s" %
335 (old_mb_type, new_mb_type, path))
336 needs_clobber = True
337 else:
338 # There is no 'mb_type' file in the build directory, so this probably
339 # means that the prior build(s) were not done through mb, and we
340 # have no idea if this was a GYP build or a GN build. Clobber it
341 # to be safe.
342 self.Print("%s/mb_type missing, clobbering to be safe" % path)
343 needs_clobber = True
345 if needs_clobber:
346 self.RemoveDirectory(build_dir)
348 self.MaybeMakeDirectory(build_dir)
349 self.WriteFile(mb_type_path, new_mb_type)
351 def RunGNGen(self, vals):
352 path = self.args.path[0]
354 cmd = self.GNCmd('gen', path, vals['gn_args'])
356 swarming_targets = []
357 if self.args.swarming_targets_file:
358 # We need GN to generate the list of runtime dependencies for
359 # the compile targets listed (one per line) in the file so
360 # we can run them via swarming. We use ninja_to_gn.pyl to convert
361 # the compile targets to the matching GN labels.
362 contents = self.ReadFile(self.args.swarming_targets_file)
363 swarming_targets = contents.splitlines()
364 gn_isolate_map = ast.literal_eval(self.ReadFile(os.path.join(
365 self.chromium_src_dir, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
366 gn_labels = []
367 for target in swarming_targets:
368 if not target in gn_isolate_map:
369 raise MBErr('test target "%s" not found in %s' %
370 (target, '//testing/buildbot/gn_isolate_map.pyl'))
371 gn_labels.append(gn_isolate_map[target]['label'])
373 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
375 # Since GN hasn't run yet, the build directory may not even exist.
376 self.MaybeMakeDirectory(self.ToAbsPath(path))
378 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
379 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
381 ret, _, _ = self.Run(cmd)
382 if ret:
383 # If `gn gen` failed, we should exit early rather than trying to
384 # generate isolates. Run() will have already logged any error output.
385 self.Print('GN gen failed: %d' % ret)
386 return ret
388 for target in swarming_targets:
389 if gn_isolate_map[target]['type'] == 'gpu_browser_test':
390 runtime_deps_target = 'browser_tests'
391 elif gn_isolate_map[target]['type'] == 'script':
392 # For script targets, the build target is usually a group,
393 # for which gn generates the runtime_deps next to the stamp file
394 # for the label, which lives under the obj/ directory.
395 label = gn_isolate_map[target]['label']
396 runtime_deps_target = 'obj/%s.stamp' % label.replace(':', '/')
397 else:
398 runtime_deps_target = target
399 if sys.platform == 'win32':
400 deps_path = self.ToAbsPath(path,
401 runtime_deps_target + '.exe.runtime_deps')
402 else:
403 deps_path = self.ToAbsPath(path,
404 runtime_deps_target + '.runtime_deps')
405 if not self.Exists(deps_path):
406 raise MBErr('did not generate %s' % deps_path)
408 command, extra_files = self.GetIsolateCommand(target, vals,
409 gn_isolate_map)
411 runtime_deps = self.ReadFile(deps_path).splitlines()
413 isolate_path = self.ToAbsPath(path, target + '.isolate')
414 self.WriteFile(isolate_path,
415 pprint.pformat({
416 'variables': {
417 'command': command,
418 'files': sorted(runtime_deps + extra_files),
420 }) + '\n')
422 self.WriteJSON(
424 'args': [
425 '--isolated',
426 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)),
427 '--isolate',
428 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)),
430 'dir': self.chromium_src_dir,
431 'version': 1,
433 isolate_path + 'd.gen.json',
436 return ret
438 def GNCmd(self, subcommand, path, gn_args=''):
439 if self.platform == 'linux2':
440 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64',
441 'gn')
442 elif self.platform == 'darwin':
443 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac',
444 'gn')
445 else:
446 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'win',
447 'gn.exe')
449 cmd = [gn_path, subcommand, path]
450 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir)
451 if gn_args:
452 cmd.append('--args=%s' % gn_args)
453 return cmd
455 def RunGYPGen(self, vals):
456 path = self.args.path[0]
458 output_dir = self.ParseGYPConfigPath(path)
459 cmd = self.GYPCmd(output_dir, vals['gyp_defines'])
460 env = None
461 if vals['gyp_crosscompile']:
462 if self.args.verbose:
463 self.Print('Setting GYP_CROSSCOMPILE=1 in the environment')
464 env = os.environ.copy()
465 env['GYP_CROSSCOMPILE'] = '1'
466 ret, _, _ = self.Run(cmd, env=env)
467 return ret
469 def RunGYPAnalyze(self, vals):
470 output_dir = self.ParseGYPConfigPath(self.args.path[0])
471 if self.args.verbose:
472 inp = self.ReadInputJSON(['files', 'targets'])
473 self.Print()
474 self.Print('analyze input:')
475 self.PrintJSON(inp)
476 self.Print()
478 cmd = self.GYPCmd(output_dir, vals['gyp_defines'])
479 cmd.extend(['-f', 'analyzer',
480 '-G', 'config_path=%s' % self.args.input_path[0],
481 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
482 ret, _, _ = self.Run(cmd)
483 if not ret and self.args.verbose:
484 outp = json.loads(self.ReadFile(self.args.output_path[0]))
485 self.Print()
486 self.Print('analyze output:')
487 self.PrintJSON(outp)
488 self.Print()
490 return ret
492 def GetIsolateCommand(self, target, vals, gn_isolate_map):
493 # This needs to mirror the settings in //build/config/ui.gni:
494 # use_x11 = is_linux && !use_ozone.
495 # TODO(dpranke): Figure out how to keep this in sync better.
496 use_x11 = (sys.platform == 'linux2' and
497 not 'target_os="android"' in vals['gn_args'] and
498 not 'use_ozone=true' in vals['gn_args'])
500 asan = 'is_asan=true' in vals['gn_args']
501 msan = 'is_msan=true' in vals['gn_args']
502 tsan = 'is_tsan=true' in vals['gn_args']
504 executable_suffix = '.exe' if sys.platform == 'win32' else ''
506 test_type = gn_isolate_map[target]['type']
507 cmdline = []
508 extra_files = []
510 if use_x11 and test_type == 'windowed_test_launcher':
511 extra_files = [
512 'xdisplaycheck',
513 '../../testing/test_env.py',
514 '../../testing/xvfb.py',
516 cmdline = [
517 '../../testing/xvfb.py',
518 '.',
519 './' + str(target),
520 '--brave-new-test-launcher',
521 '--test-launcher-bot-mode',
522 '--asan=%d' % asan,
523 '--msan=%d' % msan,
524 '--tsan=%d' % tsan,
526 elif test_type in ('windowed_test_launcher', 'console_test_launcher'):
527 extra_files = [
528 '../../testing/test_env.py'
530 cmdline = [
531 '../../testing/test_env.py',
532 './' + str(target) + executable_suffix,
533 '--brave-new-test-launcher',
534 '--test-launcher-bot-mode',
535 '--asan=%d' % asan,
536 '--msan=%d' % msan,
537 '--tsan=%d' % tsan,
539 elif test_type == 'gpu_browser_test':
540 extra_files = [
541 '../../testing/test_env.py'
543 gtest_filter = gn_isolate_map[target]['gtest_filter']
544 cmdline = [
545 '../../testing/test_env.py',
546 './browser_tests' + executable_suffix,
547 '--test-launcher-bot-mode',
548 '--enable-gpu',
549 '--test-launcher-jobs=1',
550 '--gtest_filter=%s' % gtest_filter,
552 elif test_type == 'script':
553 extra_files = [
554 '../../testing/test_env.py'
556 cmdline = [
557 '../../testing/test_env.py',
558 ] + ['../../' + self.ToSrcRelPath(gn_isolate_map[target]['script'])]
559 elif test_type in ('raw'):
560 extra_files = []
561 cmdline = [
562 './' + str(target) + executable_suffix,
563 ] + gn_isolate_map[target].get('args')
565 else:
566 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
567 % (target, test_type), output_path=None)
569 return cmdline, extra_files
571 def ToAbsPath(self, build_path, *comps):
572 return os.path.join(self.chromium_src_dir,
573 self.ToSrcRelPath(build_path),
574 *comps)
576 def ToSrcRelPath(self, path):
577 """Returns a relative path from the top of the repo."""
578 # TODO: Support normal paths in addition to source-absolute paths.
579 assert(path.startswith('//'))
580 return path[2:].replace('/', os.sep)
582 def ParseGYPConfigPath(self, path):
583 rpath = self.ToSrcRelPath(path)
584 output_dir, _, _ = rpath.rpartition('/')
585 return output_dir
587 def GYPCmd(self, output_dir, gyp_defines):
588 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir)
589 cmd = [
590 sys.executable,
591 os.path.join('build', 'gyp_chromium'),
592 '-G',
593 'output_dir=' + output_dir,
595 for d in shlex.split(gyp_defines):
596 cmd += ['-D', d]
597 return cmd
599 def RunGNAnalyze(self, vals):
600 # analyze runs before 'gn gen' now, so we need to run gn gen
601 # in order to ensure that we have a build directory.
602 ret = self.RunGNGen(vals)
603 if ret:
604 return ret
606 inp = self.ReadInputJSON(['files', 'targets'])
607 if self.args.verbose:
608 self.Print()
609 self.Print('analyze input:')
610 self.PrintJSON(inp)
611 self.Print()
613 output_path = self.args.output_path[0]
615 # Bail out early if a GN file was modified, since 'gn refs' won't know
616 # what to do about it.
617 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']):
618 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
619 return 0
621 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it.
622 if 'all' in inp['targets']:
623 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
624 return 0
626 # This shouldn't normally happen, but could due to unusual race conditions,
627 # like a try job that gets scheduled before a patch lands but runs after
628 # the patch has landed.
629 if not inp['files']:
630 self.Print('Warning: No files modified in patch, bailing out early.')
631 self.WriteJSON({'targets': [],
632 'build_targets': [],
633 'status': 'No dependency'}, output_path)
634 return 0
636 ret = 0
637 response_file = self.TempFile()
638 response_file.write('\n'.join(inp['files']) + '\n')
639 response_file.close()
641 matching_targets = []
642 try:
643 cmd = self.GNCmd('refs', self.args.path[0]) + [
644 '@%s' % response_file.name, '--all', '--as=output']
645 ret, out, _ = self.Run(cmd, force_verbose=False)
646 if ret and not 'The input matches no targets' in out:
647 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
648 output_path)
649 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep
650 for output in out.splitlines():
651 build_output = output.replace(build_dir, '')
652 if build_output in inp['targets']:
653 matching_targets.append(build_output)
655 cmd = self.GNCmd('refs', self.args.path[0]) + [
656 '@%s' % response_file.name, '--all']
657 ret, out, _ = self.Run(cmd, force_verbose=False)
658 if ret and not 'The input matches no targets' in out:
659 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
660 output_path)
661 for label in out.splitlines():
662 build_target = label[2:]
663 # We want to accept 'chrome/android:chrome_public_apk' and
664 # just 'chrome_public_apk'. This may result in too many targets
665 # getting built, but we can adjust that later if need be.
666 for input_target in inp['targets']:
667 if (input_target == build_target or
668 build_target.endswith(':' + input_target)):
669 matching_targets.append(input_target)
670 finally:
671 self.RemoveFile(response_file.name)
673 if matching_targets:
674 # TODO: it could be that a target X might depend on a target Y
675 # and both would be listed in the input, but we would only need
676 # to specify target X as a build_target (whereas both X and Y are
677 # targets). I'm not sure if that optimization is generally worth it.
678 self.WriteJSON({'targets': sorted(set(matching_targets)),
679 'build_targets': sorted(set(matching_targets)),
680 'status': 'Found dependency'}, output_path)
681 else:
682 self.WriteJSON({'targets': [],
683 'build_targets': [],
684 'status': 'No dependency'}, output_path)
686 if self.args.verbose:
687 outp = json.loads(self.ReadFile(output_path))
688 self.Print()
689 self.Print('analyze output:')
690 self.PrintJSON(outp)
691 self.Print()
693 return 0
695 def ReadInputJSON(self, required_keys):
696 path = self.args.input_path[0]
697 output_path = self.args.output_path[0]
698 if not self.Exists(path):
699 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
701 try:
702 inp = json.loads(self.ReadFile(path))
703 except Exception as e:
704 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
705 (path, e), output_path)
707 for k in required_keys:
708 if not k in inp:
709 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
710 output_path)
712 return inp
714 def WriteFailureAndRaise(self, msg, output_path):
715 if output_path:
716 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
717 raise MBErr(msg)
719 def WriteJSON(self, obj, path, force_verbose=False):
720 try:
721 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
722 force_verbose=force_verbose)
723 except Exception as e:
724 raise MBErr('Error %s writing to the output path "%s"' %
725 (e, path))
727 def PrintCmd(self, cmd):
728 if cmd[0] == sys.executable:
729 cmd = ['python'] + cmd[1:]
730 self.Print(*[pipes.quote(c) for c in cmd])
732 def PrintJSON(self, obj):
733 self.Print(json.dumps(obj, indent=2, sort_keys=True))
735 def Print(self, *args, **kwargs):
736 # This function largely exists so it can be overridden for testing.
737 print(*args, **kwargs)
739 def Run(self, cmd, env=None, force_verbose=True):
740 # This function largely exists so it can be overridden for testing.
741 if self.args.dryrun or self.args.verbose or force_verbose:
742 self.PrintCmd(cmd)
743 if self.args.dryrun:
744 return 0, '', ''
746 ret, out, err = self.Call(cmd, env=env)
747 if self.args.verbose or force_verbose:
748 if out:
749 self.Print(out, end='')
750 if err:
751 self.Print(err, end='', file=sys.stderr)
752 return ret, out, err
754 def Call(self, cmd, env=None):
755 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
756 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
757 env=env)
758 out, err = p.communicate()
759 return p.returncode, out, err
761 def ExpandUser(self, path):
762 # This function largely exists so it can be overridden for testing.
763 return os.path.expanduser(path)
765 def Exists(self, path):
766 # This function largely exists so it can be overridden for testing.
767 return os.path.exists(path)
769 def MaybeMakeDirectory(self, path):
770 try:
771 os.makedirs(path)
772 except OSError, e:
773 if e.errno != errno.EEXIST:
774 raise
776 def ReadFile(self, path):
777 # This function largely exists so it can be overriden for testing.
778 with open(path) as fp:
779 return fp.read()
781 def RemoveFile(self, path):
782 # This function largely exists so it can be overriden for testing.
783 os.remove(path)
785 def RemoveDirectory(self, abs_path):
786 if sys.platform == 'win32':
787 # In other places in chromium, we often have to retry this command
788 # because we're worried about other processes still holding on to
789 # file handles, but when MB is invoked, it will be early enough in the
790 # build that their should be no other processes to interfere. We
791 # can change this if need be.
792 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
793 else:
794 shutil.rmtree(abs_path, ignore_errors=True)
796 def TempFile(self, mode='w'):
797 # This function largely exists so it can be overriden for testing.
798 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
800 def WriteFile(self, path, contents, force_verbose=False):
801 # This function largely exists so it can be overriden for testing.
802 if self.args.dryrun or self.args.verbose or force_verbose:
803 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
804 with open(path, 'w') as fp:
805 return fp.write(contents)
808 class MBErr(Exception):
809 pass
812 if __name__ == '__main__':
813 try:
814 sys.exit(main(sys.argv[1:]))
815 except MBErr as e:
816 print(e)
817 sys.exit(1)
818 except KeyboardInterrupt:
819 print("interrupted, exiting", stream=sys.stderr)
820 sys.exit(130)