Arduino v3.3.1
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
+392
-406
File diff suppressed because it is too large
Load Diff
+35
-43
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user