Skip to content

Commit e0f2822

Browse files
authored
Update main.py
1 parent 7f5adc2 commit e0f2822

File tree

1 file changed

+8
-301
lines changed

1 file changed

+8
-301
lines changed

builder/main.py

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

1515
import locale
16-
import json
1716
import os
1817
import re
19-
import site
20-
import semantic_version
2118
import shlex
2219
import subprocess
2320
import sys
@@ -33,34 +30,9 @@
3330
)
3431

3532
from platformio.project.helpers import get_project_dir
36-
from platformio.package.version import pepver_to_semver
3733
from platformio.util import get_serial_ports
3834
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
6436

6537
# Initialize environment and configuration
6638
env = DefaultEnvironment()
@@ -70,270 +42,8 @@
7042
FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
7143
platformio_dir = projectconfig.get("platformio", "core_dir")
7244

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)
33747

33848

33949
def BeforeUpload(target, source, env):
@@ -708,7 +418,7 @@ def switch_off_ldf():
708418
filesystem = board.get("build.filesystem", "littlefs")
709419

710420
# 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"):
712422
toolchain_arch = "riscv32-esp"
713423

714424
# Initialize integration extra data if not present
@@ -736,13 +446,10 @@ def switch_off_ldf():
736446
GDB=join(
737447
platform.get_package_dir(
738448
"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",
746453
)
747454
else "tool-xtensa-esp-elf-gdb"
748455
)

0 commit comments

Comments
 (0)