[esp8266] Use our own compiler builds for the native toolchain (#19849)

This commit is contained in:
J. Nick Koston
2026-09-29 18:31:24 +02:00
committed by GitHub
parent 5fdb73aceb
commit 5be41dec15
7 changed files with 301 additions and 31 deletions
+94 -2
View File
@@ -54,6 +54,91 @@ def test_tools_path_default_and_prefix(tmp_path: Path) -> None:
assert path != Path.cwd()
def test_toolchain_builds_are_pinned() -> None:
"""A release must not be pinned without its checksums."""
for sha256, size in framework.TOOLCHAIN_BUILDS.values():
assert len(sha256) == 64
assert size > 0
def test_toolchain_download() -> None:
sha256, size = framework.TOOLCHAIN_BUILDS["darwin_arm64"]
with (
patch.object(framework, "TOOLCHAIN_VERSION", "1.2.3"),
patch.object(framework, "get_systype", return_value="darwin_arm64"),
):
download = framework.toolchain_download()
releases = "https://github.com/esphome-libs/xtensa-lx106-elf-toolchain/releases/"
archive = "toolchain-xtensa-lx106-elf-1.2.3-darwin_arm64.tar.gz"
assert download == (f"{releases}download/1.2.3/{archive}", sha256, size)
def test_toolchain_download_unsupported_system() -> None:
with (
patch.object(framework, "get_systype", return_value="linux_armv7l"),
pytest.raises(
EsphomeError, match=r"linux_armv7l.*darwin_arm64.*toolchain: platformio"
),
):
framework.toolchain_download()
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()
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_TOOLCHAIN_MIRRORS", ["http://m"]),
patch.object(framework, "install_package") as mock_install,
patch.object(framework, "prefetch_packages") as mock_prefetch,
patch.object(framework, "find_ninja", return_value=tmp_path / "ninja"),
):
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
def test_check_and_install_installed_toolchain_on_unsupported_host(
tmp_path: Path,
) -> None:
"""The host is only checked when the toolchain must be downloaded."""
_fake_framework(tmp_path)
toolchain = tmp_path / "toolchains" / framework.TOOLCHAIN_VERSION
for sub in ("bin", "xtensa-lx106-elf"):
(toolchain / sub).mkdir(parents=True)
(toolchain / ".esphome_extracted").touch()
with (
patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}),
patch.object(framework, "get_systype", return_value="linux_armv7l"),
patch.object(framework, "prefetch_packages"),
patch.object(framework, "find_ninja", return_value=tmp_path / "ninja"),
):
paths = framework.check_and_install(cv.Version(3, 1, 2))
assert paths.toolchain == toolchain
def test_check_and_install_unsupported_host_without_toolchain_raises(
tmp_path: Path,
) -> None:
"""Without an installed toolchain the download is needed, and the host
error names the ways out."""
_fake_framework(tmp_path)
with (
patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}),
patch.object(framework, "get_systype", return_value="linux_armv7l"),
patch.object(framework, "prefetch_packages"),
patch.object(framework, "find_ninja", return_value=tmp_path / "ninja"),
pytest.raises(EsphomeError, match=r"linux_armv7l.*toolchain: platformio"),
):
framework.check_and_install(cv.Version(3, 1, 2))
def test_check_and_install_returns_paths(tmp_path: Path) -> None:
with (
patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}),
@@ -76,7 +161,10 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
framework.ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
tmp_path / "downloads",
)
assert fw_call.kwargs["expect"] == ("cores/esp8266", "tools/sdk", "libraries")
assert fw_call.kwargs == {
"expect": ("cores/esp8266", "tools/sdk", "libraries"),
"resolve": None,
}
assert tc_call.args == (
framework.TOOLCHAIN_PACKAGE,
framework.TOOLCHAIN_VERSION,
@@ -84,7 +172,10 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
framework.ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS,
tmp_path / "downloads",
)
assert tc_call.kwargs["expect"] == ("bin", "xtensa-lx106-elf")
assert tc_call.kwargs == {
"expect": ("bin", "xtensa-lx106-elf"),
"resolve": framework.toolchain_download,
}
# The prefetch sees the same package specs as the installs
assert mock_prefetch.call_args.args == (
[
@@ -102,6 +193,7 @@ def test_check_and_install_returns_paths(tmp_path: Path) -> None:
),
],
tmp_path / "downloads",
{framework.TOOLCHAIN_PACKAGE: framework.toolchain_download},
)
+20
View File
@@ -5999,6 +5999,26 @@ def test_compile_program_esp8266_runs_rosetta_check(tmp_path: Path) -> None:
mock_check.assert_called_once()
def test_compile_program_esp8266_native_skips_rosetta_check(tmp_path: Path) -> None:
"""The native toolchain has an arm64 macOS build, so nothing is probed."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
with (
patch("esphome.components.esp8266.check_rosetta") as mock_check,
patch(
"esphome.components.esp8266.run_compile",
side_effect=EsphomeError("compile reached"),
),
pytest.raises(EsphomeError, match="compile reached"),
):
compile_program(MockArgs(), config)
mock_check.assert_not_called()
def test_compile_program_skips_rosetta_check_on_other_platforms(
tmp_path: Path,
mock_compile_build_info_run_compile: Mock,
+76 -3
View File
@@ -311,6 +311,56 @@ def test_install_package_downloads_via_registry(tmp_path: Path) -> None:
assert mock_download.call_args[1] == {"sha256": "abc123", "size": 42}
def test_install_package_downloads_pinned(tmp_path: Path) -> None:
"""A resolver's download is verified like a registry one, with no lookup."""
dest = tmp_path / "pkg"
pinned = registry.Download("http://y/pinned.tar.gz", "def456", 7)
with (
patch.object(registry, "download_with_resume") as mock_download,
patch.object(registry, "archive_extract_all") as mock_extract,
patch.object(registry, "registry_download") as mock_registry,
):
mock_extract.side_effect = lambda *_a, **_kw: (dest / "payload").mkdir(
parents=True
)
registry.install_package(
"pkg",
"1.0.0",
dest,
[],
tmp_path / "dl",
expect=("payload",),
resolve=lambda: pinned,
)
mock_registry.assert_not_called()
assert mock_download.call_args[0][0] == "http://y/pinned.tar.gz"
assert mock_download.call_args[1] == {"sha256": "def456", "size": 7}
def test_install_package_mirror_wins_over_pinned(tmp_path: Path) -> None:
"""A mirror override replaces the pinned download."""
dest = tmp_path / "pkg"
with (
patch.object(registry, "download_from_mirrors") as mock_mirrors,
patch.object(registry, "download_with_resume") as mock_download,
patch.object(registry, "archive_extract_all") as mock_extract,
):
mock_extract.side_effect = lambda *_a, **_kw: (dest / "payload").mkdir(
parents=True
)
registry.install_package(
"pkg",
"1.0.0",
dest,
["http://mirror/{VERSION}"],
tmp_path / "dl",
expect=("payload",),
resolve=lambda: registry.Download("http://y/pinned.tar.gz", "def456", 7),
)
mock_mirrors.assert_called_once()
mock_download.assert_not_called()
def test_install_package_validates_expected_layout(tmp_path: Path) -> None:
"""The success marker is only written when the extracted tree is usable."""
dest = tmp_path / "pkg"
@@ -535,6 +585,29 @@ def test_prefetch_packages_downloads_pending_in_parallel(tmp_path: Path) -> None
assert callable(call[1]["progress"])
def test_prefetch_packages_uses_pinned_download(tmp_path: Path) -> None:
"""A package with a resolver skips the registry; the rest use it."""
with (
patch.object(registry, "download_with_resume") as mock_download,
patch.object(
registry, "registry_download", side_effect=_resolve_for({"a": 10})
) as mock_registry,
):
registry.prefetch_packages(
[
("a", "1.0", tmp_path / "a", []),
("b", "2.0", tmp_path / "b", []),
],
tmp_path / "dl",
{"b": lambda: registry.Download("http://y/b.tar.gz", "def456", 20)},
)
mock_registry.assert_called_once_with("a", "1.0")
calls = sorted(mock_download.call_args_list, key=lambda c: c[0][0])
assert [c[0][0] for c in calls] == ["http://x/a.tar.gz", "http://y/b.tar.gz"]
assert calls[1][1]["sha256"] == "def456"
assert calls[1][1]["size"] == 20
def test_prefetch_packages_skips_freshly_installed_dest(tmp_path: Path) -> None:
"""A dest whose marker appeared while the worker waited on the lock is
already installed; re-downloading would orphan an archive copy."""
@@ -620,13 +693,13 @@ def test_prefetch_packages_leaves_a_long_held_lock_to_its_holder(
mock_download.assert_not_called()
def test_already_installed_probe(tmp_path: Path) -> None:
def test_is_installed_probe(tmp_path: Path) -> None:
"""Both arms of the marker probe the prefetch worker keys on."""
dest = tmp_path / "pkg"
dest.mkdir()
assert registry._already_installed(dest) is False
assert registry.is_installed(dest) is False
(dest / ".esphome_extracted").touch()
assert registry._already_installed(dest) is True
assert registry.is_installed(dest) is True
def test_prefetch_packages_dedupes_duplicate_entries(tmp_path: Path) -> None: