blob: ae436662fc03388714d7be75faf16cad3924f431 [file] [log] [blame]
#!/usr/bin/env vpython3
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Validate Zephyr project configuration files."""
import logging
import os
import pathlib
import site
import sys
import tempfile
import preupload.lib
EC_BASE = pathlib.Path(__file__).parent.parent
if "ZEPHYR_BASE" in os.environ:
ZEPHYR_BASE = pathlib.Path(os.environ.get("ZEPHYR_BASE"))
else:
ZEPHYR_BASE = pathlib.Path(
EC_BASE.resolve().parent.parent / "third_party" / "zephyr" / "main"
)
if not os.path.exists(ZEPHYR_BASE):
raise FileNotFoundError(
f"ZEPHYR_BASE path does not exist!\nZEPHYR_BASE={ZEPHYR_BASE}"
)
site.addsitedir(ZEPHYR_BASE / "scripts")
site.addsitedir(ZEPHYR_BASE / "scripts" / "kconfig")
# pylint:disable=import-error,wrong-import-position
import kconfiglib
import zephyr_module
# pylint:enable=import-error,wrong-import-position
# Known configuration file extensions.
CONF_FILE_EXT = (".conf", ".overlay", "_defconfig")
def _parse_args(argv):
parser = preupload.lib.argument_parser(description=__doc__)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Verbose Output"
)
parser.add_argument(
"-d",
"--dt-has",
action="store_true",
help="Check for options that depends on a DT_HAS_..._ENABLE symbol.",
)
args = parser.parse_args(argv)
preupload.lib.populate_default_filenames(args)
return args
def _init_log(verbose):
"""Initialize a logger object."""
console = logging.StreamHandler()
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
log = logging.getLogger(__file__)
log.addHandler(console)
if verbose:
log.setLevel(logging.DEBUG)
return log
def _default_y(defaults):
"""Return true if the symbol default is 'default y'
True if the symbol has any 'default y' definition, regardless of other
conditions.
"""
for val, _ in defaults:
if (
isinstance(val, kconfiglib.Symbol)
and val.is_constant
and val.str_value == "y"
):
return True
return False
def _default_y_if_ztest(defaults):
"""Return true if the symbol default is 'default y' if ZTEST
True if the symbol has any 'default y if ZTEST' definition.
"""
for val, cond in defaults:
if (
isinstance(val, kconfiglib.Symbol)
and val.is_constant
and val.str_value == "y"
and isinstance(cond, kconfiglib.Symbol)
and cond.name == "ZTEST"
):
return True
return False
class KconfigCheck:
"""Validate Zephyr project configuration files.
Attributes:
verbose: whether to enable verbose mode logging
"""
def __init__(self, verbose):
self.log = _init_log(verbose)
self.fail_count = 0
# Preload the upstream Kconfig.
self.program_kconf = {None: self._init_kconfig(None)}
def _init_kconfig(self, filename):
"""Initialize a kconfiglib object with all boards and arch options.
Args:
filename: the path of the Kconfig file to load.
Returns:
A kconfiglib.Kconfig object.
"""
with tempfile.TemporaryDirectory() as temp_dir:
modules = zephyr_module.parse_modules(
ZEPHYR_BASE, modules=[EC_BASE]
)
kconfig = ""
for module in modules:
kconfig += zephyr_module.process_kconfig(
module.project, module.meta
)
# generate Kconfig.modules file
with open(
pathlib.Path(temp_dir) / "Kconfig.modules",
"w",
encoding="utf-8",
) as file:
file.write(kconfig)
# generate few more stub files
(pathlib.Path(temp_dir) / "Kconfig.dts").touch()
(pathlib.Path(temp_dir) / "soc").mkdir()
(pathlib.Path(temp_dir) / "soc" / "Kconfig.soc").touch()
(pathlib.Path(temp_dir) / "soc" / "Kconfig.defconfig").touch()
(pathlib.Path(temp_dir) / "arch").mkdir()
(pathlib.Path(temp_dir) / "arch" / "Kconfig").touch()
os.environ["ZEPHYR_BASE"] = str(ZEPHYR_BASE)
os.environ["srctree"] = str(ZEPHYR_BASE)
os.environ["KCONFIG_BINARY_DIR"] = temp_dir
os.environ["ARCH_DIR"] = "arch"
os.environ["ARCH"] = "*"
os.environ["HWM_SCHEME"] = "v2"
os.environ["BOARD_DIR"] = "boards/posix/native_posix"
if not filename:
filename = os.path.join(ZEPHYR_BASE, "Kconfig")
self.log.info("Loading Kconfig: %s", filename)
return kconfiglib.Kconfig(filename)
def _kconf_from_path(self, path):
"""Return a Kconfig object for the specified path.
If path resides under zephyr/program, find the name of the program and
look for a corresponding program specific Kconfig file. If one is
present, return a corresponding Kconfig object for the program.
Stores a list of per-program Kconfig objects internally, so each
program Kconfig is only loaded once.
Args:
path: the path of the Kconfig file to load.
Returns:
A kconfiglib.Kconfig object.
"""
program_path = pathlib.Path(EC_BASE, "zephyr", "program")
file_path = pathlib.Path(path).resolve()
program = None
program_kconfig = None
if program_path in file_path.parents:
idx = file_path.parents.index(program_path)
program = file_path.parts[-(idx + 1)]
kconfig_path = pathlib.Path(program_path, program, "Kconfig")
if kconfig_path.is_file():
program_kconfig = kconfig_path
self.log.info(
"Path: %s, program: %s, program_kconfig: %s",
path,
program,
program_kconfig,
)
if program not in self.program_kconf:
if not program_kconfig:
self.program_kconf[program] = self.program_kconf[None]
else:
self.program_kconf[program] = self._init_kconfig(
program_kconfig
)
return self.program_kconf[program]
def _fail(self, *args):
"""Report a fail in the error log and increment the fail counter."""
self.fail_count += 1
self.log.error(*args)
def _filter_config_files(self, files):
"""Yield files with known config suffixes from the command line."""
for file in files:
if not file.exists():
self.log.info("Ignoring %s: file has been removed", file)
continue
if not file.name.endswith(CONF_FILE_EXT):
self.log.info("Ignoring %s: unrecognized suffix", file)
continue
yield file
def _check_dt_has(self, file_name):
"""Check file_name for known automatic config options.
Check file_name for any explicitly enabled option that has a dependency
on a devicetree symbol. These are normally enabled automatically so
there's no point enabling them explicitly.
"""
kconf = self._kconf_from_path(file_name)
symbols = {}
for name, val in kconf.syms.items():
dep = kconfiglib.expr_str(val.direct_dep)
if "DT_HAS_" in dep:
if _default_y(val.orig_defaults) and not _default_y_if_ztest(
val.orig_defaults
):
symbols[name] = dep
self.log.info("Checking %s", file_name)
with open(file_name, "r", encoding="utf-8") as file:
for line_num, line in enumerate(file.readlines(), start=1):
for name, dep in symbols.items():
match = f"CONFIG_{name}=y"
if line.startswith(match):
self.log.warning(
"%s:%d: unnecessary config option %s (depends on %s)",
file_name,
line_num,
match,
dep,
)
def run_checks(self, files, dt_has):
"""Run all config checks."""
config_files = self._filter_config_files(files)
for file in config_files:
if dt_has:
self._check_dt_has(file)
return self.fail_count
def main(argv):
"""Main function"""
args = _parse_args(argv)
kconfig_checker = KconfigCheck(args.verbose)
return kconfig_checker.run_checks(args.filename, args.dt_has)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
OSZAR »