Last active
March 26, 2026 15:14
-
-
Save noahp/da7ade90de22664b2e3fbf5c56a1013a to your computer and use it in GitHub Desktop.
Zephyr compliance check wrapper for adding exceptions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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