Home | History | Annotate | Download | only in bindings
      1 #!/usr/bin/env python
      2 # Copyright 2013 The Chromium 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 """The frontend for the Mojo bindings system."""
      7 
      8 
      9 import argparse
     10 import importlib
     11 import json
     12 import os
     13 import pprint
     14 import re
     15 import sys
     16 
     17 # Disable lint check for finding modules:
     18 # pylint: disable=F0401
     19 
     20 def _GetDirAbove(dirname):
     21   """Returns the directory "above" this file containing |dirname| (which must
     22   also be "above" this file)."""
     23   path = os.path.abspath(__file__)
     24   while True:
     25     path, tail = os.path.split(path)
     26     assert tail
     27     if tail == dirname:
     28       return path
     29 
     30 # Manually check for the command-line flag. (This isn't quite right, since it
     31 # ignores, e.g., "--", but it's close enough.)
     32 if "--use_bundled_pylibs" in sys.argv[1:]:
     33   sys.path.insert(0, os.path.join(_GetDirAbove("mojo"), "third_party"))
     34 
     35 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
     36                                 "pylib"))
     37 
     38 from mojom.error import Error
     39 import mojom.fileutil as fileutil
     40 from mojom.generate import translate
     41 from mojom.generate import template_expander
     42 from mojom.parse.parser import Parse
     43 
     44 
     45 _BUILTIN_GENERATORS = {
     46   "c++": "mojom_cpp_generator",
     47   "javascript": "mojom_js_generator",
     48   "java": "mojom_java_generator",
     49 }
     50 
     51 
     52 def LoadGenerators(generators_string):
     53   if not generators_string:
     54     return []  # No generators.
     55 
     56   script_dir = os.path.dirname(os.path.abspath(__file__))
     57   generators = {}
     58   for generator_name in [s.strip() for s in generators_string.split(",")]:
     59     language = generator_name.lower()
     60     if language not in _BUILTIN_GENERATORS:
     61       print "Unknown generator name %s" % generator_name
     62       sys.exit(1)
     63     generator_module = importlib.import_module(
     64         "generators.%s" % _BUILTIN_GENERATORS[language])
     65     generators[language] = generator_module
     66   return generators
     67 
     68 
     69 def MakeImportStackMessage(imported_filename_stack):
     70   """Make a (human-readable) message listing a chain of imports. (Returned
     71   string begins with a newline (if nonempty) and does not end with one.)"""
     72   return ''.join(
     73       reversed(["\n  %s was imported by %s" % (a, b) for (a, b) in \
     74                     zip(imported_filename_stack[1:], imported_filename_stack)]))
     75 
     76 
     77 class RelativePath(object):
     78   """Represents a path relative to the source tree."""
     79   def __init__(self, path, source_root):
     80     self.path = path
     81     self.source_root = source_root
     82 
     83   def relative_path(self):
     84     return os.path.relpath(os.path.abspath(self.path),
     85                            os.path.abspath(self.source_root))
     86 
     87 
     88 def FindImportFile(rel_dir, file_name, search_rel_dirs):
     89   """Finds |file_name| in either |rel_dir| or |search_rel_dirs|. Returns a
     90   RelativePath with first file found, or an arbitrary non-existent file
     91   otherwise."""
     92   for rel_search_dir in [rel_dir] + search_rel_dirs:
     93     path = os.path.join(rel_search_dir.path, file_name)
     94     if os.path.isfile(path):
     95       return RelativePath(path, rel_search_dir.source_root)
     96   return RelativePath(os.path.join(rel_dir.path, file_name),
     97                       rel_dir.source_root)
     98 
     99 
    100 class MojomProcessor(object):
    101   """Parses mojom files and creates ASTs for them.
    102 
    103   Attributes:
    104     _processed_files: {Dict[str, mojom.generate.module.Module]} Mapping from
    105         relative mojom filename paths to the module AST for that mojom file.
    106   """
    107   def __init__(self, should_generate):
    108     self._should_generate = should_generate
    109     self._processed_files = {}
    110     self._parsed_files = {}
    111     self._typemap = {}
    112 
    113   def LoadTypemaps(self, typemaps):
    114     # Support some very simple single-line comments in typemap JSON.
    115     comment_expr = r"^\s*//.*$"
    116     def no_comments(line):
    117       return not re.match(comment_expr, line)
    118     for filename in typemaps:
    119       with open(filename) as f:
    120         typemaps = json.loads("".join(filter(no_comments, f.readlines())))
    121         for language, typemap in typemaps.iteritems():
    122           language_map = self._typemap.get(language, {})
    123           language_map.update(typemap)
    124           self._typemap[language] = language_map
    125 
    126   def ProcessFile(self, args, remaining_args, generator_modules, filename):
    127     self._ParseFileAndImports(RelativePath(filename, args.depth),
    128                               args.import_directories, [])
    129 
    130     return self._GenerateModule(args, remaining_args, generator_modules,
    131         RelativePath(filename, args.depth))
    132 
    133   def _GenerateModule(self, args, remaining_args, generator_modules,
    134                       rel_filename):
    135     # Return the already-generated module.
    136     if rel_filename.path in self._processed_files:
    137       return self._processed_files[rel_filename.path]
    138     tree = self._parsed_files[rel_filename.path]
    139 
    140     dirname, name = os.path.split(rel_filename.path)
    141 
    142     # Process all our imports first and collect the module object for each.
    143     # We use these to generate proper type info.
    144     imports = {}
    145     for parsed_imp in tree.import_list:
    146       rel_import_file = FindImportFile(
    147           RelativePath(dirname, rel_filename.source_root),
    148           parsed_imp.import_filename, args.import_directories)
    149       imports[parsed_imp.import_filename] = self._GenerateModule(
    150           args, remaining_args, generator_modules, rel_import_file)
    151 
    152     module = translate.OrderedModule(tree, name, imports)
    153 
    154     # Set the path as relative to the source root.
    155     module.path = rel_filename.relative_path()
    156 
    157     # Normalize to unix-style path here to keep the generators simpler.
    158     module.path = module.path.replace('\\', '/')
    159 
    160     if self._should_generate(rel_filename.path):
    161       for language, generator_module in generator_modules.iteritems():
    162         generator = generator_module.Generator(
    163             module, args.output_dir, typemap=self._typemap.get(language, {}),
    164             variant=args.variant, bytecode_path=args.bytecode_path,
    165             for_blink=args.for_blink,
    166             use_once_callback=args.use_once_callback,
    167             export_attribute=args.export_attribute,
    168             export_header=args.export_header,
    169             generate_non_variant_code=args.generate_non_variant_code)
    170         filtered_args = []
    171         if hasattr(generator_module, 'GENERATOR_PREFIX'):
    172           prefix = '--' + generator_module.GENERATOR_PREFIX + '_'
    173           filtered_args = [arg for arg in remaining_args
    174                            if arg.startswith(prefix)]
    175         generator.GenerateFiles(filtered_args)
    176 
    177     # Save result.
    178     self._processed_files[rel_filename.path] = module
    179     return module
    180 
    181   def _ParseFileAndImports(self, rel_filename, import_directories,
    182       imported_filename_stack):
    183     # Ignore already-parsed files.
    184     if rel_filename.path in self._parsed_files:
    185       return
    186 
    187     if rel_filename.path in imported_filename_stack:
    188       print "%s: Error: Circular dependency" % rel_filename.path + \
    189           MakeImportStackMessage(imported_filename_stack + [rel_filename.path])
    190       sys.exit(1)
    191 
    192     try:
    193       with open(rel_filename.path) as f:
    194         source = f.read()
    195     except IOError as e:
    196       print "%s: Error: %s" % (rel_filename.path, e.strerror) + \
    197           MakeImportStackMessage(imported_filename_stack + [rel_filename.path])
    198       sys.exit(1)
    199 
    200     try:
    201       tree = Parse(source, rel_filename.path)
    202     except Error as e:
    203       full_stack = imported_filename_stack + [rel_filename.path]
    204       print str(e) + MakeImportStackMessage(full_stack)
    205       sys.exit(1)
    206 
    207     dirname = os.path.split(rel_filename.path)[0]
    208     for imp_entry in tree.import_list:
    209       import_file_entry = FindImportFile(
    210           RelativePath(dirname, rel_filename.source_root),
    211           imp_entry.import_filename, import_directories)
    212       self._ParseFileAndImports(import_file_entry, import_directories,
    213           imported_filename_stack + [rel_filename.path])
    214 
    215     self._parsed_files[rel_filename.path] = tree
    216 
    217 
    218 def _Generate(args, remaining_args):
    219   if args.variant == "none":
    220     args.variant = None
    221 
    222   for idx, import_dir in enumerate(args.import_directories):
    223     tokens = import_dir.split(":")
    224     if len(tokens) >= 2:
    225       args.import_directories[idx] = RelativePath(tokens[0], tokens[1])
    226     else:
    227       args.import_directories[idx] = RelativePath(tokens[0], args.depth)
    228   generator_modules = LoadGenerators(args.generators_string)
    229 
    230   fileutil.EnsureDirectoryExists(args.output_dir)
    231 
    232   processor = MojomProcessor(lambda filename: filename in args.filename)
    233   processor.LoadTypemaps(set(args.typemaps))
    234   for filename in args.filename:
    235     processor.ProcessFile(args, remaining_args, generator_modules, filename)
    236   if args.depfile:
    237     assert args.depfile_target
    238     with open(args.depfile, 'w') as f:
    239       f.write('%s: %s' % (
    240           args.depfile_target,
    241           ' '.join(processor._parsed_files.keys())))
    242 
    243   return 0
    244 
    245 
    246 def _Precompile(args, _):
    247   generator_modules = LoadGenerators(",".join(_BUILTIN_GENERATORS.keys()))
    248 
    249   template_expander.PrecompileTemplates(generator_modules, args.output_dir)
    250   return 0
    251 
    252 
    253 
    254 def main():
    255   parser = argparse.ArgumentParser(
    256       description="Generate bindings from mojom files.")
    257   parser.add_argument("--use_bundled_pylibs", action="store_true",
    258                       help="use Python modules bundled in the SDK")
    259 
    260   subparsers = parser.add_subparsers()
    261   generate_parser = subparsers.add_parser(
    262       "generate", description="Generate bindings from mojom files.")
    263   generate_parser.add_argument("filename", nargs="+",
    264                                help="mojom input file")
    265   generate_parser.add_argument("-d", "--depth", dest="depth", default=".",
    266                                help="depth from source root")
    267   generate_parser.add_argument("-o", "--output_dir", dest="output_dir",
    268                                default=".",
    269                                help="output directory for generated files")
    270   generate_parser.add_argument("-g", "--generators",
    271                                dest="generators_string",
    272                                metavar="GENERATORS",
    273                                default="c++,javascript,java",
    274                                help="comma-separated list of generators")
    275   generate_parser.add_argument(
    276       "-I", dest="import_directories", action="append", metavar="directory",
    277       default=[],
    278       help="add a directory to be searched for import files. The depth from "
    279            "source root can be specified for each import by appending it after "
    280            "a colon")
    281   generate_parser.add_argument("--typemap", action="append", metavar="TYPEMAP",
    282                                default=[], dest="typemaps",
    283                                help="apply TYPEMAP to generated output")
    284   generate_parser.add_argument("--variant", dest="variant", default=None,
    285                                help="output a named variant of the bindings")
    286   generate_parser.add_argument(
    287       "--bytecode_path", type=str, required=True, help=(
    288           "the path from which to load template bytecode; to generate template "
    289           "bytecode, run %s precompile BYTECODE_PATH" % os.path.basename(
    290               sys.argv[0])))
    291   generate_parser.add_argument("--for_blink", action="store_true",
    292                                help="Use WTF types as generated types for mojo "
    293                                "string/array/map.")
    294   generate_parser.add_argument(
    295       "--use_once_callback", action="store_true",
    296       help="Use base::OnceCallback instead of base::RepeatingCallback.")
    297   generate_parser.add_argument(
    298       "--export_attribute", type=str, default="",
    299       help="Optional attribute to specify on class declaration to export it "
    300       "for the component build.")
    301   generate_parser.add_argument(
    302       "--export_header", type=str, default="",
    303       help="Optional header to include in the generated headers to support the "
    304       "component build.")
    305   generate_parser.add_argument(
    306       "--generate_non_variant_code", action="store_true",
    307       help="Generate code that is shared by different variants.")
    308   generate_parser.add_argument(
    309       "--depfile", type=str,
    310       help="A file into which the list of input files will be written.")
    311   generate_parser.add_argument(
    312       "--depfile_target", type=str,
    313       help="The target name to use in the depfile.")
    314   generate_parser.set_defaults(func=_Generate)
    315 
    316   precompile_parser = subparsers.add_parser("precompile",
    317       description="Precompile templates for the mojom bindings generator.")
    318   precompile_parser.add_argument(
    319       "-o", "--output_dir", dest="output_dir", default=".",
    320       help="output directory for precompiled templates")
    321   precompile_parser.set_defaults(func=_Precompile)
    322 
    323   args, remaining_args = parser.parse_known_args()
    324   return args.func(args, remaining_args)
    325 
    326 
    327 if __name__ == "__main__":
    328   sys.exit(main())
    329