• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2014 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import collections
8from datetime import date
9import re
10import optparse
11import os
12from string import Template
13import sys
14import zipfile
15
16from util import build_utils
17
18# List of C++ types that are compatible with the Java code generated by this
19# script.
20#
21# This script can parse .idl files however, at present it ignores special
22# rules such as [cpp_enum_prefix_override="ax_attr"].
23ENUM_FIXED_TYPE_WHITELIST = ['char', 'unsigned char',
24  'short', 'unsigned short',
25  'int', 'int8_t', 'int16_t', 'int32_t', 'uint8_t', 'uint16_t']
26
27class EnumDefinition(object):
28  def __init__(self, original_enum_name=None, class_name_override=None,
29               enum_package=None, entries=None, fixed_type=None):
30    self.original_enum_name = original_enum_name
31    self.class_name_override = class_name_override
32    self.enum_package = enum_package
33    self.entries = collections.OrderedDict(entries or [])
34    self.prefix_to_strip = None
35    self.fixed_type = fixed_type
36
37  def AppendEntry(self, key, value):
38    if key in self.entries:
39      raise Exception('Multiple definitions of key %s found.' % key)
40    self.entries[key] = value
41
42  @property
43  def class_name(self):
44    return self.class_name_override or self.original_enum_name
45
46  def Finalize(self):
47    self._Validate()
48    self._AssignEntryIndices()
49    self._StripPrefix()
50
51  def _Validate(self):
52    assert self.class_name
53    assert self.enum_package
54    assert self.entries
55    if self.fixed_type and self.fixed_type not in ENUM_FIXED_TYPE_WHITELIST:
56      raise Exception('Fixed type %s for enum %s not whitelisted.' %
57          (self.fixed_type, self.class_name))
58
59  def _AssignEntryIndices(self):
60    # Enums, if given no value, are given the value of the previous enum + 1.
61    if not all(self.entries.values()):
62      prev_enum_value = -1
63      for key, value in self.entries.iteritems():
64        if not value:
65          self.entries[key] = prev_enum_value + 1
66        elif value in self.entries:
67          self.entries[key] = self.entries[value]
68        else:
69          try:
70            self.entries[key] = int(value)
71          except ValueError:
72            raise Exception('Could not interpret integer from enum value "%s" '
73                            'for key %s.' % (value, key))
74        prev_enum_value = self.entries[key]
75
76
77  def _StripPrefix(self):
78    prefix_to_strip = self.prefix_to_strip
79    if not prefix_to_strip:
80      prefix_to_strip = self.original_enum_name
81      prefix_to_strip = re.sub('(?!^)([A-Z]+)', r'_\1', prefix_to_strip).upper()
82      prefix_to_strip += '_'
83      if not all([w.startswith(prefix_to_strip) for w in self.entries.keys()]):
84        prefix_to_strip = ''
85
86    entries = collections.OrderedDict()
87    for (k, v) in self.entries.iteritems():
88      stripped_key = k.replace(prefix_to_strip, '', 1)
89      if isinstance(v, basestring):
90        stripped_value = v.replace(prefix_to_strip, '', 1)
91      else:
92        stripped_value = v
93      entries[stripped_key] = stripped_value
94
95    self.entries = entries
96
97class DirectiveSet(object):
98  class_name_override_key = 'CLASS_NAME_OVERRIDE'
99  enum_package_key = 'ENUM_PACKAGE'
100  prefix_to_strip_key = 'PREFIX_TO_STRIP'
101
102  known_keys = [class_name_override_key, enum_package_key, prefix_to_strip_key]
103
104  def __init__(self):
105    self._directives = {}
106
107  def Update(self, key, value):
108    if key not in DirectiveSet.known_keys:
109      raise Exception("Unknown directive: " + key)
110    self._directives[key] = value
111
112  @property
113  def empty(self):
114    return len(self._directives) == 0
115
116  def UpdateDefinition(self, definition):
117    definition.class_name_override = self._directives.get(
118        DirectiveSet.class_name_override_key, '')
119    definition.enum_package = self._directives.get(
120        DirectiveSet.enum_package_key)
121    definition.prefix_to_strip = self._directives.get(
122        DirectiveSet.prefix_to_strip_key)
123
124
125class HeaderParser(object):
126  single_line_comment_re = re.compile(r'\s*//')
127  multi_line_comment_start_re = re.compile(r'\s*/\*')
128  enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?')
129  enum_end_re = re.compile(r'^\s*}\s*;\.*$')
130  generator_directive_re = re.compile(
131      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$')
132  multi_line_generator_directive_start_re = re.compile(
133      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*\(([\.\w]*)$')
134  multi_line_directive_continuation_re = re.compile(
135      r'^\s*//\s+([\.\w]+)$')
136  multi_line_directive_end_re = re.compile(
137      r'^\s*//\s+([\.\w]*)\)$')
138
139  optional_class_or_struct_re = r'(class|struct)?'
140  enum_name_re = r'(\w+)'
141  optional_fixed_type_re = r'(\:\s*(\w+\s*\w+?))?'
142  enum_start_re = re.compile(r'^\s*(?:\[cpp.*\])?\s*enum\s+' +
143      optional_class_or_struct_re + '\s*' + enum_name_re + '\s*' +
144      optional_fixed_type_re + '\s*{\s*$')
145
146  def __init__(self, lines, path=None):
147    self._lines = lines
148    self._path = path
149    self._enum_definitions = []
150    self._in_enum = False
151    self._current_definition = None
152    self._generator_directives = DirectiveSet()
153    self._multi_line_generator_directive = None
154
155  def _ApplyGeneratorDirectives(self):
156    self._generator_directives.UpdateDefinition(self._current_definition)
157    self._generator_directives = DirectiveSet()
158
159  def ParseDefinitions(self):
160    for line in self._lines:
161      self._ParseLine(line)
162    return self._enum_definitions
163
164  def _ParseLine(self, line):
165    if self._multi_line_generator_directive:
166      self._ParseMultiLineDirectiveLine(line)
167    elif not self._in_enum:
168      self._ParseRegularLine(line)
169    else:
170      self._ParseEnumLine(line)
171
172  def _ParseEnumLine(self, line):
173    if HeaderParser.single_line_comment_re.match(line):
174      return
175    if HeaderParser.multi_line_comment_start_re.match(line):
176      raise Exception('Multi-line comments in enums are not supported in ' +
177                      self._path)
178    enum_end = HeaderParser.enum_end_re.match(line)
179    enum_entry = HeaderParser.enum_line_re.match(line)
180    if enum_end:
181      self._ApplyGeneratorDirectives()
182      self._current_definition.Finalize()
183      self._enum_definitions.append(self._current_definition)
184      self._in_enum = False
185    elif enum_entry:
186      enum_key = enum_entry.groups()[0]
187      enum_value = enum_entry.groups()[2]
188      self._current_definition.AppendEntry(enum_key, enum_value)
189
190  def _ParseMultiLineDirectiveLine(self, line):
191    multi_line_directive_continuation = (
192        HeaderParser.multi_line_directive_continuation_re.match(line))
193    multi_line_directive_end = (
194        HeaderParser.multi_line_directive_end_re.match(line))
195
196    if multi_line_directive_continuation:
197      value_cont = multi_line_directive_continuation.groups()[0]
198      self._multi_line_generator_directive[1].append(value_cont)
199    elif multi_line_directive_end:
200      directive_name = self._multi_line_generator_directive[0]
201      directive_value = "".join(self._multi_line_generator_directive[1])
202      directive_value += multi_line_directive_end.groups()[0]
203      self._multi_line_generator_directive = None
204      self._generator_directives.Update(directive_name, directive_value)
205    else:
206      raise Exception('Malformed multi-line directive declaration in ' +
207                      self._path)
208
209  def _ParseRegularLine(self, line):
210    enum_start = HeaderParser.enum_start_re.match(line)
211    generator_directive = HeaderParser.generator_directive_re.match(line)
212    multi_line_generator_directive_start = (
213        HeaderParser.multi_line_generator_directive_start_re.match(line))
214
215    if generator_directive:
216      directive_name = generator_directive.groups()[0]
217      directive_value = generator_directive.groups()[1]
218      self._generator_directives.Update(directive_name, directive_value)
219    elif multi_line_generator_directive_start:
220      directive_name = multi_line_generator_directive_start.groups()[0]
221      directive_value = multi_line_generator_directive_start.groups()[1]
222      self._multi_line_generator_directive = (directive_name, [directive_value])
223    elif enum_start:
224      if self._generator_directives.empty:
225        return
226      self._current_definition = EnumDefinition(
227          original_enum_name=enum_start.groups()[1],
228          fixed_type=enum_start.groups()[3])
229      self._in_enum = True
230
231def GetScriptName():
232  return os.path.basename(os.path.abspath(sys.argv[0]))
233
234def DoGenerate(source_paths):
235  for source_path in source_paths:
236    enum_definitions = DoParseHeaderFile(source_path)
237    if not enum_definitions:
238      raise Exception('No enums found in %s\n'
239                      'Did you forget prefixing enums with '
240                      '"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' %
241                      source_path)
242    for enum_definition in enum_definitions:
243      package_path = enum_definition.enum_package.replace('.', os.path.sep)
244      file_name = enum_definition.class_name + '.java'
245      output_path = os.path.join(package_path, file_name)
246      output = GenerateOutput(source_path, enum_definition)
247      yield output_path, output
248
249
250def DoParseHeaderFile(path):
251  with open(path) as f:
252    return HeaderParser(f.readlines(), path).ParseDefinitions()
253
254
255def GenerateOutput(source_path, enum_definition):
256  template = Template("""
257// Copyright ${YEAR} The Chromium Authors. All rights reserved.
258// Use of this source code is governed by a BSD-style license that can be
259// found in the LICENSE file.
260
261// This file is autogenerated by
262//     ${SCRIPT_NAME}
263// From
264//     ${SOURCE_PATH}
265
266package ${PACKAGE};
267
268public class ${CLASS_NAME} {
269${ENUM_ENTRIES}
270}
271""")
272
273  enum_template = Template('  public static final int ${NAME} = ${VALUE};')
274  enum_entries_string = []
275  for enum_name, enum_value in enum_definition.entries.iteritems():
276    values = {
277        'NAME': enum_name,
278        'VALUE': enum_value,
279    }
280    enum_entries_string.append(enum_template.substitute(values))
281  enum_entries_string = '\n'.join(enum_entries_string)
282
283  values = {
284      'CLASS_NAME': enum_definition.class_name,
285      'ENUM_ENTRIES': enum_entries_string,
286      'PACKAGE': enum_definition.enum_package,
287      'SCRIPT_NAME': GetScriptName(),
288      'SOURCE_PATH': source_path,
289      'YEAR': str(date.today().year)
290  }
291  return template.substitute(values)
292
293
294def AssertFilesList(output_paths, assert_files_list):
295  actual = set(output_paths)
296  expected = set(assert_files_list)
297  if not actual == expected:
298    need_to_add = list(actual - expected)
299    need_to_remove = list(expected - actual)
300    raise Exception('Output files list does not match expectations. Please '
301                    'add %s and remove %s.' % (need_to_add, need_to_remove))
302
303def DoMain(argv):
304  usage = 'usage: %prog [options] [output_dir] input_file(s)...'
305  parser = optparse.OptionParser(usage=usage)
306  build_utils.AddDepfileOption(parser)
307
308  parser.add_option('--assert_file', action="append", default=[],
309                    dest="assert_files_list", help='Assert that the given '
310                    'file is an output. There can be multiple occurrences of '
311                    'this flag.')
312  parser.add_option('--srcjar',
313                    help='When specified, a .srcjar at the given path is '
314                    'created instead of individual .java files.')
315  parser.add_option('--print_output_only', help='Only print output paths.',
316                    action='store_true')
317  parser.add_option('--verbose', help='Print more information.',
318                    action='store_true')
319
320  options, args = parser.parse_args(argv)
321
322  if options.srcjar:
323    if not args:
324      parser.error('Need to specify at least one input file')
325    input_paths = args
326  else:
327    if len(args) < 2:
328      parser.error(
329          'Need to specify output directory and at least one input file')
330    output_dir = args[0]
331    input_paths = args[1:]
332
333  if options.depfile:
334    python_deps = build_utils.GetPythonDependencies()
335    build_utils.WriteDepfile(options.depfile, input_paths + python_deps)
336
337  if options.srcjar:
338    if options.print_output_only:
339      parser.error('--print_output_only does not work with --srcjar')
340    if options.assert_files_list:
341      parser.error('--assert_file does not work with --srcjar')
342
343    with zipfile.ZipFile(options.srcjar, 'w', zipfile.ZIP_STORED) as srcjar:
344      for output_path, data in DoGenerate(input_paths):
345        build_utils.AddToZipHermetic(srcjar, output_path, data=data)
346  else:
347    # TODO(agrieve): Delete this non-srcjar branch once GYP is gone.
348    output_paths = []
349    for output_path, data in DoGenerate(input_paths):
350      full_path = os.path.join(output_dir, output_path)
351      output_paths.append(full_path)
352      if not options.print_output_only:
353        build_utils.MakeDirectory(os.path.dirname(full_path))
354        with open(full_path, 'w') as out_file:
355          out_file.write(data)
356
357    if options.assert_files_list:
358      AssertFilesList(output_paths, options.assert_files_list)
359
360    if options.verbose:
361      print 'Output paths:'
362      print '\n'.join(output_paths)
363
364    # Used by GYP.
365    return ' '.join(output_paths)
366
367
368if __name__ == '__main__':
369  DoMain(sys.argv[1:])
370