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 9import argparse 10import imp 11import json 12import os 13import pprint 14import re 15import sys 16 17# Disable lint check for finding modules: 18# pylint: disable=F0401 19 20def _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.) 32if "--use_bundled_pylibs" in sys.argv[1:]: 33 sys.path.insert(0, os.path.join(_GetDirAbove("mojo"), "third_party")) 34 35sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 36 "pylib")) 37 38from mojom.error import Error 39import mojom.fileutil as fileutil 40from mojom.generate.data import OrderedModuleFromData 41from mojom.generate import template_expander 42from mojom.parse.parser import Parse 43from mojom.parse.translate import Translate 44 45 46_BUILTIN_GENERATORS = { 47 "c++": "mojom_cpp_generator.py", 48 "javascript": "mojom_js_generator.py", 49 "java": "mojom_java_generator.py", 50} 51 52 53def LoadGenerators(generators_string): 54 if not generators_string: 55 return [] # No generators. 56 57 script_dir = os.path.dirname(os.path.abspath(__file__)) 58 generators = {} 59 for generator_name in [s.strip() for s in generators_string.split(",")]: 60 language = generator_name.lower() 61 if language in _BUILTIN_GENERATORS: 62 generator_name = os.path.join(script_dir, "generators", 63 _BUILTIN_GENERATORS[language]) 64 else: 65 print "Unknown generator name %s" % generator_name 66 sys.exit(1) 67 generator_module = imp.load_source(os.path.basename(generator_name)[:-3], 68 generator_name) 69 generators[language] = generator_module 70 return generators 71 72 73def 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 81class 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 92def 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 104class MojomProcessor(object): 105 def __init__(self, should_generate): 106 self._should_generate = should_generate 107 self._processed_files = {} 108 self._parsed_files = {} 109 self._typemap = {} 110 111 def LoadTypemaps(self, typemaps): 112 # Support some very simple single-line comments in typemap JSON. 113 comment_expr = r"^\s*//.*$" 114 def no_comments(line): 115 return not re.match(comment_expr, line) 116 for filename in typemaps: 117 with open(filename) as f: 118 typemaps = json.loads("".join(filter(no_comments, f.readlines()))) 119 for language, typemap in typemaps.iteritems(): 120 language_map = self._typemap.get(language, {}) 121 language_map.update(typemap) 122 self._typemap[language] = language_map 123 124 def ProcessFile(self, args, remaining_args, generator_modules, filename): 125 self._ParseFileAndImports(RelativePath(filename, args.depth), 126 args.import_directories, []) 127 128 return self._GenerateModule(args, remaining_args, generator_modules, 129 RelativePath(filename, args.depth)) 130 131 def _GenerateModule(self, args, remaining_args, generator_modules, 132 rel_filename): 133 # Return the already-generated module. 134 if rel_filename.path in self._processed_files: 135 return self._processed_files[rel_filename.path] 136 tree = self._parsed_files[rel_filename.path] 137 138 dirname, name = os.path.split(rel_filename.path) 139 mojom = Translate(tree, name) 140 if args.debug_print_intermediate: 141 pprint.PrettyPrinter().pprint(mojom) 142 143 # Process all our imports first and collect the module object for each. 144 # We use these to generate proper type info. 145 for import_data in mojom['imports']: 146 rel_import_file = FindImportFile( 147 RelativePath(dirname, rel_filename.source_root), 148 import_data['filename'], args.import_directories) 149 import_data['module'] = self._GenerateModule( 150 args, remaining_args, generator_modules, rel_import_file) 151 152 module = OrderedModuleFromData(mojom) 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_new_wrapper_types=args.use_new_wrapper_types) 167 filtered_args = [] 168 if hasattr(generator_module, 'GENERATOR_PREFIX'): 169 prefix = '--' + generator_module.GENERATOR_PREFIX + '_' 170 filtered_args = [arg for arg in remaining_args 171 if arg.startswith(prefix)] 172 generator.GenerateFiles(filtered_args) 173 174 # Save result. 175 self._processed_files[rel_filename.path] = module 176 return module 177 178 def _ParseFileAndImports(self, rel_filename, import_directories, 179 imported_filename_stack): 180 # Ignore already-parsed files. 181 if rel_filename.path in self._parsed_files: 182 return 183 184 if rel_filename.path in imported_filename_stack: 185 print "%s: Error: Circular dependency" % rel_filename.path + \ 186 MakeImportStackMessage(imported_filename_stack + [rel_filename.path]) 187 sys.exit(1) 188 189 try: 190 with open(rel_filename.path) as f: 191 source = f.read() 192 except IOError as e: 193 print "%s: Error: %s" % (e.rel_filename.path, e.strerror) + \ 194 MakeImportStackMessage(imported_filename_stack + [rel_filename.path]) 195 sys.exit(1) 196 197 try: 198 tree = Parse(source, rel_filename.path) 199 except Error as e: 200 full_stack = imported_filename_stack + [rel_filename.path] 201 print str(e) + MakeImportStackMessage(full_stack) 202 sys.exit(1) 203 204 dirname = os.path.split(rel_filename.path)[0] 205 for imp_entry in tree.import_list: 206 import_file_entry = FindImportFile( 207 RelativePath(dirname, rel_filename.source_root), 208 imp_entry.import_filename, import_directories) 209 self._ParseFileAndImports(import_file_entry, import_directories, 210 imported_filename_stack + [rel_filename.path]) 211 212 self._parsed_files[rel_filename.path] = tree 213 214 215def _Generate(args, remaining_args): 216 if args.variant == "none": 217 args.variant = None 218 219 for idx, import_dir in enumerate(args.import_directories): 220 tokens = import_dir.split(":") 221 if len(tokens) >= 2: 222 args.import_directories[idx] = RelativePath(tokens[0], tokens[1]) 223 else: 224 args.import_directories[idx] = RelativePath(tokens[0], args.depth) 225 generator_modules = LoadGenerators(args.generators_string) 226 227 fileutil.EnsureDirectoryExists(args.output_dir) 228 229 processor = MojomProcessor(lambda filename: filename in args.filename) 230 processor.LoadTypemaps(set(args.typemaps)) 231 for filename in args.filename: 232 processor.ProcessFile(args, remaining_args, generator_modules, filename) 233 234 return 0 235 236 237def _Precompile(args, _): 238 generator_modules = LoadGenerators(",".join(_BUILTIN_GENERATORS.keys())) 239 240 template_expander.PrecompileTemplates(generator_modules, args.output_dir) 241 return 0 242 243 244 245def main(): 246 parser = argparse.ArgumentParser( 247 description="Generate bindings from mojom files.") 248 parser.add_argument("--use_bundled_pylibs", action="store_true", 249 help="use Python modules bundled in the SDK") 250 251 subparsers = parser.add_subparsers() 252 generate_parser = subparsers.add_parser( 253 "generate", description="Generate bindings from mojom files.") 254 generate_parser.add_argument("filename", nargs="+", 255 help="mojom input file") 256 generate_parser.add_argument("-d", "--depth", dest="depth", default=".", 257 help="depth from source root") 258 generate_parser.add_argument("-o", "--output_dir", dest="output_dir", 259 default=".", 260 help="output directory for generated files") 261 generate_parser.add_argument("--debug_print_intermediate", 262 action="store_true", 263 help="print the intermediate representation") 264 generate_parser.add_argument("-g", "--generators", 265 dest="generators_string", 266 metavar="GENERATORS", 267 default="c++,javascript,java", 268 help="comma-separated list of generators") 269 generate_parser.add_argument( 270 "-I", dest="import_directories", action="append", metavar="directory", 271 default=[], 272 help="add a directory to be searched for import files. The depth from " 273 "source root can be specified for each import by appending it after " 274 "a colon") 275 generate_parser.add_argument("--typemap", action="append", metavar="TYPEMAP", 276 default=[], dest="typemaps", 277 help="apply TYPEMAP to generated output") 278 generate_parser.add_argument("--variant", dest="variant", default=None, 279 help="output a named variant of the bindings") 280 generate_parser.add_argument( 281 "--bytecode_path", type=str, required=True, help=( 282 "the path from which to load template bytecode; to generate template " 283 "bytecode, run %s precompile BYTECODE_PATH" % os.path.basename( 284 sys.argv[0]))) 285 generate_parser.add_argument("--for_blink", action="store_true", 286 help="Use WTF types as generated types for mojo " 287 "string/array/map.") 288 generate_parser.add_argument( 289 "--use_new_wrapper_types", action="store_true", 290 help="Map mojom array/map/string to STL (for chromium variant) or WTF " 291 "(for blink variant) types directly.") 292 generate_parser.set_defaults(func=_Generate) 293 294 precompile_parser = subparsers.add_parser("precompile", 295 description="Precompile templates for the mojom bindings generator.") 296 precompile_parser.add_argument( 297 "-o", "--output_dir", dest="output_dir", default=".", 298 help="output directory for precompiled templates") 299 precompile_parser.set_defaults(func=_Precompile) 300 301 args, remaining_args = parser.parse_known_args() 302 return args.func(args, remaining_args) 303 304 305if __name__ == "__main__": 306 sys.exit(main()) 307