|
13 | 13 | # limitations under the License.
|
14 | 14 |
|
15 | 15 | import locale
|
16 |
| -import json |
17 | 16 | import os
|
18 | 17 | import re
|
19 |
| -import site |
20 |
| -import semantic_version |
21 | 18 | import shlex
|
22 | 19 | import subprocess
|
23 | 20 | import sys
|
|
33 | 30 | )
|
34 | 31 |
|
35 | 32 | from platformio.project.helpers import get_project_dir
|
36 |
| -from platformio.package.version import pepver_to_semver |
37 | 33 | from platformio.util import get_serial_ports
|
38 | 34 | from platformio.compat import IS_WINDOWS
|
39 |
| - |
40 |
| -# Check Python version requirement |
41 |
| -if sys.version_info < (3, 10): |
42 |
| - sys.stderr.write( |
43 |
| - f"Error: Python 3.10 or higher is required. " |
44 |
| - f"Current version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\n" |
45 |
| - f"Please update your Python installation.\n" |
46 |
| - ) |
47 |
| - sys.exit(1) |
48 |
| - |
49 |
| -# Python dependencies required for the build process |
50 |
| -python_deps = { |
51 |
| - "uv": ">=0.1.0", |
52 |
| - "platformio": "https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip", |
53 |
| - "pyyaml": ">=6.0.2", |
54 |
| - "rich-click": ">=1.8.6", |
55 |
| - "zopfli": ">=0.2.2", |
56 |
| - "intelhex": ">=2.3.0", |
57 |
| - "rich": ">=14.0.0", |
58 |
| - "cryptography": ">=45.0.3", |
59 |
| - "ecdsa": ">=0.19.1", |
60 |
| - "bitstring": ">=4.3.1", |
61 |
| - "reedsolo": ">=1.5.3,<1.8", |
62 |
| - "esp-idf-size": ">=1.6.1" |
63 |
| -} |
| 35 | +from penv_setup import setup_python_environment |
64 | 36 |
|
65 | 37 | # Initialize environment and configuration
|
66 | 38 | env = DefaultEnvironment()
|
|
70 | 42 | FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
|
71 | 43 | platformio_dir = projectconfig.get("platformio", "core_dir")
|
72 | 44 |
|
73 |
| -# Global Python executable path, replaced later with venv python path |
74 |
| -PYTHON_EXE = env.subst("$PYTHONEXE") |
75 |
| -penv_dir = os.path.join(platformio_dir, "penv") |
76 |
| - |
77 |
| - |
78 |
| -def get_executable_path(executable_name): |
79 |
| - """ |
80 |
| - Get the path to an executable based on the penv_dir. |
81 |
| - """ |
82 |
| - exe_suffix = ".exe" if IS_WINDOWS else "" |
83 |
| - scripts_dir = "Scripts" if IS_WINDOWS else "bin" |
84 |
| - |
85 |
| - return os.path.join(penv_dir, scripts_dir, f"{executable_name}{exe_suffix}") |
86 |
| - |
87 |
| - |
88 |
| -def setup_pipenv_in_package(): |
89 |
| - """ |
90 |
| - Checks if 'penv' folder exists in platformio dir and creates virtual environment if not. |
91 |
| - """ |
92 |
| - if not os.path.exists(penv_dir): |
93 |
| - env.Execute( |
94 |
| - env.VerboseAction( |
95 |
| - '"$PYTHONEXE" -m venv --clear "%s"' % penv_dir, |
96 |
| - "Creating pioarduino Python virtual environment: %s" % penv_dir, |
97 |
| - ) |
98 |
| - ) |
99 |
| - assert os.path.isfile( |
100 |
| - get_executable_path("pip") |
101 |
| - ), "Error: Failed to create a proper virtual environment. Missing the `pip` binary!" |
102 |
| - |
103 |
| - |
104 |
| -# Setup virtual environment if needed |
105 |
| -setup_pipenv_in_package() |
106 |
| - |
107 |
| -# Set Python Scons Var to env Python |
108 |
| -penv_python = get_executable_path("python") |
109 |
| -env.Replace(PYTHONEXE=penv_python) |
110 |
| -PYTHON_EXE = penv_python |
111 |
| - |
112 |
| -# check for python binary, exit with error when not found |
113 |
| -assert os.path.isfile(PYTHON_EXE), f"Python executable not found: {PYTHON_EXE}" |
114 |
| - |
115 |
| - |
116 |
| -def setup_python_paths(): |
117 |
| - """Setup Python module search paths using the penv_dir.""" |
118 |
| - # Add penv_dir to module search path |
119 |
| - site.addsitedir(penv_dir) |
120 |
| - |
121 |
| - # Add site-packages directory |
122 |
| - python_ver = f"python{sys.version_info.major}.{sys.version_info.minor}" |
123 |
| - site_packages = ( |
124 |
| - os.path.join(penv_dir, "Lib", "site-packages") if IS_WINDOWS |
125 |
| - else os.path.join(penv_dir, "lib", python_ver, "site-packages") |
126 |
| - ) |
127 |
| - |
128 |
| - if os.path.isdir(site_packages): |
129 |
| - site.addsitedir(site_packages) |
130 |
| - |
131 |
| - |
132 |
| -setup_python_paths() |
133 |
| - |
134 |
| -# Set executable paths from tools |
135 |
| -esptool_binary_path = get_executable_path("esptool") |
136 |
| -uv_executable = get_executable_path("uv") |
137 |
| - |
138 |
| - |
139 |
| -def get_packages_to_install(deps, installed_packages): |
140 |
| - """ |
141 |
| - Generator for Python packages that need to be installed. |
142 |
| - |
143 |
| - Args: |
144 |
| - deps (dict): Dictionary of package names and version specifications |
145 |
| - installed_packages (dict): Dictionary of currently installed packages |
146 |
| - |
147 |
| - Yields: |
148 |
| - str: Package name that needs to be installed |
149 |
| - """ |
150 |
| - for package, spec in deps.items(): |
151 |
| - if package not in installed_packages: |
152 |
| - yield package |
153 |
| - elif package == "platformio": |
154 |
| - # Enforce the version from the direct URL if it looks like one. |
155 |
| - # If version can't be parsed, fall back to accepting any installed version. |
156 |
| - m = re.search(r'/v?(\d+\.\d+\.\d+(?:\.\d+)?)(?:\.(?:zip|tar\.gz|tar\.bz2))?$', spec) |
157 |
| - if m: |
158 |
| - expected_ver = semantic_version.Version(m.group(1)) |
159 |
| - if installed_packages.get(package) != expected_ver: |
160 |
| - # Reinstall to align with the pinned URL version |
161 |
| - yield package |
162 |
| - else: |
163 |
| - continue |
164 |
| - else: |
165 |
| - version_spec = semantic_version.Spec(spec) |
166 |
| - if not version_spec.match(installed_packages[package]): |
167 |
| - yield package |
168 |
| - |
169 |
| - |
170 |
| -def install_python_deps(): |
171 |
| - """ |
172 |
| - Ensure uv package manager is available and install required Python dependencies. |
173 |
| - |
174 |
| - Returns: |
175 |
| - bool: True if successful, False otherwise |
176 |
| - """ |
177 |
| - try: |
178 |
| - result = subprocess.run( |
179 |
| - [uv_executable, "--version"], |
180 |
| - capture_output=True, |
181 |
| - text=True, |
182 |
| - timeout=3 |
183 |
| - ) |
184 |
| - uv_available = result.returncode == 0 |
185 |
| - except (FileNotFoundError, subprocess.TimeoutExpired): |
186 |
| - uv_available = False |
187 |
| - |
188 |
| - if not uv_available: |
189 |
| - try: |
190 |
| - result = subprocess.run( |
191 |
| - [PYTHON_EXE, "-m", "pip", "install", "uv>=0.1.0", "-q", "-q", "-q"], |
192 |
| - capture_output=True, |
193 |
| - text=True, |
194 |
| - timeout=30 # 30 second timeout |
195 |
| - ) |
196 |
| - if result.returncode != 0: |
197 |
| - if result.stderr: |
198 |
| - print(f"Error output: {result.stderr.strip()}") |
199 |
| - return False |
200 |
| - |
201 |
| - except subprocess.TimeoutExpired: |
202 |
| - print("Error: uv installation timed out") |
203 |
| - return False |
204 |
| - except FileNotFoundError: |
205 |
| - print("Error: Python executable not found") |
206 |
| - return False |
207 |
| - except Exception as e: |
208 |
| - print(f"Error installing uv package manager: {e}") |
209 |
| - return False |
210 |
| - |
211 |
| - |
212 |
| - def _get_installed_uv_packages(): |
213 |
| - """ |
214 |
| - Get list of installed packages in virtual env 'penv' using uv. |
215 |
| - |
216 |
| - Returns: |
217 |
| - dict: Dictionary of installed packages with versions |
218 |
| - """ |
219 |
| - result = {} |
220 |
| - try: |
221 |
| - cmd = [uv_executable, "pip", "list", f"--python={PYTHON_EXE}", "--format=json"] |
222 |
| - result_obj = subprocess.run( |
223 |
| - cmd, |
224 |
| - capture_output=True, |
225 |
| - text=True, |
226 |
| - encoding='utf-8', |
227 |
| - timeout=30 # 30 second timeout |
228 |
| - ) |
229 |
| - |
230 |
| - if result_obj.returncode == 0: |
231 |
| - content = result_obj.stdout.strip() |
232 |
| - if content: |
233 |
| - packages = json.loads(content) |
234 |
| - for p in packages: |
235 |
| - result[p["name"]] = pepver_to_semver(p["version"]) |
236 |
| - else: |
237 |
| - print(f"Warning: uv pip list failed with exit code {result_obj.returncode}") |
238 |
| - if result_obj.stderr: |
239 |
| - print(f"Error output: {result_obj.stderr.strip()}") |
240 |
| - |
241 |
| - except subprocess.TimeoutExpired: |
242 |
| - print("Warning: uv pip list command timed out") |
243 |
| - except (json.JSONDecodeError, KeyError) as e: |
244 |
| - print(f"Warning: Could not parse package list: {e}") |
245 |
| - except FileNotFoundError: |
246 |
| - print("Warning: uv command not found") |
247 |
| - except Exception as e: |
248 |
| - print(f"Warning! Couldn't extract the list of installed Python packages: {e}") |
249 |
| - |
250 |
| - return result |
251 |
| - |
252 |
| - installed_packages = _get_installed_uv_packages() |
253 |
| - packages_to_install = list(get_packages_to_install(python_deps, installed_packages)) |
254 |
| - |
255 |
| - if packages_to_install: |
256 |
| - packages_list = [] |
257 |
| - for p in packages_to_install: |
258 |
| - spec = python_deps[p] |
259 |
| - if spec.startswith(('http://', 'https://', 'git+', 'file://')): |
260 |
| - packages_list.append(spec) |
261 |
| - else: |
262 |
| - packages_list.append(f"{p}{spec}") |
263 |
| - |
264 |
| - cmd = [ |
265 |
| - uv_executable, "pip", "install", |
266 |
| - f"--python={PYTHON_EXE}", |
267 |
| - "--quiet", "--upgrade" |
268 |
| - ] + packages_list |
269 |
| - |
270 |
| - try: |
271 |
| - result = subprocess.run( |
272 |
| - cmd, |
273 |
| - capture_output=True, |
274 |
| - text=True, |
275 |
| - timeout=30 # 30 second timeout for package installation |
276 |
| - ) |
277 |
| - |
278 |
| - if result.returncode != 0: |
279 |
| - print(f"Error: Failed to install Python dependencies (exit code: {result.returncode})") |
280 |
| - if result.stderr: |
281 |
| - print(f"Error output: {result.stderr.strip()}") |
282 |
| - return False |
283 |
| - |
284 |
| - except subprocess.TimeoutExpired: |
285 |
| - print("Error: Python dependencies installation timed out") |
286 |
| - return False |
287 |
| - except FileNotFoundError: |
288 |
| - print("Error: uv command not found") |
289 |
| - return False |
290 |
| - except Exception as e: |
291 |
| - print(f"Error installing Python dependencies: {e}") |
292 |
| - return False |
293 |
| - |
294 |
| - return True |
295 |
| - |
296 |
| - |
297 |
| -def install_esptool(): |
298 |
| - """ |
299 |
| - Install esptool from package folder "tool-esptoolpy" using uv package manager. |
300 |
| -
|
301 |
| - Raises: |
302 |
| - SystemExit: If esptool installation fails |
303 |
| - """ |
304 |
| - try: |
305 |
| - subprocess.check_call( |
306 |
| - [PYTHON_EXE, "-c", "import esptool"], |
307 |
| - stdout=subprocess.DEVNULL, |
308 |
| - stderr=subprocess.DEVNULL |
309 |
| - ) |
310 |
| - return |
311 |
| - except (subprocess.CalledProcessError, FileNotFoundError): |
312 |
| - pass |
313 |
| - |
314 |
| - esptool_repo_path = env.subst(platform.get_package_dir("tool-esptoolpy") or "") |
315 |
| - if not esptool_repo_path or not os.path.isdir(esptool_repo_path): |
316 |
| - print("Error: esptool package directory not found") |
317 |
| - sys.exit(1) |
318 |
| - |
319 |
| - try: |
320 |
| - subprocess.check_call([ |
321 |
| - uv_executable, "pip", "install", "--quiet", |
322 |
| - f"--python={PYTHON_EXE}", |
323 |
| - "-e", esptool_repo_path |
324 |
| - ]) |
325 |
| - |
326 |
| - return |
327 |
| - |
328 |
| - except subprocess.CalledProcessError as e: |
329 |
| - print(f"Error: Failed to install esptool: {e}") |
330 |
| - sys.exit(1) |
331 |
| - |
332 |
| - |
333 |
| -# Install espressif32 Python dependencies |
334 |
| -install_python_deps() |
335 |
| -# Install esptool after dependencies |
336 |
| -install_esptool() |
| 45 | +# Setup Python virtual environment and get executable paths |
| 46 | +PYTHON_EXE, esptool_binary_path = setup_python_environment(env, platform, platformio_dir) |
337 | 47 |
|
338 | 48 |
|
339 | 49 | def BeforeUpload(target, source, env):
|
@@ -708,7 +418,7 @@ def switch_off_ldf():
|
708 | 418 | filesystem = board.get("build.filesystem", "littlefs")
|
709 | 419 |
|
710 | 420 | # Set toolchain architecture for RISC-V based ESP32 variants
|
711 |
| -if mcu in ("esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4"): |
| 421 | +if mcu not in ("esp32", "esp32s2", "esp32s3"): |
712 | 422 | toolchain_arch = "riscv32-esp"
|
713 | 423 |
|
714 | 424 | # Initialize integration extra data if not present
|
@@ -736,13 +446,10 @@ def switch_off_ldf():
|
736 | 446 | GDB=join(
|
737 | 447 | platform.get_package_dir(
|
738 | 448 | "tool-riscv32-esp-elf-gdb"
|
739 |
| - if mcu in ( |
740 |
| - "esp32c2", |
741 |
| - "esp32c3", |
742 |
| - "esp32c5", |
743 |
| - "esp32c6", |
744 |
| - "esp32h2", |
745 |
| - "esp32p4", |
| 449 | + if mcu not in ( |
| 450 | + "esp32", |
| 451 | + "esp32s2", |
| 452 | + "esp32s3", |
746 | 453 | )
|
747 | 454 | else "tool-xtensa-esp-elf-gdb"
|
748 | 455 | )
|
|
0 commit comments