CMake Components #
CMake provide means of dividing a library into parts. This allows consumers of the library to express which parts of the library they depend on.
Example: Setting #
Let’s consider the case of developing a library useful. It has only one
dependency finicky and it provides functionality that’s not required in all
usecases of useful. The assumption is that finicky extends the capabilities
of useful and therefore the API available to consumers of useful.
Regardless, we’d like it to be an optional dependency of useful, maybe
because finicky is prone to build failure or takes too long to compile.
The things to consider are:
- How does
usefulmakefinickyoptional? - How do consumers of
usefulstate that they need the optional API that relies onfinicky?
What Are CMake Components? #
Simplistic answer: components are the things behind the two colons, e.g.
target_link_libraries(app PUBLIC useful::finicky)
where useful::finicky is a component of useful. This will setup all the
required include and linking flags required to link with the finicky
component of useful.
How to write useful?
#
Step 1: optionally compile the optional API. #
Start with an option to turn finicky support on:
option(USEFUL_HAS_FINICKY "Enable finicky support" Off)
This allows us to perform step such as finding, linking with finicky and
later installing the optional API.
if(USEFUL_HAS_FINICKY)
find_package(finicky REQUIRED)
add_library(${PROJECT_NAME}_finicky)
target_include_directories(${PROJECT_NAME}_finicky ...)
target_sources(${PROJECT_NAME}_finicky ...)
set_target_properties(${PROJECT_NAME}_finicky PROPERTIES EXPORT_NAME finicky)
target_link_libraries(${PROJECT_NAME}_finicky PUBLIC finicky::finicky)
target_compile_definitions(${PROJECT_NAME}_finicky PUBLIC USEFUL_HAS_FINICKY)
endif()
Step 2: Generate targets for the core. #
These next steps are standard and we’ll rush through them:
add_library(${PROJECT_NAME} SHARED)
target_sources(${PROJECT_NAME} ...)
target_include_directories(${PROJECT_NAME} ...)
Step 3: Generate and install usefulConfig.cmake.
#
The code in CMakeLists.txt is the same as always:
# Generate usefulConfig.cmake
configure_file(...)
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmake
DESTINATION lib/cmake/${PROJECT_NAME}
)
Note that the name usefulConfig.cmake has meaning to CMake. When finding a
package, CMake will look for usefulConfig.cmake and useful-config.cmake in
a fixed set of directories. Once it finds one of them it runs the commands
inside. We are responsible for performing the concrete steps required when
loading a component.
# file: cmake/usefulConfig.cmake.in
include(CMakeFindDependencyMacro)
# These targets we provide unconditionally:
include("${CMAKE_CURRENT_LIST_DIR}/usefulTargets.cmake")
# If `finicky` is requested:
if(finicky IN_LIST useful_FIND_COMPONENTS)
message("-- Preparing useful::finicky")
# Hard-code `USEFUL_HAS_FINICKY` so that the installed version knows which
# settings it was built with:
set(USEFUL_HAS_FINICKY @USEFUL_HAS_FINICKY@)
if(NOT USEFUL_HAS_FINICKY)
message(FATAL_ERROR "Library wasn't built with `useful::finicky` component.")
endif()
# Make the dependency `finicky` available.
find_dependency(finicky)
# Provide all target needed for `useful::finicky`.
include(${CMAKE_CURRENT_LIST_DIR}/usefulFinickyTargets.cmake)
endif()
The two files ${CMAKE_CURRENT_LIST_DIR}/useful*Targets.cmake will be
generated and installed for us.
Step 4: Generate and install target files. #
We’ll want two *Target.cmake files. One containing the targets the provide
unconditionally and another for all the targets associated with the finicky
component.
Generating a *Target.cmake file is done in two steps:
install(
TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}Targets
)
install(
EXPORT ${PROJECT_NAME}Targets
FILE ${PROJECT_NAME}Targets.cmake
NAMESPACE ${PROJECT_NAME}::
DESTINATION lib/cmake/${PROJECT_NAME}
)
Similarly for the second set of targets:
if(USEFUL_HAS_FINICKY)
install(
TARGETS ${PROJECT_NAME}_finicky
EXPORT ${PROJECT_NAME}FinickyTargets
COMPONENT finicky
)
install(
EXPORT ${PROJECT_NAME}FinickyTargets
FILE ${PROJECT_NAME}FinickyTargets.cmake
NAMESPACE ${PROJECT_NAME}::
DESTINATION lib/cmake/${PROJECT_NAME}
)
endif()
Step 5: Install the headers. #
Next we need to make a choice about which headers we install. We’ll follow the low-effort path and install everything. This way the user needs to pick what they want to include.
install(
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/useful
DESTINATION include
)
How to use useful?
#
Without finicky
#
It’s built with
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=build/install -B build .
$ cmake --build build --target install
and then consumers find and link as follows:
find_package(useful)
target_link_libraries(app PUBLIC useful::useful)
With finicky
#
It’s built with
$ cmake -DCMAKE_PREFIX_PATH=${FINICKY_DIR} \
-DCMAKE_INSTALL_PREFIX=build-finicky/install \
-DUSEFUL_HAS_FINICKY=On \
-B build .
$ cmake --build build --target install
and then consumers find and link as follows:
find_package(useful COMPONENTS finicky CONFIG)
target_link_libraries(${PROJECT_NAME} PUBLIC useful::useful useful::finicky)
Note that CMake will only search for the library finicky if the component
finicky of the useful package is loaded. If not, not attempt is made to
find finicky.