• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
9MB is a wrapper script for GYP and GN that can be used to generate build files
10for sets of canned configurations and analyze them.
11"""
12
13from __future__ import print_function
14
15import argparse
16import ast
17import errno
18import json
19import os
20import pipes
21import pprint
22import re
23import shutil
24import sys
25import subprocess
26import tempfile
27import traceback
28import urllib2
29
30from collections import OrderedDict
31
32CHROMIUM_SRC_DIR = os.path.dirname(os.path.dirname(os.path.dirname(
33    os.path.abspath(__file__))))
34sys.path = [os.path.join(CHROMIUM_SRC_DIR, 'build')] + sys.path
35
36import gn_helpers
37
38
39def main(args):
40  mbw = MetaBuildWrapper()
41  return mbw.Main(args)
42
43
44class 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    # Override msvs infra environment variables.
781    # TODO(machenbach): Remove after GYP_MSVS_VERSION is removed on infra side.
782    env = {}
783    env.update(os.environ)
784    env['GYP_MSVS_VERSION'] = '2015'
785
786    ret, _, _ = self.Run(cmd, env=env)
787    if ret:
788        # If `gn gen` failed, we should exit early rather than trying to
789        # generate isolates. Run() will have already logged any error output.
790        self.Print('GN gen failed: %d' % ret)
791        return ret
792
793    android = 'target_os="android"' in vals['gn_args']
794    for target in swarming_targets:
795      if android:
796        # Android targets may be either android_apk or executable. The former
797        # will result in runtime_deps associated with the stamp file, while the
798        # latter will result in runtime_deps associated with the executable.
799        target_name = self.GNTargetName(target)
800        label = gn_isolate_map[target_name]['label']
801        runtime_deps_targets = [
802            target_name + '.runtime_deps',
803            'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
804      elif gn_isolate_map[target]['type'] == 'gpu_browser_test':
805        if self.platform == 'win32':
806          runtime_deps_targets = ['browser_tests.exe.runtime_deps']
807        else:
808          runtime_deps_targets = ['browser_tests.runtime_deps']
809      elif (gn_isolate_map[target]['type'] == 'script' or
810            gn_isolate_map[target].get('label_type') == 'group'):
811        # For script targets, the build target is usually a group,
812        # for which gn generates the runtime_deps next to the stamp file
813        # for the label, which lives under the obj/ directory.
814        label = gn_isolate_map[target]['label']
815        runtime_deps_targets = [
816            'obj/%s.stamp.runtime_deps' % label.replace(':', '/')]
817      elif self.platform == 'win32':
818        runtime_deps_targets = [target + '.exe.runtime_deps']
819      else:
820        runtime_deps_targets = [target + '.runtime_deps']
821
822      for r in runtime_deps_targets:
823        runtime_deps_path = self.ToAbsPath(build_dir, r)
824        if self.Exists(runtime_deps_path):
825          break
826      else:
827        raise MBErr('did not generate any of %s' %
828                    ', '.join(runtime_deps_targets))
829
830      command, extra_files = self.GetIsolateCommand(target, vals,
831                                                    gn_isolate_map)
832
833      runtime_deps = self.ReadFile(runtime_deps_path).splitlines()
834
835      self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
836                             extra_files)
837
838    return 0
839
840  def RunGNIsolate(self, vals):
841    gn_isolate_map = ast.literal_eval(self.ReadFile(self.PathJoin(
842        self.chromium_src_dir, 'testing', 'buildbot', 'gn_isolate_map.pyl')))
843
844    build_dir = self.args.path[0]
845    target = self.args.target[0]
846    target_name = self.GNTargetName(target)
847    command, extra_files = self.GetIsolateCommand(target, vals, gn_isolate_map)
848
849    label = gn_isolate_map[target_name]['label']
850    cmd = self.GNCmd('desc', build_dir, label, 'runtime_deps')
851    ret, out, _ = self.Call(cmd)
852    if ret:
853      if out:
854        self.Print(out)
855      return ret
856
857    runtime_deps = out.splitlines()
858
859    self.WriteIsolateFiles(build_dir, command, target, runtime_deps,
860                           extra_files)
861
862    ret, _, _ = self.Run([
863        self.executable,
864        self.PathJoin('tools', 'swarming_client', 'isolate.py'),
865        'check',
866        '-i',
867        self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
868        '-s',
869        self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target))],
870        buffer_output=False)
871
872    return ret
873
874  def WriteIsolateFiles(self, build_dir, command, target, runtime_deps,
875                        extra_files):
876    isolate_path = self.ToAbsPath(build_dir, target + '.isolate')
877    self.WriteFile(isolate_path,
878      pprint.pformat({
879        'variables': {
880          'command': command,
881          'files': sorted(runtime_deps + extra_files),
882        }
883      }) + '\n')
884
885    self.WriteJSON(
886      {
887        'args': [
888          '--isolated',
889          self.ToSrcRelPath('%s/%s.isolated' % (build_dir, target)),
890          '--isolate',
891          self.ToSrcRelPath('%s/%s.isolate' % (build_dir, target)),
892        ],
893        'dir': self.chromium_src_dir,
894        'version': 1,
895      },
896      isolate_path + 'd.gen.json',
897    )
898
899  def GNCmd(self, subcommand, path, *args):
900    if self.platform == 'linux2':
901      subdir, exe = 'linux64', 'gn'
902    elif self.platform == 'darwin':
903      subdir, exe = 'mac', 'gn'
904    else:
905      subdir, exe = 'win', 'gn.exe'
906
907    gn_path = self.PathJoin(self.chromium_src_dir, 'buildtools', subdir, exe)
908
909    return [gn_path, subcommand, path] + list(args)
910
911  def GNArgs(self, vals):
912    if vals['cros_passthrough']:
913      if not 'GN_ARGS' in os.environ:
914        raise MBErr('MB is expecting GN_ARGS to be in the environment')
915      gn_args = os.environ['GN_ARGS']
916      if not re.search('target_os.*=.*"chromeos"', gn_args):
917        raise MBErr('GN_ARGS is missing target_os = "chromeos": (GN_ARGS=%s)' %
918                    gn_args)
919    else:
920      gn_args = vals['gn_args']
921
922    if self.args.goma_dir:
923      gn_args += ' goma_dir="%s"' % self.args.goma_dir
924
925    android_version_code = self.args.android_version_code
926    if android_version_code:
927      gn_args += ' android_default_version_code="%s"' % android_version_code
928
929    android_version_name = self.args.android_version_name
930    if android_version_name:
931      gn_args += ' android_default_version_name="%s"' % android_version_name
932
933    # Canonicalize the arg string into a sorted, newline-separated list
934    # of key-value pairs, and de-dup the keys if need be so that only
935    # the last instance of each arg is listed.
936    gn_args = gn_helpers.ToGNString(gn_helpers.FromGNArgs(gn_args))
937
938    args_file = vals.get('args_file', None)
939    if args_file:
940      gn_args = ('import("%s")\n' % vals['args_file']) + gn_args
941    return gn_args
942
943  def RunGYPGen(self, vals):
944    path = self.args.path[0]
945
946    output_dir = self.ParseGYPConfigPath(path)
947    cmd, env = self.GYPCmd(output_dir, vals)
948    ret, _, _ = self.Run(cmd, env=env)
949    return ret
950
951  def RunGYPAnalyze(self, vals):
952    output_dir = self.ParseGYPConfigPath(self.args.path[0])
953    if self.args.verbose:
954      inp = self.ReadInputJSON(['files', 'test_targets',
955                                'additional_compile_targets'])
956      self.Print()
957      self.Print('analyze input:')
958      self.PrintJSON(inp)
959      self.Print()
960
961    cmd, env = self.GYPCmd(output_dir, vals)
962    cmd.extend(['-f', 'analyzer',
963                '-G', 'config_path=%s' % self.args.input_path[0],
964                '-G', 'analyzer_output_path=%s' % self.args.output_path[0]])
965    ret, _, _ = self.Run(cmd, env=env)
966    if not ret and self.args.verbose:
967      outp = json.loads(self.ReadFile(self.args.output_path[0]))
968      self.Print()
969      self.Print('analyze output:')
970      self.PrintJSON(outp)
971      self.Print()
972
973    return ret
974
975  def GetIsolateCommand(self, target, vals, gn_isolate_map):
976    android = 'target_os="android"' in vals['gn_args']
977
978    # This needs to mirror the settings in //build/config/ui.gni:
979    # use_x11 = is_linux && !use_ozone.
980    use_x11 = (self.platform == 'linux2' and
981               not android and
982               not 'use_ozone=true' in vals['gn_args'])
983
984    asan = 'is_asan=true' in vals['gn_args']
985    msan = 'is_msan=true' in vals['gn_args']
986    tsan = 'is_tsan=true' in vals['gn_args']
987
988    target_name = self.GNTargetName(target)
989    test_type = gn_isolate_map[target_name]['type']
990
991    executable = gn_isolate_map[target_name].get('executable', target_name)
992    executable_suffix = '.exe' if self.platform == 'win32' else ''
993
994    cmdline = []
995    extra_files = []
996
997    if android and test_type != "script":
998      logdog_command = [
999          '--logdog-bin-cmd', './../../bin/logdog_butler',
1000          '--project', 'chromium',
1001          '--service-account-json',
1002          '/creds/service_accounts/service-account-luci-logdog-publisher.json',
1003          '--prefix', 'android/swarming/logcats/${SWARMING_TASK_ID}',
1004          '--source', '${ISOLATED_OUTDIR}/logcats',
1005          '--name', 'unified_logcats',
1006      ]
1007      test_cmdline = [
1008          self.PathJoin('bin', 'run_%s' % target_name),
1009          '--logcat-output-file', '${ISOLATED_OUTDIR}/logcats',
1010          '--target-devices-file', '${SWARMING_BOT_FILE}',
1011          '-v'
1012      ]
1013      cmdline = (['./../../build/android/test_wrapper/logdog_wrapper.py']
1014                 + logdog_command + test_cmdline)
1015    elif use_x11 and test_type == 'windowed_test_launcher':
1016      extra_files = [
1017          '../../testing/test_env.py',
1018          '../../testing/xvfb.py',
1019      ]
1020      cmdline = [
1021        '../../testing/xvfb.py',
1022        '.',
1023        './' + str(executable) + executable_suffix,
1024        '--brave-new-test-launcher',
1025        '--test-launcher-bot-mode',
1026        '--asan=%d' % asan,
1027        '--msan=%d' % msan,
1028        '--tsan=%d' % tsan,
1029      ]
1030    elif test_type in ('windowed_test_launcher', 'console_test_launcher'):
1031      extra_files = [
1032          '../../testing/test_env.py'
1033      ]
1034      cmdline = [
1035          '../../testing/test_env.py',
1036          './' + str(executable) + executable_suffix,
1037          '--brave-new-test-launcher',
1038          '--test-launcher-bot-mode',
1039          '--asan=%d' % asan,
1040          '--msan=%d' % msan,
1041          '--tsan=%d' % tsan,
1042      ]
1043    elif test_type == 'gpu_browser_test':
1044      extra_files = [
1045          '../../testing/test_env.py'
1046      ]
1047      gtest_filter = gn_isolate_map[target]['gtest_filter']
1048      cmdline = [
1049          '../../testing/test_env.py',
1050          './browser_tests' + executable_suffix,
1051          '--test-launcher-bot-mode',
1052          '--enable-gpu',
1053          '--test-launcher-jobs=1',
1054          '--gtest_filter=%s' % gtest_filter,
1055      ]
1056    elif test_type == 'script':
1057      extra_files = [
1058          '../../testing/test_env.py'
1059      ]
1060      cmdline = [
1061          '../../testing/test_env.py',
1062          '../../' + self.ToSrcRelPath(gn_isolate_map[target]['script'])
1063      ]
1064    elif test_type in ('raw'):
1065      extra_files = []
1066      cmdline = [
1067          './' + str(target) + executable_suffix,
1068      ]
1069
1070    else:
1071      self.WriteFailureAndRaise('No command line for %s found (test type %s).'
1072                                % (target, test_type), output_path=None)
1073
1074    cmdline += gn_isolate_map[target_name].get('args', [])
1075
1076    return cmdline, extra_files
1077
1078  def ToAbsPath(self, build_path, *comps):
1079    return self.PathJoin(self.chromium_src_dir,
1080                         self.ToSrcRelPath(build_path),
1081                         *comps)
1082
1083  def ToSrcRelPath(self, path):
1084    """Returns a relative path from the top of the repo."""
1085    if path.startswith('//'):
1086      return path[2:].replace('/', self.sep)
1087    return self.RelPath(path, self.chromium_src_dir)
1088
1089  def ParseGYPConfigPath(self, path):
1090    rpath = self.ToSrcRelPath(path)
1091    output_dir, _, _ = rpath.rpartition(self.sep)
1092    return output_dir
1093
1094  def GYPCmd(self, output_dir, vals):
1095    if vals['cros_passthrough']:
1096      if not 'GYP_DEFINES' in os.environ:
1097        raise MBErr('MB is expecting GYP_DEFINES to be in the environment')
1098      gyp_defines = os.environ['GYP_DEFINES']
1099      if not 'chromeos=1' in gyp_defines:
1100        raise MBErr('GYP_DEFINES is missing chromeos=1: (GYP_DEFINES=%s)' %
1101                    gyp_defines)
1102    else:
1103      gyp_defines = vals['gyp_defines']
1104
1105    goma_dir = self.args.goma_dir
1106
1107    # GYP uses shlex.split() to split the gyp defines into separate arguments,
1108    # so we can support backslashes and and spaces in arguments by quoting
1109    # them, even on Windows, where this normally wouldn't work.
1110    if goma_dir and ('\\' in goma_dir or ' ' in goma_dir):
1111      goma_dir = "'%s'" % goma_dir
1112
1113    if goma_dir:
1114      gyp_defines += ' gomadir=%s' % goma_dir
1115
1116    android_version_code = self.args.android_version_code
1117    if android_version_code:
1118      gyp_defines += ' app_manifest_version_code=%s' % android_version_code
1119
1120    android_version_name = self.args.android_version_name
1121    if android_version_name:
1122      gyp_defines += ' app_manifest_version_name=%s' % android_version_name
1123
1124    cmd = [
1125        self.executable,
1126        self.args.gyp_script,
1127        '-G',
1128        'output_dir=' + output_dir,
1129    ]
1130
1131    # Ensure that we have an environment that only contains
1132    # the exact values of the GYP variables we need.
1133    env = os.environ.copy()
1134
1135    # This is a terrible hack to work around the fact that
1136    # //tools/clang/scripts/update.py is invoked by GYP and GN but
1137    # currently relies on an environment variable to figure out
1138    # what revision to embed in the command line #defines.
1139    # For GN, we've made this work via a gn arg that will cause update.py
1140    # to get an additional command line arg, but getting that to work
1141    # via GYP_DEFINES has proven difficult, so we rewrite the GYP_DEFINES
1142    # to get rid of the arg and add the old var in, instead.
1143    # See crbug.com/582737 for more on this. This can hopefully all
1144    # go away with GYP.
1145    m = re.search('llvm_force_head_revision=1\s*', gyp_defines)
1146    if m:
1147      env['LLVM_FORCE_HEAD_REVISION'] = '1'
1148      gyp_defines = gyp_defines.replace(m.group(0), '')
1149
1150    # This is another terrible hack to work around the fact that
1151    # GYP sets the link concurrency to use via the GYP_LINK_CONCURRENCY
1152    # environment variable, and not via a proper GYP_DEFINE. See
1153    # crbug.com/611491 for more on this.
1154    m = re.search('gyp_link_concurrency=(\d+)(\s*)', gyp_defines)
1155    if m:
1156      env['GYP_LINK_CONCURRENCY'] = m.group(1)
1157      gyp_defines = gyp_defines.replace(m.group(0), '')
1158
1159    env['GYP_GENERATORS'] = 'ninja'
1160    if 'GYP_CHROMIUM_NO_ACTION' in env:
1161      del env['GYP_CHROMIUM_NO_ACTION']
1162    if 'GYP_CROSSCOMPILE' in env:
1163      del env['GYP_CROSSCOMPILE']
1164    env['GYP_DEFINES'] = gyp_defines
1165    if vals['gyp_crosscompile']:
1166      env['GYP_CROSSCOMPILE'] = '1'
1167    return cmd, env
1168
1169  def RunGNAnalyze(self, vals):
1170    # analyze runs before 'gn gen' now, so we need to run gn gen
1171    # in order to ensure that we have a build directory.
1172    ret = self.RunGNGen(vals)
1173    if ret:
1174      return ret
1175
1176    inp = self.ReadInputJSON(['files', 'test_targets',
1177                              'additional_compile_targets'])
1178    if self.args.verbose:
1179      self.Print()
1180      self.Print('analyze input:')
1181      self.PrintJSON(inp)
1182      self.Print()
1183
1184    # TODO(crbug.com/555273) - currently GN treats targets and
1185    # additional_compile_targets identically since we can't tell the
1186    # difference between a target that is a group in GN and one that isn't.
1187    # We should eventually fix this and treat the two types differently.
1188    targets = (set(inp['test_targets']) |
1189               set(inp['additional_compile_targets']))
1190
1191    output_path = self.args.output_path[0]
1192
1193    # Bail out early if a GN file was modified, since 'gn refs' won't know
1194    # what to do about it. Also, bail out early if 'all' was asked for,
1195    # since we can't deal with it yet.
1196    if (any(f.endswith('.gn') or f.endswith('.gni') for f in inp['files']) or
1197        'all' in targets):
1198      self.WriteJSON({
1199            'status': 'Found dependency (all)',
1200            'compile_targets': sorted(targets),
1201            'test_targets': sorted(targets & set(inp['test_targets'])),
1202          }, output_path)
1203      return 0
1204
1205    # This shouldn't normally happen, but could due to unusual race conditions,
1206    # like a try job that gets scheduled before a patch lands but runs after
1207    # the patch has landed.
1208    if not inp['files']:
1209      self.Print('Warning: No files modified in patch, bailing out early.')
1210      self.WriteJSON({
1211            'status': 'No dependency',
1212            'compile_targets': [],
1213            'test_targets': [],
1214          }, output_path)
1215      return 0
1216
1217    ret = 0
1218    response_file = self.TempFile()
1219    response_file.write('\n'.join(inp['files']) + '\n')
1220    response_file.close()
1221
1222    matching_targets = set()
1223    try:
1224      cmd = self.GNCmd('refs',
1225                       self.args.path[0],
1226                       '@%s' % response_file.name,
1227                       '--all',
1228                       '--as=output')
1229      ret, out, _ = self.Run(cmd, force_verbose=False)
1230      if ret and not 'The input matches no targets' in out:
1231        self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
1232                                  output_path)
1233      build_dir = self.ToSrcRelPath(self.args.path[0]) + self.sep
1234      for output in out.splitlines():
1235        build_output = output.replace(build_dir, '')
1236        if build_output in targets:
1237          matching_targets.add(build_output)
1238
1239      cmd = self.GNCmd('refs',
1240                       self.args.path[0],
1241                       '@%s' % response_file.name,
1242                       '--all')
1243      ret, out, _ = self.Run(cmd, force_verbose=False)
1244      if ret and not 'The input matches no targets' in out:
1245        self.WriteFailureAndRaise('gn refs returned %d: %s' % (ret, out),
1246                                  output_path)
1247      for label in out.splitlines():
1248        build_target = label[2:]
1249        # We want to accept 'chrome/android:chrome_public_apk' and
1250        # just 'chrome_public_apk'. This may result in too many targets
1251        # getting built, but we can adjust that later if need be.
1252        for input_target in targets:
1253          if (input_target == build_target or
1254              build_target.endswith(':' + input_target)):
1255            matching_targets.add(input_target)
1256    finally:
1257      self.RemoveFile(response_file.name)
1258
1259    if matching_targets:
1260      self.WriteJSON({
1261            'status': 'Found dependency',
1262            'compile_targets': sorted(matching_targets),
1263            'test_targets': sorted(matching_targets &
1264                                   set(inp['test_targets'])),
1265          }, output_path)
1266    else:
1267      self.WriteJSON({
1268          'status': 'No dependency',
1269          'compile_targets': [],
1270          'test_targets': [],
1271      }, output_path)
1272
1273    if self.args.verbose:
1274      outp = json.loads(self.ReadFile(output_path))
1275      self.Print()
1276      self.Print('analyze output:')
1277      self.PrintJSON(outp)
1278      self.Print()
1279
1280    return 0
1281
1282  def ReadInputJSON(self, required_keys):
1283    path = self.args.input_path[0]
1284    output_path = self.args.output_path[0]
1285    if not self.Exists(path):
1286      self.WriteFailureAndRaise('"%s" does not exist' % path, output_path)
1287
1288    try:
1289      inp = json.loads(self.ReadFile(path))
1290    except Exception as e:
1291      self.WriteFailureAndRaise('Failed to read JSON input from "%s": %s' %
1292                                (path, e), output_path)
1293
1294    for k in required_keys:
1295      if not k in inp:
1296        self.WriteFailureAndRaise('input file is missing a "%s" key' % k,
1297                                  output_path)
1298
1299    return inp
1300
1301  def WriteFailureAndRaise(self, msg, output_path):
1302    if output_path:
1303      self.WriteJSON({'error': msg}, output_path, force_verbose=True)
1304    raise MBErr(msg)
1305
1306  def WriteJSON(self, obj, path, force_verbose=False):
1307    try:
1308      self.WriteFile(path, json.dumps(obj, indent=2, sort_keys=True) + '\n',
1309                     force_verbose=force_verbose)
1310    except Exception as e:
1311      raise MBErr('Error %s writing to the output path "%s"' %
1312                 (e, path))
1313
1314  def CheckCompile(self, master, builder):
1315    url_template = self.args.url_template + '/{builder}/builds/_all?as_text=1'
1316    url = urllib2.quote(url_template.format(master=master, builder=builder),
1317                        safe=':/()?=')
1318    try:
1319      builds = json.loads(self.Fetch(url))
1320    except Exception as e:
1321      return str(e)
1322    successes = sorted(
1323        [int(x) for x in builds.keys() if "text" in builds[x] and
1324          cmp(builds[x]["text"][:2], ["build", "successful"]) == 0],
1325        reverse=True)
1326    if not successes:
1327      return "no successful builds"
1328    build = builds[str(successes[0])]
1329    step_names = set([step["name"] for step in build["steps"]])
1330    compile_indicators = set(["compile", "compile (with patch)", "analyze"])
1331    if compile_indicators & step_names:
1332      return "compiles"
1333    return "does not compile"
1334
1335  def PrintCmd(self, cmd, env):
1336    if self.platform == 'win32':
1337      env_prefix = 'set '
1338      env_quoter = QuoteForSet
1339      shell_quoter = QuoteForCmd
1340    else:
1341      env_prefix = ''
1342      env_quoter = pipes.quote
1343      shell_quoter = pipes.quote
1344
1345    def print_env(var):
1346      if env and var in env:
1347        self.Print('%s%s=%s' % (env_prefix, var, env_quoter(env[var])))
1348
1349    print_env('GYP_CROSSCOMPILE')
1350    print_env('GYP_DEFINES')
1351    print_env('GYP_LINK_CONCURRENCY')
1352    print_env('LLVM_FORCE_HEAD_REVISION')
1353
1354    if cmd[0] == self.executable:
1355      cmd = ['python'] + cmd[1:]
1356    self.Print(*[shell_quoter(arg) for arg in cmd])
1357
1358  def PrintJSON(self, obj):
1359    self.Print(json.dumps(obj, indent=2, sort_keys=True))
1360
1361  def GNTargetName(self, target):
1362    return target
1363
1364  def Build(self, target):
1365    build_dir = self.ToSrcRelPath(self.args.path[0])
1366    ninja_cmd = ['ninja', '-C', build_dir]
1367    if self.args.jobs:
1368      ninja_cmd.extend(['-j', '%d' % self.args.jobs])
1369    ninja_cmd.append(target)
1370    ret, _, _ = self.Run(ninja_cmd, force_verbose=False, buffer_output=False)
1371    return ret
1372
1373  def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
1374    # This function largely exists so it can be overridden for testing.
1375    if self.args.dryrun or self.args.verbose or force_verbose:
1376      self.PrintCmd(cmd, env)
1377    if self.args.dryrun:
1378      return 0, '', ''
1379
1380    ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
1381    if self.args.verbose or force_verbose:
1382      if ret:
1383        self.Print('  -> returned %d' % ret)
1384      if out:
1385        self.Print(out, end='')
1386      if err:
1387        self.Print(err, end='', file=sys.stderr)
1388    return ret, out, err
1389
1390  def Call(self, cmd, env=None, buffer_output=True):
1391    if buffer_output:
1392      p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
1393                           stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1394                           env=env)
1395      out, err = p.communicate()
1396    else:
1397      p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
1398                           env=env)
1399      p.wait()
1400      out = err = ''
1401    return p.returncode, out, err
1402
1403  def ExpandUser(self, path):
1404    # This function largely exists so it can be overridden for testing.
1405    return os.path.expanduser(path)
1406
1407  def Exists(self, path):
1408    # This function largely exists so it can be overridden for testing.
1409    return os.path.exists(path)
1410
1411  def Fetch(self, url):
1412    # This function largely exists so it can be overridden for testing.
1413    f = urllib2.urlopen(url)
1414    contents = f.read()
1415    f.close()
1416    return contents
1417
1418  def MaybeMakeDirectory(self, path):
1419    try:
1420      os.makedirs(path)
1421    except OSError, e:
1422      if e.errno != errno.EEXIST:
1423        raise
1424
1425  def PathJoin(self, *comps):
1426    # This function largely exists so it can be overriden for testing.
1427    return os.path.join(*comps)
1428
1429  def Print(self, *args, **kwargs):
1430    # This function largely exists so it can be overridden for testing.
1431    print(*args, **kwargs)
1432    if kwargs.get('stream', sys.stdout) == sys.stdout:
1433      sys.stdout.flush()
1434
1435  def ReadFile(self, path):
1436    # This function largely exists so it can be overriden for testing.
1437    with open(path) as fp:
1438      return fp.read()
1439
1440  def RelPath(self, path, start='.'):
1441    # This function largely exists so it can be overriden for testing.
1442    return os.path.relpath(path, start)
1443
1444  def RemoveFile(self, path):
1445    # This function largely exists so it can be overriden for testing.
1446    os.remove(path)
1447
1448  def RemoveDirectory(self, abs_path):
1449    if self.platform == 'win32':
1450      # In other places in chromium, we often have to retry this command
1451      # because we're worried about other processes still holding on to
1452      # file handles, but when MB is invoked, it will be early enough in the
1453      # build that their should be no other processes to interfere. We
1454      # can change this if need be.
1455      self.Run(['cmd.exe', '/c', 'rmdir', '/q', '/s', abs_path])
1456    else:
1457      shutil.rmtree(abs_path, ignore_errors=True)
1458
1459  def TempFile(self, mode='w'):
1460    # This function largely exists so it can be overriden for testing.
1461    return tempfile.NamedTemporaryFile(mode=mode, delete=False)
1462
1463  def WriteFile(self, path, contents, force_verbose=False):
1464    # This function largely exists so it can be overriden for testing.
1465    if self.args.dryrun or self.args.verbose or force_verbose:
1466      self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
1467    with open(path, 'w') as fp:
1468      return fp.write(contents)
1469
1470
1471class MBErr(Exception):
1472  pass
1473
1474
1475# See http://goo.gl/l5NPDW and http://goo.gl/4Diozm for the painful
1476# details of this next section, which handles escaping command lines
1477# so that they can be copied and pasted into a cmd window.
1478UNSAFE_FOR_SET = set('^<>&|')
1479UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
1480ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
1481
1482
1483def QuoteForSet(arg):
1484  if any(a in UNSAFE_FOR_SET for a in arg):
1485    arg = ''.join('^' + a if a in UNSAFE_FOR_SET else a for a in arg)
1486  return arg
1487
1488
1489def QuoteForCmd(arg):
1490  # First, escape the arg so that CommandLineToArgvW will parse it properly.
1491  # From //tools/gyp/pylib/gyp/msvs_emulation.py:23.
1492  if arg == '' or ' ' in arg or '"' in arg:
1493    quote_re = re.compile(r'(\\*)"')
1494    arg = '"%s"' % (quote_re.sub(lambda mo: 2 * mo.group(1) + '\\"', arg))
1495
1496  # Then check to see if the arg contains any metacharacters other than
1497  # double quotes; if it does, quote everything (including the double
1498  # quotes) for safety.
1499  if any(a in UNSAFE_FOR_CMD for a in arg):
1500    arg = ''.join('^' + a if a in ALL_META_CHARS else a for a in arg)
1501  return arg
1502
1503
1504if __name__ == '__main__':
1505  sys.exit(main(sys.argv[1:]))
1506