mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 14:22:58 +08:00
[core] Native idf full support (#14678)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
co-authored by
Jonathan Swoboda
Jonathan Swoboda
pre-commit-ci-lite[bot]
Jesse Hills
parent
66e2dcffc4
commit
e9cc10fedc
@@ -783,6 +783,91 @@ jobs:
|
||||
# Run compilation with grouping and isolation
|
||||
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
|
||||
|
||||
test-native-idf:
|
||||
name: Test components with native ESP-IDF
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
||||
TEST_COMPONENTS: esp32,api,heatpumpir,bme280_i2c,bh1750,aht10,esp32_ble,esp32_ble_beacon,esp32_ble_client,esp32_ble_server,esp32_ble_tracker,ble_client,ble_presence,ble_rssi,ble_scanner
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache ESPHome
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.esphome-idf
|
||||
key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Run native ESP-IDF compile test
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
# Check if /mnt has more free space than / before bind mounting
|
||||
# Extract available space in KB for comparison
|
||||
root_avail=$(df -k / | awk 'NR==2 {print $4}')
|
||||
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
|
||||
|
||||
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
|
||||
|
||||
# Only use /mnt if it has more space than /
|
||||
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
|
||||
echo "Using /mnt for build files (more space available)"
|
||||
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
|
||||
sudo mkdir -p /mnt/esphome-idf
|
||||
sudo chown $USER:$USER /mnt/esphome-idf
|
||||
mkdir -p ~/.esphome-idf
|
||||
sudo mount --bind /mnt/esphome-idf ~/.esphome-idf
|
||||
|
||||
# Bind mount test build directory to /mnt
|
||||
sudo mkdir -p /mnt/test_build_components_build
|
||||
sudo chown $USER:$USER /mnt/test_build_components_build
|
||||
mkdir -p tests/test_build_components/build
|
||||
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
|
||||
else
|
||||
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
|
||||
fi
|
||||
|
||||
echo "Testing components: $TEST_COMPONENTS"
|
||||
echo ""
|
||||
|
||||
# Show disk space before validation (after bind mounts setup)
|
||||
echo "Disk space before config validation:"
|
||||
df -h
|
||||
echo ""
|
||||
|
||||
# Run config validation (auto-grouped by test_build_components.py)
|
||||
python3 script/test_build_components.py -e config -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf
|
||||
|
||||
echo ""
|
||||
echo "Config validation passed! Starting compilation..."
|
||||
echo ""
|
||||
|
||||
# Show disk space before compilation
|
||||
echo "Disk space before compilation:"
|
||||
df -h
|
||||
echo ""
|
||||
|
||||
# Run compilation (auto-grouped by test_build_components.py)
|
||||
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf
|
||||
|
||||
- name: Save ESPHome cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.esphome-idf
|
||||
key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
|
||||
|
||||
pre-commit-ci-lite:
|
||||
name: pre-commit.ci lite
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1114,6 +1199,7 @@ jobs:
|
||||
- determine-jobs
|
||||
- device-builder
|
||||
- test-build-components-split
|
||||
- test-native-idf
|
||||
- pre-commit-ci-lite
|
||||
- memory-impact-target-branch
|
||||
- memory-impact-pr-branch
|
||||
|
||||
+80
-44
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.writer import update_storage_json
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
@@ -54,7 +55,7 @@ def get_project_cmakelists() -> str:
|
||||
idf_target = variant.lower().replace("-", "")
|
||||
|
||||
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||
compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")]
|
||||
compile_defs = [flag for flag in sorted(CORE.build_flags) if flag.startswith("-D")]
|
||||
extra_compile_options = "\n".join(
|
||||
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
|
||||
for compile_def in compile_defs
|
||||
@@ -64,6 +65,22 @@ def get_project_cmakelists() -> str:
|
||||
# Auto-generated by ESPHome
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# On Windows, Ninja can fail with:
|
||||
# "CreateProcess: The parameter is incorrect (is the command line too long?)"
|
||||
# when compiler/linker command lines exceed the OS length limit.
|
||||
#
|
||||
# The following settings force CMake/Ninja to use *response files* (@file.rsp)
|
||||
# to pass long lists of includes, objects, and other arguments indirectly,
|
||||
# avoiding command-line length limits and fixing the build failure.
|
||||
#
|
||||
# This is especially useful for large ESP-IDF / ESPHome projects with many
|
||||
# source files or include directories.
|
||||
set(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES 1)
|
||||
set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES 1)
|
||||
set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS 1)
|
||||
set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS 1)
|
||||
set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
|
||||
|
||||
set(IDF_TARGET {idf_target})
|
||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
@@ -124,6 +141,11 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
|
||||
def write_project(minimal: bool = False) -> None:
|
||||
"""Write ESP-IDF project files."""
|
||||
# Refresh <data_dir>/storage/<name>.yaml.json so the dashboard's
|
||||
# /info and /downloads endpoints can locate the build (they 404
|
||||
# otherwise). This mirrors the PlatformIO build-gen path's call
|
||||
# in build_gen/platformio.py:write_ini().
|
||||
update_storage_json()
|
||||
mkdir_p(CORE.build_path)
|
||||
mkdir_p(CORE.relative_src_path())
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,6 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_NATIVE_IDF,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
@@ -705,7 +704,7 @@ def _filter_source_files() -> list[str]:
|
||||
# and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x)
|
||||
if eth_type != "JL1101":
|
||||
excluded.append("esp_eth_phy_jl1101.c")
|
||||
elif CORE.is_esp32 and not CORE.data.get(KEY_NATIVE_IDF, False):
|
||||
elif CORE.is_esp32 and not CORE.using_toolchain_esp_idf:
|
||||
from esphome.components.esp32 import idf_version
|
||||
|
||||
# pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver
|
||||
|
||||
@@ -244,6 +244,7 @@ async def to_code(config):
|
||||
# disable built in rgb support as it uses the new RMT drivers and will
|
||||
# conflict with NeoPixelBus which uses the legacy drivers
|
||||
cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN")
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library("makuna/NeoPixelBus", "2.8.0")
|
||||
else:
|
||||
cg.add_library("makuna/NeoPixelBus", "2.7.3")
|
||||
|
||||
@@ -142,7 +142,7 @@ async def _smpmgr_upload_connected(
|
||||
with open(firmware, "rb") as file:
|
||||
image = file.read()
|
||||
upload_size = len(image)
|
||||
progress = ProgressBar()
|
||||
progress = ProgressBar("Uploading")
|
||||
progress.update(0)
|
||||
try:
|
||||
async for offset in smp_client.upload(image):
|
||||
|
||||
+8
-1
@@ -15,6 +15,13 @@ VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
ARGUMENT_HELP_DEVICE = "Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses. Use 'OTA' for resolving from MQTT, DNS or mDNS and avoiding the interactive prompt."
|
||||
|
||||
|
||||
class Toolchain(StrEnum):
|
||||
"""Toolchain identifiers for ESPHome."""
|
||||
|
||||
PLATFORMIO = "platformio"
|
||||
ESP_IDF = "esp-idf"
|
||||
|
||||
|
||||
class Platform(StrEnum):
|
||||
"""Platform identifiers for ESPHome."""
|
||||
|
||||
@@ -1036,6 +1043,7 @@ CONF_TO = "to"
|
||||
CONF_TO_NTC_RESISTANCE = "to_ntc_resistance"
|
||||
CONF_TO_NTC_TEMPERATURE = "to_ntc_temperature"
|
||||
CONF_TOLERANCE = "tolerance"
|
||||
CONF_TOOLCHAIN = "toolchain"
|
||||
CONF_TOPIC = "topic"
|
||||
CONF_TOPIC_PREFIX = "topic_prefix"
|
||||
CONF_TOTAL = "total"
|
||||
@@ -1393,7 +1401,6 @@ KEY_FRAMEWORK_VERSION = "framework_version"
|
||||
KEY_NAME = "name"
|
||||
KEY_VARIANT = "variant"
|
||||
KEY_PAST_SAFE_MODE = "past_safe_mode"
|
||||
KEY_NATIVE_IDF = "native_idf"
|
||||
|
||||
# Entity categories
|
||||
ENTITY_CATEGORY_NONE = ""
|
||||
|
||||
@@ -17,7 +17,6 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WIFI,
|
||||
KEY_CORE,
|
||||
KEY_NATIVE_IDF,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_BK72XX,
|
||||
@@ -28,6 +27,7 @@ from esphome.const import (
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
Toolchain,
|
||||
)
|
||||
|
||||
# pylint: disable=unused-import
|
||||
@@ -618,6 +618,10 @@ class EsphomeCore:
|
||||
# When True, skip network freshness checks for cached external files
|
||||
# (e.g. for `esphome logs`, where remote downloads aren't needed)
|
||||
self.skip_external_update: bool = False
|
||||
# Toolchain used for building the configuration. None until resolved
|
||||
# by CLI (--toolchain) or by `esphome.toolchain:` in YAML during
|
||||
# preload_core_config; defaults to PLATFORMIO if neither sets it.
|
||||
self.toolchain: Toolchain | None = None
|
||||
|
||||
def reset(self):
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||
@@ -648,6 +652,7 @@ class EsphomeCore:
|
||||
self.address_cache = None
|
||||
self._config_hash = None
|
||||
self.skip_external_update = False
|
||||
self.toolchain = None
|
||||
PIN_SCHEMA_REGISTRY.reset()
|
||||
|
||||
@contextmanager
|
||||
@@ -772,8 +777,8 @@ class EsphomeCore:
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
if self.data.get(KEY_NATIVE_IDF, False):
|
||||
# Check if using ESP-IDF toolchain
|
||||
if self.using_toolchain_esp_idf:
|
||||
return self.relative_build_path("build", f"{self.name}.bin")
|
||||
if self.is_libretiny:
|
||||
return self.relative_pioenvs_path(self.name, "firmware.uf2")
|
||||
@@ -781,10 +786,10 @@ class EsphomeCore:
|
||||
|
||||
@property
|
||||
def partition_table_bin(self) -> Path:
|
||||
# Native ESP-IDF (--native-idf): the partition table image is emitted under
|
||||
# Native ESP-IDF (--toolchain esp-idf): the partition table image is emitted under
|
||||
# build/partition_table/partition-table.bin alongside firmware.bin. PlatformIO writes the
|
||||
# equivalent file as partitions.bin in the env-specific .pioenvs directory.
|
||||
if self.data.get(KEY_NATIVE_IDF):
|
||||
if self.using_toolchain_esp_idf:
|
||||
return self.relative_build_path(
|
||||
"build", "partition_table", "partition-table.bin"
|
||||
)
|
||||
@@ -792,7 +797,7 @@ class EsphomeCore:
|
||||
|
||||
@property
|
||||
def bootloader_bin(self) -> Path:
|
||||
if self.data.get(KEY_NATIVE_IDF):
|
||||
if self.using_toolchain_esp_idf:
|
||||
return self.relative_build_path("build", "bootloader", "bootloader.bin")
|
||||
return self.relative_pioenvs_path(self.name, "bootloader.bin")
|
||||
|
||||
@@ -853,6 +858,14 @@ class EsphomeCore:
|
||||
)
|
||||
return self.target_framework == "esp-idf"
|
||||
|
||||
@property
|
||||
def using_toolchain_esp_idf(self):
|
||||
return self.toolchain == Toolchain.ESP_IDF
|
||||
|
||||
@property
|
||||
def using_toolchain_platformio(self):
|
||||
return self.toolchain == Toolchain.PLATFORMIO
|
||||
|
||||
@property
|
||||
def using_zephyr(self):
|
||||
return self.target_framework == "zephyr"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
"""Print JSON ``{paths_to_export, export_vars}`` for ESP-IDF tools.
|
||||
|
||||
Run via ``python <this file> <idf_framework_root>``. PYTHONPATH must include
|
||||
``<idf_framework_root>/tools`` so ``idf_tools`` is importable. Exits with
|
||||
status 1 and prints ``Missing ESP-IDF tools: ...`` on stderr if any tool is
|
||||
not installed.
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error # idf_tools is on PYTHONPATH at runtime only
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
from idf_tools import (
|
||||
TOOLS_FILE,
|
||||
IDFEnv,
|
||||
IDFTool,
|
||||
filter_tools_info,
|
||||
g,
|
||||
load_tools_info,
|
||||
process_tool,
|
||||
)
|
||||
|
||||
g.idf_path = sys.argv[1]
|
||||
g.idf_tools_path = os.environ.get("IDF_TOOLS_PATH")
|
||||
g.tools_json = os.path.join(g.idf_path, TOOLS_FILE)
|
||||
|
||||
tools_info = filter_tools_info(IDFEnv.get_idf_env(), load_tools_info())
|
||||
args = SimpleNamespace(prefer_system=False)
|
||||
paths_to_export: list[str] = []
|
||||
export_vars: dict[str, str] = {}
|
||||
missing_tools: list[str] = []
|
||||
|
||||
for name, tool in tools_info.items():
|
||||
if tool.get_install_type() == IDFTool.INSTALL_NEVER:
|
||||
continue
|
||||
tool_paths, tool_vars, found = process_tool(
|
||||
tool, name, args, "install_cmd", "prefer_system_hint"
|
||||
)
|
||||
if not found:
|
||||
missing_tools.append(name)
|
||||
paths_to_export += tool_paths
|
||||
export_vars |= tool_vars
|
||||
|
||||
if missing_tools:
|
||||
print("Missing ESP-IDF tools: " + ", ".join(missing_tools), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
print(json.dumps({"paths_to_export": paths_to_export, "export_vars": export_vars}))
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Print the ESP-IDF version of a given framework root.
|
||||
|
||||
Run via ``python <this file> <idf_framework_root>``. PYTHONPATH must include
|
||||
``<idf_framework_root>/tools`` so ``idf_tools`` is importable.
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error # idf_tools is on PYTHONPATH at runtime only
|
||||
|
||||
import sys
|
||||
|
||||
from idf_tools import g, get_idf_version
|
||||
|
||||
g.idf_path = sys.argv[1]
|
||||
print(get_idf_version())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -441,7 +441,7 @@ def perform_ota(
|
||||
start_time = time.perf_counter()
|
||||
|
||||
offset = 0
|
||||
progress = ProgressBar()
|
||||
progress = ProgressBar("Uploading")
|
||||
while True:
|
||||
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
|
||||
if not chunk:
|
||||
|
||||
+10
-3
@@ -11,7 +11,7 @@ import shutil
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, TextIO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from esphome.const import __version__ as ESPHOME_VERSION
|
||||
@@ -617,10 +617,15 @@ def sanitize(value):
|
||||
class ProgressBar:
|
||||
"""A simple terminal progress bar for upload operations."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, header: str, stream: TextIO | None = None) -> None:
|
||||
self.header = header
|
||||
self.stream = stream or sys.stderr
|
||||
self.last_progress: int | None = None
|
||||
self.enabled = hasattr(self.stream, "isatty") and self.stream.isatty()
|
||||
|
||||
def update(self, progress: float) -> None:
|
||||
if not self.enabled:
|
||||
return
|
||||
bar_length = 60
|
||||
status = ""
|
||||
if progress >= 1:
|
||||
@@ -631,11 +636,13 @@ class ProgressBar:
|
||||
return
|
||||
self.last_progress = new_progress
|
||||
block = int(round(bar_length * progress))
|
||||
text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}"
|
||||
text = f"\r{self.header}: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}"
|
||||
sys.stderr.write(text)
|
||||
sys.stderr.flush()
|
||||
|
||||
def done(self) -> None:
|
||||
if not self.enabled:
|
||||
return
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class _MultipartStreamer:
|
||||
self._idx = 0
|
||||
self._total = len(prefix) + file_size + len(suffix)
|
||||
self._sent = 0
|
||||
self.progress = ProgressBar()
|
||||
self.progress = ProgressBar("Uploading")
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self._total
|
||||
|
||||
+1
-1
@@ -562,7 +562,7 @@ def lint_constants_usage():
|
||||
# Maximum allowed CONF_ constants in esphome/const.py.
|
||||
# This file is frozen — new constants go in esphome/components/const/__init__.py.
|
||||
# Decrease this number when constants are moved out of const.py.
|
||||
CONST_PY_MAX_CONF = 1011
|
||||
CONST_PY_MAX_CONF = 1012
|
||||
|
||||
|
||||
@lint_content_check(include=["esphome/const.py"])
|
||||
|
||||
@@ -175,7 +175,7 @@ def group_components_by_platform(
|
||||
}
|
||||
|
||||
|
||||
def format_github_summary(test_results: list[TestResult]) -> str:
|
||||
def format_github_summary(test_results: list[TestResult], toolchain=None) -> str:
|
||||
"""Format test results as GitHub Actions job summary markdown.
|
||||
|
||||
Args:
|
||||
@@ -225,11 +225,12 @@ def format_github_summary(test_results: list[TestResult]) -> str:
|
||||
lines.append("```bash\n")
|
||||
|
||||
# Generate one command per platform and test type
|
||||
extra_arguments = f" --toolchain {toolchain}" if toolchain else ""
|
||||
platform_components = group_components_by_platform(failed_results)
|
||||
for platform, test_type in sorted(platform_components.keys()):
|
||||
components_csv = ",".join(platform_components[(platform, test_type)])
|
||||
lines.append(
|
||||
f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}\n"
|
||||
f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}{extra_arguments}\n"
|
||||
)
|
||||
|
||||
lines.append("```\n")
|
||||
@@ -274,13 +275,15 @@ def format_github_summary(test_results: list[TestResult]) -> str:
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def write_github_summary(test_results: list[TestResult]) -> None:
|
||||
def write_github_summary(
|
||||
test_results: list[TestResult], toolchain: str | None = None
|
||||
) -> None:
|
||||
"""Write GitHub Actions job summary with test results and timing.
|
||||
|
||||
Args:
|
||||
test_results: List of all test results
|
||||
"""
|
||||
summary_content = format_github_summary(test_results)
|
||||
summary_content = format_github_summary(test_results, toolchain)
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as f:
|
||||
f.write(summary_content)
|
||||
|
||||
@@ -308,6 +311,7 @@ def run_esphome_test(
|
||||
esphome_command: str,
|
||||
continue_on_fail: bool,
|
||||
use_testing_mode: bool = False,
|
||||
toolchain: str | None = None,
|
||||
) -> TestResult:
|
||||
"""Run esphome test for a single component.
|
||||
|
||||
@@ -367,8 +371,14 @@ def run_esphome_test(
|
||||
]
|
||||
)
|
||||
|
||||
# Add command and config file
|
||||
cmd.extend([esphome_command, str(output_file)])
|
||||
if toolchain:
|
||||
cmd.extend(["--toolchain", toolchain])
|
||||
|
||||
# Add command
|
||||
cmd.append(esphome_command)
|
||||
|
||||
# Add config file
|
||||
cmd.append(str(output_file))
|
||||
|
||||
# Build command string for display/logging
|
||||
cmd_str = " ".join(cmd)
|
||||
@@ -432,6 +442,7 @@ def run_grouped_test(
|
||||
tests_dir: Path,
|
||||
esphome_command: str,
|
||||
continue_on_fail: bool,
|
||||
toolchain: str | None = None,
|
||||
) -> TestResult:
|
||||
"""Run esphome test for a group of components with shared bus configs.
|
||||
|
||||
@@ -510,10 +521,16 @@ def run_grouped_test(
|
||||
"-s",
|
||||
"target_platform",
|
||||
platform,
|
||||
esphome_command,
|
||||
str(output_file),
|
||||
]
|
||||
|
||||
if toolchain:
|
||||
cmd.extend(["--toolchain", toolchain])
|
||||
|
||||
# Add command
|
||||
cmd.append(esphome_command)
|
||||
|
||||
cmd.append(str(output_file))
|
||||
|
||||
# Build command string for display/logging
|
||||
cmd_str = " ".join(cmd)
|
||||
|
||||
@@ -576,6 +593,7 @@ def run_grouped_component_tests(
|
||||
esphome_command: str,
|
||||
continue_on_fail: bool,
|
||||
additional_isolated: set[str] | None = None,
|
||||
toolchain: str | None = None,
|
||||
) -> tuple[set[tuple[str, str]], list[TestResult]]:
|
||||
"""Run grouped component tests.
|
||||
|
||||
@@ -879,6 +897,7 @@ def run_grouped_component_tests(
|
||||
tests_dir=tests_dir,
|
||||
esphome_command=esphome_command,
|
||||
continue_on_fail=continue_on_fail,
|
||||
toolchain=toolchain,
|
||||
)
|
||||
|
||||
# Mark all components as tested
|
||||
@@ -902,6 +921,7 @@ def run_individual_component_test(
|
||||
continue_on_fail: bool,
|
||||
tested_components: set[tuple[str, str]],
|
||||
test_results: list[TestResult],
|
||||
toolchain: str | None = None,
|
||||
) -> None:
|
||||
"""Run an individual component test if not already tested in a group.
|
||||
|
||||
@@ -930,6 +950,7 @@ def run_individual_component_test(
|
||||
build_dir=build_dir,
|
||||
esphome_command=esphome_command,
|
||||
continue_on_fail=continue_on_fail,
|
||||
toolchain=toolchain,
|
||||
)
|
||||
test_results.append(test_result)
|
||||
|
||||
@@ -942,6 +963,7 @@ def test_components(
|
||||
enable_grouping: bool = True,
|
||||
isolated_components: set[str] | None = None,
|
||||
base_only: bool = False,
|
||||
toolchain: str | None = None,
|
||||
) -> int:
|
||||
"""Test components with optional intelligent grouping.
|
||||
|
||||
@@ -1018,6 +1040,7 @@ def test_components(
|
||||
esphome_command=esphome_command,
|
||||
continue_on_fail=continue_on_fail,
|
||||
additional_isolated=isolated_components,
|
||||
toolchain=toolchain,
|
||||
)
|
||||
test_results.extend(grouped_results)
|
||||
|
||||
@@ -1046,6 +1069,7 @@ def test_components(
|
||||
continue_on_fail=continue_on_fail,
|
||||
tested_components=tested_components,
|
||||
test_results=test_results,
|
||||
toolchain=toolchain,
|
||||
)
|
||||
else:
|
||||
# Platform-specific test
|
||||
@@ -1078,6 +1102,7 @@ def test_components(
|
||||
continue_on_fail=continue_on_fail,
|
||||
tested_components=tested_components,
|
||||
test_results=test_results,
|
||||
toolchain=toolchain,
|
||||
)
|
||||
|
||||
# Separate results into passed and failed
|
||||
@@ -1098,17 +1123,18 @@ def test_components(
|
||||
print("\n" + "=" * 80)
|
||||
print("Commands to reproduce failures (copy-paste to reproduce locally):")
|
||||
print("=" * 80)
|
||||
extra_arguments = f" --toolchain {toolchain}" if toolchain else ""
|
||||
platform_components = group_components_by_platform(failed_results)
|
||||
for platform, test_type in sorted(platform_components.keys()):
|
||||
components_csv = ",".join(platform_components[(platform, test_type)])
|
||||
print(
|
||||
f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}"
|
||||
f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}{extra_arguments}"
|
||||
)
|
||||
print()
|
||||
|
||||
# Write GitHub Actions job summary if in CI
|
||||
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
||||
write_github_summary(test_results)
|
||||
write_github_summary(test_results, toolchain=toolchain)
|
||||
|
||||
if failed_results:
|
||||
return 1
|
||||
@@ -1161,6 +1187,10 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="Only test base test files (test.*.yaml), not variant files (test-*.yaml)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--toolchain",
|
||||
help="Select toolchain for compiling.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -1180,6 +1210,7 @@ def main() -> int:
|
||||
enable_grouping=not args.no_grouping,
|
||||
isolated_components=isolated_components,
|
||||
base_only=args.base_only,
|
||||
toolchain=args.toolchain,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_IGNORE_PIN_VALIDATION_ERROR,
|
||||
CONF_NUMBER,
|
||||
KEY_NATIVE_IDF,
|
||||
PlatformFramework,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
@@ -266,7 +266,7 @@ def test_native_idf_enables_reproducible_build(
|
||||
|
||||
CORE.config_path = component_config_path("reproducible_build.yaml")
|
||||
CORE.config = read_config({})
|
||||
CORE.data[KEY_NATIVE_IDF] = True
|
||||
CORE.toolchain = Toolchain.ESP_IDF
|
||||
generate_cpp_contents(CORE.config)
|
||||
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
|
||||
@@ -855,7 +855,7 @@ class TestEsphomeCore:
|
||||
|
||||
def test_bootloader_bin__native_idf(self, target):
|
||||
"""Native ESP-IDF builds emit the bootloader under build/bootloader/bootloader.bin."""
|
||||
target.data[const.KEY_NATIVE_IDF] = True
|
||||
target.toolchain = const.Toolchain.ESP_IDF
|
||||
|
||||
assert target.bootloader_bin == Path(
|
||||
"foo/build/build/bootloader/bootloader.bin"
|
||||
@@ -864,7 +864,7 @@ class TestEsphomeCore:
|
||||
def test_bootloader_bin__platformio(self, target):
|
||||
"""For PlatformIO builds bootloader.bin lives in the env-specific .pioenvs directory."""
|
||||
target.name = "test-device"
|
||||
target.data[const.KEY_NATIVE_IDF] = False
|
||||
target.toolchain = const.Toolchain.PLATFORMIO
|
||||
|
||||
assert target.bootloader_bin == Path(
|
||||
"foo/build/.pioenvs/test-device/bootloader.bin"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -604,7 +604,8 @@ def test_run_ota_wrapper(mock_run_ota_impl: Mock) -> None:
|
||||
|
||||
def test_progress_bar(capsys: CaptureFixture[str]) -> None:
|
||||
"""Test ProgressBar functionality."""
|
||||
progress = espota2.ProgressBar()
|
||||
progress = espota2.ProgressBar("Uploading")
|
||||
progress.enabled = True # Fake TTY
|
||||
|
||||
# Test initial update
|
||||
progress.update(0.0)
|
||||
|
||||
@@ -88,6 +88,7 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.espota2 import (
|
||||
@@ -148,6 +149,7 @@ def setup_core(
|
||||
config[CONF_WIFI] = {CONF_USE_ADDRESS: address}
|
||||
|
||||
CORE.config = config
|
||||
CORE.toolchain = Toolchain.PLATFORMIO
|
||||
|
||||
if platform is not None:
|
||||
CORE.data[KEY_CORE] = {}
|
||||
|
||||
Reference in New Issue
Block a user