1#!/usr/bin/python3
2
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17"""A script to generate Java files and CPP header files based on annotations in VehicleProperty.aidl
18
19   Need ANDROID_BUILD_TOP environmental variable to be set. This script will update
20   ChangeModeForVehicleProperty.h and AccessForVehicleProperty.h under generated_lib/version/cpp and
21   ChangeModeForVehicleProperty.java, AccessForVehicleProperty.java, EnumForVehicleProperty.java
22   UnitsForVehicleProperty.java under generated_lib/version/java.
23
24   Usage:
25   $ python generate_annotation_enums.py
26"""
27import argparse
28import filecmp
29import os
30import re
31import sys
32import tempfile
33
34# Keep this updated with the latest in-development property version.
35PROPERTY_VERSION = '4'
36
37PROP_AIDL_FILE_PATH = ('hardware/interfaces/automotive/vehicle/aidl_property/android/hardware/' +
38    'automotive/vehicle/VehicleProperty.aidl')
39GENERATED_LIB = ('hardware/interfaces/automotive/vehicle/aidl/generated_lib/' + PROPERTY_VERSION +
40        '/')
41CHANGE_MODE_CPP_FILE_PATH = GENERATED_LIB + '/cpp/ChangeModeForVehicleProperty.h'
42ACCESS_CPP_FILE_PATH = GENERATED_LIB + '/cpp/AccessForVehicleProperty.h'
43CHANGE_MODE_JAVA_FILE_PATH = GENERATED_LIB + '/java/ChangeModeForVehicleProperty.java'
44ACCESS_JAVA_FILE_PATH = GENERATED_LIB + '/java/AccessForVehicleProperty.java'
45ENUM_JAVA_FILE_PATH = GENERATED_LIB + '/java/EnumForVehicleProperty.java'
46UNITS_JAVA_FILE_PATH = GENERATED_LIB + '/java/UnitsForVehicleProperty.java'
47VERSION_CPP_FILE_PATH = GENERATED_LIB + '/cpp/VersionForVehicleProperty.h'
48SCRIPT_PATH = 'hardware/interfaces/automotive/vehicle/tools/generate_annotation_enums.py'
49
50TAB = '    '
51RE_ENUM_START = re.compile('\s*enum VehicleProperty \{')
52RE_ENUM_END = re.compile('\s*\}\;')
53RE_COMMENT_BEGIN = re.compile('\s*\/\*\*?')
54RE_COMMENT_END = re.compile('\s*\*\/')
55RE_CHANGE_MODE = re.compile('\s*\* @change_mode (\S+)\s*')
56RE_VERSION = re.compile('\s*\* @version (\S+)\s*')
57RE_ACCESS = re.compile('\s*\* @access (\S+)\s*')
58RE_DATA_ENUM = re.compile('\s*\* @data_enum (\S+)\s*')
59RE_UNIT = re.compile('\s*\* @unit (\S+)\s+')
60RE_VALUE = re.compile('\s*(\w+)\s*=(.*)')
61
62LICENSE = """/*
63 * Copyright (C) 2023 The Android Open Source Project
64 *
65 * Licensed under the Apache License, Version 2.0 (the "License");
66 * you may not use this file except in compliance with the License.
67 * You may obtain a copy of the License at
68 *
69 *      http://www.apache.org/licenses/LICENSE-2.0
70 *
71 * Unless required by applicable law or agreed to in writing, software
72 * distributed under the License is distributed on an "AS IS" BASIS,
73 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
74 * See the License for the specific language governing permissions and
75 * limitations under the License.
76 */
77
78/**
79 * DO NOT EDIT MANUALLY!!!
80 *
81 * Generated by tools/generate_annotation_enums.py.
82 */
83
84// clang-format off
85
86"""
87
88CHANGE_MODE_CPP_HEADER = """#pragma once
89
90#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>
91#include <aidl/android/hardware/automotive/vehicle/VehiclePropertyChangeMode.h>
92
93#include <unordered_map>
94
95namespace aidl {
96namespace android {
97namespace hardware {
98namespace automotive {
99namespace vehicle {
100
101std::unordered_map<VehicleProperty, VehiclePropertyChangeMode> ChangeModeForVehicleProperty = {
102"""
103
104CPP_FOOTER = """
105};
106
107}  // namespace vehicle
108}  // namespace automotive
109}  // namespace hardware
110}  // namespace android
111}  // aidl
112"""
113
114ACCESS_CPP_HEADER = """#pragma once
115
116#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>
117#include <aidl/android/hardware/automotive/vehicle/VehiclePropertyAccess.h>
118
119#include <unordered_map>
120
121namespace aidl {
122namespace android {
123namespace hardware {
124namespace automotive {
125namespace vehicle {
126
127std::unordered_map<VehicleProperty, VehiclePropertyAccess> AccessForVehicleProperty = {
128"""
129
130VERSION_CPP_HEADER = """#pragma once
131
132#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>
133
134#include <unordered_map>
135
136namespace aidl {
137namespace android {
138namespace hardware {
139namespace automotive {
140namespace vehicle {
141
142std::unordered_map<VehicleProperty, int32_t> VersionForVehicleProperty = {
143"""
144
145CHANGE_MODE_JAVA_HEADER = """package android.hardware.automotive.vehicle;
146
147import java.util.Map;
148
149public final class ChangeModeForVehicleProperty {
150
151    public static final Map<Integer, Integer> values = Map.ofEntries(
152"""
153
154JAVA_FOOTER = """
155    );
156
157}
158"""
159
160ACCESS_JAVA_HEADER = """package android.hardware.automotive.vehicle;
161
162import java.util.Map;
163
164public final class AccessForVehicleProperty {
165
166    public static final Map<Integer, Integer> values = Map.ofEntries(
167"""
168
169ENUM_JAVA_HEADER = """package android.hardware.automotive.vehicle;
170
171import java.util.List;
172import java.util.Map;
173
174public final class EnumForVehicleProperty {
175
176    public static final Map<Integer, List<Class<?>>> values = Map.ofEntries(
177"""
178
179UNITS_JAVA_HEADER = """package android.hardware.automotive.vehicle;
180
181import java.util.Map;
182
183public final class UnitsForVehicleProperty {
184
185    public static final Map<Integer, Integer> values = Map.ofEntries(
186"""
187
188
189class PropertyConfig:
190    """Represents one VHAL property definition in VehicleProperty.aidl."""
191
192    def __init__(self):
193        self.name = None
194        self.description = None
195        self.comment = None
196        self.change_mode = None
197        self.access_modes = []
198        self.enum_types = []
199        self.unit_type = None
200        self.version = None
201
202    def __repr__(self):
203        return self.__str__()
204
205    def __str__(self):
206        return ('PropertyConfig{{' +
207            'name: {}, description: {}, change_mode: {}, access_modes: {}, enum_types: {}' +
208            ', unit_type: {}, version: {}, comment: {}}}').format(self.name, self.description,
209                self.change_mode, self.access_modes, self.enum_types, self.unit_type,
210                self.version, self.comment)
211
212
213class FileParser:
214
215    def __init__(self):
216        self.configs = None
217
218    def parseFile(self, input_file):
219        """Parses the input VehicleProperty.aidl file into a list of property configs."""
220        processing = False
221        in_comment = False
222        configs = []
223        config = None
224        with open(input_file, 'r') as f:
225            for line in f.readlines():
226                if RE_ENUM_START.match(line):
227                    processing = True
228                elif RE_ENUM_END.match(line):
229                    processing = False
230                if not processing:
231                    continue
232                if RE_COMMENT_BEGIN.match(line):
233                    in_comment = True
234                    config = PropertyConfig()
235                    description = ''
236                    continue
237
238                if RE_COMMENT_END.match(line):
239                    in_comment = False
240                if in_comment:
241                    match = RE_CHANGE_MODE.match(line)
242                    if match:
243                        config.change_mode = match.group(1).replace('VehiclePropertyChangeMode.', '')
244                        continue
245                    match = RE_ACCESS.match(line)
246                    if match:
247                        config.access_modes.append(match.group(1).replace('VehiclePropertyAccess.', ''))
248                        continue
249                    match = RE_UNIT.match(line)
250                    if match:
251                        config.unit_type = match.group(1)
252                        continue
253                    match = RE_DATA_ENUM.match(line)
254                    if match:
255                        config.enum_types.append(match.group(1))
256                        continue
257                    match = RE_VERSION.match(line)
258                    if match:
259                        if config.version != None:
260                            raise Exception('Duplicate version annotation for property: ' + prop_name)
261                        config.version = match.group(1)
262                        continue
263
264                    sline = line.strip()
265                    if sline.startswith('*'):
266                        # Remove the '*'.
267                        sline = sline[1:].strip()
268
269                    if not config.description:
270                        # We reach an empty line of comment, the description part is ending.
271                        if sline == '':
272                            config.description = description
273                        else:
274                            if description != '':
275                                description += ' '
276                            description += sline
277                    else:
278                        if not config.comment:
279                            if sline != '':
280                                # This is the first line for comment.
281                                config.comment = sline
282                        else:
283                            if sline != '':
284                                # Concat this line with the previous line's comment with a space.
285                                config.comment += ' ' + sline
286                            else:
287                                # Treat empty line comment as a new line.
288                                config.comment += '\n'
289                else:
290                    match = RE_VALUE.match(line)
291                    if match:
292                        prop_name = match.group(1)
293                        if prop_name == 'INVALID':
294                            continue
295                        if not config.change_mode:
296                            raise Exception(
297                                    'No change_mode annotation for property: ' + prop_name)
298                        if not config.access_modes:
299                            raise Exception(
300                                    'No access_mode annotation for property: ' + prop_name)
301                        if not config.version:
302                            raise Exception(
303                                    'no version annotation for property: ' + prop_name)
304                        config.name = prop_name
305                        configs.append(config)
306
307        self.configs = configs
308
309    def convert(self, output, header, footer, cpp, field):
310        """Converts the property config file to C++/Java output file."""
311        counter = 0
312        content = LICENSE + header
313        for config in self.configs:
314            if field == 'change_mode':
315                if cpp:
316                    annotation = "VehiclePropertyChangeMode::" + config.change_mode
317                else:
318                    annotation = "VehiclePropertyChangeMode." + config.change_mode
319            elif field == 'access_mode':
320                if cpp:
321                    annotation = "VehiclePropertyAccess::" + config.access_modes[0]
322                else:
323                    annotation = "VehiclePropertyAccess." + config.access_modes[0]
324            elif field == 'enum_types':
325                if len(config.enum_types) < 1:
326                    continue;
327                if not cpp:
328                    annotation = "List.of(" + ', '.join([class_name + ".class" for class_name in config.enum_types]) + ")"
329            elif field == 'unit_type':
330                if not config.unit_type:
331                    continue
332                if not cpp:
333                    annotation = config.unit_type
334
335            elif field == 'version':
336                if cpp:
337                    annotation = config.version
338            else:
339                raise Exception('Unknown field: ' + field)
340            if counter != 0:
341                content += '\n'
342            if cpp:
343                content += (TAB + TAB + '{VehicleProperty::' + config.name + ', ' +
344                            annotation + '},')
345            else:
346                content += (TAB + TAB + 'Map.entry(VehicleProperty.' + config.name + ', ' +
347                            annotation + '),')
348            counter += 1
349
350        # Remove the additional ',' at the end for the Java file.
351        if not cpp:
352            content = content[:-1]
353
354        content += footer
355
356        with open(output, 'w') as f:
357            f.write(content)
358
359    def outputAsCsv(self, output):
360        content = 'name,description,change mode,access mode,enum type,unit type,comment\n'
361        for config in self.configs:
362            enum_types = None
363            if not config.enum_types:
364                enum_types = '/'
365            else:
366                enum_types = '/'.join(config.enum_types)
367            unit_type = config.unit_type
368            if not unit_type:
369                unit_type = '/'
370            access_modes = ''
371            comment = config.comment
372            if not comment:
373                comment = ''
374            content += '"{}","{}","{}","{}","{}","{}", "{}"\n'.format(
375                    config.name,
376                    # Need to escape quote as double quote.
377                    config.description.replace('"', '""'),
378                    config.change_mode,
379                    '/'.join(config.access_modes),
380                    enum_types,
381                    unit_type,
382                    comment.replace('"', '""'))
383
384        with open(output, 'w+') as f:
385            f.write(content)
386
387
388def createTempFile():
389    f = tempfile.NamedTemporaryFile(delete=False);
390    f.close();
391    return f.name
392
393
394class GeneratedFile:
395
396    def __init__(self, type):
397        self.type = type
398        self.cpp_file_path = None
399        self.java_file_path = None
400        self.cpp_header = None
401        self.java_header = None
402        self.cpp_footer = None
403        self.java_footer = None
404        self.cpp_output_file = None
405        self.java_output_file = None
406
407    def setCppFilePath(self, cpp_file_path):
408        self.cpp_file_path = cpp_file_path
409
410    def setJavaFilePath(self, java_file_path):
411        self.java_file_path = java_file_path
412
413    def setCppHeader(self, cpp_header):
414        self.cpp_header = cpp_header
415
416    def setCppFooter(self, cpp_footer):
417        self.cpp_footer = cpp_footer
418
419    def setJavaHeader(self, java_header):
420        self.java_header = java_header
421
422    def setJavaFooter(self, java_footer):
423        self.java_footer = java_footer
424
425    def convert(self, file_parser, check_only, temp_files):
426        if self.cpp_file_path:
427            output_file = GeneratedFile._getOutputFile(self.cpp_file_path, check_only, temp_files)
428            file_parser.convert(output_file, self.cpp_header, self.cpp_footer, True, self.type)
429            self.cpp_output_file = output_file
430
431        if self.java_file_path:
432            output_file = GeneratedFile._getOutputFile(self.java_file_path, check_only, temp_files)
433            file_parser.convert(output_file, self.java_header, self.java_footer, False, self.type)
434            self.java_output_file = output_file
435
436    def cmp(self):
437        if self.cpp_file_path:
438            if not filecmp.cmp(self.cpp_output_file, self.cpp_file_path):
439                return False
440
441        if self.java_file_path:
442            if not filecmp.cmp(self.java_output_file, self.java_file_path):
443                return False
444
445        return True
446
447    @staticmethod
448    def _getOutputFile(file_path, check_only, temp_files):
449        if not check_only:
450            return file_path
451
452        temp_file = createTempFile()
453        temp_files.append(temp_file)
454        return temp_file
455
456
457def main():
458    parser = argparse.ArgumentParser(
459            description='Generate Java and C++ enums based on annotations in VehicleProperty.aidl')
460    parser.add_argument('--android_build_top', required=False, help='Path to ANDROID_BUILD_TOP')
461    parser.add_argument('--preupload_files', nargs='*', required=False, help='modified files')
462    parser.add_argument('--check_only', required=False, action='store_true',
463            help='only check whether the generated files need update')
464    parser.add_argument('--output_csv', required=False,
465            help='Path to the parsing result in CSV style, useful for doc generation')
466    args = parser.parse_args();
467    android_top = None
468    output_folder = None
469    if args.android_build_top:
470        android_top = args.android_build_top
471        vehiclePropertyUpdated = False
472        for preuload_file in args.preupload_files:
473            if preuload_file.endswith('VehicleProperty.aidl'):
474                vehiclePropertyUpdated = True
475                break
476        if not vehiclePropertyUpdated:
477            return
478    else:
479        android_top = os.environ['ANDROID_BUILD_TOP']
480    if not android_top:
481        print('ANDROID_BUILD_TOP is not in environmental variable, please run source and lunch ' +
482            'at the android root')
483
484    aidl_file = os.path.join(android_top, PROP_AIDL_FILE_PATH)
485    f = FileParser();
486    f.parseFile(aidl_file)
487
488    if args.output_csv:
489        f.outputAsCsv(args.output_csv)
490        return
491
492    generated_files = []
493
494    change_mode = GeneratedFile('change_mode')
495    change_mode.setCppFilePath(os.path.join(android_top, CHANGE_MODE_CPP_FILE_PATH))
496    change_mode.setJavaFilePath(os.path.join(android_top, CHANGE_MODE_JAVA_FILE_PATH))
497    change_mode.setCppHeader(CHANGE_MODE_CPP_HEADER)
498    change_mode.setCppFooter(CPP_FOOTER)
499    change_mode.setJavaHeader(CHANGE_MODE_JAVA_HEADER)
500    change_mode.setJavaFooter(JAVA_FOOTER)
501    generated_files.append(change_mode)
502
503    access_mode = GeneratedFile('access_mode')
504    access_mode.setCppFilePath(os.path.join(android_top, ACCESS_CPP_FILE_PATH))
505    access_mode.setJavaFilePath(os.path.join(android_top, ACCESS_JAVA_FILE_PATH))
506    access_mode.setCppHeader(ACCESS_CPP_HEADER)
507    access_mode.setCppFooter(CPP_FOOTER)
508    access_mode.setJavaHeader(ACCESS_JAVA_HEADER)
509    access_mode.setJavaFooter(JAVA_FOOTER)
510    generated_files.append(access_mode)
511
512    enum_types = GeneratedFile('enum_types')
513    enum_types.setJavaFilePath(os.path.join(android_top, ENUM_JAVA_FILE_PATH))
514    enum_types.setJavaHeader(ENUM_JAVA_HEADER)
515    enum_types.setJavaFooter(JAVA_FOOTER)
516    generated_files.append(enum_types)
517
518    unit_type = GeneratedFile('unit_type')
519    unit_type.setJavaFilePath(os.path.join(android_top, UNITS_JAVA_FILE_PATH))
520    unit_type.setJavaHeader(UNITS_JAVA_HEADER)
521    unit_type.setJavaFooter(JAVA_FOOTER)
522    generated_files.append(unit_type)
523
524    version = GeneratedFile('version')
525    version.setCppFilePath(os.path.join(android_top, VERSION_CPP_FILE_PATH))
526    version.setCppHeader(VERSION_CPP_HEADER)
527    version.setCppFooter(CPP_FOOTER)
528    generated_files.append(version)
529
530    temp_files = []
531
532    try:
533        for generated_file in generated_files:
534            generated_file.convert(f, args.check_only, temp_files)
535
536        if not args.check_only:
537            return
538
539        for generated_file in generated_files:
540            if not generated_file.cmp():
541                print('The generated enum files for VehicleProperty.aidl requires update, ')
542                print('Run \npython ' + android_top + '/' + SCRIPT_PATH)
543                sys.exit(1)
544    except Exception as e:
545        print('Error parsing VehicleProperty.aidl')
546        print(e)
547        sys.exit(1)
548    finally:
549        for file in temp_files:
550            os.remove(file)
551
552
553if __name__ == '__main__':
554    main()