33
33
34
34
# Python dependencies required for the build process
35
35
python_deps = {
36
- "uv" : ">=0.1.0" ,
37
36
"platformio" : "https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip" ,
38
37
"pyyaml" : ">=6.0.2" ,
39
38
"rich-click" : ">=1.8.6" ,
40
39
"zopfli" : ">=0.2.2" ,
41
40
"intelhex" : ">=2.3.0" ,
42
41
"rich" : ">=14.0.0" ,
43
42
"cryptography" : ">=45.0.3" ,
43
+ "certifi" : ">=2025.8.3" ,
44
44
"ecdsa" : ">=0.19.1" ,
45
45
"bitstring" : ">=4.3.1" ,
46
46
"reedsolo" : ">=1.5.3,<1.8" ,
@@ -74,24 +74,61 @@ def get_executable_path(penv_dir, executable_name):
74
74
def setup_pipenv_in_package (env , penv_dir ):
75
75
"""
76
76
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
77
81
"""
78
82
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
83
102
)
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
85
121
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
88
128
89
129
90
130
def setup_python_paths (penv_dir ):
91
131
"""Setup Python module search paths using the penv_dir."""
92
- # Add penv_dir to module search path
93
- site .addsitedir (penv_dir )
94
-
95
132
# Add site-packages directory
96
133
python_ver = f"python{ sys .version_info .major } .{ sys .version_info .minor } "
97
134
site_packages = (
@@ -136,46 +173,78 @@ def get_packages_to_install(deps, installed_packages):
136
173
yield package
137
174
138
175
139
- def install_python_deps (python_exe , uv_executable ):
176
+ def install_python_deps (python_exe , external_uv_executable ):
140
177
"""
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)
142
183
143
184
Returns:
144
185
bool: True if successful, False otherwise
145
186
"""
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
146
193
try :
147
194
result = subprocess .run (
148
- [uv_executable , "--version" ],
195
+ [penv_uv_executable , "--version" ],
149
196
capture_output = True ,
150
197
text = True ,
151
- timeout = 3
198
+ timeout = 10
152
199
)
153
- uv_available = result .returncode == 0
200
+ uv_in_penv_available = result .returncode == 0
154
201
except (FileNotFoundError , subprocess .TimeoutExpired ):
155
- uv_available = False
202
+ uv_in_penv_available = False
156
203
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 } " )
168
247
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
179
248
180
249
181
250
def _get_installed_uv_packages ():
@@ -187,13 +256,13 @@ def _get_installed_uv_packages():
187
256
"""
188
257
result = {}
189
258
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" ]
191
260
result_obj = subprocess .run (
192
261
cmd ,
193
262
capture_output = True ,
194
263
text = True ,
195
264
encoding = 'utf-8' ,
196
- timeout = 30 # 30 second timeout
265
+ timeout = 120
197
266
)
198
267
199
268
if result_obj .returncode == 0 :
@@ -231,25 +300,22 @@ def _get_installed_uv_packages():
231
300
packages_list .append (f"{ p } { spec } " )
232
301
233
302
cmd = [
234
- uv_executable , "pip" , "install" ,
303
+ penv_uv_executable , "pip" , "install" ,
235
304
f"--python={ python_exe } " ,
236
305
"--quiet" , "--upgrade"
237
306
] + packages_list
238
307
239
308
try :
240
- result = subprocess .run (
309
+ subprocess .check_call (
241
310
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
245
314
)
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
252
315
316
+ except subprocess .CalledProcessError as e :
317
+ print (f"Error: Failed to install Python dependencies (exit code: { e .returncode } )" )
318
+ return False
253
319
except subprocess .TimeoutExpired :
254
320
print ("Error: Python dependencies installation timed out" )
255
321
return False
@@ -315,7 +381,7 @@ def install_esptool(env, platform, python_exe, uv_executable):
315
381
uv_executable , "pip" , "install" , "--quiet" , "--force-reinstall" ,
316
382
f"--python={ python_exe } " ,
317
383
"-e" , esptool_repo_path
318
- ])
384
+ ], timeout = 60 )
319
385
320
386
except subprocess .CalledProcessError as e :
321
387
sys .stderr .write (
@@ -351,7 +417,7 @@ def setup_python_environment(env, platform, platformio_dir):
351
417
penv_dir = os .path .join (platformio_dir , "penv" )
352
418
353
419
# Setup virtual environment if needed
354
- setup_pipenv_in_package (env , penv_dir )
420
+ used_uv_executable = setup_pipenv_in_package (env , penv_dir )
355
421
356
422
# Set Python Scons Var to env Python
357
423
penv_python = get_executable_path (penv_dir , "python" )
@@ -369,7 +435,7 @@ def setup_python_environment(env, platform, platformio_dir):
369
435
370
436
# Install espressif32 Python dependencies
371
437
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 ):
373
439
sys .stderr .write ("Error: Failed to install Python dependencies into penv\n " )
374
440
sys .exit (1 )
375
441
else :
@@ -378,4 +444,28 @@ def setup_python_environment(env, platform, platformio_dir):
378
444
# Install esptool after dependencies
379
445
install_esptool (env , platform , penv_python , uv_executable )
380
446
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
+
381
471
return penv_python , esptool_binary_path
0 commit comments