All of the sudden I found myself in a situation that I have been successfully avoiding so far - I needed to make a C++ library with CMake.

CMake and a library

To clarify, this will be about so-called normal kind of library. And right away, the picture above shows a bad example of naming a library, because there should be no lib neither in the beginning nor in the end of the library name.

The library

Folder structure and sources

For the sake of focusing on CMake side of things, the library itself is very trivial:

├── CMakeLists.txt
├── include
│   └── some.h
└── src
    ├── some.cpp
    └── things.h

This particular folder structure is not enforced, but I’ve seen it being used around and I think it serves nicely for the purpose of keeping library files organized. Following this structure, you put internal library sources and headers to src folder, and public headers go to include folder. This article, however, lists a few disadvantages of such approach.

Public headers is something other projects will use to interface with your library. That is how they will know what functions are available in it and what is their signature (parameters names and types). So, include/some.h is a public header, and here are its contents:

#ifndef SOME_H
#define SOME_H

namespace sm
{
    namespace lbr
    {
        void printSomething();
    }
}

#endif // SOME_H

So our library has just one function. Its definition is in src/some.cpp:

#include <iostream>
#include "things.h"

namespace sm
{
    namespace lbr
    {
        void printSomething()
        {
            std::cout << "ololo, " << someString << std::endl;
        }
    }
}

As you can see, this function just prints a message to standard output. The someString variable comes from internal header src/things.h:

#include <string>

const std::string someString = "some string";

CMakeLists

Making a library with CMake is not that different from making an application - instead of add_executable you call add_library. But doing just that would be too easy, wouldn’t it.

Here are some of the things you need to take care of:

  • what artifacts should the library produce at install step
  • where install artifacts should be placed
  • how other applications can find the library
    • when they are using it pre-built as an external dependency
    • when its sources are nested in their source tree
  • will it be static or shared library
    • will you need to have it as DLL on Windows

Everything from this list is handled by CMake. So let’s gradually create a CMakeLists.txt for the library project.

Top-level and nested projects

In CMake projects there is a variable called CMAKE_PROJECT_NAME. It stores the top-level project name that you set with project command. This variable persists across all the nested projects, and so calling project command from nested projects will not change CMAKE_PROJECT_NAME, but will set another variable called PROJECT_NAME.

Knowing that, here’s how you can check if you are in the top-level project or not:

project("SomeLibrary"
    # ...
)

