1#------------------------------------------------------------------------------- 2# Copyright (c) 2022-2023, Arm Limited. All rights reserved. 3# SPDX-License-Identifier: BSD-3-Clause 4# 5#------------------------------------------------------------------------------- 6 7import argparse 8import logging 9import os 10import re 11 12from kconfiglib import Kconfig, TRI_TO_STR, BOOL, TRISTATE 13import menuconfig 14import guiconfig 15 16# NOTE: in_component_label is related with Kconfig menu prompt. 17in_component_label = 'TF-M component configs' 18 19def parse_args(): 20 parser = argparse.ArgumentParser(description=\ 21 'TF-M Kconfig tool generates CMake configurations and header file \ 22 component configurations. Terminal UI and GUI can help quickly \ 23 configurate TF-M build options.') 24 25 parser.add_argument( 26 '-k', '--kconfig-file', 27 dest = 'kconfig_file', 28 required = True, 29 help = 'The Top-level Kconfig file' 30 ) 31 32 parser.add_argument( 33 '-o', '--output-path', 34 dest = 'output_path', 35 required = True, 36 help = 'The output file folder' 37 ) 38 39 parser.add_argument( 40 '--envs', 41 dest='envs', 42 default = None, 43 nargs = '*', 44 help = 'The environment variables for Kconfig files. Use absolute paths for directories.\ 45 The format must be key-value pairs with "=" in the middle, for example:\ 46 FOO=foo BAR=bar' 47 ) 48 49 parser.add_argument( 50 '--config-files', 51 dest='config_files', 52 default = None, 53 nargs = '*', 54 help = 'The config files to be load and merge. The load order is the same as this list order,\ 55 The later ones override the former ones.\ 56 If .config is found in output-path, this file list is ignored.' 57 ) 58 59 parser.add_argument( 60 '-u', '--ui', 61 dest = 'ui', 62 required = False, 63 default = None, 64 choices = ['gui', 'tui'], 65 help = 'Which config UI to display' 66 ) 67 68 args = parser.parse_args() 69 70 return args 71 72def set_env_var(envs): 73 ''' 74 The Kconfig files might use some environment variables. 75 This method sets environment variables for Kconfig files. 76 Each item in 'envs' should be in the key-value format, for example: 77 'FOO=foo BAR=bar' 78 ''' 79 if envs is None: 80 return 81 82 for env in envs: 83 env_entries = env.strip('\r\n').split() 84 for _env in env_entries: 85 key, value = _env.split('=') 86 os.environ[key] = value 87 88def generate_file(dot_config): 89 ''' 90 The .config file is the generated result from Kconfig files. It contains 91 the set and un-set configs and their values. 92 93 TF-M splits the configs to build options and component options. The former 94 will be written into CMake file. The latter are all under a menu which has 95 the prompt which contains in_component_label. These configs will be written 96 into header file. 97 ''' 98 99 output_dir = os.path.dirname(dot_config) 100 cmake_file = os.path.join(output_dir, 'project_config.cmake') 101 header_file = os.path.join(output_dir, 'project_config.h') 102 103 in_component_options, menu_start = False, False 104 105 ''' 106 The regular expression is used to parse the text like: 107 - CONFIG_FOO=val 108 - # CONFIG_FOO is not set 109 The 'FOO' will be saved into the name part of groupdict, and the 'val' will 110 be saved into the 'val' part of groupdict. 111 ''' 112 pattern_set = re.compile('CONFIG_(?P<name>[A-Za-z|_|0-9]*)=(?P<val>\S+)') 113 pattern_not_set = re.compile('# CONFIG_(?P<name>[A-Za-z|_|0-9]*) is not set') 114 115 with open(cmake_file, 'w') as f_cmake, open(header_file, 'w') as f_header, \ 116 open(dot_config, 'r') as f_config: 117 118 for line in f_config: 119 ''' 120 Extract in_component_options flag from start line and end line 121 which has the in_component_label. 122 ''' 123 if line.startswith('# ' + in_component_label): 124 in_component_options = True 125 continue 126 if line.startswith('end of ' + in_component_label): 127 in_component_options =False 128 continue 129 130 ''' 131 Extract the menu prompt. It forms like: 132 ... 133 # 134 # FOO Module 135 # 136 ... 137 Here get the text 'FOO Module', and write it as comment in 138 output files. 139 ''' 140 if line == '#\n' and not menu_start: 141 menu_start = True 142 continue 143 if line == '#\n' and menu_start: 144 menu_start = False 145 continue 146 147 # Write the menu prompt. 148 if menu_start and not in_component_options: 149 f_cmake.write('\n# {}\n'.format(line[2:-1])) 150 continue 151 if menu_start and in_component_options: 152 f_header.write('\n/* {} */\n'.format(line[2:-1])) 153 continue 154 155 ''' 156 Parse dot_config text by regular expression and get the config's 157 name, value and type. Then write the result into CMake and 158 header files. 159 160 CONFIG_FOO=y 161 - CMake: set(FOO ON CACHE BOOL '') 162 - Header: #define FOO 1 163 CONFIG_FOO='foo' 164 - CMake: set(FOO 'foo' CACHE STRING '') 165 - Header: #define FOO 'foo' 166 # CONFIG_FOO is not set 167 - CMake: set(FOO OFF CACHE BOOL '') 168 - Header: #define FOO 0 169 ''' 170 name, cmake_type, cmake_val, header_val = '', '', '', '' 171 172 # Search the configs set by Kconfig. 173 ret = pattern_set.match(line) 174 if ret: 175 name = ret.groupdict()['name'] 176 val = ret.groupdict()['val'] 177 if val == 'y': 178 cmake_val = 'ON' 179 cmake_type = 'BOOL' 180 header_val = '1' 181 else: 182 cmake_val = val 183 cmake_type = 'STRING' 184 header_val = val 185 186 # Search the not set configs. 187 ret = pattern_not_set.match(line) 188 if ret: 189 name = ret.groupdict()['name'] 190 cmake_val = 'OFF' 191 cmake_type = 'BOOL' 192 header_val = '0' 193 194 # Write the result into cmake and header files. 195 if name and not in_component_options: 196 f_cmake.write('set({:<45} {:<15} CACHE {:<6} "" FORCE)\n'. 197 format(name, cmake_val, cmake_type)) 198 if name and in_component_options: 199 f_header.write('#define {:<45} {}\n'.format(name, header_val)) 200 201 logging.info('TF-M build configs saved to \'{}\''.format(cmake_file)) 202 logging.info('TF-M component configs saved to \'{}\''.format(header_file)) 203 204def validate_promptless_sym(kconfig): 205 """ 206 Check if any assignments to promptless symbols. 207 """ 208 209 ret = True 210 211 for sym in kconfig.unique_defined_syms: 212 if sym.user_value and not any(node.prompt for node in sym.nodes): 213 logging.error('Assigning value to promptless symbol {}'.format(sym.name)) 214 ret = False 215 216 return ret 217 218def validate_assigned_sym(kconfig): 219 """ 220 Checks if all assigned symbols have the expected values 221 """ 222 223 ret = True 224 225 for sym in kconfig.unique_defined_syms: 226 if not sym.user_value: 227 continue 228 229 if sym.type in (BOOL, TRISTATE): 230 user_val = TRI_TO_STR[sym.user_value] 231 else: 232 user_val = sym.user_value 233 234 if user_val != sym.str_value: 235 logging.error('Tried to set [{}] to <{}>, but is <{}> finally.'.format( 236 sym.name, user_val, sym.str_value)) 237 ret = False 238 239 return ret 240 241if __name__ == '__main__': 242 logging.basicConfig(format='[%(filename)s] %(levelname)s: %(message)s', 243 level = logging.INFO) 244 245 args = parse_args() 246 247 # dot_config has a fixed name. Do NOT rename it. 248 dot_config = os.path.abspath(os.path.join(args.output_path, '.config')) 249 mtime_prv = 0 250 251 set_env_var(args.envs) 252 253 # Load Kconfig file. kconfig_file is the root Kconfig file. The path is 254 # input by users from the command. 255 tfm_kconfig = Kconfig(args.kconfig_file) 256 tfm_kconfig.disable_undef_warnings() # Disable warnings for undefined symbols when loading 257 tfm_kconfig.disable_override_warnings() # Overriding would happen when loading multiple config files 258 tfm_kconfig.disable_redun_warnings() # Redundant definitions might happen when loading multiple config files 259 260 if not os.path.exists(args.output_path): 261 os.mkdir(args.output_path) 262 263 if os.path.exists(dot_config): 264 # Load .config which contains the previous configurations. 265 # Input config files are ignored. 266 logging.info('.config file found, other config files are ignored.') 267 mtime_prv = os.stat(dot_config).st_mtime 268 logging.info(tfm_kconfig.load_config(dot_config)) 269 elif args.config_files is not None: 270 # Load input config files if .config is not found and write the .config file. 271 for conf in args.config_files: 272 logging.info(tfm_kconfig.load_config(conf, replace = False)) 273 274 if not validate_promptless_sym(tfm_kconfig) or not validate_assigned_sym(tfm_kconfig): 275 exit(1) 276 277 # Change program execution path to the output folder path because menuconfigs do not support 278 # writing .config to arbitrary folders. 279 os.chdir(args.output_path) 280 281 # UI options 282 if args.ui == 'tui': 283 menuconfig.menuconfig(tfm_kconfig) 284 elif args.ui == 'gui': 285 guiconfig.menuconfig(tfm_kconfig) 286 else: 287 logging.info(tfm_kconfig.write_config(dot_config)) 288 289 if not os.path.exists(dot_config): 290 # This could happend when the user did not "Save" the config file when using menuconfig 291 # We should abort here in such case. 292 logging.error('No .config is saved!') 293 exit(1) 294 295 # Generate output files if .config has been changed. 296 if os.stat(dot_config).st_mtime != mtime_prv: 297 generate_file(dot_config) 298