8507dd8e7f
#1006, #1007, resolves #1013 Now IDF Python deps are installed in a pre-сreated virtual environment. The name of the IDF venv contains the IDF version to avoid possible conflicts and unnecessary reinstallation of Python dependencies in cases when Arduino as an IDF component requires a different version of the IDF package and hence a different set of Python deps or their versions
1573 lines
51 KiB
Python
1573 lines
51 KiB
Python
# Copyright 2020-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.
|
|
|
|
"""
|
|
Espressif IDF
|
|
|
|
Espressif IoT Development Framework for ESP32 MCU
|
|
|
|
https://github.com/espressif/esp-idf
|
|
"""
|
|
|
|
import copy
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import pkg_resources
|
|
|
|
import click
|
|
import semantic_version
|
|
|
|
from SCons.Script import (
|
|
ARGUMENTS,
|
|
COMMAND_LINE_TARGETS,
|
|
DefaultEnvironment,
|
|
)
|
|
|
|
from platformio import fs
|
|
from platformio.compat import IS_WINDOWS
|
|
from platformio.proc import exec_command
|
|
from platformio.builder.tools.piolib import ProjectAsLibBuilder
|
|
from platformio.package.version import get_original_version, pepver_to_semver
|
|
|
|
# Added to avoid conflicts between installed Python packages from
|
|
# the IDF virtual environment and PlatformIO Core
|
|
# Note: This workaround can be safely deleted when PlatformIO 6.1.7 is released
|
|
if os.environ.get("PYTHONPATH"):
|
|
del os.environ["PYTHONPATH"]
|
|
|
|
env = DefaultEnvironment()
|
|
env.SConscript("_embed_files.py", exports="env")
|
|
|
|
platform = env.PioPlatform()
|
|
board = env.BoardConfig()
|
|
mcu = board.get("build.mcu", "esp32")
|
|
idf_variant = mcu.lower()
|
|
|
|
# Required until Arduino switches to v5
|
|
IDF5 = platform.get_package_version("framework-espidf").split(".")[1].startswith("5")
|
|
FRAMEWORK_DIR = platform.get_package_dir("framework-espidf")
|
|
TOOLCHAIN_DIR = platform.get_package_dir(
|
|
"toolchain-%s" % ("riscv32-esp" if mcu == "esp32c3" else ("xtensa-%s" % mcu))
|
|
)
|
|
|
|
|
|
assert os.path.isdir(FRAMEWORK_DIR)
|
|
assert os.path.isdir(TOOLCHAIN_DIR)
|
|
|
|
# Arduino framework as a component is not compatible with ESP-IDF >=4.1
|
|
if "arduino" in env.subst("$PIOFRAMEWORK"):
|
|
ARDUINO_FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32")
|
|
# Possible package names in 'package@version' format is not compatible with CMake
|
|
if "@" in os.path.basename(ARDUINO_FRAMEWORK_DIR):
|
|
new_path = os.path.join(
|
|
os.path.dirname(ARDUINO_FRAMEWORK_DIR),
|
|
os.path.basename(ARDUINO_FRAMEWORK_DIR).replace("@", "-"),
|
|
)
|
|
os.rename(ARDUINO_FRAMEWORK_DIR, new_path)
|
|
ARDUINO_FRAMEWORK_DIR = new_path
|
|
assert ARDUINO_FRAMEWORK_DIR and os.path.isdir(ARDUINO_FRAMEWORK_DIR)
|
|
|
|
BUILD_DIR = env.subst("$BUILD_DIR")
|
|
PROJECT_DIR = env.subst("$PROJECT_DIR")
|
|
PROJECT_SRC_DIR = env.subst("$PROJECT_SRC_DIR")
|
|
CMAKE_API_REPLY_PATH = os.path.join(".cmake", "api", "v1", "reply")
|
|
SDKCONFIG_PATH = board.get(
|
|
"build.esp-idf.sdkconfig_path",
|
|
os.path.join(PROJECT_DIR, "sdkconfig.%s" % env.subst("$PIOENV")),
|
|
)
|
|
|
|
|
|
def get_project_lib_includes(env):
|
|
project = ProjectAsLibBuilder(env, "$PROJECT_DIR")
|
|
project.install_dependencies()
|
|
project.search_deps_recursive()
|
|
|
|
paths = []
|
|
for lb in env.GetLibBuilders():
|
|
if not lb.dependent:
|
|
continue
|
|
lb.env.PrependUnique(CPPPATH=lb.get_include_dirs())
|
|
paths.extend(lb.env["CPPPATH"])
|
|
|
|
DefaultEnvironment().Replace(__PIO_LIB_BUILDERS=None)
|
|
|
|
return paths
|
|
|
|
|
|
def is_cmake_reconfigure_required(cmake_api_reply_dir):
|
|
cmake_cache_file = os.path.join(BUILD_DIR, "CMakeCache.txt")
|
|
cmake_txt_files = [
|
|
os.path.join(PROJECT_DIR, "CMakeLists.txt"),
|
|
os.path.join(PROJECT_SRC_DIR, "CMakeLists.txt"),
|
|
]
|
|
cmake_preconf_dir = os.path.join(BUILD_DIR, "config")
|
|
deafult_sdk_config = os.path.join(PROJECT_DIR, "sdkconfig.defaults")
|
|
|
|
for d in (cmake_api_reply_dir, cmake_preconf_dir):
|
|
if not os.path.isdir(d) or not os.listdir(d):
|
|
return True
|
|
if not os.path.isfile(cmake_cache_file):
|
|
return True
|
|
if not os.path.isfile(os.path.join(BUILD_DIR, "build.ninja")):
|
|
return True
|
|
if not os.path.isfile(SDKCONFIG_PATH) or os.path.getmtime(
|
|
SDKCONFIG_PATH
|
|
) > os.path.getmtime(cmake_cache_file):
|
|
return True
|
|
if os.path.isfile(deafult_sdk_config) and os.path.getmtime(
|
|
deafult_sdk_config
|
|
) > os.path.getmtime(cmake_cache_file):
|
|
return True
|
|
if any(
|
|
os.path.getmtime(f) > os.path.getmtime(cmake_cache_file)
|
|
for f in cmake_txt_files + [cmake_preconf_dir, FRAMEWORK_DIR]
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_proper_idf_project():
|
|
return all(
|
|
os.path.isfile(path)
|
|
for path in (
|
|
os.path.join(PROJECT_DIR, "CMakeLists.txt"),
|
|
os.path.join(PROJECT_SRC_DIR, "CMakeLists.txt"),
|
|
)
|
|
)
|
|
|
|
|
|
def collect_src_files():
|
|
return [
|
|
f
|
|
for f in env.MatchSourceFiles("$PROJECT_SRC_DIR", env.get("SRC_FILTER"))
|
|
if not f.endswith((".h", ".hpp"))
|
|
]
|
|
|
|
|
|
def normalize_path(path):
|
|
if PROJECT_DIR in path:
|
|
path = path.replace(PROJECT_DIR, "${CMAKE_SOURCE_DIR}")
|
|
return fs.to_unix_path(path)
|
|
|
|
|
|
def create_default_project_files():
|
|
root_cmake_tpl = """cmake_minimum_required(VERSION 3.16.0)
|
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
|
project(%s)
|
|
"""
|
|
prj_cmake_tpl = """# This file was automatically generated for projects
|
|
# without default 'CMakeLists.txt' file.
|
|
|
|
FILE(GLOB_RECURSE app_sources %s/*.*)
|
|
|
|
idf_component_register(SRCS ${app_sources})
|
|
"""
|
|
|
|
if not os.listdir(PROJECT_SRC_DIR):
|
|
# create a default main file to make CMake happy during first init
|
|
with open(os.path.join(PROJECT_SRC_DIR, "main.c"), "w") as fp:
|
|
fp.write("void app_main() {}")
|
|
|
|
project_dir = PROJECT_DIR
|
|
if not os.path.isfile(os.path.join(project_dir, "CMakeLists.txt")):
|
|
with open(os.path.join(project_dir, "CMakeLists.txt"), "w") as fp:
|
|
fp.write(root_cmake_tpl % os.path.basename(project_dir))
|
|
|
|
project_src_dir = PROJECT_SRC_DIR
|
|
if not os.path.isfile(os.path.join(project_src_dir, "CMakeLists.txt")):
|
|
with open(os.path.join(project_src_dir, "CMakeLists.txt"), "w") as fp:
|
|
fp.write(prj_cmake_tpl % normalize_path(PROJECT_SRC_DIR))
|
|
|
|
|
|
def get_cmake_code_model(src_dir, build_dir, extra_args=None):
|
|
cmake_api_dir = os.path.join(build_dir, ".cmake", "api", "v1")
|
|
cmake_api_query_dir = os.path.join(cmake_api_dir, "query")
|
|
cmake_api_reply_dir = os.path.join(cmake_api_dir, "reply")
|
|
query_file = os.path.join(cmake_api_query_dir, "codemodel-v2")
|
|
|
|
if not os.path.isfile(query_file):
|
|
os.makedirs(os.path.dirname(query_file))
|
|
open(query_file, "a").close() # create an empty file
|
|
|
|
if not is_proper_idf_project():
|
|
create_default_project_files()
|
|
|
|
if is_cmake_reconfigure_required(cmake_api_reply_dir):
|
|
run_cmake(src_dir, build_dir, extra_args)
|
|
|
|
if not os.path.isdir(cmake_api_reply_dir) or not os.listdir(cmake_api_reply_dir):
|
|
sys.stderr.write("Error: Couldn't find CMake API response file\n")
|
|
env.Exit(1)
|
|
|
|
codemodel = {}
|
|
for target in os.listdir(cmake_api_reply_dir):
|
|
if target.startswith("codemodel-v2"):
|
|
with open(os.path.join(cmake_api_reply_dir, target), "r") as fp:
|
|
codemodel = json.load(fp)
|
|
|
|
assert codemodel["version"]["major"] == 2
|
|
return codemodel
|
|
|
|
|
|
def populate_idf_env_vars(idf_env):
|
|
idf_env["IDF_PATH"] = fs.to_unix_path(FRAMEWORK_DIR)
|
|
additional_packages = [
|
|
os.path.join(TOOLCHAIN_DIR, "bin"),
|
|
platform.get_package_dir("tool-ninja"),
|
|
os.path.join(platform.get_package_dir("tool-cmake"), "bin"),
|
|
os.path.dirname(get_python_exe()),
|
|
]
|
|
|
|
if mcu != "esp32c3":
|
|
additional_packages.append(
|
|
os.path.join(platform.get_package_dir("toolchain-esp32ulp"), "bin"),
|
|
)
|
|
|
|
if IS_WINDOWS:
|
|
additional_packages.append(platform.get_package_dir("tool-mconf"))
|
|
|
|
idf_env["PATH"] = os.pathsep.join(additional_packages + [idf_env["PATH"]])
|
|
|
|
# Some users reported that the `IDF_TOOLS_PATH` var can seep into the
|
|
# underlying build system. Unsetting it is a safe workaround.
|
|
if "IDF_TOOLS_PATH" in idf_env:
|
|
del idf_env["IDF_TOOLS_PATH"]
|
|
|
|
|
|
def get_target_config(project_configs, target_index, cmake_api_reply_dir):
|
|
target_json = project_configs.get("targets")[target_index].get("jsonFile", "")
|
|
target_config_file = os.path.join(cmake_api_reply_dir, target_json)
|
|
if not os.path.isfile(target_config_file):
|
|
sys.stderr.write("Error: Couldn't find target config %s\n" % target_json)
|
|
env.Exit(1)
|
|
|
|
with open(target_config_file) as fp:
|
|
return json.load(fp)
|
|
|
|
|
|
def load_target_configurations(cmake_codemodel, cmake_api_reply_dir):
|
|
configs = {}
|
|
project_configs = cmake_codemodel.get("configurations")[0]
|
|
for config in project_configs.get("projects", []):
|
|
for target_index in config.get("targetIndexes", []):
|
|
target_config = get_target_config(
|
|
project_configs, target_index, cmake_api_reply_dir
|
|
)
|
|
configs[target_config["name"]] = target_config
|
|
|
|
return configs
|
|
|
|
|
|
def build_library(
|
|
default_env, lib_config, project_src_dir, prepend_dir=None, debug_allowed=True
|
|
):
|
|
lib_name = lib_config["nameOnDisk"]
|
|
lib_path = lib_config["paths"]["build"]
|
|
if prepend_dir:
|
|
lib_path = os.path.join(prepend_dir, lib_path)
|
|
lib_objects = compile_source_files(
|
|
lib_config, default_env, project_src_dir, prepend_dir, debug_allowed
|
|
)
|
|
return default_env.Library(
|
|
target=os.path.join("$BUILD_DIR", lib_path, lib_name), source=lib_objects
|
|
)
|
|
|
|
|
|
def get_app_includes(app_config):
|
|
plain_includes = []
|
|
sys_includes = []
|
|
cg = app_config["compileGroups"][0]
|
|
for inc in cg.get("includes", []):
|
|
inc_path = inc["path"]
|
|
if inc.get("isSystem", False):
|
|
sys_includes.append(inc_path)
|
|
else:
|
|
plain_includes.append(inc_path)
|
|
|
|
return {"plain_includes": plain_includes, "sys_includes": sys_includes}
|
|
|
|
|
|
def extract_defines(compile_group):
|
|
result = []
|
|
result.extend(
|
|
[
|
|
d.get("define").replace('"', '\\"').strip()
|
|
for d in compile_group.get("defines", [])
|
|
]
|
|
)
|
|
for f in compile_group.get("compileCommandFragments", []):
|
|
if f.get("fragment", "").startswith("-D"):
|
|
result.append(f["fragment"][2:])
|
|
return result
|
|
|
|
|
|
def get_app_defines(app_config):
|
|
return extract_defines(app_config["compileGroups"][0])
|
|
|
|
|
|
def extract_link_args(target_config):
|
|
def _add_to_libpath(lib_path, link_args):
|
|
if lib_path not in link_args["LIBPATH"]:
|
|
link_args["LIBPATH"].append(lib_path)
|
|
|
|
def _add_archive(archive_path, link_args):
|
|
archive_name = os.path.basename(archive_path)
|
|
if archive_name not in link_args["LIBS"]:
|
|
_add_to_libpath(os.path.dirname(archive_path), link_args)
|
|
link_args["LIBS"].append(archive_name)
|
|
|
|
link_args = {"LINKFLAGS": [], "LIBS": [], "LIBPATH": [], "__LIB_DEPS": []}
|
|
|
|
for f in target_config.get("link", {}).get("commandFragments", []):
|
|
fragment = f.get("fragment", "").strip()
|
|
fragment_role = f.get("role", "").strip()
|
|
if not fragment or not fragment_role:
|
|
continue
|
|
args = click.parser.split_arg_string(fragment)
|
|
if fragment_role == "flags":
|
|
link_args["LINKFLAGS"].extend(args)
|
|
elif fragment_role == "libraries":
|
|
if fragment.startswith("-l"):
|
|
link_args["LIBS"].extend(args)
|
|
elif fragment.startswith("-L"):
|
|
lib_path = fragment.replace("-L", "").strip(" '\"")
|
|
_add_to_libpath(lib_path, link_args)
|
|
elif fragment.startswith("-") and not fragment.startswith("-l"):
|
|
# CMake mistakenly marks LINKFLAGS as libraries
|
|
link_args["LINKFLAGS"].extend(args)
|
|
elif fragment.endswith(".a"):
|
|
archive_path = fragment
|
|
# process static archives
|
|
if archive_path.startswith(FRAMEWORK_DIR):
|
|
# In case of precompiled archives from framework package
|
|
_add_archive(archive_path, link_args)
|
|
else:
|
|
# In case of archives within project
|
|
if archive_path.startswith(".."):
|
|
# Precompiled archives from project component
|
|
_add_archive(
|
|
os.path.normpath(os.path.join(BUILD_DIR, archive_path)),
|
|
link_args,
|
|
)
|
|
else:
|
|
# Internally built libraries used for dependency resolution
|
|
link_args["__LIB_DEPS"].append(os.path.basename(archive_path))
|
|
|
|
return link_args
|
|
|
|
|
|
def filter_args(args, allowed, ignore=None):
|
|
if not allowed:
|
|
return []
|
|
|
|
ignore = ignore or []
|
|
result = []
|
|
i = 0
|
|
length = len(args)
|
|
while i < length:
|
|
if any(args[i].startswith(f) for f in allowed) and not any(
|
|
args[i].startswith(f) for f in ignore
|
|
):
|
|
result.append(args[i])
|
|
if i + 1 < length and not args[i + 1].startswith("-"):
|
|
i += 1
|
|
result.append(args[i])
|
|
i += 1
|
|
return result
|
|
|
|
|
|
def get_app_flags(app_config, default_config):
|
|
def _extract_flags(config):
|
|
flags = {}
|
|
for cg in config["compileGroups"]:
|
|
flags[cg["language"]] = []
|
|
for ccfragment in cg["compileCommandFragments"]:
|
|
fragment = ccfragment.get("fragment", "")
|
|
if not fragment.strip() or fragment.startswith("-D"):
|
|
continue
|
|
flags[cg["language"]].extend(
|
|
click.parser.split_arg_string(fragment.strip())
|
|
)
|
|
|
|
return flags
|
|
|
|
app_flags = _extract_flags(app_config)
|
|
default_flags = _extract_flags(default_config)
|
|
|
|
# Flags are sorted because CMake randomly populates build flags in code model
|
|
return {
|
|
"ASFLAGS": sorted(app_flags.get("ASM", default_flags.get("ASM"))),
|
|
"CFLAGS": sorted(app_flags.get("C", default_flags.get("C"))),
|
|
"CXXFLAGS": sorted(app_flags.get("CXX", default_flags.get("CXX"))),
|
|
}
|
|
|
|
|
|
def get_sdk_configuration():
|
|
config_path = os.path.join(BUILD_DIR, "config", "sdkconfig.json")
|
|
if not os.path.isfile(config_path):
|
|
print('Warning: Could not find "sdkconfig.json" file\n')
|
|
|
|
try:
|
|
with open(config_path, "r") as fp:
|
|
return json.load(fp)
|
|
except:
|
|
return {}
|
|
|
|
|
|
def load_component_paths(framework_components_dir, ignored_component_prefixes=None):
|
|
def _scan_components_from_framework():
|
|
result = []
|
|
for component in os.listdir(framework_components_dir):
|
|
component_path = os.path.join(framework_components_dir, component)
|
|
if component.startswith(ignored_component_prefixes) or not os.path.isdir(
|
|
component_path
|
|
):
|
|
continue
|
|
result.append(component_path)
|
|
|
|
return result
|
|
|
|
# First of all, try to load the list of used components from the project description
|
|
components = []
|
|
ignored_component_prefixes = ignored_component_prefixes or []
|
|
project_description_file = os.path.join(BUILD_DIR, "project_description.json")
|
|
if os.path.isfile(project_description_file):
|
|
with open(project_description_file) as fp:
|
|
try:
|
|
data = json.load(fp)
|
|
for path in data.get("build_component_paths", []):
|
|
if not os.path.basename(path).startswith(
|
|
ignored_component_prefixes
|
|
):
|
|
components.append(path)
|
|
except:
|
|
print(
|
|
"Warning: Could not find load components from project description!\n"
|
|
)
|
|
|
|
return components or _scan_components_from_framework()
|
|
|
|
|
|
def extract_linker_script_fragments(framework_components_dir, sdk_config):
|
|
# Hardware-specific components are excluded from search and added manually below
|
|
project_components = load_component_paths(
|
|
framework_components_dir, ignored_component_prefixes=("esp32", "riscv")
|
|
)
|
|
|
|
result = []
|
|
for component_path in project_components:
|
|
linker_fragment = os.path.join(component_path, "linker.lf")
|
|
if os.path.isfile(linker_fragment):
|
|
result.append(linker_fragment)
|
|
|
|
if not result:
|
|
sys.stderr.write("Error: Failed to extract paths to linker script fragments\n")
|
|
env.Exit(1)
|
|
|
|
if mcu == "esp32c3":
|
|
result.append(os.path.join(framework_components_dir, "riscv", "linker.lf"))
|
|
|
|
# Add extra linker fragments
|
|
for fragment in (
|
|
os.path.join("esp_system", "app.lf"),
|
|
os.path.join("esp_common", "common.lf"),
|
|
os.path.join("esp_common", "soc.lf"),
|
|
os.path.join("newlib", "system_libs.lf"),
|
|
os.path.join("newlib", "newlib.lf"),
|
|
):
|
|
result.append(os.path.join(framework_components_dir, fragment))
|
|
|
|
if sdk_config.get("SPIRAM_CACHE_WORKAROUND", False):
|
|
result.append(
|
|
os.path.join(
|
|
framework_components_dir, "newlib", "esp32-spiram-rom-functions-c.lf"
|
|
)
|
|
)
|
|
|
|
if board.get("build.esp-idf.extra_lf_files", ""):
|
|
result.extend(
|
|
[
|
|
lf if os.path.isabs(lf) else os.path.join(PROJECT_DIR, lf)
|
|
for lf in board.get("build.esp-idf.extra_lf_files").splitlines()
|
|
if lf.strip()
|
|
]
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def create_custom_libraries_list(ldgen_libraries_file, ignore_targets):
|
|
if not os.path.isfile(ldgen_libraries_file):
|
|
sys.stderr.write("Error: Couldn't find the list of framework libraries\n")
|
|
env.Exit(1)
|
|
|
|
pio_libraries_file = ldgen_libraries_file + "_pio"
|
|
|
|
if os.path.isfile(pio_libraries_file):
|
|
return pio_libraries_file
|
|
|
|
lib_paths = []
|
|
with open(ldgen_libraries_file, "r") as fp:
|
|
lib_paths = fp.readlines()
|
|
|
|
with open(pio_libraries_file, "w") as fp:
|
|
for lib_path in lib_paths:
|
|
if all(
|
|
"lib%s.a" % t.replace("__idf_", "") not in lib_path
|
|
for t in ignore_targets
|
|
):
|
|
fp.write(lib_path)
|
|
|
|
return pio_libraries_file
|
|
|
|
|
|
def generate_project_ld_script(sdk_config, ignore_targets=None):
|
|
ignore_targets = ignore_targets or []
|
|
linker_script_fragments = extract_linker_script_fragments(
|
|
os.path.join(FRAMEWORK_DIR, "components"), sdk_config
|
|
)
|
|
|
|
# Create a new file to avoid automatically generated library entry as files from
|
|
# this library are built internally by PlatformIO
|
|
libraries_list = create_custom_libraries_list(
|
|
os.path.join(BUILD_DIR, "ldgen_libraries"), ignore_targets
|
|
)
|
|
|
|
args = {
|
|
"script": os.path.join(FRAMEWORK_DIR, "tools", "ldgen", "ldgen.py"),
|
|
"config": SDKCONFIG_PATH,
|
|
"fragments": " ".join(
|
|
['"%s"' % fs.to_unix_path(f) for f in linker_script_fragments]
|
|
),
|
|
"kconfig": os.path.join(FRAMEWORK_DIR, "Kconfig"),
|
|
"env_file": os.path.join("$BUILD_DIR", "config.env"),
|
|
"libraries_list": libraries_list,
|
|
"objdump": os.path.join(
|
|
TOOLCHAIN_DIR,
|
|
"bin",
|
|
env.subst("$CC").replace("-gcc", "-objdump"),
|
|
),
|
|
}
|
|
|
|
cmd = (
|
|
'"$ESPIDF_PYTHONEXE" "{script}" --input $SOURCE '
|
|
'--config "{config}" --fragments {fragments} --output $TARGET '
|
|
'--kconfig "{kconfig}" --env-file "{env_file}" '
|
|
'--libraries-file "{libraries_list}" '
|
|
'--objdump "{objdump}"'
|
|
).format(**args)
|
|
|
|
return env.Command(
|
|
os.path.join("$BUILD_DIR", "sections.ld"),
|
|
os.path.join(
|
|
FRAMEWORK_DIR,
|
|
"components",
|
|
"esp_system",
|
|
"ld",
|
|
idf_variant,
|
|
"sections.ld.in",
|
|
),
|
|
env.VerboseAction(cmd, "Generating project linker script $TARGET"),
|
|
)
|
|
|
|
|
|
# A temporary workaround to avoid modifying CMake mainly for the "heap" library.
|
|
# The "tlsf.c" source file in this library has an include flag relative
|
|
# to CMAKE_CURRENT_SOURCE_DIR which breaks PlatformIO builds that have a
|
|
# different working directory
|
|
def _fix_component_relative_include(config, build_flags, source_index):
|
|
source_file_path = config["sources"][source_index]["path"]
|
|
build_flags = build_flags.replace("..", os.path.dirname(source_file_path) + "/..")
|
|
return build_flags
|
|
|
|
|
|
def prepare_build_envs(config, default_env, debug_allowed=True):
|
|
build_envs = []
|
|
target_compile_groups = config.get("compileGroups")
|
|
|
|
is_build_type_debug = "debug" in env.GetBuildType() and debug_allowed
|
|
for cg in target_compile_groups:
|
|
includes = []
|
|
sys_includes = []
|
|
for inc in cg.get("includes", []):
|
|
inc_path = inc["path"]
|
|
if inc.get("isSystem", False):
|
|
sys_includes.append(inc_path)
|
|
else:
|
|
includes.append(inc_path)
|
|
|
|
defines = extract_defines(cg)
|
|
compile_commands = cg.get("compileCommandFragments", [])
|
|
build_env = default_env.Clone()
|
|
for cc in compile_commands:
|
|
build_flags = cc.get("fragment")
|
|
if not build_flags.startswith("-D"):
|
|
if build_flags.startswith("-include") and ".." in build_flags:
|
|
source_index = cg.get("sourceIndexes")[0]
|
|
build_flags = _fix_component_relative_include(
|
|
config, build_flags, source_index)
|
|
build_env.AppendUnique(**build_env.ParseFlags(build_flags))
|
|
build_env.AppendUnique(CPPDEFINES=defines, CPPPATH=includes)
|
|
if sys_includes:
|
|
build_env.Append(CCFLAGS=[("-isystem", inc) for inc in sys_includes])
|
|
build_env.ProcessUnFlags(default_env.get("BUILD_UNFLAGS"))
|
|
if is_build_type_debug:
|
|
build_env.ConfigureDebugFlags()
|
|
build_envs.append(build_env)
|
|
|
|
return build_envs
|
|
|
|
|
|
def compile_source_files(
|
|
config, default_env, project_src_dir, prepend_dir=None, debug_allowed=True
|
|
):
|
|
build_envs = prepare_build_envs(config, default_env, debug_allowed)
|
|
objects = []
|
|
components_dir = fs.to_unix_path(os.path.join(FRAMEWORK_DIR, "components"))
|
|
for source in config.get("sources", []):
|
|
if source["path"].endswith(".rule"):
|
|
continue
|
|
compile_group_idx = source.get("compileGroupIndex")
|
|
if compile_group_idx is not None:
|
|
src_dir = config["paths"]["source"]
|
|
if not os.path.isabs(src_dir):
|
|
src_dir = os.path.join(project_src_dir, config["paths"]["source"])
|
|
src_path = source.get("path")
|
|
if not os.path.isabs(src_path):
|
|
# For cases when sources are located near CMakeLists.txt
|
|
src_path = os.path.join(project_src_dir, src_path)
|
|
|
|
obj_path = os.path.join("$BUILD_DIR", prepend_dir or "")
|
|
if src_path.startswith(components_dir):
|
|
obj_path = os.path.join(
|
|
obj_path, os.path.relpath(src_path, components_dir)
|
|
)
|
|
else:
|
|
if not os.path.isabs(source["path"]):
|
|
obj_path = os.path.join(obj_path, source["path"])
|
|
else:
|
|
obj_path = os.path.join(obj_path, os.path.basename(src_path))
|
|
|
|
preserve_source_file_extension = board.get(
|
|
"build.esp-idf.preserve_source_file_extension", False
|
|
)
|
|
|
|
objects.append(
|
|
build_envs[compile_group_idx].StaticObject(
|
|
target=(
|
|
obj_path
|
|
if preserve_source_file_extension
|
|
else os.path.splitext(obj_path)[0]
|
|
) + ".o",
|
|
source=os.path.realpath(src_path),
|
|
)
|
|
)
|
|
|
|
return objects
|
|
|
|
|
|
def run_tool(cmd):
|
|
idf_env = os.environ.copy()
|
|
populate_idf_env_vars(idf_env)
|
|
|
|
result = exec_command(cmd, env=idf_env)
|
|
if result["returncode"] != 0:
|
|
sys.stderr.write(result["out"] + "\n")
|
|
sys.stderr.write(result["err"] + "\n")
|
|
env.Exit(1)
|
|
|
|
if int(ARGUMENTS.get("PIOVERBOSE", 0)):
|
|
print(result["out"])
|
|
print(result["err"])
|
|
|
|
|
|
def RunMenuconfig(target, source, env):
|
|
idf_env = os.environ.copy()
|
|
populate_idf_env_vars(idf_env)
|
|
|
|
rc = subprocess.call(
|
|
[
|
|
os.path.join(platform.get_package_dir("tool-cmake"), "bin", "cmake"),
|
|
"--build",
|
|
BUILD_DIR,
|
|
"--target",
|
|
"menuconfig",
|
|
],
|
|
env=idf_env,
|
|
)
|
|
|
|
if rc != 0:
|
|
sys.stderr.write("Error: Couldn't execute 'menuconfig' target.\n")
|
|
env.Exit(1)
|
|
|
|
|
|
def run_cmake(src_dir, build_dir, extra_args=None):
|
|
cmd = [
|
|
os.path.join(platform.get_package_dir("tool-cmake") or "", "bin", "cmake"),
|
|
"-S",
|
|
src_dir,
|
|
"-B",
|
|
build_dir,
|
|
"-G",
|
|
"Ninja",
|
|
]
|
|
|
|
if extra_args:
|
|
cmd.extend(extra_args)
|
|
|
|
run_tool(cmd)
|
|
|
|
|
|
def find_lib_deps(components_map, elf_config, link_args, ignore_components=None):
|
|
ignore_components = ignore_components or []
|
|
result = [
|
|
components_map[d["id"]]["lib"]
|
|
for d in elf_config.get("dependencies", [])
|
|
if components_map.get(d["id"], {})
|
|
and not d["id"].startswith(tuple(ignore_components))
|
|
]
|
|
|
|
implicit_lib_deps = link_args.get("__LIB_DEPS", [])
|
|
for component in components_map.values():
|
|
component_config = component["config"]
|
|
if (
|
|
component_config["type"] not in ("STATIC_LIBRARY", "OBJECT_LIBRARY")
|
|
or component_config["name"] in ignore_components
|
|
):
|
|
continue
|
|
if (
|
|
component_config["nameOnDisk"] in implicit_lib_deps
|
|
and component["lib"] not in result
|
|
):
|
|
result.append(component["lib"])
|
|
|
|
return result
|
|
|
|
|
|
def build_bootloader(sdk_config):
|
|
bootloader_src_dir = os.path.join(
|
|
FRAMEWORK_DIR, "components", "bootloader", "subproject"
|
|
)
|
|
code_model = get_cmake_code_model(
|
|
bootloader_src_dir,
|
|
os.path.join(BUILD_DIR, "bootloader"),
|
|
[
|
|
"-DIDF_TARGET=" + idf_variant,
|
|
"-DPYTHON_DEPS_CHECKED=1",
|
|
"-DPYTHON=" + get_python_exe(),
|
|
"-DIDF_PATH=" + FRAMEWORK_DIR,
|
|
"-DSDKCONFIG=" + SDKCONFIG_PATH,
|
|
"-DLEGACY_INCLUDE_COMMON_HEADERS=",
|
|
"-DEXTRA_COMPONENT_DIRS="
|
|
+ os.path.join(FRAMEWORK_DIR, "components", "bootloader"),
|
|
],
|
|
)
|
|
|
|
if not code_model:
|
|
sys.stderr.write("Error: Couldn't find code model for bootloader\n")
|
|
env.Exit(1)
|
|
|
|
target_configs = load_target_configurations(
|
|
code_model,
|
|
os.path.join(BUILD_DIR, "bootloader", ".cmake", "api", "v1", "reply"),
|
|
)
|
|
|
|
elf_config = get_project_elf(target_configs)
|
|
if not elf_config:
|
|
sys.stderr.write(
|
|
"Error: Couldn't load the main firmware target of the project\n"
|
|
)
|
|
env.Exit(1)
|
|
|
|
bootloader_env = env.Clone()
|
|
components_map = get_components_map(
|
|
target_configs, ["STATIC_LIBRARY", "OBJECT_LIBRARY"]
|
|
)
|
|
|
|
# Note: By default the size of bootloader is limited to 0x2000 bytes,
|
|
# in debug mode the footprint size can be easily grow beyond this limit
|
|
build_components(
|
|
bootloader_env,
|
|
components_map,
|
|
bootloader_src_dir,
|
|
"bootloader",
|
|
debug_allowed=sdk_config.get("BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG", False),
|
|
)
|
|
link_args = extract_link_args(elf_config)
|
|
extra_flags = filter_args(link_args["LINKFLAGS"], ["-T", "-u"])
|
|
link_args["LINKFLAGS"] = sorted(
|
|
list(set(link_args["LINKFLAGS"]) - set(extra_flags))
|
|
)
|
|
|
|
bootloader_env.MergeFlags(link_args)
|
|
bootloader_env.Append(LINKFLAGS=extra_flags)
|
|
bootloader_libs = find_lib_deps(components_map, elf_config, link_args)
|
|
|
|
bootloader_env.Prepend(__RPATH="-Wl,--start-group ")
|
|
bootloader_env.Append(
|
|
CPPDEFINES=["__BOOTLOADER_BUILD"], _LIBDIRFLAGS=" -Wl,--end-group"
|
|
)
|
|
|
|
return bootloader_env.ElfToBin(
|
|
os.path.join("$BUILD_DIR", "bootloader"),
|
|
bootloader_env.Program(
|
|
os.path.join("$BUILD_DIR", "bootloader.elf"), bootloader_libs
|
|
),
|
|
)
|
|
|
|
|
|
def get_targets_by_type(target_configs, target_types, ignore_targets=None):
|
|
ignore_targets = ignore_targets or []
|
|
result = []
|
|
for target_config in target_configs.values():
|
|
if (
|
|
target_config["type"] in target_types
|
|
and target_config["name"] not in ignore_targets
|
|
):
|
|
result.append(target_config)
|
|
|
|
return result
|
|
|
|
|
|
def get_components_map(target_configs, target_types, ignore_components=None):
|
|
result = {}
|
|
for config in get_targets_by_type(target_configs, target_types, ignore_components):
|
|
if "nameOnDisk" not in config:
|
|
config["nameOnDisk"] = "lib%s.a" % config["name"]
|
|
result[config["id"]] = {"config": config}
|
|
|
|
return result
|
|
|
|
|
|
def build_components(
|
|
env, components_map, project_src_dir, prepend_dir=None, debug_allowed=True
|
|
):
|
|
for k, v in components_map.items():
|
|
components_map[k]["lib"] = build_library(
|
|
env, v["config"], project_src_dir, prepend_dir, debug_allowed
|
|
)
|
|
|
|
|
|
def get_project_elf(target_configs):
|
|
exec_targets = get_targets_by_type(target_configs, ["EXECUTABLE"])
|
|
if len(exec_targets) > 1:
|
|
print(
|
|
"Warning: Multiple elf targets found. The %s will be used!"
|
|
% exec_targets[0]["name"]
|
|
)
|
|
|
|
return exec_targets[0]
|
|
|
|
|
|
def generate_default_component():
|
|
# Used to force CMake generate build environments for all supported languages
|
|
|
|
prj_cmake_tpl = """# Warning! Do not delete this auto-generated file.
|
|
file(GLOB component_sources *.c* *.S)
|
|
idf_component_register(SRCS ${component_sources})
|
|
"""
|
|
dummy_component_path = os.path.join(FRAMEWORK_DIR, "components", "__pio_env")
|
|
if os.path.isdir(dummy_component_path):
|
|
return
|
|
|
|
os.makedirs(dummy_component_path)
|
|
|
|
for ext in (".cpp", ".c", ".S"):
|
|
dummy_file = os.path.join(dummy_component_path, "__dummy" + ext)
|
|
if not os.path.isfile(dummy_file):
|
|
open(dummy_file, "a").close()
|
|
|
|
component_cmake = os.path.join(dummy_component_path, "CMakeLists.txt")
|
|
if not os.path.isfile(component_cmake):
|
|
with open(component_cmake, "w") as fp:
|
|
fp.write(prj_cmake_tpl)
|
|
|
|
|
|
def find_default_component(target_configs):
|
|
for config in target_configs:
|
|
if "__pio_env" in config:
|
|
return config
|
|
sys.stderr.write(
|
|
"Error! Failed to find the default IDF component with build information for "
|
|
"generic files.\nCheck that the `EXTRA_COMPONENT_DIRS` option is not overridden "
|
|
"in your CMakeLists.txt.\nSee an example with an extra component here "
|
|
"https://docs.platformio.org/en/latest/frameworks/espidf.html#esp-idf-components\n"
|
|
)
|
|
env.Exit(1)
|
|
|
|
|
|
def create_version_file():
|
|
version_file = os.path.join(FRAMEWORK_DIR, "version.txt")
|
|
if not os.path.isfile(version_file):
|
|
with open(version_file, "w") as fp:
|
|
package_version = platform.get_package_version("framework-espidf")
|
|
fp.write(get_original_version(package_version) or package_version)
|
|
|
|
|
|
def generate_empty_partition_image(binary_path, image_size):
|
|
empty_partition = env.Command(
|
|
binary_path,
|
|
None,
|
|
env.VerboseAction(
|
|
'"$ESPIDF_PYTHONEXE" "%s" %s $TARGET'
|
|
% (
|
|
os.path.join(
|
|
FRAMEWORK_DIR,
|
|
"components",
|
|
"partition_table",
|
|
"gen_empty_partition.py",
|
|
),
|
|
image_size,
|
|
),
|
|
"Generating an empty partition $TARGET",
|
|
),
|
|
)
|
|
|
|
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", empty_partition)
|
|
|
|
|
|
def get_partition_info(pt_path, pt_offset, pt_params):
|
|
if not os.path.isfile(pt_path):
|
|
sys.stderr.write(
|
|
"Missing partition table file `%s`\n" % os.path.basename(pt_path)
|
|
)
|
|
env.Exit(1)
|
|
|
|
cmd = [
|
|
get_python_exe(),
|
|
os.path.join(FRAMEWORK_DIR, "components", "partition_table", "parttool.py"),
|
|
"-q",
|
|
"--partition-table-offset",
|
|
hex(pt_offset),
|
|
"--partition-table-file",
|
|
pt_path,
|
|
"get_partition_info",
|
|
"--info",
|
|
"size",
|
|
"offset",
|
|
]
|
|
|
|
if pt_params["name"] == "boot":
|
|
cmd.append("--partition-boot-default")
|
|
else:
|
|
cmd.extend(
|
|
[
|
|
"--partition-type",
|
|
pt_params["type"],
|
|
"--partition-subtype",
|
|
pt_params["subtype"],
|
|
]
|
|
)
|
|
|
|
result = exec_command(cmd)
|
|
if result["returncode"] != 0:
|
|
sys.stderr.write(
|
|
"Couldn't extract information for %s/%s from the partition table\n"
|
|
% (pt_params["type"], pt_params["subtype"])
|
|
)
|
|
sys.stderr.write(result["out"] + "\n")
|
|
sys.stderr.write(result["err"] + "\n")
|
|
env.Exit(1)
|
|
|
|
size = offset = 0
|
|
if result["out"].strip():
|
|
size, offset = result["out"].strip().split(" ", 1)
|
|
|
|
return {"size": size, "offset": offset}
|
|
|
|
|
|
def get_app_partition_offset(pt_table, pt_offset):
|
|
# Get the default boot partition offset
|
|
app_params = get_partition_info(pt_table, pt_offset, {"name": "boot"})
|
|
return app_params.get("offset", "0x10000")
|
|
|
|
|
|
def generate_mbedtls_bundle(sdk_config):
|
|
bundle_path = os.path.join("$BUILD_DIR", "x509_crt_bundle")
|
|
if os.path.isfile(env.subst(bundle_path)):
|
|
return
|
|
|
|
default_crt_dir = os.path.join(
|
|
FRAMEWORK_DIR, "components", "mbedtls", "esp_crt_bundle"
|
|
)
|
|
|
|
cmd = [get_python_exe(), os.path.join(default_crt_dir, "gen_crt_bundle.py")]
|
|
|
|
crt_args = ["--input"]
|
|
if sdk_config.get("MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", False):
|
|
crt_args.append(os.path.join(default_crt_dir, "cacrt_all.pem"))
|
|
elif sdk_config.get("MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", False):
|
|
crt_args.append(os.path.join(default_crt_dir, "cacrt_all.pem"))
|
|
cmd.extend(
|
|
["--filter", os.path.join(default_crt_dir, "cmn_crt_authorities.csv")]
|
|
)
|
|
|
|
if sdk_config.get("MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE", False):
|
|
cert_path = sdk_config.get("MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH", "")
|
|
if os.path.isfile(cert_path) or os.path.isdir(cert_path):
|
|
crt_args.append(os.path.abspath(cert_path))
|
|
else:
|
|
print("Warning! Couldn't find custom certificate bundle %s" % cert_path)
|
|
|
|
crt_args.append("-q")
|
|
|
|
# Use exec_command to change working directory
|
|
exec_command(cmd + crt_args, cwd=BUILD_DIR)
|
|
bundle_path = os.path.join("$BUILD_DIR", "x509_crt_bundle")
|
|
env.Execute(
|
|
env.VerboseAction(
|
|
" ".join(
|
|
[
|
|
os.path.join(
|
|
env.PioPlatform().get_package_dir("tool-cmake"),
|
|
"bin",
|
|
"cmake",
|
|
),
|
|
"-DDATA_FILE=" + bundle_path,
|
|
"-DSOURCE_FILE=%s.S" % bundle_path,
|
|
"-DFILE_TYPE=BINARY",
|
|
"-P",
|
|
os.path.join(
|
|
FRAMEWORK_DIR,
|
|
"tools",
|
|
"cmake",
|
|
"scripts",
|
|
"data_file_embed_asm.cmake",
|
|
),
|
|
]
|
|
),
|
|
"Generating assembly for certificate bundle...",
|
|
)
|
|
)
|
|
|
|
|
|
def install_python_deps():
|
|
def _get_installed_pip_packages(python_exe_path):
|
|
result = {}
|
|
packages = {}
|
|
pip_output = subprocess.check_output(
|
|
[
|
|
python_exe_path,
|
|
"-m",
|
|
"pip",
|
|
"list",
|
|
"--format=json",
|
|
"--disable-pip-version-check",
|
|
]
|
|
)
|
|
try:
|
|
packages = json.loads(pip_output)
|
|
except:
|
|
print("Warning! Couldn't extract the list of installed Python packages.")
|
|
return {}
|
|
for p in packages:
|
|
result[p["name"]] = pepver_to_semver(p["version"])
|
|
|
|
return result
|
|
|
|
deps = {
|
|
# https://github.com/platformio/platform-espressif32/issues/635
|
|
"cryptography": ">=2.1.4,<35.0.0",
|
|
"future": ">=0.15.2",
|
|
"pyparsing": ">=2.0.3,<2.4.0",
|
|
"kconfiglib": "==13.7.1",
|
|
"idf-component-manager": "~=1.0",
|
|
}
|
|
|
|
if IDF5:
|
|
# Remove specific versions for IDF5 as not required
|
|
deps = {dep: "" for dep in deps}
|
|
|
|
python_exe_path = get_python_exe()
|
|
installed_packages = _get_installed_pip_packages(python_exe_path)
|
|
packages_to_install = []
|
|
for package, spec in deps.items():
|
|
if package not in installed_packages:
|
|
packages_to_install.append(package)
|
|
elif spec:
|
|
version_spec = semantic_version.Spec(spec)
|
|
if not version_spec.match(installed_packages[package]):
|
|
packages_to_install.append(package)
|
|
|
|
if packages_to_install:
|
|
env.Execute(
|
|
env.VerboseAction(
|
|
(
|
|
'"%s" -m pip install -U ' % python_exe_path
|
|
+ " ".join(['"%s%s"' % (p, deps[p]) for p in packages_to_install])
|
|
),
|
|
"Installing ESP-IDF's Python dependencies",
|
|
)
|
|
)
|
|
|
|
if IS_WINDOWS and "windows-curses" not in installed_packages:
|
|
env.Execute(
|
|
env.VerboseAction(
|
|
'"%s" -m pip install windows-curses' % python_exe_path,
|
|
"Installing windows-curses package",
|
|
)
|
|
)
|
|
|
|
# A special "esp-windows-curses" python package is required on Windows
|
|
# for Menuconfig on IDF <5
|
|
if not IDF5 and "esp-windows-curses" not in {
|
|
pkg.key for pkg in pkg_resources.working_set
|
|
}:
|
|
env.Execute(
|
|
env.VerboseAction(
|
|
'"%s" -m pip install "file://%s/tools/kconfig_new/esp-windows-curses"'
|
|
% (python_exe_path, FRAMEWORK_DIR),
|
|
"Installing windows-curses package",
|
|
)
|
|
)
|
|
|
|
|
|
def get_python_exe():
|
|
def _create_venv(venv_dir):
|
|
pip_path = os.path.join(
|
|
venv_dir,
|
|
"Scripts" if IS_WINDOWS else "bin",
|
|
"pip" + (".exe" if IS_WINDOWS else ""),
|
|
)
|
|
if not os.path.isfile(pip_path):
|
|
# Use the built-in PlatformIO Python to create a standalone IDF virtual env
|
|
env.Execute(
|
|
env.VerboseAction(
|
|
'"$PYTHONEXE" -m venv --clear "%s"' % venv_dir,
|
|
"Creating a virtual environment for IDF Python dependencies",
|
|
)
|
|
)
|
|
|
|
assert os.path.isfile(
|
|
pip_path
|
|
), "Error: Failed to create a proper virtual environment. Missing the pip binary!"
|
|
|
|
# The name of the IDF venv contains the IDF version to avoid possible conflicts and
|
|
# unnecessary reinstallation of Python dependencies in cases when Arduino
|
|
# as an IDF component requires a different version of the IDF package and
|
|
# hence a different set of Python deps or their versions
|
|
idf_version = get_original_version(platform.get_package_version("framework-espidf"))
|
|
venv_dir = os.path.join(
|
|
env.subst("$PROJECT_CORE_DIR"), "penv", ".espidf-" + idf_version)
|
|
|
|
if not os.path.isdir(venv_dir):
|
|
_create_venv(venv_dir)
|
|
|
|
python_exe_path = os.path.join(
|
|
venv_dir,
|
|
"Scripts" if IS_WINDOWS else "bin",
|
|
"python" + (".exe" if IS_WINDOWS else ""),
|
|
)
|
|
|
|
assert os.path.isfile(python_exe_path), (
|
|
"Error: Missing Python executable file `%s`" % python_exe_path
|
|
)
|
|
|
|
return python_exe_path
|
|
|
|
|
|
#
|
|
# ESP-IDF requires Python packages with specific versions in a virtual environment
|
|
#
|
|
|
|
install_python_deps()
|
|
|
|
# ESP-IDF package doesn't contain .git folder, instead package version is specified
|
|
# in a special file "version.h" in the root folder of the package
|
|
|
|
create_version_file()
|
|
|
|
# Generate a default component with dummy C/C++/ASM source files in the framework
|
|
# folder. This component is used to force the IDF build system generate build
|
|
# information for generic C/C++/ASM sources regardless of whether such files are used in project
|
|
|
|
generate_default_component()
|
|
|
|
#
|
|
# Generate final linker script
|
|
#
|
|
|
|
if not board.get("build.ldscript", ""):
|
|
linker_script = env.Command(
|
|
os.path.join("$BUILD_DIR", "memory.ld"),
|
|
board.get(
|
|
"build.esp-idf.ldscript",
|
|
os.path.join(
|
|
FRAMEWORK_DIR,
|
|
"components",
|
|
"esp_system",
|
|
"ld",
|
|
idf_variant,
|
|
"memory.ld.in",
|
|
),
|
|
),
|
|
env.VerboseAction(
|
|
'$CC -I"$BUILD_DIR/config" -I"%s" -C -P -x c -E $SOURCE -o $TARGET'
|
|
% os.path.join(FRAMEWORK_DIR, "components", "esp_system", "ld"),
|
|
"Generating LD script $TARGET",
|
|
),
|
|
)
|
|
|
|
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", linker_script)
|
|
env.Replace(LDSCRIPT_PATH="memory.ld")
|
|
|
|
|
|
#
|
|
# Current build script limitations
|
|
#
|
|
|
|
if any(" " in p for p in (FRAMEWORK_DIR, BUILD_DIR)):
|
|
sys.stderr.write("Error: Detected a whitespace character in project paths.\n")
|
|
env.Exit(1)
|
|
|
|
if not os.path.isdir(PROJECT_SRC_DIR):
|
|
sys.stderr.write(
|
|
"Error: Missing the `%s` folder with project sources.\n"
|
|
% os.path.basename(PROJECT_SRC_DIR)
|
|
)
|
|
env.Exit(1)
|
|
|
|
if env.subst("$SRC_FILTER"):
|
|
print(
|
|
(
|
|
"Warning: the 'src_filter' option cannot be used with ESP-IDF. Select source "
|
|
"files to build in the project CMakeLists.txt file.\n"
|
|
)
|
|
)
|
|
|
|
if os.path.isfile(os.path.join(PROJECT_SRC_DIR, "sdkconfig.h")):
|
|
print(
|
|
"Warning! Starting with ESP-IDF v4.0, new project structure is required: \n"
|
|
"https://docs.platformio.org/en/latest/frameworks/espidf.html#project-structure"
|
|
)
|
|
|
|
#
|
|
# Initial targets loading
|
|
#
|
|
|
|
# By default 'main' folder is used to store source files. In case when a user has
|
|
# default 'src' folder we need to add this as an extra component. If there is no 'main'
|
|
# folder CMake won't generate dependencies properly
|
|
extra_components = []
|
|
if PROJECT_SRC_DIR != os.path.join(PROJECT_DIR, "main"):
|
|
extra_components.append(PROJECT_SRC_DIR)
|
|
if "arduino" in env.subst("$PIOFRAMEWORK"):
|
|
print(
|
|
"Warning! Arduino framework as an ESP-IDF component doesn't handle "
|
|
"the `variant` field! The default `esp32` variant will be used."
|
|
)
|
|
extra_components.append(ARDUINO_FRAMEWORK_DIR)
|
|
|
|
print("Reading CMake configuration...")
|
|
project_codemodel = get_cmake_code_model(
|
|
PROJECT_DIR,
|
|
BUILD_DIR,
|
|
[
|
|
"-DIDF_TARGET=" + idf_variant,
|
|
"-DPYTHON_DEPS_CHECKED=1",
|
|
"-DEXTRA_COMPONENT_DIRS:PATH=" + ";".join(extra_components),
|
|
"-DPYTHON=" + get_python_exe(),
|
|
"-DSDKCONFIG=" + SDKCONFIG_PATH,
|
|
]
|
|
+ click.parser.split_arg_string(board.get("build.cmake_extra_args", "")),
|
|
)
|
|
|
|
# At this point the sdkconfig file should be generated by the underlying build system
|
|
assert os.path.isfile(SDKCONFIG_PATH), (
|
|
"Missing auto-generated SDK configuration file `%s`" % SDKCONFIG_PATH
|
|
)
|
|
|
|
if not project_codemodel:
|
|
sys.stderr.write("Error: Couldn't find code model generated by CMake\n")
|
|
env.Exit(1)
|
|
|
|
target_configs = load_target_configurations(
|
|
project_codemodel, os.path.join(BUILD_DIR, CMAKE_API_REPLY_PATH)
|
|
)
|
|
|
|
sdk_config = get_sdk_configuration()
|
|
|
|
project_target_name = "__idf_%s" % os.path.basename(PROJECT_SRC_DIR)
|
|
if project_target_name not in target_configs:
|
|
sys.stderr.write("Error: Couldn't find the main target of the project!\n")
|
|
env.Exit(1)
|
|
|
|
if project_target_name != "__idf_main" and "__idf_main" in target_configs:
|
|
sys.stderr.write(
|
|
(
|
|
"Warning! Detected two different targets with project sources. Please use "
|
|
"either %s or specify 'main' folder in 'platformio.ini' file.\n"
|
|
% project_target_name
|
|
)
|
|
)
|
|
env.Exit(1)
|
|
|
|
project_ld_scipt = generate_project_ld_script(
|
|
sdk_config, [project_target_name, "__pio_env"]
|
|
)
|
|
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", project_ld_scipt)
|
|
|
|
elf_config = get_project_elf(target_configs)
|
|
default_config_name = find_default_component(target_configs)
|
|
framework_components_map = get_components_map(
|
|
target_configs,
|
|
["STATIC_LIBRARY", "OBJECT_LIBRARY"],
|
|
[project_target_name, default_config_name],
|
|
)
|
|
|
|
build_components(env, framework_components_map, PROJECT_DIR)
|
|
|
|
if not elf_config:
|
|
sys.stderr.write("Error: Couldn't load the main firmware target of the project\n")
|
|
env.Exit(1)
|
|
|
|
for component_config in framework_components_map.values():
|
|
env.Depends(project_ld_scipt, component_config["lib"])
|
|
|
|
project_config = target_configs.get(project_target_name, {})
|
|
default_config = target_configs.get(default_config_name, {})
|
|
project_defines = get_app_defines(project_config)
|
|
project_flags = get_app_flags(project_config, default_config)
|
|
link_args = extract_link_args(elf_config)
|
|
app_includes = get_app_includes(elf_config)
|
|
|
|
#
|
|
# Compile bootloader
|
|
#
|
|
|
|
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", build_bootloader(sdk_config))
|
|
|
|
#
|
|
# Target: ESP-IDF menuconfig
|
|
#
|
|
|
|
env.AddPlatformTarget(
|
|
"menuconfig",
|
|
None,
|
|
[env.VerboseAction(RunMenuconfig, "Running menuconfig...")],
|
|
"Run Menuconfig",
|
|
)
|
|
|
|
#
|
|
# Process main parts of the framework
|
|
#
|
|
|
|
libs = find_lib_deps(
|
|
framework_components_map, elf_config, link_args, [project_target_name]
|
|
)
|
|
|
|
# Extra flags which need to be explicitly specified in LINKFLAGS section because SCons
|
|
# cannot merge them correctly
|
|
extra_flags = filter_args(link_args["LINKFLAGS"], ["-T", "-u"])
|
|
link_args["LINKFLAGS"] = sorted(list(set(link_args["LINKFLAGS"]) - set(extra_flags)))
|
|
|
|
# remove the main linker script flags '-T memory.ld'
|
|
try:
|
|
ld_index = extra_flags.index("memory.ld")
|
|
extra_flags.pop(ld_index)
|
|
extra_flags.pop(ld_index - 1)
|
|
except:
|
|
print("Warning! Couldn't find the main linker script in the CMake code model.")
|
|
|
|
#
|
|
# Process project sources
|
|
#
|
|
|
|
|
|
# Remove project source files from following build stages as they're
|
|
# built as part of the framework
|
|
def _skip_prj_source_files(node):
|
|
if node.srcnode().get_path().lower().startswith(PROJECT_SRC_DIR.lower()):
|
|
return None
|
|
return node
|
|
|
|
|
|
env.AddBuildMiddleware(_skip_prj_source_files)
|
|
|
|
#
|
|
# Generate partition table
|
|
#
|
|
|
|
fwpartitions_dir = os.path.join(FRAMEWORK_DIR, "components", "partition_table")
|
|
partitions_csv = board.get("build.partitions", "partitions_singleapp.csv")
|
|
partition_table_offset = sdk_config.get("PARTITION_TABLE_OFFSET", 0x8000)
|
|
|
|
env.Replace(
|
|
PARTITIONS_TABLE_CSV=os.path.abspath(
|
|
os.path.join(fwpartitions_dir, partitions_csv)
|
|
if os.path.isfile(os.path.join(fwpartitions_dir, partitions_csv))
|
|
else partitions_csv
|
|
)
|
|
)
|
|
|
|
partition_table = env.Command(
|
|
os.path.join("$BUILD_DIR", "partitions.bin"),
|
|
"$PARTITIONS_TABLE_CSV",
|
|
env.VerboseAction(
|
|
'"$ESPIDF_PYTHONEXE" "%s" -q --offset "%s" --flash-size "%s" $SOURCE $TARGET'
|
|
% (
|
|
os.path.join(
|
|
FRAMEWORK_DIR, "components", "partition_table", "gen_esp32part.py"
|
|
),
|
|
partition_table_offset,
|
|
board.get("upload.flash_size", "4MB"),
|
|
),
|
|
"Generating partitions $TARGET",
|
|
),
|
|
)
|
|
|
|
env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", partition_table)
|
|
|
|
#
|
|
# Main environment configuration
|
|
#
|
|
|
|
project_flags.update(link_args)
|
|
env.MergeFlags(project_flags)
|
|
env.Prepend(
|
|
CPPPATH=app_includes["plain_includes"],
|
|
CPPDEFINES=project_defines,
|
|
ESPIDF_PYTHONEXE=get_python_exe(),
|
|
LINKFLAGS=extra_flags,
|
|
LIBS=libs,
|
|
FLASH_EXTRA_IMAGES=[
|
|
(
|
|
board.get(
|
|
"upload.bootloader_offset",
|
|
"0x0" if mcu in ("esp32c3", "esp32s3") else "0x1000",
|
|
),
|
|
os.path.join("$BUILD_DIR", "bootloader.bin"),
|
|
),
|
|
(
|
|
board.get("upload.partition_table_offset", hex(partition_table_offset)),
|
|
os.path.join("$BUILD_DIR", "partitions.bin"),
|
|
),
|
|
],
|
|
)
|
|
|
|
#
|
|
# Propagate Arduino defines to the main build environment
|
|
#
|
|
|
|
if "arduino" in env.subst("$PIOFRAMEWORK"):
|
|
arduino_config_name = list(
|
|
filter(
|
|
lambda config_name: config_name.startswith(
|
|
"__idf_framework-arduinoespressif32"
|
|
),
|
|
target_configs,
|
|
)
|
|
)[0]
|
|
env.AppendUnique(
|
|
CPPDEFINES=extract_defines(
|
|
target_configs.get(arduino_config_name, {}).get("compileGroups", [])[0]
|
|
)
|
|
)
|
|
|
|
# Project files should be compiled only when a special
|
|
# option is enabled when running 'test' command
|
|
if "__test" not in COMMAND_LINE_TARGETS or env.GetProjectOption(
|
|
"test_build_project_src"
|
|
):
|
|
project_env = env.Clone()
|
|
if project_target_name != "__idf_main":
|
|
# Manually add dependencies to CPPPATH since ESP-IDF build system doesn't generate
|
|
# this info if the folder with sources is not named 'main'
|
|
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html#rename-main
|
|
project_env.AppendUnique(CPPPATH=app_includes["plain_includes"])
|
|
|
|
# Add include dirs from PlatformIO build system to project CPPPATH so
|
|
# they're visible to PIOBUILDFILES
|
|
project_env.AppendUnique(
|
|
CPPPATH=["$PROJECT_INCLUDE_DIR", "$PROJECT_SRC_DIR"]
|
|
+ get_project_lib_includes(env)
|
|
)
|
|
|
|
env.Append(
|
|
PIOBUILDFILES=compile_source_files(
|
|
target_configs.get(project_target_name),
|
|
project_env,
|
|
project_env.subst("$PROJECT_DIR"),
|
|
)
|
|
)
|
|
|
|
#
|
|
# Generate mbedtls bundle
|
|
#
|
|
|
|
if sdk_config.get("MBEDTLS_CERTIFICATE_BUNDLE", False):
|
|
generate_mbedtls_bundle(sdk_config)
|
|
|
|
#
|
|
# To embed firmware checksum a special argument for esptool.py is required
|
|
#
|
|
|
|
action = copy.deepcopy(env["BUILDERS"]["ElfToBin"].action)
|
|
action.cmd_list = env["BUILDERS"]["ElfToBin"].action.cmd_list.replace(
|
|
"-o", "--elf-sha256-offset 0xb0 -o"
|
|
)
|
|
env["BUILDERS"]["ElfToBin"].action = action
|
|
|
|
#
|
|
# Compile ULP sources in 'ulp' folder
|
|
#
|
|
|
|
ulp_dir = os.path.join(PROJECT_DIR, "ulp")
|
|
if os.path.isdir(ulp_dir) and os.listdir(ulp_dir) and mcu != "esp32c3":
|
|
env.SConscript("ulp.py", exports="env sdk_config project_config idf_variant")
|
|
|
|
#
|
|
# Process OTA partition and image
|
|
#
|
|
|
|
ota_partition_params = get_partition_info(
|
|
env.subst("$PARTITIONS_TABLE_CSV"),
|
|
partition_table_offset,
|
|
{"name": "ota", "type": "data", "subtype": "ota"},
|
|
)
|
|
|
|
if ota_partition_params["size"] and ota_partition_params["offset"]:
|
|
# Generate an empty image if OTA is enabled in partition table
|
|
ota_partition_image = os.path.join("$BUILD_DIR", "ota_data_initial.bin")
|
|
generate_empty_partition_image(ota_partition_image, ota_partition_params["size"])
|
|
|
|
env.Append(
|
|
FLASH_EXTRA_IMAGES=[
|
|
(
|
|
board.get(
|
|
"upload.ota_partition_offset", ota_partition_params["offset"]
|
|
),
|
|
ota_partition_image,
|
|
)
|
|
]
|
|
)
|
|
|
|
#
|
|
# Configure application partition offset
|
|
#
|
|
|
|
env.Replace(
|
|
ESP32_APP_OFFSET=get_app_partition_offset(
|
|
env.subst("$PARTITIONS_TABLE_CSV"), partition_table_offset
|
|
)
|
|
)
|
|
|
|
# Propagate application offset to debug configurations
|
|
env["INTEGRATION_EXTRA_DATA"].update(
|
|
{"application_offset": env.subst("$ESP32_APP_OFFSET")}
|
|
)
|