CMake part 3: create a config file to be found by find_package()

Introduction

We write CMake for our library and it compiles correctly. However, a user of the library needs to write some CMake code to find the include headers, shared/static libraries, and executables. This extra task for the user of our library can be a pain. We as the developers of the library can add several boilerplate lines to our CMake script to make everything get imported by the user with a simple line of find_package().

In this post, with an example, I show how to create

  • a version file and
  • a config file for a library so it can be easily imported to other projects.

Prerequisites

This post is the third on CMake, I assume you had a look at the previous ones : CMake programming and build with CMake.

Example

The example code is on GitHub here. It is a shared library with the file structure below:

--- geometry
    |
    ---- shape
    |      |
    |      --- shape.h, shape.cpp
    |      |
    |      --- CMakeLists.txt
    |
    ---- square
    |      |
    |      --- square.h, square.cpp, info.h
    |      |
    |      --- CMakeLists.txt
    |
    ---- example
    |      |
    |      --- app.cpp
    |
    ---- CMakeLists.txt

CMakeLists.txt looks like this:

cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX VERSION 5.4.3)

if (MSVC)
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()

add_library(geo SHARED)
target_include_directories(geo PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")

install(TARGETS geo 
    EXPORT geoTargets
    FILE_SET HEADERS
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include)

install(EXPORT geoTargets
    FILE geoTargets.cmake
    NAMESPACE geo::
    DESTINATION lib/cmake/geo)

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    "geoConfigVersion.cmake"
    VERSION ${geo_VERSION}
    COMPATIBILITY AnyNewerVersion)

install(FILES "geoConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/geoConfigVersion.cmake"
    DESTINATION lib/cmake/geo)

The targets and dependencies in this example are explained in the previous post on CMake. So I continue from there.

  • install(TARGETS geo EXPORT geoTargets ...): here EXPORT defines geoTargets variable. Somehow it says that if you need to export targets of geo, work with geoTargets.

  • install(EXPORT geoTargets): This is the mate of the previous install command, we define the name export target file, namespace, and where this file will be installed. The namespace is a prefix added to name of the package when it is loaded in other projects. So geo target will be seen as geo::geo in other projects.

  • include(CMakePackageConfigHelpers): This is a module loaded by CMake to create a config file.

  • write_basic_package_version_file(): This function creates geoConfigVersion.cmake file. It stores version information of the package and its compatibility. AnyNewerVersion means CMake imports the package if its version is newer or the same version as the requested one. For more details, see CMake Manual.

  • install(FILES...): to copy the config file and version file into the installation directory. The version file is created by this script. But geoConfig.cmake is written by us.

geometry/geoConfig.cmake is

include(CMakeFindDependencyMacro)
# find_dependency(xxx 2.0)
include(${CMAKE_CURRENT_LIST_DIR}/geoTargets.cmake)

I commented find_dependency() line because this example doesn’t have any other dependency. If your package has any dependency, they need to be mentioned here.

That’s it.

Build and Install

Now we can compile and install the library. In the source directory in a terminal, run

mkdir build
cd build

For single configuration generators like make or Ninja

cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
cmake --install . --prefix ../../geo-install

For a multi-configuration generator like VS C++ or Xcode:

cmake ..
cmake --build . --config release
cmake --install . --prefix ../../geo-install --config release

Find the package in another project

I created another example (see the code on GitHub), mesh project, which uses geo library. The file structure of it is like this:

--- mesh
    |
    ---- app.cpp
    |
    ---- CMakeLists.txt

app.cpp is

#include "square/square.h"
#include<iostream>
int main() {
	
	Square s;
	s.WriteInfo();
	PrintShape(s);

	return 0;
}

So I am using a header of the library and some implementations.

mesh/CMakeLists.txt file is

cmake_minimum_required(VERSION 3.23)
project(mesh LANGUAGES CXX)

set(geo_DIR "../geo-install/lib/cmake/geo")
find_package(geo)
add_executable(app)
target_sources(app PRIVATE "app.cpp")
target_link_libraries(app PRIVATE geo::geo)
if(MSVC)
    get_target_property(geo_dll geo::geo IMPORTED_LOCATION_RELEASE)
    add_custom_command(TARGET app POST_BUILD 
        COMMAND "${CMAKE_COMMAND}" -E copy 
        ${geo_dll}
        ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG> )
endif()

The key lines for importing geo library are

  • set(geo_DIR ...): (nameOfpackage)_DIR is where CMake looks for a package. This is the address of the installed config files.
  • find_package(geo): asking CMake to include the package and its targets.
  • target_link_libraries: remember to use the namespace you set for your package, here geo::geo.
  • if(MSVC) ...: this block is only for Windows to copy dll files from the package installed directory to the executable directory. This is because on Windows, the location of shared libraries, dll files, is not coded into the executable that uses them.
  • get_target_property(geo_dll ...): sets geo_dll to the path to the binary file (dll) of installed geo.
  • add_custom_command: I used it here for copying a file after build. You can run any customized task with this command, read CMake Manual here.
  • $<CONFIG>: will be defined as Debug, Release or so on depending on the cofiguration at build time.
    Now you can compile and run your program, in the source directory
mkdir build
cmake ..
cmake --build .
Debug/app.exe

More on CMake

In case you haven’t seen them, my previous posts on CMake are:

Even more

Designing a big project needs a good understanding of namespaces, see my post on how namespaces are used in big projects.

Did you know CMake is supported by Visual Studio code? have a look at my essential list of VS code extensions for C++.

Tags ➡ C++

Subscribe

I notify you of my new posts

Latest Posts

Comments

4 comments
Skywalker347510 30-Sep-2022
Thanks very much for the 3 posts, it really helped me to understand what can be done with cmake and how to do it.
BDisp 24-Dec-2022
Great posts, thanks. Instead of use in the mesh project: ${CMAKE_CURRENT_BINARY_DIR}/Release I would use a per-configuration output sub-directory: ${CMAKE_CFG_INTDIR} In this case on Windows it will work for `\mesh\out\build\x64-debug\app.exe` and for `\mesh\build\Debug\app.exe`
tinu73 29-Jul-2023
Thanks for this great post. I'm using a similar project structure, compared to your example, sources are in shape/src and square/src, headers are in shape/include/shape and square/include/square. And in my case headers do include header from other library folder. When i try to compile i get the error: fatal error: .h: No such file or directory, #include ".h". What i'm missing in CMakeLists.txt?
Sorush 20-May-2024
Thanks @BDISP for the good point. I used $ instead of ${CMAKE_CFG_INTDIR}.