[core] Clean build when the toolchain changes (#16744)

This commit is contained in:
Jonathan Swoboda
2026-05-31 16:29:16 -04:00
committed by GitHub
parent 7865dc33bc
commit 48844a68ba
2 changed files with 37 additions and 4 deletions

View File

@@ -90,10 +90,12 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
"""Return True when the build tree must be wiped before reuse.
Predicate is True when *old* is missing (first build),
``src_version`` differs, ``build_path`` differs, or a previously
loaded integration was removed in *new*. Adding integrations or
changing unrelated fields (friendly name, esphome version, etc.)
does not trigger a clean.
``src_version`` differs, ``build_path`` differs, the build
``toolchain`` differs (e.g. switching between the PlatformIO and
native ESP-IDF toolchains, which produce incompatible build trees),
or a previously loaded integration was removed in *new*. Adding
integrations or changing unrelated fields (friendly name, esphome
version, etc.) does not trigger a clean.
Used by esphome-device-builder (esphome/device-builder) to gate
its remote-build artifact materialiser so a local → remote → local
@@ -109,6 +111,8 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
return True
if old.build_path != new.build_path:
return True
if old.toolchain != new.toolchain:
return True
# Check if any components have been removed
return bool(old.loaded_integrations - new.loaded_integrations)
@@ -505,6 +509,10 @@ def clean_build(clear_pio_cache: bool = True):
if dependencies_lock.is_file():
_LOGGER.info("Deleting %s", dependencies_lock)
dependencies_lock.unlink()
idedata_cache = CORE.relative_internal_path("idedata", f"{CORE.name}.json")
if idedata_cache.is_file():
_LOGGER.info("Deleting %s", idedata_cache)
idedata_cache.unlink()
# Native ESP-IDF toolchain artifacts: the IDF CMake/ninja build dir
# and the Component Manager's fetched managed components live under
# the project's build path, not under .pioenvs / .piolibdeps.

View File

@@ -111,6 +111,7 @@ def create_storage() -> Callable[..., StorageJSON]:
no_mdns=kwargs.get("no_mdns", False),
framework=kwargs.get("framework", "arduino"),
core_platform=kwargs.get("core_platform", "esp32"),
toolchain=kwargs.get("toolchain", "platformio"),
)
return _create
@@ -142,6 +143,20 @@ def test_storage_should_clean_when_build_path_changes(
assert storage_should_clean(old, new) is True
def test_storage_should_clean_when_toolchain_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is triggered when the build toolchain changes.
Switching between the PlatformIO and native ESP-IDF toolchains produces
incompatible build trees (and toolchain-specific idedata), so the build
must be wiped.
"""
old = create_storage(loaded_integrations=["api", "wifi"], toolchain="platformio")
new = create_storage(loaded_integrations=["api", "wifi"], toolchain="esp-idf")
assert storage_should_clean(old, new) is True
def test_storage_should_clean_when_component_removed(
create_storage: Callable[..., StorageJSON],
) -> None:
@@ -479,6 +494,11 @@ def test_clean_build(
dependencies_lock = tmp_path / "dependencies.lock"
dependencies_lock.write_text("lock file")
# idedata cache lives under the data dir, not the build path.
idedata_cache = tmp_path / "idedata" / "test.json"
idedata_cache.parent.mkdir()
idedata_cache.write_text("{}")
# Native ESP-IDF toolchain artifacts.
idf_build_dir = tmp_path / "build"
idf_build_dir.mkdir()
@@ -499,11 +519,14 @@ def test_clean_build(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.name = "test"
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
# Verify all exist before
assert pioenvs_dir.exists()
assert piolibdeps_dir.exists()
assert dependencies_lock.exists()
assert idedata_cache.exists()
assert idf_build_dir.exists()
assert managed_components_dir.exists()
assert platformio_cache_dir.exists()
@@ -528,6 +551,7 @@ def test_clean_build(
assert not pioenvs_dir.exists()
assert not piolibdeps_dir.exists()
assert not dependencies_lock.exists()
assert not idedata_cache.exists()
assert not idf_build_dir.exists()
assert not managed_components_dir.exists()
assert not platformio_cache_dir.exists()
@@ -537,6 +561,7 @@ def test_clean_build(
assert ".pioenvs" in caplog.text
assert ".piolibdeps" in caplog.text
assert "dependencies.lock" in caplog.text
assert str(idedata_cache) in caplog.text
assert str(idf_build_dir) in caplog.text
assert str(managed_components_dir) in caplog.text
assert "PlatformIO cache" in caplog.text