Pin board_build.flash_mode unconditionally, dispatch native backends by toolchain, report a skipped size summary

This commit is contained in:
J. Nick Koston
2026-08-23 17:46:23 -05:00
parent d59f6eb46d
commit fde0a835b6
7 changed files with 51 additions and 50 deletions
+18 -14
View File
@@ -1936,24 +1936,27 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return run_multiple_configs(files, build_command)
# Native build backend per toolchain; keep in sync with NATIVE_TOOLCHAINS
# in esphome.const. Keyed by toolchain rather than a platform hook so the
# serial upload/logs fast path never imports the platform component package
# (see the esp32 variant comment in upload_using_esptool).
_NATIVE_TOOLCHAIN_MODULES = {
Toolchain.ESP_IDF: "esphome.espidf.toolchain",
Toolchain.ARDUINO: "esphome.arduino8266.toolchain",
}
def _native_toolchain_module():
"""The native build backend module for the resolved toolchain, via the
target platform's ``native_toolchain_module`` hook."""
"""The native build backend module for the resolved toolchain."""
if not CORE.using_native_toolchain:
# Both hooks return None for PlatformIO anyway; returning early
# keeps the serial upload/logs fast path from importing the
# platform component package (see the esp32 variant comment in
# upload_using_esptool)
return None
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
if native is None and CORE.using_native_toolchain:
if (module_path := _NATIVE_TOOLCHAIN_MODULES.get(CORE.toolchain)) is None:
# A native toolchain missing from the table is a bug; degrading to
# the PlatformIO path would build with the wrong backend
raise EsphomeError(
f"Platform {CORE.target_platform} resolved toolchain "
f"'{CORE.toolchain.value}' but provides no native toolchain module"
f"Toolchain '{CORE.toolchain.value}' has no native build backend module"
)
return native
return importlib.import_module(module_path)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
@@ -2007,7 +2010,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
native_toolchain = _native_toolchain_module()
if native_toolchain is None and not CORE.using_toolchain_platformio:
_LOGGER.error(
"analyze-memory is not supported with the '%s' toolchain",
"analyze-memory is not supported with the '%s' toolchain; it "
"requires a PlatformIO, ESP-IDF, or native Arduino build",
CORE.toolchain.value if CORE.toolchain else "unresolved",
)
return 1
+11 -7
View File
@@ -158,7 +158,10 @@ def run_compile(config: ConfigType, verbose: bool) -> int:
_LOGGER.error("Build produced no %s", artifact)
return 1
_print_size_summary(build_dir, paths)
if not _print_size_summary(build_dir, paths):
# The cause was already warned; name the consequence so a build
# contributing no RAM/Flash metric is visible to CI harnesses
_LOGGER.warning("Firmware size summary unavailable for this build")
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
try:
@@ -243,8 +246,8 @@ def _parse_app_size(build_dir: Path, paths: framework.InstalledPaths) -> int | N
return app_size
def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> None:
"""Print the PlatformIO-shaped RAM/Flash lines.
def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> bool:
"""Print the PlatformIO-shaped RAM/Flash lines; False when skipped.
The exact shape (including the bar) is parsed by
``script/ci_memory_impact_extract.py``; ``print_size_line`` matches it.
@@ -264,10 +267,10 @@ def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> Non
# The summary is a bonus artifact like idedata; a truncated
# toolchain extraction must not discard an already-linked build
_LOGGER.warning("Could not summarize firmware size: %s", err)
return
return False
if result.returncode != 0:
_LOGGER.warning("Could not summarize firmware size: %s", result.stderr)
return
return False
sections: dict[str, int] = {}
for line in result.stdout.splitlines():
parts = line.split()
@@ -284,17 +287,18 @@ def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> Non
"Size output is missing section(s) %s; skipping the size summary",
", ".join(sorted(missing)),
)
return
return False
# 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, paths)
if not app_size:
return
return False
ram = sum(sections[s] for s in _RAM_SECTIONS)
flash = sum(sections[s] for s in _FLASH_SECTIONS)
print_size_line("RAM", ram, _MAX_RAM_SIZE)
print_size_line("Flash", flash, app_size)
return True
# Sentinel: "resolve for me"; None is a real value meaning disabled.
-14
View File
@@ -3446,20 +3446,6 @@ def process_stacktrace(config, line, backtrace_state):
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);
esp32's crash decode still branches inline in _decode_pc. Same seam
the esp8266 component provides.
"""
if not CORE.using_toolchain_esp_idf:
return None
from esphome.espidf import toolchain
return toolchain
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
FILTER_SOURCE_FILES = filter_source_files_from_defines(
+5 -5
View File
@@ -323,7 +323,8 @@ CONFIG_SCHEMA = cv.All(
def native_toolchain_module():
"""The native build backend for the resolved toolchain, if any.
Hook for ``__main__``'s shared dispatch (idedata, analyze_memory).
``__main__`` dispatches from its own toolchain-keyed table; this helper
serves the component's internal callers.
"""
if not CORE.using_toolchain_arduino:
return None
@@ -476,10 +477,9 @@ async def to_code(config: ConfigType) -> None:
# implementation in the Arduino ESP8266 core.
cg.add_build_flag("-Wl,--wrap=millis")
if use_platformio:
cg.add_platformio_option(
"board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]
)
# Unconditional: the native build generator reads the same option,
# keeping one source of truth
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
cg.add_define(
+4 -2
View File
@@ -564,11 +564,13 @@ NATIVE_ARDUINO_PIO_OPTIONS = frozenset({"board_build.f_cpu", "board_build.ldscri
# that is stored rather than translated away. Consumed by the esp8266 native
# backend (later in this chain) for its ignored-option warning; defined here
# so it stays adjacent to the routing.
# build_src_flags: set unconditionally by esp8266/__init__ (throw_stubs) and
# read by the native generator; not user-routable, so not in the set above
# build_src_flags and board_build.flash_mode: set unconditionally by
# esp8266/__init__ and read by the native generator; not user-routable, so
# not in the set above
NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS = NATIVE_ARDUINO_PIO_OPTIONS | {
"lib_ignore",
"build_src_flags",
"board_build.flash_mode",
}
@@ -500,12 +500,13 @@ def test_print_size_summary_missing_section_skips_summary(
def test_warn_ignored_platformio_options(caplog: pytest.LogCaptureFixture) -> None:
"""Component-added options the native build drops are warned by name;
the honored ones (lib_ignore, f_cpu, ldscript, build_src_flags) stay
quiet."""
the honored ones (lib_ignore, f_cpu, ldscript, build_src_flags,
flash_mode) stay quiet."""
CORE.platformio_options = {
"board_build.ldscript": "eagle.flash.4m2m.ld",
"board_build.f_cpu": "160000000L",
"board_build.filesystem": "littlefs",
"board_build.flash_mode": "dio",
"build_src_flags": "-include throw_stubs.h",
"lib_ignore": ["Updater"],
"upload_speed": "460800",
@@ -517,6 +518,7 @@ def test_warn_ignored_platformio_options(caplog: pytest.LogCaptureFixture) -> No
assert "board_build.f_cpu is ignored" not in caplog.text
assert "lib_ignore" not in caplog.text
assert "build_src_flags" not in caplog.text
assert "flash_mode" not in caplog.text
# Component-added upload_speed never gets read under the native
# toolchain, so it must warn
assert "upload_speed" in caplog.text
+9 -6
View File
@@ -7390,12 +7390,15 @@ def test_compile_program_espidf_idedata_none_warns(
assert "No idedata was generated" in caplog.text
def test_native_toolchain_module_missing_hook_raises(tmp_path: Path) -> None:
"""A resolved native toolchain whose platform lacks the hook is a bug
and must fail, not silently degrade to the PlatformIO path."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO # esp32 provides no hook
with pytest.raises(EsphomeError, match="no native toolchain module"):
def test_native_toolchain_module_missing_backend_raises(tmp_path: Path) -> None:
"""A native toolchain missing from the backend table is a bug and must
fail, not silently degrade to the PlatformIO path."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO
with (
patch.dict(main._NATIVE_TOOLCHAIN_MODULES, clear=True),
pytest.raises(EsphomeError, match="no native build backend"),
):
_native_toolchain_module()