13
13
# limitations under the License.
14
14
15
15
import locale
16
+ import json
16
17
import os
17
18
import re
19
+ import semantic_version
18
20
import shlex
19
21
import subprocess
20
22
import sys
30
32
)
31
33
32
34
from platformio .project .helpers import get_project_dir
35
+ from platformio .package .version import pepver_to_semver
33
36
from platformio .util import get_serial_ports
34
37
38
+
35
39
# Initialize environment and configuration
36
40
env = DefaultEnvironment ()
37
41
platform = env .PioPlatform ()
41
45
# Framework directory path
42
46
FRAMEWORK_DIR = platform .get_package_dir ("framework-arduinoespressif32" )
43
47
48
+ python_deps = {
49
+ "uv" : ">=0.1.0" ,
50
+ "pyyaml" : ">=6.0.2" ,
51
+ "rich-click" : ">=1.8.6" ,
52
+ "zopfli" : ">=0.2.2" ,
53
+ "intelhex" : ">=2.3.0" ,
54
+ "rich" : ">=14.0.0" ,
55
+ "esp-idf-size" : ">=1.6.1"
56
+ }
57
+
58
+
59
+ def get_packages_to_install (deps , installed_packages ):
60
+ """Generator for Python packages to install"""
61
+ for package , spec in deps .items ():
62
+ if package not in installed_packages :
63
+ yield package
64
+ else :
65
+ version_spec = semantic_version .Spec (spec )
66
+ if not version_spec .match (installed_packages [package ]):
67
+ yield package
68
+
69
+
70
+ def install_python_deps ():
71
+ """Ensure uv package manager is available, install with pip if not"""
72
+ try :
73
+ result = subprocess .run (
74
+ ["uv" , "--version" ],
75
+ capture_output = True ,
76
+ text = True ,
77
+ timeout = 3
78
+ )
79
+ uv_available = result .returncode == 0
80
+ except (FileNotFoundError , subprocess .TimeoutExpired ):
81
+ uv_available = False
82
+
83
+ if not uv_available :
84
+ try :
85
+ result = subprocess .run (
86
+ [env .subst ("$PYTHONEXE" ), "-m" , "pip" , "install" , "uv>=0.1.0" , "-q" , "-q" , "-q" ],
87
+ capture_output = True ,
88
+ text = True ,
89
+ timeout = 30 # 30 second timeout
90
+ )
91
+ if result .returncode != 0 :
92
+ if result .stderr :
93
+ print (f"Error output: { result .stderr .strip ()} " )
94
+ return False
95
+ except subprocess .TimeoutExpired :
96
+ print ("Error: uv installation timed out" )
97
+ return False
98
+ except FileNotFoundError :
99
+ print ("Error: Python executable not found" )
100
+ return False
101
+ except Exception as e :
102
+ print (f"Error installing uv package manager: { e } " )
103
+ return False
104
+
105
+
106
+ def _get_installed_uv_packages ():
107
+ result = {}
108
+ try :
109
+ cmd = ["uv" , "pip" , "list" , "--format=json" ]
110
+ result_obj = subprocess .run (
111
+ cmd ,
112
+ capture_output = True ,
113
+ text = True ,
114
+ encoding = 'utf-8' ,
115
+ timeout = 30 # 30 second timeout
116
+ )
117
+
118
+ if result_obj .returncode == 0 :
119
+ content = result_obj .stdout .strip ()
120
+ if content :
121
+ packages = json .loads (content )
122
+ for p in packages :
123
+ result [p ["name" ]] = pepver_to_semver (p ["version" ])
124
+ else :
125
+ print (f"Warning: pip list failed with exit code { result_obj .returncode } " )
126
+ if result_obj .stderr :
127
+ print (f"Error output: { result_obj .stderr .strip ()} " )
128
+
129
+ except subprocess .TimeoutExpired :
130
+ print ("Warning: uv pip list command timed out" )
131
+ except (json .JSONDecodeError , KeyError ) as e :
132
+ print (f"Warning: Could not parse package list: { e } " )
133
+ except FileNotFoundError :
134
+ print ("Warning: uv command not found" )
135
+ except Exception as e :
136
+ print (f"Warning! Couldn't extract the list of installed Python packages: { e } " )
137
+
138
+ return result
139
+
140
+ installed_packages = _get_installed_uv_packages ()
141
+ packages_to_install = list (get_packages_to_install (python_deps , installed_packages ))
142
+
143
+ if packages_to_install :
144
+ packages_list = [f"{ p } { python_deps [p ]} " for p in packages_to_install ]
145
+
146
+ cmd = [
147
+ "uv" , "pip" , "install" ,
148
+ f"--python={ env .subst ('$PYTHONEXE' )} " ,
149
+ "--quiet" , "--upgrade"
150
+ ] + packages_list
151
+
152
+ try :
153
+ result = subprocess .run (
154
+ cmd ,
155
+ capture_output = True ,
156
+ text = True ,
157
+ timeout = 30 # 30 second timeout for package installation
158
+ )
159
+
160
+ if result .returncode != 0 :
161
+ print (f"Error: Failed to install Python dependencies (exit code: { result .returncode } )" )
162
+ if result .stderr :
163
+ print (f"Error output: { result .stderr .strip ()} " )
164
+ return False
165
+
166
+ except subprocess .TimeoutExpired :
167
+ print ("Error: Python dependencies installation timed out" )
168
+ return False
169
+ except FileNotFoundError :
170
+ print ("Error: uv command not found" )
171
+ return False
172
+ except Exception as e :
173
+ print (f"Error installing Python dependencies: { e } " )
174
+ return False
175
+
176
+ return True
177
+
178
+
179
+ def install_esptool (env ):
180
+ """Install esptool from package folder "tool-esptoolpy" using uv package manager"""
181
+ try :
182
+ subprocess .check_call ([env .subst ("$PYTHONEXE" ), "-c" , "import esptool" ],
183
+ stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
184
+ return True
185
+ except (subprocess .CalledProcessError , FileNotFoundError ):
186
+ pass
187
+
188
+ esptool_repo_path = env .subst (platform .get_package_dir ("tool-esptoolpy" ) or "" )
189
+ if esptool_repo_path and os .path .isdir (esptool_repo_path ):
190
+ try :
191
+ subprocess .check_call ([
192
+ "uv" , "pip" , "install" , "--quiet" ,
193
+ f"--python={ env .subst ("$PYTHONEXE" )} " ,
194
+ "-e" , esptool_repo_path
195
+ ])
196
+ return True
197
+ except subprocess .CalledProcessError as e :
198
+ print (f"Warning: Failed to install esptool: { e } " )
199
+ return False
200
+
201
+ return False
202
+
203
+
204
+ install_python_deps ()
205
+ install_esptool (env )
206
+
44
207
45
208
def BeforeUpload (target , source , env ):
46
209
"""
@@ -346,7 +509,7 @@ def check_lib_archive_exists():
346
509
"bin" ,
347
510
"%s-elf-gdb" % toolchain_arch ,
348
511
),
349
- OBJCOPY = join ( platform . get_package_dir ( "tool-esptoolpy" ) or "" , " esptool.py" ) ,
512
+ OBJCOPY = ' esptool' ,
350
513
RANLIB = "%s-elf-gcc-ranlib" % toolchain_arch ,
351
514
SIZETOOL = "%s-elf-size" % toolchain_arch ,
352
515
ARFLAGS = ["rc" ],
@@ -356,7 +519,7 @@ def check_lib_archive_exists():
356
519
SIZECHECKCMD = "$SIZETOOL -A -d $SOURCES" ,
357
520
SIZEPRINTCMD = "$SIZETOOL -B -d $SOURCES" ,
358
521
ERASEFLAGS = ["--chip" , mcu , "--port" , '"$UPLOAD_PORT"' ],
359
- ERASECMD = '"$PYTHONEXE" "$ OBJCOPY" $ERASEFLAGS erase-flash' ,
522
+ ERASECMD = '"$OBJCOPY" $ERASEFLAGS erase-flash' ,
360
523
# mkspiffs package contains two different binaries for IDF and Arduino
361
524
MKFSTOOL = "mk%s" % filesystem
362
525
+ (
@@ -373,6 +536,7 @@ def check_lib_archive_exists():
373
536
),
374
537
# Legacy `ESP32_SPIFFS_IMAGE_NAME` is used as the second fallback value
375
538
# for backward compatibility
539
+
376
540
ESP32_FS_IMAGE_NAME = env .get (
377
541
"ESP32_FS_IMAGE_NAME" ,
378
542
env .get ("ESP32_SPIFFS_IMAGE_NAME" , filesystem ),
@@ -401,7 +565,7 @@ def check_lib_archive_exists():
401
565
action = env .VerboseAction (
402
566
" " .join (
403
567
[
404
- '"$PYTHONEXE" "$ OBJCOPY"' ,
568
+ "$ OBJCOPY" ,
405
569
"--chip" ,
406
570
mcu ,
407
571
"elf2image" ,
@@ -444,6 +608,7 @@ def check_lib_archive_exists():
444
608
if not env .get ("PIOFRAMEWORK" ):
445
609
env .SConscript ("frameworks/_bare.py" , exports = "env" )
446
610
611
+
447
612
def firmware_metrics (target , source , env ):
448
613
"""
449
614
Custom target to run esp-idf-size with support for command line parameters
@@ -463,11 +628,7 @@ def firmware_metrics(target, source, env):
463
628
print ("Make sure the project is built first with 'pio run'" )
464
629
return
465
630
466
- try :
467
- import subprocess
468
- import sys
469
- import shlex
470
-
631
+ try :
471
632
cmd = [env .subst ("$PYTHONEXE" ), "-m" , "esp_idf_size" , "--ng" ]
472
633
473
634
# Parameters from platformio.ini
@@ -510,6 +671,7 @@ def firmware_metrics(target, source, env):
510
671
print (f"Error: Failed to run firmware metrics: { e } " )
511
672
print ("Make sure esp-idf-size is installed: pip install esp-idf-size" )
512
673
674
+
513
675
#
514
676
# Target: Build executable and linkable firmware or FS image
515
677
#
@@ -604,9 +766,7 @@ def firmware_metrics(target, source, env):
604
766
# Configure upload protocol: esptool
605
767
elif upload_protocol == "esptool" :
606
768
env .Replace (
607
- UPLOADER = join (
608
- platform .get_package_dir ("tool-esptoolpy" ) or "" , "esptool.py"
609
- ),
769
+ UPLOADER = "esptool" ,
610
770
UPLOADERFLAGS = [
611
771
"--chip" ,
612
772
mcu ,
@@ -627,8 +787,7 @@ def firmware_metrics(target, source, env):
627
787
"--flash-size" ,
628
788
"detect" ,
629
789
],
630
- UPLOADCMD = '"$PYTHONEXE" "$UPLOADER" $UPLOADERFLAGS '
631
- "$ESP32_APP_OFFSET $SOURCE" ,
790
+ UPLOADCMD = '$UPLOADER $UPLOADERFLAGS $ESP32_APP_OFFSET $SOURCE'
632
791
)
633
792
for image in env .get ("FLASH_EXTRA_IMAGES" , []):
634
793
env .Append (UPLOADERFLAGS = [image [0 ], env .subst (image [1 ])])
@@ -656,7 +815,7 @@ def firmware_metrics(target, source, env):
656
815
"detect" ,
657
816
"$FS_START" ,
658
817
],
659
- UPLOADCMD = '"$PYTHONEXE" "$ UPLOADER" $UPLOADERFLAGS $SOURCE' ,
818
+ UPLOADCMD = '"$UPLOADER" $UPLOADERFLAGS $SOURCE' ,
660
819
)
661
820
662
821
upload_actions = [
0 commit comments