1#!/usr/bin/env python3
2
3# Copyright (c) 2018-2023 Nordic Semiconductor ASA and Ulf Magnusson
4# Originally modified from:
5# https://github.com/ulfalizer/Kconfiglib/blob/master/examples/merge_config.py
6
7# SPDX-License-Identifier: ISC
8
9# Writes/updates the zephyr/.config configuration file by merging configuration
10# files passed as arguments, e.g. board *_defconfig and application prj.conf
11# files.
12#
13# When fragments haven't changed, zephyr/.config is both the input and the
14# output, which just updates it. This is handled in the CMake files.
15#
16# Also does various checks (most via Kconfiglib warnings).
17
18import argparse
19import os
20import re
21import sys
22import textwrap
23
24# Zephyr doesn't use tristate symbols. They're supported here just to make the
25# script a bit more generic.
26from kconfiglib import (
27    AND,
28    BOOL,
29    OR,
30    TRI_TO_STR,
31    TRISTATE,
32    Kconfig,
33    expr_str,
34    expr_value,
35    split_expr,
36)
37
38
39def main():
40    args = parse_args()
41
42    if args.zephyr_base:
43        os.environ['ZEPHYR_BASE'] = args.zephyr_base
44
45    print("Parsing " + args.kconfig_file)
46    kconf = Kconfig(args.kconfig_file, warn_to_stderr=False,
47                    suppress_traceback=True)
48
49    if args.handwritten_input_configs:
50        # Warn for assignments to undefined symbols, but only for handwritten
51        # fragments, to avoid warnings-turned-errors when using an old
52        # configuration file together with updated Kconfig files
53        kconf.warn_assign_undef = True
54
55        # prj.conf may override settings from the board configuration, so
56        # disable warnings about symbols being assigned more than once
57        kconf.warn_assign_override = False
58        kconf.warn_assign_redun = False
59
60    if args.forced_input_configs:
61        # Do not warn on a redundant config.
62        # The reason is that a regular .config will be followed by the forced
63        # config which under normal circumstances should be identical to the
64        # configured setting.
65        # Only if user has modified to a value that gets overruled by the forced
66        # a warning shall be issued.
67        kconf.warn_assign_redun = False
68
69    # Load files
70    print(kconf.load_config(args.configs_in[0]))
71    for config in args.configs_in[1:]:
72        # replace=False creates a merged configuration
73        print(kconf.load_config(config, replace=False))
74
75    if args.handwritten_input_configs:
76        # Check that there are no assignments to promptless symbols, which
77        # have no effect.
78        #
79        # This only makes sense when loading handwritten fragments and not when
80        # loading zephyr/.config, because zephyr/.config is configuration
81        # output and also assigns promptless symbols.
82        check_no_promptless_assign(kconf)
83
84        # Print warnings for symbols that didn't get the assigned value. Only
85        # do this for handwritten input too, to avoid likely unhelpful warnings
86        # when using an old configuration and updating Kconfig files.
87        check_assigned_sym_values(kconf)
88        check_assigned_choice_values(kconf)
89
90    if kconf.syms.get('WARN_DEPRECATED', kconf.y).tri_value == 2:
91        check_deprecated(kconf)
92
93    if kconf.syms.get('WARN_EXPERIMENTAL', kconf.y).tri_value == 2:
94        check_experimental(kconf)
95
96    check_not_secure(kconf)
97
98    # Hack: Force all symbols to be evaluated, to catch warnings generated
99    # during evaluation. Wait till the end to write the actual output files, so
100    # that we don't generate any output if there are warnings-turned-errors.
101    #
102    # Kconfiglib caches calculated symbol values internally, so this is still
103    # fast.
104    kconf.write_config(os.devnull)
105
106    warn_only = r"warning:.*set more than once."
107
108    if kconf.warnings:
109        if args.forced_input_configs:
110            error_out = False
111        else:
112            error_out = True
113
114        # Put a blank line between warnings to make them easier to read
115        for warning in kconf.warnings:
116            print("\n" + warning, file=sys.stderr)
117
118            if not error_out and not re.search(warn_only, warning):
119                # The warning is not a warn_only, fail the Kconfig.
120                error_out = True
121
122        # Turn all warnings into errors, so that e.g. assignments to undefined
123        # Kconfig symbols become errors.
124        #
125        # A warning is generated by this script whenever a symbol gets a
126        # different value than the one it was assigned. Keep that one as just a
127        # warning for now.
128        if error_out:
129            err("Aborting due to Kconfig warnings")
130
131    # Write the merged configuration and the C header
132    print(kconf.write_config(args.config_out))
133    print(kconf.write_autoconf(args.header_out))
134
135    # Write the list of parsed Kconfig files to a file
136    write_kconfig_filenames(kconf, args.kconfig_list_out)
137
138
139def check_no_promptless_assign(kconf):
140    # Checks that no promptless symbols are assigned
141
142    for sym in kconf.unique_defined_syms:
143        if sym.user_value is not None and promptless(sym):
144            err(f"""\
145{sym.name_and_loc} is assigned in a configuration file, but is not directly
146user-configurable (has no prompt). It gets its value indirectly from other
147symbols. """ + SYM_INFO_HINT.format(sym))
148
149
150def check_assigned_sym_values(kconf):
151    # Verifies that the values assigned to symbols "took" (matches the value
152    # the symbols actually got), printing warnings otherwise. Choice symbols
153    # are checked separately, in check_assigned_choice_values().
154
155    for sym in kconf.unique_defined_syms:
156        if sym.choice:
157            continue
158
159        user_value = sym.user_value
160        if user_value is None:
161            continue
162
163        # Tristate values are represented as 0, 1, 2. Having them as "n", "m",
164        # "y" is more convenient here, so convert.
165        if sym.type in (BOOL, TRISTATE):
166            user_value = TRI_TO_STR[user_value]
167
168        if user_value != sym.str_value:
169            msg = f"{sym.name_and_loc} was assigned the value '{user_value}'" \
170                  f" but got the value '{sym.str_value}'. "
171
172            # List any unsatisfied 'depends on' dependencies in the warning
173            mdeps = missing_deps(sym)
174            if mdeps:
175                expr_strs = []
176                for expr in mdeps:
177                    estr = expr_str(expr)
178                    if isinstance(expr, tuple):
179                        # Add () around dependencies that aren't plain symbols.
180                        # Gives '(FOO || BAR) (=n)' instead of
181                        # 'FOO || BAR (=n)', which might be clearer.
182                        estr = f"({estr})"
183                    expr_strs.append(f"{estr} "
184                                     f"(={TRI_TO_STR[expr_value(expr)]})")
185
186                msg += "Check these unsatisfied dependencies: " + \
187                    ", ".join(expr_strs) + ". "
188
189            warn(msg + SYM_INFO_HINT.format(sym))
190
191
192def missing_deps(sym):
193    # check_assigned_sym_values() helper for finding unsatisfied dependencies.
194    #
195    # Given direct dependencies
196    #
197    #     depends on <expr> && <expr> && ... && <expr>
198    #
199    # on 'sym' (which can also come from e.g. a surrounding 'if'), returns a
200    # list of all <expr>s with a value less than the value 'sym' was assigned
201    # ("less" instead of "not equal" just to be general and handle tristates,
202    # even though Zephyr doesn't use them).
203    #
204    # For string/int/hex symbols, just looks for <expr> = n.
205    #
206    # Note that <expr>s can be something more complicated than just a symbol,
207    # like 'FOO || BAR' or 'FOO = "string"'.
208
209    deps = split_expr(sym.direct_dep, AND)
210
211    if sym.type in (BOOL, TRISTATE):
212        return [dep for dep in deps if expr_value(dep) < sym.user_value]
213    # string/int/hex
214    return [dep for dep in deps if expr_value(dep) == 0]
215
216
217def check_assigned_choice_values(kconf):
218    # Verifies that any choice symbols that were selected (by setting them to
219    # y) ended up as the selection, printing warnings otherwise.
220    #
221    # We check choice symbols separately to avoid warnings when two different
222    # choice symbols within the same choice are set to y. This might happen if
223    # a choice selection from a board defconfig is overridden in a prj.conf,
224    # for example. The last choice symbol set to y becomes the selection (and
225    # all other choice symbols get the value n).
226    #
227    # Without special-casing choices, we'd detect that the first symbol set to
228    # y ended up as n, and print a spurious warning.
229
230    for choice in kconf.unique_choices:
231        if choice.user_selection and \
232           choice.user_selection is not choice.selection:
233
234            warn(f"""\
235The choice symbol {choice.user_selection.name_and_loc} was selected (set =y),
236but {choice.selection.name_and_loc if choice.selection else "no symbol"} ended
237up as the choice selection. """ + SYM_INFO_HINT.format(choice.user_selection))
238
239
240# Hint on where to find symbol information. Used like
241# SYM_INFO_HINT.format(sym).
242SYM_INFO_HINT = """\
243See http://docs.zephyrproject.org/latest/kconfig.html#CONFIG_{0.name} and/or
244look up {0.name} in the menuconfig/guiconfig interface. The Application
245Development Primer, Setting Configuration Values, and Kconfig - Tips and Best
246Practices sections of the manual might be helpful too.\
247"""
248
249
250def check_deprecated(kconf):
251    deprecated = kconf.syms.get('DEPRECATED')
252    dep_expr = kconf.n if deprecated is None else deprecated.rev_dep
253
254    if dep_expr is not kconf.n:
255        selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
256        for selector in selectors:
257            selector_name = split_expr(selector, AND)[0].name
258            warn(f'Deprecated symbol {selector_name} is enabled.')
259
260
261def check_experimental(kconf):
262    experimental = kconf.syms.get('EXPERIMENTAL')
263    dep_expr = kconf.n if experimental is None else experimental.rev_dep
264
265    if dep_expr is not kconf.n:
266        selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
267        for selector in selectors:
268            selector_name = split_expr(selector, AND)[0].name
269            warn(f'Experimental symbol {selector_name} is enabled.')
270
271def check_not_secure(kconf):
272    not_secure = kconf.syms.get('NOT_SECURE')
273    dep_expr = kconf.n if not_secure is None else not_secure.rev_dep
274
275    if dep_expr is not kconf.n:
276        selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
277        for selector in selectors:
278            selector_name = split_expr(selector, AND)[0].name
279            warn(f'Not secure symbol {selector_name} is enabled.')
280
281
282def promptless(sym):
283    # Returns True if 'sym' has no prompt. Since the symbol might be defined in
284    # multiple locations, we need to check all locations.
285
286    return not any(node.prompt for node in sym.nodes)
287
288
289def write_kconfig_filenames(kconf, kconfig_list_path):
290    # Writes a sorted list with the absolute paths of all parsed Kconfig files
291    # to 'kconfig_list_path'. The paths are realpath()'d, and duplicates are
292    # removed. This file is used by CMake to look for changed Kconfig files. It
293    # needs to be deterministic.
294
295    with open(kconfig_list_path, 'w') as out:
296        for path in sorted({os.path.realpath(os.path.join(kconf.srctree, path))
297                            for path in set(kconf.kconfig_filenames)}):
298            print(path, file=out)
299
300
301def parse_args():
302    parser = argparse.ArgumentParser(allow_abbrev=False)
303
304    parser.add_argument("--handwritten-input-configs",
305                        action="store_true",
306                        help="Assume the input configuration fragments are "
307                             "handwritten fragments and do additional checks "
308                             "on them, like no promptless symbols being "
309                             "assigned")
310    parser.add_argument("--forced-input-configs",
311                        action="store_true",
312                        help="Indicate the input configuration files are "
313                             "followed by an forced configuration file."
314                             "The forced configuration is used to forcefully "
315                             "set specific configuration settings to a "
316                             "pre-defined value and thereby remove any user "
317                             " adjustments.")
318    parser.add_argument("--zephyr-base",
319                        help="Path to current Zephyr installation")
320    parser.add_argument("kconfig_file",
321                        help="Top-level Kconfig file")
322    parser.add_argument("config_out",
323                        help="Output configuration file")
324    parser.add_argument("header_out",
325                        help="Output header file")
326    parser.add_argument("kconfig_list_out",
327                        help="Output file for list of parsed Kconfig files")
328    parser.add_argument("configs_in",
329                        nargs="+",
330                        help="Input configuration fragments. Will be merged "
331                             "together.")
332
333    return parser.parse_args()
334
335
336def warn(msg):
337    # Use a large fill() width to try to avoid linebreaks in the symbol
338    # reference link, and add some extra newlines to set the message off from
339    # surrounding text (this usually gets printed as part of spammy CMake
340    # output)
341    print("\n" + textwrap.fill("warning: " + msg, 100) + "\n", file=sys.stderr)
342
343
344def err(msg):
345    sys.exit("\n" + textwrap.fill("error: " + msg, 100) + "\n")
346
347
348if __name__ == "__main__":
349    main()
350