Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(26)

Side by Side Diff: tools/mb/mb.py

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

Powered by Google App Engine
This is Rietveld 408576698