#------------------------------------------------------------------------------- # Copyright (c) 2022-2023, Arm Limited. All rights reserved. # SPDX-License-Identifier: BSD-3-Clause # #------------------------------------------------------------------------------- import argparse import logging import os import re from kconfiglib import Kconfig, TRI_TO_STR, BOOL, TRISTATE import menuconfig import guiconfig # NOTE: in_component_label is related with Kconfig menu prompt. in_component_label = 'TF-M component configs' def parse_args(): parser = argparse.ArgumentParser(description=\ 'TF-M Kconfig tool generates CMake configurations and header file \ component configurations. Terminal UI and GUI can help quickly \ configurate TF-M build options.') parser.add_argument( '-k', '--kconfig-file', dest = 'kconfig_file', required = True, help = 'The Top-level Kconfig file' ) parser.add_argument( '-o', '--output-path', dest = 'output_path', required = True, help = 'The output file folder' ) parser.add_argument( '--envs', dest='envs', default = None, nargs = '*', help = 'The environment variables for Kconfig files. Use absolute paths for directories.\ The format must be key-value pairs with "=" in the middle, for example:\ FOO=foo BAR=bar' ) parser.add_argument( '--config-files', dest='config_files', default = None, nargs = '*', help = 'The config files to be load and merge. The load order is the same as this list order,\ The later ones override the former ones.\ If .config is found in output-path, this file list is ignored.' ) parser.add_argument( '-u', '--ui', dest = 'ui', required = False, default = None, choices = ['gui', 'tui'], help = 'Which config UI to display' ) args = parser.parse_args() return args def set_env_var(envs): ''' The Kconfig files might use some environment variables. This method sets environment variables for Kconfig files. Each item in 'envs' should be in the key-value format, for example: 'FOO=foo BAR=bar' ''' if envs is None: return for env in envs: env_entries = env.strip('\r\n').split() for _env in env_entries: key, value = _env.split('=') os.environ[key] = value def generate_file(dot_config): ''' The .config file is the generated result from Kconfig files. It contains the set and un-set configs and their values. TF-M splits the configs to build options and component options. The former will be written into CMake file. The latter are all under a menu which has the prompt which contains in_component_label. These configs will be written into header file. ''' output_dir = os.path.dirname(dot_config) cmake_file = os.path.join(output_dir, 'project_config.cmake') header_file = os.path.join(output_dir, 'project_config.h') in_component_options, menu_start = False, False ''' The regular expression is used to parse the text like: - CONFIG_FOO=val - # CONFIG_FOO is not set The 'FOO' will be saved into the name part of groupdict, and the 'val' will be saved into the 'val' part of groupdict. ''' pattern_set = re.compile('CONFIG_(?P[A-Za-z|_|0-9]*)=(?P\S+)') pattern_not_set = re.compile('# CONFIG_(?P[A-Za-z|_|0-9]*) is not set') with open(cmake_file, 'w') as f_cmake, open(header_file, 'w') as f_header, \ open(dot_config, 'r') as f_config: for line in f_config: ''' Extract in_component_options flag from start line and end line which has the in_component_label. ''' if line.startswith('# ' + in_component_label): in_component_options = True continue if line.startswith('end of ' + in_component_label): in_component_options =False continue ''' Extract the menu prompt. It forms like: ... # # FOO Module # ... Here get the text 'FOO Module', and write it as comment in output files. ''' if line == '#\n' and not menu_start: menu_start = True continue if line == '#\n' and menu_start: menu_start = False continue # Write the menu prompt. if menu_start and not in_component_options: f_cmake.write('\n# {}\n'.format(line[2:-1])) continue if menu_start and in_component_options: f_header.write('\n/* {} */\n'.format(line[2:-1])) continue ''' Parse dot_config text by regular expression and get the config's name, value and type. Then write the result into CMake and header files. CONFIG_FOO=y - CMake: set(FOO ON CACHE BOOL '') - Header: #define FOO 1 CONFIG_FOO='foo' - CMake: set(FOO 'foo' CACHE STRING '') - Header: #define FOO 'foo' # CONFIG_FOO is not set - CMake: set(FOO OFF CACHE BOOL '') - Header: #define FOO 0 ''' name, cmake_type, cmake_val, header_val = '', '', '', '' # Search the configs set by Kconfig. ret = pattern_set.match(line) if ret: name = ret.groupdict()['name'] val = ret.groupdict()['val'] if val == 'y': cmake_val = 'ON' cmake_type = 'BOOL' header_val = '1' else: cmake_val = val cmake_type = 'STRING' header_val = val # Search the not set configs. ret = pattern_not_set.match(line) if ret: name = ret.groupdict()['name'] cmake_val = 'OFF' cmake_type = 'BOOL' header_val = '0' # Write the result into cmake and header files. if name and not in_component_options: f_cmake.write('set({:<45} {:<15} CACHE {:<6} "" FORCE)\n'. format(name, cmake_val, cmake_type)) if name and in_component_options: f_header.write('#define {:<45} {}\n'.format(name, header_val)) logging.info('TF-M build configs saved to \'{}\''.format(cmake_file)) logging.info('TF-M component configs saved to \'{}\''.format(header_file)) def validate_promptless_sym(kconfig): """ Check if any assignments to promptless symbols. """ ret = True for sym in kconfig.unique_defined_syms: if sym.user_value and not any(node.prompt for node in sym.nodes): logging.error('Assigning value to promptless symbol {}'.format(sym.name)) ret = False return ret def validate_assigned_sym(kconfig): """ Checks if all assigned symbols have the expected values """ ret = True for sym in kconfig.unique_defined_syms: if not sym.user_value: continue if sym.type in (BOOL, TRISTATE): user_val = TRI_TO_STR[sym.user_value] else: user_val = sym.user_value if user_val != sym.str_value: logging.error('Tried to set [{}] to <{}>, but is <{}> finally.'.format( sym.name, user_val, sym.str_value)) ret = False return ret if __name__ == '__main__': logging.basicConfig(format='[%(filename)s] %(levelname)s: %(message)s', level = logging.INFO) args = parse_args() # dot_config has a fixed name. Do NOT rename it. dot_config = os.path.abspath(os.path.join(args.output_path, '.config')) mtime_prv = 0 set_env_var(args.envs) # Load Kconfig file. kconfig_file is the root Kconfig file. The path is # input by users from the command. tfm_kconfig = Kconfig(args.kconfig_file) tfm_kconfig.disable_undef_warnings() # Disable warnings for undefined symbols when loading tfm_kconfig.disable_override_warnings() # Overriding would happen when loading multiple config files tfm_kconfig.disable_redun_warnings() # Redundant definitions might happen when loading multiple config files if not os.path.exists(args.output_path): os.mkdir(args.output_path) if os.path.exists(dot_config): # Load .config which contains the previous configurations. # Input config files are ignored. logging.info('.config file found, other config files are ignored.') mtime_prv = os.stat(dot_config).st_mtime logging.info(tfm_kconfig.load_config(dot_config)) elif args.config_files is not None: # Load input config files if .config is not found and write the .config file. for conf in args.config_files: logging.info(tfm_kconfig.load_config(conf, replace = False)) if not validate_promptless_sym(tfm_kconfig) or not validate_assigned_sym(tfm_kconfig): exit(1) # Change program execution path to the output folder path because menuconfigs do not support # writing .config to arbitrary folders. os.chdir(args.output_path) # UI options if args.ui == 'tui': menuconfig.menuconfig(tfm_kconfig) elif args.ui == 'gui': guiconfig.menuconfig(tfm_kconfig) else: logging.info(tfm_kconfig.write_config(dot_config)) if not os.path.exists(dot_config): # This could happend when the user did not "Save" the config file when using menuconfig # We should abort here in such case. logging.error('No .config is saved!') exit(1) # Generate output files if .config has been changed. if os.stat(dot_config).st_mtime != mtime_prv: generate_file(dot_config)