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.

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.

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" DESCRIPTION "Some library")

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()

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 go 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)

target_sources(${PROJECT_NAME}
    PRIVATE
        src/some.cpp
)

Here the library is defined as STATIC, but there will be a section about shared libraries too.

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.

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()

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}")

Not related to public headers, but 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")

And here finally come the actual installation instructions:

# definitions of CMAKE_INSTALL_LIBDIR, CMAKE_INSTALL_INCLUDEDIR and others
include(GNUInstallDirs)

# paths for binaries and headers
install(TARGETS ${PROJECT_NAME}
    EXPORT "${PROJECT_NAME}Config"
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # lib
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # lib
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # bin
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} # include
    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} # include/SomeLibrary
)
# CMake config name, namespace and path
install(
    EXPORT "${PROJECT_NAME}Config"
    FILE "${PROJECT_NAME}Config.cmake"
    NAMESPACE some::
    DESTINATION cmake
)

The NAMESPACE property is exactly what is looks like - a namespace of your library. I reckon, that will help to avoid names collision, but most likely it will allow to group stuff in the 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 zip the install folder contents and distribute it to your users.

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 library installation folder, so CMake could find its config
set(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.

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!

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.

Going deeper

Package version

You can set a version for your library (cmake-library-example/internal-project/libraries/SomeLibrary/CMakeLists.txt):

project("SomeLibrary" DESCRIPTION "Some library")
set(version 0.9.1)

# ...

include(CMakePackageConfigHelpers)

write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
    VERSION "${version}"
    COMPATIBILITY AnyNewerVersion
)
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
    DESTINATION cmake
)

As a result you will get one more CMake config file in the install folder: SomeLibraryConfigVersion.cmake.

If you now try to find the 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

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, simply replace STATIC with SHARED in add_library statement (in the library’s CMakeLists.txt).

So, first thing that is 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.

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=TRUE ..

If you haven’t set SHARED in add_library and it also doesn’t explicitly state STATIC, then you can also set -DBUILD_SHARED_LIBS=TRUE option to make that library SHARED.

Either way, now you can build and install the library as usual. However, if you are using Visual Studio CMake generator, then -DCMAKE_BUILD_TYPE won’t work, because you need to specify configuration on build step:

$ 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 is available in this repository.