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 cPickle
     11 import hashlib
     12 import importlib
     13 import json
     14 import os
     15 import pprint
     16 import re
     17 import struct
     18 import sys
     19 
     20 # Disable lint check for finding modules:
     21 # pylint: disable=F0401
     22 
     23 def _GetDirAbove(dirname):
     24   """Returns the directory "above" this file containing |dirname| (which must
     25   also be "above" this file)."""
     26   path = os.path.abspath(__file__)
     27   while True:
     28     path, tail = os.path.split(path)
     29     assert tail
     30     if tail == dirname:
     31       return path
     32 
     33 # Manually check for the command-line flag. (This isn't quite right, since it
     34 # ignores, e.g., "--", but it's close enough.)
     35 if "--use_bundled_pylibs" in sys.argv[1:]:
     36   sys.path.insert(0, os.path.join(_GetDirAbove("mojo"), "third_party"))
     37 
     38 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
     39                                 "pylib"))
     40 
     41 from mojom.error import Error
     42 import mojom.fileutil as fileutil
     43 from mojom.generate import template_expander
     44 from mojom.generate import translate
     45 from mojom.generate.generator import AddComputedData, WriteFile
     46 from mojom.parse.conditional_features import RemoveDisabledDefinitions
     47 from mojom.parse.parser import Parse
     48 
     49 
     50 _BUILTIN_GENERATORS = {
     51   "c++": "mojom_cpp_generator",
     52   "javascript": "mojom_js_generator",
     53   "java": "mojom_java_generator",
     54 }
     55 
     56 
     57 def LoadGenerators(generators_string):
     58   if not generators_string:
     59     return []  # No generators.
     60 
     61   generators = {}
     62   for generator_name in [s.strip() for s in generators_string.split(",")]:
     63     language = generator_name.lower()
     64     if language not in _BUILTIN_GENERATORS:
     65       print "Unknown generator name %s" % generator_name
     66       sys.exit(1)
     67     generator_module = importlib.import_module(
     68         "generators.%s" % _BUILTIN_GENERATORS[language])
     69     generators[language] = generator_module
     70   return generators
     71 
     72 
     73 def MakeImportStackMessage(imported_filename_stack):
     74   """Make a (human-readable) message listing a chain of imports. (Returned
     75   string begins with a newline (if nonempty) and does not end with one.)"""
     76   return ''.join(
     77       reversed(["\n  %s was imported by %s" % (a, b) for (a, b) in \
     78                     zip(imported_filename_stack[1:], imported_filename_stack)]))
     79 
     80 
     81 class RelativePath(object):
     82   """Represents a path relative to the source tree."""
     83   def __init__(self, path, source_root):
     84     self.path = path
     85     self.source_root = source_root
     86 
     87   def relative_path(self):
     88     return os.path.relpath(os.path.abspath(self.path),
     89                            os.path.abspath(self.source_root))
     90 
     91 
     92 def FindImportFile(rel_dir, file_name, search_rel_dirs):
     93   """Finds |file_name| in either |rel_dir| or |search_rel_dirs|. Returns a
     94   RelativePath with first file found, or an arbitrary non-existent file
     95   otherwise."""
     96   for rel_search_dir in [rel_dir] + search_rel_dirs:
     97     path = os.path.join(rel_search_dir.path, file_name)
     98     if os.path.isfile(path):
     99       return RelativePath(path, rel_search_dir.source_root)
    100   return RelativePath(os.path.join(rel_dir.path, file_name),
    101                       rel_dir.source_root)
    102 
    103 
    104 def ScrambleMethodOrdinals(interfaces, salt):
    105   already_generated = set()
    106   for interface in interfaces:
    107     i = 0
    108     already_generated.clear()
    109     for method in interface.methods:
    110       while True:
    111         i = i + 1
    112         if i == 1000000:
    113           raise Exception("Could not generate %d method ordinals for %s" %
    114               (len(interface.methods), interface.mojom_name))
    115         # Generate a scrambled method.ordinal value. The algorithm doesn't have
    116         # to be very strong, cryptographically. It just needs to be non-trivial
    117         # to guess the results without the secret salt, in order to make it
    118         # harder for a compromised process to send fake Mojo messages.
    119         sha256 = hashlib.sha256(salt)
    120         sha256.update(interface.mojom_name)
    121         sha256.update(str(i))
    122         # Take the first 4 bytes as a little-endian uint32.
    123         ordinal = struct.unpack('<L', sha256.digest()[:4])[0]
    124         # Trim to 31 bits, so it always fits into a Java (signed) int.
    125         ordinal = ordinal & 0x7fffffff
    126         if ordinal in already_generated:
    127           continue
    128         already_generated.add(ordinal)
    129         method.ordinal = ordinal
    130         method.ordinal_comment = (
    131             'The %s value is based on sha256(salt + "%s%d").' %
    132             (ordinal, interface.mojom_name, i))
    133         break
    134 
    135 
    136 def ReadFileContents(filename):
    137   with open(filename, 'rb') as f:
    138     return f.read()
    139 
    140 
    141 class MojomProcessor(object):
    142   """Parses mojom files and creates ASTs for them.
    143 
    144   Attributes:
    145     _processed_files: {Dict[str, mojom.generate.module.Module]} Mapping from
    146         relative mojom filename paths to the module AST for that mojom file.
    147   """
    148   def __init__(self, should_generate):
    149     self._should_generate = should_generate
    150     self._processed_files = {}
    151     self._typemap = {}
    152 
    153   def LoadTypemaps(self, typemaps):
    154     # Support some very simple single-line comments in typemap JSON.
    155     comment_expr = r"^\s*//.*$"
    156     def no_comments(line):
    157       return not re.match(comment_expr, line)
    158     for filename in typemaps:
    159       with open(filename) as f:
    160         typemaps = json.loads("".join(filter(no_comments, f.readlines())))
    161         for language, typemap in typemaps.iteritems():
    162           language_map = self._typemap.get(language, {})
    163           language_map.update(typemap)
    164           self._typemap[language] = language_map
    165 
    166   def _GenerateModule(self, args, remaining_args, generator_modules,
    167                       rel_filename, imported_filename_stack):
    168     # Return the already-generated module.
    169     if rel_filename.path in self._processed_files:
    170       return self._processed_files[rel_filename.path]
    171 
    172     if rel_filename.path in imported_filename_stack:
    173       print "%s: Error: Circular dependency" % rel_filename.path + \
    174           MakeImportStackMessage(imported_filename_stack + [rel_filename.path])
    175       sys.exit(1)
    176 
    177     tree = _UnpickleAST(_FindPicklePath(rel_filename, args.gen_directories +
    178                                         [args.output_dir]))
    179     dirname = os.path.dirname(rel_filename.path)
    180 
    181     # Process all our imports first and collect the module object for each.
    182     # We use these to generate proper type info.
    183     imports = {}
    184     for parsed_imp in tree.import_list:
    185       rel_import_file = FindImportFile(
    186           RelativePath(dirname, rel_filename.source_root),
    187           parsed_imp.import_filename, args.import_directories)
    188       imports[parsed_imp.import_filename] = self._GenerateModule(
    189           args, remaining_args, generator_modules, rel_import_file,
    190           imported_filename_stack + [rel_filename.path])
    191 
    192     # Set the module path as relative to the source root.
    193     # Normalize to unix-style path here to keep the generators simpler.
    194     module_path = rel_filename.relative_path().replace('\\', '/')
    195 
    196     module = translate.OrderedModule(tree, module_path, imports)
    197 
    198     if args.scrambled_message_id_salt_paths:
    199       salt = ''.join(
    200           map(ReadFileContents, args.scrambled_message_id_salt_paths))
    201       ScrambleMethodOrdinals(module.interfaces, salt)
    202 
    203     if self._should_generate(rel_filename.path):
    204       AddComputedData(module)
    205       for language, generator_module in generator_modules.iteritems():
    206         generator = generator_module.Generator(
    207             module, args.output_dir, typemap=self._typemap.get(language, {}),
    208             variant=args.variant, bytecode_path=args.bytecode_path,
    209             for_blink=args.for_blink,
    210             use_once_callback=args.use_once_callback,
    211             js_bindings_mode=args.js_bindings_mode,
    212             export_attribute=args.export_attribute,
    213             export_header=args.export_header,
    214             generate_non_variant_code=args.generate_non_variant_code,
    215             support_lazy_serialization=args.support_lazy_serialization,
    216             disallow_native_types=args.disallow_native_types,
    217             disallow_interfaces=args.disallow_interfaces,
    218             generate_message_ids=args.generate_message_ids,
    219             generate_fuzzing=args.generate_fuzzing)
    220         filtered_args = []
    221         if hasattr(generator_module, 'GENERATOR_PREFIX'):
    222           prefix = '--' + generator_module.GENERATOR_PREFIX + '_'
    223           filtered_args = [arg for arg in remaining_args
    224                            if arg.startswith(prefix)]
    225         generator.GenerateFiles(filtered_args)
    226 
    227     # Save result.
    228     self._processed_files[rel_filename.path] = module
    229     return module
    230 
    231 
    232 def _Generate(args, remaining_args):
    233   if args.variant == "none":
    234     args.variant = None
    235 
    236   for idx, import_dir in enumerate(args.import_directories):
    237     tokens = import_dir.split(":")
    238     if len(tokens) >= 2:
    239       args.import_directories[idx] = RelativePath(tokens[0], tokens[1])
    240     else:
    241       args.import_directories[idx] = RelativePath(tokens[0], args.depth)
    242   generator_modules = LoadGenerators(args.generators_string)
    243 
    244   fileutil.EnsureDirectoryExists(args.output_dir)
    245 
    246   processor = MojomProcessor(lambda filename: filename in args.filename)
    247   processor.LoadTypemaps(set(args.typemaps))
    248 
    249   if args.filelist:
    250     with open(args.filelist) as f:
    251       args.filename.extend(f.read().split())
    252 
    253   for filename in args.filename:
    254     processor._GenerateModule(args, remaining_args, generator_modules,
    255                               RelativePath(filename, args.depth), [])
    256 
    257   return 0
    258 
    259 
    260 def _FindPicklePath(rel_filename, search_dirs):
    261   filename, _ = os.path.splitext(rel_filename.relative_path())
    262   pickle_path = filename + '.p'
    263   for search_dir in search_dirs:
    264     path = os.path.join(search_dir, pickle_path)
    265     if os.path.isfile(path):
    266       return path
    267   raise Exception("%s: Error: Could not find file in %r" % (pickle_path, search_dirs))
    268 
    269 
    270 def _GetPicklePath(rel_filename, output_dir):
    271   filename, _ = os.path.splitext(rel_filename.relative_path())
    272   pickle_path = filename + '.p'
    273   return os.path.join(output_dir, pickle_path)
    274 
    275 
    276 def _PickleAST(ast, output_file):
    277   full_dir = os.path.dirname(output_file)
    278   fileutil.EnsureDirectoryExists(full_dir)
    279 
    280   try:
    281     WriteFile(cPickle.dumps(ast), output_file)
    282   except (IOError, cPickle.PicklingError) as e:
    283     print "%s: Error: %s" % (output_file, str(e))
    284     sys.exit(1)
    285 
    286 def _UnpickleAST(input_file):
    287     try:
    288       with open(input_file, "rb") as f:
    289         return cPickle.load(f)
    290     except (IOError, cPickle.UnpicklingError) as e:
    291       print "%s: Error: %s" % (input_file, str(e))
    292       sys.exit(1)
    293 
    294 def _ParseFile(args, rel_filename):
    295   try:
    296     with open(rel_filename.path) as f:
    297       source = f.read()
    298   except IOError as e:
    299     print "%s: Error: %s" % (rel_filename.path, e.strerror)
    300     sys.exit(1)
    301 
    302   try:
    303     tree = Parse(source, rel_filename.path)
    304     RemoveDisabledDefinitions(tree, args.enabled_features)
    305   except Error as e:
    306     print "%s: Error: %s" % (rel_filename.path, str(e))
    307     sys.exit(1)
    308   _PickleAST(tree, _GetPicklePath(rel_filename, args.output_dir))
    309 
    310 
    311 def _Parse(args, _):
    312   fileutil.EnsureDirectoryExists(args.output_dir)
    313 
    314   if args.filelist:
    315     with open(args.filelist) as f:
    316       args.filename.extend(f.read().split())
    317 
    318   for filename in args.filename:
    319     _ParseFile(args, RelativePath(filename, args.depth))
    320   return 0
    321 
    322 
    323 def _Precompile(args, _):
    324   generator_modules = LoadGenerators(",".join(_BUILTIN_GENERATORS.keys()))
    325 
    326   template_expander.PrecompileTemplates(generator_modules, args.output_dir)
    327   return 0
    328 
    329 def _VerifyImportDeps(args, __):
    330   fileutil.EnsureDirectoryExists(args.gen_dir)
    331 
    332   if args.filelist:
    333     with open(args.filelist) as f:
    334       args.filename.extend(f.read().split())
    335 
    336   for filename in args.filename:
    337     rel_path = RelativePath(filename, args.depth)
    338     tree = _UnpickleAST(_GetPicklePath(rel_path, args.gen_dir))
    339 
    340     mojom_imports = set(
    341       parsed_imp.import_filename for parsed_imp in tree.import_list
    342       )
    343 
    344     # read the paths from the file
    345     f_deps = open(args.deps_file, 'r')
    346     deps_sources = set()
    347     for deps_path in f_deps:
    348       deps_path = deps_path.rstrip('\n')
    349       f_sources = open(deps_path, 'r')
    350 
    351       for source_file in f_sources:
    352         source_dir = deps_path.split(args.gen_dir + "/", 1)[1]
    353         full_source_path = os.path.dirname(source_dir) + "/" +  \
    354           source_file
    355         deps_sources.add(full_source_path.rstrip('\n'))
    356 
    357     if (not deps_sources.issuperset(mojom_imports)):
    358       print ">>> [%s] Missing dependencies for the following imports: %s" % ( \
    359         args.filename[0], \
    360         list(mojom_imports.difference(deps_sources)))
    361       sys.exit(1)
    362 
    363     source_filename, _ = os.path.splitext(rel_path.relative_path())
    364     output_file = source_filename + '.v'
    365     output_file_path = os.path.join(args.gen_dir, output_file)
    366     WriteFile("", output_file_path)
    367 
    368   return 0
    369 
    370 def main():
    371   parser = argparse.ArgumentParser(
    372       description="Generate bindings from mojom files.")
    373   parser.add_argument("--use_bundled_pylibs", action="store_true",
    374                       help="use Python modules bundled in the SDK")
    375 
    376   subparsers = parser.add_subparsers()
    377 
    378   parse_parser = subparsers.add_parser(
    379       "parse", description="Parse mojom to AST and remove disabled definitions."
    380                            " Pickle pruned AST into output_dir.")
    381   parse_parser.add_argument("filename", nargs="*", help="mojom input file")
    382   parse_parser.add_argument("--filelist", help="mojom input file list")
    383   parse_parser.add_argument(
    384       "-o",
    385       "--output_dir",
    386       dest="output_dir",
    387       default=".",
    388       help="output directory for generated files")
    389   parse_parser.add_argument(
    390       "-d", "--depth", dest="depth", default=".", help="depth from source root")
    391   parse_parser.add_argument(
    392       "--enable_feature",
    393       dest = "enabled_features",
    394       default=[],
    395       action="append",
    396       help="Controls which definitions guarded by an EnabledIf attribute "
    397       "will be enabled. If an EnabledIf attribute does not specify a value "
    398       "that matches one of the enabled features, it will be disabled.")
    399   parse_parser.set_defaults(func=_Parse)
    400 
    401   generate_parser = subparsers.add_parser(
    402       "generate", description="Generate bindings from mojom files.")
    403   generate_parser.add_argument("filename", nargs="*",
    404                                help="mojom input file")
    405   generate_parser.add_argument("--filelist", help="mojom input file list")
    406   generate_parser.add_argument("-d", "--depth", dest="depth", default=".",
    407                                help="depth from source root")
    408   generate_parser.add_argument("-o", "--output_dir", dest="output_dir",
    409                                default=".",
    410                                help="output directory for generated files")
    411   generate_parser.add_argument("-g", "--generators",
    412                                dest="generators_string",
    413                                metavar="GENERATORS",
    414                                default="c++,javascript,java",
    415                                help="comma-separated list of generators")
    416   generate_parser.add_argument(
    417       "--gen_dir", dest="gen_directories", action="append", metavar="directory",
    418       default=[], help="add a directory to be searched for the syntax trees.")
    419   generate_parser.add_argument(
    420       "-I", dest="import_directories", action="append", metavar="directory",
    421       default=[],
    422       help="add a directory to be searched for import files. The depth from "
    423            "source root can be specified for each import by appending it after "
    424            "a colon")
    425   generate_parser.add_argument("--typemap", action="append", metavar="TYPEMAP",
    426                                default=[], dest="typemaps",
    427                                help="apply TYPEMAP to generated output")
    428   generate_parser.add_argument("--variant", dest="variant", default=None,
    429                                help="output a named variant of the bindings")
    430   generate_parser.add_argument(
    431       "--bytecode_path", required=True, help=(
    432           "the path from which to load template bytecode; to generate template "
    433           "bytecode, run %s precompile BYTECODE_PATH" % os.path.basename(
    434               sys.argv[0])))
    435   generate_parser.add_argument("--for_blink", action="store_true",
    436                                help="Use WTF types as generated types for mojo "
    437                                "string/array/map.")
    438   generate_parser.add_argument(
    439       "--use_once_callback", action="store_true",
    440       help="Use base::OnceCallback instead of base::RepeatingCallback.")
    441   generate_parser.add_argument(
    442       "--js_bindings_mode", choices=["new", "both", "old"], default="new",
    443       help="This option only affects the JavaScript bindings. The value could "
    444       "be: \"new\" - generate only the new-style JS bindings, which use the "
    445       "new module loading approach and the core api exposed by Web IDL; "
    446       "\"both\" - generate both the old- and new-style bindings; \"old\" - "
    447       "generate only the old-style bindings.")
    448   generate_parser.add_argument(
    449       "--export_attribute", default="",
    450       help="Optional attribute to specify on class declaration to export it "
    451       "for the component build.")
    452   generate_parser.add_argument(
    453       "--export_header", default="",
    454       help="Optional header to include in the generated headers to support the "
    455       "component build.")
    456   generate_parser.add_argument(
    457       "--generate_non_variant_code", action="store_true",
    458       help="Generate code that is shared by different variants.")
    459   generate_parser.add_argument(
    460       "--scrambled_message_id_salt_path",
    461       dest="scrambled_message_id_salt_paths",
    462       help="If non-empty, the path to a file whose contents should be used as"
    463       "a salt for generating scrambled message IDs. If this switch is specified"
    464       "more than once, the contents of all salt files are concatenated to form"
    465       "the salt value.", default=[], action="append")
    466   generate_parser.add_argument(
    467       "--support_lazy_serialization",
    468       help="If set, generated bindings will serialize lazily when possible.",
    469       action="store_true")
    470   generate_parser.add_argument(
    471       "--disallow_native_types",
    472       help="Disallows the [Native] attribute to be specified on structs or "
    473       "enums within the mojom file.", action="store_true")
    474   generate_parser.add_argument(
    475       "--disallow_interfaces",
    476       help="Disallows interface definitions within the mojom file. It is an "
    477       "error to specify this flag when processing a mojom file which defines "
    478       "any interface.", action="store_true")
    479   generate_parser.add_argument(
    480       "--generate_message_ids",
    481       help="Generates only the message IDs header for C++ bindings. Note that "
    482       "this flag only matters if --generate_non_variant_code is also "
    483       "specified.", action="store_true")
    484   generate_parser.add_argument(
    485       "--generate_fuzzing",
    486       action="store_true",
    487       help="Generates additional bindings for fuzzing in JS.")
    488   generate_parser.set_defaults(func=_Generate)
    489 
    490   precompile_parser = subparsers.add_parser("precompile",
    491       description="Precompile templates for the mojom bindings generator.")
    492   precompile_parser.add_argument(
    493       "-o", "--output_dir", dest="output_dir", default=".",
    494       help="output directory for precompiled templates")
    495   precompile_parser.set_defaults(func=_Precompile)
    496 
    497   verify_parser = subparsers.add_parser("verify", description="Checks "
    498       "the set of imports against the set of dependencies.")
    499   verify_parser.add_argument("filename", nargs="*",
    500       help="mojom input file")
    501   verify_parser.add_argument("--filelist", help="mojom input file list")
    502   verify_parser.add_argument("-f", "--file", dest="deps_file",
    503       help="file containing paths to the sources files for "
    504       "dependencies")
    505   verify_parser.add_argument("-g", "--gen_dir",
    506       dest="gen_dir",
    507       help="directory with the syntax tree")
    508   verify_parser.add_argument(
    509       "-d", "--depth", dest="depth",
    510       help="depth from source root")
    511 
    512   verify_parser.set_defaults(func=_VerifyImportDeps)
    513 
    514   args, remaining_args = parser.parse_known_args()
    515   return args.func(args, remaining_args)
    516 
    517 
    518 if __name__ == "__main__":
    519   sys.exit(main())
    520