Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ endif()

set(CMAKE_C_STANDARD 17)

set(APP_COLCON_META "${PROJECT_DIR}/app-colcon.meta")
if(NOT EXISTS "${APP_COLCON_META}")
set(APP_COLCON_META "")
endif()
# Detect creation/removal as well as edits of the optional application metadata.
file(GLOB APP_COLCON_META CONFIGURE_DEPENDS "${PROJECT_DIR}/app-colcon.meta")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${COMPONENT_DIR}/colcon.meta" ${APP_COLCON_META})

set(EXTRA_ROS_PACKAGES "${PROJECT_DIR}/extra_ros_packages")
if(NOT EXISTS "${EXTRA_ROS_PACKAGES}")
Expand Down Expand Up @@ -60,20 +60,29 @@ foreach(package ${PACKAGES})
string(APPEND IDF_INCLUDES "${${package}_includes_string}")
endforeach()

include(${CMAKE_CURRENT_LIST_DIR}/cmake/microros_flags.cmake)
microros_get_flags(C MICROROS_C_FLAGS)
microros_get_flags(CXX MICROROS_CXX_FLAGS)
idf_build_get_property(IDF_TARGET_ARCH IDF_TARGET_ARCH)
set(BUILD_CONFIG_DIR "${CMAKE_BINARY_DIR}/config")
file(SHA256 "${BUILD_CONFIG_DIR}/sdkconfig.h" MICROROS_SDKCONFIG_HASH)
set(MICROROS_APP_META_HASH "")
if(APP_COLCON_META)
file(SHA256 "${APP_COLCON_META}" MICROROS_APP_META_HASH)
endif()
# configure_file only updates the output when its contents change. Generate it
# here rather than substituting compiler flags through Make and sed.
configure_file(esp32_toolchain.cmake.in "${COMPONENT_DIR}/esp32_toolchain.cmake" @ONLY)

execute_process(
WORKING_DIRECTORY ${COMPONENT_DIR}
RESULT_VARIABLE libmicroros_ret
COMMAND
${submake} -j -f libmicroros.mk
X_CC=${CMAKE_C_COMPILER}
X_AR=${CMAKE_AR}
X_CXX=${CMAKE_CXX_COMPILER}
C_STANDARD=${CMAKE_C_STANDARD}
MIDDLEWARE=${MIDDLEWARE}
BUILD_DIR=${CMAKE_BINARY_DIR}
IDF_INCLUDES=${IDF_INCLUDES}
IDF_PATH=${IDF_PATH}
IDF_TARGET=${IDF_TARGET}
APP_COLCON_META=${APP_COLCON_META}
EXTRA_ROS_PACKAGES=${EXTRA_ROS_PACKAGES}
)
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ git clone -b rolling https://github.com/micro-ROS/micro_ros_espidf_component.git

If you encounter issues during the build process, ensure that you are running in a clean shell environment _without_ the ROS 2 setup script sourced.

The micro-ROS target libraries inherit IDF's C/C++ compiler flags and global
compile definitions, including architecture, ABI, libc, hardware workarounds,
optimization and stack protection. ROS packages retain their own warning policy
and language standards. Changes to `sdkconfig`, the exported flags or colcon
metadata invalidate the library build and refresh the target packages' CMake
caches; the host-side build tools continue to use the native compiler.

The library is still built during CMake configuration. Set shared compiler
options in the outer IDF build before this component is processed; options added
afterwards and generator expressions cannot be propagated by this configure-time
integration.

## Example

In order to test a int32_publisher example:
Expand Down
60 changes: 60 additions & 0 deletions cmake/microros_flags.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0

