[espidf] Regenerate bundled CMakeLists; auto-REQUIRE via IDF build properties (#16406)
This commit is contained in:
1 parent
06786da7dd
commit
a3b6f92433
6 files changed
+344
-191
No files matched your search
+76
-20
@@ -10,11 +10,14 @@ from esphome.writer import update_storage_json
|
|||||||
|
|
||||||
|
|
||||||
def get_available_components() -> list[str] | None:
|
def get_available_components() -> list[str] | None:
|
||||||
"""Get list of available ESP-IDF components from project_description.json.
|
"""Get list of built-in ESP-IDF components from project_description.json.
|
||||||
|
|
||||||
Returns only internal ESP-IDF components, excluding external/managed
|
Excludes ``src``, IDF-managed components (``managed_components/``), and
|
||||||
components (from idf_component.yml).
|
converted PIO libs (``pio_components/``). Returns ``None`` if the build
|
||||||
|
dir or ``project_description.json`` isn't ready yet.
|
||||||
"""
|
"""
|
||||||
|
if CORE.build_path is None:
|
||||||
|
return None
|
||||||
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
|
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
|
||||||
if not project_desc.exists():
|
if not project_desc.exists():
|
||||||
return None
|
return None
|
||||||
@@ -31,9 +34,9 @@ def get_available_components() -> list[str] | None:
|
|||||||
if name == "src":
|
if name == "src":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Exclude managed/external components
|
# Exclude IDF-managed and converted-PIO components (external).
|
||||||
comp_dir = info.get("dir", "")
|
comp_dir = info.get("dir", "")
|
||||||
if "managed_components" in comp_dir:
|
if "managed_components" in comp_dir or "pio_components" in comp_dir:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result.append(name)
|
result.append(name)
|
||||||
@@ -48,8 +51,12 @@ def has_discovered_components() -> bool:
|
|||||||
return get_available_components() is not None
|
return get_available_components() is not None
|
||||||
|
|
||||||
|
|
||||||
def get_project_cmakelists() -> str:
|
def get_project_cmakelists(minimal: bool = False) -> str:
|
||||||
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
|
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
|
||||||
|
|
||||||
|
When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS``
|
||||||
|
since ``project_description.json`` may be stale on the first write.
|
||||||
|
"""
|
||||||
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
||||||
variant = get_esp32_variant()
|
variant = get_esp32_variant()
|
||||||
idf_target = variant.lower().replace("-", "")
|
idf_target = variant.lower().replace("-", "")
|
||||||
@@ -72,6 +79,37 @@ def get_project_cmakelists() -> str:
|
|||||||
for flag in project_compile_opts
|
for flag in project_compile_opts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Per-project list exposed as a CMake variable so converted PIO libs
|
||||||
|
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
||||||
|
# project-specific names into their cached CMakeLists.
|
||||||
|
#
|
||||||
|
# Emit via idf_build_set_property (not plain set()) so the value is
|
||||||
|
# serialised into build_properties.temp.cmake and visible to IDF's
|
||||||
|
# early requirements-expansion pass (component_get_requirements.cmake
|
||||||
|
# runs as a separate CMake script invocation that doesn't load the
|
||||||
|
# project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_
|
||||||
|
# MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty).
|
||||||
|
from esphome.components.esp32 import get_managed_component_require_names
|
||||||
|
|
||||||
|
managed_components_property = "\n".join(
|
||||||
|
f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)"
|
||||||
|
for name in get_managed_component_require_names()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Built-in IDF components exposed via our own property (not IDF's
|
||||||
|
# __COMPONENT_REQUIRES_COMMON, which would append them to every
|
||||||
|
# component's REQUIRES including real IDF components). Referenced by
|
||||||
|
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
|
||||||
|
# on minimal writes because project_description.json may be stale.
|
||||||
|
builtin_components_property = (
|
||||||
|
""
|
||||||
|
if minimal
|
||||||
|
else "\n".join(
|
||||||
|
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
|
||||||
|
for name in sorted(get_available_components() or [])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
# Auto-generated by ESPHome
|
# Auto-generated by ESPHome
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
@@ -99,6 +137,10 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
|||||||
|
|
||||||
{extra_compile_options}
|
{extra_compile_options}
|
||||||
|
|
||||||
|
{managed_components_property}
|
||||||
|
|
||||||
|
{builtin_components_property}
|
||||||
|
|
||||||
project({CORE.name})
|
project({CORE.name})
|
||||||
|
|
||||||
# Emit raw JSON size data for ESPHome to read post-build.
|
# Emit raw JSON size data for ESPHome to read post-build.
|
||||||
@@ -113,11 +155,12 @@ add_custom_command(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_component_cmakelists(minimal: bool = False) -> str:
|
def get_component_cmakelists() -> str:
|
||||||
"""Generate the main component CMakeLists.txt."""
|
"""Generate the main component CMakeLists.txt.
|
||||||
idf_requires = [] if minimal else (get_available_components() or [])
|
|
||||||
requires_str = " ".join(idf_requires)
|
|
||||||
|
|
||||||
|
REQUIRES pulls in the discovered built-in IDF components via the
|
||||||
|
project-level variables set in the top-level CMakeLists.
|
||||||
|
"""
|
||||||
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
||||||
# emitted project-wide via idf_build_set_property in
|
# emitted project-wide via idf_build_set_property in
|
||||||
# get_project_cmakelists so they reach every component, not just src/.
|
# get_project_cmakelists so they reach every component, not just src/.
|
||||||
@@ -126,17 +169,30 @@ def get_component_cmakelists(minimal: bool = False) -> str:
|
|||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
# Auto-generated by ESPHome
|
# Auto-generated by ESPHome
|
||||||
file(GLOB_RECURSE app_sources
|
# CONFIGURE_DEPENDS asks CMake to re-check the glob each build so test
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
# runs that reuse the build dir don't compile stale source paths. It's
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
# invalid in script mode (cmake -P), which is how IDF's
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
# component_get_requirements.cmake includes us, so skip it there.
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
if(CMAKE_SCRIPT_MODE_FILE)
|
||||||
)
|
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"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS ${{app_sources}}
|
SRCS ${{app_sources}}
|
||||||
INCLUDE_DIRS "." "esphome"
|
INCLUDE_DIRS "." "esphome"
|
||||||
REQUIRES {requires_str}
|
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply C++ standard
|
# Apply C++ standard
|
||||||
@@ -162,11 +218,11 @@ def write_project(minimal: bool = False) -> None:
|
|||||||
# Write top-level CMakeLists.txt
|
# Write top-level CMakeLists.txt
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_build_path("CMakeLists.txt"),
|
CORE.relative_build_path("CMakeLists.txt"),
|
||||||
get_project_cmakelists(),
|
get_project_cmakelists(minimal=minimal),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Write component CMakeLists.txt in src/
|
# Write component CMakeLists.txt in src/
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_src_path("CMakeLists.txt"),
|
CORE.relative_src_path("CMakeLists.txt"),
|
||||||
get_component_cmakelists(minimal=minimal),
|
get_component_cmakelists(),
|
||||||
)
|
)
|
||||||
@@ -588,6 +588,18 @@ def add_idf_component(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_managed_component_require_names() -> list[str]:
|
||||||
|
"""Return sorted IDF require names for components added via
|
||||||
|
``add_idf_component`` (``owner/name`` -> ``owner__name``).
|
||||||
|
|
||||||
|
The build_gen layer (``build_gen.espidf.get_project_cmakelists``)
|
||||||
|
feeds this list into ``ESPHOME_PROJECT_MANAGED_COMPONENTS`` so
|
||||||
|
converted PIO libraries can REQUIRE them by name at configure time.
|
||||||
|
"""
|
||||||
|
components_registry = CORE.data.get(KEY_ESP32, {}).get(KEY_COMPONENTS, {})
|
||||||
|
return sorted(name.replace("/", "__") for name in components_registry)
|
||||||
|
|
||||||
|
|
||||||
def exclude_builtin_idf_component(name: str) -> None:
|
def exclude_builtin_idf_component(name: str) -> None:
|
||||||
"""Exclude an ESP-IDF component from the build.
|
"""Exclude an ESP-IDF component from the build.
|
||||||
|
|
||||||
|
|||||||
+29
-132
@@ -12,7 +12,6 @@ from typing import TypeVar
|
|||||||
from urllib.parse import urlparse, urlsplit, urlunsplit
|
from urllib.parse import urlparse, urlsplit, urlunsplit
|
||||||
|
|
||||||
from esphome import git, yaml_util
|
from esphome import git, yaml_util
|
||||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
|
||||||
from esphome.core import CORE, Library
|
from esphome.core import CORE, Library
|
||||||
from esphome.espidf.framework import archive_extract_all, download_from_mirrors, rmdir
|
from esphome.espidf.framework import archive_extract_all, download_from_mirrors, rmdir
|
||||||
from esphome.helpers import write_file_if_changed
|
from esphome.helpers import write_file_if_changed
|
||||||
@@ -50,28 +49,6 @@ SRC_FILE_EXTENSIONS = [
|
|||||||
ESP32_PLATFORM = "espressif32"
|
ESP32_PLATFORM = "espressif32"
|
||||||
DOMAIN = "pio_components"
|
DOMAIN = "pio_components"
|
||||||
|
|
||||||
#
|
|
||||||
# Constants for workarounds
|
|
||||||
#
|
|
||||||
|
|
||||||
REQUIRES_DETECT_PATTERNS = {
|
|
||||||
"mbedtls": [re.compile(r'^\s*#\s*include\s*[<"]mbedtls[^">]*[">]', re.MULTILINE)],
|
|
||||||
"esp_netif": [
|
|
||||||
re.compile(r'^\s*#\s*include\s*[<"]esp_netif[^">]*[">]', re.MULTILINE)
|
|
||||||
],
|
|
||||||
"esp_driver_gpio": [
|
|
||||||
re.compile(r'^\s*#\s*include\s*[<"]driver/gpio\.h[^">]*[">]', re.MULTILINE)
|
|
||||||
],
|
|
||||||
"esp_timer": [
|
|
||||||
re.compile(r'^\s*#\s*include\s*[<"]esp_timer\.h[^">]*[">]', re.MULTILINE)
|
|
||||||
],
|
|
||||||
"esp_wifi": [
|
|
||||||
re.compile(
|
|
||||||
r'^\s*#\s*include\s*[<"]WiFi\.h[^">]*[">]', re.MULTILINE
|
|
||||||
) # Arduino WiFi
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
ESPHOME_DATA_KEY = "ESPHOME"
|
ESPHOME_DATA_KEY = "ESPHOME"
|
||||||
ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE"
|
ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE"
|
||||||
|
|
||||||
@@ -86,10 +63,7 @@ class URLSource(Source):
|
|||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
def download(self, dir_suffix: str, force: bool = False) -> Path:
|
def download(self, dir_suffix: str, force: bool = False) -> Path:
|
||||||
# Partition by framework: generated idf_component.yml content
|
base_dir = Path(CORE.data_dir) / DOMAIN
|
||||||
# depends on CORE.using_arduino, so caches can't be shared.
|
|
||||||
framework = "arduino" if CORE.using_arduino else "idf"
|
|
||||||
base_dir = Path(CORE.data_dir) / DOMAIN / framework
|
|
||||||
h = hashlib.new("sha256")
|
h = hashlib.new("sha256")
|
||||||
h.update(self.url.encode())
|
h.update(self.url.encode())
|
||||||
path = base_dir / h.hexdigest()[:8] / dir_suffix
|
path = base_dir / h.hexdigest()[:8] / dir_suffix
|
||||||
@@ -124,12 +98,11 @@ class GitSource(Source):
|
|||||||
self.ref = ref
|
self.ref = ref
|
||||||
|
|
||||||
def download(self, dir_suffix: str, force: bool = False) -> Path:
|
def download(self, dir_suffix: str, force: bool = False) -> Path:
|
||||||
framework = "arduino" if CORE.using_arduino else "idf"
|
|
||||||
path, _ = git.clone_or_update(
|
path, _ = git.clone_or_update(
|
||||||
url=self.url,
|
url=self.url,
|
||||||
ref=self.ref,
|
ref=self.ref,
|
||||||
refresh=git.NEVER_REFRESH if not force else None,
|
refresh=git.NEVER_REFRESH if not force else None,
|
||||||
domain=f"{DOMAIN}/{framework}",
|
domain=DOMAIN,
|
||||||
submodules=[],
|
submodules=[],
|
||||||
subpath=Path(dir_suffix),
|
subpath=Path(dir_suffix),
|
||||||
)
|
)
|
||||||
@@ -282,46 +255,6 @@ def _get_package_from_pio_registry(
|
|||||||
return owner, name, version["name"], pkgfile["download_url"]
|
return owner, name, version["name"], pkgfile["download_url"]
|
||||||
|
|
||||||
|
|
||||||
def _patch_component(component: IDFComponent, first_pass: bool):
|
|
||||||
"""
|
|
||||||
Apply patches/workarounds to specific components that have known issues.
|
|
||||||
|
|
||||||
This function modifies component data to fix compatibility issues or missing
|
|
||||||
dependencies for certain libraries. It applies different patches based on
|
|
||||||
whether it's the first or second pass of processing.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
component: The IDFComponent object to potentially patch
|
|
||||||
first_pass: Boolean indicating if this is the first pass of processing
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Patch only on the second step
|
|
||||||
if not first_pass and CORE.using_arduino:
|
|
||||||
# Add the missing dependency to Arduino framework. Source is None so
|
|
||||||
# the IDF component manager resolves it from the registry instead of
|
|
||||||
# cloning the 2 GB arduino-esp32 git history.
|
|
||||||
component.dependencies.append(
|
|
||||||
IDFComponent(
|
|
||||||
"espressif/arduino-esp32",
|
|
||||||
str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
#
|
|
||||||
# fastled/FastLED
|
|
||||||
#
|
|
||||||
|
|
||||||
# Patch only on the first step
|
|
||||||
if (
|
|
||||||
first_pass
|
|
||||||
and component.name == _owner_pkgname_to_name("fastled", "FastLED")
|
|
||||||
and not (component.path / "idf_component.yml").is_file()
|
|
||||||
):
|
|
||||||
# Force fake idf_component: This project already support ESP-IDF
|
|
||||||
(component.path / "idf_component.yml").write_text("")
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_extra_script(component: IDFComponent) -> None:
|
def _apply_extra_script(component: IDFComponent) -> None:
|
||||||
"""Run a PIO ``extraScript`` and fold its captured env vars into
|
"""Run a PIO ``extraScript`` and fold its captured env vars into
|
||||||
``component.data["build"]["flags"]`` so the existing -L/-l/-D
|
``component.data["build"]["flags"]`` so the existing -L/-l/-D
|
||||||
@@ -506,43 +439,6 @@ def _convert_library_to_component(library: Library) -> IDFComponent:
|
|||||||
return IDFComponent(name, version, source)
|
return IDFComponent(name, version, source)
|
||||||
|
|
||||||
|
|
||||||
def _detect_requires(build_src_files: list[str]) -> set[str]:
|
|
||||||
"""
|
|
||||||
Detect required components from source files.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_src_files: List of source file paths to analyze
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Set of detected required components
|
|
||||||
"""
|
|
||||||
detected = set()
|
|
||||||
|
|
||||||
# 1. Process each source file
|
|
||||||
for file in build_src_files:
|
|
||||||
path = Path(file)
|
|
||||||
|
|
||||||
if not path.is_file():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
||||||
except Exception: # pylint: disable=broad-exception-caught
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Add required component if one of these patterns matches
|
|
||||||
for require_name, patterns in REQUIRES_DETECT_PATTERNS.items():
|
|
||||||
if require_name in detected:
|
|
||||||
continue # already found
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
if pattern.search(content):
|
|
||||||
detected.add(require_name)
|
|
||||||
break
|
|
||||||
|
|
||||||
return detected
|
|
||||||
|
|
||||||
|
|
||||||
def _split_list_by_condition(
|
def _split_list_by_condition(
|
||||||
items: list[str], match_fn: Callable[[str], str | None]
|
items: list[str], match_fn: Callable[[str], str | None]
|
||||||
) -> tuple[list[str], list[str]]:
|
) -> tuple[list[str], list[str]]:
|
||||||
@@ -609,13 +505,14 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
|
|||||||
component.path / Path(build_src_dir), build_src_filter
|
component.path / Path(build_src_dir), build_src_filter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Detect in the files which requirements to add
|
# Only bake library.json-declared deps here. Project-managed and
|
||||||
# By default in platformio, all the components are added: we need to detect them when using ESP-IDF
|
# built-in components come in via ${ESPHOME_PROJECT_MANAGED_COMPONENTS}
|
||||||
requires = _detect_requires(build_src_files)
|
# / ${ESPHOME_PROJECT_BUILTIN_COMPONENTS} set in the top-level
|
||||||
|
# CMakeLists, so this file stays project-agnostic when shared from
|
||||||
# Dependencies are required
|
# the pio_components cache.
|
||||||
for dependency in component.dependencies:
|
requires: set[str] = {
|
||||||
requires.add(dependency.get_require_name())
|
dependency.get_require_name() for dependency in component.dependencies
|
||||||
|
}
|
||||||
|
|
||||||
# Only keep sources
|
# Only keep sources
|
||||||
build_src_files = [os.path.relpath(p, component.path) for p in build_src_files]
|
build_src_files = [os.path.relpath(p, component.path) for p in build_src_files]
|
||||||
@@ -654,9 +551,19 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
|
|||||||
if build_include_dirs:
|
if build_include_dirs:
|
||||||
str_include_dirs = " ".join([escape_entry(p) for p in build_include_dirs])
|
str_include_dirs = " ".join([escape_entry(p) for p in build_include_dirs])
|
||||||
content += f" INCLUDE_DIRS {str_include_dirs}\n"
|
content += f" INCLUDE_DIRS {str_include_dirs}\n"
|
||||||
if requires:
|
# Project-managed and built-in component lists are set per-project
|
||||||
str_requires = " ".join(sorted(requires))
|
# via idf_build_set_property in the top-level CMakeLists; expanded
|
||||||
content += f" REQUIRES {str_requires}\n"
|
# here at configure time. Keeping them out of the per-lib REQUIRES
|
||||||
|
# means this CMakeLists is project-agnostic and reusable from the
|
||||||
|
# pio_components cache across builds.
|
||||||
|
str_requires = " ".join(
|
||||||
|
[
|
||||||
|
*sorted(requires),
|
||||||
|
"${ESPHOME_PROJECT_MANAGED_COMPONENTS}",
|
||||||
|
"${ESPHOME_PROJECT_BUILTIN_COMPONENTS}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
content += f" REQUIRES {str_requires}\n"
|
||||||
content += ")\n"
|
content += ")\n"
|
||||||
|
|
||||||
# Add public and private build flags
|
# Add public and private build flags
|
||||||
@@ -732,13 +639,10 @@ def generate_idf_component_yml(component: IDFComponent) -> str:
|
|||||||
try:
|
try:
|
||||||
dep["override_path"] = str(dependency.path)
|
dep["override_path"] = str(dependency.path)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
# No local path; let the IDF component manager resolve.
|
# No local path: only a GitSource can substitute its URL.
|
||||||
# GitSource gives an explicit URL; arduino-esp32 is resolved by
|
if not isinstance(dependency.source, GitSource):
|
||||||
# version from the registry. Anything else is a bug.
|
|
||||||
if isinstance(dependency.source, GitSource):
|
|
||||||
dep["git"] = dependency.source.url
|
|
||||||
elif dependency.name != "espressif/arduino-esp32":
|
|
||||||
raise e
|
raise e
|
||||||
|
dep["git"] = dependency.source.url
|
||||||
|
|
||||||
data["dependencies"][dependency.get_sanitized_name()] = dep
|
data["dependencies"][dependency.get_sanitized_name()] = dep
|
||||||
|
|
||||||
@@ -903,12 +807,9 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone
|
|||||||
cmakelists_txt_path = component.path / "CMakeLists.txt"
|
cmakelists_txt_path = component.path / "CMakeLists.txt"
|
||||||
idf_component_yml_path = component.path / "idf_component.yml"
|
idf_component_yml_path = component.path / "idf_component.yml"
|
||||||
|
|
||||||
# Apply patches to the library metadata
|
# Bundled CMakeLists.txt / idf_component.yml are ignored -- library
|
||||||
_patch_component(component, True)
|
# authors' IDF support is frequently broken (bogus REQUIRES, hard-coded
|
||||||
|
# arduino-esp32, etc.). We always regenerate.
|
||||||
if cmakelists_txt_path.is_file() and idf_component_yml_path.is_file():
|
|
||||||
# Already an ESP-IDF component
|
|
||||||
return component
|
|
||||||
|
|
||||||
if library_json_path.is_file():
|
if library_json_path.is_file():
|
||||||
component.data = _parse_library_json(library_json_path)
|
component.data = _parse_library_json(library_json_path)
|
||||||
@@ -919,9 +820,6 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone
|
|||||||
"Invalid PIO library: missing library.json and/or library.properties"
|
"Invalid PIO library: missing library.json and/or library.properties"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply additional patches to the library metadata
|
|
||||||
_patch_component(component, False)
|
|
||||||
|
|
||||||
# Check if the component is usable with ESP-IDF before executing any
|
# Check if the component is usable with ESP-IDF before executing any
|
||||||
# third-party Python from the library (``_apply_extra_script`` below).
|
# third-party Python from the library (``_apply_extra_script`` below).
|
||||||
_check_library_data(component.data)
|
_check_library_data(component.data)
|
||||||
@@ -936,7 +834,6 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone
|
|||||||
# Handle the dependencies (convert PlatformIO library to ESP-IDF component if needed)
|
# Handle the dependencies (convert PlatformIO library to ESP-IDF component if needed)
|
||||||
_process_dependencies(component)
|
_process_dependencies(component)
|
||||||
|
|
||||||
# Generate files
|
|
||||||
_LOGGER.debug("Generating CMakeLists.txt for %s@%s ...", name, version)
|
_LOGGER.debug("Generating CMakeLists.txt for %s@%s ...", name, version)
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
cmakelists_txt_path,
|
cmakelists_txt_path,
|
||||||
|
|||||||
@@ -302,10 +302,21 @@ def run_compile(config, verbose: bool) -> int:
|
|||||||
return rc
|
return rc
|
||||||
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
|
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
|
||||||
write_project(minimal=False)
|
write_project(minimal=False)
|
||||||
|
# The post-discovery rewrite leaves CMakeLists newer than
|
||||||
|
# CMakeCache.txt. CMake won't re-touch CMakeCache.txt on a
|
||||||
|
# configure that only changes idf_build_set_property values
|
||||||
|
# (those aren't cache variables), so has_outdated_files() would
|
||||||
|
# return True on every subsequent build, perpetually retriggering
|
||||||
|
# the two-pass. Touch CMakeCache.txt now so its mtime stays past
|
||||||
|
# the rewritten CMakeLists.
|
||||||
|
cmakecache = CORE.relative_build_path("build/CMakeCache.txt")
|
||||||
|
if cmakecache.is_file():
|
||||||
|
os.utime(cmakecache)
|
||||||
if CORE.testing_mode:
|
if CORE.testing_mode:
|
||||||
# Reconfigure again so cmake is up to date with the full component
|
# Reconfigure again so cmake is up to date with the full
|
||||||
# list. This ensures idf.py build won't re-run cmake, which would
|
# component list before the build's idf.py invocation runs --
|
||||||
# regenerate memory.ld and wipe the DRAM/IRAM patches applied below.
|
# idf.py build would otherwise re-run cmake and regenerate
|
||||||
|
# memory.ld, wiping the DRAM/IRAM patches applied below.
|
||||||
rc = run_reconfigure()
|
rc = run_reconfigure()
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
_LOGGER.error("Reconfigure with discovered components failed")
|
_LOGGER.error("Reconfigure with discovered components failed")
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""Tests for esphome.build_gen.espidf module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome.components.esp32 import (
|
||||||
|
KEY_COMPONENTS,
|
||||||
|
KEY_ESP32,
|
||||||
|
KEY_PATH,
|
||||||
|
KEY_REF,
|
||||||
|
KEY_REPO,
|
||||||
|
)
|
||||||
|
from esphome.const import KEY_CORE
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _reset_core(tmp_path: Path) -> None:
|
||||||
|
"""Give each test its own CORE.build_path and a clean esp32 data slot."""
|
||||||
|
CORE.build_path = str(tmp_path)
|
||||||
|
CORE.data.setdefault(KEY_CORE, {})
|
||||||
|
CORE.data[KEY_ESP32] = {KEY_COMPONENTS: {}}
|
||||||
|
|
||||||
|
|
||||||
|
def _write_project_description(tmp_path: Path, components: dict[str, str]) -> None:
|
||||||
|
"""Stub a project_description.json with the given component_name -> dir map."""
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir(exist_ok=True)
|
||||||
|
(build_dir / "project_description.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"build_component_info": {
|
||||||
|
name: {"dir": dir_} for name, dir_ in components.items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_available_components_returns_none_without_build_path() -> None:
|
||||||
|
"""No build_path set yet: must not raise on Path(None)."""
|
||||||
|
CORE.build_path = None
|
||||||
|
from esphome.build_gen.espidf import get_available_components
|
||||||
|
|
||||||
|
assert get_available_components() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_available_components_returns_none_without_project_description(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
from esphome.build_gen.espidf import get_available_components
|
||||||
|
|
||||||
|
assert get_available_components() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_available_components_filters_src_managed_and_pio(tmp_path: Path) -> None:
|
||||||
|
"""Built-ins are returned; src/, managed_components/, pio_components/ skipped."""
|
||||||
|
_write_project_description(
|
||||||
|
tmp_path,
|
||||||
|
{
|
||||||
|
"src": f"{tmp_path}/src",
|
||||||
|
"esp_lcd": "/idf/components/esp_lcd",
|
||||||
|
"espressif__arduino-esp32": f"{tmp_path}/managed_components/arduino",
|
||||||
|
"JPEGDEC": f"{tmp_path}/pio_components/arduino/abc/bitbank2/JPEGDEC",
|
||||||
|
"freertos": "/idf/components/freertos",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
from esphome.build_gen.espidf import get_available_components
|
||||||
|
|
||||||
|
assert sorted(get_available_components()) == ["esp_lcd", "freertos"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_cmakelists_minimal_omits_builtin_components_property(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Minimal write must not emit ESPHOME_PROJECT_BUILTIN_COMPONENTS even
|
||||||
|
when project_description.json exists (the data may be stale on the
|
||||||
|
first write before the discovery pass refreshes it)."""
|
||||||
|
_write_project_description(tmp_path, {"esp_lcd": "/idf/components/esp_lcd"})
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||||
|
patch.object(CORE, "name", "test"),
|
||||||
|
):
|
||||||
|
from esphome.build_gen.espidf import get_project_cmakelists
|
||||||
|
|
||||||
|
content = get_project_cmakelists(minimal=True)
|
||||||
|
|
||||||
|
assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_cmakelists_full_emits_builtin_components_property(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Non-minimal write emits one idf_build_set_property line per built-in,
|
||||||
|
sorted, and excludes src/managed/pio components."""
|
||||||
|
_write_project_description(
|
||||||
|
tmp_path,
|
||||||
|
{
|
||||||
|
"src": f"{tmp_path}/src",
|
||||||
|
"esp_lcd": "/idf/components/esp_lcd",
|
||||||
|
"freertos": "/idf/components/freertos",
|
||||||
|
"espressif__esp-dsp": f"{tmp_path}/managed_components/esp-dsp",
|
||||||
|
"JPEGDEC": f"{tmp_path}/pio_components/arduino/abc/bitbank2/JPEGDEC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||||
|
patch.object(CORE, "name", "test"),
|
||||||
|
):
|
||||||
|
from esphome.build_gen.espidf import get_project_cmakelists
|
||||||
|
|
||||||
|
content = get_project_cmakelists(minimal=False)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS esp_lcd APPEND)"
|
||||||
|
in content
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS freertos APPEND)"
|
||||||
|
in content
|
||||||
|
)
|
||||||
|
# Excluded by get_available_components filtering.
|
||||||
|
assert "espressif__esp-dsp APPEND" not in content
|
||||||
|
assert "JPEGDEC APPEND" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_cmakelists_emits_managed_components_property(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""ESPHOME_PROJECT_MANAGED_COMPONENTS is always emitted (both modes)
|
||||||
|
from the esp32 add_idf_component registry."""
|
||||||
|
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {
|
||||||
|
"espressif/esp-dsp": {KEY_REPO: None, KEY_REF: "1.7.1", KEY_PATH: None},
|
||||||
|
"espressif/arduino-esp32": {KEY_REPO: None, KEY_REF: "3.3.8", KEY_PATH: None},
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||||
|
patch.object(CORE, "name", "test"),
|
||||||
|
):
|
||||||
|
from esphome.build_gen.espidf import get_project_cmakelists
|
||||||
|
|
||||||
|
for minimal in (True, False):
|
||||||
|
content = get_project_cmakelists(minimal=minimal)
|
||||||
|
assert (
|
||||||
|
"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS"
|
||||||
|
" espressif__arduino-esp32 APPEND)"
|
||||||
|
) in content
|
||||||
|
assert (
|
||||||
|
"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS"
|
||||||
|
" espressif__esp-dsp APPEND)"
|
||||||
|
) in content
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -21,7 +22,6 @@ from esphome.espidf.component import (
|
|||||||
_check_library_data,
|
_check_library_data,
|
||||||
_collect_filtered_files,
|
_collect_filtered_files,
|
||||||
_convert_library_to_component,
|
_convert_library_to_component,
|
||||||
_detect_requires,
|
|
||||||
_parse_library_json,
|
_parse_library_json,
|
||||||
_parse_library_properties,
|
_parse_library_properties,
|
||||||
_process_dependencies,
|
_process_dependencies,
|
||||||
@@ -83,19 +83,6 @@ def test_collect_filtered_files_exclude(tmp_path):
|
|||||||
assert str(f2) not in result
|
assert str(f2) not in result
|
||||||
|
|
||||||
|
|
||||||
def test_detect_requires(tmp_path):
|
|
||||||
f = tmp_path / "main.c"
|
|
||||||
f.write_text('#include "mbedtls/foo.h"')
|
|
||||||
|
|
||||||
result = _detect_requires([str(f)])
|
|
||||||
assert "mbedtls" in result
|
|
||||||
|
|
||||||
|
|
||||||
def test_detect_requires_ignores_invalid_file(tmp_path):
|
|
||||||
result = _detect_requires([str(tmp_path / "missing.c")])
|
|
||||||
assert result == set()
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_list_by_condition():
|
def test_split_list_by_condition():
|
||||||
items = ["-Iinclude", "-Llib", "-Wall"]
|
items = ["-Iinclude", "-Llib", "-Wall"]
|
||||||
|
|
||||||
@@ -142,7 +129,7 @@ def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path):
|
|||||||
== f"""idf_component_register(
|
== f"""idf_component_register(
|
||||||
SRCS "src{sep}main.c"
|
SRCS "src{sep}main.c"
|
||||||
INCLUDE_DIRS "src"
|
INCLUDE_DIRS "src"
|
||||||
REQUIRES dep
|
REQUIRES dep ${{ESPHOME_PROJECT_MANAGED_COMPONENTS}} ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||||
)
|
)
|
||||||
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
"-DTEST"
|
"-DTEST"
|
||||||
@@ -160,6 +147,58 @@ target_link_libraries(${{COMPONENT_LIB}} INTERFACE
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_cmakelists_txt_references_project_managed_components_variable(
|
||||||
|
tmp_component: IDFComponent,
|
||||||
|
) -> None:
|
||||||
|
# The CMakeLists is cached under pio_components/<hash>/ and shared
|
||||||
|
# across projects, so the project-managed REQUIRES list is exposed via
|
||||||
|
# a CMake variable expanded at configure time rather than baked here.
|
||||||
|
src_dir = tmp_component.path / "src"
|
||||||
|
src_dir.mkdir()
|
||||||
|
(src_dir / "main.c").write_text("int main() {}")
|
||||||
|
tmp_component.data = {}
|
||||||
|
|
||||||
|
content = generate_cmakelists_txt(tmp_component)
|
||||||
|
assert "${ESPHOME_PROJECT_MANAGED_COMPONENTS}" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_idf_component_overwrites_bundled_files(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
esp32_idf_core: None,
|
||||||
|
) -> None:
|
||||||
|
# A library that ships its own CMakeLists.txt + idf_component.yml must
|
||||||
|
# have both replaced by ESPHome's generated content. Library authors'
|
||||||
|
# bundled IDF metadata is frequently broken (bogus REQUIRES, hard-coded
|
||||||
|
# frameworks), so we always regenerate from library.json.
|
||||||
|
from esphome.espidf.component import _generate_idf_component
|
||||||
|
|
||||||
|
(tmp_path / "src").mkdir()
|
||||||
|
(tmp_path / "src" / "main.cpp").write_text("// dummy\n")
|
||||||
|
(tmp_path / "library.json").write_text(json.dumps({"name": "tripwire-lib"}))
|
||||||
|
(tmp_path / "CMakeLists.txt").write_text("# TRIPWIRE_BUNDLED_CMAKELISTS\n")
|
||||||
|
(tmp_path / "idf_component.yml").write_text("# TRIPWIRE_BUNDLED_MANIFEST\n")
|
||||||
|
|
||||||
|
fake_component = IDFComponent(
|
||||||
|
"owner/tripwire-lib", "1.0.0", source=URLSource("http://dummy")
|
||||||
|
)
|
||||||
|
fake_component.path = tmp_path
|
||||||
|
monkeypatch.setattr(
|
||||||
|
esphome.espidf.component,
|
||||||
|
"_convert_library_to_component",
|
||||||
|
lambda _lib: fake_component,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(fake_component, "download", lambda force=False: None)
|
||||||
|
|
||||||
|
_generate_idf_component(Library("owner/tripwire-lib", "1.0.0", None))
|
||||||
|
|
||||||
|
cml = (tmp_path / "CMakeLists.txt").read_text()
|
||||||
|
manifest = (tmp_path / "idf_component.yml").read_text()
|
||||||
|
assert "TRIPWIRE_BUNDLED_CMAKELISTS" not in cml
|
||||||
|
assert "TRIPWIRE_BUNDLED_MANIFEST" not in manifest
|
||||||
|
assert "idf_component_register" in cml
|
||||||
|
|
||||||
|
|
||||||
def test_generate_idf_component_yml_basic(tmp_component):
|
def test_generate_idf_component_yml_basic(tmp_component):
|
||||||
tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}}
|
tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}}
|
||||||
result = generate_idf_component_yml(tmp_component)
|
result = generate_idf_component_yml(tmp_component)
|
||||||
@@ -187,27 +226,6 @@ dependencies:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_idf_component_yml_arduino_registry_dep(tmp_component):
|
|
||||||
# Synthetic arduino-esp32 dep with no source / no path: should emit a
|
|
||||||
# version-only entry so the IDF component manager resolves it from the
|
|
||||||
# registry instead of via git.
|
|
||||||
dep = IDFComponent("espressif/arduino-esp32", "3.3.8", source=None)
|
|
||||||
|
|
||||||
tmp_component.dependencies = [dep]
|
|
||||||
tmp_component.data = {}
|
|
||||||
|
|
||||||
result = generate_idf_component_yml(tmp_component)
|
|
||||||
|
|
||||||
assert (
|
|
||||||
result
|
|
||||||
== """version: 1.0.0
|
|
||||||
dependencies:
|
|
||||||
espressif/arduino-esp32:
|
|
||||||
version: 3.3.8
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_idf_component_yml_missing_path_reraises(tmp_component):
|
def test_generate_idf_component_yml_missing_path_reraises(tmp_component):
|
||||||
# A dep without a path and without a recognised source should re-raise
|
# A dep without a path and without a recognised source should re-raise
|
||||||
# the underlying RuntimeError instead of silently producing a bad manifest.
|
# the underlying RuntimeError instead of silently producing a bad manifest.
|
||||||
|
|||||||
Reference in new issue
Block a user