mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2024-12-29 04:50:03 +01:00
tools/genconfig: add invalid configurations checks
This commit is contained in:
parent
3ec806dda5
commit
e815863aa9
268
dist/tools/kconfiglib/genconfig.py
vendored
268
dist/tools/kconfiglib/genconfig.py
vendored
@ -31,6 +31,7 @@ appropriate.
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import kconfiglib
|
||||
|
||||
@ -38,6 +39,17 @@ import kconfiglib
|
||||
DEFAULT_SYNC_DEPS_PATH = "deps/"
|
||||
|
||||
|
||||
class Colors:
|
||||
"""
|
||||
ASCII colors for logging.
|
||||
"""
|
||||
GREEN = "\033[1;32m"
|
||||
RED = "\033[1;31m"
|
||||
YELLOW = "\033[1;33m"
|
||||
PURPLE = "\033[1;35m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
class NoConfigurationFile(Exception):
|
||||
"""
|
||||
Raised when an operation that requires a configuration input file is
|
||||
@ -46,7 +58,57 @@ class NoConfigurationFile(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def is_module(symbol):
|
||||
"""
|
||||
Checks if a given symbol represents a module, depending on its prefix.
|
||||
"""
|
||||
return symbol.name.startswith("MODULE_")
|
||||
|
||||
|
||||
def is_error(symbol):
|
||||
"""
|
||||
Checks if a given symbol represents an error, depending on its prefix.
|
||||
"""
|
||||
return symbol.name.startswith("ERROR_")
|
||||
|
||||
|
||||
def log_error(message):
|
||||
"""
|
||||
Convenience function to log an error.
|
||||
"""
|
||||
log(message, level=logging.ERROR)
|
||||
|
||||
|
||||
def log(message, level=logging.DEBUG, color=None):
|
||||
"""
|
||||
Logs a message using 'logging', with a given color and level. If no level is
|
||||
passed the message is logged using debug level. If no color is passed the
|
||||
following rules apply:
|
||||
- error messages are RED
|
||||
- warning messages are YELLOW
|
||||
- all other messages have no color
|
||||
"""
|
||||
if sys.stdout.isatty():
|
||||
# running in a real terminal
|
||||
if color is None:
|
||||
if level == logging.ERROR:
|
||||
color = Colors.RED
|
||||
elif level == logging.WARNING:
|
||||
color = Colors.YELLOW
|
||||
else:
|
||||
color = Colors.RESET
|
||||
|
||||
logging.log(level, "{}{}{}".format(color, message, Colors.RESET))
|
||||
else:
|
||||
# being piped or redirected
|
||||
logging.log(level, "{}".format(message))
|
||||
|
||||
|
||||
def merge_configs(kconf, configs=[]):
|
||||
"""
|
||||
Merges multiple configuration files given a Kconfig tree. configs should be
|
||||
an array of paths to the files that need to be merged.
|
||||
"""
|
||||
# Enable warnings for assignments to undefined symbols
|
||||
kconf.warn_assign_undef = True
|
||||
|
||||
@ -61,8 +123,173 @@ def merge_configs(kconf, configs=[]):
|
||||
kconf.warn_assign_redun = False
|
||||
|
||||
# Create a merged configuration by loading the fragments with replace=False.
|
||||
for config in configs:
|
||||
logging.debug(kconf.load_config(config, replace=False))
|
||||
if configs:
|
||||
for config in configs:
|
||||
log(kconf.load_config(config, replace=False))
|
||||
|
||||
|
||||
def check_config_symbols(kconf):
|
||||
"""
|
||||
Verifies that symbols got the values assigned by the user. This does not
|
||||
check choices. For that, please refer to check_config_choices.
|
||||
"""
|
||||
ret = True
|
||||
|
||||
for sym in kconf.unique_defined_syms:
|
||||
# choices are evaluated separately because when merging configurations
|
||||
# the choice could be overridden
|
||||
if sym.choice:
|
||||
continue
|
||||
|
||||
# Was the symbol assigned to?
|
||||
if sym.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 (kconfiglib.BOOL, kconfiglib.TRISTATE):
|
||||
user_value = kconfiglib.TRI_TO_STR[sym.user_value]
|
||||
else:
|
||||
user_value = sym.user_value
|
||||
|
||||
if user_value != sym.str_value:
|
||||
log("{} was assigned the value '{}' but got the value"
|
||||
" '{}'. Check the dependencies.".format(sym.name, user_value,
|
||||
sym.str_value))
|
||||
|
||||
symbol_type = "module" if is_module(sym) else "parameter"
|
||||
log_error("=> The {} {} could not be set to {}."
|
||||
.format(symbol_type, sym.name, user_value))
|
||||
|
||||
msg = ""
|
||||
|
||||
missing_deps = get_sym_missing_deps(sym)
|
||||
if len(missing_deps):
|
||||
msg = " Check the following unmet dependencies: "
|
||||
msg += ", ".join(missing_deps) + "\n"
|
||||
elif sym.type == kconfiglib.HEX or sym.type == kconfiglib.INT:
|
||||
rng = get_sym_applying_range(sym)
|
||||
if rng is not None:
|
||||
msg = " Check that the value is in the correct range:"
|
||||
msg += "[{} - {}] {}\n".format(rng[0], rng[1], rng[2])
|
||||
|
||||
log_error(msg)
|
||||
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
|
||||
def check_config_choices(kconf):
|
||||
"""
|
||||
Verifies that the choice options that have been selected after processing
|
||||
the configuration match what the user selected.
|
||||
This is verified separately from the rest of the symbols because as we
|
||||
are merging multiple configuration files the choice selection can be
|
||||
overridden, so this check needs a different logic.
|
||||
"""
|
||||
ret = True
|
||||
|
||||
for choice in kconf.unique_choices:
|
||||
if choice.user_selection and choice.user_selection is not choice.selection:
|
||||
ret = False
|
||||
log("{} choice option could not be set".format(choice.name))
|
||||
log_error("=> The choice {} was selected but was not set.\n"
|
||||
.format(choice.user_selection.name_and_loc))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def check_application_symbol(kconf):
|
||||
"""
|
||||
Check that the APPLICATION symbol is set.
|
||||
|
||||
If the special symbol APPLICATION is present it should be set to 'y'. It is
|
||||
used to:
|
||||
- imply modules that are optional for the application
|
||||
- add dependencies on other symbols or conditions (e.g. only use 32-bits
|
||||
architectures on this application)
|
||||
"""
|
||||
app_sym = list(filter(lambda s: s.name == "APPLICATION", kconf.unique_defined_syms))
|
||||
if len(app_sym) > 1:
|
||||
log("=> The special symbol APPLICATION is defined more than once",
|
||||
level=logging.WARNING)
|
||||
log_error("=> The special symbol APPLICATION is defined more than once")
|
||||
return False
|
||||
elif len(app_sym) == 1:
|
||||
app = app_sym[0]
|
||||
if app.str_value != 'y':
|
||||
log("=> The application symbol (APPLICATION) is not set.",
|
||||
level=logging.WARNING)
|
||||
log_error("=> The application symbol (APPLICATION) is not set.")
|
||||
log_error(" Check that the symbol defaults to 'y'.")
|
||||
missing_deps = get_sym_missing_deps(app)
|
||||
if len(missing_deps):
|
||||
msg = " Check the following unmet dependencies: "
|
||||
msg += ", ".join(missing_deps) + "\n"
|
||||
log_error(msg)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_configs(kconf):
|
||||
"""
|
||||
Verifies that the generated configuration is valid.
|
||||
|
||||
A configuration is not valid when:
|
||||
- A module could not be set to the value defined by the user.
|
||||
- A configuration parameter could not be set to value defined by the
|
||||
user.
|
||||
"""
|
||||
app_check = check_application_symbol(kconf)
|
||||
sym_check = check_config_symbols(kconf)
|
||||
choice_check = check_config_choices(kconf)
|
||||
return app_check and sym_check and choice_check
|
||||
|
||||
|
||||
def get_sym_missing_deps(sym):
|
||||
"""
|
||||
Returns an array of strings, where each element is the string representation
|
||||
of the expressions on which `sym` depends and are missing.
|
||||
"""
|
||||
# this splits the top expressions that are connected via AND (&&)
|
||||
# this will be the case, for example, for expressions that are defined in
|
||||
# multiple lines
|
||||
top_deps = kconfiglib.split_expr(sym.direct_dep, kconfiglib.AND)
|
||||
|
||||
# we only need the expressions that are not met
|
||||
expr = [dep for dep in top_deps if kconfiglib.expr_value(dep) == 0]
|
||||
|
||||
# convert each expression to strings and add the value for a friendlier
|
||||
# output message
|
||||
expr_str = []
|
||||
for expr in expr:
|
||||
s = kconfiglib.expr_str(expr)
|
||||
if isinstance(expr, tuple):
|
||||
s = "({})".format(s)
|
||||
expr_str.append("{} (={})".format(s, kconfiglib.TRI_TO_STR[kconfiglib.expr_value(expr)]))
|
||||
|
||||
return expr_str
|
||||
|
||||
|
||||
def get_sym_applying_range(sym):
|
||||
"""
|
||||
Returns the first range that applies to a symbol (the active range) and the
|
||||
condition when it is not a constant.
|
||||
The return value is a tuple holding string representations of the range like
|
||||
so:
|
||||
(low, high, condition)
|
||||
When the condition is a constant (e.g. 'y' when there is no condition) it
|
||||
will be an empty string.
|
||||
"""
|
||||
# multiple ranges could apply to a symbol
|
||||
for rng in sym.ranges:
|
||||
(low, high, cond) = rng
|
||||
# get the first active one
|
||||
if kconfiglib.expr_value(cond):
|
||||
return (low.str_value, high.str_value,
|
||||
"(if {})".format(cond.name) if not cond.is_constant else "")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
@ -124,6 +351,17 @@ included, and not variables referenced with the older $VAR syntax (which is
|
||||
only supported for backwards compatibility).
|
||||
""")
|
||||
|
||||
parser.add_argument(
|
||||
"--ignore-config-errors",
|
||||
action="store_true",
|
||||
help="""Configuration errors are reported but the script does not exit
|
||||
with error.""")
|
||||
|
||||
parser.add_argument(
|
||||
"--warnings-are-not-errors",
|
||||
action="store_true",
|
||||
help="Kconfig warnings are not considered errors")
|
||||
|
||||
parser.add_argument(
|
||||
"-d", "--debug",
|
||||
action="store_true",
|
||||
@ -140,9 +378,27 @@ only supported for backwards compatibility).
|
||||
logging.basicConfig(format='[genconfig.py]:%(levelname)s-%(message)s',
|
||||
level=log_level)
|
||||
|
||||
kconf = kconfiglib.Kconfig(args.kconfig_filename)
|
||||
kconf = kconfiglib.Kconfig(args.kconfig_filename, warn_to_stderr=False)
|
||||
merge_configs(kconf, args.config_sources)
|
||||
|
||||
# HACK: Force all symbols to be evaluated, to catch warnings generated
|
||||
# during evaluation (such as out-of-range integers)
|
||||
kconf.write_config(os.devnull)
|
||||
|
||||
if not check_configs(kconf) and not args.ignore_config_errors:
|
||||
sys.exit(1)
|
||||
|
||||
if kconf.warnings:
|
||||
if args.warnings_are_not_errors:
|
||||
for warning in kconf.warnings:
|
||||
log(warning, level=logging.WARNING)
|
||||
else:
|
||||
log_error("Treating Kconfig warnings as errors:")
|
||||
for warning in kconf.warnings:
|
||||
log_error("=> {}".format(warning))
|
||||
if not args.ignore_config_errors:
|
||||
sys.exit(1)
|
||||
|
||||
if args.config_out is not None:
|
||||
logging.debug(kconf.write_config(args.config_out, save_old=False))
|
||||
|
||||
@ -150,13 +406,13 @@ only supported for backwards compatibility).
|
||||
logging.debug(kconf.write_autoconf(args.header_path))
|
||||
|
||||
if args.sync_deps is not None:
|
||||
logging.debug("Incremental build header files generated at '{}'".format(args.sync_deps))
|
||||
log("Incremental build header files generated at '{}'".format(args.sync_deps))
|
||||
kconf.sync_deps(args.sync_deps)
|
||||
|
||||
if args.file_list is not None:
|
||||
if args.config_out is None:
|
||||
raise NoConfigurationFile("Can't generate Kconfig dependency file without configuration file")
|
||||
logging.debug("Kconfig dependencies written to '{}'".format(args.file_list))
|
||||
log("Kconfig dependencies written to '{}'".format(args.file_list))
|
||||
with open(args.file_list, "w", encoding="utf-8") as f:
|
||||
f.write("{}: \\\n".format(args.config_out))
|
||||
# add dependencies
|
||||
@ -168,7 +424,7 @@ only supported for backwards compatibility).
|
||||
f.write("{}:\n\n".format(os.path.abspath(path)))
|
||||
|
||||
if args.env_list is not None:
|
||||
logging.debug("Kconfig environmental variables written to '{}'".format(args.env_list))
|
||||
log("Kconfig environmental variables written to '{}'".format(args.env_list))
|
||||
with open(args.env_list, "w", encoding="utf-8") as f:
|
||||
for env_var in kconf.env_vars:
|
||||
f.write("{}={}\n".format(env_var, os.environ[env_var]))
|
||||
|
@ -144,7 +144,8 @@ $(KCONFIG_OUT_CONFIG): $(GENERATED_DEPENDENCIES_DEP) $(GENCONFIG) $(MERGE_SOURCE
|
||||
$(Q) $(GENCONFIG) \
|
||||
--config-out=$(KCONFIG_OUT_CONFIG) \
|
||||
--file-list $(KCONFIG_OUT_DEP) \
|
||||
--kconfig-filename $(KCONFIG) \
|
||||
--kconfig-filename $(KCONFIG) $(if $(Q),,--debug )\
|
||||
$(if $(filter 1,$(KCONFIG_IGNORE_CONFIG_ERRORS)), --ignore-config-errors) \
|
||||
--config-sources $(MERGE_SOURCES) && \
|
||||
touch $(KCONFIG_OUT_CONFIG)
|
||||
|
||||
@ -159,7 +160,8 @@ $(KCONFIG_GENERATED_AUTOCONF_HEADER_C): $(KCONFIG_OUT_CONFIG) $(GENERATED_DIR_DE
|
||||
$(Q) $(GENCONFIG) \
|
||||
--header-path $(KCONFIG_GENERATED_AUTOCONF_HEADER_C) \
|
||||
--sync-deps $(KCONFIG_SYNC_DIR) \
|
||||
--kconfig-filename $(KCONFIG) \
|
||||
--kconfig-filename $(KCONFIG) $(if $(Q),,--debug ) \
|
||||
$(if $(filter 1,$(KCONFIG_IGNORE_CONFIG_ERRORS)), --ignore-config-errors) \
|
||||
--config-sources $(KCONFIG_OUT_CONFIG) && \
|
||||
touch $(KCONFIG_GENERATED_AUTOCONF_HEADER_C)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user