blob: 9d568ced126beb08910b49a76f98fd37ac4a3550 [file] [log] [blame]
kjellandera013a022016-11-14 05:54:22 -08001#!/usr/bin/env python
2# Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
3#
4# Use of this source code is governed by a BSD-style license
5# that can be found in the LICENSE file in the root of the source
6# tree. An additional intellectual property rights grant can be found
7# in the file PATENTS. All contributing project authors may
8# be found in the AUTHORS file in the root of the source tree.
9
10"""MB - the Meta-Build wrapper around GYP and GN
11
12MB is a wrapper script for GYP and GN that can be used to generate build files
13for sets of canned configurations and analyze them.
14"""
15
16from __future__ import print_function
17
18import argparse
19import ast
20import errno
21import json
22import os
23import pipes
24import pprint
25import re
26import shutil
27import sys
28import subprocess
29import tempfile
30import traceback
31import urllib2
32
33from collections import OrderedDict
34
Henrik Kjellanderb2d55772016-12-18 22:14:50 +010035SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
kjellander1c3548c2017-02-15 22:38:22 -080036SRC_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
37sys.path = [os.path.join(SRC_DIR, 'build')] + sys.path
kjellandera013a022016-11-14 05:54:22 -080038
39import gn_helpers
40
41
42def main(args):
43 mbw = MetaBuildWrapper()
44 return mbw.Main(args)
45
46
47class MetaBuildWrapper(object):
48 def __init__(self):
kjellander1c3548c2017-02-15 22:38:22 -080049 self.src_dir = SRC_DIR
Henrik Kjellanderb2d55772016-12-18 22:14:50 +010050 self.default_config = os.path.join(SCRIPT_DIR, 'mb_config.pyl')
Mirko Bonadeib63a8ac2017-01-25 09:36:50 +010051 self.default_isolate_map = os.path.join(SCRIPT_DIR, 'gn_isolate_map.pyl')
kjellandera013a022016-11-14 05:54:22 -080052 self.executable = sys.executable
53 self.platform = sys.platform
54 self.sep = os.sep
55 self.args = argparse.Namespace()
56 self.configs = {}
57 self.masters = {}
58 self.mixins = {}
59
60 def Main(self, args):
61 self.ParseArgs(args)
62 try:
63 ret = self.args.func()
64 if ret:
65 self.DumpInputFiles()
66 return ret
67 except KeyboardInterrupt:
68 self.Print('interrupted, exiting')
69 return 130
70 except Exception:
71 self.DumpInputFiles()
72 s = traceback.format_exc()
73 for l in s.splitlines():
74 self.Print(l)
75 return 1
76
77 def ParseArgs(self, argv):
78 def AddCommonOptions(subp):
79 subp.add_argument('-b', '--builder',
80 help='builder name to look up config from')
81 subp.add_argument('-m', '--master',
82 help='master name to look up config from')
83 subp.add_argument('-c', '--config',
84 help='configuration to analyze')
85 subp.add_argument('--phase',
86 help='optional phase name (used when builders '
87 'do multiple compiles with different '
88 'arguments in a single build)')
89 subp.add_argument('-f', '--config-file', metavar='PATH',
90 default=self.default_config,
91 help='path to config file '
92 '(default is %(default)s)')
93 subp.add_argument('-i', '--isolate-map-file', metavar='PATH',
94 default=self.default_isolate_map,
95 help='path to isolate map file '
96 '(default is %(default)s)')
97 subp.add_argument('-g', '--goma-dir',
98 help='path to goma directory')
99 subp.add_argument('--gyp-script', metavar='PATH',
100 default=self.PathJoin('build', 'gyp_chromium'),
101 help='path to gyp script relative to project root '
102 '(default is %(default)s)')
103 subp.add_argument('--android-version-code',
104 help='Sets GN arg android_default_version_code and '
105 'GYP_DEFINE app_manifest_version_code')
106 subp.add_argument('--android-version-name',
107 help='Sets GN arg android_default_version_name and '
108 'GYP_DEFINE app_manifest_version_name')
109 subp.add_argument('-n', '--dryrun', action='store_true',
110 help='Do a dry run (i.e., do nothing, just print '
111 'the commands that will run)')
112 subp.add_argument('-v', '--verbose', action='store_true',
113 help='verbose logging')
114
115 parser = argparse.ArgumentParser(prog='mb')
116 subps = parser.add_subparsers()
117
118 subp = subps.add_parser('analyze',
119 help='analyze whether changes to a set of files '
120 'will cause a set of binaries to be rebuilt.')
121 AddCommonOptions(subp)
122 subp.add_argument('path', nargs=1,
123 help='path build was generated into.')
124 subp.add_argument('input_path', nargs=1,
125 help='path to a file containing the input arguments '
126 'as a JSON object.')
127 subp.add_argument('output_path', nargs=1,
128 help='path to a file containing the output arguments '
129 'as a JSON object.')
130 subp.set_defaults(func=self.CmdAnalyze)
131
132 subp = subps.add_parser('export',
133 help='print out the expanded configuration for'
134 'each builder as a JSON object')
135 subp.add_argument('-f', '--config-file', metavar='PATH',
136 default=self.default_config,
137 help='path to config file (default is %(default)s)')
138 subp.add_argument('-g', '--goma-dir',
139 help='path to goma directory')
140 subp.set_defaults(func=self.CmdExport)
141
142 subp = subps.add_parser('gen',
143 help='generate a new set of build files')
144 AddCommonOptions(subp)
145 subp.add_argument('--swarming-targets-file',
146 help='save runtime dependencies for targets listed '
147 'in file.')
148 subp.add_argument('path', nargs=1,
149 help='path to generate build into')
150 subp.set_defaults(func=self.CmdGen)
151
152 subp = subps.add_parser('isolate',
153 help='generate the .isolate files for a given'
154 'binary')
155 AddCommonOptions(subp)
156 subp.add_argument('path', nargs=1,
157 help='path build was generated into')
158 subp.add_argument('target', nargs=1,
159 help='ninja target to generate the isolate for')
160 subp.set_defaults(func=self.CmdIsolate)
161
162 subp = subps.add_parser('lookup',
163 help='look up the command for a given config or '
164 'builder')
165 AddCommonOptions(subp)
166 subp.set_defaults(func=self.CmdLookup)
167
168 subp = subps.add_parser(
169 'run',
170 help='build and run the isolated version of a '
171 'binary',
172 formatter_class=argparse.RawDescriptionHelpFormatter)
173 subp.description = (
174 'Build, isolate, and run the given binary with the command line\n'
175 'listed in the isolate. You may pass extra arguments after the\n'
176 'target; use "--" if the extra arguments need to include switches.\n'
177 '\n'
178 'Examples:\n'
179 '\n'
180 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
181 ' //out/Default content_browsertests\n'
182 '\n'
183 ' % tools/mb/mb.py run out/Default content_browsertests\n'
184 '\n'
185 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
186 ' --test-launcher-retry-limit=0'
187 '\n'
188 )
189
190 AddCommonOptions(subp)
191 subp.add_argument('-j', '--jobs', dest='jobs', type=int,
192 help='Number of jobs to pass to ninja')
193 subp.add_argument('--no-build', dest='build', default=True,
194 action='store_false',
195 help='Do not build, just isolate and run')
196 subp.add_argument('path', nargs=1,
197 help=('path to generate build into (or use).'
198 ' This can be either a regular path or a '
199 'GN-style source-relative path like '
200 '//out/Default.'))
201 subp.add_argument('target', nargs=1,
202 help='ninja target to build and run')
203 subp.add_argument('extra_args', nargs='*',
204 help=('extra args to pass to the isolate to run. Use '
205 '"--" as the first arg if you need to pass '
206 'switches'))
207 subp.set_defaults(func=self.CmdRun)
208
209 subp = subps.add_parser('validate',
210 help='validate the config file')
211 subp.add_argument('-f', '--config-file', metavar='PATH',
212 default=self.default_config,
213 help='path to config file (default is %(default)s)')
214 subp.set_defaults(func=self.CmdValidate)
215
216 subp = subps.add_parser('audit',
217 help='Audit the config file to track progress')
218 subp.add_argument('-f', '--config-file', metavar='PATH',
219 default=self.default_config,
220 help='path to config file (default is %(default)s)')
221 subp.add_argument('-i', '--internal', action='store_true',
222 help='check internal masters also')
223 subp.add_argument('-m', '--master', action='append',
224 help='master to audit (default is all non-internal '
225 'masters in file)')
226 subp.add_argument('-u', '--url-template', action='store',
227 default='https://build.chromium.org/p/'
228 '{master}/json/builders',
229 help='URL scheme for JSON APIs to buildbot '
230 '(default: %(default)s) ')
231 subp.add_argument('-c', '--check-compile', action='store_true',
232 help='check whether tbd and master-only bots actually'
233 ' do compiles')
234 subp.set_defaults(func=self.CmdAudit)
235
236 subp = subps.add_parser('help',
237 help='Get help on a subcommand.')
238 subp.add_argument(nargs='?', action='store', dest='subcommand',
239 help='The command to get help for.')
240 subp.set_defaults(func=self.CmdHelp)
241
242 self.args = parser.parse_args(argv)
243
244 def DumpInputFiles(self):
245
246 def DumpContentsOfFilePassedTo(arg_name, path):
247 if path and self.Exists(path):
248 self.Print("\n# To recreate the file passed to %s:" % arg_name)
249 self.Print("%% cat > %s <<EOF" % path)
250 contents = self.ReadFile(path)
251 self.Print(contents)
252 self.Print("EOF\n%\n")
253
254 if getattr(self.args, 'input_path', None):
255 DumpContentsOfFilePassedTo(
256 'argv[0] (input_path)', self.args.input_path[0])
257 if getattr(self.args, 'swarming_targets_file', None):
258 DumpContentsOfFilePassedTo(
259 '--swarming-targets-file', self.args.swarming_targets_file)
260
261 def CmdAnalyze(self):
262 vals = self.Lookup()
263 self.ClobberIfNeeded(vals)
264 if vals['type'] == 'gn':
265 return self.RunGNAnalyze(vals)
266 else:
267 return self.RunGYPAnalyze(vals)
268
269 def CmdExport(self):
270 self.ReadConfigFile()
271 obj = {}
272 for master, builders in self.masters.items():
273 obj[master] = {}
274 for builder in builders:
275 config = self.masters[master][builder]
276 if not config:
277 continue
278
279 if isinstance(config, dict):
280 args = {k: self.FlattenConfig(v)['gn_args']
281 for k, v in config.items()}
282 elif config.startswith('//'):
283 args = config
284 else:
285 args = self.FlattenConfig(config)['gn_args']
286 if 'error' in args:
287 continue
288
289 obj[master][builder] = args
290
291 # Dump object and trim trailing whitespace.
292 s = '\n'.join(l.rstrip() for l in
293 json.dumps(obj, sort_keys=True, indent=2).splitlines())
294 self.Print(s)
295 return 0
296
297 def CmdGen(self):
298 vals = self.Lookup()
299 self.ClobberIfNeeded(vals)
300 if vals['type'] == 'gn':
301 return self.RunGNGen(vals)
302 else:
303 return self.RunGYPGen(vals)
304
305 def CmdHelp(self):
306 if self.args.subcommand:
307 self.ParseArgs([self.args.subcommand, '--help'])
308 else:
309 self.ParseArgs(['--help'])
310
311 def CmdIsolate(self):
312 vals = self.GetConfig()
313 if not vals:
314 return 1
315
316 if vals['type'] == 'gn':
317 return self.RunGNIsolate(vals)
318 else:
319 return self.Build('%s_run' % self.args.target[0])
320
321 def CmdLookup(self):
322 vals = self.Lookup()
323 if vals['type'] == 'gn':
324 cmd = self.GNCmd('gen', '_path_')
325 gn_args = self.GNArgs(vals)
326 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
327 env = None
328 else:
329 cmd, env = self.GYPCmd('_path_', vals)
330
331 self.PrintCmd(cmd, env)
332 return 0
333
334 def CmdRun(self):
335 vals = self.GetConfig()
336 if not vals:
337 return 1
338
339 build_dir = self.args.path[0]
340 target = self.args.target[0]
341
342 if vals['type'] == 'gn':
343 if self.args.build:
344 ret = self.Build(target)
345 if ret:
346 return ret
347 ret = self.RunGNIsolate(vals)
348 if ret:
349 return ret
350 else:
351 ret = self.Build('%s_run' % target)
352 if ret:
353 return ret
354
355 cmd = [
356 self.executable,
357 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
358 'run',
359 '-s',
360 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
361 ]
362 if self.args.extra_args:
363 cmd += ['--'] + self.args.extra_args
364
365 ret, _, _ = self.Run(cmd, force_verbose=False, buffer_output=False)
366
367 return ret
368
369 def CmdValidate(self, print_ok=True):
370 errs = []
371
372 # Read the file to make sure it parses.
373 self.ReadConfigFile()
374
375 # Build a list of all of the configs referenced by builders.
376 all_configs = {}
377 for master in self.masters:
378 for config in self.masters[master].values():
379 if isinstance(config, dict):
380 for c in config.values():
381 all_configs[c] = master
382 else:
383 all_configs[config] = master
384
385 # Check that every referenced args file or config actually exists.
386 for config, loc in all_configs.items():
387 if config.startswith('//'):
388 if not self.Exists(self.ToAbsPath(config)):
389 errs.append('Unknown args file "%s" referenced from "%s".' %
390 (config, loc))
391 elif not config in self.configs:
392 errs.append('Unknown config "%s" referenced from "%s".' %
393 (config, loc))
394
395 # Check that every actual config is actually referenced.
396 for config in self.configs:
397 if not config in all_configs:
398 errs.append('Unused config "%s".' % config)
399
400 # Figure out the whole list of mixins, and check that every mixin
401 # listed by a config or another mixin actually exists.
402 referenced_mixins = set()
403 for config, mixins in self.configs.items():
404 for mixin in mixins:
405 if not mixin in self.mixins:
406 errs.append('Unknown mixin "%s" referenced by config "%s".' %
407 (mixin, config))
408 referenced_mixins.add(mixin)
409
410 for mixin in self.mixins:
411 for sub_mixin in self.mixins[mixin].get('mixins', []):
412 if not sub_mixin in self.mixins:
413 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
414 (sub_mixin, mixin))
415 referenced_mixins.add(sub_mixin)
416
417 # Check that every mixin defined is actually referenced somewhere.
418 for mixin in self.mixins:
419 if not mixin in referenced_mixins:
420 errs.append('Unreferenced mixin "%s".' % mixin)
421
kjellandera013a022016-11-14 05:54:22 -0800422 if errs:
423 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
424 '\n ' + '\n '.join(errs))
425
426 if print_ok:
427 self.Print('mb config file %s looks ok.' % self.args.config_file)
428 return 0
429
430 def CmdAudit(self):
431 """Track the progress of the GYP->GN migration on the bots."""
432
433 # First, make sure the config file is okay, but don't print anything
434 # if it is (it will throw an error if it isn't).
435 self.CmdValidate(print_ok=False)
436
437 stats = OrderedDict()
438 STAT_MASTER_ONLY = 'Master only'
439 STAT_CONFIG_ONLY = 'Config only'
440 STAT_TBD = 'Still TBD'
441 STAT_GYP = 'Still GYP'
442 STAT_DONE = 'Done (on GN)'
443 stats[STAT_MASTER_ONLY] = 0
444 stats[STAT_CONFIG_ONLY] = 0
445 stats[STAT_TBD] = 0
446 stats[STAT_GYP] = 0
447 stats[STAT_DONE] = 0
448
449 def PrintBuilders(heading, builders, notes):
450 stats.setdefault(heading, 0)
451 stats[heading] += len(builders)
452 if builders:
453 self.Print(' %s:' % heading)
454 for builder in sorted(builders):
455 self.Print(' %s%s' % (builder, notes[builder]))
456
457 self.ReadConfigFile()
458
459 masters = self.args.master or self.masters
460 for master in sorted(masters):
461 url = self.args.url_template.replace('{master}', master)
462
463 self.Print('Auditing %s' % master)
464
465 MASTERS_TO_SKIP = (
466 'client.skia',
467 'client.v8.fyi',
468 'tryserver.v8',
469 )
470 if master in MASTERS_TO_SKIP:
471 # Skip these bots because converting them is the responsibility of
472 # those teams and out of scope for the Chromium migration to GN.
473 self.Print(' Skipped (out of scope)')
474 self.Print('')
475 continue
476
477 INTERNAL_MASTERS = ('official.desktop', 'official.desktop.continuous',
478 'internal.client.kitchensync')
479 if master in INTERNAL_MASTERS and not self.args.internal:
480 # Skip these because the servers aren't accessible by default ...
481 self.Print(' Skipped (internal)')
482 self.Print('')
483 continue
484
485 try:
486 # Fetch the /builders contents from the buildbot master. The
487 # keys of the dict are the builder names themselves.
488 json_contents = self.Fetch(url)
489 d = json.loads(json_contents)
490 except Exception as e:
491 self.Print(str(e))
492 return 1
493
494 config_builders = set(self.masters[master])
495 master_builders = set(d.keys())
496 both = master_builders & config_builders
497 master_only = master_builders - config_builders
498 config_only = config_builders - master_builders
499 tbd = set()
500 gyp = set()
501 done = set()
502 notes = {builder: '' for builder in config_builders | master_builders}
503
504 for builder in both:
505 config = self.masters[master][builder]
506 if config == 'tbd':
507 tbd.add(builder)
508 elif isinstance(config, dict):
509 vals = self.FlattenConfig(config.values()[0])
510 if vals['type'] == 'gyp':
511 gyp.add(builder)
512 else:
513 done.add(builder)
514 elif config.startswith('//'):
515 done.add(builder)
516 else:
517 vals = self.FlattenConfig(config)
518 if vals['type'] == 'gyp':
519 gyp.add(builder)
520 else:
521 done.add(builder)
522
523 if self.args.check_compile and (tbd or master_only):
524 either = tbd | master_only
525 for builder in either:
526 notes[builder] = ' (' + self.CheckCompile(master, builder) +')'
527
528 if master_only or config_only or tbd or gyp:
529 PrintBuilders(STAT_MASTER_ONLY, master_only, notes)
530 PrintBuilders(STAT_CONFIG_ONLY, config_only, notes)
531 PrintBuilders(STAT_TBD, tbd, notes)
532 PrintBuilders(STAT_GYP, gyp, notes)
533 else:
534 self.Print(' All GN!')
535
536 stats[STAT_DONE] += len(done)
537
538 self.Print('')
539
540 fmt = '{:<27} {:>4}'
541 self.Print(fmt.format('Totals', str(sum(int(v) for v in stats.values()))))
542 self.Print(fmt.format('-' * 27, '----'))
543 for stat, count in stats.items():
544 self.Print(fmt.format(stat, str(count)))
545
546 return 0
547
548 def GetConfig(self):
549 build_dir = self.args.path[0]
550
551 vals = self.DefaultVals()
552 if self.args.builder or self.args.master or self.args.config:
553 vals = self.Lookup()
554 if vals['type'] == 'gn':
555 # Re-run gn gen in order to ensure the config is consistent with the
556 # build dir.
557 self.RunGNGen(vals)
558 return vals
559
560 mb_type_path = self.PathJoin(self.ToAbsPath(build_dir), 'mb_type')
561 if not self.Exists(mb_type_path):
562 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
563 'toolchain.ninja')
564 if not self.Exists(toolchain_path):
565 self.Print('Must either specify a path to an existing GN build dir '
566 'or pass in a -m/-b pair or a -c flag to specify the '
567 'configuration')
568 return {}
569 else:
570 mb_type = 'gn'
571 else:
572 mb_type = self.ReadFile(mb_type_path).strip()
573
574 if mb_type == 'gn':
575 vals['gn_args'] = self.GNArgsFromDir(build_dir)
576 vals['type'] = mb_type
577
578 return vals
579
580 def GNArgsFromDir(self, build_dir):
581 args_contents = ""
582 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
583 if self.Exists(gn_args_path):
584 args_contents = self.ReadFile(gn_args_path)
585 gn_args = []
586 for l in args_contents.splitlines():
587 fields = l.split(' ')
588 name = fields[0]
589 val = ' '.join(fields[2:])
590 gn_args.append('%s=%s' % (name, val))
591
592 return ' '.join(gn_args)
593
594 def Lookup(self):
595 vals = self.ReadIOSBotConfig()
596 if not vals:
597 self.ReadConfigFile()
598 config = self.ConfigFromArgs()
599 if config.startswith('//'):
600 if not self.Exists(self.ToAbsPath(config)):
601 raise MBErr('args file "%s" not found' % config)
602 vals = self.DefaultVals()
603 vals['args_file'] = config
604 else:
605 if not config in self.configs:
606 raise MBErr('Config "%s" not found in %s' %
607 (config, self.args.config_file))
608 vals = self.FlattenConfig(config)
609
610 # Do some basic sanity checking on the config so that we
611 # don't have to do this in every caller.
612 if 'type' not in vals:
613 vals['type'] = 'gn'
614 assert vals['type'] in ('gn', 'gyp'), (
615 'Unknown meta-build type "%s"' % vals['gn_args'])
616
617 return vals
618
619 def ReadIOSBotConfig(self):
620 if not self.args.master or not self.args.builder:
621 return {}
kjellander1c3548c2017-02-15 22:38:22 -0800622 path = self.PathJoin(self.src_dir, 'tools-webrtc', 'ios',
623 self.args.master,
624 self.args.builder.replace(' ', '_') + '.json')
kjellandera013a022016-11-14 05:54:22 -0800625 if not self.Exists(path):
626 return {}
627
628 contents = json.loads(self.ReadFile(path))
629 gyp_vals = contents.get('GYP_DEFINES', {})
630 if isinstance(gyp_vals, dict):
631 gyp_defines = ' '.join('%s=%s' % (k, v) for k, v in gyp_vals.items())
632 else:
633 gyp_defines = ' '.join(gyp_vals)
634 gn_args = ' '.join(contents.get('gn_args', []))
635
636 vals = self.DefaultVals()
637 vals['gn_args'] = gn_args
638 vals['gyp_defines'] = gyp_defines
639 vals['type'] = contents.get('mb_type', 'gn')
640 return vals
641
642 def ReadConfigFile(self):
643 if not self.Exists(self.args.config_file):
644 raise MBErr('config file not found at %s' % self.args.config_file)
645
646 try:
647 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
648 except SyntaxError as e:
649 raise MBErr('Failed to parse config file "%s": %s' %
650 (self.args.config_file, e))
651
652 self.configs = contents['configs']
653 self.masters = contents['masters']
654 self.mixins = contents['mixins']
655
656 def ReadIsolateMap(self):
657 if not self.Exists(self.args.isolate_map_file):
658 raise MBErr('isolate map file not found at %s' %
659 self.args.isolate_map_file)
660 try:
661 return ast.literal_eval(self.ReadFile(self.args.isolate_map_file))
662 except SyntaxError as e:
663 raise MBErr('Failed to parse isolate map file "%s": %s' %
664 (self.args.isolate_map_file, e))
665
666 def ConfigFromArgs(self):
667 if self.args.config:
668 if self.args.master or self.args.builder:
669 raise MBErr('Can not specific both -c/--config and -m/--master or '
670 '-b/--builder')
671
672 return self.args.config
673
674 if not self.args.master or not self.args.builder:
675 raise MBErr('Must specify either -c/--config or '
676 '(-m/--master and -b/--builder)')
677
678 if not self.args.master in self.masters:
679 raise MBErr('Master name "%s" not found in "%s"' %
680 (self.args.master, self.args.config_file))
681
682 if not self.args.builder in self.masters[self.args.master]:
683 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
684 (self.args.builder, self.args.master, self.args.config_file))
685
686 config = self.masters[self.args.master][self.args.builder]
687 if isinstance(config, dict):
688 if self.args.phase is None:
689 raise MBErr('Must specify a build --phase for %s on %s' %
690 (self.args.builder, self.args.master))
691 phase = str(self.args.phase)
692 if phase not in config:
693 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
694 (phase, self.args.builder, self.args.master))
695 return config[phase]
696
697 if self.args.phase is not None:
698 raise MBErr('Must not specify a build --phase for %s on %s' %
699 (self.args.builder, self.args.master))
700 return config
701
702 def FlattenConfig(self, config):
703 mixins = self.configs[config]
704 vals = self.DefaultVals()
705
706 visited = []
707 self.FlattenMixins(mixins, vals, visited)
708 return vals
709
710 def DefaultVals(self):
711 return {
712 'args_file': '',
713 'cros_passthrough': False,
714 'gn_args': '',
715 'gyp_defines': '',
716 'gyp_crosscompile': False,
717 'type': 'gn',
718 }
719
720 def FlattenMixins(self, mixins, vals, visited):
721 for m in mixins:
722 if m not in self.mixins:
723 raise MBErr('Unknown mixin "%s"' % m)
724
725 visited.append(m)
726
727 mixin_vals = self.mixins[m]
728
729 if 'cros_passthrough' in mixin_vals:
730 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
731 if 'gn_args' in mixin_vals:
732 if vals['gn_args']:
733 vals['gn_args'] += ' ' + mixin_vals['gn_args']
734 else:
735 vals['gn_args'] = mixin_vals['gn_args']
736 if 'gyp_crosscompile' in mixin_vals:
737 vals['gyp_crosscompile'] = mixin_vals['gyp_crosscompile']
738 if 'gyp_defines' in mixin_vals:
739 if vals['gyp_defines']:
740 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
741 else:
742 vals['gyp_defines'] = mixin_vals['gyp_defines']
743 if 'type' in mixin_vals:
744 vals['type'] = mixin_vals['type']
745
746 if 'mixins' in mixin_vals:
747 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
748 return vals
749
750 def ClobberIfNeeded(self, vals):
751 path = self.args.path[0]
752 build_dir = self.ToAbsPath(path)
753 mb_type_path = self.PathJoin(build_dir, 'mb_type')
754 needs_clobber = False
755 new_mb_type = vals['type']
756 if self.Exists(build_dir):
757 if self.Exists(mb_type_path):
758 old_mb_type = self.ReadFile(mb_type_path)
759 if old_mb_type != new_mb_type:
760 self.Print("Build type mismatch: was %s, will be %s, clobbering %s" %
761 (old_mb_type, new_mb_type, path))
762 needs_clobber = True
763 else:
764 # There is no 'mb_type' file in the build directory, so this probably
765 # means that the prior build(s) were not done through mb, and we
766 # have no idea if this was a GYP build or a GN build. Clobber it
767 # to be safe.
768 self.Print("%s/mb_type missing, clobbering to be safe" % path)
769 needs_clobber = True
770
771 if self.args.dryrun:
772 return
773
774 if needs_clobber:
775 self.RemoveDirectory(build_dir)
776
777 self.MaybeMakeDirectory(build_dir)
778 self.WriteFile(mb_type_path, new_mb_type)
779
780 def RunGNGen(self, vals):
781 build_dir = self.args.path[0]
782
783 cmd = self.GNCmd('gen', build_dir, '--check')
784 gn_args = self.GNArgs(vals)
785
786 # Since GN hasn't run yet, the build directory may not even exist.
787 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
788
789 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
790 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
791
792 swarming_targets = []
793 if getattr(self.args, 'swarming_targets_file', None):
794 # We need GN to generate the list of runtime dependencies for
795 # the compile targets listed (one per line) in the file so
796 # we can run them via swarming. We use gn_isolate_map.pyl to convert
797 # the compile targets to the matching GN labels.
798 path = self.args.swarming_targets_file
799 if not self.Exists(path):
800 self.WriteFailureAndRaise('"%s" does not exist' % path,
801 output_path=None)
802 contents = self.ReadFile(path)
803 swarming_targets = set(contents.splitlines())
804
805 isolate_map = self.ReadIsolateMap()
806 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
807 if err:
808 raise MBErr(err)
809
810 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
811 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
812 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
813
814 ret, _, _ = self.Run(cmd)
815 if ret:
816 # If `gn gen` failed, we should exit early rather than trying to
817 # generate isolates. Run() will have already logged any error output.
818 self.Print('GN gen failed: %d' % ret)
819 return ret
820
821 android = 'target_os="android"' in vals['gn_args']
822 for target in swarming_targets:
823 if android:
824 # Android targets may be either android_apk or executable. The former
825 # will result in runtime_deps associated with the stamp file, while the
826 # latter will result in runtime_deps associated with the executable.
827 label = isolate_map[target]['label']
828 runtime_deps_targets = [
829 target + '.runtime_deps',
830 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
831 elif isolate_map[target]['type'] == 'gpu_browser_test':
832 if self.platform == 'win32':
833 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
834 else:
835 runtime_deps_targets = ['browser_tests.runtime_deps']
836 elif (isolate_map[target]['type'] == 'script' or
837 isolate_map[target].get('label_type') == 'group'):
838 # For script targets, the build target is usually a group,
839 # for which gn generates the runtime_deps next to the stamp file
840 # for the label, which lives under the obj/ directory, but it may
841 # also be an executable.
842 label = isolate_map[target]['label']
843 runtime_deps_targets = [
844 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
845 if self.platform == 'win32':
846 runtime_deps_targets += [ target + '.exe.runtime_deps' ]
847 else:
848 runtime_deps_targets += [ target + '.runtime_deps' ]
849 elif self.platform == 'win32':
850 runtime_deps_targets = [target + '.exe.runtime_deps']
851 else:
852 runtime_deps_targets = [target + '.runtime_deps']
853
854 for r in runtime_deps_targets:
855 runtime_deps_path = self.ToAbsPath(build_dir, r)
856 if self.Exists(runtime_deps_path):
857 break
858 else:
859 raise MBErr('did not generate any of %s' %
860 ', '.join(runtime_deps_targets))
861
862 command, extra_files = self.GetIsolateCommand(target, vals)
863
864 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
865
866 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
867 extra_files)
868
869 return 0
870
871 def RunGNIsolate(self, vals):
872 target = self.args.target[0]
873 isolate_map = self.ReadIsolateMap()
874 err, labels = self.MapTargetsToLabels(isolate_map, [target])
875 if err:
876 raise MBErr(err)
877 label = labels[0]
878
879 build_dir = self.args.path[0]
880 command, extra_files = self.GetIsolateCommand(target, vals)
881
882 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
883 ret, out, _ = self.Call(cmd)
884 if ret:
885 if out:
886 self.Print(out)
887 return ret
888
889 runtime_deps = out.splitlines()
890
891 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
892 extra_files)
893
894 ret, _, _ = self.Run([
895 self.executable,
896 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
897 'check',
898 '-i',
899 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
900 '-s',
901 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
902 buffer_output=False)
903
904 return ret
905
906 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
907 extra_files):
908 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
909 self.WriteFile(isolate_path,
910 pprint.pformat({
911 'variables': {
912 'command': command,
913 'files': sorted(runtime_deps + extra_files),
914 }
915 }) + '\n')
916
917 self.WriteJSON(
918 {
919 'args': [
920 '--isolated',
921 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
922 '--isolate',
923 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
924 ],
kjellander1c3548c2017-02-15 22:38:22 -0800925 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800926 'version': 1,
927 },
928 isolate_path + 'd.gen.json',
929 )
930
931 def MapTargetsToLabels(self, isolate_map, targets):
932 labels = []
933 err = ''
934
935 def StripTestSuffixes(target):
936 for suffix in ('_apk_run', '_apk', '_run'):
937 if target.endswith(suffix):
938 return target[:-len(suffix)], suffix
939 return None, None
940
941 for target in targets:
942 if target == 'all':
943 labels.append(target)
944 elif target.startswith('//'):
945 labels.append(target)
946 else:
947 if target in isolate_map:
948 stripped_target, suffix = target, ''
949 else:
950 stripped_target, suffix = StripTestSuffixes(target)
951 if stripped_target in isolate_map:
952 if isolate_map[stripped_target]['type'] == 'unknown':
953 err += ('test target "%s" type is unknown\n' % target)
954 else:
955 labels.append(isolate_map[stripped_target]['label'] + suffix)
956 else:
957 err += ('target "%s" not found in '
958 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
959
960 return err, labels
961
962 def GNCmd(self, subcommand, path, *args):
963 if self.platform == 'linux2':
964 subdir, exe = 'linux64', 'gn'
965 elif self.platform == 'darwin':
966 subdir, exe = 'mac', 'gn'
967 else:
968 subdir, exe = 'win', 'gn.exe'
969
kjellander1c3548c2017-02-15 22:38:22 -0800970 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800971 return [gn_path, subcommand, path] + list(args)
972
973
974 def GNArgs(self, vals):
975 if vals['cros_passthrough']:
976 if not 'GN_ARGS' in os.environ:
977 raise MBErr('MB is expecting GN_ARGS to be in the environment')
978 gn_args = os.environ['GN_ARGS']
979 if not re.search('target_os.*=.*"chromeos"', gn_args):
980 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
981 gn_args)
982 else:
983 gn_args = vals['gn_args']
984
985 if self.args.goma_dir:
986 gn_args += ' goma_dir="%s"' % self.args.goma_dir
987
988 android_version_code = self.args.android_version_code
989 if android_version_code:
990 gn_args += ' android_default_version_code="%s"' % android_version_code
991
992 android_version_name = self.args.android_version_name
993 if android_version_name:
994 gn_args += ' android_default_version_name="%s"' % android_version_name
995
996 # Canonicalize the arg string into a sorted, newline-separated list
997 # of key-value pairs, and de-dup the keys if need be so that only
998 # the last instance of each arg is listed.
999 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
1000
1001 args_file = vals.get('args_file', None)
1002 if args_file:
1003 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
1004 return gn_args
1005
1006 def RunGYPGen(self, vals):
1007 path = self.args.path[0]
1008
1009 output_dir = self.ParseGYPConfigPath(path)
1010 cmd, env = self.GYPCmd(output_dir, vals)
1011 ret, _, _ = self.Run(cmd, env=env)
1012 return ret
1013
1014 def RunGYPAnalyze(self, vals):
1015 output_dir = self.ParseGYPConfigPath(self.args.path[0])
1016 if self.args.verbose:
1017 inp = self.ReadInputJSON(['files', 'test_targets',
1018 'additional_compile_targets'])
1019 self.Print()
1020 self.Print('analyze input:')
1021 self.PrintJSON(inp)
1022 self.Print()
1023
1024 cmd, env = self.GYPCmd(output_dir, vals)
1025 cmd.extend(['-f', 'analyzer',
1026 '-G', 'config_path=%s' % self.args.input_path[0],
1027 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
1028 ret, _, _ = self.Run(cmd, env=env)
1029 if not ret and self.args.verbose:
1030 outp = json.loads(self.ReadFile(self.args.output_path[0]))
1031 self.Print()
1032 self.Print('analyze output:')
1033 self.PrintJSON(outp)
1034 self.Print()
1035
1036 return ret
1037
1038 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -08001039 isolate_map = self.ReadIsolateMap()
1040 test_type = isolate_map[target]['type']
1041
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001042 android = 'target_os="android"' in vals['gn_args']
1043 is_linux = self.platform == 'linux2' and not android
kjellandera013a022016-11-14 05:54:22 -08001044
1045 if test_type == 'nontest':
1046 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
1047 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001048 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
1049 'non_parallel_console_test_launcher',
1050 'additional_compile_target', 'junit_test'):
1051 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
1052 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -08001053
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001054 cmdline = []
1055 extra_files = []
1056
1057 if android:
kjellanderf9e2a362017-03-24 12:17:33 -07001058 cmdline = ['../../build/android/test_wrapper/logdog_wrapper.py',
1059 '--target', target,
1060 '--logdog-bin-cmd', '../../bin/logdog_butler']
kjellander84308252017-03-16 01:35:09 -07001061 if test_type != 'junit_test':
kjellanderf9e2a362017-03-24 12:17:33 -07001062 cmdline += ['--target-devices-file', '${SWARMING_BOT_FILE}']
kjellandera013a022016-11-14 05:54:22 -08001063 else:
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001064 extra_files = ['../../testing/test_env.py']
kjellandera013a022016-11-14 05:54:22 -08001065
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001066 # This needs to mirror the settings in //build/config/ui.gni:
1067 # use_x11 = is_linux && !use_ozone.
1068 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
1069
1070 xvfb = use_x11 and test_type == 'windowed_test_launcher'
1071 if xvfb:
1072 extra_files += [
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001073 '../../testing/xvfb.py',
1074 ]
1075
1076 # Memcheck is only supported for linux. Ignore in other platforms.
1077 memcheck = is_linux and 'rtc_use_memcheck=true' in vals['gn_args']
1078 memcheck_cmdline = [
1079 'bash',
kjellanderafd54942016-12-17 12:21:39 -08001080 '../../tools-webrtc/valgrind/webrtc_tests.sh',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001081 '--tool',
1082 'memcheck',
1083 '--target',
1084 'Release',
1085 '--build-dir',
1086 '..',
1087 '--test',
1088 ]
1089
kjellander382f2b22017-04-11 04:07:01 -07001090 if not memcheck:
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001091 extra_files += [
1092 '../../third_party/gtest-parallel/gtest-parallel',
ehmaldonado3ff7a952017-03-29 09:42:32 -07001093 '../../tools-webrtc/gtest-parallel-wrapper.py',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001094 ]
ehmaldonado55833842017-02-13 03:58:13 -08001095 sep = '\\' if self.platform == 'win32' else '/'
1096 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
1097 gtest_parallel_wrapper = [
ehmaldonado3ff7a952017-03-29 09:42:32 -07001098 '../../tools-webrtc/gtest-parallel-wrapper.py',
ehmaldonado950614e2017-05-02 05:52:57 -07001099 '--gtest_color=no',
ehmaldonado55833842017-02-13 03:58:13 -08001100 '--output_dir=%s' % output_dir,
1101 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001102
1103 asan = 'is_asan=true' in vals['gn_args']
1104 msan = 'is_msan=true' in vals['gn_args']
1105 tsan = 'is_tsan=true' in vals['gn_args']
1106
1107 executable_prefix = '.\\' if self.platform == 'win32' else './'
1108 executable_suffix = '.exe' if self.platform == 'win32' else ''
1109 executable = executable_prefix + target + executable_suffix
1110
kjellander7e1070d2016-12-08 00:49:03 -08001111 cmdline = (['../../testing/xvfb.py'] if xvfb else
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001112 ['../../testing/test_env.py'])
kjellander382f2b22017-04-11 04:07:01 -07001113 cmdline += memcheck_cmdline if memcheck else gtest_parallel_wrapper
1114 cmdline.append(executable)
1115 if test_type == 'non_parallel_console_test_launcher' and not memcheck:
1116 # Still use the gtest-parallel-wrapper.py script since we need it to
1117 # run tests on swarming, but don't execute tests in parallel.
1118 cmdline.append('--workers=1')
1119
1120 cmdline.extend([
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001121 '--',
1122 '--asan=%d' % asan,
1123 '--msan=%d' % msan,
1124 '--tsan=%d' % tsan,
kjellander382f2b22017-04-11 04:07:01 -07001125 ])
kjellandera013a022016-11-14 05:54:22 -08001126
kjellander74e81262017-03-23 00:51:11 -07001127 cmdline += isolate_map[target].get('args', [])
1128
kjellandera013a022016-11-14 05:54:22 -08001129 return cmdline, extra_files
1130
1131 def ToAbsPath(self, build_path, *comps):
kjellander1c3548c2017-02-15 22:38:22 -08001132 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001133 self.ToSrcRelPath(build_path),
1134 *comps)
1135
1136 def ToSrcRelPath(self, path):
1137 """Returns a relative path from the top of the repo."""
1138 if path.startswith('//'):
1139 return path[2:].replace('/', self.sep)
kjellander1c3548c2017-02-15 22:38:22 -08001140 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -08001141
1142 def ParseGYPConfigPath(self, path):
1143 rpath = self.ToSrcRelPath(path)
1144 output_dir, _, _ = rpath.rpartition(self.sep)
1145 return output_dir
1146
1147 def GYPCmd(self, output_dir, vals):
1148 if vals['cros_passthrough']:
1149 if not 'GYP_DEFINES' in os.environ:
1150 raise MBErr('MB is expecting GYP_DEFINES to be in the environment')
1151 gyp_defines = os.environ['GYP_DEFINES']
1152 if not 'chromeos=1' in gyp_defines:
1153 raise MBErr('GYP_DEFINES is missing chromeos=1: (GYP_DEFINES=%s)' %
1154 gyp_defines)
1155 else:
1156 gyp_defines = vals['gyp_defines']
1157
1158 goma_dir = self.args.goma_dir
1159
1160 # GYP uses shlex.split() to split the gyp defines into separate arguments,
1161 # so we can support backslashes and and spaces in arguments by quoting
1162 # them, even on Windows, where this normally wouldn't work.
1163 if goma_dir and ('\\' in goma_dir or ' ' in goma_dir):
1164 goma_dir = "'%s'" % goma_dir
1165
1166 if goma_dir:
1167 gyp_defines += ' gomadir=%s' % goma_dir
1168
1169 android_version_code = self.args.android_version_code
1170 if android_version_code:
1171 gyp_defines += ' app_manifest_version_code=%s' % android_version_code
1172
1173 android_version_name = self.args.android_version_name
1174 if android_version_name:
1175 gyp_defines += ' app_manifest_version_name=%s' % android_version_name
1176
1177 cmd = [
1178 self.executable,
1179 self.args.gyp_script,
1180 '-G',
1181 'output_dir=' + output_dir,
1182 ]
1183
1184 # Ensure that we have an environment that only contains
1185 # the exact values of the GYP variables we need.
1186 env = os.environ.copy()
1187
1188 # This is a terrible hack to work around the fact that
1189 # //tools/clang/scripts/update.py is invoked by GYP and GN but
1190 # currently relies on an environment variable to figure out
1191 # what revision to embed in the command line #defines.
1192 # For GN, we've made this work via a gn arg that will cause update.py
1193 # to get an additional command line arg, but getting that to work
1194 # via GYP_DEFINES has proven difficult, so we rewrite the GYP_DEFINES
1195 # to get rid of the arg and add the old var in, instead.
1196 # See crbug.com/582737 for more on this. This can hopefully all
1197 # go away with GYP.
1198 m = re.search('llvm_force_head_revision=1\s*', gyp_defines)
1199 if m:
1200 env['LLVM_FORCE_HEAD_REVISION'] = '1'
1201 gyp_defines = gyp_defines.replace(m.group(0), '')
1202
1203 # This is another terrible hack to work around the fact that
1204 # GYP sets the link concurrency to use via the GYP_LINK_CONCURRENCY
1205 # environment variable, and not via a proper GYP_DEFINE. See
1206 # crbug.com/611491 for more on this.
1207 m = re.search('gyp_link_concurrency=(\d+)(\s*)', gyp_defines)
1208 if m:
1209 env['GYP_LINK_CONCURRENCY'] = m.group(1)
1210 gyp_defines = gyp_defines.replace(m.group(0), '')
1211
1212 env['GYP_GENERATORS'] = 'ninja'
1213 if 'GYP_CHROMIUM_NO_ACTION' in env:
1214 del env['GYP_CHROMIUM_NO_ACTION']
1215 if 'GYP_CROSSCOMPILE' in env:
1216 del env['GYP_CROSSCOMPILE']
1217 env['GYP_DEFINES'] = gyp_defines
1218 if vals['gyp_crosscompile']:
1219 env['GYP_CROSSCOMPILE'] = '1'
1220 return cmd, env
1221
1222 def RunGNAnalyze(self, vals):
1223 # Analyze runs before 'gn gen' now, so we need to run gn gen
1224 # in order to ensure that we have a build directory.
1225 ret = self.RunGNGen(vals)
1226 if ret:
1227 return ret
1228
1229 build_path = self.args.path[0]
1230 input_path = self.args.input_path[0]
1231 gn_input_path = input_path + '.gn'
1232 output_path = self.args.output_path[0]
1233 gn_output_path = output_path + '.gn'
1234
1235 inp = self.ReadInputJSON(['files', 'test_targets',
1236 'additional_compile_targets'])
1237 if self.args.verbose:
1238 self.Print()
1239 self.Print('analyze input:')
1240 self.PrintJSON(inp)
1241 self.Print()
1242
1243
1244 # This shouldn't normally happen, but could due to unusual race conditions,
1245 # like a try job that gets scheduled before a patch lands but runs after
1246 # the patch has landed.
1247 if not inp['files']:
1248 self.Print('Warning: No files modified in patch, bailing out early.')
1249 self.WriteJSON({
1250 'status': 'No dependency',
1251 'compile_targets': [],
1252 'test_targets': [],
1253 }, output_path)
1254 return 0
1255
1256 gn_inp = {}
1257 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
1258
1259 isolate_map = self.ReadIsolateMap()
1260 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
1261 isolate_map, inp['additional_compile_targets'])
1262 if err:
1263 raise MBErr(err)
1264
1265 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
1266 isolate_map, inp['test_targets'])
1267 if err:
1268 raise MBErr(err)
1269 labels_to_targets = {}
1270 for i, label in enumerate(gn_inp['test_targets']):
1271 labels_to_targets[label] = inp['test_targets'][i]
1272
1273 try:
1274 self.WriteJSON(gn_inp, gn_input_path)
1275 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
1276 ret, _, _ = self.Run(cmd, force_verbose=True)
1277 if ret:
1278 return ret
1279
1280 gn_outp_str = self.ReadFile(gn_output_path)
1281 try:
1282 gn_outp = json.loads(gn_outp_str)
1283 except Exception as e:
1284 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
1285 % (repr(gn_outp_str), str(e)))
1286 raise
1287
1288 outp = {}
1289 if 'status' in gn_outp:
1290 outp['status'] = gn_outp['status']
1291 if 'error' in gn_outp:
1292 outp['error'] = gn_outp['error']
1293 if 'invalid_targets' in gn_outp:
1294 outp['invalid_targets'] = gn_outp['invalid_targets']
1295 if 'compile_targets' in gn_outp:
1296 if 'all' in gn_outp['compile_targets']:
1297 outp['compile_targets'] = ['all']
1298 else:
1299 outp['compile_targets'] = [
1300 label.replace('//', '') for label in gn_outp['compile_targets']]
1301 if 'test_targets' in gn_outp:
1302 outp['test_targets'] = [
1303 labels_to_targets[label] for label in gn_outp['test_targets']]
1304
1305 if self.args.verbose:
1306 self.Print()
1307 self.Print('analyze output:')
1308 self.PrintJSON(outp)
1309 self.Print()
1310
1311 self.WriteJSON(outp, output_path)
1312
1313 finally:
1314 if self.Exists(gn_input_path):
1315 self.RemoveFile(gn_input_path)
1316 if self.Exists(gn_output_path):
1317 self.RemoveFile(gn_output_path)
1318
1319 return 0
1320
1321 def ReadInputJSON(self, required_keys):
1322 path = self.args.input_path[0]
1323 output_path = self.args.output_path[0]
1324 if not self.Exists(path):
1325 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1326
1327 try:
1328 inp = json.loads(self.ReadFile(path))
1329 except Exception as e:
1330 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1331 (path, e), output_path)
1332
1333 for k in required_keys:
1334 if not k in inp:
1335 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1336 output_path)
1337
1338 return inp
1339
1340 def WriteFailureAndRaise(self, msg, output_path):
1341 if output_path:
1342 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1343 raise MBErr(msg)
1344
1345 def WriteJSON(self, obj, path, force_verbose=False):
1346 try:
1347 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1348 force_verbose=force_verbose)
1349 except Exception as e:
1350 raise MBErr('Error %s writing to the output path "%s"' %
1351 (e, path))
1352
1353 def CheckCompile(self, master, builder):
1354 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1355 url = urllib2.quote(url_template.format(master=master, builder=builder),
1356 safe=':/()?=')
1357 try:
1358 builds = json.loads(self.Fetch(url))
1359 except Exception as e:
1360 return str(e)
1361 successes = sorted(
1362 [int(x) for x in builds.keys() if "text" in builds[x] and
1363 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1364 reverse=True)
1365 if not successes:
1366 return "no successful builds"
1367 build = builds[str(successes[0])]
1368 step_names = set([step["name"] for step in build["steps"]])
1369 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1370 if compile_indicators & step_names:
1371 return "compiles"
1372 return "does not compile"
1373
1374 def PrintCmd(self, cmd, env):
1375 if self.platform == 'win32':
1376 env_prefix = 'set '
1377 env_quoter = QuoteForSet
1378 shell_quoter = QuoteForCmd
1379 else:
1380 env_prefix = ''
1381 env_quoter = pipes.quote
1382 shell_quoter = pipes.quote
1383
1384 def print_env(var):
1385 if env and var in env:
1386 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1387
1388 print_env('GYP_CROSSCOMPILE')
1389 print_env('GYP_DEFINES')
1390 print_env('GYP_LINK_CONCURRENCY')
1391 print_env('LLVM_FORCE_HEAD_REVISION')
1392
1393 if cmd[0] == self.executable:
1394 cmd = ['python'] + cmd[1:]
1395 self.Print(*[shell_quoter(arg) for arg in cmd])
1396
1397 def PrintJSON(self, obj):
1398 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1399
1400 def Build(self, target):
1401 build_dir = self.ToSrcRelPath(self.args.path[0])
1402 ninja_cmd = ['ninja', '-C', build_dir]
1403 if self.args.jobs:
1404 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1405 ninja_cmd.append(target)
1406 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1407 return ret
1408
1409 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1410 # This function largely exists so it can be overridden for testing.
1411 if self.args.dryrun or self.args.verbose or force_verbose:
1412 self.PrintCmd(cmd, env)
1413 if self.args.dryrun:
1414 return 0, '', ''
1415
1416 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1417 if self.args.verbose or force_verbose:
1418 if ret:
1419 self.Print(' -> returned %d' % ret)
1420 if out:
1421 self.Print(out, end='')
1422 if err:
1423 self.Print(err, end='', file=sys.stderr)
1424 return ret, out, err
1425
1426 def Call(self, cmd, env=None, buffer_output=True):
1427 if buffer_output:
kjellander1c3548c2017-02-15 22:38:22 -08001428 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001429 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1430 env=env)
1431 out, err = p.communicate()
1432 else:
kjellander1c3548c2017-02-15 22:38:22 -08001433 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001434 env=env)
1435 p.wait()
1436 out = err = ''
1437 return p.returncode, out, err
1438
1439 def ExpandUser(self, path):
1440 # This function largely exists so it can be overridden for testing.
1441 return os.path.expanduser(path)
1442
1443 def Exists(self, path):
1444 # This function largely exists so it can be overridden for testing.
1445 return os.path.exists(path)
1446
1447 def Fetch(self, url):
1448 # This function largely exists so it can be overridden for testing.
1449 f = urllib2.urlopen(url)
1450 contents = f.read()
1451 f.close()
1452 return contents
1453
1454 def MaybeMakeDirectory(self, path):
1455 try:
1456 os.makedirs(path)
1457 except OSError, e:
1458 if e.errno != errno.EEXIST:
1459 raise
1460
1461 def PathJoin(self, *comps):
1462 # This function largely exists so it can be overriden for testing.
1463 return os.path.join(*comps)
1464
1465 def Print(self, *args, **kwargs):
1466 # This function largely exists so it can be overridden for testing.
1467 print(*args, **kwargs)
1468 if kwargs.get('stream', sys.stdout) == sys.stdout:
1469 sys.stdout.flush()
1470
1471 def ReadFile(self, path):
1472 # This function largely exists so it can be overriden for testing.
1473 with open(path) as fp:
1474 return fp.read()
1475
1476 def RelPath(self, path, start='.'):
1477 # This function largely exists so it can be overriden for testing.
1478 return os.path.relpath(path, start)
1479
1480 def RemoveFile(self, path):
1481 # This function largely exists so it can be overriden for testing.
1482 os.remove(path)
1483
1484 def RemoveDirectory(self, abs_path):
1485 if self.platform == 'win32':
1486 # In other places in chromium, we often have to retry this command
1487 # because we're worried about other processes still holding on to
1488 # file handles, but when MB is invoked, it will be early enough in the
1489 # build that their should be no other processes to interfere. We
1490 # can change this if need be.
1491 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1492 else:
1493 shutil.rmtree(abs_path, ignore_errors=True)
1494
1495 def TempFile(self, mode='w'):
1496 # This function largely exists so it can be overriden for testing.
1497 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1498
1499 def WriteFile(self, path, contents, force_verbose=False):
1500 # This function largely exists so it can be overriden for testing.
1501 if self.args.dryrun or self.args.verbose or force_verbose:
1502 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1503 with open(path, 'w') as fp:
1504 return fp.write(contents)
1505
1506
1507class MBErr(Exception):
1508 pass
1509
1510
1511# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1512# details of this next section, which handles escaping command lines
1513# so that they can be copied and pasted into a cmd window.
1514UNSAFE_FOR_SET = set('^<>&|')
1515UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1516ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1517
1518
1519def QuoteForSet(arg):
1520 if any(a in UNSAFE_FOR_SET for a in arg):
1521 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1522 return arg
1523
1524
1525def QuoteForCmd(arg):
1526 # First, escape the arg so that CommandLineToArgvW will parse it properly.
1527 # From //tools/gyp/pylib/gyp/msvs_emulation.py:23.
1528 if arg == '' or ' ' in arg or '"' in arg:
1529 quote_re = re.compile(r'(\\*)"')
1530 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1531
1532 # Then check to see if the arg contains any metacharacters other than
1533 # double quotes; if it does, quote everything (including the double
1534 # quotes) for safety.
1535 if any(a in UNSAFE_FOR_CMD for a in arg):
1536 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1537 return arg
1538
1539
1540if __name__ == '__main__':
1541 sys.exit(main(sys.argv[1:]))