mirror of
https://github.com/esphome/esphome.git
synced 2026-08-24 07:06:20 +00:00
Merge branch 'esp8266-native-build-surgery' into esp8266-native-toolchain-plumbing
This commit is contained in:
@@ -20,6 +20,7 @@ from esphome.platformio.library import (
|
||||
DEFAULT_BUILD_SRC_FILTER,
|
||||
ESPHOME_DATA_EXTRA_CMAKE_KEY,
|
||||
ESPHOME_DATA_KEY,
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY,
|
||||
SRC_FILE_EXTENSIONS,
|
||||
ConvertedLibrary as IDFComponent,
|
||||
LibraryBackend,
|
||||
@@ -205,6 +206,16 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
|
||||
content += f" {str_build_flag}\n"
|
||||
content += ")\n"
|
||||
|
||||
# Extra-script LINKFLAGS: routed to the link line; in
|
||||
# target_compile_options they would be silently ineffective
|
||||
if link_flags := component.data.get(ESPHOME_DATA_KEY, {}).get(
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY, []
|
||||
):
|
||||
content += "target_link_options(${COMPONENT_LIB} INTERFACE\n"
|
||||
for link_flag in link_flags:
|
||||
content += f" {escape_entry(link_flag)}\n"
|
||||
content += ")\n"
|
||||
|
||||
# Add custom CMake scripts
|
||||
content += "\n".join(
|
||||
component.data.get(ESPHOME_DATA_KEY, {}).get(ESPHOME_DATA_EXTRA_CMAKE_KEY, [])
|
||||
|
||||
@@ -17,6 +17,7 @@ import shlex
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.platformio.library import ESPHOME_DATA_KEY, ESPHOME_DATA_LINK_FLAGS_KEY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.platformio.library import ConvertedLibrary
|
||||
@@ -63,6 +64,11 @@ def apply_extra_script(
|
||||
board_mcu=board_mcu(),
|
||||
pio_platform=pio_platform,
|
||||
)
|
||||
if link_flags := _str_entries(result.linkflags, "LINKFLAGS"):
|
||||
# Kept apart from build.flags: the CMake emitters route those to
|
||||
# target_compile_options, where a link flag is silently ineffective
|
||||
esphome_data = component.data.setdefault(ESPHOME_DATA_KEY, {})
|
||||
esphome_data.setdefault(ESPHOME_DATA_LINK_FLAGS_KEY, []).extend(link_flags)
|
||||
extra_flags = captured_as_build_flags(result, library_dir=source_path)
|
||||
if not extra_flags:
|
||||
return
|
||||
@@ -148,6 +154,12 @@ class _FakeSConsEnv:
|
||||
return self._vars.get(key, "")
|
||||
|
||||
def Append(self, **kwargs) -> None: # noqa: N802 (SCons API name)
|
||||
self._add(kwargs, prepend=False)
|
||||
|
||||
def Prepend(self, **kwargs) -> None: # noqa: N802 (SCons API name)
|
||||
self._add(kwargs, prepend=True)
|
||||
|
||||
def _add(self, kwargs: dict[str, Any], *, prepend: bool) -> None:
|
||||
for key, value in kwargs.items():
|
||||
if key not in _CAPTURED_KEYS:
|
||||
# Warn once per key so a loop of Appends cannot spam
|
||||
@@ -163,13 +175,16 @@ class _FakeSConsEnv:
|
||||
else:
|
||||
items = list(value) if isinstance(value, (list, tuple)) else [value]
|
||||
bucket = getattr(self.result, key.lower())
|
||||
bucket.extend(items)
|
||||
if prepend:
|
||||
# SCons order: new values ahead of what is already there
|
||||
# (scripts prepend LIBS for static-link symbol resolution)
|
||||
bucket[:0] = items
|
||||
else:
|
||||
bucket.extend(items)
|
||||
|
||||
# Same keys, same flattened capture; ordering/dedup don't matter since
|
||||
# the consumer re-orders anyway
|
||||
Prepend = Append
|
||||
# Dedup is not modelled; a repeated flag is harmless on the command line
|
||||
AppendUnique = Append
|
||||
PrependUnique = Append
|
||||
PrependUnique = Prepend
|
||||
|
||||
# ----- Everything else is a no-op so unsupported scripts don't crash -----
|
||||
|
||||
@@ -263,22 +278,22 @@ def run_extra_script(
|
||||
return env.result
|
||||
|
||||
|
||||
def _str_entries(bucket: list, kind: str) -> list[str]:
|
||||
# Third-party scripts legally append SCons nodes, ints, or dicts;
|
||||
# stringifying those into flags would hand the compiler garbage
|
||||
good = [entry for entry in bucket if isinstance(entry, str)]
|
||||
for entry in bucket:
|
||||
if not isinstance(entry, str):
|
||||
_LOGGER.warning("Ignoring unsupported %s entry %r", kind, entry)
|
||||
return good
|
||||
|
||||
|
||||
def captured_as_build_flags(
|
||||
result: ExtraScriptResult, *, library_dir: Path
|
||||
) -> list[str]:
|
||||
"""Translate captured env vars into -L/-l/-D/raw build flags; path
|
||||
entries anchor to ``library_dir`` so the build files stay portable."""
|
||||
flags: list[str] = []
|
||||
|
||||
def _strs(bucket: list, kind: str) -> list[str]:
|
||||
# Third-party scripts legally append SCons nodes, ints, or dicts;
|
||||
# stringifying those into flags would hand the compiler garbage
|
||||
good = [entry for entry in bucket if isinstance(entry, str)]
|
||||
for entry in bucket:
|
||||
if not isinstance(entry, str):
|
||||
_LOGGER.warning("Ignoring unsupported %s entry %r", kind, entry)
|
||||
return good
|
||||
|
||||
library_root = library_dir.resolve()
|
||||
|
||||
def _anchored(path: str) -> str:
|
||||
@@ -292,12 +307,14 @@ def captured_as_build_flags(
|
||||
|
||||
# shlex.quote so a spaced path survives lex_build_flags as one token
|
||||
flags.extend(
|
||||
f"-I{shlex.quote(_anchored(path))}" for path in _strs(result.cpppath, "CPPPATH")
|
||||
f"-I{shlex.quote(_anchored(path))}"
|
||||
for path in _str_entries(result.cpppath, "CPPPATH")
|
||||
)
|
||||
flags.extend(
|
||||
f"-L{shlex.quote(_anchored(path))}" for path in _strs(result.libpath, "LIBPATH")
|
||||
f"-L{shlex.quote(_anchored(path))}"
|
||||
for path in _str_entries(result.libpath, "LIBPATH")
|
||||
)
|
||||
flags.extend(f"-l{shlex.quote(lib)}" for lib in _strs(result.libs, "LIBS"))
|
||||
flags.extend(f"-l{shlex.quote(lib)}" for lib in _str_entries(result.libs, "LIBS"))
|
||||
for define in result.cppdefines:
|
||||
# SCons also accepts nested containers; formatting those blind
|
||||
# would hand the compiler garbage like -D{'FOO': '1'}
|
||||
@@ -317,7 +334,8 @@ def captured_as_build_flags(
|
||||
else:
|
||||
_LOGGER.warning("Ignoring unsupported CPPDEFINES entry %r", define)
|
||||
# Each captured entry is one argv token in SCons; quote so the
|
||||
# lex_build_flags round-trip cannot split a spaced value into two
|
||||
flags.extend(shlex.quote(f) for f in _strs(result.linkflags, "LINKFLAGS"))
|
||||
flags.extend(shlex.quote(f) for f in _strs(result.cppflags, "CPPFLAGS"))
|
||||
# lex_build_flags round-trip cannot split a spaced value into two.
|
||||
# LINKFLAGS are deliberately absent: they travel via
|
||||
# ESPHOME_DATA_LINK_FLAGS_KEY straight to the link line.
|
||||
flags.extend(shlex.quote(f) for f in _str_entries(result.cppflags, "CPPFLAGS"))
|
||||
return flags
|
||||
|
||||
@@ -83,6 +83,9 @@ _EXTRACTED_MARKER = ".esphome_extracted"
|
||||
|
||||
ESPHOME_DATA_KEY = "ESPHOME"
|
||||
ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE"
|
||||
# Captured extra-script LINKFLAGS; kept apart from build.flags so they reach
|
||||
# the link line (target_link_options), not target_compile_options
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY = "LINK_FLAGS"
|
||||
|
||||
|
||||
class Source:
|
||||
|
||||
@@ -23,6 +23,8 @@ from esphome.espidf.component import (
|
||||
)
|
||||
import esphome.platformio.library
|
||||
from esphome.platformio.library import (
|
||||
ESPHOME_DATA_KEY,
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY,
|
||||
ConvertedLibrary as IDFComponent,
|
||||
GitSource,
|
||||
URLSource,
|
||||
@@ -292,6 +294,25 @@ def test_generate_cmakelists_txt_multi_token_flag(tmp_component):
|
||||
assert ' "-include"\n "cp_custom_alloc.h"\n' in content
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_extra_script_link_flags(tmp_component):
|
||||
"""Captured extra-script LINKFLAGS come out as target_link_options, not
|
||||
compile options where they would be silently ineffective."""
|
||||
src_dir = tmp_component.path / "src"
|
||||
src_dir.mkdir()
|
||||
(src_dir / "main.c").write_text("int main() {}")
|
||||
|
||||
tmp_component.data = {
|
||||
ESPHOME_DATA_KEY: {ESPHOME_DATA_LINK_FLAGS_KEY: ["-Wl,--gc-sections"]}
|
||||
}
|
||||
|
||||
content = generate_cmakelists_txt(tmp_component)
|
||||
assert (
|
||||
'target_link_options(${COMPONENT_LIB} INTERFACE\n "-Wl,--gc-sections"\n)'
|
||||
in content
|
||||
)
|
||||
assert "target_compile_options" not in content
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_space_separated_classified_flags(tmp_component):
|
||||
# Space-separated -I/-L/-l entries routed to INCLUDE_DIRS and the link
|
||||
# handling before the shlex split was added; splitting must not leak
|
||||
|
||||
@@ -18,6 +18,8 @@ from esphome.platformio.extra_script import (
|
||||
run_extra_script,
|
||||
)
|
||||
from esphome.platformio.library import (
|
||||
ESPHOME_DATA_KEY,
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY,
|
||||
ConvertedLibrary as IDFComponent,
|
||||
URLSource,
|
||||
lex_build_flags,
|
||||
@@ -62,7 +64,8 @@ def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
|
||||
assert "-lalgobsec" in tokens
|
||||
assert "-DFOO" in tokens
|
||||
assert "-DBAR=1" in tokens
|
||||
assert "-Wl,--gc-sections" in tokens
|
||||
# LINKFLAGS travel via the link-flags channel, never the compile flags
|
||||
assert "-Wl,--gc-sections" not in tokens
|
||||
|
||||
|
||||
def test_extra_script_libpath_relative_resolves_against_library_dir(
|
||||
@@ -194,9 +197,11 @@ def test_captured_nonstring_buckets_warn_and_skip(tmp_path, caplog) -> None:
|
||||
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
|
||||
|
||||
flags = c.data["build"]["flags"]
|
||||
assert "-lm" in flags and "-Wl,-x" in flags and "-Os" in flags
|
||||
assert "-lm" in flags and "-Os" in flags
|
||||
assert c.data[ESPHOME_DATA_KEY][ESPHOME_DATA_LINK_FLAGS_KEY] == ["-Wl,-x"]
|
||||
assert not any("42" in f or "no" in f or "3.5" in f for f in flags)
|
||||
assert "Ignoring unsupported LIBS entry 42" in caplog.text
|
||||
assert "Ignoring unsupported LINKFLAGS entry {'no': 1}" in caplog.text
|
||||
assert "Ignoring unsupported LIBPATH entry 7" in caplog.text
|
||||
|
||||
|
||||
@@ -443,6 +448,18 @@ def test_append_variants_capture_like_append(method: str) -> None:
|
||||
assert env.result.libpath == ["lib"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ("Prepend", "PrependUnique"))
|
||||
def test_prepend_inserts_ahead_of_existing(method: str) -> None:
|
||||
"""Prepend keeps SCons order: new values land ahead of what is already
|
||||
captured (scripts prepend LIBS for static-link symbol resolution)."""
|
||||
env = _FakeSConsEnv(
|
||||
board_mcu="esp8266", pio_env="esphome_esp8266", pio_platform="espressif8266"
|
||||
)
|
||||
env.Append(LIBS=["m"])
|
||||
getattr(env, method)(LIBS=["algobsec", "bsec"])
|
||||
assert env.result.libs == ["algobsec", "bsec", "m"]
|
||||
|
||||
|
||||
def test_env_get_unknown_key_warns_once(caplog) -> None:
|
||||
"""A script branching on an unmodelled env var is diagnosable."""
|
||||
env = _FakeSConsEnv(
|
||||
@@ -455,10 +472,9 @@ def test_env_get_unknown_key_warns_once(caplog) -> None:
|
||||
assert "BOARD_MCU" not in caplog.text
|
||||
|
||||
|
||||
def test_spaced_linkflag_survives_relexing(tmp_path) -> None:
|
||||
def test_spaced_cppflag_survives_relexing(tmp_path) -> None:
|
||||
"""A captured argv token with a space stays one token after lexing."""
|
||||
result = ExtraScriptResult(
|
||||
linkflags=["-Wl,-T my linker.ld"],
|
||||
cppflags=["-include my hdr.h"],
|
||||
cppdefines=[("MSG", '"hello world"'), "PLAIN"],
|
||||
)
|
||||
@@ -466,7 +482,6 @@ def test_spaced_linkflag_survives_relexing(tmp_path) -> None:
|
||||
assert lex_build_flags(flags, "test") == [
|
||||
'-DMSG="hello world"',
|
||||
"-DPLAIN",
|
||||
"-Wl,-T my linker.ld",
|
||||
"-include my hdr.h",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user