Skip to content

Version check for tool-esp_install (renamed tl-install) and auto install version listed in platform.json #237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion platform.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@
"package-version": "5.0.1",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/esptoolpy-v5.0.1.zip"
},
"tl-install": {
"tool-esp_install": {
"type": "tool",
"optional": false,
"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"
},
"contrib-piohome": {
Expand Down
192 changes: 186 additions & 6 deletions platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
SUBPROCESS_TIMEOUT = 300
DEFAULT_DEBUG_SPEED = "5000"
DEFAULT_APP_OFFSET = "0x10000"
tl_install_name = "tool-esp_install"
ARDUINO_ESP32_PACKAGE_URL = "https://raw.githubusercontent.com/espressif/arduino-esp32/master/package/package_esp32_index.template.json"

# MCUs that support ESP-builtin debug
Expand Down Expand Up @@ -109,6 +110,15 @@ def wrapper(*args, **kwargs):
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
def safe_remove_directory(path: str) -> bool:
"""Safely remove directories with error handling."""
Expand Down Expand Up @@ -141,6 +151,15 @@ def safe_copy_file(src: str, dst: str) -> bool:
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):
"""ESP32 platform implementation for PlatformIO with optimized toolchain management."""

Expand All @@ -159,6 +178,151 @@ def packages_dir(self) -> str:
self._packages_dir = config.get("platformio", "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 compability 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 compability 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 compability
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 _get_tool_paths(self, tool_name: str) -> Dict[str, str]:
"""Get centralized path calculation for tools with caching."""
if tool_name not in self._tools_cache:
Expand All @@ -182,7 +346,7 @@ def _get_tool_paths(self, tool_name: str) -> Dict[str, str]:
'tools_json_path': os.path.join(tool_path, "tools.json"),
'piopm_path': os.path.join(tool_path, ".piopm"),
'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]
Expand Down Expand Up @@ -341,7 +505,7 @@ def _handle_existing_tool(
return self.install_tool(tool_name, retry_count + 1)

def _configure_arduino_framework(self, frameworks: List[str]) -> None:
"""Configure Arduino framework"""
"""Configure Arduino framework dependencies."""
if "arduino" not in frameworks:
return

Expand Down Expand Up @@ -423,12 +587,28 @@ def _configure_mcu_toolchains(
self.install_tool("tool-openocd-esp32")

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(
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):
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:
"""Install esptool package required for all builds."""
Expand Down Expand Up @@ -463,7 +643,7 @@ def _ensure_mklittlefs_version(self) -> None:
os.remove(piopm_path)
logger.info(f"Incompatible mklittlefs version {version} removed (required: 3.x)")
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:
"""Setup mklittlefs for download functionality with version 4.x."""
Expand Down