diff --git a/esphome/writer.py b/esphome/writer.py index cf04e4f8d2a..72c2c355dcc 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -490,6 +490,14 @@ def clean_build(clear_pio_cache: bool = True): if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) dependencies_lock.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. + for name in ("build", "managed_components"): + idf_path = CORE.relative_build_path(name) + if idf_path.is_dir(): + _LOGGER.info("Deleting %s", idf_path) + rmtree(idf_path) if not clear_pio_cache: return diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index e76769e6a83..91b4bd8e87b 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -443,6 +443,14 @@ def test_clean_build( dependencies_lock = tmp_path / "dependencies.lock" dependencies_lock.write_text("lock file") + # Native ESP-IDF toolchain artifacts. + idf_build_dir = tmp_path / "build" + idf_build_dir.mkdir() + (idf_build_dir / "CMakeCache.txt").write_text("cache") + managed_components_dir = tmp_path / "managed_components" + managed_components_dir.mkdir() + (managed_components_dir / "espressif__arduino-esp32").mkdir() + # Create PlatformIO cache directory platformio_cache_dir = tmp_path / ".platformio" / ".cache" platformio_cache_dir.mkdir(parents=True) @@ -454,12 +462,14 @@ def test_clean_build( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify all exist before assert pioenvs_dir.exists() assert piolibdeps_dir.exists() assert dependencies_lock.exists() + assert idf_build_dir.exists() + assert managed_components_dir.exists() assert platformio_cache_dir.exists() # Mock PlatformIO's ProjectConfig cache_dir @@ -482,6 +492,8 @@ def test_clean_build( assert not pioenvs_dir.exists() assert not piolibdeps_dir.exists() assert not dependencies_lock.exists() + assert not idf_build_dir.exists() + assert not managed_components_dir.exists() assert not platformio_cache_dir.exists() # Verify logging @@ -489,6 +501,8 @@ def test_clean_build( assert ".pioenvs" in caplog.text assert ".piolibdeps" in caplog.text assert "dependencies.lock" 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 @@ -510,7 +524,7 @@ def test_clean_build_partial_exists( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify only pioenvs exists assert pioenvs_dir.exists() @@ -547,7 +561,7 @@ def test_clean_build_nothing_exists( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify nothing exists assert not pioenvs_dir.exists() @@ -583,7 +597,7 @@ def test_clean_build_platformio_not_available( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify all exist before assert pioenvs_dir.exists() @@ -621,7 +635,7 @@ def test_clean_build_empty_cache_dir( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" - mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify pioenvs exists before assert pioenvs_dir.exists() @@ -1349,7 +1363,7 @@ def test_clean_build_handles_readonly_files( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" - mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify file is read-only assert not os.access(readonly_file, os.W_OK) @@ -1413,7 +1427,7 @@ def test_clean_build_reraises_for_other_errors( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" - mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name try: # Mock os.access in writer module to return True (writable)