blob: 7f9aab69fa61f18178c0b66c8da13a08ccb61e9a [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
dprankec3441d12015-06-23 23:01:3516import errno
dprankefe4602312015-04-08 16:20:3517import json
18import os
19import pipes
dpranked8113582015-06-05 20:08:2520import pprint
dprankefe4602312015-04-08 16:20:3521import shlex
22import shutil
23import sys
24import subprocess
dprankef61de2f2015-05-14 04:09:5625import tempfile
dprankefe4602312015-04-08 16:20:3526
27
28def main(args):
dprankeee5b51f62015-04-09 00:03:2229 mbw = MetaBuildWrapper()
30 mbw.ParseArgs(args)
31 return mbw.args.func()
dprankefe4602312015-04-08 16:20:3532
33
34class MetaBuildWrapper(object):
35 def __init__(self):
36 p = os.path
37 d = os.path.dirname
38 self.chromium_src_dir = p.normpath(d(d(d(p.abspath(__file__)))))
39 self.default_config = p.join(self.chromium_src_dir, 'tools', 'mb',
40 'mb_config.pyl')
dpranked1fba482015-04-14 20:54:5141 self.platform = sys.platform
dprankefe4602312015-04-08 16:20:3542 self.args = argparse.Namespace()
43 self.configs = {}
44 self.masters = {}
45 self.mixins = {}
46 self.private_configs = []
47 self.common_dev_configs = []
48 self.unsupported_configs = []
49
50 def ParseArgs(self, argv):
51 def AddCommonOptions(subp):
52 subp.add_argument('-b', '--builder',
53 help='builder name to look up config from')
54 subp.add_argument('-m', '--master',
55 help='master name to look up config from')
56 subp.add_argument('-c', '--config',
57 help='configuration to analyze')
58 subp.add_argument('-f', '--config-file', metavar='PATH',
59 default=self.default_config,
60 help='path to config file '
61 '(default is //tools/mb/mb_config.pyl)')
62 subp.add_argument('-g', '--goma-dir', default=self.ExpandUser('~/goma'),
63 help='path to goma directory (default is %(default)s).')
64 subp.add_argument('-n', '--dryrun', action='store_true',
65 help='Do a dry run (i.e., do nothing, just print '
66 'the commands that will run)')
67 subp.add_argument('-q', '--quiet', action='store_true',
68 help='Do not print anything, just return an exit '
69 'code.')
70 subp.add_argument('-v', '--verbose', action='count',
71 help='verbose logging (may specify multiple times).')
72
73 parser = argparse.ArgumentParser(prog='mb')
74 subps = parser.add_subparsers()
75
76 subp = subps.add_parser('analyze',
77 help='analyze whether changes to a set of files '
78 'will cause a set of binaries to be rebuilt.')
79 AddCommonOptions(subp)
Dirk Pranke0fd41bcd2015-06-19 00:05:5080 subp.add_argument('--swarming-targets-file',
81 help='save runtime dependencies for targets listed '
82 'in file.')
dpranked8113582015-06-05 20:08:2583 subp.add_argument('path', nargs=1,
dprankefe4602312015-04-08 16:20:3584 help='path build was generated into.')
85 subp.add_argument('input_path', nargs=1,
86 help='path to a file containing the input arguments '
87 'as a JSON object.')
88 subp.add_argument('output_path', nargs=1,
89 help='path to a file containing the output arguments '
90 'as a JSON object.')
91 subp.set_defaults(func=self.CmdAnalyze)
92
93 subp = subps.add_parser('gen',
94 help='generate a new set of build files')
95 AddCommonOptions(subp)
dpranke74559b52015-06-10 21:20:3996 subp.add_argument('--swarming-targets-file',
97 help='save runtime dependencies for targets listed '
98 'in file.')
dpranked8113582015-06-05 20:08:2599 subp.add_argument('path', nargs=1,
dprankefe4602312015-04-08 16:20:35100 help='path to generate build into')
101 subp.set_defaults(func=self.CmdGen)
102
103 subp = subps.add_parser('lookup',
104 help='look up the command for a given config or '
105 'builder')
106 AddCommonOptions(subp)
107 subp.set_defaults(func=self.CmdLookup)
108
109 subp = subps.add_parser('validate',
110 help='validate the config file')
111 AddCommonOptions(subp)
112 subp.set_defaults(func=self.CmdValidate)
113
114 subp = subps.add_parser('help',
115 help='Get help on a subcommand.')
116 subp.add_argument(nargs='?', action='store', dest='subcommand',
117 help='The command to get help for.')
118 subp.set_defaults(func=self.CmdHelp)
119
120 self.args = parser.parse_args(argv)
121
122 def CmdAnalyze(self):
123 vals = self.GetConfig()
124 if vals['type'] == 'gn':
125 return self.RunGNAnalyze(vals)
126 elif vals['type'] == 'gyp':
127 return self.RunGYPAnalyze(vals)
128 else:
129 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
130
131 def CmdGen(self):
132 vals = self.GetConfig()
133 if vals['type'] == 'gn':
Dirk Pranke0fd41bcd2015-06-19 00:05:50134 return self.RunGNGen(vals)
dpranke08d2ab12015-04-24 21:54:20135 if vals['type'] == 'gyp':
Dirk Pranke0fd41bcd2015-06-19 00:05:50136 return self.RunGYPGen(vals)
dpranke08d2ab12015-04-24 21:54:20137
138 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
dprankefe4602312015-04-08 16:20:35139
140 def CmdLookup(self):
141 vals = self.GetConfig()
142 if vals['type'] == 'gn':
dpranked1fba482015-04-14 20:54:51143 cmd = self.GNCmd('gen', '<path>', vals['gn_args'])
dprankefe4602312015-04-08 16:20:35144 elif vals['type'] == 'gyp':
145 cmd = self.GYPCmd('<path>', vals['gyp_defines'], vals['gyp_config'])
146 else:
147 raise MBErr('Unknown meta-build type "%s"' % vals['type'])
148
149 self.PrintCmd(cmd)
150 return 0
151
152 def CmdHelp(self):
153 if self.args.subcommand:
154 self.ParseArgs([self.args.subcommand, '--help'])
155 else:
156 self.ParseArgs(['--help'])
157
158 def CmdValidate(self):
159 errs = []
160
161 # Read the file to make sure it parses.
162 self.ReadConfigFile()
163
164 # Figure out the whole list of configs and ensure that no config is
165 # listed in more than one category.
166 all_configs = {}
167 for config in self.common_dev_configs:
168 all_configs[config] = 'common_dev_configs'
169 for config in self.private_configs:
170 if config in all_configs:
171 errs.append('config "%s" listed in "private_configs" also '
172 'listed in "%s"' % (config, all_configs['config']))
173 else:
174 all_configs[config] = 'private_configs'
175 for config in self.unsupported_configs:
176 if config in all_configs:
177 errs.append('config "%s" listed in "unsupported_configs" also '
178 'listed in "%s"' % (config, all_configs['config']))
179 else:
180 all_configs[config] = 'unsupported_configs'
181
182 for master in self.masters:
183 for builder in self.masters[master]:
184 config = self.masters[master][builder]
185 if config in all_configs and all_configs[config] not in self.masters:
186 errs.append('Config "%s" used by a bot is also listed in "%s".' %
187 (config, all_configs[config]))
188 else:
189 all_configs[config] = master
190
191 # Check that every referenced config actually exists.
192 for config, loc in all_configs.items():
193 if not config in self.configs:
194 errs.append('Unknown config "%s" referenced from "%s".' %
195 (config, loc))
196
197 # Check that every actual config is actually referenced.
198 for config in self.configs:
199 if not config in all_configs:
200 errs.append('Unused config "%s".' % config)
201
202 # Figure out the whole list of mixins, and check that every mixin
203 # listed by a config or another mixin actually exists.
204 referenced_mixins = set()
205 for config, mixins in self.configs.items():
206 for mixin in mixins:
207 if not mixin in self.mixins:
208 errs.append('Unknown mixin "%s" referenced by config "%s".' %
209 (mixin, config))
210 referenced_mixins.add(mixin)
211
212 for mixin in self.mixins:
213 for sub_mixin in self.mixins[mixin].get('mixins', []):
214 if not sub_mixin in self.mixins:
215 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
216 (sub_mixin, mixin))
217 referenced_mixins.add(sub_mixin)
218
219 # Check that every mixin defined is actually referenced somewhere.
220 for mixin in self.mixins:
221 if not mixin in referenced_mixins:
222 errs.append('Unreferenced mixin "%s".' % mixin)
223
224 if errs:
225 raise MBErr('mb config file %s has problems:\n ' + '\n '.join(errs))
226
227 if not self.args.quiet:
228 self.Print('mb config file %s looks ok.' % self.args.config_file)
229 return 0
230
231 def GetConfig(self):
232 self.ReadConfigFile()
233 config = self.ConfigFromArgs()
234 if not config in self.configs:
235 raise MBErr('Config "%s" not found in %s' %
236 (config, self.args.config_file))
237
238 return self.FlattenConfig(config)
239
240 def ReadConfigFile(self):
241 if not self.Exists(self.args.config_file):
242 raise MBErr('config file not found at %s' % self.args.config_file)
243
244 try:
245 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
246 except SyntaxError as e:
247 raise MBErr('Failed to parse config file "%s": %s' %
248 (self.args.config_file, e))
249
250 self.common_dev_configs = contents['common_dev_configs']
251 self.configs = contents['configs']
252 self.masters = contents['masters']
253 self.mixins = contents['mixins']
254 self.private_configs = contents['private_configs']
255 self.unsupported_configs = contents['unsupported_configs']
256
257 def ConfigFromArgs(self):
258 if self.args.config:
259 if self.args.master or self.args.builder:
260 raise MBErr('Can not specific both -c/--config and -m/--master or '
261 '-b/--builder')
262
263 return self.args.config
264
265 if not self.args.master or not self.args.builder:
266 raise MBErr('Must specify either -c/--config or '
267 '(-m/--master and -b/--builder)')
268
269 if not self.args.master in self.masters:
270 raise MBErr('Master name "%s" not found in "%s"' %
271 (self.args.master, self.args.config_file))
272
273 if not self.args.builder in self.masters[self.args.master]:
274 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
275 (self.args.builder, self.args.master, self.args.config_file))
276
277 return self.masters[self.args.master][self.args.builder]
278
279 def FlattenConfig(self, config):
280 mixins = self.configs[config]
281 vals = {
282 'type': None,
283 'gn_args': [],
284 'gyp_config': [],
285 'gyp_defines': [],
286 }
287
288 visited = []
289 self.FlattenMixins(mixins, vals, visited)
290 return vals
291
292 def FlattenMixins(self, mixins, vals, visited):
293 for m in mixins:
294 if m not in self.mixins:
295 raise MBErr('Unknown mixin "%s"' % m)
dprankeee5b51f62015-04-09 00:03:22296
297 # TODO: check for cycles in mixins.
dprankefe4602312015-04-08 16:20:35298
299 visited.append(m)
300
301 mixin_vals = self.mixins[m]
302 if 'type' in mixin_vals:
303 vals['type'] = mixin_vals['type']
304 if 'gn_args' in mixin_vals:
305 if vals['gn_args']:
306 vals['gn_args'] += ' ' + mixin_vals['gn_args']
307 else:
308 vals['gn_args'] = mixin_vals['gn_args']
309 if 'gyp_config' in mixin_vals:
310 vals['gyp_config'] = mixin_vals['gyp_config']
311 if 'gyp_defines' in mixin_vals:
312 if vals['gyp_defines']:
313 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
314 else:
315 vals['gyp_defines'] = mixin_vals['gyp_defines']
316 if 'mixins' in mixin_vals:
317 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
318 return vals
319
Dirk Pranke0fd41bcd2015-06-19 00:05:50320 def RunGNGen(self, vals):
321 path = self.args.path[0]
322
dpranked1fba482015-04-14 20:54:51323 cmd = self.GNCmd('gen', path, vals['gn_args'])
dpranke74559b52015-06-10 21:20:39324
325 swarming_targets = []
326 if self.args.swarming_targets_file:
327 # We need GN to generate the list of runtime dependencies for
328 # the compile targets listed (one per line) in the file so
329 # we can run them via swarming. We use ninja_to_gn.pyl to convert
330 # the compile targets to the matching GN labels.
331 contents = self.ReadFile(self.args.swarming_targets_file)
332 swarming_targets = contents.splitlines()
333 ninja_targets_to_labels = ast.literal_eval(self.ReadFile(os.path.join(
334 self.chromium_src_dir, 'testing', 'buildbot', 'ninja_to_gn.pyl')))
335 gn_labels = []
336 for target in swarming_targets:
337 if not target in ninja_targets_to_labels:
338 raise MBErr('test target "%s" not found in %s' %
339 (target, '//testing/buildbot/ninja_to_gn.pyl'))
340 gn_labels.append(ninja_targets_to_labels[target])
341
342 gn_runtime_deps_path = self.ToAbsPath(path, 'runtime_deps')
dprankec3441d12015-06-23 23:01:35343
344 # Since GN hasn't run yet, the build directory may not even exist.
345 self.MaybeMakeDirectory(self.ToAbsPath(path))
346
dpranke74559b52015-06-10 21:20:39347 self.WriteFile(gn_runtime_deps_path, '\n'.join(gn_labels) + '\n')
348 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
349
dprankefe4602312015-04-08 16:20:35350 ret, _, _ = self.Run(cmd)
dpranke74559b52015-06-10 21:20:39351
352 for target in swarming_targets:
dpranke34bd39d2015-06-24 02:36:52353 if sys.platform == 'win32':
354 deps_path = self.ToAbsPath(path, target + '.exe.runtime_deps')
355 else:
356 deps_path = self.ToAbsPath(path, target + '.runtime_deps')
dpranke74559b52015-06-10 21:20:39357 if not self.Exists(deps_path):
358 raise MBErr('did not generate %s' % deps_path)
359
dpranked5b2b9432015-06-23 16:55:30360 command, extra_files = self.GetIsolateCommand(target, vals)
361
362 runtime_deps = self.ReadFile(deps_path).splitlines()
363
364 isolate_path = self.ToAbsPath(path, target + '.isolate')
365 self.WriteFile(isolate_path,
366 pprint.pformat({
367 'variables': {
368 'command': command,
369 'files': sorted(runtime_deps + extra_files),
370 'read_only': 1,
371 }
372 }) + '\n')
373
374 self.WriteJSON(
375 {
376 'args': [
377 '--isolated',
dpranke34bd39d2015-06-24 02:36:52378 self.ToSrcRelPath('%s%s%s.isolated' % (path, os.sep, target)),
dpranked5b2b9432015-06-23 16:55:30379 '--isolate',
dpranke34bd39d2015-06-24 02:36:52380 self.ToSrcRelPath('%s%s%s.isolate' % (path, os.sep, target)),
dpranked5b2b9432015-06-23 16:55:30381 ],
382 'dir': self.chromium_src_dir,
383 'version': 1,
384 },
385 isolate_path + 'd.gen.json',
386 )
387
388
dprankefe4602312015-04-08 16:20:35389 return ret
390
dpranked1fba482015-04-14 20:54:51391 def GNCmd(self, subcommand, path, gn_args=''):
392 if self.platform == 'linux2':
393 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'linux64',
394 'gn')
395 elif self.platform == 'darwin':
396 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'mac',
397 'gn')
398 else:
399 gn_path = os.path.join(self.chromium_src_dir, 'buildtools', 'win',
400 'gn.exe')
401
402 cmd = [gn_path, subcommand, path]
dprankeee5b51f62015-04-09 00:03:22403 gn_args = gn_args.replace("$(goma_dir)", self.args.goma_dir)
dprankefe4602312015-04-08 16:20:35404 if gn_args:
405 cmd.append('--args=%s' % gn_args)
406 return cmd
407
Dirk Pranke0fd41bcd2015-06-19 00:05:50408 def RunGYPGen(self, vals):
409 path = self.args.path[0]
410
dprankefe4602312015-04-08 16:20:35411 output_dir, gyp_config = self.ParseGYPConfigPath(path)
412 if gyp_config != vals['gyp_config']:
413 raise MBErr('The last component of the path (%s) must match the '
414 'GYP configuration specified in the config (%s), and '
415 'it does not.' % (gyp_config, vals['gyp_config']))
416 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
417 ret, _, _ = self.Run(cmd)
418 return ret
419
420 def RunGYPAnalyze(self, vals):
421 output_dir, gyp_config = self.ParseGYPConfigPath(self.args.path[0])
422 if gyp_config != vals['gyp_config']:
423 raise MBErr('The last component of the path (%s) must match the '
424 'GYP configuration specified in the config (%s), and '
425 'it does not.' % (gyp_config, vals['gyp_config']))
dprankecda00332015-04-11 04:18:32426 if self.args.verbose:
427 inp = self.GetAnalyzeInput()
428 self.Print()
429 self.Print('analyze input:')
430 self.PrintJSON(inp)
431 self.Print()
432
dprankefe4602312015-04-08 16:20:35433 cmd = self.GYPCmd(output_dir, vals['gyp_defines'], config=gyp_config)
434 cmd.extend(['-G', 'config_path=%s' % self.args.input_path[0],
435 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
436 ret, _, _ = self.Run(cmd)
dprankecda00332015-04-11 04:18:32437 if not ret and self.args.verbose:
438 outp = json.loads(self.ReadFile(self.args.output_path[0]))
439 self.Print()
440 self.Print('analyze output:')
dpranke74559b52015-06-10 21:20:39441 self.PrintJSON(outp)
dprankecda00332015-04-11 04:18:32442 self.Print()
443
dprankefe4602312015-04-08 16:20:35444 return ret
445
dpranked8113582015-06-05 20:08:25446 def RunGNIsolate(self, vals):
447 build_path = self.args.path[0]
448 inp = self.ReadInputJSON(['targets'])
449 if self.args.verbose:
450 self.Print()
451 self.Print('isolate input:')
452 self.PrintJSON(inp)
453 self.Print()
454 output_path = self.args.output_path[0]
455
456 for target in inp['targets']:
457 runtime_deps_path = self.ToAbsPath(build_path, target + '.runtime_deps')
458
459 if not self.Exists(runtime_deps_path):
460 self.WriteFailureAndRaise('"%s" does not exist' % runtime_deps_path,
461 output_path)
462
463 command, extra_files = self.GetIsolateCommand(target, vals)
464
465 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
466
467
468 isolate_path = self.ToAbsPath(build_path, target + '.isolate')
469 self.WriteFile(isolate_path,
470 pprint.pformat({
471 'variables': {
472 'command': command,
473 'files': sorted(runtime_deps + extra_files),
474 'read_only': 1,
475 }
476 }) + '\n')
477
478 self.WriteJSON(
479 {
480 'args': [
481 '--isolated',
482 self.ToSrcRelPath('%s/%s.isolated' % (build_path, target)),
483 '--isolate',
484 self.ToSrcRelPath('%s/%s.isolate' % (build_path, target)),
485 ],
486 'dir': self.chromium_src_dir,
487 'version': 1,
488 },
dpranked5b2b9432015-06-23 16:55:30489 isolate_path + 'd.gen.json',
dpranked8113582015-06-05 20:08:25490 )
491
492 return 0
493
494 def GetIsolateCommand(self, target, vals):
dpranked8113582015-06-05 20:08:25495 extra_files = []
496
497 # TODO(dpranke): We should probably pull this from
498 # the test list info in //testing/buildbot/*.json,
499 # and assert that the test has can_use_on_swarming_builders: True,
500 # but we hardcode it here for now.
501 test_type = {}.get(target, 'gtest_test')
502
503 # This needs to mirror the settings in //build/config/ui.gni:
504 # use_x11 = is_linux && !use_ozone.
505 # TODO(dpranke): Figure out how to keep this in sync better.
506 use_x11 = (sys.platform == 'linux2' and
507 not 'target_os="android"' in vals['gn_args'] and
508 not 'use_ozone=true' in vals['gn_args'])
509
510 asan = 'is_asan=true' in vals['gn_args']
511 msan = 'is_msan=true' in vals['gn_args']
512 tsan = 'is_tsan=true' in vals['gn_args']
513
514 executable_suffix = '.exe' if sys.platform == 'win32' else ''
515
516 if test_type == 'gtest_test':
517 extra_files.append('../../testing/test_env.py')
518
519 if use_x11:
520 # TODO(dpranke): Figure out some way to figure out which
521 # test steps really need xvfb.
522 extra_files.append('xdisplaycheck')
523 extra_files.append('../../testing/xvfb.py')
524
525 cmdline = [
526 '../../testing/xvfb.py',
527 '.',
528 './' + str(target),
529 '--brave-new-test-launcher',
530 '--test-launcher-bot-mode',
531 '--asan=%d' % asan,
532 '--msan=%d' % msan,
533 '--tsan=%d' % tsan,
534 ]
535 else:
536 cmdline = [
537 '../../testing/test_env.py',
538 '.',
539 './' + str(target) + executable_suffix,
540 '--brave-new-test-launcher',
541 '--test-launcher-bot-mode',
542 '--asan=%d' % asan,
543 '--msan=%d' % msan,
544 '--tsan=%d' % tsan,
545 ]
546 else:
dpranked5b2b9432015-06-23 16:55:30547 # TODO(dpranke): Handle script_tests and other types of swarmed tests.
dpranked8113582015-06-05 20:08:25548 self.WriteFailureAndRaise('unknown test type "%s" for %s' %
dpranked5b2b9432015-06-23 16:55:30549 (test_type, target), output_path=None)
dpranked8113582015-06-05 20:08:25550
551
552 return cmdline, extra_files
553
dpranke74559b52015-06-10 21:20:39554 def ToAbsPath(self, build_path, *comps):
dpranked8113582015-06-05 20:08:25555 return os.path.join(self.chromium_src_dir,
556 self.ToSrcRelPath(build_path),
dpranke74559b52015-06-10 21:20:39557 *comps)
dpranked8113582015-06-05 20:08:25558
dprankeee5b51f62015-04-09 00:03:22559 def ToSrcRelPath(self, path):
560 """Returns a relative path from the top of the repo."""
561 # TODO: Support normal paths in addition to source-absolute paths.
dprankefe4602312015-04-08 16:20:35562 assert(path.startswith('//'))
dpranke34bd39d2015-06-24 02:36:52563 return path[2:].replace('/', os.sep)
dprankefe4602312015-04-08 16:20:35564
565 def ParseGYPConfigPath(self, path):
dprankeee5b51f62015-04-09 00:03:22566 rpath = self.ToSrcRelPath(path)
567 output_dir, _, config = rpath.rpartition('/')
dprankefe4602312015-04-08 16:20:35568 self.CheckGYPConfigIsSupported(config, path)
569 return output_dir, config
570
571 def CheckGYPConfigIsSupported(self, config, path):
572 if config not in ('Debug', 'Release'):
573 if (sys.platform in ('win32', 'cygwin') and
574 config not in ('Debug_x64', 'Release_x64')):
575 raise MBErr('Unknown or unsupported config type "%s" in "%s"' %
576 config, path)
577
578 def GYPCmd(self, output_dir, gyp_defines, config):
579 gyp_defines = gyp_defines.replace("$(goma_dir)", self.args.goma_dir)
580 cmd = [
581 sys.executable,
582 os.path.join('build', 'gyp_chromium'),
583 '-G',
584 'output_dir=' + output_dir,
585 '-G',
586 'config=' + config,
587 ]
588 for d in shlex.split(gyp_defines):
589 cmd += ['-D', d]
590 return cmd
591
Dirk Pranke0fd41bcd2015-06-19 00:05:50592 def RunGNAnalyze(self, vals):
593 # analyze runs before 'gn gen' now, so we need to run gn gen
594 # in order to ensure that we have a build directory.
595 ret = self.RunGNGen(vals)
596 if ret:
597 return ret
598
dpranked8113582015-06-05 20:08:25599 inp = self.ReadInputJSON(['files', 'targets'])
dprankecda00332015-04-11 04:18:32600 if self.args.verbose:
601 self.Print()
602 self.Print('analyze input:')
603 self.PrintJSON(inp)
604 self.Print()
605
606 output_path = self.args.output_path[0]
dprankefe4602312015-04-08 16:20:35607
608 # Bail out early if a GN file was modified, since 'gn refs' won't know
609 # what to do about it.
610 if any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']):
Dirk Prankec965fa32015-04-14 23:46:29611 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
dprankefe4602312015-04-08 16:20:35612 return 0
613
dprankef61de2f2015-05-14 04:09:56614 # Bail out early if 'all' was asked for, since 'gn refs' won't recognize it.
615 if 'all' in inp['targets']:
dpranke76734662015-04-16 02:17:50616 self.WriteJSON({'status': 'Found dependency (all)'}, output_path)
617 return 0
618
Dirk Pranke12ee2db2015-04-14 23:15:32619 ret = 0
dprankef61de2f2015-05-14 04:09:56620 response_file = self.TempFile()
621 response_file.write('\n'.join(inp['files']) + '\n')
622 response_file.close()
623
624 matching_targets = []
625 try:
dpranked1fba482015-04-14 20:54:51626 cmd = self.GNCmd('refs', self.args.path[0]) + [
dpranke067d0142015-05-14 22:52:45627 '@%s' % response_file.name, '--all', '--as=output']
dprankecda00332015-04-11 04:18:32628 ret, out, _ = self.Run(cmd)
dpranke0b3b7882015-04-24 03:38:12629 if ret and not 'The input matches no targets' in out:
dprankecda00332015-04-11 04:18:32630 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
631 output_path)
dprankef61de2f2015-05-14 04:09:56632 build_dir = self.ToSrcRelPath(self.args.path[0]) + os.sep
633 for output in out.splitlines():
634 build_output = output.replace(build_dir, '')
635 if build_output in inp['targets']:
636 matching_targets.append(build_output)
dpranke067d0142015-05-14 22:52:45637
638 cmd = self.GNCmd('refs', self.args.path[0]) + [
639 '@%s' % response_file.name, '--all']
640 ret, out, _ = self.Run(cmd)
641 if ret and not 'The input matches no targets' in out:
642 self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
643 output_path)
644 for label in out.splitlines():
645 build_target = label[2:]
646 # We want to accept 'chrome/android:chrome_shell_apk' and
647 # just 'chrome_shell_apk'. This may result in too many targets
648 # getting built, but we can adjust that later if need be.
649 for input_target in inp['targets']:
650 if (input_target == build_target or
651 build_target.endswith(':' + input_target)):
652 matching_targets.append(input_target)
dprankef61de2f2015-05-14 04:09:56653 finally:
654 self.RemoveFile(response_file.name)
dprankefe4602312015-04-08 16:20:35655
dprankef61de2f2015-05-14 04:09:56656 if matching_targets:
dprankefe4602312015-04-08 16:20:35657 # TODO: it could be that a target X might depend on a target Y
658 # and both would be listed in the input, but we would only need
659 # to specify target X as a build_target (whereas both X and Y are
660 # targets). I'm not sure if that optimization is generally worth it.
dprankef61de2f2015-05-14 04:09:56661 self.WriteJSON({'targets': sorted(matching_targets),
662 'build_targets': sorted(matching_targets),
dprankecda00332015-04-11 04:18:32663 'status': 'Found dependency'}, output_path)
dprankefe4602312015-04-08 16:20:35664 else:
665 self.WriteJSON({'targets': [],
666 'build_targets': [],
dprankecda00332015-04-11 04:18:32667 'status': 'No dependency'}, output_path)
668
669 if not ret and self.args.verbose:
670 outp = json.loads(self.ReadFile(output_path))
671 self.Print()
672 self.Print('analyze output:')
673 self.PrintJSON(outp)
674 self.Print()
dprankefe4602312015-04-08 16:20:35675
676 return 0
677
dpranked8113582015-06-05 20:08:25678 def ReadInputJSON(self, required_keys):
dprankefe4602312015-04-08 16:20:35679 path = self.args.input_path[0]
dprankecda00332015-04-11 04:18:32680 output_path = self.args.output_path[0]
dprankefe4602312015-04-08 16:20:35681 if not self.Exists(path):
dprankecda00332015-04-11 04:18:32682 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
dprankefe4602312015-04-08 16:20:35683
684 try:
685 inp = json.loads(self.ReadFile(path))
686 except Exception as e:
687 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
dprankecda00332015-04-11 04:18:32688 (path, e), output_path)
dpranked8113582015-06-05 20:08:25689
690 for k in required_keys:
691 if not k in inp:
692 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
693 output_path)
dprankefe4602312015-04-08 16:20:35694
695 return inp
696
dpranked5b2b9432015-06-23 16:55:30697 def WriteFailureAndRaise(self, msg, output_path):
698 if output_path:
699 self.WriteJSON({'error': msg}, output_path)
dprankefe4602312015-04-08 16:20:35700 raise MBErr(msg)
701
dprankecda00332015-04-11 04:18:32702 def WriteJSON(self, obj, path):
703 try:
704 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n')
705 except Exception as e:
706 raise MBErr('Error %s writing to the output path "%s"' %
707 (e, path))
dprankefe4602312015-04-08 16:20:35708
709 def PrintCmd(self, cmd):
710 if cmd[0] == sys.executable:
711 cmd = ['python'] + cmd[1:]
712 self.Print(*[pipes.quote(c) for c in cmd])
713
dprankecda00332015-04-11 04:18:32714 def PrintJSON(self, obj):
715 self.Print(json.dumps(obj, indent=2, sort_keys=True))
716
dprankefe4602312015-04-08 16:20:35717 def Print(self, *args, **kwargs):
718 # This function largely exists so it can be overridden for testing.
719 print(*args, **kwargs)
720
721 def Run(self, cmd):
722 # This function largely exists so it can be overridden for testing.
723 if self.args.dryrun or self.args.verbose:
724 self.PrintCmd(cmd)
725 if self.args.dryrun:
726 return 0, '', ''
727 ret, out, err = self.Call(cmd)
728 if self.args.verbose:
729 if out:
dprankeee5b51f62015-04-09 00:03:22730 self.Print(out, end='')
dprankefe4602312015-04-08 16:20:35731 if err:
dprankeee5b51f62015-04-09 00:03:22732 self.Print(err, end='', file=sys.stderr)
dprankefe4602312015-04-08 16:20:35733 return ret, out, err
734
735 def Call(self, cmd):
736 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
737 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
738 out, err = p.communicate()
739 return p.returncode, out, err
740
741 def ExpandUser(self, path):
742 # This function largely exists so it can be overridden for testing.
743 return os.path.expanduser(path)
744
745 def Exists(self, path):
746 # This function largely exists so it can be overridden for testing.
747 return os.path.exists(path)
748
dprankec3441d12015-06-23 23:01:35749 def MaybeMakeDirectory(self, path):
750 try:
751 os.makedirs(path)
752 except OSError, e:
753 if e.errno != errno.EEXIST:
754 raise
755
dprankefe4602312015-04-08 16:20:35756 def ReadFile(self, path):
757 # This function largely exists so it can be overriden for testing.
758 with open(path) as fp:
759 return fp.read()
760
dprankef61de2f2015-05-14 04:09:56761 def RemoveFile(self, path):
762 # This function largely exists so it can be overriden for testing.
763 os.remove(path)
764
765 def TempFile(self, mode='w'):
766 # This function largely exists so it can be overriden for testing.
767 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
768
dprankefe4602312015-04-08 16:20:35769 def WriteFile(self, path, contents):
770 # This function largely exists so it can be overriden for testing.
dpranked5b2b9432015-06-23 16:55:30771 if self.args.dryrun or self.args.verbose:
772 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
dprankefe4602312015-04-08 16:20:35773 with open(path, 'w') as fp:
774 return fp.write(contents)
775
dprankef61de2f2015-05-14 04:09:56776
dprankefe4602312015-04-08 16:20:35777class MBErr(Exception):
778 pass
779
780
781if __name__ == '__main__':
782 try:
783 sys.exit(main(sys.argv[1:]))
784 except MBErr as e:
785 print(e)
786 sys.exit(1)
787 except KeyboardInterrupt:
788 print("interrupted, exiting", stream=sys.stderr)
789 sys.exit(130)