CMake part 2: Examples to build executable and library projects

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 of app.
  • target_sources(): we add source files necessary to compile app target.
  • PRIVATE: to say that sources are just for creating app target and nothing else.
  • install(): will copy the compiled executable into path/to/install/directory/bin when you run cmake --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 define app target.

  • target_sources(): to add the source in the currrent directory, app.cpp, to app 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: for target_* 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 their CMakeLists.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 like cxx_std_11, cxx_std_14 and low-level ones like cxx_constexpr and cxx_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 with STATIC keyword, or an object file with OBJECT 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 that app is dependent on geo library. So first compile geo then link it to app executable.

  • install(TARGETS): to install compiled libraries and their headers in the assigned install directory you set when running cmake --install blah blah. Executables and windows dll files go into bin directory, libraries go into lib directory, and public headers go into include 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 after PUBLIC 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 to geo, 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 as square/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(): here geometry.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++.

Tags ➡ C++

Subscribe

I notify you of my new posts

Latest Posts

Comments

0 comment