#!/usr/bin/env python # # SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 """ Generate Kconfig.soc_caps.in with defines from soc_caps.h """ import argparse import inspect import io import logging import sys from difflib import unified_diff from os import path from pathlib import Path from string import Template import pyparsing from pyparsing import (CaselessLiteral, Char, Combine, Group, Literal, OneOrMore, # pylint: disable=unused-import Optional, ParserElement, QuotedString, Word, alphas, hexnums, nums) pyparsing.usePackrat = True try: import typing # noqa: F401 # pylint: disable=unused-import except ImportError: pass class KconfigWriter(): PREAMBLE = inspect.cleandoc(''' ##################################################### # This file is auto-generated from SoC caps # using gen_soc_caps_kconfig.py, do not edit manually ##################################################### ''') KCONFIG_ENTRY_TEMPLATE = Template( inspect.cleandoc(''' config $name $entry_type default $value ''')) def __init__(self): # type: () -> None self.entries = set('') self.kconfig_text = io.StringIO('') self.kconfig_text.write(self.PREAMBLE) def add_entry(self, name, entry_type, value): # type: (str, str, typing.Any) -> None if name in self.entries: logging.info('Duplicate entry: {}'.format(name)) return self.entries.add(name) self.kconfig_text.write('\n\n') # Format values for kconfig if entry_type == 'bool': value = 'y' if value else 'n' elif entry_type == 'string': value = '"' + value + '"' entry = self.KCONFIG_ENTRY_TEMPLATE.substitute(name=name, entry_type=entry_type, value=value) self.kconfig_text.write(entry) def update_file(self, kconfig_path, always_write): # type: (Path, bool) -> bool try: with open(kconfig_path, 'r') as f: old_content = f.readlines() except FileNotFoundError: old_content = [''] self.kconfig_text.seek(0) new_content = self.kconfig_text.readlines() new_content[-1] += '\n' # Add final newline to end of file file_needs_update = always_write # Check if file was updated and print diff for users diff = unified_diff(old_content, new_content, fromfile=str(kconfig_path), n=2) for line in diff: print(line, end='') file_needs_update = True if file_needs_update: print('\n' + 'Updating file: {}'.format(kconfig_path)) with open(kconfig_path, 'w') as f: f.writelines(new_content) return file_needs_update def parse_define(define_line): # type: (str) -> typing.Any[typing.Type[ParserElement]] # Group for parsing literal suffix of a numbers, e.g. 100UL literal_symbol = Group(CaselessLiteral('L') | CaselessLiteral('U')) literal_suffix = OneOrMore(literal_symbol) # Define name name = Word(alphas, alphas + nums + '_') # Define value, either a hex, int or a string hex_value = Combine(Literal('0x') + Word(hexnums) + Optional(literal_suffix).suppress())('hex_value') int_value = Word(nums)('int_value') + ~Char('.') + Optional(literal_suffix)('literal_suffix') str_value = QuotedString('"')('str_value') # Remove optional parenthesis around values value = Optional('(').suppress() + (hex_value ^ int_value ^ str_value)('value') + Optional(')').suppress() expr = '#define' + Optional(name)('name') + Optional(value) res = expr.parseString(define_line) return res def generate_defines(soc_caps_dir, filename, always_write): # type: (Path, str, bool) -> bool soc_headers = list(soc_caps_dir.glob(filename)) if soc_headers == []: return False # Sort header files to make the generated files deterministic soc_headers.sort(key=lambda file: file.name) defines = [] for soc_header in soc_headers: defines.extend(get_defines(soc_header)) writer = KconfigWriter() for line in defines: try: res = parse_define(line) except pyparsing.ParseException: logging.debug('Failed to parse: {}'.format(line)) continue # Add the kconfig entry corresponding to the type we parsed if 'str_value' in res: writer.add_entry(res.name, 'string', res.str_value) elif 'int_value' in res: # defines with an integer value of 0 or 1 are # added as bool entries as long they have no literal suffix if 'literal_suffix' not in res and res.int_value == '0': writer.add_entry(res.name, 'bool', False) elif 'literal_suffix' not in res and res.int_value == '1': writer.add_entry(res.name, 'bool', True) else: writer.add_entry(res.name, 'int', res.int_value) elif 'hex_value' in res: writer.add_entry(res.name, 'hex', res.hex_value) # Updates output if necessary updated = writer.update_file(Path(soc_caps_dir) / 'Kconfig.soc_caps.in', always_write) return updated def get_defines(header_path): # type: (Path) -> list[str] defines = [] logging.info('Reading macros from {}...'.format(header_path)) with open(header_path, 'r') as f: output = f.read() for line in output.split('\n'): line = line.strip() if len(line): defines.append(line) return defines if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-d', '--dir', help='SoC caps folder paths, support wildcards', nargs='+', default=[]) parser.add_argument('-n', '--filename', nargs='?', default='*caps.h', help='SoC caps filename, support wildcards') parser.add_argument('-v', '--verbose', action='count', help='Increase the logging level of the script. Can be specified multiple times.') parser.add_argument('--always-write', help='Always generate new output files', action='store_true') args = parser.parse_args() if not args.verbose: log_level = logging.WARNING elif args.verbose == 1: log_level = logging.INFO else: log_level = logging.DEBUG logging.basicConfig(level=log_level) files_updated = [] for caps_dir in args.dir: soc_caps_dirs = Path().glob(caps_dir) files_updated += [generate_defines(d, args.filename, args.always_write) for d in soc_caps_dirs if path.isdir(d)] print('Updated {} files'.format(sum(files_updated))) sys.exit(all(files_updated))