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 7 import collections 8 import re 9 import optparse 10 import os 11 from string import Template 12 import sys 13 14 from util import build_utils 15 16 class EnumDefinition(object): 17 def __init__(self, class_name=None, class_package=None, entries=None): 18 self.class_name = class_name 19 self.class_package = class_package 20 self.entries = collections.OrderedDict(entries or []) 21 self.prefix_to_strip = '' 22 23 def AppendEntry(self, key, value): 24 if key in self.entries: 25 raise Exception('Multiple definitions of key %s found.' % key) 26 self.entries[key] = value 27 28 def Finalize(self): 29 self._Validate() 30 self._AssignEntryIndices() 31 self._StripPrefix() 32 33 def _Validate(self): 34 assert self.class_name 35 assert self.class_package 36 assert self.entries 37 38 def _AssignEntryIndices(self): 39 # Supporting the same set enum value assignments the compiler does is rather 40 # complicated, so we limit ourselves to these cases: 41 # - all the enum constants have values assigned, 42 # - enum constants reference other enum constants or have no value assigned. 43 44 if not all(self.entries.values()): 45 index = 0 46 for key, value in self.entries.iteritems(): 47 if not value: 48 self.entries[key] = index 49 index = index + 1 50 elif value in self.entries: 51 self.entries[key] = self.entries[value] 52 else: 53 raise Exception('You can only reference other enum constants unless ' 54 'you assign values to all of the constants.') 55 56 def _StripPrefix(self): 57 if not self.prefix_to_strip: 58 prefix_to_strip = re.sub('(?!^)([A-Z]+)', r'_\1', self.class_name).upper() 59 prefix_to_strip += '_' 60 if not all([w.startswith(prefix_to_strip) for w in self.entries.keys()]): 61 prefix_to_strip = '' 62 else: 63 prefix_to_strip = self.prefix_to_strip 64 entries = ((k.replace(prefix_to_strip, '', 1), v) for (k, v) in 65 self.entries.iteritems()) 66 self.entries = collections.OrderedDict(entries) 67 68 class HeaderParser(object): 69 single_line_comment_re = re.compile(r'\s*//') 70 multi_line_comment_start_re = re.compile(r'\s*/\*') 71 enum_start_re = re.compile(r'^\s*enum\s+(\w+)\s+{\s*$') 72 enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?\s*$') 73 enum_end_re = re.compile(r'^\s*}\s*;\s*$') 74 generator_directive_re = re.compile( 75 r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$') 76 77 def __init__(self, lines): 78 self._lines = lines 79 self._enum_definitions = [] 80 self._in_enum = False 81 self._current_definition = None 82 self._generator_directives = {} 83 84 def ParseDefinitions(self): 85 for line in self._lines: 86 self._ParseLine(line) 87 return self._enum_definitions 88 89 def _ParseLine(self, line): 90 if not self._in_enum: 91 self._ParseRegularLine(line) 92 else: 93 self._ParseEnumLine(line) 94 95 def _ParseEnumLine(self, line): 96 if HeaderParser.single_line_comment_re.match(line): 97 return 98 if HeaderParser.multi_line_comment_start_re.match(line): 99 raise Exception('Multi-line comments in enums are not supported.') 100 enum_end = HeaderParser.enum_end_re.match(line) 101 enum_entry = HeaderParser.enum_line_re.match(line) 102 if enum_end: 103 self._ApplyGeneratorDirectives() 104 self._current_definition.Finalize() 105 self._enum_definitions.append(self._current_definition) 106 self._in_enum = False 107 elif enum_entry: 108 enum_key = enum_entry.groups()[0] 109 enum_value = enum_entry.groups()[2] 110 self._current_definition.AppendEntry(enum_key, enum_value) 111 112 def _GetCurrentEnumPackageName(self): 113 return self._generator_directives.get('ENUM_PACKAGE') 114 115 def _GetCurrentEnumPrefixToStrip(self): 116 return self._generator_directives.get('PREFIX_TO_STRIP', '') 117 118 def _ApplyGeneratorDirectives(self): 119 current_definition = self._current_definition 120 current_definition.class_package = self._GetCurrentEnumPackageName() 121 current_definition.prefix_to_strip = self._GetCurrentEnumPrefixToStrip() 122 self._generator_directives = {} 123 124 def _ParseRegularLine(self, line): 125 enum_start = HeaderParser.enum_start_re.match(line) 126 generator_directive = HeaderParser.generator_directive_re.match(line) 127 if enum_start: 128 if not self._GetCurrentEnumPackageName(): 129 return 130 self._current_definition = EnumDefinition() 131 self._current_definition.class_name = enum_start.groups()[0] 132 self._in_enum = True 133 elif generator_directive: 134 directive_name = generator_directive.groups()[0] 135 directive_value = generator_directive.groups()[1] 136 self._generator_directives[directive_name] = directive_value 137 138 139 def GetScriptName(): 140 script_components = os.path.abspath(sys.argv[0]).split(os.path.sep) 141 build_index = script_components.index('build') 142 return os.sep.join(script_components[build_index:]) 143 144 145 def DoGenerate(options, source_paths): 146 output_paths = [] 147 for source_path in source_paths: 148 enum_definitions = DoParseHeaderFile(source_path) 149 for enum_definition in enum_definitions: 150 package_path = enum_definition.class_package.replace('.', os.path.sep) 151 file_name = enum_definition.class_name + '.java' 152 output_path = os.path.join(options.output_dir, package_path, file_name) 153 output_paths.append(output_path) 154 if not options.print_output_only: 155 build_utils.MakeDirectory(os.path.dirname(output_path)) 156 DoWriteOutput(source_path, output_path, enum_definition) 157 return output_paths 158 159 160 def DoParseHeaderFile(path): 161 with open(path) as f: 162 return HeaderParser(f.readlines()).ParseDefinitions() 163 164 165 def GenerateOutput(source_path, enum_definition): 166 template = Template(""" 167 // Copyright 2014 The Chromium Authors. All rights reserved. 168 // Use of this source code is governed by a BSD-style license that can be 169 // found in the LICENSE file. 170 171 // This file is autogenerated by 172 // ${SCRIPT_NAME} 173 // From 174 // ${SOURCE_PATH} 175 176 package ${PACKAGE}; 177 178 public class ${CLASS_NAME} { 179 ${ENUM_ENTRIES} 180 } 181 """) 182 183 enum_template = Template(' public static final int ${NAME} = ${VALUE};') 184 enum_entries_string = [] 185 for enum_name, enum_value in enum_definition.entries.iteritems(): 186 values = { 187 'NAME': enum_name, 188 'VALUE': enum_value, 189 } 190 enum_entries_string.append(enum_template.substitute(values)) 191 enum_entries_string = '\n'.join(enum_entries_string) 192 193 values = { 194 'CLASS_NAME': enum_definition.class_name, 195 'ENUM_ENTRIES': enum_entries_string, 196 'PACKAGE': enum_definition.class_package, 197 'SCRIPT_NAME': GetScriptName(), 198 'SOURCE_PATH': source_path, 199 } 200 return template.substitute(values) 201 202 203 def DoWriteOutput(source_path, output_path, enum_definition): 204 with open(output_path, 'w') as out_file: 205 out_file.write(GenerateOutput(source_path, enum_definition)) 206 207 def AssertFilesList(output_paths, assert_files_list): 208 actual = set(output_paths) 209 expected = set(assert_files_list) 210 if not actual == expected: 211 need_to_add = list(actual - expected) 212 need_to_remove = list(expected - actual) 213 raise Exception('Output files list does not match expectations. Please ' 214 'add %s and remove %s.' % (need_to_add, need_to_remove)) 215 216 def DoMain(argv): 217 parser = optparse.OptionParser() 218 219 parser.add_option('--assert_file', action="append", default=[], 220 dest="assert_files_list", help='Assert that the given ' 221 'file is an output. There can be multiple occurrences of ' 222 'this flag.') 223 parser.add_option('--output_dir', help='Base path for generated files.') 224 parser.add_option('--print_output_only', help='Only print output paths.', 225 action='store_true') 226 227 options, args = parser.parse_args(argv) 228 229 output_paths = DoGenerate(options, args) 230 231 if options.assert_files_list: 232 AssertFilesList(output_paths, options.assert_files_list) 233 234 return " ".join(output_paths) 235 236 if __name__ == '__main__': 237 DoMain(sys.argv[1:]) 238