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 compileapptarget.PRIVATE: to say that sources are just for creatingapptarget and nothing else.install(): will copy the compiled executable intopath/to/install/directory/binwhen 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 defineapptarget.target_sources(): to add the source in the currrent directory,app.cpp, toapptarget.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.hcan 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.txtfiles.
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_14and low-level ones likecxx_constexprandcxx_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.SHAREDmeans a shared library, you can also make a static library withSTATICkeyword, or an object file withOBJECTkeyword.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 thatappis dependent ongeolibrary. So first compilegeothen link it toappexecutable.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 intobindirectory, libraries go intolibdirectory, and public headers go intoincludedirectory 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.his a private header of this library, a user doesn’t need to know about it. The same for*.cppfiles as they are only needed to compile the library, but a user doesn’t need them.PUBLIC: Any files added afterPUBLICis 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.hwill 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.his 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.
Adding custom commands and custom targets can make your CMake file versatile , see my post here.
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++.
