From: "Ahmed S. Darwish" <darwi@linutronix.de>
To: Clark Williams <clrkwllms@kernel.org>,
Steven Rostedt <rostedt@goodmis.org>,
Ingo Molnar <mingo@redhat.com>,
Nathan Chancellor <nathan@kernel.org>,
Nicolas Schier <nsc@kernel.org>
Cc: "Sebastian Andrzej Siewior" <bigeasy@linutronix.de>,
"Frederic Weisbecker" <frederic@kernel.org>,
"Greg Kroah-Hartman" <gregkh@linuxfoundation.org>,
"Jonathan Corbet" <corbet@lwn.net>,
"John Ogness" <john.ogness@linutronix.de>,
"Derek Barbosa" <debarbos@redhat.com>,
"Zbigniew Jędrzejewski-Szmek" <zbyszek@amutable.com>,
"Daan De Meyer" <daan.j.demeyer@gmail.com>,
"Luca Boccassi" <luca.boccassi@gmail.com>,
linux-rt-devel@lists.linux.dev, debian-kernel@lists.debian.org,
linux-kbuild@vger.kernel.org, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org,
"Ahmed S. Darwish" <darwi@linutronix.de>
Subject: [PATCH v1 03/10] kconfig-checker: Add the configuration checker code
Date: Fri, 6 Nov 2026 10:19:15 +0100 [thread overview]
Message-ID: <20261106091922.7560-4-darwi@linutronix.de> (raw)
In-Reply-To: <20261106091922.7560-1-darwi@linutronix.de>
Introduce a kernel configuration checker.
Validate the passed kernel .config against a YAML rules file and report
problematic configuration issues by severity.
For correctness, readability, and long-term maintainability, type-annotate
all the Python code and split the tool's logic into a clean set of classes.
Create a proper Python objects data model for the kconfig checker to work
with. Confine all direct YAML dictionary access to YAML files loading,
validation, and data model construction.
Document the tool's development rules on top.
Signed-off-by: Ahmed S. Darwish <darwi@linutronix.de>
---
tools/kconfig-checker/check.py | 558 +++++++++++++++++++++++++++++++++
1 file changed, 558 insertions(+)
create mode 100755 tools/kconfig-checker/check.py
diff --git a/tools/kconfig-checker/check.py b/tools/kconfig-checker/check.py
new file mode 100755
index 000000000000..476cd13e59eb
--- /dev/null
+++ b/tools/kconfig-checker/check.py
@@ -0,0 +1,558 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Kernel .config checker
+#
+# Main usage:
+#
+# $ check.py list-rules
+# $ check.py check -c path/to/linux/.config -r preempt_rt
+#
+# Notes:
+#
+# - All code is to be fully type-annotated. Before introducing changes,
+# please verify type-safety using:
+#
+# $ mypy --strict --no-warn-return-any check.py
+#
+# - The minimum required Python version must not be newer than what is
+# required by the kernel build at Documentation/process/changes.rst.
+#
+# - The tool must not depend on being run inside the kernel source tree.
+# This simplifies invocation, testing and possible future packaging.
+#
+# - The YAML rule files are only treated as a serialization format. After
+# loading and schema validation, such rules are parsed into an internal
+# data model: Severity, Rule, SymbolRule, PatternRule, Rules, and other
+# related python objects.
+#
+# All checker logic must operate on that typed model, and not on the raw
+# YAML dictionaries.
+#
+# - Minimize external dependencies: this tool is also intended to be run
+# from distribution kernel-package scripts in the future.
+#
+# Script exit codes:
+#
+# os.EX_CONFIG passed kernel .config does not match rules
+# os.EX_NOINPUT input file(s) could not be opened
+# os.EX_DATAERR YAML/schema/rule data is malformed
+# os.EX_UNAVAILABLE Python version or dependency is unavailable
+#
+
+from __future__ import annotations
+
+import argparse
+import os
+import re
+import sys
+import textwrap
+
+from dataclasses import dataclass
+from enum import Enum, auto
+from pathlib import Path
+from typing import Any, NoReturn, Optional
+
+#
+# Initial sanity checks:
+#
+
+def error(message: str, code: int) -> NoReturn:
+ print(f'Error: {message}', file=sys.stderr)
+ sys.exit(code)
+
+if sys.version_info < (3, 9):
+ error('Python 3.9 or later is required', os.EX_UNAVAILABLE)
+
+try:
+ import jsonschema # type: ignore
+ import yaml as yaml_m # type: ignore
+except ModuleNotFoundError as e:
+ error(f'Missing Python module: {e.name}', os.EX_UNAVAILABLE)
+
+#
+# Actual script:
+#
+
+class KernelConfig:
+ """.config file parser"""
+
+ def __init__(self, file: Path):
+ try:
+ self.lines = file.read_text().splitlines()
+ except OSError as e:
+ error(f'Could not open {file}: {e.strerror}', os.EX_NOINPUT)
+
+ self.symbols = self._parse_symbols()
+
+ def _parse_symbols(self) -> dict[str, str]:
+ """Extract CONFIG_* symbol assignments from .config"""
+ symbol_set = re.compile(r'^(CONFIG_[A-Za-z0-9_]+)=([ym])$')
+ symbol_unset = re.compile(r'^# (CONFIG_[A-Za-z0-9_]+) is not set$')
+
+ symbols: dict[str, str] = {}
+ for line in self.lines:
+ line = line.rstrip('\n')
+
+ if m := symbol_set.match(line):
+ symbols[m.group(1)] = m.group(2)
+ continue
+
+ if m := symbol_unset.match(line):
+ symbols[m.group(1)] = 'n'
+
+ return symbols
+
+ def get_symbol_value(self, sym_name: str) -> Optional[str]:
+ """Return a .config symbol value: 'y', 'm', or 'n'"""
+ return self.symbols.get(sym_name)
+
+ def get_matching_symbols(self, pa: re.Pattern[str]) -> list[str]:
+ """Get all CONFIG_* symbol names matching passed regex"""
+ return [ sym for sym in self.symbols if pa.search(sym) ]
+
+YamlDict = dict[str, Any]
+
+class Helpers:
+ """YAML file access helpers"""
+
+ @staticmethod
+ def load_yaml(yaml_f: Path) -> YamlDict:
+ try:
+ with open(yaml_f, 'r') as f:
+ return yaml_m.safe_load(f)
+ except OSError as e:
+ error(f'Could not open {yaml_f}: {e.strerror}', os.EX_NOINPUT)
+ except yaml_m.YAMLError as e:
+ error(f'Invalid YAML syntax at {yaml_f}:\n {e}', os.EX_DATAERR)
+
+ @staticmethod
+ def validate_yaml(yaml: YamlDict, schema: YamlDict, f_name: str) -> None:
+ try:
+ jsonschema.validate(yaml, schema)
+ except jsonschema.exceptions.ValidationError as e:
+ error(
+ f'{f_name} does not match its schema\n'
+ f'Reason: {e.message}',
+ os.EX_DATAERR
+ )
+
+class Schema:
+ """Rules file schema, in json-schema.org format"""
+
+ def __init__(self, schema_f: Path):
+ self.schema = Helpers.load_yaml(schema_f)
+ # Validate the schema itself against draft-07 metaschema
+ jsonschema.Draft7Validator.check_schema(self.schema)
+
+ def as_dict(self) -> YamlDict:
+ return self.schema
+
+class ExpectedValue(Enum):
+ """Expected values for a .config symbol"""
+
+ y = auto() # 'y' implies a built-in or a module; as in schema
+ n = auto()
+ allowed = auto()
+
+class Severity(Enum):
+ """Rule-check severity"""
+
+ fatal = auto()
+ high = auto()
+ medium = auto()
+ low = auto()
+ info = auto()
+
+class Rule:
+ """Single rule entry"""
+
+ expected: ExpectedValue
+ severity: Severity
+ rationale: Optional[str] = None
+ docs_hint: Optional[str] = None
+
+ def __init__(self, rule: YamlDict) -> None:
+ self.expected = ExpectedValue[rule['expected']]
+ self.severity = Severity[rule['severity']]
+ self.rationale = rule.get('rationale')
+ self.docs_hint = rule.get('docs_hint')
+ self._normalize_docs_hint()
+
+ def _normalize_docs_hint(self) -> None:
+ """Transform the hint to a URI fragment identifier"""
+ if self.docs_hint:
+ self.docs_hint = '#' + self.docs_hint
+
+class SymbolRule(Rule):
+ """Rule entry for one .config symbol"""
+
+ symbol: str
+
+ def __init__(self, rule: YamlDict):
+ self.symbol = rule['name']
+ super().__init__(rule)
+
+ def _normalize_docs_hint(self) -> None:
+ """Transform the hint to a docs.kernel.org URI fragment identifier"""
+ if self.docs_hint == 'config':
+ self.docs_hint = self.symbol.lower().replace('_', '-')
+
+ super()._normalize_docs_hint()
+
+class PatternRule(Rule):
+ """Rule entry for a regex pattern of .config symbols"""
+
+ symbol_regex: re.Pattern[str]
+
+ def __init__(self, rule: YamlDict):
+ regex = rule['name_pattern']
+ try:
+ self.symbol_regex = re.compile(regex)
+ except re.error as e:
+ error(f'Invalid symbol regex {regex}: {e}', os.EX_DATAERR)
+
+ super().__init__(rule)
+
+@dataclass
+class Rules:
+ """The .config checker rules repository / data model"""
+
+ rules: list[Rule]
+ docs_url: Optional[str] = None
+
+ def add_rule(self, rule: Rule) -> None:
+ """Add given rule to the existing rules repository"""
+ if isinstance(rule, SymbolRule):
+ symbol = rule.symbol
+ if self.get_symbol_rule(symbol):
+ error(f'Duplicate rule name: {symbol}', os.EX_DATAERR)
+
+ if isinstance(rule, PatternRule):
+ pattern = rule.symbol_regex.pattern
+ if self.get_pattern_rule(pattern):
+ error(f'Duplicate rule pattern: {pattern}', os.EX_DATAERR)
+
+ self.rules.append(rule)
+
+ def get_docs_url_for(self, rule: Rule) -> Optional[str]:
+ """Return full documentation URL for a rule, if available"""
+ if not self.docs_url or not rule.docs_hint:
+ return None
+
+ return f'{self.docs_url}{rule.docs_hint}'
+
+ def get_symbol_rule(self, kconfig_symbol: str) -> Optional[SymbolRule]:
+ """Return symbol rule matching passed .config symbol, if any"""
+ for rule in self.rules:
+ if isinstance(rule, SymbolRule):
+ if rule.symbol == kconfig_symbol:
+ return rule
+
+ return None
+
+ def get_pattern_rule(self, pattern: str) -> Optional[PatternRule]:
+ """Return pattern rule matching passed .config symbol, if any"""
+ for rule in self.rules:
+ if isinstance(rule, PatternRule):
+ if rule.symbol_regex.pattern == pattern:
+ return rule
+
+ return None
+
+class RulesFile:
+ """YAML rules file access"""
+
+ yaml: YamlDict
+
+ def __init__(self, yaml_f: Path, schema: Schema):
+ self.yaml = Helpers.load_yaml(yaml_f)
+ Helpers.validate_yaml(self.yaml, schema.as_dict(), yaml_f.name)
+
+ def parse(self) -> Rules:
+ """Parse the YAML rules into a python Rules object"""
+ rules = Rules([])
+
+ for rule in self.yaml.get('rules', []):
+ if 'name' in rule:
+ rules.add_rule(SymbolRule(rule))
+ continue
+
+ if 'name_pattern' in rule:
+ rules.add_rule(PatternRule(rule))
+ continue
+
+ rules.docs_url = self.yaml.get('docs_url')
+ return rules
+
+@dataclass
+class Issue:
+ message: str
+ docs_url: Optional[str]
+ severity: Severity
+ rationale: Optional[str]
+
+@dataclass
+class SymbolIssue(Issue):
+ """Checker issue concerning a single .config symbol"""
+ symbol: str
+
+@dataclass
+class PatternIssue(Issue):
+ """Checker issue concerning a regex pattern of .config symbols"""
+ symbols: list[str]
+
+class Issues:
+ """Repository of all issues found by the checker"""
+
+ def __init__(self, issues_l: list[Issue]):
+ # Index all issues by severity
+ self.issues: dict[Severity, list[Issue]] = {
+ severity: [] for severity in Severity
+ }
+ for issue in issues_l:
+ self.issues[issue.severity].append(issue)
+
+ def by_severity(self, severity: Severity) -> list[Issue]:
+ return self.issues[severity]
+
+@dataclass
+class Report:
+ """kconfig checker sanity report. Main script output"""
+
+ issues: Issues
+
+ def _print_header(self, header: str) -> None:
+ print(header)
+ print('=' * len(header))
+ print()
+
+ def _print_symbol(self, issue: SymbolIssue) -> None:
+ print(f'* {issue.symbol} is {issue.message}')
+ print()
+
+ def _print_pattern(self, issue: PatternIssue) -> None:
+ print(f'* Below options are {issue.message}:\n')
+ for symbol in issue.symbols:
+ print(f' {symbol}')
+ print()
+
+ def _print_issue(self, issue: Issue) -> None:
+ if isinstance(issue, SymbolIssue):
+ self._print_symbol(issue)
+ elif isinstance(issue, PatternIssue):
+ self._print_pattern(issue)
+
+ def _print_rationale(self, issue: Issue) -> None:
+ if not issue.docs_url and not issue.rationale:
+ return
+
+ print(' Rationale:')
+
+ if issue.docs_url:
+ print(' ', issue.docs_url)
+ if issue.rationale:
+ print()
+
+ if issue.rationale:
+ print(textwrap.fill(
+ issue.rationale,
+ width=75,
+ initial_indent=' ',
+ subsequent_indent=' ',
+ ))
+
+ print()
+
+ def print(self) -> None:
+ """Print the checker's .config report, in order of severity"""
+ print()
+ for severity in Severity:
+ issues = self.issues.by_severity(severity)
+ if not issues:
+ continue
+
+ self._print_header('Configuration Issues with severity = ' +
+ severity.name.upper())
+
+ for issue in issues:
+ self._print_issue(issue)
+ self._print_rationale(issue)
+
+@dataclass
+class Checker:
+ """The actual kconfig checker machinery"""
+
+ kconfig: KernelConfig
+ rules: Rules
+
+ def _find_issue_symbol(self, rule: SymbolRule) -> Optional[SymbolIssue]:
+ """Check the .config against passed rule. Return found issues, if any"""
+ value = self.kconfig.get_symbol_value(rule.symbol)
+ docs_url = self.rules.get_docs_url_for(rule)
+
+ if rule.expected == ExpectedValue.y:
+ if value is None:
+ return SymbolIssue(
+ 'not in .config',
+ docs_url,
+ rule.severity,
+ rule.rationale,
+ rule.symbol,
+ )
+ if value == 'n':
+ return SymbolIssue(
+ 'unset',
+ docs_url,
+ rule.severity,
+ rule.rationale,
+ rule.symbol,
+ )
+
+ if rule.expected == ExpectedValue.n:
+ if value in ('y', 'm'):
+ return SymbolIssue(
+ 'set',
+ docs_url,
+ rule.severity,
+ rule.rationale,
+ rule.symbol,
+ )
+
+ return None
+
+ def _find_issue_pattern(self, pattern_rule: PatternRule) -> Optional[PatternIssue]:
+ """Check the .config against a pattern rule. Return found issues, if any."""
+ if pattern_rule.expected == ExpectedValue.allowed:
+ return None
+
+ violation_type = 'unset' if pattern_rule.expected == ExpectedValue.y else 'set'
+ docs_url = self.rules.get_docs_url_for(pattern_rule)
+
+ violating_symbols = []
+
+ for symbol in self.kconfig.get_matching_symbols(pattern_rule.symbol_regex):
+ symbol_value = self.kconfig.get_symbol_value(symbol)
+
+ if pattern_rule.expected == ExpectedValue.y and symbol_value == 'n':
+ # Exclude the config symbol if it's explicitly disabled elsewhere
+ symbol_rule = self.rules.get_symbol_rule(symbol)
+ if symbol_rule and symbol_rule.expected == ExpectedValue.n:
+ continue
+
+ violating_symbols.append(symbol)
+
+ if pattern_rule.expected == ExpectedValue.n and symbol_value in ('y', 'm'):
+ # Exclude the config symbol if it's explicitly allowed elsewhere
+ symbol_rule = self.rules.get_symbol_rule(symbol)
+ if symbol_rule and symbol_rule.expected in (ExpectedValue.y, ExpectedValue.allowed):
+ continue
+
+ violating_symbols.append(symbol)
+
+ if not violating_symbols:
+ return None
+
+ return PatternIssue(
+ violation_type,
+ docs_url,
+ pattern_rule.severity,
+ pattern_rule.rationale,
+ violating_symbols,
+ )
+
+ def _find_issue(self, rule: Rule) -> Optional[Issue]:
+ """Check the .config against passed rule. Return found issues, if any"""
+ if isinstance(rule, SymbolRule):
+ return self._find_issue_symbol(rule)
+
+ if isinstance(rule, PatternRule):
+ return self._find_issue_pattern(rule)
+
+ return None
+
+ def find_issues(self) -> Optional[Report]:
+ """Check the .config file against required rules"""
+ issues: list[Issue] = []
+ for rule in self.rules.rules:
+ if issue := self._find_issue(rule):
+ issues.append(issue)
+
+ if issues:
+ return Report(Issues(issues))
+
+ return None
+
+def parse_args(rules: list[str]) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ prog='check.py',
+ description='Kernel .config options checker',
+ )
+
+ subparsers = parser.add_subparsers(
+ dest='command',
+ required=True,
+ )
+
+ subparsers.add_parser(
+ 'list-rules',
+ help='return available .config rule checking profiles',
+ )
+
+ check_parser = subparsers.add_parser(
+ 'check',
+ help='check a .config file against a rule profile',
+ )
+
+ check_parser.add_argument(
+ '-c',
+ '--config',
+ metavar='KCONFIG_FILE',
+ type=Path,
+ required=True,
+ help='.config file to be checked',
+ )
+
+ check_parser.add_argument(
+ '-r',
+ '--rules',
+ metavar='RULES',
+ choices=rules,
+ required=True,
+ help='rules file to check against',
+ )
+
+ return parser.parse_args()
+
+def main() -> None:
+ base_dir = Path(__file__).resolve().parent
+ rules_dir = base_dir / 'rules'
+
+ rules_l = sorted(path.stem for path in rules_dir.glob('*.yml'))
+ args = parse_args(rules_l)
+
+ if args.command == 'list-rules':
+ print(*rules_l, sep=', ')
+ return
+
+ kconfig_f = args.config.resolve()
+ kconfig = KernelConfig(kconfig_f)
+
+ schema_f = base_dir / 'schema.yml'
+ schema = Schema(schema_f)
+
+ rules_f = rules_dir / f'{args.rules}.yml'
+ rules = RulesFile(rules_f, schema).parse()
+
+ checker = Checker(kconfig, rules)
+ report = checker.find_issues()
+
+ if report:
+ report.print()
+ error(f'{kconfig_f} does not match required rules', os.EX_CONFIG)
+
+ print(f'Success: {kconfig_f} matches required rules')
+
+if __name__ == '__main__':
+ main()
--
2.56.0
next prev parent reply other threads:[~2026-11-06 9:19 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-06 9:19 [PATCH v1 00/10] tools: Introduce a kernel configuration checker Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 01/10] kconfig-checker: Add schema for rule files Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 02/10] kconfig-checker: Introduce PREEMPT_RT configuration rules Ahmed S. Darwish
2026-11-06 9:19 ` Ahmed S. Darwish [this message]
2026-11-06 9:19 ` [PATCH v1 04/10] kconfig: Introduce checkconfig target Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 05/10] Documentation: Describe kconfig-checker usage and design Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 06/10] kconfig-checker: Split schema to support component reuse Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 07/10] kconfig-checker: Introduce overlays schema Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 08/10] kconfig-checker: Add support for rule overlays Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 09/10] Documentation: Describe kconfig-checker overlays Ahmed S. Darwish
2026-11-06 9:19 ` [PATCH v1 10/10] kconfig: Introduce OVERLAY= for make checkconfig Ahmed S. Darwish
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261106091922.7560-4-darwi@linutronix.de \
--to=darwi@linutronix.de \
--cc=bigeasy@linutronix.de \
--cc=clrkwllms@kernel.org \
--cc=corbet@lwn.net \
--cc=daan.j.demeyer@gmail.com \
--cc=debarbos@redhat.com \
--cc=debian-kernel@lists.debian.org \
--cc=frederic@kernel.org \
--cc=gregkh@linuxfoundation.org \
--cc=john.ogness@linutronix.de \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-rt-devel@lists.linux.dev \
--cc=luca.boccassi@gmail.com \
--cc=mingo@redhat.com \
--cc=nathan@kernel.org \
--cc=nsc@kernel.org \
--cc=rostedt@goodmis.org \
--cc=zbyszek@amutable.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®