Revert of Attempt to fix expansion of the goma directory in MB on windows. (patchset...
[chromium-blink-merge.git] / tools / mb / mb.py
blob10cff5365d7c1e80c767dde8db36001a1c257937
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.executable = sys.executable
41 self.platform = sys.platform
42 self.sep = os.sep
43 self.args = argparse.Namespace()
44 self.configs = {}
45 self.masters = {}
46 self.mixins = {}
47 self.private_configs = []
48 self.common_dev_configs = []
49 self.unsupported_configs = []
51 def ParseArgs(self, argv):
52 def AddCommonOptions(subp):
53 subp.add_argument('-b', '--builder',
54 help='builder name to look up config from')
55 subp.add_argument('-m', '--master',
56 help='master name to look up config from')
57 subp.add_argument('-c', '--config',
58 help='configuration to analyze')
59 subp.add_argument('-f', '--config-file', metavar='PATH',
60 default=self.default_config,
61 help='path to config file '
62 '(default is //tools/mb/mb_config.pyl)')
63 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
64 help='path to goma directory (default is %(default)s).')
65 subp.add_argument('-n', '--dryrun', action='store_true',
66 help='Do a dry run (i.e., do nothing, just print '
67 'the commands that will run)')
68 subp.add_argument('-v', '--verbose', action='store_true',
69 help='verbose logging')
71 parser = argparse.ArgumentParser(prog='mb')
72 subps = parser.add_subparsers()
74 subp = subps.add_parser('analyze',
75 help='analyze whether changes to a set of files '
76 'will cause a set of binaries to be rebuilt.')
77 AddCommonOptions(subp)
78 subp.add_argument('--swarming-targets-file',
79 help='save runtime dependencies for targets listed '
80 'in file.')
81 subp.add_argument('path', nargs=1,
82 help='path build was generated into.')
83 subp.add_argument('input_path', nargs=1,
84 help='path to a file containing the input arguments '
85 'as a JSON object.')
86 subp.add_argument('output_path', nargs=1,
87 help='path to a file containing the output arguments '
88 'as a JSON object.')
89 subp.set_defaults(func=self.CmdAnalyze)
91 subp = subps.add_parser('gen',
92 help='generate a new set of build files')
93 AddCommonOptions(subp)
94 subp.add_argument('--swarming-targets-file',
95 help='save runtime dependencies for targets listed '
96 'in file.')
97 subp.add_argument('path', nargs=1,
98 help='path to generate build into')
99 subp.set_defaults(func=self.CmdGen)
101 subp = subps.add_parser('lookup',
102 help='look up the command for a given config or '
103 'builder')
104 AddCommonOptions(subp)
105 subp.set_defaults(func=self.CmdLookup)
107 subp = subps.add_parser('validate',
108 help='validate the config file')
109 subp.add_argument('-f', '--config-file', metavar='PATH',
110 default=self.default_config,
111 help='path to config file '
112 '(default is //tools/mb/mb_config.pyl)')
113 subp.set_defaults(func=self.CmdValidate)
115 subp = subps.add_parser('help',
116 help='Get help on a subcommand.')
117 subp.add_argument(nargs='?', action='store', dest='subcommand',
118 help='The command to get help for.')
119 subp.set_defaults(func=self.CmdHelp)
121 self.args = parser.parse_args(argv)
123 def CmdAnalyze(self):
124 vals = self.GetConfig()
125 if vals['type'] == 'gn':
126 return self.RunGNAnalyze(vals)
127 elif vals['type'] == 'gyp':
128 return self.RunGYPAnalyze(vals)
129 else:
130 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
132 def CmdGen(self):
133 vals = self.GetConfig()
135 self.ClobberIfNeeded(vals)
137 if vals['type'] == 'gn':
138 return self.RunGNGen(vals)
139 if vals['type'] == 'gyp':
140 return self.RunGYPGen(vals)
142 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
144 def CmdLookup(self):
145 vals = self.GetConfig()
146 if vals['type'] == 'gn':
147 cmd = self.GNCmd('gen', '<path>', vals['gn_args'])
148 elif vals['type'] == 'gyp':
149 if vals['gyp_crosscompile']:
150 self.Print('GYP_CROSSCOMPILE=1')
151 cmd = self.GYPCmd('<path>', vals['gyp_defines'])
152 else:
153 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
155 self.PrintCmd(cmd)
156 return 0
158 def CmdHelp(self):
159 if self.args.subcommand:
160 self.ParseArgs([self.args.subcommand, '--help'])
161 else:
162 self.ParseArgs(['--help'])
164 def CmdValidate(self):
165 errs = []
167 # Read the file to make sure it parses.
168 self.ReadConfigFile()
170 # Figure out the whole list of configs and ensure that no config is
171 # listed in more than one category.
172 all_configs = {}
173 for config in self.common_dev_configs:
174 all_configs[config] = 'common_dev_configs'
175 for config in self.private_configs:
176 if config in all_configs:
177 errs.append('config "%s" listed in "private_configs" also '
178 'listed in "%s"' % (config, all_configs['config']))
179 else:
180 all_configs[config] = 'private_configs'
181 for config in self.unsupported_configs:
182 if config in all_configs:
183 errs.append('config "%s" listed in "unsupported_configs" also '
184 'listed in "%s"' % (config, all_configs['config']))
185 else:
186 all_configs[config] = 'unsupported_configs'
188 for master in self.masters:
189 for builder in self.masters[master]:
190 config = self.masters[master][builder]
191 if config in all_configs and all_configs[config] not in self.masters:
192 errs.append('Config "%s" used by a bot is also listed in "%s".' %
193 (config, all_configs[config]))
194 else:
195 all_configs[config] = master
197 # Check that every referenced config actually exists.
198 for config, loc in all_configs.items():
199 if not config in self.configs:
200 errs.append('Unknown config "%s" referenced from "%s".' %
201 (config, loc))
203 # Check that every actual config is actually referenced.
204 for config in self.configs:
205 if not config in all_configs:
206 errs.append('Unused config "%s".' % config)
208 # Figure out the whole list of mixins, and check that every mixin
209 # listed by a config or another mixin actually exists.
210 referenced_mixins = set()
211 for config, mixins in self.configs.items():
212 for mixin in mixins:
213 if not mixin in self.mixins:
214 errs.append('Unknown mixin "%s" referenced by config "%s".' %
215 (mixin, config))
216 referenced_mixins.add(mixin)
218 for mixin in self.mixins:
219 for sub_mixin in self.mixins[mixin].get('mixins', []):
220 if not sub_mixin in self.mixins:
221 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
222 (sub_mixin, mixin))
223 referenced_mixins.add(sub_mixin)
225 # Check that every mixin defined is actually referenced somewhere.
226 for mixin in self.mixins:
227 if not mixin in referenced_mixins:
228 errs.append('Unreferenced mixin "%s".' % mixin)
230 if errs:
231 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
232 '\n ' + '\n '.join(errs))
234 self.Print('mb config file %s looks ok.' % self.args.config_file)
235 return 0
237 def GetConfig(self):
238 self.ReadConfigFile()
239 config = self.ConfigFromArgs()
240 if not config in self.configs:
241 raise MBErr('Config "%s" not found in %s' %
242 (config, self.args.config_file))
244 return self.FlattenConfig(config)
246 def ReadConfigFile(self):
247 if not self.Exists(self.args.config_file):
248 raise MBErr('config file not found at %s' % self.args.config_file)
250 try:
251 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
252 except SyntaxError as e:
253 raise MBErr('Failed to parse config file "%s": %s' %
254 (self.args.config_file, e))
256 self.common_dev_configs = contents['common_dev_configs']
257 self.configs = contents['configs']
258 self.masters = contents['masters']
259 self.mixins = contents['mixins']
260 self.private_configs = contents['private_configs']
261 self.unsupported_configs = contents['unsupported_configs']
263 def ConfigFromArgs(self):
264 if self.args.config:
265 if self.args.master or self.args.builder:
266 raise MBErr('Can not specific both -c/--config and -m/--master or '
267 '-b/--builder')
269 return self.args.config
271 if not self.args.master or not self.args.builder:
272 raise MBErr('Must specify either -c/--config or '
273 '(-m/--master and -b/--builder)')
275 if not self.args.master in self.masters:
276 raise MBErr('Master name "%s" not found in "%s"' %
277 (self.args.master, self.args.config_file))
279 if not self.args.builder in self.masters[self.args.master]:
280 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
281 (self.args.builder, self.args.master, self.args.config_file))
283 return self.masters[self.args.master][self.args.builder]
285 def FlattenConfig(self, config):
286 mixins = self.configs[config]
287 vals = {
288 'type': None,
289 'gn_args': [],
290 'gyp_defines': '',
291 'gyp_crosscompile': False,
294 visited = []
295 self.FlattenMixins(mixins, vals, visited)
296 return vals
298 def FlattenMixins(self, mixins, vals, visited):
299 for m in mixins:
300 if m not in self.mixins:
301 raise MBErr('Unknown mixin "%s"' % m)
303 # TODO: check for cycles in mixins.
305 visited.append(m)
307 mixin_vals = self.mixins[m]
308 if 'type' in mixin_vals:
309 vals['type'] = mixin_vals['type']
310 if 'gn_args' in mixin_vals:
311 if vals['gn_args']:
312 vals['gn_args'] += ' ' + mixin_vals['gn_args']
313 else:
314 vals['gn_args'] = mixin_vals['gn_args']
315 if 'gyp_crosscompile' in mixin_vals:
316 vals['gyp_crosscompile'] = mixin_vals['gyp_crosscompile']
317 if 'gyp_defines' in mixin_vals:
318 if vals['gyp_defines']:
319 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
320 else:
321 vals['gyp_defines'] = mixin_vals['gyp_defines']
322 if 'mixins' in mixin_vals:
323 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
324 return vals
326 def ClobberIfNeeded(self, vals):
327 path = self.args.path[0]
328 build_dir = self.ToAbsPath(path)
329 mb_type_path = self.PathJoin(build_dir, 'mb_type')
330 needs_clobber = False
331 new_mb_type = vals['type']
332 if self.Exists(build_dir):
333 if self.Exists(mb_type_path):
334 old_mb_type = self.ReadFile(mb_type_path)
335 if old_mb_type != new_mb_type:
336 self.Print("Build type mismatch: was %s, will be %s, clobbering %s" %
337 (old_mb_type, new_mb_type, path))
338 needs_clobber = True
339 else:
340 # There is no 'mb_type' file in the build directory, so this probably
341 # means that the prior build(s) were not done through mb, and we
342 # have no idea if this was a GYP build or a GN build. Clobber it
343 # to be safe.
344 self.Print("%s/mb_type missing, clobbering to be safe" % path)
345 needs_clobber = True
347 if needs_clobber:
348 self.RemoveDirectory(build_dir)
350 self.MaybeMakeDirectory(build_dir)
351 self.WriteFile(mb_type_path, new_mb_type)
353 def RunGNGen(self, vals):
354 path = self.args.path[0]
356 cmd = self.GNCmd('gen', path, vals['gn_args'])
358 swarming_targets = []
359 if self.args.swarming_targets_file:
360 # We need GN to generate the list of runtime dependencies for
361 # the compile targets listed (one per line) in the file so
362 # we can run them via swarming. We use ninja_to_gn.pyl to convert
363 # the compile targets to the matching GN labels.
364 contents = self.ReadFile(self.args.swarming_targets_file)
365 swarming_targets = contents.splitlines()
366 gn_isolate_map = ast.literal_eval(self.ReadFile(self.PathJoin(
367 self.chromium_src_dir, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
368 gn_labels = []
369 for target in swarming_targets:
370 if not target in gn_isolate_map:
371 raise MBErr('test target "%s" not found in %s' %
372 (target, '//testing/buildbot/gn_isolate_map.pyl'))
373 gn_labels.append(gn_isolate_map[target]['label'])
375 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
377 # Since GN hasn't run yet, the build directory may not even exist.
378 self.MaybeMakeDirectory(self.ToAbsPath(path))
380 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
381 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
383 ret, _, _ = self.Run(cmd)
384 if ret:
385 # If `gn gen` failed, we should exit early rather than trying to
386 # generate isolates. Run() will have already logged any error output.
387 self.Print('GN gen failed: %d' % ret)
388 return ret
390 for target in swarming_targets:
391 if gn_isolate_map[target]['type'] == 'gpu_browser_test':
392 runtime_deps_target = 'browser_tests'
393 elif gn_isolate_map[target]['type'] == 'script':
394 # For script targets, the build target is usually a group,
395 # for which gn generates the runtime_deps next to the stamp file
396 # for the label, which lives under the obj/ directory.
397 label = gn_isolate_map[target]['label']
398 runtime_deps_target = 'obj/%s.stamp' % label.replace(':', '/')
399 else:
400 runtime_deps_target = target
401 if self.platform == 'win32':
402 deps_path = self.ToAbsPath(path,
403 runtime_deps_target + '.exe.runtime_deps')
404 else:
405 deps_path = self.ToAbsPath(path,
406 runtime_deps_target + '.runtime_deps')
407 if not self.Exists(deps_path):
408 raise MBErr('did not generate %s' % deps_path)
410 command, extra_files = self.GetIsolateCommand(target, vals,
411 gn_isolate_map)
413 runtime_deps = self.ReadFile(deps_path).splitlines()
415 isolate_path = self.ToAbsPath(path, target + '.isolate')
416 self.WriteFile(isolate_path,
417 pprint.pformat({
418 'variables': {
419 'command': command,
420 'files': sorted(runtime_deps + extra_files),
422 }) + '\n')
424 self.WriteJSON(
426 'args': [
427 '--isolated',
428 self.ToSrcRelPath('%s%s%s.isolated' % (path, self.sep, target)),
429 '--isolate',
430 self.ToSrcRelPath('%s%s%s.isolate' % (path, self.sep, target)),
432 'dir': self.chromium_src_dir,
433 'version': 1,
435 isolate_path + 'd.gen.json',
438 return ret
440 def GNCmd(self, subcommand, path, gn_args=''):
441 if self.platform == 'linux2':
442 subdir = 'linux64'
443 elif self.platform == 'darwin':
444 subdir = 'mac'
445 else:
446 subdir = 'win'
447 gn_path = self.PathJoin(self.chromium_src_dir, 'buildtools', subdir, 'gn')
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 = (self.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 self.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 self.PathJoin(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('/', self.sep)
582 def ParseGYPConfigPath(self, path):
583 rpath = self.ToSrcRelPath(path)
584 output_dir, _, _ = rpath.rpartition(self.sep)
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 self.executable,
591 self.PathJoin('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]) + self.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] == self.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 PathJoin(self, *comps):
777 # This function largely exists so it can be overriden for testing.
778 return os.path.join(*comps)
780 def ReadFile(self, path):
781 # This function largely exists so it can be overriden for testing.
782 with open(path) as fp:
783 return fp.read()
785 def RemoveFile(self, path):
786 # This function largely exists so it can be overriden for testing.
787 os.remove(path)
789 def RemoveDirectory(self, abs_path):
790 if self.platform == 'win32':
791 # In other places in chromium, we often have to retry this command
792 # because we're worried about other processes still holding on to
793 # file handles, but when MB is invoked, it will be early enough in the
794 # build that their should be no other processes to interfere. We
795 # can change this if need be.
796 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
797 else:
798 shutil.rmtree(abs_path, ignore_errors=True)
800 def TempFile(self, mode='w'):
801 # This function largely exists so it can be overriden for testing.
802 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
804 def WriteFile(self, path, contents, force_verbose=False):
805 # This function largely exists so it can be overriden for testing.
806 if self.args.dryrun or self.args.verbose or force_verbose:
807 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
808 with open(path, 'w') as fp:
809 return fp.write(contents)
812 class MBErr(Exception):
813 pass
816 if __name__ == '__main__':
817 try:
818 sys.exit(main(sys.argv[1:]))
819 except MBErr as e:
820 print(e)
821 sys.exit(1)
822 except KeyboardInterrupt:
823 print("interrupted, exiting", stream=sys.stderr)
824 sys.exit(130)