mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:07:33 +00:00
[core] Make set_cpp_standard work on the native IDF toolchain (#16907)
This commit is contained in:
@@ -8,6 +8,17 @@ import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
|
||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
||||
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
||||
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
||||
# i.e. after IDF appends its default and before the options are consumed, and
|
||||
# applies project-wide like PlatformIO build_unflags.
|
||||
CPP_STANDARD_TEMPLATE = """\
|
||||
idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
|
||||
list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
|
||||
list(APPEND esphome_cxx_compile_options "-std={standard}")
|
||||
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
"""Get list of built-in ESP-IDF components from project_description.json.
|
||||
@@ -84,6 +95,12 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
for flag in project_compile_opts
|
||||
)
|
||||
|
||||
cpp_standard_options = (
|
||||
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
|
||||
if CORE.cpp_standard
|
||||
else ""
|
||||
)
|
||||
|
||||
# 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.
|
||||
@@ -140,6 +157,8 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
{cpp_standard_options}
|
||||
|
||||
{extra_compile_options}
|
||||
|
||||
{managed_components_property}
|
||||
@@ -200,9 +219,6 @@ idf_component_register(
|
||||
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||
)
|
||||
|
||||
# Apply C++ standard
|
||||
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||
|
||||
# ESPHome linker options
|
||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{link_opts_str}
|
||||
|
||||
@@ -33,12 +33,27 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
|
||||
return content
|
||||
|
||||
|
||||
# All -std= variants a platform/framework may set by default, in both the GNU
|
||||
# and strict dialects; unflagged so the cg.set_cpp_standard() value is the
|
||||
# only standard left in the build.
|
||||
CPP_STD_VARIANTS = [
|
||||
f"{prefix}{year}"
|
||||
for year in ("11", "14", "17", "20", "23", "26", "2a", "2b", "2c")
|
||||
for prefix in ("gnu++", "c++")
|
||||
]
|
||||
|
||||
|
||||
def get_ini_content():
|
||||
CORE.add_platformio_option(
|
||||
"lib_deps",
|
||||
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||
+ ["${common.lib_deps}"],
|
||||
)
|
||||
if CORE.cpp_standard:
|
||||
for variant in CPP_STD_VARIANTS:
|
||||
if variant != CORE.cpp_standard:
|
||||
CORE.add_build_unflag(f"-std={variant}")
|
||||
CORE.add_build_flag(f"-std={CORE.cpp_standard}")
|
||||
# Sort to avoid changing build flags order
|
||||
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||
|
||||
|
||||
@@ -593,6 +593,8 @@ class EsphomeCore:
|
||||
self.build_flags: set[str] = set()
|
||||
# A set of build unflags to set in the platformio project
|
||||
self.build_unflags: set[str] = set()
|
||||
# The C++ language standard for the build (e.g. "gnu++20"), set via cg.set_cpp_standard()
|
||||
self.cpp_standard: str | None = None
|
||||
# A set of defines to set for the compile process in esphome/core/defines.h
|
||||
self.defines: set[Define] = set()
|
||||
# A map of all platformio options to apply
|
||||
@@ -649,6 +651,7 @@ class EsphomeCore:
|
||||
self.platformio_libraries = {}
|
||||
self.build_flags = set()
|
||||
self.build_unflags = set()
|
||||
self.cpp_standard = None
|
||||
self.defines = set()
|
||||
self.platformio_options = {}
|
||||
self.loaded_integrations = set()
|
||||
|
||||
@@ -705,15 +705,8 @@ def add_build_unflag(build_unflag: str) -> None:
|
||||
|
||||
|
||||
def set_cpp_standard(standard: str) -> None:
|
||||
"""Set C++ standard with compiler flag `-std={standard}`."""
|
||||
CORE.add_build_unflag("-std=gnu++11")
|
||||
CORE.add_build_unflag("-std=gnu++14")
|
||||
CORE.add_build_unflag("-std=gnu++17")
|
||||
CORE.add_build_unflag("-std=gnu++23")
|
||||
CORE.add_build_unflag("-std=gnu++2a")
|
||||
CORE.add_build_unflag("-std=gnu++2b")
|
||||
CORE.add_build_unflag("-std=gnu++2c")
|
||||
CORE.add_build_flag(f"-std={standard}")
|
||||
"""Set the C++ language standard for the build (e.g. ``gnu++20``)."""
|
||||
CORE.cpp_standard = standard
|
||||
|
||||
|
||||
def add_define(name: str, value: SafeExpType = None):
|
||||
|
||||
@@ -162,3 +162,53 @@ def test_get_project_cmakelists_emits_managed_components_property(
|
||||
"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS"
|
||||
" espressif__esp-dsp APPEND)"
|
||||
) in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_replaces_cpp_standard(tmp_path: Path) -> None:
|
||||
"""cg.set_cpp_standard() replaces the IDF default -std in
|
||||
CXX_COMPILE_OPTIONS between include(project.cmake) and project()."""
|
||||
with (
|
||||
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch.object(CORE, "cpp_standard", "gnu++20"),
|
||||
):
|
||||
from esphome.build_gen.espidf import get_project_cmakelists
|
||||
|
||||
content = get_project_cmakelists(minimal=True)
|
||||
|
||||
assert (
|
||||
"idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)"
|
||||
in content
|
||||
)
|
||||
assert 'list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")' in content
|
||||
assert 'list(APPEND esphome_cxx_compile_options "-std=gnu++20")' in content
|
||||
# The replacement must come after project.cmake (which appends the IDF
|
||||
# default) and before project() (which consumes the options).
|
||||
include_pos = content.index("tools/cmake/project.cmake")
|
||||
replace_pos = content.index("CXX_COMPILE_OPTIONS")
|
||||
project_pos = content.index("project(test)")
|
||||
assert include_pos < replace_pos < project_pos
|
||||
|
||||
|
||||
def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None:
|
||||
with (
|
||||
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch.object(CORE, "cpp_standard", None),
|
||||
):
|
||||
from esphome.build_gen.espidf import get_project_cmakelists
|
||||
|
||||
content = get_project_cmakelists(minimal=True)
|
||||
|
||||
assert "CXX_COMPILE_OPTIONS" not in content
|
||||
|
||||
|
||||
def test_get_component_cmakelists_no_compile_features() -> None:
|
||||
"""The C++ standard is pinned project-wide via CXX_COMPILE_OPTIONS in the
|
||||
top-level CMakeLists; the src component must not set its own."""
|
||||
with patch.object(CORE, "build_flags", set()):
|
||||
from esphome.build_gen.espidf import get_component_cmakelists
|
||||
|
||||
content = get_component_cmakelists()
|
||||
|
||||
assert "target_compile_features" not in content
|
||||
|
||||
@@ -160,3 +160,43 @@ def test_write_ini_no_change_when_content_same(
|
||||
call_args = mock_write_file_if_changed.call_args[0]
|
||||
assert call_args[0] == ini_file
|
||||
assert content in call_args[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clean_core(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(CORE, "name", "test")
|
||||
monkeypatch.setattr(CORE, "platformio_options", {})
|
||||
monkeypatch.setattr(CORE, "platformio_libraries", {})
|
||||
monkeypatch.setattr(CORE, "build_flags", set())
|
||||
monkeypatch.setattr(CORE, "build_unflags", set())
|
||||
|
||||
|
||||
def test_get_ini_content_pins_cpp_standard(
|
||||
clean_core: None, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""cg.set_cpp_standard() pins -std via build_flags and unflags every other
|
||||
known standard so the platform/framework default is stripped."""
|
||||
monkeypatch.setattr(CORE, "cpp_standard", "gnu++20")
|
||||
|
||||
content = platformio.get_ini_content()
|
||||
|
||||
flags_section = content.split("build_flags =")[1].split("build_unflags =")[0]
|
||||
unflags_section = content.split("build_unflags =")[1].split("extra_scripts")[0]
|
||||
assert "-std=gnu++20\n" in flags_section
|
||||
# Both the GNU and strict dialects of every other standard are stripped.
|
||||
for year in ("11", "14", "17", "23", "26", "2a", "2b", "2c"):
|
||||
assert f"-std=gnu++{year}\n" in unflags_section
|
||||
assert f"-std=c++{year}\n" in unflags_section
|
||||
assert "-std=c++20\n" in unflags_section
|
||||
# The selected standard must not unflag itself.
|
||||
assert "-std=gnu++20\n" not in unflags_section
|
||||
|
||||
|
||||
def test_get_ini_content_no_cpp_standard(
|
||||
clean_core: None, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(CORE, "cpp_standard", None)
|
||||
|
||||
content = platformio.get_ini_content()
|
||||
|
||||
assert "-std=" not in content
|
||||
|
||||
Reference in New Issue
Block a user