Home | History | Annotate | Download | only in mb
      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     # 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 
   1471 class 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.
   1478 UNSAFE_FOR_SET = set('^<>&|')
   1479 UNSAFE_FOR_CMD = UNSAFE_FOR_SET.union(set('()%'))
   1480 ALL_META_CHARS = UNSAFE_FOR_CMD.union(set('"'))
   1481 
   1482 
   1483 def 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 
   1489 def 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 
   1504 if __name__ == '__main__':
   1505   sys.exit(main(sys.argv[1:]))
   1506