diff --git a/CMakeLists.txt b/CMakeLists.txt index e1d5c82e..8b496bbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}") @@ -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} ) diff --git a/README.md b/README.md index 85a8343a..2d7a3fd7 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/cmake/microros_flags.cmake b/cmake/microros_flags.cmake new file mode 100644 index 00000000..80d7cf06 --- /dev/null +++ b/cmake/microros_flags.cmake @@ -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() diff --git a/esp32_toolchain.cmake.in b/esp32_toolchain.cmake.in index 3ec2333c..b7833ed7 100644 --- a/esp32_toolchain.cmake.in +++ b/esp32_toolchain.cmake.in @@ -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) @@ -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) diff --git a/libmicroros.mk b/libmicroros.mk index d1be4951..ecabe6b3 100644 --- a/libmicroros.mk +++ b/libmicroros.mk @@ -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; \ @@ -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) \ @@ -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/"); \ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..e45b0311 --- /dev/null +++ b/tests/CMakeLists.txt @@ -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")