Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 1 | #------------------------------------------------------------------------------- |
| 2 | # Copyright (c) 2022, Arm Limited. All rights reserved. |
| 3 | # SPDX-License-Identifier: BSD-3-Clause |
| 4 | # |
| 5 | #------------------------------------------------------------------------------- |
| 6 | |
| 7 | import argparse |
| 8 | import logging |
| 9 | import os |
| 10 | import re |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 11 | |
| 12 | from kconfiglib import Kconfig |
| 13 | import menuconfig |
| 14 | import guiconfig |
| 15 | |
| 16 | # NOTE: in_component_label is related with Kconfig menu prompt. |
| 17 | in_component_label = 'TF-M component configs' |
| 18 | |
| 19 | def 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 | '-u', '--ui', |
| 41 | dest = 'ui', |
| 42 | required = False, |
| 43 | default = None, |
| 44 | choices = ['gui', 'tui'], |
| 45 | help = 'Which config UI to display' |
| 46 | ) |
| 47 | |
| 48 | parser.add_argument( |
| 49 | '-p', '--platform-path', |
| 50 | dest = 'platform_path', |
| 51 | required = False, |
Jianliang Shen | 2b24f25 | 2022-11-25 11:08:54 +0800 | [diff] [blame] | 52 | help = 'The platform path which contains specific Kconfig and defconfig files' |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 53 | ) |
| 54 | |
| 55 | args = parser.parse_args() |
| 56 | |
| 57 | return args |
| 58 | |
| 59 | def generate_file(dot_config): |
| 60 | ''' |
| 61 | The .config file is the generated result from Kconfig files. It contains |
| 62 | the set and un-set configs and their values. |
| 63 | |
| 64 | TF-M splits the configs to build options and component options. The former |
| 65 | will be written into CMake file. The latter are all under a menu which has |
| 66 | the prompt which contains in_component_label. These configs will be written |
| 67 | into header file. |
| 68 | ''' |
| 69 | cmake_file, header_file = 'project_config.cmake', 'project_config.h' |
| 70 | in_component_options, menu_start = False, False |
| 71 | |
| 72 | ''' |
| 73 | The regular expression is used to parse the text like: |
| 74 | - CONFIG_FOO=val |
| 75 | - # CONFIG_FOO is not set |
Jianliang Shen | 2b24f25 | 2022-11-25 11:08:54 +0800 | [diff] [blame] | 76 | The 'FOO' will be saved into the name part of groupdict, and the 'val' will |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 77 | be saved into the 'val' part of groupdict. |
| 78 | ''' |
| 79 | pattern_set = re.compile('CONFIG_(?P<name>[A-Za-z|_|0-9]*)=(?P<val>\S+)') |
| 80 | pattern_not_set = re.compile('# CONFIG_(?P<name>[A-Za-z|_|0-9]*) is not set') |
| 81 | |
| 82 | with open(cmake_file, 'w') as f_cmake, open(header_file, 'w') as f_header, \ |
| 83 | open(dot_config, 'r') as f_config: |
| 84 | |
| 85 | for line in f_config: |
| 86 | ''' |
| 87 | Extract in_component_options flag from start line and end line |
| 88 | which has the in_component_label. |
| 89 | ''' |
| 90 | if line.startswith('# ' + in_component_label): |
| 91 | in_component_options = True |
| 92 | continue |
| 93 | if line.startswith('end of ' + in_component_label): |
| 94 | in_component_options =False |
| 95 | continue |
| 96 | |
| 97 | ''' |
| 98 | Extract the menu prompt. It forms like: |
| 99 | ... |
| 100 | # |
| 101 | # FOO Module |
| 102 | # |
| 103 | ... |
| 104 | Here get the text 'FOO Module', and write it as comment in |
| 105 | output files. |
| 106 | ''' |
| 107 | if line == '#\n' and not menu_start: |
| 108 | menu_start = True |
| 109 | continue |
| 110 | if line == '#\n' and menu_start: |
| 111 | menu_start = False |
| 112 | continue |
| 113 | |
| 114 | # Write the menu prompt. |
| 115 | if menu_start and not in_component_options: |
| 116 | f_cmake.write('\n# {}\n'.format(line[2:-1])) |
| 117 | continue |
| 118 | if menu_start and in_component_options: |
| 119 | f_header.write('\n/* {} */\n'.format(line[2:-1])) |
| 120 | continue |
| 121 | |
| 122 | ''' |
| 123 | Parse dot_config text by regular expression and get the config's |
| 124 | name, value and type. Then write the result into CMake and |
| 125 | header files. |
| 126 | |
| 127 | CONFIG_FOO=y |
| 128 | - CMake: set(FOO ON CACHE BOOL '') |
| 129 | - Header: #define FOO 1 |
| 130 | CONFIG_FOO='foo' |
| 131 | - CMake: set(FOO 'foo' CACHE STRING '') |
| 132 | - Header: #define FOO 'foo' |
| 133 | # CONFIG_FOO is not set |
| 134 | - CMake: set(FOO OFF CACHE BOOL '') |
| 135 | - Header: #define FOO 0 |
| 136 | ''' |
| 137 | name, cmake_type, cmake_val, header_val = '', '', '', '' |
| 138 | |
| 139 | # Search the configs set by Kconfig. |
| 140 | ret = pattern_set.match(line) |
| 141 | if ret: |
| 142 | name = ret.groupdict()['name'] |
| 143 | val = ret.groupdict()['val'] |
| 144 | if val == 'y': |
| 145 | cmake_val = 'ON' |
| 146 | cmake_type = 'BOOL' |
| 147 | header_val = '1' |
| 148 | else: |
| 149 | cmake_val = val |
| 150 | cmake_type = 'STRING' |
| 151 | header_val = val |
| 152 | |
| 153 | # Search the not set configs. |
| 154 | ret = pattern_not_set.match(line) |
| 155 | if ret: |
| 156 | name = ret.groupdict()['name'] |
| 157 | cmake_val = 'OFF' |
| 158 | cmake_type = 'BOOL' |
| 159 | header_val = '0' |
| 160 | |
| 161 | # Write the result into cmake and header files. |
| 162 | if name and not in_component_options: |
| 163 | f_cmake.write('set({:<45} {:<15} CACHE {:<6} "")\n'. |
| 164 | format(name, cmake_val, cmake_type)) |
| 165 | if name and in_component_options: |
| 166 | f_header.write('#define {:<45} {}\n'.format(name, header_val)) |
| 167 | |
| 168 | logging.info('TF-M build configs saved to \'{}\''.format(cmake_file)) |
| 169 | logging.info('TF-M component configs saved to \'{}\''.format(header_file)) |
| 170 | |
| 171 | if __name__ == '__main__': |
| 172 | logging.basicConfig(format='[%(filename)s] %(levelname)s: %(message)s', |
| 173 | level = logging.INFO) |
| 174 | |
| 175 | args = parse_args() |
| 176 | |
| 177 | # dot_config has a fixed name. Do NOT rename it. |
| 178 | dot_config = '.config' |
| 179 | def_config = '' |
| 180 | mtime_prv = 0 |
| 181 | |
Jianliang Shen | 2b24f25 | 2022-11-25 11:08:54 +0800 | [diff] [blame] | 182 | if args.platform_path and os.path.exists(args.platform_path): |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 183 | platform_abs_path = os.path.abspath(args.platform_path) |
Jianliang Shen | 2b24f25 | 2022-11-25 11:08:54 +0800 | [diff] [blame] | 184 | def_config = os.path.join(platform_abs_path, 'defconfig') |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 185 | |
| 186 | # Pass environment variable to Kconfig to load extra Kconfig file. |
| 187 | os.environ['PLATFORM_PATH'] = platform_abs_path |
| 188 | |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 189 | # Load Kconfig file. kconfig_file is the root Kconfig file. The path is |
| 190 | # input by users from the command. |
| 191 | tfm_kconfig = Kconfig(args.kconfig_file) |
| 192 | |
Jianliang Shen | 2b24f25 | 2022-11-25 11:08:54 +0800 | [diff] [blame] | 193 | if not os.path.exists(args.output_path): |
| 194 | os.mkdir(args.output_path) |
| 195 | |
| 196 | # Change program execution path to the output folder path. |
| 197 | os.chdir(args.output_path) |
| 198 | |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 199 | if os.path.exists(dot_config): |
| 200 | # Load .config which contains the previous configurations. |
| 201 | mtime_prv = os.stat(dot_config).st_mtime |
| 202 | tfm_kconfig.load_config(dot_config) |
| 203 | logging.info('Load configs from \'{}\''.format(dot_config)) |
| 204 | elif os.path.exists(def_config): |
| 205 | # Load platform specific defconfig if exists. |
| 206 | tfm_kconfig.load_config(def_config) |
| 207 | logging.info('Load configs from \'{}\''.format(def_config)) |
| 208 | |
Jianliang Shen | 002edf9 | 2022-11-01 17:46:16 +0800 | [diff] [blame] | 209 | # UI options |
| 210 | if args.ui == 'tui': |
| 211 | menuconfig.menuconfig(tfm_kconfig) |
| 212 | elif args.ui == 'gui': |
| 213 | guiconfig.menuconfig(tfm_kconfig) |
| 214 | else: |
| 215 | # Save .config if UI is not created. |
| 216 | # The previous .config will be saved as .config.old. |
| 217 | tfm_kconfig.write_config(dot_config) |
| 218 | |
| 219 | # Generate output files if .config has been changed. |
| 220 | if os.path.exists(dot_config) and os.stat(dot_config).st_mtime != mtime_prv: |
| 221 | generate_file(dot_config) |