mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:07:33 +00:00
[espidf] Warn when the install path is too long for Windows MAX_PATH (#16896)
This commit is contained in:
@@ -85,6 +85,75 @@ def _get_idf_tools_path() -> Path:
|
||||
return CORE.data_dir / "idf"
|
||||
|
||||
|
||||
# Windows' default MAX_PATH is 260 characters. ESP-IDF toolchains nest deeply
|
||||
# below the IDF tools directory: the longest file on disk (picolibc C++
|
||||
# headers) sits ~209 characters down, but the operative number is worse -- gcc
|
||||
# probes its multilib include dirs via un-normalized self-relative paths
|
||||
# ("bin/../lib/gcc/<target>/<ver>/../../../../<target>/include/..."), and
|
||||
# Windows checks the path string as given, before collapsing "..". Measured
|
||||
# worst case (riscv32, esp-15.2.0, longest multilib + no-rtti, probing
|
||||
# bits/c++config.h): ~243 characters below the tools directory. Exceeding the
|
||||
# limit surfaces as cryptic build failures -- missing headers ("fatal error:
|
||||
# bits/c++config.h: No such file or directory") or partial extraction
|
||||
# ("cannot execute 'as'"). Warn up front so the user can shorten the path or
|
||||
# enable long path support.
|
||||
_WINDOWS_MAX_PATH = 260
|
||||
# Measured 243 plus a small safety margin for future toolchain growth.
|
||||
_TOOLCHAIN_NESTED_PATH_LEN = 245
|
||||
|
||||
|
||||
def _windows_long_paths_enabled() -> bool:
|
||||
"""Return True if Windows long path support is enabled in the registry."""
|
||||
try:
|
||||
import winreg # pylint: disable=import-error # Windows-only module
|
||||
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
r"SYSTEM\CurrentControlSet\Control\FileSystem",
|
||||
) as key:
|
||||
value, _ = winreg.QueryValueEx(key, "LongPathsEnabled")
|
||||
return value == 1
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _check_windows_path_length() -> None:
|
||||
"""Warn when the install path is too long for Windows' MAX_PATH limit.
|
||||
|
||||
No-op off Windows or when long path support is enabled. Otherwise warns if
|
||||
the deepest toolchain file would exceed the 260-character limit, which makes
|
||||
ESP-IDF toolchains extract incompletely and fail to build.
|
||||
"""
|
||||
if platform.system() != "Windows" or _windows_long_paths_enabled():
|
||||
return
|
||||
tools_path = str(_get_idf_tools_path())
|
||||
projected = len(tools_path) + _TOOLCHAIN_NESTED_PATH_LEN
|
||||
if projected <= _WINDOWS_MAX_PATH:
|
||||
return
|
||||
_LOGGER.warning(
|
||||
"ESP-IDF tools path is too long for the default Windows path limit:\n"
|
||||
" %s (%d characters)\n"
|
||||
"ESP-IDF toolchain paths reach up to ~%d characters deeper (including the\n"
|
||||
"compiler's internal 'bin/../lib/...' relative paths), projecting to ~%d\n"
|
||||
"characters -- over the %d-character limit. This causes cryptic build\n"
|
||||
"failures such as:\n"
|
||||
" fatal error: bits/c++config.h: No such file or directory\n"
|
||||
" cannot execute 'as': CreateProcess: No such file or directory\n"
|
||||
"To fix, either:\n"
|
||||
" - Enable Windows long path support: set\n"
|
||||
" HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem\\LongPathsEnabled\n"
|
||||
" to 1 and reboot, or\n"
|
||||
" - Move your ESPHome project to a shorter path\n"
|
||||
"Then delete the ESP-IDF tools directory above so the toolchain "
|
||||
"reinstalls cleanly.",
|
||||
tools_path,
|
||||
len(tools_path),
|
||||
_TOOLCHAIN_NESTED_PATH_LEN,
|
||||
projected,
|
||||
_WINDOWS_MAX_PATH,
|
||||
)
|
||||
|
||||
|
||||
def _get_framework_path(version: str) -> Path:
|
||||
"""
|
||||
Get the path to the ESPHome ESP-IDF framework directory for a specific version.
|
||||
@@ -705,6 +774,8 @@ def check_esp_idf_install(
|
||||
Returns:
|
||||
tuple of (framework_path, python_env_path)
|
||||
"""
|
||||
_check_windows_path_length()
|
||||
|
||||
env = {}
|
||||
env["IDF_TOOLS_PATH"] = str(_get_idf_tools_path())
|
||||
env["IDF_PATH"] = ""
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
from contextlib import contextmanager
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tarfile
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
@@ -13,6 +16,7 @@ import pytest
|
||||
|
||||
from esphome.espidf.framework import (
|
||||
_check_stamp,
|
||||
_check_windows_path_length,
|
||||
_clone_idf_with_submodules,
|
||||
_get_framework_path,
|
||||
_get_idf_tool_paths,
|
||||
@@ -22,6 +26,7 @@ from esphome.espidf.framework import (
|
||||
_get_python_version,
|
||||
_parse_git_source,
|
||||
_patch_tools_json_for_linux_arm64,
|
||||
_windows_long_paths_enabled,
|
||||
_write_idf_version_txt,
|
||||
_write_stamp,
|
||||
check_esp_idf_install,
|
||||
@@ -682,3 +687,116 @@ def test_write_idf_version_txt_warns_on_write_error(tmp_path: Path) -> None:
|
||||
with patch("pathlib.Path.write_text", side_effect=OSError("denied")):
|
||||
# write failure is caught and warned, not raised
|
||||
_write_idf_version_txt(tmp_path, "5.1.2")
|
||||
|
||||
|
||||
def _fake_winreg(
|
||||
query_result: int | None = None, query_error: OSError | None = None
|
||||
) -> SimpleNamespace:
|
||||
"""Build a minimal winreg stand-in (the real module is Windows-only)."""
|
||||
|
||||
@contextmanager
|
||||
def open_key(root, path):
|
||||
yield "hkey"
|
||||
|
||||
def query_value_ex(key, name):
|
||||
if query_error is not None:
|
||||
raise query_error
|
||||
return query_result, 4 # (value, REG_DWORD)
|
||||
|
||||
return SimpleNamespace(
|
||||
HKEY_LOCAL_MACHINE=object(),
|
||||
OpenKey=open_key,
|
||||
QueryValueEx=query_value_ex,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("reg_value", "expected"), [(1, True), (0, False)])
|
||||
def test_windows_long_paths_enabled_reads_registry(
|
||||
reg_value: int, expected: bool
|
||||
) -> None:
|
||||
with patch.dict(sys.modules, {"winreg": _fake_winreg(query_result=reg_value)}):
|
||||
assert _windows_long_paths_enabled() is expected
|
||||
|
||||
|
||||
def test_windows_long_paths_enabled_missing_value() -> None:
|
||||
"""A missing registry value (FileNotFoundError is an OSError) reads as disabled."""
|
||||
fake = _fake_winreg(query_error=FileNotFoundError("no such value"))
|
||||
with patch.dict(sys.modules, {"winreg": fake}):
|
||||
assert _windows_long_paths_enabled() is False
|
||||
|
||||
|
||||
# 8 chars -> projected well under the 260 limit even with the ~245-char reserve
|
||||
_SHORT_IDF_PATH = "C:\\e\\idf"
|
||||
# 25 chars -> projected over the limit
|
||||
_LONG_IDF_PATH = "C:\\Users\\bob\\.esphome\\idf"
|
||||
|
||||
|
||||
def test_check_windows_path_length_noop_off_windows(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Off Windows the check returns before touching the registry or the path."""
|
||||
with (
|
||||
patch("esphome.espidf.framework.platform.system", return_value="Linux"),
|
||||
patch(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled"
|
||||
) as long_paths_mock,
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
_check_windows_path_length()
|
||||
long_paths_mock.assert_not_called()
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
def test_check_windows_path_length_noop_when_long_paths_enabled(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with (
|
||||
patch("esphome.espidf.framework.platform.system", return_value="Windows"),
|
||||
patch(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled", return_value=True
|
||||
),
|
||||
patch("esphome.espidf.framework._get_idf_tools_path") as get_path_mock,
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
_check_windows_path_length()
|
||||
get_path_mock.assert_not_called()
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
def test_check_windows_path_length_short_path_silent(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with (
|
||||
patch("esphome.espidf.framework.platform.system", return_value="Windows"),
|
||||
patch(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled", return_value=False
|
||||
),
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
return_value=_SHORT_IDF_PATH,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
_check_windows_path_length()
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
def test_check_windows_path_length_long_path_warns(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with (
|
||||
patch("esphome.espidf.framework.platform.system", return_value="Windows"),
|
||||
patch(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled", return_value=False
|
||||
),
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
return_value=_LONG_IDF_PATH,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
_check_windows_path_length()
|
||||
assert len(caplog.records) == 1
|
||||
message = caplog.records[0].getMessage()
|
||||
assert _LONG_IDF_PATH in message
|
||||
assert "long path support" in message
|
||||
|
||||
Reference in New Issue
Block a user