Skip to content

Commit b528557

Browse files
authored
Use uv for creating penv when available (#268)
* Use uv for creating penv when available * Setup certifi environment variables
1 parent 42bcda1 commit b528557

File tree

1 file changed

+144
-54
lines changed

1 file changed

+144
-54
lines changed

builder/penv_setup.py

Lines changed: 144 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@
3333

3434
# Python dependencies required for the build process
3535
python_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):
7474
def 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

90130
def 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

Comments
 (0)