# IDF 5 uses ordinary flag strings; IDF 6 also uses GCC response files.
# Snapshot their contents so changing flags at the same path invalidates the
# generated micro-ROS toolchain, without rebuilding on timestamp-only changes.
function(microros_expand_flags output)
set(result "")
foreach(arg IN LISTS ARGN)
if(arg MATCHES "^@(.+)$")
set(response_file "${CMAKE_MATCH_1}")
file(READ "${response_file}" contents)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${response_file}")
separate_arguments(nested UNIX_COMMAND "${contents}")
microros_expand_flags(expanded ${nested})
list(APPEND result ${expanded})
elseif(arg MATCHES "^SHELL:(.*)$")
separate_arguments(expanded UNIX_COMMAND "${CMAKE_MATCH_1}")
list(APPEND result ${expanded})
elseif(arg MATCHES "\\$<")
message(FATAL_ERROR
"micro-ROS is built during CMake configuration and cannot evaluate "
"the IDF compile option '${arg}'.")
else()
list(APPEND result "${arg}")
endif()
endforeach()
set(${output} "${result}" PARENT_SCOPE)
endfunction()

function(microros_get_flags language output)
separate_arguments(toolchain_flags UNIX_COMMAND "${CMAKE_${language}_FLAGS}")
idf_build_get_property(common_flags COMPILE_OPTIONS)
idf_build_get_property(language_flags ${language}_COMPILE_OPTIONS)
idf_build_get_property(definitions COMPILE_DEFINITIONS)
microros_expand_flags(flags ${toolchain_flags} ${common_flags} ${language_flags})

# ROS packages own their warning policy and language standards. Keep IDF's
# architecture, ABI, libc, workaround, optimization and instrumentation flags.
# Keep -Wa, / -Wp, / -Wl, driver forwarding options; they are not warnings.
list(FILTER flags EXCLUDE REGEX "^-W([^alp]|[alp][^,]|[alp]$)")
list(FILTER flags EXCLUDE REGEX "^-std=")
foreach(definition IN LISTS definitions)
if(NOT definition MATCHES "^-D")
set(definition "-D${definition}")
endif()
list(APPEND flags "${definition}")
endforeach()
microros_expand_flags(flags ${flags})

# Preserve argument boundaries, including macro values and paths with spaces,
# when CMake passes this string to the child compiler command line.
set(quoted_flags "")
foreach(flag IN LISTS flags)
string(REPLACE "\\" "\\\\" flag "${flag}")
string(REPLACE "\"" "\\\"" flag "${flag}")
string(APPEND quoted_flags " \"${flag}\"")
endforeach()
set(${output} "${quoted_flags}" PARENT_SCOPE)
endfunction()
39 changes: 21 additions & 18 deletions esp32_toolchain.cmake.in
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
include(CMakeForceCompiler)

# Configuration fingerprints also invalidate the Make build stamp when settings
# change without changing compiler flags (including removal of app-colcon.meta).
# sdkconfig: @MICROROS_SDKCONFIG_HASH@
# app-colcon.meta: @MICROROS_APP_META_HASH@

set(CMAKE_SYSTEM_NAME Generic)

set(idf_target "@IDF_TARGET@")
set(idf_path "@IDF_PATH@")

set(RISCV_TARGETS "esp32c3" "esp32c6")

if("${idf_target}" IN_LIST RISCV_TARGETS)
set(CMAKE_SYSTEM_PROCESSOR riscv)
set(FLAGS "-ffunction-sections -fdata-sections" CACHE STRING "" FORCE)
else()
set(CMAKE_SYSTEM_PROCESSOR xtensa)
set(FLAGS "-mlongcalls -ffunction-sections -fdata-sections" CACHE STRING "" FORCE)
endif()
set(CMAKE_SYSTEM_PROCESSOR "@IDF_TARGET_ARCH@")

include("@BUILD_CONFIG_DIR@/sdkconfig.cmake" OPTIONAL)
if(CONFIG_LIBC_PICOLIBC)
set(FLAGS "${FLAGS} --specs=picolibc.specs" CACHE STRING "" FORCE)
# `__STDC_WANT_LIB_EXT1__` (C11 Annex K) is only implemented by picolibc.
# Temporary workaround for ros2/rcutils#552. Remove this once micro-ROS/rcutils picks up ros2/rcutils#555
add_compile_definitions(__STDC_WANT_LIB_EXT1__=1)
Expand All @@ -31,14 +27,21 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

