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