mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 12:51:11 +00:00
[esp32] Decouple esp-idf toolchain version check from PIO, honor framework source: override (#16516)
This commit is contained in:
committed by
Jesse Hills
parent
ecf823b871
commit
cd7e2d79c4
@@ -792,19 +792,15 @@ PLATFORM_VERSION_LOOKUP = {
|
||||
}
|
||||
|
||||
|
||||
def _check_pio_versions(config):
|
||||
config = config.copy()
|
||||
value = config[CONF_FRAMEWORK]
|
||||
def _resolve_framework_version(value: ConfigType) -> cv.Version:
|
||||
"""Resolve a named or raw framework version and validate the minimum.
|
||||
|
||||
Normalises value[CONF_VERSION] to its string form and returns the parsed
|
||||
cv.Version. Shared between the PIO and esp-idf toolchain paths; toolchain-
|
||||
specific concerns (source defaults, platform_version) live in the per-
|
||||
toolchain functions.
|
||||
"""
|
||||
if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
|
||||
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
|
||||
raise cv.Invalid(
|
||||
"Version needs to be explicitly set when a custom source or platform_version is used."
|
||||
)
|
||||
|
||||
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup))
|
||||
|
||||
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||
else:
|
||||
@@ -817,7 +813,38 @@ def _check_pio_versions(config):
|
||||
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
if version < cv.Version(3, 0, 0):
|
||||
raise cv.Invalid("Only Arduino 3.0+ is supported.")
|
||||
recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||
recommended = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||
else:
|
||||
if version < cv.Version(5, 0, 0):
|
||||
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
|
||||
recommended = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||
|
||||
if version != recommended:
|
||||
_LOGGER.warning(
|
||||
"The selected framework version is not the recommended one. "
|
||||
"If there are connectivity or build issues please remove the manual version."
|
||||
)
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def _check_pio_versions(config: ConfigType) -> ConfigType:
|
||||
config = config.copy()
|
||||
value = config[CONF_FRAMEWORK]
|
||||
|
||||
is_named_version = value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP
|
||||
if is_named_version and (CONF_SOURCE in value or CONF_PLATFORM_VERSION in value):
|
||||
raise cv.Invalid(
|
||||
"Version needs to be explicitly set when a custom source or platform_version is used."
|
||||
)
|
||||
if is_named_version:
|
||||
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(
|
||||
str(PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]])
|
||||
)
|
||||
|
||||
version = _resolve_framework_version(value)
|
||||
|
||||
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||
platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
|
||||
value[CONF_SOURCE] = value.get(
|
||||
CONF_SOURCE, _format_framework_arduino_version(version)
|
||||
@@ -825,9 +852,6 @@ def _check_pio_versions(config):
|
||||
if _is_framework_url(value[CONF_SOURCE]):
|
||||
value[CONF_SOURCE] = f"{ARDUINO_FRAMEWORK_PKG}@{value[CONF_SOURCE]}"
|
||||
else:
|
||||
if version < cv.Version(5, 0, 0):
|
||||
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
|
||||
recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
|
||||
value[CONF_SOURCE] = value.get(
|
||||
CONF_SOURCE,
|
||||
@@ -843,12 +867,6 @@ def _check_pio_versions(config):
|
||||
)
|
||||
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup))
|
||||
|
||||
if version != recommended_version:
|
||||
_LOGGER.warning(
|
||||
"The selected framework version is not the recommended one. "
|
||||
"If there are connectivity or build issues please remove the manual version."
|
||||
)
|
||||
|
||||
if value[CONF_PLATFORM_VERSION] != _parse_pio_platform_version(
|
||||
str(PLATFORM_VERSION_LOOKUP["recommended"])
|
||||
):
|
||||
@@ -860,19 +878,26 @@ def _check_pio_versions(config):
|
||||
return config
|
||||
|
||||
|
||||
def _check_esp_idf_versions(config):
|
||||
config = _check_pio_versions(config)
|
||||
def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
|
||||
config = config.copy()
|
||||
value = config[CONF_FRAMEWORK]
|
||||
|
||||
# Remove unwanted keys if present
|
||||
for key in (CONF_SOURCE, CONF_PLATFORM_VERSION):
|
||||
value.pop(key, None)
|
||||
# platform_version is a PlatformIO concept; drop it if a user carried it
|
||||
# over from a PIO-style config. CONF_SOURCE, on the other hand, is kept:
|
||||
# it lets a user override the framework tarball URL under the esp-idf
|
||||
# toolchain (the espidf framework downloader consults it).
|
||||
value.pop(CONF_PLATFORM_VERSION, None)
|
||||
|
||||
# Official ESP-IDF frameworks don't use extra
|
||||
version = cv.Version.parse(value[CONF_VERSION])
|
||||
version = cv.Version(version.major, version.minor, version.patch)
|
||||
version = _resolve_framework_version(value)
|
||||
|
||||
value[CONF_VERSION] = str(version)
|
||||
if CONF_SOURCE in value:
|
||||
_LOGGER.warning(
|
||||
"A custom framework source is set. "
|
||||
"If there are connectivity or build issues please remove the manual source."
|
||||
)
|
||||
|
||||
# Official ESP-IDF frameworks don't use the 'extra' semver component.
|
||||
value[CONF_VERSION] = str(cv.Version(version.major, version.minor, version.patch))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -786,6 +786,7 @@ def _check_esphome_idf_framework_install(
|
||||
tools: list[str],
|
||||
force: bool = False,
|
||||
env: dict[str, str] | None = None,
|
||||
source_url: str | None = None,
|
||||
) -> tuple[Path, bool]:
|
||||
"""
|
||||
Check and install ESP-IDF framework.
|
||||
@@ -796,6 +797,11 @@ def _check_esphome_idf_framework_install(
|
||||
tools: list of tools to install
|
||||
force: If True, force reinstallation
|
||||
env: Optional dictionary of environment variables to set
|
||||
source_url: Optional override URL for the framework tarball. Supports
|
||||
the same ``{VERSION}`` / ``{MAJOR}`` / ``{MINOR}`` / ``{PATCH}`` /
|
||||
``{EXTRA}`` substitutions as ESPHOME_IDF_FRAMEWORK_MIRRORS. When
|
||||
set, it replaces the default mirror list — no implicit fallback,
|
||||
so a misspelled URL fails loudly.
|
||||
|
||||
Returns:
|
||||
tuple of (framework_path, install_flag)
|
||||
@@ -817,6 +823,10 @@ def _check_esphome_idf_framework_install(
|
||||
env_stamp_file = framework_path / ESPHOME_STAMP_FILE
|
||||
idf_tools_path = framework_path / "tools" / "idf_tools.py"
|
||||
_LOGGER.info("Checking ESP-IDF %s framework ...", version)
|
||||
# Logged every invocation (not just on install) so the user can verify the
|
||||
# override. A changed URL needs ``esphome clean`` to force a re-download.
|
||||
if source_url:
|
||||
_LOGGER.info("Using framework source override: %s", source_url)
|
||||
|
||||
# 2. Download and extract the framework if not already extracted.
|
||||
# The marker is written last after extraction succeeds, so its presence
|
||||
@@ -844,9 +854,8 @@ def _check_esphome_idf_framework_install(
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
download_from_mirrors(
|
||||
ESPHOME_IDF_FRAMEWORK_MIRRORS, substitutions, tmp.file
|
||||
)
|
||||
mirrors = [source_url] if source_url else ESPHOME_IDF_FRAMEWORK_MIRRORS
|
||||
download_from_mirrors(mirrors, substitutions, tmp.file)
|
||||
|
||||
_LOGGER.info("Extracting ESP-IDF %s framework ...", version)
|
||||
archive_extract_all(tmp.file, framework_path, progress_header="Extracting")
|
||||
@@ -1008,6 +1017,7 @@ def check_esp_idf_install(
|
||||
tools: list[str] | None = None,
|
||||
features: list[str] | None = None,
|
||||
force: bool = False,
|
||||
source_url: str | None = None,
|
||||
) -> tuple[Path, Path]:
|
||||
"""
|
||||
Check and install ESP-IDF framework and Python environment.
|
||||
@@ -1018,6 +1028,10 @@ def check_esp_idf_install(
|
||||
tools: list of tools to install
|
||||
features: Features to install
|
||||
force: If True, force reinstallation
|
||||
source_url: Optional override URL for the framework tarball. When
|
||||
set, it replaces the default mirror list (no fallback). Forwarded
|
||||
to ``_check_esphome_idf_framework_install``; supports the same URL
|
||||
substitutions.
|
||||
|
||||
Returns:
|
||||
tuple of (framework_path, python_env_path)
|
||||
@@ -1040,7 +1054,7 @@ def check_esp_idf_install(
|
||||
|
||||
# 1) Framework
|
||||
framework_path, installed = _check_esphome_idf_framework_install(
|
||||
version, targets, tools, force=force, env=env
|
||||
version, targets, tools, force=force, env=env, source_url=source_url
|
||||
)
|
||||
|
||||
features = features or ESPHOME_IDF_DEFAULT_FEATURES
|
||||
|
||||
@@ -10,6 +10,7 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION
|
||||
from esphome.const import CONF_FRAMEWORK, CONF_SOURCE
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.espidf.framework import check_esp_idf_install, get_framework_env
|
||||
from esphome.espidf.size_summary import print_summary
|
||||
@@ -37,13 +38,27 @@ def _get_core_framework_version():
|
||||
return str(CORE.data[KEY_ESP32][KEY_IDF_VERSION])
|
||||
|
||||
|
||||
def _get_framework_source_override() -> str | None:
|
||||
"""Return the user-supplied esp32.framework.source override, if any.
|
||||
|
||||
The override lets a user point the IDF tarball download at a custom URL
|
||||
(mirror, fork, local server). Substitutions like ``{VERSION}`` /
|
||||
``{MAJOR}`` etc. work the same as in the default mirror list.
|
||||
"""
|
||||
if CORE.config is None:
|
||||
return None
|
||||
return CORE.config.get(KEY_ESP32, {}).get(CONF_FRAMEWORK, {}).get(CONF_SOURCE)
|
||||
|
||||
|
||||
def _get_esphome_esp_idf_paths(
|
||||
version: str | None = None,
|
||||
) -> tuple[os.PathLike, os.PathLike]:
|
||||
version = version or _get_core_framework_version()
|
||||
paths = _cache().paths
|
||||
if version not in paths:
|
||||
paths[version] = check_esp_idf_install(version)
|
||||
paths[version] = check_esp_idf_install(
|
||||
version, source_url=_get_framework_source_override()
|
||||
)
|
||||
return paths[version]
|
||||
|
||||
|
||||
|
||||
58
tests/unit_tests/test_espidf_toolchain.py
Normal file
58
tests/unit_tests/test_espidf_toolchain.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Tests for esphome.espidf.toolchain helpers."""
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from esphome.const import CONF_FRAMEWORK, CONF_SOURCE
|
||||
from esphome.core import CORE
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
|
||||
def test_get_framework_source_override_no_config():
|
||||
"""When CORE.config hasn't been set, no override is returned."""
|
||||
CORE.config = None
|
||||
assert toolchain._get_framework_source_override() is None
|
||||
|
||||
|
||||
def test_get_framework_source_override_no_esp32_section():
|
||||
"""A config without an esp32 section yields no override."""
|
||||
CORE.config = {}
|
||||
assert toolchain._get_framework_source_override() is None
|
||||
|
||||
|
||||
def test_get_framework_source_override_no_framework_source():
|
||||
"""An esp32 section without framework.source yields no override."""
|
||||
CORE.config = {"esp32": {CONF_FRAMEWORK: {}}}
|
||||
assert toolchain._get_framework_source_override() is None
|
||||
|
||||
|
||||
def test_get_framework_source_override_returns_value():
|
||||
"""A user-supplied framework source is returned verbatim."""
|
||||
url = "https://example.com/esp-idf-v{VERSION}.tar.xz"
|
||||
CORE.config = {"esp32": {CONF_FRAMEWORK: {CONF_SOURCE: url}}}
|
||||
assert toolchain._get_framework_source_override() == url
|
||||
|
||||
|
||||
def test_get_esphome_esp_idf_paths_forwards_source_override():
|
||||
"""_get_esphome_esp_idf_paths threads the override into check_esp_idf_install."""
|
||||
url = "https://my-mirror/esp-idf-v{VERSION}.tar.xz"
|
||||
CORE.config = {"esp32": {CONF_FRAMEWORK: {CONF_SOURCE: url}}}
|
||||
# Hit a fresh cache key so check_esp_idf_install is actually called.
|
||||
toolchain._cache().paths.clear()
|
||||
with patch.object(
|
||||
toolchain, "check_esp_idf_install", return_value=("/fw", "/penv")
|
||||
) as mock_install:
|
||||
toolchain._get_esphome_esp_idf_paths("5.5.4")
|
||||
mock_install.assert_called_once_with("5.5.4", source_url=url)
|
||||
|
||||
|
||||
def test_get_esphome_esp_idf_paths_no_override():
|
||||
"""When no source override is configured, source_url=None is passed."""
|
||||
CORE.config = {}
|
||||
toolchain._cache().paths.clear()
|
||||
with patch.object(
|
||||
toolchain, "check_esp_idf_install", return_value=("/fw", "/penv")
|
||||
) as mock_install:
|
||||
toolchain._get_esphome_esp_idf_paths("5.5.4")
|
||||
mock_install.assert_called_once_with("5.5.4", source_url=None)
|
||||
Reference in New Issue
Block a user