blob: b304930d9d763089862948d8085b7304e24ff0a0 [file] [log] [blame]
Ben Murdoch097c5b22016-05-18 11:27:45 +01001#!/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:])