Lex build flags once in write_project, warn on unflags that match nothing

The pre-lexed tokens feed both _flag_defines and _project_flags so a
malformed entry warns once per generation. An unflag that hits nothing
in the user flags or any framework set (a typo, or -DUSE_FOO against
-DUSE_FOO=1) is named instead of silently leaving the flag in force.
This commit is contained in:
J. Nick Koston
2026-08-21 13:33:11 -05:00
parent c0cd64cd76
commit 046c7e5eb0
2 changed files with 40 additions and 2 deletions
+15 -2
View File
@@ -591,7 +591,9 @@ def write_project(paths: InstalledPaths) -> bool:
mkdir_p(build_dir)
unflags = _unflag_tokens()
flag_defines = _flag_defines(unflags)
# Lexed once so a malformed entry warns once, not per consumer
build_tokens = _lexed_build_flags()
flag_defines = _flag_defines(unflags, build_tokens)
config = _resolve_build_config(flag_defines)
esp8266_data = CORE.data[KEY_ESP8266]
board = esp8266_data[KEY_BOARD]
@@ -652,7 +654,7 @@ def write_project(paths: InstalledPaths) -> bool:
project_link_flags,
project_lib_dirs,
project_libs,
) = _project_flags(unflags)
) = _project_flags(unflags, build_tokens)
defines = _defines_flags(
config, esp8266_data[KEY_FLASH_MODE], board, board_build["defines"]
)
@@ -681,6 +683,17 @@ def write_project(paths: InstalledPaths) -> bool:
# build_unflags applies to the framework flag sets too (compile and link),
# as under PlatformIO (a silently ignored ``build_unflags: -Os`` would
# diverge between the toolchains).
# Matching is whole-token, so an unflag that hits nothing in the user
# flags or any framework set (a typo, or -DUSE_FOO against -DUSE_FOO=1)
# must be visible: the user believes the flag is gone while it still
# drives the compile line and the knob selection
flag_universe = set(build_tokens)
for flags in (cflags, cxxflags, asflags, _LINKFLAGS):
flag_universe.update(flags)
if unmatched := sorted(unflags - flag_universe):
_LOGGER.warning(
"build_unflags entries matched no build flag: %s", ", ".join(unmatched)
)
cflags, cxxflags, asflags, link_flags = (
[f for f in flags if f not in unflags]
for flags in (cflags, cxxflags, asflags, _LINKFLAGS)
@@ -1074,3 +1074,28 @@ def test_generate_ld_scripts_surgery_failure_is_named(tmp_path: Path) -> None:
pytest.raises(EsphomeError, match="anchor not found"),
):
_run_generate_ld_scripts(paths)
def test_write_project_unmatched_unflag_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An unflag that removes nothing is named; a matching one is silent."""
paths = _make_framework(tmp_path)
_set_flags("-DUSE_FOO=1")
CORE.build_unflags = {"-DUSE_FOO", "-Os"}
content = _write_ninja(paths)
assert "matched no build flag: -DUSE_FOO" in caplog.text
assert "-Os" not in caplog.text.split("matched no build flag")[-1].splitlines()[0]
# The matching -Os unflag really removed the framework flag
cflags = next(line for line in content.splitlines() if line.startswith("cflags"))
assert " -Os " not in cflags
def test_write_project_lexes_build_flags_once(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A malformed build_flags entry warns once per generation."""
paths = _make_framework(tmp_path)
_set_flags("-DFOO=1 -l")
_write_ninja(paths)
assert caplog.text.count("Ignoring trailing '-l'") == 1