mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:07:33 +00:00
[esp32] Run clang-tidy via the native ESP-IDF toolchain (#16748)
This commit is contained in:
@@ -1 +1 @@
|
||||
44db8a62d94c8fba83b95b73938db4377ebacc0adb504881387389f1cd8f2f3a
|
||||
0550a8ea4182dbc007660de060dd023ce22c865c8e95040a36f3d07a5b354fc6
|
||||
|
||||
440
esphome/espidf/clang_tidy.py
Normal file
440
esphome/espidf/clang_tidy.py
Normal file
@@ -0,0 +1,440 @@
|
||||
"""Generate clang-tidy idedata via the native ESP-IDF toolchain.
|
||||
|
||||
Produces idedata for clang-tidy **without an ESPHome YAML config**. Instead of
|
||||
running codegen on a config, it generates a minimal ESP-IDF CMake project:
|
||||
|
||||
* the managed-component dependencies come from ESPHome's own
|
||||
``idf_component.yml`` (arduinojson, lvgl, mdns, ...);
|
||||
* the PlatformIO ``lib_deps`` (qr-code, mlx90393, ...) are converted to local
|
||||
IDF components via the ESPHome PlatformIO->IDF converter;
|
||||
* the ``main`` component ``REQUIRES`` every target-available builtin IDF
|
||||
component, so their public include dirs land on the translation unit;
|
||||
* the repo ``sdkconfig.defaults`` enables sdkconfig-gated components (bt, ...).
|
||||
|
||||
then runs ``idf.py reconfigure`` (configure only, no compile) and reads the
|
||||
resulting ``build/compile_commands.json``. The IDF version is the esp32
|
||||
component's recommended version.
|
||||
|
||||
``ESPHOME_IDF_COMPILE_COMMANDS`` may point at an existing build's
|
||||
``compile_commands.json`` to skip generation (fast iteration).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
TIDY_PROJECT_NAME = "esphome_tidy"
|
||||
|
||||
# A do-nothing C++ app: just enough for IDF to configure a valid project. It's
|
||||
# C++ (not C) so the compile command uses the C++ compiler and flags, matching
|
||||
# how clang-tidy analyzes ESPHome's C++ sources.
|
||||
_TIDY_MAIN_CPP = 'extern "C" void app_main() {}\n'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Settings:
|
||||
"""Per-environment build settings derived from the tidy env name.
|
||||
|
||||
The platform defines below are what a real ESPHome build adds via
|
||||
cg.add_define; defines.h only *consumes* them, so without them
|
||||
esphome/core/hal.h errors with "not implemented for this platform".
|
||||
"""
|
||||
|
||||
idf_target: str # esp32, esp32s3, ...
|
||||
variant: str # ESP32, ESP32S3, ...
|
||||
idf_version: str # ESP-IDF version to build with
|
||||
target_framework: str # "espidf" or "arduino"
|
||||
platform_defines: tuple[str, ...]
|
||||
# Extra idf_component.yml deps the framework needs (e.g. arduino-esp32).
|
||||
framework_deps: dict[str, dict]
|
||||
|
||||
|
||||
def _settings_for(environment: str) -> _Settings:
|
||||
"""Derive build settings from a ``<target>-<framework>-tidy`` env name.
|
||||
|
||||
Arduino on esp32 is itself a native ESP-IDF build with the
|
||||
``espressif/arduino-esp32`` component added, so both frameworks use this
|
||||
path -- only the defines, IDF version, and that one component differ.
|
||||
"""
|
||||
from esphome.components.esp32 import (
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP,
|
||||
ARDUINO_IDF_VERSION_LOOKUP,
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP,
|
||||
)
|
||||
|
||||
parts = environment.split("-")
|
||||
if len(parts) != 3 or parts[2] != "tidy" or parts[1] not in ("idf", "arduino"):
|
||||
raise ValueError(
|
||||
f"Unsupported clang-tidy environment {environment!r}: expected "
|
||||
"<target>-<framework>-tidy with framework 'idf' or 'arduino' "
|
||||
"(e.g. esp32-idf-tidy, esp32s3-arduino-tidy)"
|
||||
)
|
||||
idf_target, framework, _ = parts
|
||||
variant = idf_target.upper()
|
||||
# Defines shared by both frameworks. ESPHOME_LOG_LEVEL must be set up front
|
||||
# (as the PlatformIO tidy build_flags do) -- otherwise log.h's ``#ifndef``
|
||||
# sets it to NONE before defines.h redefines it, a macro-redefined warning
|
||||
# across nearly every source.
|
||||
common_defines = (
|
||||
"USE_ESP32",
|
||||
f"USE_ESP32_VARIANT_{variant}",
|
||||
"ESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE",
|
||||
)
|
||||
|
||||
if framework == "arduino":
|
||||
fw_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
|
||||
return _Settings(
|
||||
idf_target=idf_target,
|
||||
variant=variant,
|
||||
idf_version=str(ARDUINO_IDF_VERSION_LOOKUP[fw_version]),
|
||||
target_framework="arduino",
|
||||
platform_defines=(
|
||||
*common_defines,
|
||||
"USE_ARDUINO",
|
||||
"USE_ESP32_FRAMEWORK_ARDUINO",
|
||||
),
|
||||
framework_deps=_arduino_framework_deps(str(fw_version)),
|
||||
)
|
||||
return _Settings(
|
||||
idf_target=idf_target,
|
||||
variant=variant,
|
||||
idf_version=str(ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]),
|
||||
target_framework="espidf",
|
||||
platform_defines=(
|
||||
*common_defines,
|
||||
"USE_ESP_IDF",
|
||||
"USE_ESP32_FRAMEWORK_ESP_IDF",
|
||||
),
|
||||
framework_deps={},
|
||||
)
|
||||
|
||||
|
||||
def _arduino_framework_deps(version: str) -> dict[str, dict]:
|
||||
"""Arduino-only managed deps merged on top of esphome/idf_component.yml.
|
||||
|
||||
arduino-esp32 provides Arduino.h and the arduino libraries; its version is
|
||||
the recommended arduino framework version so the tidy build matches what
|
||||
ESPHome ships.
|
||||
"""
|
||||
from esphome.components.esp32 import ARDUINO_ESP32_COMPONENT_NAME
|
||||
|
||||
return {ARDUINO_ESP32_COMPONENT_NAME: {"version": version}}
|
||||
|
||||
|
||||
_TOP_CMAKELISTS = """\
|
||||
# Auto-generated by ESPHome (clang-tidy idedata project)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
set(IDF_TARGET {idf_target})
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
{compile_options}
|
||||
project({name})
|
||||
"""
|
||||
|
||||
_MAIN_CMAKELISTS = """\
|
||||
# Auto-generated by ESPHome (clang-tidy idedata project)
|
||||
idf_component_register(
|
||||
SRCS "tidy.cpp"
|
||||
REQUIRES {requires}
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _setup_core(work_dir: Path, settings: _Settings) -> None:
|
||||
"""Point CORE at the tidy project + IDF version, without any YAML config."""
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION, KEY_VARIANT
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM
|
||||
from esphome.core import CORE
|
||||
|
||||
CORE.name = TIDY_PROJECT_NAME
|
||||
# config_path's parent is the data dir root: the IDF install lives at
|
||||
# ``<parent>/.esphome/idf`` -- keep it beside (not inside) the per-run
|
||||
# project dir so clearing the project doesn't force an IDF re-download.
|
||||
CORE.config_path = work_dir.parent / "tidy.yaml"
|
||||
CORE.build_path = work_dir
|
||||
esp32 = CORE.data.setdefault(KEY_ESP32, {})
|
||||
esp32[KEY_IDF_VERSION] = cv.Version.parse(settings.idf_version)
|
||||
esp32[KEY_VARIANT] = settings.variant
|
||||
# The target framework drives the PlatformIO-library -> IDF-component
|
||||
# converter and ESPHome's CORE.using_arduino / using_esp_idf helpers.
|
||||
CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = "esp32"
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = settings.target_framework
|
||||
|
||||
|
||||
# Special IDF "components" that are tools/subprojects, not requirable by an app
|
||||
# (they provide no public includes and break requirement resolution), plus our
|
||||
# own ``main``.
|
||||
_NON_REQUIRABLE_COMPONENTS = frozenset(
|
||||
{"bootloader", "esptool_py", "partition_table", "main"}
|
||||
)
|
||||
|
||||
|
||||
def _parse_lib_deps(platformio_ini: Path, framework: str):
|
||||
"""Parse the framework's ``lib_deps`` from platformio.ini into Library specs.
|
||||
|
||||
These are the PlatformIO libraries ESPHome components pull in via
|
||||
``cg.add_library``. The set is framework-specific: the arduino envs add
|
||||
libs (FastLED, NeoPixelBus, MideaUART, ...) the idf envs don't. We read the
|
||||
relevant ``[common*]`` sections directly (resolving the env's ``extends``
|
||||
chain) and skip the ``${...}`` cross-references and non-library entries.
|
||||
"""
|
||||
import configparser
|
||||
|
||||
from esphome.core import Library
|
||||
|
||||
parser = configparser.ConfigParser(interpolation=None, strict=False)
|
||||
parser.read(platformio_ini)
|
||||
|
||||
sections = [("common", "lib_deps_base"), ("common", "lib_deps")]
|
||||
if framework == "arduino":
|
||||
sections += [
|
||||
("common:arduino", "lib_deps"),
|
||||
("common:esp32-arduino", "lib_deps"),
|
||||
]
|
||||
else:
|
||||
sections += [
|
||||
("common:idf", "lib_deps"),
|
||||
("common:esp32-idf", "lib_deps"),
|
||||
]
|
||||
|
||||
tokens: list[str] = []
|
||||
for section, key in sections:
|
||||
if parser.has_option(section, key):
|
||||
tokens += parser.get(section, key).splitlines()
|
||||
|
||||
libs: list[Library] = []
|
||||
seen: set[str] = set()
|
||||
for token in tokens:
|
||||
token = token.split(";", 1)[0].strip() # drop trailing ; comment
|
||||
# Skip blanks, ${...} cross-refs, and +<...> source filters.
|
||||
if not token or token.startswith(("${", "+<")) or token in seen:
|
||||
continue
|
||||
seen.add(token)
|
||||
if "://" in token or ".git" in token:
|
||||
libs.append(Library(token, None, token)) # git repository (with #ref)
|
||||
elif "@" in token:
|
||||
name, _, version = token.partition("@")
|
||||
libs.append(Library(name, version))
|
||||
# A bare name (SPI, Wire, WiFi, Networking, "ESP32 Async UDP", ...) is an
|
||||
# Arduino framework built-in provided by arduino-esp32, not a convertible
|
||||
# registry library (no owner/version), so skip it.
|
||||
return libs
|
||||
|
||||
|
||||
def _convert_pio_libs(
|
||||
platformio_ini: Path, framework: str
|
||||
) -> dict[str, dict[str, str]]:
|
||||
"""Convert the PlatformIO libs to IDF components; return manifest deps.
|
||||
|
||||
Returns a mapping suitable for an ``idf_component.yml`` ``dependencies``
|
||||
block (``{name: {"override_path": <converted component dir>}}``), reusing
|
||||
ESPHome's own PlatformIO->IDF converter (registry/git resolution, no pio).
|
||||
|
||||
The whole library set is resolved as a single batch so a shared transitive
|
||||
dependency (e.g. esphome/libsodium pulled by both noise-c and esp_wireguard)
|
||||
is deduplicated to one component instead of clashing override_path entries.
|
||||
"""
|
||||
from esphome.espidf.component import generate_idf_components
|
||||
|
||||
libraries = _parse_lib_deps(platformio_ini, framework)
|
||||
deps: dict[str, dict[str, str]] = {}
|
||||
for component in generate_idf_components(libraries):
|
||||
deps[component.get_sanitized_name()] = {"override_path": str(component.path)}
|
||||
return deps
|
||||
|
||||
|
||||
def _arduino_excluded_stubs(work_dir: Path) -> dict[str, dict]:
|
||||
"""Stub the arduino-bundled IDF components ESPHome doesn't use.
|
||||
|
||||
arduino-esp32 declares deps (libsodium, RainMaker, modbus, ...) that ESPHome
|
||||
replaces with its own library (noise-c) or doesn't use; point each at an
|
||||
empty override_path component so the IDF manager doesn't resolve/download
|
||||
them -- notably so ``espressif/libsodium`` doesn't clash with the converted
|
||||
noise-c's ``libsodium``. Mirrors esp32's ``_write_idf_component_yml``.
|
||||
|
||||
Components ESPHome's own idf_component.yml provides (e.g. lan867x for
|
||||
ethernet) are NOT stubbed -- those are real deps we need, and arduino-esp32
|
||||
resolves to the same component rather than conflicting.
|
||||
"""
|
||||
import yaml
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
ARDUINO_EXCLUDED_IDF_COMPONENTS,
|
||||
_idf_component_dep_name,
|
||||
_idf_component_stub_name,
|
||||
)
|
||||
|
||||
esphome_dir = Path(__file__).resolve().parent.parent
|
||||
base_manifest = yaml.safe_load(
|
||||
(esphome_dir / "idf_component.yml").read_text(encoding="utf-8")
|
||||
)
|
||||
esphome_deps = set(base_manifest.get("dependencies") or {})
|
||||
|
||||
stubs_dir = work_dir / "component_stubs"
|
||||
stubs_dir.mkdir(parents=True, exist_ok=True)
|
||||
deps: dict[str, dict] = {}
|
||||
for component in sorted(ARDUINO_EXCLUDED_IDF_COMPONENTS):
|
||||
if _idf_component_dep_name(component) in esphome_deps:
|
||||
continue # ESPHome needs this one for real (don't stub it away)
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component)
|
||||
stub_path.mkdir(exist_ok=True)
|
||||
(stub_path / "CMakeLists.txt").write_text(
|
||||
"idf_component_register()\n", encoding="utf-8"
|
||||
)
|
||||
deps[_idf_component_dep_name(component)] = {
|
||||
"version": "*",
|
||||
"override_path": str(stub_path),
|
||||
}
|
||||
return deps
|
||||
|
||||
|
||||
def _write_tidy_project(
|
||||
work_dir: Path,
|
||||
requires: list[str],
|
||||
extra_deps: dict[str, dict[str, str]],
|
||||
settings: _Settings,
|
||||
) -> None:
|
||||
"""Generate the minimal IDF CMake project (top + main + idf_component.yml)."""
|
||||
main_dir = work_dir / "main"
|
||||
main_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
compile_options = "\n".join(
|
||||
f'idf_build_set_property(COMPILE_OPTIONS "-D{define}" APPEND)'
|
||||
for define in settings.platform_defines
|
||||
)
|
||||
(work_dir / "CMakeLists.txt").write_text(
|
||||
_TOP_CMAKELISTS.format(
|
||||
name=TIDY_PROJECT_NAME,
|
||||
compile_options=compile_options,
|
||||
idf_target=settings.idf_target,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(main_dir / "CMakeLists.txt").write_text(
|
||||
_MAIN_CMAKELISTS.format(requires=" ".join(requires)), encoding="utf-8"
|
||||
)
|
||||
(main_dir / "tidy.cpp").write_text(_TIDY_MAIN_CPP, encoding="utf-8")
|
||||
|
||||
# Managed components: ESPHome's own manifest (arduinojson, lvgl, mdns, ...),
|
||||
# plus the converted PlatformIO libs as local (override_path) deps. Placing
|
||||
# it in main/ makes every dep a requirement of the main component, so their
|
||||
# public includes land on the tidy translation unit.
|
||||
import yaml
|
||||
|
||||
esphome_dir = Path(__file__).resolve().parent.parent # esphome/espidf -> esphome
|
||||
manifest = yaml.safe_load(
|
||||
(esphome_dir / "idf_component.yml").read_text(encoding="utf-8")
|
||||
)
|
||||
manifest.setdefault("dependencies", {}).update(extra_deps)
|
||||
(main_dir / "idf_component.yml").write_text(
|
||||
yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
# ESPHome's static-analysis sdkconfig (repo root): enables the flags any
|
||||
# component sets (e.g. CONFIG_BT_ENABLED) so sdkconfig-gated IDF components
|
||||
# register and expose their includes. IDF reads ``sdkconfig.defaults`` from
|
||||
# the project root.
|
||||
(work_dir / "sdkconfig.defaults").write_text(
|
||||
(esphome_dir.parent / "sdkconfig.defaults").read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _generate_compile_commands(
|
||||
work_dir: Path, settings: _Settings, platformio_ini: Path
|
||||
) -> Path:
|
||||
"""Generate the tidy project and run ``idf.py reconfigure`` (no build).
|
||||
|
||||
Two-phase, like a real ESPHome build: a first configure with no builtin
|
||||
requires discovers which components actually register for the target (e.g.
|
||||
``esp_tee`` only registers on c5/c6/h2), then a second configure requires
|
||||
that discovered set so their public includes reach the tidy TU.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from esphome.build_gen.espidf import get_available_components
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
# Surface ESPHome's INFO logs (ESP-IDF framework download/extract/install,
|
||||
# git-library clones) -- they go through logging, which the clang-tidy
|
||||
# script otherwise leaves at WARNING so the first-run downloads look silent.
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
_setup_core(work_dir, settings)
|
||||
|
||||
# Framework deps (e.g. arduino-esp32) + PlatformIO libs converted to local
|
||||
# IDF components, all added to the manifest as deps.
|
||||
extra_deps = dict(settings.framework_deps)
|
||||
extra_deps.update(_convert_pio_libs(platformio_ini, settings.target_framework))
|
||||
if settings.target_framework == "arduino":
|
||||
# Stub the arduino-bundled components ESPHome doesn't use (avoids the
|
||||
# libsodium clash with noise-c and ~26 unused heavy downloads).
|
||||
extra_deps.update(_arduino_excluded_stubs(work_dir))
|
||||
|
||||
# Phase 1: discover the components available for this target.
|
||||
_write_tidy_project(work_dir, [], extra_deps, settings)
|
||||
if toolchain.run_reconfigure() != 0:
|
||||
raise RuntimeError("idf.py reconfigure (discovery) failed")
|
||||
|
||||
requires = sorted(
|
||||
set(get_available_components() or []) - _NON_REQUIRABLE_COMPONENTS
|
||||
)
|
||||
|
||||
# Phase 2: require every available builtin component.
|
||||
_write_tidy_project(work_dir, requires, extra_deps, settings)
|
||||
if toolchain.run_reconfigure() != 0:
|
||||
raise RuntimeError("idf.py reconfigure failed")
|
||||
|
||||
return work_dir / "build" / "compile_commands.json"
|
||||
|
||||
|
||||
def _idedata_from_tidy_project(compile_commands: Path) -> dict:
|
||||
"""Assemble idedata from the single tidy translation unit.
|
||||
|
||||
Unlike a real ESPHome build (many ``/src/esphome/`` TUs unioned), the tidy
|
||||
project has one TU (``main/tidy.cpp``) that -- by requiring every component --
|
||||
already carries the full include set, so we parse it directly.
|
||||
"""
|
||||
import json
|
||||
|
||||
from esphome.espidf.idedata import _get_toolchain_includes, _parse_entry
|
||||
|
||||
entries = json.loads(Path(compile_commands).read_text(encoding="utf-8"))
|
||||
entry = next((e for e in entries if e["file"].endswith("tidy.cpp")), None)
|
||||
if entry is None:
|
||||
raise RuntimeError(f"tidy.cpp not found in {compile_commands}")
|
||||
cxx_path, defines, includes, cxx_flags = _parse_entry(entry)
|
||||
|
||||
return {
|
||||
"cxx_path": cxx_path,
|
||||
"cxx_flags": cxx_flags,
|
||||
"defines": defines,
|
||||
"includes": {
|
||||
"build": includes,
|
||||
"toolchain": _get_toolchain_includes(cxx_path),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_idedata(environment: str, temp_folder: str, platformio_ini: Path) -> dict:
|
||||
if explicit := os.environ.get("ESPHOME_IDF_COMPILE_COMMANDS"):
|
||||
compile_commands = Path(explicit)
|
||||
else:
|
||||
# The tidy env is ``<target>-<framework>-tidy`` (e.g. esp32-idf-tidy,
|
||||
# esp32s3-arduino-tidy); derive the target, variant and framework.
|
||||
settings = _settings_for(environment)
|
||||
# Resolve to an absolute path: ``override_path`` entries in the generated
|
||||
# component manifests are interpreted by the IDF component manager relative
|
||||
# to the manifest's own directory, so a relative work dir would be
|
||||
# mis-resolved (doubled under ``main/``).
|
||||
work_dir = (
|
||||
Path(temp_folder)
|
||||
/ f"idf-tidy-{settings.idf_target}-{settings.target_framework}"
|
||||
).resolve()
|
||||
compile_commands = _generate_compile_commands(
|
||||
work_dir, settings, platformio_ini
|
||||
)
|
||||
|
||||
if not compile_commands.is_file():
|
||||
raise RuntimeError(f"compile_commands.json not found: {compile_commands}")
|
||||
return _idedata_from_tidy_project(compile_commands)
|
||||
@@ -28,6 +28,12 @@ from helpers import (
|
||||
temp_header_file,
|
||||
)
|
||||
|
||||
# Limit the ESP-IDF tool install to esp32 for clang-tidy: the one xtensa-esp-elf
|
||||
# toolchain bundles the s2/s3 compilers too, so all xtensa tidy envs still
|
||||
# reconfigure while the large riscv32-esp-elf toolchain is skipped. Must be set
|
||||
# before esphome.espidf.framework is imported (lazily, via load_idedata).
|
||||
os.environ.setdefault("ESPHOME_IDF_DEFAULT_TARGETS", "esp32")
|
||||
|
||||
|
||||
def clang_options(idedata):
|
||||
cmd = []
|
||||
@@ -52,6 +58,10 @@ def clang_options(idedata):
|
||||
"-mfix-esp32-psram-cache-issue",
|
||||
"-mfix-esp32-psram-cache-strategy=memw",
|
||||
"-fno-tree-switch-conversion",
|
||||
# GCC-only flags emitted by the native ESP-IDF toolchain build
|
||||
"-freorder-blocks",
|
||||
"-fno-jump-tables",
|
||||
"-fno-shrink-wrap",
|
||||
)
|
||||
|
||||
if "zephyr" in triplet:
|
||||
@@ -97,8 +107,20 @@ def clang_options(idedata):
|
||||
]
|
||||
)
|
||||
|
||||
# copy compiler flags, except those clang doesn't understand.
|
||||
cmd.extend(flag for flag in idedata["cxx_flags"] if flag not in omit_flags)
|
||||
# Copy compiler flags, dropping: ones clang doesn't understand; -Werror*
|
||||
# (clang-tidy enforces .clang-tidy's WarningsAsErrors, and a build -Werror
|
||||
# would bypass the -clang-diagnostic-* suppressions); and -std= (the native
|
||||
# ESP-IDF build defaults to gnu++2b, but ESPHome compiles with gnu++20 per
|
||||
# platformio.ini -- analyzing as C++23 flags code that doesn't build under
|
||||
# gnu++20). Force gnu++20 to match the real build.
|
||||
cmd.extend(
|
||||
flag
|
||||
for flag in idedata["cxx_flags"]
|
||||
if flag not in omit_flags
|
||||
and not flag.startswith("-Werror")
|
||||
and not flag.startswith("-std=")
|
||||
)
|
||||
cmd.append("-std=gnu++20")
|
||||
|
||||
# defines
|
||||
cmd.extend(f"-D{define}" for define in idedata["defines"])
|
||||
|
||||
@@ -664,26 +664,22 @@ def load_idedata(environment: str) -> dict[str, Any]:
|
||||
start_time = time.time()
|
||||
print(f"Loading IDE data for environment '{environment}'...")
|
||||
|
||||
platformio_ini = Path(root_path) / "platformio.ini"
|
||||
# Reuse the clang-tidy input hash as the cache key: it already covers every
|
||||
# file baked into the generated idedata (platformio.ini, sdkconfig.defaults,
|
||||
# esphome/idf_component.yml), so this can't drift from that file list. A
|
||||
# content hash -- unlike an mtime comparison -- stays correct across git
|
||||
# checkouts, which don't preserve mtimes.
|
||||
from clang_tidy_hash import calculate_clang_tidy_hash
|
||||
|
||||
temp_idedata = Path(temp_folder) / f"idedata-{environment}.json"
|
||||
changed = False
|
||||
if (
|
||||
not platformio_ini.is_file()
|
||||
or not temp_idedata.is_file()
|
||||
or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime
|
||||
):
|
||||
changed = True
|
||||
temp_hash = Path(temp_folder) / f"idedata-{environment}.hash"
|
||||
|
||||
if "idf" in environment:
|
||||
# remove full sdkconfig when the defaults have changed so that it is regenerated
|
||||
default_sdkconfig = Path(root_path) / "sdkconfig.defaults"
|
||||
temp_sdkconfig = Path(temp_folder) / f"sdkconfig-{environment}"
|
||||
|
||||
if not temp_sdkconfig.is_file():
|
||||
changed = True
|
||||
elif default_sdkconfig.stat().st_mtime >= temp_sdkconfig.stat().st_mtime:
|
||||
temp_sdkconfig.unlink()
|
||||
changed = True
|
||||
cache_key = calculate_clang_tidy_hash()
|
||||
changed = (
|
||||
not temp_idedata.is_file()
|
||||
or not temp_hash.is_file()
|
||||
or temp_hash.read_text().strip() != cache_key
|
||||
)
|
||||
|
||||
if not changed:
|
||||
data = json.loads(temp_idedata.read_text())
|
||||
@@ -694,7 +690,12 @@ def load_idedata(environment: str) -> dict[str, Any]:
|
||||
# ensure temp directory exists before running pio, as it writes sdkconfig to it
|
||||
Path(temp_folder).mkdir(exist_ok=True)
|
||||
|
||||
if "nrf" in environment:
|
||||
platformio_ini = Path(root_path) / "platformio.ini"
|
||||
if "esp32" in environment:
|
||||
from esphome.espidf.clang_tidy import load_idedata as idf_load_idedata
|
||||
|
||||
data = idf_load_idedata(environment, temp_folder, platformio_ini)
|
||||
elif "nrf" in environment:
|
||||
from helpers_zephyr import load_idedata as zephyr_load_idedata
|
||||
|
||||
data = zephyr_load_idedata(environment, temp_folder, platformio_ini)
|
||||
@@ -705,6 +706,7 @@ def load_idedata(environment: str) -> dict[str, Any]:
|
||||
match = re.search(r'{\s*".*}', stdout.decode("utf-8"))
|
||||
data = json.loads(match.group())
|
||||
temp_idedata.write_text(json.dumps(data, indent=2) + "\n")
|
||||
temp_hash.write_text(cache_key + "\n")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"IDE data generated and cached in {elapsed:.2f} seconds")
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
# ESP-IDF sdkconfig defaults used for development purposes only, not used during runtime. Used when PlatformIO is ran
|
||||
# directly from the source directory, e.g. by IDEs or for static analysis (clang-tidy). This should enable all flags
|
||||
# that are set by any component.
|
||||
# ESP-IDF sdkconfig defaults used for development purposes only, not used during runtime. Used for static analysis
|
||||
# (clang-tidy) -- by both the PlatformIO and the native ESP-IDF toolchain paths -- and when PlatformIO is run directly
|
||||
# from the source directory (e.g. by IDEs). This should enable all flags that are set by any component.
|
||||
|
||||
# esp32
|
||||
CONFIG_COMPILER_OPTIMIZATION_DEFAULT=n
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
#CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP=n
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_ESP_TASK_WDT=y
|
||||
CONFIG_ESP_TASK_WDT_INIT=y
|
||||
CONFIG_ESP_TASK_WDT_PANIC=y
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
|
||||
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
|
||||
@@ -18,8 +14,7 @@ CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
|
||||
CONFIG_BT_ENABLED=y
|
||||
|
||||
# esp32_camera
|
||||
CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC=y
|
||||
CONFIG_ESP32_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM=y
|
||||
|
||||
# zigbee
|
||||
CONFIG_ZB_ENABLED=y
|
||||
|
||||
Reference in New Issue
Block a user