diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh index c88a78f97e..65d8fd61b5 100755 --- a/docker/docker_entrypoint.sh +++ b/docker/docker_entrypoint.sh @@ -21,10 +21,14 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" -# Keep the native toolchain installs on the persistent cache root, not the -# container's ephemeral user cache dir (re-downloaded on every restart). +# Keep the native toolchain installs and compiler caches on the persistent +# cache root, not the container's user cache dir: it is lost on every +# restart, and not writable when the container runs as a non-root user. export ESPHOME_ESP_IDF_PREFIX="$(dirname "${pio_cache_base}")/idf" export ESPHOME_SDK_NRF_PREFIX="$(dirname "${pio_cache_base}")/sdk-nrf" +export ESPHOME_ARDUINO8266_PREFIX="$(dirname "${pio_cache_base}")/arduino8266" +export ESPHOME_HOST_PREFIX="$(dirname "${pio_cache_base}")/host" +export ESPHOME_PLATFORMIO_CCACHE_DIR="$(dirname "${pio_cache_base}")/platformio-ccache" # If /build is mounted, use that as the build path # otherwise use path in /config (so that builds aren't lost on container restart) diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run index 20fada5f13..dd7f1bddc2 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run @@ -15,10 +15,14 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" -# Keep the native toolchain installs on the persistent /data volume, not the -# container's ephemeral user cache dir (wiped on every add-on update/restart). +# Keep the native toolchain installs and compiler caches on the persistent +# /data volume, not the container's ephemeral user cache dir (wiped on every +# add-on update/restart). export ESPHOME_ESP_IDF_PREFIX=/data/cache/idf export ESPHOME_SDK_NRF_PREFIX=/data/cache/sdk-nrf +export ESPHOME_ARDUINO8266_PREFIX=/data/cache/arduino8266 +export ESPHOME_HOST_PREFIX=/data/cache/host +export ESPHOME_PLATFORMIO_CCACHE_DIR=/data/cache/platformio-ccache if bashio::config.true 'leave_front_door_open'; then export DISABLE_HA_AUTHENTICATION=true diff --git a/esphome/build_helpers/tools_cache.py b/esphome/build_helpers/tools_cache.py index 2dbbb2ffc2..e3d38e7a98 100644 --- a/esphome/build_helpers/tools_cache.py +++ b/esphome/build_helpers/tools_cache.py @@ -35,9 +35,12 @@ SDK_NRF_TOOLS_CACHE = ("ESPHOME_SDK_NRF_PREFIX", "sdk-nrf") ARDUINO8266_TOOLS_CACHE = ("ESPHOME_ARDUINO8266_PREFIX", "arduino8266") # The host backend installs nothing; the entry only holds its ccache dir HOST_TOOLS_CACHE = ("ESPHOME_HOST_PREFIX", "host") +# PlatformIO installs into its own dirs; the entry is its ccache dir itself +PLATFORMIO_CCACHE = ("ESPHOME_PLATFORMIO_CCACHE_DIR", "platformio-ccache") TOOLS_CACHE_SPECS = ( IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE, HOST_TOOLS_CACHE, + PLATFORMIO_CCACHE, ) diff --git a/esphome/platformio/toolchain.py b/esphome/platformio/toolchain.py index 97b32420da..2322b37dfc 100644 --- a/esphome/platformio/toolchain.py +++ b/esphome/platformio/toolchain.py @@ -7,9 +7,8 @@ import re import sys from typing import TYPE_CHECKING, Any -import platformdirs - from esphome.build_helpers.ccache import resolve_ccache_path +from esphome.build_helpers.tools_cache import PLATFORMIO_CCACHE, tools_cache_path from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError from esphome.framework_helpers import strip_win_long_path_prefix @@ -235,8 +234,9 @@ def _ccache_env() -> dict[str, str]: these values would hand it the wrong cache dir and a stale basedir. This mirrors ``_ccache_env()`` in ``esphome/espidf/framework.py``. The - cache lives under the machine-global ESPHome cache dir, so it is shared - across all projects and removed by ``esphome clean-all``. Unlike the + cache lives under the machine-global ESPHome cache dir, or where + ``ESPHOME_PLATFORMIO_CCACHE_DIR`` points (the containers set it to their + writable cache mount), and is removed by ``esphome clean-all``. Unlike the ESP-IDF path, ``CCACHE_DEPEND`` is not set: SCons compiles don't emit the depfiles depend mode needs, so ccache's default preprocessor mode is used. @@ -264,10 +264,7 @@ def _ccache_env() -> dict[str, str]: ) env["CCACHE_BASEDIR"] = str(Path(CORE.build_path).resolve()) defaults = { - "CCACHE_DIR": str( - Path(platformdirs.user_cache_dir("esphome", appauthor=False)) - / "platformio-ccache" - ), + "CCACHE_DIR": str(tools_cache_path(*PLATFORMIO_CCACHE)), "CCACHE_NOHASHDIR": "true", } env.update({k: v for k, v in defaults.items() if k not in os.environ}) diff --git a/tests/unit_tests/test_platformio_toolchain.py b/tests/unit_tests/test_platformio_toolchain.py index fb99ea9208..6138d74524 100644 --- a/tests/unit_tests/test_platformio_toolchain.py +++ b/tests/unit_tests/test_platformio_toolchain.py @@ -447,6 +447,47 @@ def test_ccache_env_enabled_by_default(setup_core: Path) -> None: assert "ESPHOME_CCACHE_ENABLE" not in os.environ +def test_ccache_env_uses_cache_dir_override(setup_core: Path, tmp_path: Path) -> None: + """The containers point ccache at their writable cache mount.""" + CORE.build_path = setup_core / "build" / "test" + ccache_dir = tmp_path / "cache" / "platformio-ccache" + + with ( + patch.dict( + os.environ, + {"HOME": "/", "ESPHOME_PLATFORMIO_CCACHE_DIR": str(ccache_dir)}, + clear=True, + ), + patch("shutil.which", return_value="/usr/bin/ccache"), + patch("esphome.framework_helpers.subprocess.run"), + ): + env = toolchain._ccache_env() + + assert env["CCACHE_DIR"] == str(ccache_dir.resolve()) + + +def test_ccache_env_ignores_platformio_cache_dir( + setup_core: Path, tmp_path: Path +) -> None: + """PLATFORMIO_CACHE_DIR does not move ccache; only the override does.""" + CORE.build_path = setup_core / "build" / "test" + cache_root = tmp_path / "user-cache" + + with ( + patch.dict( + os.environ, + {"PLATFORMIO_CACHE_DIR": str(tmp_path / "platformio" / "cache")}, + clear=True, + ), + patch("platformdirs.user_cache_dir", return_value=str(cache_root)), + patch("shutil.which", return_value="/usr/bin/ccache"), + patch("esphome.framework_helpers.subprocess.run"), + ): + env = toolchain._ccache_env() + + assert env["CCACHE_DIR"] == str((cache_root / "platformio-ccache").resolve()) + + @pytest.mark.parametrize( ("env_vars", "expect_warning"), [ @@ -582,6 +623,7 @@ def test_ccache_env_respects_user_values_and_refreshes_basedir( user_env = { "CCACHE_DIR": "/custom/cache", "CCACHE_BASEDIR": "/stale/other-device", + "ESPHOME_PLATFORMIO_CCACHE_DIR": "/mounted/platformio-ccache", } CORE.build_path = setup_core / "build" / "test" diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 3dcc4b12b8..7fd03062a2 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1085,6 +1085,28 @@ def test_clean_all_removes_global_arduino8266_install( assert str(arduino8266_install.resolve()) in caplog.text +@patch("esphome.writer.CORE") +def test_clean_all_removes_platformio_ccache( + mock_core: MagicMock, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """clean_all removes the PlatformIO ccache dir the containers relocate.""" + ccache_dir = tmp_path / "platformio-ccache" + (ccache_dir / "0").mkdir(parents=True) + monkeypatch.setenv("ESPHOME_PLATFORMIO_CCACHE_DIR", str(ccache_dir)) + + config_dir = tmp_path / "config" + config_dir.mkdir() + + with caplog.at_level("INFO"): + clean_all([str(config_dir)]) + + assert not ccache_dir.exists() + assert str(ccache_dir.resolve()) in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all_removes_default_cache_root( mock_core: MagicMock,