Prepare a CMake project for find_package()

Although I’ve already published an article on how to bring an external CMake project into your own CMake project, I recently realized that I haven’t written about how one can prepare a CMake project for the use with find_package() — despite the fact that I’ve used it already for quite a while in RandFill for my own WPDLib(rary); so let me make up leeway for that oversight with this post.

First off, please be aware that this post will focus solely on the above mentioned part; I assume that you already have a basic understanding of how CMake and the C++ development process work.

CMake offers the find_package() command to let one CMake project (e.g. RandomApplication) find a different CMake-based project (or ‘package’, let’s call it LibX) and import LibX’s targets into RandomApplication’s scope for further use.

That begs the question: If you are the author of LibX, how can you set it up, so that someone else (for example RandomApplication) can easily import and use it?

Part 1: Single-Unit Package

I’ll be taking one of my projects as a reference for LibX. The project consists of multiple parts: The library itself, a test application, plus some other stuff:

C:\LibX\src\CMakeLists.txt  
            ...
            Application\
                ...
            Library\
                CMakeLists.txt ----- (1)
                config.cmake.in ---- (2)
                ...

We’re only interested in making the actual library of the project available to a consuming project (i.e. RandomApplication) by a call to find_package(), so the following instructions needs only to be applied to two files beneath the _C:\LibX\src\Library_ subdirectory:

The CMakeLists.txt begins as usual: First we define the (sub)project and create the target Library (a DLL and part of the overarching project LibX), which we will then make available for find_package() during the course of this post, by preparing and exporting the required information — I’ve omitted all the other CMake stuff that is not relevant to the article’s topic:

# You are here: LibX/src/Library/CMakeLists.txt
project ("Library")
add_library (${PROJECT_NAME} SHARED)
# [...]

1.1: Library Alias

Further down in the same file, such a line should appear:

# You are here: LibX/src/Library/CMakeLists.txt
add_library (LibX::${PROJECT_NAME} ALIAS ${PROJECT_NAME})

This creates an alias, so that other projects can use the target by that name (even if add_subdirectory() instead of find_package() is used). That would be a way to build your project from source rather than against a pre-built/installed package. Without the alias, the consumer wouldn’t be able to modify any of the targets' properties.1

1.2: Configuration Files, Target Export and Installation Rules

Then it’s time to prepare the library for installation and export; meaning we setup and configure the INSTALL target and several configuration files, so that other CMake projects can use this with find_package().

