Creating a C++ library with CMake
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.
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 addinclude
, because that’s where public headers are in the library’s source folderINSTALL_INTERFACE
is meant for external projects, and here you don’t need to addinclude
, 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 #includethat 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:
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.
2023-07-22 | About target_link_libraries() scopes
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.
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks