[esp8266] Use our own Arduino core builds for the native toolchain (#19876)

This commit is contained in:
J. Nick Koston
2026-09-29 18:38:51 +02:00
committed by GitHub
parent 5be41dec15
commit a7e0afeb46
6 changed files with 152 additions and 99 deletions
@@ -2,9 +2,14 @@
import pytest
from esphome.components.esp8266 import _arduino_check_versions
from esphome.components.esp8266 import (
_arduino_check_versions,
_format_framework_arduino_version,
_framework_package_version,
)
import esphome.config_validation as cv
from esphome.const import CONF_PLATFORM_VERSION, CONF_VERSION
from esphome.core import EsphomeError
def test_versions_before_3_are_rejected() -> None:
@@ -21,3 +26,27 @@ def test_supported_versions_pass() -> None:
value = _arduino_check_versions({CONF_VERSION: "recommended"})
assert value[CONF_VERSION] == "3.1.2"
assert "espressif8266@4.2.1" in value[CONF_PLATFORM_VERSION]
def test_framework_package_version() -> None:
assert _framework_package_version(cv.Version(3, 1, 2)) == "3.30102.0"
assert _framework_package_version(cv.Version(3, 2, 0)) == "3.30200.0"
# A future major bump needs its own encoding, not a doomed registry lookup
with pytest.raises(EsphomeError, match="not supported yet"):
_framework_package_version(cv.Version(4, 0, 0))
# Cores before 3.x cannot build ESPHome (C++20) and are rejected
with pytest.raises(EsphomeError, match="requires core 3"):
_framework_package_version(cv.Version(2, 7, 4))
def test_format_framework_arduino_version_pins_all_series() -> None:
"""The PlatformIO source formatter across every encoding era."""
assert _format_framework_arduino_version(cv.Version(3, 1, 2)) == "~3.30102.0"
# Pre-3 cores are rejected with the version line anchored
with pytest.raises(cv.Invalid, match="requires core 3"):
_format_framework_arduino_version(cv.Version(2, 7, 4))
# Anchored to the framework version line, not a bare EsphomeError
with pytest.raises(cv.Invalid, match="not supported yet") as excinfo:
_format_framework_arduino_version(cv.Version(4, 0, 0))
assert excinfo.value.path == ["version"]
@@ -67,21 +67,24 @@ def test_valid_config_passes() -> None:
def test_platformio_toolchain_skips_checks() -> None:
# 3.0.2 is pio-legal (>= the global 3.0.0 floor) but below the native
# toolchain's own 3.1.1 floor; the bogus board only the native path checks
# 3.0.2 is pio-legal (>= the global 3.0.0 floor) but has no native build;
# the bogus board only the native path checks
CORE.toolchain = Toolchain.PLATFORMIO
config = _config(board="not_a_board", version="3.0.2")
assert _validate_native_toolchain(config) is config
def test_version_below_floor_rejected() -> None:
# 3.1.0 has no registry package, so the native floor is 3.1.1
with pytest.raises(cv.Invalid, match="3.1.1 or newer"):
_validate_native_toolchain(_config(version="3.1.0"))
def test_version_without_build_rejected() -> None:
"""Only the core versions built in esphome-libs/arduino-esp8266 work."""
with pytest.raises(
cv.Invalid, match=r"3\.1\.1.*available: 3\.1\.2.*platformio"
) as excinfo:
_validate_native_toolchain(_config(version="3.1.1"))
assert excinfo.value.path == [CONF_FRAMEWORK, CONF_VERSION]
def test_version_at_floor_accepted() -> None:
_validate_native_toolchain(_config(version="3.1.1"))
def test_built_version_accepted() -> None:
_validate_native_toolchain(_config(version="3.1.2"))
def test_custom_platform_version_warns_and_is_dropped(
+43 -40
View File
@@ -9,6 +9,7 @@ from unittest.mock import patch
import pytest
from esphome.arduino8266 import framework
from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION
import esphome.config_validation as cv
from esphome.core import CORE, EsphomeError
@@ -18,32 +19,6 @@ def _build_path(tmp_path: Path) -> None:
CORE.build_path = tmp_path
def test_framework_package_version() -> None:
assert framework.framework_package_version(cv.Version(3, 1, 2)) == "3.30102.0"
assert framework.framework_package_version(cv.Version(3, 2, 0)) == "3.30200.0"
# A future major bump needs its own encoding, not a doomed registry lookup
with pytest.raises(EsphomeError, match="not supported yet"):
framework.framework_package_version(cv.Version(4, 0, 0))
# Cores before 3.x cannot build ESPHome (C++20) and are rejected
with pytest.raises(EsphomeError, match="requires core 3"):
framework.framework_package_version(cv.Version(2, 7, 4))
def test_format_framework_arduino_version_pins_all_series() -> None:
"""The esp8266 component's PIO source formatter across every encoding
era, including the 4.x rejection it now shares with the installer."""
from esphome.components.esp8266 import _format_framework_arduino_version as fmt
assert fmt(cv.Version(3, 1, 2)) == "~3.30102.0"
# Pre-3 cores are rejected with the version line anchored
with pytest.raises(cv.Invalid, match="requires core 3"):
fmt(cv.Version(2, 7, 4))
# Anchored to the framework version line, not a bare EsphomeError
with pytest.raises(cv.Invalid, match="not supported yet") as excinfo:
fmt(cv.Version(4, 0, 0))
assert excinfo.value.path == ["version"]
def test_tools_path_default_and_prefix(tmp_path: Path) -> None:
with patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}):
assert framework.get_arduino8266_tools_path() == tmp_path.resolve()
@@ -54,6 +29,27 @@ def test_tools_path_default_and_prefix(tmp_path: Path) -> None:
assert path != Path.cwd()
def _recommended() -> framework.FrameworkRelease:
return framework.FRAMEWORK_RELEASES[RECOMMENDED_ARDUINO_FRAMEWORK_VERSION]
def test_framework_releases_are_pinned() -> None:
"""A release must not be pinned without its checksum, and the recommended
core must have one."""
assert RECOMMENDED_ARDUINO_FRAMEWORK_VERSION in framework.FRAMEWORK_RELEASES
for version, release in framework.FRAMEWORK_RELEASES.items():
assert release.tag.startswith(f"{version}-esphome.")
assert len(release.sha256) == 64
assert release.size > 0
def test_framework_download() -> None:
download = framework.FrameworkRelease("1.2.3-esphome.4", "a" * 64, 5).download()
releases = "https://github.com/esphome-libs/arduino-esp8266/releases/"
archive = "arduino-esp8266-1.2.3-esphome.4.tar.gz"
assert download == (f"{releases}download/1.2.3-esphome.4/{archive}", "a" * 64, 5)
def test_toolchain_builds_are_pinned() -> None:
"""A release must not be pinned without its checksums."""
for sha256, size in framework.TOOLCHAIN_BUILDS.values():
@@ -86,14 +82,15 @@ def test_toolchain_download_unsupported_system() -> None:
def _fake_framework(tmp_path: Path) -> None:
"""The layout install_package expects of an installed framework."""
for sub in ("cores/esp8266", "tools/sdk", "libraries"):
(tmp_path / "frameworks" / "3.30102.0" / sub).mkdir(parents=True)
(tmp_path / "frameworks" / "3.30102.0" / ".esphome_extracted").touch()
(tmp_path / "frameworks" / _recommended().tag / sub).mkdir(parents=True)
(tmp_path / "frameworks" / _recommended().tag / ".esphome_extracted").touch()
def test_check_and_install_mirror_skips_pinned_toolchain(tmp_path: Path) -> None:
"""With a mirror override an unsupported host can bring its own toolchain."""
with (
patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}),
patch.object(framework, "ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS", ["http://f"]),
patch.object(framework, "ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS", ["http://m"]),
patch.object(framework, "install_package") as mock_install,
patch.object(framework, "prefetch_packages") as mock_prefetch,
@@ -101,7 +98,10 @@ def test_check_and_install_mirror_skips_pinned_toolchain(tmp_path: Path) -> None
):
framework.check_and_install(cv.Version(3, 1, 2))
assert mock_prefetch.call_args.args[2] == {}
assert mock_install.call_args_list[1].kwargs["resolve"] is None
assert [call.kwargs["resolve"] for call in mock_install.call_args_list] == [
None,
None,
]
def test_check_and_install_installed_toolchain_on_unsupported_host(
@@ -147,7 +147,7 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
patch.object(framework, "find_ninja", return_value=tmp_path / "ninja"),
):
paths = framework.check_and_install(cv.Version(3, 1, 2))
assert paths.framework == tmp_path / "frameworks" / "3.30102.0"
assert paths.framework == tmp_path / "frameworks" / _recommended().tag
assert paths.toolchain == tmp_path / "toolchains" / framework.TOOLCHAIN_VERSION
assert paths.ninja == tmp_path / "ninja"
assert mock_install.call_count == 2
@@ -156,14 +156,14 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
fw_call, tc_call = mock_install.call_args_list
assert fw_call.args == (
framework.FRAMEWORK_PACKAGE,
"3.30102.0",
tmp_path / "frameworks" / "3.30102.0",
_recommended().tag,
tmp_path / "frameworks" / _recommended().tag,
framework.ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
tmp_path / "downloads",
)
assert fw_call.kwargs == {
"expect": ("cores/esp8266", "tools/sdk", "libraries"),
"resolve": None,
"resolve": _recommended().download,
}
assert tc_call.args == (
framework.TOOLCHAIN_PACKAGE,
@@ -181,8 +181,8 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
[
(
framework.FRAMEWORK_PACKAGE,
"3.30102.0",
tmp_path / "frameworks" / "3.30102.0",
_recommended().tag,
tmp_path / "frameworks" / _recommended().tag,
framework.ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
),
(
@@ -193,7 +193,10 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
),
],
tmp_path / "downloads",
{framework.TOOLCHAIN_PACKAGE: framework.toolchain_download},
{
framework.FRAMEWORK_PACKAGE: _recommended().download,
framework.TOOLCHAIN_PACKAGE: framework.toolchain_download,
},
)
@@ -204,10 +207,10 @@ def test_get_build_env_prepends_toolchain_bin(tmp_path: Path) -> None:
assert env["CCACHE_DIR"] == "x"
def test_check_and_install_rejects_old_core(tmp_path: Path) -> None:
"""Calling the installer below the floor fails before any download."""
with pytest.raises(EsphomeError, match=">= 3.1.1"):
framework.check_and_install(cv.Version(3, 0, 2))
def test_check_and_install_rejects_unbuilt_core(tmp_path: Path) -> None:
"""A core version without a build fails before any download."""
with pytest.raises(EsphomeError, match=r"3\.1\.1.*available: 3\.1\.2"):
framework.check_and_install(cv.Version(3, 1, 1))
def test_get_build_env_without_path_has_no_empty_entry(tmp_path: Path) -> None: