From 11875ec040d0caab60b13b4227366f8d4decc1a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 14:37:42 -0500 Subject: [PATCH] Dump the main.cpp config comment with sorted keys voluptuous fills schema defaults in set-iteration order, so the validated dict's key order changes with the process hash seed; the unsorted dump churned the comment block in main.cpp and relinked the firmware on every esphome run for any config using a defaults-heavy action (logger.log in a button's on_press was enough). Affects the PlatformIO path identically. --- esphome/__main__.py | 6 +++++- tests/unit_tests/test_main.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 25ff986b0b..49981de9d3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -762,7 +762,11 @@ def _wrap_to_code(name, comp, yaml_util): async def wrapped(conf): cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: - conf_str = yaml_util.dump(conf) + # sort_keys: voluptuous fills schema defaults in set-iteration + # order, so the validated dict's key order changes with the + # process hash seed; an unsorted dump would churn main.cpp and + # relink the firmware on every run + conf_str = yaml_util.dump(conf, sort_keys=True) conf_str = conf_str.replace("//", "") # remove tailing \ to avoid multi-line comment warning conf_str = conf_str.replace("\\\n", "\n") diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index d20832af6b..5eb48cb725 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -7207,3 +7207,29 @@ def test_compile_program_espidf_idedata_none_warns( ): assert compile_program(MagicMock(), {}) == 0 assert "No idedata was generated" in caplog.text + + +@pytest.mark.asyncio +async def test_wrap_to_code_comment_is_insertion_order_independent() -> None: + """The config comment dumps with sorted keys: voluptuous fills schema + defaults in set-iteration order, so an unsorted dump would churn + main.cpp and relink the firmware on every run.""" + from types import SimpleNamespace + + from esphome import yaml_util + from esphome.__main__ import _wrap_to_code + + comments: list[str] = [] + + async def to_code(conf): + pass + + comp = SimpleNamespace(to_code=to_code, config_schema=object()) + wrapped = _wrap_to_code("demo", comp, yaml_util) + with patch("esphome.codegen.add", side_effect=lambda st: comments.append(str(st))): + await wrapped({"beta": 1, "alpha": 2}) + first = list(comments) + comments.clear() + await wrapped({"alpha": 2, "beta": 1}) + assert first == comments + assert "alpha: 2" in comments[1]