blob: ea7219877c14b7ec5ab76c8a30a2a51a362ac584 [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__))
kjellander73f01de2017-02-15 12:34:03 -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):
kjellander73f01de2017-02-15 12:34:03 -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 {}
kjellander73f01de2017-02-15 12:34:03 -0800622 path = self.PathJoin(self.src_dir, 'tools-webrtc', 'ios',
kjellandera013a022016-11-14 05:54:22 -0800623 self.args.master, self.args.builder + '.json')
624 if not self.Exists(path):
625 return {}
626
627 contents = json.loads(self.ReadFile(path))
628 gyp_vals = contents.get('GYP_DEFINES', {})
629 if isinstance(gyp_vals, dict):
630 gyp_defines = ' '.join('%s=%s' % (k, v) for k, v in gyp_vals.items())
631 else:
632 gyp_defines = ' '.join(gyp_vals)
633 gn_args = ' '.join(contents.get('gn_args', []))
634
635 vals = self.DefaultVals()
636 vals['gn_args'] = gn_args
637 vals['gyp_defines'] = gyp_defines
638 vals['type'] = contents.get('mb_type', 'gn')
639 return vals
640
641 def ReadConfigFile(self):
642 if not self.Exists(self.args.config_file):
643 raise MBErr('config file not found at %s' % self.args.config_file)
644
645 try:
646 contents = ast.literal_eval(self.ReadFile(self.args.config_file))
647 except SyntaxError as e:
648 raise MBErr('Failed to parse config file "%s": %s' %
649 (self.args.config_file, e))
650
651 self.configs = contents['configs']
652 self.masters = contents['masters']
653 self.mixins = contents['mixins']
654
655 def ReadIsolateMap(self):
656 if not self.Exists(self.args.isolate_map_file):
657 raise MBErr('isolate map file not found at %s' %
658 self.args.isolate_map_file)
659 try:
660 return ast.literal_eval(self.ReadFile(self.args.isolate_map_file))
661 except SyntaxError as e:
662 raise MBErr('Failed to parse isolate map file "%s": %s' %
663 (self.args.isolate_map_file, e))
664
665 def ConfigFromArgs(self):
666 if self.args.config:
667 if self.args.master or self.args.builder:
668 raise MBErr('Can not specific both -c/--config and -m/--master or '
669 '-b/--builder')
670
671 return self.args.config
672
673 if not self.args.master or not self.args.builder:
674 raise MBErr('Must specify either -c/--config or '
675 '(-m/--master and -b/--builder)')
676
677 if not self.args.master in self.masters:
678 raise MBErr('Master name "%s" not found in "%s"' %
679 (self.args.master, self.args.config_file))
680
681 if not self.args.builder in self.masters[self.args.master]:
682 raise MBErr('Builder name "%s" not found under masters[%s] in "%s"' %
683 (self.args.builder, self.args.master, self.args.config_file))
684
685 config = self.masters[self.args.master][self.args.builder]
686 if isinstance(config, dict):
687 if self.args.phase is None:
688 raise MBErr('Must specify a build --phase for %s on %s' %
689 (self.args.builder, self.args.master))
690 phase = str(self.args.phase)
691 if phase not in config:
692 raise MBErr('Phase %s doesn\'t exist for %s on %s' %
693 (phase, self.args.builder, self.args.master))
694 return config[phase]
695
696 if self.args.phase is not None:
697 raise MBErr('Must not specify a build --phase for %s on %s' %
698 (self.args.builder, self.args.master))
699 return config
700
701 def FlattenConfig(self, config):
702 mixins = self.configs[config]
703 vals = self.DefaultVals()
704
705 visited = []
706 self.FlattenMixins(mixins, vals, visited)
707 return vals
708
709 def DefaultVals(self):
710 return {
711 'args_file': '',
712 'cros_passthrough': False,
713 'gn_args': '',
714 'gyp_defines': '',
715 'gyp_crosscompile': False,
716 'type': 'gn',
717 }
718
719 def FlattenMixins(self, mixins, vals, visited):
720 for m in mixins:
721 if m not in self.mixins:
722 raise MBErr('Unknown mixin "%s"' % m)
723
724 visited.append(m)
725
726 mixin_vals = self.mixins[m]
727
728 if 'cros_passthrough' in mixin_vals:
729 vals['cros_passthrough'] = mixin_vals['cros_passthrough']
730 if 'gn_args' in mixin_vals:
731 if vals['gn_args']:
732 vals['gn_args'] += ' ' + mixin_vals['gn_args']
733 else:
734 vals['gn_args'] = mixin_vals['gn_args']
735 if 'gyp_crosscompile' in mixin_vals:
736 vals['gyp_crosscompile'] = mixin_vals['gyp_crosscompile']
737 if 'gyp_defines' in mixin_vals:
738 if vals['gyp_defines']:
739 vals['gyp_defines'] += ' ' + mixin_vals['gyp_defines']
740 else:
741 vals['gyp_defines'] = mixin_vals['gyp_defines']
742 if 'type' in mixin_vals:
743 vals['type'] = mixin_vals['type']
744
745 if 'mixins' in mixin_vals:
746 self.FlattenMixins(mixin_vals['mixins'], vals, visited)
747 return vals
748
749 def ClobberIfNeeded(self, vals):
750 path = self.args.path[0]
751 build_dir = self.ToAbsPath(path)
752 mb_type_path = self.PathJoin(build_dir, 'mb_type')
753 needs_clobber = False
754 new_mb_type = vals['type']
755 if self.Exists(build_dir):
756 if self.Exists(mb_type_path):
757 old_mb_type = self.ReadFile(mb_type_path)
758 if old_mb_type != new_mb_type:
759 self.Print("Build type mismatch: was %s, will be %s, clobbering %s" %
760 (old_mb_type, new_mb_type, path))
761 needs_clobber = True
762 else:
763 # There is no 'mb_type' file in the build directory, so this probably
764 # means that the prior build(s) were not done through mb, and we
765 # have no idea if this was a GYP build or a GN build. Clobber it
766 # to be safe.
767 self.Print("%s/mb_type missing, clobbering to be safe" % path)
768 needs_clobber = True
769
770 if self.args.dryrun:
771 return
772
773 if needs_clobber:
774 self.RemoveDirectory(build_dir)
775
776 self.MaybeMakeDirectory(build_dir)
777 self.WriteFile(mb_type_path, new_mb_type)
778
779 def RunGNGen(self, vals):
780 build_dir = self.args.path[0]
781
782 cmd = self.GNCmd('gen', build_dir, '--check')
783 gn_args = self.GNArgs(vals)
784
785 # Since GN hasn't run yet, the build directory may not even exist.
786 self.MaybeMakeDirectory(self.ToAbsPath(build_dir))
787
788 gn_args_path = self.ToAbsPath(build_dir, 'args.gn')
789 self.WriteFile(gn_args_path, gn_args, force_verbose=True)
790
791 swarming_targets = []
792 if getattr(self.args, 'swarming_targets_file', None):
793 # We need GN to generate the list of runtime dependencies for
794 # the compile targets listed (one per line) in the file so
795 # we can run them via swarming. We use gn_isolate_map.pyl to convert
796 # the compile targets to the matching GN labels.
797 path = self.args.swarming_targets_file
798 if not self.Exists(path):
799 self.WriteFailureAndRaise('"%s" does not exist' % path,
800 output_path=None)
801 contents = self.ReadFile(path)
802 swarming_targets = set(contents.splitlines())
803
804 isolate_map = self.ReadIsolateMap()
805 err, labels = self.MapTargetsToLabels(isolate_map, swarming_targets)
806 if err:
807 raise MBErr(err)
808
809 gn_runtime_deps_path = self.ToAbsPath(build_dir, 'runtime_deps')
810 self.WriteFile(gn_runtime_deps_path, '\n'.join(labels) + '\n')
811 cmd.append('--runtime-deps-list-file=%s' % gn_runtime_deps_path)
812
813 ret, _, _ = self.Run(cmd)
814 if ret:
815 # If `gn gen` failed, we should exit early rather than trying to
816 # generate isolates. Run() will have already logged any error output.
817 self.Print('GN gen failed: %d' % ret)
818 return ret
819
820 android = 'target_os="android"' in vals['gn_args']
821 for target in swarming_targets:
822 if android:
823 # Android targets may be either android_apk or executable. The former
824 # will result in runtime_deps associated with the stamp file, while the
825 # latter will result in runtime_deps associated with the executable.
826 label = isolate_map[target]['label']
827 runtime_deps_targets = [
828 target + '.runtime_deps',
829 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
830 elif isolate_map[target]['type'] == 'gpu_browser_test':
831 if self.platform == 'win32':
832 runtime_deps_targets = ['browser_tests.exe.runtime_deps']
833 else:
834 runtime_deps_targets = ['browser_tests.runtime_deps']
835 elif (isolate_map[target]['type'] == 'script' or
836 isolate_map[target].get('label_type') == 'group'):
837 # For script targets, the build target is usually a group,
838 # for which gn generates the runtime_deps next to the stamp file
839 # for the label, which lives under the obj/ directory, but it may
840 # also be an executable.
841 label = isolate_map[target]['label']
842 runtime_deps_targets = [
843 'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
844 if self.platform == 'win32':
845 runtime_deps_targets += [ target + '.exe.runtime_deps' ]
846 else:
847 runtime_deps_targets += [ target + '.runtime_deps' ]
848 elif self.platform == 'win32':
849 runtime_deps_targets = [target + '.exe.runtime_deps']
850 else:
851 runtime_deps_targets = [target + '.runtime_deps']
852
853 for r in runtime_deps_targets:
854 runtime_deps_path = self.ToAbsPath(build_dir, r)
855 if self.Exists(runtime_deps_path):
856 break
857 else:
858 raise MBErr('did not generate any of %s' %
859 ', '.join(runtime_deps_targets))
860
861 command, extra_files = self.GetIsolateCommand(target, vals)
862
863 runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
864
865 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
866 extra_files)
867
868 return 0
869
870 def RunGNIsolate(self, vals):
871 target = self.args.target[0]
872 isolate_map = self.ReadIsolateMap()
873 err, labels = self.MapTargetsToLabels(isolate_map, [target])
874 if err:
875 raise MBErr(err)
876 label = labels[0]
877
878 build_dir = self.args.path[0]
879 command, extra_files = self.GetIsolateCommand(target, vals)
880
881 cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
882 ret, out, _ = self.Call(cmd)
883 if ret:
884 if out:
885 self.Print(out)
886 return ret
887
888 runtime_deps = out.splitlines()
889
890 self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
891 extra_files)
892
893 ret, _, _ = self.Run([
894 self.executable,
895 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
896 'check',
897 '-i',
898 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
899 '-s',
900 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
901 buffer_output=False)
902
903 return ret
904
905 def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
906 extra_files):
907 isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
908 self.WriteFile(isolate_path,
909 pprint.pformat({
910 'variables': {
911 'command': command,
912 'files': sorted(runtime_deps + extra_files),
913 }
914 }) + '\n')
915
916 self.WriteJSON(
917 {
918 'args': [
919 '--isolated',
920 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
921 '--isolate',
922 self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
923 ],
kjellander73f01de2017-02-15 12:34:03 -0800924 'dir': self.src_dir,
kjellandera013a022016-11-14 05:54:22 -0800925 'version': 1,
926 },
927 isolate_path + 'd.gen.json',
928 )
929
930 def MapTargetsToLabels(self, isolate_map, targets):
931 labels = []
932 err = ''
933
934 def StripTestSuffixes(target):
935 for suffix in ('_apk_run', '_apk', '_run'):
936 if target.endswith(suffix):
937 return target[:-len(suffix)], suffix
938 return None, None
939
940 for target in targets:
941 if target == 'all':
942 labels.append(target)
943 elif target.startswith('//'):
944 labels.append(target)
945 else:
946 if target in isolate_map:
947 stripped_target, suffix = target, ''
948 else:
949 stripped_target, suffix = StripTestSuffixes(target)
950 if stripped_target in isolate_map:
951 if isolate_map[stripped_target]['type'] == 'unknown':
952 err += ('test target "%s" type is unknown\n' % target)
953 else:
954 labels.append(isolate_map[stripped_target]['label'] + suffix)
955 else:
956 err += ('target "%s" not found in '
957 '//testing/buildbot/gn_isolate_map.pyl\n' % target)
958
959 return err, labels
960
961 def GNCmd(self, subcommand, path, *args):
962 if self.platform == 'linux2':
963 subdir, exe = 'linux64', 'gn'
964 elif self.platform == 'darwin':
965 subdir, exe = 'mac', 'gn'
966 else:
967 subdir, exe = 'win', 'gn.exe'
968
kjellander73f01de2017-02-15 12:34:03 -0800969 gn_path = self.PathJoin(self.src_dir, 'buildtools', subdir, exe)
kjellandera013a022016-11-14 05:54:22 -0800970 return [gn_path, subcommand, path] + list(args)
971
972
973 def GNArgs(self, vals):
974 if vals['cros_passthrough']:
975 if not 'GN_ARGS' in os.environ:
976 raise MBErr('MB is expecting GN_ARGS to be in the environment')
977 gn_args = os.environ['GN_ARGS']
978 if not re.search('target_os.*=.*"chromeos"', gn_args):
979 raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
980 gn_args)
981 else:
982 gn_args = vals['gn_args']
983
984 if self.args.goma_dir:
985 gn_args += ' goma_dir="%s"' % self.args.goma_dir
986
987 android_version_code = self.args.android_version_code
988 if android_version_code:
989 gn_args += ' android_default_version_code="%s"' % android_version_code
990
991 android_version_name = self.args.android_version_name
992 if android_version_name:
993 gn_args += ' android_default_version_name="%s"' % android_version_name
994
995 # Canonicalize the arg string into a sorted, newline-separated list
996 # of key-value pairs, and de-dup the keys if need be so that only
997 # the last instance of each arg is listed.
998 gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
999
1000 args_file = vals.get('args_file', None)
1001 if args_file:
1002 gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
1003 return gn_args
1004
1005 def RunGYPGen(self, vals):
1006 path = self.args.path[0]
1007
1008 output_dir = self.ParseGYPConfigPath(path)
1009 cmd, env = self.GYPCmd(output_dir, vals)
1010 ret, _, _ = self.Run(cmd, env=env)
1011 return ret
1012
1013 def RunGYPAnalyze(self, vals):
1014 output_dir = self.ParseGYPConfigPath(self.args.path[0])
1015 if self.args.verbose:
1016 inp = self.ReadInputJSON(['files', 'test_targets',
1017 'additional_compile_targets'])
1018 self.Print()
1019 self.Print('analyze input:')
1020 self.PrintJSON(inp)
1021 self.Print()
1022
1023 cmd, env = self.GYPCmd(output_dir, vals)
1024 cmd.extend(['-f', 'analyzer',
1025 '-G', 'config_path=%s' % self.args.input_path[0],
1026 '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
1027 ret, _, _ = self.Run(cmd, env=env)
1028 if not ret and self.args.verbose:
1029 outp = json.loads(self.ReadFile(self.args.output_path[0]))
1030 self.Print()
1031 self.Print('analyze output:')
1032 self.PrintJSON(outp)
1033 self.Print()
1034
1035 return ret
1036
1037 def GetIsolateCommand(self, target, vals):
kjellandera013a022016-11-14 05:54:22 -08001038 isolate_map = self.ReadIsolateMap()
1039 test_type = isolate_map[target]['type']
1040
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001041 android = 'target_os="android"' in vals['gn_args']
1042 is_linux = self.platform == 'linux2' and not android
kjellandera013a022016-11-14 05:54:22 -08001043
1044 if test_type == 'nontest':
1045 self.WriteFailureAndRaise('We should not be isolating %s.' % target,
1046 output_path=None)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001047 if test_type not in ('console_test_launcher', 'windowed_test_launcher',
1048 'non_parallel_console_test_launcher',
1049 'additional_compile_target', 'junit_test'):
1050 self.WriteFailureAndRaise('No command line for %s found (test type %s).'
1051 % (target, test_type), output_path=None)
kjellandera013a022016-11-14 05:54:22 -08001052
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001053 cmdline = []
1054 extra_files = []
1055
1056 if android:
kjellandera013a022016-11-14 05:54:22 -08001057 logdog_command = [
1058 '--logdog-bin-cmd', './../../bin/logdog_butler',
1059 '--project', 'chromium',
1060 '--service-account-json',
1061 '/creds/service_accounts/service-account-luci-logdog-publisher.json',
1062 '--prefix', 'android/swarming/logcats/${SWARMING_TASK_ID}',
1063 '--source', '${ISOLATED_OUTDIR}/logcats',
1064 '--name', 'unified_logcats',
1065 ]
1066 test_cmdline = [
1067 self.PathJoin('bin', 'run_%s' % target),
1068 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
kjellandera013a022016-11-14 05:54:22 -08001069 ]
ehmaldonadoe1a13f82016-11-28 07:14:09 -08001070 if test_type != 'junit_test':
1071 test_cmdline += ['--target-devices-file', '${SWARMING_BOT_FILE}',]
kjellandera013a022016-11-14 05:54:22 -08001072 cmdline = (['./../../build/android/test_wrapper/logdog_wrapper.py']
ehmaldonadoe1a13f82016-11-28 07:14:09 -08001073 + logdog_command + test_cmdline + ['-v'])
kjellandera013a022016-11-14 05:54:22 -08001074 else:
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001075 extra_files = ['../../testing/test_env.py']
kjellandera013a022016-11-14 05:54:22 -08001076
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001077 # This needs to mirror the settings in //build/config/ui.gni:
1078 # use_x11 = is_linux && !use_ozone.
1079 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
1080
1081 xvfb = use_x11 and test_type == 'windowed_test_launcher'
1082 if xvfb:
1083 extra_files += [
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001084 '../../testing/xvfb.py',
1085 ]
1086
1087 # Memcheck is only supported for linux. Ignore in other platforms.
1088 memcheck = is_linux and 'rtc_use_memcheck=true' in vals['gn_args']
1089 memcheck_cmdline = [
1090 'bash',
kjellanderafd54942016-12-17 12:21:39 -08001091 '../../tools-webrtc/valgrind/webrtc_tests.sh',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001092 '--tool',
1093 'memcheck',
1094 '--target',
1095 'Release',
1096 '--build-dir',
1097 '..',
1098 '--test',
1099 ]
1100
1101 gtest_parallel = (test_type != 'non_parallel_console_test_launcher' and
1102 not memcheck)
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001103 if gtest_parallel:
1104 extra_files += [
1105 '../../third_party/gtest-parallel/gtest-parallel',
1106 '../../third_party/gtest-parallel/gtest-parallel-wrapper.py',
1107 ]
ehmaldonado55833842017-02-13 03:58:13 -08001108 sep = '\\' if self.platform == 'win32' else '/'
1109 output_dir = '${ISOLATED_OUTDIR}' + sep + 'test_logs'
1110 gtest_parallel_wrapper = [
1111 '../../third_party/gtest-parallel/gtest-parallel-wrapper.py',
1112 '--output_dir=%s' % output_dir,
1113 ]
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001114
1115 asan = 'is_asan=true' in vals['gn_args']
1116 msan = 'is_msan=true' in vals['gn_args']
1117 tsan = 'is_tsan=true' in vals['gn_args']
1118
1119 executable_prefix = '.\\' if self.platform == 'win32' else './'
1120 executable_suffix = '.exe' if self.platform == 'win32' else ''
1121 executable = executable_prefix + target + executable_suffix
1122
kjellander7e1070d2016-12-08 00:49:03 -08001123 cmdline = (['../../testing/xvfb.py'] if xvfb else
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001124 ['../../testing/test_env.py'])
1125 if memcheck:
1126 cmdline += memcheck_cmdline
1127 elif gtest_parallel:
1128 cmdline += gtest_parallel_wrapper
1129 cmdline += [
1130 executable,
1131 '--',
1132 '--asan=%d' % asan,
1133 '--msan=%d' % msan,
1134 '--tsan=%d' % tsan,
1135 ]
kjellandera013a022016-11-14 05:54:22 -08001136
1137 return cmdline, extra_files
1138
1139 def ToAbsPath(self, build_path, *comps):
kjellander73f01de2017-02-15 12:34:03 -08001140 return self.PathJoin(self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001141 self.ToSrcRelPath(build_path),
1142 *comps)
1143
1144 def ToSrcRelPath(self, path):
1145 """Returns a relative path from the top of the repo."""
1146 if path.startswith('//'):
1147 return path[2:].replace('/', self.sep)
kjellander73f01de2017-02-15 12:34:03 -08001148 return self.RelPath(path, self.src_dir)
kjellandera013a022016-11-14 05:54:22 -08001149
1150 def ParseGYPConfigPath(self, path):
1151 rpath = self.ToSrcRelPath(path)
1152 output_dir, _, _ = rpath.rpartition(self.sep)
1153 return output_dir
1154
1155 def GYPCmd(self, output_dir, vals):
1156 if vals['cros_passthrough']:
1157 if not 'GYP_DEFINES' in os.environ:
1158 raise MBErr('MB is expecting GYP_DEFINES to be in the environment')
1159 gyp_defines = os.environ['GYP_DEFINES']
1160 if not 'chromeos=1' in gyp_defines:
1161 raise MBErr('GYP_DEFINES is missing chromeos=1: (GYP_DEFINES=%s)' %
1162 gyp_defines)
1163 else:
1164 gyp_defines = vals['gyp_defines']
1165
1166 goma_dir = self.args.goma_dir
1167
1168 # GYP uses shlex.split() to split the gyp defines into separate arguments,
1169 # so we can support backslashes and and spaces in arguments by quoting
1170 # them, even on Windows, where this normally wouldn't work.
1171 if goma_dir and ('\\' in goma_dir or ' ' in goma_dir):
1172 goma_dir = "'%s'" % goma_dir
1173
1174 if goma_dir:
1175 gyp_defines += ' gomadir=%s' % goma_dir
1176
1177 android_version_code = self.args.android_version_code
1178 if android_version_code:
1179 gyp_defines += ' app_manifest_version_code=%s' % android_version_code
1180
1181 android_version_name = self.args.android_version_name
1182 if android_version_name:
1183 gyp_defines += ' app_manifest_version_name=%s' % android_version_name
1184
1185 cmd = [
1186 self.executable,
1187 self.args.gyp_script,
1188 '-G',
1189 'output_dir=' + output_dir,
1190 ]
1191
1192 # Ensure that we have an environment that only contains
1193 # the exact values of the GYP variables we need.
1194 env = os.environ.copy()
1195
1196 # This is a terrible hack to work around the fact that
1197 # //tools/clang/scripts/update.py is invoked by GYP and GN but
1198 # currently relies on an environment variable to figure out
1199 # what revision to embed in the command line #defines.
1200 # For GN, we've made this work via a gn arg that will cause update.py
1201 # to get an additional command line arg, but getting that to work
1202 # via GYP_DEFINES has proven difficult, so we rewrite the GYP_DEFINES
1203 # to get rid of the arg and add the old var in, instead.
1204 # See crbug.com/582737 for more on this. This can hopefully all
1205 # go away with GYP.
1206 m = re.search('llvm_force_head_revision=1\s*', gyp_defines)
1207 if m:
1208 env['LLVM_FORCE_HEAD_REVISION'] = '1'
1209 gyp_defines = gyp_defines.replace(m.group(0), '')
1210
1211 # This is another terrible hack to work around the fact that
1212 # GYP sets the link concurrency to use via the GYP_LINK_CONCURRENCY
1213 # environment variable, and not via a proper GYP_DEFINE. See
1214 # crbug.com/611491 for more on this.
1215 m = re.search('gyp_link_concurrency=(\d+)(\s*)', gyp_defines)
1216 if m:
1217 env['GYP_LINK_CONCURRENCY'] = m.group(1)
1218 gyp_defines = gyp_defines.replace(m.group(0), '')
1219
1220 env['GYP_GENERATORS'] = 'ninja'
1221 if 'GYP_CHROMIUM_NO_ACTION' in env:
1222 del env['GYP_CHROMIUM_NO_ACTION']
1223 if 'GYP_CROSSCOMPILE' in env:
1224 del env['GYP_CROSSCOMPILE']
1225 env['GYP_DEFINES'] = gyp_defines
1226 if vals['gyp_crosscompile']:
1227 env['GYP_CROSSCOMPILE'] = '1'
1228 return cmd, env
1229
1230 def RunGNAnalyze(self, vals):
1231 # Analyze runs before 'gn gen' now, so we need to run gn gen
1232 # in order to ensure that we have a build directory.
1233 ret = self.RunGNGen(vals)
1234 if ret:
1235 return ret
1236
1237 build_path = self.args.path[0]
1238 input_path = self.args.input_path[0]
1239 gn_input_path = input_path + '.gn'
1240 output_path = self.args.output_path[0]
1241 gn_output_path = output_path + '.gn'
1242
1243 inp = self.ReadInputJSON(['files', 'test_targets',
1244 'additional_compile_targets'])
1245 if self.args.verbose:
1246 self.Print()
1247 self.Print('analyze input:')
1248 self.PrintJSON(inp)
1249 self.Print()
1250
1251
1252 # This shouldn't normally happen, but could due to unusual race conditions,
1253 # like a try job that gets scheduled before a patch lands but runs after
1254 # the patch has landed.
1255 if not inp['files']:
1256 self.Print('Warning: No files modified in patch, bailing out early.')
1257 self.WriteJSON({
1258 'status': 'No dependency',
1259 'compile_targets': [],
1260 'test_targets': [],
1261 }, output_path)
1262 return 0
1263
1264 gn_inp = {}
1265 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
1266
1267 isolate_map = self.ReadIsolateMap()
1268 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
1269 isolate_map, inp['additional_compile_targets'])
1270 if err:
1271 raise MBErr(err)
1272
1273 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
1274 isolate_map, inp['test_targets'])
1275 if err:
1276 raise MBErr(err)
1277 labels_to_targets = {}
1278 for i, label in enumerate(gn_inp['test_targets']):
1279 labels_to_targets[label] = inp['test_targets'][i]
1280
1281 try:
1282 self.WriteJSON(gn_inp, gn_input_path)
1283 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
1284 ret, _, _ = self.Run(cmd, force_verbose=True)
1285 if ret:
1286 return ret
1287
1288 gn_outp_str = self.ReadFile(gn_output_path)
1289 try:
1290 gn_outp = json.loads(gn_outp_str)
1291 except Exception as e:
1292 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
1293 % (repr(gn_outp_str), str(e)))
1294 raise
1295
1296 outp = {}
1297 if 'status' in gn_outp:
1298 outp['status'] = gn_outp['status']
1299 if 'error' in gn_outp:
1300 outp['error'] = gn_outp['error']
1301 if 'invalid_targets' in gn_outp:
1302 outp['invalid_targets'] = gn_outp['invalid_targets']
1303 if 'compile_targets' in gn_outp:
1304 if 'all' in gn_outp['compile_targets']:
1305 outp['compile_targets'] = ['all']
1306 else:
1307 outp['compile_targets'] = [
1308 label.replace('//', '') for label in gn_outp['compile_targets']]
1309 if 'test_targets' in gn_outp:
1310 outp['test_targets'] = [
1311 labels_to_targets[label] for label in gn_outp['test_targets']]
1312
1313 if self.args.verbose:
1314 self.Print()
1315 self.Print('analyze output:')
1316 self.PrintJSON(outp)
1317 self.Print()
1318
1319 self.WriteJSON(outp, output_path)
1320
1321 finally:
1322 if self.Exists(gn_input_path):
1323 self.RemoveFile(gn_input_path)
1324 if self.Exists(gn_output_path):
1325 self.RemoveFile(gn_output_path)
1326
1327 return 0
1328
1329 def ReadInputJSON(self, required_keys):
1330 path = self.args.input_path[0]
1331 output_path = self.args.output_path[0]
1332 if not self.Exists(path):
1333 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1334
1335 try:
1336 inp = json.loads(self.ReadFile(path))
1337 except Exception as e:
1338 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1339 (path, e), output_path)
1340
1341 for k in required_keys:
1342 if not k in inp:
1343 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1344 output_path)
1345
1346 return inp
1347
1348 def WriteFailureAndRaise(self, msg, output_path):
1349 if output_path:
1350 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1351 raise MBErr(msg)
1352
1353 def WriteJSON(self, obj, path, force_verbose=False):
1354 try:
1355 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1356 force_verbose=force_verbose)
1357 except Exception as e:
1358 raise MBErr('Error %s writing to the output path "%s"' %
1359 (e, path))
1360
1361 def CheckCompile(self, master, builder):
1362 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1363 url = urllib2.quote(url_template.format(master=master, builder=builder),
1364 safe=':/()?=')
1365 try:
1366 builds = json.loads(self.Fetch(url))
1367 except Exception as e:
1368 return str(e)
1369 successes = sorted(
1370 [int(x) for x in builds.keys() if "text" in builds[x] and
1371 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1372 reverse=True)
1373 if not successes:
1374 return "no successful builds"
1375 build = builds[str(successes[0])]
1376 step_names = set([step["name"] for step in build["steps"]])
1377 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1378 if compile_indicators & step_names:
1379 return "compiles"
1380 return "does not compile"
1381
1382 def PrintCmd(self, cmd, env):
1383 if self.platform == 'win32':
1384 env_prefix = 'set '
1385 env_quoter = QuoteForSet
1386 shell_quoter = QuoteForCmd
1387 else:
1388 env_prefix = ''
1389 env_quoter = pipes.quote
1390 shell_quoter = pipes.quote
1391
1392 def print_env(var):
1393 if env and var in env:
1394 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1395
1396 print_env('GYP_CROSSCOMPILE')
1397 print_env('GYP_DEFINES')
1398 print_env('GYP_LINK_CONCURRENCY')
1399 print_env('LLVM_FORCE_HEAD_REVISION')
1400
1401 if cmd[0] == self.executable:
1402 cmd = ['python'] + cmd[1:]
1403 self.Print(*[shell_quoter(arg) for arg in cmd])
1404
1405 def PrintJSON(self, obj):
1406 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1407
1408 def Build(self, target):
1409 build_dir = self.ToSrcRelPath(self.args.path[0])
1410 ninja_cmd = ['ninja', '-C', build_dir]
1411 if self.args.jobs:
1412 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1413 ninja_cmd.append(target)
1414 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1415 return ret
1416
1417 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1418 # This function largely exists so it can be overridden for testing.
1419 if self.args.dryrun or self.args.verbose or force_verbose:
1420 self.PrintCmd(cmd, env)
1421 if self.args.dryrun:
1422 return 0, '', ''
1423
1424 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1425 if self.args.verbose or force_verbose:
1426 if ret:
1427 self.Print(' -> returned %d' % ret)
1428 if out:
1429 self.Print(out, end='')
1430 if err:
1431 self.Print(err, end='', file=sys.stderr)
1432 return ret, out, err
1433
1434 def Call(self, cmd, env=None, buffer_output=True):
1435 if buffer_output:
kjellander73f01de2017-02-15 12:34:03 -08001436 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001437 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1438 env=env)
1439 out, err = p.communicate()
1440 else:
kjellander73f01de2017-02-15 12:34:03 -08001441 p = subprocess.Popen(cmd, shell=False, cwd=self.src_dir,
kjellandera013a022016-11-14 05:54:22 -08001442 env=env)
1443 p.wait()
1444 out = err = ''
1445 return p.returncode, out, err
1446
1447 def ExpandUser(self, path):
1448 # This function largely exists so it can be overridden for testing.
1449 return os.path.expanduser(path)
1450
1451 def Exists(self, path):
1452 # This function largely exists so it can be overridden for testing.
1453 return os.path.exists(path)
1454
1455 def Fetch(self, url):
1456 # This function largely exists so it can be overridden for testing.
1457 f = urllib2.urlopen(url)
1458 contents = f.read()
1459 f.close()
1460 return contents
1461
1462 def MaybeMakeDirectory(self, path):
1463 try:
1464 os.makedirs(path)
1465 except OSError, e:
1466 if e.errno != errno.EEXIST:
1467 raise
1468
1469 def PathJoin(self, *comps):
1470 # This function largely exists so it can be overriden for testing.
1471 return os.path.join(*comps)
1472
1473 def Print(self, *args, **kwargs):
1474 # This function largely exists so it can be overridden for testing.
1475 print(*args, **kwargs)
1476 if kwargs.get('stream', sys.stdout) == sys.stdout:
1477 sys.stdout.flush()
1478
1479 def ReadFile(self, path):
1480 # This function largely exists so it can be overriden for testing.
1481 with open(path) as fp:
1482 return fp.read()
1483
1484 def RelPath(self, path, start='.'):
1485 # This function largely exists so it can be overriden for testing.
1486 return os.path.relpath(path, start)
1487
1488 def RemoveFile(self, path):
1489 # This function largely exists so it can be overriden for testing.
1490 os.remove(path)
1491
1492 def RemoveDirectory(self, abs_path):
1493 if self.platform == 'win32':
1494 # In other places in chromium, we often have to retry this command
1495 # because we're worried about other processes still holding on to
1496 # file handles, but when MB is invoked, it will be early enough in the
1497 # build that their should be no other processes to interfere. We
1498 # can change this if need be.
1499 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1500 else:
1501 shutil.rmtree(abs_path, ignore_errors=True)
1502
1503 def TempFile(self, mode='w'):
1504 # This function largely exists so it can be overriden for testing.
1505 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1506
1507 def WriteFile(self, path, contents, force_verbose=False):
1508 # This function largely exists so it can be overriden for testing.
1509 if self.args.dryrun or self.args.verbose or force_verbose:
1510 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1511 with open(path, 'w') as fp:
1512 return fp.write(contents)
1513
1514
1515class MBErr(Exception):
1516 pass
1517
1518
1519# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1520# details of this next section, which handles escaping command lines
1521# so that they can be copied and pasted into a cmd window.
1522UNSAFE_FOR_SET = set('^<>&|')
1523UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1524ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1525
1526
1527def QuoteForSet(arg):
1528 if any(a in UNSAFE_FOR_SET for a in arg):
1529 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1530 return arg
1531
1532
1533def QuoteForCmd(arg):
1534 # First, escape the arg so that CommandLineToArgvW will parse it properly.
1535 # From //tools/gyp/pylib/gyp/msvs_emulation.py:23.
1536 if arg == '' or ' ' in arg or '"' in arg:
1537 quote_re = re.compile(r'(\\*)"')
1538 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1539
1540 # Then check to see if the arg contains any metacharacters other than
1541 # double quotes; if it does, quote everything (including the double
1542 # quotes) for safety.
1543 if any(a in UNSAFE_FOR_CMD for a in arg):
1544 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1545 return arg
1546
1547
1548if __name__ == '__main__':
1549 sys.exit(main(sys.argv[1:]))