#!/usr/bin/python3 # Copyright (C) 2022 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """A script to generate Java files and CPP header files based on annotations in VehicleProperty.aidl Need ANDROID_BUILD_TOP environmental variable to be set. This script will update ChangeModeForVehicleProperty.h and AccessForVehicleProperty.h under generated_lib/version/cpp and ChangeModeForVehicleProperty.java, AccessForVehicleProperty.java, EnumForVehicleProperty.java UnitsForVehicleProperty.java under generated_lib/version/java. Usage: $ python generate_annotation_enums.py """ import argparse import filecmp import os import re import sys import tempfile # Keep this updated with the latest in-development property version. PROPERTY_VERSION = '4' PROP_AIDL_FILE_PATH = ('hardware/interfaces/automotive/vehicle/aidl_property/android/hardware/' + 'automotive/vehicle/VehicleProperty.aidl') GENERATED_LIB = ('hardware/interfaces/automotive/vehicle/aidl/generated_lib/' + PROPERTY_VERSION + '/') CHANGE_MODE_CPP_FILE_PATH = GENERATED_LIB + '/cpp/ChangeModeForVehicleProperty.h' ACCESS_CPP_FILE_PATH = GENERATED_LIB + '/cpp/AccessForVehicleProperty.h' CHANGE_MODE_JAVA_FILE_PATH = GENERATED_LIB + '/java/ChangeModeForVehicleProperty.java' ACCESS_JAVA_FILE_PATH = GENERATED_LIB + '/java/AccessForVehicleProperty.java' ENUM_JAVA_FILE_PATH = GENERATED_LIB + '/java/EnumForVehicleProperty.java' UNITS_JAVA_FILE_PATH = GENERATED_LIB + '/java/UnitsForVehicleProperty.java' VERSION_CPP_FILE_PATH = GENERATED_LIB + '/cpp/VersionForVehicleProperty.h' SCRIPT_PATH = 'hardware/interfaces/automotive/vehicle/tools/generate_annotation_enums.py' TAB = ' ' RE_ENUM_START = re.compile('\s*enum VehicleProperty \{') RE_ENUM_END = re.compile('\s*\}\;') RE_COMMENT_BEGIN = re.compile('\s*\/\*\*?') RE_COMMENT_END = re.compile('\s*\*\/') RE_CHANGE_MODE = re.compile('\s*\* @change_mode (\S+)\s*') RE_VERSION = re.compile('\s*\* @version (\S+)\s*') RE_ACCESS = re.compile('\s*\* @access (\S+)\s*') RE_DATA_ENUM = re.compile('\s*\* @data_enum (\S+)\s*') RE_UNIT = re.compile('\s*\* @unit (\S+)\s+') RE_VALUE = re.compile('\s*(\w+)\s*=(.*)') LICENSE = """/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * DO NOT EDIT MANUALLY!!! * * Generated by tools/generate_annotation_enums.py. */ // clang-format off """ CHANGE_MODE_CPP_HEADER = """#pragma once #include #include #include namespace aidl { namespace android { namespace hardware { namespace automotive { namespace vehicle { std::unordered_map ChangeModeForVehicleProperty = { """ CPP_FOOTER = """ }; } // namespace vehicle } // namespace automotive } // namespace hardware } // namespace android } // aidl """ ACCESS_CPP_HEADER = """#pragma once #include #include #include namespace aidl { namespace android { namespace hardware { namespace automotive { namespace vehicle { std::unordered_map AccessForVehicleProperty = { """ VERSION_CPP_HEADER = """#pragma once #include #include namespace aidl { namespace android { namespace hardware { namespace automotive { namespace vehicle { std::unordered_map VersionForVehicleProperty = { """ CHANGE_MODE_JAVA_HEADER = """package android.hardware.automotive.vehicle; import java.util.Map; public final class ChangeModeForVehicleProperty { public static final Map values = Map.ofEntries( """ JAVA_FOOTER = """ ); } """ ACCESS_JAVA_HEADER = """package android.hardware.automotive.vehicle; import java.util.Map; public final class AccessForVehicleProperty { public static final Map values = Map.ofEntries( """ ENUM_JAVA_HEADER = """package android.hardware.automotive.vehicle; import java.util.List; import java.util.Map; public final class EnumForVehicleProperty { public static final Map>> values = Map.ofEntries( """ UNITS_JAVA_HEADER = """package android.hardware.automotive.vehicle; import java.util.Map; public final class UnitsForVehicleProperty { public static final Map values = Map.ofEntries( """ class PropertyConfig: """Represents one VHAL property definition in VehicleProperty.aidl.""" def __init__(self): self.name = None self.description = None self.comment = None self.change_mode = None self.access_modes = [] self.enum_types = [] self.unit_type = None self.version = None def __repr__(self): return self.__str__() def __str__(self): return ('PropertyConfig{{' + 'name: {}, description: {}, change_mode: {}, access_modes: {}, enum_types: {}' + ', unit_type: {}, version: {}, comment: {}}}').format(self.name, self.description, self.change_mode, self.access_modes, self.enum_types, self.unit_type, self.version, self.comment) class FileParser: def __init__(self): self.configs = None def parseFile(self, input_file): """Parses the input VehicleProperty.aidl file into a list of property configs.""" processing = False in_comment = False configs = [] config = None with open(input_file, 'r') as f: for line in f.readlines(): if RE_ENUM_START.match(line): processing = True elif RE_ENUM_END.match(line): processing = False if not processing: continue if RE_COMMENT_BEGIN.match(line): in_comment = True config = PropertyConfig() description = '' continue if RE_COMMENT_END.match(line): in_comment = False if in_comment: match = RE_CHANGE_MODE.match(line) if match: config.change_mode = match.group(1).replace('VehiclePropertyChangeMode.', '') continue match = RE_ACCESS.match(line) if match: config.access_modes.append(match.group(1).replace('VehiclePropertyAccess.', '')) continue match = RE_UNIT.match(line) if match: config.unit_type = match.group(1) continue match = RE_DATA_ENUM.match(line) if match: config.enum_types.append(match.group(1)) continue match = RE_VERSION.match(line) if match: if config.version != None: raise Exception('Duplicate version annotation for property: ' + prop_name) config.version = match.group(1) continue sline = line.strip() if sline.startswith('*'): # Remove the '*'. sline = sline[1:].strip() if not config.description: # We reach an empty line of comment, the description part is ending. if sline == '': config.description = description else: if description != '': description += ' ' description += sline else: if not config.comment: if sline != '': # This is the first line for comment. config.comment = sline else: if sline != '': # Concat this line with the previous line's comment with a space. config.comment += ' ' + sline else: # Treat empty line comment as a new line. config.comment += '\n' else: match = RE_VALUE.match(line) if match: prop_name = match.group(1) if prop_name == 'INVALID': continue if not config.change_mode: raise Exception( 'No change_mode annotation for property: ' + prop_name) if not config.access_modes: raise Exception( 'No access_mode annotation for property: ' + prop_name) if not config.version: raise Exception( 'no version annotation for property: ' + prop_name) config.name = prop_name configs.append(config) self.configs = configs def convert(self, output, header, footer, cpp, field): """Converts the property config file to C++/Java output file.""" counter = 0 content = LICENSE + header for config in self.configs: if field == 'change_mode': if cpp: annotation = "VehiclePropertyChangeMode::" + config.change_mode else: annotation = "VehiclePropertyChangeMode." + config.change_mode elif field == 'access_mode': if cpp: annotation = "VehiclePropertyAccess::" + config.access_modes[0] else: annotation = "VehiclePropertyAccess." + config.access_modes[0] elif field == 'enum_types': if len(config.enum_types) < 1: continue; if not cpp: annotation = "List.of(" + ', '.join([class_name + ".class" for class_name in config.enum_types]) + ")" elif field == 'unit_type': if not config.unit_type: continue if not cpp: annotation = config.unit_type elif field == 'version': if cpp: annotation = config.version else: raise Exception('Unknown field: ' + field) if counter != 0: content += '\n' if cpp: content += (TAB + TAB + '{VehicleProperty::' + config.name + ', ' + annotation + '},') else: content += (TAB + TAB + 'Map.entry(VehicleProperty.' + config.name + ', ' + annotation + '),') counter += 1 # Remove the additional ',' at the end for the Java file. if not cpp: content = content[:-1] content += footer with open(output, 'w') as f: f.write(content) def outputAsCsv(self, output): content = 'name,description,change mode,access mode,enum type,unit type,comment\n' for config in self.configs: enum_types = None if not config.enum_types: enum_types = '/' else: enum_types = '/'.join(config.enum_types) unit_type = config.unit_type if not unit_type: unit_type = '/' access_modes = '' comment = config.comment if not comment: comment = '' content += '"{}","{}","{}","{}","{}","{}", "{}"\n'.format( config.name, # Need to escape quote as double quote. config.description.replace('"', '""'), config.change_mode, '/'.join(config.access_modes), enum_types, unit_type, comment.replace('"', '""')) with open(output, 'w+') as f: f.write(content) def createTempFile(): f = tempfile.NamedTemporaryFile(delete=False); f.close(); return f.name class GeneratedFile: def __init__(self, type): self.type = type self.cpp_file_path = None self.java_file_path = None self.cpp_header = None self.java_header = None self.cpp_footer = None self.java_footer = None self.cpp_output_file = None self.java_output_file = None def setCppFilePath(self, cpp_file_path): self.cpp_file_path = cpp_file_path def setJavaFilePath(self, java_file_path): self.java_file_path = java_file_path def setCppHeader(self, cpp_header): self.cpp_header = cpp_header def setCppFooter(self, cpp_footer): self.cpp_footer = cpp_footer def setJavaHeader(self, java_header): self.java_header = java_header def setJavaFooter(self, java_footer): self.java_footer = java_footer def convert(self, file_parser, check_only, temp_files): if self.cpp_file_path: output_file = GeneratedFile._getOutputFile(self.cpp_file_path, check_only, temp_files) file_parser.convert(output_file, self.cpp_header, self.cpp_footer, True, self.type) self.cpp_output_file = output_file if self.java_file_path: output_file = GeneratedFile._getOutputFile(self.java_file_path, check_only, temp_files) file_parser.convert(output_file, self.java_header, self.java_footer, False, self.type) self.java_output_file = output_file def cmp(self): if self.cpp_file_path: if not filecmp.cmp(self.cpp_output_file, self.cpp_file_path): return False if self.java_file_path: if not filecmp.cmp(self.java_output_file, self.java_file_path): return False return True @staticmethod def _getOutputFile(file_path, check_only, temp_files): if not check_only: return file_path temp_file = createTempFile() temp_files.append(temp_file) return temp_file def main(): parser = argparse.ArgumentParser( description='Generate Java and C++ enums based on annotations in VehicleProperty.aidl') parser.add_argument('--android_build_top', required=False, help='Path to ANDROID_BUILD_TOP') parser.add_argument('--preupload_files', nargs='*', required=False, help='modified files') parser.add_argument('--check_only', required=False, action='store_true', help='only check whether the generated files need update') parser.add_argument('--output_csv', required=False, help='Path to the parsing result in CSV style, useful for doc generation') args = parser.parse_args(); android_top = None output_folder = None if args.android_build_top: android_top = args.android_build_top vehiclePropertyUpdated = False for preuload_file in args.preupload_files: if preuload_file.endswith('VehicleProperty.aidl'): vehiclePropertyUpdated = True break if not vehiclePropertyUpdated: return else: android_top = os.environ['ANDROID_BUILD_TOP'] if not android_top: print('ANDROID_BUILD_TOP is not in environmental variable, please run source and lunch ' + 'at the android root') aidl_file = os.path.join(android_top, PROP_AIDL_FILE_PATH) f = FileParser(); f.parseFile(aidl_file) if args.output_csv: f.outputAsCsv(args.output_csv) return generated_files = [] change_mode = GeneratedFile('change_mode') change_mode.setCppFilePath(os.path.join(android_top, CHANGE_MODE_CPP_FILE_PATH)) change_mode.setJavaFilePath(os.path.join(android_top, CHANGE_MODE_JAVA_FILE_PATH)) change_mode.setCppHeader(CHANGE_MODE_CPP_HEADER) change_mode.setCppFooter(CPP_FOOTER) change_mode.setJavaHeader(CHANGE_MODE_JAVA_HEADER) change_mode.setJavaFooter(JAVA_FOOTER) generated_files.append(change_mode) access_mode = GeneratedFile('access_mode') access_mode.setCppFilePath(os.path.join(android_top, ACCESS_CPP_FILE_PATH)) access_mode.setJavaFilePath(os.path.join(android_top, ACCESS_JAVA_FILE_PATH)) access_mode.setCppHeader(ACCESS_CPP_HEADER) access_mode.setCppFooter(CPP_FOOTER) access_mode.setJavaHeader(ACCESS_JAVA_HEADER) access_mode.setJavaFooter(JAVA_FOOTER) generated_files.append(access_mode) enum_types = GeneratedFile('enum_types') enum_types.setJavaFilePath(os.path.join(android_top, ENUM_JAVA_FILE_PATH)) enum_types.setJavaHeader(ENUM_JAVA_HEADER) enum_types.setJavaFooter(JAVA_FOOTER) generated_files.append(enum_types) unit_type = GeneratedFile('unit_type') unit_type.setJavaFilePath(os.path.join(android_top, UNITS_JAVA_FILE_PATH)) unit_type.setJavaHeader(UNITS_JAVA_HEADER) unit_type.setJavaFooter(JAVA_FOOTER) generated_files.append(unit_type) version = GeneratedFile('version') version.setCppFilePath(os.path.join(android_top, VERSION_CPP_FILE_PATH)) version.setCppHeader(VERSION_CPP_HEADER) version.setCppFooter(CPP_FOOTER) generated_files.append(version) temp_files = [] try: for generated_file in generated_files: generated_file.convert(f, args.check_only, temp_files) if not args.check_only: return for generated_file in generated_files: if not generated_file.cmp(): print('The generated enum files for VehicleProperty.aidl requires update, ') print('Run \npython ' + android_top + '/' + SCRIPT_PATH) sys.exit(1) except Exception as e: print('Error parsing VehicleProperty.aidl') print(e) sys.exit(1) finally: for file in temp_files: os.remove(file) if __name__ == '__main__': main()