blob: 0c3d446913a6a6d972ebbd675afb70f81395248b [file] [log] [blame]
dprankefe4602312015-04-08 16:20:351#!/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
8MB is a wrapper script for GYP and GN that can be used to generate build files
9for sets of canned configurations and analyze them.
10"""
11
12from __future__ import print_function
13
14import argparse
15import ast
16import json
17import os
18import pipes
dpranked8113582015-06-05 20:08:2519import pprint
dprankefe4602312015-04-08 16:20:3520import shlex
21import shutil
22import sys
23import subprocess
dprankef61de2f2015-05-14 04:09:5624import tempfile
dprankefe4602312015-04-08 16:20:3525
26
27def main(args):
dprankeee5b51f62015-04-09 00:03:2228 mbw = MetaBuildWrapper()
29 mbw.ParseArgs(args)
30 return mbw.args.func()
dprankefe4602312015-04-08 16:20:3531
32
33class 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')
dpranked1fba482015-04-14 20:54:5140 self.platform = sys.platform
dprankefe4602312015-04-08 16:20:3541 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 = []
48
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('-q', '--quiet', action='store_true',
67 help='Do not print anything, just return an exit '
68 'code.')
69 subp.add_argument('-v', '--verbose', action='count',
70 help='verbose logging (may specify multiple times).')
71
72 parser = argparse.ArgumentParser(prog='mb')
73 subps = parser.add_subparsers()
74
75 subp = subps.add_parser('analyze',
76 help='analyze whether changes to a set of files '
77 'will cause a set of binaries to be rebuilt.')
78 AddCommonOptions(subp)
dpranked8113582015-06-05 20:08:2579 subp.add_argument('path', nargs=1,
dprankefe4602312015-04-08 16:20:3580 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)
88
89 subp = subps.add_parser('gen',
90 help='generate a new set of build files')
91 AddCommonOptions(subp)
dpranked8113582015-06-05 20:08:2592 subp.add_argument('path', nargs=1,
dprankefe4602312015-04-08 16:20:3593 help='path to generate build into')
94 subp.set_defaults(func=self.CmdGen)
95
dpranked8113582015-06-05 20:08:2596 subp = subps.add_parser('isolate',
97 help='build isolates')
98 AddCommonOptions(subp)
99 subp.add_argument('path', nargs=1,
100 help='path build was generated into.')
101 subp.add_argument('input_path', nargs=1,
102 help='path to a file containing the input arguments '
103 'as a JSON object.')
104 subp.add_argument('output_path', nargs=1,
105 help='path to a file containing the output arguments '
106 'as a JSON object.')
107 subp.set_defaults(func=self.CmdIsolate)
108
dprankefe4602312015-04-08 16:20:35109 subp = subps.add_parser('lookup',
110 help='look up the command for a given config or '
111 'builder')
112 AddCommonOptions(subp)
113 subp.set_defaults(func=self.CmdLookup)
114
115 subp = subps.add_parser('validate',
116 help='validate the config file')
117 AddCommonOptions(subp)
118 subp.set_defaults(func=self.CmdValidate)
119
120 subp = subps.add_parser('help',
121 help='Get help on a subcommand.')
122 subp.add_argument(nargs='?', action='store', dest='subcommand',
123 help='The command to get help for.')
124 subp.set_defaults(func=self.CmdHelp)
125
126 self.args = parser.parse_args(argv)
127
128 def CmdAnalyze(self):
129 vals = self.GetConfig()
130 if vals['type'] == 'gn':
131 return self.RunGNAnalyze(vals)
132 elif vals['type'] == 'gyp':
133 return self.RunGYPAnalyze(vals)
134 else:
135 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
136
137 def CmdGen(self):
138 vals = self.GetConfig()
139 if vals['type'] == 'gn':
dpranke08d2ab12015-04-24 21:54:20140 return self.RunGNGen(self.args.path[0], vals)
141 if vals['type'] == 'gyp':
142 return self.RunGYPGen(self.args.path[0], vals)
143
144 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
dprankefe4602312015-04-08 16:20:35145
dpranked8113582015-06-05 20:08:25146 def CmdIsolate(self):
147 vals = self.GetConfig()
148 if vals['type'] == 'gn':
149 return self.RunGNIsolate(vals)
150 if vals['type'] == 'gyp':
151 # For GYP builds the .isolate files are checked in and the
152 # .isolate.gen.json files are generated during the compile,
153 # so there is no work to do here.
154 return 0
155 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
156
dprankefe4602312015-04-08 16:20:35157 def CmdLookup(self):
158 vals = self.GetConfig()
159 if vals['type'] == 'gn':
dpranked1fba482015-04-14 20:54:51160 cmd = self.GNCmd('gen', '<path>', vals['gn_args'])
dprankefe4602312015-04-08 16:20:35161 elif vals['type'] == 'gyp':
162 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config'])
163 else:
164 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
165
166 self.PrintCmd(cmd)
167 return 0
168
169 def CmdHelp(self):
170 if self.args.subcommand:
171 self.ParseArgs([self.args.subcommand, '--help'])
172 else:
173 self.ParseArgs(['--help'])
174
175 def CmdValidate(self):
176 errs = []
177
178 # Read the file to make sure it parses.
179 self.ReadConfigFile()
180
181 # Figure out the whole list of configs and ensure that no config is
182 # listed in more than one category.
183 all_configs = {}
184 for config in self.common_dev_configs:
185 all_configs[config] = 'common_dev_configs'
186 for config in self.private_configs:
187 if config in all_configs:
188 errs.append('config "%s" listed in "private_configs" also '
189 'listed in "%s"' % (config, all_configs['config']))
190 else:
191 all_configs[config] = 'private_configs'
192 for config in self.unsupported_configs:
193 if config in all_configs:
194 errs.append('config "%s" listed in "unsupported_configs" also '
195 'listed in "%s"' % (config, all_configs['config']))
196 else:
197 all_configs[config] = 'unsupported_configs'
198
199 for master in self.masters:
200 for builder in self.masters[master]:
201 config = self.masters[master][builder]
202 if config in all_configs and all_configs[config] not in self.masters:
203 errs.append('Config "%s" used by a bot is also listed in "%s".' %
204 (config, all_configs[config]))
205 else:
206 all_configs[config] = master
207
208 # Check that every referenced config actually exists.
209 for config, loc in all_configs.items():
210 if not config in self.configs:
211 errs.append('Unknown config "%s" referenced from "%s".' %
212 (config, loc))
213
214 # Check that every actual config is actually referenced.
215 for config in self.configs:
216 if not config in all_configs:
217 errs.append('Unused config "%s".' % config)
218
219 # Figure out the whole list of mixins, and check that every mixin
220 # listed by a config or another mixin actually exists.
221 referenced_mixins = set()
222 for config, mixins in self.configs.items():
223 for mixin in mixins:
224 if not mixin in self.mixins:
225 errs.append('Unknown mixin "%s" referenced by config "%s".' %
226 (mixin, config))
227 referenced_mixins.add(mixin)
228
229 for mixin in self.mixins:
230 for sub_mixin in self.mixins[mixin].get('mixins', []):
231 if not sub_mixin in self.mixins:
232 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
233 (sub_mixin, mixin))
234 referenced_mixins.add(sub_mixin)
235
236 # Check that every mixin defined is actually referenced somewhere.
237 for mixin in self.mixins:
238 if not mixin in referenced_mixins:
239 errs.append('Unreferenced mixin "%s".' % mixin)
240
241 if errs:
242 raise MBErr('mb config file %s has problems:\n ' + '\n '.join(errs))
243
244 if not self.args.quiet:
245 self.Print('mb config file %s looks ok.' % self.args.config_file)
246 return 0
247
248 def GetConfig(self):
249 self.ReadConfigFile()
250 config = self.ConfigFromArgs()
251 if not config in self.configs:
252 raise MBErr('Config "%s" not found in %s' %
253 (config, self.args.config_file))
254
255 return self.FlattenConfig(config)
256
257 def ReadConfigFile(self):
258 if not self.Exists(self.args.config_file):
259 raise MBErr('config file not found at %s' % self.args.config_file)
260
261 try:
262 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
263 except SyntaxError as e:
264 raise MBErr('Failed to parse config file "%s": %s' %
265 (self.args.config_file, e))
266
267 self.common_dev_configs = contents['common_dev_configs']
268 self.configs = contents['configs']
269 self.masters = contents['masters']
270 self.mixins = contents['mixins']
271 self.private_configs = contents['private_configs']
272 self.unsupported_configs = contents['unsupported_configs']
273
274 def ConfigFromArgs(self):
275 if self.args.config:
276 if self.args.master or self.args.builder:
277 raise MBErr('Can not specific both -c/--config and -m/--master or '
278 '-b/--builder')
279
280 return self.args.config
281
282 if not self.args.master or not self.args.builder:
283 raise MBErr('Must specify either -c/--config or '
284 '(-m/--master and -b/--builder)')
285
286 if not self.args.master in self.masters:
287 raise MBErr('Master name "%s" not found in "%s"' %
288 (self.args.master, self.args.config_file))
289
290 if not self.args.builder in self.masters[self.args.master]:
291 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
292 (self.args.builder, self.args.master, self.args.config_file))
293
294 return self.masters[self.args.master][self.args.builder]
295
296 def FlattenConfig(self, config):
297 mixins = self.configs[config]
298 vals = {
299 'type': None,
300 'gn_args': [],
301 'gyp_config': [],
302 'gyp_defines': [],
303 }
304
305 visited = []
306 self.FlattenMixins(mixins, vals, visited)
307 return vals
308
309 def FlattenMixins(self, mixins, vals, visited):
310 for m in mixins:
311 if m not in self.mixins:
312 raise MBErr('Unknown mixin "%s"' % m)
dprankeee5b51f62015-04-09 00:03:22313
314 # TODO: check for cycles in mixins.
dprankefe4602312015-04-08 16:20:35315
316 visited.append(m)
317
318 mixin_vals = self.mixins[m]
319 if 'type' in mixin_vals:
320 vals['type'] = mixin_vals['type']
321 if 'gn_args' in mixin_vals:
322 if vals['gn_args']:
323 vals['gn_args'] += ' ' + mixin_vals['gn_args']
324 else:
325 vals['gn_args'] = mixin_vals['gn_args']
326 if 'gyp_config' in mixin_vals:
327 vals['gyp_config'] = mixin_vals['gyp_config']
328 if 'gyp_defines' in mixin_vals:
329 if vals['gyp_defines']:
330 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
331 else:
332 vals['gyp_defines'] = mixin_vals['gyp_defines']
333 if 'mixins' in mixin_vals:
334 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
335 return vals
336
337 def RunGNGen(self, path, vals):
dpranked1fba482015-04-14 20:54:51338 cmd = self.GNCmd('gen', path, vals['gn_args'])
dprankefe4602312015-04-08 16:20:35339 ret, _, _ = self.Run(cmd)
340 return ret
341
dpranked1fba482015-04-14 20:54:51342 def GNCmd(self, subcommand, path, gn_args=''):
343 if self.platform == 'linux2':
344 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64',
345 'gn')
346 elif self.platform == 'darwin':
347 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac',
348 'gn')
349 else:
350 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'win',
351 'gn.exe')
352
353 cmd = [gn_path, subcommand, path]
dprankeee5b51f62015-04-09 00:03:22354 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir)
dprankefe4602312015-04-08 16:20:35355 if gn_args:
356 cmd.append('--args=%s' % gn_args)
357 return cmd
358
359 def RunGYPGen(self, path, vals):
360 output_dir, gyp_config = self.ParseGYPConfigPath(path)
361 if gyp_config != vals['gyp_config']:
362 raise MBErr('The last component of the path (%s) must match the '
363 'GYP configuration specified in the config (%s), and '
364 'it does not.' % (gyp_config, vals['gyp_config']))
365 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
366 ret, _, _ = self.Run(cmd)
367 return ret
368
369 def RunGYPAnalyze(self, vals):
370 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0])
371 if gyp_config != vals['gyp_config']:
372 raise MBErr('The last component of the path (%s) must match the '
373 'GYP configuration specified in the config (%s), and '
374 'it does not.' % (gyp_config, vals['gyp_config']))
dprankecda00332015-04-11 04:18:32375 if self.args.verbose:
376 inp = self.GetAnalyzeInput()
377 self.Print()
378 self.Print('analyze input:')
379 self.PrintJSON(inp)
380 self.Print()
381
dprankefe4602312015-04-08 16:20:35382 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
383 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0],
384 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
385 ret, _, _ = self.Run(cmd)
dprankecda00332015-04-11 04:18:32386 if not ret and self.args.verbose:
387 outp = json.loads(self.ReadFile(self.args.output_path[0]))
388 self.Print()
389 self.Print('analyze output:')
390 self.PrintJSON(inp)
391 self.Print()
392
dprankefe4602312015-04-08 16:20:35393 return ret
394
dpranked8113582015-06-05 20:08:25395 def RunGNIsolate(self, vals):
396 build_path = self.args.path[0]
397 inp = self.ReadInputJSON(['targets'])
398 if self.args.verbose:
399 self.Print()
400 self.Print('isolate input:')
401 self.PrintJSON(inp)
402 self.Print()
403 output_path = self.args.output_path[0]
404
405 for target in inp['targets']:
406 runtime_deps_path = self.ToAbsPath(build_path, target + '.runtime_deps')
407
408 if not self.Exists(runtime_deps_path):
409 self.WriteFailureAndRaise('"%s" does not exist' % runtime_deps_path,
410 output_path)
411
412 command, extra_files = self.GetIsolateCommand(target, vals)
413
414 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
415
416
417 isolate_path = self.ToAbsPath(build_path, target + '.isolate')
418 self.WriteFile(isolate_path,
419 pprint.pformat({
420 'variables': {
421 'command': command,
422 'files': sorted(runtime_deps + extra_files),
423 'read_only': 1,
424 }
425 }) + '\n')
426
427 self.WriteJSON(
428 {
429 'args': [
430 '--isolated',
431 self.ToSrcRelPath('%s/%s.isolated' % (build_path, target)),
432 '--isolate',
433 self.ToSrcRelPath('%s/%s.isolate' % (build_path, target)),
434 ],
435 'dir': self.chromium_src_dir,
436 'version': 1,
437 },
438 isolate_path + '.gen.json',
439 )
440
441 return 0
442
443 def GetIsolateCommand(self, target, vals):
444 output_path = self.args.output_path[0]
445
446 extra_files = []
447
448 # TODO(dpranke): We should probably pull this from
449 # the test list info in //testing/buildbot/*.json,
450 # and assert that the test has can_use_on_swarming_builders: True,
451 # but we hardcode it here for now.
452 test_type = {}.get(target, 'gtest_test')
453
454 # This needs to mirror the settings in //build/config/ui.gni:
455 # use_x11 = is_linux && !use_ozone.
456 # TODO(dpranke): Figure out how to keep this in sync better.
457 use_x11 = (sys.platform == 'linux2' and
458 not 'target_os="android"' in vals['gn_args'] and
459 not 'use_ozone=true' in vals['gn_args'])
460
461 asan = 'is_asan=true' in vals['gn_args']
462 msan = 'is_msan=true' in vals['gn_args']
463 tsan = 'is_tsan=true' in vals['gn_args']
464
465 executable_suffix = '.exe' if sys.platform == 'win32' else ''
466
467 if test_type == 'gtest_test':
468 extra_files.append('../../testing/test_env.py')
469
470 if use_x11:
471 # TODO(dpranke): Figure out some way to figure out which
472 # test steps really need xvfb.
473 extra_files.append('xdisplaycheck')
474 extra_files.append('../../testing/xvfb.py')
475
476 cmdline = [
477 '../../testing/xvfb.py',
478 '.',
479 './' + str(target),
480 '--brave-new-test-launcher',
481 '--test-launcher-bot-mode',
482 '--asan=%d' % asan,
483 '--msan=%d' % msan,
484 '--tsan=%d' % tsan,
485 ]
486 else:
487 cmdline = [
488 '../../testing/test_env.py',
489 '.',
490 './' + str(target) + executable_suffix,
491 '--brave-new-test-launcher',
492 '--test-launcher-bot-mode',
493 '--asan=%d' % asan,
494 '--msan=%d' % msan,
495 '--tsan=%d' % tsan,
496 ]
497 else:
498 # TODO(dpranke): Handle script_tests and other types of
499 # swarmed tests.
500 self.WriteFailureAndRaise('unknown test type "%s" for %s' %
501 (test_type, target),
502 output_path)
503
504
505 return cmdline, extra_files
506
507 def ToAbsPath(self, build_path, relpath):
508 return os.path.join(self.chromium_src_dir,
509 self.ToSrcRelPath(build_path),
510 relpath)
511
dprankeee5b51f62015-04-09 00:03:22512 def ToSrcRelPath(self, path):
513 """Returns a relative path from the top of the repo."""
514 # TODO: Support normal paths in addition to source-absolute paths.
dprankefe4602312015-04-08 16:20:35515 assert(path.startswith('//'))
516 return path[2:]
517
518 def ParseGYPConfigPath(self, path):
dprankeee5b51f62015-04-09 00:03:22519 rpath = self.ToSrcRelPath(path)
520 output_dir, _, config = rpath.rpartition('/')
dprankefe4602312015-04-08 16:20:35521 self.CheckGYPConfigIsSupported(config, path)
522 return output_dir, config
523
524 def CheckGYPConfigIsSupported(self, config, path):
525 if config not in ('Debug', 'Release'):
526 if (sys.platform in ('win32', 'cygwin') and
527 config not in ('Debug_x64', 'Release_x64')):
528 raise MBErr('Unknown or unsupported config type "%s" in "%s"' %
529 config, path)
530
531 def GYPCmd(self, output_dir, gyp_defines, config):
532 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir)
533 cmd = [
534 sys.executable,
535 os.path.join('build', 'gyp_chromium'),
536 '-G',
537 'output_dir=' + output_dir,
538 '-G',
539 'config=' + config,
540 ]
541 for d in shlex.split(gyp_defines):
542 cmd += ['-D', d]
543 return cmd
544
545 def RunGNAnalyze(self, _vals):
dpranked8113582015-06-05 20:08:25546 inp = self.ReadInputJSON(['files', 'targets'])
dprankecda00332015-04-11 04:18:32547 if self.args.verbose:
548 self.Print()
549 self.Print('analyze input:')
550 self.PrintJSON(inp)
551 self.Print()
552
553 output_path = self.args.output_path[0]
dprankefe4602312015-04-08 16:20:35554
555 # Bail out early if a GN file was modified, since 'gn refs' won't know
556 # what to do about it.
557 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']):
Dirk Prankec965fa32015-04-14 23:46:29558 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
dprankefe4602312015-04-08 16:20:35559 return 0
560
dprankef61de2f2015-05-14 04:09:56561 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it.
562 if 'all' in inp['targets']:
dpranke76734662015-04-16 02:17:50563 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
564 return 0
565
Dirk Pranke12ee2db2015-04-14 23:15:32566 ret = 0
dprankef61de2f2015-05-14 04:09:56567 response_file = self.TempFile()
568 response_file.write('\n'.join(inp['files']) + '\n')
569 response_file.close()
570
571 matching_targets = []
572 try:
dpranked1fba482015-04-14 20:54:51573 cmd = self.GNCmd('refs', self.args.path[0]) + [
dpranke067d0142015-05-14 22:52:45574 '@%s' % response_file.name, '--all', '--as=output']
dprankecda00332015-04-11 04:18:32575 ret, out, _ = self.Run(cmd)
dpranke0b3b7882015-04-24 03:38:12576 if ret and not 'The input matches no targets' in out:
dprankecda00332015-04-11 04:18:32577 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
578 output_path)
dprankef61de2f2015-05-14 04:09:56579 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep
580 for output in out.splitlines():
581 build_output = output.replace(build_dir, '')
582 if build_output in inp['targets']:
583 matching_targets.append(build_output)
dpranke067d0142015-05-14 22:52:45584
585 cmd = self.GNCmd('refs', self.args.path[0]) + [
586 '@%s' % response_file.name, '--all']
587 ret, out, _ = self.Run(cmd)
588 if ret and not 'The input matches no targets' in out:
589 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
590 output_path)
591 for label in out.splitlines():
592 build_target = label[2:]
593 # We want to accept 'chrome/android:chrome_shell_apk' and
594 # just 'chrome_shell_apk'. This may result in too many targets
595 # getting built, but we can adjust that later if need be.
596 for input_target in inp['targets']:
597 if (input_target == build_target or
598 build_target.endswith(':' + input_target)):
599 matching_targets.append(input_target)
dprankef61de2f2015-05-14 04:09:56600 finally:
601 self.RemoveFile(response_file.name)
dprankefe4602312015-04-08 16:20:35602
dprankef61de2f2015-05-14 04:09:56603 if matching_targets:
dprankefe4602312015-04-08 16:20:35604 # TODO: it could be that a target X might depend on a target Y
605 # and both would be listed in the input, but we would only need
606 # to specify target X as a build_target (whereas both X and Y are
607 # targets). I'm not sure if that optimization is generally worth it.
dprankef61de2f2015-05-14 04:09:56608 self.WriteJSON({'targets': sorted(matching_targets),
609 'build_targets': sorted(matching_targets),
dprankecda00332015-04-11 04:18:32610 'status': 'Found dependency'}, output_path)
dprankefe4602312015-04-08 16:20:35611 else:
612 self.WriteJSON({'targets': [],
613 'build_targets': [],
dprankecda00332015-04-11 04:18:32614 'status': 'No dependency'}, output_path)
615
616 if not ret and self.args.verbose:
617 outp = json.loads(self.ReadFile(output_path))
618 self.Print()
619 self.Print('analyze output:')
620 self.PrintJSON(outp)
621 self.Print()
dprankefe4602312015-04-08 16:20:35622
623 return 0
624
dpranked8113582015-06-05 20:08:25625 def ReadInputJSON(self, required_keys):
dprankefe4602312015-04-08 16:20:35626 path = self.args.input_path[0]
dprankecda00332015-04-11 04:18:32627 output_path = self.args.output_path[0]
dprankefe4602312015-04-08 16:20:35628 if not self.Exists(path):
dprankecda00332015-04-11 04:18:32629 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
dprankefe4602312015-04-08 16:20:35630
631 try:
632 inp = json.loads(self.ReadFile(path))
633 except Exception as e:
634 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
dprankecda00332015-04-11 04:18:32635 (path, e), output_path)
dpranked8113582015-06-05 20:08:25636
637 for k in required_keys:
638 if not k in inp:
639 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
640 output_path)
dprankefe4602312015-04-08 16:20:35641
642 return inp
643
dprankecda00332015-04-11 04:18:32644 def WriteFailureAndRaise(self, msg, path):
645 self.WriteJSON({'error': msg}, path)
dprankefe4602312015-04-08 16:20:35646 raise MBErr(msg)
647
dprankecda00332015-04-11 04:18:32648 def WriteJSON(self, obj, path):
649 try:
650 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n')
651 except Exception as e:
652 raise MBErr('Error %s writing to the output path "%s"' %
653 (e, path))
dprankefe4602312015-04-08 16:20:35654
655 def PrintCmd(self, cmd):
656 if cmd[0] == sys.executable:
657 cmd = ['python'] + cmd[1:]
658 self.Print(*[pipes.quote(c) for c in cmd])
659
dprankecda00332015-04-11 04:18:32660 def PrintJSON(self, obj):
661 self.Print(json.dumps(obj, indent=2, sort_keys=True))
662
dprankefe4602312015-04-08 16:20:35663 def Print(self, *args, **kwargs):
664 # This function largely exists so it can be overridden for testing.
665 print(*args, **kwargs)
666
667 def Run(self, cmd):
668 # This function largely exists so it can be overridden for testing.
669 if self.args.dryrun or self.args.verbose:
670 self.PrintCmd(cmd)
671 if self.args.dryrun:
672 return 0, '', ''
673 ret, out, err = self.Call(cmd)
674 if self.args.verbose:
675 if out:
dprankeee5b51f62015-04-09 00:03:22676 self.Print(out, end='')
dprankefe4602312015-04-08 16:20:35677 if err:
dprankeee5b51f62015-04-09 00:03:22678 self.Print(err, end='', file=sys.stderr)
dprankefe4602312015-04-08 16:20:35679 return ret, out, err
680
681 def Call(self, cmd):
682 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
683 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
684 out, err = p.communicate()
685 return p.returncode, out, err
686
687 def ExpandUser(self, path):
688 # This function largely exists so it can be overridden for testing.
689 return os.path.expanduser(path)
690
691 def Exists(self, path):
692 # This function largely exists so it can be overridden for testing.
693 return os.path.exists(path)
694
695 def ReadFile(self, path):
696 # This function largely exists so it can be overriden for testing.
697 with open(path) as fp:
698 return fp.read()
699
dprankef61de2f2015-05-14 04:09:56700 def RemoveFile(self, path):
701 # This function largely exists so it can be overriden for testing.
702 os.remove(path)
703
704 def TempFile(self, mode='w'):
705 # This function largely exists so it can be overriden for testing.
706 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
707
dprankefe4602312015-04-08 16:20:35708 def WriteFile(self, path, contents):
709 # This function largely exists so it can be overriden for testing.
710 with open(path, 'w') as fp:
711 return fp.write(contents)
712
dprankef61de2f2015-05-14 04:09:56713
dprankefe4602312015-04-08 16:20:35714class MBErr(Exception):
715 pass
716
717
718if __name__ == '__main__':
719 try:
720 sys.exit(main(sys.argv[1:]))
721 except MBErr as e:
722 print(e)
723 sys.exit(1)
724 except KeyboardInterrupt:
725 print("interrupted, exiting", stream=sys.stderr)
726 sys.exit(130)