mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[esp8266] Fail fast when Rosetta 2 is missing on Apple Silicon Macs (#17737)
This commit is contained in:
@@ -776,6 +776,13 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
|
|
||||||
check_placeholder_credentials(config)
|
check_placeholder_credentials(config)
|
||||||
|
|
||||||
|
# Keep this here, NOT in codegen: config-hash and --only-generate must keep
|
||||||
|
# working on machines that cannot run the toolchain.
|
||||||
|
if CORE.is_esp8266:
|
||||||
|
from esphome.components.esp8266 import check_rosetta
|
||||||
|
|
||||||
|
check_rosetta()
|
||||||
|
|
||||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||||
# If you change this format, update the regex in that script as well
|
# If you change this format, update the regex in that script as well
|
||||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -20,9 +21,15 @@ from esphome.const import (
|
|||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
ThreadModel,
|
ThreadModel,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
from esphome.core import (
|
||||||
|
CORE,
|
||||||
|
CoroPriority,
|
||||||
|
EsphomeError,
|
||||||
|
Lambda,
|
||||||
|
coroutine_with_priority,
|
||||||
|
)
|
||||||
from esphome.core.config import BOARD_MAX_LENGTH
|
from esphome.core.config import BOARD_MAX_LENGTH
|
||||||
from esphome.helpers import copy_file_if_changed
|
from esphome.helpers import IS_MACOS, copy_file_if_changed
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||||
@@ -237,6 +244,32 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_rosetta() -> None:
|
||||||
|
"""Fail fast when the x86_64 ESP8266 toolchain cannot run on this Mac.
|
||||||
|
|
||||||
|
There is no native arm64 build of the xtensa-lx106 toolchain; on Apple
|
||||||
|
Silicon it runs under Rosetta 2, which macOS updates can remove.
|
||||||
|
"""
|
||||||
|
if not IS_MACOS or platform.machine() != "arm64":
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/bin/arch", "-x86_64", "/usr/bin/true"],
|
||||||
|
capture_output=True,
|
||||||
|
close_fds=False,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
return # arch(1) unavailable; let the build proceed
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise EsphomeError(
|
||||||
|
"ESP8266 builds on Apple Silicon Macs use an Intel (x86_64) "
|
||||||
|
"compiler that requires Rosetta 2, which is not installed on "
|
||||||
|
"this system. Install it with:\n"
|
||||||
|
" softwareupdate --install-rosetta --agree-to-license"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add(esp8266_ns.setup_preferences())
|
cg.add(esp8266_ns.setup_preferences())
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
"""Tests for ESP8266 component."""
|
"""Tests for ESP8266 component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from esphome.components.esp8266 import lambdas_use_scanf_float
|
from esphome.components import esp8266
|
||||||
from esphome.core import Lambda
|
from esphome.components.esp8266 import check_rosetta, lambdas_use_scanf_float
|
||||||
|
from esphome.core import EsphomeError, Lambda
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
|
|
||||||
@@ -60,3 +66,54 @@ def test_lambdas_use_scanf_float_nested() -> None:
|
|||||||
"""Test detection in deeply nested config."""
|
"""Test detection in deeply nested config."""
|
||||||
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
|
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
|
||||||
assert lambdas_use_scanf_float(config) is True
|
assert lambdas_use_scanf_float(config) is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def apple_silicon_run(monkeypatch: pytest.MonkeyPatch) -> Generator[MagicMock]:
|
||||||
|
"""Simulate an Apple Silicon Mac and yield the mocked subprocess.run."""
|
||||||
|
monkeypatch.setattr(esp8266, "IS_MACOS", True)
|
||||||
|
with (
|
||||||
|
patch("esphome.components.esp8266.platform.machine", return_value="arm64"),
|
||||||
|
patch("esphome.components.esp8266.subprocess.run") as mock_run,
|
||||||
|
):
|
||||||
|
yield mock_run
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("is_macos", "machine"),
|
||||||
|
[
|
||||||
|
(False, "arm64"),
|
||||||
|
(True, "x86_64"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_rosetta_skips_other_systems(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, is_macos: bool, machine: str
|
||||||
|
) -> None:
|
||||||
|
"""The check only probes on Apple Silicon Macs."""
|
||||||
|
monkeypatch.setattr(esp8266, "IS_MACOS", is_macos)
|
||||||
|
with (
|
||||||
|
patch("esphome.components.esp8266.platform.machine", return_value=machine),
|
||||||
|
patch("esphome.components.esp8266.subprocess.run") as mock_run,
|
||||||
|
):
|
||||||
|
check_rosetta()
|
||||||
|
mock_run.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_rosetta_installed(apple_silicon_run: MagicMock) -> None:
|
||||||
|
"""No error when the x86_64 probe succeeds (Rosetta present)."""
|
||||||
|
apple_silicon_run.return_value = MagicMock(returncode=0)
|
||||||
|
check_rosetta()
|
||||||
|
apple_silicon_run.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_rosetta_missing(apple_silicon_run: MagicMock) -> None:
|
||||||
|
"""A failing x86_64 probe raises an actionable error."""
|
||||||
|
apple_silicon_run.return_value = MagicMock(returncode=1)
|
||||||
|
with pytest.raises(EsphomeError, match="softwareupdate --install-rosetta"):
|
||||||
|
check_rosetta()
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_rosetta_arch_unavailable(apple_silicon_run: MagicMock) -> None:
|
||||||
|
"""The build proceeds when arch(1) cannot be executed."""
|
||||||
|
apple_silicon_run.side_effect = OSError("no such file")
|
||||||
|
check_rosetta()
|
||||||
|
|||||||
@@ -5450,6 +5450,43 @@ def _setup_build_info_test(
|
|||||||
return build_info_path, firmware_path
|
return build_info_path, firmware_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_program_esp8266_runs_rosetta_check(tmp_path: Path) -> None:
|
||||||
|
"""Test that compile_program runs the Rosetta preflight for ESP8266 targets."""
|
||||||
|
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device")
|
||||||
|
|
||||||
|
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"esphome.components.esp8266.check_rosetta",
|
||||||
|
side_effect=EsphomeError("Rosetta 2 is not installed"),
|
||||||
|
) as mock_check,
|
||||||
|
pytest.raises(EsphomeError, match="Rosetta 2 is not installed"),
|
||||||
|
):
|
||||||
|
compile_program(args, config)
|
||||||
|
|
||||||
|
mock_check.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_program_skips_rosetta_check_on_other_platforms(
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_compile_build_info_run_compile: Mock,
|
||||||
|
mock_compile_build_info_get_idedata: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the Rosetta preflight does not run for non-ESP8266 targets."""
|
||||||
|
_setup_build_info_test(tmp_path, firmware_first=True)
|
||||||
|
|
||||||
|
config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
with patch("esphome.components.esp8266.check_rosetta") as mock_check:
|
||||||
|
result = compile_program(args, config)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
mock_check.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_compile_program_emits_build_info_when_firmware_rebuilt(
|
def test_compile_program_emits_build_info_when_firmware_rebuilt(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
|||||||
Reference in New Issue
Block a user