Release 55.03.30-2

This commit is contained in:
Jason2866
2025-08-03 13:07:04 +02:00
parent d57b0864a4
commit 02a8dc883a
3 changed files with 303 additions and 45 deletions
+79 -14
View File
@@ -34,6 +34,16 @@ from SCons.Script import (
from platformio.project.helpers import get_project_dir from platformio.project.helpers import get_project_dir
from platformio.package.version import pepver_to_semver from platformio.package.version import pepver_to_semver
from platformio.util import get_serial_ports from platformio.util import get_serial_ports
from platformio.compat import IS_WINDOWS
# Check Python version requirement
if sys.version_info < (3, 10):
sys.stderr.write(
f"Error: Python 3.10 or higher is required. "
f"Current version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\n"
f"Please update your Python installation.\n"
)
sys.exit(1)
# Python dependencies required for the build process # Python dependencies required for the build process
python_deps = { python_deps = {
@@ -56,6 +66,68 @@ PYTHON_EXE = env.subst("$PYTHONEXE") # Global Python executable path
# Framework directory path # Framework directory path
FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32") FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
platformio_dir = projectconfig.get("platformio", "core_dir")
penv_dir = os.path.join(platformio_dir, "penv")
pip_path = os.path.join(
penv_dir,
"Scripts" if IS_WINDOWS else "bin",
"pip" + (".exe" if IS_WINDOWS else ""),
)
def setup_pipenv_in_package():
"""
Checks if 'penv' folder exists in platformio dir and creates virtual environment if not.
"""
if not os.path.exists(penv_dir):
env.Execute(
env.VerboseAction(
'"$PYTHONEXE" -m venv --clear "%s"' % penv_dir,
"Creating a new virtual environment for Python dependencies",
)
)
assert os.path.isfile(
pip_path
), "Error: Failed to create a proper virtual environment. Missing the `pip` binary!"
penv_python = os.path.join(penv_dir, "Scripts", "python.exe") if IS_WINDOWS else os.path.join(penv_dir, "bin", "python")
env.Replace(PYTHONEXE=penv_python)
print(f"PYTHONEXE updated to penv environment: {penv_python}")
setup_pipenv_in_package()
# Update global PYTHON_EXE variable after potential pipenv setup
PYTHON_EXE = env.subst("$PYTHONEXE")
python_exe = PYTHON_EXE
# Ensure penv Python directory is in PATH for subprocess calls
python_dir = os.path.dirname(PYTHON_EXE)
current_path = os.environ.get("PATH", "")
if python_dir not in current_path:
os.environ["PATH"] = python_dir + os.pathsep + current_path
# Verify the Python executable exists
assert os.path.isfile(PYTHON_EXE), f"Python executable not found: {PYTHON_EXE}"
if os.path.isfile(python_exe):
# Update sys.path to include penv site-packages
if IS_WINDOWS:
penv_site_packages = os.path.join(penv_dir, "Lib", "site-packages")
else:
# Find the actual site-packages directory in the venv
penv_lib_dir = os.path.join(penv_dir, "lib")
if os.path.isdir(penv_lib_dir):
for python_dir in os.listdir(penv_lib_dir):
if python_dir.startswith("python"):
penv_site_packages = os.path.join(penv_lib_dir, python_dir, "site-packages")
break
else:
penv_site_packages = None
else:
penv_site_packages = None
if penv_site_packages and os.path.isdir(penv_site_packages) and penv_site_packages not in sys.path:
sys.path.insert(0, penv_site_packages)
def add_to_pythonpath(path): def add_to_pythonpath(path):
""" """
@@ -80,14 +152,10 @@ def add_to_pythonpath(path):
if normalized_path not in sys.path: if normalized_path not in sys.path:
sys.path.insert(0, normalized_path) sys.path.insert(0, normalized_path)
def setup_python_paths(): def setup_python_paths():
""" """
Setup Python paths based on the actual Python executable being used. Setup Python paths based on the actual Python executable being used.
""" """
if not PYTHON_EXE or not os.path.isfile(PYTHON_EXE):
return
# Get the directory containing the Python executable # Get the directory containing the Python executable
python_dir = os.path.dirname(PYTHON_EXE) python_dir = os.path.dirname(PYTHON_EXE)
add_to_pythonpath(python_dir) add_to_pythonpath(python_dir)
@@ -107,7 +175,6 @@ def setup_python_paths():
# Setup Python paths based on the actual Python executable # Setup Python paths based on the actual Python executable
setup_python_paths() setup_python_paths()
def _get_executable_path(python_exe, executable_name): def _get_executable_path(python_exe, executable_name):
""" """
Get the path to an executable binary (esptool, uv, etc.) based on the Python executable path. Get the path to an executable binary (esptool, uv, etc.) based on the Python executable path.
@@ -119,14 +186,11 @@ def _get_executable_path(python_exe, executable_name):
Returns: Returns:
str: Path to executable or fallback to executable name str: Path to executable or fallback to executable name
""" """
if not python_exe or not os.path.isfile(python_exe):
return executable_name # Fallback to command name
python_dir = os.path.dirname(python_exe) python_dir = os.path.dirname(python_exe)
if sys.platform == "win32": if IS_WINDOWS:
scripts_dir = os.path.join(python_dir, "Scripts") executable_path = os.path.join(python_dir, f"{executable_name}.exe")
executable_path = os.path.join(scripts_dir, f"{executable_name}.exe")
else: else:
# For Unix-like systems, executables are typically in the same directory as python # For Unix-like systems, executables are typically in the same directory as python
# or in a bin subdirectory # or in a bin subdirectory
@@ -228,7 +292,7 @@ def install_python_deps():
uv_executable = _get_uv_executable_path(PYTHON_EXE) uv_executable = _get_uv_executable_path(PYTHON_EXE)
# Add Scripts directory to PATH for Windows # Add Scripts directory to PATH for Windows
if sys.platform == "win32": if IS_WINDOWS:
python_dir = os.path.dirname(PYTHON_EXE) python_dir = os.path.dirname(PYTHON_EXE)
scripts_dir = os.path.join(python_dir, "Scripts") scripts_dir = os.path.join(python_dir, "Scripts")
if os.path.isdir(scripts_dir): if os.path.isdir(scripts_dir):
@@ -366,8 +430,10 @@ def install_esptool():
return 'esptool' # Fallback return 'esptool' # Fallback
# Install Python dependencies and esptool # Install Python dependencies
install_python_deps() install_python_deps()
# Install esptool after dependencies
esptool_binary_path = install_esptool() esptool_binary_path = install_esptool()
@@ -756,7 +822,6 @@ objcopy_value = (
if ' ' in esptool_binary_path if ' ' in esptool_binary_path
else esptool_binary_path else esptool_binary_path
) )
# Configure build tools and environment variables # Configure build tools and environment variables
env.Replace( env.Replace(
__get_board_boot_mode=_get_board_boot_mode, __get_board_boot_mode=_get_board_boot_mode,
+4 -3
View File
@@ -92,13 +92,14 @@
"type": "uploader", "type": "uploader",
"optional": true, "optional": true,
"owner": "pioarduino", "owner": "pioarduino",
"package-version": "5.0.1", "package-version": "5.0.2",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/esptoolpy-v5.0.1.zip" "version": "https://github.com/pioarduino/registry/releases/download/0.0.1/esptoolpy-v5.0.2.zip"
}, },
"tl-install": { "tool-esp_install": {
"type": "tool", "type": "tool",
"optional": false, "optional": false,
"owner": "pioarduino", "owner": "pioarduino",
"package-version": "5.1.0",
"version": "https://github.com/pioarduino/esp_install/releases/download/v5.1.0/esp_install-v5.1.0.zip" "version": "https://github.com/pioarduino/esp_install/releases/download/v5.1.0/esp_install-v5.1.0.zip"
}, },
"contrib-piohome": { "contrib-piohome": {
+220 -28
View File
@@ -31,6 +31,7 @@ RETRY_LIMIT = 3
SUBPROCESS_TIMEOUT = 300 SUBPROCESS_TIMEOUT = 300
DEFAULT_DEBUG_SPEED = "5000" DEFAULT_DEBUG_SPEED = "5000"
DEFAULT_APP_OFFSET = "0x10000" DEFAULT_APP_OFFSET = "0x10000"
tl_install_name = "tool-esp_install"
# MCUs that support ESP-builtin debug # MCUs that support ESP-builtin debug
ESP_BUILTIN_DEBUG_MCUS = frozenset([ ESP_BUILTIN_DEBUG_MCUS = frozenset([
@@ -98,6 +99,15 @@ def safe_file_operation(operation_func):
return wrapper return wrapper
@safe_file_operation
def safe_remove_file(path: str) -> bool:
"""Safely remove a file with error handling."""
if os.path.exists(path) and os.path.isfile(path):
os.remove(path)
logger.debug(f"File removed: {path}")
return True
@safe_file_operation @safe_file_operation
def safe_remove_directory(path: str) -> bool: def safe_remove_directory(path: str) -> bool:
"""Safely remove directories with error handling.""" """Safely remove directories with error handling."""
@@ -130,6 +140,15 @@ def safe_copy_file(src: str, dst: str) -> bool:
return True return True
@safe_file_operation
def safe_copy_directory(src: str, dst: str) -> bool:
"""Safely copy directories with error handling."""
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copytree(src, dst, dirs_exist_ok=True)
logger.debug(f"Directory copied: {src} -> {dst}")
return True
class Espressif32Platform(PlatformBase): class Espressif32Platform(PlatformBase):
"""ESP32 platform implementation for PlatformIO with optimized toolchain management.""" """ESP32 platform implementation for PlatformIO with optimized toolchain management."""
@@ -148,22 +167,183 @@ class Espressif32Platform(PlatformBase):
self._packages_dir = config.get("platformio", "packages_dir") self._packages_dir = config.get("platformio", "packages_dir")
return self._packages_dir return self._packages_dir
def _check_tl_install_version(self) -> bool:
"""
Check if tool-esp_install is installed in the correct version.
Install the correct version only if version differs.
Returns:
bool: True if correct version is available, False on error
"""
# Get required version from platform.json
required_version = self.packages.get(tl_install_name, {}).get("version")
if not required_version:
logger.debug(f"No version check required for {tl_install_name}")
return True
# Check if tool is already installed
tl_install_path = os.path.join(self.packages_dir, tl_install_name)
package_json_path = os.path.join(tl_install_path, "package.json")
if not os.path.exists(package_json_path):
logger.info(f"{tl_install_name} not installed, installing version {required_version}")
return self._install_tl_install(required_version)
# Read installed version
try:
with open(package_json_path, 'r', encoding='utf-8') as f:
package_data = json.load(f)
installed_version = package_data.get("version")
if not installed_version:
logger.warning(f"Installed version for {tl_install_name} unknown, installing {required_version}")
return self._install_tl_install(required_version)
# IMPORTANT: Compare versions correctly
if self._compare_tl_install_versions(installed_version, required_version):
logger.debug(f"{tl_install_name} version {installed_version} is already correctly installed")
# IMPORTANT: Set package as available, but do NOT reinstall
self.packages[tl_install_name]["optional"] = True
return True
else:
logger.info(
f"Version mismatch for {tl_install_name}: "
f"installed={installed_version}, required={required_version}, installing correct version"
)
return self._install_tl_install(required_version)
except (json.JSONDecodeError, FileNotFoundError) as e:
logger.error(f"Error reading package data for {tl_install_name}: {e}")
return self._install_tl_install(required_version)
def _compare_tl_install_versions(self, installed: str, required: str) -> bool:
"""
Compare installed and required version of tool-esp_install.
Args:
installed: Currently installed version string
required: Required version string from platform.json
Returns:
bool: True if versions match, False otherwise
"""
# For URL-based versions: Extract version string from URL
installed_clean = self._extract_version_from_url(installed)
required_clean = self._extract_version_from_url(required)
logger.debug(f"Version comparison: installed='{installed_clean}' vs required='{required_clean}'")
return installed_clean == required_clean
def _extract_version_from_url(self, version_string: str) -> str:
"""
Extract version information from URL or return version directly.
Args:
version_string: Version string or URL containing version
Returns:
str: Extracted version string
"""
if version_string.startswith(('http://', 'https://')):
# Extract version from URL like: .../v5.1.0/esp_install-v5.1.0.zip
import re
version_match = re.search(r'v(\d+\.\d+\.\d+)', version_string)
if version_match:
return version_match.group(1) # Returns "5.1.0"
else:
# Fallback: Use entire URL
return version_string
else:
# Direct version number
return version_string.strip()
def _install_tl_install(self, version: str) -> bool:
"""
Install tool-esp_install ONLY when necessary
and handles backwards compatibility for tl-install.
Args:
version: Version string or URL to install
Returns:
bool: True if installation successful, False otherwise
"""
tl_install_path = os.path.join(self.packages_dir, tl_install_name)
old_tl_install_path = os.path.join(self.packages_dir, "tl-install")
try:
old_tl_install_exists = os.path.exists(old_tl_install_path)
if old_tl_install_exists:
# remove outdated tl-install
safe_remove_directory(old_tl_install_path)
if os.path.exists(tl_install_path):
logger.info(f"Removing old {tl_install_name} installation")
safe_remove_directory(tl_install_path)
logger.info(f"Installing {tl_install_name} version {version}")
self.packages[tl_install_name]["optional"] = False
self.packages[tl_install_name]["version"] = version
pm.install(version)
# Ensure backward compatibility by removing pio install status indicator
tl_piopm_path = os.path.join(tl_install_path, ".piopm")
safe_remove_file(tl_piopm_path)
if os.path.exists(os.path.join(tl_install_path, "package.json")):
logger.info(f"{tl_install_name} successfully installed and verified")
self.packages[tl_install_name]["optional"] = True
# Handle old tl-install to keep backwards compatibility
if old_tl_install_exists:
# Copy tool-esp_install content to tl-install location
if safe_copy_directory(tl_install_path, old_tl_install_path):
logger.info(f"Content copied from {tl_install_name} to old tl-install location")
else:
logger.warning("Failed to copy content to old tl-install location")
return True
else:
logger.error(f"{tl_install_name} installation failed - package.json not found")
return False
except Exception as e:
logger.error(f"Error installing {tl_install_name}: {e}")
return False
def _cleanup_versioned_tool_directories(self, tool_name: str) -> None:
"""
Clean up versioned tool directories containing '@' or version suffixes.
This function should be called during every tool version check.
Args:
tool_name: Name of the tool to clean up
"""
if not os.path.exists(self.packages_dir) or not os.path.isdir(self.packages_dir):
return
try:
# Remove directories with '@' in their name (e.g., tool-name@version, tool-name@src)
safe_remove_directory_pattern(self.packages_dir, f"{tool_name}@*")
# Remove directories with version suffixes (e.g., tool-name.12345)
safe_remove_directory_pattern(self.packages_dir, f"{tool_name}.*")
# Also check for any directory that starts with tool_name and contains '@'
for item in os.listdir(self.packages_dir):
if item.startswith(tool_name) and '@' in item:
item_path = os.path.join(self.packages_dir, item)
if os.path.isdir(item_path):
safe_remove_directory(item_path)
logger.debug(f"Removed versioned directory: {item_path}")
except OSError as e:
logger.error(f"Error cleaning up versioned directories for {tool_name}: {e}")
def _get_tool_paths(self, tool_name: str) -> Dict[str, str]: def _get_tool_paths(self, tool_name: str) -> Dict[str, str]:
"""Get centralized path calculation for tools with caching.""" """Get centralized path calculation for tools with caching."""
if tool_name not in self._tools_cache: if tool_name not in self._tools_cache:
tool_path = os.path.join(self.packages_dir, tool_name) tool_path = os.path.join(self.packages_dir, tool_name)
# Remove all directories containing '@' in their name
try:
if os.path.exists(self.packages_dir) and os.path.isdir(self.packages_dir):
for item in os.listdir(self.packages_dir):
if '@' in item and item.startswith(tool_name):
item_path = os.path.join(self.packages_dir, item)
if os.path.isdir(item_path):
safe_remove_directory(item_path)
logger.debug(f"Removed directory with '@' in name: {item_path}")
except OSError as e:
logger.error(f"Error scanning packages directory for '@' directories: {e}")
self._tools_cache[tool_name] = { self._tools_cache[tool_name] = {
'tool_path': tool_path, 'tool_path': tool_path,
@@ -171,7 +351,7 @@ class Espressif32Platform(PlatformBase):
'tools_json_path': os.path.join(tool_path, "tools.json"), 'tools_json_path': os.path.join(tool_path, "tools.json"),
'piopm_path': os.path.join(tool_path, ".piopm"), 'piopm_path': os.path.join(tool_path, ".piopm"),
'idf_tools_path': os.path.join( 'idf_tools_path': os.path.join(
self.packages_dir, "tl-install", "tools", "idf_tools.py" self.packages_dir, tl_install_name, "tools", "idf_tools.py"
) )
} }
return self._tools_cache[tool_name] return self._tools_cache[tool_name]
@@ -223,6 +403,9 @@ class Espressif32Platform(PlatformBase):
def _check_tool_version(self, tool_name: str) -> bool: def _check_tool_version(self, tool_name: str) -> bool:
"""Check if the installed tool version matches the required version.""" """Check if the installed tool version matches the required version."""
# Clean up versioned directories FIRST, before any version checks
self._cleanup_versioned_tool_directories(tool_name)
paths = self._get_tool_paths(tool_name) paths = self._get_tool_paths(tool_name)
try: try:
@@ -314,23 +497,16 @@ class Espressif32Platform(PlatformBase):
logger.debug(f"Tool {tool_name} found with correct version") logger.debug(f"Tool {tool_name} found with correct version")
return True return True
# Wrong version, reinstall - remove similar paths too # Wrong version, reinstall - cleanup is already done in _check_tool_version
logger.info(f"Reinstalling {tool_name} due to version mismatch") logger.info(f"Reinstalling {tool_name} due to version mismatch")
tool_base_name = os.path.basename(paths['tool_path']) # Remove the main tool directory (if it still exists after cleanup)
packages_dir = os.path.dirname(paths['tool_path'])
# Remove similar directories with version suffixes FIRST (e.g., xtensa@src, xtensa.12232)
safe_remove_directory_pattern(packages_dir, f"{tool_base_name}@*")
safe_remove_directory_pattern(packages_dir, f"{tool_base_name}.*")
# Then remove the main tool directory (if it still exists)
safe_remove_directory(paths['tool_path']) safe_remove_directory(paths['tool_path'])
return self.install_tool(tool_name, retry_count + 1) return self.install_tool(tool_name, retry_count + 1)
def _configure_arduino_framework(self, frameworks: List[str]) -> None: def _configure_arduino_framework(self, frameworks: List[str]) -> None:
"""Configure Arduino framework""" """Configure Arduino framework dependencies."""
if "arduino" not in frameworks: if "arduino" not in frameworks:
return return
@@ -403,12 +579,28 @@ class Espressif32Platform(PlatformBase):
self.install_tool("tool-openocd-esp32") self.install_tool("tool-openocd-esp32")
def _configure_installer(self) -> None: def _configure_installer(self) -> None:
"""Configure the ESP-IDF tools installer.""" """Configure the ESP-IDF tools installer with proper version checking."""
# Check version - installs only when needed
if not self._check_tl_install_version():
logger.error("Error during tool-esp_install version check / installation")
return
# Remove pio install marker to avoid issues when switching versions
old_tl_piopm_path = os.path.join(self.packages_dir, "tl-install", ".piopm")
if os.path.exists(old_tl_piopm_path):
safe_remove_file(old_tl_piopm_path)
# Check if idf_tools.py is available
installer_path = os.path.join( installer_path = os.path.join(
self.packages_dir, "tl-install", "tools", "idf_tools.py" self.packages_dir, tl_install_name, "tools", "idf_tools.py"
) )
if os.path.exists(installer_path): if os.path.exists(installer_path):
self.packages["tl-install"]["optional"] = True logger.debug(f"{tl_install_name} is available and ready")
self.packages[tl_install_name]["optional"] = True
else:
logger.warning(f"idf_tools.py not found in {installer_path}")
def _install_esptool_package(self) -> None: def _install_esptool_package(self) -> None:
"""Install esptool package required for all builds.""" """Install esptool package required for all builds."""
@@ -443,7 +635,7 @@ class Espressif32Platform(PlatformBase):
os.remove(piopm_path) os.remove(piopm_path)
logger.info(f"Incompatible mklittlefs version {version} removed (required: 3.x)") logger.info(f"Incompatible mklittlefs version {version} removed (required: 3.x)")
except (json.JSONDecodeError, KeyError) as e: except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Error reading mklittlefs package data: {e}") logger.error(f"Error reading mklittlefs package {e}")
def _setup_mklittlefs_for_download(self) -> None: def _setup_mklittlefs_for_download(self) -> None:
"""Setup mklittlefs for download functionality with version 4.x.""" """Setup mklittlefs for download functionality with version 4.x."""