1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00
This commit is contained in:
J. Nick Koston
2025-12-01 11:24:49 -06:00
parent a157b4431e
commit 8a0b412e1f
2 changed files with 28 additions and 5 deletions

View File

@@ -1,3 +1,4 @@
from collections.abc import Callable
import importlib
import logging
import os
@@ -5,6 +6,7 @@ from pathlib import Path
import re
import shutil
import stat
from types import TracebackType
from esphome import loader
from esphome.config import iter_component_configs, iter_components
@@ -303,18 +305,21 @@ def clean_cmake_cache():
pioenvs_cmake_path.unlink()
def _rmtree_error_handler(func, path, exc_info):
def _rmtree_error_handler(
func: Callable[[str], object],
path: str,
exc_info: tuple[type[BaseException], BaseException, TracebackType | None],
) -> None:
"""Error handler for shutil.rmtree to handle read-only files on Windows.
On Windows, git pack files and other files may be marked read-only,
causing shutil.rmtree to fail with "Access is denied". This handler
removes the read-only flag and retries the deletion.
"""
if not os.access(path, os.W_OK):
os.chmod(path, stat.S_IWUSR | stat.S_IRUSR)
func(path)
else:
if os.access(path, os.W_OK):
raise exc_info[1].with_traceback(exc_info[2])
os.chmod(path, stat.S_IWUSR | stat.S_IRUSR)
func(path)
def clean_build(clear_pio_cache: bool = True):

View File

@@ -15,6 +15,7 @@ from esphome.writer import (
CPP_INCLUDE_BEGIN,
CPP_INCLUDE_END,
GITIGNORE_CONTENT,
_rmtree_error_handler,
clean_build,
clean_cmake_cache,
storage_should_clean,
@@ -1137,3 +1138,20 @@ def test_clean_all_handles_readonly_files(
# Verify directory was removed despite read-only files
assert not subdir.exists()
assert build_dir.exists() # .esphome dir itself is preserved
def test_rmtree_error_handler_reraises_for_writable_files() -> None:
"""Test _rmtree_error_handler re-raises exception for writable files."""
# Create a mock exception
original_error = PermissionError("Some other permission error")
exc_info = (type(original_error), original_error, original_error.__traceback__)
# Patch os.access to return True (file is writable)
with (
patch("esphome.writer.os.access", return_value=True),
pytest.raises(PermissionError) as exc_info_caught,
):
_rmtree_error_handler(lambda p: None, "/some/path", exc_info)
# Verify the original exception was re-raised
assert exc_info_caught.value is original_error