mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
[esp32] Add support for native ESP-IDF builds (#13272)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
@@ -43,6 +43,7 @@ from esphome.const import (
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_TOPIC,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_NATIVE_IDF,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
@@ -116,6 +117,7 @@ class ArgsProtocol(Protocol):
|
||||
configuration: str
|
||||
name: str
|
||||
upload_speed: str | None
|
||||
native_idf: bool
|
||||
|
||||
|
||||
def choose_prompt(options, purpose: str = None):
|
||||
@@ -500,12 +502,15 @@ def wrap_to_code(name, comp):
|
||||
return wrapped
|
||||
|
||||
|
||||
def write_cpp(config: ConfigType) -> int:
|
||||
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
# Store native_idf flag so esp32 component can check it
|
||||
CORE.data[KEY_NATIVE_IDF] = native_idf
|
||||
|
||||
generate_cpp_contents(config)
|
||||
return write_cpp_file()
|
||||
return write_cpp_file(native_idf=native_idf)
|
||||
|
||||
|
||||
def generate_cpp_contents(config: ConfigType) -> None:
|
||||
@@ -519,32 +524,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
|
||||
CORE.flush_tasks()
|
||||
|
||||
|
||||
def write_cpp_file() -> int:
|
||||
def write_cpp_file(native_idf: bool = False) -> int:
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
from esphome.build_gen import platformio
|
||||
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
||||
from esphome.build_gen import espidf
|
||||
|
||||
platformio.write_project()
|
||||
espidf.write_project()
|
||||
else:
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
platformio.write_project()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome import platformio_api
|
||||
native_idf = getattr(args, "native_idf", False)
|
||||
|
||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||
# If you change this format, update the regex in that script as well
|
||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
||||
from esphome import espidf_api
|
||||
|
||||
rc = espidf_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# Create factory.bin and ota.bin
|
||||
espidf_api.create_factory_bin()
|
||||
espidf_api.create_ota_bin()
|
||||
else:
|
||||
from esphome import platformio_api
|
||||
|
||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
if idedata is None:
|
||||
return 1
|
||||
|
||||
# Check if firmware was rebuilt and emit build_info + create manifest
|
||||
_check_and_emit_build_info()
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
return 0 if idedata is not None else 1
|
||||
return 0
|
||||
|
||||
|
||||
def _check_and_emit_build_info() -> None:
|
||||
@@ -801,7 +828,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
native_idf = getattr(args, "native_idf", False)
|
||||
exit_code = write_cpp(config, native_idf=native_idf)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
if args.only_generate:
|
||||
@@ -856,7 +884,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
|
||||
|
||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
native_idf = getattr(args, "native_idf", False)
|
||||
exit_code = write_cpp(config, native_idf=native_idf)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
exit_code = compile_program(args, config)
|
||||
@@ -1310,6 +1339,11 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--native-idf",
|
||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
@@ -1391,6 +1425,11 @@ def parse_args(argv):
|
||||
help="Reset the device before starting serial logs.",
|
||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--native-idf",
|
||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
"clean-mqtt",
|
||||
|
||||
139
esphome/build_gen/espidf.py
Normal file
139
esphome/build_gen/espidf.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""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("-", "")
|
||||
|
||||
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)
|
||||
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 definitions from build flags (-DXXX -> XXX)
|
||||
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
|
||||
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
|
||||
|
||||
# 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(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(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 definitions
|
||||
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
|
||||
{compile_defs_str}
|
||||
)
|
||||
|
||||
# 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),
|
||||
)
|
||||
@@ -34,6 +34,7 @@ from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_NAME,
|
||||
KEY_NATIVE_IDF,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
@@ -53,6 +54,7 @@ from .const import ( # noqa
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
@@ -199,6 +201,7 @@ def set_core_data(config):
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
||||
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
|
||||
|
||||
@@ -962,12 +965,54 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||
cg.add_platformio_option(
|
||||
"board_upload.maximum_size",
|
||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||
)
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
|
||||
if use_platformio:
|
||||
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
|
||||
# but keep them when using --native-idf for native ESP-IDF builds
|
||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||
os.environ.pop(clean_var, None)
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||
cg.add_platformio_option(
|
||||
"board_upload.maximum_size",
|
||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||
)
|
||||
|
||||
if CONF_SOURCE in conf:
|
||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"pre_build.py",
|
||||
Path(__file__).parent / "pre_build.py.script",
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"post",
|
||||
"post_build.py",
|
||||
Path(__file__).parent / "post_build.py.script",
|
||||
)
|
||||
|
||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||
# Similar to ESP8266's approach but for ESP-IDF
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"iram_fix.py",
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
else:
|
||||
cg.add_build_flag("-Wno-error=format")
|
||||
|
||||
cg.set_cpp_standard("gnu++20")
|
||||
cg.add_build_flag("-DUSE_ESP32")
|
||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||
@@ -977,79 +1022,49 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
|
||||
cg.add_define(ThreadModel.MULTI_ATOMICS)
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
if CONF_SOURCE in conf:
|
||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||
|
||||
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
|
||||
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
|
||||
|
||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||
os.environ.pop(clean_var, None)
|
||||
|
||||
# Set the location of the IDF component manager cache
|
||||
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"pre_build.py",
|
||||
Path(__file__).parent / "pre_build.py.script",
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"post",
|
||||
"post_build.py",
|
||||
Path(__file__).parent / "post_build.py.script",
|
||||
)
|
||||
|
||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||
# Similar to ESP8266's approach but for ESP-IDF
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"iram_fix.py",
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
else:
|
||||
cg.add_platformio_option("framework", "arduino, espidf")
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "arduino, espidf")
|
||||
|
||||
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
||||
# the ESP-IDF framework
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[_format_framework_espidf_version(idf_ver, None)],
|
||||
)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
|
||||
cg.add_define(
|
||||
"USE_ARDUINO_VERSION_CODE",
|
||||
cg.RawExpression(
|
||||
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
|
||||
),
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
||||
# the ESP-IDF framework
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
|
||||
)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||
@@ -1196,7 +1211,8 @@ async def to_code(config):
|
||||
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
|
||||
)
|
||||
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
add_extra_build_file(
|
||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||
@@ -1361,19 +1377,16 @@ def copy_files():
|
||||
_write_idf_component_yml()
|
||||
|
||||
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
if CORE.using_arduino:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_arduino_partition_csv(
|
||||
CORE.platformio_options.get("board_upload.flash_size")
|
||||
),
|
||||
get_arduino_partition_csv(flash_size),
|
||||
)
|
||||
else:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_idf_partition_csv(
|
||||
CORE.platformio_options.get("board_upload.flash_size")
|
||||
),
|
||||
get_idf_partition_csv(flash_size),
|
||||
)
|
||||
# IDF build scripts look for version string to put in the build.
|
||||
# However, if the build path does not have an initialized git repo,
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
|
||||
KEY_ESP32 = "esp32"
|
||||
KEY_BOARD = "board"
|
||||
KEY_FLASH_SIZE = "flash_size"
|
||||
KEY_VARIANT = "variant"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
KEY_COMPONENTS = "components"
|
||||
|
||||
@@ -1379,6 +1379,7 @@ KEY_FRAMEWORK_VERSION = "framework_version"
|
||||
KEY_NAME = "name"
|
||||
KEY_VARIANT = "variant"
|
||||
KEY_PAST_SAFE_MODE = "past_safe_mode"
|
||||
KEY_NATIVE_IDF = "native_idf"
|
||||
|
||||
# Entity categories
|
||||
ENTITY_CATEGORY_NONE = ""
|
||||
|
||||
@@ -17,6 +17,7 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WIFI,
|
||||
KEY_CORE,
|
||||
KEY_NATIVE_IDF,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_BK72XX,
|
||||
@@ -763,6 +764,9 @@ class EsphomeCore:
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
if self.data.get(KEY_NATIVE_IDF, False):
|
||||
return self.relative_build_path("build", f"{self.name}.bin")
|
||||
if self.is_libretiny:
|
||||
return self.relative_pioenvs_path(self.name, "firmware.uf2")
|
||||
return self.relative_pioenvs_path(self.name, "firmware.bin")
|
||||
|
||||
229
esphome/espidf_api.py
Normal file
229
esphome/espidf_api.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""ESP-IDF direct build API for ESPHome."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE
|
||||
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_idf_path() -> Path | None:
|
||||
"""Get IDF_PATH from environment or common locations."""
|
||||
# Check environment variable first
|
||||
if "IDF_PATH" in os.environ:
|
||||
path = Path(os.environ["IDF_PATH"])
|
||||
if path.is_dir():
|
||||
return path
|
||||
|
||||
# Check common installation locations
|
||||
common_paths = [
|
||||
Path.home() / "esp" / "esp-idf",
|
||||
Path.home() / ".espressif" / "esp-idf",
|
||||
Path("/opt/esp-idf"),
|
||||
]
|
||||
|
||||
for path in common_paths:
|
||||
if path.is_dir() and (path / "tools" / "idf.py").is_file():
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_idf_env() -> dict[str, str]:
|
||||
"""Get environment variables needed for ESP-IDF build.
|
||||
|
||||
Requires the user to have sourced export.sh before running esphome.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
|
||||
idf_path = _get_idf_path()
|
||||
if idf_path is None:
|
||||
raise EsphomeError(
|
||||
"ESP-IDF not found. Please install ESP-IDF and source export.sh:\n"
|
||||
" git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf\n"
|
||||
" cd ~/esp-idf && ./install.sh\n"
|
||||
" source ~/esp-idf/export.sh\n"
|
||||
"See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/"
|
||||
)
|
||||
|
||||
env["IDF_PATH"] = str(idf_path)
|
||||
return env
|
||||
|
||||
|
||||
def run_idf_py(
|
||||
*args, cwd: Path | None = None, capture_output: bool = False
|
||||
) -> int | str:
|
||||
"""Run idf.py with the given arguments."""
|
||||
idf_path = _get_idf_path()
|
||||
if idf_path is None:
|
||||
raise EsphomeError("ESP-IDF not found")
|
||||
|
||||
env = _get_idf_env()
|
||||
idf_py = idf_path / "tools" / "idf.py"
|
||||
|
||||
cmd = ["python", str(idf_py)] + list(args)
|
||||
|
||||
if cwd is None:
|
||||
cwd = CORE.build_path
|
||||
|
||||
_LOGGER.debug("Running: %s", " ".join(cmd))
|
||||
_LOGGER.debug(" in directory: %s", cwd)
|
||||
|
||||
if capture_output:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_LOGGER.error("idf.py failed:\n%s", result.stderr)
|
||||
return result.stdout
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode
|
||||
|
||||
|
||||
def run_reconfigure() -> int:
|
||||
"""Run cmake reconfigure only (no build)."""
|
||||
return run_idf_py("reconfigure")
|
||||
|
||||
|
||||
def run_compile(config, verbose: bool) -> int:
|
||||
"""Compile the ESP-IDF project.
|
||||
|
||||
Uses two-phase configure to auto-discover available components:
|
||||
1. If no previous build, configure with minimal REQUIRES to discover components
|
||||
2. Regenerate CMakeLists.txt with discovered components
|
||||
3. Run full build
|
||||
"""
|
||||
from esphome.build_gen.espidf import has_discovered_components, write_project
|
||||
|
||||
# Check if we need to do discovery phase
|
||||
if not has_discovered_components():
|
||||
_LOGGER.info("Discovering available ESP-IDF components...")
|
||||
write_project(minimal=True)
|
||||
rc = run_reconfigure()
|
||||
if rc != 0:
|
||||
_LOGGER.error("Component discovery failed")
|
||||
return rc
|
||||
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
|
||||
write_project(minimal=False)
|
||||
|
||||
# Build
|
||||
args = ["build"]
|
||||
|
||||
if verbose:
|
||||
args.append("-v")
|
||||
|
||||
# Add parallel job limit if configured
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config.get(CONF_ESPHOME, {}):
|
||||
limit = config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]
|
||||
args.extend(["-j", str(limit)])
|
||||
|
||||
# Set the sdkconfig file
|
||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
if sdkconfig_path.is_file():
|
||||
args.extend(["-D", f"SDKCONFIG={sdkconfig_path}"])
|
||||
|
||||
return run_idf_py(*args)
|
||||
|
||||
|
||||
def get_firmware_path() -> Path:
|
||||
"""Get the path to the compiled firmware binary."""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
return build_dir / f"{CORE.name}.bin"
|
||||
|
||||
|
||||
def get_factory_firmware_path() -> Path:
|
||||
"""Get the path to the factory firmware (with bootloader)."""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
return build_dir / f"{CORE.name}.factory.bin"
|
||||
|
||||
|
||||
def create_factory_bin() -> bool:
|
||||
"""Create factory.bin by merging bootloader, partition table, and app."""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
flasher_args_path = build_dir / "flasher_args.json"
|
||||
|
||||
if not flasher_args_path.is_file():
|
||||
_LOGGER.warning("flasher_args.json not found, cannot create factory.bin")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(flasher_args_path, encoding="utf-8") as f:
|
||||
flash_data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
_LOGGER.error("Failed to read flasher_args.json: %s", e)
|
||||
return False
|
||||
|
||||
# Get flash size from config
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
|
||||
# Build esptool merge command
|
||||
sections = []
|
||||
for addr, fname in sorted(
|
||||
flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16)
|
||||
):
|
||||
file_path = build_dir / fname
|
||||
if file_path.is_file():
|
||||
sections.extend([addr, str(file_path)])
|
||||
else:
|
||||
_LOGGER.warning("Flash file not found: %s", file_path)
|
||||
|
||||
if not sections:
|
||||
_LOGGER.warning("No flash sections found")
|
||||
return False
|
||||
|
||||
output_path = get_factory_firmware_path()
|
||||
chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32")
|
||||
|
||||
cmd = [
|
||||
"python",
|
||||
"-m",
|
||||
"esptool",
|
||||
"--chip",
|
||||
chip,
|
||||
"merge_bin",
|
||||
"--flash_size",
|
||||
flash_size,
|
||||
"--output",
|
||||
str(output_path),
|
||||
] + sections
|
||||
|
||||
_LOGGER.info("Creating factory.bin...")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
|
||||
if result.returncode != 0:
|
||||
_LOGGER.error("Failed to create factory.bin: %s", result.stderr)
|
||||
return False
|
||||
|
||||
_LOGGER.info("Created: %s", output_path)
|
||||
return True
|
||||
|
||||
|
||||
def create_ota_bin() -> bool:
|
||||
"""Copy the firmware to .ota.bin for ESPHome OTA compatibility."""
|
||||
firmware_path = get_firmware_path()
|
||||
ota_path = firmware_path.with_suffix(".ota.bin")
|
||||
|
||||
if not firmware_path.is_file():
|
||||
_LOGGER.warning("Firmware not found: %s", firmware_path)
|
||||
return False
|
||||
|
||||
shutil.copy(firmware_path, ota_path)
|
||||
_LOGGER.info("Created: %s", ota_path)
|
||||
return True
|
||||
Reference in New Issue
Block a user