diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index ee5bfb1871..1f2fbb651f 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -36,6 +36,7 @@ from esphome.build_helpers.ninja import ( from esphome.build_helpers.pch import ( PCH_CORE_HEADER, PCH_HEADER_NAME, + mark_pch_emitted, pch_checksum, pch_enabled, pch_header_text, @@ -1284,6 +1285,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: ] lines.append(f"srccxxflags = {' '.join(cxx_parts)}") src_cxx_override = ("$srccxxflags", gch) + mark_pch_emitted() src_objs = _ninja_compile_edges( lines, _collect_sources(src_dir), diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index bf57a157e3..eb0e0a831c 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -8,6 +8,7 @@ it too; Arduino.h visibility there is intended (esphome#8693). from __future__ import annotations from collections.abc import Iterable +from dataclasses import dataclass import hashlib import json import logging @@ -25,6 +26,27 @@ from esphome.build_helpers.idedata import ( split_command, ) +_DOMAIN = "pch" + + +@dataclass +class _PCHData: + emitted: bool = False + + +def _pch_data() -> _PCHData: + from esphome.core import CORE + + if _DOMAIN not in CORE.data: + CORE.data[_DOMAIN] = _PCHData() + return CORE.data[_DOMAIN] + + +def mark_pch_emitted() -> None: + """Record that this build's consumers reference the pch.""" + _pch_data().emitted = True + + _LOGGER = logging.getLogger(__name__) # The header and its .gch/.sum sidecars live in the build directory. @@ -81,9 +103,10 @@ def pch_enabled() -> bool: def ccache_pch_env() -> dict[str, str]: """Settings ccache needs to cache compiles that consume the .gch; - empty when the pch is disabled. User-set values win. Native backends - export these process-wide; only time_macros affects non-pch TUs.""" - if not pch_enabled(): + empty unless this build actually emitted one. User-set values win. + Native backends export these process-wide; only time_macros affects + non-pch TUs.""" + if not (pch_enabled() and _pch_data().emitted): return {} env = {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ} user_sloppiness = os.environ.get("CCACHE_SLOPPINESS") diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 4009e91f01..dd0d9f5d40 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -386,6 +386,10 @@ def test_write_project_pch(tmp_path: Path) -> None: '#include "esphome/core/defines.h"', ] assert (build_dir / "esphome_pch.h.gch.sum").read_text().strip() + # Emission recorded so the framework env exports the ccache settings + from esphome.build_helpers.pch import ccache_pch_env + + assert "CCACHE_PCH_EXTSUM" in ccache_pch_env() def test_write_project_pch_sum_only_with_ccache(tmp_path: Path) -> None: @@ -456,6 +460,10 @@ def test_write_project_pch_skipped_when_user_force_include_precedes( assert "esphome_pch" not in content assert "srccxxflags" not in content assert "prevents the precompiled header" in caplog.text + # No pch emitted: the ccache relaxation must stay off + from esphome.build_helpers.pch import ccache_pch_env + + assert ccache_pch_env() == {} def test_write_project_pch_disabled( diff --git a/tests/unit_tests/build_helpers/test_pch.py b/tests/unit_tests/build_helpers/test_pch.py index e8ea0c18bb..bb6ee81a7f 100644 --- a/tests/unit_tests/build_helpers/test_pch.py +++ b/tests/unit_tests/build_helpers/test_pch.py @@ -33,7 +33,14 @@ def test_pch_enabled(value: str | None, expected: bool) -> None: assert pch.pch_enabled() is expected +def test_ccache_pch_env_empty_until_emitted() -> None: + """No sloppiness relaxation for a build that skipped the pch.""" + with patch.dict(os.environ, {}, clear=True): + assert pch.ccache_pch_env() == {} + + def test_ccache_pch_env_enabled() -> None: + pch.mark_pch_emitted() with patch.dict(os.environ, {}, clear=True): env = pch.ccache_pch_env() assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros" @@ -48,6 +55,7 @@ def test_ccache_pch_env_disabled() -> None: def test_ccache_pch_env_token_check_is_membership_not_substring( caplog: pytest.LogCaptureFixture, ) -> None: + pch.mark_pch_emitted() """A token merely containing ours must not suppress the union.""" with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "pch_defines_extra"}, clear=True): env = pch.ccache_pch_env() @@ -57,6 +65,7 @@ def test_ccache_pch_env_token_check_is_membership_not_substring( def test_ccache_pch_env_unions_user_sloppiness( caplog: pytest.LogCaptureFixture, ) -> None: + pch.mark_pch_emitted() """Without pch_defines/time_macros ccache declines every pch-consuming compile, so missing tokens are unioned onto the user's value.""" with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "locale"}, clear=True): diff --git a/tests/unit_tests/test_arduino8266_toolchain.py b/tests/unit_tests/test_arduino8266_toolchain.py index 7f41687909..a17dec4397 100644 --- a/tests/unit_tests/test_arduino8266_toolchain.py +++ b/tests/unit_tests/test_arduino8266_toolchain.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, patch import pytest from esphome.arduino8266 import framework, toolchain +from esphome.build_helpers.pch import mark_pch_emitted import esphome.config_validation as cv from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, @@ -632,6 +633,7 @@ def test_get_idedata_accepts_preresolved_ccache() -> None: def test_ccache_env_includes_pch_settings() -> None: + mark_pch_emitted() """The native build exports the ccache settings the pch needs.""" with patch.dict(os.environ, {}, clear=True): env = framework.ccache_env("/usr/bin/ccache") @@ -647,6 +649,7 @@ def test_ccache_env_pch_disabled() -> None: def test_ccache_env_respects_user_sloppiness() -> None: + mark_pch_emitted() with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "locale"}, clear=True): env = framework.ccache_env("/usr/bin/ccache") # The user's tokens survive; the ones the pch needs are unioned on