Skip to content

Instantly share code, notes, and snippets.

@noahp
Last active March 26, 2026 15:14
Show Gist options
  • Select an option

  • Save noahp/da7ade90de22664b2e3fbf5c56a1013a to your computer and use it in GitHub Desktop.

Select an option

Save noahp/da7ade90de22664b2e3fbf5c56a1013a to your computer and use it in GitHub Desktop.
Zephyr compliance check wrapper for adding exceptions
#!/usr/bin/env python3
"""
Wrapper for check_compliance.py that patches Kconfig allowlists
with extra entries from config files or environment variables.
Patches three checks:
- UNDEF_KCONFIG_ALLOWLIST: for undefined CONFIG_FOO references outside Kconfig
files (C files, CMake, etc.)
- check_no_undef_within_kconfig: for undefined symbol references inside Kconfig
files (kconfiglib warnings)
- kconfiglib.Kconfig._check_undef_syms: the underlying kconfiglib method that
generates undefined symbol warnings
All three use the same extra entries so one config covers all checks.
Usage:
python3 check_compliance_wrapper.py [args...]
With config file (.kconfig_allowlist_extras.yaml):
extra_allowlist:
- MY_SYMBOL
- ANOTHER_SYMBOL
python3 check_compliance_wrapper.py [args...]
"""
import os
import re
import sys
from pathlib import Path
import yaml
def load_extra_allowlist():
"""
Load extra allowlist entries from multiple sources:
1. .kconfig_allowlist_extras.yaml in current directory
2. ~/.kconfig_allowlist_extras.yaml in home directory
3. KCONFIG_ALLOWLIST_EXTRAS environment variable (comma-separated)
Returns:
set: Additional symbols to add to allowlist
"""
extra_entries = set()
# Try loading from config files (project-specific and home directory)
config_files = [
Path.cwd() / ".kconfig_allowlist_extras.yaml",
Path.home() / ".kconfig_allowlist_extras.yaml",
]
for config_file in config_files:
if config_file.exists():
try:
with open(config_file) as f:
config = yaml.safe_load(f) or {}
entries = config.get("extra_allowlist", [])
if isinstance(entries, list):
extra_entries.update(entries)
if entries:
print(
f"[DEBUG] Loaded {len(entries)} entries from {config_file}",
file=sys.stderr,
)
except Exception as e:
print(f"[WARNING] Could not load {config_file}: {e}", file=sys.stderr)
# Also check environment variable
env_extras = os.environ.get("KCONFIG_ALLOWLIST_EXTRAS", "")
if env_extras:
env_entries = {item.strip() for item in env_extras.split(",") if item.strip()}
extra_entries.update(env_entries)
if env_entries:
print(
f"[DEBUG] Loaded {len(env_entries)} entries from KCONFIG_ALLOWLIST_EXTRAS",
file=sys.stderr,
)
return extra_entries
def patch_kconfig_allowlist(extra_entries):
"""
Patch KconfigCheck class(es) to include extra allowlist entries in both:
1. UNDEF_KCONFIG_ALLOWLIST (used by check_no_undef_outside_kconfig)
2. check_no_undef_within_kconfig (kconfiglib undefined-symbol warnings)
Args:
extra_entries (set): Additional symbols to add to allowlist
"""
if not extra_entries:
return 0 # No patches needed
import check_compliance
patched_count = 0
# Only patch the two primary Kconfig check classes.
# Subclasses (KconfigHWMv2Check, KconfigBasicNoModulesCheck, etc.) inherit
# both the allowlist set and the method from their patched parent, so they
# don't need (and shouldn't receive) their own patch.
target_names = {"KconfigCheck", "KconfigBasicCheck"}
for name in dir(check_compliance):
if name not in target_names:
continue
obj = getattr(check_compliance, name)
if not (
isinstance(obj, type) and issubclass(obj, check_compliance.ComplianceTest)
):
continue
# Patch 1: UNDEF_KCONFIG_ALLOWLIST (outside-Kconfig check)
if hasattr(obj, "UNDEF_KCONFIG_ALLOWLIST"):
original_size = len(obj.UNDEF_KCONFIG_ALLOWLIST)
obj.UNDEF_KCONFIG_ALLOWLIST = obj.UNDEF_KCONFIG_ALLOWLIST.union(
extra_entries
)
new_additions = len(obj.UNDEF_KCONFIG_ALLOWLIST) - original_size
print(
f"[INFO] Patched {name}.UNDEF_KCONFIG_ALLOWLIST: added {new_additions} entries "
f"(total: {len(obj.UNDEF_KCONFIG_ALLOWLIST)})",
file=sys.stderr,
)
# Patch 2: check_no_undef_within_kconfig (inside-Kconfig kconfiglib warnings)
if hasattr(obj, "check_no_undef_within_kconfig"):
_patch_check_no_undef_within_kconfig(obj)
print(
f"[INFO] Patched {name}.check_no_undef_within_kconfig to filter by allowlist",
file=sys.stderr,
)
patched_count += 1
return patched_count
def _patch_check_no_undef_within_kconfig(cls):
"""
Replace check_no_undef_within_kconfig on 'cls' with a version that filters
out kconfiglib warnings for symbols present in UNDEF_KCONFIG_ALLOWLIST.
kconfiglib warning format:
"warning: undefined symbol MY_SYMBOL:\n\n- Referenced at ..."
"""
_sym_re = re.compile(r"undefined symbol (\w+)")
def patched_check_no_undef_within_kconfig(self, kconf):
def is_allowlisted(warning):
m = _sym_re.search(warning)
return m is not None and m.group(1) in self.UNDEF_KCONFIG_ALLOWLIST
undef_ref_warnings = "\n\n\n".join(
warning
for warning in kconf.warnings
if "undefined symbol" in warning and not is_allowlisted(warning)
)
if undef_ref_warnings:
self.failure(f"Undefined Kconfig symbols:\n\n {undef_ref_warnings}")
cls.check_no_undef_within_kconfig = patched_check_no_undef_within_kconfig
def patch_kconfiglib_check_undef_syms(extra_entries, allowlist):
"""
Patch kconfiglib.Kconfig._check_undef_syms to filter out allowlisted symbols.
This patches the underlying kconfiglib method that generates undefined symbol
warnings when parsing Kconfig files.
Args:
extra_entries (set): Additional symbols to add to allowlist
allowlist (set): Full allowlist from check_compliance classes
"""
if not extra_entries and not allowlist:
return False
try:
# Import kconfiglib from the scripts/kconfig directory
kconfig_dir = Path(__file__).resolve().parent.parent / "kconfig"
if str(kconfig_dir) not in sys.path:
sys.path.insert(0, str(kconfig_dir))
import kconfiglib
# Combine all allowlist entries
full_allowlist = allowlist.union(extra_entries) if allowlist else extra_entries
print(
f"[DEBUG] Patching kconfiglib.Kconfig._check_undef_syms with {len(full_allowlist)} allowlist entries",
file=sys.stderr,
)
# Store the original method
original_check_undef_syms = kconfiglib.Kconfig._check_undef_syms
def patched_check_undef_syms(self):
"""Patched version that filters out allowlisted symbols"""
def is_num(s):
# Same logic as original
try:
int(s)
except ValueError:
if not s.startswith(("0x", "0X")):
return False
try:
int(s, 16)
except ValueError:
return False
return True
# Iterate over symbols, but skip allowlisted ones
_IS_PY2 = sys.version_info[0] < 3
for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)():
if not sym.nodes and not is_num(sym.name) and sym.name != "MODULES":
# Skip if symbol is in allowlist
if sym.name in full_allowlist:
print(
f"[DEBUG] Skipping allowlisted symbol: {sym.name}",
file=sys.stderr,
)
continue
msg = "undefined symbol {}:".format(sym.name)
for node in self.node_iter():
if sym in node.referenced:
msg += "\n\n- Referenced at {}:{}:\n\n{}".format(
node.item.loc[0], node.item.loc[1], node
)
self._warn(msg)
# Apply the patch
kconfiglib.Kconfig._check_undef_syms = patched_check_undef_syms
print(
f"[INFO] Patched kconfiglib.Kconfig._check_undef_syms to filter {len(full_allowlist)} allowlisted symbols",
file=sys.stderr,
)
return True
except Exception as e:
print(
f"[WARNING] Could not patch kconfiglib._check_undef_syms: {e}",
file=sys.stderr,
)
return False
def main():
"""Main entry point"""
# Add scripts/ci directory to path so we can import check_compliance
scripts_ci = Path(__file__).resolve().parent
sys.path.insert(0, str(scripts_ci))
# Load extra entries from all sources
extra_entries = load_extra_allowlist()
# Import and patch
import check_compliance
# Collect the full allowlist for kconfiglib patching
full_allowlist = set()
if extra_entries:
patched = patch_kconfig_allowlist(extra_entries)
if patched:
print(
f"[INFO] Successfully patched {patched} class(es) with "
f"{len(extra_entries)} extra allowlist entries",
file=sys.stderr,
)
else:
print("[WARNING] No patchable classes found", file=sys.stderr)
else:
print("[DEBUG] No extra allowlist entries to patch", file=sys.stderr)
# Collect the full allowlist from KconfigCheck classes
for name in ["KconfigCheck", "KconfigBasicCheck"]:
if hasattr(check_compliance, name):
cls = getattr(check_compliance, name)
if hasattr(cls, "UNDEF_KCONFIG_ALLOWLIST"):
full_allowlist.update(cls.UNDEF_KCONFIG_ALLOWLIST)
# Patch kconfiglib._check_undef_syms with the combined allowlist
if extra_entries or full_allowlist:
patch_kconfiglib_check_undef_syms(extra_entries, full_allowlist)
# Run check_compliance with original arguments
check_compliance.main(sys.argv[1:])
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment