Home | History | Annotate | Download | only in dev
      1 #!/usr/bin/env python
      2 # Copyright 2016 the V8 project authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Script to generate V8's gn arguments based on common developer defaults
      7 or builder configurations.
      8 
      9 Goma is used by default if detected. The compiler proxy is assumed to run.
     10 
     11 This script can be added to the PATH and be used on other checkouts. It always
     12 runs for the checkout nesting the CWD.
     13 
     14 Configurations of this script live in infra/mb/mb_config.pyl.
     15 
     16 Available actions are: {gen,list}. Omitting the action defaults to "gen".
     17 
     18 -------------------------------------------------------------------------------
     19 
     20 Examples:
     21 
     22 # Generate the ia32.release config in out.gn/ia32.release.
     23 v8gen.py ia32.release
     24 
     25 # Generate into out.gn/foo without goma auto-detect.
     26 v8gen.py gen -b ia32.release foo --no-goma
     27 
     28 # Pass additional gn arguments after -- (don't use spaces within gn args).
     29 v8gen.py ia32.optdebug -- v8_enable_slow_dchecks=true
     30 
     31 # Generate gn arguments of 'V8 Linux64 - builder' from 'client.v8'. To switch
     32 # off goma usage here, the args.gn file must be edited manually.
     33 v8gen.py -m client.v8 -b 'V8 Linux64 - builder'
     34 
     35 # Show available configurations.
     36 v8gen.py list
     37 
     38 -------------------------------------------------------------------------------
     39 """
     40 
     41 import argparse
     42 import os
     43 import re
     44 import subprocess
     45 import sys
     46 
     47 CONFIG = os.path.join('infra', 'mb', 'mb_config.pyl')
     48 GOMA_DEFAULT = os.path.join(os.path.expanduser("~"), 'goma')
     49 OUT_DIR = 'out.gn'
     50 
     51 TOOLS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     52 sys.path.append(os.path.join(TOOLS_PATH, 'mb'))
     53 
     54 import mb
     55 
     56 
     57 def _sanitize_nonalpha(text):
     58   return re.sub(r'[^a-zA-Z0-9.]', '_', text)
     59 
     60 
     61 class GenerateGnArgs(object):
     62   def __init__(self, args):
     63     # Split args into this script's arguments and gn args passed to the
     64     # wrapped gn.
     65     index = args.index('--') if '--' in args else len(args)
     66     self._options = self._parse_arguments(args[:index])
     67     self._gn_args = args[index + 1:]
     68 
     69   def _parse_arguments(self, args):
     70     self.parser = argparse.ArgumentParser(
     71       description=__doc__,
     72       formatter_class=argparse.RawTextHelpFormatter,
     73     )
     74 
     75     def add_common_options(p):
     76       p.add_argument(
     77           '-m', '--master', default='developer_default',
     78           help='config group or master from mb_config.pyl - default: '
     79                'developer_default')
     80       p.add_argument(
     81           '-v', '--verbosity', action='count',
     82           help='print wrapped commands (use -vv to print output of wrapped '
     83                'commands)')
     84 
     85     subps = self.parser.add_subparsers()
     86 
     87     # Command: gen.
     88     gen_cmd = subps.add_parser(
     89         'gen', help='generate a new set of build files (default)')
     90     gen_cmd.set_defaults(func=self.cmd_gen)
     91     add_common_options(gen_cmd)
     92     gen_cmd.add_argument(
     93         'outdir', nargs='?',
     94         help='optional gn output directory')
     95     gen_cmd.add_argument(
     96         '-b', '--builder',
     97         help='build configuration or builder name from mb_config.pyl, e.g. '
     98              'x64.release')
     99     gen_cmd.add_argument(
    100         '-p', '--pedantic', action='store_true',
    101         help='run gn over command-line gn args to catch errors early')
    102 
    103     goma = gen_cmd.add_mutually_exclusive_group()
    104     goma.add_argument(
    105         '-g' , '--goma',
    106         action='store_true', default=None, dest='goma',
    107         help='force using goma')
    108     goma.add_argument(
    109         '--nogoma', '--no-goma',
    110         action='store_false', default=None, dest='goma',
    111         help='don\'t use goma auto detection - goma might still be used if '
    112              'specified as a gn arg')
    113 
    114     # Command: list.
    115     list_cmd = subps.add_parser(
    116         'list', help='list available configurations')
    117     list_cmd.set_defaults(func=self.cmd_list)
    118     add_common_options(list_cmd)
    119 
    120     # Default to "gen" unless global help is requested.
    121     if not args or args[0] not in subps.choices.keys() + ['-h', '--help']:
    122       args = ['gen'] + args
    123 
    124     return self.parser.parse_args(args)
    125 
    126   def cmd_gen(self):
    127     if not self._options.outdir and not self._options.builder:
    128       self.parser.error('please specify either an output directory or '
    129                         'a builder/config name (-b), e.g. x64.release')
    130 
    131     if not self._options.outdir:
    132       # Derive output directory from builder name.
    133       self._options.outdir = _sanitize_nonalpha(self._options.builder)
    134     else:
    135       # Also, if this should work on windows, we might need to use \ where
    136       # outdir is used as path, while using / if it's used in a gn context.
    137       if self._options.outdir.startswith('/'):
    138         self.parser.error(
    139             'only output directories relative to %s are supported' % OUT_DIR)
    140 
    141     if not self._options.builder:
    142       # Derive builder from output directory.
    143       self._options.builder = self._options.outdir
    144 
    145     # Check for builder/config in mb config.
    146     if self._options.builder not in self._mbw.masters[self._options.master]:
    147       print '%s does not exist in %s for %s' % (
    148           self._options.builder, CONFIG, self._options.master)
    149       return 1
    150 
    151     # TODO(machenbach): Check if the requested configurations has switched to
    152     # gn at all.
    153 
    154     # The directories are separated with slashes in a gn context (platform
    155     # independent).
    156     gn_outdir = '/'.join([OUT_DIR, self._options.outdir])
    157 
    158     # Call MB to generate the basic configuration.
    159     self._call_cmd([
    160       sys.executable,
    161       '-u', os.path.join('tools', 'mb', 'mb.py'),
    162       'gen',
    163       '-f', CONFIG,
    164       '-m', self._options.master,
    165       '-b', self._options.builder,
    166       gn_outdir,
    167     ])
    168 
    169     # Handle extra gn arguments.
    170     gn_args_path = os.path.join(OUT_DIR, self._options.outdir, 'args.gn')
    171 
    172     # Append command-line args.
    173     modified = self._append_gn_args(
    174         'command-line', gn_args_path, '\n'.join(self._gn_args))
    175 
    176     # Append goma args.
    177     # TODO(machenbach): We currently can't remove existing goma args from the
    178     # original config. E.g. to build like a bot that uses goma, but switch
    179     # goma off.
    180     modified |= self._append_gn_args(
    181         'goma', gn_args_path, self._goma_args)
    182 
    183     # Regenerate ninja files to check for errors in the additional gn args.
    184     if modified and self._options.pedantic:
    185       self._call_cmd(['gn', 'gen', gn_outdir])
    186     return 0
    187 
    188   def cmd_list(self):
    189     print '\n'.join(sorted(self._mbw.masters[self._options.master]))
    190     return 0
    191 
    192   def verbose_print_1(self, text):
    193     if self._options.verbosity >= 1:
    194       print '#' * 80
    195       print text
    196 
    197   def verbose_print_2(self, text):
    198     if self._options.verbosity >= 2:
    199       indent = ' ' * 2
    200       for l in text.splitlines():
    201         print indent + l
    202 
    203   def _call_cmd(self, args):
    204     self.verbose_print_1(' '.join(args))
    205     try:
    206       output = subprocess.check_output(
    207         args=args,
    208         stderr=subprocess.STDOUT,
    209       )
    210       self.verbose_print_2(output)
    211     except subprocess.CalledProcessError as e:
    212       self.verbose_print_2(e.output)
    213       raise
    214 
    215   def _find_work_dir(self, path):
    216     """Find the closest v8 root to `path`."""
    217     if os.path.exists(os.path.join(path, 'tools', 'dev', 'v8gen.py')):
    218       # Approximate the v8 root dir by a folder where this script exists
    219       # in the expected place.
    220       return path
    221     elif os.path.dirname(path) == path:
    222       raise Exception(
    223           'This appears to not be called from a recent v8 checkout')
    224     else:
    225       return self._find_work_dir(os.path.dirname(path))
    226 
    227   @property
    228   def _goma_dir(self):
    229     return os.path.normpath(os.environ.get('GOMA_DIR') or GOMA_DEFAULT)
    230 
    231   @property
    232   def _need_goma_dir(self):
    233     return self._goma_dir != GOMA_DEFAULT
    234 
    235   @property
    236   def _use_goma(self):
    237     if self._options.goma is None:
    238       # Auto-detect.
    239       return os.path.exists(self._goma_dir) and os.path.isdir(self._goma_dir)
    240     else:
    241       return self._options.goma
    242 
    243   @property
    244   def _goma_args(self):
    245     """Gn args for using goma."""
    246     # Specify goma args if we want to use goma and if goma isn't specified
    247     # via command line already. The command-line always has precedence over
    248     # any other specification.
    249     if (self._use_goma and
    250         not any(re.match(r'use_goma\s*=.*', x) for x in self._gn_args)):
    251       if self._need_goma_dir:
    252         return 'use_goma=true\ngoma_dir="%s"' % self._goma_dir
    253       else:
    254         return 'use_goma=true'
    255     else:
    256       return ''
    257 
    258   def _append_gn_args(self, type, gn_args_path, more_gn_args):
    259     """Append extra gn arguments to the generated args.gn file."""
    260     if not more_gn_args:
    261       return False
    262     self.verbose_print_1('Appending """\n%s\n""" to %s.' % (
    263         more_gn_args, os.path.abspath(gn_args_path)))
    264     with open(gn_args_path, 'a') as f:
    265       f.write('\n# Additional %s args:\n' % type)
    266       f.write(more_gn_args)
    267       f.write('\n')
    268 
    269     # Artificially increment modification time as our modifications happen too
    270     # fast. This makes sure that gn is properly rebuilding the ninja files.
    271     mtime = os.path.getmtime(gn_args_path) + 1
    272     with open(gn_args_path, 'a'):
    273       os.utime(gn_args_path, (mtime, mtime))
    274 
    275     return True
    276 
    277   def main(self):
    278     # Always operate relative to the base directory for better relative-path
    279     # handling. This script can be used in any v8 checkout.
    280     workdir = self._find_work_dir(os.getcwd())
    281     if workdir != os.getcwd():
    282       self.verbose_print_1('cd ' + workdir)
    283       os.chdir(workdir)
    284 
    285     # Initialize MB as a library.
    286     self._mbw = mb.MetaBuildWrapper()
    287 
    288     # TODO(machenbach): Factor out common methods independent of mb arguments.
    289     self._mbw.ParseArgs(['lookup', '-f', CONFIG])
    290     self._mbw.ReadConfigFile()
    291 
    292     if not self._options.master in self._mbw.masters:
    293       print '%s not found in %s\n' % (self._options.master, CONFIG)
    294       print 'Choose one of:\n%s\n' % (
    295           '\n'.join(sorted(self._mbw.masters.keys())))
    296       return 1
    297 
    298     return self._options.func()
    299 
    300 
    301 if __name__ == "__main__":
    302   gen = GenerateGnArgs(sys.argv[1:])
    303   try:
    304     sys.exit(gen.main())
    305   except Exception:
    306     if gen._options.verbosity < 2:
    307       print ('\nHint: You can raise verbosity (-vv) to see the output of '
    308              'failed commands.\n')
    309     raise
    310