[ci] Add ESP32 Variants clang-tidy run (S3/P4/C6) (#16825)

This commit is contained in:
Jonathan Swoboda
2026-06-05 22:03:08 -04:00
committed by GitHub
parent 8aa4157574
commit 6996b7ed1c
13 changed files with 233 additions and 20 deletions

View File

@@ -1 +1 @@
d583091c0f465aed86a825138e309af6d9db6834106ab424f36712424a6c2223 d9c755e5f019b2ecb324834717bc1fb8563e622f5751794cb7156d324884481e

View File

@@ -722,6 +722,93 @@ jobs:
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() if: always()
clang-tidy-esp32-variants:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
# The variant tidy envs install ESP-IDF natively; share the native IDF cache.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
max-parallel: 3
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 S3
options: --environment esp32s3-idf-tidy --grep USE_ESP32_VARIANT_ESP32S3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
# yamllint disable-line rule:line-length
options: --environment esp32c6-idf-tidy --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
# Shared with the IDF/Arduino clang-tidy jobs + native-IDF build (same install).
uses: ./.github/actions/cache-esp-idf
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
# OR the ci-run-all label forced --force-all. Independent of the
# hash check, both must produce a full scan in the job itself.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
elif python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
# Limited variant scan: only the files carrying that variant's code paths
# (no --all-headers; the comprehensive esp32-idf pass covers the shared tree).
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
script/clang-tidy --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --fix --changed ${{ matrix.options }}
fi
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
test-build-components-split: test-build-components-split:
name: Test components batch (${{ matrix.components }}) name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -1273,6 +1360,7 @@ jobs:
- clang-tidy-single - clang-tidy-single
- clang-tidy-nosplit - clang-tidy-nosplit
- clang-tidy-split - clang-tidy-split
- clang-tidy-esp32-variants
- determine-jobs - determine-jobs
- device-builder - device-builder
- test-build-components-split - test-build-components-split

1
.gitignore vendored
View File

@@ -141,6 +141,7 @@ tests/.esphome/
sdkconfig.* sdkconfig.*
!sdkconfig.defaults !sdkconfig.defaults
!sdkconfig.defaults.*
.tests/ .tests/

View File

@@ -64,6 +64,7 @@
#define USE_ESP32_BLE_PSRAM #define USE_ESP32_BLE_PSRAM
#define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_CAMERA_JPEG_CONVERSION
#define USE_ESP32_HOSTED #define USE_ESP32_HOSTED
#define USE_ESP32_HOSTED_HTTP_UPDATE
#define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT #define USE_EVENT
#define USE_FAN #define USE_FAN
@@ -312,6 +313,7 @@
#define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2 #define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2
#define USE_WIFI_RUNTIME_POWER_SAVE #define USE_WIFI_RUNTIME_POWER_SAVE
#define USB_HOST_MAX_REQUESTS 16 #define USB_HOST_MAX_REQUESTS 16
#define USB_HOST_MAX_PACKET_SIZE 64
#define USB_UART_OUTPUT_CHUNK_COUNT 5 #define USB_UART_OUTPUT_CHUNK_COUNT 5
#ifdef USE_ARDUINO #ifdef USE_ARDUINO

View File

@@ -339,11 +339,18 @@ def _write_tidy_project(
# ESPHome's static-analysis sdkconfig (repo root): enables the flags any # ESPHome's static-analysis sdkconfig (repo root): enables the flags any
# component sets (e.g. CONFIG_BT_ENABLED) so sdkconfig-gated IDF components # component sets (e.g. CONFIG_BT_ENABLED) so sdkconfig-gated IDF components
# register and expose their includes. IDF reads ``sdkconfig.defaults`` from # register and expose their includes. IDF reads ``sdkconfig.defaults`` from
# the project root. # the project root, plus a per-target ``sdkconfig.defaults.<idf_target>``
# for variant-only components (e.g. openthread on c6/h2).
repo_root = esphome_dir.parent
(work_dir / "sdkconfig.defaults").write_text( (work_dir / "sdkconfig.defaults").write_text(
(esphome_dir.parent / "sdkconfig.defaults").read_text(encoding="utf-8"), (repo_root / "sdkconfig.defaults").read_text(encoding="utf-8"),
encoding="utf-8", encoding="utf-8",
) )
target_defaults = repo_root / f"sdkconfig.defaults.{settings.idf_target}"
if target_defaults.is_file():
(work_dir / target_defaults.name).write_text(
target_defaults.read_text(encoding="utf-8"), encoding="utf-8"
)
def _generate_compile_commands( def _generate_compile_commands(

View File

@@ -392,6 +392,17 @@ build_flags =
${flags:runtime.build_flags} ${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32P4 -DUSE_ESP32_VARIANT_ESP32P4
[env:esp32p4-idf-tidy]
extends = common:esp32-idf
board = esp32-p4-evboard
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32p4-idf-tidy
build_flags =
${common:esp32-idf.build_flags}
${flags:clangtidy.build_flags}
-DUSE_ESP32_VARIANT_ESP32P4
build_unflags =
${common.build_unflags}
;;;;;;;; ESP32-S2 ;;;;;;;; ;;;;;;;; ESP32-S2 ;;;;;;;;
[env:esp32s2-arduino] [env:esp32s2-arduino]

View File

@@ -99,11 +99,12 @@ def calculate_clang_tidy_hash(repo_root: Path | None = None) -> str:
platformio_content = read_file_bytes(platformio_path) platformio_content = read_file_bytes(platformio_path)
hasher.update(platformio_content) hasher.update(platformio_content)
# Hash sdkconfig.defaults file # Hash sdkconfig.defaults and any per-target sdkconfig.defaults.<target>:
sdkconfig_path = repo_root / "sdkconfig.defaults" # the per-target files flip CONFIG flags that change which variant code
if sdkconfig_path.exists(): # paths clang-tidy sees. Include the filename so a rename is detected.
sdkconfig_content = read_file_bytes(sdkconfig_path) for sdkconfig_path in sorted(repo_root.glob("sdkconfig.defaults*")):
hasher.update(sdkconfig_content) hasher.update(sdkconfig_path.name.encode())
hasher.update(read_file_bytes(sdkconfig_path))
# Hash esphome/idf_component.yml: its managed deps drive the ESP-IDF # Hash esphome/idf_component.yml: its managed deps drive the ESP-IDF
# build's include set, which clang-tidy analyzes. # build's include set, which clang-tidy analyzes.

View File

@@ -15,8 +15,3 @@ CONFIG_BT_ENABLED=y
# esp32_camera # esp32_camera
CONFIG_SPIRAM=y CONFIG_SPIRAM=y
# zigbee
CONFIG_ZB_ENABLED=y
CONFIG_ZB_ZED=y
CONFIG_ZB_RADIO_NATIVE=y

View File

@@ -0,0 +1,14 @@
# Per-target ESP-IDF sdkconfig defaults for esp32c6 static analysis (clang-tidy) only.
# Read by IDF in addition to sdkconfig.defaults. Enables variant-only components so
# their headers register for the tidy translation unit (these are normally set at
# codegen via add_idf_sdkconfig_option, which the stub tidy build skips).
# openthread (only the C6/H2 variants have the 802.15.4 radio)
CONFIG_IEEE802154_ENABLED=y
CONFIG_OPENTHREAD_ENABLED=y
CONFIG_OPENTHREAD_RADIO_NATIVE=y
# zigbee
CONFIG_ZB_ENABLED=y
CONFIG_ZB_ZED=y
CONFIG_ZB_RADIO_NATIVE=y

View File

@@ -0,0 +1,31 @@
# Per-target ESP-IDF sdkconfig defaults for esp32p4 static analysis (clang-tidy) only.
# Read by IDF in addition to sdkconfig.defaults. Enables variant-only components so
# their headers register for the tidy translation unit (these are normally set at
# codegen via add_idf_sdkconfig_option, which the stub tidy build skips).
# esp32_hosted (P4 has no native Wi-Fi; it drives a co-processor over SDIO/SPI).
# Mirrors a default SDIO 4-bit setup (slot 1, ESP32-C6 slave) so the esp_hosted
# code paths compile under static analysis.
CONFIG_SLAVE_IDF_TARGET_ESP32C6=y
CONFIG_ESP_HOSTED_SDIO_SLOT_1=y
CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS=y
CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS=y
CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=40000
CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH=y
CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=54
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=14
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=15
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=16
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=17
# BLE runs over the hosted co-processor on P4 (no native BT controller), so
# esp32_ble_tracker must take the hosted bluedroid path instead of <esp_bt.h>.
CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID=y
# tinyusb CDC (usb_cdc_acm), same as esp32s3
CONFIG_TINYUSB_CDC_ENABLED=y
CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_TINYUSB_CDC_RX_BUFSIZE=256
CONFIG_TINYUSB_CDC_TX_BUFSIZE=256

View File

@@ -0,0 +1,12 @@
# Per-target ESP-IDF sdkconfig defaults for esp32s3 static analysis (clang-tidy) only.
# Read by IDF in addition to sdkconfig.defaults. Enables variant-only components so
# their headers register for the tidy translation unit (these are normally set at
# codegen via add_idf_sdkconfig_option, which the stub tidy build skips).
# tinyusb CDC (usb_cdc_acm) -- the esp_tinyusb managed component is already in
# esphome/idf_component.yml; these enable its CDC class so tud_cdc_* and the
# CONFIG_TINYUSB_CDC_* macros are declared.
CONFIG_TINYUSB_CDC_ENABLED=y
CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_TINYUSB_CDC_RX_BUFSIZE=256
CONFIG_TINYUSB_CDC_TX_BUFSIZE=256

View File

@@ -63,6 +63,7 @@ def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None:
expected_hasher.update(clang_tidy_content) expected_hasher.update(clang_tidy_content)
expected_hasher.update(requirements_version.encode()) expected_hasher.update(requirements_version.encode())
expected_hasher.update(platformio_content) expected_hasher.update(platformio_content)
expected_hasher.update(b"sdkconfig.defaults")
expected_hasher.update(sdkconfig_content) expected_hasher.update(sdkconfig_content)
expected_hash = expected_hasher.hexdigest() expected_hash = expected_hasher.hexdigest()
@@ -71,6 +72,29 @@ def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None:
assert result == expected_hash assert result == expected_hash
def test_calculate_clang_tidy_hash_includes_per_target_sdkconfig(
tmp_path: Path,
) -> None:
"""Per-target sdkconfig.defaults.<target> files must be part of the hash."""
(tmp_path / ".clang-tidy").write_bytes(b"Checks: '-*'\n")
(tmp_path / "platformio.ini").write_bytes(b"[env:esp32]\n")
(tmp_path / "requirements_dev.txt").write_text("clang-tidy==18.1.5\n")
(tmp_path / "sdkconfig.defaults").write_bytes(b"CONFIG_BASE=y\n")
before = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path)
# Adding a per-target file must change the hash.
per_target = tmp_path / "sdkconfig.defaults.esp32c6"
per_target.write_bytes(b"CONFIG_OPENTHREAD_ENABLED=y\n")
after_add = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path)
assert after_add != before
# Editing the per-target file must change the hash again.
per_target.write_bytes(b"CONFIG_OPENTHREAD_ENABLED=n\n")
after_edit = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path)
assert after_edit != after_add
def test_calculate_clang_tidy_hash_without_sdkconfig(tmp_path: Path) -> None: def test_calculate_clang_tidy_hash_without_sdkconfig(tmp_path: Path) -> None:
"""Test calculating hash without sdkconfig.defaults file.""" """Test calculating hash without sdkconfig.defaults file."""
clang_tidy_content = b"Checks: '-*,readability-*'\n" clang_tidy_content = b"Checks: '-*,readability-*'\n"

View File

@@ -1,24 +1,51 @@
"""Tests for esphome.espidf.clang_tidy tidy-project setup.""" """Tests for esphome.espidf.clang_tidy tidy-project generation."""
import os import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from esphome.espidf.clang_tidy import _Settings, _setup_core from esphome.espidf.clang_tidy import _Settings, _setup_core, _write_tidy_project
REPO_ROOT = Path(__file__).resolve().parents[2]
def _settings(target_framework: str) -> _Settings: def _settings(idf_target: str = "esp32", target_framework: str = "espidf") -> _Settings:
return _Settings( return _Settings(
idf_target="esp32", idf_target=idf_target,
variant="ESP32", variant=idf_target.upper(),
idf_version="5.5.4", idf_version="5.5.4",
target_framework=target_framework, target_framework=target_framework,
platform_defines=("USE_ESP32",), platform_defines=(
"USE_ESP32",
f"USE_ESP32_VARIANT_{idf_target.upper()}",
"USE_ESP_IDF",
),
framework_deps={}, framework_deps={},
) )
def test_write_tidy_project_copies_base_sdkconfig(tmp_path: Path) -> None:
"""The shared sdkconfig.defaults is always copied; no per-target file for esp32."""
_write_tidy_project(tmp_path, [], {}, _settings("esp32"))
assert (tmp_path / "sdkconfig.defaults").is_file()
# esp32 has no sdkconfig.defaults.esp32, so nothing extra is copied.
assert not (tmp_path / "sdkconfig.defaults.esp32").exists()
def test_write_tidy_project_copies_per_target_sdkconfig(tmp_path: Path) -> None:
"""A repo-root sdkconfig.defaults.<target> is also copied into the build dir."""
_write_tidy_project(tmp_path, [], {}, _settings("esp32c6"))
target = tmp_path / "sdkconfig.defaults.esp32c6"
assert (tmp_path / "sdkconfig.defaults").is_file()
assert target.is_file()
assert target.read_text(encoding="utf-8") == (
REPO_ROOT / "sdkconfig.defaults.esp32c6"
).read_text(encoding="utf-8")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("target_framework", "expected"), ("target_framework", "expected"),
[("arduino", "1"), ("espidf", "0")], [("arduino", "1"), ("espidf", "0")],
@@ -34,6 +61,6 @@ def test_setup_core_sets_arduino_env(
# restored after the test instead of leaking into later tests. # restored after the test instead of leaking into later tests.
monkeypatch.delenv("ESPHOME_ARDUINO", raising=False) monkeypatch.delenv("ESPHOME_ARDUINO", raising=False)
_setup_core(tmp_path / "proj", _settings(target_framework)) _setup_core(tmp_path / "proj", _settings(target_framework=target_framework))
assert os.environ["ESPHOME_ARDUINO"] == expected assert os.environ["ESPHOME_ARDUINO"] == expected