[libretiny] Load the precompiled header on Apple Silicon Macs (#19857)

This commit is contained in:
J. Nick Koston
2026-09-29 18:26:40 +02:00
committed by GitHub
parent c1303c048b
commit 5fdb73aceb
6 changed files with 208 additions and 42 deletions
@@ -113,23 +113,12 @@ def test_pch_checksum_tracks_closure_content(tmp_path: Path) -> None:
"platform", ["bk72xx", "esp32", "esp8266", "ln882x", "rp2", "rtl87xx"]
)
def test_pch_script_enabled(platform: str, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pch.sys, "platform", "linux")
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
assert pch.pch_script_enabled()
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
assert not pch.pch_script_enabled()
@pytest.mark.parametrize("host", ["darwin", "win32"])
@pytest.mark.parametrize("platform", sorted(pch.PCH_SCRIPT_LINUX_ONLY_PLATFORMS))
def test_pch_script_linux_only_platform(
platform: str, host: str, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(pch.sys, "platform", host)
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
assert not pch.pch_script_enabled()
@pytest.mark.parametrize("platform", sorted(pch.PCH_SCRIPT_EXCLUDED_PLATFORMS))
def test_pch_script_excluded_platform(platform: str) -> None:
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
+121 -1
View File
@@ -6,6 +6,8 @@ from collections.abc import Callable
import os
from pathlib import Path
import stat
import subprocess
import sys
from types import SimpleNamespace
from unittest.mock import patch
@@ -22,6 +24,7 @@ _SCRIPT = Path(toolchain.__file__).parent / "pch.py.script"
class _FakePlatform:
name = "fake"
packages = {"framework-x": {}, "toolchain-y": {}}
def get_package_version(self, name: str) -> str | None:
@@ -72,7 +75,13 @@ class _FakeSConsEnv(dict):
def _fake_cxx(tmp_path: Path, fail: bool = False) -> Path:
"""A compiler stand-in that records its argv and writes the -o target."""
cxx = tmp_path / "fake-gxx"
# As GCC: the program's path when it exists next to the driver, else its
# name; the version from FAKE_GCC_VERSION
body = (
'case "$1" in -print-prog-name=*) n=${1#*=};'
' p="$(dirname "$0")/../libexec/gcc/arm-none-eabi/10.3.1/$n";'
' [ -x "$p" ] && echo "$p" || echo "$n"; exit 0;;'
' -dumpversion) echo "${FAKE_GCC_VERSION:-10.3.1}"; exit 0;; esac\n'
'printf -- ---call---\\\\n >> "$0.argv"; printf \'%s\\n\' "$@" >> "$0.argv"\n'
)
if fail:
@@ -93,6 +102,7 @@ def _run_script(
name: str = "dev",
platform_cls: type[_FakePlatform] = _FakePlatform,
build_files: Callable[[tuple], list] | None = None,
cxx: Path | None = None,
) -> _FakeSConsEnv:
proj = tmp_path / name
src = proj / "src"
@@ -101,7 +111,8 @@ def _run_script(
(src / "esphome" / "core" / "pch_prefix.h").write_text(
'#include "esphome/core/defines.h"\n'
)
cxx = _fake_cxx(tmp_path, fail=fail)
if cxx is None:
cxx = _fake_cxx(tmp_path, fail=fail)
args = (proj, src, str(cxx), flags or ["-DX=1"], platform_cls)
# Distinct objects: the -include flags must land on projenv only
global_env = _FakeSConsEnv(*args)
@@ -137,6 +148,114 @@ def test_pch_script_builds_and_prepends_relative_include(tmp_path: Path) -> None
assert scons_env.global_env.prepended == []
class _LibreTinyPlatform(_FakePlatform):
name = "libretiny"
def _run_on_host(
tmp_path: Path,
host: str,
machine: str,
platform_cls: type[_FakePlatform],
cxx: Path | None = None,
gcc_version: str = "10.3.1",
) -> _FakeSConsEnv:
with (
patch.object(sys, "platform", host),
patch("platform.machine", return_value=machine),
):
return _run_script(
tmp_path,
platform_cls=platform_cls,
cxx=cxx,
env_vars={"FAKE_GCC_VERSION": gcc_version},
)
def _fake_toolchain(tmp_path: Path) -> tuple[Path, Path]:
"""A driver in bin/ and a cc1plus that records its argv in libexec/."""
toolchain = tmp_path / "toolchain"
(toolchain / "bin").mkdir(parents=True)
cxx = _fake_cxx(toolchain / "bin")
real = toolchain / "libexec" / "gcc" / "arm-none-eabi" / "10.3.1" / "cc1plus"
real.parent.mkdir(parents=True)
real.write_text('#!/bin/sh\nprintf \'%s\\n\' "$@" > "$0.argv"\n')
real.chmod(0o755)
return cxx, real
def test_pch_script_gcc10_wrapper_on_apple_silicon(tmp_path: Path) -> None:
"""The LibreTiny .gch compile and the consumers get a -B directory
holding a cc1plus that starts the real one."""
cxx, real = _fake_toolchain(tmp_path)
scons_env = _run_on_host(tmp_path, "darwin", "arm64", _LibreTinyPlatform, cxx)
wrapper = tmp_path / "dev" / pch.PCH_CC1_DIR / "cc1plus"
assert wrapper.stat().st_mode & stat.S_IXUSR
assert repr(str(real)) in wrapper.read_text(encoding="utf-8")
argv = Path(f"{cxx}.argv").read_text(encoding="utf-8").split("\n")
assert f"-B{pch.PCH_CC1_DIR}/" in argv
assert scons_env.prepended == [f"-B{pch.PCH_CC1_DIR}/", *pch.pch_consumer_flags()]
@pytest.mark.skipif(sys.platform != "darwin", reason="the wrapper is macOS only")
def test_pch_script_gcc10_wrapper_starts_the_real_cc1plus(tmp_path: Path) -> None:
cxx, real = _fake_toolchain(tmp_path)
_run_on_host(tmp_path, "darwin", "arm64", _LibreTinyPlatform, cxx)
wrapper = tmp_path / "dev" / pch.PCH_CC1_DIR / "cc1plus"
result = subprocess.run(
[str(wrapper), "-quiet", "x.cpp"], capture_output=True, text=True, check=False
)
assert result.returncode == 0, result.stderr
assert Path(f"{real}.argv").read_text(encoding="utf-8") == "-quiet\nx.cpp\n"
def test_pch_script_gcc10_without_cc1plus_builds_plainly(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A driver without a cc1plus of its own gets no wrapper and no header."""
scons_env = _run_on_host(tmp_path, "darwin", "arm64", _LibreTinyPlatform)
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
assert scons_env.prepended == []
assert "compiling without it" in capsys.readouterr().out
@pytest.mark.parametrize(
("host", "machine"), [("darwin", "x86_64"), ("win32", "AMD64")]
)
def test_pch_script_gcc10_skipped_elsewhere(
tmp_path: Path, host: str, machine: str, capsys: pytest.CaptureFixture[str]
) -> None:
"""Where the GCC 10 .gch cannot load, the build runs without it."""
scons_env = _run_on_host(tmp_path, host, machine, _LibreTinyPlatform)
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
assert scons_env.prepended == []
assert "compiling without it" in capsys.readouterr().out
@pytest.mark.parametrize(
("host", "machine", "platform_cls", "gcc_version"),
[
("darwin", "arm64", _FakePlatform, "10.3.1"),
("linux", "aarch64", _LibreTinyPlatform, "10.3.1"),
# From GCC 12 the .gch loads at any address, so the rule retires itself
("darwin", "arm64", _LibreTinyPlatform, "12.2.0"),
("win32", "AMD64", _LibreTinyPlatform, "14.2.0"),
],
)
def test_pch_script_no_wrapper_where_the_gch_loads(
tmp_path: Path,
host: str,
machine: str,
platform_cls: type[_FakePlatform],
gcc_version: str,
) -> None:
scons_env = _run_on_host(
tmp_path, host, machine, platform_cls, gcc_version=gcc_version
)
assert not (tmp_path / "dev" / pch.PCH_CC1_DIR).exists()
assert scons_env.prepended == pch.pch_consumer_flags()
def test_pch_script_names_match_the_python_side(tmp_path: Path) -> None:
"""The script cannot import esphome, so its copies are pinned."""
namespace: dict[str, object] = {
@@ -152,6 +271,7 @@ def test_pch_script_names_match_the_python_side(tmp_path: Path) -> None:
assert namespace["_CONSUMER_FLAGS"] == pch.pch_consumer_flags()
assert namespace["_GUARD_TEXT"] == pch.PCH_GUARD_TEXT
assert namespace["_INCLUDE_RE"].pattern == pch._INCLUDE_RE.pattern
assert namespace["_CC1_DIR"] == pch.PCH_CC1_DIR
def test_pch_script_compile_failure_stops_the_build(tmp_path: Path) -> None:
+8 -2
View File
@@ -684,11 +684,17 @@ def test_clean_build_partial_removes_pch_artifacts(
) -> None:
"""The PlatformIO pch sidecars live at the project root and must go in
a partial clean, like the native backend's under .pioenvs."""
from esphome.build_helpers.pch import PCH_ARTIFACT_NAMES as names
from esphome.build_helpers.pch import (
PCH_ARTIFACT_DIRS as dirs,
PCH_ARTIFACT_NAMES as names,
)
assert "esphome_pch.h.gch" in names
for name in names:
(tmp_path / name).write_text("x")
for name in dirs:
(tmp_path / name).mkdir()
(tmp_path / name / "cc1plus").write_text("x")
mock_core.relative_pioenvs_path.return_value = tmp_path / ".pioenvs"
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
@@ -696,7 +702,7 @@ def test_clean_build_partial_removes_pch_artifacts(
clean_build()
for name in names:
for name in (*names, *dirs):
assert not (tmp_path / name).exists()