if (NOT CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
    message(STATUS "This project has a top-level one called [${CMAKE_PROJECT_NAME}]")
else()
    message(STATUS "This project is a top-level one")
endif()

Later CMake versions added a PROJECT_IS_TOP_LEVEL variable, which might be more convenient.

Why even bother with this? Because later we will be setting certain properties for the target (our library). And I saw in lots of places how people copy-paste project name value to every command, which I believe is just a bad idea - it is much better to use already defined PROJECT_NAME variable instead, innit.

Target

Here goes the library target and its sources:

# here you can see how instead of writing "SomeLibrary"
# we can just use the PROJECT_NAME variable
add_library(${PROJECT_NAME} STATIC)

# no need to add headers here, only sources are required
target_sources(${PROJECT_NAME}
    PRIVATE
        src/some.cpp
)

In this case the library is declared as STATIC, but actually it is not a good idea to hardcode libraries type like that in their project files, because CMake has a global flag for this exact purpose - BUILD_SHARED_LIBS - and in general it’s better to rely on that flag instead of setting libraries type inline.

There will be also a section about shared libraries later, but in this particular case hardcoding the library type to STATIC somewhat makes sense, because I do not export its symbols for making a DLL, so on Windows it wouldn’t build a proper library as SHARED.

Name without lib prefix/postfix

Like I said in the beginning, the someLib name on the picture is a bad example of naming a library, as there should be no lib anywhere in the name (/(^[lL][iI][bB])|([lL][iI][bB]$)/g), so libSome would also be a bad name. The SomeLibrary name, on the other hand, is okay-ish, because it neither starts with lib nor ends with lib (it only has lib in the middle).

The reason why one should not have lib prefix/postfix in one’s library name is because it will be set in an appropriate manner (depending on the target platform) by CMake automatically. So, if you have your library named someLib, then on Unix platforms it will become libsomeLib.a (and someLib.lib on Windows), which already doesn’t look great, and if you have it named libSome, then the resulting binary will be liblibSome.a, which is even worse.

It is of course possible to manipulate the output names explicitly, but there is already enough things to worry about, so I would just leave this to CMake and rely on the default behaviour by simply not having lib in the library name.

Include directories

Setting include directories correctly with target_include_directories is very important:

target_include_directories(${PROJECT_NAME}
    PRIVATE
        # where the library itself will look for its internal headers
        ${CMAKE_CURRENT_SOURCE_DIR}/src
    PUBLIC
        # where top-level project will look for the library's public headers
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        # where external projects will look for the library's public headers
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Paths in PRIVATE section are used by the library to find its own internal headers. So if you would place things.h to ./src/hdrs/things.h, then you will need to set this path to ${CMAKE_CURRENT_SOURCE_DIR}/src/hdrs.

Paths in PUBLIC section are used by projects that link to this library. That’s where they will look for its public headers:

  • BUILD_INTERFACE path is meant for projects that will build the library from their source tree, and here you need to add include, because that’s where public headers are in the library’s source folder
  • INSTALL_INTERFACE is meant for external projects, and here you don’t need to add include, because CMake config will do that for you

Install instructions

We need to declare what artifacts should be put to installation directory after building the library. You also need to specify the path of installation directory (where you would like it to be).

Certainly, just building the library is already enough to be able to link to it, but we want to do it in the most comfortable way: not by providing paths to its binaries and headers from both build and sources directories, but by installing just the artifacts we need and using find_package command.

With find_package you let CMake to worry about finding the library, its public headers and configuring all that. Here’s a more detailed documentation about how find_package works, and here’s how you can create a CMake config of your own.

Installation path

First thing to think about it is the installation path. If you will not set it during configuration step via CMAKE_INSTALL_PREFIX (-DCMAKE_INSTALL_PREFIX="/some/path"), then CMake will set it to some system libraries path, which you might not want to use, especially if you are building your library for distribution.

Here’s how you can overwrite default installation path to install the artifacts into install folder in your source tree:

# note that it is not CMAKE_INSTALL_PREFIX we are checking here
if(DEFINED CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
    message(
        STATUS
        "CMAKE_INSTALL_PREFIX is not set\n"
        "Default value: ${CMAKE_INSTALL_PREFIX}\n"
        "Will set it to ${CMAKE_SOURCE_DIR}/install"
    )
    set(CMAKE_INSTALL_PREFIX
        "${CMAKE_SOURCE_DIR}/install"
        CACHE PATH "Where the library will be installed to" FORCE
    )
else()
    message(
        STATUS
        "CMAKE_INSTALL_PREFIX was already set\n"
        "Current value: ${CMAKE_INSTALL_PREFIX}"
    )
endif()

Public headers

Next thing you need to do is to declare PUBLIC_HEADER property:

# without it public headers won't get installed
set(public_headers
    include/some.h
)
# note that ${public_headers} has to be in quotes
set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER "${public_headers}")

Note, however, that this doesn’t preserve the folder structure, and so for more complex projects that won’t give the desired result. For instance, here are the public headers of the glad library:

├── include
     ├── KHR
          └── khrplatform.h
     └── glad
         └── glad.h
└── src
    └── glad.c

Trying to install it, you’ll get the following result in the installation path:

├── cmake
     ├── gladConfig-release.cmake
     └── gladConfig.cmake
├── include
     └── glad
         ├── glad.h
         └── khrplatform.h
└── lib
    └── libglad.a

which won’t work for khrplatform.h header, as it is expected to be included like this:

#include <KHR/khrplatform.h>

In order to preserve the folder structure, you can use the following trickery:

# for CMAKE_INSTALL_INCLUDEDIR definition
include(GNUInstallDirs)

# the variant with PUBLIC_HEADER property unfortunately does not preserve the folder structure
#set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER "${public_headers}")
# so instead we iterate through public headers and install them "manually"
foreach(header ${public_headers})
    file(RELATIVE_PATH header_file_path "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}" "${header}")
    get_filename_component(header_directory_path "${header_file_path}" DIRECTORY)
    install(
        FILES ${header}
        DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${header_directory_path}"
    )
endforeach()

Debug suffix

It might be a good idea to add d suffix to debug binaries - that way you’ll get libSomeLibraryd.a with Debug configuration and libSomeLibrary.a with Release. To do that you need to set the DEBUG_POSTFIX property:

set_target_properties(${PROJECT_NAME} PROPERTIES DEBUG_POSTFIX "d")

Or you can set it globally for the entire project on configuration with -DCMAKE_DEBUG_POSTFIX="d".

Destinations

Here come the actual installation instructions:

# if you haven't included it already
# definitions of CMAKE_INSTALL_LIBDIR, CMAKE_INSTALL_INCLUDEDIR and others
include(GNUInstallDirs)

# install the target and create export-set
install(TARGETS ${PROJECT_NAME}
    EXPORT "${PROJECT_NAME}Targets"
    # these get default values from GNUInstallDirs, no need to set them
    #RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # bin
    #LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # lib
    #ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # lib
    # except for public headers, as we want them to be inside a library folder
    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} # include/SomeLibrary
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} # include
)

