#!/usr/bin/env python # Copyright 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """The frontend for the Mojo bindings system.""" import argparse import importlib import json import os import pprint import re import sys # Disable lint check for finding modules: # pylint: disable=F0401 def _GetDirAbove(dirname): """Returns the directory "above" this file containing |dirname| (which must also be "above" this file).""" path = os.path.abspath(__file__) while True: path, tail = os.path.split(path) assert tail if tail == dirname: return path # Manually check for the command-line flag. (This isn't quite right, since it # ignores, e.g., "--", but it's close enough.) if "--use_bundled_pylibs" in sys.argv[1:]: sys.path.insert(0, os.path.join(_GetDirAbove("mojo"), "third_party")) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "pylib")) from mojom.error import Error import mojom.fileutil as fileutil from mojom.generate import translate from mojom.generate import template_expander from mojom.parse.parser import Parse _BUILTIN_GENERATORS = { "c++": "mojom_cpp_generator", "javascript": "mojom_js_generator", "java": "mojom_java_generator", } def LoadGenerators(generators_string): if not generators_string: return [] # No generators. script_dir = os.path.dirname(os.path.abspath(__file__)) generators = {} for generator_name in [s.strip() for s in generators_string.split(",")]: language = generator_name.lower() if language not in _BUILTIN_GENERATORS: print "Unknown generator name %s" % generator_name sys.exit(1) generator_module = importlib.import_module( "generators.%s" % _BUILTIN_GENERATORS[language]) generators[language] = generator_module return generators def MakeImportStackMessage(imported_filename_stack): """Make a (human-readable) message listing a chain of imports. (Returned string begins with a newline (if nonempty) and does not end with one.)""" return ''.join( reversed(["\n %s was imported by %s" % (a, b) for (a, b) in \ zip(imported_filename_stack[1:], imported_filename_stack)])) class RelativePath(object): """Represents a path relative to the source tree.""" def __init__(self, path, source_root): self.path = path self.source_root = source_root def relative_path(self): return os.path.relpath(os.path.abspath(self.path), os.path.abspath(self.source_root)) def FindImportFile(rel_dir, file_name, search_rel_dirs): """Finds |file_name| in either |rel_dir| or |search_rel_dirs|. Returns a RelativePath with first file found, or an arbitrary non-existent file otherwise.""" for rel_search_dir in [rel_dir] + search_rel_dirs: path = os.path.join(rel_search_dir.path, file_name) if os.path.isfile(path): return RelativePath(path, rel_search_dir.source_root) return RelativePath(os.path.join(rel_dir.path, file_name), rel_dir.source_root) class MojomProcessor(object): """Parses mojom files and creates ASTs for them. Attributes: _processed_files: {Dict[str, mojom.generate.module.Module]} Mapping from relative mojom filename paths to the module AST for that mojom file. """ def __init__(self, should_generate): self._should_generate = should_generate self._processed_files = {} self._parsed_files = {} self._typemap = {} def LoadTypemaps(self, typemaps): # Support some very simple single-line comments in typemap JSON. comment_expr = r"^\s*//.*$" def no_comments(line): return not re.match(comment_expr, line) for filename in typemaps: with open(filename) as f: typemaps = json.loads("".join(filter(no_comments, f.readlines()))) for language, typemap in typemaps.iteritems(): language_map = self._typemap.get(language, {}) language_map.update(typemap) self._typemap[language] = language_map def ProcessFile(self, args, remaining_args, generator_modules, filename): self._ParseFileAndImports(RelativePath(filename, args.depth), args.import_directories, []) return self._GenerateModule(args, remaining_args, generator_modules, RelativePath(filename, args.depth)) def _GenerateModule(self, args, remaining_args, generator_modules, rel_filename): # Return the already-generated module. if rel_filename.path in self._processed_files: return self._processed_files[rel_filename.path] tree = self._parsed_files[rel_filename.path] dirname, name = os.path.split(rel_filename.path) # Process all our imports first and collect the module object for each. # We use these to generate proper type info. imports = {} for parsed_imp in tree.import_list: rel_import_file = FindImportFile( RelativePath(dirname, rel_filename.source_root), parsed_imp.import_filename, args.import_directories) imports[parsed_imp.import_filename] = self._GenerateModule( args, remaining_args, generator_modules, rel_import_file) module = translate.OrderedModule(tree, name, imports) # Set the path as relative to the source root. module.path = rel_filename.relative_path() # Normalize to unix-style path here to keep the generators simpler. module.path = module.path.replace('\\', '/') if self._should_generate(rel_filename.path): for language, generator_module in generator_modules.iteritems(): generator = generator_module.Generator( module, args.output_dir, typemap=self._typemap.get(language, {}), variant=args.variant, bytecode_path=args.bytecode_path, for_blink=args.for_blink, use_once_callback=args.use_once_callback, export_attribute=args.export_attribute, export_header=args.export_header, generate_non_variant_code=args.generate_non_variant_code) filtered_args = [] if hasattr(generator_module, 'GENERATOR_PREFIX'): prefix = '--' + generator_module.GENERATOR_PREFIX + '_' filtered_args = [arg for arg in remaining_args if arg.startswith(prefix)] generator.GenerateFiles(filtered_args) # Save result. self._processed_files[rel_filename.path] = module return module def _ParseFileAndImports(self, rel_filename, import_directories, imported_filename_stack): # Ignore already-parsed files. if rel_filename.path in self._parsed_files: return if rel_filename.path in imported_filename_stack: print "%s: Error: Circular dependency" % rel_filename.path + \ MakeImportStackMessage(imported_filename_stack + [rel_filename.path]) sys.exit(1) try: with open(rel_filename.path) as f: source = f.read() except IOError as e: print "%s: Error: %s" % (rel_filename.path, e.strerror) + \ MakeImportStackMessage(imported_filename_stack + [rel_filename.path]) sys.exit(1) try: tree = Parse(source, rel_filename.path) except Error as e: full_stack = imported_filename_stack + [rel_filename.path] print str(e) + MakeImportStackMessage(full_stack) sys.exit(1) dirname = os.path.split(rel_filename.path)[0] for imp_entry in tree.import_list: import_file_entry = FindImportFile( RelativePath(dirname, rel_filename.source_root), imp_entry.import_filename, import_directories) self._ParseFileAndImports(import_file_entry, import_directories, imported_filename_stack + [rel_filename.path]) self._parsed_files[rel_filename.path] = tree def _Generate(args, remaining_args): if args.variant == "none": args.variant = None for idx, import_dir in enumerate(args.import_directories): tokens = import_dir.split(":") if len(tokens) >= 2: args.import_directories[idx] = RelativePath(tokens[0], tokens[1]) else: args.import_directories[idx] = RelativePath(tokens[0], args.depth) generator_modules = LoadGenerators(args.generators_string) fileutil.EnsureDirectoryExists(args.output_dir) processor = MojomProcessor(lambda filename: filename in args.filename) processor.LoadTypemaps(set(args.typemaps)) for filename in args.filename: processor.ProcessFile(args, remaining_args, generator_modules, filename) if args.depfile: assert args.depfile_target with open(args.depfile, 'w') as f: f.write('%s: %s' % ( args.depfile_target, ' '.join(processor._parsed_files.keys()))) return 0 def _Precompile(args, _): generator_modules = LoadGenerators(",".join(_BUILTIN_GENERATORS.keys())) template_expander.PrecompileTemplates(generator_modules, args.output_dir) return 0 def main(): parser = argparse.ArgumentParser( description="Generate bindings from mojom files.") parser.add_argument("--use_bundled_pylibs", action="store_true", help="use Python modules bundled in the SDK") subparsers = parser.add_subparsers() generate_parser = subparsers.add_parser( "generate", description="Generate bindings from mojom files.") generate_parser.add_argument("filename", nargs="+", help="mojom input file") generate_parser.add_argument("-d", "--depth", dest="depth", default=".", help="depth from source root") generate_parser.add_argument("-o", "--output_dir", dest="output_dir", default=".", help="output directory for generated files") generate_parser.add_argument("-g", "--generators", dest="generators_string", metavar="GENERATORS", default="c++,javascript,java", help="comma-separated list of generators") generate_parser.add_argument( "-I", dest="import_directories", action="append", metavar="directory", default=[], help="add a directory to be searched for import files. The depth from " "source root can be specified for each import by appending it after " "a colon") generate_parser.add_argument("--typemap", action="append", metavar="TYPEMAP", default=[], dest="typemaps", help="apply TYPEMAP to generated output") generate_parser.add_argument("--variant", dest="variant", default=None, help="output a named variant of the bindings") generate_parser.add_argument( "--bytecode_path", type=str, required=True, help=( "the path from which to load template bytecode; to generate template " "bytecode, run %s precompile BYTECODE_PATH" % os.path.basename( sys.argv[0]))) generate_parser.add_argument("--for_blink", action="store_true", help="Use WTF types as generated types for mojo " "string/array/map.") generate_parser.add_argument( "--use_once_callback", action="store_true", help="Use base::OnceCallback instead of base::RepeatingCallback.") generate_parser.add_argument( "--export_attribute", type=str, default="", help="Optional attribute to specify on class declaration to export it " "for the component build.") generate_parser.add_argument( "--export_header", type=str, default="", help="Optional header to include in the generated headers to support the " "component build.") generate_parser.add_argument( "--generate_non_variant_code", action="store_true", help="Generate code that is shared by different variants.") generate_parser.add_argument( "--depfile", type=str, help="A file into which the list of input files will be written.") generate_parser.add_argument( "--depfile_target", type=str, help="The target name to use in the depfile.") generate_parser.set_defaults(func=_Generate) precompile_parser = subparsers.add_parser("precompile", description="Precompile templates for the mojom bindings generator.") precompile_parser.add_argument( "-o", "--output_dir", dest="output_dir", default=".", help="output directory for precompiled templates") precompile_parser.set_defaults(func=_Precompile) args, remaining_args = parser.parse_known_args() return args.func(args, remaining_args) if __name__ == "__main__": sys.exit(main())