Skip to content

Commit ec4b554

Browse files
authored
Version check for tool-esp_install (renamed tl-install) and auto install version listed in platform.json (#237)
1 parent 71d2832 commit ec4b554

File tree

2 files changed

+188
-7
lines changed

2 files changed

+188
-7
lines changed

platform.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,11 @@
9595
"package-version": "5.0.1",
9696
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/esptoolpy-v5.0.1.zip"
9797
},
98-
"tl-install": {
98+
"tool-esp_install": {
9999
"type": "tool",
100100
"optional": false,
101101
"owner": "pioarduino",
102+
"package-version": "5.1.0",
102103
"version": "https://github.com/pioarduino/esp_install/releases/download/v5.1.0/esp_install-v5.1.0.zip"
103104
},
104105
"contrib-piohome": {

platform.py

Lines changed: 186 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
SUBPROCESS_TIMEOUT = 300
3434
DEFAULT_DEBUG_SPEED = "5000"
3535
DEFAULT_APP_OFFSET = "0x10000"
36+
tl_install_name = "tool-esp_install"
3637
ARDUINO_ESP32_PACKAGE_URL = "https://raw.githubusercontent.com/espressif/arduino-esp32/master/package/package_esp32_index.template.json"
3738

3839
# MCUs that support ESP-builtin debug
@@ -109,6 +110,15 @@ def wrapper(*args, **kwargs):
109110
return wrapper
110111

111112

113+
@safe_file_operation
114+
def safe_remove_file(path: str) -> bool:
115+
"""Safely remove a file with error handling."""
116+
if os.path.exists(path) and os.path.isfile(path):
117+
os.remove(path)
118+
logger.debug(f"File removed: {path}")
119+
return True
120+
121+
112122
@safe_file_operation
113123
def safe_remove_directory(path: str) -> bool:
114124
"""Safely remove directories with error handling."""
@@ -141,6 +151,15 @@ def safe_copy_file(src: str, dst: str) -> bool:
141151
return True
142152

143153

154+
@safe_file_operation
155+
def safe_copy_directory(src: str, dst: str) -> bool:
156+
"""Safely copy directories with error handling."""
157+
os.makedirs(os.path.dirname(dst), exist_ok=True)
158+
shutil.copytree(src, dst, dirs_exist_ok=True)
159+
logger.debug(f"Directory copied: {src} -> {dst}")
160+
return True
161+
162+
144163
class Espressif32Platform(PlatformBase):
145164
"""ESP32 platform implementation for PlatformIO with optimized toolchain management."""
146165

@@ -159,6 +178,151 @@ def packages_dir(self) -> str:
159178
self._packages_dir = config.get("platformio", "packages_dir")
160179
return self._packages_dir
161180

181+
def _check_tl_install_version(self) -> bool:
182+
"""
183+
Check if tool-esp_install is installed in the correct version.
184+
Install the correct version only if version differs.
185+
186+
Returns:
187+
bool: True if correct version is available, False on error
188+
"""
189+
190+
# Get required version from platform.json
191+
required_version = self.packages.get(tl_install_name, {}).get("version")
192+
if not required_version:
193+
logger.debug(f"No version check required for {tl_install_name}")
194+
return True
195+
196+
# Check if tool is already installed
197+
tl_install_path = os.path.join(self.packages_dir, tl_install_name)
198+
package_json_path = os.path.join(tl_install_path, "package.json")
199+
200+
if not os.path.exists(package_json_path):
201+
logger.info(f"{tl_install_name} not installed, installing version {required_version}")
202+
return self._install_tl_install(required_version)
203+
204+
# Read installed version
205+
try:
206+
with open(package_json_path, 'r', encoding='utf-8') as f:
207+
package_data = json.load(f)
208+
209+
installed_version = package_data.get("version")
210+
if not installed_version:
211+
logger.warning(f"Installed version for {tl_install_name} unknown, installing {required_version}")
212+
return self._install_tl_install(required_version)
213+
214+
# IMPORTANT: Compare versions correctly
215+
if self._compare_tl_install_versions(installed_version, required_version):
216+
logger.debug(f"{tl_install_name} version {installed_version} is already correctly installed")
217+
# IMPORTANT: Set package as available, but do NOT reinstall
218+
self.packages[tl_install_name]["optional"] = True
219+
return True
220+
else:
221+
logger.info(
222+
f"Version mismatch for {tl_install_name}: "
223+
f"installed={installed_version}, required={required_version}, installing correct version"
224+
)
225+
return self._install_tl_install(required_version)
226+
227+
except (json.JSONDecodeError, FileNotFoundError) as e:
228+
logger.error(f"Error reading package data for {tl_install_name}: {e}")
229+
return self._install_tl_install(required_version)
230+
231+
def _compare_tl_install_versions(self, installed: str, required: str) -> bool:
232+
"""
233+
Compare installed and required version of tool-esp_install.
234+
235+
Args:
236+
installed: Currently installed version string
237+
required: Required version string from platform.json
238+
239+
Returns:
240+
bool: True if versions match, False otherwise
241+
"""
242+
# For URL-based versions: Extract version string from URL
243+
installed_clean = self._extract_version_from_url(installed)
244+
required_clean = self._extract_version_from_url(required)
245+
246+
logger.debug(f"Version comparison: installed='{installed_clean}' vs required='{required_clean}'")
247+
248+
return installed_clean == required_clean
249+
250+
def _extract_version_from_url(self, version_string: str) -> str:
251+
"""
252+
Extract version information from URL or return version directly.
253+
254+
Args:
255+
version_string: Version string or URL containing version
256+
257+
Returns:
258+
str: Extracted version string
259+
"""
260+
if version_string.startswith(('http://', 'https://')):
261+
# Extract version from URL like: .../v5.1.0/esp_install-v5.1.0.zip
262+
import re
263+
version_match = re.search(r'v(\d+\.\d+\.\d+)', version_string)
264+
if version_match:
265+
return version_match.group(1) # Returns "5.1.0"
266+
else:
267+
# Fallback: Use entire URL
268+
return version_string
269+
else:
270+
# Direct version number
271+
return version_string.strip()
272+
273+
def _install_tl_install(self, version: str) -> bool:
274+
"""
275+
Install tool-esp_install ONLY when necessary
276+
and handles backwards compability for tl-install.
277+
278+
Args:
279+
version: Version string or URL to install
280+
281+
Returns:
282+
bool: True if installation successful, False otherwise
283+
"""
284+
tl_install_path = os.path.join(self.packages_dir, tl_install_name)
285+
old_tl_install_path = os.path.join(self.packages_dir, "tl-install")
286+
287+
try:
288+
old_tl_install_exists = os.path.exists(old_tl_install_path)
289+
if old_tl_install_exists:
290+
# remove outdated tl-install
291+
safe_remove_directory(old_tl_install_path)
292+
293+
if os.path.exists(tl_install_path):
294+
logger.info(f"Removing old {tl_install_name} installation")
295+
safe_remove_directory(tl_install_path)
296+
297+
logger.info(f"Installing {tl_install_name} version {version}")
298+
self.packages[tl_install_name]["optional"] = False
299+
self.packages[tl_install_name]["version"] = version
300+
pm.install(version)
301+
# Ensure backward compability by removing pio install status indicator
302+
tl_piopm_path = os.path.join(tl_install_path, ".piopm")
303+
safe_remove_file(tl_piopm_path)
304+
305+
if os.path.exists(os.path.join(tl_install_path, "package.json")):
306+
logger.info(f"{tl_install_name} successfully installed and verified")
307+
self.packages[tl_install_name]["optional"] = True
308+
309+
# Handle old tl-install to keep backwards compability
310+
if old_tl_install_exists:
311+
# Copy tool-esp_install content to tl-install location
312+
if safe_copy_directory(tl_install_path, old_tl_install_path):
313+
logger.info(f"Content copied from {tl_install_name} to old tl-install location")
314+
else:
315+
logger.warning("Failed to copy content to old tl-install location")
316+
return True
317+
else:
318+
logger.error(f"{tl_install_name} installation failed - package.json not found")
319+
return False
320+
321+
except Exception as e:
322+
logger.error(f"Error installing {tl_install_name}: {e}")
323+
return False
324+
325+
162326
def _get_tool_paths(self, tool_name: str) -> Dict[str, str]:
163327
"""Get centralized path calculation for tools with caching."""
164328
if tool_name not in self._tools_cache:
@@ -182,7 +346,7 @@ def _get_tool_paths(self, tool_name: str) -> Dict[str, str]:
182346
'tools_json_path': os.path.join(tool_path, "tools.json"),
183347
'piopm_path': os.path.join(tool_path, ".piopm"),
184348
'idf_tools_path': os.path.join(
185-
self.packages_dir, "tl-install", "tools", "idf_tools.py"
349+
self.packages_dir, tl_install_name, "tools", "idf_tools.py"
186350
)
187351
}
188352
return self._tools_cache[tool_name]
@@ -341,7 +505,7 @@ def _handle_existing_tool(
341505
return self.install_tool(tool_name, retry_count + 1)
342506

343507
def _configure_arduino_framework(self, frameworks: List[str]) -> None:
344-
"""Configure Arduino framework"""
508+
"""Configure Arduino framework dependencies."""
345509
if "arduino" not in frameworks:
346510
return
347511

@@ -423,12 +587,28 @@ def _configure_mcu_toolchains(
423587
self.install_tool("tool-openocd-esp32")
424588

425589
def _configure_installer(self) -> None:
426-
"""Configure the ESP-IDF tools installer."""
590+
"""Configure the ESP-IDF tools installer with proper version checking."""
591+
592+
# Check version - installs only when needed
593+
if not self._check_tl_install_version():
594+
logger.error("Error during tool-esp_install version check / installation")
595+
return
596+
597+
# Remove pio install marker to avoid issues when switching versions
598+
old_tl_piopm_path = os.path.join(self.packages_dir, "tl-install", ".piopm")
599+
if os.path.exists(old_tl_piopm_path):
600+
safe_remove_file(old_tl_piopm_path)
601+
602+
# Check if idf_tools.py is available
427603
installer_path = os.path.join(
428-
self.packages_dir, "tl-install", "tools", "idf_tools.py"
604+
self.packages_dir, tl_install_name, "tools", "idf_tools.py"
429605
)
606+
430607
if os.path.exists(installer_path):
431-
self.packages["tl-install"]["optional"] = True
608+
logger.debug(f"{tl_install_name} is available and ready")
609+
self.packages[tl_install_name]["optional"] = True
610+
else:
611+
logger.warning(f"idf_tools.py not found in {installer_path}")
432612

433613
def _install_esptool_package(self) -> None:
434614
"""Install esptool package required for all builds."""
@@ -463,7 +643,7 @@ def _ensure_mklittlefs_version(self) -> None:
463643
os.remove(piopm_path)
464644
logger.info(f"Incompatible mklittlefs version {version} removed (required: 3.x)")
465645
except (json.JSONDecodeError, KeyError) as e:
466-
logger.error(f"Error reading mklittlefs package data: {e}")
646+
logger.error(f"Error reading mklittlefs package {e}")
467647

468648
def _setup_mklittlefs_for_download(self) -> None:
469649
"""Setup mklittlefs for download functionality with version 4.x."""

0 commit comments

Comments
 (0)