diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 77a6364c15..97b4c5ccb5 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -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) diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index da803bd7bb..b54c023864 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -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