mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
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:
@@ -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 }}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user