blob: 994ffb47cc3ff11eb221482cc46185057743ea84 [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 subprocess
import json
import re
from pathlib import Path
import kconfig_utils
SUPPORTED_ARCHITECTURES = ["arc", "arm", "arm64", "mips", "posix", "riscv", "sparc", "xtensa", "x86", "rx"]
# Add Zephyr's kconfiglib to sys.path
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--zephyr-base", required=True)
parser.add_argument("--app-dir", required=True)
parser.add_argument("--board", required=True)
parser.add_argument("--board-dir", required=True)
parser.add_argument("--parent-platform", required=True)
parser.add_argument("--modules", nargs="+", default=[])
parser.add_argument("--output-dir", required=True)
parser.add_argument("--oot-dts-roots", nargs="+", default=[])
parser.add_argument("--app-name", default="zephyr_app")
parser.add_argument("--toolchain-variant", default=None)
parser.add_argument("--conf-fragments", nargs="+", default=[])
return parser.parse_args()
def setup_environment(args, zephyr_base, output_dir, board_dir):
os.environ["srctree"] = zephyr_base
os.environ["ZEPHYR_BASE"] = zephyr_base
os.environ["KCONFIG_BINARY_DIR"] = output_dir
# Replicate Zephyr SDK compiler capabilities for Kconfig
os.environ["TOOLCHAIN_HAS_PICOLIBC"] = "y"
os.environ["TOOLCHAIN_HAS_NEWLIB"] = "y"
if "/" in args.board:
board_name, board_qualifiers = args.board.split("/", 1)
os.environ["BOARD"] = board_name
os.environ["BOARD_QUALIFIERS"] = board_qualifiers
else:
os.environ["BOARD"] = args.board
os.environ["BOARD_QUALIFIERS"] = ""
os.environ["ARCH"] = "*"
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"
# Determine toolchain variant similar to Zephyr's FindHostTools.cmake
toolchain_variant = args.toolchain_variant
if not toolchain_variant:
board_dir_lower = board_dir.lower()
if "native" in args.board or "boards/native" in board_dir_lower or "posix" in board_dir_lower:
toolchain_variant = "host"
else:
toolchain_variant = "zephyr"
os.environ["TOOLCHAIN_KCONFIG_DIR"] = os.path.join(zephyr_base, "cmake", "toolchain", toolchain_variant)
os.environ["KCONFIG_BOARD_DIR"] = board_dir
os.environ["SOC_NAME"] = "none" # Will be overridden by defconfig
kconfig_env_file = os.path.join(output_dir, "kconfig_module_dirs.env")
os.makedirs(output_dir, exist_ok=True)
with open(kconfig_env_file, "w") as f:
if args.modules:
for m in args.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_aggregations(zephyr_base, args, output_dir, board_dir):
# Generate aggregation Kconfig files
soc_dirs = []
# Try to find SoC name from board.yml
soc_names = []
for board_file in ["board.yml", "board.yaml"]:
board_yml_path = os.path.join(board_dir, board_file)
if os.path.exists(board_yml_path):
with open(board_yml_path, 'r') as f:
content = f.read()
# Simple heuristic to find soc name in board.yml
# It looks like:
# socs:
# - name: same70q21b
matches = re.findall(r"-\s*name:\s*(\S+)", content)
if matches:
soc_names.extend(matches)
break
soc_roots = [Path(zephyr_base)] + ([Path(m) for m in args.modules] if args.modules else [])
for sroot in soc_roots:
if not sroot.exists():
continue
for root, _, files in os.walk(sroot, followlinks=True):
if "soc.yml" in files:
soc_dirs.append(root)
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "soc", "Kconfig.defconfig"), soc_dirs, "SoC defconfigs")
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "soc", "Kconfig"), soc_dirs, "SoC Kconfigs")
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "soc", "Kconfig.soc"), soc_dirs, "SoC Kconfigs")
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "soc", "Kconfig.sysbuild"), soc_dirs, "SoC sysbuild Kconfigs")
shield_defconfig = os.path.join(output_dir, "Kconfig.shield.defconfig")
os.makedirs(os.path.dirname(shield_defconfig), exist_ok=True)
with open(shield_defconfig, "w") as f:
f.write("# Empty shield defconfig\n")
arch_dirs = []
arch_root = os.path.join(zephyr_base, "arch")
for d in os.listdir(arch_root):
if os.path.isdir(os.path.join(arch_root, d)):
arch_dirs.append(os.path.join(arch_root, d))
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "arch", "Kconfig"), arch_dirs, "Arch Kconfigs")
board_dirs = [board_dir]
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "boards", "Kconfig.defconfig"), board_dirs, "Board defconfigs")
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "boards", "Kconfig"), board_dirs, "Board Kconfigs")
kconfig_utils.generate_aggregation_file(os.path.join(output_dir, "boards", "Kconfig.sysbuild"), board_dirs, "Board sysbuild Kconfigs")
board_name = os.environ["BOARD"]
board_kconfig_agg = os.path.join(output_dir, "boards", f"Kconfig.{board_name}")
os.makedirs(os.path.dirname(board_kconfig_agg), exist_ok=True)
with open(board_kconfig_agg, "w") as f:
board_kconfig = os.path.join(board_dir, "Kconfig")
if not os.path.exists(board_kconfig):
board_kconfig = os.path.join(board_dir, f"Kconfig.{board_name}")
if os.path.exists(board_kconfig):
f.write(f'osource "{os.path.abspath(board_kconfig)}"\n')
kconfig_modules = os.path.join(output_dir, "Kconfig.modules")
kconfig_utils.generate_kconfig_modules(zephyr_base, args.modules, kconfig_modules)
def discover_overlays(app_dir, board_name, board_qualifiers):
board_qualifiers = board_qualifiers.lstrip("/")
overlays = []
# 1. Board-specific overlays in app
if board_qualifiers:
safe_q = board_qualifiers.replace("/", "_")
soc_board_overlay = os.path.join(
app_dir, "boards", f"{board_name}_{safe_q}.overlay"
)
if os.path.exists(soc_board_overlay):
overlays.append(soc_board_overlay)
board_overlay = os.path.join(app_dir, "boards", f"{board_name}.overlay")
if os.path.exists(board_overlay):
overlays.append(board_overlay)
# 2. Default app overlay
app_overlay = os.path.join(app_dir, "app.overlay")
if os.path.exists(app_overlay):
overlays.append(app_overlay)
return overlays
def preprocess_dts(args, zephyr_base, output_dir, board_dir):
# Determine input DTS file
board_name = os.environ["BOARD"]
board_qualifiers = os.environ.get("BOARD_QUALIFIERS", "")
candidates = [
os.path.join(board_dir, f"{args.board.replace('/', '_')}.dts"),
os.path.join(board_dir, f"{board_name}.dts"),
]
# In HWM v2, try to find a DTS file matching any SoC in board.yml
for board_file in ["board.yml", "board.yaml"]:
board_yml_path = os.path.join(board_dir, board_file)
if os.path.exists(board_yml_path):
with open(board_yml_path, 'r') as f:
content = f.read()
matches = re.findall(r"-\s*name:\s*(\S+)", content)
for soc in matches:
candidates.append(os.path.join(board_dir, f"{board_name}_{soc}.dts"))
break
dts_file = None
for candidate in candidates:
if os.path.exists(candidate):
dts_file = candidate
break
if not dts_file:
raise RuntimeError(f"Could not find DTS file for board {args.board} in {board_dir}. Tried: {candidates}")
overlays = discover_overlays(args.app_dir, board_name, board_qualifiers)
dts_input_path = os.path.join(output_dir, "dts_input.c")
os.makedirs(output_dir, exist_ok=True)
with open(dts_input_path, "w") as f:
f.write("/* Generated by zephyr-bazel - do not edit! */\n")
f.write(f'#include "{os.path.abspath(dts_file)}"\n')
for overlay in overlays:
f.write(f'#include "{os.path.abspath(overlay)}"\n')
merged_dts_path = os.path.join(output_dir, "zephyr.dts")
# Preprocess DTS using pcpp
import pcpp
pcmd_path = os.path.join(os.path.dirname(pcpp.__file__), "pcmd.py")
pcpp_args = [
sys.executable,
pcmd_path,
dts_input_path,
]
raw_roots = [Path(r) for r in args.oot_dts_roots] + [Path(m) for m in args.modules] + [Path(zephyr_base)]
dts_roots = []
for r in raw_roots:
if r not in dts_roots:
dts_roots.append(r)
for dts_root in dts_roots:
directories = [
"include",
"include/zephyr",
"dts",
"dts/common",
] + ["dts/" + a for a in SUPPORTED_ARCHITECTURES] + ["dts/vendor"]
for directory in directories:
path = dts_root / directory
if path.exists():
pcpp_args.append("-I" + str(path.resolve()))
pcpp_args.extend(["-o", merged_dts_path])
subprocess.run(pcpp_args, check=True)
return merged_dts_path, dts_roots
def generate_edt_pickle(zephyr_base, output_dir, dts_roots, merged_dts_path):
output_edt_pickle_path = os.path.join(output_dir, "edt.pickle")
bindings_dirs = []
for dts_root in dts_roots:
bdir = dts_root / "zephyr" / "dts" / "bindings"
if bdir.exists():
if str(bdir.resolve()) not in bindings_dirs:
bindings_dirs.append(str(bdir.resolve()))
else:
bdir = dts_root / "dts" / "bindings"
if bdir.exists():
if str(bdir.resolve()) not in bindings_dirs:
bindings_dirs.append(str(bdir.resolve()))
gen_edt_args = [
sys.executable,
os.path.join(zephyr_base, "scripts", "dts", "gen_edt.py"),
"--edt-pickle-out",
output_edt_pickle_path,
"--dts",
merged_dts_path,
"--bindings-dirs",
]
gen_edt_args.extend(bindings_dirs)
gen_edt_args.extend([
"--dts-out",
os.path.join(output_dir, "zephyr.dts.debug"),
"--dtc-flags",
"Wno-simple_bus_reg",
])
# Add PYTHONPATH for edtlib
env = os.environ.copy()
env["PYTHONPATH"] = os.path.join(zephyr_base, "scripts", "dts") + ":" + os.path.join(zephyr_base, "scripts", "dts", "python-devicetree", "src") + ":" + env.get("PYTHONPATH", "")
print(f"DEBUG PYTHONPATH: {env['PYTHONPATH']}")
subprocess.run(gen_edt_args, env=env, check=True)
os.environ["EDT_PICKLE"] = output_edt_pickle_path
def generate_dts_headers(zephyr_base, output_dir, edt_pickle_path):
gen_defines_py = os.path.join(zephyr_base, "scripts", "dts", "gen_defines.py")
output_header = os.path.join(output_dir, "zephyr", "devicetree_generated.h")
os.makedirs(os.path.dirname(output_header), exist_ok=True)
cmd = [
sys.executable,
gen_defines_py,
"--edt-pickle", edt_pickle_path,
"--header-out", output_header,
]
# Ensure edtlib is in PYTHONPATH (portable using os.pathsep)
env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join([
os.path.join(zephyr_base, "scripts", "dts"),
os.path.join(zephyr_base, "scripts", "dts", "python-devicetree", "src"),
env.get("PYTHONPATH", ""),
])
subprocess.run(cmd, env=env, check=True)
def get_conf_files(args, board_dir):
board_name = os.environ["BOARD"]
board_qualifiers = os.environ.get("BOARD_QUALIFIERS", "").lstrip("/")
conf_files = []
# Try qualified defconfig (HWM v2)
if board_qualifiers:
safe_qualifiers = board_qualifiers.replace("/", "_")
qualified_defconfig = os.path.join(board_dir, f"{board_name}_{safe_qualifiers}_defconfig")
if os.path.exists(qualified_defconfig):
conf_files.append(qualified_defconfig)
# Fallback to base defconfig
if not conf_files:
board_defconfig = os.path.join(board_dir, f"{board_name}_defconfig")
if os.path.exists(board_defconfig):
conf_files.append(board_defconfig)
prj_conf = os.path.join(args.app_dir, "prj.conf")
if os.path.exists(prj_conf):
conf_files.append(prj_conf)
# Also try board-specific conf in app dir
if board_qualifiers:
soc_board_conf = os.path.join(args.app_dir, "boards", f"{board_name}_{board_qualifiers}.conf")
if os.path.exists(soc_board_conf):
conf_files.append(soc_board_conf)
board_conf = os.path.join(args.app_dir, "boards", f"{board_name}.conf")
if os.path.exists(board_conf):
conf_files.append(board_conf)
return conf_files
def generate_config_c(kconf, output_dir, args):
os.makedirs(os.path.join(output_dir, "zephyr"), exist_ok=True)
with open(os.path.join(output_dir, "zephyr", "config.c"), "w") as f:
f.write("/* AUTO-GENERATED by kconfig_gen_values.py, do not edit! */\n\n")
f.write("#include <zephyr/toolchain.h>\n\n")
f.write("GEN_ABS_SYM_BEGIN (_ConfigAbsSyms)\n")
# Call scripts/build/generate_configs_dot_c.py methodology
for sym in kconf.unique_defined_syms:
if not sym.nodes:
continue
val = None
# Import here because it's not available at the top level due to path setup.
import kconfiglib
if sym.type in [kconfiglib.BOOL, kconfiglib.TRISTATE]:
if sym.str_value == "y":
val = "1"
elif sym.type == kconfiglib.INT:
if sym.str_value:
val = sym.str_value
elif sym.type == kconfiglib.HEX:
if sym.str_value:
val = "0x" + sym.str_value.replace("0x", "")
elif sym.type == kconfiglib.STRING:
if sym.str_value:
val = "1"
if val is not None:
f.write(f"GEN_ABSOLUTE_SYM_KCONFIG(CONFIG_{sym.name}, {val});\n")
f.write("GEN_ABS_SYM_END\n")
def generate_bazel_build(args, kconf, output_dir, kconfiglib):
resolved_arch = "arm"
for a in SUPPORTED_ARCHITECTURES:
sym_name = "ARCH_POSIX" if a == "posix" else a.upper()
sym = kconf.syms.get(sym_name)
if sym and sym.str_value == "y":
resolved_arch = a
break
if resolved_arch == "arm":
# Fallback check for ARC if standard symbol is missing
sym = kconf.syms.get("DT_HAS_SNPS_ARCEM_ENABLED")
if sym and sym.str_value == "y":
resolved_arch = "arc"
arch_macro = "__arm__"
if resolved_arch == "arm64":
arch_macro = "__aarch64__"
elif resolved_arch == "x86":
arch_macro = "__i386__"
elif resolved_arch == "riscv":
arch_macro = "__riscv"
soc_name = kconf.syms.get("SOC").str_value
if not soc_name:
# Try SOC_SERIES or other fallbacks if SOC is empty
soc_name = kconf.syms.get("SOC_SERIES").str_value
if not soc_name:
soc_name = os.environ.get("BOARD", "unknown")
target_name = kconfig_utils.sanitize_name_for_target(soc_name)
print(f"DEBUG: soc_name={soc_name}, target_name={target_name}")
with open(os.path.join(output_dir, "BUILD.bazel"), "w") as f:
f.write('load("@rules_cc//cc:defs.bzl", "cc_library")\n')
f.write('load("@zephyr//:cc.bzl", "zephyr_cc_library")\n')
f.write('package(default_visibility = ["//visibility:public"])\n\n')
f.write('exports_files(["zephyr/autoconf.h", "zephyr/devicetree_generated.h", ".config"])\n\n')
f.write('cc_library(\n')
f.write(' name = "devicetree_generated",\n')
f.write(' hdrs = ["zephyr/devicetree_generated.h"],\n')
f.write(' includes = ["."],\n')
f.write(')\n\n')
f.write('cc_library(\n')
f.write(' name = "autoconf_library",\n')
f.write(' hdrs = ["zephyr/autoconf.h"],\n')
f.write(' includes = ["."],\n')
f.write(' deps = [":devicetree_generated"],\n')
f.write(')\n\n')
f.write('zephyr_cc_library(\n')
f.write(' name = "autoconf_symbols",\n')
f.write(' srcs = ["zephyr/config.c"],\n')
f.write(' copts = ["-D%s"],\n' % arch_macro)
f.write(' deps = [\n')
f.write(' ":autoconf_library",\n')
f.write(' "@zephyr//include:zephyr",\n')
f.write(' ],\n')
f.write(')\n\n')
f.write('platform(\n')
f.write(' name = "%s",\n' % target_name)
f.write(' parents = ["%s"],\n' % args.parent_platform)
f.write(' flags = [\n')
for sym in kconf.unique_defined_syms:
if not sym.nodes:
continue
sname = kconfig_utils.sanitize_name_for_target(sym.name)
full_name = "CONFIG_" + sname
if sym.type in [kconfiglib.BOOL, kconfiglib.TRISTATE]:
val = "true" if sym.str_value == "y" else "false"
f.write(' "--@zephyr_kconfig//:%s=%s",\n' % (full_name, val))
elif sym.type == kconfiglib.INT:
if sym.str_value:
f.write(' "--@zephyr_kconfig//:%s=%s",\n' % (full_name, sym.str_value))
elif sym.type == kconfiglib.HEX:
if sym.str_value:
try:
val = int(sym.str_value, 16)
f.write(' "--@zephyr_kconfig//:%s=%d",\n' % (full_name, val))
except:
pass
elif sym.type == kconfiglib.STRING:
if sym.str_value:
f.write(' "--@zephyr_kconfig//:%s=%s",\n' % (full_name, sym.str_value))
# Resolve board DTS target
board_label = args.parent_platform
board_pkg = board_label.split(":")[0]
f.write(' "--@zephyr//:autoconf_file=@zc_target//:zephyr/autoconf.h",\n')
f.write(' "--@zephyr//:autoconf_library=@zc_target//:autoconf_library",\n')
f.write(' "--@zephyr//:autoconf_symbols_to_link=@zc_target//:autoconf_symbols",\n')
f.write(' "--@zephyr//:dts_cc_library=@zc_target//:devicetree_generated",\n')
f.write(' ],\n')
f.write(')\n\n')
f.write('alias(name = "platform", actual = ":%s")\n' % target_name)
def main():
args = parse_args()
kconfig_utils.setup_kconfiglib(args.zephyr_base)
# kconfiglib is in Zephyr's scripts/kconfig directory, which is added to path by setup_kconfiglib above.
import kconfiglib
zephyr_base = os.path.abspath(args.zephyr_base)
output_dir = os.path.abspath(args.output_dir)
board_dir = os.path.abspath(args.board_dir)
board_name = args.board.split("/")[0]
if not os.path.exists(os.path.join(board_dir, "board.yml")) and not os.path.exists(os.path.join(board_dir, f"{board_name}.dts")):
found = False
for root in [board_dir] + args.oot_dts_roots:
for dirpath, _, filenames in os.walk(root, followlinks=True):
if f"{board_name}.dts" in filenames:
board_dir = os.path.abspath(dirpath)
found = True
break
if found: break
setup_environment(args, zephyr_base, output_dir, board_dir)
generate_kconfig_aggregations(zephyr_base, args, output_dir, board_dir)
merged_dts_path, dts_roots = preprocess_dts(args, zephyr_base, output_dir, board_dir)
generate_edt_pickle(zephyr_base, output_dir, dts_roots, merged_dts_path)
generate_dts_headers(zephyr_base, output_dir, os.path.join(output_dir, "edt.pickle"))
kconfig_utils.generate_kconfig_dts(zephyr_base, os.path.join(output_dir, "Kconfig.dts"), dts_roots)
# Re-verify environment for kconfiglib
os.environ["ARCH"] = "*"
os.environ["ZEPHYR_BASE"] = zephyr_base
conf_files = get_conf_files(args, board_dir)
conf_files.extend(args.conf_fragments)
os.makedirs(os.path.join(output_dir, "zephyr"), exist_ok=True)
# Call Zephyr's kconfig.py
kconfig_py = os.path.join(zephyr_base, "scripts", "kconfig", "kconfig.py")
cmd = [
sys.executable,
kconfig_py,
"--handwritten-input-configs",
os.path.join(zephyr_base, "Kconfig"),
os.path.join(output_dir, ".config"),
os.path.join(output_dir, "zephyr", "autoconf.h"),
os.path.join(output_dir, "kconfig_list.txt"),
] + conf_files
env = os.environ.copy()
res = subprocess.run(cmd, env=env, capture_output=True, text=True)
if res.stderr:
print(res.stderr, file=sys.stderr)
if res.returncode != 0:
sys.exit(f"Kconfig generation failed with exit code {res.returncode}")
# Add the app name to the autoconf.h since bazel does not propagate build flags to dependencies.
with open(os.path.join(output_dir, "zephyr", "autoconf.h"), "a") as f:
f.write(f'#define APP_NAME "{args.app_name}"\n')
# Load generated .config with kconfiglib
kconf = kconfiglib.Kconfig(os.path.join(zephyr_base, "Kconfig"))
kconf.load_config(os.path.join(output_dir, ".config"))
generate_config_c(kconf, output_dir, args)
generate_bazel_build(args, kconf, output_dir, kconfiglib)
if __name__ == "__main__":
main()