From sources to executables
Questions
How do we use CMake to compile source files to executables?
Objectives
Learn what tools available in the CMake suite.
Learn how to write a simple
CMakeLists.txt
.Learn the difference between build systems, build tools, and build system generator.
Learn to distinguish between configuration, generation, and build time.
What is CMake and why should you care?
Software is everywhere and so are build systems. Whenever you run a piece of software, anything from calendar apps to computationally-intensive programs, there was a build system involved in transforming the plain-text source code into binary files that could run on the device you are using.
CMake is a build-system generator: it provides a family of tools and a domain-specific language (DSL) to describe what the build system should achieve when the appropriate build tools are invoked. The DSL is platform- and compiler-agnostic: you can reuse the same CMake scripts to obtain native build systems on any platform.
A CMake-based build system:
can bring your software closer to being platform- and compiler-agnostic.
has good support within many integrated development environments (IDEs).
automatically tracks and propagates internal dependencies in your project.
is built on top of well-maintained functionality for automated dependency detection.
Hello, CMake!
Compiling “Hello, world” with CMake
We will now proceed to compile a single source file to an executable. Choose your favorite language and start typing along!
You can find the file with the complete source code in the
content/code/day-1/00_hello-cxx
folder.
#include <cstdlib>
#include <iostream>
int main() {
std::cout << "Hello world" << std::endl;
return EXIT_SUCCESS;
}
A working solution is in the solution
subfolder.
You can find the file with the complete source code in the
content/code/day-1/00_hello-f
folder.
pure function say_hello() result(message)
implicit none
character(len=11) :: message
message = 'Hello world'
end function
program hello_world
implicit none
character(len=11) :: say_hello
print *, say_hello()
end program
A working solution is in the solution
subfolder.
The folder contains only the source code. We need to add a file called
CMakeLists.txt
to it. CMake reads the contents of these special files when generating the build system.The first thing we will do is declare the requirement on minimum version of CMake:
cmake_minimum_required(VERSION 3.18)
Next, we declare our project and its programming language:
project(Hello LANGUAGES CXX)
We create an executable target. CMake will generate rules in the build system to compile and link our source file into an executable:
add_executable(hello hello.cpp)
We are ready to call CMake and get our build system:
$ cmake -S. -Bbuild
And finally build our executable:
$ cmake --build build
There are few things to note here:
Any CMake build system will invoke the following commands in its root
CMakeLists.txt
:cmake_minimum_required(VERSION <min>[...<max>] [FATAL_ERROR])
Parameters
VERSION
Minimum and, optionally, maximum version of CMake to use.
FATAL_ERROR
Raise a fatal error if the version constraint is not satisfied. This option is ignored by CMake >=2.6
project(<PROJECT-NAME> [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]] [DESCRIPTION <project-description-string>] [HOMEPAGE_URL <url-string>] [LANGUAGES <language-name>...])
Parameters
<PROJECT-NAME>
The name of the project.
LANGUAGES
Languages in the project.
The case of CMake commands does not matter: the DSL is case-insensitive. However, the plain-text files that CMake parses must be called
CMakeLists.txt
and the case matters! The variable names are also case sensitive!The command to add executables to the build system is, unsurprisingly,
add_executable
:add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source1] [source2 ...])
Using CMake you can abstract the generation of the build system and also the invocation of the build tools.
Put your CMakeLists.txt
under version control
All CMake-related files will evolve together with your codebase. It’s a good
idea to put them under version control. On the contrary, any of the
generated native build-system files, e.g. Makefile
-s, should not be
version-controlled.
The command-line interface to CMake
Let us get acquainted with the CMake and especially its command-line interface.
We can get help at any time with:
$ cmake --help
This will output quite a number of options to your screen. We can analyze the last few lines first:
Generators
The following generators are available on this platform (* marks default):
* Unix Makefiles = Generates standard UNIX makefiles.
Green Hills MULTI = Generates Green Hills MULTI files
(experimental, work-in-progress).
Ninja = Generates build.ninja files.
Ninja Multi-Config = Generates build-<Config>.ninja files.
Watcom WMake = Generates Watcom WMake makefiles.
CodeBlocks - Ninja = Generates CodeBlocks project files.
CodeBlocks - Unix Makefiles = Generates CodeBlocks project files.
CodeLite - Ninja = Generates CodeLite project files.
CodeLite - Unix Makefiles = Generates CodeLite project files.
Sublime Text 2 - Ninja = Generates Sublime Text 2 project files.
Sublime Text 2 - Unix Makefiles
= Generates Sublime Text 2 project files.
Kate - Ninja = Generates Kate project files.
Kate - Unix Makefiles = Generates Kate project files.
Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files.
Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
In CMake terminology, the native build scripts and build tools are called
generators. On any particular platform, the list will show which native
build tools can be used through CMake. They can either be “plain”, such as
Makefile
-s or Ninja, or IDE-like projects.
The -S
switch specifies which source directory CMake should scan: this is
the folder containing the root CMakeLists.txt
, i.e. the one containing
the project
command.
By default, CMake will allow in-source builds, i.e. storing build
artifacts alongside source files. This is not good practice: you should
always keep build artifacts from sources separate. Fortunately, the -B
switch helps with that, as it is used to give where to store build artifacts,
including the generated build system. This is the minimal invocation of cmake
:
$ cmake -S. -Bbuild
To switch to another generator, we will use the -G
switch:
$ cmake -S. -Bbuild -GNinja
Options to be used at build-system generation are passed with the -D
switch. For example, to change compilers:
$ cmake -S. -Bbuild -GNinja -DCMAKE_CXX_COMPILER=clang++
Finally, you can access the full CMake manual with:
$ cmake --help-full
You can also inquire about a specific module, command or variable:
$ cmake --help-variable CMAKE_GENERATOR
A complete toolchain
The family of tools provided with CMake offers a complete toolchain to manage the development cycle: from sources to build artifacts, testing, and deployment. We refer to these stages as CMake times and each tool is appropriate at a specific time. In this workshop, we will discuss:
CMake time or configure time. This is the stage when
cmake
is invoked to parse theCMakeLists.txt
in your project, configure and generate the build system.Build time. This is handled by the native build tools, but, as we have seen, these can be effectively wrapped by
cmake
itself.CTest time or test time. At this stage, you will test your build artifacts.
Producing libraries
CMake can of course be used to produce libraries as well as executables.
The relevant command is add_library
:
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[<source>...])
You can link libraries into executables with target_link_libraries
:
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
Executables and libraries are targets
We will encounter the term target repeatedly. In CMake, a target is any
object given as first argument to add_executable
or add_library
. Targets
are the basic atom in CMake. Whenever you will need to organize complex
projects, think in terms of its targets and their mutual dependencies. The
whole family of CMake commands target_*
can be used to express chains of
dependencies and is much more effective than keeping track of state with
variables. We will clarify these concepts in Target-based build systems with CMake.
Exercise 1: Producing libraries
You can find a scaffold project in the
content/code/day-1/01_libraries-cxx
folder.
Write a
CMakeLists.txt
to compile the source filesMessage.hpp
andMessage.cpp
into a library. Do not specify the type of library, shared or static, explicitly.Add an executable from the
hello-world.cpp
source file.Link the library into the executable.
A working solution is in the solution
subfolder.
You can find a scaffold project in the
content/code/day-1/01_libraries-f
folder.
Write a
CMakeLists.txt
to compile the source filesmessage.f90
into a library. Do not specify the type of library, shared or static, explicitly.Add an executable from the
hello-world.f90
source file.Link the library into the executable.
A working solution is in the solution
subfolder.
What kind of library did you get? Static or shared?
Keypoints
CMake is a build system generator, not a build system.
You write
CMakeLists.txt
to describe how the build tools will create artifacts from sources.You can use the CMake suite of tools to manage the whole lifetime: from source files to tests to deployment.
The structure of the project is mirrored in the build folder.