Note: Some variables used here in the snippets (such as ${CPU_ARCHITECTURE or ${${PROJECT_NAME}_INSTALL_CMAKEDIR} etc.) are custom ones from my reference project; in general, you should be aware that you can and should adjust the names and paths to your own needs and liking.

Let’s define the variables to the following:

# You are here: LibX/src/Library/CMakeLists.txt
# (if/then...)                    
set (CPU_ARCHITECTURE "x64")
# ...

… and the subdirectories (which should be considered relative to a CMAKE_INSTALL_PREFIX-defined base path of LibX) to:

# You are here: LibX/src/Library/CMakeLists.txt
set (${PROJECT_NAME}_INSTALL_BINDIR     ${CMAKE_PROJECT_NAME}/bin)
set (${PROJECT_NAME}_INSTALL_LIBDIR     ${CMAKE_PROJECT_NAME}/lib)
set (${PROJECT_NAME}_INSTALL_INCLUDEDIR ${CMAKE_PROJECT_NAME}/include)
set (${PROJECT_NAME}_INSTALL_CMAKEDIR   ${CMAKE_PROJECT_NAME}/cmake)

Both are just conventions of my own, no need to copy it verbatim, but the use of a subdirectory named “cmake” fits nicely with the default search procedure of find_package().

1.2.1: Basic Installation Rules

This is first and foremost a basic installation rule for the target’s artifacts.
But in this step, we also mark the target to be put into the ‘export set’ (by the EXPORT statement), which we will need later.

# You are here: LibX/src/Library/CMakeLists.txt
install (
    TARGETS ${PROJECT_NAME}
    EXPORT  ${CMAKE_PROJECT_NAME}Targets
    RUNTIME DESTINATION ${${PROJECT_NAME}_INSTALL_LIBDIR}/${CMAKE_GENERATOR}/${CPU_ARCHITECTURE}
    LIBRARY DESTINATION ${${PROJECT_NAME}_INSTALL_LIBDIR}/${CMAKE_GENERATOR}/${CPU_ARCHITECTURE}
    ARCHIVE DESTINATION ${${PROJECT_NAME}_INSTALL_LIBDIR}/${CMAKE_GENERATOR}/${CPU_ARCHITECTURE}
)

Usually one also sets up an installation rule for other files, e.g. the public header files of the library.
(This is a rather common action for library headers, not strictly needed for the export of the target/package information.)

# You are here: LibX/src/Library/CMakeLists.txt
install (
    FILES
        foo.h
        bar.h
        # ...
    DESTINATION ${${PROJECT_NAME}_INSTALL_INCLUDEDIR}
)

1.2.2: Target Export

Here we make use of the export set that we generated above, and use it to create and an installation rule for a *Target.cmake file (and together with it also specifying the namespace for the target).

This file contains code that is used by an outside CMake project to import targets from your project. By that, the outside CMake project can import and then use your target as if it were one of its own (I believe to remember that there are few exceptions, where an imported target differs slightly to a native target in some edge cases, but I’d need to dig deeper to find it again in the CMake documentation…):

# You are here: LibX/src/Library/CMakeLists.txt
install (
    EXPORT      ${CMAKE_PROJECT_NAME}Targets
    FILE        ${CMAKE_PROJECT_NAME}Targets.cmake
    NAMESPACE   ${CMAKE_PROJECT_NAME}::
    DESTINATION ${${PROJECT_NAME}_INSTALL_CMAKEDIR}
)

We should also export the targets from the build-tree for use by outside projects:

# You are here: LibX/src/Library/CMakeLists.txt
export (
    TARGETS ${PROJECT_NAME}
    FILE    ${PROJECT_BINARY_DIR}/${CMAKE_PROJECT_NAME}Exports.cmake
)

The reason for this is that typically projects are built and installed before being used by an outside project. However, in some cases, an outside project may reference the targets in the build tree (of LibX’s Library) directly, without any prior installation. As such, this target definition is not relocatable.

1.2.3: Package Configuration File and Package Version File

Those files are required by find_package(), so that other projects can find, import and use the targets; see the CMake documentation for Packages for details.

1.3: Usage of a SUP by a client/consumer application

And now that all this is done, I’ll finish this post with an example on how this single-unit package (SUP) that we have created can be used in another project:

RandomApplication wants to make use of the DLL produced by LibX; that means that it first needs to be pointed in the right direction during its configuration step on where the configuration files can be cound; we use CMAKE_PREFIX_PATH for that when we configure RandomApplication:

cmake.exe -D CMAKE_PREFIX_PATH=C:\shared\LibX\cmake -S src -B build

Hereafter, a call to find_package(LibX) should be able to find it.

What follows is a sanitized snippet that I use in one of my projects:

  1. First, it checks whether a local version of LibX can be found (at the place where CMAKE_PREFIX_PATH points, plus some default CMake search locations).
  2. And if not, it will fall back on getting it from the Git repository (more about the use of FetchContent_* can be found in another post of mine):
# You are here: RandomApplication/src/CMakeLists.txt
find_package(LibX)

if (LibX_FOUND)
    message(STATUS "Local installation of LibX found.")
else ()
    message(STATUS "No local installation of LibX found.")
    message(STATUS "Hint: Did you maybe forget to define CMAKE_PREFIX_PATH?")
    message(STATUS "Attempting now to fetch the content from the net...")

    find_package(Git REQUIRED)

    include(FetchContent)

    FetchContent_Declare(
        LibX
        GIT_REPOSITORY https://example.net/git/LibX.git
        GIT_PROGRESS   ON
    )

    if (NOT LibX_POPULATED)
        FetchContent_MakeAvailable(LibX)
        add_subdirectory(${LibX_SOURCE_DIR}/src ${LibX_BINARY_DIR})
    endif ()
endif()

Either way, the LibX targets should then be available within the CMake scripts of RandomApplication, just as if they were native targets of that project, accessible by its namespace:

# You are here: RandomApplication/src/CMakeLists.txt
target_link_libraries (${PROJECT_NAME}
    PUBLIC
        LibX::Library
        Qt5::Core   
        Qt5::Gui    
        Qt5::Widgets
)

Part 2: Multi-Component Package

As mentioned in the beginning, I used this technique already for one of my libraries, but that was a single unit, without much to configure. But I have seen in other projects that one can also control which modules, or components of such a package are included in the other project’s CMake-based build process.

And as it just happens, I’m currently working on a toolkit which consists of multiple individual components under one “umbrella” project; so for that kind of work, let’s now prepare a CMake package from which you can pick individual segments, like ComponentA, ComponentB etc.

2.1: Setup

So, the basics from the previous parts still apply, but have to be modified a bit: LibX acts now as an ‘holding project’ for multiple components (libraries) in its directory hierarchy, which can later be picked explictly by an application.

We now also have to touch the files CMakeLists.txt (1) and config.cmake.in (2) on the top level, not only those same named files (3) and (4) in the component (i.e. library) directories:

LibX/
    src/
        CMakeLists.txt ------------ (1)
        config.cmake.in ----------- (2)
        ComponentA/
            CMakeLists.txt -------- (3)
            config.cmake.in ------- (4)
            ...
        ...

2.1.1: Top-Level/Project Files

For CMake to find this new “umbrella” package by its name, it needs additional configuration files on its project’s root level (beside also the configuration files for each individual component, which you will find further down on this page).

The top-level file CMakeLists.txt of LibX (1) should contain something like what is shown below, which is pretty similar to what we already did in the other part for the single unit.
Excepy that in this file, a few details are not needed, so it’s slightly shorter on this level then what those in the component/library-level files:

# You are here: LibX/src/CMakeLists.txt
# [...]
add_subdirectory("ComponentA")

# ~ Package Configuration File and Package Version File ~
include (CMakePackageConfigHelpers)

configure_package_config_file (
    ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
    INSTALL_DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
)

write_basic_package_version_file (
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    VERSION       ${CMAKE_PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)

install (
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    DESTINATION ${LIBX_INSTALL_CMAKEDIR}
)
# [...]

And the content of the top-level config.cmake.in file (2) here also differs:

We loop over all the components of this project (the list will be assembled automatically by CMake), to load the configuration files into the umbrella project’s scope, so that RandomApplication will have access to all components later on:

# You are here: LibX/src/config.cmake.in
@PACKAGE_INIT@
foreach (component ${@CMAKE_PROJECT_NAME@_FIND_COMPONENTS})
    include (${CMAKE_CURRENT_LIST_DIR}/${component}Config.cmake)
endforeach ()

2.1.2: Component-Level Files

In the ComponentA’s CMakeLists.txt (3) also a few things need to be adjusted:

Most notably the COMPONENT entry in install (EXPORT...) and the NAMESPACE entry in export (TARGETS ...), both highlighted below with a comment.

Other than that, depending on your structure or requirements, maybe some variables name or paths may need to change. In one of my cases, when I switched from an existing single-unit package to a multi-component package, I needed to replace the ${CMAKE_PROJECT_NAME} variable (that has the name of the top-level project) at several locations in the script with the sub-project/component/library’s local name, ${PROJECT_NAME}:

# You are here: LibX/src/ComponentA/CMakeLists.txt

# Namespaced alias for find_package(); it's up to you, if/how you use it.
add_library (LibX::${PROJECT_NAME} ALIAS ${PROJECT_NAME})

# [...]

# ~ Basic Installation Rules (header-only library in this example) ~
install (
    FILES
        foo.h
	DESTINATION ${LIBX_INSTALL_INCLUDEDIR}
)

install (
    TARGETS ${PROJECT_NAME}
    EXPORT  ${PROJECT_NAME}Targets
)

# ~ Export the targets ~
install (
    EXPORT      ${PROJECT_NAME}Targets
    FILE        ${PROJECT_NAME}Targets.cmake
    NAMESPACE   LibX::
    COMPONENT   ${PROJECT_NAME} # <--------------------- COMPONENT NAME
    DESTINATION ${LIBX_INSTALL_CMAKEDIR}
)

export (
    TARGETS   ${PROJECT_NAME}
    NAMESPACE LibX:: # <------------------------------ NAMESPACE PREFIX
    FILE      ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Exports.cmake
)

# ~ Package Configuration File and Package Version File ~
include (CMakePackageConfigHelpers)

configure_package_config_file (
    ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
    INSTALL_DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
)

write_basic_package_version_file (
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    VERSION       ${CMAKE_PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)

install (
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    DESTINATION ${LIBX_INSTALL_CMAKEDIR}
)

The config.cmake.in of ComponentA (4) stays more or less as described in part 2, just that we should replace @CMAKE_PROJECT_NAME@ with @PROJECT_NAME@:

# You are here: LibX/src/ComponentA/config.cmake.in
@PACKAGE_INIT@
include (${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake)
check_required_components (@PROJECT_NAME@) # Recommended

And that’s it; repeat this for any other ComponentB, ComponentC and so on, then you can use it in a client/consumer applications like it was described at the top of the page.

This works of course only when you install the files at the correct place, at which you’ve pointed the client before via the aforementioned define:
cmake -D CMAKE_PREFIX_PATH:PATH=C:\shared\LibX\cmake ...

2.2: Usage of a MCP by a client/consumer application

After all that, a client/consumer application like RandomApplication then could make use of such multi-component package (MCP) by selecting specific components with find_package().

But first, the client again needs to point the CMake call of RandomApplication to the right directory for the package configuration; that happens again via a command line definition of the prefix path; e.g. -D CMAKE_PREFIX_PATH:PATH=C:\shared\LibX\cmake.

After that, the following code in the CMakeLists.txt file of RandomApplication should work like this:

# Your are here: RandomApplication/src/CMakeLists.txt
# [...]

find_package (LibX REQUIRED
    COMPONENTS ComponentA
)

add_executable (${PROJECT_NAME})

target_sources (${PROJECT_NAME}
    PRIVATE
        main.cpp
)

target_link_libraries (${PROJECT_NAME}
    LibX::ComponentA
)
# [...]

And that’s it for now; have fun!


  1. ↩︎