Important to note here that INCLUDES is not part of the RUNTIME/LIBRARY/ARCHIVE/PUBLIC_HEADER group. See for yourself in the install() signature:

install(
    TARGETS targets... [EXPORT <export-name>]
    [RUNTIME_DEPENDENCIES args...|RUNTIME_DEPENDENCY_SET <set-name>]
    [
        [
            ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE
        ]
        [DESTINATION <dir>]
        [PERMISSIONS permissions...]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>]
        [NAMELINK_COMPONENT <component>]
        [OPTIONAL] [EXCLUDE_FROM_ALL]
        [NAMELINK_ONLY|NAMELINK_SKIP]
    ] [...]
    [INCLUDES DESTINATION [<dir> ...]]
)

If you won’t have INCLUDES in the install(), then SomeLibraryTargets.cmake won’t have these lines:

set_target_properties(some::SomeLibrary PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)

and when you’ll try to use this (installed) library in an external project, it will configure fine, but it will fail to build:

$ cmake --build .
[ 50%] Building CXX object CMakeFiles/another-application.dir/main.cpp.o
/path/to/cmake-library-example/external-project/main.cpp:2:10: fatal error: 'SomeLibrary/some.h' file not found
#include <SomeLibrary/some.h>
         ^~~~~~~~~~~~~~~~~~~~
1 error generated.
make[2]: *** [CMakeFiles/another-application.dir/main.cpp.o] Error 1
make[1]: *** [CMakeFiles/another-application.dir/all] Error 2
make: *** [all] Error 2

Configs

Create Config.cmake.in file:

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")

check_required_components(@PROJECT_NAME@)

CMake documentation doesn’t mention it in a clear way, but you can still use the PROJECT_NAME variable here too - just wrap it in @@.

And then in CMakeLists.txt:

# generate and install export file
install(EXPORT "${PROJECT_NAME}Targets"
    FILE "${PROJECT_NAME}Targets.cmake"
    NAMESPACE ${namespace}::
    DESTINATION cmake
)

include(CMakePackageConfigHelpers)

# generate the version file for the config file
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
    VERSION "${version}"
    COMPATIBILITY AnyNewerVersion
)
# create config file
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
    "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
    INSTALL_DESTINATION cmake
)
# install config files
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
    DESTINATION cmake
)
# generate the export targets for the build tree
# (can't say what this one is for, but so far it has been only causing me problems,
# so I stopped adding it to projects)
# export(EXPORT "${PROJECT_NAME}Targets"
#     FILE "${CMAKE_CURRENT_BINARY_DIR}/cmake/${PROJECT_NAME}Targets.cmake"
#     NAMESPACE ${namespace}::
#)

The write_basic_package_version_file() function from above will create SomeLibraryConfigVersion.cmake file in the install folder. Having it, if you now try to find your package in external project (cmake-library-example/external-project/CMakeLists.txt) like this:

find_package(SomeLibrary 0.9.2 CONFIG REQUIRED)

then you will get the following error on configuration:

CMake Error at CMakeLists.txt:9 (find_package):
  Could not find a configuration file for package "SomeLibrary" that is
  compatible with requested version "0.9.2".

  The following configuration files were considered but not accepted:

    /Users/YOURNAME/code/cpp/cmake-library-example/internal-project/install/cmake/SomeLibraryConfig.cmake, version: 0.9.1

