| #!/usr/bin/env python3 | |
| # | |
| # 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 textwrap | |
| import zipfile | |
| from util import build_utils | |
| from util import java_cpp_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_ALLOWLIST = [ | |
| '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, comments=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.comments = collections.OrderedDict(comments 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 | |
| def AppendEntryComment(self, key, value): | |
| if key in self.comments: | |
| raise Exception('Multiple definitions of key %s found.' % key) | |
| self.comments[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() | |
| self._NormalizeNames() | |
| 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_ALLOWLIST: | |
| raise Exception('Fixed type %s for enum %s not in allowlist.' % | |
| (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.items(): | |
| 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: | |
| shout_case = self.original_enum_name | |
| shout_case = re.sub('(?!^)([A-Z]+)', r'_\1', shout_case).upper() | |
| shout_case += '_' | |
| prefixes = [shout_case, self.original_enum_name, | |
| 'k' + self.original_enum_name] | |
| for prefix in prefixes: | |
| if all([w.startswith(prefix) for w in self.entries.keys()]): | |
| prefix_to_strip = prefix | |
| break | |
| else: | |
| prefix_to_strip = '' | |
| def StripEntries(entries): | |
| ret = collections.OrderedDict() | |
| for k, v in entries.items(): | |
| stripped_key = k.replace(prefix_to_strip, '', 1) | |
| if isinstance(v, str): | |
| stripped_value = v.replace(prefix_to_strip, '') | |
| else: | |
| stripped_value = v | |
| ret[stripped_key] = stripped_value | |
| return ret | |
| self.entries = StripEntries(self.entries) | |
| self.comments = StripEntries(self.comments) | |
| def _NormalizeNames(self): | |
| self.entries = _TransformKeys(self.entries, java_cpp_utils.KCamelToShouty) | |
| self.comments = _TransformKeys(self.comments, java_cpp_utils.KCamelToShouty) | |
| def _TransformKeys(d, func): | |
| """Normalize keys in |d| and update references to old keys in |d| values.""" | |
| keys_map = {k: func(k) for k in d} | |
| ret = collections.OrderedDict() | |
| for k, v in d.items(): | |
| # Need to transform values as well when the entry value was explicitly set | |
| # (since it could contain references to other enum entry values). | |
| if isinstance(v, str): | |
| # First check if a full replacement is available. This avoids issues when | |
| # one key is a substring of another. | |
| if v in d: | |
| v = keys_map[v] | |
| else: | |
| for old_key, new_key in keys_map.items(): | |
| v = v.replace(old_key, new_key) | |
| ret[keys_map[k]] = v | |
| return ret | |
| 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*//\s*([^\n]*)') | |
| 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_error_re = re.compile(r'^\s*//\s+GENERATED_JAVA_(\w+)\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*') | |
| enum_single_line_re = re.compile( | |
| r'^\s*(?:\[cpp.*\])?\s*enum.*{(?P<enum_entries>.*)}.*$') | |
| def __init__(self, lines, path=''): | |
| self._lines = lines | |
| self._path = path | |
| self._enum_definitions = [] | |
| self._in_enum = False | |
| self._current_definition = None | |
| self._current_comments = [] | |
| self._generator_directives = DirectiveSet() | |
| self._multi_line_generator_directive = None | |
| self._current_enum_entry = '' | |
| 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.multi_line_comment_start_re.match(line): | |
| raise Exception('Multi-line comments in enums are not supported in ' + | |
| self._path) | |
| enum_comment = HeaderParser.single_line_comment_re.match(line) | |
| if enum_comment: | |
| comment = enum_comment.groups()[0] | |
| if comment: | |
| self._current_comments.append(comment) | |
| elif HeaderParser.enum_end_re.match(line): | |
| self._FinalizeCurrentEnumDefinition() | |
| else: | |
| self._AddToCurrentEnumEntry(line) | |
| if ',' in line: | |
| self._ParseCurrentEnumEntry() | |
| def _ParseSingleLineEnum(self, line): | |
| for entry in line.split(','): | |
| self._AddToCurrentEnumEntry(entry) | |
| self._ParseCurrentEnumEntry() | |
| self._FinalizeCurrentEnumDefinition() | |
| def _ParseCurrentEnumEntry(self): | |
| if not self._current_enum_entry: | |
| return | |
| enum_entry = HeaderParser.enum_line_re.match(self._current_enum_entry) | |
| if not enum_entry: | |
| raise Exception('Unexpected error while attempting to parse %s as enum ' | |
| 'entry.' % self._current_enum_entry) | |
| enum_key = enum_entry.groups()[0] | |
| enum_value = enum_entry.groups()[2] | |
| self._current_definition.AppendEntry(enum_key, enum_value) | |
| if self._current_comments: | |
| self._current_definition.AppendEntryComment( | |
| enum_key, ' '.join(self._current_comments)) | |
| self._current_comments = [] | |
| self._current_enum_entry = '' | |
| def _AddToCurrentEnumEntry(self, line): | |
| self._current_enum_entry += ' ' + line.strip() | |
| def _FinalizeCurrentEnumDefinition(self): | |
| if self._current_enum_entry: | |
| self._ParseCurrentEnumEntry() | |
| self._ApplyGeneratorDirectives() | |
| self._current_definition.Finalize() | |
| self._enum_definitions.append(self._current_definition) | |
| self._current_definition = None | |
| self._in_enum = False | |
| 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_error = HeaderParser.generator_error_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)) | |
| single_line_enum = HeaderParser.enum_single_line_re.match(line) | |
| if generator_directive_error: | |
| raise Exception('Malformed directive declaration in ' + self._path + | |
| '. Use () for multi-line directives. E.g.\n' + | |
| '// GENERATED_JAVA_ENUM_PACKAGE: (\n' + | |
| '// foo.package)') | |
| elif 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 or single_line_enum: | |
| 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 | |
| if single_line_enum: | |
| self._ParseSingleLineEnum(single_line_enum.group('enum_entries')) | |
| 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: | |
| output_path = java_cpp_utils.GetJavaFilePath(enum_definition.enum_package, | |
| enum_definition.class_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}; | |
| import androidx.annotation.IntDef; | |
| import java.lang.annotation.Retention; | |
| import java.lang.annotation.RetentionPolicy; | |
| @IntDef({ | |
| ${INT_DEF} | |
| }) | |
| @Retention(RetentionPolicy.SOURCE) | |
| public @interface ${CLASS_NAME} { | |
| ${ENUM_ENTRIES} | |
| } | |
| """) | |
| enum_template = Template(' int ${NAME} = ${VALUE};') | |
| enum_entries_string = [] | |
| enum_names = [] | |
| for enum_name, enum_value in enum_definition.entries.items(): | |
| values = { | |
| 'NAME': enum_name, | |
| 'VALUE': enum_value, | |
| } | |
| enum_comments = enum_definition.comments.get(enum_name) | |
| if enum_comments: | |
| enum_comments_indent = ' * ' | |
| comments_line_wrapper = textwrap.TextWrapper( | |
| initial_indent=enum_comments_indent, | |
| subsequent_indent=enum_comments_indent, | |
| width=100) | |
| enum_entries_string.append(' /**') | |
| enum_entries_string.append('\n'.join( | |
| comments_line_wrapper.wrap(enum_comments))) | |
| enum_entries_string.append(' */') | |
| enum_entries_string.append(enum_template.substitute(values)) | |
| if enum_name != "NUM_ENTRIES": | |
| enum_names.append(enum_definition.class_name + '.' + enum_name) | |
| enum_entries_string = '\n'.join(enum_entries_string) | |
| enum_names_indent = ' ' * 4 | |
| wrapper = textwrap.TextWrapper(initial_indent = enum_names_indent, | |
| subsequent_indent = enum_names_indent, | |
| width = 100) | |
| enum_names_string = '\n'.join(wrapper.wrap(', '.join(enum_names))) | |
| values = { | |
| 'CLASS_NAME': enum_definition.class_name, | |
| 'ENUM_ENTRIES': enum_entries_string, | |
| 'PACKAGE': enum_definition.enum_package, | |
| 'INT_DEF': enum_names_string, | |
| 'SCRIPT_NAME': java_cpp_utils.GetScriptName(), | |
| 'SOURCE_PATH': source_path, | |
| 'YEAR': str(date.today().year) | |
| } | |
| return template.substitute(values) | |
| def DoMain(argv): | |
| usage = 'usage: %prog [options] [output_dir] input_file(s)...' | |
| parser = optparse.OptionParser(usage=usage) | |
| parser.add_option('--srcjar', | |
| help='When specified, a .srcjar at the given path is ' | |
| 'created instead of individual .java files.') | |
| options, args = parser.parse_args(argv) | |
| if not args: | |
| parser.error('Need to specify at least one input file') | |
| input_paths = args | |
| with build_utils.AtomicOutput(options.srcjar) as f: | |
| with zipfile.ZipFile(f, 'w', zipfile.ZIP_STORED) as srcjar: | |
| for output_path, data in DoGenerate(input_paths): | |
| build_utils.AddToZipHermetic(srcjar, output_path, data=data) | |
| if __name__ == '__main__': | |
| DoMain(sys.argv[1:]) |