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