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