Introduction
CMake is a cross-platform software for building projects written in C, C++, Fortran, CUDA and so on. CMake utilizes build-systems such as Ninja, Linux make, Visual Studio, and Xcode. It compiles projects with compilers like GCC, Clang, Intel, MS Visual C++.
CMake is frequently used in compiling open-source and commercial projects. It has comprehensive but daunting manual instruction. In this post, instead of throwing instructions for some random commands, I aim to explain how to employ modern CMake step by step to build executables (applications) and static/shared/header-only libraries from C++ projects.
Prerequisits
I am assuming
- you had a look at my post on CMake programming,
- you have CMake v3.23 on your machine,
- you have a compiler like GCC, Clang, Intel, or MS Visual C++ installed on your operating system.
Compile examples
Examples are on GitHub here and their links are mentioned in each section as well. To build an example, go to its directory in a terminal and run
mkdir build
cd build
Usual build configurations are Debug, Release, RelWithDebInfo and MinSizeRel.
For single configuration generators like make
and Ninja run:
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
For multi-configuration generators like Visual Studio or Xcode, the configuration can be set at the building stage:
cmake ..
cmake --build . --config Release
To install targets, after building the project, run CMake like
cmake --install . --prefix "/home/myuser/installdir " --config Release
Note that --config Release
only works for multi-configuration generators. For a single configuration generator, you already set the CMAKE_BUILD_TYPE before building the project.
Basic executable
The code for this example is here on GitHub. It is a simple executable (or application). The project folder is like this:
---example1
|
----- app.cpp
|
----- shape.cpp
|
----- shape.h
|
----- CMakeLists.txt
app.cpp
includes shape.h
. The CMakeLists.txt
is
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
add_executable(app)
target_sources(app PRIVATE app.cpp shape.cpp shape.h)
install(TARGETS app)
add_executable()
: used to declare the executable target,app
. You can choose any other name instead ofapp
.target_sources()
: we add source files necessary to compileapp
target.PRIVATE
: to say that sources are just for creatingapp
target and nothing else.install()
: will copy the compiled executable intopath/to/install/directory/bin
when you runcmake --install blah blah
Executable with subdirectories
The code for this example is here on GitHub.
In this example, we have source files and headers distributed in subdirectories:
--- geometry
|
---- shape
| |
| --- shape.h
| |
| --- shape.cpp
| |
| --- CMakeLists.txt
|
---- square
| |
| --- square.h
| |
| --- square.cpp
| |
| --- CMakeLists.txt
|
---- app.cpp
|
---- CMakeLists.txt
Where geometries/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.20)
project(geometries LANGUAGES CXX)
add_executable(app)
target_sources(app PRIVATE "app.cpp")
target_include_directories(app PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")
install(TARGETS app)
add_executable()
: is to defineapp
target.target_sources()
: to add the source in the currrent directory,app.cpp
, toapp
target.target_include_directories()
: To tell CMake that the project directory tree contains headers. In this way, we can have headers from different directories added to each other with a relative path to the project directory. For example,square.h
can have#include "shape/shape.h"
.PRIVATE
: fortarget_*
means the added files and directories are just for creating targets, not for linking to them.add_subdirectory()
: to tell CMake to go into those subdirectories as there are more logics there in theirCMakeLists.txt
files.
shape/CMakeLists.txt
is just
target_sources(app PRIVATE shape.cpp shape.h)
and square/CMakeLists.txt
is
target_sources(app PRIVATE square.cpp square.h)
So we added sources in the subdirectories to app
target.
Executable with namespace
The code for this example is here on GitHub.
This example is similar to a big project with namespaces. Namespaces are used to avoid name conflicts in a project, read more on them in this post. The CMake script is very similar to the previous example.
The project folder is like
--- geometry
|
---- shape
| |
| --- base.h, base.cpp
| |
| --- CMakeLists.txt
|
---- rectangle
| |
| --- base.h, base.cpp
| |
| --- CMakeLists.txt
|
---- square
| |
| --- base.h, base.cpp
| |
| --- CMakeLists.txt
|
---- app.cpp
|
---- CMakeLists.txt
app.cpp
is
#include "square/base.h"
#include "rectangle/base.h"
using namespace Geometry;
int main() {
Square::Base s;
Rectangle::Base r;
Shape::Print(s);
Shape::Print(r);
return 0;
}
base
files represent base class for different shapes. It is assumed the developer will derived more classes from them. We have files and classes with the same name, but they are elegently resolved with namespaces and CMake.
The app/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.20)
project(geometry LANGUAGES CXX)
add_executable(app)
target_sources(app PRIVATE "app.cpp")
target_compile_features(app PRIVATE cxx_std_20)
target_include_directories(app PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")
add_subdirectory("rectangle")
install(TARGETS app)
target_compile_features()
: to tell CMake that we need C++20 for compiling this project. There are haigh-level features likecxx_std_11
,cxx_std_14
and low-level ones likecxx_constexpr
andcxx_auto_type
. Adding low-level ones, CMake automatically figures out which standard to use. See more features here.
CMakeLists.txt
in shape, rectangle and square are the same:
target_sources(app PRIVATE base.cpp base.h)
Shared library
The code for this example is here on GitHub.
In this example, we compile a library and link it to an executable
--- geometry
|
---- shape
| |
| --- shape.h, shape.cpp
| |
| --- CMakeLists.txt
|
---- square
| |
| --- square.h, square.cpp, info.h
| |
| --- CMakeLists.txt
|
---- example
| |
| --- app.cpp
|
---- CMakeLists.txt
The geometries/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
if (MSVC)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
add_library(geo SHARED)
target_include_directories(geo PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")
add_executable(app)
target_sources(app PRIVATE "example/app.cpp")
target_link_libraries(app PRIVATE geo)
install(TARGETS geo FILE_SET HEADERS)
if (MSVC)
: checking CMake is employing MS Visual C++.CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
: This is necessary for MSVC to create a symbol file,.lib
, besides a shared library,.dll
.add_library()
: to define a library target,geo
.SHARED
means a shared library, you can also make a static library withSTATIC
keyword, or an object file withOBJECT
keyword.target_include_directories()
: is for making source files aware of the location of private headers relative to the project directory.target_link_libraries()
: to tell CMake thatapp
is dependent ongeo
library. So first compilegeo
then link it toapp
executable.install(TARGETS)
: to install compiled libraries and their headers in the assigned install directory you set when runningcmake --install blah blah
. Executables and windows dll files go intobin
directory, libraries go intolib
directory, and public headers go intoinclude
directory at the destination.Install(TARGETS)
has keywords to fine-tune the destination directories, see this example:
install(TARGETS myTarget
# for executables and dll on Win
RUNTIME DESTINATION bin
# shared libraries
LIBRARY DESTINATION lib
# for static libraries
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include)
For more info see CMake manual here.
shape/CMakeLists.txt
is
target_sources(geo
PRIVATE shape.cpp
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES shape.h)
square/CMakeLists.txt
is
target_sources(geo
PRIVATE square.cpp info.h
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES square.h)
PRIVATE
:info.h
is a private header of this library, a user doesn’t need to know about it. The same for*.cpp
files as they are only needed to compile the library, but a user doesn’t need them.PUBLIC
: Any files added afterPUBLIC
is used for compiling the library and included for any other target that linking to this library.FILE_SET HEADERS BASE_DIRS ${PROJECT_SOURCE_DIR} FILES square.h
: is a CMake 3.23 feature. We know to link to a library, we need its public headers. This line makes sure any other target linking togeo
, gets aware of the header location. The base directory is droped from header file path so it will be accessible with a relative path. For example/path/to/geometry/square/square.h
will be included assquare/square.h
.
Therefore, in the geometry/CMakeLists.txt
, this line
install(TARGETS geo FILE_SET HEADERS)
means that CMake installs the public headers in the include
directory with their relative path, like install/path/include/square/square.h
.
Header-only library
The code for this example is here on GitHub.
A header-only library has all the implementations defined in headers. There are .h
/.hpp
files and but no .cpp
files except for tests. This type of library is not compiled standalone. But other projects link to them. Therefore, when a header-only library is installed, only header files are copied at the destination.
The example for this case has the below structure:
--- geometry
|
---- shape
| |
| --- shape.h
| |
| --- CMakeLists.txt
|
---- square
| |
| --- square.h
| |
| --- CMakeLists.txt
|
---- examples
| |
| --- example1.cpp
|
--- geometry.h
|
---- CMakeLists.txt
The example1.cpp
is like this
#include "geometry.h"
int main() {
Square s;
PrintShape(s);
return 0;
}
The geometry/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
add_library(geo INTERFACE)
target_sources(geo INTERFACE
FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES "geometry.h")
add_subdirectory("shape")
add_subdirectory("square")
add_executable(example1)
target_sources(example1 PRIVATE "examples/example1.cpp")
target_link_libraries(example1 PRIVATE geo)
install(TARGETS geo FILE_SET HEADERS)
add_library(geo INTERFACE)
: Here INTERFACE means it is a header-only library, i.e. this library is not compiled.target_sources()
: heregeometry.h
is declared and mentioned to be an interface header. Any other project linking to this library should be aware of this and its location. The subdirectories using the same technique.
shape/CMakeLists.txt
is
target_sources(geo
INTERFACE FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES shape.h)
and square/CMakeLists.txt
is
target_sources(geo
INTERFACE FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES square.h)
More on CMake
My next post is on creating config files in CMake to find package. And in case you haven’t seen it, my previous post was on programming in CMake.
Even more
Designing a big project needs a good understanding of namespaces, see my post on how namespaces are used in big projects.
Did you know CMake is supported by Visual Studio code? have a look at my essential list of VS code extensions for C++.
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks