diff --git a/package.json b/package.json new file mode 100644 index 00000000000..300df4c30ee --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "framework-mbed-ce", + "version": "6.99.0", + "title": "Mbed OS Community Edition", + "description": "Mbed CE is a platform operating system designed for the internet of things", + "keywords": [ + "framework", + "os", + "arm", + "hal" + ], + "homepage": "http://mbed-ce.dev", + "repository": { + "type": "git", + "url": "https://github.com/mbed-ce/mbed-os" + } +} diff --git a/tools/cmake/mbed_create_distro.cmake b/tools/cmake/mbed_create_distro.cmake index 6dc83e4d174..60598499121 100644 --- a/tools/cmake/mbed_create_distro.cmake +++ b/tools/cmake/mbed_create_distro.cmake @@ -142,7 +142,7 @@ function(mbed_extract_flags NAME) # ARGN: modules... foreach(SUBMODULE ${SUBMODULES}) if(NOT "${SUBMODULE}" MATCHES "::@") # remove CMake internal CMAKE_DIRECTORY_ID_SEP markers # Remove LINK_ONLY genexes from target_link_libraries(... PRIVATE). We can ignore things wrapped in these - # because they will already have been handled by the target_link_libraries earlier on. + # because they are for private dependencies. if(NOT "${SUBMODULE}" MATCHES "\\$") if(NOT ${SUBMODULE} IN_LIST COMPLETED_MODULES) list(APPEND REMAINING_MODULES ${SUBMODULE}) diff --git a/tools/cmake/mbed_generate_configuration.cmake b/tools/cmake/mbed_generate_configuration.cmake index 19a788aa19a..cd3e5eb1b0f 100644 --- a/tools/cmake/mbed_generate_configuration.cmake +++ b/tools/cmake/mbed_generate_configuration.cmake @@ -11,6 +11,30 @@ set(MBED_NEED_TO_RECONFIGURE FALSE) +# Check that path variables (MBED_APP_JSON_PATH, CUSTOM_TARGETS_JSON_PATH) are valid and set +# vars (HAS_CUSTOM_TARGETS_JSON, HAS_MBED_APP_JSON) based on whether they exist. +# Also, convert all relative paths to absolute paths, rooted at CMAKE_SOURCE_DIR. +# This makes sure that they are interpreted the same way everywhere. +foreach(json_var_name MBED_APP_JSON CUSTOM_TARGETS_JSON) + + if("${${json_var_name}_PATH}" STREQUAL "") + set(HAS_${json_var_name} FALSE) + else() + get_filename_component(${json_var_name}_PATH "${${json_var_name}_PATH}" ABSOLUTE BASE_DIR ${CMAKE_SOURCE_DIR}) + if(NOT EXISTS ${${json_var_name}_PATH} OR IS_DIRECTORY ${${json_var_name}_PATH}) + message(FATAL_ERROR "${json_var_name}_PATH value of ${${json_var_name}_PATH} is not a valid file!") + endif() + set(HAS_${json_var_name} TRUE) + endif() +endforeach() + +if("${CUSTOM_TARGETS_JSON_PATH}" STREQUAL "") + set(HAS_CUSTOM_TARGETS_JSON FALSE) +else() + set(HAS_CUSTOM_TARGETS_JSON TRUE) + get_filename_component(CUSTOM_TARGETS_JSON_PATH "${CUSTOM_TARGETS_JSON_PATH}" ABSOLUTE BASE_DIR ${CMAKE_SOURCE_DIR}) +endif() + # First, verify that MBED_TARGET has not changed if(DEFINED MBED_INTERNAL_LAST_MBED_TARGET) if(NOT "${MBED_INTERNAL_LAST_MBED_TARGET}" STREQUAL "${MBED_TARGET}") @@ -42,9 +66,19 @@ endif() if(NOT MBED_NEED_TO_RECONFIGURE) file(TIMESTAMP ${CMAKE_CURRENT_BINARY_DIR}/mbed_config.cmake MBED_CONFIG_CMAKE_TIMESTAMP "%s" UTC) + set(MBED_APP_JSON_FOUND FALSE) + set(CUSTOM_TARGETS_JSON_FOUND FALSE) + foreach(CONFIG_JSON ${MBED_CONFIG_JSON_SOURCE_FILES}) get_filename_component(CONFIG_JSON_ABSPATH ${CONFIG_JSON} ABSOLUTE) + if(CONFIG_JSON_ABSPATH STREQUAL MBED_APP_JSON_PATH) + set(MBED_APP_JSON_FOUND TRUE) + endif() + if(CONFIG_JSON_ABSPATH STREQUAL CUSTOM_TARGETS_JSON_PATH) + set(CUSTOM_TARGETS_JSON_FOUND TRUE) + endif() + if(NOT EXISTS ${CONFIG_JSON_ABSPATH}) message(STATUS "Mbed: ${CONFIG_JSON} deleted or renamed, regenerating configs...") set(MBED_NEED_TO_RECONFIGURE TRUE) @@ -60,20 +94,28 @@ if(NOT MBED_NEED_TO_RECONFIGURE) endforeach() endif() -# Convert all relative paths to absolute paths, rooted at CMAKE_SOURCE_DIR. -# This makes sure that they are interpreted the same way everywhere. -get_filename_component(MBED_APP_JSON_PATH "${MBED_APP_JSON_PATH}" ABSOLUTE BASE_DIR ${CMAKE_SOURCE_DIR}) -get_filename_component(CUSTOM_TARGETS_JSON_PATH "${CUSTOM_TARGETS_JSON_PATH}" ABSOLUTE BASE_DIR ${CMAKE_SOURCE_DIR}) +if(NOT MBED_NEED_TO_RECONFIGURE) + # Corner case: if we previously had not set an mbed_app.json and now we do, we need to detect that + # and reconfigure. + if(HAS_MBED_APP_JSON AND NOT MBED_APP_JSON_FOUND) + message(STATUS "Mbed: mbed_app.json added/moved, regenerating configs...") + set(MBED_NEED_TO_RECONFIGURE TRUE) + endif() + if(HAS_CUSTOM_TARGETS_JSON AND NOT CUSTOM_TARGETS_JSON_FOUND) + message(STATUS "Mbed: custom_targets.json added/moved, regenerating configs...") + set(MBED_NEED_TO_RECONFIGURE TRUE) + endif() +endif() if(MBED_NEED_TO_RECONFIGURE) # Generate mbed_config.cmake for this target - if(EXISTS "${MBED_APP_JSON_PATH}" AND (NOT IS_DIRECTORY "${MBED_APP_JSON_PATH}")) + if(HAS_MBED_APP_JSON) set(APP_CONFIG_ARGUMENT --app-config "${MBED_APP_JSON_PATH}") else() set(APP_CONFIG_ARGUMENT "") endif() - if(EXISTS "${CUSTOM_TARGETS_JSON_PATH}" AND (NOT IS_DIRECTORY "${CUSTOM_TARGETS_JSON_PATH}")) + if(HAS_CUSTOM_TARGETS_JSON) set(CUSTOM_TARGET_ARGUMENT --custom-targets-json "${CUSTOM_TARGETS_JSON_PATH}") else() set(CUSTOM_TARGET_ARGUMENT "") diff --git a/tools/pyproject.toml b/tools/pyproject.toml index 0087c13fcb0..f96f0d00b29 100644 --- a/tools/pyproject.toml +++ b/tools/pyproject.toml @@ -72,6 +72,13 @@ unit-tests = [ "lxml" ] +# Install this optional dependency group to get IDE completion for packages used by the mbed_platformio package. +# Note that this package is actually run in platformio's venv, not the Mbed venv, so there is no other need to install these. +platformio = [ + "SCons", + "platformio" +] + [tool.hatch.build.targets.wheel] packages = [ "python/mbed_host_tests", diff --git a/tools/python/mbed_platformio/CMakeLists.txt b/tools/python/mbed_platformio/CMakeLists.txt new file mode 100644 index 00000000000..4e9ca6e7b2d --- /dev/null +++ b/tools/python/mbed_platformio/CMakeLists.txt @@ -0,0 +1,22 @@ +# +# This file is used as the CMakeLists.txt when building a PlatformIO project. +# + +cmake_minimum_required(VERSION 3.19) +cmake_policy(VERSION 3.19...3.22) + +set(MBED_APP_JSON_PATH ${PLATFORMIO_PROJECT_PATH}/mbed_app.json5) + +include(${PLATFORMIO_MBED_OS_PATH}/tools/cmake/mbed_toolchain_setup.cmake) +project(PlatformIOMbedProject + LANGUAGES C CXX ASM) +include(mbed_project_setup) + +add_subdirectory(${PLATFORMIO_MBED_OS_PATH} mbed-os) + +# Dummy executable. Not built, but needed to get the compile and linker flags via the CMake file API +add_executable(PIODummyExecutable + dummy_source_file.c + dummy_source_file.cpp + dummy_source_file.S) +target_link_libraries(PIODummyExecutable mbed-os) \ No newline at end of file diff --git a/tools/python/mbed_platformio/__init__.py b/tools/python/mbed_platformio/__init__.py new file mode 100644 index 00000000000..8546dded510 --- /dev/null +++ b/tools/python/mbed_platformio/__init__.py @@ -0,0 +1,4 @@ +""" +Copyright (c) 2025 Jamie Smith +SPDX-License-Identifier: Apache-2.0 +""" \ No newline at end of file diff --git a/tools/python/mbed_platformio/build_mbed_ce.py b/tools/python/mbed_platformio/build_mbed_ce.py new file mode 100644 index 00000000000..27233bf112d --- /dev/null +++ b/tools/python/mbed_platformio/build_mbed_ce.py @@ -0,0 +1,320 @@ +""" +PlatformIO build file for Mbed OS Community Edition. + +This script acts as an SCons buildfile which gets configuration from PlatformIO, configures Mbed (if needed), +and returns information about the configuration to the PIO build system. + +Copyright (c) 2025 Jamie Smith +SPDX-License-Identifier: Apache-2.0 +""" +from __future__ import annotations + +import pathlib +from pathlib import Path +import json +import sys + +from SCons.Script import DefaultEnvironment, ARGUMENTS +from SCons.Environment import Base as Environment +from platformio.proc import exec_command +import click + +env: Environment = DefaultEnvironment() +platform = env.PioPlatform() +board = env.BoardConfig() + +# Directories +FRAMEWORK_DIR = Path(platform.get_package_dir("framework-mbed-ce")) +BUILD_DIR = Path(env.subst("$BUILD_DIR")) +PROJECT_DIR = Path(env.subst("$PROJECT_DIR")) +PROJECT_SRC_DIR = Path(env.subst("$PROJECT_SRC_DIR")) +CMAKE_API_DIR = BUILD_DIR / ".cmake" / "api" / "v1" +CMAKE_API_QUERY_DIR = CMAKE_API_DIR / "query" +CMAKE_API_REPLY_DIR = CMAKE_API_DIR / "reply" + +PROJECT_CMAKELISTS_TXT = FRAMEWORK_DIR / "tools" / "python" / "mbed_platformio" / "CMakeLists.txt" +PROJECT_MBED_APP_JSON5 = PROJECT_DIR / "mbed_app.json5" +PROJECT_TARGET_CONFIG_H = BUILD_DIR / "mbed-os" / "generated-headers" / "mbed-target-config.h" + +CMAKE_BUILD_TYPE = "Debug" if ("debug" in env.GetBuildType()) else "Release" + +NINJA_PATH = pathlib.Path(platform.get_package_dir("tool-ninja")) / "ninja" +CMAKE_PATH = pathlib.Path(platform.get_package_dir("tool-cmake")) / "bin" / "cmake" + +# Add mbed-os/tools/python dir to PYTHONPATH so we can import from it. +# This script is run by SCons so it does not have access to any other Python modules by default. +sys.path.append(str(FRAMEWORK_DIR / "tools" / "python")) + +from mbed_platformio.pio_variants import PIO_VARIANT_TO_MBED_TARGET +from mbed_platformio.cmake_to_scons_converter import build_library, extract_defines, extract_flags, extract_includes, extract_link_args, find_included_files + +def get_mbed_target(): + board_type = env.subst("$BOARD") + variant = ( + PIO_VARIANT_TO_MBED_TARGET[board_type] + if board_type in PIO_VARIANT_TO_MBED_TARGET + else board_type.upper() + ) + return board.get("build.mbed_variant", variant) + +def is_proper_mbed_ce_project(): + return all( + path.is_file() + for path in ( + PROJECT_MBED_APP_JSON5, + ) + ) + +def create_default_project_files(): + print("Mbed CE: Creating default project files") + if not PROJECT_MBED_APP_JSON5.exists(): + PROJECT_MBED_APP_JSON5.write_text( +""" +{ + "target_overrides": { + "*": { + "platform.stdio-baud-rate": 9600, // matches PlatformIO default + "platform.stdio-buffered-serial": 1, + + // Uncomment to use mbed-baremetal instead of mbed-os + // "target.application-profile": "bare-metal" + } + } +} +""" + ) + +def is_cmake_reconfigure_required(): + cmake_cache_file = BUILD_DIR / "CMakeCache.txt" + cmake_config_files = [ + PROJECT_MBED_APP_JSON5, + PROJECT_CMAKELISTS_TXT + ] + ninja_buildfile = BUILD_DIR / "build.ninja" + + if not cmake_cache_file.exists(): + print("Mbed CE: Reconfigure required because CMake cache does not exist") + return True + if not CMAKE_API_REPLY_DIR.is_dir() or not any(CMAKE_API_REPLY_DIR.iterdir()): + print("Mbed CE: Reconfigure required because CMake API reply dir is missing") + return True + if not ninja_buildfile.exists(): + print("Mbed CE: Reconfigure required because Ninja buildfile does not exist") + return True + + # If the JSON files have 'Debug' in their names that means this project was previously configured as Debug. + if not any(CMAKE_API_REPLY_DIR.glob(f"directory-*{CMAKE_BUILD_TYPE}*.json")): + print("Mbed CE: Reconfigure required because build type (debug / release) changed.") + return True + + cache_file_mtime = cmake_cache_file.stat().st_mtime + for file in cmake_config_files: + if file.stat().st_mtime > cache_file_mtime: + print(f"Mbed CE: Reconfigure required because {file.name} was modified") + return True + + return False + + +def run_tool(command_and_args: list[str] | None = None): + result = exec_command(command_and_args) + 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 get_cmake_code_model(cmake_args: list) -> dict: + + query_file = CMAKE_API_QUERY_DIR / "codemodel-v2" + + if not query_file.exists(): + query_file.parent.mkdir(parents=True, exist_ok=True) + query_file.touch() + + if not is_proper_mbed_ce_project(): + create_default_project_files() + + if is_cmake_reconfigure_required(): + print("Mbed CE: Configuring CMake build system...") + cmake_command = [str(CMAKE_PATH), *cmake_args] + run_tool(cmake_command) + + # Seems like CMake doesn't update the timestamp on the cache file if nothing actually changed. + # Ensure that the timestamp is updated so we won't reconfigure next time. + (BUILD_DIR / "CMakeCache.txt").touch() + + if not CMAKE_API_REPLY_DIR.is_dir() or not any(CMAKE_API_REPLY_DIR.iterdir()): + sys.stderr.write("Error: Couldn't find CMake API response file\n") + env.Exit(1) + + codemodel = {} + for target in CMAKE_API_REPLY_DIR.iterdir(): + if target.name.startswith("codemodel-v2"): + with open(target, "r") as fp: + codemodel = json.load(fp) + + assert codemodel["version"]["major"] == 2 + return codemodel + +def get_target_config(project_configs: dict, target_index): + target_json = project_configs.get("targets")[target_index].get("jsonFile", "") + target_config_file = CMAKE_API_REPLY_DIR / target_json + if not target_config_file.is_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: dict) -> dict: + 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 + ) + configs[target_config["name"]] = target_config + + return configs + +def generate_project_ld_script() -> pathlib.Path: + + # Run Ninja to build the target which generates the linker script. + # Note that we don't want to use CMake as running it has the side effect of redoing + # the file API query. + cmd = [ + str(pathlib.Path(platform.get_package_dir("tool-ninja")) / "ninja"), + "-C", + str(BUILD_DIR), + "mbed-linker-script" + ] + run_tool(cmd) + + # Find the linker script. It gets saved in the build dir as + # .link-script.ld. + return next(BUILD_DIR.glob("*.link_script.ld")) + + +def get_targets_by_type(target_configs: dict, target_types: list[str], ignore_targets: list[str] | None=None) -> list: + 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: dict, target_types: list[str], ignore_components: list[str] | None=None) -> dict: + 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: Environment, components_map: dict, project_src_dir: pathlib.Path +): + for k, v in components_map.items(): + components_map[k]["lib"] = build_library( + env, v["config"], project_src_dir, FRAMEWORK_DIR, pathlib.Path("$BUILD_DIR/mbed-os") + ) + +def get_app_defines(app_config: dict): + return extract_defines(app_config["compileGroups"][0]) + +## CMake configuration ------------------------------------------------------------------------------------------------- + +project_codemodel = get_cmake_code_model( + [ + "-S", + PROJECT_CMAKELISTS_TXT.parent, + "-B", + BUILD_DIR, + "-G", + "Ninja", + "-DCMAKE_MAKE_PROGRAM=" + str(NINJA_PATH.as_posix()), # Note: CMake prefers to be passed paths with forward slashes, so use as_posix() + "-DCMAKE_BUILD_TYPE=" + CMAKE_BUILD_TYPE, + "-DPLATFORMIO_MBED_OS_PATH=" + str(FRAMEWORK_DIR.as_posix()), + "-DPLATFORMIO_PROJECT_PATH=" + str(PROJECT_DIR.as_posix()), + "-DMBED_TARGET=" + get_mbed_target(), + "-DUPLOAD_METHOD=NONE", # Disable Mbed CE upload method system as PlatformIO has its own + ] + + click.parser.split_arg_string(board.get("build.cmake_extra_args", "")), +) + +if not project_codemodel: + sys.stderr.write("Error: Couldn't find code model generated by CMake\n") + env.Exit(1) + +print("Mbed CE: Reading CMake configuration...") +target_configs = load_target_configurations(project_codemodel) + +framework_components_map = get_components_map( + target_configs, + ["STATIC_LIBRARY", "OBJECT_LIBRARY"], + [], +) + +## Convert targets & flags from CMake to SCons ------------------------------------------------------------------------- + +build_components(env, framework_components_map, PROJECT_DIR) + +mbed_os_lib_target_json = target_configs.get("mbed-os", {}) +app_target_json = target_configs.get("PIODummyExecutable", {}) +project_defines = get_app_defines(app_target_json) +project_flags = extract_flags(app_target_json) +app_includes = extract_includes(app_target_json) + +## Linker flags -------------------------------------------------------------------------------------------------------- + +# Link the main Mbed OS library using -Wl,--whole-archive. This is needed for the resolution of weak symbols +# within this archive. +link_args = ["-Wl,--whole-archive", "$BUILD_DIR\\mbed-os\\libmbed-os.a", "-Wl,--no-whole-archive"] +env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", "$BUILD_DIR\\mbed-os\\libmbed-os.a") + +# Get other linker flags from Mbed. We want these to appear after the application objects and Mbed libraries +# because they contain the C/C++ library link flags. +link_args.extend(extract_link_args(app_target_json)) + +# The CMake build system adds a flag in mbed_set_post_build() to output a map file. +# We need to do that here. +map_file = BUILD_DIR / 'firmware.map' +link_args.append(f"-Wl,-Map={str(map_file)}") + +## Build environment configuration ------------------------------------------------------------------------------------- + +env.MergeFlags(project_flags) +env.Prepend( + CPPPATH=app_includes["plain_includes"], + CPPDEFINES=project_defines, +) +env.Append(_LIBFLAGS=link_args) + +# Set up a dependency between all application source files and mbed-target-config.h. +# This ensures that the app will be recompiled if the header changes. +env.Append(PIO_EXTRA_APP_SOURCE_DEPS=find_included_files(env)) + +## Linker script ------------------------------------------------------------------------------------------------------- + +# Run Ninja to produce the linker script. +# Note that this seems to execute CMake, causing the code model query to be re-done. +# So, we have to do this after we are done using the results of said query. +print("Mbed CE: Generating linker script...") +project_ld_script = generate_project_ld_script() +env.Depends("$BUILD_DIR/$PROGNAME$PROGSUFFIX", str(project_ld_script)) +env.Append(LDSCRIPT_PATH=str(project_ld_script)) + +print("Mbed CE: Build environment configured.") \ No newline at end of file diff --git a/tools/python/mbed_platformio/cmake_to_scons_converter.py b/tools/python/mbed_platformio/cmake_to_scons_converter.py new file mode 100644 index 00000000000..f733c3791a0 --- /dev/null +++ b/tools/python/mbed_platformio/cmake_to_scons_converter.py @@ -0,0 +1,206 @@ +""" +Functions for converting build system information from the CMake File API into SCons build targets. + +Copyright (c) 2025 Jamie Smith +SPDX-License-Identifier: Apache-2.0 +""" + +from __future__ import annotations + +import collections + +from SCons.Environment import Base as Environment +import pathlib +import click + +def extract_defines(compile_group: dict) -> list[tuple[str, str]]: + def _normalize_define(define_string): + define_string = define_string.strip() + if "=" in define_string: + define, value = define_string.split("=", maxsplit=1) + if any(char in value for char in (' ', '<', '>')): + value = f'"{value}"' + elif '"' in value and not value.startswith("\\"): + value = value.replace('"', '\\"') + return define, value + return define_string + + result = [ + _normalize_define(d.get("define", "")) + for d in compile_group.get("defines", []) if d + ] + + for f in compile_group.get("compileCommandFragments", []): + fragment = f.get("fragment", "").strip() + if fragment.startswith('"'): + fragment = fragment.strip('"') + if fragment.startswith("-D"): + result.append(_normalize_define(fragment[2:])) + + return result + +def prepare_build_envs(target_json: dict, default_env: Environment) -> list[Environment]: + """ + Creates the Scons Environment(s) needed to build the source files in a CMake target + """ + build_envs = [] + target_compile_groups = target_json.get("compileGroups", []) + if not target_compile_groups: + print("Warning! The `%s` component doesn't register any source files. " + "Check if sources are set in component's CMakeLists.txt!" % target_json["name"] + ) + + 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) + flags = extract_flags(target_json) + build_env = default_env.Clone() + build_env.SetOption("implicit_cache", 1) + build_env.MergeFlags(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")) + build_envs.append(build_env) + + return build_envs + +def compile_source_files( + config: dict, default_env: Environment, project_src_dir: pathlib.Path, framework_dir: pathlib.Path, framework_obj_dir: pathlib.Path) -> list: + """ + Generates SCons rules to compile the source files in a target. + Returns list of object files to build. + + :param framework_dir: Path to the Mbed CE framework source + :param framework_obj_dir: Path to the directory where object files for Mbed CE will be saved. + """ + build_envs = prepare_build_envs(config, default_env) + objects = [] + 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: + + # Get absolute path to source, resolving relative to source dir if needed + src_path = pathlib.Path(source.get("path")) + if not src_path.is_absolute(): + src_path = project_src_dir / src_path + + # Figure out object path + if src_path.is_relative_to(project_src_dir): + obj_path = (pathlib.Path("$BUILD_DIR") / src_path.relative_to(project_src_dir)).with_suffix(".o") + elif src_path.is_relative_to(framework_dir): + obj_path = (framework_obj_dir / src_path.relative_to(framework_dir)).with_suffix(".o") + else: + raise RuntimeError(f"Source path {src_path!s} outside of project source dir and framework dir, don't know where to save object file!") + + env = build_envs[compile_group_idx] + + objects.append(env.StaticObject(target=str(obj_path), source=str(src_path))) + + # SCons isn't smart enough to add a dependency based on the "-include" compiler flag, so + # manually add one. + for included_file in find_included_files(env): + env.Depends(str(obj_path), included_file) + + + return objects + +def build_library( + default_env: Environment, lib_config: dict, project_src_dir: pathlib.Path, framework_dir: pathlib.Path, framework_obj_dir: pathlib.Path +): + lib_name = lib_config["nameOnDisk"] + lib_path = lib_config["paths"]["build"] + lib_objects = compile_source_files( + lib_config, default_env, project_src_dir, framework_dir, framework_obj_dir + ) + + #print(f"Created build rule for " + str(pathlib.Path("$BUILD_DIR") / lib_path / lib_name)) + + return default_env.Library( + target=str(pathlib.Path("$BUILD_DIR") / lib_path / lib_name), source=lib_objects + ) + +def _get_flags_for_compile_group(compile_group_json: dict) -> list[str]: + """ + Extract the flags from a CMake compile group. + """ + flags = [] + for ccfragment in compile_group_json["compileCommandFragments"]: + fragment = ccfragment.get("fragment", "").strip("\" ") + if not fragment or fragment.startswith("-D"): + continue + flags.extend( + click.parser.split_arg_string(fragment.strip()) + ) + return flags + +def extract_flags(target_json: dict) -> dict[str, list[str]]: + """ + Returns a dictionary with flags for SCons based on a given CMake target + """ + default_flags = collections.defaultdict(list) + for cg in target_json["compileGroups"]: + default_flags[cg["language"]].extend(_get_flags_for_compile_group(cg)) + + # Flags are sorted because CMake randomly populates build flags in code model + return { + "ASPPFLAGS": default_flags.get("ASM"), + "CFLAGS": default_flags.get("C"), + "CXXFLAGS": default_flags.get("CXX"), + } + +def find_included_files(environment: Environment) -> set[str]: + """ + Process a list of flags produced by extract_flags() to find files manually included by '-include' + """ + result = set() + for flag_var in ["CFLAGS", "CXXFLAGS", "CCFLAGS"]: + language_flags = environment.get(flag_var) + for index in range(0, len(language_flags)): + if language_flags[index] == "-include" and index < len(language_flags) - 1: + result.add(language_flags[index + 1]) + return result + +def extract_includes(target_json: dict) -> dict[str, list[str]]: + """ + Extract the includes from a CMake target and return an SCons-style dict + """ + plain_includes = [] + sys_includes = [] + cg = target_json["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_link_args(target_json: dict) -> list[str]: + """ + Extract the linker flags from a CMake target + """ + + result = [] + + for f in target_json.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": + result.extend(args) + + return result \ No newline at end of file diff --git a/tools/python/mbed_platformio/dummy_source_file.S b/tools/python/mbed_platformio/dummy_source_file.S new file mode 100644 index 00000000000..4496a7c23a2 --- /dev/null +++ b/tools/python/mbed_platformio/dummy_source_file.S @@ -0,0 +1,3 @@ +// Empty ASM source file so that we can scan the ASM flags via the CMake API +// Copyright (c) 2025 Jamie Smith +// SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/python/mbed_platformio/dummy_source_file.c b/tools/python/mbed_platformio/dummy_source_file.c new file mode 100644 index 00000000000..ab3ee9857ab --- /dev/null +++ b/tools/python/mbed_platformio/dummy_source_file.c @@ -0,0 +1,3 @@ +// Empty C++ source file so that we can scan the C++ flags via the CMake API +// Copyright (c) 2025 Jamie Smith +// SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/python/mbed_platformio/dummy_source_file.cpp b/tools/python/mbed_platformio/dummy_source_file.cpp new file mode 100644 index 00000000000..ab3ee9857ab --- /dev/null +++ b/tools/python/mbed_platformio/dummy_source_file.cpp @@ -0,0 +1,3 @@ +// Empty C++ source file so that we can scan the C++ flags via the CMake API +// Copyright (c) 2025 Jamie Smith +// SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tools/python/mbed_platformio/pio_variants.py b/tools/python/mbed_platformio/pio_variants.py new file mode 100644 index 00000000000..76aa4b16c27 --- /dev/null +++ b/tools/python/mbed_platformio/pio_variants.py @@ -0,0 +1,26 @@ +""" +Maps PIO variant name to Mbed target name, in the situation where the Mbed target name is different +from the uppercased version of the variant name + +Copyright (c) 2025 Jamie Smith +SPDX-License-Identifier: Apache-2.0 +""" + +PIO_VARIANT_TO_MBED_TARGET = { + "seeedArchPro": "ARCH_PRO", + "seeedArchMax": "ARCH_MAX", + "frdm_kl25z": "KL25Z", + "frdm_kl43z": "KL43Z", + "frdm_kl46z": "KL46Z", + "frdm_k64f": "K64F", + "frdm_k82f": "K82F", + "IBMEthernetKit": "K64F", + "frdm_k66f": "K66F", + "frdm_k22f": "K22F", + "frdm_kw41z": "KW41Z", + "cloud_jam": "NUCLEO_F401RE", + "cloud_jam_l4": "NUCLEO_L476RG", + "nucleo_h743zi": "NUCLEO_H743ZI2", + "genericSTM32F103RB": "NUCLEO_F103RB", + "disco_h747xi": "DISCO_H747I" +}