Finally, the NAMESPACE property is exactly what is looks like - a namespace of your library. I reckon, that will help to avoid names collision and also allow to group related stuff in one namespace, similar to how Qt does it:

target_link_libraries(helloworld Qt5::Widgets)

Building and installing

Go to library source tree root and run the usual:

$ mkdir build && cd $_

$ cmake ..
-- The C compiler identification is AppleClang 12.0.0.12000032
-- The CXX compiler identification is AppleClang 12.0.0.12000032
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- This project is a top-level one
-- CMAKE_INSTALL_PREFIX is not set
Default value: /usr/local
Will set it to /Users/YOURNAME/code/cpp/someLibrary/install
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/YOURNAME/code/cpp/someLibrary/build

$ cmake --build . --target install
Scanning dependencies of target SomeLibrary
[ 50%] Building CXX object CMakeFiles/SomeLibrary.dir/src/some.cpp.o
[100%] Linking CXX static library libSomeLibrary.a
[100%] Built target SomeLibrary
Install the project...
-- Install configuration: ""
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/lib/libSomeLibrary.a
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/include/SomeLibrary/some.h
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/cmake/SomeLibraryConfig.cmake
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/cmake/SomeLibraryConfig-noconfig.cmake

$ tree ../install/
├── cmake
│   ├── SomeLibraryConfig-noconfig.cmake
│   └── SomeLibraryConfig.cmake
├── include
│   └── SomeLibrary
│       └── some.h
└── lib
    └── libSomeLibrary.a

Note that SomeLibraryConfig-noconfig.cmake has this weird noconfig suffix. This is because we ran configuration without specifying the build type - better to explicitly set it then, both Debug and Release:

