[esp32] Run clang-tidy via the native ESP-IDF toolchain (#16748)

This commit is contained in:
Jonathan Swoboda
2026-06-04 11:47:42 -04:00
committed by GitHub
parent c765e22622
commit 148a5ba68e
5 changed files with 491 additions and 32 deletions

View File

@@ -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"])

View File

@@ -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")