Interesting.
I have recently extended this same line of thought to other compile options, to actually build totally distinct targets, e.g. with different -march=...
arguments. This comes up a lot in embedded SDK’s, where one outer project will contain “executables” (statically linked ELFs) for multiple different chips.
This is the minimum working example I ended up with:
cmake_minimum_required(VERSION 3.25)
# This include will set up the toolchain for the embedded target; arm-gcc will be found from your system path
include(tools/cmake/toolchains/arm-gcc.cmake)
## Flags for ALL C/C++ compilation could are set here
# TODO: verify here that having the "-f..." args come first doesn't cause any issues
set( COMMON_FLAGS "-ffunction-sections -fdata-sections -fmessage-length=0" )
SET(CMAKE_CXX_FLAGS_INIT "${COMMON_FLAGS}")
SET(CMAKE_C_FLAGS_INIT "${COMMON_FLAGS}")
set(CMAKE_ASM_FLAGS_INIT "-mcpu=cortex-m4 ${FPU_FLAGS} -mthumb") ## TODO: unclear how to resolve this ... maybe -x assembler-as-cpp ?
SET(CMAKE_EXE_LINKER_FLAGS_INIT "-Wl,-gc-sections,--print-memory-usage")
## These will be universal across all targets
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 14)
project(cmake_multi_cpu_target_test C CXX ASM)
add_library( CMx_common INTERFACE )
set( CMx_common_opts "-mthumb;-mno-unaligned-access;-Og;-g3;-Wall" )
target_compile_options( CMx_common BEFORE INTERFACE ${CMx_common_opts} )
target_link_options( CMx_common BEFORE INTERFACE ${CMx_common_opts} )
add_library( CM3_softfp INTERFACE )
set( CM3_opts "-mcpu=cortex-m3;-mfloat-abi=soft" )
target_compile_options( CM3_softfp BEFORE INTERFACE ${CM3_opts} )
target_compile_definitions( CM3_softfp INTERFACE -DARM_MATH_CM3 )
target_link_options( CM3_softfp BEFORE INTERFACE ${CM3_opts} )
add_library( CM4F_hardfp INTERFACE )
set( CM4f_opts "-mcpu=cortex-m4;-mfloat-abi=hard;-mfpu=fpv4-sp-d16" )
target_compile_options( CM4F_hardfp BEFORE INTERFACE ${CM4f_opts} )
target_compile_definitions( CM4F_hardfp INTERFACE -DARM_MATH_CM4 )
target_link_options( CM4F_hardfp BEFORE INTERFACE ${CM4f_opts} )
add_library( CM7_hardfp INTERFACE )
set( CM7_opts "-mcpu=cortex-m7;-mfloat-abi=hard;-mfpu=fpv5-d16" )
target_compile_options( CM7_hardfp BEFORE INTERFACE ${CM7_opts} )
target_compile_definitions( CM7_hardfp INTERFACE -DARM_MATH_CM7 )
target_link_options( CM7_hardfp BEFORE INTERFACE ${CM7_opts} )
add_library( nosys.specs INTERFACE )
target_compile_options( nosys.specs BEFORE INTERFACE "--specs=nosys.specs" )
target_link_options( nosys.specs BEFORE INTERFACE "--specs=nosys.specs" )
## Exes ##
add_executable(exe_m3 main.c syscalls.c )
target_link_libraries( exe_m3 PUBLIC CMx_common CM3_softfp nosys.specs )
add_executable(exe_m4f main.c syscalls.c )
target_link_libraries( exe_m4f PUBLIC CMx_common CM4F_hardfp nosys.specs )
add_executable(exe_m7 main.c syscalls.c )
target_link_libraries( exe_m7 PUBLIC CMx_common CM7_hardfp nosys.specs )
This seems to work well and is fairly clean, although it does force me to manually create a variant and name-append and target_link_libraries every single dependency.
Maybe there is something wrt to the CMake include scoping that I am skipping over here?
Say, if target-wide options were declared in one file, then all the libraries cmake scripts included after that, etc, so the “trickle-down” or “tree”-like effect of settings options would happen.
I would guess that the generator-based method you are describing is something like that.
I have seen Makefile systems accomplish this, because they set some vars as they go down their tree of includes, then targets get defined with those, etc etc.
Given a dependency graph with the exe as the root, it should be possible to traverse the whole graph and add various options & defines, or link everything in that graph to a specific package that accomplishes that. No idea if that is possible with cmake tooling, though.
And then you’d still have name collisions unless that tree walk also auto-created the new name-appended variants and swapped the dependency link. Which is getting very complex.
A fallback approach that I am going to prototype would be to just elide all the library sub-targets,
and set up the includes & exe targets in a way that a long list of sources, include paths, and macros for each are accumulated, and then the exe itself is just one target with a giant list of sources of include paths.