import os import re # The directories to scan for Kconfig files SUBSYSTEM_DIRS = ["04_hal", "05_executors", "05_service", "06_behaviors", "tests"] TEMPLATE_SCAN_DIRS = SUBSYSTEM_DIRS + ["sweekar_demos"] BASE_DIR = os.path.dirname(os.path.abspath(__file__)) OUTPUT_FILE = os.path.join(BASE_DIR, "Kconfig.modules") SOURCE_EXTENSIONS = (".c", ".cc", ".cpp") HEADER_EXTENSIONS = (".h", ".hpp") SKIP_DIR_NAMES = {"build", "__pycache__"} def iter_child_dirs(parent_dir): if not os.path.exists(parent_dir): return [] return sorted( [ name for name in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, name)) and not name.startswith(".") ] ) def compute_default_config_name(subsystem_dir, module_name): if subsystem_dir == "tests": prefix = "TEST" else: prefix = subsystem_dir.split("_")[1].upper() if prefix == "EXECUTORS": prefix = "EXEC" elif prefix == "BEHAVIORS": prefix = "BEHAV" elif prefix == "SERVICE": prefix = "SVC" return f"CONFIG_ENABLE_{prefix}_{module_name.upper()}" def contains_module_files(module_dir): for _, _, files in os.walk(module_dir): for filename in files: if filename.endswith(SOURCE_EXTENSIONS) or filename.endswith(HEADER_EXTENSIONS): return True return False def collect_existing_module_cmake_files(): module_files = [] for root, _, files in os.walk(BASE_DIR): if "module.cmake" in files: module_files.append(os.path.join(root, "module.cmake")) return sorted(module_files) def collect_module_files(module_dir): srcs = [] include_dirs = {"."} for root, dirs, files in os.walk(module_dir): dirs[:] = [ d for d in dirs if d not in SKIP_DIR_NAMES and not d.startswith(".") ] rel_root = os.path.relpath(root, module_dir).replace(os.sep, "/") rel_root = "." if rel_root == "." else rel_root has_header = False for filename in files: if filename.endswith(SOURCE_EXTENSIONS): rel_path = os.path.normpath(os.path.join(rel_root, filename)).replace(os.sep, "/") if rel_path.startswith("./"): rel_path = rel_path[2:] srcs.append(rel_path) elif filename.endswith(HEADER_EXTENSIONS): has_header = True if has_header: include_dirs.add(rel_root) srcs = sorted(set(srcs)) include_dirs = sorted(set(include_dirs)) return srcs, include_dirs def is_already_aggregated(rel_module_dir, existing_module_files): rel_module_prefix = f'"{rel_module_dir}/' for module_file in existing_module_files: try: with open(module_file, "r", encoding="utf-8") as f: content = f.read() except OSError: continue if rel_module_prefix in content: return True return False def render_module_template(subsystem_dir, module_name, module_dir): srcs, include_dirs = collect_module_files(module_dir) rel_module_dir = os.path.relpath(module_dir, BASE_DIR).replace(os.sep, "/") lines = [ f"# main/{subsystem_dir}/{module_name}/module.cmake", "# 自动生成的模块脚手架,可直接手工修改。", "# 如果后续添加了新文件,可以手工在此处添加,或者删除本文件让脚本重新生成。", "", ] if subsystem_dir != "sweekar_demos": config_name = compute_default_config_name(subsystem_dir, module_name) lines.extend( [ f"if(NOT {config_name} AND NOT EARLY_EXPANSION)", f' message(STATUS " [Skip] {module_name} module disabled by menuconfig.")', " return()", "endif()", "", ] ) lines.append("list(APPEND MODULE_SRCS") for src in srcs: lines.append(f' "{rel_module_dir}/{src}"') lines.append(")") lines.append("") lines.append("list(APPEND MODULE_INCLUDE_DIRS") for inc in include_dirs: if inc == ".": lines.append(f' "{rel_module_dir}"') else: lines.append(f' "{rel_module_dir}/{inc}"') lines.append(")") lines.append("") lines.extend( [ "# 需要公开依赖时,在这里手工补充。例如:", "# list(APPEND MODULE_REQUIRES", "# esp_timer", "# driver", "# )", "", "# 需要私有依赖时,在这里手工补充。例如:", "# list(APPEND MODULE_PRIV_REQUIRES", "# nvs_flash", "# )", "", ] ) return "\n".join(lines) def ensure_module_templates(): created_files = [] existing_module_files = collect_existing_module_cmake_files() for subsystem_dir in TEMPLATE_SCAN_DIRS: dir_path = os.path.join(BASE_DIR, subsystem_dir) if not os.path.exists(dir_path): continue for module_name in iter_child_dirs(dir_path): if subsystem_dir == "sweekar_demos" and not module_name.startswith("sweekar_demo"): continue module_dir = os.path.join(dir_path, module_name) module_cmake = os.path.join(module_dir, "module.cmake") if os.path.exists(module_cmake): continue if not contains_module_files(module_dir): continue rel_module_dir = os.path.relpath(module_dir, BASE_DIR).replace(os.sep, "/") if subsystem_dir != "sweekar_demos" and is_already_aggregated(rel_module_dir, existing_module_files): continue template_content = render_module_template(subsystem_dir, module_name, module_dir) with open(module_cmake, "w", encoding="utf-8") as f: f.write(template_content) created_files.append(module_cmake) return created_files def generate_kconfig_modules(): created_templates = ensure_module_templates() kconfig_content = 'menu "Sweekar Subsystems Configuration"\n\n' for d in SUBSYSTEM_DIRS: dir_path = os.path.join(BASE_DIR, d) if not os.path.exists(dir_path): continue # Create a pretty menu name from the directory name # e.g., "05_executors" -> "05 Executors Modules" parts = d.split('_') menu_name = parts[0] + " " + " ".join([p.capitalize() for p in parts[1:]]) + " Modules" kconfig_content += f' menu "{menu_name}"\n' # Sort subdirectories to ensure consistent ordering subdirs = iter_child_dirs(dir_path) for module_name in subdirs: module_dir = os.path.join(dir_path, module_name) kconfig_file = os.path.join(module_dir, "Kconfig") if os.path.exists(kconfig_file): # If the module has its own Kconfig, just source it # Make sure the source path is relative to the project root or uses proper env vars # Since Kconfig.projbuild is processed relative to project root usually rel_path = os.path.join("main", d, module_name, "Kconfig") kconfig_content += f' source "{rel_path}"\n' else: # If no Kconfig exists, generate a default toggle for it config_name = compute_default_config_name(d, module_name) pretty_name = " ".join([word.capitalize() for word in module_name.split('_')]) # Default to 'n' for tests, 'y' for everything else default_val = 'n' if d == 'tests' else 'y' kconfig_content += f''' config {config_name.replace("CONFIG_", "")} bool "Enable {pretty_name}" default {default_val} help Enable the {module_name} module in {d}. ''' kconfig_content += ' endmenu\n\n' kconfig_content += 'endmenu\n' with open(OUTPUT_FILE, 'w') as f: f.write(kconfig_content) print(f"Generated {OUTPUT_FILE} successfully.") if created_templates: print("Generated module.cmake templates:") for path in created_templates: print(f" - {path}") if __name__ == "__main__": generate_kconfig_modules()