#!/usr/bin/env python3 # Copyright (c) 2018-2023 Nordic Semiconductor ASA and Ulf Magnusson # Originally modified from: # https://github.com/ulfalizer/Kconfiglib/blob/master/examples/merge_config.py # SPDX-License-Identifier: ISC # Writes/updates the zephyr/.config configuration file by merging configuration # files passed as arguments, e.g. board *_defconfig and application prj.conf # files. # # When fragments haven't changed, zephyr/.config is both the input and the # output, which just updates it. This is handled in the CMake files. # # Also does various checks (most via Kconfiglib warnings). import argparse import os import re import sys import textwrap # Zephyr doesn't use tristate symbols. They're supported here just to make the # script a bit more generic. from kconfiglib import Kconfig, split_expr, expr_value, expr_str, BOOL, \ TRISTATE, TRI_TO_STR, AND, OR def main(): args = parse_args() if args.zephyr_base: os.environ['ZEPHYR_BASE'] = args.zephyr_base print("Parsing " + args.kconfig_file) kconf = Kconfig(args.kconfig_file, warn_to_stderr=False, suppress_traceback=True) if args.handwritten_input_configs: # Warn for assignments to undefined symbols, but only for handwritten # fragments, to avoid warnings-turned-errors when using an old # configuration file together with updated Kconfig files kconf.warn_assign_undef = True # prj.conf may override settings from the board configuration, so # disable warnings about symbols being assigned more than once kconf.warn_assign_override = False kconf.warn_assign_redun = False if args.forced_input_configs: # Do not warn on a redundant config. # The reason is that a regular .config will be followed by the forced # config which under normal circumstances should be identical to the # configured setting. # Only if user has modified to a value that gets overruled by the forced # a warning shall be issued. kconf.warn_assign_redun = False # Load files print(kconf.load_config(args.configs_in[0])) for config in args.configs_in[1:]: # replace=False creates a merged configuration print(kconf.load_config(config, replace=False)) if args.handwritten_input_configs: # Check that there are no assignments to promptless symbols, which # have no effect. # # This only makes sense when loading handwritten fragments and not when # loading zephyr/.config, because zephyr/.config is configuration # output and also assigns promptless symbols. check_no_promptless_assign(kconf) # Print warnings for symbols that didn't get the assigned value. Only # do this for handwritten input too, to avoid likely unhelpful warnings # when using an old configuration and updating Kconfig files. check_assigned_sym_values(kconf) check_assigned_choice_values(kconf) if kconf.syms.get('WARN_DEPRECATED', kconf.y).tri_value == 2: check_deprecated(kconf) if kconf.syms.get('WARN_EXPERIMENTAL', kconf.y).tri_value == 2: check_experimental(kconf) # Hack: Force all symbols to be evaluated, to catch warnings generated # during evaluation. Wait till the end to write the actual output files, so # that we don't generate any output if there are warnings-turned-errors. # # Kconfiglib caches calculated symbol values internally, so this is still # fast. kconf.write_config(os.devnull) warn_only = r"warning:.*set more than once." if kconf.warnings: if args.forced_input_configs: error_out = False else: error_out = True # Put a blank line between warnings to make them easier to read for warning in kconf.warnings: print("\n" + warning, file=sys.stderr) if not error_out and not re.search(warn_only, warning): # The warning is not a warn_only, fail the Kconfig. error_out = True # Turn all warnings into errors, so that e.g. assignments to undefined # Kconfig symbols become errors. # # A warning is generated by this script whenever a symbol gets a # different value than the one it was assigned. Keep that one as just a # warning for now. if error_out: err("Aborting due to Kconfig warnings") # Write the merged configuration and the C header print(kconf.write_config(args.config_out)) print(kconf.write_autoconf(args.header_out)) # Write the list of parsed Kconfig files to a file write_kconfig_filenames(kconf, args.kconfig_list_out) def check_no_promptless_assign(kconf): # Checks that no promptless symbols are assigned for sym in kconf.unique_defined_syms: if sym.user_value is not None and promptless(sym): err(f"""\ {sym.name_and_loc} is assigned in a configuration file, but is not directly user-configurable (has no prompt). It gets its value indirectly from other symbols. """ + SYM_INFO_HINT.format(sym)) def check_assigned_sym_values(kconf): # Verifies that the values assigned to symbols "took" (matches the value # the symbols actually got), printing warnings otherwise. Choice symbols # are checked separately, in check_assigned_choice_values(). for sym in kconf.unique_defined_syms: if sym.choice: continue user_value = sym.user_value if user_value is None: continue # Tristate values are represented as 0, 1, 2. Having them as "n", "m", # "y" is more convenient here, so convert. if sym.type in (BOOL, TRISTATE): user_value = TRI_TO_STR[user_value] if user_value != sym.str_value: msg = f"{sym.name_and_loc} was assigned the value '{user_value}'" \ f" but got the value '{sym.str_value}'. " # List any unsatisfied 'depends on' dependencies in the warning mdeps = missing_deps(sym) if mdeps: expr_strs = [] for expr in mdeps: estr = expr_str(expr) if isinstance(expr, tuple): # Add () around dependencies that aren't plain symbols. # Gives '(FOO || BAR) (=n)' instead of # 'FOO || BAR (=n)', which might be clearer. estr = f"({estr})" expr_strs.append(f"{estr} " f"(={TRI_TO_STR[expr_value(expr)]})") msg += "Check these unsatisfied dependencies: " + \ ", ".join(expr_strs) + ". " warn(msg + SYM_INFO_HINT.format(sym)) def missing_deps(sym): # check_assigned_sym_values() helper for finding unsatisfied dependencies. # # Given direct dependencies # # depends on && && ... && # # on 'sym' (which can also come from e.g. a surrounding 'if'), returns a # list of all s with a value less than the value 'sym' was assigned # ("less" instead of "not equal" just to be general and handle tristates, # even though Zephyr doesn't use them). # # For string/int/hex symbols, just looks for = n. # # Note that s can be something more complicated than just a symbol, # like 'FOO || BAR' or 'FOO = "string"'. deps = split_expr(sym.direct_dep, AND) if sym.type in (BOOL, TRISTATE): return [dep for dep in deps if expr_value(dep) < sym.user_value] # string/int/hex return [dep for dep in deps if expr_value(dep) == 0] def check_assigned_choice_values(kconf): # Verifies that any choice symbols that were selected (by setting them to # y) ended up as the selection, printing warnings otherwise. # # We check choice symbols separately to avoid warnings when two different # choice symbols within the same choice are set to y. This might happen if # a choice selection from a board defconfig is overridden in a prj.conf, # for example. The last choice symbol set to y becomes the selection (and # all other choice symbols get the value n). # # Without special-casing choices, we'd detect that the first symbol set to # y ended up as n, and print a spurious warning. for choice in kconf.unique_choices: if choice.user_selection and \ choice.user_selection is not choice.selection: warn(f"""\ The choice symbol {choice.user_selection.name_and_loc} was selected (set =y), but {choice.selection.name_and_loc if choice.selection else "no symbol"} ended up as the choice selection. """ + SYM_INFO_HINT.format(choice.user_selection)) # Hint on where to find symbol information. Used like # SYM_INFO_HINT.format(sym). SYM_INFO_HINT = """\ See http://docs.zephyrproject.org/latest/kconfig.html#CONFIG_{0.name} and/or look up {0.name} in the menuconfig/guiconfig interface. The Application Development Primer, Setting Configuration Values, and Kconfig - Tips and Best Practices sections of the manual might be helpful too.\ """ def check_deprecated(kconf): deprecated = kconf.syms.get('DEPRECATED') dep_expr = kconf.n if deprecated is None else deprecated.rev_dep if dep_expr is not kconf.n: selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2] for selector in selectors: selector_name = split_expr(selector, AND)[0].name warn(f'Deprecated symbol {selector_name} is enabled.') def check_experimental(kconf): experimental = kconf.syms.get('EXPERIMENTAL') dep_expr = kconf.n if experimental is None else experimental.rev_dep if dep_expr is not kconf.n: selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2] for selector in selectors: selector_name = split_expr(selector, AND)[0].name warn(f'Experimental symbol {selector_name} is enabled.') def promptless(sym): # Returns True if 'sym' has no prompt. Since the symbol might be defined in # multiple locations, we need to check all locations. return not any(node.prompt for node in sym.nodes) def write_kconfig_filenames(kconf, kconfig_list_path): # Writes a sorted list with the absolute paths of all parsed Kconfig files # to 'kconfig_list_path'. The paths are realpath()'d, and duplicates are # removed. This file is used by CMake to look for changed Kconfig files. It # needs to be deterministic. with open(kconfig_list_path, 'w') as out: for path in sorted({os.path.realpath(os.path.join(kconf.srctree, path)) for path in kconf.kconfig_filenames}): print(path, file=out) def parse_args(): parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("--handwritten-input-configs", action="store_true", help="Assume the input configuration fragments are " "handwritten fragments and do additional checks " "on them, like no promptless symbols being " "assigned") parser.add_argument("--forced-input-configs", action="store_true", help="Indicate the input configuration files are " "followed by an forced configuration file." "The forced configuration is used to forcefully " "set specific configuration settings to a " "pre-defined value and thereby remove any user " " adjustments.") parser.add_argument("--zephyr-base", help="Path to current Zephyr installation") parser.add_argument("kconfig_file", help="Top-level Kconfig file") parser.add_argument("config_out", help="Output configuration file") parser.add_argument("header_out", help="Output header file") parser.add_argument("kconfig_list_out", help="Output file for list of parsed Kconfig files") parser.add_argument("configs_in", nargs="+", help="Input configuration fragments. Will be merged " "together.") return parser.parse_args() def warn(msg): # Use a large fill() width to try to avoid linebreaks in the symbol # reference link, and add some extra newlines to set the message off from # surrounding text (this usually gets printed as part of spammy CMake # output) print("\n" + textwrap.fill("warning: " + msg, 100) + "\n", file=sys.stderr) def err(msg): sys.exit("\n" + textwrap.fill("error: " + msg, 100) + "\n") if __name__ == "__main__": main()