From 27fa18dcec29db3d0cd49df5dd49fcbf53095c0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Sep 2025 15:09:35 -0500 Subject: [PATCH] [core] Fix clean build files to properly clear PlatformIO cache (#10754) --- esphome/writer.py | 13 ++++ tests/unit_tests/test_writer.py | 103 +++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index b8fe44abdd..2a9c6a770d 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -315,6 +315,19 @@ def clean_build(): _LOGGER.info("Deleting %s", dependencies_lock) os.remove(dependencies_lock) + # Clean PlatformIO cache to resolve CMake compiler detection issues + # This helps when toolchain paths change or get corrupted + try: + from platformio.project.helpers import get_project_cache_dir + except ImportError: + # PlatformIO is not available, skip cache cleaning + pass + else: + cache_dir = get_project_cache_dir() + if cache_dir and cache_dir.strip() and os.path.isdir(cache_dir): + _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) + shutil.rmtree(cache_dir) + GITIGNORE_CONTENT = """# Gitignore settings for ESPHome # This is an example and may include too much for your use-case. diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index f1f86a322e..970e0fada6 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -349,6 +349,14 @@ def test_clean_build( dependencies_lock = tmp_path / "dependencies.lock" dependencies_lock.write_text("lock file") + # Create PlatformIO cache directory + platformio_cache_dir = tmp_path / ".platformio" / ".cache" + platformio_cache_dir.mkdir(parents=True) + (platformio_cache_dir / "downloads").mkdir() + (platformio_cache_dir / "http").mkdir() + (platformio_cache_dir / "tmp").mkdir() + (platformio_cache_dir / "downloads" / "package.tar.gz").write_text("package") + # Setup mocks mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir) mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir) @@ -358,21 +366,30 @@ def test_clean_build( assert pioenvs_dir.exists() assert piolibdeps_dir.exists() assert dependencies_lock.exists() + assert platformio_cache_dir.exists() - # Call the function - with caplog.at_level("INFO"): - clean_build() + # Mock PlatformIO's get_project_cache_dir + with patch( + "platformio.project.helpers.get_project_cache_dir" + ) as mock_get_cache_dir: + mock_get_cache_dir.return_value = str(platformio_cache_dir) + + # Call the function + with caplog.at_level("INFO"): + clean_build() # Verify all were removed assert not pioenvs_dir.exists() assert not piolibdeps_dir.exists() assert not dependencies_lock.exists() + assert not platformio_cache_dir.exists() # Verify logging assert "Deleting" in caplog.text assert ".pioenvs" in caplog.text assert ".piolibdeps" in caplog.text assert "dependencies.lock" in caplog.text + assert "PlatformIO cache" in caplog.text @patch("esphome.writer.CORE") @@ -446,6 +463,86 @@ def test_clean_build_nothing_exists( assert not dependencies_lock.exists() +@patch("esphome.writer.CORE") +def test_clean_build_platformio_not_available( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_build when PlatformIO is not available.""" + # Create directory structure and files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + + piolibdeps_dir = tmp_path / ".piolibdeps" + piolibdeps_dir.mkdir() + + dependencies_lock = tmp_path / "dependencies.lock" + dependencies_lock.write_text("lock file") + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir) + mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir) + mock_core.relative_build_path.return_value = str(dependencies_lock) + + # Verify all exist before + assert pioenvs_dir.exists() + assert piolibdeps_dir.exists() + assert dependencies_lock.exists() + + # Mock import error for platformio + with ( + patch.dict("sys.modules", {"platformio.project.helpers": None}), + caplog.at_level("INFO"), + ): + # Call the function + clean_build() + + # Verify standard paths were removed but no cache cleaning attempted + assert not pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + + # Verify no cache logging + assert "PlatformIO cache" not in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_build_empty_cache_dir( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_build when get_project_cache_dir returns empty/whitespace.""" + # Create directory structure and files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir) + mock_core.relative_piolibdeps_path.return_value = str(tmp_path / ".piolibdeps") + mock_core.relative_build_path.return_value = str(tmp_path / "dependencies.lock") + + # Verify pioenvs exists before + assert pioenvs_dir.exists() + + # Mock PlatformIO's get_project_cache_dir to return whitespace + with patch( + "platformio.project.helpers.get_project_cache_dir" + ) as mock_get_cache_dir: + mock_get_cache_dir.return_value = " " # Whitespace only + + # Call the function + with caplog.at_level("INFO"): + clean_build() + + # Verify pioenvs was removed + assert not pioenvs_dir.exists() + + # Verify no cache cleaning was attempted due to empty string + assert "PlatformIO cache" not in caplog.text + + @patch("esphome.writer.CORE") def test_write_gitignore_creates_new_file( mock_core: MagicMock,