• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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