mirror of
https://github.com/esphome/esphome.git
synced 2026-09-10 06:48:45 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6473f7ae6c | ||
|
|
944e1cbf7c | ||
|
|
61ac515dcf | ||
|
|
72dbe4edbc | ||
|
|
7d26655eb4 |
@@ -1,773 +0,0 @@
|
||||
"""Build specification for the native ESP8266 Arduino toolchain.
|
||||
|
||||
Transliterates the PlatformIO build spec for the Arduino ESP8266 framework
|
||||
(``framework-arduinoespressif8266/tools/platformio-build.py`` plus
|
||||
``platform-espressif8266/builder/main.py``): the flag sets, defines, and
|
||||
linker-script generation deliberately match what PlatformIO produces so the
|
||||
binaries stay near-identical between the two toolchains. The ninja emission
|
||||
(``write_project``) builds on these pieces.
|
||||
|
||||
The ``PIO_FRAMEWORK_ARDUINO_*`` knob defines (lwIP variant, NONOS SDK
|
||||
version, MMU layout, exceptions, waveform phase) keep working: they are read
|
||||
from the build flags with the same precedence as the PlatformIO builder.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from esphome.arduino8266.framework import toolchain_tool
|
||||
from esphome.build_helpers.ninja import shell_token as _shell_token
|
||||
from esphome.components.esp8266 import build_surgery
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.platformio.library import lex_build_flags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.arduino8266.framework import InstalledPaths
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Values that land unquoted on generated command lines are shape-checked
|
||||
# against these before use. re.ASCII: a Unicode digit or word character
|
||||
# (Arabic-Indic numerals) would pass \d/\w and defeat the named error
|
||||
_MMU_VALUE_RE = re.compile(r"(?:0[xX][0-9a-fA-F]+|\d+)[uUlL]*", re.ASCII)
|
||||
_MMU_HEX_VALUE_RE = re.compile(r"0[xX][0-9a-fA-F]+[uUlL]*", re.ASCII)
|
||||
# Only these land in the preprocessed script's ``len =`` fields, which
|
||||
# build_surgery's segment parser reads back as hex; the other MMU_* macros
|
||||
# (MMU_EXTERNAL_HEAP=128) are consumed by mmu_iram.h and may be decimal
|
||||
_MMU_SEGMENT_SIZE_NAMES = ("MMU_IRAM_SIZE", "MMU_ICACHE_SIZE")
|
||||
_BOARD_NAME_RE = re.compile(r"[\w.-]+", re.ASCII)
|
||||
_F_CPU_RE = re.compile(r"\d+L?", re.ASCII)
|
||||
_FLASH_LD_NAME_RE = re.compile(r"[\w.-]+\.ld", re.ASCII)
|
||||
|
||||
# Every supported board ships this clock; board_build.f_cpu overrides
|
||||
_DEFAULT_F_CPU = "80000000L"
|
||||
|
||||
# The SDK linker-script template and the preprocessed copy the build links
|
||||
# against; the cache stamp and stderr sidecars derive from the output name
|
||||
_COMMON_LD_HEADER = "eagle.app.v6.common.ld.h"
|
||||
_COMMON_LD_NAME = "local.eagle.app.v6.common.ld"
|
||||
# Testing mode shadows the SDK flash ld with a patched copy under this name
|
||||
_TESTING_LD_PREFIX = "testing_"
|
||||
|
||||
# The recovery hint for a half-extracted or damaged framework cache
|
||||
_CLEAN_HINT = "run 'esphome clean-all' and retry"
|
||||
|
||||
|
||||
def _sdk_ld_dir(framework: Path) -> Path:
|
||||
return framework / "tools" / "sdk" / "ld"
|
||||
|
||||
|
||||
def _apply_surgery(fn, *args: object) -> str:
|
||||
"""Run one build_surgery edit, naming a failed anchor instead of a
|
||||
traceback (the surgery module raises bare RuntimeError so its
|
||||
``.py.script`` twins stay importable without esphome)."""
|
||||
try:
|
||||
return fn(*args)
|
||||
except RuntimeError as err:
|
||||
raise EsphomeError(str(err)) from err
|
||||
|
||||
|
||||
# From platformio-build.py. Knob suffix -> SDK define; the first entry is
|
||||
# the default (dicts preserve insertion order). With multiple SDK knobs set
|
||||
# (a pathological config) ties break by table order, since upstream's
|
||||
# tie-break depends on define order and is not reproducible here.
|
||||
_NONOSDK_VERSIONS = {
|
||||
"SDK22x_190703": "NONOSDK22x_190703",
|
||||
"SDK221": "NONOSDK221",
|
||||
"SDK22x_190313": "NONOSDK22x_190313",
|
||||
"SDK22x_191024": "NONOSDK22x_191024",
|
||||
"SDK22x_191105": "NONOSDK22x_191105",
|
||||
"SDK22x_191122": "NONOSDK22x_191122",
|
||||
"SDK305": "NONOSDK305",
|
||||
}
|
||||
|
||||
|
||||
class _LwipVariant(NamedTuple):
|
||||
"""One lwIP build variant: the defines and the prebuilt library that
|
||||
was compiled with them."""
|
||||
|
||||
tcp_mss: int
|
||||
features: int
|
||||
ipv6: int
|
||||
lib: str
|
||||
|
||||
|
||||
# Knob define -> variant; first match wins, in insertion order (as in
|
||||
# platformio-build.py)
|
||||
_LWIP_VARIANTS = {
|
||||
"PIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY": _LwipVariant(
|
||||
536, 1, 1, "lwip6-536-feat"
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_HIGHER_BANDWIDTH": _LwipVariant(
|
||||
1460, 1, 1, "lwip6-1460-feat"
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH": _LwipVariant(
|
||||
1460, 1, 0, "lwip2-1460-feat"
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY_LOW_FLASH": _LwipVariant(
|
||||
536, 0, 0, "lwip2-536"
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH": _LwipVariant(
|
||||
1460, 0, 0, "lwip2-1460"
|
||||
),
|
||||
}
|
||||
# The default is PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY's variant: upstream
|
||||
# has no branch for that spelling (it is the else), so any listed knob wins
|
||||
# over it -- sntp emits LOW_MEMORY while esp8266 always emits
|
||||
# HIGHER_BANDWIDTH_LOW_FLASH, and the latter must win as under PlatformIO
|
||||
_LWIP_DEFAULT = _LwipVariant(536, 1, 0, "lwip2-536-feat")
|
||||
|
||||
# Knob define -> MMU_* defines; first match wins, in insertion order (as
|
||||
# in platformio-build.py)
|
||||
_MMU_VARIANTS = {
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48": (
|
||||
"MMU_IRAM_SIZE=0xC000",
|
||||
"MMU_ICACHE_SIZE=0x4000",
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED": (
|
||||
"MMU_IRAM_SIZE=0xC000",
|
||||
"MMU_ICACHE_SIZE=0x4000",
|
||||
"MMU_IRAM_HEAP",
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM32_SECHEAP_NOTSHARED": (
|
||||
"MMU_IRAM_SIZE=0x8000",
|
||||
"MMU_ICACHE_SIZE=0x4000",
|
||||
"MMU_SEC_HEAP_SIZE=0x4000",
|
||||
"MMU_SEC_HEAP=0x40108000",
|
||||
),
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_EXTERNAL_128K": (
|
||||
"MMU_IRAM_SIZE=0x8000",
|
||||
"MMU_ICACHE_SIZE=0x8000",
|
||||
"MMU_EXTERNAL_HEAP=128",
|
||||
),
|
||||
# Upstream really does cap the 1024K option's heap knob at 256
|
||||
# (platformio-build.py's MMU_EXTERNAL_1024K branch); transliterated
|
||||
# verbatim
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_EXTERNAL_1024K": (
|
||||
"MMU_IRAM_SIZE=0x8000",
|
||||
"MMU_ICACHE_SIZE=0x8000",
|
||||
"MMU_EXTERNAL_HEAP=256",
|
||||
),
|
||||
}
|
||||
# From platformio-build.py: the invariant framework defines every TU gets
|
||||
# (ARDUINO=10805 encodes the IDE compatibility level); the board, flash-mode,
|
||||
# knob, and MMU defines are composed around them in _defines_flags, in
|
||||
# upstream's order.
|
||||
_FRAMEWORK_DEFINES = ("__ets__", "ICACHE_FLASH", "_GNU_SOURCE", "ARDUINO=10805")
|
||||
_ARCH_DEFINES = ("ESP8266", "ARDUINO_ARCH_ESP8266")
|
||||
|
||||
# Upstream reads these from the board manifest (build.mmu_iram_size etc.);
|
||||
# no supported board sets them, so the platformio-build.py defaults are
|
||||
# hardcoded here rather than drift
|
||||
_MMU_DEFAULT = ("MMU_IRAM_SIZE=0x8000", "MMU_ICACHE_SIZE=0x8000")
|
||||
|
||||
# Upstream's CXXFLAGS (-fno-rtti, the -std level, -f(no-)exceptions) and the
|
||||
# trailing stdc++/m/c/gcc system libs are composed at emission
|
||||
# (write_project) from CORE.cpp_standard and _BuildConfig.exceptions.
|
||||
_ASFLAGS = ["-mlongcalls", "-mtext-section-literals"]
|
||||
_CFLAGS = [
|
||||
"-std=gnu17",
|
||||
"-Wpointer-arith",
|
||||
"-Wno-implicit-function-declaration",
|
||||
"-Wl,-EL",
|
||||
"-fno-inline-functions",
|
||||
"-nostdlib",
|
||||
]
|
||||
_CCFLAGS = [
|
||||
"-Os",
|
||||
"-mlongcalls",
|
||||
"-mtext-section-literals",
|
||||
"-falign-functions=4",
|
||||
"-U__STRICT_ANSI__",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-Wall",
|
||||
"-Werror=return-type",
|
||||
"-free",
|
||||
"-fipa-pta",
|
||||
]
|
||||
# Upstream's -u _scanf_float is deliberately absent: it is re-added from
|
||||
# KEY_SCANF_FLOAT at emission (the remove_float_scanf extra script's job).
|
||||
_LINKFLAGS = [
|
||||
"-Os",
|
||||
"-nostdlib",
|
||||
"-Wl,--no-check-sections",
|
||||
"-Wl,-static",
|
||||
"-Wl,--gc-sections",
|
||||
"-Wl,-wrap,system_restart_local",
|
||||
"-Wl,-wrap,spi_flash_read",
|
||||
"-u",
|
||||
"app_entry",
|
||||
"-u",
|
||||
"_printf_float",
|
||||
"-u",
|
||||
"_DebugExceptionVector",
|
||||
"-u",
|
||||
"_DoubleExceptionVector",
|
||||
"-u",
|
||||
"_KernelExceptionVector",
|
||||
"-u",
|
||||
"_NMIExceptionVector",
|
||||
"-u",
|
||||
"_UserExceptionVector",
|
||||
]
|
||||
_SYSTEM_LIBS_PRE_LWIP = ["hal", "phy", "pp", "net80211"]
|
||||
_SYSTEM_LIBS_POST_LWIP = [
|
||||
"wpa",
|
||||
"crypto",
|
||||
"main",
|
||||
"wps",
|
||||
"bearssl",
|
||||
"espnow",
|
||||
"smartconfig",
|
||||
"airkiss",
|
||||
"wpa2",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class _BuildConfig:
|
||||
"""Knob-derived build configuration (PIO_FRAMEWORK_ARDUINO_* defines)."""
|
||||
|
||||
nonosdk: str
|
||||
lwip_lib: str
|
||||
exceptions: bool
|
||||
vtables: str
|
||||
fp_in_irom: bool
|
||||
knob_defines: list[str]
|
||||
mmu_defines: list[str]
|
||||
|
||||
|
||||
def _lexed_build_flags() -> list[str]:
|
||||
"""Shell-lex ``CORE.build_flags`` as PlatformIO's ``ParseFlags`` does,
|
||||
sorted so duplicate defines resolve deterministically.
|
||||
|
||||
Lex once per build; consumers share the tokens.
|
||||
"""
|
||||
# The funnel warns and drops empty glued arguments (-D "") itself
|
||||
return lex_build_flags(sorted(CORE.build_flags), "esphome")
|
||||
|
||||
|
||||
def _flag_defines(unflags: set[str], tokens: list[str]) -> dict[str, str]:
|
||||
"""Map define name -> full ``NAME[=VALUE]`` for every -D build flag.
|
||||
|
||||
``tokens`` comes from one ``_lexed_build_flags()`` call shared with
|
||||
``_project_flags``, which already warned about and dropped any bare "-D".
|
||||
"""
|
||||
defines: dict[str, str] = {}
|
||||
for tok in tokens:
|
||||
# An unflagged knob must not drive lwIP/SDK/MMU selection while
|
||||
# being absent from the compile line
|
||||
if tok in unflags:
|
||||
continue
|
||||
if tok.startswith("-D"):
|
||||
body = tok[2:]
|
||||
defines[body.split("=", 1)[0]] = body
|
||||
return defines
|
||||
|
||||
|
||||
def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
|
||||
nonosdk = next(
|
||||
(
|
||||
define
|
||||
for name, define in _NONOSDK_VERSIONS.items()
|
||||
if f"PIO_FRAMEWORK_ARDUINO_ESPRESSIF_{name}" in defines
|
||||
),
|
||||
next(iter(_NONOSDK_VERSIONS.values())),
|
||||
)
|
||||
# Same compile-line/linked-artifact split as the lwIP knobs below: a
|
||||
# raw NONOSDK* would define a second SDK macro while the link still
|
||||
# resolves against the knob's libraries
|
||||
if raw_sdk := sorted(n for n in defines if n.startswith("NONOSDK")):
|
||||
raise EsphomeError(
|
||||
f"{', '.join(raw_sdk)} are set by the "
|
||||
"PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK* knobs; drop the raw "
|
||||
"build flags"
|
||||
)
|
||||
|
||||
lwip = next(
|
||||
(variant for knob, variant in _LWIP_VARIANTS.items() if knob in defines),
|
||||
_LWIP_DEFAULT,
|
||||
)
|
||||
|
||||
# The lwIP triple selects a prebuilt library; a raw override would win
|
||||
# the compile line (user tokens come last here) while the link still
|
||||
# pulls the library built for the knob's values
|
||||
if owned := sorted(
|
||||
n for n in ("TCP_MSS", "LWIP_FEATURES", "LWIP_IPV6") if n in defines
|
||||
):
|
||||
raise EsphomeError(
|
||||
f"{', '.join(owned)} are set by the PIO_FRAMEWORK_ARDUINO_LWIP2_* "
|
||||
"knobs; drop the raw build flags"
|
||||
)
|
||||
knob_defines = [
|
||||
f"{nonosdk}=1",
|
||||
f"TCP_MSS={lwip.tcp_mss}",
|
||||
f"LWIP_FEATURES={lwip.features}",
|
||||
f"LWIP_IPV6={lwip.ipv6}",
|
||||
]
|
||||
if "PIO_FRAMEWORK_ARDUINO_WAVEFORM_LOCKED_PHASE" in defines:
|
||||
knob_defines.append("WAVEFORM_LOCKED_PHASE=1")
|
||||
|
||||
# Sorted so the pick is deterministic: the dict is built from a set of
|
||||
# build flags, whose iteration order varies between processes.
|
||||
vtables_knobs = sorted(name for name in defines if name.startswith("VTABLES_IN_"))
|
||||
known_vtables = {"VTABLES_IN_FLASH", "VTABLES_IN_DRAM", "VTABLES_IN_IRAM"}
|
||||
# A typo'd or conflicting knob would otherwise fail obscurely in the
|
||||
# SDK header's #error
|
||||
if unknown := [k for k in vtables_knobs if k not in known_vtables]:
|
||||
raise EsphomeError(f"Unknown VTABLES_IN_* define(s): {', '.join(unknown)}")
|
||||
# A body (e.g. VTABLES_IN_FLASH=0) would split the compile line from the
|
||||
# linker script, which always defines the bare name
|
||||
if valued := [defines[k] for k in vtables_knobs if defines[k] not in (k, f"{k}=1")]:
|
||||
raise EsphomeError(f"VTABLES_IN_* defines take no value: {', '.join(valued)}")
|
||||
if len(vtables_knobs) > 1:
|
||||
raise EsphomeError(
|
||||
f"Conflicting VTABLES_IN_* defines: {', '.join(vtables_knobs)}"
|
||||
)
|
||||
vtables = vtables_knobs[0] if vtables_knobs else "VTABLES_IN_FLASH"
|
||||
|
||||
mmu_knob = next((knob for knob in _MMU_VARIANTS if knob in defines), None)
|
||||
if mmu_knob is not None:
|
||||
if raw := sorted(n for n in defines if n.startswith("MMU_")):
|
||||
# Same compile-line/linker-script split as the no-knob case below
|
||||
fix = (
|
||||
f"drop {mmu_knob} to use the custom sizes"
|
||||
if "PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM" in defines
|
||||
else "drop the raw MMU_* build flags or use "
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM"
|
||||
)
|
||||
raise EsphomeError(f"{', '.join(raw)} conflict with {mmu_knob}; {fix}")
|
||||
mmu = list(_MMU_VARIANTS[mmu_knob])
|
||||
elif "PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM" in defines:
|
||||
if "MMU_IRAM_SIZE" not in defines or "MMU_ICACHE_SIZE" not in defines:
|
||||
raise EsphomeError(
|
||||
"PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM requires MMU_IRAM_SIZE and "
|
||||
"MMU_ICACHE_SIZE build flags"
|
||||
)
|
||||
for name in _MMU_SEGMENT_SIZE_NAMES:
|
||||
# A bare -Dname would preprocess to len = 1 and fail far away
|
||||
if "=" not in defines[name]:
|
||||
raise EsphomeError(
|
||||
f"{name} must be a hex literal (e.g. 0x8000), got (no value)"
|
||||
)
|
||||
for name, body in defines.items():
|
||||
if not name.startswith("MMU_") or "=" not in body:
|
||||
# Valueless flags (MMU_IRAM_HEAP) are legitimate switches
|
||||
continue
|
||||
# Every valued MMU_* reaches the linker-script preprocessor; a
|
||||
# bare or non-numeric value would corrupt it and fail far away
|
||||
# in ld. The two segment sizes must additionally be hex:
|
||||
# build_surgery's segment parser cannot read decimal back.
|
||||
value = body.partition("=")[2]
|
||||
rule = (
|
||||
_MMU_HEX_VALUE_RE if name in _MMU_SEGMENT_SIZE_NAMES else _MMU_VALUE_RE
|
||||
)
|
||||
if not rule.fullmatch(value):
|
||||
shape = (
|
||||
"a hex literal (e.g. 0x8000)"
|
||||
if name in _MMU_SEGMENT_SIZE_NAMES
|
||||
else "a numeric literal"
|
||||
)
|
||||
raise EsphomeError(
|
||||
f"{name} must be {shape}, got {value or '(no value)'}"
|
||||
)
|
||||
# Sorted so build.ninja and the linker-script stamp stay
|
||||
# byte-stable across runs (the flag set has no deterministic
|
||||
# iteration order).
|
||||
mmu = sorted(body for name, body in defines.items() if name.startswith("MMU_"))
|
||||
else:
|
||||
if raw := sorted(n for n in defines if n.startswith("MMU_")):
|
||||
# Unlike PlatformIO (whose defaults win the compile line), user
|
||||
# MMU_* here would win the compile but not the linker script;
|
||||
# refuse them all, like the knob branch above.
|
||||
raise EsphomeError(
|
||||
f"Raw {', '.join(raw)} build flags require "
|
||||
"-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM"
|
||||
)
|
||||
mmu = list(_MMU_DEFAULT)
|
||||
|
||||
return _BuildConfig(
|
||||
nonosdk=nonosdk,
|
||||
lwip_lib=lwip.lib,
|
||||
exceptions="PIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS" in defines,
|
||||
vtables=vtables,
|
||||
fp_in_irom="FP_IN_IROM" in defines,
|
||||
knob_defines=knob_defines,
|
||||
mmu_defines=mmu,
|
||||
)
|
||||
|
||||
|
||||
def _pio_option(key: str, default: str) -> str:
|
||||
"""A platformio_options value the native build honors (str-normalized).
|
||||
|
||||
core/config.py routes these into ``CORE.platformio_options`` under the
|
||||
arduino toolchain and already collapses a repeated option to its last
|
||||
value (like a later platformio.ini line), so a scalar always arrives.
|
||||
"""
|
||||
value = CORE.platformio_options.get(key)
|
||||
if value is None:
|
||||
return default
|
||||
value = str(value).strip()
|
||||
if not value:
|
||||
raise EsphomeError(f"platformio_options {key} is empty")
|
||||
return value
|
||||
|
||||
|
||||
def _defines_flags(
|
||||
config: _BuildConfig, flash_mode: str, board: str, board_defines: tuple[str, ...]
|
||||
) -> list[str]:
|
||||
r"""The framework/board -D tokens for the compile line.
|
||||
|
||||
The returned tokens already carry shell-level escaping (the board
|
||||
defines embed ``\"``), so they must be emitted unquoted; wrapping
|
||||
them in ``_shell_token`` would deliver literal backslashes to gcc.
|
||||
``flash_mode`` also lands unquoted: callers pass it pre-validated
|
||||
against ``BUILD_FLASH_MODES`` (cv.one_of at config time, the
|
||||
``_FLASH_MODES`` check at the emission half's read site).
|
||||
"""
|
||||
if not _BOARD_NAME_RE.fullmatch(board):
|
||||
# The name lands unquoted in two -D bodies; reject it by name
|
||||
# instead of corrupting the compile line
|
||||
raise EsphomeError(f"Invalid board name {board!r}")
|
||||
# Every supported board ships 80 MHz; board_build.f_cpu overrides
|
||||
f_cpu = _pio_option("board_build.f_cpu", _DEFAULT_F_CPU)
|
||||
if not _F_CPU_RE.fullmatch(f_cpu):
|
||||
# The value lands unquoted on the compile line; reject by name
|
||||
# instead of corrupting it
|
||||
raise EsphomeError(f"Invalid board_build.f_cpu value {f_cpu!r}")
|
||||
return [
|
||||
f"-D{d}"
|
||||
for d in (
|
||||
f"F_CPU={f_cpu}",
|
||||
*_FRAMEWORK_DEFINES,
|
||||
f'ARDUINO_BOARD=\\"PLATFORMIO_{board.upper()}\\"',
|
||||
f'ARDUINO_BOARD_ID=\\"{board}\\"',
|
||||
f"FLASHMODE_{flash_mode.upper()}",
|
||||
"LWIP_OPEN_SRC",
|
||||
*config.knob_defines,
|
||||
config.vtables,
|
||||
# User-supplied bodies re-quote like every other user token
|
||||
# (a no-op for real MMU values)
|
||||
*(_shell_token(d) for d in config.mmu_defines),
|
||||
*_ARCH_DEFINES,
|
||||
*board_defines,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _unflag_tokens() -> set[str]:
|
||||
"""``build_unflags`` entries shell-lexed to tokens, as PlatformIO matches."""
|
||||
# Lexed like _lexed_build_flags reads build_flags, so "-D FOO" removes
|
||||
# -DFOO in both spellings (PlatformIO's ProcessUnFlags parses the same
|
||||
# way) and no bare half can collaterally drop an unrelated token
|
||||
return set(lex_build_flags(list(CORE.build_unflags), "esphome build_unflags"))
|
||||
|
||||
|
||||
def _project_flags(
|
||||
unflags: set[str], tokens: list[str]
|
||||
) -> tuple[list[str], list[str], list[Path], list[str]]:
|
||||
"""Split the ESPHome build flags into compile, linker, -L, and -l lists.
|
||||
|
||||
Plain-form linker flags (``_PLAIN_LINKER_FLAGS``/``_PLAIN_LINKER_PREFIXES``)
|
||||
raise: they would be inert on the ``-c`` compile line.
|
||||
``compile_flags``/``link_flags`` come back shell-quoted;
|
||||
``lib_dirs``/``libs`` are raw, quote at emission.
|
||||
"""
|
||||
compile_flags: list[str] = []
|
||||
link_flags: list[str] = []
|
||||
lib_dirs: list[Path] = []
|
||||
libs: list[str] = []
|
||||
for tok in tokens:
|
||||
if tok in unflags:
|
||||
continue
|
||||
# _lexed_build_flags warned about and dropped any bare -I/-D/-L/-l
|
||||
if tok.startswith("-Wl,"):
|
||||
link_flags.append(_shell_token(tok))
|
||||
elif tok.startswith("-L"):
|
||||
lib_dirs.append(Path(tok[2:]))
|
||||
elif tok.startswith("-l"):
|
||||
libs.append(tok[2:])
|
||||
else:
|
||||
if tok.startswith(_PLAIN_DRIVER_LINK_PREFIXES):
|
||||
# Driver options with no -Wl, spelling; ld would reject them
|
||||
raise EsphomeError(
|
||||
f"Link flag {tok} in build_flags is not supported by the "
|
||||
"native toolchain"
|
||||
)
|
||||
if tok in _PLAIN_LINKER_FLAGS or tok.startswith(_PLAIN_LINKER_PREFIXES):
|
||||
raise EsphomeError(
|
||||
f"Linker flag {tok} in build_flags is not routed to the "
|
||||
"link line; use the -Wl, form"
|
||||
)
|
||||
if tok.startswith("-") and not tok.startswith(_COMPILE_FLAG_PREFIXES):
|
||||
# The linker deny lists are not exhaustive; an unlisted
|
||||
# link-only spelling would be inert on the -c compile line,
|
||||
# so at least surface the odd shape
|
||||
_LOGGER.warning(
|
||||
"Build flag %s is not a recognized compile-flag shape; "
|
||||
"it is passed to the compile line only",
|
||||
tok,
|
||||
)
|
||||
compile_flags.append(_shell_token(tok))
|
||||
return compile_flags, link_flags, lib_dirs, libs
|
||||
|
||||
|
||||
# Recognized compile-flag shapes: the allow-list feeding the fall-through
|
||||
# warning in _project_flags (an unlisted link-only spelling still reaches
|
||||
# the compile line, but not silently)
|
||||
_COMPILE_FLAG_PREFIXES = (
|
||||
"-D",
|
||||
"-I",
|
||||
"-U",
|
||||
"-W",
|
||||
"-f",
|
||||
"-m",
|
||||
"-O",
|
||||
"-g",
|
||||
"-std=",
|
||||
"-include",
|
||||
)
|
||||
# Plain-form linker flags rejected by _project_flags: inert on a -c compile
|
||||
# line, so the firmware would silently lack the requested link behavior.
|
||||
# Best-effort, not exhaustive; see _COMPILE_FLAG_PREFIXES above.
|
||||
_PLAIN_LINKER_FLAGS = (
|
||||
"-u",
|
||||
"-e",
|
||||
"-s",
|
||||
"-static",
|
||||
"-nostartfiles",
|
||||
"-nodefaultlibs",
|
||||
"-nostdlib",
|
||||
"-rdynamic",
|
||||
)
|
||||
_PLAIN_LINKER_PREFIXES = ("-T", "-Xlinker")
|
||||
# Driver options, not ld options: -Wl, has no equivalent for these
|
||||
_PLAIN_DRIVER_LINK_PREFIXES = ("-fuse-ld=", "--specs=", "-specs=")
|
||||
|
||||
|
||||
def _stat_sig(path: Path) -> str:
|
||||
"""Size and mtime cache-stamp signature for one input file.
|
||||
|
||||
Absent stays deterministic ("missing": the spawn names it); unreadable
|
||||
forces a cache miss every run rather than pinning the stamp to a
|
||||
constant that can never notice a later edit.
|
||||
"""
|
||||
try:
|
||||
st = path.stat()
|
||||
return f"{st.st_size}:{st.st_mtime_ns}"
|
||||
except FileNotFoundError:
|
||||
return "missing"
|
||||
except OSError as err:
|
||||
_LOGGER.warning(
|
||||
"Could not stat %s (%s); regenerating the linker script every "
|
||||
"build. Run 'esphome clean-all' to reinstall the framework.",
|
||||
path,
|
||||
err,
|
||||
)
|
||||
return f"unreadable:{os.urandom(8).hex()}"
|
||||
|
||||
|
||||
def _write_note(path: Path, text: str, *, warn: bool = False) -> bool:
|
||||
"""Best-effort bookkeeping write; a failure never fails the build.
|
||||
|
||||
``warn`` marks notes whose loss drops a diagnostic on later cached
|
||||
builds; a lost stamp only costs a cache miss and stays at debug.
|
||||
Returns whether the write persisted, so a lost warn note can veto
|
||||
the cache stamp and keep the diagnostic re-derivable.
|
||||
"""
|
||||
try:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
except OSError as err:
|
||||
log = _LOGGER.warning if warn else _LOGGER.debug
|
||||
log("Could not write %s: %s", path, err)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def generate_ld_scripts(
|
||||
paths: InstalledPaths, config: _BuildConfig, flash_ld_name: str
|
||||
) -> None:
|
||||
"""Generate the common linker script (and testing-mode flash ld copy).
|
||||
|
||||
Runs the same preprocessor invocation as the PlatformIO builder over
|
||||
``eagle.app.v6.common.ld.h``, then applies ESPHome's surgeries: the wifi
|
||||
rate-table DRAM relocation, and enlarged memory segments in testing mode.
|
||||
"""
|
||||
if not _FLASH_LD_NAME_RE.fullmatch(flash_ld_name):
|
||||
# Joined under the SDK and build ld dirs; never a path or traversal
|
||||
raise EsphomeError(f"Invalid flash linker script name {flash_ld_name!r}")
|
||||
framework = paths.framework
|
||||
gcc = toolchain_tool(paths.toolchain, "gcc")
|
||||
ld_dir = CORE.relative_pioenvs_path(CORE.name, "ld")
|
||||
mkdir_p(ld_dir)
|
||||
|
||||
cmd = [str(gcc), "-CC", "-E", "-P", f"-D{config.vtables}"]
|
||||
cmd += [f"-D{d}" for d in config.mmu_defines]
|
||||
if config.fp_in_irom:
|
||||
cmd.append("-DFP_IN_IROM")
|
||||
header = _sdk_ld_dir(framework) / _COMMON_LD_HEADER
|
||||
cmd += [str(header), "-o", "-"]
|
||||
|
||||
# The inputs are the command line (defines + framework version, which is
|
||||
# baked into the paths) plus testing mode; skip the preprocessor spawn on
|
||||
# incremental builds when nothing changed.
|
||||
output = ld_dir / _COMMON_LD_NAME
|
||||
stamp = ld_dir / f".{_COMMON_LD_NAME}.stamp"
|
||||
# Stamp includes the header/gcc stat (catches in-place re-extraction)
|
||||
# and the surgery fingerprint (a build_surgery edit invalidates old
|
||||
# build dirs)
|
||||
stamp_content = (
|
||||
# shlex.join: a spaced path stays one quoted element, so two
|
||||
# different cmd lists can never collide to the same stamp string
|
||||
shlex.join(cmd)
|
||||
+ f" testing={CORE.testing_mode}"
|
||||
+ f" header={_stat_sig(header)}"
|
||||
+ f" gcc={_stat_sig(gcc)}"
|
||||
+ f" {build_surgery.surgery_fingerprint()}"
|
||||
)
|
||||
|
||||
stderr_note = ld_dir / f".{_COMMON_LD_NAME}.stderr"
|
||||
|
||||
def _note_digest() -> str:
|
||||
# The note is an output like the script itself; folding its state
|
||||
# into the stamp makes an externally removed or edited note a cache
|
||||
# miss that re-runs -E and re-derives the diagnostic
|
||||
if not stderr_note.is_file():
|
||||
return "none"
|
||||
return hashlib.sha256(stderr_note.read_bytes()).hexdigest()
|
||||
|
||||
def _cached_ld_is_valid() -> bool:
|
||||
# Any damaged cache regenerates; never abort the build over it. The
|
||||
# stamp records the sha256 of the content written, so an externally
|
||||
# edited script regenerates too.
|
||||
try:
|
||||
if not (output.is_file() and stamp.is_file()):
|
||||
return False
|
||||
rest, sep, digest = stamp.read_text(encoding="utf-8").rpartition(
|
||||
" content="
|
||||
)
|
||||
inputs, note_sep, note_digest = rest.rpartition(" note=")
|
||||
return (
|
||||
bool(sep)
|
||||
and bool(note_sep)
|
||||
and inputs == stamp_content
|
||||
and note_digest == _note_digest()
|
||||
and hashlib.sha256(output.read_bytes()).hexdigest() == digest
|
||||
)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return False
|
||||
|
||||
if not _cached_ld_is_valid():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
except OSError as err:
|
||||
# A half-extracted or half-deleted toolchain cache reaches here
|
||||
raise EsphomeError(f"Could not run {gcc}: {err}; {_CLEAN_HINT}") from err
|
||||
# Localized gcc diagnostics on a non-UTF-8 console must degrade,
|
||||
# not UnicodeDecodeError the build; the script itself (below) is
|
||||
# decoded strictly instead, so a mangled byte can never be cached
|
||||
stderr_text = result.stderr.decode("utf-8", errors="replace")
|
||||
if result.returncode != 0:
|
||||
raise EsphomeError(f"Generating the linker script failed:\n{stderr_text}")
|
||||
note_persisted = True
|
||||
if stderr_text.strip():
|
||||
# Preprocessor warnings on the success path must reach the user
|
||||
# on this and every later cached build (see the re-emit below)
|
||||
_LOGGER.warning("Linker-script preprocessor: %s", stderr_text.strip())
|
||||
note_persisted = _write_note(stderr_note, stderr_text.strip(), warn=True)
|
||||
else:
|
||||
try:
|
||||
stderr_note.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
# A kept stale note would re-emit an obsolete diagnostic on
|
||||
# every cache hit; skip the stamp so -E re-derives the truth
|
||||
_LOGGER.warning(
|
||||
"Could not remove %s (%s); the linker script will "
|
||||
"regenerate every build until it is removable; %s",
|
||||
stderr_note,
|
||||
err,
|
||||
_CLEAN_HINT,
|
||||
)
|
||||
note_persisted = False
|
||||
try:
|
||||
stdout_text = result.stdout.decode("utf-8")
|
||||
except UnicodeDecodeError as err:
|
||||
# -CC keeps header comments verbatim; a non-UTF-8 byte replaced
|
||||
# with U+FFFD would be cached as valid for the build dir's life
|
||||
raise EsphomeError(
|
||||
f"Preprocessed linker script from {header} is not UTF-8: "
|
||||
f"{err}; {_CLEAN_HINT}"
|
||||
) from err
|
||||
if "SECTIONS" not in stdout_text:
|
||||
# A degenerate zero-exit run must not be stamped as a good cache
|
||||
raise EsphomeError(
|
||||
f"Generated linker script is missing its SECTIONS block; {_CLEAN_HINT}"
|
||||
)
|
||||
content = _apply_surgery(build_surgery.relocate_ratetable, stdout_text)
|
||||
if CORE.testing_mode:
|
||||
content = _apply_surgery(
|
||||
build_surgery.apply_testing_memory_patches, content, ("iram1_0_seg",)
|
||||
)
|
||||
write_file_if_changed(output, content)
|
||||
if note_persisted:
|
||||
# An unstamped cache re-runs -E next build, re-deriving the
|
||||
# diagnostic the lost note would have re-emitted
|
||||
_write_note(
|
||||
stamp,
|
||||
f"{stamp_content} note={_note_digest()} "
|
||||
f"content={hashlib.sha256(content.encode('utf-8')).hexdigest()}",
|
||||
)
|
||||
elif stderr_note.is_file():
|
||||
# Re-emit cached preprocessor warnings on cache hits
|
||||
try:
|
||||
_LOGGER.warning(
|
||||
"Linker-script preprocessor: %s",
|
||||
stderr_note.read_text(encoding="utf-8"),
|
||||
)
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
_LOGGER.warning(
|
||||
"A cached linker-script preprocessor diagnostic exists at %s "
|
||||
"but could not be read: %s",
|
||||
stderr_note,
|
||||
err,
|
||||
)
|
||||
|
||||
if CORE.testing_mode:
|
||||
_generate_testing_flash_ld(framework, ld_dir, flash_ld_name)
|
||||
|
||||
|
||||
def _generate_testing_flash_ld(
|
||||
framework: Path, ld_dir: Path, flash_ld_name: str
|
||||
) -> None:
|
||||
"""A patched copy of the flash ld in the build dir; resolved through the
|
||||
same -L path as the SDK original it shadows."""
|
||||
flash_ld = _sdk_ld_dir(framework) / flash_ld_name
|
||||
try:
|
||||
flash_ld_text = flash_ld.read_text(encoding="utf-8")
|
||||
except OSError as err:
|
||||
# Same half-extracted-cache hazard as the preprocessor spawn
|
||||
raise EsphomeError(f"Could not read {flash_ld}: {err}; {_CLEAN_HINT}") from err
|
||||
patched_flash_ld = _apply_surgery(
|
||||
build_surgery.apply_testing_memory_patches,
|
||||
flash_ld_text,
|
||||
("dram0_0_seg", "irom0_0_seg"),
|
||||
)
|
||||
write_file_if_changed(
|
||||
ld_dir / f"{_TESTING_LD_PREFIX}{flash_ld_name}", patched_flash_ld
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
request_http_client,
|
||||
require_certificate_bundle,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
@@ -334,8 +334,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_http_client")
|
||||
request_http_client()
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ from .const import (
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_FULL_CERT_BUNDLE,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_MBEDTLS_SDKCONFIG,
|
||||
KEY_NETWORK_SDKCONFIG,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
@@ -225,7 +226,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
|
||||
"console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured
|
||||
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
|
||||
"esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf
|
||||
"esp-tls", # TLS wrapper - re-included by request_tls()
|
||||
"esp_adc", # ADC driver - only needed by adc component
|
||||
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
|
||||
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
|
||||
@@ -747,6 +748,48 @@ def request_software_coexistence() -> None:
|
||||
include_builtin_idf_component("esp_coex")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MbedtlsSdkconfigData:
|
||||
"""Inputs for the mbedTLS sdkconfig flags, reconciled at FINAL.
|
||||
|
||||
Components call request_tls() / require_mbedtls_*() instead of writing the
|
||||
CONFIG_MBEDTLS_* flags directly; _reconcile_mbedtls_sdkconfig() decides the
|
||||
final values once every to_code has run.
|
||||
"""
|
||||
|
||||
tls_required: bool = False # TLS/DTLS handshake user
|
||||
ecp_required: bool = False # ECDH/ECDSA without TLS (openthread SRP host key)
|
||||
peer_cert_required: bool = False # keep the peer certificate after the handshake
|
||||
pkcs7_required: bool = False # PKCS#7 parsing
|
||||
sha512_required: bool = False # SHA-384/SHA-512
|
||||
# esp32 advanced disable_mbedtls_peer_cert / disable_mbedtls_pkcs7 options
|
||||
disable_peer_cert: bool = True
|
||||
disable_pkcs7: bool = True
|
||||
|
||||
|
||||
def _mbedtls_sdkconfig() -> MbedtlsSdkconfigData:
|
||||
data = CORE.data[KEY_ESP32]
|
||||
if KEY_MBEDTLS_SDKCONFIG not in data:
|
||||
data[KEY_MBEDTLS_SDKCONFIG] = MbedtlsSdkconfigData()
|
||||
return data[KEY_MBEDTLS_SDKCONFIG]
|
||||
|
||||
|
||||
def request_tls() -> None:
|
||||
"""Request the mbedTLS TLS stack and the esp-tls wrapper.
|
||||
|
||||
Without a request TLS and its ECP/PEM-write/CRL/CSR crypto compile out;
|
||||
hashes, AES and RSA stay available.
|
||||
"""
|
||||
_mbedtls_sdkconfig().tls_required = True
|
||||
include_builtin_idf_component("esp-tls")
|
||||
|
||||
|
||||
def request_http_client() -> None:
|
||||
"""Request ESP-IDF's HTTP client; it links esp_tls even for plain http."""
|
||||
include_builtin_idf_component("esp_http_client")
|
||||
request_tls()
|
||||
|
||||
|
||||
def add_idf_component(
|
||||
*,
|
||||
name: str,
|
||||
@@ -1744,10 +1787,7 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
|
||||
# Feature requirement tracking - components can call require_* functions to re-enable
|
||||
# These are stored in CORE.data[KEY_ESP32] dict
|
||||
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED = "libc_picolibc_newlib_compat_required"
|
||||
|
||||
@@ -1786,6 +1826,8 @@ def require_certificate_bundle() -> None:
|
||||
certificates (http_request, audio streaming) call this so the bundle is
|
||||
compiled and gen_crt_bundle runs only when something uses it.
|
||||
"""
|
||||
# esp_crt_bundle.c calls mbedtls_ssl_conf_*, so a bundle always needs TLS.
|
||||
request_tls()
|
||||
CORE.data[KEY_ESP32][KEY_CERT_BUNDLE] = True
|
||||
|
||||
|
||||
@@ -1811,33 +1853,32 @@ def require_usb_serial_jtag_secondary() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_peer_cert() -> None:
|
||||
"""Mark that mbedTLS peer certificate retention is required by a component.
|
||||
def require_mbedtls_ecp() -> None:
|
||||
"""Keep mbedTLS elliptic curve support (ECDH/ECDSA) without requesting TLS.
|
||||
|
||||
Call this from components that need access to the peer certificate after
|
||||
the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
|
||||
from being disabled.
|
||||
Call this from components that sign or verify with ECDSA outside a TLS
|
||||
handshake (openthread's SRP host key). WiFi, Bluetooth and secure boot
|
||||
select it through Kconfig on their own.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True
|
||||
_mbedtls_sdkconfig().ecp_required = True
|
||||
|
||||
|
||||
def require_mbedtls_peer_cert() -> None:
|
||||
"""Keep the peer certificate after the TLS handshake (CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE).
|
||||
|
||||
A user sdkconfig_options value takes precedence.
|
||||
"""
|
||||
_mbedtls_sdkconfig().peer_cert_required = True
|
||||
|
||||
|
||||
def require_mbedtls_pkcs7() -> None:
|
||||
"""Mark that mbedTLS PKCS#7 support is required by a component.
|
||||
|
||||
Call this from components that need PKCS#7 certificate validation.
|
||||
This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||
"""Keep mbedTLS PKCS#7 support (CONFIG_MBEDTLS_PKCS7_C). A user sdkconfig_options value takes precedence."""
|
||||
_mbedtls_sdkconfig().pkcs7_required = True
|
||||
|
||||
|
||||
def require_mbedtls_sha512() -> None:
|
||||
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
||||
|
||||
Call this from components that need to verify TLS certificates or signatures
|
||||
using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C
|
||||
and CONFIG_MBEDTLS_SHA512_C from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True
|
||||
"""Keep mbedTLS SHA-384/SHA-512 (CONFIG_MBEDTLS_SHA384_C / CONFIG_MBEDTLS_SHA512_C)."""
|
||||
_mbedtls_sdkconfig().sha512_required = True
|
||||
|
||||
|
||||
def idf_version() -> cv.Version:
|
||||
@@ -2302,6 +2343,81 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||
|
||||
|
||||
# User sdkconfig_options that mean "keep TLS on" when set to y.
|
||||
_MBEDTLS_TLS_ON_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_TLS_ENABLED",
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
|
||||
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
|
||||
)
|
||||
# Any user option under these prefixes only makes sense with TLS compiled in.
|
||||
_TLS_OPTION_PREFIXES = ("CONFIG_ESP_TLS_", "CONFIG_MBEDTLS_SSL_", "CONFIG_ESP_HTTPS_")
|
||||
|
||||
|
||||
def _user_sdkconfig_wants_tls(options: dict[str, Any]) -> bool:
|
||||
"""True when sdkconfig_options turn TLS on or tune something under it; an `n` is never a request."""
|
||||
return any(
|
||||
(name in _MBEDTLS_TLS_ON_OPTIONS and value == "y")
|
||||
or (name == "CONFIG_MBEDTLS_TLS_DISABLED" and value == "n")
|
||||
or (name.startswith(_TLS_OPTION_PREFIXES) and value != "n")
|
||||
for name, value in options.items()
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_mbedtls_sdkconfig() -> None:
|
||||
"""Reconcile the mbedTLS sdkconfig flags after every request_tls() / require_mbedtls_*() call.
|
||||
|
||||
mbedtls cannot be excluded from an IDF build (bootloader_support needs its
|
||||
SHA-256), but with no TLS user the ssl_*.c sources and the TLS-only crypto
|
||||
compile to empty objects. User sdkconfig_options win.
|
||||
"""
|
||||
data = _mbedtls_sdkconfig()
|
||||
idf6 = idf_version() >= cv.Version(6, 0, 0)
|
||||
# A component that re-includes esp-tls on its own (external components
|
||||
# predating request_tls()) wants TLS just as much as a request_tls() call.
|
||||
tls_required = (
|
||||
data.tls_required
|
||||
or "esp-tls" not in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
)
|
||||
|
||||
if not CORE.using_arduino and not tls_required:
|
||||
# IDF 6 made CONFIG_MBEDTLS_TLS_ENABLED a normal bool; on IDF 5 it has
|
||||
# no prompt and is only reachable through the "None" TLS role choice.
|
||||
if idf6:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_ENABLED", False)
|
||||
else:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_DISABLED", True)
|
||||
# Enterprise WiFi selects TLS back on; wifi writes this itself, but
|
||||
# esp_wifi can also be in the build without a wifi: block (openthread).
|
||||
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
|
||||
# WiFi (ESP_WIFI_MBEDTLS_CRYPTO), Bluetooth and signed apps
|
||||
# (SECURE_SIGNED_APPS) select ECP back on through Kconfig.
|
||||
if not data.ecp_required:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_ECP_C", False)
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PEM_WRITE_C", False)
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CRL_PARSE_C", False)
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CSR_PARSE_C", False)
|
||||
|
||||
# Keeping the peer certificate costs ~4KB heap per connection.
|
||||
if data.peer_cert_required:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
|
||||
elif data.disable_peer_cert:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
|
||||
|
||||
if data.pkcs7_required:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", True)
|
||||
elif data.disable_pkcs7:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", False)
|
||||
|
||||
# SHA-384 shares the SHA-512 compression function, so both go together.
|
||||
# Only IDF 6.0's PSA engine links a ~3KB software fallback for them; on
|
||||
# IDF 5 they are a single hardware-only option with no code size cost.
|
||||
if idf6 and not data.sha512_required:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA384_C", False)
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA512_C", False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_network_sdkconfig() -> None:
|
||||
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
|
||||
@@ -2950,38 +3066,6 @@ async def to_code(config):
|
||||
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
|
||||
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
|
||||
|
||||
# Disable keeping peer certificate after TLS handshake
|
||||
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
|
||||
# Components that need it can call require_mbedtls_peer_cert()
|
||||
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
|
||||
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
|
||||
|
||||
# Disable PKCS#7 support in mbedTLS
|
||||
# Only needed for specific certificate validation scenarios
|
||||
# Components that need it can call require_mbedtls_pkcs7()
|
||||
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
|
||||
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
|
||||
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
|
||||
|
||||
# Disable SHA-384 and SHA-512 in mbedTLS
|
||||
# ESPHome doesn't use either algorithm. SHA-384 shares the same
|
||||
# compression function as SHA-512 (mbedtls_internal_sha512_process),
|
||||
# so both must be disabled to eliminate the ~3KB software fallback
|
||||
# that IDF 6.0's PSA parallel engine always links in.
|
||||
# On IDF < 6.0 these are a single config and hardware-only (no
|
||||
# software fallback), so there was no code size cost to leaving
|
||||
# them enabled.
|
||||
# Components that need SHA-384/SHA-512 can call require_mbedtls_sha512()
|
||||
if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get(
|
||||
KEY_MBEDTLS_SHA512_REQUIRED, False
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
|
||||
|
||||
# FINAL priority: runs after every require_libc_picolibc_newlib_compat() call
|
||||
CORE.add_job(_set_libc_picolibc_newlib_compat)
|
||||
|
||||
@@ -2991,6 +3075,12 @@ async def to_code(config):
|
||||
# FINAL priority: runs after every require_certificate_bundle() call
|
||||
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
|
||||
|
||||
# FINAL priority: runs after every request_tls() / require_mbedtls_*() call
|
||||
mbedtls = _mbedtls_sdkconfig()
|
||||
mbedtls.disable_peer_cert = advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]
|
||||
mbedtls.disable_pkcs7 = advanced[CONF_DISABLE_MBEDTLS_PKCS7]
|
||||
CORE.add_job(_reconcile_mbedtls_sdkconfig)
|
||||
|
||||
# FINAL: require_*() calls can come from to_code at or below this priority, so an
|
||||
# inline read would be iteration-order-dependent; reconcile once after every job ran.
|
||||
CORE.add_job(
|
||||
@@ -3022,6 +3112,8 @@ async def to_code(config):
|
||||
# so it still gets the CMN variant pinned.
|
||||
if conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE") == "y":
|
||||
require_certificate_bundle()
|
||||
if _user_sdkconfig_wants_tls(conf[CONF_SDKCONFIG_OPTIONS]):
|
||||
request_tls()
|
||||
|
||||
# Components from YAML are added in a separate coroutine with FINAL priority
|
||||
# Schedule it to run after all other components
|
||||
|
||||
@@ -23,6 +23,7 @@ KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||
KEY_CERT_BUNDLE = "cert_bundle"
|
||||
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
|
||||
KEY_NETWORK_SDKCONFIG = "network_sdkconfig"
|
||||
KEY_MBEDTLS_SDKCONFIG = "mbedtls_sdkconfig"
|
||||
|
||||
VARIANT_ESP32 = "ESP32"
|
||||
VARIANT_ESP32C2 = "ESP32C2"
|
||||
|
||||
@@ -202,11 +202,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_watchdog_timeout(timeout_ms))
|
||||
|
||||
if CORE.is_esp32:
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time).
|
||||
# esp-tls is re-enabled too because http_request includes <esp_tls.h>
|
||||
# directly and esp_http_client only pulls it in as a private dependency.
|
||||
esp32.include_builtin_idf_component("esp_http_client")
|
||||
esp32.include_builtin_idf_component("esp-tls")
|
||||
esp32.request_http_client()
|
||||
|
||||
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
|
||||
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
idf_version,
|
||||
include_builtin_idf_component,
|
||||
request_tls,
|
||||
)
|
||||
from esphome.config_helpers import (
|
||||
filter_source_files_from_defines,
|
||||
@@ -364,8 +365,8 @@ async def to_code(config):
|
||||
add_idf_component(name="espressif/mqtt", ref="1.0.0")
|
||||
else:
|
||||
include_builtin_idf_component("mqtt")
|
||||
# mqtt_client.h drags in esp_tls types; esp-tls is excluded by default
|
||||
include_builtin_idf_component("esp-tls")
|
||||
# esp-mqtt links transport_ssl.c (esp_tls) even for plain MQTT
|
||||
request_tls()
|
||||
|
||||
cg.add_define("USE_MQTT")
|
||||
cg.add_global(mqtt_ns.using)
|
||||
|
||||
@@ -284,10 +284,7 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
if CORE.is_esp32:
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||
# and esp-tls, whose sdkconfig options below need the component present
|
||||
esp32.include_builtin_idf_component("esp_http_client")
|
||||
esp32.include_builtin_idf_component("esp-tls")
|
||||
esp32.request_http_client()
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
only_on_variant,
|
||||
require_mbedtls_ecp,
|
||||
require_vfs_select,
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
@@ -282,6 +283,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Re-enable openthread IDF component (excluded by default)
|
||||
if CORE.is_esp32:
|
||||
include_builtin_idf_component("openthread")
|
||||
# OPENTHREAD_CONFIG_ECDSA_ENABLE: the SRP client host key uses mbedtls_ecdsa_*
|
||||
require_mbedtls_ecp()
|
||||
|
||||
cg.add_define("USE_OPENTHREAD")
|
||||
if config.get(CONF_FORCE_DATASET):
|
||||
|
||||
@@ -17,9 +17,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
|
||||
add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024)
|
||||
# Re-enable esp-tls (excluded by default to save compile time);
|
||||
# web_server_idf.cpp includes <esp_tls_crypto.h> for digest auth
|
||||
include_builtin_idf_component("esp-tls")
|
||||
# Re-enable ESP-IDF's HTTP server (excluded by default to save compile time).
|
||||
# Basic auth uses mbedtls_base64_encode directly, so no TLS stack is needed.
|
||||
include_builtin_idf_component("esp_http_server")
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "esp_tls_crypto.h"
|
||||
#include <mbedtls/base64.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
@@ -544,14 +544,14 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
|
||||
constexpr size_t max_digest_len = 350;
|
||||
char digest[max_digest_len];
|
||||
size_t out;
|
||||
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
|
||||
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
|
||||
mbedtls_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
|
||||
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
|
||||
|
||||
// Constant-time comparison to avoid timing side channels.
|
||||
// No early return on length mismatch — the length difference is folded
|
||||
// into the accumulator so any mismatch is rejected.
|
||||
const char *provided = auth_str + auth_prefix_len;
|
||||
size_t digest_len = out; // length from esp_crypto_base64_encode
|
||||
size_t digest_len = out;
|
||||
// Derive provided_len from the already-sized std::string rather than
|
||||
// rescanning with strlen (avoids attacker-controlled scan length).
|
||||
size_t provided_len = auth.value().size() - auth_prefix_len;
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.components.esp32 import (
|
||||
const,
|
||||
get_esp32_variant,
|
||||
only_on_variant,
|
||||
request_tls,
|
||||
request_wifi,
|
||||
)
|
||||
from esphome.components.network import (
|
||||
@@ -658,6 +659,9 @@ async def to_code(config):
|
||||
# Disable Enterprise WiFi support if no EAP is configured
|
||||
if CORE.is_esp32:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
|
||||
# The supplicant's Kconfig select cannot override the IDF 5 TLS role choice
|
||||
if has_eap:
|
||||
request_tls()
|
||||
|
||||
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
|
||||
if has_manual_ip:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-c6-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
openthread:
|
||||
channel: 13
|
||||
network_name: OpenThread-8f28
|
||||
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
|
||||
pan_id: 0x8f28
|
||||
ext_pan_id: 0xd63e8e3e495ebbc3
|
||||
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
|
||||
mesh_local_prefix: fd53:145f:ed22:ad81::/64
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_ESP_TLS_INSECURE: y
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE: n
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_TLS_DISABLED: n
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_TLS_ENABLED: n
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_TLS_CLIENT_ONLY: y
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: "test_ssid"
|
||||
eap:
|
||||
username: username
|
||||
password: password
|
||||
identity: identity
|
||||
@@ -16,15 +16,19 @@ from esphome.components.esp32 import (
|
||||
KEY_VFS_TERMIOS_REQUIRED,
|
||||
VARIANT_ESP32,
|
||||
VARIANTS,
|
||||
MbedtlsSdkconfigData,
|
||||
NetworkSdkconfigData,
|
||||
RawSdkconfigValue,
|
||||
_ota_downgrade_protection_errors,
|
||||
_reconcile_mbedtls_sdkconfig,
|
||||
_reconcile_network_sdkconfig,
|
||||
_reconcile_vfs_fatfs_sdkconfig,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_ESP32,
|
||||
KEY_EXCLUDE_COMPONENTS,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_MBEDTLS_SDKCONFIG,
|
||||
KEY_NETWORK_SDKCONFIG,
|
||||
KEY_SDKCONFIG_OPTIONS,
|
||||
KEY_VARIANT,
|
||||
@@ -272,8 +276,8 @@ def test_esp32_configuration_errors(
|
||||
("esp_driver_i2c", "esp_driver_ledc", "esp_driver_gptimer"),
|
||||
id="i2c_ledc_ac_dimmer",
|
||||
),
|
||||
# esp-tls has three owners; a per-owner config makes a dropped
|
||||
# re-include from any single one fail the test.
|
||||
# esp-tls comes back through request_tls(); a per-owner config makes
|
||||
# a dropped request from any single one fail the test.
|
||||
pytest.param(
|
||||
"exclusion_reincludes_http_request.yaml",
|
||||
("esp-tls", "esp_http_client"),
|
||||
@@ -287,8 +291,9 @@ def test_esp32_configuration_errors(
|
||||
id="mqtt",
|
||||
),
|
||||
pytest.param(
|
||||
# Basic auth uses mbedtls_base64_encode directly, so no esp-tls.
|
||||
"exclusion_reincludes_web_server.yaml",
|
||||
("esp-tls", "esp_http_server"),
|
||||
("esp_http_server",),
|
||||
id="web_server_idf",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -430,6 +435,221 @@ def test_user_sdkconfig_certificate_bundle_wins(
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL") is False
|
||||
|
||||
|
||||
_TLS_OFF_CRYPTO = {
|
||||
"CONFIG_MBEDTLS_ECP_C": False,
|
||||
"CONFIG_MBEDTLS_PEM_WRITE_C": False,
|
||||
"CONFIG_MBEDTLS_X509_CRL_PARSE_C": False,
|
||||
"CONFIG_MBEDTLS_X509_CSR_PARSE_C": False,
|
||||
}
|
||||
_TLS_OFF_IDF5 = {
|
||||
"CONFIG_MBEDTLS_TLS_DISABLED": True,
|
||||
"CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT": False,
|
||||
**_TLS_OFF_CRYPTO,
|
||||
}
|
||||
_TLS_OFF_IDF6 = {
|
||||
"CONFIG_MBEDTLS_TLS_ENABLED": False,
|
||||
"CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT": False,
|
||||
**_TLS_OFF_CRYPTO,
|
||||
}
|
||||
_PEER_CERT_PKCS7_OFF = {
|
||||
"CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE": False,
|
||||
"CONFIG_MBEDTLS_PKCS7_C": False,
|
||||
}
|
||||
_IDF5 = cv.Version(5, 5, 5)
|
||||
_IDF6 = cv.Version(6, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("framework", "idf", "data", "preset", "expected", "excluded"),
|
||||
[
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(),
|
||||
{},
|
||||
{**_TLS_OFF_IDF5, **_PEER_CERT_PKCS7_OFF},
|
||||
{"esp-tls"},
|
||||
id="idf5_no_tls_user",
|
||||
),
|
||||
pytest.param(
|
||||
# An external component that only re-included esp-tls keeps TLS.
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(),
|
||||
{},
|
||||
_PEER_CERT_PKCS7_OFF,
|
||||
set(),
|
||||
id="idf_esp_tls_reincluded",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF6,
|
||||
MbedtlsSdkconfigData(),
|
||||
{},
|
||||
{
|
||||
**_TLS_OFF_IDF6,
|
||||
**_PEER_CERT_PKCS7_OFF,
|
||||
"CONFIG_MBEDTLS_SHA384_C": False,
|
||||
"CONFIG_MBEDTLS_SHA512_C": False,
|
||||
},
|
||||
{"esp-tls"},
|
||||
id="idf6_drops_sha512",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF6,
|
||||
MbedtlsSdkconfigData(sha512_required=True),
|
||||
{},
|
||||
{**_TLS_OFF_IDF6, **_PEER_CERT_PKCS7_OFF},
|
||||
{"esp-tls"},
|
||||
id="idf6_sha512_required",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(tls_required=True),
|
||||
{},
|
||||
_PEER_CERT_PKCS7_OFF,
|
||||
{"esp-tls"},
|
||||
id="idf_tls_requested",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(ecp_required=True),
|
||||
{},
|
||||
{
|
||||
"CONFIG_MBEDTLS_TLS_DISABLED": True,
|
||||
"CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT": False,
|
||||
"CONFIG_MBEDTLS_PEM_WRITE_C": False,
|
||||
"CONFIG_MBEDTLS_X509_CRL_PARSE_C": False,
|
||||
"CONFIG_MBEDTLS_X509_CSR_PARSE_C": False,
|
||||
**_PEER_CERT_PKCS7_OFF,
|
||||
},
|
||||
{"esp-tls"},
|
||||
id="idf_ecp_without_tls",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(),
|
||||
{"CONFIG_MBEDTLS_ECP_C": RawSdkconfigValue("y")},
|
||||
{
|
||||
**_TLS_OFF_IDF5,
|
||||
"CONFIG_MBEDTLS_ECP_C": RawSdkconfigValue("y"),
|
||||
**_PEER_CERT_PKCS7_OFF,
|
||||
},
|
||||
{"esp-tls"},
|
||||
id="idf_user_ecp_wins",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(peer_cert_required=True, pkcs7_required=True),
|
||||
{},
|
||||
{
|
||||
**_TLS_OFF_IDF5,
|
||||
"CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE": True,
|
||||
"CONFIG_MBEDTLS_PKCS7_C": True,
|
||||
},
|
||||
{"esp-tls"},
|
||||
id="idf_peer_cert_pkcs7_required",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(disable_peer_cert=False, disable_pkcs7=False),
|
||||
{},
|
||||
_TLS_OFF_IDF5,
|
||||
{"esp-tls"},
|
||||
id="idf_advanced_disables_off",
|
||||
),
|
||||
pytest.param(
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
_IDF5,
|
||||
MbedtlsSdkconfigData(),
|
||||
{},
|
||||
_PEER_CERT_PKCS7_OFF,
|
||||
{"esp-tls"},
|
||||
id="arduino_keeps_tls",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_reconcile_mbedtls_sdkconfig(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
framework: PlatformFramework,
|
||||
idf: cv.Version,
|
||||
data: MbedtlsSdkconfigData,
|
||||
preset: dict[str, Any],
|
||||
expected: dict[str, Any],
|
||||
excluded: set[str],
|
||||
) -> None:
|
||||
"""The FINAL-priority reconciler turns TLS off only when nothing requested it;
|
||||
user sdkconfig_options always win."""
|
||||
set_core_config(framework)
|
||||
CORE.data[KEY_ESP32] = {
|
||||
KEY_IDF_VERSION: idf,
|
||||
KEY_SDKCONFIG_OPTIONS: dict(preset),
|
||||
KEY_MBEDTLS_SDKCONFIG: data,
|
||||
KEY_EXCLUDE_COMPONENTS: excluded,
|
||||
}
|
||||
|
||||
asyncio.run(_reconcile_mbedtls_sdkconfig())
|
||||
|
||||
assert CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "tls_off", "ecp_off"),
|
||||
[
|
||||
pytest.param("network_ethernet_only.yaml", True, True, id="ethernet_api"),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_web_server.yaml", True, True, id="web_server_idf"
|
||||
),
|
||||
# openthread's SRP host key needs ECDSA, so ECP stays while TLS is off
|
||||
pytest.param("tls_openthread_c6.yaml", True, False, id="openthread"),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_http_request.yaml", False, False, id="http_request"
|
||||
),
|
||||
pytest.param("exclusion_reincludes_mqtt.yaml", False, False, id="mqtt"),
|
||||
pytest.param("exclusion_reincludes_nextion.yaml", False, False, id="nextion"),
|
||||
pytest.param("tls_wifi_eap.yaml", False, False, id="wifi_eap"),
|
||||
pytest.param(
|
||||
"certificate_bundle_sdkconfig.yaml", False, False, id="raw_bundle"
|
||||
),
|
||||
pytest.param("tls_sdkconfig_esp_tls.yaml", False, False, id="raw_esp_tls"),
|
||||
pytest.param("tls_sdkconfig_tls_role.yaml", False, False, id="raw_tls_role"),
|
||||
# A role option set to n is not a request.
|
||||
pytest.param(
|
||||
"tls_sdkconfig_tls_enabled_n.yaml", True, True, id="raw_tls_enabled_n"
|
||||
),
|
||||
# Disabling a TLS sub-option is not a request either.
|
||||
pytest.param(
|
||||
"tls_sdkconfig_peer_cert_n.yaml", True, True, id="raw_peer_cert_n"
|
||||
),
|
||||
# CONFIG_MBEDTLS_TLS_DISABLED=n is the IDF 5 way to keep TLS.
|
||||
pytest.param(
|
||||
"tls_sdkconfig_tls_disabled_n.yaml", False, False, id="raw_tls_disabled_n"
|
||||
),
|
||||
# SECURE_SIGNED_APPS selects ECP back on in Kconfig; ESPHome still writes the default.
|
||||
pytest.param("signed_ota_ecdsa256_c6.yaml", True, True, id="signed_ota_ecdsa"),
|
||||
],
|
||||
)
|
||||
def test_tls_disabled_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
tls_off: bool,
|
||||
ecp_off: bool,
|
||||
) -> None:
|
||||
"""TLS is compiled out unless a component or a raw sdkconfig option asks for it."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert (sdkconfig.get("CONFIG_MBEDTLS_TLS_DISABLED") is True) is tls_off
|
||||
assert ("CONFIG_MBEDTLS_ECP_C" in sdkconfig) is ecp_off
|
||||
assert ("esp-tls" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]) is tls_off
|
||||
|
||||
|
||||
def test_execute_from_psram_s3_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user