"""ESP-IDF direct build generator for ESPHome.""" import json from pathlib import Path from esphome.components.esp32 import get_esp32_variant from esphome.core import CORE from esphome.helpers import mkdir_p, write_file_if_changed def get_available_components() -> list[str] | None: """Get list of available ESP-IDF components from project_description.json. Returns only internal ESP-IDF components, excluding external/managed components (from idf_component.yml). """ project_desc = Path(CORE.build_path) / "build" / "project_description.json" if not project_desc.exists(): return None try: with open(project_desc, encoding="utf-8") as f: data = json.load(f) component_info = data.get("build_component_info", {}) result = [] for name, info in component_info.items(): # Exclude our own src component if name == "src": continue # Exclude managed/external components comp_dir = info.get("dir", "") if "managed_components" in comp_dir: continue result.append(name) return result except (json.JSONDecodeError, OSError): return None def has_discovered_components() -> bool: """Check if we have discovered components from a previous configure.""" return get_available_components() is not None def get_project_cmakelists() -> str: """Generate the top-level CMakeLists.txt for ESP-IDF project.""" # Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3) variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") # Extract compile definitions from build flags (-DXXX -> XXX) compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")] extra_compile_options = "\n".join( f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' for compile_def in compile_defs ) return f"""\ # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) {extra_compile_options} project({CORE.name}) """ def get_component_cmakelists(minimal: bool = False) -> str: """Generate the main component CMakeLists.txt.""" idf_requires = [] if minimal else (get_available_components() or []) requires_str = " ".join(idf_requires) # Extract compile options (-W flags, excluding linker flags) compile_opts = [ flag for flag in CORE.build_flags if flag.startswith("-W") and not flag.startswith("-Wl,") ] compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else "" # Extract linker options (-Wl, flags) link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")] link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else "" return f"""\ # Auto-generated by ESPHome file(GLOB_RECURSE app_sources "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp" "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c" "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp" "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c" ) idf_component_register( SRCS ${{app_sources}} INCLUDE_DIRS "." "esphome" REQUIRES {requires_str} ) # Apply C++ standard target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20) # ESPHome compile options target_compile_options(${{COMPONENT_LIB}} PUBLIC {compile_opts_str} ) # ESPHome linker options target_link_options(${{COMPONENT_LIB}} PUBLIC {link_opts_str} ) """ def write_project(minimal: bool = False) -> None: """Write ESP-IDF project files.""" mkdir_p(CORE.build_path) mkdir_p(CORE.relative_src_path()) # Write top-level CMakeLists.txt write_file_if_changed( CORE.relative_build_path("CMakeLists.txt"), get_project_cmakelists(), ) # Write component CMakeLists.txt in src/ write_file_if_changed( CORE.relative_src_path("CMakeLists.txt"), get_component_cmakelists(minimal=minimal), )