Skip to content

Commit 2112e03

Browse files
authored
Update main.py
1 parent a3a15af commit 2112e03

File tree

1 file changed

+173
-14
lines changed

1 file changed

+173
-14
lines changed

builder/main.py

Lines changed: 173 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
# limitations under the License.
1414

1515
import locale
16+
import json
1617
import os
1718
import re
19+
import semantic_version
1820
import shlex
1921
import subprocess
2022
import sys
@@ -30,8 +32,10 @@
3032
)
3133

3234
from platformio.project.helpers import get_project_dir
35+
from platformio.package.version import pepver_to_semver
3336
from platformio.util import get_serial_ports
3437

38+
3539
# Initialize environment and configuration
3640
env = DefaultEnvironment()
3741
platform = env.PioPlatform()
@@ -41,6 +45,165 @@
4145
# Framework directory path
4246
FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
4347

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+
44207

45208
def BeforeUpload(target, source, env):
46209
"""
@@ -346,7 +509,7 @@ def check_lib_archive_exists():
346509
"bin",
347510
"%s-elf-gdb" % toolchain_arch,
348511
),
349-
OBJCOPY=join(platform.get_package_dir("tool-esptoolpy") or "", "esptool.py"),
512+
OBJCOPY='esptool',
350513
RANLIB="%s-elf-gcc-ranlib" % toolchain_arch,
351514
SIZETOOL="%s-elf-size" % toolchain_arch,
352515
ARFLAGS=["rc"],
@@ -356,7 +519,7 @@ def check_lib_archive_exists():
356519
SIZECHECKCMD="$SIZETOOL -A -d $SOURCES",
357520
SIZEPRINTCMD="$SIZETOOL -B -d $SOURCES",
358521
ERASEFLAGS=["--chip", mcu, "--port", '"$UPLOAD_PORT"'],
359-
ERASECMD='"$PYTHONEXE" "$OBJCOPY" $ERASEFLAGS erase-flash',
522+
ERASECMD='"$OBJCOPY" $ERASEFLAGS erase-flash',
360523
# mkspiffs package contains two different binaries for IDF and Arduino
361524
MKFSTOOL="mk%s" % filesystem
362525
+ (
@@ -373,6 +536,7 @@ def check_lib_archive_exists():
373536
),
374537
# Legacy `ESP32_SPIFFS_IMAGE_NAME` is used as the second fallback value
375538
# for backward compatibility
539+
376540
ESP32_FS_IMAGE_NAME=env.get(
377541
"ESP32_FS_IMAGE_NAME",
378542
env.get("ESP32_SPIFFS_IMAGE_NAME", filesystem),
@@ -401,7 +565,7 @@ def check_lib_archive_exists():
401565
action=env.VerboseAction(
402566
" ".join(
403567
[
404-
'"$PYTHONEXE" "$OBJCOPY"',
568+
"$OBJCOPY",
405569
"--chip",
406570
mcu,
407571
"elf2image",
@@ -444,6 +608,7 @@ def check_lib_archive_exists():
444608
if not env.get("PIOFRAMEWORK"):
445609
env.SConscript("frameworks/_bare.py", exports="env")
446610

611+
447612
def firmware_metrics(target, source, env):
448613
"""
449614
Custom target to run esp-idf-size with support for command line parameters
@@ -463,11 +628,7 @@ def firmware_metrics(target, source, env):
463628
print("Make sure the project is built first with 'pio run'")
464629
return
465630

466-
try:
467-
import subprocess
468-
import sys
469-
import shlex
470-
631+
try:
471632
cmd = [env.subst("$PYTHONEXE"), "-m", "esp_idf_size", "--ng"]
472633

473634
# Parameters from platformio.ini
@@ -510,6 +671,7 @@ def firmware_metrics(target, source, env):
510671
print(f"Error: Failed to run firmware metrics: {e}")
511672
print("Make sure esp-idf-size is installed: pip install esp-idf-size")
512673

674+
513675
#
514676
# Target: Build executable and linkable firmware or FS image
515677
#
@@ -604,9 +766,7 @@ def firmware_metrics(target, source, env):
604766
# Configure upload protocol: esptool
605767
elif upload_protocol == "esptool":
606768
env.Replace(
607-
UPLOADER=join(
608-
platform.get_package_dir("tool-esptoolpy") or "", "esptool.py"
609-
),
769+
UPLOADER="esptool",
610770
UPLOADERFLAGS=[
611771
"--chip",
612772
mcu,
@@ -627,8 +787,7 @@ def firmware_metrics(target, source, env):
627787
"--flash-size",
628788
"detect",
629789
],
630-
UPLOADCMD='"$PYTHONEXE" "$UPLOADER" $UPLOADERFLAGS '
631-
"$ESP32_APP_OFFSET $SOURCE",
790+
UPLOADCMD='$UPLOADER $UPLOADERFLAGS $ESP32_APP_OFFSET $SOURCE'
632791
)
633792
for image in env.get("FLASH_EXTRA_IMAGES", []):
634793
env.Append(UPLOADERFLAGS=[image[0], env.subst(image[1])])
@@ -656,7 +815,7 @@ def firmware_metrics(target, source, env):
656815
"detect",
657816
"$FS_START",
658817
],
659-
UPLOADCMD='"$PYTHONEXE" "$UPLOADER" $UPLOADERFLAGS $SOURCE',
818+
UPLOADCMD='"$UPLOADER" $UPLOADERFLAGS $SOURCE',
660819
)
661820

662821
upload_actions = [

0 commit comments

Comments
 (0)