3333
3434# Python dependencies required for the build process
3535python_deps = {
36- "uv" : ">=0.1.0" ,
3736 "platformio" : "https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip" ,
3837 "pyyaml" : ">=6.0.2" ,
3938 "rich-click" : ">=1.8.6" ,
4039 "zopfli" : ">=0.2.2" ,
4140 "intelhex" : ">=2.3.0" ,
4241 "rich" : ">=14.0.0" ,
4342 "cryptography" : ">=45.0.3" ,
43+ "certifi" : ">=2025.8.3" ,
4444 "ecdsa" : ">=0.19.1" ,
4545 "bitstring" : ">=4.3.1" ,
4646 "reedsolo" : ">=1.5.3,<1.8" ,
@@ -74,24 +74,61 @@ def get_executable_path(penv_dir, executable_name):
7474def setup_pipenv_in_package (env , penv_dir ):
7575 """
7676 Checks if 'penv' folder exists in platformio dir and creates virtual environment if not.
77+ First tries to create with uv, falls back to python -m venv if uv is not available.
78+
79+ Returns:
80+ str or None: Path to uv executable if uv was used, None if python -m venv was used
7781 """
7882 if not os .path .exists (penv_dir ):
79- env .Execute (
80- env .VerboseAction (
81- '"$PYTHONEXE" -m venv --clear "%s"' % penv_dir ,
82- "Creating pioarduino Python virtual environment: %s" % penv_dir ,
83+ # First try to create virtual environment with uv
84+ uv_success = False
85+ uv_cmd = None
86+ try :
87+ # Derive uv path from PYTHONEXE path
88+ python_exe = env .subst ("$PYTHONEXE" )
89+ python_dir = os .path .dirname (python_exe )
90+ uv_exe_suffix = ".exe" if IS_WINDOWS else ""
91+ uv_cmd = os .path .join (python_dir , f"uv{ uv_exe_suffix } " )
92+
93+ # Fall back to system uv if derived path doesn't exist
94+ if not os .path .isfile (uv_cmd ):
95+ uv_cmd = "uv"
96+
97+ subprocess .check_call (
98+ [uv_cmd , "venv" , "--clear" , f"--python={ python_exe } " , penv_dir ],
99+ stdout = subprocess .DEVNULL ,
100+ stderr = subprocess .DEVNULL ,
101+ timeout = 90
83102 )
84- )
103+ uv_success = True
104+ print (f"Created pioarduino Python virtual environment using uv: { penv_dir } " )
105+
106+ except Exception :
107+ pass
108+
109+ # Fallback to python -m venv if uv failed or is not available
110+ if not uv_success :
111+ uv_cmd = None
112+ env .Execute (
113+ env .VerboseAction (
114+ '"$PYTHONEXE" -m venv --clear "%s"' % penv_dir ,
115+ "Created pioarduino Python virtual environment: %s" % penv_dir ,
116+ )
117+ )
118+
119+ # Verify that the virtual environment was created properly
120+ # Check for python executable
85121 assert os .path .isfile (
86- get_executable_path (penv_dir , "pip" )
87- ), "Error: Failed to create a proper virtual environment. Missing the `pip` binary!"
122+ get_executable_path (penv_dir , "python" )
123+ ), f"Error: Failed to create a proper virtual environment. Missing the `python` binary! Created with uv: { uv_success } "
124+
125+ return uv_cmd if uv_success else None
126+
127+ return None
88128
89129
90130def setup_python_paths (penv_dir ):
91131 """Setup Python module search paths using the penv_dir."""
92- # Add penv_dir to module search path
93- site .addsitedir (penv_dir )
94-
95132 # Add site-packages directory
96133 python_ver = f"python{ sys .version_info .major } .{ sys .version_info .minor } "
97134 site_packages = (
@@ -136,46 +173,78 @@ def get_packages_to_install(deps, installed_packages):
136173 yield package
137174
138175
139- def install_python_deps (python_exe , uv_executable ):
176+ def install_python_deps (python_exe , external_uv_executable ):
140177 """
141- Ensure uv package manager is available and install required Python dependencies.
178+ Ensure uv package manager is available in penv and install required Python dependencies.
179+
180+ Args:
181+ python_exe: Path to Python executable in the penv
182+ external_uv_executable: Path to external uv executable used to create the penv (can be None)
142183
143184 Returns:
144185 bool: True if successful, False otherwise
145186 """
187+ # Get the penv directory to locate uv within it
188+ penv_dir = os .path .dirname (os .path .dirname (python_exe ))
189+ penv_uv_executable = get_executable_path (penv_dir , "uv" )
190+
191+ # Check if uv is available in the penv
192+ uv_in_penv_available = False
146193 try :
147194 result = subprocess .run (
148- [uv_executable , "--version" ],
195+ [penv_uv_executable , "--version" ],
149196 capture_output = True ,
150197 text = True ,
151- timeout = 3
198+ timeout = 10
152199 )
153- uv_available = result .returncode == 0
200+ uv_in_penv_available = result .returncode == 0
154201 except (FileNotFoundError , subprocess .TimeoutExpired ):
155- uv_available = False
202+ uv_in_penv_available = False
156203
157- if not uv_available :
158- try :
159- result = subprocess .run (
160- [python_exe , "-m" , "pip" , "install" , "uv>=0.1.0" , "-q" , "-q" , "-q" ],
161- capture_output = True ,
162- text = True ,
163- timeout = 30 # 30 second timeout
164- )
165- if result .returncode != 0 :
166- if result .stderr :
167- print (f"Error output: { result .stderr .strip ()} " )
204+ # Install uv into penv if not available
205+ if not uv_in_penv_available :
206+ if external_uv_executable :
207+ # Use external uv to install uv into the penv
208+ try :
209+ subprocess .check_call (
210+ [external_uv_executable , "pip" , "install" , "uv>=0.1.0" , f"--python={ python_exe } " , "--quiet" ],
211+ stdout = subprocess .DEVNULL ,
212+ stderr = subprocess .STDOUT ,
213+ timeout = 120
214+ )
215+ except subprocess .CalledProcessError as e :
216+ print (f"Error: uv installation failed with exit code { e .returncode } " )
217+ return False
218+ except subprocess .TimeoutExpired :
219+ print ("Error: uv installation timed out" )
220+ return False
221+ except FileNotFoundError :
222+ print ("Error: External uv executable not found" )
223+ return False
224+ except Exception as e :
225+ print (f"Error installing uv package manager into penv: { e } " )
226+ return False
227+ else :
228+ # No external uv available, use pip to install uv into penv
229+ try :
230+ subprocess .check_call (
231+ [python_exe , "-m" , "pip" , "install" , "uv>=0.1.0" , "--quiet" ],
232+ stdout = subprocess .DEVNULL ,
233+ stderr = subprocess .STDOUT ,
234+ timeout = 120
235+ )
236+ except subprocess .CalledProcessError as e :
237+ print (f"Error: uv installation via pip failed with exit code { e .returncode } " )
238+ return False
239+ except subprocess .TimeoutExpired :
240+ print ("Error: uv installation via pip timed out" )
241+ return False
242+ except FileNotFoundError :
243+ print ("Error: Python executable not found" )
244+ return False
245+ except Exception as e :
246+ print (f"Error installing uv package manager via pip: { e } " )
168247 return False
169-
170- except subprocess .TimeoutExpired :
171- print ("Error: uv installation timed out" )
172- return False
173- except FileNotFoundError :
174- print ("Error: Python executable not found" )
175- return False
176- except Exception as e :
177- print (f"Error installing uv package manager: { e } " )
178- return False
179248
180249
181250 def _get_installed_uv_packages ():
@@ -187,13 +256,13 @@ def _get_installed_uv_packages():
187256 """
188257 result = {}
189258 try :
190- cmd = [uv_executable , "pip" , "list" , f"--python={ python_exe } " , "--format=json" ]
259+ cmd = [penv_uv_executable , "pip" , "list" , f"--python={ python_exe } " , "--format=json" ]
191260 result_obj = subprocess .run (
192261 cmd ,
193262 capture_output = True ,
194263 text = True ,
195264 encoding = 'utf-8' ,
196- timeout = 30 # 30 second timeout
265+ timeout = 120
197266 )
198267
199268 if result_obj .returncode == 0 :
@@ -231,25 +300,22 @@ def _get_installed_uv_packages():
231300 packages_list .append (f"{ p } { spec } " )
232301
233302 cmd = [
234- uv_executable , "pip" , "install" ,
303+ penv_uv_executable , "pip" , "install" ,
235304 f"--python={ python_exe } " ,
236305 "--quiet" , "--upgrade"
237306 ] + packages_list
238307
239308 try :
240- result = subprocess .run (
309+ subprocess .check_call (
241310 cmd ,
242- capture_output = True ,
243- text = True ,
244- timeout = 30 # 30 second timeout for package installation
311+ stdout = subprocess . DEVNULL ,
312+ stderr = subprocess . STDOUT ,
313+ timeout = 120
245314 )
246-
247- if result .returncode != 0 :
248- print (f"Error: Failed to install Python dependencies (exit code: { result .returncode } )" )
249- if result .stderr :
250- print (f"Error output: { result .stderr .strip ()} " )
251- return False
252315
316+ except subprocess .CalledProcessError as e :
317+ print (f"Error: Failed to install Python dependencies (exit code: { e .returncode } )" )
318+ return False
253319 except subprocess .TimeoutExpired :
254320 print ("Error: Python dependencies installation timed out" )
255321 return False
@@ -315,7 +381,7 @@ def install_esptool(env, platform, python_exe, uv_executable):
315381 uv_executable , "pip" , "install" , "--quiet" , "--force-reinstall" ,
316382 f"--python={ python_exe } " ,
317383 "-e" , esptool_repo_path
318- ])
384+ ], timeout = 60 )
319385
320386 except subprocess .CalledProcessError as e :
321387 sys .stderr .write (
@@ -351,7 +417,7 @@ def setup_python_environment(env, platform, platformio_dir):
351417 penv_dir = os .path .join (platformio_dir , "penv" )
352418
353419 # Setup virtual environment if needed
354- setup_pipenv_in_package (env , penv_dir )
420+ used_uv_executable = setup_pipenv_in_package (env , penv_dir )
355421
356422 # Set Python Scons Var to env Python
357423 penv_python = get_executable_path (penv_dir , "python" )
@@ -369,7 +435,7 @@ def setup_python_environment(env, platform, platformio_dir):
369435
370436 # Install espressif32 Python dependencies
371437 if has_internet_connection () or github_actions :
372- if not install_python_deps (penv_python , uv_executable ):
438+ if not install_python_deps (penv_python , used_uv_executable ):
373439 sys .stderr .write ("Error: Failed to install Python dependencies into penv\n " )
374440 sys .exit (1 )
375441 else :
@@ -378,4 +444,28 @@ def setup_python_environment(env, platform, platformio_dir):
378444 # Install esptool after dependencies
379445 install_esptool (env , platform , penv_python , uv_executable )
380446
447+ # Setup certifi environment variables
448+ def setup_certifi_env ():
449+ try :
450+ import certifi
451+ except ImportError :
452+ print ("Info: certifi not available; skipping CA environment setup." )
453+ return
454+ cert_path = certifi .where ()
455+ os .environ ["CERTIFI_PATH" ] = cert_path
456+ os .environ ["SSL_CERT_FILE" ] = cert_path
457+ os .environ ["REQUESTS_CA_BUNDLE" ] = cert_path
458+ os .environ ["CURL_CA_BUNDLE" ] = cert_path
459+ # Also propagate to SCons environment for future env.Execute calls
460+ env_vars = dict (env .get ("ENV" , {}))
461+ env_vars .update ({
462+ "CERTIFI_PATH" : cert_path ,
463+ "SSL_CERT_FILE" : cert_path ,
464+ "REQUESTS_CA_BUNDLE" : cert_path ,
465+ "CURL_CA_BUNDLE" : cert_path ,
466+ })
467+ env .Replace (ENV = env_vars )
468+
469+ setup_certifi_env ()
470+
381471 return penv_python , esptool_binary_path
0 commit comments