| # Copyright 2026 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| |
| import os |
| import sys |
| import argparse |
| import tempfile |
| import re |
| import subprocess |
| from pathlib import Path |
| |
| import kconfig_utils |
| |
| try: |
| import yaml |
| try: |
| from yaml import CSafeLoader as SafeLoader |
| except ImportError: |
| from yaml import SafeLoader |
| except ImportError: |
| yaml = None |
| |
| def parse_args(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--zephyr-base", required=True) |
| parser.add_argument("--modules", nargs="+", default=[]) |
| parser.add_argument("--board-dirs", nargs="+", default=[]) |
| parser.add_argument("--oot-dts-roots", nargs="+", default=[]) |
| parser.add_argument("--output", required=True) |
| return parser.parse_args() |
| |
| def find_soc_source_directories(zephyr_base, modules=None): |
| soc_roots = [Path(zephyr_base)] + ([Path(m) for m in modules] if modules else []) |
| result = [] |
| for root in soc_roots: |
| soc_path = root / 'soc' |
| if soc_path.is_dir(): |
| for dirpath, _, filenames in os.walk(soc_path, followlinks=True): |
| if 'soc.yml' in filenames: |
| result.append(dirpath) |
| return result |
| |
| def find_board_source_directories(board_roots): |
| result = [] |
| for root in board_roots: |
| root_path = Path(root) |
| if not root_path.exists(): |
| continue |
| for dirpath, _, filenames in os.walk(root, followlinks=True): |
| if 'board.yml' in filenames: |
| result.append(dirpath) |
| # HWM v1 uses Kconfig.board or board.Kconfig |
| for kconfig_file in root_path.rglob('Kconfig.board'): |
| result.append(str(kconfig_file.parent)) |
| for kconfig_file in root_path.rglob('board.Kconfig'): |
| result.append(str(kconfig_file.parent)) |
| return list(set(result)) |
| |
| def get_soc_to_clusters(soc_dirs): |
| """Parses soc.yml files to map SoC names to their clusters.""" |
| soc_to_clusters = {} |
| if yaml is None: |
| return soc_to_clusters |
| |
| for sdir in soc_dirs: |
| soc_yml_path = os.path.join(sdir, "soc.yml") |
| if os.path.exists(soc_yml_path): |
| try: |
| with open(soc_yml_path, "r") as f: |
| content = yaml.load(f, Loader=SafeLoader) |
| |
| def find_socs(node): |
| if isinstance(node, dict): |
| if "socs" in node: |
| for soc in node["socs"]: |
| sn = soc.get("name") |
| if sn: |
| if sn not in soc_to_clusters: |
| soc_to_clusters[sn] = [] |
| if "cpuclusters" in soc: |
| for cluster in soc["cpuclusters"]: |
| cn = cluster.get("name") |
| if cn and cn not in soc_to_clusters[sn]: |
| soc_to_clusters[sn].append(cn) |
| for v in node.values(): |
| find_socs(v) |
| elif isinstance(node, list): |
| for item in node: |
| find_socs(item) |
| |
| find_socs(content) |
| except Exception as e: |
| raise Exception("DEBUG: Failed to parse %s: %s" % (soc_yml_path, e)) |
| return soc_to_clusters |
| |
| def discover_board_variants(board_dirs, soc_to_clusters, symbols): |
| """Discovers board and SoC variants from board.yml files using YAML parsing.""" |
| if yaml is None: |
| return |
| |
| for bdir in board_dirs: |
| for bfile in ["board.yml", "board.yaml"]: |
| bpath = os.path.join(bdir, bfile) |
| if os.path.exists(bpath): |
| try: |
| with open(bpath, "r") as f: |
| content = yaml.load(f, Loader=SafeLoader) |
| |
| if not content: |
| continue |
| |
| # Handle both 'board' (singular) and 'boards' (plural) formats |
| boards = [] |
| if isinstance(content.get("board"), dict): |
| boards.append(content["board"]) |
| if isinstance(content.get("boards"), list): |
| boards.extend(content["boards"]) |
| |
| for board in boards: |
| b_name = board.get("name", "").upper() |
| if not b_name: |
| continue |
| |
| socs = board.get("socs", []) |
| if not isinstance(socs, list): |
| continue |
| |
| for soc in socs: |
| sn_raw = soc.get("name") |
| if not sn_raw: |
| continue |
| |
| sn_upper = sn_raw.upper() |
| base_name = "CONFIG_BOARD_" + kconfig_utils.sanitize_name_for_target(b_name) + "_" + kconfig_utils.sanitize_name_for_target(sn_upper) |
| if base_name not in symbols: |
| symbols[base_name] = "bool" |
| |
| # Add board/soc variants from board.yml |
| variants = soc.get("variants", []) |
| if isinstance(variants, list): |
| for var in variants: |
| vn_raw = var.get("name") |
| if vn_raw: |
| vn_upper = vn_raw.upper() |
| var_name = base_name + "_" + kconfig_utils.sanitize_name_for_target(vn_upper) |
| if var_name not in symbols: |
| symbols[var_name] = "bool" |
| |
| # Add cluster variants |
| if sn_raw in soc_to_clusters: |
| for cn in soc_to_clusters[sn_raw]: |
| full_name = base_name + "_" + kconfig_utils.sanitize_name_for_target(cn.upper()) |
| if full_name not in symbols: |
| symbols[full_name] = "bool" |
| except Exception as e: |
| raise Exception("DEBUG: Failed to parse %s: %s" % (bpath, e)) |
| |
| def setup_kconfig_environment(zephyr_base, tmp_dir, modules=None): |
| """Sets up environment variables required for Kconfig parsing.""" |
| os.environ["ZEPHYR_BASE"] = zephyr_base |
| os.environ["srctree"] = zephyr_base |
| os.environ["KCONFIG_BINARY_DIR"] = tmp_dir |
| os.environ["CMAKE_BINARY_DIR"] = tmp_dir |
| os.environ["HWM_SCHEME"] = "v2" |
| os.environ["KCONFIG_FUNCTIONS"] = "kconfigfunctions" |
| os.environ["TOOLCHAIN_HAS_PICOLIBC"] = "y" |
| os.environ["TOOLCHAIN_HAS_NEWLIB"] = "y" |
| os.environ["ZEPHYR_TOOLCHAIN_VARIANT"] = "zephyr" |
| |
| kconfig_env_file = os.path.join(tmp_dir, "kconfig_module_dirs.env") |
| os.makedirs(tmp_dir, exist_ok=True) |
| with open(kconfig_env_file, "w") as f: |
| if modules: |
| for m in modules: |
| if os.path.isdir(m): |
| module_name = os.path.basename(m.rstrip('/')).split('+')[-1].replace('-', '_').upper() |
| m_abs = os.path.abspath(m) |
| f.write(f"ZEPHYR_{module_name}_MODULE_DIR={m_abs}\n") |
| os.environ[f"ZEPHYR_{module_name}_MODULE_DIR"] = m_abs |
| os.environ["KCONFIG_ENV_FILE"] = kconfig_env_file |
| |
| def generate_kconfig_scaffolding(tmp_dir, zephyr_base, soc_dirs, board_dirs, modules, oot_dts_roots=[]): |
| """Generates the necessary Kconfig tree scaffolding for super-schema parsing.""" |
| os.makedirs(tmp_dir, exist_ok=True) |
| |
| # Generate aggregation files |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "soc", "Kconfig.defconfig"), soc_dirs, "SoC defconfigs") |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "soc", "Kconfig"), soc_dirs, "SoC Kconfigs") |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "soc", "Kconfig.soc"), soc_dirs, "SoC Kconfigs") |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "soc", "Kconfig.sysbuild"), soc_dirs, "SoC sysbuild Kconfigs") |
| |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "boards", "Kconfig.defconfig"), board_dirs, "Board defconfigs") |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "boards", "Kconfig"), board_dirs, "Board Kconfigs", True) |
| kconfig_utils.generate_aggregation_file(os.path.join(tmp_dir, "boards", "Kconfig.sysbuild"), board_dirs, "Board sysbuild Kconfigs") |
| |
| # Mock arch Kconfig (Kconfig.zephyr sources it but we'll loop over archs) |
| os.makedirs(os.path.join(tmp_dir, "arch"), exist_ok=True) |
| with open(os.path.join(tmp_dir, "arch", "Kconfig"), "w") as f: |
| f.write("# Placeholder\n") |
| |
| kconfig_modules = os.path.join(tmp_dir, "Kconfig.modules") |
| kconfig_utils.generate_kconfig_modules(zephyr_base, modules, kconfig_modules) |
| |
| dts_roots = [Path(zephyr_base)] + [Path(m) for m in modules] + [Path(r) for r in oot_dts_roots] |
| kconfig_utils.generate_kconfig_dts(zephyr_base, os.path.join(tmp_dir, "Kconfig.dts"), dts_roots) |
| |
| # Mock currently unsupported parts of the Kconfig tree |
| for f_name in ["Kconfig.sysbuild.modules", "Kconfig.shield", "Kconfig.shield.defconfig"]: |
| with open(os.path.join(tmp_dir, f_name), "w") as f: |
| f.write("# Dummy\n") |
| |
| def parse_kconfig_authoritative(zephyr_base, tmp_dir, archs, symbols, warnings_list): |
| """Uses kconfiglib to perform an authoritative parse for each architecture.""" |
| # kconfiglib is in Zephyr's scripts/kconfig directory, which is added to path by setup_kconfiglib in main. |
| import kconfiglib |
| for arch in archs: |
| try: |
| os.environ["ARCH"] = arch |
| os.environ["BOARD"] = "generic" |
| os.environ["BOARD_QUALIFIERS"] = "" |
| os.environ["SOC_NAME"] = "none" |
| os.environ["KCONFIG_BOARD_DIR"] = tmp_dir |
| |
| # Update arch/Kconfig mock to source the real arch Kconfig |
| with open(os.path.join(tmp_dir, "arch", "Kconfig"), "w") as f: |
| arch_kconfig = os.path.join(zephyr_base, "arch", arch, "Kconfig") |
| if os.path.exists(arch_kconfig): |
| f.write('source "%s"\n' % arch_kconfig) |
| |
| toolchain_arch = "host" if arch == "posix" else "gnuarmemb" if arch == "arm" else "zephyr" |
| toolchain_kconfig_dir = os.path.join(zephyr_base, "cmake", "toolchain", toolchain_arch) |
| if not os.path.exists(toolchain_kconfig_dir): |
| toolchain_kconfig_dir = os.path.join(zephyr_base, "cmake", "toolchain", "zephyr") |
| os.environ["TOOLCHAIN_KCONFIG_DIR"] = toolchain_kconfig_dir |
| |
| kconf_path = os.path.join(zephyr_base, "Kconfig") |
| kconf = kconfiglib.Kconfig(kconf_path) |
| if kconf.warnings: |
| for warning in kconf.warnings: |
| print("\n" + warning, file=sys.stderr) |
| warnings_list.append(warning) |
| print("\nWarnings detected during Kconfig parsing, proceeding anyway.", file=sys.stderr) |
| |
| for name, sym in kconf.syms.items(): |
| if sym.type == kconfiglib.UNKNOWN: continue |
| full_name = "CONFIG_" + kconfig_utils.sanitize_name_for_target(name) |
| stype = "bool" |
| if sym.type in [kconfiglib.INT, kconfiglib.HEX]: stype = "int" |
| elif sym.type == kconfiglib.STRING: stype = "string" |
| |
| if full_name not in symbols or (stype != "bool" and symbols[full_name] == "bool"): |
| symbols[full_name] = stype |
| except Exception as e: |
| print("DEBUG: Exception parsing arch %s: %s" % (arch, e), file=sys.stderr) |
| pass |
| |
| def scan_files_for_dynamic_symbols(zephyr_base, board_dirs, modules, symbols): |
| """Scans files for symbols that might not be visible in the static Kconfig tree.""" |
| search_dirs = [zephyr_base] + list(board_dirs) + list(modules) |
| for d in search_dirs: |
| if not os.path.isdir(d): continue |
| for root, _, files in os.walk(d, followlinks=True): |
| for fname in files: |
| if fname == "Kconfig" or fname.startswith("Kconfig."): |
| try: |
| with open(os.path.join(root, fname), "r", errors="ignore") as f: |
| content = f.read() |
| # config/choice |
| for match in re.finditer(r"^\s*(?:menu)?(?:config|choice)\s+([a-zA-Z0-9_.-]+)", content, re.MULTILINE): |
| name = match.group(1) |
| if name == "MODULES": continue |
| full_name = "CONFIG_" + kconfig_utils.sanitize_name_for_target(name) |
| |
| # Look ahead for type |
| start = match.end() |
| next_match = re.search(r"^\s*(?:menu)?(?:config|choice)\s+", content[start:], re.MULTILINE) |
| end = start + next_match.start() if next_match else len(content) |
| block = content[start:end] |
| |
| stype = None |
| if re.search(r"^\s*int\b", block, re.MULTILINE): stype = "int" |
| elif re.search(r"^\s*hex\b", block, re.MULTILINE): stype = "int" |
| elif re.search(r"^\s*string\b", block, re.MULTILINE): stype = "string" |
| elif re.search(r"^\s*bool\b", block, re.MULTILINE): stype = "bool" |
| |
| if stype: |
| if full_name not in symbols: |
| symbols[full_name] = stype |
| else: |
| existing_type = symbols[full_name] |
| if existing_type != stype: |
| raise Exception(f"Conflict: Symbol {full_name} defined with types '{existing_type}' and '{stype}'") |
| else: |
| if full_name not in symbols: |
| symbols[full_name] = "bool" |
| |
| # Logging Template expansion |
| if 'subsys/logging/Kconfig.template.log_config' in content: |
| mod_match = re.search(r"module\s*=\s*([a-zA-Z0-9_]+)", content) |
| if mod_match: |
| mod_name = mod_match.group(1) |
| for suffix in ["_LOG_LEVEL_OFF", "_LOG_LEVEL_ERR", "_LOG_LEVEL_WRN", "_LOG_LEVEL_INF", "_LOG_LEVEL_DBG", "_LOG_LEVEL_DEFAULT", "_LOG_LEVEL"]: |
| full_name = "CONFIG_" + mod_name + suffix |
| if full_name not in symbols: |
| symbols[full_name] = "bool" if suffix != "_LOG_LEVEL" else "int" |
| except: |
| pass |
| |
| def write_bazel_build_file(output_file, symbols): |
| """Writes the discovered symbols into a BUILD.bazel file as Bazel flags.""" |
| with open(output_file, "w") as f: |
| f.write('load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "int_flag", "string_flag")\n') |
| f.write('load("@rules_cc//cc:defs.bzl", "cc_library")\n') |
| f.write('load("@zephyr//:cc.bzl", "zephyr_cc_library")\n\n') |
| f.write('package(default_visibility = ["//visibility:public"])\n\n') |
| f.write('exports_files(["local_modules.bzl"])\n\n') |
| f.write('zephyr_cc_library(\n') |
| f.write(' name = "kconfig_warnings",\n') |
| f.write(' srcs = ["kconfig_warnings.c"],\n') |
| f.write(')\n\n') |
| |
| for name in sorted(symbols.keys()): |
| stype = symbols[name] |
| if stype == "bool": |
| f.write('bool_flag(name = "%s", build_setting_default = False)\n' % name) |
| f.write('config_setting(name = "%s=true", flag_values = {":%s": "true"})\n' % (name, name)) |
| f.write('config_setting(name = "%s=false", flag_values = {":%s": "false"})\n' % (name, name)) |
| elif stype == "int": |
| f.write('int_flag(name = "%s", build_setting_default = 0)\n' % name) |
| f.write('config_setting(name = "%s=0", flag_values = {":%s": "0"})\n' % (name, name)) |
| else: |
| f.write('string_flag(name = "%s", build_setting_default = "")\n' % name) |
| |
| def main(): |
| args = parse_args() |
| kconfig_utils.setup_kconfiglib(args.zephyr_base) |
| |
| zephyr_base = os.path.abspath(args.zephyr_base) |
| tmp_dir = os.path.abspath("kconfig_bin") |
| symbols = {} |
| |
| # 1. Discover all hardware |
| soc_dirs = find_soc_source_directories(zephyr_base, args.modules) |
| board_roots = [os.path.join(zephyr_base, 'boards')] + args.board_dirs |
| board_dirs = find_board_source_directories(board_roots) |
| |
| soc_to_clusters = get_soc_to_clusters(soc_dirs) |
| discover_board_variants(board_dirs, soc_to_clusters, symbols) |
| |
| arch_root = os.path.join(zephyr_base, "arch") |
| archs = [d for d in os.listdir(arch_root) if os.path.isdir(os.path.join(arch_root, d))] |
| |
| # 2. Setup environment and scaffolding |
| setup_kconfig_environment(zephyr_base, tmp_dir, args.modules) |
| generate_kconfig_scaffolding(tmp_dir, zephyr_base, soc_dirs, board_dirs, args.modules, args.oot_dts_roots) |
| |
| # 3. Authoritative Parse using kconfiglib |
| all_warnings = [] |
| parse_kconfig_authoritative(zephyr_base, tmp_dir, archs, symbols, all_warnings) |
| |
| # Generate kconfig_warnings.c |
| output_dir = os.path.dirname(os.path.abspath(args.output)) |
| with open(os.path.join(output_dir, "kconfig_warnings.c"), "w") as f: |
| f.write("/* AUTO-GENERATED by kconfig_gen_symbols.py, do not edit! */\n\n") |
| if all_warnings: |
| for warning in all_warnings: |
| msg = warning.replace('"', '\\"').replace('\n', ' ') |
| f.write(f'#warning "Kconfig warning: {msg}"\n') |
| else: |
| f.write("/* No Kconfig warnings */\n") |
| |
| # 4. Fallback: Scan files for dynamic symbols |
| scan_files_for_dynamic_symbols(zephyr_base, args.board_dirs, args.modules, symbols) |
| |
| # 5. Generate BUILD.bazel |
| write_bazel_build_file(args.output, symbols) |
| |
| |
| if __name__ == "__main__": |
| main() |