#!/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 from datetime import date import re import optparse import os from string import Template import sys import zipfile from util import build_utils # List of C++ types that are compatible with the Java code generated by this # script. # # This script can parse .idl files however, at present it ignores special # rules such as [cpp_enum_prefix_override="ax_attr"]. ENUM_FIXED_TYPE_WHITELIST = ['char', 'unsigned char', 'short', 'unsigned short', 'int', 'int8_t', 'int16_t', 'int32_t', 'uint8_t', 'uint16_t'] class EnumDefinition(object): def __init__(self, original_enum_name=None, class_name_override=None, enum_package=None, entries=None, fixed_type=None): self.original_enum_name = original_enum_name self.class_name_override = class_name_override self.enum_package = enum_package self.entries = collections.OrderedDict(entries or []) self.prefix_to_strip = None self.fixed_type = fixed_type def AppendEntry(self, key, value): if key in self.entries: raise Exception('Multiple definitions of key %s found.' % key) self.entries[key] = value @property def class_name(self): return self.class_name_override or self.original_enum_name def Finalize(self): self._Validate() self._AssignEntryIndices() self._StripPrefix() def _Validate(self): assert self.class_name assert self.enum_package assert self.entries if self.fixed_type and self.fixed_type not in ENUM_FIXED_TYPE_WHITELIST: raise Exception('Fixed type %s for enum %s not whitelisted.' % (self.fixed_type, self.class_name)) def _AssignEntryIndices(self): # Enums, if given no value, are given the value of the previous enum + 1. if not all(self.entries.values()): prev_enum_value = -1 for key, value in self.entries.iteritems(): if not value: self.entries[key] = prev_enum_value + 1 elif value in self.entries: self.entries[key] = self.entries[value] else: try: self.entries[key] = int(value) except ValueError: raise Exception('Could not interpret integer from enum value "%s" ' 'for key %s.' % (value, key)) prev_enum_value = self.entries[key] def _StripPrefix(self): prefix_to_strip = self.prefix_to_strip if not prefix_to_strip: prefix_to_strip = self.original_enum_name prefix_to_strip = re.sub('(?!^)([A-Z]+)', r'_\1', prefix_to_strip).upper() prefix_to_strip += '_' if not all([w.startswith(prefix_to_strip) for w in self.entries.keys()]): prefix_to_strip = '' entries = collections.OrderedDict() for (k, v) in self.entries.iteritems(): stripped_key = k.replace(prefix_to_strip, '', 1) if isinstance(v, basestring): stripped_value = v.replace(prefix_to_strip, '', 1) else: stripped_value = v entries[stripped_key] = stripped_value self.entries = entries class DirectiveSet(object): class_name_override_key = 'CLASS_NAME_OVERRIDE' enum_package_key = 'ENUM_PACKAGE' prefix_to_strip_key = 'PREFIX_TO_STRIP' known_keys = [class_name_override_key, enum_package_key, prefix_to_strip_key] def __init__(self): self._directives = {} def Update(self, key, value): if key not in DirectiveSet.known_keys: raise Exception("Unknown directive: " + key) self._directives[key] = value @property def empty(self): return len(self._directives) == 0 def UpdateDefinition(self, definition): definition.class_name_override = self._directives.get( DirectiveSet.class_name_override_key, '') definition.enum_package = self._directives.get( DirectiveSet.enum_package_key) definition.prefix_to_strip = self._directives.get( DirectiveSet.prefix_to_strip_key) class HeaderParser(object): single_line_comment_re = re.compile(r'\s*//') multi_line_comment_start_re = re.compile(r'\s*/\*') enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?') enum_end_re = re.compile(r'^\s*}\s*;\.*$') generator_directive_re = re.compile( r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$') multi_line_generator_directive_start_re = re.compile( r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*\(([\.\w]*)$') multi_line_directive_continuation_re = re.compile( r'^\s*//\s+([\.\w]+)$') multi_line_directive_end_re = re.compile( r'^\s*//\s+([\.\w]*)\)$') optional_class_or_struct_re = r'(class|struct)?' enum_name_re = r'(\w+)' optional_fixed_type_re = r'(\:\s*(\w+\s*\w+?))?' enum_start_re = re.compile(r'^\s*(?:\[cpp.*\])?\s*enum\s+' + optional_class_or_struct_re + '\s*' + enum_name_re + '\s*' + optional_fixed_type_re + '\s*{\s*$') def __init__(self, lines, path=None): self._lines = lines self._path = path self._enum_definitions = [] self._in_enum = False self._current_definition = None self._generator_directives = DirectiveSet() self._multi_line_generator_directive = None def _ApplyGeneratorDirectives(self): self._generator_directives.UpdateDefinition(self._current_definition) self._generator_directives = DirectiveSet() def ParseDefinitions(self): for line in self._lines: self._ParseLine(line) return self._enum_definitions def _ParseLine(self, line): if self._multi_line_generator_directive: self._ParseMultiLineDirectiveLine(line) elif 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 in ' + self._path) 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 _ParseMultiLineDirectiveLine(self, line): multi_line_directive_continuation = ( HeaderParser.multi_line_directive_continuation_re.match(line)) multi_line_directive_end = ( HeaderParser.multi_line_directive_end_re.match(line)) if multi_line_directive_continuation: value_cont = multi_line_directive_continuation.groups()[0] self._multi_line_generator_directive[1].append(value_cont) elif multi_line_directive_end: directive_name = self._multi_line_generator_directive[0] directive_value = "".join(self._multi_line_generator_directive[1]) directive_value += multi_line_directive_end.groups()[0] self._multi_line_generator_directive = None self._generator_directives.Update(directive_name, directive_value) else: raise Exception('Malformed multi-line directive declaration in ' + self._path) def _ParseRegularLine(self, line): enum_start = HeaderParser.enum_start_re.match(line) generator_directive = HeaderParser.generator_directive_re.match(line) multi_line_generator_directive_start = ( HeaderParser.multi_line_generator_directive_start_re.match(line)) if generator_directive: directive_name = generator_directive.groups()[0] directive_value = generator_directive.groups()[1] self._generator_directives.Update(directive_name, directive_value) elif multi_line_generator_directive_start: directive_name = multi_line_generator_directive_start.groups()[0] directive_value = multi_line_generator_directive_start.groups()[1] self._multi_line_generator_directive = (directive_name, [directive_value]) elif enum_start: if self._generator_directives.empty: return self._current_definition = EnumDefinition( original_enum_name=enum_start.groups()[1], fixed_type=enum_start.groups()[3]) self._in_enum = True def GetScriptName(): return os.path.basename(os.path.abspath(sys.argv[0])) def DoGenerate(source_paths): for source_path in source_paths: enum_definitions = DoParseHeaderFile(source_path) if not enum_definitions: raise Exception('No enums found in %s\n' 'Did you forget prefixing enums with ' '"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' % source_path) for enum_definition in enum_definitions: package_path = enum_definition.enum_package.replace('.', os.path.sep) file_name = enum_definition.class_name + '.java' output_path = os.path.join(package_path, file_name) output = GenerateOutput(source_path, enum_definition) yield output_path, output def DoParseHeaderFile(path): with open(path) as f: return HeaderParser(f.readlines(), path).ParseDefinitions() def GenerateOutput(source_path, enum_definition): template = Template(""" // Copyright ${YEAR} 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.enum_package, 'SCRIPT_NAME': GetScriptName(), 'SOURCE_PATH': source_path, 'YEAR': str(date.today().year) } return template.substitute(values) 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): usage = 'usage: %prog [options] [output_dir] input_file(s)...' parser = optparse.OptionParser(usage=usage) build_utils.AddDepfileOption(parser) 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('--srcjar', help='When specified, a .srcjar at the given path is ' 'created instead of individual .java files.') parser.add_option('--print_output_only', help='Only print output paths.', action='store_true') parser.add_option('--verbose', help='Print more information.', action='store_true') options, args = parser.parse_args(argv) if options.srcjar: if not args: parser.error('Need to specify at least one input file') input_paths = args else: if len(args) < 2: parser.error( 'Need to specify output directory and at least one input file') output_dir = args[0] input_paths = args[1:] if options.depfile: python_deps = build_utils.GetPythonDependencies() build_utils.WriteDepfile(options.depfile, input_paths + python_deps) if options.srcjar: if options.print_output_only: parser.error('--print_output_only does not work with --srcjar') if options.assert_files_list: parser.error('--assert_file does not work with --srcjar') with zipfile.ZipFile(options.srcjar, 'w', zipfile.ZIP_STORED) as srcjar: for output_path, data in DoGenerate(input_paths): build_utils.AddToZipHermetic(srcjar, output_path, data=data) else: # TODO(agrieve): Delete this non-srcjar branch once GYP is gone. output_paths = [] for output_path, data in DoGenerate(input_paths): full_path = os.path.join(output_dir, output_path) output_paths.append(full_path) if not options.print_output_only: build_utils.MakeDirectory(os.path.dirname(full_path)) with open(full_path, 'w') as out_file: out_file.write(data) if options.assert_files_list: AssertFilesList(output_paths, options.assert_files_list) if options.verbose: print 'Output paths:' print '\n'.join(output_paths) # Used by GYP. return ' '.join(output_paths) if __name__ == '__main__': DoMain(sys.argv[1:])