mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[core] Add add_cmake_arg (#18498)
This commit is contained in:
+23
-15
@@ -72,6 +72,13 @@ def has_discovered_components() -> bool:
|
||||
return get_available_components() is not None
|
||||
|
||||
|
||||
def _cmake_quote(value: str) -> str:
|
||||
"""Quote a cmake arg value for a set() line. add_cmake_arg rejects
|
||||
whitespace, quotes, and '$', so only backslashes need escaping."""
|
||||
escaped = value.replace("\\", "\\\\")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
|
||||
|
||||
@@ -114,6 +121,15 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
else ""
|
||||
)
|
||||
|
||||
# CMake variables registered via cg.add_cmake_arg(). Emitted before
|
||||
# include(project.cmake) so values like EXCLUDE_COMPONENTS are already
|
||||
# set when project.cmake seeds the component list, and on minimal
|
||||
# (discovery) writes too so excluded components never register.
|
||||
cmake_args = "\n".join(
|
||||
f"set({name} {_cmake_quote(value)})"
|
||||
for name, value in sorted(CORE.cmake_args.items())
|
||||
)
|
||||
|
||||
# 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.
|
||||
@@ -129,18 +145,6 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
for name in get_managed_component_require_names()
|
||||
)
|
||||
|
||||
# Components excluded from the build (DEFAULT_EXCLUDED_IDF_COMPONENTS
|
||||
# minus per-component re-includes). project.cmake reads the plain
|
||||
# EXCLUDE_COMPONENTS variable when seeding the component list, so this
|
||||
# must be set before project(). Emitted on minimal writes too so the
|
||||
# discovery reconfigure never registers the excluded components.
|
||||
excluded_components = get_excluded_builtin_components()
|
||||
exclude_components_var = (
|
||||
f'set(EXCLUDE_COMPONENTS "{";".join(excluded_components)}")'
|
||||
if excluded_components
|
||||
else ""
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -150,13 +154,17 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
# project_description.json from a build without exclusions may still
|
||||
# list them, and requiring an excluded component pulls it back into
|
||||
# the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS).
|
||||
# Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the
|
||||
# two can never disagree within one generated file.
|
||||
builtin_components_property = (
|
||||
""
|
||||
if minimal
|
||||
else "\n".join(
|
||||
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
|
||||
for name in sorted(
|
||||
set(get_available_components() or []).difference(excluded_components)
|
||||
set(get_available_components() or []).difference(
|
||||
CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -184,9 +192,9 @@ set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
|
||||
set(IDF_TARGET {idf_target})
|
||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
{cmake_args}
|
||||
|
||||
{exclude_components_var}
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
{cpp_standard_options}
|
||||
|
||||
|
||||
@@ -63,6 +63,17 @@ def get_ini_content():
|
||||
# Add extra script for C++ flags
|
||||
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||
|
||||
# Add CMake args. A user-supplied value (str or list) is deliberately
|
||||
# replaced; this option was always overwritten at FINAL priority.
|
||||
if CORE.cmake_args:
|
||||
CORE.add_platformio_option(
|
||||
"board_build.cmake_extra_args",
|
||||
" ".join(
|
||||
f"-D{name}={value}" for name, value in sorted(CORE.cmake_args.items())
|
||||
),
|
||||
replace=True,
|
||||
)
|
||||
|
||||
content = "[platformio]\n"
|
||||
content += f"description = ESPHome {__version__}\n"
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
add,
|
||||
add_build_flag,
|
||||
add_build_unflag,
|
||||
add_cmake_arg,
|
||||
add_cxx_build_flag,
|
||||
add_define,
|
||||
add_global,
|
||||
|
||||
@@ -760,9 +760,10 @@ def include_builtin_idf_component(name: str) -> None:
|
||||
def get_excluded_builtin_components() -> list[str]:
|
||||
"""Return the sorted built-in IDF components excluded from the build.
|
||||
|
||||
Single accessor for both build writers: the PlatformIO path passes it as
|
||||
``-DEXCLUDE_COMPONENTS`` and the native ESP-IDF path emits it into the
|
||||
generated CMakeLists.
|
||||
The set reaches both build writers as the ``EXCLUDE_COMPONENTS`` CMake
|
||||
arg (registered via ``cg.add_cmake_arg`` at FINAL priority); the native
|
||||
ESP-IDF writer also reads it directly to filter the built-in component
|
||||
list.
|
||||
"""
|
||||
return sorted(CORE.data.get(KEY_ESP32, {}).get(KEY_EXCLUDE_COMPONENTS, ()))
|
||||
|
||||
@@ -2148,14 +2149,16 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
|
||||
|
||||
|
||||
def register_exclude_components_cmake_arg() -> None:
|
||||
"""Register the current exclusion set as the EXCLUDE_COMPONENTS cmake arg."""
|
||||
if excluded := get_excluded_builtin_components():
|
||||
cg.add_cmake_arg("EXCLUDE_COMPONENTS", ";".join(excluded))
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _write_exclude_components() -> None:
|
||||
"""Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions."""
|
||||
if excluded := get_excluded_builtin_components():
|
||||
cg.add_platformio_option(
|
||||
"board_build.cmake_extra_args",
|
||||
f"-DEXCLUDE_COMPONENTS={';'.join(excluded)}",
|
||||
)
|
||||
register_exclude_components_cmake_arg()
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
|
||||
@@ -641,6 +641,8 @@ class EsphomeCore:
|
||||
self.platformio_libraries: dict[str, Library] = {}
|
||||
# A set of build flags to set in the platformio project
|
||||
self.build_flags: set[str] = set()
|
||||
# A map of CMake args to apply to build systems that use CMake.
|
||||
self.cmake_args: dict[str, str] = {}
|
||||
# A set of build flags that apply to C++ compiles only (CXXFLAGS /
|
||||
# CXX_COMPILE_OPTIONS), for flags GCC rejects or warns about on C
|
||||
self.cxx_build_flags: set[str] = set()
|
||||
@@ -704,6 +706,7 @@ class EsphomeCore:
|
||||
self.global_statements = []
|
||||
self.platformio_libraries = {}
|
||||
self.build_flags = set()
|
||||
self.cmake_args = {}
|
||||
self.cxx_build_flags = set()
|
||||
self.build_unflags = set()
|
||||
self.cpp_standard = None
|
||||
@@ -1062,6 +1065,30 @@ class EsphomeCore:
|
||||
_LOGGER.debug("Adding build flag: %s", build_flag)
|
||||
return build_flag
|
||||
|
||||
def add_cmake_arg(self, name: str, value: str) -> None:
|
||||
"""Register a CMake variable for CMake-based toolchains.
|
||||
|
||||
The value must not contain whitespace or quotes (the PlatformIO
|
||||
backend passes all args to CMake as a single space-joined string
|
||||
of ``-DNAME=VALUE`` pairs) or ``$`` (expanded by CMake on the
|
||||
ESP-IDF path but interpolated differently or passed through by
|
||||
PlatformIO).
|
||||
"""
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
|
||||
raise ValueError(f"Invalid CMake arg name: {name!r}")
|
||||
if re.search(r"[\s\"'$]", value):
|
||||
raise ValueError(
|
||||
f"CMake arg {name} value {value!r} must not contain "
|
||||
"whitespace, quotes, or '$'"
|
||||
)
|
||||
old = self.cmake_args.get(name)
|
||||
if old is not None and old != value:
|
||||
_LOGGER.warning(
|
||||
"CMake arg %s already set to %s; overwriting with %s", name, old, value
|
||||
)
|
||||
self.cmake_args[name] = value
|
||||
_LOGGER.debug("Adding CMake arg: %s=%s", name, value)
|
||||
|
||||
def add_cxx_build_flag(self, build_flag: str) -> str:
|
||||
self.cxx_build_flags.add(build_flag)
|
||||
_LOGGER.debug("Adding C++ build flag: %s", build_flag)
|
||||
@@ -1091,10 +1118,14 @@ class EsphomeCore:
|
||||
_LOGGER.debug("Adding define: %s", define)
|
||||
return define
|
||||
|
||||
def add_platformio_option(self, key: str, value: str | list[str]) -> None:
|
||||
def add_platformio_option(
|
||||
self, key: str, value: str | list[str], *, replace: bool = False
|
||||
) -> None:
|
||||
"""Set a platformio.ini option; list values append to an existing list
|
||||
unless ``replace`` is True, which overwrites any existing value."""
|
||||
new_val = value
|
||||
old_val = self.platformio_options.get(key)
|
||||
if isinstance(old_val, list):
|
||||
if not replace and isinstance(old_val, list):
|
||||
assert isinstance(value, list)
|
||||
new_val = old_val + value
|
||||
self.platformio_options[key] = new_val
|
||||
|
||||
@@ -699,6 +699,11 @@ def add_build_flag(build_flag: str):
|
||||
CORE.add_build_flag(build_flag)
|
||||
|
||||
|
||||
def add_cmake_arg(name: str, value: str) -> None:
|
||||
"""Add a CMake arg for CMake-based toolchains; see ``EsphomeCore.add_cmake_arg``."""
|
||||
CORE.add_cmake_arg(name, value)
|
||||
|
||||
|
||||
def add_cxx_build_flag(build_flag: str) -> None:
|
||||
"""Add a global build flag that applies to C++ compiles only.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.components.esp32 import (
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
register_exclude_components_cmake_arg,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE
|
||||
@@ -137,6 +138,27 @@ def test_get_project_cmakelists_full_emits_builtin_components_property(
|
||||
assert "JPEGDEC APPEND" not in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_emits_cmake_args() -> None:
|
||||
"""Args registered via CORE.add_cmake_arg() are emitted as set() lines,
|
||||
on minimal writes too."""
|
||||
CORE.add_cmake_arg("EXECUTABLE_COMPONENT_NAME", "src")
|
||||
|
||||
content = _render(minimal=True)
|
||||
|
||||
assert 'set(EXECUTABLE_COMPONENT_NAME "src")' in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_escapes_backslashes_in_cmake_args() -> None:
|
||||
"""Backslashes (the only character escaping applies to; the rest are
|
||||
rejected at registration) are doubled so CMake reads the value back
|
||||
verbatim."""
|
||||
CORE.add_cmake_arg("MY_PATH", r"C:\esp\idf")
|
||||
|
||||
content = _render(minimal=True)
|
||||
|
||||
assert r'set(MY_PATH "C:\\esp\\idf")' in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_emits_exclude_components(tmp_path: Path) -> None:
|
||||
"""Excluded components are passed to IDF via EXCLUDE_COMPONENTS and are
|
||||
dropped from ESPHOME_PROJECT_BUILTIN_COMPONENTS even when a stale
|
||||
@@ -151,6 +173,7 @@ def test_get_project_cmakelists_emits_exclude_components(tmp_path: Path) -> None
|
||||
},
|
||||
)
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity", "esp_lcd"}
|
||||
register_exclude_components_cmake_arg()
|
||||
|
||||
content = _render()
|
||||
|
||||
@@ -169,6 +192,7 @@ def test_get_project_cmakelists_minimal_emits_exclude_components() -> None:
|
||||
"""The discovery (minimal) write also excludes components so they never
|
||||
register in project_description.json."""
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = {"unity"}
|
||||
register_exclude_components_cmake_arg()
|
||||
|
||||
content = _render(minimal=True)
|
||||
|
||||
@@ -177,6 +201,8 @@ def test_get_project_cmakelists_minimal_emits_exclude_components() -> None:
|
||||
|
||||
def test_get_project_cmakelists_no_exclude_components_line_when_empty() -> None:
|
||||
"""No EXCLUDE_COMPONENTS line at all when nothing is excluded."""
|
||||
register_exclude_components_cmake_arg()
|
||||
|
||||
content = _render()
|
||||
|
||||
assert "EXCLUDE_COMPONENTS" not in content
|
||||
@@ -197,6 +223,7 @@ def test_include_builtin_idf_component_removes_exclusion() -> None:
|
||||
|
||||
assert get_excluded_builtin_components() == ["unity"]
|
||||
|
||||
register_exclude_components_cmake_arg()
|
||||
content = _render()
|
||||
|
||||
assert 'set(EXCLUDE_COMPONENTS "unity")' in content
|
||||
|
||||
@@ -169,6 +169,7 @@ def clean_core(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(CORE, "platformio_libraries", {})
|
||||
monkeypatch.setattr(CORE, "build_flags", set())
|
||||
monkeypatch.setattr(CORE, "build_unflags", set())
|
||||
monkeypatch.setattr(CORE, "cmake_args", {})
|
||||
|
||||
|
||||
def test_get_ini_content_pins_cpp_standard(
|
||||
@@ -202,6 +203,49 @@ def test_get_ini_content_no_cpp_standard(
|
||||
assert "-std=" not in content
|
||||
|
||||
|
||||
def test_get_ini_content_emits_cmake_args(
|
||||
clean_core: None, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Registered args are space-joined into one option, sorted by name."""
|
||||
monkeypatch.setattr(
|
||||
CORE,
|
||||
"cmake_args",
|
||||
{"EXECUTABLE_COMPONENT_NAME": "src", "EXCLUDE_COMPONENTS": "unity"},
|
||||
)
|
||||
|
||||
content = platformio.get_ini_content()
|
||||
|
||||
assert (
|
||||
"board_build.cmake_extra_args = "
|
||||
"-DEXCLUDE_COMPONENTS=unity -DEXECUTABLE_COMPONENT_NAME=src" in content
|
||||
)
|
||||
|
||||
|
||||
def test_get_ini_content_no_cmake_option_when_no_args(clean_core: None) -> None:
|
||||
"""No board_build.cmake_extra_args line at all when nothing registered
|
||||
(ESP8266/RP2040/LibreTiny builds must not get a blank option)."""
|
||||
content = platformio.get_ini_content()
|
||||
|
||||
assert "board_build.cmake_extra_args" not in content
|
||||
|
||||
|
||||
def test_get_ini_content_overwrites_list_valued_user_cmake_option(
|
||||
clean_core: None, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A user-supplied board_build.cmake_extra_args may be a list; the
|
||||
registered args must replace it without tripping add_platformio_option's
|
||||
list-append assert."""
|
||||
monkeypatch.setattr(
|
||||
CORE, "platformio_options", {"board_build.cmake_extra_args": ["-DFOO=1"]}
|
||||
)
|
||||
monkeypatch.setattr(CORE, "cmake_args", {"EXECUTABLE_COMPONENT_NAME": "src"})
|
||||
|
||||
content = platformio.get_ini_content()
|
||||
|
||||
assert "board_build.cmake_extra_args = -DEXECUTABLE_COMPONENT_NAME=src" in content
|
||||
assert "-DFOO=1" not in content
|
||||
|
||||
|
||||
def test_write_cxx_flags_script_emits_registered_flags(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -990,3 +990,35 @@ class TestEsphomeCore:
|
||||
)
|
||||
# The unflag is still recorded either way.
|
||||
assert target.build_unflags == {"-fno-rtti", "-fno-exceptions"}
|
||||
|
||||
def test_add_cmake_arg(self, target) -> None:
|
||||
target.add_cmake_arg("EXCLUDE_COMPONENTS", "unity;esp_lcd")
|
||||
assert target.cmake_args == {"EXCLUDE_COMPONENTS": "unity;esp_lcd"}
|
||||
|
||||
@pytest.mark.parametrize("name", ["", "BAD NAME", 'A"B', "A(B)", "1ABC"])
|
||||
def test_add_cmake_arg__rejects_invalid_name(self, target, name: str) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid CMake arg name"):
|
||||
target.add_cmake_arg(name, "value")
|
||||
|
||||
@pytest.mark.parametrize("value", ["a b", "a\tb", 'a"b', "a'b", "a${FOO}b"])
|
||||
def test_add_cmake_arg__rejects_invalid_value(self, target, value: str) -> None:
|
||||
"""Whitespace and quotes are rejected (the PlatformIO backend passes
|
||||
args as one space-joined string, which would split such a value), and
|
||||
so is '$' (expanded differently by CMake and PlatformIO)."""
|
||||
with pytest.raises(ValueError, match="must not contain"):
|
||||
target.add_cmake_arg("MY_ARG", value)
|
||||
|
||||
def test_add_cmake_arg__warns_on_overwrite(
|
||||
self, target, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Re-registering with a different value is last-writer-wins; warn so
|
||||
the silently dropped value is diagnosable."""
|
||||
target.add_cmake_arg("MY_ARG", "one")
|
||||
target.add_cmake_arg("MY_ARG", "one")
|
||||
assert "overwriting" not in caplog.text
|
||||
|
||||
target.add_cmake_arg("MY_ARG", "two")
|
||||
assert (
|
||||
"CMake arg MY_ARG already set to one; overwriting with two" in caplog.text
|
||||
)
|
||||
assert target.cmake_args == {"MY_ARG": "two"}
|
||||
|
||||
Reference in New Issue
Block a user