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:
- (1) The library’s CMakeLists.txt file
- (2) and the library’s configuration input file (in this example named config.cmake.in)
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.
-
CMake also provides some helper functions, which we will use in the following steps; therefore we have to include them first:
# You are here: LibX/src/Library/CMakeLists.txt include (CMakePackageConfigHelpers)
-
Next up, we generate the package configuration file (*Config.cmake) with one of the previously mentioned helper functions, based on an input file (config.cmake.in) which we need to prepare beforehand (see below):
# You are here: LibX/src/Library/CMakeLists.txt configure_package_config_file ( ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}Config.cmake INSTALL_DESTINATION ${CMAKE_CURRENT_BINARY_DIR} )
The above referenced input file config.cmake.in is also located in C:\LibX\src\Library\ in this example.
It’s a very short and simple three-line file, whose variables (enclosed in@...@
) will automatically be replaced by CMake with the proper values:# You are here: LibX/src/Library/config.cmake.in @PACKAGE_INIT@ include (${CMAKE_CURRENT_LIST_DIR}/@CMAKE_PROJECT_NAME@Targets.cmake) check_required_components (@CMAKE_PROJECT_NAME@) # Recommended
(
@PACKAGE_INIT@
is predefined by CMake when youinclude (CMakePackageConfigHelpers)
.) -
Then we generate the package version file (*ConfigVersion.cmake), again with the help one of CMake’s helper functions:
# You are here: LibX/src/Library/CMakeLists.txt write_basic_package_version_file ( ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}ConfigVersion.cmake VERSION ${CMAKE_PROJECT_VERSION} COMPATIBILITY AnyNewerVersion )
-
And at the end, we specify the installation rule for the two previously generated files:
# You are here: LibX/src/Library/CMakeLists.txt install ( FILES ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}Config.cmake ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}ConfigVersion.cmake DESTINATION ${${PROJECT_NAME}_INSTALL_CMAKEDIR} )
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:
- 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). - 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!
Film & Television (54)
How To (63)
Journal (17)
Miscellaneous (4)
News & Announcements (21)
On Software (12)
Projects (26)