Arduino v3.3.1

This commit is contained in:
Jason2866
2025-09-17 00:09:47 +02:00
parent d408614d66
commit 8715c1708f
389 changed files with 2101 additions and 100083 deletions
+34 -29
View File
@@ -14,13 +14,24 @@
import shutil
from os import SEEK_CUR, SEEK_END
from os.path import basename, isfile, join
from os.path import basename, isfile
from pathlib import Path
from SCons.Script import Builder
Import("env")
board = env.BoardConfig()
mcu = board.get("build.mcu", "esp32")
is_xtensa = mcu in ("esp32", "esp32s2", "esp32s3")
cmake_dir = str(env.PioPlatform().get_package_dir("tool-cmake"))
cmake_cmd = f'"{Path(cmake_dir) / "bin" / "cmake"}"'
idf_dir = str(env.PioPlatform().get_package_dir("framework-espidf"))
data_embed_script = (
f'"{Path(idf_dir) / "tools" / "cmake" / "scripts" / "data_file_embed_asm.cmake"}"'
)
#
# Embedded files helpers
@@ -31,7 +42,7 @@ def extract_files(cppdefines, files_type):
result = []
files = env.GetProjectOption("board_build.%s" % files_type, "").splitlines()
if files:
result.extend([join("$PROJECT_DIR", f.strip()) for f in files if f])
result.extend([str(Path("$PROJECT_DIR") / f.strip()) for f in files if f.strip()])
else:
files_define = "COMPONENT_" + files_type.upper()
for define in cppdefines:
@@ -51,9 +62,10 @@ def extract_files(cppdefines, files_type):
return []
for f in value.split(":"):
f = f.strip()
if not f:
continue
result.append(join("$PROJECT_DIR", f))
result.append(str(Path("$PROJECT_DIR") / f))
for f in result:
if not isfile(env.subst(f)):
@@ -74,10 +86,14 @@ def prepare_file(source, target, env):
shutil.copy(filepath, filepath + ".piobkp")
with open(filepath, "rb+") as fp:
fp.seek(-1, SEEK_END)
if fp.read(1) != "\0":
fp.seek(0, SEEK_CUR)
fp.seek(0, SEEK_END)
size = fp.tell()
if size == 0:
fp.write(b"\0")
else:
fp.seek(-1, SEEK_END)
if fp.read(1) != b"\0":
fp.write(b"\0")
def revert_original_file(source, target, env):
@@ -89,20 +105,19 @@ def revert_original_file(source, target, env):
def embed_files(files, files_type):
for f in files:
filename = basename(f) + ".txt.o"
file_target = env.TxtToBin(join("$BUILD_DIR", filename), f)
file_target = env.TxtToBin(str(Path("$BUILD_DIR") / filename), f)
env.Depends("$PIOMAINPROG", file_target)
if files_type == "embed_txtfiles":
env.AddPreAction(file_target, prepare_file)
env.AddPostAction(file_target, revert_original_file)
env.AppendUnique(PIOBUILDFILES=[env.File(join("$BUILD_DIR", filename))])
env.AppendUnique(PIOBUILDFILES=[env.File(str(Path("$BUILD_DIR") / filename))])
def transform_to_asm(target, source, env):
files = [join("$BUILD_DIR", s.name + ".S") for s in source]
return files, source
asm_targets = [str(Path("$BUILD_DIR") / (s.name + ".S")) for s in source]
return asm_targets, source
mcu = board.get("build.mcu", "esp32")
env.Append(
BUILDERS=dict(
TxtToBin=Builder(
@@ -110,14 +125,14 @@ env.Append(
" ".join(
[
"riscv32-esp-elf-objcopy"
if mcu in ("esp32c2","esp32c3","esp32c5","esp32c6","esp32h2","esp32p4")
else "xtensa-%s-elf-objcopy" % mcu,
if not is_xtensa
else f"xtensa-{mcu}-elf-objcopy",
"--input-target",
"binary",
"--output-target",
"elf32-littleriscv" if mcu in ("esp32c2","esp32c3","esp32c5","esp32c6","esp32h2","esp32p4") else "elf32-xtensa-le",
"elf32-littleriscv" if not is_xtensa else "elf32-xtensa-le",
"--binary-architecture",
"riscv" if mcu in ("esp32c2","esp32c3","esp32c5","esp32c6","esp32h2","esp32p4") else "xtensa",
"riscv" if not is_xtensa else "xtensa",
"--rename-section",
".data=.rodata.embedded",
"$SOURCE",
@@ -132,22 +147,12 @@ env.Append(
action=env.VerboseAction(
" ".join(
[
join(
env.PioPlatform().get_package_dir("tool-cmake") or "",
"bin",
"cmake",
),
cmake_cmd,
"-DDATA_FILE=$SOURCE",
"-DSOURCE_FILE=$TARGET",
"-DFILE_TYPE=$FILE_TYPE",
"-P",
join(
env.PioPlatform().get_package_dir("framework-espidf") or "",
"tools",
"cmake",
"scripts",
"data_file_embed_asm.cmake",
),
data_embed_script,
]
),
"Generating assembly for $TARGET",
@@ -170,7 +175,7 @@ for files_type in ("embed_txtfiles", "embed_files"):
files = extract_files(flags, files_type)
if "espidf" in env.subst("$PIOFRAMEWORK"):
env.Requires(
join("$BUILD_DIR", "${PROGNAME}.elf"),
str(Path("$BUILD_DIR") / "${PROGNAME}.elf"),
env.FileToAsm(
files,
FILE_TYPE="TEXT" if files_type == "embed_txtfiles" else "BINARY",
+8 -11
View File
@@ -35,8 +35,7 @@ from typing import Union, List
from SCons.Script import DefaultEnvironment, SConscript
from platformio import fs
from platformio.package.manager.tool import ToolPackageManager
IS_WINDOWS = sys.platform.startswith("win")
from platformio.compat import IS_WINDOWS
# Constants for better performance
UNICORE_FLAGS = {
@@ -73,7 +72,7 @@ def get_platform_default_threshold(mcu):
"esp32": 32000, # Standard ESP32
"esp32s2": 32000, # ESP32-S2
"esp32s3": 32766, # ESP32-S3
"esp32c3": 30000, # ESP32-C3
"esp32c3": 32000, # ESP32-C3
"esp32c2": 32000, # ESP32-C2
"esp32c6": 31600, # ESP32-C6
"esp32h2": 32000, # ESP32-H2
@@ -311,7 +310,7 @@ class PathCache:
def sdk_dir(self):
if self._sdk_dir is None:
self._sdk_dir = fs.to_unix_path(
join(self.framework_lib_dir, self.mcu, "include")
str(Path(self.framework_lib_dir) / self.mcu / "include")
)
return self._sdk_dir
@@ -507,7 +506,7 @@ def safe_remove_sdkconfig_files():
envs = [section.replace("env:", "") for section in config.sections()
if section.startswith("env:")]
for env_name in envs:
file_path = join(project_dir, f"sdkconfig.{env_name}")
file_path = str(Path(project_dir) / f"sdkconfig.{env_name}")
if exists(file_path):
safe_delete_file(file_path)
@@ -566,9 +565,7 @@ FRAMEWORK_LIB_DIR = path_cache.framework_lib_dir
SConscript("_embed_files.py", exports="env")
flag_any_custom_sdkconfig = exists(join(
platform.get_package_dir("framework-arduinoespressif32-libs"),
"sdkconfig"))
flag_any_custom_sdkconfig = exists(str(Path(FRAMEWORK_LIB_DIR) / "sdkconfig"))
def has_unicore_flags():
@@ -599,7 +596,7 @@ def matching_custom_sdkconfig():
if not flag_any_custom_sdkconfig:
return True, cust_sdk_is_present
last_sdkconfig_path = join(project_dir, "sdkconfig.defaults")
last_sdkconfig_path = str(Path(project_dir) / "sdkconfig.defaults")
if not exists(last_sdkconfig_path):
return False, cust_sdk_is_present
@@ -901,12 +898,12 @@ if ("arduino" in pioframework and "espidf" not in pioframework and
component_manager = ComponentManager(env)
component_manager.handle_component_settings()
silent_action = env.Action(component_manager.restore_pioarduino_build_py)
# hack to silence scons command output
# silence scons command output
silent_action.strfunction = lambda target, source, env: ''
env.AddPostAction("checkprogsize", silent_action)
if IS_WINDOWS:
env.AddBuildMiddleware(smart_include_length_shorten)
build_script_path = join(FRAMEWORK_DIR, "tools", "pioarduino-build.py")
build_script_path = str(Path(FRAMEWORK_DIR) / "tools" / "pioarduino-build.py")
SConscript(build_script_path)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+35 -43
View File
@@ -14,10 +14,11 @@
import os
import sys
from pathlib import Path
from platformio import fs
from platformio.util import get_systype
from platformio.proc import where_is_program, exec_command
from platformio.proc import exec_command
from SCons.Script import Import
@@ -27,42 +28,41 @@ ulp_env = env.Clone()
platform = ulp_env.PioPlatform()
FRAMEWORK_DIR = platform.get_package_dir("framework-espidf")
BUILD_DIR = ulp_env.subst("$BUILD_DIR")
ULP_BUILD_DIR = os.path.join(
BUILD_DIR, "esp-idf", project_config["name"].replace("__idf_", ""), "ulp_main"
)
ULP_BUILD_DIR = str(Path(BUILD_DIR) / "esp-idf" / project_config["name"].replace("__idf_", "") / "ulp_main")
is_xtensa = idf_variant in ("esp32", "esp32s2", "esp32s3")
def prepare_ulp_env_vars(env):
ulp_env.PrependENVPath("IDF_PATH", FRAMEWORK_DIR)
toolchain_path = platform.get_package_dir(
"toolchain-xtensa-esp-elf"
if idf_variant not in ("esp32c5","esp32c6", "esp32p4")
if is_xtensa
else "toolchain-riscv32-esp"
)
toolchain_path_ulp = platform.get_package_dir(
"toolchain-esp32ulp"
if sdk_config.get("ULP_COPROC_TYPE_FSM", False)
else ""
else None
)
additional_packages = [
toolchain_path,
toolchain_path_ulp,
platform.get_package_dir("tool-ninja"),
os.path.join(platform.get_package_dir("tool-cmake"), "bin"),
os.path.dirname(where_is_program("python")),
str(Path(platform.get_package_dir("tool-cmake")) / "bin"),
]
for package in additional_packages:
ulp_env.PrependENVPath("PATH", package)
if package and os.path.isdir(package):
ulp_env.PrependENVPath("PATH", package)
def collect_ulp_sources():
return [
os.path.join(ulp_env.subst("$PROJECT_DIR"), "ulp", f)
for f in os.listdir(os.path.join(ulp_env.subst("$PROJECT_DIR"), "ulp"))
str(Path(ulp_env.subst("$PROJECT_DIR")) / "ulp" / f)
for f in os.listdir(str(Path(ulp_env.subst("$PROJECT_DIR")) / "ulp"))
if f.endswith((".c", ".S", ".s"))
]
@@ -77,7 +77,7 @@ def get_component_includes(target_config):
]
]
return [os.path.join(BUILD_DIR, "config")]
return [str(Path(BUILD_DIR) / "config")]
def generate_ulp_config(target_config):
@@ -85,7 +85,7 @@ def generate_ulp_config(target_config):
riscv_ulp_enabled = sdk_config.get("ULP_COPROC_TYPE_RISCV", False)
lp_core_ulp_enabled = sdk_config.get("ULP_COPROC_TYPE_LP_CORE", False)
if lp_core_ulp_enabled == False:
if not lp_core_ulp_enabled:
ulp_toolchain = "toolchain-%sulp%s.cmake"% (
"" if riscv_ulp_enabled else idf_variant + "-",
"-riscv" if riscv_ulp_enabled else "",
@@ -93,36 +93,30 @@ def generate_ulp_config(target_config):
else:
ulp_toolchain = "toolchain-lp-core-riscv.cmake"
comp_includes = ";".join(get_component_includes(target_config))
plain_includes = ";".join(app_includes["plain_includes"])
comp_includes = comp_includes + plain_includes
comp_includes_list = get_component_includes(target_config)
plain_includes_list = app_includes["plain_includes"]
comp_includes = ";".join(comp_includes_list + plain_includes_list)
cmd = (
os.path.join(platform.get_package_dir("tool-cmake"), "bin", "cmake"),
str(Path(platform.get_package_dir("tool-cmake")) / "bin" / "cmake"),
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DCMAKE_GENERATOR=Ninja",
"-DCMAKE_TOOLCHAIN_FILE="
+ os.path.join(
FRAMEWORK_DIR,
"components",
"ulp",
"cmake",
ulp_toolchain,
),
+ str(Path(FRAMEWORK_DIR) / "components" / "ulp" / "cmake" / ulp_toolchain),
"-DULP_S_SOURCES=%s" % ";".join([fs.to_unix_path(s.get_abspath()) for s in source]),
"-DULP_APP_NAME=ulp_main",
"-DCOMPONENT_DIR=" + os.path.join(ulp_env.subst("$PROJECT_DIR"), "ulp"),
"-DCOMPONENT_INCLUDES=" + comp_includes,
"-DCOMPONENT_DIR=" + str(Path(ulp_env.subst("$PROJECT_DIR")) / "ulp"),
"-DCOMPONENT_INCLUDES=%s" % comp_includes,
"-DIDF_TARGET=%s" % idf_variant,
"-DIDF_PATH=" + fs.to_unix_path(FRAMEWORK_DIR),
"-DSDKCONFIG_HEADER=" + os.path.join(BUILD_DIR, "config", "sdkconfig.h"),
"-DSDKCONFIG_HEADER=" + str(Path(BUILD_DIR) / "config" / "sdkconfig.h"),
"-DPYTHON=" + env.subst("$PYTHONEXE"),
"-DSDKCONFIG_CMAKE=" + os.path.join(BUILD_DIR, "config", "sdkconfig.cmake"),
"-DCMAKE_MODULE_PATH=" + fs.to_unix_path(os.path.join(FRAMEWORK_DIR, "components", "ulp", "cmake")),
"-DSDKCONFIG_CMAKE=" + str(Path(BUILD_DIR) / "config" / "sdkconfig.cmake"),
"-DCMAKE_MODULE_PATH=" + fs.to_unix_path(str(Path(FRAMEWORK_DIR) / "components" / "ulp" / "cmake")),
"-GNinja",
"-B",
ULP_BUILD_DIR,
os.path.join(FRAMEWORK_DIR, "components", "ulp", "cmake"),
str(Path(FRAMEWORK_DIR) / "components" / "ulp" / "cmake"),
)
result = exec_command(cmd)
@@ -134,7 +128,7 @@ def generate_ulp_config(target_config):
ulp_sources.sort()
return ulp_env.Command(
os.path.join(ULP_BUILD_DIR, "build.ninja"),
str(Path(ULP_BUILD_DIR) / "build.ninja"),
ulp_sources,
ulp_env.VerboseAction(
_generate_ulp_configuration_action, "Generating ULP configuration"
@@ -144,7 +138,7 @@ def generate_ulp_config(target_config):
def compile_ulp_binary():
cmd = (
os.path.join(platform.get_package_dir("tool-cmake"), "bin", "cmake"),
str(Path(platform.get_package_dir("tool-cmake")) / "bin" / "cmake"),
"--build",
ULP_BUILD_DIR,
"--target",
@@ -158,9 +152,9 @@ def compile_ulp_binary():
return ulp_binary_env.Command(
[
os.path.join(ULP_BUILD_DIR, "ulp_main.h"),
os.path.join(ULP_BUILD_DIR, "ulp_main.ld"),
os.path.join(ULP_BUILD_DIR, "ulp_main.bin"),
str(Path(ULP_BUILD_DIR) / "ulp_main.h"),
str(Path(ULP_BUILD_DIR) / "ulp_main.ld"),
str(Path(ULP_BUILD_DIR) / "ulp_main.bin"),
],
None,
ulp_binary_env.VerboseAction(" ".join(cmd), "Generating ULP project files $TARGETS"),
@@ -169,19 +163,17 @@ def compile_ulp_binary():
def generate_ulp_assembly():
cmd = (
os.path.join(platform.get_package_dir("tool-cmake"), "bin", "cmake"),
str(Path(platform.get_package_dir("tool-cmake")) / "bin" / "cmake"),
"-DDATA_FILE=$SOURCE",
"-DSOURCE_FILE=$TARGET",
"-DFILE_TYPE=BINARY",
"-P",
os.path.join(
FRAMEWORK_DIR, "tools", "cmake", "scripts", "data_file_embed_asm.cmake"
),
str(Path(FRAMEWORK_DIR) / "tools" / "cmake" / "scripts" / "data_file_embed_asm.cmake"),
)
return ulp_env.Command(
os.path.join(BUILD_DIR, "ulp_main.bin.S"),
os.path.join(ULP_BUILD_DIR, "ulp_main.bin"),
str(Path(BUILD_DIR) / "ulp_main.bin.S"),
str(Path(ULP_BUILD_DIR) / "ulp_main.bin"),
ulp_env.VerboseAction(" ".join(cmd), "Generating ULP assembly file $TARGET"),
)
@@ -190,7 +182,7 @@ prepare_ulp_env_vars(ulp_env)
ulp_assembly = generate_ulp_assembly()
ulp_env.Depends(compile_ulp_binary(), generate_ulp_config(project_config))
ulp_env.Depends(os.path.join("$BUILD_DIR", "${PROGNAME}.elf"), ulp_assembly)
ulp_env.Requires(os.path.join("$BUILD_DIR", "${PROGNAME}.elf"), ulp_assembly)
ulp_env.Depends(str(Path("$BUILD_DIR") / "${PROGNAME}.elf"), ulp_assembly)
ulp_env.Requires(str(Path("$BUILD_DIR") / "${PROGNAME}.elf"), ulp_assembly)
env.AppendUnique(CPPPATH=ULP_BUILD_DIR, LIBPATH=ULP_BUILD_DIR)
+59 -431
View File
@@ -13,14 +13,14 @@
# limitations under the License.
import locale
import json
import os
import re
import semantic_version
import shlex
import subprocess
import sys
from os.path import isfile, join
from pathlib import Path
import importlib.util
from SCons.Script import (
ARGUMENTS,
@@ -32,410 +32,52 @@ from SCons.Script import (
)
from platformio.project.helpers import get_project_dir
from platformio.package.version import pepver_to_semver
from platformio.util import get_serial_ports
from platformio.compat import IS_WINDOWS
# Check Python version requirement
if sys.version_info < (3, 10):
sys.stderr.write(
f"Error: Python 3.10 or higher is required. "
f"Current version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\n"
f"Please update your Python installation.\n"
)
sys.exit(1)
# Python dependencies required for the build process
python_deps = {
"uv": ">=0.1.0",
"pyyaml": ">=6.0.2",
"rich-click": ">=1.8.6",
"zopfli": ">=0.2.2",
"intelhex": ">=2.3.0",
"rich": ">=14.0.0",
"esp-idf-size": ">=1.6.1"
}
from penv_setup import setup_python_environment
# Initialize environment and configuration
env = DefaultEnvironment()
platform = env.PioPlatform()
projectconfig = env.GetProjectConfig()
terminal_cp = locale.getpreferredencoding().lower()
PYTHON_EXE = env.subst("$PYTHONEXE") # Global Python executable path
platform_dir = Path(env.PioPlatform().get_dir())
framework_dir = platform.get_package_dir("framework-arduinoespressif32")
core_dir = projectconfig.get("platformio", "core_dir")
build_dir = Path(projectconfig.get("platformio", "build_dir"))
# Framework directory path
FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
# Setup Python virtual environment and get executable paths
PYTHON_EXE, esptool_binary_path = setup_python_environment(env, platform, core_dir)
platformio_dir = projectconfig.get("platformio", "core_dir")
penv_dir = os.path.join(platformio_dir, "penv")
pip_path = os.path.join(
penv_dir,
"Scripts" if IS_WINDOWS else "bin",
"pip" + (".exe" if IS_WINDOWS else ""),
)
def setup_pipenv_in_package():
"""
Checks if 'penv' folder exists in platformio dir and creates virtual environment if not.
"""
if not os.path.exists(penv_dir):
env.Execute(
env.VerboseAction(
'"$PYTHONEXE" -m venv --clear "%s"' % penv_dir,
"Creating a new virtual environment for Python dependencies",
)
)
assert os.path.isfile(
pip_path
), "Error: Failed to create a proper virtual environment. Missing the `pip` binary!"
penv_python = os.path.join(penv_dir, "Scripts", "python.exe") if IS_WINDOWS else os.path.join(penv_dir, "bin", "python")
env.Replace(PYTHONEXE=penv_python)
print(f"PYTHONEXE updated to penv environment: {penv_python}")
setup_pipenv_in_package()
# Update global PYTHON_EXE variable after potential pipenv setup
PYTHON_EXE = env.subst("$PYTHONEXE")
python_exe = PYTHON_EXE
# Ensure penv Python directory is in PATH for subprocess calls
python_dir = os.path.dirname(PYTHON_EXE)
current_path = os.environ.get("PATH", "")
if python_dir not in current_path:
os.environ["PATH"] = python_dir + os.pathsep + current_path
# Verify the Python executable exists
assert os.path.isfile(PYTHON_EXE), f"Python executable not found: {PYTHON_EXE}"
if os.path.isfile(python_exe):
# Update sys.path to include penv site-packages
if IS_WINDOWS:
penv_site_packages = os.path.join(penv_dir, "Lib", "site-packages")
else:
# Find the actual site-packages directory in the venv
penv_lib_dir = os.path.join(penv_dir, "lib")
if os.path.isdir(penv_lib_dir):
for python_dir in os.listdir(penv_lib_dir):
if python_dir.startswith("python"):
penv_site_packages = os.path.join(penv_lib_dir, python_dir, "site-packages")
break
else:
penv_site_packages = None
else:
penv_site_packages = None
if penv_site_packages and os.path.isdir(penv_site_packages) and penv_site_packages not in sys.path:
sys.path.insert(0, penv_site_packages)
def add_to_pythonpath(path):
"""
Add a path to the PYTHONPATH environment variable (cross-platform).
Args:
path (str): The path to add to PYTHONPATH
"""
# Normalize the path for the current OS
normalized_path = os.path.normpath(path)
# Add to PYTHONPATH environment variable
if "PYTHONPATH" in os.environ:
current_paths = os.environ["PYTHONPATH"].split(os.pathsep)
normalized_current_paths = [os.path.normpath(p) for p in current_paths]
if normalized_path not in normalized_current_paths:
os.environ["PYTHONPATH"] = normalized_path + os.pathsep + os.environ.get("PYTHONPATH", "")
else:
os.environ["PYTHONPATH"] = normalized_path
# Also add to sys.path for immediate availability
if normalized_path not in sys.path:
sys.path.insert(0, normalized_path)
def setup_python_paths():
"""
Setup Python paths based on the actual Python executable being used.
"""
# Get the directory containing the Python executable
python_dir = os.path.dirname(PYTHON_EXE)
add_to_pythonpath(python_dir)
# Try to find site-packages directory using the actual Python executable
result = subprocess.run(
[PYTHON_EXE, "-c", "import site; print(site.getsitepackages()[0])"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
site_packages = result.stdout.strip()
if os.path.isdir(site_packages):
add_to_pythonpath(site_packages)
# Setup Python paths based on the actual Python executable
setup_python_paths()
def _get_executable_path(python_exe, executable_name):
"""
Get the path to an executable binary (esptool, uv, etc.) based on the Python executable path.
Args:
python_exe (str): Path to Python executable
executable_name (str): Name of the executable to find (e.g., 'esptool', 'uv')
Returns:
str: Path to executable or fallback to executable name
"""
python_dir = os.path.dirname(python_exe)
if IS_WINDOWS:
executable_path = os.path.join(python_dir, f"{executable_name}.exe")
else:
# For Unix-like systems, executables are typically in the same directory as python
# or in a bin subdirectory
executable_path = os.path.join(python_dir, executable_name)
# If not found in python directory, try bin subdirectory
if not os.path.isfile(executable_path):
bin_dir = os.path.join(python_dir, "bin")
executable_path = os.path.join(bin_dir, executable_name)
if os.path.isfile(executable_path):
return executable_path
return executable_name # Fallback to command name
# Initialize board configuration and MCU settings
board = env.BoardConfig()
board_id = env.subst("$BOARD")
mcu = board.get("build.mcu", "esp32")
is_xtensa = mcu in ("esp32", "esp32s2", "esp32s3")
toolchain_arch = "xtensa-%s" % mcu
filesystem = board.get("build.filesystem", "littlefs")
def _get_esptool_executable_path(python_exe):
"""
Get the path to the esptool executable binary.
Args:
python_exe (str): Path to Python executable
Returns:
str: Path to esptool executable
"""
return _get_executable_path(python_exe, "esptool")
def load_board_script(env):
if not board_id:
return
script_path = platform_dir / "boards" / f"{board_id}.py"
def _get_uv_executable_path(python_exe):
"""
Get the path to the uv executable binary.
Args:
python_exe (str): Path to Python executable
Returns:
str: Path to uv executable
"""
return _get_executable_path(python_exe, "uv")
def get_packages_to_install(deps, installed_packages):
"""
Generator for Python packages that need to be installed.
Args:
deps (dict): Dictionary of package names and version specifications
installed_packages (dict): Dictionary of currently installed packages
Yields:
str: Package name that needs to be installed
"""
for package, spec in deps.items():
if package not in installed_packages:
yield package
else:
version_spec = semantic_version.Spec(spec)
if not version_spec.match(installed_packages[package]):
yield package
def install_python_deps():
"""
Ensure uv package manager is available and install required Python dependencies.
Returns:
bool: True if successful, False otherwise
"""
# Get uv executable path
uv_executable = _get_uv_executable_path(PYTHON_EXE)
try:
result = subprocess.run(
[uv_executable, "--version"],
capture_output=True,
text=True,
timeout=3
)
uv_available = result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
uv_available = False
if not uv_available:
if script_path.exists():
try:
result = subprocess.run(
[PYTHON_EXE, "-m", "pip", "install", "uv>=0.1.0", "-q", "-q", "-q"],
capture_output=True,
text=True,
timeout=30, # 30 second timeout
env=os.environ # Use modified environment with custom PYTHONPATH
spec = importlib.util.spec_from_file_location(
f"board_{board_id}",
str(script_path)
)
if result.returncode != 0:
if result.stderr:
print(f"Error output: {result.stderr.strip()}")
return False
# Update uv executable path after installation
uv_executable = _get_uv_executable_path(PYTHON_EXE)
# Add Scripts directory to PATH for Windows
if IS_WINDOWS:
python_dir = os.path.dirname(PYTHON_EXE)
scripts_dir = os.path.join(python_dir, "Scripts")
if os.path.isdir(scripts_dir):
os.environ["PATH"] = scripts_dir + os.pathsep + os.environ.get("PATH", "")
except subprocess.TimeoutExpired:
print("Error: uv installation timed out")
return False
except FileNotFoundError:
print("Error: Python executable not found")
return False
board_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(board_module)
if hasattr(board_module, 'configure_board'):
board_module.configure_board(env)
except Exception as e:
print(f"Error installing uv package manager: {e}")
return False
def _get_installed_uv_packages():
"""
Get list of installed packages using uv.
Returns:
dict: Dictionary of installed packages with versions
"""
result = {}
try:
cmd = [uv_executable, "pip", "list", "--format=json"]
result_obj = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
timeout=30, # 30 second timeout
env=os.environ # Use modified environment with custom PYTHONPATH
)
if result_obj.returncode == 0:
content = result_obj.stdout.strip()
if content:
packages = json.loads(content)
for p in packages:
result[p["name"]] = pepver_to_semver(p["version"])
else:
print(f"Warning: pip list failed with exit code {result_obj.returncode}")
if result_obj.stderr:
print(f"Error output: {result_obj.stderr.strip()}")
except subprocess.TimeoutExpired:
print("Warning: uv pip list command timed out")
except (json.JSONDecodeError, KeyError) as e:
print(f"Warning: Could not parse package list: {e}")
except FileNotFoundError:
print("Warning: uv command not found")
except Exception as e:
print(f"Warning! Couldn't extract the list of installed Python packages: {e}")
return result
installed_packages = _get_installed_uv_packages()
packages_to_install = list(get_packages_to_install(python_deps, installed_packages))
if packages_to_install:
packages_list = [f"{p}{python_deps[p]}" for p in packages_to_install]
cmd = [
uv_executable, "pip", "install",
f"--python={PYTHON_EXE}",
"--quiet", "--upgrade"
] + packages_list
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30, # 30 second timeout for package installation
env=os.environ # Use modified environment with custom PYTHONPATH
)
if result.returncode != 0:
print(f"Error: Failed to install Python dependencies (exit code: {result.returncode})")
if result.stderr:
print(f"Error output: {result.stderr.strip()}")
return False
except subprocess.TimeoutExpired:
print("Error: Python dependencies installation timed out")
return False
except FileNotFoundError:
print("Error: uv command not found")
return False
except Exception as e:
print(f"Error installing Python dependencies: {e}")
return False
return True
def install_esptool():
"""
Install esptool from package folder "tool-esptoolpy" using uv package manager.
Also determines the path to the esptool executable binary.
Returns:
str: Path to esptool executable, or 'esptool' as fallback
"""
try:
subprocess.check_call(
[PYTHON_EXE, "-c", "import esptool"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=os.environ
)
esptool_binary_path = _get_esptool_executable_path(PYTHON_EXE)
return esptool_binary_path
except (subprocess.CalledProcessError, FileNotFoundError):
pass
esptool_repo_path = env.subst(platform.get_package_dir("tool-esptoolpy") or "")
if esptool_repo_path and os.path.isdir(esptool_repo_path):
uv_executable = _get_uv_executable_path(PYTHON_EXE)
try:
subprocess.check_call([
uv_executable, "pip", "install", "--quiet",
f"--python={PYTHON_EXE}",
"-e", esptool_repo_path
], env=os.environ)
esptool_binary_path = _get_esptool_executable_path(PYTHON_EXE)
return esptool_binary_path
except subprocess.CalledProcessError as e:
print(f"Warning: Failed to install esptool: {e}")
return 'esptool' # Fallback
return 'esptool' # Fallback
# Install Python dependencies
install_python_deps()
# Install esptool after dependencies
esptool_binary_path = install_esptool()
print(f"Error loading board script {board_id}.py: {e}")
def BeforeUpload(target, source, env):
"""
@@ -802,14 +444,11 @@ def switch_off_ldf():
projectconfig.set(env_section, "lib_ldf_mode", "off")
# Initialize board configuration and MCU settings
board = env.BoardConfig()
mcu = board.get("build.mcu", "esp32")
toolchain_arch = "xtensa-%s" % mcu
filesystem = board.get("build.filesystem", "littlefs")
# Board specific script
load_board_script(env)
# Set toolchain architecture for RISC-V based ESP32 variants
if mcu in ("esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4"):
if not is_xtensa:
toolchain_arch = "riscv32-esp"
# Initialize integration extra data if not present
@@ -817,7 +456,7 @@ if "INTEGRATION_EXTRA_DATA" not in env:
env["INTEGRATION_EXTRA_DATA"] = {}
# Take care of possible whitespaces in path
objcopy_value = (
uploader_path = (
f'"{esptool_binary_path}"'
if ' ' in esptool_binary_path
else esptool_binary_path
@@ -837,21 +476,14 @@ env.Replace(
GDB=join(
platform.get_package_dir(
"tool-riscv32-esp-elf-gdb"
if mcu in (
"esp32c2",
"esp32c3",
"esp32c5",
"esp32c6",
"esp32h2",
"esp32p4",
)
if not is_xtensa
else "tool-xtensa-esp-elf-gdb"
)
or "",
"bin",
"%s-elf-gdb" % toolchain_arch,
),
OBJCOPY=objcopy_value,
OBJCOPY=uploader_path,
RANLIB="%s-elf-gcc-ranlib" % toolchain_arch,
SIZETOOL="%s-elf-size" % toolchain_arch,
ARFLAGS=["rc"],
@@ -861,8 +493,8 @@ env.Replace(
SIZECHECKCMD="$SIZETOOL -A -d $SOURCES",
SIZEPRINTCMD="$SIZETOOL -B -d $SOURCES",
ERASEFLAGS=["--chip", mcu, "--port", '"$UPLOAD_PORT"'],
ERASECMD='"$OBJCOPY" $ERASEFLAGS erase-flash',
# mkspiffs package contains two different binaries for IDF and Arduino
ERASETOOL=uploader_path,
ERASECMD='$ERASETOOL $ERASEFLAGS erase-flash',
MKFSTOOL="mk%s" % filesystem
+ (
(
@@ -907,7 +539,7 @@ env.Append(
action=env.VerboseAction(
" ".join(
[
"$OBJCOPY",
"$ERASETOOL",
"--chip",
mcu,
"elf2image",
@@ -918,8 +550,8 @@ env.Append(
"--flash-size",
board.get("upload.flash_size", "4MB"),
"-o",
"$TARGET",
"$SOURCES",
"\"$TARGET\"",
"\"$SOURCES\"",
]
),
"Building $TARGET",
@@ -969,12 +601,12 @@ def firmware_metrics(target, source, env):
print("Firmware metrics can not be shown. Set the terminal codepage to \"utf-8\"")
return
map_file = os.path.join(env.subst("$BUILD_DIR"), env.subst("$PROGNAME") + ".map")
if not os.path.isfile(map_file):
map_file = str(Path(env.subst("$BUILD_DIR")) / (env.subst("$PROGNAME") + ".map"))
if not Path(map_file).is_file():
# map file can be in project dir
map_file = os.path.join(get_project_dir(), env.subst("$PROGNAME") + ".map")
map_file = str(Path(get_project_dir()) / (env.subst("$PROGNAME") + ".map"))
if not os.path.isfile(map_file):
if not Path(map_file).is_file():
print(f"Error: Map file not found: {map_file}")
print("Make sure the project is built first with 'pio run'")
return
@@ -993,7 +625,6 @@ def firmware_metrics(target, source, env):
dash_index = sys.argv.index("--")
if dash_index + 1 < len(sys.argv):
cli_args = sys.argv[dash_index + 1:]
cmd.extend(cli_args)
# Add CLI arguments before the map file
if cli_args:
@@ -1011,16 +642,13 @@ def firmware_metrics(target, source, env):
if result.returncode != 0:
print(f"Warning: esp-idf-size exited with code {result.returncode}")
except ImportError:
print("Error: esp-idf-size module not found.")
print("Install with: pip install esp-idf-size")
except FileNotFoundError:
print("Error: Python executable not found.")
print("Check your Python installation.")
except Exception as e:
print(f"Error: Failed to run firmware metrics: {e}")
print("Make sure esp-idf-size is installed: pip install esp-idf-size")
print(f'Make sure esp-idf-size is installed: uv pip install --python "{PYTHON_EXE}" esp-idf-size')
#
@@ -1029,12 +657,12 @@ def firmware_metrics(target, source, env):
target_elf = None
if "nobuild" in COMMAND_LINE_TARGETS:
target_elf = join("$BUILD_DIR", "${PROGNAME}.elf")
target_elf = str(Path("$BUILD_DIR") / "${PROGNAME}.elf")
if set(["uploadfs", "uploadfsota"]) & set(COMMAND_LINE_TARGETS):
fetch_fs_size(env)
target_firm = join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}.bin")
target_firm = str(Path("$BUILD_DIR") / "${ESP32_FS_IMAGE_NAME}.bin")
else:
target_firm = join("$BUILD_DIR", "${PROGNAME}.bin")
target_firm = str(Path("$BUILD_DIR") / "${PROGNAME}.bin")
else:
target_elf = env.BuildProgram()
silent_action = env.Action(firmware_metrics)
@@ -1043,12 +671,12 @@ else:
env.AddPostAction(target_elf, silent_action)
if set(["buildfs", "uploadfs", "uploadfsota"]) & set(COMMAND_LINE_TARGETS):
target_firm = env.DataToBin(
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
str(Path("$BUILD_DIR") / "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
)
env.NoCache(target_firm)
AlwaysBuild(target_firm)
else:
target_firm = env.ElfToBin(join("$BUILD_DIR", "${PROGNAME}"), target_elf)
target_firm = env.ElfToBin(str(Path("$BUILD_DIR") / "${PROGNAME}"), target_elf)
env.Depends(target_firm, "checkprogsize")
# Configure platform targets
@@ -1078,7 +706,7 @@ target_size = env.AddPlatformTarget(
)
# Target: Upload firmware or FS image
upload_protocol = env.subst("$UPLOAD_PROTOCOL")
upload_protocol = env.subst("$UPLOAD_PROTOCOL") or "esptool"
debug_tools = board.get("debug.tools", {})
upload_actions = []
@@ -1106,7 +734,7 @@ if upload_protocol == "espota":
"espressif32.html#over-the-air-ota-update\n"
)
env.Replace(
UPLOADER=join(FRAMEWORK_DIR, "tools", "espota.py"),
UPLOADER=str(Path(framework_dir).resolve() / "tools" / "espota.py"),
UPLOADERFLAGS=["--debug", "--progress", "-i", "$UPLOAD_PORT"],
UPLOADCMD=f'"{PYTHON_EXE}" "$UPLOADER" $UPLOADERFLAGS -f $SOURCE',
)
@@ -1117,7 +745,7 @@ if upload_protocol == "espota":
# Configure upload protocol: esptool
elif upload_protocol == "esptool":
env.Replace(
UPLOADER=objcopy_value,
UPLOADER=uploader_path,
UPLOADERFLAGS=[
"--chip",
mcu,
@@ -1166,7 +794,7 @@ elif upload_protocol == "esptool":
"detect",
"$FS_START",
],
UPLOADCMD='"$UPLOADER" $UPLOADERFLAGS $SOURCE',
UPLOADCMD='$UPLOADER $UPLOADERFLAGS $SOURCE',
)
upload_actions = [
@@ -1183,8 +811,8 @@ elif upload_protocol == "dfu":
upload_actions = [env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE")]
env.Replace(
UPLOADER=join(
platform.get_package_dir("tool-dfuutil-arduino") or "", "dfu-util"
UPLOADER=str(
Path(platform.get_package_dir("tool-dfuutil-arduino")).resolve() / "dfu-util"
),
UPLOADERFLAGS=[
"-d",
+481
View File
@@ -0,0 +1,481 @@
# Copyright 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import re
import site
import semantic_version
import subprocess
import sys
import socket
from pathlib import Path
from platformio.package.version import pepver_to_semver
from platformio.compat import IS_WINDOWS
# Check Python version requirement
if sys.version_info < (3, 10):
sys.stderr.write(
f"Error: Python 3.10 or higher is required. "
f"Current version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\n"
f"Please update your Python installation.\n"
)
sys.exit(1)
github_actions = os.getenv('GITHUB_ACTIONS')
PLATFORMIO_URL_VERSION_RE = re.compile(
r'/v?(\d+\.\d+\.\d+(?:[.-]\w+)?(?:\.\d+)?)(?:\.(?:zip|tar\.gz|tar\.bz2))?$',
re.IGNORECASE,
)
# Python dependencies required for the build process
python_deps = {
"platformio": "https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip",
"pyyaml": ">=6.0.2",
"rich-click": ">=1.8.6",
"zopfli": ">=0.2.2",
"intelhex": ">=2.3.0",
"rich": ">=14.0.0",
"cryptography": ">=45.0.3",
"certifi": ">=2025.8.3",
"ecdsa": ">=0.19.1",
"bitstring": ">=4.3.1",
"reedsolo": ">=1.5.3,<1.8",
"esp-idf-size": ">=1.6.1"
}
def has_internet_connection(host="1.1.1.1", port=53, timeout=2):
"""
Checks if an internet connection is available (default: Cloudflare DNS server).
Returns True if a connection is possible, otherwise False.
"""
try:
socket.setdefaulttimeout(timeout)
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
return True
except Exception:
return False
def get_executable_path(penv_dir, executable_name):
"""
Get the path to an executable based on the penv_dir.
"""
exe_suffix = ".exe" if IS_WINDOWS else ""
scripts_dir = "Scripts" if IS_WINDOWS else "bin"
return str(Path(penv_dir) / scripts_dir / f"{executable_name}{exe_suffix}")
def setup_pipenv_in_package(env, penv_dir):
"""
Checks if 'penv' folder exists in platformio dir and creates virtual environment if not.
First tries to create with uv, falls back to python -m venv if uv is not available.
Returns:
str or None: Path to uv executable if uv was used, None if python -m venv was used
"""
if not os.path.exists(penv_dir):
# First try to create virtual environment with uv
uv_success = False
uv_cmd = None
try:
# Derive uv path from PYTHONEXE path
python_exe = env.subst("$PYTHONEXE")
python_dir = os.path.dirname(python_exe)
uv_exe_suffix = ".exe" if IS_WINDOWS else ""
uv_cmd = str(Path(python_dir) / f"uv{uv_exe_suffix}")
# Fall back to system uv if derived path doesn't exist
if not os.path.isfile(uv_cmd):
uv_cmd = "uv"
subprocess.check_call(
[uv_cmd, "venv", "--clear", f"--python={python_exe}", penv_dir],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=90
)
uv_success = True
print(f"Created pioarduino Python virtual environment using uv: {penv_dir}")
except Exception:
pass
# Fallback to python -m venv if uv failed or is not available
if not uv_success:
uv_cmd = None
env.Execute(
env.VerboseAction(
'"$PYTHONEXE" -m venv --clear "%s"' % penv_dir,
"Created pioarduino Python virtual environment: %s" % penv_dir,
)
)
# Verify that the virtual environment was created properly
# Check for python executable
assert os.path.isfile(
get_executable_path(penv_dir, "python")
), f"Error: Failed to create a proper virtual environment. Missing the `python` binary! Created with uv: {uv_success}"
return uv_cmd if uv_success else None
return None
def setup_python_paths(penv_dir):
"""Setup Python module search paths using the penv_dir."""
# Add site-packages directory
python_ver = f"python{sys.version_info.major}.{sys.version_info.minor}"
site_packages = (
str(Path(penv_dir) / "Lib" / "site-packages") if IS_WINDOWS
else str(Path(penv_dir) / "lib" / python_ver / "site-packages")
)
if os.path.isdir(site_packages):
site.addsitedir(site_packages)
def get_packages_to_install(deps, installed_packages):
"""
Generator for Python packages that need to be installed.
Compares package names case-insensitively.
Args:
deps (dict): Dictionary of package names and version specifications
installed_packages (dict): Dictionary of currently installed packages (keys should be lowercase)
Yields:
str: Package name that needs to be installed
"""
for package, spec in deps.items():
name = package.lower()
if name not in installed_packages:
yield package
elif name == "platformio":
# Enforce the version from the direct URL if it looks like one.
# If version can't be parsed, fall back to accepting any installed version.
m = PLATFORMIO_URL_VERSION_RE.search(spec)
if m:
expected_ver = pepver_to_semver(m.group(1))
if installed_packages.get(name) != expected_ver:
# Reinstall to align with the pinned URL version
yield package
else:
continue
else:
version_spec = semantic_version.SimpleSpec(spec)
if not version_spec.match(installed_packages[name]):
yield package
def install_python_deps(python_exe, external_uv_executable):
"""
Ensure uv package manager is available in penv and install required Python dependencies.
Args:
python_exe: Path to Python executable in the penv
external_uv_executable: Path to external uv executable used to create the penv (can be None)
Returns:
bool: True if successful, False otherwise
"""
# Get the penv directory to locate uv within it
penv_dir = os.path.dirname(os.path.dirname(python_exe))
penv_uv_executable = get_executable_path(penv_dir, "uv")
# Check if uv is available in the penv
uv_in_penv_available = False
try:
result = subprocess.run(
[penv_uv_executable, "--version"],
capture_output=True,
text=True,
timeout=10
)
uv_in_penv_available = result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
uv_in_penv_available = False
# Install uv into penv if not available
if not uv_in_penv_available:
if external_uv_executable:
# Use external uv to install uv into the penv
try:
subprocess.check_call(
[external_uv_executable, "pip", "install", "uv>=0.1.0", f"--python={python_exe}", "--quiet"],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
timeout=120
)
except subprocess.CalledProcessError as e:
print(f"Error: uv installation failed with exit code {e.returncode}")
return False
except subprocess.TimeoutExpired:
print("Error: uv installation timed out")
return False
except FileNotFoundError:
print("Error: External uv executable not found")
return False
except Exception as e:
print(f"Error installing uv package manager into penv: {e}")
return False
else:
# No external uv available, use pip to install uv into penv
try:
subprocess.check_call(
[python_exe, "-m", "pip", "install", "uv>=0.1.0", "--quiet"],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
timeout=120
)
except subprocess.CalledProcessError as e:
print(f"Error: uv installation via pip failed with exit code {e.returncode}")
return False
except subprocess.TimeoutExpired:
print("Error: uv installation via pip timed out")
return False
except FileNotFoundError:
print("Error: Python executable not found")
return False
except Exception as e:
print(f"Error installing uv package manager via pip: {e}")
return False
def _get_installed_uv_packages():
"""
Get list of installed packages in virtual env 'penv' using uv.
Returns:
dict: Dictionary of installed packages with versions
"""
result = {}
try:
cmd = [penv_uv_executable, "pip", "list", f"--python={python_exe}", "--format=json"]
result_obj = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
timeout=120
)
if result_obj.returncode == 0:
content = result_obj.stdout.strip()
if content:
packages = json.loads(content)
for p in packages:
result[p["name"].lower()] = pepver_to_semver(p["version"])
else:
print(f"Warning: uv pip list failed with exit code {result_obj.returncode}")
if result_obj.stderr:
print(f"Error output: {result_obj.stderr.strip()}")
except subprocess.TimeoutExpired:
print("Warning: uv pip list command timed out")
except (json.JSONDecodeError, KeyError) as e:
print(f"Warning: Could not parse package list: {e}")
except FileNotFoundError:
print("Warning: uv command not found")
except Exception as e:
print(f"Warning! Couldn't extract the list of installed Python packages: {e}")
return result
installed_packages = _get_installed_uv_packages()
packages_to_install = list(get_packages_to_install(python_deps, installed_packages))
if packages_to_install:
packages_list = []
for p in packages_to_install:
spec = python_deps[p]
if spec.startswith(('http://', 'https://', 'git+', 'file://')):
packages_list.append(spec)
else:
packages_list.append(f"{p}{spec}")
cmd = [
penv_uv_executable, "pip", "install",
f"--python={python_exe}",
"--quiet", "--upgrade"
] + packages_list
try:
subprocess.check_call(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
timeout=120
)
except subprocess.CalledProcessError as e:
print(f"Error: Failed to install Python dependencies (exit code: {e.returncode})")
return False
except subprocess.TimeoutExpired:
print("Error: Python dependencies installation timed out")
return False
except FileNotFoundError:
print("Error: uv command not found")
return False
except Exception as e:
print(f"Error installing Python dependencies: {e}")
return False
return True
def install_esptool(env, platform, python_exe, uv_executable):
"""
Install esptool from package folder "tool-esptoolpy" using uv package manager.
Ensures esptool is installed from the specific tool-esptoolpy package directory.
Args:
env: SCons environment object
platform: PlatformIO platform object
python_exe (str): Path to Python executable in virtual environment
uv_executable (str): Path to uv executable
Raises:
SystemExit: If esptool installation fails or package directory not found
"""
esptool_repo_path = env.subst(platform.get_package_dir("tool-esptoolpy") or "")
if not esptool_repo_path or not os.path.isdir(esptool_repo_path):
sys.stderr.write(
f"Error: 'tool-esptoolpy' package directory not found: {esptool_repo_path!r}\n"
)
sys.exit(1)
# Check if esptool is already installed from the correct path
try:
result = subprocess.run(
[
python_exe,
"-c",
(
"import esptool, os, sys; "
"expected_path = os.path.normcase(os.path.realpath(sys.argv[1])); "
"actual_path = os.path.normcase(os.path.realpath(os.path.dirname(esptool.__file__))); "
"print('MATCH' if actual_path.startswith(expected_path) else 'MISMATCH')"
),
esptool_repo_path,
],
capture_output=True,
check=True,
text=True,
timeout=5
)
if result.stdout.strip() == "MATCH":
return
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
pass
try:
subprocess.check_call([
uv_executable, "pip", "install", "--quiet", "--force-reinstall",
f"--python={python_exe}",
"-e", esptool_repo_path
], timeout=60)
except subprocess.CalledProcessError as e:
sys.stderr.write(
f"Error: Failed to install esptool from {esptool_repo_path} (exit {e.returncode})\n"
)
sys.exit(1)
def setup_python_environment(env, platform, platformio_dir):
"""
Main function to setup the Python virtual environment and dependencies.
Args:
env: SCons environment object
platform: PlatformIO platform object
platformio_dir (str): Path to PlatformIO core directory
Returns:
tuple[str, str]: (Path to penv Python executable, Path to esptool script)
Raises:
SystemExit: If Python version < 3.10 or dependency installation fails
"""
# Check Python version requirement
if sys.version_info < (3, 10):
sys.stderr.write(
f"Error: Python 3.10 or higher is required. "
f"Current version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\n"
f"Please update your Python installation.\n"
)
sys.exit(1)
penv_dir = str(Path(platformio_dir) / "penv")
# Setup virtual environment if needed
used_uv_executable = setup_pipenv_in_package(env, penv_dir)
# Set Python Scons Var to env Python
penv_python = get_executable_path(penv_dir, "python")
env.Replace(PYTHONEXE=penv_python)
# check for python binary, exit with error when not found
assert os.path.isfile(penv_python), f"Python executable not found: {penv_python}"
# Setup Python module search paths
setup_python_paths(penv_dir)
# Set executable paths from tools
esptool_binary_path = get_executable_path(penv_dir, "esptool")
uv_executable = get_executable_path(penv_dir, "uv")
# Install espressif32 Python dependencies
if has_internet_connection() or github_actions:
if not install_python_deps(penv_python, used_uv_executable):
sys.stderr.write("Error: Failed to install Python dependencies into penv\n")
sys.exit(1)
else:
print("Warning: No internet connection detected, Python dependency check will be skipped.")
# Install esptool after dependencies
install_esptool(env, platform, penv_python, uv_executable)
# Setup certifi environment variables
def setup_certifi_env():
try:
import certifi
except ImportError:
print("Info: certifi not available; skipping CA environment setup.")
return
cert_path = certifi.where()
os.environ["CERTIFI_PATH"] = cert_path
os.environ["SSL_CERT_FILE"] = cert_path
os.environ["REQUESTS_CA_BUNDLE"] = cert_path
os.environ["CURL_CA_BUNDLE"] = cert_path
# Also propagate to SCons environment for future env.Execute calls
env_vars = dict(env.get("ENV", {}))
env_vars.update({
"CERTIFI_PATH": cert_path,
"SSL_CERT_FILE": cert_path,
"REQUESTS_CA_BUNDLE": cert_path,
"CURL_CA_BUNDLE": cert_path,
})
env.Replace(ENV=env_vars)
setup_certifi_env()
return penv_python, esptool_binary_path