Simplify the native dispatch and CI cache wiring

A cache-arduino8266 composite action mirrors cache-esp-idf (one key
resolver, dev-writes/PR-restores) and both jobs pin
ESPHOME_ARDUINO8266_PREFIX so YAML and Python agree on the path by
construction. esp32 provides the native_toolchain_module hook, so the
shared dispatcher never names a backend and the unclaimed-native raise
loses its esp-idf carve-out; the esp8266 decode and run_compile hooks
resolve through the same seam. run_compile resolves ccache once and
threads it to the generator, env, and idedata; the compdb rule names
follow the shared kinds; the pio-options warner inlines into the driver
and derives its consumed set from the core routing constant; the
per-toolchain CI narrowing shares one body; create_components_graph is
memoized per run; the decode rate-limit drops its math sentinel.
This commit is contained in:
J. Nick Koston
2026-08-21 20:55:27 -05:00
parent 16bc83032d
commit 17f718e8c9
10 changed files with 147 additions and 114 deletions
@@ -0,0 +1,36 @@
name: Cache Arduino ESP8266
description: >
Resolve the pinned Arduino core and xtensa toolchain versions and cache the
native ESP8266 install (~110 MB framework + toolchain; no ccache store, the
seed job saves before any compile runs). Callers must set env
ESPHOME_ARDUINO8266_PREFIX: ~/.esphome-arduino8266 and have the Python venv
already restored. Mirrors cache-esp-idf: only dev-branch pushes write the
shared cache, everything else restores.
runs:
using: composite
steps:
- name: Resolve the native toolchain cache key
# The versions are pinned in code, not in a hashable file, so resolve
# them for the key (actions/cache never overwrites a key, so a bump
# must change it). Assignment form so errexit catches a resolver
# failure; a nested $(...) inside echo would silently yield a
# degenerate key.
id: version
shell: bash
run: |
. venv/bin/activate
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
[ -n "$key" ] || exit 1
echo "key=$key" >> "$GITHUB_OUTPUT"
- name: Cache the native toolchain (write on dev)
if: github.ref == 'refs/heads/dev'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.esphome-arduino8266
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
- name: Restore the native toolchain (off dev)
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.esphome-arduino8266
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
+8 -36
View File
@@ -187,6 +187,10 @@ jobs:
# seed-apt-cache / cache-esp-idf).
if: github.event_name == 'push'
timeout-minutes: 15
env:
# The composite action and the install below agree on this path by
# construction, not by matching the Python-side default
ESPHOME_ARDUINO8266_PREFIX: ~/.esphome-arduino8266
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -195,23 +199,9 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Resolve the native toolchain cache key
id: esp8266-native-cache-key
run: |
. venv/bin/activate
# Assignment form so errexit catches a resolver failure; a nested
# $(...) inside echo would silently yield a degenerate key
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
[ -n "$key" ] || exit 1
echo "key=esp8266-native-$key" >> $GITHUB_OUTPUT
- name: Cache the native toolchain
id: esp8266-native-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/esphome/arduino8266
key: ${{ steps.esp8266-native-cache-key.outputs.key }}
uses: ./.github/actions/cache-arduino8266
- name: Install the native toolchain
if: steps.esp8266-native-cache.outputs.cache-hit != 'true'
run: |
. venv/bin/activate
python -c "from esphome.arduino8266.framework import check_and_install; from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION; check_and_install(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION)"
@@ -1181,6 +1171,7 @@ jobs:
# Single source of truth -- the full list lives in
# script/determine-jobs.py::ESP8266_NATIVE_TEST_COMPONENTS.
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
ESPHOME_ARDUINO8266_PREFIX: ~/.esphome-arduino8266
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -1191,27 +1182,8 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
# ~110 MB of framework + toolchain (no ccache: the seed job saves
# before any compile runs, so the store would always be empty). The versions
# are pinned in code, not in a hashable file, so resolve them for the
# key (actions/cache never overwrites a key, so a bump must change it).
# PRs are restore-only; the shared entry is seeded on pushes to dev by
# seed-esp8266-native-cache, mirroring the seed-apt-cache scoping.
- name: Resolve the native toolchain cache key
id: esp8266-native-cache-key
run: |
. venv/bin/activate
# Assignment form so errexit catches a resolver failure; a nested
# $(...) inside echo would silently yield a degenerate key
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
[ -n "$key" ] || exit 1
echo "key=esp8266-native-$key" >> $GITHUB_OUTPUT
- name: Restore the native toolchain
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/esphome/arduino8266
key: ${{ steps.esp8266-native-cache-key.outputs.key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
- name: Run native toolchain compile test
run: |
+8 -11
View File
@@ -849,13 +849,6 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
platform_run_compile = getattr(module, "run_compile", None)
if platform_run_compile is not None and platform_run_compile(args, config):
pass
elif CORE.using_native_toolchain and not CORE.using_toolchain_esp_idf:
# A resolved native toolchain must be claimed by its platform hook;
# falling through would build a mis-configured PlatformIO project
raise EsphomeError(
f"Toolchain '{CORE.toolchain.value}' resolved but no platform "
"backend claimed the build"
)
elif CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
@@ -883,6 +876,14 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
# (ValueError/LookupError) must not fail a successful build
# either.
_LOGGER.warning("Could not generate idedata: %s", err)
elif CORE.using_native_toolchain:
# A resolved native toolchain must be claimed by its platform hook
# or a branch above; falling through would build a mis-configured
# PlatformIO project
raise EsphomeError(
f"Toolchain '{CORE.toolchain.value}' resolved but no platform "
"backend claimed the build"
)
else:
from esphome.platformio import toolchain
@@ -1951,10 +1952,6 @@ def _native_toolchain_module():
``native_toolchain_module`` hook (the same per-platform module seam
``compile_program`` uses), so shared dispatch never names a backend.
"""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return toolchain
module = importlib.import_module("esphome.components." + CORE.target_platform)
get_native = getattr(module, "native_toolchain_module", None)
native = get_native() if get_native is not None else None
+38 -28
View File
@@ -7,7 +7,6 @@ from pathlib import Path
import subprocess
from esphome.arduino8266 import framework
from esphome.build_helpers.pio_options import warn_ignored_platformio_options
from esphome.const import (
CONF_COMPILE_PROCESS_LIMIT,
CONF_ESPHOME,
@@ -15,7 +14,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import IS_WINDOWS, write_file_if_changed
from esphome.helpers import write_file_if_changed
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -23,13 +22,26 @@ _LOGGER = logging.getLogger(__name__)
# ESP8266 user RAM (matches upload.maximum_ram_size in every board manifest)
_MAX_RAM_SIZE = 81920
# platformio_options keys the native build consumes. YAML-set upload_speed
# never reaches CORE.platformio_options under the native toolchain (it is
# read from the raw config at upload time), so anything here came from a
# component and genuinely is dropped; warn for it.
_CONSUMED_PIO_OPTIONS = frozenset(
{"lib_ignore", "board_build.f_cpu", "board_build.ldscript"}
)
def _warn_ignored_platformio_options() -> None:
"""Warn for component-added platformio options the native build drops.
The consumed set derives from the routing constant in core/config.py so
the two lists cannot drift. YAML-set upload_speed never reaches
CORE.platformio_options under the native toolchain (it is read from the
raw config at upload time), so anything unconsumed came from a
component and genuinely is dropped.
"""
from esphome.core.config import NATIVE_ARDUINO_PIO_OPTIONS
consumed = NATIVE_ARDUINO_PIO_OPTIONS | {"lib_ignore"}
for key in sorted(CORE.platformio_options or {}):
if key not in consumed:
_LOGGER.warning(
"platformio_options->%s is ignored when building with the "
"native 'arduino' toolchain",
key,
)
_RAM_SECTIONS = (".data", ".rodata", ".bss")
@@ -44,14 +56,8 @@ def get_elf_path() -> Path:
return get_build_dir() / "firmware.elf"
# Windows binutils carry the executable suffix; is_file() checks need it
_EXE_SUFFIX = ".exe" if IS_WINDOWS else ""
def _toolchain_tool(name: str) -> Path:
return (
framework.get_toolchain_path() / "bin" / f"xtensa-lx106-elf-{name}{_EXE_SUFFIX}"
)
return framework.toolchain_tool(framework.get_toolchain_path(), name)
def get_addr2line_path() -> Path:
@@ -69,12 +75,15 @@ def get_readelf_path() -> Path:
def run_compile(config: ConfigType, verbose: bool) -> int:
from esphome.build_gen import arduino8266 as build_gen
warn_ignored_platformio_options(_CONSUMED_PIO_OPTIONS, "arduino")
_warn_ignored_platformio_options()
paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
ninja_changed = build_gen.write_project(paths)
# Resolved once per build: the resolution probes PATH and spawns the
# runnability check, and three consumers need the same answer
ccache = framework.ccache_path()
ninja_changed = build_gen.write_project(paths, ccache)
build_dir = get_build_dir()
env = framework.get_build_env(paths.toolchain)
env = framework.get_build_env(paths.toolchain, ccache)
# The compile database is a pure function of build.ninja (no compilation
# involved), so regenerate it before the build: a failed build can then
@@ -94,9 +103,9 @@ def run_compile(config: ConfigType, verbose: bool) -> int:
if rc != 0:
return rc
_print_size_summary(build_dir)
_print_size_summary(build_dir, paths)
try:
idedata = get_idedata()
idedata = get_idedata(ccache)
except (EsphomeError, LookupError, OSError, RuntimeError, ValueError) as err:
# The firmware already built; idedata is a bonus artifact here.
# Broad on purpose: a vanished compiler (OSError), a failed include
@@ -116,7 +125,7 @@ def _write_compile_commands(
ninja_path: Path, build_dir: Path, env: dict[str, str]
) -> None:
result = subprocess.run(
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", "cc", "cxx", "asm"],
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", "c", "cxx", "asm"],
env=env,
capture_output=True,
text=True,
@@ -133,14 +142,14 @@ def _write_compile_commands(
write_file_if_changed(build_dir / "compile_commands.json", result.stdout)
def _parse_app_size(build_dir: Path) -> int | None:
def _parse_app_size(build_dir: Path, paths: framework.InstalledPaths) -> int | None:
"""Read the app flash budget (irom0_0_seg length) from the linker script."""
from esphome.build_gen.arduino8266 import get_flash_ld_path
from esphome.components.esp8266.build_surgery import segment_length
# Warnings, not debug: without the app size the Flash summary line is
# dropped and CI's memory-impact extraction loses its flash metric.
ld_path = get_flash_ld_path(build_dir)
ld_path = get_flash_ld_path(build_dir, paths)
try:
ld_text = ld_path.read_text(encoding="utf-8")
except OSError as err:
@@ -157,7 +166,7 @@ def _parse_app_size(build_dir: Path) -> int | None:
return app_size
def _print_size_summary(build_dir: Path) -> None:
def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> None:
"""Print the PlatformIO-shaped RAM/Flash lines.
The exact shape (including the bar) is parsed by
@@ -196,7 +205,7 @@ def _print_size_summary(build_dir: Path) -> None:
# Resolve the flash budget before printing anything: a RAM line without
# its Flash line would let CI's memory-impact extraction sum the two
# metrics over different build counts (_parse_app_size already warned).
app_size = _parse_app_size(build_dir)
app_size = _parse_app_size(build_dir, paths)
if not app_size:
return
ram = sum(sections[s] for s in _RAM_SECTIONS)
@@ -205,7 +214,7 @@ def _print_size_summary(build_dir: Path) -> None:
print_size_line("Flash", flash, app_size)
def get_idedata() -> dict | None:
def get_idedata(ccache: str | None = framework.CCACHE_UNRESOLVED) -> dict | None:
"""Derive idedata from the build's compile_commands.json.
Same contract as ``espidf.toolchain.get_idedata``: the fields IDE
@@ -213,7 +222,8 @@ def get_idedata() -> dict | None:
"""
from esphome.build_helpers.idedata import load_or_build_idedata
ccache = framework.ccache_path()
if ccache is framework.CCACHE_UNRESOLVED:
ccache = framework.ccache_path()
return load_or_build_idedata(
get_build_dir() / "compile_commands.json",
get_elf_path(),
+13
View File
@@ -3442,3 +3442,16 @@ def process_stacktrace(config, line, backtrace_state):
_decode_pc(config, addr.group())
return backtrace_state
def native_toolchain_module():
"""The native build backend for the resolved toolchain, if any.
Hook for ``__main__``'s shared dispatch (idedata, analyze_memory,
decode); same seam the esp8266 component provides.
"""
if not CORE.using_toolchain_esp_idf:
return None
from esphome.espidf import toolchain
return toolchain
+5 -8
View File
@@ -1,5 +1,4 @@
import logging
import math
from pathlib import Path
import platform
import re
@@ -542,10 +541,9 @@ async def finalize_serial_config() -> None:
# PlatformIO toolchain.
def run_compile(args, config: ConfigType) -> bool:
# Positive check: the native backend only runs when explicitly resolved
if not CORE.using_toolchain_arduino:
toolchain = native_toolchain_module()
if toolchain is None:
return False
from esphome.arduino8266 import toolchain
if toolchain.run_compile(config, CORE.verbose) != 0:
raise EsphomeError("ESP8266 native build failed")
return True
@@ -625,16 +623,15 @@ def _warn_decode_problem(key: str, message: str, *args) -> None:
the suppression expires instead of living for the process lifetime.
"""
now = time.monotonic()
if now - _DECODE_WARNED_AT.get(key, -math.inf) < 30:
last = _DECODE_WARNED_AT.get(key)
if last is not None and now - last < 30:
return
_DECODE_WARNED_AT[key] = now
_LOGGER.warning(message, *args)
def _decode_pc(config, addr):
if CORE.using_toolchain_arduino:
from esphome.arduino8266 import toolchain as native_toolchain
if (native_toolchain := native_toolchain_module()) is not None:
addr2line = native_toolchain.get_addr2line_path()
elf = native_toolchain.get_elf_path()
for path in (addr2line, elf):
+18 -9
View File
@@ -50,6 +50,7 @@ from __future__ import annotations
import argparse
from collections import Counter
from collections.abc import Callable
from enum import StrEnum
from functools import cache
import json
@@ -595,12 +596,23 @@ def esp32_platformio_components_to_test(branch: str | None = None) -> list[str]:
Returns:
Sorted list of component names to compile.
"""
return _native_components_to_test(
branch, ESP32_PLATFORMIO_TEST_COMPONENTS, _esp32_platformio_path_or_file_trigger
)
def _native_components_to_test(
branch: str | None,
test_set: frozenset[str] | set[str],
infra_trigger: Callable[[list[str]], bool],
) -> list[str]:
"""The shared narrowing rule for the per-toolchain smoke-test jobs."""
files = changed_files(branch)
if core_changed(files) or _esp32_platformio_path_or_file_trigger(files):
return sorted(ESP32_PLATFORMIO_TEST_COMPONENTS)
if core_changed(files) or infra_trigger(files):
return sorted(test_set)
return sorted(ESP32_PLATFORMIO_TEST_COMPONENTS & _changed_components_closure(files))
return sorted(test_set & _changed_components_closure(files))
def should_run_esp32_platformio(branch: str | None = None) -> bool:
@@ -682,12 +694,9 @@ def esp8266_native_components_to_test(branch: str | None = None) -> list[str]:
list on core or infrastructure changes, otherwise the intersection with
the changed-component dependency closure (empty list skips the job).
"""
files = changed_files(branch)
if core_changed(files) or _esp8266_native_path_or_file_trigger(files):
return sorted(ESP8266_NATIVE_TEST_COMPONENTS)
return sorted(ESP8266_NATIVE_TEST_COMPONENTS & _changed_components_closure(files))
return _native_components_to_test(
branch, ESP8266_NATIVE_TEST_COMPONENTS, _esp8266_native_path_or_file_trigger
)
def determine_cpp_unit_tests(
+2
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import ast
from collections.abc import Callable
from dataclasses import dataclass, field
import functools
from functools import cache
import hashlib
import json
@@ -1248,6 +1249,7 @@ def get_components_graph_cache_key() -> str:
return hasher.hexdigest()
@functools.cache
def create_components_graph() -> dict[str, list[str]]:
"""Create a graph of component dependencies (cached).
+17 -20
View File
@@ -2,13 +2,13 @@
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from esphome.arduino8266 import framework, toolchain
from esphome.build_helpers.pio_options import warn_ignored_platformio_options
import esphome.config_validation as cv
from esphome.const import (
CONF_COMPILE_PROCESS_LIMIT,
@@ -51,14 +51,11 @@ def _paths(tmp_path: Path) -> framework.InstalledPaths:
def test_path_getters(tmp_path: Path) -> None:
assert toolchain.get_build_dir() == CORE.relative_pioenvs_path("test8266")
assert toolchain.get_elf_path().name == "firmware.elf"
# Pin both suffix variants so the test passes on every host platform
with patch.object(toolchain, "_EXE_SUFFIX", ""):
assert toolchain.get_addr2line_path().name == "xtensa-lx106-elf-addr2line"
assert toolchain.get_objdump_path().name == "xtensa-lx106-elf-objdump"
assert toolchain.get_readelf_path().name == "xtensa-lx106-elf-readelf"
# Windows binutils carry the executable suffix
with patch.object(toolchain, "_EXE_SUFFIX", ".exe"):
assert toolchain.get_addr2line_path().name == "xtensa-lx106-elf-addr2line.exe"
# The framework accessor owns the layout and the Windows suffix
suffix = ".exe" if os.name == "nt" else ""
assert toolchain.get_addr2line_path().name == f"xtensa-lx106-elf-addr2line{suffix}"
assert toolchain.get_objdump_path().name == f"xtensa-lx106-elf-objdump{suffix}"
assert toolchain.get_readelf_path().name == f"xtensa-lx106-elf-readelf{suffix}"
def test_run_compile_build_failure(tmp_path: Path) -> None:
@@ -152,22 +149,22 @@ def test_parse_app_size(tmp_path: Path) -> None:
ld = tmp_path / "eagle.flash.4m.ld"
ld.write_text("MEMORY\n{\n irom0_0_seg : org = 0x40201010, len = 0xfeff0\n}\n")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path) == 0xFEFF0
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) == 0xFEFF0
ld.write_text("MEMORY { }\n")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path) is None
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None
# A zero-length segment is bad data, not a budget; warn and drop it
ld.write_text("MEMORY\n{\n irom0_0_seg : org = 0x40201010, len = 0x0\n}\n")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path) is None
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None
with patch(
"esphome.build_gen.arduino8266.get_flash_ld_path",
return_value=tmp_path / "missing.ld",
):
assert toolchain._parse_app_size(tmp_path) is None
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None
def test_print_size_summary(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
@@ -179,7 +176,7 @@ def test_print_size_summary(tmp_path: Path, capsys: pytest.CaptureFixture[str])
),
patch.object(toolchain, "_parse_app_size", return_value=1044464),
):
toolchain._print_size_summary(tmp_path)
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
out = capsys.readouterr().out
# Exact PlatformIO shape so script/ci_memory_impact_extract.py can parse it
assert "RAM: [==== ] 37.9% (used 31016 bytes from 81920 bytes)" in out
@@ -197,7 +194,7 @@ def test_print_size_summary_no_app_size(
),
patch.object(toolchain, "_parse_app_size", return_value=None),
):
toolchain._print_size_summary(tmp_path)
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
out = capsys.readouterr().out
# Both lines are skipped together: a RAM line without Flash would skew
# CI's memory-impact sums across builds
@@ -214,7 +211,7 @@ def test_print_size_summary_size_tool_failure(
"run",
return_value=MagicMock(returncode=1, stdout="", stderr="bad elf"),
):
toolchain._print_size_summary(tmp_path)
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert capsys.readouterr().out == ""
assert "Could not summarize firmware size" in caplog.text
@@ -287,7 +284,7 @@ def test_print_size_summary_unparsable_section(
"run",
return_value=MagicMock(returncode=0, stdout=bad),
):
toolchain._print_size_summary(tmp_path)
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert capsys.readouterr().out == ""
assert "Unparsable size output" in caplog.text
@@ -301,7 +298,7 @@ def test_print_size_summary_unparsable_section(
),
patch.object(toolchain, "_parse_app_size", return_value=1044464),
):
toolchain._print_size_summary(tmp_path)
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert "RAM:" in capsys.readouterr().out
assert "Unparsable size output" in caplog.text
@@ -320,7 +317,7 @@ def test_print_size_summary_missing_section_skips_summary(
"run",
return_value=MagicMock(returncode=0, stdout=without_bss),
):
toolchain._print_size_summary(tmp_path)
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert capsys.readouterr().out == ""
assert "missing section(s) .bss" in caplog.text
@@ -335,7 +332,7 @@ def test_warn_ignored_platformio_options(caplog: pytest.LogCaptureFixture) -> No
"lib_ignore": ["Updater"],
"upload_speed": "460800",
}
warn_ignored_platformio_options(toolchain._CONSUMED_PIO_OPTIONS, "arduino")
toolchain._warn_ignored_platformio_options()
assert "platformio_options->board_build.filesystem is ignored" in caplog.text
assert "native 'arduino' toolchain" in caplog.text
assert "board_build.ldscript is ignored" not in caplog.text
+2 -2
View File
@@ -6944,7 +6944,7 @@ def test_command_run_rp2040_bootsel_redetects_serial_port() -> None:
def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None:
"""Under the native ESP-IDF toolchain, idedata is emitted as JSON."""
setup_core()
setup_core(platform=PLATFORM_ESP32)
CORE.toolchain = Toolchain.ESP_IDF
data = {"cxx_path": "g++", "prog_path": "/build/firmware.elf"}
@@ -6958,7 +6958,7 @@ def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None:
def test_command_idedata_esp_idf_no_build_errors() -> None:
"""Under ESP-IDF, a missing build (no idedata) returns an error, not a crash."""
setup_core()
setup_core(platform=PLATFORM_ESP32)
CORE.toolchain = Toolchain.ESP_IDF
with patch("esphome.espidf.toolchain.get_idedata", return_value=None):