mirror of
https://github.com/esphome/esphome.git
synced 2026-10-09 21:13:12 +00:00
[esp8266] Force-include the throw stubs into the core and libraries on the native toolchain (#20336)
This commit is contained in:
@@ -54,6 +54,7 @@ from esphome.components.esp8266.const import (
|
||||
KEY_ESP8266,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_SCANF_FLOAT,
|
||||
THROW_STUBS_HEADER,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import get_project_cxx_compile_flags
|
||||
@@ -1087,32 +1088,8 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
if "USE_ESP8266_WAVEFORM_STUBS" in flag_defines:
|
||||
core_exclude |= _CORE_EXCLUDE_WAVEFORM
|
||||
|
||||
archives = []
|
||||
# variant_dir existence was already enforced with the include dirs
|
||||
variant_sources = collect_sources(variant_dir)
|
||||
if variant_sources:
|
||||
objs = compile_edges(lines, variant_sources, variant_dir, "variant")
|
||||
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
|
||||
archives.append("libFrameworkArduinoVariant.a")
|
||||
|
||||
core_objs = compile_edges(
|
||||
lines, collect_sources(core_dir, core_exclude), core_dir, "core"
|
||||
)
|
||||
if not core_objs:
|
||||
# An empty archive would link into a wall of undefined references
|
||||
# (app_entry, the exception vectors) far from the cause
|
||||
raise EsphomeError(
|
||||
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
|
||||
)
|
||||
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
|
||||
archives.append("libFrameworkArduino.a")
|
||||
|
||||
lib_archives, direct_objs = library_edges(lines, libraries)
|
||||
archives += lib_archives
|
||||
|
||||
# One source of truth with the PlatformIO path: esp8266/__init__ pins
|
||||
# build_src_flags (the throw_stubs force-include); -include paths
|
||||
# resolve against the source root
|
||||
# esp8266/__init__ pins build_src_flags (the throw_stubs force-include), as on
|
||||
# the PlatformIO path; -include paths resolve against the source root
|
||||
src_other: list[str] = []
|
||||
src_includes: list[str] = []
|
||||
src_it = iter(
|
||||
@@ -1131,7 +1108,45 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
src_includes.append(tok[len("-include") :])
|
||||
else:
|
||||
src_other.append(_shell_token(tok))
|
||||
# The throw stubs abort: none anywhere with exceptions on, else the core and
|
||||
# libraries get them too (src keeps them after its pch include)
|
||||
if config.exceptions:
|
||||
src_includes = [h for h in src_includes if h != THROW_STUBS_HEADER]
|
||||
include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes]
|
||||
framework_flags = ""
|
||||
if THROW_STUBS_HEADER in src_includes:
|
||||
stubs_flag = include_flags[src_includes.index(THROW_STUBS_HEADER)]
|
||||
lines.append(f"frameworkflags = {stubs_flag}")
|
||||
framework_flags = "$frameworkflags"
|
||||
archives = []
|
||||
# variant_dir existence was already enforced with the include dirs
|
||||
variant_sources = collect_sources(variant_dir)
|
||||
if variant_sources:
|
||||
objs = compile_edges(
|
||||
lines, variant_sources, variant_dir, "variant", flags=framework_flags
|
||||
)
|
||||
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
|
||||
archives.append("libFrameworkArduinoVariant.a")
|
||||
|
||||
core_objs = compile_edges(
|
||||
lines,
|
||||
collect_sources(core_dir, core_exclude),
|
||||
core_dir,
|
||||
"core",
|
||||
flags=framework_flags,
|
||||
)
|
||||
if not core_objs:
|
||||
# An empty archive would link into a wall of undefined references
|
||||
# (app_entry, the exception vectors) far from the cause
|
||||
raise EsphomeError(
|
||||
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
|
||||
)
|
||||
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
|
||||
archives.append("libFrameworkArduino.a")
|
||||
|
||||
lib_archives, direct_objs = library_edges(lines, libraries, framework_flags)
|
||||
archives += lib_archives
|
||||
|
||||
# One shared variable instead of repeating the flags line on every src
|
||||
# edge (hundreds of edges in a real project)
|
||||
lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
|
||||
|
||||
@@ -207,10 +207,12 @@ def compile_edges(
|
||||
|
||||
|
||||
def library_edges(
|
||||
lines: list[str], libraries: list[ArduinoLibrary]
|
||||
lines: list[str], libraries: list[ArduinoLibrary], extra_flags: str = ""
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Emit every library's compile and archive edges.
|
||||
|
||||
``extra_flags`` (raw ninja text) precede each library's own flags.
|
||||
|
||||
Returns the archive names and the objects that link directly. A
|
||||
library's own include dirs lead its compile lines, as PlatformIO searched
|
||||
them first: the include path is one global list, so another library's
|
||||
@@ -232,7 +234,7 @@ def library_edges(
|
||||
lib.sources,
|
||||
common_parent(lib.sources),
|
||||
f"lib/{lib.name}",
|
||||
flags=" ".join(_shell_token(f) for f in lib.flags),
|
||||
flags=" ".join(filter(None, [extra_flags, *map(_shell_token, lib.flags)])),
|
||||
own_includes=" ".join(f"-I{_q(d)}" for d in lib.include_dirs),
|
||||
)
|
||||
if not lib.lib_archive:
|
||||
|
||||
@@ -53,6 +53,7 @@ from .const import (
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
THROW_STUBS_HEADER,
|
||||
enable_serial,
|
||||
enable_serial1,
|
||||
esp8266_ns,
|
||||
@@ -477,11 +478,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
|
||||
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
|
||||
# See throw_stubs.h. Unconditional: the native build generator reads
|
||||
# the same option, keeping one source of truth.
|
||||
cg.add_platformio_option(
|
||||
"build_src_flags", "-include esphome/components/esp8266/throw_stubs.h"
|
||||
)
|
||||
# See throw_stubs.h. The native build generator reads this option, also
|
||||
# passes it to the core and libraries, and drops it with exceptions on.
|
||||
cg.add_platformio_option("build_src_flags", f"-include {THROW_STUBS_HEADER}")
|
||||
|
||||
# In testing mode, fake larger memory to allow linking grouped component tests
|
||||
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
|
||||
|
||||
@@ -17,6 +17,8 @@ KEY_SERIAL_REQUIRED = "serial_required"
|
||||
KEY_SERIAL1_REQUIRED = "serial1_required"
|
||||
# Set for the native (non-PlatformIO) toolchain's build generator
|
||||
KEY_SCANF_FLOAT = "scanf_float"
|
||||
# Force-included std::__throw_* overrides, relative to the src dir
|
||||
THROW_STUBS_HEADER = "esphome/components/esp8266/throw_stubs.h"
|
||||
# Per-board flash-layout override consumed by board_ld_script()
|
||||
KEY_LDSCRIPT = "ldscript"
|
||||
# Crash-decode tools, resolved once per run by __init__
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
* unused, dead-strip the load, and drop the string entirely -- no LTO needed.
|
||||
* Behavior is identical to today: a bare abort() (the message was never
|
||||
* printed). This header MUST be force-included before <string>, so it is
|
||||
* wired up via build_src_flags "-include ..." in this component's __init__.py.
|
||||
* wired up via build_src_flags "-include ..." in this component's __init__.py;
|
||||
* the native toolchain also force-includes it into the core and libraries.
|
||||
*
|
||||
* Note: this defines functions in namespace std (technically UB). It is safe
|
||||
* here because the definitions match the existing abort() behavior exactly.
|
||||
|
||||
@@ -34,7 +34,12 @@ from esphome.components.esp8266.boards import (
|
||||
board_ld_script,
|
||||
)
|
||||
from esphome.components.esp8266.build_surgery import RATETABLE_RULE
|
||||
from esphome.components.esp8266.const import KEY_BOARD, KEY_ESP8266, KEY_SCANF_FLOAT
|
||||
from esphome.components.esp8266.const import (
|
||||
KEY_BOARD,
|
||||
KEY_ESP8266,
|
||||
KEY_SCANF_FLOAT,
|
||||
THROW_STUBS_HEADER,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, EsphomeError
|
||||
@@ -360,18 +365,22 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None:
|
||||
# Assembly and C sources compile through their own rules
|
||||
assert "cont.S.o: aspp" in content
|
||||
assert "abi.c.o: c" in content
|
||||
# throw_stubs is force-included for ESPHome sources only, via one shared
|
||||
# srcflags variable rather than a copy of the flags line per edge
|
||||
# throw_stubs reaches src through srcflags (after the pch include) and the
|
||||
# core through frameworkflags: one shared variable each, not a copy per edge
|
||||
src_lines = [line for line in content.splitlines() if "obj/src/" in line]
|
||||
assert any("main.cpp.o: cxx" in line for line in src_lines)
|
||||
assert content.count("throw_stubs.h") == 1
|
||||
assert content.count("throw_stubs.h") == 2
|
||||
assert "srcflags = -include" in content
|
||||
flags_lines = [
|
||||
line for line in content.splitlines() if line.startswith(" flags = ")
|
||||
]
|
||||
assert flags_lines
|
||||
# C++ src edges consume the precompiled header; C/assembly keep srcflags
|
||||
assert set(flags_lines) == {" flags = $srcflags", " flags = $srccxxflags"}
|
||||
assert set(flags_lines) == {
|
||||
" flags = $srcflags",
|
||||
" flags = $srccxxflags",
|
||||
" flags = $frameworkflags",
|
||||
}
|
||||
|
||||
|
||||
def test_write_project_pch(tmp_path: Path) -> None:
|
||||
@@ -715,6 +724,8 @@ def test_write_project_libraries_and_variant(
|
||||
assert "libHeadersOnly.a" not in content
|
||||
assert "Library HeadersOnly has no source files" in caplog.text
|
||||
assert " flags = -DMYLIB=1" in content
|
||||
# With exceptions on, nothing gets the stubs
|
||||
assert "throw_stubs.h" not in content
|
||||
# A library's own include dirs lead its compile lines
|
||||
assert " own_includes = -I" in content
|
||||
assert "$own_includes $cxxflags $flags" in content
|
||||
@@ -729,6 +740,30 @@ def test_write_project_libraries_and_variant(
|
||||
assert f"ccache = {_shq('/cc/ccache')}" in content
|
||||
|
||||
|
||||
def test_write_project_throw_stubs_reach_core_and_libraries(tmp_path: Path) -> None:
|
||||
"""Without exceptions the core and libraries take the throw stubs too."""
|
||||
paths = _make_framework(tmp_path)
|
||||
lib_dir = tmp_path / "libsrc"
|
||||
lib_dir.mkdir()
|
||||
(lib_dir / "lib.cpp").write_text("")
|
||||
library = ArduinoLibrary(
|
||||
name="MyLib",
|
||||
sources=[lib_dir / "lib.cpp"],
|
||||
include_dirs=[lib_dir],
|
||||
flags=["-DMYLIB=1"],
|
||||
)
|
||||
content = _write_ninja(paths, libraries=[library])
|
||||
stubs = CORE.relative_src_path() / THROW_STUBS_HEADER
|
||||
assert f"frameworkflags = -include {_shq(str(stubs))}" in content
|
||||
# Before the library's own flags; core edges take the variable alone
|
||||
assert " flags = $frameworkflags -DMYLIB=1" in content
|
||||
lines = content.splitlines()
|
||||
core_edge = next(
|
||||
i for i, line in enumerate(lines) if "core_esp8266_main.cpp.o: cxx" in line
|
||||
)
|
||||
assert lines[core_edge + 1] == " flags = $frameworkflags"
|
||||
|
||||
|
||||
def test_get_flash_ld_path(tmp_path: Path) -> None:
|
||||
|
||||
paths = InstalledPaths(
|
||||
|
||||
Reference in New Issue
Block a user