CMake Components

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:

  1. How does useful make finicky optional?
  2. How do consumers of useful state that they need the optional API that relies on finicky?

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.