#!/usr/bin/env python # # Copyright 2014 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import collections import re import optparse import os from string import Template import sys from util import build_utils class EnumDefinition(object): def __init__(self, class_name=None, class_package=None, entries=None): self.class_name = class_name self.class_package = class_package self.entries = collections.OrderedDict(entries or []) self.prefix_to_strip = '' def AppendEntry(self, key, value): if key in self.entries: raise Exception('Multiple definitions of key %s found.' % key) self.entries[key] = value def Finalize(self): self._Validate() self._AssignEntryIndices() self._StripPrefix() def _Validate(self): assert self.class_name assert self.class_package assert self.entries def _AssignEntryIndices(self): # Supporting the same set enum value assignments the compiler does is rather # complicated, so we limit ourselves to these cases: # - all the enum constants have values assigned, # - enum constants reference other enum constants or have no value assigned. if not all(self.entries.values()): index = 0 for key, value in self.entries.iteritems(): if not value: self.entries[key] = index index = index + 1 elif value in self.entries: self.entries[key] = self.entries[value] else: raise Exception('You can only reference other enum constants unless ' 'you assign values to all of the constants.') def _StripPrefix(self): if not self.prefix_to_strip: prefix_to_strip = re.sub('(?!^)([A-Z]+)', r'_\1', self.class_name).upper() prefix_to_strip += '_' if not all([w.startswith(prefix_to_strip) for w in self.entries.keys()]): prefix_to_strip = '' else: prefix_to_strip = self.prefix_to_strip entries = ((k.replace(prefix_to_strip, '', 1), v) for (k, v) in self.entries.iteritems()) self.entries = collections.OrderedDict(entries) class HeaderParser(object): single_line_comment_re = re.compile(r'\s*//') multi_line_comment_start_re = re.compile(r'\s*/\*') enum_start_re = re.compile(r'^\s*enum\s+(\w+)\s+{\s*$') enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?\s*$') enum_end_re = re.compile(r'^\s*}\s*;\s*$') generator_directive_re = re.compile( r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$') def __init__(self, lines): self._lines = lines self._enum_definitions = [] self._in_enum = False self._current_definition = None self._generator_directives = {} def ParseDefinitions(self): for line in self._lines: self._ParseLine(line) return self._enum_definitions def _ParseLine(self, line): if not self._in_enum: self._ParseRegularLine(line) else: self._ParseEnumLine(line) def _ParseEnumLine(self, line): if HeaderParser.single_line_comment_re.match(line): return if HeaderParser.multi_line_comment_start_re.match(line): raise Exception('Multi-line comments in enums are not supported.') enum_end = HeaderParser.enum_end_re.match(line) enum_entry = HeaderParser.enum_line_re.match(line) if enum_end: self._ApplyGeneratorDirectives() self._current_definition.Finalize() self._enum_definitions.append(self._current_definition) self._in_enum = False elif enum_entry: enum_key = enum_entry.groups()[0] enum_value = enum_entry.groups()[2] self._current_definition.AppendEntry(enum_key, enum_value) def _GetCurrentEnumPackageName(self): return self._generator_directives.get('ENUM_PACKAGE') def _GetCurrentEnumPrefixToStrip(self): return self._generator_directives.get('PREFIX_TO_STRIP', '') def _ApplyGeneratorDirectives(self): current_definition = self._current_definition current_definition.class_package = self._GetCurrentEnumPackageName() current_definition.prefix_to_strip = self._GetCurrentEnumPrefixToStrip() self._generator_directives = {} def _ParseRegularLine(self, line): enum_start = HeaderParser.enum_start_re.match(line) generator_directive = HeaderParser.generator_directive_re.match(line) if enum_start: if not self._GetCurrentEnumPackageName(): return self._current_definition = EnumDefinition() self._current_definition.class_name = enum_start.groups()[0] self._in_enum = True elif generator_directive: directive_name = generator_directive.groups()[0] directive_value = generator_directive.groups()[1] self._generator_directives[directive_name] = directive_value def GetScriptName(): script_components = os.path.abspath(sys.argv[0]).split(os.path.sep) build_index = script_components.index('build') return os.sep.join(script_components[build_index:]) def DoGenerate(options, source_paths): output_paths = [] for source_path in source_paths: enum_definitions = DoParseHeaderFile(source_path) for enum_definition in enum_definitions: package_path = enum_definition.class_package.replace('.', os.path.sep) file_name = enum_definition.class_name + '.java' output_path = os.path.join(options.output_dir, package_path, file_name) output_paths.append(output_path) if not options.print_output_only: build_utils.MakeDirectory(os.path.dirname(output_path)) DoWriteOutput(source_path, output_path, enum_definition) return output_paths def DoParseHeaderFile(path): with open(path) as f: return HeaderParser(f.readlines()).ParseDefinitions() def GenerateOutput(source_path, enum_definition): template = Template(""" // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file is autogenerated by // ${SCRIPT_NAME} // From // ${SOURCE_PATH} package ${PACKAGE}; public class ${CLASS_NAME} { ${ENUM_ENTRIES} } """) enum_template = Template(' public static final int ${NAME} = ${VALUE};') enum_entries_string = [] for enum_name, enum_value in enum_definition.entries.iteritems(): values = { 'NAME': enum_name, 'VALUE': enum_value, } enum_entries_string.append(enum_template.substitute(values)) enum_entries_string = '\n'.join(enum_entries_string) values = { 'CLASS_NAME': enum_definition.class_name, 'ENUM_ENTRIES': enum_entries_string, 'PACKAGE': enum_definition.class_package, 'SCRIPT_NAME': GetScriptName(), 'SOURCE_PATH': source_path, } return template.substitute(values) def DoWriteOutput(source_path, output_path, enum_definition): with open(output_path, 'w') as out_file: out_file.write(GenerateOutput(source_path, enum_definition)) def AssertFilesList(output_paths, assert_files_list): actual = set(output_paths) expected = set(assert_files_list) if not actual == expected: need_to_add = list(actual - expected) need_to_remove = list(expected - actual) raise Exception('Output files list does not match expectations. Please ' 'add %s and remove %s.' % (need_to_add, need_to_remove)) def DoMain(argv): parser = optparse.OptionParser() parser.add_option('--assert_file', action="append", default=[], dest="assert_files_list", help='Assert that the given ' 'file is an output. There can be multiple occurrences of ' 'this flag.') parser.add_option('--output_dir', help='Base path for generated files.') parser.add_option('--print_output_only', help='Only print output paths.', action='store_true') options, args = parser.parse_args(argv) output_paths = DoGenerate(options, args) if options.assert_files_list: AssertFilesList(output_paths, options.assert_files_list) return " ".join(output_paths) if __name__ == '__main__': DoMain(sys.argv[1:])