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