[core] Fix clean-all to handle custom build paths (#15146)

Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
Jonathan Swoboda
2026-03-24 19:48:17 -04:00
committed by GitHub
parent 13baf26050
commit 4ff85e2a1e
2 changed files with 151 additions and 4 deletions

View File

@@ -18,7 +18,6 @@ from esphome.core import CORE, EsphomeError
from esphome.helpers import (
copy_file_if_changed,
cpp_string_escape,
get_str_env,
is_ha_addon,
read_file,
rmtree,
@@ -441,18 +440,42 @@ def clean_build(clear_pio_cache: bool = True):
rmtree(cache_dir)
def _get_custom_build_dir(item: Path, data_dir: Path) -> Path | None:
"""Parse a YAML config to find a custom build directory."""
from esphome import yaml_util
try:
raw = yaml_util.load_yaml(item)
except (EsphomeError, OSError) as e:
_LOGGER.debug("Could not parse %s to find build_path: %s", item, e)
return None
if not isinstance(raw, dict):
return None
esphome_conf = raw.get("esphome", {})
if not isinstance(esphome_conf, dict):
return None
if build_path := esphome_conf.get("build_path"):
return data_dir / build_path
return None
def clean_all(configuration: list[str]):
data_dirs = []
for config in configuration:
item = Path(config)
if item.is_file() and item.suffix in (".yaml", ".yml"):
data_dirs.append(item.parent / ".esphome")
data_dir = item.parent / ".esphome"
data_dirs.append(data_dir)
if custom := _get_custom_build_dir(item, data_dir):
data_dirs.append(custom)
else:
data_dirs.append(item / ".esphome")
if is_ha_addon():
data_dirs.append(Path("/data"))
if "ESPHOME_DATA_DIR" in os.environ:
data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None)))
if env_data_dir := os.environ.get("ESPHOME_DATA_DIR"):
data_dirs.append(Path(env_data_dir))
if env_build_path := os.environ.get("ESPHOME_BUILD_PATH"):
data_dirs.append(Path(env_build_path))
# Clean build dir
for dir in data_dirs:

View File

@@ -866,6 +866,130 @@ def test_clean_all_with_yaml_file(
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_with_yaml_build_path(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all cleans absolute build_path specified in YAML config."""
config_dir = tmp_path / "config"
config_dir.mkdir()
# Create an absolute custom build path directory with contents
custom_build = tmp_path / "custom_build"
custom_build.mkdir()
(custom_build / "firmware.bin").write_text("x")
sub = custom_build / "subdir"
sub.mkdir()
(sub / "file.txt").write_text("x")
yaml_file = config_dir / "test.yaml"
# Absolute build_path: data_dir / absolute = absolute (Python Path behavior)
yaml_file.write_text(f"esphome:\n name: test\n build_path: {custom_build}\n")
# Also create the normal .esphome dir
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(yaml_file)])
# Both .esphome and custom build_path should be cleaned
assert build_dir.exists()
assert not (build_dir / "dummy.txt").exists()
assert custom_build.exists()
assert not (custom_build / "firmware.bin").exists()
assert not sub.exists()
@patch("esphome.writer.CORE")
def test_clean_all_with_yaml_parse_error(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all still cleans .esphome when YAML parse fails."""
config_dir = tmp_path / "config"
config_dir.mkdir()
yaml_file = config_dir / "test.yaml"
yaml_file.write_text("invalid: yaml: content: [")
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(yaml_file)])
# .esphome should still be cleaned despite YAML parse failure
assert build_dir.exists()
assert not (build_dir / "dummy.txt").exists()
@patch("esphome.writer.CORE")
def test_clean_all_with_env_build_path(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all cleans ESPHOME_BUILD_PATH directory."""
config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
# Create env build path directory
env_build = tmp_path / "env_build"
env_build.mkdir()
(env_build / "firmware.bin").write_text("x")
from esphome.writer import clean_all
with (
caplog.at_level("INFO"),
patch.dict(os.environ, {"ESPHOME_BUILD_PATH": str(env_build)}),
):
clean_all([str(config_dir)])
# Both should be cleaned
assert not (build_dir / "dummy.txt").exists()
assert env_build.exists()
assert not (env_build / "firmware.bin").exists()
@patch("esphome.writer.CORE")
def test_clean_all_ignores_empty_env_vars(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""Test clean_all ignores empty ESPHOME_BUILD_PATH/ESPHOME_DATA_DIR."""
config_dir = tmp_path / "config"
config_dir.mkdir()
# Create a file in cwd that must NOT be cleaned
marker = tmp_path / "important.txt"
marker.write_text("do not delete")
from esphome.writer import clean_all
with patch.dict(
os.environ,
{"ESPHOME_BUILD_PATH": "", "ESPHOME_DATA_DIR": ""},
):
clean_all([str(config_dir)])
# Empty env vars must not cause cwd to be cleaned
assert marker.exists()
@patch("esphome.writer.CORE")
def test_clean_all(
mock_core: MagicMock,