3333SUBPROCESS_TIMEOUT  =  300 
3434DEFAULT_DEBUG_SPEED  =  "5000" 
3535DEFAULT_APP_OFFSET  =  "0x10000" 
36+ tl_install_name  =  "tool-esp_install" 
3637ARDUINO_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  
113123def  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+ 
144163class  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