blob: 4d3827bc822c39b19853240365e9818043fa5c99 [file] [edit]
# 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()