mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[platformio] Skip ccache when the binary on PATH fails to run (#18407)
This commit is contained in:
@@ -5,6 +5,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
@@ -234,6 +235,35 @@ def _check_platformio_python_stamp(config: "ProjectConfig") -> None:
|
|||||||
_write_pio_stamp_python(stamp_file, current)
|
_write_pio_stamp_python(stamp_file, current)
|
||||||
|
|
||||||
|
|
||||||
|
def _ccache_usable() -> bool:
|
||||||
|
"""Return True when the ``ccache`` on PATH actually runs.
|
||||||
|
|
||||||
|
``shutil.which`` proves existence, not runnability: on Windows it also
|
||||||
|
matches ``.bat``/``.cmd`` wrappers and stale package-manager shims whose
|
||||||
|
target is gone. Wrapping compiles around such a find fails every compile
|
||||||
|
step with an opaque OS error, so probe once and fall back to compiling
|
||||||
|
without ccache when the probe fails.
|
||||||
|
"""
|
||||||
|
ccache = shutil.which("ccache")
|
||||||
|
if ccache is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[ccache, "--version"],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring ccache at %s because it failed to run; compiling without ccache",
|
||||||
|
ccache,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _ccache_env() -> dict[str, str]:
|
def _ccache_env() -> dict[str, str]:
|
||||||
"""Return ccache settings for PlatformIO builds.
|
"""Return ccache settings for PlatformIO builds.
|
||||||
|
|
||||||
@@ -266,7 +296,7 @@ def _ccache_env() -> dict[str, str]:
|
|||||||
if "ESPHOME_CCACHE_ENABLE" in os.environ:
|
if "ESPHOME_CCACHE_ENABLE" in os.environ:
|
||||||
enabled = get_bool_env("ESPHOME_CCACHE_ENABLE")
|
enabled = get_bool_env("ESPHOME_CCACHE_ENABLE")
|
||||||
else:
|
else:
|
||||||
enabled = shutil.which("ccache") is not None
|
enabled = _ccache_usable()
|
||||||
env = {"ESPHOME_CCACHE_ENABLE": "1" if enabled else "0"}
|
env = {"ESPHOME_CCACHE_ENABLE": "1" if enabled else "0"}
|
||||||
if not enabled:
|
if not enabled:
|
||||||
return env
|
return env
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -431,6 +432,7 @@ def test_ccache_env_enabled_by_default(setup_core: Path) -> None:
|
|||||||
with (
|
with (
|
||||||
patch.dict(os.environ, {}, clear=True),
|
patch.dict(os.environ, {}, clear=True),
|
||||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||||
|
patch.object(toolchain.subprocess, "run"),
|
||||||
):
|
):
|
||||||
env = toolchain._ccache_env()
|
env = toolchain._ccache_env()
|
||||||
|
|
||||||
@@ -457,6 +459,44 @@ def test_ccache_env_disabled_without_binary(setup_core: Path) -> None:
|
|||||||
assert env == {"ESPHOME_CCACHE_ENABLE": "0"}
|
assert env == {"ESPHOME_CCACHE_ENABLE": "0"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"probe_error",
|
||||||
|
[
|
||||||
|
pytest.param(OSError("not runnable"), id="oserror"),
|
||||||
|
pytest.param(subprocess.CalledProcessError(1, "ccache"), id="nonzero-exit"),
|
||||||
|
pytest.param(subprocess.TimeoutExpired("ccache", 15), id="timeout"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_ccache_env_disabled_when_probe_fails(
|
||||||
|
setup_core: Path, probe_error: Exception
|
||||||
|
) -> None:
|
||||||
|
"""A ccache that resolves on PATH but fails to run stays disabled."""
|
||||||
|
CORE.build_path = setup_core / "build" / "test"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {}, clear=True),
|
||||||
|
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||||
|
patch.object(toolchain.subprocess, "run", side_effect=probe_error),
|
||||||
|
):
|
||||||
|
env = toolchain._ccache_env()
|
||||||
|
|
||||||
|
assert env == {"ESPHOME_CCACHE_ENABLE": "0"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_ccache_env_forced_on_skips_probe(setup_core: Path) -> None:
|
||||||
|
"""An explicit ESPHOME_CCACHE_ENABLE=1 does not probe the binary."""
|
||||||
|
CORE.build_path = setup_core / "build" / "test"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {"ESPHOME_CCACHE_ENABLE": "1"}, clear=True),
|
||||||
|
patch.object(toolchain.subprocess, "run") as mock_probe,
|
||||||
|
):
|
||||||
|
env = toolchain._ccache_env()
|
||||||
|
|
||||||
|
assert env["ESPHOME_CCACHE_ENABLE"] == "1"
|
||||||
|
mock_probe.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_ccache_env_opt_out(setup_core: Path) -> None:
|
def test_ccache_env_opt_out(setup_core: Path) -> None:
|
||||||
"""ESPHOME_CCACHE_ENABLE=0 disables ccache even with the binary present."""
|
"""ESPHOME_CCACHE_ENABLE=0 disables ccache even with the binary present."""
|
||||||
CORE.build_path = setup_core / "build" / "test"
|
CORE.build_path = setup_core / "build" / "test"
|
||||||
@@ -496,6 +536,7 @@ def test_ccache_env_respects_user_values_and_refreshes_basedir(
|
|||||||
with (
|
with (
|
||||||
patch.dict(os.environ, user_env, clear=True),
|
patch.dict(os.environ, user_env, clear=True),
|
||||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||||
|
patch.object(toolchain.subprocess, "run"),
|
||||||
):
|
):
|
||||||
env = toolchain._ccache_env()
|
env = toolchain._ccache_env()
|
||||||
|
|
||||||
@@ -514,6 +555,7 @@ def test_run_platformio_cli_passes_ccache_env_to_subprocess_only(
|
|||||||
with (
|
with (
|
||||||
patch.dict(os.environ, {}, clear=False),
|
patch.dict(os.environ, {}, clear=False),
|
||||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||||
|
patch.object(toolchain.subprocess, "run"),
|
||||||
):
|
):
|
||||||
os.environ.pop("ESPHOME_CCACHE_ENABLE", None)
|
os.environ.pop("ESPHOME_CCACHE_ENABLE", None)
|
||||||
mock_run_external_process.return_value = 0
|
mock_run_external_process.return_value = 0
|
||||||
@@ -533,6 +575,7 @@ def test_ccache_env_requires_build_path(setup_core: Path) -> None:
|
|||||||
with (
|
with (
|
||||||
patch.dict(os.environ, {}, clear=True),
|
patch.dict(os.environ, {}, clear=True),
|
||||||
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||||
|
patch.object(toolchain.subprocess, "run"),
|
||||||
pytest.raises(ValueError, match="CORE.build_path must be set"),
|
pytest.raises(ValueError, match="CORE.build_path must be set"),
|
||||||
):
|
):
|
||||||
toolchain._ccache_env()
|
toolchain._ccache_env()
|
||||||
@@ -544,7 +587,10 @@ def test_run_platformio_cli_merges_caller_env(
|
|||||||
"""A caller-supplied env is the base and gains the ccache settings."""
|
"""A caller-supplied env is the base and gains the ccache settings."""
|
||||||
CORE.build_path = str(setup_core / "build" / "test")
|
CORE.build_path = str(setup_core / "build" / "test")
|
||||||
|
|
||||||
with patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"):
|
with (
|
||||||
|
patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"),
|
||||||
|
patch.object(toolchain.subprocess, "run"),
|
||||||
|
):
|
||||||
mock_run_external_process.return_value = 0
|
mock_run_external_process.return_value = 0
|
||||||
toolchain.run_platformio_cli(
|
toolchain.run_platformio_cli(
|
||||||
"test", env={"CUSTOM_VAR": "1", "ESPHOME_CCACHE_ENABLE": "0"}
|
"test", env={"CUSTOM_VAR": "1", "ESPHOME_CCACHE_ENABLE": "0"}
|
||||||
|
|||||||
Reference in New Issue
Block a user