mirror of
https://github.com/esphome/esphome.git
synced 2025-09-26 07:02:21 +01:00
[core] Add a clean-platform option (#10831)
This commit is contained in:
@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
return clean_mqtt(config, args)
|
return clean_mqtt(config, args)
|
||||||
|
|
||||||
|
|
||||||
|
def command_clean_platform(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
|
try:
|
||||||
|
writer.clean_platform()
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Error deleting platform files: %s", err)
|
||||||
|
return 1
|
||||||
|
_LOGGER.info("Done!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
from esphome import mqtt
|
from esphome import mqtt
|
||||||
|
|
||||||
@@ -929,9 +939,10 @@ POST_CONFIG_ACTIONS = {
|
|||||||
"upload": command_upload,
|
"upload": command_upload,
|
||||||
"logs": command_logs,
|
"logs": command_logs,
|
||||||
"run": command_run,
|
"run": command_run,
|
||||||
"clean-mqtt": command_clean_mqtt,
|
|
||||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
|
||||||
"clean": command_clean,
|
"clean": command_clean,
|
||||||
|
"clean-mqtt": command_clean_mqtt,
|
||||||
|
"clean-platform": command_clean_platform,
|
||||||
|
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||||
"idedata": command_idedata,
|
"idedata": command_idedata,
|
||||||
"rename": command_rename,
|
"rename": command_rename,
|
||||||
"discover": command_discover,
|
"discover": command_discover,
|
||||||
@@ -940,6 +951,7 @@ POST_CONFIG_ACTIONS = {
|
|||||||
SIMPLE_CONFIG_ACTIONS = [
|
SIMPLE_CONFIG_ACTIONS = [
|
||||||
"clean",
|
"clean",
|
||||||
"clean-mqtt",
|
"clean-mqtt",
|
||||||
|
"clean-platform",
|
||||||
"config",
|
"config",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1144,6 +1156,13 @@ def parse_args(argv):
|
|||||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser_clean = subparsers.add_parser(
|
||||||
|
"clean-platform", help="Delete all platform files."
|
||||||
|
)
|
||||||
|
parser_clean.add_argument(
|
||||||
|
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||||
|
)
|
||||||
|
|
||||||
parser_dashboard = subparsers.add_parser(
|
parser_dashboard = subparsers.add_parser(
|
||||||
"dashboard", help="Create a simple web server for a dashboard."
|
"dashboard", help="Create a simple web server for a dashboard."
|
||||||
)
|
)
|
||||||
|
@@ -479,6 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
|
|||||||
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
|
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
|
||||||
|
|
||||||
|
|
||||||
|
class EsphomeCleanPlatformHandler(EsphomeCommandWebSocket):
|
||||||
|
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||||
|
config_file = settings.rel_path(json_message["configuration"])
|
||||||
|
return [*DASHBOARD_COMMAND, "clean-platform", config_file]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeCleanHandler(EsphomeCommandWebSocket):
|
class EsphomeCleanHandler(EsphomeCommandWebSocket):
|
||||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||||
config_file = settings.rel_path(json_message["configuration"])
|
config_file = settings.rel_path(json_message["configuration"])
|
||||||
@@ -1313,6 +1319,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
|
|||||||
(f"{rel}compile", EsphomeCompileHandler),
|
(f"{rel}compile", EsphomeCompileHandler),
|
||||||
(f"{rel}validate", EsphomeValidateHandler),
|
(f"{rel}validate", EsphomeValidateHandler),
|
||||||
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
||||||
|
(f"{rel}clean-platform", EsphomeCleanPlatformHandler),
|
||||||
(f"{rel}clean", EsphomeCleanHandler),
|
(f"{rel}clean", EsphomeCleanHandler),
|
||||||
(f"{rel}vscode", EsphomeVscodeHandler),
|
(f"{rel}vscode", EsphomeVscodeHandler),
|
||||||
(f"{rel}ace", EsphomeAceEditorHandler),
|
(f"{rel}ace", EsphomeAceEditorHandler),
|
||||||
|
@@ -323,17 +323,39 @@ def clean_build():
|
|||||||
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
||||||
# This helps when toolchain paths change or get corrupted
|
# This helps when toolchain paths change or get corrupted
|
||||||
try:
|
try:
|
||||||
from platformio.project.helpers import get_project_cache_dir
|
from platformio.project.config import ProjectConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# PlatformIO is not available, skip cache cleaning
|
# PlatformIO is not available, skip cache cleaning
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
cache_dir = get_project_cache_dir()
|
config = ProjectConfig.get_instance()
|
||||||
if cache_dir and cache_dir.strip():
|
cache_dir = Path(config.get("platformio", "cache_dir"))
|
||||||
cache_path = Path(cache_dir)
|
if cache_dir.is_dir():
|
||||||
if cache_path.is_dir():
|
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
||||||
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
shutil.rmtree(cache_dir)
|
||||||
shutil.rmtree(cache_dir)
|
|
||||||
|
|
||||||
|
def clean_platform():
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Clean entire build dir
|
||||||
|
if CORE.build_path.is_dir():
|
||||||
|
_LOGGER.info("Deleting %s", CORE.build_path)
|
||||||
|
shutil.rmtree(CORE.build_path)
|
||||||
|
|
||||||
|
# Clean PlatformIO project files
|
||||||
|
try:
|
||||||
|
from platformio.project.config import ProjectConfig
|
||||||
|
except ImportError:
|
||||||
|
# PlatformIO is not available, skip cleaning
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
config = ProjectConfig.get_instance()
|
||||||
|
for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]:
|
||||||
|
path = Path(config.get("platformio", pio_dir))
|
||||||
|
if path.is_dir():
|
||||||
|
_LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path)
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
|
||||||
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
||||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -16,6 +17,7 @@ from esphome import platformio_api
|
|||||||
from esphome.__main__ import (
|
from esphome.__main__ import (
|
||||||
Purpose,
|
Purpose,
|
||||||
choose_upload_log_host,
|
choose_upload_log_host,
|
||||||
|
command_clean_platform,
|
||||||
command_rename,
|
command_rename,
|
||||||
command_update_all,
|
command_update_all,
|
||||||
command_wizard,
|
command_wizard,
|
||||||
@@ -1853,3 +1855,101 @@ esp32:
|
|||||||
# Should not have any Python error messages
|
# Should not have any Python error messages
|
||||||
assert "TypeError" not in clean_output
|
assert "TypeError" not in clean_output
|
||||||
assert "can only concatenate str" not in clean_output
|
assert "can only concatenate str" not in clean_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_success(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_clean_platform when writer.clean_platform() succeeds."""
|
||||||
|
args = MockArgs()
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Set logger level to capture INFO messages
|
||||||
|
with (
|
||||||
|
caplog.at_level(logging.INFO),
|
||||||
|
patch("esphome.writer.clean_platform") as mock_clean_platform,
|
||||||
|
):
|
||||||
|
result = command_clean_platform(args, config)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
mock_clean_platform.assert_called_once()
|
||||||
|
|
||||||
|
# Check that success message was logged
|
||||||
|
assert "Done!" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_oserror(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_clean_platform when writer.clean_platform() raises OSError."""
|
||||||
|
args = MockArgs()
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Create a mock OSError with a specific message
|
||||||
|
mock_error = OSError("Permission denied: cannot delete directory")
|
||||||
|
|
||||||
|
# Set logger level to capture ERROR and INFO messages
|
||||||
|
with (
|
||||||
|
caplog.at_level(logging.INFO),
|
||||||
|
patch(
|
||||||
|
"esphome.writer.clean_platform", side_effect=mock_error
|
||||||
|
) as mock_clean_platform,
|
||||||
|
):
|
||||||
|
result = command_clean_platform(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
mock_clean_platform.assert_called_once()
|
||||||
|
|
||||||
|
# Check that error message was logged
|
||||||
|
assert (
|
||||||
|
"Error deleting platform files: Permission denied: cannot delete directory"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
# Should not have success message
|
||||||
|
assert "Done!" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_oserror_no_message(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_clean_platform when writer.clean_platform() raises OSError without message."""
|
||||||
|
args = MockArgs()
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Create a mock OSError without a message
|
||||||
|
mock_error = OSError()
|
||||||
|
|
||||||
|
# Set logger level to capture ERROR and INFO messages
|
||||||
|
with (
|
||||||
|
caplog.at_level(logging.INFO),
|
||||||
|
patch(
|
||||||
|
"esphome.writer.clean_platform", side_effect=mock_error
|
||||||
|
) as mock_clean_platform,
|
||||||
|
):
|
||||||
|
result = command_clean_platform(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
mock_clean_platform.assert_called_once()
|
||||||
|
|
||||||
|
# Check that error message was logged (should show empty string for OSError without message)
|
||||||
|
assert "Error deleting platform files:" in caplog.text
|
||||||
|
# Should not have success message
|
||||||
|
assert "Done!" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_clean_platform_args_and_config_ignored() -> None:
|
||||||
|
"""Test that command_clean_platform ignores args and config parameters."""
|
||||||
|
# Test with various args and config to ensure they don't affect the function
|
||||||
|
args1 = MockArgs(name="test1", file="test.bin")
|
||||||
|
config1 = {"wifi": {"ssid": "test"}}
|
||||||
|
|
||||||
|
args2 = MockArgs(name="test2", dashboard=True)
|
||||||
|
config2 = {"api": {}, "ota": {}}
|
||||||
|
|
||||||
|
with patch("esphome.writer.clean_platform") as mock_clean_platform:
|
||||||
|
result1 = command_clean_platform(args1, config1)
|
||||||
|
result2 = command_clean_platform(args2, config2)
|
||||||
|
|
||||||
|
assert result1 == 0
|
||||||
|
assert result2 == 0
|
||||||
|
assert mock_clean_platform.call_count == 2
|
||||||
|
@@ -362,11 +362,17 @@ def test_clean_build(
|
|||||||
assert dependencies_lock.exists()
|
assert dependencies_lock.exists()
|
||||||
assert platformio_cache_dir.exists()
|
assert platformio_cache_dir.exists()
|
||||||
|
|
||||||
# Mock PlatformIO's get_project_cache_dir
|
# Mock PlatformIO's ProjectConfig cache_dir
|
||||||
with patch(
|
with patch(
|
||||||
"platformio.project.helpers.get_project_cache_dir"
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
) as mock_get_cache_dir:
|
) as mock_get_instance:
|
||||||
mock_get_cache_dir.return_value = str(platformio_cache_dir)
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
mock_config.get.side_effect = (
|
||||||
|
lambda section, option: str(platformio_cache_dir)
|
||||||
|
if (section, option) == ("platformio", "cache_dir")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Call the function
|
# Call the function
|
||||||
with caplog.at_level("INFO"):
|
with caplog.at_level("INFO"):
|
||||||
@@ -486,7 +492,7 @@ def test_clean_build_platformio_not_available(
|
|||||||
|
|
||||||
# Mock import error for platformio
|
# Mock import error for platformio
|
||||||
with (
|
with (
|
||||||
patch.dict("sys.modules", {"platformio.project.helpers": None}),
|
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||||
caplog.at_level("INFO"),
|
caplog.at_level("INFO"),
|
||||||
):
|
):
|
||||||
# Call the function
|
# Call the function
|
||||||
@@ -520,11 +526,17 @@ def test_clean_build_empty_cache_dir(
|
|||||||
# Verify pioenvs exists before
|
# Verify pioenvs exists before
|
||||||
assert pioenvs_dir.exists()
|
assert pioenvs_dir.exists()
|
||||||
|
|
||||||
# Mock PlatformIO's get_project_cache_dir to return whitespace
|
# Mock PlatformIO's ProjectConfig cache_dir to return whitespace
|
||||||
with patch(
|
with patch(
|
||||||
"platformio.project.helpers.get_project_cache_dir"
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
) as mock_get_cache_dir:
|
) as mock_get_instance:
|
||||||
mock_get_cache_dir.return_value = " " # Whitespace only
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
mock_config.get.side_effect = (
|
||||||
|
lambda section, option: " " # Whitespace only
|
||||||
|
if (section, option) == ("platformio", "cache_dir")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Call the function
|
# Call the function
|
||||||
with caplog.at_level("INFO"):
|
with caplog.at_level("INFO"):
|
||||||
@@ -723,3 +735,126 @@ def test_write_cpp_with_duplicate_markers(
|
|||||||
# Call should raise an error
|
# Call should raise an error
|
||||||
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
||||||
write_cpp("// New code")
|
write_cpp("// New code")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("esphome.writer.CORE")
|
||||||
|
def test_clean_platform(
|
||||||
|
mock_core: MagicMock,
|
||||||
|
tmp_path: Path,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean_platform removes build and PlatformIO dirs."""
|
||||||
|
# Create build directory
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
(build_dir / "dummy.txt").write_text("x")
|
||||||
|
|
||||||
|
# Create PlatformIO directories
|
||||||
|
pio_cache = tmp_path / "pio_cache"
|
||||||
|
pio_packages = tmp_path / "pio_packages"
|
||||||
|
pio_platforms = tmp_path / "pio_platforms"
|
||||||
|
pio_core = tmp_path / "pio_core"
|
||||||
|
for d in (pio_cache, pio_packages, pio_platforms, pio_core):
|
||||||
|
d.mkdir()
|
||||||
|
(d / "keep").write_text("x")
|
||||||
|
|
||||||
|
# Setup CORE
|
||||||
|
mock_core.build_path = build_dir
|
||||||
|
|
||||||
|
# Mock ProjectConfig
|
||||||
|
with patch(
|
||||||
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
|
) as mock_get_instance:
|
||||||
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
|
||||||
|
def cfg_get(section: str, option: str) -> str:
|
||||||
|
mapping = {
|
||||||
|
("platformio", "cache_dir"): str(pio_cache),
|
||||||
|
("platformio", "packages_dir"): str(pio_packages),
|
||||||
|
("platformio", "platforms_dir"): str(pio_platforms),
|
||||||
|
("platformio", "core_dir"): str(pio_core),
|
||||||
|
}
|
||||||
|
return mapping.get((section, option), "")
|
||||||
|
|
||||||
|
mock_config.get.side_effect = cfg_get
|
||||||
|
|
||||||
|
# Call
|
||||||
|
from esphome.writer import clean_platform
|
||||||
|
|
||||||
|
with caplog.at_level("INFO"):
|
||||||
|
clean_platform()
|
||||||
|
|
||||||
|
# Verify deletions
|
||||||
|
assert not build_dir.exists()
|
||||||
|
assert not pio_cache.exists()
|
||||||
|
assert not pio_packages.exists()
|
||||||
|
assert not pio_platforms.exists()
|
||||||
|
assert not pio_core.exists()
|
||||||
|
|
||||||
|
# Verify logging mentions each
|
||||||
|
assert "Deleting" in caplog.text
|
||||||
|
assert str(build_dir) in caplog.text
|
||||||
|
assert "PlatformIO cache" in caplog.text
|
||||||
|
assert "PlatformIO packages" in caplog.text
|
||||||
|
assert "PlatformIO platforms" in caplog.text
|
||||||
|
assert "PlatformIO core" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@patch("esphome.writer.CORE")
|
||||||
|
def test_clean_platform_platformio_not_available(
|
||||||
|
mock_core: MagicMock,
|
||||||
|
tmp_path: Path,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean_platform when PlatformIO is not available."""
|
||||||
|
# Build dir
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
mock_core.build_path = build_dir
|
||||||
|
|
||||||
|
# PlatformIO dirs that should remain untouched
|
||||||
|
pio_cache = tmp_path / "pio_cache"
|
||||||
|
pio_cache.mkdir()
|
||||||
|
|
||||||
|
from esphome.writer import clean_platform
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||||
|
caplog.at_level("INFO"),
|
||||||
|
):
|
||||||
|
clean_platform()
|
||||||
|
|
||||||
|
# Build dir removed, PlatformIO dirs remain
|
||||||
|
assert not build_dir.exists()
|
||||||
|
assert pio_cache.exists()
|
||||||
|
|
||||||
|
# No PlatformIO-specific logs
|
||||||
|
assert "PlatformIO" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@patch("esphome.writer.CORE")
|
||||||
|
def test_clean_platform_partial_exists(
|
||||||
|
mock_core: MagicMock,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean_platform when only build dir exists."""
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
mock_core.build_path = build_dir
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"platformio.project.config.ProjectConfig.get_instance"
|
||||||
|
) as mock_get_instance:
|
||||||
|
mock_config = MagicMock()
|
||||||
|
mock_get_instance.return_value = mock_config
|
||||||
|
# Return non-existent dirs
|
||||||
|
mock_config.get.side_effect = lambda *_args, **_kw: str(
|
||||||
|
tmp_path / "does_not_exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
from esphome.writer import clean_platform
|
||||||
|
|
||||||
|
clean_platform()
|
||||||
|
|
||||||
|
assert not build_dir.exists()
|
||||||
|
Reference in New Issue
Block a user