Files
platform-espressif32/builder/frameworks/component_manager.py
T

1146 lines
46 KiB
Python
Raw Normal View History

2025-07-03 17:12:25 +02:00
"""
Component manager for ESP32 Arduino framework builds in PlatformIO.
This module provides the ComponentManager class for handling IDF component
addition/removal, library ignore processing, and build script modifications.
It supports managing ESP-IDF components within Arduino framework projects,
allowing developers to add or remove specific components and handle library
dependencies efficiently.
"""
import os
import shutil
import re
import yaml
from yaml import SafeLoader
from os.path import join
from typing import Set, Optional, Dict, Any, List, Tuple
class ComponentManagerConfig:
"""
Handles configuration and environment setup for component management.
This class centralizes all configuration-related operations and provides
a unified interface for accessing PlatformIO environment settings,
board configurations, and framework paths.
"""
def __init__(self, env):
"""
Initialize the configuration manager with PlatformIO environment.
Extracts and stores essential configuration parameters from the PlatformIO
environment including platform details, board configuration, MCU type,
and framework paths. This initialization ensures all dependent classes
have consistent access to configuration data.
Args:
env: PlatformIO environment object containing project configuration,
board settings, and platform information
"""
self.env = env
self.platform = env.PioPlatform()
self.config = env.GetProjectConfig()
self.board = env.BoardConfig()
# Extract MCU type from board configuration, defaulting to esp32
self.mcu = self.board.get("build.mcu", "esp32").lower()
# Get project source directory path
self.project_src_dir = env.subst("$PROJECT_SRC_DIR")
# Get Arduino framework installation directory
self.arduino_framework_dir = self.platform.get_package_dir("framework-arduinoespressif32")
# Get MCU-specific Arduino libraries directory
self.arduino_libs_mcu = join(self.platform.get_package_dir("framework-arduinoespressif32-libs"), self.mcu)
class ComponentLogger:
"""
Simple logging functionality for component operations.
Provides centralized logging for all component management operations,
tracking changes made during the build process and offering summary
reporting capabilities.
"""
def __init__(self):
"""
Initialize the logger with empty change tracking.
Sets up internal data structures for tracking component changes
and modifications made during the build process.
"""
# List to store all change messages for summary reporting
self.component_changes: List[str] = []
def log_change(self, message: str) -> None:
"""
Log a change message with immediate console output.
Records the change message internally for summary reporting and
immediately prints it to the console with a component manager prefix
for real-time feedback during build operations.
Args:
message: Descriptive message about the change or operation performed
"""
self.component_changes.append(message)
print(f"[ComponentManager] {message}")
def get_changes_summary(self) -> List[str]:
"""
Get a copy of all changes made during the session.
Returns a defensive copy of the change log to prevent external
modification while allowing access to the complete change history.
Returns:
List of change messages in chronological order
"""
return self.component_changes.copy()
def print_changes_summary(self) -> None:
"""
Print a formatted summary of all changes made.
Outputs a nicely formatted summary of all component changes if any
were made, or a simple message indicating no changes occurred.
Useful for end-of-build reporting and debugging.
"""
if self.component_changes:
print("\n=== Component Manager Changes ===")
for change in self.component_changes:
print(f" {change}")
print("=" * 35)
else:
print("[ComponentManager] No changes made")
class ComponentHandler:
"""
Handles IDF component addition and removal operations.
Manages the core functionality for adding and removing ESP-IDF components
from Arduino framework projects, including YAML file manipulation,
component validation, and cleanup operations.
"""
def __init__(self, config: ComponentManagerConfig, logger: ComponentLogger):
"""
Initialize the component handler with configuration and logging.
Sets up the component handler with necessary dependencies for
configuration access and change logging. Initializes tracking
for removed components to enable proper cleanup operations.
Args:
config: Configuration manager instance providing access to paths and settings
logger: Logger instance for recording component operations
"""
self.config = config
self.logger = logger
# Track removed components for cleanup operations
self.removed_components: Set[str] = set()
def handle_component_settings(self, add_components: bool = False, remove_components: bool = False) -> None:
"""
Handle adding and removing IDF components based on project configuration.
Main entry point for component management operations. Processes both
component additions and removals based on project configuration options,
manages backup creation, and handles cleanup of removed components.
Args:
add_components: Whether to process component additions from custom_component_add
remove_components: Whether to process component removals from custom_component_remove
"""
# Create backup before first component removal and on every add of a component
if remove_components and not self.removed_components or add_components:
self._backup_pioarduino_build_py()
self.logger.log_change("Created backup of build file")
# Check if env and GetProjectOption are available
if hasattr(self.config, 'env') and hasattr(self.config.env, 'GetProjectOption'):
component_yml_path = self._get_or_create_component_yml()
component_data = self._load_component_yml(component_yml_path)
if remove_components:
self._process_component_removals(component_data)
if add_components:
self._process_component_additions(component_data)
self._save_component_yml(component_yml_path, component_data)
# Clean up removed components
if self.removed_components:
self._cleanup_removed_components()
def _process_component_removals(self, component_data: Dict[str, Any]) -> None:
"""
Process component removal requests from project configuration.
Reads the custom_component_remove option from platformio.ini and
processes each component for removal from the dependency list.
Handles errors gracefully and logs all operations.
Args:
component_data: Component configuration data dictionary containing dependencies
"""
try:
remove_option = self.config.env.GetProjectOption("custom_component_remove", None)
if remove_option:
# Split multiline option into individual components
components_to_remove = remove_option.splitlines()
self._remove_components(component_data, components_to_remove)
except Exception as e:
self.logger.log_change(f"Error removing components: {str(e)}")
def _process_component_additions(self, component_data: Dict[str, Any]) -> None:
"""
Process component addition requests from project configuration.
Reads the custom_component_add option from platformio.ini and
processes each component for addition to the dependency list.
Handles errors gracefully and logs all operations.
Args:
component_data: Component configuration data dictionary containing dependencies
"""
try:
add_option = self.config.env.GetProjectOption("custom_component_add", None)
if add_option:
# Split multiline option into individual components
components_to_add = add_option.splitlines()
self._add_components(component_data, components_to_add)
except Exception as e:
self.logger.log_change(f"Error adding components: {str(e)}")
def _get_or_create_component_yml(self) -> str:
"""
Get path to idf_component.yml, creating it if necessary.
Searches for existing idf_component.yml files in the Arduino framework
directory first, then in the project source directory. If no file
exists, creates a new one in the project source directory with
default content.
Returns:
Absolute path to the component YAML file
"""
# Try Arduino framework first
framework_yml = join(self.config.arduino_framework_dir, "idf_component.yml")
if os.path.exists(framework_yml):
self._create_backup(framework_yml)
return framework_yml
# Try project source directory
project_yml = join(self.config.project_src_dir, "idf_component.yml")
if os.path.exists(project_yml):
self._create_backup(project_yml)
return project_yml
# Create new file in project source
self._create_default_component_yml(project_yml)
return project_yml
def _create_backup(self, file_path: str) -> None:
"""
Create backup of a file with .orig extension.
Creates a backup copy of the specified file by appending .orig
to the filename. Only creates the backup if it doesn't already
exist to preserve the original state.
Args:
file_path: Absolute path to the file to backup
"""
backup_path = f"{file_path}.orig"
if not os.path.exists(backup_path):
shutil.copy(file_path, backup_path)
def _create_default_component_yml(self, file_path: str) -> None:
"""
Create a default idf_component.yml file with basic ESP-IDF dependency.
Creates a new component YAML file with minimal default content
specifying ESP-IDF version 5.1 or higher as the base dependency.
This ensures compatibility with modern ESP-IDF features.
Args:
file_path: Absolute path where to create the new YAML file
"""
default_content = {
"dependencies": {
"idf": ">=5.1"
}
}
with open(file_path, 'w', encoding='utf-8') as f:
yaml.dump(default_content, f)
def _load_component_yml(self, file_path: str) -> Dict[str, Any]:
"""
Load and parse idf_component.yml file safely.
Attempts to load and parse the YAML file using SafeLoader for
security. Returns a default structure with empty dependencies
if the file cannot be read or parsed.
Args:
file_path: Absolute path to the YAML file to load
Returns:
Parsed YAML data as dictionary, or default structure on failure
"""
try:
with open(file_path, "r", encoding='utf-8') as f:
return yaml.load(f, Loader=SafeLoader) or {"dependencies": {}}
except Exception:
return {"dependencies": {}}
def _save_component_yml(self, file_path: str, data: Dict[str, Any]) -> None:
"""
Save component data to YAML file safely.
Attempts to write the component data dictionary to the specified
YAML file. Handles errors gracefully by silently failing to
prevent build interruption.
Args:
file_path: Absolute path to the YAML file to write
data: Component data dictionary to serialize
"""
try:
with open(file_path, "w", encoding='utf-8') as f:
yaml.dump(data, f)
except Exception:
pass
def _remove_components(self, component_data: Dict[str, Any], components_to_remove: list) -> None:
"""
Remove specified components from the configuration.
Iterates through the list of components to remove, checking if each
exists in the dependencies and removing it if found. Tracks removed
components for later cleanup operations and logs all actions.
Args:
component_data: Component configuration data dictionary
components_to_remove: List of component names to remove
"""
dependencies = component_data.setdefault("dependencies", {})
for component in components_to_remove:
component = component.strip()
if not component:
continue
if component in dependencies:
self.logger.log_change(f"Removed component: {component}")
del dependencies[component]
# Track for cleanup - convert to filesystem-safe name
filesystem_name = self._convert_component_name_to_filesystem(component)
self.removed_components.add(filesystem_name)
else:
self.logger.log_change(f"Component not found: {component}")
def _add_components(self, component_data: Dict[str, Any], components_to_add: list) -> None:
"""
Add specified components to the configuration.
Processes each component entry, parsing name and version information,
and adds new components to the dependencies. Skips components that
already exist and filters out entries that are too short to be valid.
Args:
component_data: Component configuration data dictionary
components_to_add: List of component entries to add (format: name@version or name)
"""
dependencies = component_data.setdefault("dependencies", {})
for component in components_to_add:
component = component.strip()
if len(component) <= 4: # Skip too short entries
continue
component_name, version = self._parse_component_entry(component)
if component_name not in dependencies:
dependencies[component_name] = {"version": version}
self.logger.log_change(f"Added component: {component_name} ({version})")
else:
self.logger.log_change(f"Component already exists: {component_name}")
def _parse_component_entry(self, entry: str) -> Tuple[str, str]:
"""
Parse component entry into name and version components.
Splits component entries that contain version information (format: name@version)
and returns both parts. If no version is specified, defaults to "*" for
latest version.
Args:
entry: Component entry string (e.g., "espressif/esp_timer@1.0.0" or "espressif/esp_timer")
Returns:
Tuple containing (component_name, version)
"""
if "@" in entry:
name, version = entry.split("@", 1)
return (name.strip(), version.strip())
return (entry.strip(), "*")
def _convert_component_name_to_filesystem(self, component_name: str) -> str:
"""
Convert component name from registry format to filesystem format.
Converts component names from ESP Component Registry format (using forward slashes)
to filesystem-safe format (using double underscores) for directory operations.
Args:
component_name: Component name in registry format (e.g., "espressif/esp_timer")
Returns:
Filesystem-safe component name (e.g., "espressif__esp_timer")
"""
return component_name.replace("/", "__")
def _backup_pioarduino_build_py(self) -> None:
"""
Create backup of the original pioarduino-build.py file.
Creates a backup of the Arduino framework's build script before
making modifications. Only operates when Arduino framework is active
and creates MCU-specific backup names to avoid conflicts.
"""
if "arduino" not in self.config.env.subst("$PIOFRAMEWORK"):
return
build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
if os.path.exists(build_py_path) and not os.path.exists(backup_path):
shutil.copy2(build_py_path, backup_path)
def _cleanup_removed_components(self) -> None:
"""
Clean up removed components and restore original build file.
Performs cleanup operations for all components that were removed,
including removing include directories and cleaning up CPPPATH
entries from the build script.
"""
for component in self.removed_components:
self._remove_include_directory(component)
self._remove_cpppath_entries()
def _remove_include_directory(self, component: str) -> None:
"""
Remove include directory for a specific component.
Removes the component's include directory from the Arduino framework
libraries to prevent compilation errors and reduce build overhead.
Args:
component: Component name in filesystem format
"""
include_path = join(self.config.arduino_libs_mcu, "include", component)
if os.path.exists(include_path):
shutil.rmtree(include_path)
def _remove_cpppath_entries(self) -> None:
"""
Remove CPPPATH entries for removed components from pioarduino-build.py.
Scans the Arduino build script and removes include path entries
for all components that were removed from the project. Uses
multiple regex patterns to catch different include path formats.
"""
build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
if not os.path.exists(build_py_path):
return
try:
with open(build_py_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
# Remove CPPPATH entries for each removed component
for component in self.removed_components:
patterns = [
rf'.*join\([^,]*,\s*"include",\s*"{re.escape(component)}"[^)]*\),?\n',
rf'.*"include/{re.escape(component)}"[^,\n]*,?\n',
rf'.*"[^"]*include[^"]*{re.escape(component)}[^"]*"[^,\n]*,?\n'
]
for pattern in patterns:
content = re.sub(pattern, '', content)
if content != original_content:
with open(build_py_path, 'w', encoding='utf-8') as f:
f.write(content)
except Exception:
pass
class LibraryIgnoreHandler:
"""
Handles lib_ignore processing and include removal.
Manages the processing of lib_ignore entries from platformio.ini,
converting library names to include paths and removing corresponding
entries from the build script while protecting critical components.
"""
def __init__(self, config: ComponentManagerConfig, logger: ComponentLogger):
"""
Initialize the library ignore handler.
Sets up the handler with configuration and logging dependencies,
initializes tracking for ignored libraries, and prepares caching
for Arduino library mappings.
Args:
config: Configuration manager instance for accessing paths and settings
logger: Logger instance for recording library operations
"""
self.config = config
self.logger = logger
# Track ignored libraries for processing
self.ignored_libs: Set[str] = set()
# Cache for Arduino library mappings (lazy loaded)
self._arduino_libraries_cache = None
def handle_lib_ignore(self) -> None:
"""
Handle lib_ignore entries from platformio.ini and remove corresponding includes.
Main entry point for library ignore processing. Creates backup if needed,
processes lib_ignore entries from the current environment, and removes
corresponding include paths from the build script.
"""
# Create backup before processing lib_ignore
if not self.ignored_libs:
self._backup_pioarduino_build_py()
# Get lib_ignore entries from current environment only
lib_ignore_entries = self._get_lib_ignore_entries()
if lib_ignore_entries:
self.ignored_libs.update(lib_ignore_entries)
self._remove_ignored_lib_includes()
self.logger.log_change(f"Processed {len(lib_ignore_entries)} ignored libraries")
def _get_lib_ignore_entries(self) -> List[str]:
"""
Get lib_ignore entries from current environment configuration only.
Extracts and processes lib_ignore entries from the platformio.ini
configuration, converting library names to include directory names
and filtering out critical ESP32 components that should never be ignored.
Returns:
List of processed library names ready for include path removal
"""
try:
# Get lib_ignore from current environment only
lib_ignore = self.config.env.GetProjectOption("lib_ignore", [])
if isinstance(lib_ignore, str):
lib_ignore = [lib_ignore]
elif lib_ignore is None:
lib_ignore = []
# Clean and normalize entries
cleaned_entries = []
for entry in lib_ignore:
entry = str(entry).strip()
if entry:
# Convert library names to potential include directory names
include_name = self._convert_lib_name_to_include(entry)
cleaned_entries.append(include_name)
# Filter out critical ESP32 components that should never be ignored
critical_components = [
'lwip', # Network stack
'freertos', # Real-time OS
'esp_system', # System functions
'esp_common', # Common ESP functions
'driver', # Hardware drivers
'nvs_flash', # Non-volatile storage
'spi_flash', # Flash memory access
'esp_timer', # Timer functions
'esp_event', # Event system
'log' # Logging system
]
filtered_entries = []
for entry in cleaned_entries:
if entry not in critical_components:
filtered_entries.append(entry)
return filtered_entries
except Exception:
return []
def _has_bt_ble_dependencies(self) -> bool:
"""
Check if lib_deps contains any BT/BLE related dependencies.
Scans the lib_deps configuration option for Bluetooth or BLE
related keywords to determine if BT components should be protected
from removal even if they appear in lib_ignore.
Returns:
True if BT/BLE dependencies are found in lib_deps
"""
try:
# Get lib_deps from current environment
lib_deps = self.config.env.GetProjectOption("lib_deps", [])
if isinstance(lib_deps, str):
lib_deps = [lib_deps]
elif lib_deps is None:
lib_deps = []
# Convert to string and check for BT/BLE keywords
lib_deps_str = ' '.join(str(dep) for dep in lib_deps).upper()
bt_ble_keywords = ['BLE', 'BT', 'NIMBLE', 'BLUETOOTH']
return any(keyword in lib_deps_str for keyword in bt_ble_keywords)
except Exception:
return False
def _is_bt_related_library(self, lib_name: str) -> bool:
"""
Check if a library name is related to Bluetooth/BLE functionality.
Examines library names for Bluetooth and BLE related keywords
to determine if the library should be protected when BT dependencies
are present in the project.
Args:
lib_name: Library name to check for BT/BLE relation
Returns:
True if library name contains BT/BLE related keywords
"""
lib_name_upper = lib_name.upper()
bt_related_names = [
'BT',
'BLE',
'BLUETOOTH',
'NIMBLE',
'ESP32_BLE',
'ESP32BLE',
'BLUETOOTHSERIAL',
'BLE_ARDUINO',
'ESP_BLE',
'ESP_BT'
]
return any(bt_name in lib_name_upper for bt_name in bt_related_names)
def _get_arduino_core_libraries(self) -> Dict[str, str]:
"""
Get all Arduino core libraries and their corresponding include paths.
Scans the Arduino framework libraries directory to build a mapping
of library names to their corresponding include paths. Reads
library.properties files to get official library names.
Returns:
Dictionary mapping library names to include directory names
"""
libraries_mapping = {}
# Path to Arduino Core Libraries
arduino_libs_dir = join(self.config.arduino_framework_dir, "libraries")
if not os.path.exists(arduino_libs_dir):
return libraries_mapping
try:
for entry in os.listdir(arduino_libs_dir):
lib_path = join(arduino_libs_dir, entry)
if os.path.isdir(lib_path):
lib_name = self._get_library_name_from_properties(lib_path)
if lib_name:
include_path = self._map_library_to_include_path(lib_name, entry)
libraries_mapping[lib_name.lower()] = include_path
libraries_mapping[entry.lower()] = include_path # Also use directory name as key
except Exception:
pass
return libraries_mapping
def _get_library_name_from_properties(self, lib_dir: str) -> Optional[str]:
"""
Extract library name from library.properties file.
Reads the library.properties file in the given directory and
extracts the official library name from the 'name=' field.
Args:
lib_dir: Path to library directory containing library.properties
Returns:
Official library name or None if not found or readable
"""
prop_path = join(lib_dir, "library.properties")
if not os.path.isfile(prop_path):
return None
try:
with open(prop_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('name='):
return line.split('=', 1)[1].strip()
except Exception:
pass
return None
def _map_library_to_include_path(self, lib_name: str, dir_name: str) -> str:
"""
Map library name to corresponding include path.
Converts Arduino library names to their corresponding ESP-IDF
component include paths using an extensive mapping table.
Handles common Arduino libraries and their ESP-IDF equivalents.
Args:
lib_name: Official library name from library.properties
dir_name: Directory name of the library
Returns:
Corresponding ESP-IDF component include path name
"""
lib_name_lower = lib_name.lower().replace(' ', '').replace('-', '_')
dir_name_lower = dir_name.lower()
# Extended mapping list with Arduino Core Libraries
extended_mapping = {
# Core ESP32 mappings
'wifi': 'esp_wifi',
'bluetooth': 'bt',
'bluetoothserial': 'bt',
'ble': 'bt',
'bt': 'bt',
'ethernet': 'esp_eth',
'websocket': 'esp_websocket_client',
'http': 'esp_http_client',
'https': 'esp_https_ota',
'ota': 'esp_https_ota',
'spiffs': 'spiffs',
'fatfs': 'fatfs',
'mesh': 'esp_wifi_mesh',
'smartconfig': 'esp_smartconfig',
'mdns': 'mdns',
'coap': 'coap',
'mqtt': 'mqtt',
'json': 'cjson',
'mbedtls': 'mbedtls',
'openssl': 'openssl',
# Arduino Core specific mappings (safe mappings that don't conflict with critical components)
'esp32blearduino': 'bt',
'esp32_ble_arduino': 'bt',
'esp32': 'esp32',
'wire': 'driver',
'spi': 'driver',
'i2c': 'driver',
'uart': 'driver',
'serial': 'driver',
'analogwrite': 'driver',
'ledc': 'driver',
'pwm': 'driver',
'dac': 'driver',
'adc': 'driver',
'touch': 'driver',
'hall': 'driver',
'rtc': 'driver',
'timer': 'esp_timer',
'preferences': 'arduino_preferences',
'eeprom': 'arduino_eeprom',
'update': 'esp_https_ota',
'httpupdate': 'esp_https_ota',
'httpclient': 'esp_http_client',
'httpsclient': 'esp_https_ota',
'wifimanager': 'esp_wifi',
'wificlientsecure': 'esp_wifi',
'wifiserver': 'esp_wifi',
'wifiudp': 'esp_wifi',
'wificlient': 'esp_wifi',
'wifiap': 'esp_wifi',
'wifimulti': 'esp_wifi',
'esp32webserver': 'esp_http_server',
'webserver': 'esp_http_server',
'asyncwebserver': 'esp_http_server',
'dnsserver': 'lwip',
'netbios': 'netbios',
'simpletime': 'lwip',
'fs': 'vfs',
'sd': 'fatfs',
'sd_mmc': 'fatfs',
'littlefs': 'esp_littlefs',
'ffat': 'fatfs',
'camera': 'esp32_camera',
'esp_camera': 'esp32_camera',
'arducam': 'esp32_camera',
'rainmaker': 'esp_rainmaker',
'esp_rainmaker': 'esp_rainmaker',
'provisioning': 'wifi_provisioning',
'wifiprovisioning': 'wifi_provisioning',
'espnow': 'esp_now',
'esp_now': 'esp_now',
'esptouch': 'esp_smartconfig',
'ping': 'lwip',
'netif': 'lwip',
'tcpip': 'lwip'
}
# Check extended mapping first
if lib_name_lower in extended_mapping:
return extended_mapping[lib_name_lower]
# Check directory name
if dir_name_lower in extended_mapping:
return extended_mapping[dir_name_lower]
# Fallback: Use directory name as include path
return dir_name_lower
def _convert_lib_name_to_include(self, lib_name: str) -> str:
"""
Convert library name to potential include directory name.
Converts library names from platformio.ini lib_ignore entries
to their corresponding include directory names. Uses Arduino
core library mappings and common naming conventions.
Args:
lib_name: Library name from lib_ignore configuration
Returns:
Converted include directory name for path removal
"""
# Load Arduino Core Libraries on first call
if not hasattr(self, '_arduino_libraries_cache'):
self._arduino_libraries_cache = self._get_arduino_core_libraries()
lib_name_lower = lib_name.lower()
# Check Arduino Core Libraries first
if lib_name_lower in self._arduino_libraries_cache:
return self._arduino_libraries_cache[lib_name_lower]
# Remove common prefixes and suffixes
cleaned_name = lib_name_lower
# Remove common prefixes
prefixes_to_remove = ['lib', 'arduino-', 'esp32-', 'esp-']
for prefix in prefixes_to_remove:
if cleaned_name.startswith(prefix):
cleaned_name = cleaned_name[len(prefix):]
# Remove common suffixes
suffixes_to_remove = ['-lib', '-library', '.h']
for suffix in suffixes_to_remove:
if cleaned_name.endswith(suffix):
cleaned_name = cleaned_name[:-len(suffix)]
# Check again with cleaned name
if cleaned_name in self._arduino_libraries_cache:
return self._arduino_libraries_cache[cleaned_name]
# Direct mapping for common cases not in Arduino libraries
direct_mapping = {
'ble': 'bt',
'bluetooth': 'bt',
'bluetoothserial': 'bt'
}
if cleaned_name in direct_mapping:
return direct_mapping[cleaned_name]
return cleaned_name
def _remove_ignored_lib_includes(self) -> None:
"""
Remove include entries for ignored libraries from pioarduino-build.py.
Processes the Arduino build script to remove CPPPATH entries for
all ignored libraries. Implements protection for BT/BLE and DSP
components when dependencies are detected. Uses multiple regex
patterns to catch different include path formats.
"""
build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
if not os.path.exists(build_py_path):
self.logger.log_change("Build file not found")
return
# Check if BT/BLE dependencies exist in lib_deps
bt_ble_protected = self._has_bt_ble_dependencies()
if bt_ble_protected:
self.logger.log_change("BT/BLE protection enabled")
try:
with open(build_py_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
total_removed = 0
# Remove CPPPATH entries for each ignored library
for lib_name in self.ignored_libs:
# Skip BT-related libraries if BT/BLE dependencies are present
if bt_ble_protected and self._is_bt_related_library(lib_name):
self.logger.log_change(f"Protected BT library: {lib_name}")
continue
# Hard protection for DSP components
if lib_name.lower() in ['dsp', 'esp_dsp', 'dsps', 'fft2r', 'dsps_fft2r']:
self.logger.log_change(f"Protected DSP component: {lib_name}")
continue
# Multiple patterns to catch different include formats
patterns = [
rf'.*join\([^,]*,\s*"include",\s*"{re.escape(lib_name)}"[^)]*\),?\n',
rf'.*"include/{re.escape(lib_name)}"[^,\n]*,?\n',
rf'.*"[^"]*include[^"]*{re.escape(lib_name)}[^"]*"[^,\n]*,?\n',
rf'.*"[^"]*/{re.escape(lib_name)}/include[^"]*"[^,\n]*,?\n',
rf'.*"[^"]*{re.escape(lib_name)}[^"]*include[^"]*"[^,\n]*,?\n',
rf'.*join\([^)]*"include"[^)]*"{re.escape(lib_name)}"[^)]*\),?\n',
rf'.*"{re.escape(lib_name)}/include"[^,\n]*,?\n',
rf'\s*"[^"]*/{re.escape(lib_name)}/[^"]*",?\n'
]
removed_count = 0
for pattern in patterns:
matches = re.findall(pattern, content)
if matches:
content = re.sub(pattern, '', content)
removed_count += len(matches)
if removed_count > 0:
self.logger.log_change(f"Ignored library: {lib_name} ({removed_count} entries)")
total_removed += removed_count
# Clean up empty lines and trailing commas
content = re.sub(r'\n\s*\n', '\n', content)
content = re.sub(r',\s*\n\s*\]', '\n]', content)
# Validate and write changes
if self._validate_changes(original_content, content) and content != original_content:
with open(build_py_path, 'w', encoding='utf-8') as f:
f.write(content)
self.logger.log_change(f"Updated build file ({total_removed} total removals)")
except (IOError, OSError) as e:
self.logger.log_change(f"Error processing libraries: {str(e)}")
except Exception as e:
self.logger.log_change(f"Unexpected error processing libraries: {str(e)}")
def _validate_changes(self, original_content: str, new_content: str) -> bool:
"""
Validate that the changes are reasonable and safe.
Performs sanity checks on the modified content to ensure that
the changes don't remove too much content or create invalid
modifications that could break the build process.
Args:
original_content: Original file content before modifications
new_content: Modified file content after processing
Returns:
True if changes are within acceptable limits and safe to apply
"""
original_lines = len(original_content.splitlines())
new_lines = len(new_content.splitlines())
removed_lines = original_lines - new_lines
# Don't allow removing more than 50% of the file or negative changes
return not (removed_lines > original_lines * 0.5 or removed_lines < 0)
def _backup_pioarduino_build_py(self) -> None:
"""
Create backup of the original pioarduino-build.py file.
Creates a backup copy of the Arduino build script before making
modifications. Only operates when Arduino framework is active
and uses MCU-specific backup naming to avoid conflicts.
"""
if "arduino" not in self.config.env.subst("$PIOFRAMEWORK"):
return
build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
if os.path.exists(build_py_path) and not os.path.exists(backup_path):
shutil.copy2(build_py_path, backup_path)
class BackupManager:
"""
Handles backup and restore operations for build files.
Manages the creation and restoration of backup files for the Arduino
framework build scripts, ensuring that original files can be restored
when needed or when builds are cleaned.
"""
def __init__(self, config: ComponentManagerConfig):
"""
Initialize the backup manager with configuration access.
Sets up the backup manager with access to configuration paths
and settings needed for backup and restore operations.
Args:
config: Configuration manager instance providing access to paths
"""
self.config = config
def backup_pioarduino_build_py(self) -> None:
"""
Create backup of the original pioarduino-build.py file.
Creates a backup copy of the Arduino framework's build script
with MCU-specific naming to prevent conflicts between different
ESP32 variants. Only creates backup if it doesn't already exist.
"""
if "arduino" not in self.config.env.subst("$PIOFRAMEWORK"):
return
build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
if os.path.exists(build_py_path) and not os.path.exists(backup_path):
shutil.copy2(build_py_path, backup_path)
def restore_pioarduino_build_py(self, target=None, source=None, env=None) -> None:
"""
Restore the original pioarduino-build.py from backup.
Restores the original Arduino build script from the backup copy
and removes the backup file. This is typically called during
clean operations or when resetting the build environment.
Args:
target: Build target (unused, for PlatformIO compatibility)
source: Build source (unused, for PlatformIO compatibility)
env: Environment (unused, for PlatformIO compatibility)
"""
build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
if os.path.exists(backup_path):
shutil.copy2(backup_path, build_py_path)
os.remove(backup_path)
class ComponentManager:
"""
Main component manager that orchestrates all operations.
Primary interface for component management operations, coordinating
between specialized handlers for components, libraries, and backups.
Uses composition pattern to organize functionality into focused classes.
"""
def __init__(self, env):
"""
Initialize the ComponentManager with composition pattern.
Creates and configures all specialized handler instances using
the composition pattern for better separation of concerns and
maintainability. Each handler focuses on a specific aspect
of component management.
Args:
env: PlatformIO environment object containing project configuration
"""
self.config = ComponentManagerConfig(env)
self.logger = ComponentLogger()
self.component_handler = ComponentHandler(self.config, self.logger)
self.library_handler = LibraryIgnoreHandler(self.config, self.logger)
self.backup_manager = BackupManager(self.config)
def handle_component_settings(self, add_components: bool = False, remove_components: bool = False) -> None:
"""
Handle component operations by delegating to specialized handlers.
Main entry point for component management operations. Coordinates
component addition/removal and library ignore processing, then
provides a summary of all changes made during the session.
Args:
add_components: Whether to process component additions from configuration
remove_components: Whether to process component removals from configuration
"""
self.component_handler.handle_component_settings(add_components, remove_components)
self.library_handler.handle_lib_ignore()
# Print summary
changes = self.logger.get_changes_summary()
if changes:
self.logger.log_change(f"Session completed with {len(changes)} changes")
def handle_lib_ignore(self) -> None:
"""
Delegate lib_ignore handling to specialized handler.
Provides direct access to library ignore processing for cases
where only library handling is needed without component operations.
"""
self.library_handler.handle_lib_ignore()
def restore_pioarduino_build_py(self, target=None, source=None, env=None) -> None:
"""
Delegate backup restoration to backup manager.
Provides access to backup restoration functionality, typically
used during clean operations or build environment resets.
Args:
target: Build target (unused, for PlatformIO compatibility)
source: Build source (unused, for PlatformIO compatibility)
env: Environment (unused, for PlatformIO compatibility)
"""
self.backup_manager.restore_pioarduino_build_py(target, source, env)
def get_changes_summary(self) -> List[str]:
"""
Get summary of changes from logger.
Provides access to the complete list of changes made during
the current session for reporting or debugging purposes.
Returns:
List of change messages in chronological order
"""
return self.logger.get_changes_summary()
def print_changes_summary(self) -> None:
"""
Print changes summary via logger.
Outputs a formatted summary of all changes made during the
session, useful for build reporting and debugging.
"""
self.logger.print_changes_summary()