$ rm -r ../install/* && rm -r ./* && cmake -DCMAKE_BUILD_TYPE=Debug ..

$ cmake --build . --target install
Scanning dependencies of target SomeLibrary
[ 50%] Building CXX object CMakeFiles/SomeLibrary.dir/src/some.cpp.o
[100%] Linking CXX static library libSomeLibraryd.a
[100%] Built target SomeLibrary
Install the project...
-- Install configuration: "Debug"
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/lib/libSomeLibraryd.a
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/include/SomeLibrary/some.h
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/cmake/SomeLibraryConfig.cmake
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/cmake/SomeLibraryConfig-debug.cmake

$ rm -r ./* && cmake -DCMAKE_BUILD_TYPE=Release ..

$ cmake --build . --target install
Scanning dependencies of target SomeLibrary
[ 50%] Building CXX object CMakeFiles/SomeLibrary.dir/src/some.cpp.o
[100%] Linking CXX static library libSomeLibrary.a
[100%] Built target SomeLibrary
Install the project...
-- Install configuration: "Release"
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/lib/libSomeLibrary.a
-- Up-to-date: /Users/YOURNAME/code/cpp/someLibrary/install/include/SomeLibrary/some.h
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/cmake/SomeLibraryConfig.cmake
-- Installing: /Users/YOURNAME/code/cpp/someLibrary/install/cmake/SomeLibraryConfig-release.cmake

$ tree ../install/
├── cmake
│   ├── SomeLibraryConfig-debug.cmake
│   ├── SomeLibraryConfig-release.cmake
│   └── SomeLibraryConfig.cmake
├── include
│   └── SomeLibrary
│       └── some.h
└── lib
    ├── libSomeLibrary.a
    └── libSomeLibraryd.a

So there you have it! The library has been successfully built and nicely installed, so now you can just pack the install folder contents (you might want to use CPack for that) and distribute it to your users.

CMake generators

While we are here, you should probably know about such thing as CMake generators. Trying to put it simply, I would say that generator is what transforms CMake project files/scripts into specific build instructions for a particular build tool.

As you hopefully are aware, there are several build tools available. We have GNU make, Microsoft NMAKE, Ninja, xcodebuild and so on. And depending on which one you would like to use, you need to choose a particular CMake generator for that.

To list all the available generators for the current platform you can run CMake with an “empty” -G argument, for example here’s what I have on Mac OS:

$ cmake -G
CMake Error: No generator specified for -G

Generators
* Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Ninja Multi-Config           = Generates build-<Config>.ninja files.
  Watcom WMake                 = Generates Watcom WMake makefiles.
  Xcode                        = Generate Xcode project files.
  CodeBlocks - Ninja           = Generates CodeBlocks project files
                                 (deprecated).
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files
                                 (deprecated).
  CodeLite - Ninja             = Generates CodeLite project files
                                 (deprecated).
  CodeLite - Unix Makefiles    = Generates CodeLite project files
                                 (deprecated).
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files
                                 (deprecated).
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files
                                 (deprecated).
  Kate - Ninja                 = Generates Kate project files (deprecated).
  Kate - Ninja Multi-Config    = Generates Kate project files (deprecated).
  Kate - Unix Makefiles        = Generates Kate project files (deprecated).
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files
                                 (deprecated).
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files
                                 (deprecated).

All the builds in this article are done with Unix Makefiles generator, because that is the default one on Mac OS, and back then I simply didn’t know anything about CMake generators, so I was just obliviously using what was set by default.

Now, when I know a little bit more, I would recommend to use Ninja any day of the week (for it’s the fastest of them all). But if you need to generate a project for your IDE, then you’d need to use an IDE-specific generator, such as Xcode or Visual Studio 17 2022 (there are several versions of that one).

One other thing to mention here is that specifying Release or Debug build configuration is different between generators. In particular, for Unix Makefiles and Ninja you would do it with CMAKE_BUILD_TYPE on configuration:

$ cd /path/to/project
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..
$ cmake --build . --target install

while for Xcode and Visual Studio 17 2022 that would be via config argument on build:

$ cd /path/to/project
$ mkdir build && cd $_
$ cmake -G "Visual Studio 17 2022" ..
$ cmake --build . --target install --config Release

Linking to the library

Now let’s see how your users or yourself can link to the library.

From external project

Let’s take a simple project:

$ cd ~/code/cpp/external-project

$ tree .
├── CMakeLists.txt
└── main.cpp

The source:

#include <iostream>
// note that you need to prepend some.h with the folder name,
// because that is how it is in the installation folder:
// install/include/SomeLibrary/some.h
#include <SomeLibrary/some.h>

int main(int argc, char *argv[])
{
    std::cout << "base application message" << std::endl;
    // here we call a function from the library
    sm::lbr::printSomething();
}

The project file:

project("another-application" VERSION 0.9 DESCRIPTION "A project with external library")

# provide the path to library's installation folder, so CMake could find its config
#
# for simplicity it is set right here, but actually it is a bad idea
# to hardcode these paths in project files, as usually they are set via CLI,
# for example: -DCMAKE_PREFIX_PATH="/Users/YOURNAME/code/cpp/someLibrary/install"
list(APPEND CMAKE_PREFIX_PATH "/Users/YOURNAME/code/cpp/someLibrary/install")
# the rest will be taken care of by CMake
find_package(SomeLibrary CONFIG REQUIRED)

# it is an application
add_executable(${PROJECT_NAME})

target_sources(${PROJECT_NAME}
    PRIVATE
        main.cpp
)

# linking to the library, here you need to provide the namespace too
target_link_libraries(${PROJECT_NAME} PRIVATE some::SomeLibrary)

The PRIVATE keyword means that the library will be used by this project, but it will not be available to other projects via this project’s interface. For example, if this project is in turn a library itself, then yet another external/parent project linking to it won’t be able to use printSomething() function from the underlying library (this is not exactly true, here are some more details about that).

Let’s now try to build and run the application:

$ mkdir build && cd $_

$ cmake -DCMAKE_BUILD_TYPE=Release ..

$ cmake --build .
Scanning dependencies of target another-application
[ 50%] Building CXX object CMakeFiles/another-application.dir/main.cpp.o
[100%] Linking CXX executable another-application
[100%] Built target another-application

$ ./another-application
base application message
ololo, some string

It works!

No need to set include_directories and use magic variables

You might have seen in other projects that they also set include_directories for external libraries, and probably you are now wondering why I didn’t do it here, and how then it all works.

Indeed, if we take a look at one of such projects, for example SDL2 installed via Homebrew on Mac OS:

$ brew info sdl2
sdl2: stable 2.0.16 (bottled), HEAD
Low-level access to audio, keyboard, mouse, joystick, and graphics
https://www.libsdl.org/
/usr/local/Cellar/sdl2/2.0.16 (91 files, 5.4MB)

$ tree /usr/local/Cellar/sdl2/2.0.16
/usr/local/Cellar/sdl2/2.0.16
├── ...
├── bin
 └── sdl2-config
├── include
 └── SDL2
     ├── SDL.h
     └── ...
├── lib
 ├── cmake
  └── SDL2
      ├── sdl2-config-version.cmake
      └── sdl2-config.cmake
 ├── libSDL2-2.0.0.dylib
 ├── libSDL2.a
 ├── libSDL2.dylib -> libSDL2-2.0.0.dylib
 ├── libSDL2_test.a
 ├── libSDL2main.a
 └── pkgconfig
     └── sdl2.pc
└── share
    └── aclocal
        └── sdl2.m4

Thanks to Homebrew, it is discoverable even without adding its path to CMAKE_PREFIX_PATH:

# and once again, it's better to set that via CLI: -DCMAKE_PREFIX_PATH="/usr/local/Cellar/sdl2/2.0.16"
#list(APPEND CMAKE_PREFIX_PATH "/usr/local/Cellar/sdl2/2.0.16")
find_package(SDL2 REQUIRED)
if(${SDL2_FOUND})
    message(STATUS "Found SDL: ${SDL2_PREFIX}")
endif()

# ...

target_link_libraries(${CMAKE_PROJECT_NAME}
    PRIVATE
        ${SDL2_LIBRARIES}
)

But trying to build your application you’ll get this error:

fatal error: 'SDL.h' file not found
#include <SDL.h>
         ^~~~~~~
1 error generated.

To fix that you’ll also need to add the following to the project file:

include_directories(
    ${SDL2_INCLUDE_DIRS}
)

That is because SDL2 CMake package was created in a rather obsolete manner, so you have to manually set include_directories and also to use these magic variables such as _INCLUDE_DIRS and _LIBRARIES.

Our library package is written in a more modern way, so including directories is already taken care of, and also there is no need to use any magic variables, just the package name. Nice, innit.

From internal top-level project

But what if we have our library as a part of some other top-level project, so the library lives in its source tree? Do we still need to build it first and add it to the main project via find_package? Not exactly - now there is no need to “find” it: the library will be built together with the parent project and then linked to.

Adding nested library to the main project

Here’s the full project structure:

$ cd /Users/YOURNAME/code/cpp/internal-project

$ tree .
├── CMakeLists.txt
├── libraries
│   ├── CMakeLists.txt
│   └── SomeLibrary
│       ├── CMakeLists.txt
│       ├── include
│       │   └── some.h
│       └── src
│           ├── some.cpp
│           └── things.h
└── main.cpp

The main CMakeLists.txt:

project("some-application" VERSION 0.9 DESCRIPTION "A project with nested library")

add_subdirectory(libraries)

add_executable(${PROJECT_NAME})

target_sources(${PROJECT_NAME}
    PRIVATE
        main.cpp
)

target_link_libraries(${PROJECT_NAME} PRIVATE SomeLibrary)

Yes, it is all the same as with external project - we just need to link to the library. No crazy relative paths, just the very same target_link_libraries.

But this time we don’t need find_package and also we don’t need to provide the namespace. That last part I don’t entirely understand, but perhaps that is because everything in the project is supposed to be within the same namespace already?

Following add_subdirectory statement, we get to libraries folder, which also has a CMakeLists.txt:

add_subdirectory(SomeLibrary)

And there we get to our library project.

About include paths

Let’s start with main.cpp:

#include <iostream>
#include <some.h>

int main(int argc, char *argv[])
{
    std::cout << "base application message" << std::endl;
    sm::lbr::printSomething();
}

While the code here is the same as the one from external project, there is one notable difference - some.h is not prepended with SomeLibrary/ in the #include statement.

Why is that, why is it different from the way in was included in external project? Well, it is because that is how this header is placed in the library’s source include folder - there is no SomeLibrary folder nested there. But when you install the library, then there is SomeLibrary folder inside include in the installation folder, so in that case you need to add SomeLibrary/ to #include statement.

I wasn’t sure if that is really how things are, so I went and asked about this on StackOverflow, hoping that there is perhaps some way to handle this in a unified way, but the answer was:

You think too "magically" about what CMake can. CMake just calls a compiler/linker with proper parameters. A compiler requires for #include that a header file will be in the SomeLibrary directory. CMake cannot overcome this requirement.

So if you would like to unify the way you include the library’s public headers both in external and internal projects, then you’ll need to create SomeLibrary folder inside library’s source include folder (yeah, to get the ugly SomeLibrary/include/SomeLibrary path) and adjust the library’s CMakeLists.txt accordingly.

Building

Main project

Do the usual in the project source tree root:

$ mkdir build && cd $_
$ cmake -DCMAKE_BUILD_TYPE=Debug ..

$ cmake --build .
Scanning dependencies of target SomeLibrary
[ 25%] Building CXX object libraries/SomeLibrary/CMakeFiles/SomeLibrary.dir/src/some.cpp.o
[ 50%] Linking CXX static library libSomeLibraryd.a
[ 50%] Built target SomeLibrary
Scanning dependencies of target some-application
[ 75%] Building CXX object CMakeFiles/some-application.dir/main.cpp.o
[100%] Linking CXX executable some-application
[100%] Built target some-application

$ ./some-application
base application message
ololo, some string

Eeeeeeeasy!

Library as a target

We can also build and install just the library, without building the entire project. Aside from just going to the library folder and running CMake from there, you can actually do it from the project root - by setting --target option on build:

$ rm -r ./* && cmake -DCMAKE_BUILD_TYPE=Debug ..

$ cmake --build . --target SomeLibrary
Scanning dependencies of target SomeLibrary
[ 50%] Building CXX object libraries/SomeLibrary/CMakeFiles/SomeLibrary.dir/src/some.cpp.o
[100%] Linking CXX static library libSomeLibraryd.a
[100%] Built target SomeLibrary

$ cmake --install ./libraries/SomeLibrary
-- Install configuration: "Debug"
-- Installing: /Users/YOURNAME/code/cpp/cmake-library-example/internal-project/install/lib/libSomeLibraryd.a
-- Installing: /Users/YOURNAME/code/cpp/cmake-library-example/internal-project/install/include/SomeLibrary/some.h
-- Installing: /Users/YOURNAME/code/cpp/cmake-library-example/internal-project/install/cmake/SomeLibraryConfig.cmake
-- Installing: /Users/YOURNAME/code/cpp/cmake-library-example/internal-project/install/cmake/SomeLibraryConfig-debug.cmake

Here you can also see how you can install a single target with --install option - by pointing it to the target folder inside build folder. Also note that install folder is now on the project’s source tree root level, not in the library’s nested source folder.

STATIC vs SHARED

Hopefully, you already know the difference between static and shared libraries. If not, then, to put it simple, static libraries are “bundled” into your binaries, and shared libraries are separate files which need to be discoverable by your binaries in order for the latter to work.

A little practical example: let’s build our library as static, link to it from external project, then build it as shared and link to that one.

To make our library shared, we need to replace STATIC with SHARED in add_library statement in the library’s CMakeLists.txt. And once again, like I already said, the library type should not be hardcoded like that, as it would be better to have add_library() without type and instead set -DBUILD_SHARED_LIBS=1 on project configuration.

Anyway, first thing that will be different about resulting executable (another-application) is its size:

  • statically linked with SomeLibrary: 51 920 bytes
  • dynamically linked with SomeLibrary: 51 616 bytes

Not a very noticeable difference (remember that our library just prints a line of text), but it is there: statically linked executable is bigger, because the library is “bundled” into it.

Secondly, if you now rename or delete libSomeLibrary.dylib (libSomeLibrary.so, SomeLibrary.dll), then trying to run this dynamically linked application you’ll get an error like this on Mac OS:

$ ./another-application
dyld: Library not loaded: @rpath/libSomeLibrary.dylib
  Referenced from: /Users/YOURNAME/code/cpp/cmake-library-example/external-project/build/./another-application
  Reason: image not found
Abort trap: 6

or like this on Linux:

$ ./another-application
./another-application: error while loading shared libraries: libSomeLibrary.so: cannot open shared object file: No such file or directory

So in case of a shared library on Mac OS or Linux it has to either stay available in its installation path, or be placed into the one of the system libraries paths. Be aware that simply copying it to the same folder with executable won’t work, unless you set the LD_LIBRARY_PATH variable on Linux (or DYLD_FALLBACK_LIBRARY_PATH/DYLD_LIBRARY_PATH on Mac OS) before running the application:

$ LD_LIBRARY_PATH="." ./another-application

And on Windows it will fail even if you haven’t touched the library in its install folder, however you can just copy it to the same folder where executable is, but more on that below.

SHARED DLL on Windows

Shared libraries on Windows are a special thing. There it is not enough just to replace STATIC with SHARED in add_library statement (or set -DBUILD_SHARED_LIBS=1).

If you build and install it having done nothing else, then you will get this error trying to configure a project that needs to link to it:

CMake Error at C:/code/cmake-library-example/internal-project/libraries/SomeLibrary/install/cmake/SomeLibraryConfig.cmake:72 (message):
  The imported target "some::SomeLibrary" references the file

     "C:/code/cmake-library-example/internal-project/libraries/SomeLibrary/install/lib/SomeLibrary.lib"

  but this file does not exist.  Possible reasons include:

  * The file was deleted, renamed, or moved to another location.

  * An install or uninstall procedure did not complete successfully.

  * The installation package was faulty and contained

     "C:/code/cmake-library-example/internal-project/libraries/SomeLibrary/install/cmake/SomeLibraryConfig.cmake"

  but not all the files it references.

Call Stack (most recent call first):
  CMakeLists.txt:9 (find_package)

And indeed, there is no SomeLibrary.lib in install/lib/, only SomeLibrary.dll in install/bin/. That is because a DLL on Windows needs an explicit listing of all the symbols that it will export, and apparently this is what SomeLibrary.lib is supposed to be.

As I understood, in order to produce it, in past it was required to add __declspec compiler directives to every public single class or function declaration in your library sources, which is quite a bummer, especially if you have a lot of those. Here’s one example of how this is done.

Fortunately, starting with CMake 3.4, this is no longer required. Instead you can just set the CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS option when configuring the library:

$ cmake -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=1 ..

However, I saw somewhere that it is not recommended to export all the symbols like that. I didn’t quite get why, but probably there is a reason, so just keep that in mind.

Either way, now you can build and install the library as usual. Note that if you are using Visual Studio generator, then -DCMAKE_BUILD_TYPE won’t work, because with this generator the configuration type is specified on build:

$ cmake --build . --target install --config Release

After that configuring and building the external application will also succeed, because now it will get that missing SomeLibrary.lib.

However, unlike Mac OS and Linux, trying to run the resulting application will fail even if you haven’t touched the SomeLibrary.dll in the install folder:

DLL was not found

or, if running from Git BASH:

$ ./Release/another-application.exe
C:/code/cmake-library-example/external-project/build/Release/another-application.exe: error while loading shared libraries: api-ms-win-crt-heap-l1-1-0.dll: cannot open shared object file: No such file or directory

That is because on Windows you need to explicitly put the DLL either to the same folder where the executable is or somewhere in PATH.

Final words and repository

Later I will probably update the article, because, like I said, here I touched only “normal” libraries, but there are also other kinds. Plus, one can go further than just making a CMake config and create a proper package. So there are quite a few things left to talk about.

You might have noticed that most of the stuff I was doing on Mac OS, but actually everything (library and sample applications) builds and works just fine also on Linux and Windows.

Full source code of the library, its parent project and external project are available in this repository.


Updates

2022-07-05 | Updated repository

Be aware that I’ve been updating the referenced repository as I was discovering new things, but not everything has been synced-up back to the article, and so by now the article contains a somewhat simpler CMake code, while repository has a bit more advanced things (such as installation instructions being a separate CMake module).

You might also want to take a look at the article about using CPack and its example project (you can ignore the packing parts), as in particular it has a better organized installation and demonstrates re-using “shared” CMake modules.

2022-09-17 | Dynamic libraries and paths

A useful addition about dynamic libraries and paths.

At some point later I also wrote about target_link_libraries() scopes in particular. There you will find yet another repository, which I now consider to be my best example of creating C++ libraries with CMake (until I learn some more CMake and make an even better one). Among other improvements, it includes exporting symbols for making a .lib file for DLLs on Windows.