33
33
SUBPROCESS_TIMEOUT = 300
34
34
DEFAULT_DEBUG_SPEED = "5000"
35
35
DEFAULT_APP_OFFSET = "0x10000"
36
+ tl_install_name = "tool-esp_install"
36
37
ARDUINO_ESP32_PACKAGE_URL = "https://raw.githubusercontent.com/espressif/arduino-esp32/master/package/package_esp32_index.template.json"
37
38
38
39
# MCUs that support ESP-builtin debug
@@ -109,6 +110,15 @@ def wrapper(*args, **kwargs):
109
110
return wrapper
110
111
111
112
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
+
112
122
@safe_file_operation
113
123
def safe_remove_directory (path : str ) -> bool :
114
124
"""Safely remove directories with error handling."""
@@ -141,6 +151,15 @@ def safe_copy_file(src: str, dst: str) -> bool:
141
151
return True
142
152
143
153
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
+
144
163
class Espressif32Platform (PlatformBase ):
145
164
"""ESP32 platform implementation for PlatformIO with optimized toolchain management."""
146
165
@@ -159,6 +178,151 @@ def packages_dir(self) -> str:
159
178
self ._packages_dir = config .get ("platformio" , "packages_dir" )
160
179
return self ._packages_dir
161
180
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
+
162
326
def _get_tool_paths (self , tool_name : str ) -> Dict [str , str ]:
163
327
"""Get centralized path calculation for tools with caching."""
164
328
if tool_name not in self ._tools_cache :
@@ -182,7 +346,7 @@ def _get_tool_paths(self, tool_name: str) -> Dict[str, str]:
182
346
'tools_json_path' : os .path .join (tool_path , "tools.json" ),
183
347
'piopm_path' : os .path .join (tool_path , ".piopm" ),
184
348
'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"
186
350
)
187
351
}
188
352
return self ._tools_cache [tool_name ]
@@ -341,7 +505,7 @@ def _handle_existing_tool(
341
505
return self .install_tool (tool_name , retry_count + 1 )
342
506
343
507
def _configure_arduino_framework (self , frameworks : List [str ]) -> None :
344
- """Configure Arduino framework"""
508
+ """Configure Arduino framework dependencies. """
345
509
if "arduino" not in frameworks :
346
510
return
347
511
@@ -423,12 +587,28 @@ def _configure_mcu_toolchains(
423
587
self .install_tool ("tool-openocd-esp32" )
424
588
425
589
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
427
603
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"
429
605
)
606
+
430
607
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 } " )
432
612
433
613
def _install_esptool_package (self ) -> None :
434
614
"""Install esptool package required for all builds."""
@@ -463,7 +643,7 @@ def _ensure_mklittlefs_version(self) -> None:
463
643
os .remove (piopm_path )
464
644
logger .info (f"Incompatible mklittlefs version { version } removed (required: 3.x)" )
465
645
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 } " )
467
647
468
648
def _setup_mklittlefs_for_download (self ) -> None :
469
649
"""Setup mklittlefs for download functionality with version 4.x."""
0 commit comments