From 95e92716d935c3bff0bc98ddb2f9137c93bfd3ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Feb 2026 11:19:38 -0600 Subject: [PATCH] tweak --- esphome/analyze_memory/__init__.py | 105 ++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 31 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 5fa827795f8..6aeb0ef8a66 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -397,18 +397,22 @@ class MemoryAnalyzer: return "Other Core" - def _discover_pio_libraries(self) -> dict[str, Path]: + def _discover_pio_libraries( + self, + libraries: dict[str, Path], + hash_to_name: dict[str, str], + ) -> None: """Discover PlatformIO third-party libraries from the build directory. Scans ``lib/`` directories under ``.pioenvs//`` to find library names and their ``.a`` archive paths. - Returns: - Dictionary mapping lowercase library name to ``.a`` file path. + Args: + libraries: Dict to populate with library name -> ``.a`` path mappings. + hash_to_name: Dict to populate with dir name -> library name mappings + for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``). """ - # The ELF is typically at .pioenvs//firmware.elf build_dir = self.elf_path.parent - libraries: dict[str, Path] = {} for entry in build_dir.iterdir(): if not entry.is_dir() or not entry.name.startswith("lib"): @@ -427,6 +431,7 @@ class MemoryAnalyzer: for lib_subdir in entry.iterdir(): if not lib_subdir.is_dir(): continue + lib_name = lib_subdir.name.lower() # The .a file is named lib.a (case-insensitive match) # e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a archive = entry / f"lib{lib_subdir.name}.a" @@ -435,14 +440,57 @@ class MemoryAnalyzer: archives = list(entry.glob("*.a")) archive = archives[0] if archives else None if archive and archive.exists(): - libraries[lib_subdir.name.lower()] = archive + libraries[lib_name] = archive + hash_to_name[entry.name] = lib_name _LOGGER.debug( "Discovered PlatformIO library: %s -> %s", lib_subdir.name, archive, ) - return libraries + def _discover_idf_managed_components( + self, + libraries: dict[str, Path], + hash_to_name: dict[str, str], + ) -> None: + """Discover ESP-IDF managed component libraries from the build directory. + + ESP-IDF managed components (from the IDF component registry) use a + ``__`` naming convention. Source files live under + ``managed_components/__/`` and the compiled archives are at + ``esp-idf/__/lib__.a``. + + Args: + libraries: Dict to populate with library name -> ``.a`` path mappings. + hash_to_name: Dict to populate with dir name -> library name mappings + for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``). + """ + build_dir = self.elf_path.parent + + managed_dir = build_dir / "managed_components" + if not managed_dir.is_dir(): + return + + espidf_dir = build_dir / "esp-idf" + + for entry in managed_dir.iterdir(): + if not entry.is_dir() or "__" not in entry.name: + continue + + # Extract the short name: espressif__mdns -> mdns + full_name = entry.name # e.g., espressif__mdns + short_name = full_name.split("__", 1)[1].lower() + + # Find the .a archive under esp-idf/__/ + archive = espidf_dir / full_name / f"lib{full_name}.a" + if archive.exists(): + libraries[short_name] = archive + hash_to_name[full_name] = short_name + _LOGGER.debug( + "Discovered IDF managed component: %s -> %s", + short_name, + archive, + ) def _build_library_symbol_map(self, libraries: dict[str, Path]) -> dict[str, str]: """Build a symbol-to-library mapping from library archives. @@ -484,42 +532,29 @@ class MemoryAnalyzer: return symbol_map def _scan_pio_libraries(self) -> None: - """Discover PlatformIO libraries and build symbol mapping. + """Discover third-party libraries and build symbol mapping. - Orchestrates library discovery, symbol map construction, and - hash-to-name mapping for CSWTCH attribution. + Scans both PlatformIO ``lib/`` directories (Arduino builds) and + ESP-IDF ``managed_components/`` (IDF builds) to find library archives. + Orchestrates symbol map construction and hash-to-name mapping for + CSWTCH attribution. """ - libraries = self._discover_pio_libraries() + libraries: dict[str, Path] = {} + self._discover_pio_libraries(libraries, self._lib_hash_to_name) + self._discover_idf_managed_components(libraries, self._lib_hash_to_name) + if not libraries: - _LOGGER.debug("No PlatformIO third-party libraries found") + _LOGGER.debug("No third-party libraries found") return _LOGGER.info( - "Scanning %d PlatformIO libraries: %s", + "Scanning %d libraries: %s", len(libraries), ", ".join(sorted(libraries)), ) self._lib_symbol_map = self._build_library_symbol_map(libraries) - # Build hash-to-name mapping for CSWTCH attribution - # e.g., lib641 -> espsoftwareserial - build_dir = self.elf_path.parent - for entry in build_dir.iterdir(): - if not entry.is_dir() or not entry.name.startswith("lib"): - continue - hex_part = entry.name[3:] - if not hex_part: - continue - try: - int(hex_part, 16) - except ValueError: - continue - for lib_subdir in entry.iterdir(): - if lib_subdir.is_dir(): - self._lib_hash_to_name[entry.name] = lib_subdir.name.lower() - break - _LOGGER.info( "Built library symbol map: %d symbols from %d libraries", len(self._lib_symbol_map), @@ -706,6 +741,14 @@ class MemoryAnalyzer: if parts and parts[0] in self._lib_hash_to_name: return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}" + # ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns + if ( + len(parts) >= 2 + and parts[0] == "managed_components" + and parts[1] in self._lib_hash_to_name + ): + return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}" + # Other framework/library files - return the first path component # e.g., FrameworkArduino/... -> FrameworkArduino return parts[0] if parts else source_file