Files
platform-espressif32/builder/main.py
T

1312 lines
41 KiB
Python
Raw Normal View History

2016-10-22 02:20:57 +03:00
# 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.
2025-07-03 17:12:25 +02:00
import locale
2025-07-23 17:46:36 +02:00
import json
2025-07-03 17:12:25 +02:00
import os
2019-05-11 22:13:26 +03:00
import re
2025-07-23 17:46:36 +02:00
import semantic_version
2025-07-03 17:12:25 +02:00
import shlex
import subprocess
import sys
from os.path import isfile, join
2016-10-24 20:23:25 +03:00
2019-05-28 14:09:26 +03:00
from SCons.Script import (
2025-07-03 17:12:25 +02:00
ARGUMENTS,
COMMAND_LINE_TARGETS,
AlwaysBuild,
Builder,
Default,
DefaultEnvironment,
)
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
from platformio.project.helpers import get_project_dir
2025-07-23 17:46:36 +02:00
from platformio.package.version import pepver_to_semver
2022-04-13 18:49:20 +03:00
from platformio.util import get_serial_ports
2025-08-03 13:07:04 +02:00
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)
2022-04-13 18:49:20 +03:00
2025-07-23 17:46:36 +02:00
# 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"
}
2025-07-03 17:12:25 +02:00
# Initialize environment and configuration
env = DefaultEnvironment()
platform = env.PioPlatform()
2025-07-03 17:12:25 +02:00
projectconfig = env.GetProjectConfig()
terminal_cp = locale.getpreferredencoding().lower()
2025-07-23 17:46:36 +02:00
PYTHON_EXE = env.subst("$PYTHONEXE") # Global Python executable path
2025-07-03 17:12:25 +02:00
# Framework directory path
FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
2025-08-03 13:07:04 +02:00
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)
2025-07-03 17:12:25 +02:00
2025-07-23 17:46:36 +02:00
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.
2025-08-03 13:07:04 +02:00
"""
2025-07-23 17:46:36 +02:00
# 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)
2025-08-03 13:07:04 +02:00
if IS_WINDOWS:
executable_path = os.path.join(python_dir, f"{executable_name}.exe")
2025-07-23 17:46:36 +02:00
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
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 _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:
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
)
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
2025-08-03 13:07:04 +02:00
if IS_WINDOWS:
2025-07-23 17:46:36 +02:00
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
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
2025-08-03 13:07:04 +02:00
# Install Python dependencies
2025-07-23 17:46:36 +02:00
install_python_deps()
2025-08-03 13:07:04 +02:00
# Install esptool after dependencies
2025-07-23 17:46:36 +02:00
esptool_binary_path = install_esptool()
2022-04-13 18:49:20 +03:00
def BeforeUpload(target, source, env):
2025-07-03 17:12:25 +02:00
"""
Prepare the environment before uploading firmware.
Handles port detection and special upload configurations.
2025-07-23 17:46:36 +02:00
Args:
target: SCons target
source: SCons source
env: SCons environment object
2025-07-03 17:12:25 +02:00
"""
2022-04-13 18:49:20 +03:00
upload_options = {}
if "BOARD" in env:
upload_options = env.BoardConfig().get("upload", {})
2025-07-03 17:12:25 +02:00
if not env.subst("$UPLOAD_PORT"):
env.AutodetectUploadPort()
2022-04-13 18:49:20 +03:00
before_ports = get_serial_ports()
if upload_options.get("use_1200bps_touch", False):
env.TouchSerialPort("$UPLOAD_PORT", 1200)
if upload_options.get("wait_for_upload_port", False):
env.Replace(UPLOAD_PORT=env.WaitForNewSerialPort(before_ports))
2022-09-26 15:13:52 +03:00
def _get_board_memory_type(env):
2025-07-03 17:12:25 +02:00
"""
Determine the memory type configuration for the board.
2025-07-23 17:46:36 +02:00
Args:
env: SCons environment object
Returns:
str: The appropriate memory type string based on board configuration
2025-07-03 17:12:25 +02:00
"""
2022-09-26 15:13:52 +03:00
board_config = env.BoardConfig()
default_type = "%s_%s" % (
board_config.get("build.flash_mode", "dio"),
board_config.get("build.psram_type", "qspi"),
)
return board_config.get(
"build.memory_type",
board_config.get(
"build.%s.memory_type"
% env.subst("$PIOFRAMEWORK").strip().replace(" ", "_"),
default_type,
),
)
2025-07-03 17:12:25 +02:00
2024-02-15 14:00:27 +01:00
def _normalize_frequency(frequency):
2025-07-03 17:12:25 +02:00
"""
Convert frequency value to normalized string format (e.g., "40m").
2025-07-23 17:46:36 +02:00
Args:
frequency: Frequency value to normalize
Returns:
str: Normalized frequency string with 'm' suffix
2025-07-03 17:12:25 +02:00
"""
2016-10-24 20:23:25 +03:00
frequency = str(frequency).replace("L", "")
return str(int(int(frequency) / 1000000)) + "m"
2024-02-15 14:00:27 +01:00
def _get_board_f_flash(env):
2025-07-23 17:46:36 +02:00
"""
Get the flash frequency for the board.
Args:
env: SCons environment object
Returns:
str: Flash frequency string
"""
2024-02-15 14:00:27 +01:00
frequency = env.subst("$BOARD_F_FLASH")
return _normalize_frequency(frequency)
def _get_board_f_image(env):
2025-07-23 17:46:36 +02:00
"""
Get the image frequency for the board, fallback to flash frequency.
Args:
env: SCons environment object
Returns:
str: Image frequency string
"""
2024-02-15 14:00:27 +01:00
board_config = env.BoardConfig()
if "build.f_image" in board_config:
return _normalize_frequency(board_config.get("build.f_image"))
return _get_board_f_flash(env)
2025-07-03 17:12:25 +02:00
2024-03-18 13:03:31 +01:00
def _get_board_f_boot(env):
2025-07-23 17:46:36 +02:00
"""
Get the boot frequency for the board, fallback to flash frequency.
Args:
env: SCons environment object
Returns:
str: Boot frequency string
"""
2024-03-18 13:03:31 +01:00
board_config = env.BoardConfig()
if "build.f_boot" in board_config:
return _normalize_frequency(board_config.get("build.f_boot"))
return _get_board_f_flash(env)
def _get_board_flash_mode(env):
2025-07-03 17:12:25 +02:00
"""
Determine the appropriate flash mode for the board.
Handles special cases for OPI memory types.
2025-07-23 17:46:36 +02:00
Args:
env: SCons environment object
Returns:
str: Flash mode string
2025-07-03 17:12:25 +02:00
"""
if _get_board_memory_type(env) in ("opi_opi", "opi_qspi"):
return "dout"
2022-09-26 15:13:52 +03:00
mode = env.subst("$BOARD_FLASH_MODE")
2022-09-13 18:31:16 +02:00
if mode in ("qio", "qout"):
return "dio"
return mode
2022-05-16 13:18:16 +03:00
def _get_board_boot_mode(env):
2025-07-03 17:12:25 +02:00
"""
Determine the boot mode for the board.
Handles special cases for OPI memory types.
2025-07-23 17:46:36 +02:00
Args:
env: SCons environment object
Returns:
str: Boot mode string
2025-07-03 17:12:25 +02:00
"""
memory_type = env.BoardConfig().get("build.arduino.memory_type", "")
build_boot = env.BoardConfig().get("build.boot", "$BOARD_FLASH_MODE")
if memory_type in ("opi_opi", "opi_qspi"):
build_boot = "opi"
return build_boot
2022-05-16 13:18:16 +03:00
2018-05-26 01:13:54 +03:00
def _parse_size(value):
2025-07-03 17:12:25 +02:00
"""
Parse size values from various formats (int, hex, K/M suffixes).
2025-07-23 17:46:36 +02:00
Args:
value: Size value to parse
Returns:
int: Size in bytes as an integer
2025-07-03 17:12:25 +02:00
"""
if isinstance(value, int):
return value
elif value.isdigit():
2018-05-26 01:13:54 +03:00
return int(value)
elif value.startswith("0x"):
return int(value, 16)
2018-06-15 21:35:03 +03:00
elif value[-1].upper() in ("K", "M"):
base = 1024 if value[-1].upper() == "K" else 1024 * 1024
2018-05-26 01:13:54 +03:00
return int(value[:-1]) * base
return value
def _parse_partitions(env):
2025-07-03 17:12:25 +02:00
"""
Parse the partition table CSV file and return partition information.
Also sets the application offset for the environment.
2025-07-23 17:46:36 +02:00
Args:
env: SCons environment object
Returns:
list: List of partition dictionaries
2025-07-03 17:12:25 +02:00
"""
2018-05-26 01:13:54 +03:00
partitions_csv = env.subst("$PARTITIONS_TABLE_CSV")
if not isfile(partitions_csv):
2025-07-03 17:12:25 +02:00
sys.stderr.write(
"Could not find the file %s with partitions table.\n"
% partitions_csv
)
2018-05-26 01:13:54 +03:00
env.Exit(1)
2018-05-30 00:25:12 +03:00
return
2018-05-26 01:13:54 +03:00
result = []
next_offset = 0
2025-07-03 17:12:25 +02:00
app_offset = 0x10000 # Default address for firmware
2018-05-26 01:13:54 +03:00
with open(partitions_csv) as fp:
for line in fp.readlines():
line = line.strip()
if not line or line.startswith("#"):
continue
tokens = [t.strip() for t in line.split(",")]
2018-05-26 01:13:54 +03:00
if len(tokens) < 5:
continue
2024-12-16 21:50:40 +01:00
bound = 0x10000 if tokens[1] in ("0", "app") else 4
calculated_offset = (next_offset + bound - 1) & ~(bound - 1)
partition = {
2018-05-26 01:13:54 +03:00
"name": tokens[0],
"type": tokens[1],
"subtype": tokens[2],
2024-12-16 21:50:40 +01:00
"offset": tokens[3] or calculated_offset,
2018-05-26 01:13:54 +03:00
"size": tokens[4],
2025-07-03 17:12:25 +02:00
"flags": tokens[5] if len(tokens) > 5 else None,
}
result.append(partition)
next_offset = _parse_size(partition["offset"])
2025-07-03 17:12:25 +02:00
if partition["subtype"] == "ota_0":
2024-12-16 21:50:40 +01:00
app_offset = next_offset
next_offset = next_offset + _parse_size(partition["size"])
2025-07-03 17:12:25 +02:00
# Configure application partition offset
2024-12-16 21:50:40 +01:00
env.Replace(ESP32_APP_OFFSET=str(hex(app_offset)))
# Propagate application offset to debug configurations
2025-07-03 17:12:25 +02:00
env["INTEGRATION_EXTRA_DATA"].update(
{"application_offset": str(hex(app_offset))}
)
2018-05-26 01:13:54 +03:00
return result
def _update_max_upload_size(env):
2025-07-03 17:12:25 +02:00
"""
Update the maximum upload size based on partition table configuration.
Prioritizes user-specified partition names.
2025-07-23 17:46:36 +02:00
Args:
env: SCons environment object
2025-07-03 17:12:25 +02:00
"""
2018-05-26 01:13:54 +03:00
if not env.get("PARTITIONS_TABLE_CSV"):
return
2025-07-03 17:12:25 +02:00
sizes = {
2025-07-03 17:12:25 +02:00
p["subtype"]: _parse_size(p["size"])
for p in _parse_partitions(env)
2022-05-16 13:18:16 +03:00
if p["type"] in ("0", "app")
}
partitions = {p["name"]: p for p in _parse_partitions(env)}
# User-specified partition name has the highest priority
custom_app_partition_name = board.get("build.app_partition_name", "")
if custom_app_partition_name:
selected_partition = partitions.get(custom_app_partition_name, {})
if selected_partition:
2025-07-03 17:12:25 +02:00
board.update(
"upload.maximum_size", _parse_size(selected_partition["size"])
)
return
else:
print(
2025-07-03 17:12:25 +02:00
"Warning! Selected partition `%s` is not available in the "
"partition table! Default partition will be used!"
% custom_app_partition_name
)
for p in partitions.values():
if p["type"] in ("0", "app") and p["subtype"] in ("ota_0"):
2023-08-02 13:00:27 +03:00
board.update("upload.maximum_size", _parse_size(p["size"]))
break
2018-05-26 01:13:54 +03:00
2018-12-26 02:07:03 +02:00
def _to_unix_slashes(path):
2025-07-23 17:46:36 +02:00
"""
Convert Windows-style backslashes to Unix-style forward slashes.
Args:
path (str): Path to convert
Returns:
str: Path with Unix-style slashes
"""
2022-04-17 23:58:10 +03:00
return path.replace("\\", "/")
2018-12-26 02:07:03 +02:00
2022-04-17 23:58:10 +03:00
def fetch_fs_size(env):
2025-07-03 17:12:25 +02:00
"""
Extract filesystem size and offset information from partition table.
Sets FS_START, FS_SIZE, FS_PAGE, and FS_BLOCK environment variables.
2025-07-23 17:46:36 +02:00
Args:
env: SCons environment object
2025-07-03 17:12:25 +02:00
"""
2022-04-17 23:58:10 +03:00
fs = None
2018-05-26 01:13:54 +03:00
for p in _parse_partitions(env):
2025-07-03 17:12:25 +02:00
if p["type"] == "data" and p["subtype"] in (
"spiffs",
"fat",
"littlefs",
):
2022-04-17 23:58:10 +03:00
fs = p
if not fs:
2018-05-26 01:13:54 +03:00
sys.stderr.write(
2022-04-17 23:58:10 +03:00
"Could not find the any filesystem section in the partitions "
"table %s\n" % env.subst("$PARTITIONS_TABLE_CSV")
)
2018-05-30 00:25:12 +03:00
env.Exit(1)
return
2025-07-03 17:12:25 +02:00
2022-04-17 23:58:10 +03:00
env["FS_START"] = _parse_size(fs["offset"])
env["FS_SIZE"] = _parse_size(fs["size"])
env["FS_PAGE"] = int("0x100", 16)
env["FS_BLOCK"] = int("0x1000", 16)
2022-04-17 23:58:10 +03:00
# FFat specific offsets, see:
# https://github.com/lorol/arduino-esp32fatfs-plugin#notes-for-fatfs
if filesystem == "fatfs":
env["FS_START"] += 4096
env["FS_SIZE"] -= 4096
2022-04-17 23:58:10 +03:00
def __fetch_fs_size(target, source, env):
2025-07-23 17:46:36 +02:00
"""
Wrapper function for fetch_fs_size to be used as SCons emitter.
Args:
target: SCons target
source: SCons source
env: SCons environment object
Returns:
tuple: (target, source) tuple
"""
2022-04-17 23:58:10 +03:00
fetch_fs_size(env)
return (target, source)
2025-07-03 17:12:25 +02:00
def check_lib_archive_exists():
"""
Check if lib_archive is set in platformio.ini configuration.
2025-07-23 17:46:36 +02:00
Returns:
bool: True if found, False otherwise
2025-07-03 17:12:25 +02:00
"""
for section in projectconfig.sections():
if "lib_archive" in projectconfig.options(section):
return True
return False
2025-07-23 17:46:36 +02:00
def switch_off_ldf():
"""
Disables LDF (Library Dependency Finder) for uploadfs, uploadfsota, and buildfs targets.
This optimization prevents unnecessary library dependency scanning and compilation
when only filesystem operations are performed.
"""
fs_targets = {"uploadfs", "uploadfsota", "buildfs", "erase"}
if fs_targets & set(COMMAND_LINE_TARGETS):
# Disable LDF by modifying project configuration directly
env_section = "env:" + env["PIOENV"]
if not projectconfig.has_section(env_section):
projectconfig.add_section(env_section)
projectconfig.set(env_section, "lib_ldf_mode", "off")
2025-07-03 17:12:25 +02:00
# Initialize board configuration and MCU settings
board = env.BoardConfig()
2020-05-11 21:17:48 +03:00
mcu = board.get("build.mcu", "esp32")
2021-06-23 12:32:55 +03:00
toolchain_arch = "xtensa-%s" % mcu
2025-07-03 17:12:25 +02:00
filesystem = board.get("build.filesystem", "littlefs")
# Set toolchain architecture for RISC-V based ESP32 variants
if mcu in ("esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4"):
2021-06-23 12:32:55 +03:00
toolchain_arch = "riscv32-esp"
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Initialize integration extra data if not present
if "INTEGRATION_EXTRA_DATA" not in env:
env["INTEGRATION_EXTRA_DATA"] = {}
2025-07-23 17:46:36 +02:00
# Take care of possible whitespaces in path
objcopy_value = (
f'"{esptool_binary_path}"'
if ' ' in esptool_binary_path
else esptool_binary_path
)
2025-07-03 17:12:25 +02:00
# Configure build tools and environment variables
2016-10-24 20:23:25 +03:00
env.Replace(
2022-05-16 13:18:16 +03:00
__get_board_boot_mode=_get_board_boot_mode,
2016-10-24 20:23:25 +03:00
__get_board_f_flash=_get_board_f_flash,
2024-02-15 14:00:27 +01:00
__get_board_f_image=_get_board_f_image,
2024-03-18 13:03:31 +01:00
__get_board_f_boot=_get_board_f_boot,
__get_board_flash_mode=_get_board_flash_mode,
2022-09-26 15:13:52 +03:00
__get_board_memory_type=_get_board_memory_type,
AR="%s-elf-gcc-ar" % toolchain_arch,
2021-06-23 12:32:55 +03:00
AS="%s-elf-as" % toolchain_arch,
CC="%s-elf-gcc" % toolchain_arch,
CXX="%s-elf-g++" % toolchain_arch,
GDB=join(
platform.get_package_dir(
"tool-riscv32-esp-elf-gdb"
2025-07-03 17:12:25 +02:00
if mcu in (
"esp32c2",
"esp32c3",
"esp32c5",
"esp32c6",
"esp32h2",
"esp32p4",
)
else "tool-xtensa-esp-elf-gdb"
)
2024-10-12 16:53:52 +02:00
or "",
"bin",
"%s-elf-gdb" % toolchain_arch,
),
2025-07-23 17:46:36 +02:00
OBJCOPY=objcopy_value,
RANLIB="%s-elf-gcc-ranlib" % toolchain_arch,
2021-06-23 12:32:55 +03:00
SIZETOOL="%s-elf-size" % toolchain_arch,
ARFLAGS=["rc"],
2025-07-03 17:12:25 +02:00
SIZEPROGREGEXP=r"^(?:\.iram0\.text|\.iram0\.vectors|\.dram0\.data|"
r"\.flash\.text|\.flash\.rodata|)\s+([0-9]+).*",
2018-07-02 15:33:43 +03:00
SIZEDATAREGEXP=r"^(?:\.dram0\.data|\.dram0\.bss|\.noinit)\s+([0-9]+).*",
2018-06-02 15:58:00 +03:00
SIZECHECKCMD="$SIZETOOL -A -d $SOURCES",
SIZEPRINTCMD="$SIZETOOL -B -d $SOURCES",
2025-07-03 17:12:25 +02:00
ERASEFLAGS=["--chip", mcu, "--port", '"$UPLOAD_PORT"'],
2025-07-23 17:46:36 +02:00
ERASECMD='"$OBJCOPY" $ERASEFLAGS erase-flash',
2022-04-17 23:58:10 +03:00
# mkspiffs package contains two different binaries for IDF and Arduino
MKFSTOOL="mk%s" % filesystem
+ (
(
"_${PIOPLATFORM}_"
+ (
"espidf"
if "espidf" in env.subst("$PIOFRAMEWORK")
else "${PIOFRAMEWORK}"
)
)
if filesystem == "spiffs"
else ""
),
2025-07-03 17:12:25 +02:00
# Legacy `ESP32_SPIFFS_IMAGE_NAME` is used as the second fallback value
# for backward compatibility
2025-07-23 17:46:36 +02:00
2022-04-17 23:58:10 +03:00
ESP32_FS_IMAGE_NAME=env.get(
2025-07-03 17:12:25 +02:00
"ESP32_FS_IMAGE_NAME",
env.get("ESP32_SPIFFS_IMAGE_NAME", filesystem),
),
ESP32_APP_OFFSET=env.get("INTEGRATION_EXTRA_DATA").get(
"application_offset"
2022-04-17 23:58:10 +03:00
),
2024-12-16 21:50:40 +01:00
ARDUINO_LIB_COMPILE_FLAG="Inactive",
2025-07-03 17:12:25 +02:00
PROGSUFFIX=".elf",
2018-06-02 15:58:00 +03:00
)
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Check if lib_archive is set in platformio.ini and set it to False
# if not found. This makes weak defs in framework and libs possible.
if not check_lib_archive_exists():
env_section = "env:" + env["PIOENV"]
projectconfig.set(env_section, "lib_archive", "False")
2018-01-03 18:23:00 +02:00
# Allow user to override via pre:script
if env.get("PROGNAME", "program") == "program":
env.Replace(PROGNAME="firmware")
2025-07-03 17:12:25 +02:00
# Configure build actions and builders
2016-10-24 20:23:25 +03:00
env.Append(
BUILDERS=dict(
ElfToBin=Builder(
2025-07-03 17:12:25 +02:00
action=env.VerboseAction(
" ".join(
[
2025-07-23 17:46:36 +02:00
"$OBJCOPY",
2025-07-03 17:12:25 +02:00
"--chip",
mcu,
"elf2image",
"--flash-mode",
"${__get_board_flash_mode(__env__)}",
"--flash-freq",
"${__get_board_f_image(__env__)}",
"--flash-size",
board.get("upload.flash_size", "4MB"),
"-o",
"$TARGET",
"$SOURCES",
]
),
"Building $TARGET",
),
suffix=".bin",
),
DataToBin=Builder(
2022-04-17 23:58:10 +03:00
action=env.VerboseAction(
" ".join(
['"$MKFSTOOL"', "-c", "$SOURCES", "-s", "$FS_SIZE"]
+ (
2025-07-03 17:12:25 +02:00
["-p", "$FS_PAGE", "-b", "$FS_BLOCK"]
if filesystem in ("littlefs", "spiffs")
2022-04-17 23:58:10 +03:00
else []
)
+ ["$TARGET"]
),
"Building FS image from '$SOURCES' directory to $TARGET",
),
emitter=__fetch_fs_size,
source_factory=env.Dir,
2022-04-17 23:58:10 +03:00
suffix=".bin",
),
)
)
2025-07-03 17:12:25 +02:00
# Load framework-specific configuration
if not env.get("PIOFRAMEWORK"):
env.SConscript("frameworks/_bare.py", exports="env")
2017-04-28 11:51:04 +03:00
2025-07-23 17:46:36 +02:00
# Disable LDF for filesystem operations
switch_off_ldf()
2025-07-03 17:12:25 +02:00
def firmware_metrics(target, source, env):
"""
2025-07-23 17:46:36 +02:00
Custom target to run esp-idf-size with support for command line parameters.
2025-07-03 17:12:25 +02:00
Usage: pio run -t metrics -- [esp-idf-size arguments]
2025-07-23 17:46:36 +02:00
Args:
target: SCons target
source: SCons source
env: SCons environment object
2025-07-03 17:12:25 +02:00
"""
if terminal_cp != "utf-8":
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 can be in project dir
map_file = os.path.join(get_project_dir(), env.subst("$PROGNAME") + ".map")
if not os.path.isfile(map_file):
print(f"Error: Map file not found: {map_file}")
print("Make sure the project is built first with 'pio run'")
return
2025-07-23 17:46:36 +02:00
try:
cmd = [PYTHON_EXE, "-m", "esp_idf_size", "--ng"]
2025-07-03 17:12:25 +02:00
# Parameters from platformio.ini
extra_args = env.GetProjectOption("custom_esp_idf_size_args", "")
if extra_args:
cmd.extend(shlex.split(extra_args))
# Command Line Parameter, after --
cli_args = []
if "--" in sys.argv:
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:
cmd.extend(cli_args)
# Map-file as last argument
cmd.append(map_file)
# Debug-Info if wanted
if env.GetProjectOption("custom_esp_idf_size_verbose", False):
print(f"Running command: {' '.join(cmd)}")
2025-07-23 17:46:36 +02:00
# Call esp-idf-size with modified environment
result = subprocess.run(cmd, check=False, capture_output=False, env=os.environ)
2025-07-03 17:12:25 +02:00
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")
2025-07-23 17:46:36 +02:00
2016-10-24 20:23:25 +03:00
#
2022-04-17 23:58:10 +03:00
# Target: Build executable and linkable firmware or FS image
2016-10-24 20:23:25 +03:00
#
target_elf = None
2018-01-03 18:23:00 +02:00
if "nobuild" in COMMAND_LINE_TARGETS:
2019-06-12 23:09:40 +03:00
target_elf = join("$BUILD_DIR", "${PROGNAME}.elf")
if set(["uploadfs", "uploadfsota"]) & set(COMMAND_LINE_TARGETS):
2022-04-17 23:58:10 +03:00
fetch_fs_size(env)
target_firm = join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}.bin")
else:
target_firm = join("$BUILD_DIR", "${PROGNAME}.bin")
2018-01-03 18:23:00 +02:00
else:
target_elf = env.BuildProgram()
2025-07-03 17:12:25 +02:00
silent_action = env.Action(firmware_metrics)
# Hack to silence scons command output
silent_action.strfunction = lambda target, source, env: ""
env.AddPostAction(target_elf, silent_action)
if set(["buildfs", "uploadfs", "uploadfsota"]) & set(COMMAND_LINE_TARGETS):
target_firm = env.DataToBin(
2022-04-17 23:58:10 +03:00
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
)
2020-09-07 12:56:44 +03:00
env.NoCache(target_firm)
AlwaysBuild(target_firm)
else:
2025-07-03 17:12:25 +02:00
target_firm = env.ElfToBin(join("$BUILD_DIR", "${PROGNAME}"), target_elf)
env.Depends(target_firm, "checkprogsize")
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Configure platform targets
env.AddPlatformTarget(
"buildfs", target_firm, target_firm, "Build Filesystem Image"
)
2018-01-03 18:23:00 +02:00
AlwaysBuild(env.Alias("nobuild", target_firm))
2016-11-13 21:42:12 +02:00
target_buildprog = env.Alias("buildprog", target_firm, target_firm)
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Update max upload size based on CSV file
if env.get("PIOMAINPROG"):
env.AddPreAction(
"checkprogsize",
env.VerboseAction(
lambda source, target, env: _update_max_upload_size(env),
2025-07-03 17:12:25 +02:00
"Retrieving maximum program size $SOURCES",
),
)
2018-05-26 01:13:54 +03:00
# Target: Print binary size
2020-06-10 17:16:17 +03:00
target_size = env.AddPlatformTarget(
"size",
target_elf,
env.VerboseAction("$SIZEPRINTCMD", "Calculating size $SOURCE"),
"Program Size",
"Calculate program size",
)
2022-04-17 23:58:10 +03:00
# Target: Upload firmware or FS image
upload_protocol = env.subst("$UPLOAD_PROTOCOL")
debug_tools = board.get("debug.tools", {})
upload_actions = []
2019-05-11 22:13:26 +03:00
# Compatibility with old OTA configurations
2025-07-03 17:12:25 +02:00
if upload_protocol != "espota" and re.match(
r"\"?((([0-9]{1,3}\.){3}[0-9]{1,3})|[^\\/]+\.local)\"?$",
env.get("UPLOAD_PORT", ""),
):
2019-05-11 22:13:26 +03:00
upload_protocol = "espota"
sys.stderr.write(
"Warning! We have just detected `upload_port` as IP address or host "
"name of ESP device. `upload_protocol` is switched to `espota`.\n"
"Please specify `upload_protocol = espota` in `platformio.ini` "
2025-07-03 17:12:25 +02:00
"project configuration file.\n"
)
2019-05-11 22:13:26 +03:00
2025-07-03 17:12:25 +02:00
# Configure upload protocol: ESP OTA
2019-05-11 22:13:26 +03:00
if upload_protocol == "espota":
if not env.subst("$UPLOAD_PORT"):
sys.stderr.write(
"Error: Please specify IP address or host name of ESP device "
"using `upload_port` for build environment or use "
"global `--upload-port` option.\n"
"See https://docs.platformio.org/page/platforms/"
2025-07-03 17:12:25 +02:00
"espressif32.html#over-the-air-ota-update\n"
)
env.Replace(
2025-07-03 17:12:25 +02:00
UPLOADER=join(FRAMEWORK_DIR, "tools", "espota.py"),
2019-05-11 22:13:26 +03:00
UPLOADERFLAGS=["--debug", "--progress", "-i", "$UPLOAD_PORT"],
2025-07-23 17:46:36 +02:00
UPLOADCMD=f'"{PYTHON_EXE}" "$UPLOADER" $UPLOADERFLAGS -f $SOURCE',
2019-05-11 22:13:26 +03:00
)
if set(["uploadfs", "uploadfsota"]) & set(COMMAND_LINE_TARGETS):
env.Append(UPLOADERFLAGS=["--spiffs"])
2019-05-11 22:13:26 +03:00
upload_actions = [env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE")]
2025-07-03 17:12:25 +02:00
# Configure upload protocol: esptool
2019-05-11 22:13:26 +03:00
elif upload_protocol == "esptool":
env.Replace(
2025-07-23 17:46:36 +02:00
UPLOADER=objcopy_value,
UPLOADERFLAGS=[
2025-07-03 17:12:25 +02:00
"--chip",
mcu,
"--port",
'"$UPLOAD_PORT"',
"--baud",
"$UPLOAD_SPEED",
"--before",
board.get("upload.before_reset", "default-reset"),
"--after",
board.get("upload.after_reset", "hard-reset"),
"write-flash",
"-z",
"--flash-mode",
"${__get_board_flash_mode(__env__)}",
"--flash-freq",
"${__get_board_f_image(__env__)}",
"--flash-size",
"detect",
],
2025-07-23 17:46:36 +02:00
UPLOADCMD='$UPLOADER $UPLOADERFLAGS $ESP32_APP_OFFSET $SOURCE'
)
for image in env.get("FLASH_EXTRA_IMAGES", []):
env.Append(UPLOADERFLAGS=[image[0], env.subst(image[1])])
if "uploadfs" in COMMAND_LINE_TARGETS:
env.Replace(
UPLOADERFLAGS=[
2025-07-03 17:12:25 +02:00
"--chip",
mcu,
"--port",
'"$UPLOAD_PORT"',
"--baud",
"$UPLOAD_SPEED",
"--before",
board.get("upload.before_reset", "default-reset"),
"--after",
board.get("upload.after_reset", "hard-reset"),
"write-flash",
"-z",
"--flash-mode",
"${__get_board_flash_mode(__env__)}",
"--flash-freq",
"${__get_board_f_image(__env__)}",
"--flash-size",
"detect",
"$FS_START",
],
2025-07-23 17:46:36 +02:00
UPLOADCMD='"$UPLOADER" $UPLOADERFLAGS $SOURCE',
)
2018-07-02 16:03:11 +03:00
upload_actions = [
2022-04-13 18:49:20 +03:00
env.VerboseAction(BeforeUpload, "Looking for upload port..."),
2025-07-03 17:12:25 +02:00
env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE"),
]
2025-07-03 17:12:25 +02:00
# Configure upload protocol: DFU
2023-08-01 19:38:55 +03:00
elif upload_protocol == "dfu":
hwids = board.get("build.hwids", [["0x2341", "0x0070"]])
vid = hwids[0][0]
pid = hwids[0][1]
upload_actions = [env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE")]
env.Replace(
UPLOADER=join(
platform.get_package_dir("tool-dfuutil-arduino") or "", "dfu-util"
),
UPLOADERFLAGS=[
"-d",
",".join(["%s:%s" % (hwid[0], hwid[1]) for hwid in hwids]),
"-Q",
2025-07-03 17:12:25 +02:00
"-D",
2023-08-01 19:38:55 +03:00
],
UPLOADCMD='"$UPLOADER" $UPLOADERFLAGS "$SOURCE"',
)
2025-07-03 17:12:25 +02:00
# Configure upload protocol: Debug tools (OpenOCD)
elif upload_protocol in debug_tools:
_parse_partitions(env)
openocd_args = ["-d%d" % (2 if int(ARGUMENTS.get("PIOVERBOSE", 0)) else 1)]
openocd_args.extend(
2025-07-03 17:12:25 +02:00
debug_tools.get(upload_protocol).get("server").get("arguments", [])
)
openocd_args.extend(
[
2022-04-17 23:58:10 +03:00
"-c",
2022-09-27 12:30:15 +03:00
"adapter speed %s" % env.GetProjectOption("debug_speed", "5000"),
"-c",
"program_esp {{$SOURCE}} %s verify"
% (
"$FS_START"
if "uploadfs" in COMMAND_LINE_TARGETS
else env.get("INTEGRATION_EXTRA_DATA").get("application_offset")
),
]
)
if "uploadfs" not in COMMAND_LINE_TARGETS:
for image in env.get("FLASH_EXTRA_IMAGES", []):
openocd_args.extend(
[
"-c",
"program_esp {{%s}} %s verify"
% (_to_unix_slashes(image[1]), image[0]),
]
)
openocd_args.extend(["-c", "reset run; shutdown"])
openocd_args = [
f.replace(
"$PACKAGE_DIR",
_to_unix_slashes(
2025-07-03 17:12:25 +02:00
platform.get_package_dir("tool-openocd-esp32") or ""
),
)
for f in openocd_args
]
env.Replace(
UPLOADER="openocd",
2022-04-17 23:58:10 +03:00
UPLOADERFLAGS=openocd_args,
UPLOADCMD="$UPLOADER $UPLOADERFLAGS",
)
upload_actions = [env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE")]
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Configure upload protocol: Custom
elif upload_protocol == "custom":
upload_actions = [env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE")]
2016-10-24 20:23:25 +03:00
else:
sys.stderr.write("Warning! Unknown upload protocol %s\n" % upload_protocol)
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Register upload targets
2020-06-10 17:16:17 +03:00
env.AddPlatformTarget("upload", target_firm, upload_actions, "Upload")
env.AddPlatformTarget(
2025-07-03 17:12:25 +02:00
"uploadfs", target_firm, upload_actions, "Upload Filesystem Image"
)
env.AddPlatformTarget(
"uploadfsota",
target_firm,
upload_actions,
"Upload Filesystem Image OTA",
)
2016-10-24 20:23:25 +03:00
# Target: Erase Flash and Upload
env.AddPlatformTarget(
"erase_upload",
target_firm,
[
env.VerboseAction(BeforeUpload, "Looking for upload port..."),
env.VerboseAction("$ERASECMD", "Erasing..."),
2025-07-03 17:12:25 +02:00
env.VerboseAction("$UPLOADCMD", "Uploading $SOURCE"),
],
"Erase Flash and Upload",
)
2018-11-24 15:29:21 +02:00
# Target: Erase Flash
2020-06-10 17:16:17 +03:00
env.AddPlatformTarget(
"erase",
None,
[
env.VerboseAction(BeforeUpload, "Looking for upload port..."),
2025-07-03 17:12:25 +02:00
env.VerboseAction("$ERASECMD", "Erasing..."),
2020-06-10 17:16:17 +03:00
],
"Erase Flash",
)
2018-11-24 15:29:21 +02:00
2025-07-03 17:12:25 +02:00
# Register Custom Target for firmware metrics
env.AddCustomTarget(
name="metrics",
dependencies="$BUILD_DIR/${PROGNAME}.elf",
actions=firmware_metrics,
title="Firmware Size Metrics",
description="Analyze firmware size using esp-idf-size "
"(supports CLI args after --)",
always_build=True,
)
2025-07-03 17:12:25 +02:00
# Additional Target without Build-Dependency when already compiled
env.AddCustomTarget(
name="metrics-only",
dependencies=None,
actions=firmware_metrics,
title="Firmware Size Metrics (No Build)",
description="Analyze firmware size without building first",
always_build=True,
)
2025-07-03 17:12:25 +02:00
# Override memory inspection behavior
env.SConscript("sizedata.py", exports="env")
2016-10-24 20:23:25 +03:00
2025-07-03 17:12:25 +02:00
# Set default targets
2016-10-24 20:23:25 +03:00
Default([target_buildprog, target_size])