blob: 77a81e0b5b7e1888fd3e47ca1ecfb8cb3fe2644a [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__))
36CHROMIUM_SRC_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
kjellandera013a022016-11-14 05:54:22 -080037sys.path = [os.path.join(CHROMIUM_SRC_DIR, 'build')] + sys.path
38
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):
49 self.chromium_src_dir = CHROMIUM_SRC_DIR
Henrik Kjellanderb2d55772016-12-18 22:14:50 +010050 self.default_config = os.path.join(SCRIPT_DIR, 'mb_config.pyl')
kjellandera013a022016-11-14 05:54:22 -080051 self.default_isolate_map = os.path.join(self.chromium_src_dir, 'testing',
52 'buildbot', 'gn_isolate_map.pyl')
53 self.executable = sys.executable
54 self.platform = sys.platform
55 self.sep = os.sep
56 self.args = argparse.Namespace()
57 self.configs = {}
58 self.masters = {}
59 self.mixins = {}
60
61 def Main(self, args):
62 self.ParseArgs(args)
63 try:
64 ret = self.args.func()
65 if ret:
66 self.DumpInputFiles()
67 return ret
68 except KeyboardInterrupt:
69 self.Print('interrupted, exiting')
70 return 130
71 except Exception:
72 self.DumpInputFiles()
73 s = traceback.format_exc()
74 for l in s.splitlines():
75 self.Print(l)
76 return 1
77
78 def ParseArgs(self, argv):
79 def AddCommonOptions(subp):
80 subp.add_argument('-b', '--builder',
81 help='builder name to look up config from')
82 subp.add_argument('-m', '--master',
83 help='master name to look up config from')
84 subp.add_argument('-c', '--config',
85 help='configuration to analyze')
86 subp.add_argument('--phase',
87 help='optional phase name (used when builders '
88 'do multiple compiles with different '
89 'arguments in a single build)')
90 subp.add_argument('-f', '--config-file', metavar='PATH',
91 default=self.default_config,
92 help='path to config file '
93 '(default is %(default)s)')
94 subp.add_argument('-i', '--isolate-map-file', metavar='PATH',
95 default=self.default_isolate_map,
96 help='path to isolate map file '
97 '(default is %(default)s)')
98 subp.add_argument('-g', '--goma-dir',
99 help='path to goma directory')
100 subp.add_argument('--gyp-script', metavar='PATH',
101 default=self.PathJoin('build', 'gyp_chromium'),
102 help='path to gyp script relative to project root '
103 '(default is %(default)s)')
104 subp.add_argument('--android-version-code',
105 help='Sets GN arg android_default_version_code and '
106 'GYP_DEFINE app_manifest_version_code')
107 subp.add_argument('--android-version-name',
108 help='Sets GN arg android_default_version_name and '
109 'GYP_DEFINE app_manifest_version_name')
110 subp.add_argument('-n', '--dryrun', action='store_true',
111 help='Do a dry run (i.e., do nothing, just print '
112 'the commands that will run)')
113 subp.add_argument('-v', '--verbose', action='store_true',
114 help='verbose logging')
115
116 parser = argparse.ArgumentParser(prog='mb')
117 subps = parser.add_subparsers()
118
119 subp = subps.add_parser('analyze',
120 help='analyze whether changes to a set of files '
121 'will cause a set of binaries to be rebuilt.')
122 AddCommonOptions(subp)
123 subp.add_argument('path', nargs=1,
124 help='path build was generated into.')
125 subp.add_argument('input_path', nargs=1,
126 help='path to a file containing the input arguments '
127 'as a JSON object.')
128 subp.add_argument('output_path', nargs=1,
129 help='path to a file containing the output arguments '
130 'as a JSON object.')
131 subp.set_defaults(func=self.CmdAnalyze)
132
133 subp = subps.add_parser('export',
134 help='print out the expanded configuration for'
135 'each builder as a JSON object')
136 subp.add_argument('-f', '--config-file', metavar='PATH',
137 default=self.default_config,
138 help='path to config file (default is %(default)s)')
139 subp.add_argument('-g', '--goma-dir',
140 help='path to goma directory')
141 subp.set_defaults(func=self.CmdExport)
142
143 subp = subps.add_parser('gen',
144 help='generate a new set of build files')
145 AddCommonOptions(subp)
146 subp.add_argument('--swarming-targets-file',
147 help='save runtime dependencies for targets listed '
148 'in file.')
149 subp.add_argument('path', nargs=1,
150 help='path to generate build into')
151 subp.set_defaults(func=self.CmdGen)
152
153 subp = subps.add_parser('isolate',
154 help='generate the .isolate files for a given'
155 'binary')
156 AddCommonOptions(subp)
157 subp.add_argument('path', nargs=1,
158 help='path build was generated into')
159 subp.add_argument('target', nargs=1,
160 help='ninja target to generate the isolate for')
161 subp.set_defaults(func=self.CmdIsolate)
162
163 subp = subps.add_parser('lookup',
164 help='look up the command for a given config or '
165 'builder')
166 AddCommonOptions(subp)
167 subp.set_defaults(func=self.CmdLookup)
168
169 subp = subps.add_parser(
170 'run',
171 help='build and run the isolated version of a '
172 'binary',
173 formatter_class=argparse.RawDescriptionHelpFormatter)
174 subp.description = (
175 'Build, isolate, and run the given binary with the command line\n'
176 'listed in the isolate. You may pass extra arguments after the\n'
177 'target; use "--" if the extra arguments need to include switches.\n'
178 '\n'
179 'Examples:\n'
180 '\n'
181 ' % tools/mb/mb.py run -m chromium.linux -b "Linux Builder" \\\n'
182 ' //out/Default content_browsertests\n'
183 '\n'
184 ' % tools/mb/mb.py run out/Default content_browsertests\n'
185 '\n'
186 ' % tools/mb/mb.py run out/Default content_browsertests -- \\\n'
187 ' --test-launcher-retry-limit=0'
188 '\n'
189 )
190
191 AddCommonOptions(subp)
192 subp.add_argument('-j', '--jobs', dest='jobs', type=int,
193 help='Number of jobs to pass to ninja')
194 subp.add_argument('--no-build', dest='build', default=True,
195 action='store_false',
196 help='Do not build, just isolate and run')
197 subp.add_argument('path', nargs=1,
198 help=('path to generate build into (or use).'
199 ' This can be either a regular path or a '
200 'GN-style source-relative path like '
201 '//out/Default.'))
202 subp.add_argument('target', nargs=1,
203 help='ninja target to build and run')
204 subp.add_argument('extra_args', nargs='*',
205 help=('extra args to pass to the isolate to run. Use '
206 '"--" as the first arg if you need to pass '
207 'switches'))
208 subp.set_defaults(func=self.CmdRun)
209
210 subp = subps.add_parser('validate',
211 help='validate the config file')
212 subp.add_argument('-f', '--config-file', metavar='PATH',
213 default=self.default_config,
214 help='path to config file (default is %(default)s)')
215 subp.set_defaults(func=self.CmdValidate)
216
217 subp = subps.add_parser('audit',
218 help='Audit the config file to track progress')
219 subp.add_argument('-f', '--config-file', metavar='PATH',
220 default=self.default_config,
221 help='path to config file (default is %(default)s)')
222 subp.add_argument('-i', '--internal', action='store_true',
223 help='check internal masters also')
224 subp.add_argument('-m', '--master', action='append',
225 help='master to audit (default is all non-internal '
226 'masters in file)')
227 subp.add_argument('-u', '--url-template', action='store',
228 default='https://build.chromium.org/p/'
229 '{master}/json/builders',
230 help='URL scheme for JSON APIs to buildbot '
231 '(default: %(default)s) ')
232 subp.add_argument('-c', '--check-compile', action='store_true',
233 help='check whether tbd and master-only bots actually'
234 ' do compiles')
235 subp.set_defaults(func=self.CmdAudit)
236
237 subp = subps.add_parser('help',
238 help='Get help on a subcommand.')
239 subp.add_argument(nargs='?', action='store', dest='subcommand',
240 help='The command to get help for.')
241 subp.set_defaults(func=self.CmdHelp)
242
243 self.args = parser.parse_args(argv)
244
245 def DumpInputFiles(self):
246
247 def DumpContentsOfFilePassedTo(arg_name, path):
248 if path and self.Exists(path):
249 self.Print("\n# To recreate the file passed to %s:" % arg_name)
250 self.Print("%% cat > %s <<EOF" % path)
251 contents = self.ReadFile(path)
252 self.Print(contents)
253 self.Print("EOF\n%\n")
254
255 if getattr(self.args, 'input_path', None):
256 DumpContentsOfFilePassedTo(
257 'argv[0] (input_path)', self.args.input_path[0])
258 if getattr(self.args, 'swarming_targets_file', None):
259 DumpContentsOfFilePassedTo(
260 '--swarming-targets-file', self.args.swarming_targets_file)
261
262 def CmdAnalyze(self):
263 vals = self.Lookup()
264 self.ClobberIfNeeded(vals)
265 if vals['type'] == 'gn':
266 return self.RunGNAnalyze(vals)
267 else:
268 return self.RunGYPAnalyze(vals)
269
270 def CmdExport(self):
271 self.ReadConfigFile()
272 obj = {}
273 for master, builders in self.masters.items():
274 obj[master] = {}
275 for builder in builders:
276 config = self.masters[master][builder]
277 if not config:
278 continue
279
280 if isinstance(config, dict):
281 args = {k: self.FlattenConfig(v)['gn_args']
282 for k, v in config.items()}
283 elif config.startswith('//'):
284 args = config
285 else:
286 args = self.FlattenConfig(config)['gn_args']
287 if 'error' in args:
288 continue
289
290 obj[master][builder] = args
291
292 # Dump object and trim trailing whitespace.
293 s = '\n'.join(l.rstrip() for l in
294 json.dumps(obj, sort_keys=True, indent=2).splitlines())
295 self.Print(s)
296 return 0
297
298 def CmdGen(self):
299 vals = self.Lookup()
300 self.ClobberIfNeeded(vals)
301 if vals['type'] == 'gn':
302 return self.RunGNGen(vals)
303 else:
304 return self.RunGYPGen(vals)
305
306 def CmdHelp(self):
307 if self.args.subcommand:
308 self.ParseArgs([self.args.subcommand, '--help'])
309 else:
310 self.ParseArgs(['--help'])
311
312 def CmdIsolate(self):
313 vals = self.GetConfig()
314 if not vals:
315 return 1
316
317 if vals['type'] == 'gn':
318 return self.RunGNIsolate(vals)
319 else:
320 return self.Build('%s_run' % self.args.target[0])
321
322 def CmdLookup(self):
323 vals = self.Lookup()
324 if vals['type'] == 'gn':
325 cmd = self.GNCmd('gen', '_path_')
326 gn_args = self.GNArgs(vals)
327 self.Print('\nWriting """\\\n%s""" to _path_/args.gn.\n' % gn_args)
328 env = None
329 else:
330 cmd, env = self.GYPCmd('_path_', vals)
331
332 self.PrintCmd(cmd, env)
333 return 0
334
335 def CmdRun(self):
336 vals = self.GetConfig()
337 if not vals:
338 return 1
339
340 build_dir = self.args.path[0]
341 target = self.args.target[0]
342
343 if vals['type'] == 'gn':
344 if self.args.build:
345 ret = self.Build(target)
346 if ret:
347 return ret
348 ret = self.RunGNIsolate(vals)
349 if ret:
350 return ret
351 else:
352 ret = self.Build('%s_run' % target)
353 if ret:
354 return ret
355
356 cmd = [
357 self.executable,
358 self.PathJoin('tools', 'swarming_client', 'isolate.py'),
359 'run',
360 '-s',
361 self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
362 ]
363 if self.args.extra_args:
364 cmd += ['--'] + self.args.extra_args
365
366 ret, _, _ = self.Run(cmd, force_verbose=False, buffer_output=False)
367
368 return ret
369
370 def CmdValidate(self, print_ok=True):
371 errs = []
372
373 # Read the file to make sure it parses.
374 self.ReadConfigFile()
375
376 # Build a list of all of the configs referenced by builders.
377 all_configs = {}
378 for master in self.masters:
379 for config in self.masters[master].values():
380 if isinstance(config, dict):
381 for c in config.values():
382 all_configs[c] = master
383 else:
384 all_configs[config] = master
385
386 # Check that every referenced args file or config actually exists.
387 for config, loc in all_configs.items():
388 if config.startswith('//'):
389 if not self.Exists(self.ToAbsPath(config)):
390 errs.append('Unknown args file "%s" referenced from "%s".' %
391 (config, loc))
392 elif not config in self.configs:
393 errs.append('Unknown config "%s" referenced from "%s".' %
394 (config, loc))
395
396 # Check that every actual config is actually referenced.
397 for config in self.configs:
398 if not config in all_configs:
399 errs.append('Unused config "%s".' % config)
400
401 # Figure out the whole list of mixins, and check that every mixin
402 # listed by a config or another mixin actually exists.
403 referenced_mixins = set()
404 for config, mixins in self.configs.items():
405 for mixin in mixins:
406 if not mixin in self.mixins:
407 errs.append('Unknown mixin "%s" referenced by config "%s".' %
408 (mixin, config))
409 referenced_mixins.add(mixin)
410
411 for mixin in self.mixins:
412 for sub_mixin in self.mixins[mixin].get('mixins', []):
413 if not sub_mixin in self.mixins:
414 errs.append('Unknown mixin "%s" referenced by mixin "%s".' %
415 (sub_mixin, mixin))
416 referenced_mixins.add(sub_mixin)
417
418 # Check that every mixin defined is actually referenced somewhere.
419 for mixin in self.mixins:
420 if not mixin in referenced_mixins:
421 errs.append('Unreferenced mixin "%s".' % mixin)
422
kjellandera013a022016-11-14 05:54:22 -0800423 if errs:
424 raise MBErr(('mb config file %s has problems:' % self.args.config_file) +
425 '\n ' + '\n '.join(errs))
426
427 if print_ok:
428 self.Print('mb config file %s looks ok.' % self.args.config_file)
429 return 0
430
431 def CmdAudit(self):
432 """Track the progress of the GYP->GN migration on the bots."""
433
434 # First, make sure the config file is okay, but don't print anything
435 # if it is (it will throw an error if it isn't).
436 self.CmdValidate(print_ok=False)
437
438 stats = OrderedDict()
439 STAT_MASTER_ONLY = 'Master only'
440 STAT_CONFIG_ONLY = 'Config only'
441 STAT_TBD = 'Still TBD'
442 STAT_GYP = 'Still GYP'
443 STAT_DONE = 'Done (on GN)'
444 stats[STAT_MASTER_ONLY] = 0
445 stats[STAT_CONFIG_ONLY] = 0
446 stats[STAT_TBD] = 0
447 stats[STAT_GYP] = 0
448 stats[STAT_DONE] = 0
449
450 def PrintBuilders(heading, builders, notes):
451 stats.setdefault(heading, 0)
452 stats[heading] += len(builders)
453 if builders:
454 self.Print(' %s:' % heading)
455 for builder in sorted(builders):
456 self.Print(' %s%s' % (builder, notes[builder]))
457
458 self.ReadConfigFile()
459
460 masters = self.args.master or self.masters
461 for master in sorted(masters):
462 url = self.args.url_template.replace('{master}', master)
463
464 self.Print('Auditing %s' % master)
465
466 MASTERS_TO_SKIP = (
467 'client.skia',
468 'client.v8.fyi',
469 'tryserver.v8',
470 )
471 if master in MASTERS_TO_SKIP:
472 # Skip these bots because converting them is the responsibility of
473 # those teams and out of scope for the Chromium migration to GN.
474 self.Print(' Skipped (out of scope)')
475 self.Print('')
476 continue
477
478 INTERNAL_MASTERS = ('official.desktop', 'official.desktop.continuous',
479 'internal.client.kitchensync')
480 if master in INTERNAL_MASTERS and not self.args.internal:
481 # Skip these because the servers aren't accessible by default ...
482 self.Print(' Skipped (internal)')
483 self.Print('')
484 continue
485
486 try:
487 # Fetch the /builders contents from the buildbot master. The
488 # keys of the dict are the builder names themselves.
489 json_contents = self.Fetch(url)
490 d = json.loads(json_contents)
491 except Exception as e:
492 self.Print(str(e))
493 return 1
494
495 config_builders = set(self.masters[master])
496 master_builders = set(d.keys())
497 both = master_builders & config_builders
498 master_only = master_builders - config_builders
499 config_only = config_builders - master_builders
500 tbd = set()
501 gyp = set()
502 done = set()
503 notes = {builder: '' for builder in config_builders | master_builders}
504
505 for builder in both:
506 config = self.masters[master][builder]
507 if config == 'tbd':
508 tbd.add(builder)
509 elif isinstance(config, dict):
510 vals = self.FlattenConfig(config.values()[0])
511 if vals['type'] == 'gyp':
512 gyp.add(builder)
513 else:
514 done.add(builder)
515 elif config.startswith('//'):
516 done.add(builder)
517 else:
518 vals = self.FlattenConfig(config)
519 if vals['type'] == 'gyp':
520 gyp.add(builder)
521 else:
522 done.add(builder)
523
524 if self.args.check_compile and (tbd or master_only):
525 either = tbd | master_only
526 for builder in either:
527 notes[builder] = ' (' + self.CheckCompile(master, builder) +')'
528
529 if master_only or config_only or tbd or gyp:
530 PrintBuilders(STAT_MASTER_ONLY, master_only, notes)
531 PrintBuilders(STAT_CONFIG_ONLY, config_only, notes)
532 PrintBuilders(STAT_TBD, tbd, notes)
533 PrintBuilders(STAT_GYP, gyp, notes)
534 else:
535 self.Print(' All GN!')
536
537 stats[STAT_DONE] += len(done)
538
539 self.Print('')
540
541 fmt = '{:<27} {:>4}'
542 self.Print(fmt.format('Totals', str(sum(int(v) for v in stats.values()))))
543 self.Print(fmt.format('-' * 27, '----'))
544 for stat, count in stats.items():
545 self.Print(fmt.format(stat, str(count)))
546
547 return 0
548
549 def GetConfig(self):
550 build_dir = self.args.path[0]
551
552 vals = self.DefaultVals()
553 if self.args.builder or self.args.master or self.args.config:
554 vals = self.Lookup()
555 if vals['type'] == 'gn':
556 # Re-run gn gen in order to ensure the config is consistent with the
557 # build dir.
558 self.RunGNGen(vals)
559 return vals
560
561 mb_type_path = self.PathJoin(self.ToAbsPath(build_dir), 'mb_type')
562 if not self.Exists(mb_type_path):
563 toolchain_path = self.PathJoin(self.ToAbsPath(build_dir),
564 'toolchain.ninja')
565 if not self.Exists(toolchain_path):
566 self.Print('Must either specify a path to an existing GN build dir '
567 'or pass in a -m/-b pair or a -c flag to specify the '
568 'configuration')
569 return {}
570 else:
571 mb_type = 'gn'
572 else:
573 mb_type = self.ReadFile(mb_type_path).strip()
574
575 if mb_type == 'gn':
576 vals['gn_args'] = self.GNArgsFromDir(build_dir)
577 vals['type'] = mb_type
578
579 return vals
580
581 def GNArgsFromDir(self, build_dir):
582 args_contents = ""
583 gn_args_path = self.PathJoin(self.ToAbsPath(build_dir), 'args.gn')
584 if self.Exists(gn_args_path):
585 args_contents = self.ReadFile(gn_args_path)
586 gn_args = []
587 for l in args_contents.splitlines():
588 fields = l.split(' ')
589 name = fields[0]
590 val = ' '.join(fields[2:])
591 gn_args.append('%s=%s' % (name, val))
592
593 return ' '.join(gn_args)
594
595 def Lookup(self):
596 vals = self.ReadIOSBotConfig()
597 if not vals:
598 self.ReadConfigFile()
599 config = self.ConfigFromArgs()
600 if config.startswith('//'):
601 if not self.Exists(self.ToAbsPath(config)):
602 raise MBErr('args file "%s" not found' % config)
603 vals = self.DefaultVals()
604 vals['args_file'] = config
605 else:
606 if not config in self.configs:
607 raise MBErr('Config "%s" not found in %s' %
608 (config, self.args.config_file))
609 vals = self.FlattenConfig(config)
610
611 # Do some basic sanity checking on the config so that we
612 # don't have to do this in every caller.
613 if 'type' not in vals:
614 vals['type'] = 'gn'
615 assert vals['type'] in ('gn', 'gyp'), (
616 'Unknown meta-build type "%s"' % vals['gn_args'])
617
618 return vals
619
620 def ReadIOSBotConfig(self):
621 if not self.args.master or not self.args.builder:
622 return {}
623 path = self.PathJoin(self.chromium_src_dir, 'ios', 'build', 'bots',
624 self.args.master, self.args.builder + '.json')
625 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 ],
925 'dir': self.chromium_src_dir,
926 '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
970 gn_path = self.PathJoin(self.chromium_src_dir, 'buildtools', subdir, exe)
971 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:
kjellandera013a022016-11-14 05:54:22 -08001058 logdog_command = [
1059 '--logdog-bin-cmd', './../../bin/logdog_butler',
1060 '--project', 'chromium',
1061 '--service-account-json',
1062 '/creds/service_accounts/service-account-luci-logdog-publisher.json',
1063 '--prefix', 'android/swarming/logcats/${SWARMING_TASK_ID}',
1064 '--source', '${ISOLATED_OUTDIR}/logcats',
1065 '--name', 'unified_logcats',
1066 ]
1067 test_cmdline = [
1068 self.PathJoin('bin', 'run_%s' % target),
1069 '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
kjellandera013a022016-11-14 05:54:22 -08001070 ]
ehmaldonadoe1a13f82016-11-28 07:14:09 -08001071 if test_type != 'junit_test':
1072 test_cmdline += ['--target-devices-file', '${SWARMING_BOT_FILE}',]
kjellandera013a022016-11-14 05:54:22 -08001073 cmdline = (['./../../build/android/test_wrapper/logdog_wrapper.py']
ehmaldonadoe1a13f82016-11-28 07:14:09 -08001074 + logdog_command + test_cmdline + ['-v'])
kjellandera013a022016-11-14 05:54:22 -08001075 else:
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001076 extra_files = ['../../testing/test_env.py']
kjellandera013a022016-11-14 05:54:22 -08001077
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001078 # This needs to mirror the settings in //build/config/ui.gni:
1079 # use_x11 = is_linux && !use_ozone.
1080 use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
1081
1082 xvfb = use_x11 and test_type == 'windowed_test_launcher'
1083 if xvfb:
1084 extra_files += [
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001085 '../../testing/xvfb.py',
1086 ]
1087
1088 # Memcheck is only supported for linux. Ignore in other platforms.
1089 memcheck = is_linux and 'rtc_use_memcheck=true' in vals['gn_args']
1090 memcheck_cmdline = [
1091 'bash',
kjellanderafd54942016-12-17 12:21:39 -08001092 '../../tools-webrtc/valgrind/webrtc_tests.sh',
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001093 '--tool',
1094 'memcheck',
1095 '--target',
1096 'Release',
1097 '--build-dir',
1098 '..',
1099 '--test',
1100 ]
1101
1102 gtest_parallel = (test_type != 'non_parallel_console_test_launcher' and
1103 not memcheck)
1104 gtest_parallel_wrapper = [
1105 '../../third_party/gtest-parallel/gtest-parallel-wrapper.py'
1106 ]
1107 if gtest_parallel:
1108 extra_files += [
1109 '../../third_party/gtest-parallel/gtest-parallel',
1110 '../../third_party/gtest-parallel/gtest-parallel-wrapper.py',
1111 ]
1112
1113 asan = 'is_asan=true' in vals['gn_args']
1114 msan = 'is_msan=true' in vals['gn_args']
1115 tsan = 'is_tsan=true' in vals['gn_args']
1116
1117 executable_prefix = '.\\' if self.platform == 'win32' else './'
1118 executable_suffix = '.exe' if self.platform == 'win32' else ''
1119 executable = executable_prefix + target + executable_suffix
1120
kjellander7e1070d2016-12-08 00:49:03 -08001121 cmdline = (['../../testing/xvfb.py'] if xvfb else
ehmaldonadoed8c8ed2016-11-23 12:58:35 -08001122 ['../../testing/test_env.py'])
1123 if memcheck:
1124 cmdline += memcheck_cmdline
1125 elif gtest_parallel:
1126 cmdline += gtest_parallel_wrapper
1127 cmdline += [
1128 executable,
1129 '--',
1130 '--asan=%d' % asan,
1131 '--msan=%d' % msan,
1132 '--tsan=%d' % tsan,
1133 ]
kjellandera013a022016-11-14 05:54:22 -08001134
1135 return cmdline, extra_files
1136
1137 def ToAbsPath(self, build_path, *comps):
1138 return self.PathJoin(self.chromium_src_dir,
1139 self.ToSrcRelPath(build_path),
1140 *comps)
1141
1142 def ToSrcRelPath(self, path):
1143 """Returns a relative path from the top of the repo."""
1144 if path.startswith('//'):
1145 return path[2:].replace('/', self.sep)
1146 return self.RelPath(path, self.chromium_src_dir)
1147
1148 def ParseGYPConfigPath(self, path):
1149 rpath = self.ToSrcRelPath(path)
1150 output_dir, _, _ = rpath.rpartition(self.sep)
1151 return output_dir
1152
1153 def GYPCmd(self, output_dir, vals):
1154 if vals['cros_passthrough']:
1155 if not 'GYP_DEFINES' in os.environ:
1156 raise MBErr('MB is expecting GYP_DEFINES to be in the environment')
1157 gyp_defines = os.environ['GYP_DEFINES']
1158 if not 'chromeos=1' in gyp_defines:
1159 raise MBErr('GYP_DEFINES is missing chromeos=1: (GYP_DEFINES=%s)' %
1160 gyp_defines)
1161 else:
1162 gyp_defines = vals['gyp_defines']
1163
1164 goma_dir = self.args.goma_dir
1165
1166 # GYP uses shlex.split() to split the gyp defines into separate arguments,
1167 # so we can support backslashes and and spaces in arguments by quoting
1168 # them, even on Windows, where this normally wouldn't work.
1169 if goma_dir and ('\\' in goma_dir or ' ' in goma_dir):
1170 goma_dir = "'%s'" % goma_dir
1171
1172 if goma_dir:
1173 gyp_defines += ' gomadir=%s' % goma_dir
1174
1175 android_version_code = self.args.android_version_code
1176 if android_version_code:
1177 gyp_defines += ' app_manifest_version_code=%s' % android_version_code
1178
1179 android_version_name = self.args.android_version_name
1180 if android_version_name:
1181 gyp_defines += ' app_manifest_version_name=%s' % android_version_name
1182
1183 cmd = [
1184 self.executable,
1185 self.args.gyp_script,
1186 '-G',
1187 'output_dir=' + output_dir,
1188 ]
1189
1190 # Ensure that we have an environment that only contains
1191 # the exact values of the GYP variables we need.
1192 env = os.environ.copy()
1193
1194 # This is a terrible hack to work around the fact that
1195 # //tools/clang/scripts/update.py is invoked by GYP and GN but
1196 # currently relies on an environment variable to figure out
1197 # what revision to embed in the command line #defines.
1198 # For GN, we've made this work via a gn arg that will cause update.py
1199 # to get an additional command line arg, but getting that to work
1200 # via GYP_DEFINES has proven difficult, so we rewrite the GYP_DEFINES
1201 # to get rid of the arg and add the old var in, instead.
1202 # See crbug.com/582737 for more on this. This can hopefully all
1203 # go away with GYP.
1204 m = re.search('llvm_force_head_revision=1\s*', gyp_defines)
1205 if m:
1206 env['LLVM_FORCE_HEAD_REVISION'] = '1'
1207 gyp_defines = gyp_defines.replace(m.group(0), '')
1208
1209 # This is another terrible hack to work around the fact that
1210 # GYP sets the link concurrency to use via the GYP_LINK_CONCURRENCY
1211 # environment variable, and not via a proper GYP_DEFINE. See
1212 # crbug.com/611491 for more on this.
1213 m = re.search('gyp_link_concurrency=(\d+)(\s*)', gyp_defines)
1214 if m:
1215 env['GYP_LINK_CONCURRENCY'] = m.group(1)
1216 gyp_defines = gyp_defines.replace(m.group(0), '')
1217
1218 env['GYP_GENERATORS'] = 'ninja'
1219 if 'GYP_CHROMIUM_NO_ACTION' in env:
1220 del env['GYP_CHROMIUM_NO_ACTION']
1221 if 'GYP_CROSSCOMPILE' in env:
1222 del env['GYP_CROSSCOMPILE']
1223 env['GYP_DEFINES'] = gyp_defines
1224 if vals['gyp_crosscompile']:
1225 env['GYP_CROSSCOMPILE'] = '1'
1226 return cmd, env
1227
1228 def RunGNAnalyze(self, vals):
1229 # Analyze runs before 'gn gen' now, so we need to run gn gen
1230 # in order to ensure that we have a build directory.
1231 ret = self.RunGNGen(vals)
1232 if ret:
1233 return ret
1234
1235 build_path = self.args.path[0]
1236 input_path = self.args.input_path[0]
1237 gn_input_path = input_path + '.gn'
1238 output_path = self.args.output_path[0]
1239 gn_output_path = output_path + '.gn'
1240
1241 inp = self.ReadInputJSON(['files', 'test_targets',
1242 'additional_compile_targets'])
1243 if self.args.verbose:
1244 self.Print()
1245 self.Print('analyze input:')
1246 self.PrintJSON(inp)
1247 self.Print()
1248
1249
1250 # This shouldn't normally happen, but could due to unusual race conditions,
1251 # like a try job that gets scheduled before a patch lands but runs after
1252 # the patch has landed.
1253 if not inp['files']:
1254 self.Print('Warning: No files modified in patch, bailing out early.')
1255 self.WriteJSON({
1256 'status': 'No dependency',
1257 'compile_targets': [],
1258 'test_targets': [],
1259 }, output_path)
1260 return 0
1261
1262 gn_inp = {}
1263 gn_inp['files'] = ['//' + f for f in inp['files'] if not f.startswith('//')]
1264
1265 isolate_map = self.ReadIsolateMap()
1266 err, gn_inp['additional_compile_targets'] = self.MapTargetsToLabels(
1267 isolate_map, inp['additional_compile_targets'])
1268 if err:
1269 raise MBErr(err)
1270
1271 err, gn_inp['test_targets'] = self.MapTargetsToLabels(
1272 isolate_map, inp['test_targets'])
1273 if err:
1274 raise MBErr(err)
1275 labels_to_targets = {}
1276 for i, label in enumerate(gn_inp['test_targets']):
1277 labels_to_targets[label] = inp['test_targets'][i]
1278
1279 try:
1280 self.WriteJSON(gn_inp, gn_input_path)
1281 cmd = self.GNCmd('analyze', build_path, gn_input_path, gn_output_path)
1282 ret, _, _ = self.Run(cmd, force_verbose=True)
1283 if ret:
1284 return ret
1285
1286 gn_outp_str = self.ReadFile(gn_output_path)
1287 try:
1288 gn_outp = json.loads(gn_outp_str)
1289 except Exception as e:
1290 self.Print("Failed to parse the JSON string GN returned: %s\n%s"
1291 % (repr(gn_outp_str), str(e)))
1292 raise
1293
1294 outp = {}
1295 if 'status' in gn_outp:
1296 outp['status'] = gn_outp['status']
1297 if 'error' in gn_outp:
1298 outp['error'] = gn_outp['error']
1299 if 'invalid_targets' in gn_outp:
1300 outp['invalid_targets'] = gn_outp['invalid_targets']
1301 if 'compile_targets' in gn_outp:
1302 if 'all' in gn_outp['compile_targets']:
1303 outp['compile_targets'] = ['all']
1304 else:
1305 outp['compile_targets'] = [
1306 label.replace('//', '') for label in gn_outp['compile_targets']]
1307 if 'test_targets' in gn_outp:
1308 outp['test_targets'] = [
1309 labels_to_targets[label] for label in gn_outp['test_targets']]
1310
1311 if self.args.verbose:
1312 self.Print()
1313 self.Print('analyze output:')
1314 self.PrintJSON(outp)
1315 self.Print()
1316
1317 self.WriteJSON(outp, output_path)
1318
1319 finally:
1320 if self.Exists(gn_input_path):
1321 self.RemoveFile(gn_input_path)
1322 if self.Exists(gn_output_path):
1323 self.RemoveFile(gn_output_path)
1324
1325 return 0
1326
1327 def ReadInputJSON(self, required_keys):
1328 path = self.args.input_path[0]
1329 output_path = self.args.output_path[0]
1330 if not self.Exists(path):
1331 self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1332
1333 try:
1334 inp = json.loads(self.ReadFile(path))
1335 except Exception as e:
1336 self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1337 (path, e), output_path)
1338
1339 for k in required_keys:
1340 if not k in inp:
1341 self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1342 output_path)
1343
1344 return inp
1345
1346 def WriteFailureAndRaise(self, msg, output_path):
1347 if output_path:
1348 self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1349 raise MBErr(msg)
1350
1351 def WriteJSON(self, obj, path, force_verbose=False):
1352 try:
1353 self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1354 force_verbose=force_verbose)
1355 except Exception as e:
1356 raise MBErr('Error %s writing to the output path "%s"' %
1357 (e, path))
1358
1359 def CheckCompile(self, master, builder):
1360 url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1361 url = urllib2.quote(url_template.format(master=master, builder=builder),
1362 safe=':/()?=')
1363 try:
1364 builds = json.loads(self.Fetch(url))
1365 except Exception as e:
1366 return str(e)
1367 successes = sorted(
1368 [int(x) for x in builds.keys() if "text" in builds[x] and
1369 cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1370 reverse=True)
1371 if not successes:
1372 return "no successful builds"
1373 build = builds[str(successes[0])]
1374 step_names = set([step["name"] for step in build["steps"]])
1375 compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1376 if compile_indicators & step_names:
1377 return "compiles"
1378 return "does not compile"
1379
1380 def PrintCmd(self, cmd, env):
1381 if self.platform == 'win32':
1382 env_prefix = 'set '
1383 env_quoter = QuoteForSet
1384 shell_quoter = QuoteForCmd
1385 else:
1386 env_prefix = ''
1387 env_quoter = pipes.quote
1388 shell_quoter = pipes.quote
1389
1390 def print_env(var):
1391 if env and var in env:
1392 self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1393
1394 print_env('GYP_CROSSCOMPILE')
1395 print_env('GYP_DEFINES')
1396 print_env('GYP_LINK_CONCURRENCY')
1397 print_env('LLVM_FORCE_HEAD_REVISION')
1398
1399 if cmd[0] == self.executable:
1400 cmd = ['python'] + cmd[1:]
1401 self.Print(*[shell_quoter(arg) for arg in cmd])
1402
1403 def PrintJSON(self, obj):
1404 self.Print(json.dumps(obj, indent=2, sort_keys=True))
1405
1406 def Build(self, target):
1407 build_dir = self.ToSrcRelPath(self.args.path[0])
1408 ninja_cmd = ['ninja', '-C', build_dir]
1409 if self.args.jobs:
1410 ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1411 ninja_cmd.append(target)
1412 ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1413 return ret
1414
1415 def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1416 # This function largely exists so it can be overridden for testing.
1417 if self.args.dryrun or self.args.verbose or force_verbose:
1418 self.PrintCmd(cmd, env)
1419 if self.args.dryrun:
1420 return 0, '', ''
1421
1422 ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1423 if self.args.verbose or force_verbose:
1424 if ret:
1425 self.Print(' -> returned %d' % ret)
1426 if out:
1427 self.Print(out, end='')
1428 if err:
1429 self.Print(err, end='', file=sys.stderr)
1430 return ret, out, err
1431
1432 def Call(self, cmd, env=None, buffer_output=True):
1433 if buffer_output:
1434 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
1435 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1436 env=env)
1437 out, err = p.communicate()
1438 else:
1439 p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
1440 env=env)
1441 p.wait()
1442 out = err = ''
1443 return p.returncode, out, err
1444
1445 def ExpandUser(self, path):
1446 # This function largely exists so it can be overridden for testing.
1447 return os.path.expanduser(path)
1448
1449 def Exists(self, path):
1450 # This function largely exists so it can be overridden for testing.
1451 return os.path.exists(path)
1452
1453 def Fetch(self, url):
1454 # This function largely exists so it can be overridden for testing.
1455 f = urllib2.urlopen(url)
1456 contents = f.read()
1457 f.close()
1458 return contents
1459
1460 def MaybeMakeDirectory(self, path):
1461 try:
1462 os.makedirs(path)
1463 except OSError, e:
1464 if e.errno != errno.EEXIST:
1465 raise
1466
1467 def PathJoin(self, *comps):
1468 # This function largely exists so it can be overriden for testing.
1469 return os.path.join(*comps)
1470
1471 def Print(self, *args, **kwargs):
1472 # This function largely exists so it can be overridden for testing.
1473 print(*args, **kwargs)
1474 if kwargs.get('stream', sys.stdout) == sys.stdout:
1475 sys.stdout.flush()
1476
1477 def ReadFile(self, path):
1478 # This function largely exists so it can be overriden for testing.
1479 with open(path) as fp:
1480 return fp.read()
1481
1482 def RelPath(self, path, start='.'):
1483 # This function largely exists so it can be overriden for testing.
1484 return os.path.relpath(path, start)
1485
1486 def RemoveFile(self, path):
1487 # This function largely exists so it can be overriden for testing.
1488 os.remove(path)
1489
1490 def RemoveDirectory(self, abs_path):
1491 if self.platform == 'win32':
1492 # In other places in chromium, we often have to retry this command
1493 # because we're worried about other processes still holding on to
1494 # file handles, but when MB is invoked, it will be early enough in the
1495 # build that their should be no other processes to interfere. We
1496 # can change this if need be.
1497 self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1498 else:
1499 shutil.rmtree(abs_path, ignore_errors=True)
1500
1501 def TempFile(self, mode='w'):
1502 # This function largely exists so it can be overriden for testing.
1503 return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1504
1505 def WriteFile(self, path, contents, force_verbose=False):
1506 # This function largely exists so it can be overriden for testing.
1507 if self.args.dryrun or self.args.verbose or force_verbose:
1508 self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1509 with open(path, 'w') as fp:
1510 return fp.write(contents)
1511
1512
1513class MBErr(Exception):
1514 pass
1515
1516
1517# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1518# details of this next section, which handles escaping command lines
1519# so that they can be copied and pasted into a cmd window.
1520UNSAFE_FOR_SET = set('^<>&|')
1521UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1522ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1523
1524
1525def QuoteForSet(arg):
1526 if any(a in UNSAFE_FOR_SET for a in arg):
1527 arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1528 return arg
1529
1530
1531def QuoteForCmd(arg):
1532 # First, escape the arg so that CommandLineToArgvW will parse it properly.
1533 # From //tools/gyp/pylib/gyp/msvs_emulation.py:23.
1534 if arg == '' or ' ' in arg or '"' in arg:
1535 quote_re = re.compile(r'(\\*)"')
1536 arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1537
1538 # Then check to see if the arg contains any metacharacters other than
1539 # double quotes; if it does, quote everything (including the double
1540 # quotes) for safety.
1541 if any(a in UNSAFE_FOR_CMD for a in arg):
1542 arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1543 return arg
1544
1545
1546if __name__ == '__main__':
1547 sys.exit(main(sys.argv[1:]))