Best Practice: Separate CMake targets for declaration and definition?

I have a project where we have multiple threading libraries: Threading_Linux, Threading_Baremetal, etc. They all implement the same threading interfaces (function declarations and C++ interfaces), declared in it’s own INTERFACE Threading library. In turn, our business logic libraries (or other higher level libraries) will only link to Threading, instead of a specific platform Threading implementation. Then, when our application is assembled it will look something like below.

# Threading
add_library(Threading INTERFACE)

target_sources(Threading INTERFACE IThreading.h)

target_include_directories(Threading INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

# Threading_Linux
add_library(Threading_Linux)

target_sources(Threading_Linux PRIVATE Threading_Linux.h Threading_Linux.cpp)

target_include_directories(Threading_Linux PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

target_link_libraries(Threading_Linux PUBLIC Threading)

# BusinessLogic_Foo
add_library(BusinessLogic_Foo)

target_sources(BusinessLogic_Foo PRIVATE Foo.h Foo.cpp)

target_include_directories(BusinessLogic_Foo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

target_link_libraries(BusinessLogic_Foo PRIVATE Threading) # Only the interfaces are leveraged

# App
add_executable(App)

target_source(App PRIVATE main.cpp)

target_link_libraries(App
   PRIVATE
      BusinessLogic_Foo
      Threading_Linux
)

I believe link order does become important here such that Threading_Linux needs to be last (as described here.

Is this common practice? Does it violate CMake practices to provide a target (BusinessLogic_Foo) that itself does not have the complete definitions and doesn’t explicitly specify where to find the definition? Would there be a different way to do this? It is beneficial in cross platform systems or if we wanted to inject a fake threading library for testing BusinessLogic_Foo.

I would make just one target, which in its C++ code selects the appropriate implementation using #if SOMETHING, and these macros can be added to the target using target_compile_definitions().

In this scenario, who would set/request the definition? Would the Threading library set target_compile_definitions() based on whatever platform we’re configuring for?

This wouldn’t support if we wanted the ability to link BusinessLogic_Foo to a Threading_Fake or Threading_Linux implementation on the same platform. For example, a test application could link together BusinessLogic_Foo and Threading_Fake, while the real application could link together BusinessLogic_Foo and Threading_Linux

Yes. I would expose the various backends as CMake options, but have some default logic to select a sensible backend in the event the user hasn’t selected one manually.

So you want to be able to build multiple backends simultaneously? You’re right, this strategy would not support that.

Here is a library of mine that works very similarly to the solution I suggested: Limes/libs/limes_vecops at main · benthevining/Limes · GitHub

This is a library that wraps various platform-specific vector operations libraries, like Intel IPP, Apple vDSP, etc. Within CMake, there is only one target for this library, and its selection of backend is controlled using macro definitions. In the C++ code, I do some error checking of these macros and also set their defaults if they are not defined: Limes/vecops_macros.h at main · benthevining/Limes · GitHub

And in the CMake code, the user can select any of the available backends using cache variables, and there is also some default selection logic if the user doesn’t pick one: Limes/SelectImpl.cmake at main · benthevining/Limes · GitHub