diff --git a/esphome/__main__.py b/esphome/__main__.py index c1e05d2ea7..90737fce4b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -855,7 +855,11 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: toolchain.create_factory_bin() toolchain.create_ota_bin() toolchain.create_elf_copy() - toolchain.get_idedata() + try: + toolchain.get_idedata() + except EsphomeError as err: + # The firmware already built; idedata is a bonus artifact here + _LOGGER.warning("Could not generate idedata: %s", err) else: from esphome.platformio import toolchain diff --git a/esphome/build_helpers/idedata.py b/esphome/build_helpers/idedata.py index e0b1a8efe4..31b62df528 100644 --- a/esphome/build_helpers/idedata.py +++ b/esphome/build_helpers/idedata.py @@ -297,17 +297,8 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d """ entries = json.loads(Path(compile_commands).read_text(encoding="utf-8")) - # ninja-generated compile DBs repeat one identical multi-KB command per - # source file; parse each distinct (directory, command) once. - parsed: dict[tuple[str, str], tuple[str, list[str], list[str], list[str]]] = {} - - def _parse(entry: dict) -> tuple[str, list[str], list[str], list[str]]: - key = (entry["directory"], entry["command"]) - if key not in parsed: - parsed[key] = parse_entry(entry, launcher) - return parsed[key] - - cxx_path, defines, _, cxx_flags = _parse(_pick_entry(entries)) + representative = _pick_entry(entries) + cxx_path, defines, rep_includes, cxx_flags = parse_entry(representative, launcher) if _is_launcher(cxx_path): # Checked before the toolchain probe (which would fail opaquely on # a launcher) so the unusable compile DB is named, and never @@ -317,11 +308,16 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d "compiler; the compile database is unusable" ) - build_includes: dict[str, None] = {} + # Seed with the representative's includes so it is not parsed twice + # (per-file -c/-o arguments make every command distinct, so memoizing + # whole commands would never hit) + build_includes: dict[str, None] = dict.fromkeys( + rep_includes if _is_esphome_src(representative["file"]) else () + ) for entry in entries: - if not _is_esphome_src(entry["file"]): + if entry is representative or not _is_esphome_src(entry["file"]): continue - for inc in _parse(entry)[2]: + for inc in parse_entry(entry, launcher)[2]: build_includes.setdefault(inc, None) return { diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index a40341e194..f81da9052b 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -7130,3 +7130,35 @@ def test_warn_source_tree_mismatch_falls_back_when_stat_fails( # Same tree, so the path comparison still finds them equal and stays silent assert not caplog.text + + +def test_compile_program_espidf_idedata_failure_does_not_fail_build( + caplog: pytest.LogCaptureFixture, +) -> None: + """A post-compile idedata error is a warning: the firmware already built.""" + from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + Toolchain, + ) + from esphome.core import CORE, EsphomeError + + CORE.toolchain = Toolchain.ESP_IDF + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: "esp32", + KEY_TARGET_FRAMEWORK: "esp-idf", + } + with ( + patch("esphome.espidf.toolchain.run_compile", return_value=0), + patch("esphome.espidf.toolchain.create_factory_bin"), + patch("esphome.espidf.toolchain.create_ota_bin"), + patch("esphome.espidf.toolchain.create_elf_copy"), + patch( + "esphome.espidf.toolchain.get_idedata", + side_effect=EsphomeError("compile database is unusable"), + ), + patch("esphome.__main__._check_and_emit_build_info"), + ): + assert compile_program(MagicMock(), {}) == 0 + assert "Could not generate idedata" in caplog.text diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 4088cac1f8..0acb0d69cb 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -564,3 +564,15 @@ def test_split_flag_entry_non_string_is_clean() -> None: split_flag_entry({"esp32": ["-DX"]}, "lib x") with pytest.raises(EsphomeError, match="Malformed build flag 5"): split_flag_entry(5, "lib x") + + +def test_source_kind_map_shape() -> None: + """The kind values the native compile rules key on, and the deliberate + AS/ASPP merge (.s and .S both map to asm).""" + from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX + + assert set(SOURCE_KIND_FOR_SUFFIX.values()) == {"c", "cxx", "asm"} + assert SOURCE_KIND_FOR_SUFFIX[".s"] == "asm" + assert SOURCE_KIND_FOR_SUFFIX[".S"] == "asm" + assert SOURCE_KIND_FOR_SUFFIX[".c"] == "c" + assert SOURCE_KIND_FOR_SUFFIX[".cpp"] == "cxx"