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