mirror of
https://github.com/esphome/esphome.git
synced 2026-06-02 11:08:06 +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:
@@ -783,6 +783,91 @@ jobs:
|
|||||||
# Run compilation with grouping and isolation
|
# Run compilation with grouping and isolation
|
||||||
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
|
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:
|
pre-commit-ci-lite:
|
||||||
name: pre-commit.ci lite
|
name: pre-commit.ci lite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -1114,6 +1199,7 @@ jobs:
|
|||||||
- determine-jobs
|
- determine-jobs
|
||||||
- device-builder
|
- device-builder
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
|
- test-native-idf
|
||||||
- pre-commit-ci-lite
|
- pre-commit-ci-lite
|
||||||
- memory-impact-target-branch
|
- memory-impact-target-branch
|
||||||
- memory-impact-pr-branch
|
- memory-impact-pr-branch
|
||||||
|
|||||||
+71
-35
@@ -52,12 +52,12 @@ from esphome.const import (
|
|||||||
CONF_WEB_SERVER,
|
CONF_WEB_SERVER,
|
||||||
ENV_NOGITIGNORE,
|
ENV_NOGITIGNORE,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_NATIVE_IDF,
|
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
SECRETS_FILES,
|
SECRETS_FILES,
|
||||||
|
Toolchain,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, EsphomeError, coroutine
|
from esphome.core import CORE, EsphomeError, coroutine
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
@@ -155,7 +155,6 @@ class ArgsProtocol(Protocol):
|
|||||||
configuration: str
|
configuration: str
|
||||||
name: str
|
name: str
|
||||||
upload_speed: str | None
|
upload_speed: str | None
|
||||||
native_idf: bool
|
|
||||||
|
|
||||||
|
|
||||||
def choose_prompt(options, purpose: str = None):
|
def choose_prompt(options, purpose: str = None):
|
||||||
@@ -720,17 +719,14 @@ def _wrap_to_code(name, comp, yaml_util):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
def write_cpp(config: ConfigType) -> int:
|
||||||
from esphome import writer
|
from esphome import writer
|
||||||
|
|
||||||
if not get_bool_env(ENV_NOGITIGNORE):
|
if not get_bool_env(ENV_NOGITIGNORE):
|
||||||
writer.write_gitignore()
|
writer.write_gitignore()
|
||||||
|
|
||||||
# Store native_idf flag so esp32 component can check it
|
|
||||||
CORE.data[KEY_NATIVE_IDF] = native_idf
|
|
||||||
|
|
||||||
generate_cpp_contents(config)
|
generate_cpp_contents(config)
|
||||||
return write_cpp_file(native_idf=native_idf)
|
return write_cpp_file()
|
||||||
|
|
||||||
|
|
||||||
def generate_cpp_contents(config: ConfigType) -> None:
|
def generate_cpp_contents(config: ConfigType) -> None:
|
||||||
@@ -746,13 +742,13 @@ def generate_cpp_contents(config: ConfigType) -> None:
|
|||||||
CORE.flush_tasks()
|
CORE.flush_tasks()
|
||||||
|
|
||||||
|
|
||||||
def write_cpp_file(native_idf: bool = False) -> int:
|
def write_cpp_file() -> int:
|
||||||
from esphome import writer
|
from esphome import writer
|
||||||
|
|
||||||
code_s = indent(CORE.cpp_main_section)
|
code_s = indent(CORE.cpp_main_section)
|
||||||
writer.write_cpp(code_s)
|
writer.write_cpp(code_s)
|
||||||
|
|
||||||
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
if CORE.using_toolchain_esp_idf:
|
||||||
from esphome.build_gen import espidf
|
from esphome.build_gen import espidf
|
||||||
|
|
||||||
espidf.write_project()
|
espidf.write_project()
|
||||||
@@ -765,22 +761,21 @@ def write_cpp_file(native_idf: bool = False) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||||
native_idf = getattr(args, "native_idf", False)
|
|
||||||
|
|
||||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||||
# If you change this format, update the regex in that script as well
|
# If you change this format, update the regex in that script as well
|
||||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||||
|
|
||||||
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
if CORE.using_toolchain_esp_idf:
|
||||||
from esphome import espidf_api
|
from esphome.espidf import api
|
||||||
|
|
||||||
rc = espidf_api.run_compile(config, CORE.verbose)
|
rc = api.run_compile(config, CORE.verbose)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
# Create factory.bin and ota.bin
|
# Create factory.bin, ota.bin, and firmware.elf copy
|
||||||
espidf_api.create_factory_bin()
|
api.create_factory_bin()
|
||||||
espidf_api.create_ota_bin()
|
api.create_ota_bin()
|
||||||
|
api.create_elf_copy()
|
||||||
else:
|
else:
|
||||||
from esphome import platformio_api
|
from esphome import platformio_api
|
||||||
|
|
||||||
@@ -883,6 +878,10 @@ def upload_using_esptool(
|
|||||||
|
|
||||||
if file is not None:
|
if file is not None:
|
||||||
flash_images = [FlashImage(path=file, offset="0x0")]
|
flash_images = [FlashImage(path=file, offset="0x0")]
|
||||||
|
elif CORE.using_toolchain_esp_idf:
|
||||||
|
from esphome.espidf import api
|
||||||
|
|
||||||
|
flash_images = [FlashImage(path=api.get_factory_firmware_path(), offset="0x0")]
|
||||||
else:
|
else:
|
||||||
from esphome import platformio_api
|
from esphome import platformio_api
|
||||||
|
|
||||||
@@ -1447,8 +1446,7 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
native_idf = getattr(args, "native_idf", False)
|
exit_code = write_cpp(config)
|
||||||
exit_code = write_cpp(config, native_idf=native_idf)
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
return exit_code
|
return exit_code
|
||||||
if args.only_generate:
|
if args.only_generate:
|
||||||
@@ -1458,6 +1456,11 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
return exit_code
|
return exit_code
|
||||||
if CORE.is_host:
|
if CORE.is_host:
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
from esphome.espidf import api
|
||||||
|
|
||||||
|
program_path = str(api.get_elf_path())
|
||||||
|
else:
|
||||||
from esphome.platformio_api import get_idedata
|
from esphome.platformio_api import get_idedata
|
||||||
|
|
||||||
program_path = str(get_idedata(config).firmware_elf_path)
|
program_path = str(get_idedata(config).firmware_elf_path)
|
||||||
@@ -1503,8 +1506,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
native_idf = getattr(args, "native_idf", False)
|
exit_code = write_cpp(config)
|
||||||
exit_code = write_cpp(config, native_idf=native_idf)
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
return exit_code
|
return exit_code
|
||||||
exit_code = compile_program(args, config)
|
exit_code = compile_program(args, config)
|
||||||
@@ -1512,6 +1514,11 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
return exit_code
|
return exit_code
|
||||||
_LOGGER.info("Successfully compiled program.")
|
_LOGGER.info("Successfully compiled program.")
|
||||||
if CORE.is_host:
|
if CORE.is_host:
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
from esphome.espidf import api
|
||||||
|
|
||||||
|
program_path = str(api.get_elf_path())
|
||||||
|
else:
|
||||||
from esphome.platformio_api import get_idedata
|
from esphome.platformio_api import get_idedata
|
||||||
|
|
||||||
program_path = str(get_idedata(config).firmware_elf_path)
|
program_path = str(get_idedata(config).firmware_elf_path)
|
||||||
@@ -1705,6 +1712,13 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
|||||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
if not CORE.using_toolchain_platformio:
|
||||||
|
_LOGGER.error(
|
||||||
|
"The idedata command is not compatible with %s toolchain",
|
||||||
|
CORE.toolchain.value,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
from esphome import platformio_api
|
from esphome import platformio_api
|
||||||
|
|
||||||
logging.disable(logging.INFO)
|
logging.disable(logging.INFO)
|
||||||
@@ -1724,7 +1738,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
This command compiles the configuration and performs memory analysis.
|
This command compiles the configuration and performs memory analysis.
|
||||||
Compilation is fast if sources haven't changed (just relinking).
|
Compilation is fast if sources haven't changed (just relinking).
|
||||||
"""
|
"""
|
||||||
from esphome import platformio_api
|
|
||||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||||
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
||||||
|
|
||||||
@@ -1738,10 +1751,23 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
_LOGGER.info("Successfully compiled program.")
|
_LOGGER.info("Successfully compiled program.")
|
||||||
|
|
||||||
# Get idedata for analysis
|
# Get idedata for analysis
|
||||||
|
idedata = None
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
from esphome.espidf import api
|
||||||
|
|
||||||
|
objdump_path = str(api.get_objdump_path())
|
||||||
|
readelf_path = str(api.get_readelf_path())
|
||||||
|
|
||||||
|
firmware_elf = api.get_elf_path()
|
||||||
|
else:
|
||||||
|
from esphome import platformio_api
|
||||||
|
|
||||||
idedata = platformio_api.get_idedata(config)
|
idedata = platformio_api.get_idedata(config)
|
||||||
if idedata is None:
|
if idedata is None:
|
||||||
_LOGGER.error("Failed to get IDE data for memory analysis")
|
_LOGGER.error("Failed to get IDE data for memory analysis")
|
||||||
return 1
|
return 1
|
||||||
|
objdump_path = idedata.objdump_path
|
||||||
|
readelf_path = idedata.readelf_path
|
||||||
|
|
||||||
firmware_elf = Path(idedata.firmware_elf_path)
|
firmware_elf = Path(idedata.firmware_elf_path)
|
||||||
|
|
||||||
@@ -1753,8 +1779,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
_LOGGER.info("Analyzing memory usage...")
|
_LOGGER.info("Analyzing memory usage...")
|
||||||
analyzer = MemoryAnalyzerCLI(
|
analyzer = MemoryAnalyzerCLI(
|
||||||
str(firmware_elf),
|
str(firmware_elf),
|
||||||
idedata.objdump_path,
|
objdump_path,
|
||||||
idedata.readelf_path,
|
readelf_path,
|
||||||
external_components,
|
external_components,
|
||||||
idedata=idedata,
|
idedata=idedata,
|
||||||
)
|
)
|
||||||
@@ -1770,7 +1796,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
try:
|
try:
|
||||||
ram_analyzer = RamStringsAnalyzer(
|
ram_analyzer = RamStringsAnalyzer(
|
||||||
str(firmware_elf),
|
str(firmware_elf),
|
||||||
objdump_path=idedata.objdump_path,
|
objdump_path=objdump_path,
|
||||||
platform=CORE.target_platform,
|
platform=CORE.target_platform,
|
||||||
)
|
)
|
||||||
ram_analyzer.analyze()
|
ram_analyzer.analyze()
|
||||||
@@ -2015,6 +2041,17 @@ def parse_args(argv):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
options_parser.add_argument(
|
||||||
|
"--toolchain",
|
||||||
|
type=Toolchain,
|
||||||
|
default=None,
|
||||||
|
choices=list(Toolchain),
|
||||||
|
metavar="{" + ",".join(t.value for t in Toolchain) + "}",
|
||||||
|
help=(
|
||||||
|
"Select toolchain for compiling. Overrides '<platform>.toolchain' in YAML. "
|
||||||
|
f"Default: {Toolchain.PLATFORMIO.value}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=f"ESPHome {const.__version__}", parents=[options_parser]
|
description=f"ESPHome {const.__version__}", parents=[options_parser]
|
||||||
@@ -2059,11 +2096,6 @@ def parse_args(argv):
|
|||||||
help="Only generate source code, do not compile.",
|
help="Only generate source code, do not compile.",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
)
|
)
|
||||||
parser_compile.add_argument(
|
|
||||||
"--native-idf",
|
|
||||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser_upload = subparsers.add_parser(
|
parser_upload = subparsers.add_parser(
|
||||||
"upload",
|
"upload",
|
||||||
@@ -2171,11 +2203,6 @@ def parse_args(argv):
|
|||||||
help="Reset the device before starting serial logs.",
|
help="Reset the device before starting serial logs.",
|
||||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||||
)
|
)
|
||||||
parser_run.add_argument(
|
|
||||||
"--native-idf",
|
|
||||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
parser_run.add_argument(
|
parser_run.add_argument(
|
||||||
"--ota-platform",
|
"--ota-platform",
|
||||||
choices=[CONF_ESPHOME, CONF_WEB_SERVER],
|
choices=[CONF_ESPHOME, CONF_WEB_SERVER],
|
||||||
@@ -2398,6 +2425,9 @@ def run_esphome(argv):
|
|||||||
|
|
||||||
CORE.config_path = conf_path
|
CORE.config_path = conf_path
|
||||||
CORE.dashboard = args.dashboard
|
CORE.dashboard = args.dashboard
|
||||||
|
if args.toolchain is not None:
|
||||||
|
# CLI toolchain wins over esp32.toolchain in YAML.
|
||||||
|
CORE.toolchain = args.toolchain
|
||||||
|
|
||||||
# Commands that don't need fresh external components: logs just connects
|
# Commands that don't need fresh external components: logs just connects
|
||||||
# to the device, and clean is about to delete the build directory.
|
# to the device, and clean is about to delete the build directory.
|
||||||
@@ -2410,6 +2440,12 @@ def run_esphome(argv):
|
|||||||
return 2
|
return 2
|
||||||
CORE.config = config
|
CORE.config = config
|
||||||
|
|
||||||
|
# Fallback for platforms whose validators didn't set the toolchain
|
||||||
|
# (only the esp32 component reads esp32.framework.toolchain). All
|
||||||
|
# other platforms only support PlatformIO today.
|
||||||
|
if CORE.toolchain is None:
|
||||||
|
CORE.toolchain = Toolchain.PLATFORMIO
|
||||||
|
|
||||||
if args.command not in POST_CONFIG_ACTIONS:
|
if args.command not in POST_CONFIG_ACTIONS:
|
||||||
safe_print(f"Unknown command {args.command}")
|
safe_print(f"Unknown command {args.command}")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from esphome.components.esp32 import get_esp32_variant
|
from esphome.components.esp32 import get_esp32_variant
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||||
|
from esphome.writer import update_storage_json
|
||||||
|
|
||||||
|
|
||||||
def get_available_components() -> list[str] | None:
|
def get_available_components() -> list[str] | None:
|
||||||
@@ -54,7 +55,7 @@ def get_project_cmakelists() -> str:
|
|||||||
idf_target = variant.lower().replace("-", "")
|
idf_target = variant.lower().replace("-", "")
|
||||||
|
|
||||||
# Extract compile definitions from build flags (-DXXX -> XXX)
|
# 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(
|
extra_compile_options = "\n".join(
|
||||||
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
|
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
|
||||||
for compile_def in compile_defs
|
for compile_def in compile_defs
|
||||||
@@ -64,6 +65,22 @@ def get_project_cmakelists() -> str:
|
|||||||
# Auto-generated by ESPHome
|
# Auto-generated by ESPHome
|
||||||
cmake_minimum_required(VERSION 3.16)
|
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(IDF_TARGET {idf_target})
|
||||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
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:
|
def write_project(minimal: bool = False) -> None:
|
||||||
"""Write ESP-IDF project files."""
|
"""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.build_path)
|
||||||
mkdir_p(CORE.relative_src_path())
|
mkdir_p(CORE.relative_src_path())
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from esphome.const import (
|
|||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
CONF_SIZE,
|
CONF_SIZE,
|
||||||
CONF_SOURCE,
|
CONF_SOURCE,
|
||||||
|
CONF_TOOLCHAIN,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
CONF_VARIANT,
|
CONF_VARIANT,
|
||||||
CONF_VERSION,
|
CONF_VERSION,
|
||||||
@@ -38,16 +39,17 @@ from esphome.const import (
|
|||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
KEY_NAME,
|
KEY_NAME,
|
||||||
KEY_NATIVE_IDF,
|
|
||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
ThreadModel,
|
ThreadModel,
|
||||||
|
Toolchain,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt
|
from esphome.core import CORE, HexInt, Library
|
||||||
from esphome.core.config import BOARD_MAX_LENGTH
|
from esphome.core.config import BOARD_MAX_LENGTH
|
||||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.espidf.component import generate_idf_component
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
@@ -465,6 +467,9 @@ def set_core_data(config):
|
|||||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
|
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
|
||||||
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
# Official ESP-IDF frameworks don't use extra
|
||||||
|
idf_ver = cv.Version(idf_ver.major, idf_ver.minor, idf_ver.patch)
|
||||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
|
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
|
||||||
else:
|
else:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
@@ -652,7 +657,7 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|||||||
return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
|
return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
def _format_framework_espidf_version(
|
def _format_framework_pio_espidf_version(
|
||||||
ver: cv.Version, release: str | None = None
|
ver: cv.Version, release: str | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
||||||
@@ -741,6 +746,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
|||||||
"latest": cv.Version(5, 5, 4),
|
"latest": cv.Version(5, 5, 4),
|
||||||
"dev": cv.Version(5, 5, 4),
|
"dev": cv.Version(5, 5, 4),
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(
|
cv.Version(
|
||||||
6, 0, 1
|
6, 0, 1
|
||||||
@@ -774,7 +780,7 @@ PLATFORM_VERSION_LOOKUP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _check_versions(config):
|
def _check_pio_versions(config):
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
value = config[CONF_FRAMEWORK]
|
value = config[CONF_FRAMEWORK]
|
||||||
|
|
||||||
@@ -785,7 +791,7 @@ def _check_versions(config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
|
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||||
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
|
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup))
|
||||||
|
|
||||||
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
|
||||||
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
|
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
|
||||||
@@ -813,7 +819,7 @@ def _check_versions(config):
|
|||||||
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
|
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
|
||||||
value[CONF_SOURCE] = value.get(
|
value[CONF_SOURCE] = value.get(
|
||||||
CONF_SOURCE,
|
CONF_SOURCE,
|
||||||
_format_framework_espidf_version(version, value.get(CONF_RELEASE)),
|
_format_framework_pio_espidf_version(version, value.get(CONF_RELEASE)),
|
||||||
)
|
)
|
||||||
if _is_framework_url(value[CONF_SOURCE]):
|
if _is_framework_url(value[CONF_SOURCE]):
|
||||||
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
|
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
|
||||||
@@ -823,7 +829,7 @@ def _check_versions(config):
|
|||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"Framework version not recognized; please specify platform_version"
|
"Framework version not recognized; please specify platform_version"
|
||||||
)
|
)
|
||||||
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
|
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup))
|
||||||
|
|
||||||
if version != recommended_version:
|
if version != recommended_version:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@@ -831,7 +837,7 @@ def _check_versions(config):
|
|||||||
"If there are connectivity or build issues please remove the manual version."
|
"If there are connectivity or build issues please remove the manual version."
|
||||||
)
|
)
|
||||||
|
|
||||||
if value[CONF_PLATFORM_VERSION] != _parse_platform_version(
|
if value[CONF_PLATFORM_VERSION] != _parse_pio_platform_version(
|
||||||
str(PLATFORM_VERSION_LOOKUP["recommended"])
|
str(PLATFORM_VERSION_LOOKUP["recommended"])
|
||||||
):
|
):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@@ -842,7 +848,38 @@ def _check_versions(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def _parse_platform_version(value):
|
def _check_esp_idf_versions(config):
|
||||||
|
config = _check_pio_versions(config)
|
||||||
|
value = config[CONF_FRAMEWORK]
|
||||||
|
|
||||||
|
# Remove unwanted keys if present
|
||||||
|
for key in (CONF_SOURCE, CONF_PLATFORM_VERSION):
|
||||||
|
value.pop(key, None)
|
||||||
|
|
||||||
|
# Official ESP-IDF frameworks don't use extra
|
||||||
|
version = cv.Version.parse(value[CONF_VERSION])
|
||||||
|
version = cv.Version(version.major, version.minor, version.patch)
|
||||||
|
|
||||||
|
value[CONF_VERSION] = str(version)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_toolchain(value) -> Toolchain:
|
||||||
|
return Toolchain(cv.one_of(*(t.value for t in Toolchain), lower=True)(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _check_versions(config):
|
||||||
|
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
|
||||||
|
if CORE.toolchain is None:
|
||||||
|
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
|
||||||
|
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
return _check_esp_idf_versions(config)
|
||||||
|
return _check_pio_versions(config)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pio_platform_version(value):
|
||||||
try:
|
try:
|
||||||
ver = cv.Version.parse(cv.version_number(value))
|
ver = cv.Version.parse(cv.version_number(value))
|
||||||
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
|
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
|
||||||
@@ -1272,7 +1309,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
|
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
|
||||||
cv.Optional(CONF_RELEASE): cv.string_strict,
|
cv.Optional(CONF_RELEASE): cv.string_strict,
|
||||||
cv.Optional(CONF_SOURCE): cv.string_strict,
|
cv.Optional(CONF_SOURCE): cv.string_strict,
|
||||||
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
|
cv.Optional(CONF_PLATFORM_VERSION): _parse_pio_platform_version,
|
||||||
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
|
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
|
||||||
cv.string_strict: cv.string_strict
|
cv.string_strict: cv.string_strict
|
||||||
},
|
},
|
||||||
@@ -1524,6 +1561,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
|
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
|
||||||
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
|
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
|
||||||
|
cv.Optional(CONF_TOOLCHAIN): _validate_toolchain,
|
||||||
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All(
|
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All(
|
||||||
cv.positive_time_period_seconds,
|
cv.positive_time_period_seconds,
|
||||||
cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)),
|
cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)),
|
||||||
@@ -1672,11 +1710,11 @@ async def to_code(config):
|
|||||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
conf = config[CONF_FRAMEWORK]
|
conf = config[CONF_FRAMEWORK]
|
||||||
|
|
||||||
# Check if using native ESP-IDF build (--native-idf)
|
# Check if using ESP-IDF toolchain
|
||||||
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
|
use_platformio = not CORE.using_toolchain_esp_idf
|
||||||
if use_platformio:
|
if use_platformio:
|
||||||
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
|
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
|
||||||
# but keep them when using --native-idf for native ESP-IDF builds
|
# but keep them when using ESP-IDF toolchain
|
||||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||||
os.environ.pop(clean_var, None)
|
os.environ.pop(clean_var, None)
|
||||||
|
|
||||||
@@ -1716,6 +1754,8 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cg.add_build_flag("-Wno-error=format")
|
cg.add_build_flag("-Wno-error=format")
|
||||||
|
cg.add_build_flag("-Wno-error=missing-field-initializers")
|
||||||
|
cg.add_build_flag("-Wno-error=volatile")
|
||||||
|
|
||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_build_flag("-DUSE_ESP32")
|
cg.add_build_flag("-DUSE_ESP32")
|
||||||
@@ -1792,7 +1832,7 @@ async def to_code(config):
|
|||||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||||
cg.add_platformio_option(
|
cg.add_platformio_option(
|
||||||
"platform_packages",
|
"platform_packages",
|
||||||
[_format_framework_espidf_version(idf_ver)],
|
[_format_framework_pio_espidf_version(idf_ver)],
|
||||||
)
|
)
|
||||||
# Use stub package to skip downloading precompiled libs
|
# Use stub package to skip downloading precompiled libs
|
||||||
stubs_dir = CORE.relative_build_path("arduino_libs_stub")
|
stubs_dir = CORE.relative_build_path("arduino_libs_stub")
|
||||||
@@ -2424,6 +2464,14 @@ def _write_sdkconfig():
|
|||||||
clean_build(clear_pio_cache=False)
|
clean_build(clear_pio_cache=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _platformio_library_to_dependency(library: Library) -> tuple[str, dict[str, str]]:
|
||||||
|
dependency: dict[str, str] = {}
|
||||||
|
name, version, path = generate_idf_component(library)
|
||||||
|
dependency["override_path"] = str(path)
|
||||||
|
dependency["version"] = version
|
||||||
|
return name, dependency
|
||||||
|
|
||||||
|
|
||||||
def _write_idf_component_yml():
|
def _write_idf_component_yml():
|
||||||
yml_path = CORE.relative_build_path("src/idf_component.yml")
|
yml_path = CORE.relative_build_path("src/idf_component.yml")
|
||||||
dependencies: dict[str, dict] = {}
|
dependencies: dict[str, dict] = {}
|
||||||
@@ -2465,6 +2513,21 @@ def _write_idf_component_yml():
|
|||||||
if stub_path.exists():
|
if stub_path.exists():
|
||||||
rmtree(stub_path)
|
rmtree(stub_path)
|
||||||
|
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
add_idf_component(
|
||||||
|
name="espressif/arduino-esp32",
|
||||||
|
ref=str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if CORE.using_toolchain_esp_idf:
|
||||||
|
# Try to convert PlatformIO library to ESP-IDF components
|
||||||
|
for name, library in CORE.platformio_libraries.items():
|
||||||
|
# Don't process arduino libraries
|
||||||
|
if name in ARDUINO_DISABLED_LIBRARIES:
|
||||||
|
continue
|
||||||
|
dependency_name, dependency = _platformio_library_to_dependency(library)
|
||||||
|
dependencies[dependency_name] = dependency
|
||||||
|
|
||||||
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
|
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
|
||||||
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
||||||
for name, component in components.items():
|
for name, component in components.items():
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ from esphome.const import (
|
|||||||
CONF_VALUE,
|
CONF_VALUE,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
KEY_NATIVE_IDF,
|
|
||||||
Platform,
|
Platform,
|
||||||
PlatformFramework,
|
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)
|
# and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x)
|
||||||
if eth_type != "JL1101":
|
if eth_type != "JL1101":
|
||||||
excluded.append("esp_eth_phy_jl1101.c")
|
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
|
from esphome.components.esp32 import idf_version
|
||||||
|
|
||||||
# pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver
|
# 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
|
# disable built in rgb support as it uses the new RMT drivers and will
|
||||||
# conflict with NeoPixelBus which uses the legacy drivers
|
# conflict with NeoPixelBus which uses the legacy drivers
|
||||||
cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN")
|
cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN")
|
||||||
|
cg.add_library("SPI", None)
|
||||||
cg.add_library("makuna/NeoPixelBus", "2.8.0")
|
cg.add_library("makuna/NeoPixelBus", "2.8.0")
|
||||||
else:
|
else:
|
||||||
cg.add_library("makuna/NeoPixelBus", "2.7.3")
|
cg.add_library("makuna/NeoPixelBus", "2.7.3")
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ async def _smpmgr_upload_connected(
|
|||||||
with open(firmware, "rb") as file:
|
with open(firmware, "rb") as file:
|
||||||
image = file.read()
|
image = file.read()
|
||||||
upload_size = len(image)
|
upload_size = len(image)
|
||||||
progress = ProgressBar()
|
progress = ProgressBar("Uploading")
|
||||||
progress.update(0)
|
progress.update(0)
|
||||||
try:
|
try:
|
||||||
async for offset in smp_client.upload(image):
|
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."
|
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):
|
class Platform(StrEnum):
|
||||||
"""Platform identifiers for ESPHome."""
|
"""Platform identifiers for ESPHome."""
|
||||||
|
|
||||||
@@ -1036,6 +1043,7 @@ CONF_TO = "to"
|
|||||||
CONF_TO_NTC_RESISTANCE = "to_ntc_resistance"
|
CONF_TO_NTC_RESISTANCE = "to_ntc_resistance"
|
||||||
CONF_TO_NTC_TEMPERATURE = "to_ntc_temperature"
|
CONF_TO_NTC_TEMPERATURE = "to_ntc_temperature"
|
||||||
CONF_TOLERANCE = "tolerance"
|
CONF_TOLERANCE = "tolerance"
|
||||||
|
CONF_TOOLCHAIN = "toolchain"
|
||||||
CONF_TOPIC = "topic"
|
CONF_TOPIC = "topic"
|
||||||
CONF_TOPIC_PREFIX = "topic_prefix"
|
CONF_TOPIC_PREFIX = "topic_prefix"
|
||||||
CONF_TOTAL = "total"
|
CONF_TOTAL = "total"
|
||||||
@@ -1393,7 +1401,6 @@ KEY_FRAMEWORK_VERSION = "framework_version"
|
|||||||
KEY_NAME = "name"
|
KEY_NAME = "name"
|
||||||
KEY_VARIANT = "variant"
|
KEY_VARIANT = "variant"
|
||||||
KEY_PAST_SAFE_MODE = "past_safe_mode"
|
KEY_PAST_SAFE_MODE = "past_safe_mode"
|
||||||
KEY_NATIVE_IDF = "native_idf"
|
|
||||||
|
|
||||||
# Entity categories
|
# Entity categories
|
||||||
ENTITY_CATEGORY_NONE = ""
|
ENTITY_CATEGORY_NONE = ""
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from esphome.const import (
|
|||||||
CONF_WEB_SERVER,
|
CONF_WEB_SERVER,
|
||||||
CONF_WIFI,
|
CONF_WIFI,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_NATIVE_IDF,
|
|
||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
@@ -28,6 +27,7 @@ from esphome.const import (
|
|||||||
PLATFORM_NRF52,
|
PLATFORM_NRF52,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
|
Toolchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
@@ -618,6 +618,10 @@ class EsphomeCore:
|
|||||||
# When True, skip network freshness checks for cached external files
|
# When True, skip network freshness checks for cached external files
|
||||||
# (e.g. for `esphome logs`, where remote downloads aren't needed)
|
# (e.g. for `esphome logs`, where remote downloads aren't needed)
|
||||||
self.skip_external_update: bool = False
|
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):
|
def reset(self):
|
||||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||||
@@ -648,6 +652,7 @@ class EsphomeCore:
|
|||||||
self.address_cache = None
|
self.address_cache = None
|
||||||
self._config_hash = None
|
self._config_hash = None
|
||||||
self.skip_external_update = False
|
self.skip_external_update = False
|
||||||
|
self.toolchain = None
|
||||||
PIN_SCHEMA_REGISTRY.reset()
|
PIN_SCHEMA_REGISTRY.reset()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -772,8 +777,8 @@ class EsphomeCore:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def firmware_bin(self) -> Path:
|
def firmware_bin(self) -> Path:
|
||||||
# Check if using native ESP-IDF build (--native-idf)
|
# Check if using ESP-IDF toolchain
|
||||||
if self.data.get(KEY_NATIVE_IDF, False):
|
if self.using_toolchain_esp_idf:
|
||||||
return self.relative_build_path("build", f"{self.name}.bin")
|
return self.relative_build_path("build", f"{self.name}.bin")
|
||||||
if self.is_libretiny:
|
if self.is_libretiny:
|
||||||
return self.relative_pioenvs_path(self.name, "firmware.uf2")
|
return self.relative_pioenvs_path(self.name, "firmware.uf2")
|
||||||
@@ -781,10 +786,10 @@ class EsphomeCore:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def partition_table_bin(self) -> Path:
|
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
|
# build/partition_table/partition-table.bin alongside firmware.bin. PlatformIO writes the
|
||||||
# equivalent file as partitions.bin in the env-specific .pioenvs directory.
|
# 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(
|
return self.relative_build_path(
|
||||||
"build", "partition_table", "partition-table.bin"
|
"build", "partition_table", "partition-table.bin"
|
||||||
)
|
)
|
||||||
@@ -792,7 +797,7 @@ class EsphomeCore:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def bootloader_bin(self) -> Path:
|
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_build_path("build", "bootloader", "bootloader.bin")
|
||||||
return self.relative_pioenvs_path(self.name, "bootloader.bin")
|
return self.relative_pioenvs_path(self.name, "bootloader.bin")
|
||||||
|
|
||||||
@@ -853,6 +858,14 @@ class EsphomeCore:
|
|||||||
)
|
)
|
||||||
return self.target_framework == "esp-idf"
|
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
|
@property
|
||||||
def using_zephyr(self):
|
def using_zephyr(self):
|
||||||
return self.target_framework == "zephyr"
|
return self.target_framework == "zephyr"
|
||||||
|
|||||||
@@ -0,0 +1,499 @@
|
|||||||
|
"""ESP-IDF direct build API for ESPHome."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION
|
||||||
|
from esphome.core import CORE, EsphomeError
|
||||||
|
from esphome.espidf.framework import check_esp_idf_install, get_framework_env
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = "espidf_api"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _CacheData:
|
||||||
|
paths: dict[str, tuple] = field(default_factory=dict)
|
||||||
|
env: dict[str, dict[str, str]] = field(default_factory=dict)
|
||||||
|
cmake_output: dict[Path, str] = field(default_factory=dict)
|
||||||
|
cmake_tools: dict[Path, dict[str, Path]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def _cache() -> _CacheData:
|
||||||
|
if DOMAIN not in CORE.data:
|
||||||
|
CORE.data[DOMAIN] = _CacheData()
|
||||||
|
return CORE.data[DOMAIN]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_core_framework_version():
|
||||||
|
return str(CORE.data[KEY_ESP32][KEY_IDF_VERSION])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_esphome_esp_idf_paths(
|
||||||
|
version: str | None = None,
|
||||||
|
) -> tuple[os.PathLike, os.PathLike]:
|
||||||
|
version = version or _get_core_framework_version()
|
||||||
|
paths = _cache().paths
|
||||||
|
if version not in paths:
|
||||||
|
paths[version] = check_esp_idf_install(version)
|
||||||
|
return paths[version]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_idf_path(version: str | None = None) -> Path | None:
|
||||||
|
"""Get IDF_PATH from environment or common locations."""
|
||||||
|
# Use provided IDF framework if available
|
||||||
|
if "IDF_PATH" in os.environ:
|
||||||
|
return Path(os.environ["IDF_PATH"])
|
||||||
|
return Path(_get_esphome_esp_idf_paths(version)[0])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_idf_env(version: str | None = None) -> dict[str, str]:
|
||||||
|
"""Get environment variables needed for ESP-IDF build."""
|
||||||
|
version = version or _get_core_framework_version()
|
||||||
|
env_cache = _cache().env
|
||||||
|
if version not in env_cache:
|
||||||
|
env_cache[version] = os.environ.copy()
|
||||||
|
|
||||||
|
# Use provided IDF framework if available
|
||||||
|
if "IDF_PATH" not in os.environ:
|
||||||
|
env_cache[version] |= get_framework_env(
|
||||||
|
*_get_esphome_esp_idf_paths(version)
|
||||||
|
)
|
||||||
|
return env_cache[version]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cmake_output(build_dir) -> str:
|
||||||
|
cmake_output_cache = _cache().cmake_output
|
||||||
|
if build_dir not in cmake_output_cache:
|
||||||
|
cmd = ["cmake", "-LA", "-N", "."]
|
||||||
|
|
||||||
|
env = _get_idf_env()
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=build_dir,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"CMake failed: {result.stderr}")
|
||||||
|
|
||||||
|
cmake_output_cache[build_dir] = result.stdout
|
||||||
|
return cmake_output_cache[build_dir]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cmake_tool_path(var_name: str) -> Path:
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
cmake_output = _get_cmake_output(build_dir)
|
||||||
|
|
||||||
|
cmake_tools_cache = _cache().cmake_tools
|
||||||
|
if build_dir not in cmake_tools_cache:
|
||||||
|
cmake_tools_cache[build_dir] = {}
|
||||||
|
|
||||||
|
if var_name not in cmake_tools_cache[build_dir]:
|
||||||
|
pattern = rf"^{var_name}:FILEPATH=(.+)$"
|
||||||
|
match = re.search(pattern, cmake_output, re.MULTILINE)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError(f"{var_name} not found in CMake output")
|
||||||
|
|
||||||
|
path = match.group(1).strip()
|
||||||
|
cmake_tools_cache[build_dir][var_name] = Path(path)
|
||||||
|
|
||||||
|
return cmake_tools_cache[build_dir][var_name]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_idf_tool(name: str) -> str:
|
||||||
|
"""Return the path to an executable from the ESP-IDF environment PATH or raise if not found."""
|
||||||
|
env = _get_idf_env()
|
||||||
|
executable = shutil.which(name, path=env.get("PATH", None))
|
||||||
|
if executable is None:
|
||||||
|
raise EsphomeError(
|
||||||
|
f"{name} executable not found in ESP-IDF environment. "
|
||||||
|
"Check that the IDF environment is correctly set up."
|
||||||
|
)
|
||||||
|
return executable
|
||||||
|
|
||||||
|
|
||||||
|
def run_idf_py(
|
||||||
|
*args, cwd: Path | None = None, capture_output: bool = False
|
||||||
|
) -> int | str:
|
||||||
|
"""Run idf.py with the given arguments."""
|
||||||
|
idf_path = _get_idf_path()
|
||||||
|
if idf_path is None:
|
||||||
|
raise EsphomeError("ESP-IDF not found")
|
||||||
|
|
||||||
|
env = _get_idf_env()
|
||||||
|
python_executable = _get_idf_tool("python")
|
||||||
|
idf_py = idf_path / "tools" / "idf.py"
|
||||||
|
# Dispatch idf.py through esphome.espidf.runner, which wraps
|
||||||
|
# sys.stdout/sys.stderr so ``isatty()`` reports True. This keeps CMake,
|
||||||
|
# Ninja, and idf.py's own progress-bar code emitting TTY-format output
|
||||||
|
# (``\r`` cursor moves, ANSI colors, fancy progress bars) even when our
|
||||||
|
# real stdout is a pipe — e.g. when esphome is running under the Home
|
||||||
|
# Assistant dashboard add-on. The runner is a plain script (not a
|
||||||
|
# ``python -m`` module) because IDF's Python venv does not have the
|
||||||
|
# esphome package installed.
|
||||||
|
runner_py = Path(__file__).parent / "runner.py"
|
||||||
|
|
||||||
|
cmd = [python_executable, str(runner_py), str(idf_py)] + list(args)
|
||||||
|
|
||||||
|
if cwd is None:
|
||||||
|
cwd = CORE.build_path
|
||||||
|
|
||||||
|
_LOGGER.debug("Running: %s", " ".join(cmd))
|
||||||
|
_LOGGER.debug(" in directory: %s", cwd)
|
||||||
|
|
||||||
|
if capture_output:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
_LOGGER.error("idf.py failed:\n%s", result.stderr)
|
||||||
|
return result.stdout
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
env=env,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sdkconfig_args() -> list[str]:
|
||||||
|
"""Get cmake -D flags for the sdkconfig file, if it exists."""
|
||||||
|
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||||
|
if sdkconfig_path.is_file():
|
||||||
|
return ["-D", f"SDKCONFIG={sdkconfig_path}"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def run_reconfigure() -> int:
|
||||||
|
"""Run cmake reconfigure only (no build)."""
|
||||||
|
return run_idf_py(*_get_sdkconfig_args(), "reconfigure")
|
||||||
|
|
||||||
|
|
||||||
|
def has_outdated_files():
|
||||||
|
"""Check if the build configuration is stale.
|
||||||
|
|
||||||
|
Returns True if required build files are missing or if configuration inputs
|
||||||
|
are newer than the generated CMake/Ninja build artifacts.
|
||||||
|
"""
|
||||||
|
cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt")
|
||||||
|
|
||||||
|
cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt")
|
||||||
|
cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt")
|
||||||
|
build_config_path = CORE.relative_build_path("build/config")
|
||||||
|
sdkconfig_internal_path = CORE.relative_build_path(
|
||||||
|
f"sdkconfig.{CORE.name}.esphomeinternal"
|
||||||
|
)
|
||||||
|
dependency_lock_path = CORE.relative_build_path("dependencies.lock")
|
||||||
|
build_ninja_path = CORE.relative_build_path("build/build.ninja")
|
||||||
|
|
||||||
|
if not os.path.isdir(build_config_path) or not os.listdir(build_config_path):
|
||||||
|
return True
|
||||||
|
if not os.path.isfile(cmakecache_txt_path):
|
||||||
|
return True
|
||||||
|
if not os.path.isfile(build_ninja_path):
|
||||||
|
return True
|
||||||
|
if os.path.isfile(dependency_lock_path) and os.path.getmtime(
|
||||||
|
dependency_lock_path
|
||||||
|
) > os.path.getmtime(build_ninja_path):
|
||||||
|
return True
|
||||||
|
|
||||||
|
cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path)
|
||||||
|
return any(
|
||||||
|
os.path.getmtime(f) > cmakecache_txt_mtime
|
||||||
|
for f in [
|
||||||
|
_get_idf_path(),
|
||||||
|
cmakelists_txt_build_path,
|
||||||
|
cmakelists_txt_src_path,
|
||||||
|
sdkconfig_internal_path,
|
||||||
|
build_config_path,
|
||||||
|
]
|
||||||
|
if f and os.path.exists(f)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def need_reconfigure() -> bool:
|
||||||
|
from esphome.build_gen.espidf import has_discovered_components
|
||||||
|
|
||||||
|
# We need to reconfigure either if the files are outdated or if there is no component discovered
|
||||||
|
return has_outdated_files() or not has_discovered_components()
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_memory_segments():
|
||||||
|
"""Patch memory.ld to expand IRAM/DRAM for testing mode.
|
||||||
|
|
||||||
|
Mirrors the PlatformIO iram_fix.py.script logic for native IDF builds.
|
||||||
|
Must be called after cmake configure (which generates memory.ld) and
|
||||||
|
before the build/link step.
|
||||||
|
"""
|
||||||
|
# Same sizes as iram_fix.py.script
|
||||||
|
testing_iram_size = 0x200000 # 2MB
|
||||||
|
testing_dram_size = 0x200000 # 2MB
|
||||||
|
|
||||||
|
memory_ld = CORE.relative_build_path(
|
||||||
|
"build", "esp-idf", "esp_system", "ld", "memory.ld"
|
||||||
|
)
|
||||||
|
if not memory_ld.is_file():
|
||||||
|
_LOGGER.warning("Could not find linker script at %s", memory_ld)
|
||||||
|
return
|
||||||
|
|
||||||
|
content = memory_ld.read_text()
|
||||||
|
patches = []
|
||||||
|
|
||||||
|
def _patch_segment(text, segment_name, new_size):
|
||||||
|
pattern = rf"({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)"
|
||||||
|
if match := re.search(pattern, text, re.DOTALL):
|
||||||
|
replacement = f"{match.group(1)}{new_size:#x}"
|
||||||
|
new_text = text[: match.start()] + replacement + text[match.end() :]
|
||||||
|
if new_text != text:
|
||||||
|
return new_text, True
|
||||||
|
return text, False
|
||||||
|
|
||||||
|
content, patched = _patch_segment(content, "iram0_0_seg", testing_iram_size)
|
||||||
|
if patched:
|
||||||
|
patches.append(f"IRAM={testing_iram_size:#x}")
|
||||||
|
|
||||||
|
content, patched = _patch_segment(content, "dram0_0_seg", testing_dram_size)
|
||||||
|
if patched:
|
||||||
|
patches.append(f"DRAM={testing_dram_size:#x}")
|
||||||
|
|
||||||
|
if patches:
|
||||||
|
memory_ld.write_text(content)
|
||||||
|
_LOGGER.info("Patched %s in %s for testing mode", ", ".join(patches), memory_ld)
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Could not patch memory segments in %s", memory_ld)
|
||||||
|
|
||||||
|
|
||||||
|
def run_compile(config, verbose: bool) -> int:
|
||||||
|
"""Compile the ESP-IDF project.
|
||||||
|
|
||||||
|
Uses two-phase configure to auto-discover available components:
|
||||||
|
1. If no previous build, configure with minimal REQUIRES to discover components
|
||||||
|
2. Regenerate CMakeLists.txt with discovered components
|
||||||
|
3. Run full build
|
||||||
|
"""
|
||||||
|
from esphome.build_gen.espidf import write_project
|
||||||
|
|
||||||
|
# Check if we need to do discovery phase
|
||||||
|
if need_reconfigure():
|
||||||
|
_LOGGER.info("Discovering available ESP-IDF components...")
|
||||||
|
write_project(minimal=True)
|
||||||
|
rc = run_reconfigure()
|
||||||
|
if rc != 0:
|
||||||
|
_LOGGER.error("Component discovery failed")
|
||||||
|
return rc
|
||||||
|
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
|
||||||
|
write_project(minimal=False)
|
||||||
|
if CORE.testing_mode:
|
||||||
|
# Reconfigure again so cmake is up to date with the full component
|
||||||
|
# list. This ensures idf.py build won't re-run cmake, which would
|
||||||
|
# regenerate memory.ld and wipe the DRAM/IRAM patches applied below.
|
||||||
|
rc = run_reconfigure()
|
||||||
|
if rc != 0:
|
||||||
|
_LOGGER.error("Reconfigure with discovered components failed")
|
||||||
|
return rc
|
||||||
|
|
||||||
|
# In testing mode, generate the linker script first, patch DRAM/IRAM sizes,
|
||||||
|
# then build. memory.ld is regenerated by ninja during the build phase,
|
||||||
|
# so we must patch after it's generated but before linking (same timing
|
||||||
|
# as iram_fix.py.script's AddPreAction hook in the PlatformIO path).
|
||||||
|
if CORE.testing_mode:
|
||||||
|
memory_ld = CORE.relative_build_path(
|
||||||
|
"build", "esp-idf", "esp_system", "ld", "memory.ld"
|
||||||
|
)
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
# Build just the memory.ld target - ninja needs the path relative to build dir
|
||||||
|
memory_ld_target = os.path.relpath(str(memory_ld), str(build_dir))
|
||||||
|
env = _get_idf_env()
|
||||||
|
ninja_executable = _get_idf_tool("ninja")
|
||||||
|
result = subprocess.run(
|
||||||
|
[ninja_executable, "-C", str(build_dir), memory_ld_target],
|
||||||
|
env=env,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
_LOGGER.error("Failed to generate linker script")
|
||||||
|
return result.returncode
|
||||||
|
_patch_memory_segments()
|
||||||
|
|
||||||
|
# Build
|
||||||
|
args = []
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
args.append("-v")
|
||||||
|
|
||||||
|
args.extend(_get_sdkconfig_args())
|
||||||
|
args.append("build")
|
||||||
|
|
||||||
|
return run_idf_py(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def get_firmware_path() -> Path:
|
||||||
|
"""Get the path to the compiled firmware binary.
|
||||||
|
|
||||||
|
This is the file idf.py writes directly (named after the project),
|
||||||
|
not the copy used for OTA/factory downloads below.
|
||||||
|
"""
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
return build_dir / f"{CORE.name}.bin"
|
||||||
|
|
||||||
|
|
||||||
|
def get_factory_firmware_path() -> Path:
|
||||||
|
"""Get the path to the factory firmware (with bootloader).
|
||||||
|
|
||||||
|
Uses the PlatformIO ``firmware.factory.bin`` naming convention so
|
||||||
|
the dashboard's download handler — which requests files by name
|
||||||
|
relative to ``firmware_bin_path.parent`` — finds it. Without this,
|
||||||
|
the native IDF path produced ``<name>.factory.bin`` and the
|
||||||
|
dashboard returned 500 trying to locate ``firmware.factory.bin``.
|
||||||
|
"""
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
return build_dir / "firmware.factory.bin"
|
||||||
|
|
||||||
|
|
||||||
|
def get_ota_firmware_path() -> Path:
|
||||||
|
"""Get the path to the OTA firmware binary.
|
||||||
|
|
||||||
|
Uses the PlatformIO ``firmware.ota.bin`` naming convention for the
|
||||||
|
same dashboard-compatibility reason as ``get_factory_firmware_path``.
|
||||||
|
"""
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
return build_dir / "firmware.ota.bin"
|
||||||
|
|
||||||
|
|
||||||
|
def get_elf_path() -> Path:
|
||||||
|
"""Get the path to the firmware ELF file.
|
||||||
|
|
||||||
|
idf.py writes ``<build>/<name>.elf`` directly; this returns the
|
||||||
|
``<build>/firmware.elf`` copy created by ``create_elf_copy`` so
|
||||||
|
the dashboard's "download ELF" link can find it under the
|
||||||
|
PlatformIO-convention name.
|
||||||
|
"""
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
return build_dir / "firmware.elf"
|
||||||
|
|
||||||
|
|
||||||
|
def get_objdump_path() -> Path:
|
||||||
|
return _get_cmake_tool_path("CMAKE_OBJDUMP")
|
||||||
|
|
||||||
|
|
||||||
|
def get_readelf_path() -> Path:
|
||||||
|
return _get_cmake_tool_path("CMAKE_READELF")
|
||||||
|
|
||||||
|
|
||||||
|
def get_addr2line_path() -> Path:
|
||||||
|
return _get_cmake_tool_path("CMAKE_ADDR2LINE")
|
||||||
|
|
||||||
|
|
||||||
|
def create_factory_bin() -> bool:
|
||||||
|
"""Create factory.bin by merging bootloader, partition table, and app."""
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
flasher_args_path = build_dir / "flasher_args.json"
|
||||||
|
|
||||||
|
if not flasher_args_path.is_file():
|
||||||
|
_LOGGER.warning("flasher_args.json not found, cannot create factory.bin")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(flasher_args_path, encoding="utf-8") as f:
|
||||||
|
flash_data = json.load(f)
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
_LOGGER.error("Failed to read flasher_args.json: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get flash size from config
|
||||||
|
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||||
|
|
||||||
|
# Build esptool merge command
|
||||||
|
sections = []
|
||||||
|
for addr, fname in sorted(
|
||||||
|
flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16)
|
||||||
|
):
|
||||||
|
file_path = build_dir / fname
|
||||||
|
if file_path.is_file():
|
||||||
|
sections.extend([addr, str(file_path)])
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Flash file not found: %s", file_path)
|
||||||
|
|
||||||
|
if not sections:
|
||||||
|
_LOGGER.warning("No flash sections found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
output_path = get_factory_firmware_path()
|
||||||
|
chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32")
|
||||||
|
|
||||||
|
env = _get_idf_env()
|
||||||
|
python_executable = _get_idf_tool("python")
|
||||||
|
cmd = [
|
||||||
|
python_executable,
|
||||||
|
"-m",
|
||||||
|
"esptool",
|
||||||
|
"--chip",
|
||||||
|
chip,
|
||||||
|
"merge_bin",
|
||||||
|
"--flash_size",
|
||||||
|
flash_size,
|
||||||
|
"--output",
|
||||||
|
str(output_path),
|
||||||
|
] + sections
|
||||||
|
|
||||||
|
_LOGGER.info("Creating factory.bin...")
|
||||||
|
result = subprocess.run(cmd, env=env, capture_output=True, text=True, check=False)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
_LOGGER.error("Failed to create factory.bin: %s", result.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
_LOGGER.info("Created: %s", output_path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_ota_bin() -> bool:
|
||||||
|
"""Copy the firmware to firmware.ota.bin for ESPHome OTA compatibility."""
|
||||||
|
firmware_path = get_firmware_path()
|
||||||
|
ota_path = get_ota_firmware_path()
|
||||||
|
|
||||||
|
if not firmware_path.is_file():
|
||||||
|
_LOGGER.warning("Firmware not found: %s", firmware_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
shutil.copy(firmware_path, ota_path)
|
||||||
|
_LOGGER.info("Created: %s", ota_path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_elf_copy() -> bool:
|
||||||
|
"""Copy the ELF binary to firmware.elf for dashboard compatibility.
|
||||||
|
|
||||||
|
idf.py writes the ELF at ``<build>/<name>.elf``; the dashboard's
|
||||||
|
"download ELF" link requests the literal filename ``firmware.elf``
|
||||||
|
(PlatformIO convention), so copy it to that name.
|
||||||
|
"""
|
||||||
|
build_dir = CORE.relative_build_path("build")
|
||||||
|
src_elf = build_dir / f"{CORE.name}.elf"
|
||||||
|
dst_elf = get_elf_path()
|
||||||
|
|
||||||
|
if not src_elf.is_file():
|
||||||
|
_LOGGER.warning("ELF not found: %s", src_elf)
|
||||||
|
return False
|
||||||
|
|
||||||
|
shutil.copy(src_elf, dst_elf)
|
||||||
|
_LOGGER.info("Created: %s", dst_elf)
|
||||||
|
return True
|
||||||
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())
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
r"""Subprocess entry point for running ``idf.py`` with stdio wrapping.
|
||||||
|
|
||||||
|
Invoked as ``python runner.py <script_path> [script args...]``.
|
||||||
|
|
||||||
|
Wraps ``sys.stdout`` and ``sys.stderr`` with a ``_FilteringTTYStream``
|
||||||
|
shim so that:
|
||||||
|
|
||||||
|
1. ``isatty()`` unconditionally returns True. CMake, Ninja, and idf.py's
|
||||||
|
own progress-bar code all check ``stream.isatty()`` to decide between
|
||||||
|
TTY-format output (``\\r`` cursor moves, ANSI colors, fancy progress
|
||||||
|
bars) and a plain fallback. With the wrapper in place they always
|
||||||
|
emit TTY format, even when our real stdout is a pipe to the parent
|
||||||
|
process (e.g. running under the Home Assistant dashboard add-on).
|
||||||
|
Downstream consumers — local terminals and the HA dashboard log
|
||||||
|
viewer — render the TTY control sequences correctly.
|
||||||
|
|
||||||
|
2. ``FILTER_IDF_LINES`` is applied inside the shim's ``write()`` so
|
||||||
|
noisy idf.py output is dropped before it leaves this subprocess.
|
||||||
|
Filtering is skipped when ``-v`` / ``--verbose`` appears in argv so
|
||||||
|
verbose mode still shows everything.
|
||||||
|
|
||||||
|
ESP-IDF runs under its own Python virtual environment which does not
|
||||||
|
have the ``esphome`` package installed, so the runner is intentionally
|
||||||
|
self-contained: no imports from ``esphome`` at all. The line-filtering
|
||||||
|
wrapper is inlined below rather than imported from
|
||||||
|
``esphome.util.RedirectText`` for that reason.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Regex patterns matched against each line of idf.py / CMake / Ninja
|
||||||
|
# output. Lines that match are dropped before reaching the parent
|
||||||
|
# process. Patterns are anchored at the start of the line (the shim
|
||||||
|
# uses ``re.match``). Disabled when the user passes ``-v`` /
|
||||||
|
# ``--verbose`` to ``esphome compile``.
|
||||||
|
FILTER_IDF_LINES: list[str] = [
|
||||||
|
# idf.py's "how to flash" block at the end of a successful build.
|
||||||
|
# ESPHome handles flashing itself, so these instructions just clutter
|
||||||
|
# the output.
|
||||||
|
r"Project build complete\.",
|
||||||
|
r" idf\.py ",
|
||||||
|
r" python -m esptool ",
|
||||||
|
r"or$",
|
||||||
|
r"or from the ",
|
||||||
|
# CMake dumps the full list of IDF component paths on one giant line.
|
||||||
|
# It's purely informational and bloats the log.
|
||||||
|
r"-- Component paths:",
|
||||||
|
# CMake lists every linker script it adds (dozens of lines) and the
|
||||||
|
# complete flat list of IDF components on one giant line. Neither
|
||||||
|
# has diagnostic value for end users.
|
||||||
|
r"-- Adding linker script ",
|
||||||
|
r"-- Components:",
|
||||||
|
# IDF component manager notices: emitted on first build (no lock),
|
||||||
|
# once per stubbed dependency, plus the final "Processing N
|
||||||
|
# dependencies" enumeration. Patterns allow a leading run of dots
|
||||||
|
# because the component manager prints progress dots on the same
|
||||||
|
# line, so a NOTICE often arrives prefixed with ".NOTICE:" or
|
||||||
|
# "...........NOTICE:".
|
||||||
|
r"\.*NOTICE: ",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
# ---- sys.path fix-up ---------------------------------------------------
|
||||||
|
#
|
||||||
|
# When Python runs this file as ``python runner.py``, it prepends the
|
||||||
|
# script's directory — ``<site-packages>/esphome/espidf/`` — to
|
||||||
|
# ``sys.path[0]``. That directory is part of the esphome package whose
|
||||||
|
# sibling ``types.py`` (in ``esphome/``) collides with stdlib ``types``.
|
||||||
|
# Any subsequent import that transitively touches ``types`` (``runpy``,
|
||||||
|
# ``pathlib``, ``functools``, ``typing``, ...) could resolve the wrong
|
||||||
|
# module. Drop the entry pre-emptively. ``sys`` is a built-in so
|
||||||
|
# importing it at module level earlier did not trigger the shadow.
|
||||||
|
if sys.path and sys.path[0]:
|
||||||
|
sys.path.pop(0)
|
||||||
|
# ---- end sys.path fix-up -----------------------------------------------
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import runpy
|
||||||
|
|
||||||
|
# Patch ``os.get_terminal_size`` to return a fallback size instead
|
||||||
|
# of raising ``OSError`` when the underlying fd isn't a real
|
||||||
|
# terminal.
|
||||||
|
#
|
||||||
|
# idf.py's ``fit_text_in_terminal`` (in ``idf_py_actions/tools.py``)
|
||||||
|
# unconditionally calls ``os.get_terminal_size()`` to format ninja
|
||||||
|
# progress lines. When that raises ``[Errno 25] Inappropriate
|
||||||
|
# ioctl for device`` on our pipe-backed stdout, idf.py catches the
|
||||||
|
# exception as ``EnvironmentError`` and silently exits its stdout
|
||||||
|
# reader coroutine — dropping all ninja build output from that
|
||||||
|
# point on. Returning a valid value keeps the coroutine alive so
|
||||||
|
# progress and error lines continue to flow through to the parent
|
||||||
|
# process.
|
||||||
|
#
|
||||||
|
# Honour the ``COLUMNS`` / ``LINES`` env vars if the caller set
|
||||||
|
# them explicitly. Otherwise fall back to ``(0, 0)``, which
|
||||||
|
# ``fit_text_in_terminal`` treats as "unknown width, don't
|
||||||
|
# truncate" (see the ``if not terminal_width: return out`` guard).
|
||||||
|
# Downstream log viewers (local terminals, the HA dashboard) wrap
|
||||||
|
# or scroll long lines themselves, so we'd rather emit the full
|
||||||
|
# file path than have idf.py elide its middle.
|
||||||
|
_orig_get_terminal_size = os.get_terminal_size
|
||||||
|
|
||||||
|
def _get_terminal_size_fallback(fd: int = 1) -> os.terminal_size:
|
||||||
|
try:
|
||||||
|
return _orig_get_terminal_size(fd)
|
||||||
|
except OSError:
|
||||||
|
try:
|
||||||
|
columns = int(os.environ.get("COLUMNS", "0"))
|
||||||
|
except ValueError:
|
||||||
|
columns = 0
|
||||||
|
try:
|
||||||
|
lines = int(os.environ.get("LINES", "0"))
|
||||||
|
except ValueError:
|
||||||
|
lines = 0
|
||||||
|
return os.terminal_size((columns, lines))
|
||||||
|
|
||||||
|
os.get_terminal_size = _get_terminal_size_fallback # type: ignore[assignment]
|
||||||
|
|
||||||
|
# Strip ANSI escape sequences before comparing a line against the filter
|
||||||
|
# patterns, so colorized lines still match plain-text patterns.
|
||||||
|
ansi_escape = re.compile(r"\033[@-_][0-?]*[ -/]*[@-~]")
|
||||||
|
|
||||||
|
class _FilteringTTYStream:
|
||||||
|
r"""Minimal stdout/stderr wrapper.
|
||||||
|
|
||||||
|
* ``isatty()`` unconditionally returns True, tricking downstream
|
||||||
|
code into emitting TTY-format output.
|
||||||
|
* Input is split on ``\\n`` / ``\\r`` via
|
||||||
|
``str.splitlines(keepends=True)`` and any complete line whose
|
||||||
|
ANSI-stripped, right-stripped form matches one of
|
||||||
|
``filter_lines`` is dropped.
|
||||||
|
* Incomplete trailing chunks are held in a buffer until a
|
||||||
|
terminator arrives.
|
||||||
|
|
||||||
|
Mirrors the matching semantics of ``esphome.util.RedirectText``
|
||||||
|
so filter patterns behave identically in both the PlatformIO
|
||||||
|
and IDF runner paths.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream, filter_lines: list[str] | None) -> None:
|
||||||
|
self._stream = stream
|
||||||
|
if filter_lines:
|
||||||
|
combined = r"|".join(r"(?:" + p + r")" for p in filter_lines)
|
||||||
|
self._filter_pattern: re.Pattern[str] | None = re.compile(combined)
|
||||||
|
else:
|
||||||
|
self._filter_pattern = None
|
||||||
|
self._line_buffer = ""
|
||||||
|
|
||||||
|
def __getattr__(self, name: str):
|
||||||
|
return getattr(self._stream, name)
|
||||||
|
|
||||||
|
def isatty(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
self._stream.flush()
|
||||||
|
|
||||||
|
def write(self, data) -> int:
|
||||||
|
# Text streams normally hand us ``str``; decode in case
|
||||||
|
# somebody writes bytes directly.
|
||||||
|
if not isinstance(data, str):
|
||||||
|
data = data.decode(errors="replace")
|
||||||
|
|
||||||
|
if self._filter_pattern is None:
|
||||||
|
self._stream.write(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
self._line_buffer += data
|
||||||
|
for line in self._line_buffer.splitlines(keepends=True):
|
||||||
|
if "\n" not in line and "\r" not in line:
|
||||||
|
# Incomplete — hold until we see a terminator.
|
||||||
|
self._line_buffer = line
|
||||||
|
break
|
||||||
|
self._line_buffer = ""
|
||||||
|
|
||||||
|
stripped = ansi_escape.sub("", line).rstrip()
|
||||||
|
if self._filter_pattern.match(stripped) is not None:
|
||||||
|
continue
|
||||||
|
self._stream.write(line)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(
|
||||||
|
"usage: runner.py <script_path> [args...]",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
script_path = sys.argv[1]
|
||||||
|
|
||||||
|
# Mirror the platformio_runner behaviour: verbose mode disables the
|
||||||
|
# line filter so all output reaches the user.
|
||||||
|
is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[2:])
|
||||||
|
filter_lines = None if is_verbose else FILTER_IDF_LINES or None
|
||||||
|
|
||||||
|
sys.stdout = _FilteringTTYStream(sys.stdout, filter_lines) # type: ignore[assignment]
|
||||||
|
sys.stderr = _FilteringTTYStream(sys.stderr, filter_lines) # type: ignore[assignment]
|
||||||
|
|
||||||
|
# Shift argv so the target script sees its own path as argv[0] and
|
||||||
|
# its own arguments starting at argv[1]. runpy.run_path does not
|
||||||
|
# modify sys.argv itself.
|
||||||
|
sys.argv = [script_path] + sys.argv[2:]
|
||||||
|
|
||||||
|
# Emulate Python's default behaviour of prepending the script's
|
||||||
|
# directory to sys.path[0] when running ``python script.py``.
|
||||||
|
# runpy.run_path does not do this automatically, but idf.py relies
|
||||||
|
# on it to import its sibling modules (python_version_checker,
|
||||||
|
# idf_py_actions, ...).
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(script_path))
|
||||||
|
if script_dir not in sys.path:
|
||||||
|
sys.path.insert(0, script_dir)
|
||||||
|
|
||||||
|
# If idf.py calls sys.exit(), SystemExit propagates out of run_path
|
||||||
|
# and carries the exit code back to our caller. For normal returns,
|
||||||
|
# fall through and exit with 0.
|
||||||
|
runpy.run_path(script_path, run_name="__main__")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
"""ESP-IDF direct build API for ESPHome."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE
|
|
||||||
from esphome.core import CORE, EsphomeError
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_idf_path() -> Path | None:
|
|
||||||
"""Get IDF_PATH from environment or common locations."""
|
|
||||||
# Check environment variable first
|
|
||||||
if "IDF_PATH" in os.environ:
|
|
||||||
path = Path(os.environ["IDF_PATH"])
|
|
||||||
if path.is_dir():
|
|
||||||
return path
|
|
||||||
|
|
||||||
# Check common installation locations
|
|
||||||
common_paths = [
|
|
||||||
Path.home() / "esp" / "esp-idf",
|
|
||||||
Path.home() / ".espressif" / "esp-idf",
|
|
||||||
Path("/opt/esp-idf"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for path in common_paths:
|
|
||||||
if path.is_dir() and (path / "tools" / "idf.py").is_file():
|
|
||||||
return path
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_idf_env() -> dict[str, str]:
|
|
||||||
"""Get environment variables needed for ESP-IDF build.
|
|
||||||
|
|
||||||
Requires the user to have sourced export.sh before running esphome.
|
|
||||||
"""
|
|
||||||
env = os.environ.copy()
|
|
||||||
|
|
||||||
idf_path = _get_idf_path()
|
|
||||||
if idf_path is None:
|
|
||||||
raise EsphomeError(
|
|
||||||
"ESP-IDF not found. Please install ESP-IDF and source export.sh:\n"
|
|
||||||
" git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf\n"
|
|
||||||
" cd ~/esp-idf && ./install.sh\n"
|
|
||||||
" source ~/esp-idf/export.sh\n"
|
|
||||||
"See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/"
|
|
||||||
)
|
|
||||||
|
|
||||||
env["IDF_PATH"] = str(idf_path)
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def run_idf_py(
|
|
||||||
*args, cwd: Path | None = None, capture_output: bool = False
|
|
||||||
) -> int | str:
|
|
||||||
"""Run idf.py with the given arguments."""
|
|
||||||
idf_path = _get_idf_path()
|
|
||||||
if idf_path is None:
|
|
||||||
raise EsphomeError("ESP-IDF not found")
|
|
||||||
|
|
||||||
env = _get_idf_env()
|
|
||||||
idf_py = idf_path / "tools" / "idf.py"
|
|
||||||
|
|
||||||
cmd = ["python", str(idf_py)] + list(args)
|
|
||||||
|
|
||||||
if cwd is None:
|
|
||||||
cwd = CORE.build_path
|
|
||||||
|
|
||||||
_LOGGER.debug("Running: %s", " ".join(cmd))
|
|
||||||
_LOGGER.debug(" in directory: %s", cwd)
|
|
||||||
|
|
||||||
if capture_output:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
cwd=cwd,
|
|
||||||
env=env,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
_LOGGER.error("idf.py failed:\n%s", result.stderr)
|
|
||||||
return result.stdout
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
cwd=cwd,
|
|
||||||
env=env,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
return result.returncode
|
|
||||||
|
|
||||||
|
|
||||||
def run_reconfigure() -> int:
|
|
||||||
"""Run cmake reconfigure only (no build)."""
|
|
||||||
return run_idf_py("reconfigure")
|
|
||||||
|
|
||||||
|
|
||||||
def has_outdated_files():
|
|
||||||
"""Check if the build configuration is stale.
|
|
||||||
|
|
||||||
Returns True if required build files are missing or if configuration inputs
|
|
||||||
are newer than the generated CMake/Ninja build artifacts.
|
|
||||||
"""
|
|
||||||
cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt")
|
|
||||||
|
|
||||||
cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt")
|
|
||||||
cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt")
|
|
||||||
build_config_path = CORE.relative_build_path("build/config")
|
|
||||||
sdkconfig_internal_path = CORE.relative_build_path(
|
|
||||||
f"sdkconfig.{CORE.name}.esphomeinternal"
|
|
||||||
)
|
|
||||||
dependency_lock_path = CORE.relative_build_path("dependencies.lock")
|
|
||||||
build_ninja_path = CORE.relative_build_path("build/build.ninja")
|
|
||||||
|
|
||||||
if not os.path.isdir(build_config_path) or not os.listdir(build_config_path):
|
|
||||||
return True
|
|
||||||
if not os.path.isfile(cmakecache_txt_path):
|
|
||||||
return True
|
|
||||||
if not os.path.isfile(build_ninja_path):
|
|
||||||
return True
|
|
||||||
if os.path.isfile(dependency_lock_path) and os.path.getmtime(
|
|
||||||
dependency_lock_path
|
|
||||||
) > os.path.getmtime(build_ninja_path):
|
|
||||||
return True
|
|
||||||
|
|
||||||
cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path)
|
|
||||||
return any(
|
|
||||||
os.path.getmtime(f) > cmakecache_txt_mtime
|
|
||||||
for f in [
|
|
||||||
_get_idf_path(),
|
|
||||||
cmakelists_txt_build_path,
|
|
||||||
cmakelists_txt_src_path,
|
|
||||||
sdkconfig_internal_path,
|
|
||||||
build_config_path,
|
|
||||||
]
|
|
||||||
if f and os.path.exists(f)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def need_reconfigure() -> bool:
|
|
||||||
from esphome.build_gen.espidf import has_discovered_components
|
|
||||||
|
|
||||||
# We need to reconfigure either if the files are outdated or if there is no component discovered
|
|
||||||
return has_outdated_files() or not has_discovered_components()
|
|
||||||
|
|
||||||
|
|
||||||
def run_compile(config, verbose: bool) -> int:
|
|
||||||
"""Compile the ESP-IDF project.
|
|
||||||
|
|
||||||
Uses two-phase configure to auto-discover available components:
|
|
||||||
1. If no previous build, configure with minimal REQUIRES to discover components
|
|
||||||
2. Regenerate CMakeLists.txt with discovered components
|
|
||||||
3. Run full build
|
|
||||||
"""
|
|
||||||
from esphome.build_gen.espidf import write_project
|
|
||||||
|
|
||||||
# Check if we need to do discovery phase
|
|
||||||
if need_reconfigure():
|
|
||||||
_LOGGER.info("Discovering available ESP-IDF components...")
|
|
||||||
write_project(minimal=True)
|
|
||||||
rc = run_reconfigure()
|
|
||||||
if rc != 0:
|
|
||||||
_LOGGER.error("Component discovery failed")
|
|
||||||
return rc
|
|
||||||
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
|
|
||||||
write_project(minimal=False)
|
|
||||||
|
|
||||||
# Build
|
|
||||||
args = []
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
args.append("-v")
|
|
||||||
|
|
||||||
args.append("build")
|
|
||||||
|
|
||||||
# Set the sdkconfig file
|
|
||||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
|
||||||
if sdkconfig_path.is_file():
|
|
||||||
args.extend(["-D", f"SDKCONFIG={sdkconfig_path}"])
|
|
||||||
|
|
||||||
return run_idf_py(*args)
|
|
||||||
|
|
||||||
|
|
||||||
def get_firmware_path() -> Path:
|
|
||||||
"""Get the path to the compiled firmware binary."""
|
|
||||||
build_dir = CORE.relative_build_path("build")
|
|
||||||
return build_dir / f"{CORE.name}.bin"
|
|
||||||
|
|
||||||
|
|
||||||
def get_factory_firmware_path() -> Path:
|
|
||||||
"""Get the path to the factory firmware (with bootloader)."""
|
|
||||||
build_dir = CORE.relative_build_path("build")
|
|
||||||
return build_dir / f"{CORE.name}.factory.bin"
|
|
||||||
|
|
||||||
|
|
||||||
def create_factory_bin() -> bool:
|
|
||||||
"""Create factory.bin by merging bootloader, partition table, and app."""
|
|
||||||
build_dir = CORE.relative_build_path("build")
|
|
||||||
flasher_args_path = build_dir / "flasher_args.json"
|
|
||||||
|
|
||||||
if not flasher_args_path.is_file():
|
|
||||||
_LOGGER.warning("flasher_args.json not found, cannot create factory.bin")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(flasher_args_path, encoding="utf-8") as f:
|
|
||||||
flash_data = json.load(f)
|
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
|
||||||
_LOGGER.error("Failed to read flasher_args.json: %s", e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get flash size from config
|
|
||||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
|
||||||
|
|
||||||
# Build esptool merge command
|
|
||||||
sections = []
|
|
||||||
for addr, fname in sorted(
|
|
||||||
flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16)
|
|
||||||
):
|
|
||||||
file_path = build_dir / fname
|
|
||||||
if file_path.is_file():
|
|
||||||
sections.extend([addr, str(file_path)])
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Flash file not found: %s", file_path)
|
|
||||||
|
|
||||||
if not sections:
|
|
||||||
_LOGGER.warning("No flash sections found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
output_path = get_factory_firmware_path()
|
|
||||||
chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32")
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
"python",
|
|
||||||
"-m",
|
|
||||||
"esptool",
|
|
||||||
"--chip",
|
|
||||||
chip,
|
|
||||||
"merge_bin",
|
|
||||||
"--flash_size",
|
|
||||||
flash_size,
|
|
||||||
"--output",
|
|
||||||
str(output_path),
|
|
||||||
] + sections
|
|
||||||
|
|
||||||
_LOGGER.info("Creating factory.bin...")
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
_LOGGER.error("Failed to create factory.bin: %s", result.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
_LOGGER.info("Created: %s", output_path)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def create_ota_bin() -> bool:
|
|
||||||
"""Copy the firmware to .ota.bin for ESPHome OTA compatibility."""
|
|
||||||
firmware_path = get_firmware_path()
|
|
||||||
ota_path = firmware_path.with_suffix(".ota.bin")
|
|
||||||
|
|
||||||
if not firmware_path.is_file():
|
|
||||||
_LOGGER.warning("Firmware not found: %s", firmware_path)
|
|
||||||
return False
|
|
||||||
|
|
||||||
shutil.copy(firmware_path, ota_path)
|
|
||||||
_LOGGER.info("Created: %s", ota_path)
|
|
||||||
return True
|
|
||||||
+1
-1
@@ -441,7 +441,7 @@ def perform_ota(
|
|||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
progress = ProgressBar()
|
progress = ProgressBar("Uploading")
|
||||||
while True:
|
while True:
|
||||||
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
|
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
|
||||||
if not chunk:
|
if not chunk:
|
||||||
|
|||||||
+10
-3
@@ -11,7 +11,7 @@ import shutil
|
|||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, TextIO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from esphome.const import __version__ as ESPHOME_VERSION
|
from esphome.const import __version__ as ESPHOME_VERSION
|
||||||
@@ -617,10 +617,15 @@ def sanitize(value):
|
|||||||
class ProgressBar:
|
class ProgressBar:
|
||||||
"""A simple terminal progress bar for upload operations."""
|
"""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.last_progress: int | None = None
|
||||||
|
self.enabled = hasattr(self.stream, "isatty") and self.stream.isatty()
|
||||||
|
|
||||||
def update(self, progress: float) -> None:
|
def update(self, progress: float) -> None:
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
bar_length = 60
|
bar_length = 60
|
||||||
status = ""
|
status = ""
|
||||||
if progress >= 1:
|
if progress >= 1:
|
||||||
@@ -631,11 +636,13 @@ class ProgressBar:
|
|||||||
return
|
return
|
||||||
self.last_progress = new_progress
|
self.last_progress = new_progress
|
||||||
block = int(round(bar_length * 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.write(text)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def done(self) -> None:
|
def done(self) -> None:
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
sys.stderr.write("\n")
|
sys.stderr.write("\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class _MultipartStreamer:
|
|||||||
self._idx = 0
|
self._idx = 0
|
||||||
self._total = len(prefix) + file_size + len(suffix)
|
self._total = len(prefix) + file_size + len(suffix)
|
||||||
self._sent = 0
|
self._sent = 0
|
||||||
self.progress = ProgressBar()
|
self.progress = ProgressBar("Uploading")
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self._total
|
return self._total
|
||||||
|
|||||||
+1
-1
@@ -562,7 +562,7 @@ def lint_constants_usage():
|
|||||||
# Maximum allowed CONF_ constants in esphome/const.py.
|
# Maximum allowed CONF_ constants in esphome/const.py.
|
||||||
# This file is frozen — new constants go in esphome/components/const/__init__.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.
|
# 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"])
|
@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.
|
"""Format test results as GitHub Actions job summary markdown.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -225,11 +225,12 @@ def format_github_summary(test_results: list[TestResult]) -> str:
|
|||||||
lines.append("```bash\n")
|
lines.append("```bash\n")
|
||||||
|
|
||||||
# Generate one command per platform and test type
|
# Generate one command per platform and test type
|
||||||
|
extra_arguments = f" --toolchain {toolchain}" if toolchain else ""
|
||||||
platform_components = group_components_by_platform(failed_results)
|
platform_components = group_components_by_platform(failed_results)
|
||||||
for platform, test_type in sorted(platform_components.keys()):
|
for platform, test_type in sorted(platform_components.keys()):
|
||||||
components_csv = ",".join(platform_components[(platform, test_type)])
|
components_csv = ",".join(platform_components[(platform, test_type)])
|
||||||
lines.append(
|
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")
|
lines.append("```\n")
|
||||||
@@ -274,13 +275,15 @@ def format_github_summary(test_results: list[TestResult]) -> str:
|
|||||||
return "".join(lines)
|
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.
|
"""Write GitHub Actions job summary with test results and timing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
test_results: List of all test results
|
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:
|
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as f:
|
||||||
f.write(summary_content)
|
f.write(summary_content)
|
||||||
|
|
||||||
@@ -308,6 +311,7 @@ def run_esphome_test(
|
|||||||
esphome_command: str,
|
esphome_command: str,
|
||||||
continue_on_fail: bool,
|
continue_on_fail: bool,
|
||||||
use_testing_mode: bool = False,
|
use_testing_mode: bool = False,
|
||||||
|
toolchain: str | None = None,
|
||||||
) -> TestResult:
|
) -> TestResult:
|
||||||
"""Run esphome test for a single component.
|
"""Run esphome test for a single component.
|
||||||
|
|
||||||
@@ -367,8 +371,14 @@ def run_esphome_test(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add command and config file
|
if toolchain:
|
||||||
cmd.extend([esphome_command, str(output_file)])
|
cmd.extend(["--toolchain", toolchain])
|
||||||
|
|
||||||
|
# Add command
|
||||||
|
cmd.append(esphome_command)
|
||||||
|
|
||||||
|
# Add config file
|
||||||
|
cmd.append(str(output_file))
|
||||||
|
|
||||||
# Build command string for display/logging
|
# Build command string for display/logging
|
||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
@@ -432,6 +442,7 @@ def run_grouped_test(
|
|||||||
tests_dir: Path,
|
tests_dir: Path,
|
||||||
esphome_command: str,
|
esphome_command: str,
|
||||||
continue_on_fail: bool,
|
continue_on_fail: bool,
|
||||||
|
toolchain: str | None = None,
|
||||||
) -> TestResult:
|
) -> TestResult:
|
||||||
"""Run esphome test for a group of components with shared bus configs.
|
"""Run esphome test for a group of components with shared bus configs.
|
||||||
|
|
||||||
@@ -510,10 +521,16 @@ def run_grouped_test(
|
|||||||
"-s",
|
"-s",
|
||||||
"target_platform",
|
"target_platform",
|
||||||
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
|
# Build command string for display/logging
|
||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
|
|
||||||
@@ -576,6 +593,7 @@ def run_grouped_component_tests(
|
|||||||
esphome_command: str,
|
esphome_command: str,
|
||||||
continue_on_fail: bool,
|
continue_on_fail: bool,
|
||||||
additional_isolated: set[str] | None = None,
|
additional_isolated: set[str] | None = None,
|
||||||
|
toolchain: str | None = None,
|
||||||
) -> tuple[set[tuple[str, str]], list[TestResult]]:
|
) -> tuple[set[tuple[str, str]], list[TestResult]]:
|
||||||
"""Run grouped component tests.
|
"""Run grouped component tests.
|
||||||
|
|
||||||
@@ -879,6 +897,7 @@ def run_grouped_component_tests(
|
|||||||
tests_dir=tests_dir,
|
tests_dir=tests_dir,
|
||||||
esphome_command=esphome_command,
|
esphome_command=esphome_command,
|
||||||
continue_on_fail=continue_on_fail,
|
continue_on_fail=continue_on_fail,
|
||||||
|
toolchain=toolchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mark all components as tested
|
# Mark all components as tested
|
||||||
@@ -902,6 +921,7 @@ def run_individual_component_test(
|
|||||||
continue_on_fail: bool,
|
continue_on_fail: bool,
|
||||||
tested_components: set[tuple[str, str]],
|
tested_components: set[tuple[str, str]],
|
||||||
test_results: list[TestResult],
|
test_results: list[TestResult],
|
||||||
|
toolchain: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run an individual component test if not already tested in a group.
|
"""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,
|
build_dir=build_dir,
|
||||||
esphome_command=esphome_command,
|
esphome_command=esphome_command,
|
||||||
continue_on_fail=continue_on_fail,
|
continue_on_fail=continue_on_fail,
|
||||||
|
toolchain=toolchain,
|
||||||
)
|
)
|
||||||
test_results.append(test_result)
|
test_results.append(test_result)
|
||||||
|
|
||||||
@@ -942,6 +963,7 @@ def test_components(
|
|||||||
enable_grouping: bool = True,
|
enable_grouping: bool = True,
|
||||||
isolated_components: set[str] | None = None,
|
isolated_components: set[str] | None = None,
|
||||||
base_only: bool = False,
|
base_only: bool = False,
|
||||||
|
toolchain: str | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Test components with optional intelligent grouping.
|
"""Test components with optional intelligent grouping.
|
||||||
|
|
||||||
@@ -1018,6 +1040,7 @@ def test_components(
|
|||||||
esphome_command=esphome_command,
|
esphome_command=esphome_command,
|
||||||
continue_on_fail=continue_on_fail,
|
continue_on_fail=continue_on_fail,
|
||||||
additional_isolated=isolated_components,
|
additional_isolated=isolated_components,
|
||||||
|
toolchain=toolchain,
|
||||||
)
|
)
|
||||||
test_results.extend(grouped_results)
|
test_results.extend(grouped_results)
|
||||||
|
|
||||||
@@ -1046,6 +1069,7 @@ def test_components(
|
|||||||
continue_on_fail=continue_on_fail,
|
continue_on_fail=continue_on_fail,
|
||||||
tested_components=tested_components,
|
tested_components=tested_components,
|
||||||
test_results=test_results,
|
test_results=test_results,
|
||||||
|
toolchain=toolchain,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Platform-specific test
|
# Platform-specific test
|
||||||
@@ -1078,6 +1102,7 @@ def test_components(
|
|||||||
continue_on_fail=continue_on_fail,
|
continue_on_fail=continue_on_fail,
|
||||||
tested_components=tested_components,
|
tested_components=tested_components,
|
||||||
test_results=test_results,
|
test_results=test_results,
|
||||||
|
toolchain=toolchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Separate results into passed and failed
|
# Separate results into passed and failed
|
||||||
@@ -1098,17 +1123,18 @@ def test_components(
|
|||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
print("Commands to reproduce failures (copy-paste to reproduce locally):")
|
print("Commands to reproduce failures (copy-paste to reproduce locally):")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
extra_arguments = f" --toolchain {toolchain}" if toolchain else ""
|
||||||
platform_components = group_components_by_platform(failed_results)
|
platform_components = group_components_by_platform(failed_results)
|
||||||
for platform, test_type in sorted(platform_components.keys()):
|
for platform, test_type in sorted(platform_components.keys()):
|
||||||
components_csv = ",".join(platform_components[(platform, test_type)])
|
components_csv = ",".join(platform_components[(platform, test_type)])
|
||||||
print(
|
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()
|
print()
|
||||||
|
|
||||||
# Write GitHub Actions job summary if in CI
|
# Write GitHub Actions job summary if in CI
|
||||||
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
||||||
write_github_summary(test_results)
|
write_github_summary(test_results, toolchain=toolchain)
|
||||||
|
|
||||||
if failed_results:
|
if failed_results:
|
||||||
return 1
|
return 1
|
||||||
@@ -1161,6 +1187,10 @@ def main() -> int:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Only test base test files (test.*.yaml), not variant files (test-*.yaml)",
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -1180,6 +1210,7 @@ def main() -> int:
|
|||||||
enable_grouping=not args.no_grouping,
|
enable_grouping=not args.no_grouping,
|
||||||
isolated_components=isolated_components,
|
isolated_components=isolated_components,
|
||||||
base_only=args.base_only,
|
base_only=args.base_only,
|
||||||
|
toolchain=args.toolchain,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ from esphome.const import (
|
|||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
CONF_IGNORE_PIN_VALIDATION_ERROR,
|
CONF_IGNORE_PIN_VALIDATION_ERROR,
|
||||||
CONF_NUMBER,
|
CONF_NUMBER,
|
||||||
KEY_NATIVE_IDF,
|
|
||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
|
Toolchain,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from tests.component_tests.types import SetCoreConfigCallable
|
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_path = component_config_path("reproducible_build.yaml")
|
||||||
CORE.config = read_config({})
|
CORE.config = read_config({})
|
||||||
CORE.data[KEY_NATIVE_IDF] = True
|
CORE.toolchain = Toolchain.ESP_IDF
|
||||||
generate_cpp_contents(CORE.config)
|
generate_cpp_contents(CORE.config)
|
||||||
|
|
||||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||||
|
|||||||
@@ -855,7 +855,7 @@ class TestEsphomeCore:
|
|||||||
|
|
||||||
def test_bootloader_bin__native_idf(self, target):
|
def test_bootloader_bin__native_idf(self, target):
|
||||||
"""Native ESP-IDF builds emit the bootloader under build/bootloader/bootloader.bin."""
|
"""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(
|
assert target.bootloader_bin == Path(
|
||||||
"foo/build/build/bootloader/bootloader.bin"
|
"foo/build/build/bootloader/bootloader.bin"
|
||||||
@@ -864,7 +864,7 @@ class TestEsphomeCore:
|
|||||||
def test_bootloader_bin__platformio(self, target):
|
def test_bootloader_bin__platformio(self, target):
|
||||||
"""For PlatformIO builds bootloader.bin lives in the env-specific .pioenvs directory."""
|
"""For PlatformIO builds bootloader.bin lives in the env-specific .pioenvs directory."""
|
||||||
target.name = "test-device"
|
target.name = "test-device"
|
||||||
target.data[const.KEY_NATIVE_IDF] = False
|
target.toolchain = const.Toolchain.PLATFORMIO
|
||||||
|
|
||||||
assert target.bootloader_bin == Path(
|
assert target.bootloader_bin == Path(
|
||||||
"foo/build/.pioenvs/test-device/bootloader.bin"
|
"foo/build/.pioenvs/test-device/bootloader.bin"
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome.const import (
|
||||||
|
KEY_CORE,
|
||||||
|
KEY_TARGET_FRAMEWORK,
|
||||||
|
KEY_TARGET_PLATFORM,
|
||||||
|
Framework,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE, Library
|
||||||
|
import esphome.espidf.component
|
||||||
|
from esphome.espidf.component import (
|
||||||
|
GitSource,
|
||||||
|
IDFComponent,
|
||||||
|
InvalidIDFComponent,
|
||||||
|
URLSource,
|
||||||
|
_check_library_data,
|
||||||
|
_collect_filtered_files,
|
||||||
|
_convert_library_to_component,
|
||||||
|
_detect_requires,
|
||||||
|
_parse_library_json,
|
||||||
|
_parse_library_properties,
|
||||||
|
_process_dependencies,
|
||||||
|
_split_list_by_condition,
|
||||||
|
generate_cmakelists_txt,
|
||||||
|
generate_idf_component_yml,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="tmp_component")
|
||||||
|
def fixture_tmp_component(tmp_path):
|
||||||
|
c = IDFComponent("owner/name", "1.0.0", source=MagicMock())
|
||||||
|
c.path = tmp_path
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="esp32_idf_core")
|
||||||
|
def fixture_esp32_idf_core():
|
||||||
|
CORE.data[KEY_CORE] = {}
|
||||||
|
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = str(Platform.ESP32)
|
||||||
|
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = str(Framework.ESP_IDF)
|
||||||
|
|
||||||
|
|
||||||
|
def test_idf_component_str():
|
||||||
|
c = IDFComponent("foo/bar", "1.0", source=URLSource("http://dummy.com"))
|
||||||
|
assert str(c) == "foo/bar@1.0=http://dummy.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_idf_component_sanitized_name():
|
||||||
|
c = IDFComponent("foo/bar bar-bar", "1.0", source=URLSource("http://dummy.com"))
|
||||||
|
assert c.get_sanitized_name() == "foo/bar_bar-bar"
|
||||||
|
|
||||||
|
|
||||||
|
def test_idf_component_require_name():
|
||||||
|
c = IDFComponent("foo/bar", "1.0", source=URLSource("http://dummy.com"))
|
||||||
|
assert c.get_require_name() == "foo__bar"
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_filtered_files_basic(tmp_path):
|
||||||
|
f1 = tmp_path / "a.c"
|
||||||
|
f2 = tmp_path / "b" / "b.cpp"
|
||||||
|
f1.write_text("int a;")
|
||||||
|
f2.parent.mkdir(parents=True)
|
||||||
|
f2.write_text("int b;")
|
||||||
|
|
||||||
|
result = _collect_filtered_files(tmp_path, ["+<*>"])
|
||||||
|
assert str(f1) in result
|
||||||
|
assert str(f2) in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_filtered_files_exclude(tmp_path):
|
||||||
|
f1 = tmp_path / "a.c"
|
||||||
|
f2 = tmp_path / "b.cpp"
|
||||||
|
f1.write_text("int a;")
|
||||||
|
f2.write_text("int b;")
|
||||||
|
|
||||||
|
result = _collect_filtered_files(tmp_path, ["+<*> -<*.cpp>"])
|
||||||
|
assert str(f1) in result
|
||||||
|
assert str(f2) not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_requires(tmp_path):
|
||||||
|
f = tmp_path / "main.c"
|
||||||
|
f.write_text('#include "mbedtls/foo.h"')
|
||||||
|
|
||||||
|
result = _detect_requires([str(f)])
|
||||||
|
assert "mbedtls" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_requires_ignores_invalid_file(tmp_path):
|
||||||
|
result = _detect_requires([str(tmp_path / "missing.c")])
|
||||||
|
assert result == set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_list_by_condition():
|
||||||
|
items = ["-Iinclude", "-Llib", "-Wall"]
|
||||||
|
|
||||||
|
matched, rest = _split_list_by_condition(
|
||||||
|
items, lambda x: x[2:] if x.startswith("-I") else None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matched == ["include"]
|
||||||
|
assert "-Llib" in rest
|
||||||
|
assert "-Wall" in rest
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_cmakelists_txt_basic(tmp_component):
|
||||||
|
src_dir = tmp_component.path / "src"
|
||||||
|
src_dir.mkdir()
|
||||||
|
f = src_dir / "main.c"
|
||||||
|
f.write_text("int main() {}")
|
||||||
|
|
||||||
|
tmp_component.data = {}
|
||||||
|
|
||||||
|
content = generate_cmakelists_txt(tmp_component)
|
||||||
|
|
||||||
|
assert "idf_component_register" in content
|
||||||
|
assert "main.c" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path):
|
||||||
|
src_dir = tmp_component.path / "src"
|
||||||
|
src_dir.mkdir()
|
||||||
|
(src_dir / "main.c").write_text("int main() {}")
|
||||||
|
|
||||||
|
dep = IDFComponent("dep", "1.0", source=URLSource("http://dummy.com"))
|
||||||
|
dep.path = tmp_path / "dep"
|
||||||
|
tmp_component.dependencies = [dep]
|
||||||
|
|
||||||
|
tmp_component.data = {
|
||||||
|
"build": {"flags": ["-Iinclude", "-Llib", "-lmylib", "-Wall", "-DTEST"]}
|
||||||
|
}
|
||||||
|
|
||||||
|
content = generate_cmakelists_txt(tmp_component)
|
||||||
|
sep = "\\\\" if os.name == "nt" else "/"
|
||||||
|
assert (
|
||||||
|
content
|
||||||
|
== f"""idf_component_register(
|
||||||
|
SRCS "src{sep}main.c"
|
||||||
|
INCLUDE_DIRS "src"
|
||||||
|
REQUIRES dep
|
||||||
|
)
|
||||||
|
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
|
"-DTEST"
|
||||||
|
)
|
||||||
|
target_compile_options(${{COMPONENT_LIB}} PRIVATE
|
||||||
|
"-Wall"
|
||||||
|
)
|
||||||
|
target_link_directories(${{COMPONENT_LIB}} INTERFACE
|
||||||
|
"lib"
|
||||||
|
)
|
||||||
|
target_link_libraries(${{COMPONENT_LIB}} INTERFACE
|
||||||
|
"mylib"
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_idf_component_yml_basic(tmp_component):
|
||||||
|
tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}}
|
||||||
|
result = generate_idf_component_yml(tmp_component)
|
||||||
|
|
||||||
|
assert result == "description: test\nversion: 1.0.0\nrepository: http://aaa\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_idf_component_yml_with_dependencies(tmp_component, tmp_path):
|
||||||
|
dep = IDFComponent("dep", "1.0", source=URLSource("http://dummy.com"))
|
||||||
|
dep.path = tmp_path / "dep"
|
||||||
|
|
||||||
|
tmp_component.dependencies = [dep]
|
||||||
|
tmp_component.data = {}
|
||||||
|
|
||||||
|
result = generate_idf_component_yml(tmp_component)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== f"""version: 1.0.0
|
||||||
|
dependencies:
|
||||||
|
dep:
|
||||||
|
version: '1.0'
|
||||||
|
override_path: {dep.path}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_idf_component_yml_arduino_registry_dep(tmp_component):
|
||||||
|
# Synthetic arduino-esp32 dep with no source / no path: should emit a
|
||||||
|
# version-only entry so the IDF component manager resolves it from the
|
||||||
|
# registry instead of via git.
|
||||||
|
dep = IDFComponent("espressif/arduino-esp32", "3.3.8", source=None)
|
||||||
|
|
||||||
|
tmp_component.dependencies = [dep]
|
||||||
|
tmp_component.data = {}
|
||||||
|
|
||||||
|
result = generate_idf_component_yml(tmp_component)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== """version: 1.0.0
|
||||||
|
dependencies:
|
||||||
|
espressif/arduino-esp32:
|
||||||
|
version: 3.3.8
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_idf_component_yml_missing_path_reraises(tmp_component):
|
||||||
|
# A dep without a path and without a recognised source should re-raise
|
||||||
|
# the underlying RuntimeError instead of silently producing a bad manifest.
|
||||||
|
dep = IDFComponent("foo/bar", "1.0", source=None)
|
||||||
|
|
||||||
|
tmp_component.dependencies = [dep]
|
||||||
|
tmp_component.data = {}
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
generate_idf_component_yml(tmp_component)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_valid(esp32_idf_core):
|
||||||
|
_check_library_data({"platforms": "*", "frameworks": "*"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_valid2(esp32_idf_core):
|
||||||
|
_check_library_data({"platforms": "*"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_valid3(esp32_idf_core):
|
||||||
|
_check_library_data({})
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_valid4(esp32_idf_core):
|
||||||
|
_check_library_data({"platforms": "espressif32", "frameworks": "*"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_valid5(esp32_idf_core):
|
||||||
|
_check_library_data({"platforms": "*", "frameworks": "espidf"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_invalid_platform(esp32_idf_core):
|
||||||
|
with pytest.raises(InvalidIDFComponent):
|
||||||
|
_check_library_data({"platforms": ["other"], "frameworks": "*"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_library_data_invalid_framework(esp32_idf_core):
|
||||||
|
with pytest.raises(InvalidIDFComponent):
|
||||||
|
_check_library_data({"platforms": "*", "frameworks": ["other"]})
|
||||||
|
|
||||||
|
|
||||||
|
def test_extra_script_logs_warning(caplog, esp32_idf_core):
|
||||||
|
extra_script = "myscript.sh"
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
_check_library_data({"build": {"extraScript": extra_script}})
|
||||||
|
|
||||||
|
assert "not supported" in caplog.text
|
||||||
|
assert "myscript.sh" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_library_json(tmp_path):
|
||||||
|
f = tmp_path / "library.json"
|
||||||
|
f.write_text(json.dumps({"name": "test"}))
|
||||||
|
|
||||||
|
result = _parse_library_json(f)
|
||||||
|
assert result["name"] == "test"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_library_properties(tmp_path):
|
||||||
|
f = tmp_path / "library.properties"
|
||||||
|
f.write_text(
|
||||||
|
"""
|
||||||
|
name=Test
|
||||||
|
version=1.0
|
||||||
|
# description=ABCD
|
||||||
|
empty=
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _parse_library_properties(f)
|
||||||
|
|
||||||
|
assert result["name"] == "Test"
|
||||||
|
assert result["version"] == "1.0"
|
||||||
|
assert "empty" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_library_with_repository():
|
||||||
|
lib = Library("name", None, "https://github.com/foo/bar.git#v1.2.3")
|
||||||
|
|
||||||
|
result = _convert_library_to_component(lib)
|
||||||
|
|
||||||
|
assert result.name == "foo/bar"
|
||||||
|
assert result.version == "1.2.3"
|
||||||
|
assert isinstance(result.source, GitSource)
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_library_missing_ref():
|
||||||
|
lib = Library("name", None, "https://github.com/foo/bar.git")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_convert_library_to_component(lib)
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_library_registry(monkeypatch):
|
||||||
|
lib = Library("foo/bar", "^1.0.0", None)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
esphome.espidf.component,
|
||||||
|
"_get_package_from_pio_registry",
|
||||||
|
lambda o, n, r: ("foo", "bar", "1.2.3", "http://example.com/pkg.zip"),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _convert_library_to_component(lib)
|
||||||
|
|
||||||
|
assert result.name == "foo/bar"
|
||||||
|
assert result.version == "1.2.3"
|
||||||
|
assert isinstance(result.source, URLSource)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_dependencies_adds_valid_dependency(tmp_component, monkeypatch):
|
||||||
|
tmp_component.data = {
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
"version": "1.0",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
esphome.espidf.component,
|
||||||
|
"_generate_idf_component",
|
||||||
|
lambda lib: esphome.espidf.component.IDFComponent(
|
||||||
|
lib.name, lib.version, source=URLSource("http://dummy.com")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(esphome.espidf.component, "_check_library_data", lambda x: None)
|
||||||
|
|
||||||
|
_process_dependencies(tmp_component)
|
||||||
|
|
||||||
|
assert len(tmp_component.dependencies) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_dependencies_skips_invalid(tmp_component):
|
||||||
|
tmp_component.data = {
|
||||||
|
"dependencies": [
|
||||||
|
{"name": "foo", "version": "1.0", "platforms": ["arduino"]},
|
||||||
|
{"invalid": "entry"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
_process_dependencies(tmp_component)
|
||||||
|
|
||||||
|
assert tmp_component.dependencies == []
|
||||||
@@ -604,7 +604,8 @@ def test_run_ota_wrapper(mock_run_ota_impl: Mock) -> None:
|
|||||||
|
|
||||||
def test_progress_bar(capsys: CaptureFixture[str]) -> None:
|
def test_progress_bar(capsys: CaptureFixture[str]) -> None:
|
||||||
"""Test ProgressBar functionality."""
|
"""Test ProgressBar functionality."""
|
||||||
progress = espota2.ProgressBar()
|
progress = espota2.ProgressBar("Uploading")
|
||||||
|
progress.enabled = True # Fake TTY
|
||||||
|
|
||||||
# Test initial update
|
# Test initial update
|
||||||
progress.update(0.0)
|
progress.update(0.0)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ from esphome.const import (
|
|||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
|
Toolchain,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, EsphomeError
|
from esphome.core import CORE, EsphomeError
|
||||||
from esphome.espota2 import (
|
from esphome.espota2 import (
|
||||||
@@ -148,6 +149,7 @@ def setup_core(
|
|||||||
config[CONF_WIFI] = {CONF_USE_ADDRESS: address}
|
config[CONF_WIFI] = {CONF_USE_ADDRESS: address}
|
||||||
|
|
||||||
CORE.config = config
|
CORE.config = config
|
||||||
|
CORE.toolchain = Toolchain.PLATFORMIO
|
||||||
|
|
||||||
if platform is not None:
|
if platform is not None:
|
||||||
CORE.data[KEY_CORE] = {}
|
CORE.data[KEY_CORE] = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user