#!/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_CPP_FILE_PATH = GENERATED_LIB + '/cpp/EnumForVehicleProperty.h' 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' ANNOTATIONS_CPP_FILE_PATH = GENERATED_LIB + '/cpp/AnnotationsForVehicleProperty.h' ANNOTATIONS_JAVA_FILE_PATH = GENERATED_LIB + '/java/AnnotationsForVehicleProperty.java' SCRIPT_PATH = 'hardware/interfaces/automotive/vehicle/tools/generate_annotation_enums.py' TAB = ' ' RE_ENUM_START = re.compile(r'\s*enum VehicleProperty \{') RE_ENUM_END = re.compile(r'\s*\}\;') RE_COMMENT_BEGIN = re.compile(r'\s*\/\*\*?') RE_COMMENT_END = re.compile(r'\s*\*\/') RE_CHANGE_MODE = re.compile(r'\s*\* @change_mode (\S+)\s*') RE_VERSION = re.compile(r'\s*\* @version (\S+)\s*') RE_ACCESS = re.compile(r'\s*\* @access (\S+)\s*') RE_DATA_ENUM = re.compile(r'\s*\* @data_enum (\S+)\s*') RE_UNIT = re.compile(r'\s*\* @unit (\S+)\s+') RE_VALUE = re.compile(r'\s*(\w+)\s*=(.*)') RE_ANNOTATION = re.compile(r'\s*\* @(\S+)\s*') SUPPORTED_ANNOTATIONS = ['change_mode', 'access', 'unit', 'data_enum', 'data_enum_bit_flags', 'version', 'require_min_max_supported_value', 'require_supported_values_list', 'legacy_supported_values_in_config'] # Non static data_enum properties that do not require supported values list. # These properties are either deprecated or for internal use only. ENUM_PROPERTIES_WITHOUT_SUPPORTED_VALUES = [ # deprecated 'TURN_SIGNAL_STATE', # The supported values are exposed through HVAC_FAN_DIRECTION_AVAILABLE 'HVAC_FAN_DIRECTION', # Internal use only 'HW_ROTARY_INPUT', # Internal use only 'HW_CUSTOM_INPUT', # Internal use only 'SHUTDOWN_REQUEST', # Internal use only 'CAMERA_SERVICE_CURRENT_STATE' ] LICENSE = """/* * Copyright (C) 2025 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_FORMATTER = """#pragma once #include #include #include namespace aidl::android::hardware::automotive::vehicle {{ std::unordered_map ChangeModeForVehicleProperty = {{ {0} }}; }} // aidl::android::hardware::automotive::vehicle """ ACCESS_CPP_FORMATTER = """#pragma once #include #include #include namespace aidl::android::hardware::automotive::vehicle {{ // This map represents the default access mode for each property. std::unordered_map DefaultAccessForVehicleProperty = {{ {0} }}; // This map represents the allowed access modes for each property. std::unordered_map> AllowedAccessForVehicleProperty = {{ {1} }}; }} // aidl::android::hardware::automotive::vehicle """ VERSION_CPP_FORMATTER = """#pragma once #include #include namespace aidl::android::hardware::automotive::vehicle {{ std::unordered_map VersionForVehicleProperty = {{ {0} }}; }} // aidl::android::hardware::automotive::vehicle """ ANNOTATIONS_CPP_FORMATTER = """#pragma once #include #include #include #include namespace aidl::android::hardware::automotive::vehicle {{ std::unordered_map> AnnotationsForVehicleProperty = {{ {0} }}; }} // aidl::android::hardware::automotive::vehicle """ ENUM_CPP_FORMATTER = """#pragma once #define addSupportedValues(EnumType) \\ {{ \\ constexpr auto values = ndk::internal::enum_values; \\ for (size_t i = 0; i < values.size(); i++) {{ \\ supportedValues.insert(static_cast(values[i])); \\ }} \\ }} #include #include #include namespace aidl::android::hardware::automotive::vehicle {{ std::unordered_set getSupportedEnumValuesForProperty(VehicleProperty propertyId) {{ std::unordered_set supportedValues; switch (propertyId) {{ {0} default: // Do nothing. break; }} return supportedValues; }} }} // aidl::android::hardware::automotive::vehicle """ ENUM_CPP_SWITCH_CASE_FORMATTER = """ case {0}: {1} break; """ CHANGE_MODE_JAVA_FORMATTER = """package android.hardware.automotive.vehicle; import java.util.Map; public final class ChangeModeForVehicleProperty {{ public static final Map values = Map.ofEntries( {0} ); }} """ ACCESS_JAVA_FORMATTER = """package android.hardware.automotive.vehicle; import java.util.Map; public final class AccessForVehicleProperty {{ public static final Map values = Map.ofEntries( {0} ); }} """ ENUM_JAVA_FORMATTER = """package android.hardware.automotive.vehicle; import java.util.List; import java.util.Map; public final class EnumForVehicleProperty {{ public static final Map>> values = Map.ofEntries( {0} ); }} """ UNITS_JAVA_FORMATTER = """package android.hardware.automotive.vehicle; import java.util.Map; public final class UnitsForVehicleProperty {{ public static final Map values = Map.ofEntries( {0} ); }} """ ANNOTATIONS_JAVA_FORMATTER = """package android.hardware.automotive.vehicle; import java.util.Set; import java.util.Map; public final class AnnotationsForVehicleProperty {{ public static final Map> values = Map.ofEntries( {0} ); }} """ 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 # Use a set to avoid duplicate annotation. self.annotations = set() 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() # Use an array so that we could modify the string in parseComment. description = [''] continue if RE_COMMENT_END.match(line): in_comment = False if in_comment: # We will update the string in description in this function. self.parseComment(line, config, description) 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) if ('data_enum' in config.annotations and 'require_supported_values_list' not in config.annotations and config.change_mode != 'STATIC' and prop_name not in ENUM_PROPERTIES_WITHOUT_SUPPORTED_VALUES): raise Exception( 'The property: ' + prop_name + ' has @data_enum ' 'annotation but does not have @require_supported_values_list' ', either add the annotation or add the property name to ' 'ENUM_PROPERTIES_WITHOUT_SUPPORTED_VALUES in ' 'generate_annotation_enums.py') config.name = prop_name configs.append(config) self.configs = configs def parseComment(self, line, config, description): match_annotation = RE_ANNOTATION.match(line) if match_annotation: annotation = match_annotation.group(1) if annotation not in SUPPORTED_ANNOTATIONS: raise Exception('Annotation: @' + annotation + " is not supported, typo?") config.annotations.add(annotation) match = RE_CHANGE_MODE.match(line) if match: config.change_mode = match.group(1).replace('VehiclePropertyChangeMode.', '') return match = RE_ACCESS.match(line) if match: config.access_modes.append(match.group(1).replace('VehiclePropertyAccess.', '')) return match = RE_UNIT.match(line) if match: config.unit_type = match.group(1) return match = RE_DATA_ENUM.match(line) if match: config.enum_types.append(match.group(1)) return 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) return 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[0] else: if description[0] != '': description[0] += ' ' description[0] += 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' def convert(self, output, formatter, cpp, field): """Converts the property config file to C++/Java output file.""" counter = 0 content = '' for config in self.configs: if field == 'change_mode': if cpp: value = "VehiclePropertyChangeMode::" + config.change_mode else: value = "VehiclePropertyChangeMode." + config.change_mode elif field == 'access_mode': if cpp: value = "VehiclePropertyAccess::" + config.access_modes[0] else: value = "VehiclePropertyAccess." + config.access_modes[0] elif field == 'enum_types': if len(config.enum_types) < 1: continue if cpp: switch_case = '' for index, enum_type in enumerate(config.enum_types): if index != 0: switch_case += '\n' switch_case += TAB + TAB + TAB + 'addSupportedValues({0})'.format(enum_type) content += ENUM_CPP_SWITCH_CASE_FORMATTER.format( 'VehicleProperty::' + config.name, switch_case) continue else: value = "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: value = config.unit_type elif field == 'version': if cpp: value = config.version elif field == 'annotations': if len(config.annotations) < 1: continue joined_annotation_strings = ', '.join(['"' + annotation + '"' for annotation in sorted(config.annotations)]) if cpp: value = "{" + joined_annotation_strings + "}" else: value = "Set.of(" + joined_annotation_strings + ")" else: raise Exception('Unknown field: ' + field) if counter != 0: content += '\n' if cpp: content += (TAB + TAB + '{VehicleProperty::' + config.name + ', ' + value + '},') else: content += (TAB + TAB + 'Map.entry(VehicleProperty.' + config.name + ', ' + value + '),') counter += 1 # Remove the additional ',' at the end for the Java file. if not cpp: content = content[:-1] if field != 'access_mode' or not cpp: content = LICENSE + formatter.format(content) else: content2 = '' counter = 0 for config in self.configs: if counter != 0: content2 += '\n' value = ', '. join(['VehiclePropertyAccess::' + access_mode for access_mode in config.access_modes]) content2 += TAB + TAB + '{{VehicleProperty::{0}, {{{1}}}}},'.format(config.name, value) counter += 1 content = LICENSE + formatter.format(content, content2) 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_formatter = None self.java_formatter = 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 setCppFormatter(self, cpp_formatter): self.cpp_formatter = cpp_formatter def setJavaFormatter(self, java_formatter): self.java_formatter = java_formatter 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_formatter, 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_formatter, 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.setCppFormatter(CHANGE_MODE_CPP_FORMATTER) change_mode.setJavaFormatter(CHANGE_MODE_JAVA_FORMATTER) 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.setCppFormatter(ACCESS_CPP_FORMATTER) access_mode.setJavaFormatter(ACCESS_JAVA_FORMATTER) generated_files.append(access_mode) enum_types = GeneratedFile('enum_types') enum_types.setCppFilePath(os.path.join(android_top, ENUM_CPP_FILE_PATH)) enum_types.setJavaFilePath(os.path.join(android_top, ENUM_JAVA_FILE_PATH)) enum_types.setJavaFormatter(ENUM_JAVA_FORMATTER) enum_types.setCppFormatter(ENUM_CPP_FORMATTER) generated_files.append(enum_types) unit_type = GeneratedFile('unit_type') unit_type.setJavaFilePath(os.path.join(android_top, UNITS_JAVA_FILE_PATH)) unit_type.setJavaFormatter(UNITS_JAVA_FORMATTER) generated_files.append(unit_type) version = GeneratedFile('version') version.setCppFilePath(os.path.join(android_top, VERSION_CPP_FILE_PATH)) version.setCppFormatter(VERSION_CPP_FORMATTER) generated_files.append(version) annotations = GeneratedFile('annotations') annotations.setCppFilePath(os.path.join(android_top, ANNOTATIONS_CPP_FILE_PATH)) annotations.setJavaFilePath(os.path.join(android_top, ANNOTATIONS_JAVA_FILE_PATH)) annotations.setCppFormatter(ANNOTATIONS_CPP_FORMATTER) annotations.setJavaFormatter(ANNOTATIONS_JAVA_FORMATTER) generated_files.append(annotations) 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()