mirror of
https://github.com/esphome/esphome.git
synced 2026-02-18 15:35:59 -07:00
174 lines
5.7 KiB
Python
174 lines
5.7 KiB
Python
"""Toolchain utilities for memory analysis."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# Platform-specific toolchain prefixes
|
|
TOOLCHAIN_PREFIXES = [
|
|
"xtensa-lx106-elf-", # ESP8266
|
|
"xtensa-esp32-elf-", # ESP32
|
|
"xtensa-esp-elf-", # ESP32 (newer IDF)
|
|
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
|
|
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
|
|
"", # System default (no prefix)
|
|
]
|
|
|
|
|
|
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
|
"""Search for a tool in PlatformIO package directories.
|
|
|
|
This handles cases like Zephyr SDK where tools are installed in nested
|
|
directories that aren't in PATH.
|
|
|
|
Args:
|
|
tool_name: Name of the tool (e.g., "readelf", "objdump")
|
|
|
|
Returns:
|
|
Full path to the tool or None if not found
|
|
"""
|
|
# Get PlatformIO packages directory
|
|
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
|
|
if not platformio_home.exists():
|
|
return None
|
|
|
|
# Search patterns for toolchains that might contain the tool
|
|
# Order matters - more specific patterns first
|
|
search_patterns = [
|
|
# Zephyr SDK deeply nested structure (4 levels)
|
|
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
|
|
f"toolchain-*/*/*/bin/*-{tool_name}",
|
|
# Zephyr SDK nested structure (3 levels)
|
|
f"toolchain-*/*/bin/*-{tool_name}",
|
|
f"toolchain-*/bin/*-{tool_name}",
|
|
# Standard PlatformIO toolchain structure
|
|
f"toolchain-*/bin/*{tool_name}",
|
|
]
|
|
|
|
for pattern in search_patterns:
|
|
matches = list(platformio_home.glob(pattern))
|
|
if matches:
|
|
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
|
|
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
|
|
tool_path = str(matches[0])
|
|
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
|
|
return tool_path
|
|
|
|
return None
|
|
|
|
|
|
def resolve_tool_path(
|
|
tool_name: str,
|
|
derived_path: str | None,
|
|
objdump_path: str | None = None,
|
|
) -> str | None:
|
|
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
|
|
|
|
Args:
|
|
tool_name: Name of the tool (e.g., "objdump", "readelf")
|
|
derived_path: Path derived from idedata (may not exist for some platforms)
|
|
objdump_path: Path to objdump binary to derive other tool paths from
|
|
|
|
Returns:
|
|
Resolved path to the tool, or the original derived_path if it exists
|
|
"""
|
|
if derived_path and not Path(derived_path).exists():
|
|
found = find_tool(tool_name, objdump_path)
|
|
if found:
|
|
_LOGGER.debug(
|
|
"Derived %s path %s not found, using %s",
|
|
tool_name,
|
|
derived_path,
|
|
found,
|
|
)
|
|
return found
|
|
return derived_path
|
|
|
|
|
|
def find_tool(
|
|
tool_name: str,
|
|
objdump_path: str | None = None,
|
|
) -> str | None:
|
|
"""Find a toolchain tool by name.
|
|
|
|
First tries to derive the tool path from objdump_path (if provided),
|
|
then searches PlatformIO package directories (for cross-compile toolchains),
|
|
and finally falls back to searching for platform-specific tools in PATH.
|
|
|
|
Args:
|
|
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
|
|
objdump_path: Path to objdump binary to derive other tool paths from
|
|
|
|
Returns:
|
|
Path to the tool or None if not found
|
|
"""
|
|
# Try to derive from objdump path first (most reliable)
|
|
if objdump_path and objdump_path != "objdump":
|
|
objdump_file = Path(objdump_path)
|
|
# Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-)
|
|
new_name = objdump_file.name.replace("objdump", tool_name)
|
|
potential_path = str(objdump_file.with_name(new_name))
|
|
if Path(potential_path).exists():
|
|
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
|
|
return potential_path
|
|
|
|
# Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
|
|
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
|
|
# are for the host architecture, not the target (ARM, Xtensa, etc.)
|
|
if found := _find_in_platformio_packages(tool_name):
|
|
return found
|
|
|
|
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
|
|
for prefix in TOOLCHAIN_PREFIXES:
|
|
cmd = f"{prefix}{tool_name}"
|
|
try:
|
|
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
|
_LOGGER.debug("Found %s: %s", tool_name, cmd)
|
|
return cmd
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
continue
|
|
|
|
_LOGGER.warning("Could not find %s tool", tool_name)
|
|
return None
|
|
|
|
|
|
def run_tool(
|
|
cmd: Sequence[str],
|
|
timeout: int = 30,
|
|
) -> subprocess.CompletedProcess[str] | None:
|
|
"""Run a toolchain command and return the result.
|
|
|
|
Args:
|
|
cmd: Command and arguments to run
|
|
timeout: Timeout in seconds
|
|
|
|
Returns:
|
|
CompletedProcess on success, None on failure
|
|
"""
|
|
try:
|
|
return subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
_LOGGER.warning("Command timed out: %s", " ".join(cmd))
|
|
return None
|
|
except FileNotFoundError:
|
|
_LOGGER.warning("Command not found: %s", cmd[0])
|
|
return None
|
|
except OSError as e:
|
|
_LOGGER.warning("Failed to run command %s: %s", cmd[0], e)
|
|
return None
|