set(CMAKE_C_COMPILER @CMAKE_C_COMPILER@)
set(CMAKE_CXX_COMPILER @CMAKE_CXX_COMPILER@)

if(NOT MICROROS_TOOLCHAIN_FLAGS_APPLIED)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${FLAGS} ${IDF_INCLUDES}" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti ${FLAGS} ${IDF_INCLUDES}" CACHE STRING "" FORCE)
set(MICROROS_TOOLCHAIN_FLAGS_APPLIED TRUE CACHE INTERNAL "micro-ROS toolchain flags were applied")
endif()
set(CMAKE_C_COMPILER "@CMAKE_C_COMPILER@")
set(CMAKE_CXX_COMPILER "@CMAKE_CXX_COMPILER@")

# Replace, rather than append to, cached flags on every configuration. This also
# updates existing colcon workspaces when IDF options change.
set(CMAKE_C_FLAGS [==[@MICROROS_C_FLAGS@ @IDF_INCLUDES@]==] CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS [==[@MICROROS_CXX_FLAGS@ @IDF_INCLUDES@]==] CACHE STRING "" FORCE)

# IDF supplies optimization, debug and assertion settings above. CMake's Release
# defaults (-O3 -DNDEBUG) must not override them in individual ROS packages.
foreach(language C CXX)
foreach(config DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
set(CMAKE_${language}_FLAGS_${config} "" CACHE STRING "" FORCE)
endforeach()
endforeach()

add_compile_definitions(ESP_PLATFORM LWIP_IPV4 LWIP_IPV6 PLATFORM_NAME_FREERTOS)

Expand Down
26 changes: 11 additions & 15 deletions libmicroros.mk
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ clean:
rm -rf $(EXTENSIONS_DIR)/micro_ros_dev; \
rm -rf $(EXTENSIONS_DIR)/micro_ros_src;

$(EXTENSIONS_DIR)/esp32_toolchain.cmake: $(EXTENSIONS_DIR)/esp32_toolchain.cmake.in
rm -f $(EXTENSIONS_DIR)/esp32_toolchain.cmake; \
cat $(EXTENSIONS_DIR)/esp32_toolchain.cmake.in | \
sed "s/@CMAKE_C_COMPILER@/$(subst /,\/,$(X_CC))/g" | \
sed "s/@CMAKE_CXX_COMPILER@/$(subst /,\/,$(X_CXX))/g" | \
sed "s/@IDF_TARGET@/$(subst /,\/,$(IDF_TARGET))/g" | \
sed "s/@IDF_PATH@/$(subst /,\/,$(IDF_PATH))/g" | \
sed "s/@BUILD_CONFIG_DIR@/$(subst /,\/,$(BUILD_DIR)/config)/g" \
> $(EXTENSIONS_DIR)/esp32_toolchain.cmake

$(EXTENSIONS_DIR)/micro_ros_dev/install:
rm -rf micro_ros_dev; \
mkdir micro_ros_dev; cd micro_ros_dev; \
Expand Down Expand Up @@ -91,13 +81,19 @@ $(EXTENSIONS_DIR)/micro_ros_src/src:
cp -rfL $(EXTRA_ROS_PACKAGES) src/extra_packages || :; \
test -f src/extra_packages/extra_packages.repos && cd src/extra_packages && vcs import --input extra_packages.repos || :;


$(EXTENSIONS_DIR)/micro_ros_src/install: $(EXTENSIONS_DIR)/esp32_toolchain.cmake $(EXTENSIONS_DIR)/micro_ros_dev/install $(EXTENSIONS_DIR)/micro_ros_src/src
# A directory's timestamp may stay unchanged after rebuilding its contents.
# Record successful colcon builds explicitly so the combined archive is refreshed.
$(UROS_DIR)/.built: $(EXTENSIONS_DIR)/esp32_toolchain.cmake \
$(BUILD_DIR)/config/sdkconfig.h $(BUILD_DIR)/config/sdkconfig.cmake \
$(EXTENSIONS_DIR)/colcon.meta $(APP_COLCON_META) \
$(EXTENSIONS_DIR)/libmicroros.mk \
$(EXTENSIONS_DIR)/micro_ros_dev/install $(EXTENSIONS_DIR)/micro_ros_src/src
cd $(UROS_DIR); \
unset AMENT_PREFIX_PATH; \
PATH="$(subst /opt/ros/$(ROS_DISTRO)/bin,,$(PATH))"; \
. ../micro_ros_dev/install/local_setup.sh; \
colcon build \
--cmake-clean-cache \
--merge-install \
--packages-ignore-regex=.*_cpp \
--metas $(EXTENSIONS_DIR)/colcon.meta $(APP_COLCON_META) \
Expand All @@ -110,11 +106,11 @@ $(EXTENSIONS_DIR)/micro_ros_src/install: $(EXTENSIONS_DIR)/esp32_toolchain.cmake
-DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \
-DCMAKE_TOOLCHAIN_FILE=$(EXTENSIONS_DIR)/esp32_toolchain.cmake \
-DCMAKE_VERBOSE_MAKEFILE=OFF \
-DIDF_INCLUDES='${IDF_INCLUDES}' \
-DCMAKE_C_STANDARD=$(C_STANDARD) \
-DUCLIENT_C_STANDARD=$(C_STANDARD);
-DUCLIENT_C_STANDARD=$(C_STANDARD) && \
touch $@

$(EXTENSIONS_DIR)/libmicroros.a: $(EXTENSIONS_DIR)/micro_ros_src/install
$(EXTENSIONS_DIR)/libmicroros.a: $(UROS_DIR)/.built
mkdir -p $(UROS_DIR)/libmicroros; cd $(UROS_DIR)/libmicroros; \
for file in $$(find $(UROS_DIR)/install/lib/ -name '*.a'); do \
folder=$$(echo $$file | sed -E "s/(.+)\/(.+).a/\2/"); \
Expand Down
85 changes: 85 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
# Run with: cmake -S tests -B /tmp/microros-toolchain-tests
cmake_minimum_required(VERSION 3.16)
project(microros_toolchain_tests NONE)

include("${CMAKE_CURRENT_LIST_DIR}/../cmake/microros_flags.cmake")
set(test_dir "${CMAKE_CURRENT_BINARY_DIR}/fixtures")
file(MAKE_DIRECTORY "${test_dir}/with spaces")

function(idf_build_get_property output property)
set(${output} "${test_${property}}" PARENT_SCOPE)
endfunction()

function(assert_flag variable expected)
separate_arguments(arguments UNIX_COMMAND "${${variable}}")
if(NOT "${expected}" IN_LIST arguments)
message(FATAL_ERROR "Missing '${expected}' in ${variable}: ${arguments}")
endif()
endfunction()

# IDF 5-style strings plus project-level options, including PSRAM workarounds.
set(CMAKE_C_FLAGS "-march=rv32imc_zicsr_zifencei -mabi=ilp32")
set(CMAKE_CXX_FLAGS "-march=rv32imac_zicsr_zifencei -mabi=ilp32")
set(test_COMPILE_OPTIONS "-mfix-esp32-psram-cache-issue;-Og;-g;-fstack-protector-strong;-Wall;-Werror;-Wp,-DTEST_FORWARD=1")
set(test_C_COMPILE_OPTIONS "-std=gnu17;-Wstrict-prototypes")
set(test_CXX_COMPILE_OPTIONS "-std=gnu++26;-fno-exceptions;-fno-rtti")
set(test_COMPILE_DEFINITIONS "ESP_PLATFORM;TEXT=\"hello world\"")
microros_get_flags(C c_flags)
microros_get_flags(CXX cxx_flags)
assert_flag(c_flags "-march=rv32imc_zicsr_zifencei")
assert_flag(cxx_flags "-march=rv32imac_zicsr_zifencei")
assert_flag(c_flags "-mfix-esp32-psram-cache-issue")
assert_flag(c_flags "-Og")
assert_flag(c_flags "-fstack-protector-strong")
assert_flag(c_flags "-Wp,-DTEST_FORWARD=1")
assert_flag(c_flags "-DTEXT=\"hello world\"")
assert_flag(cxx_flags "-fno-exceptions")
if(c_flags MATCHES "-Wall|-Werror|-std=|-fno-exceptions" OR cxx_flags MATCHES "-std=|-Wstrict-prototypes")
message(FATAL_ERROR "Warnings, language standards or language-specific flags leaked")
endif()

# IDF 6 response files, mixed with plain flags and nested response files.
file(WRITE "${test_dir}/with spaces/nested" "-march=rv32imc_zicsr_zifencei\n-mabi=ilp32\n")
file(WRITE "${test_dir}/flags" "@\"${test_dir}/with spaces/nested\"\n\"-specs=${test_dir}/with spaces/picolibc.specs\"\n")
set(CMAKE_C_FLAGS "@\"${test_dir}/flags\" -fno-builtin-memcpy")
microros_get_flags(C c_flags)
assert_flag(c_flags "-march=rv32imc_zicsr_zifencei")
assert_flag(c_flags "-specs=${test_dir}/with spaces/picolibc.specs")
assert_flag(c_flags "-fno-builtin-memcpy")

# Changing response contents at the same path must change the exported flags.
file(WRITE "${test_dir}/with spaces/nested" "-march=rv32imac_zicsr_zifencei\n-mabi=ilp32\n")
microros_get_flags(C updated_flags)
assert_flag(updated_flags "-march=rv32imac_zicsr_zifencei")
if(updated_flags STREQUAL c_flags)
message(FATAL_ERROR "Response file content change was lost")
endif()

# Repeated toolchain loads must replace stale cache entries, not accumulate flags.
set(MICROROS_C_FLAGS "${c_flags}")
set(MICROROS_CXX_FLAGS "${cxx_flags}")
set(BUILD_CONFIG_DIR "${test_dir}")
set(IDF_TARGET_ARCH riscv)
set(IDF_TARGET esp32c3)
configure_file("${CMAKE_CURRENT_LIST_DIR}/../esp32_toolchain.cmake.in" "${test_dir}/toolchain.cmake" @ONLY)
set(CMAKE_C_FLAGS "-march=wrong" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
set(MICROROS_TOOLCHAIN_FLAGS_APPLIED TRUE CACHE INTERNAL "Old toolchain guard")
include("${test_dir}/toolchain.cmake")
set(first_flags "${CMAKE_C_FLAGS}")
include("${test_dir}/toolchain.cmake")
if(NOT first_flags STREQUAL CMAKE_C_FLAGS OR CMAKE_C_FLAGS MATCHES "march=wrong")
message(FATAL_ERROR "Toolchain retained stale flags or duplicated them")
endif()
set(MICROROS_C_FLAGS "${updated_flags}")
configure_file("${CMAKE_CURRENT_LIST_DIR}/../esp32_toolchain.cmake.in" "${test_dir}/toolchain.cmake" @ONLY)
include("${test_dir}/toolchain.cmake")
assert_flag(CMAKE_C_FLAGS "-march=rv32imac_zicsr_zifencei")
if(CMAKE_C_FLAGS_RELEASE OR CMAKE_CXX_FLAGS_RELEASE)
message(FATAL_ERROR "Release defaults can override the IDF optimization/assertion flags")
endif()

message(STATUS "micro-ROS toolchain flag tests passed")
Loading