From sources to executables
Objectives
Learn how to write a simple
CMakeLists.txt
.Learn how to build executables and libraries.
Type-along: building an executable
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!
Create a new folder and in the folder create a source file:
#include <cstdlib> #include <iostream> void say_hello() { std::cout << "Hello world" << std::endl; } int main() { say_hello(); return EXIT_SUCCESS; }pure function say_hello() result(message) implicit none character(len=11) :: message message = 'Hello world' end function program example implicit none character(len=11) :: say_hello print *, say_hello() end program
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:
cmake_minimum_required(VERSION 3.14) project(example LANGUAGES CXX) add_executable(hello hello.cpp)cmake_minimum_required(VERSION 3.14) project(example LANGUAGES Fortran) add_executable(hello hello.f90)
We are ready to call CMake and get our build system:
$ cmake -S. -Bbuild
And finally build our executable:
$ cmake --build build
Try to also run the executable.
Discussion: We prefer out-of-source builds
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++
Why prefer out-of-source builds?
You can build several builds with the same source without having to copy the entire project and merging changes later (sequential and parallel, debug and release).
We have learned met three CMake directives (you can click on these to jump to the official documentation help text):
The case of CMake commands and variables 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!
Exercise: building and linking a library
A more modular “Hello, world”
Only rarely we have one-source-file projects and more realistically, as projects grow, we split them up into separate files. This simplifies (re)compilation but also helps humans maintaining and understanding the project.
We stay with the toy project but also here things got more real and more modular and we decided to split the project up into several files:
hello.cpp:
#include "greeting.hpp"
#include <cstdlib>
int main() {
say_hello();
return EXIT_SUCCESS;
}
greeting.cpp:
#include "greeting.hpp"
#include <iostream>
void say_hello() {
std::cout << "Hello world" << std::endl;
}
greeting.hpp:
#pragma once
void say_hello();
hello.f90:
program example
use greeting, only: say_hello
implicit none
print *, say_hello()
end program
greeting.f90:
module greeting
implicit none
public say_hello
private
contains
pure function say_hello() result(message)
implicit none
character(len=11) :: message
message = 'Hello world'
end function
end module
Your first goal: try to build this by adapting the CMakeLists.txt from
earlier by first adding all the source files into the same add_executable
.
CMake can of course be used to produce libraries as well as executables. The
relevant command is add_library
. You can link libraries can be linked into
other targets (executables or other libraries) with target_link_libraries
.
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.
Collecting files into libraries
Your second goal: now try to build a greeting library and link against this library instead of collecting all sources into the executable target:
cmake_minimum_required(VERSION 3.14)
project(example LANGUAGES CXX)
add_executable(hello hello.cpp)
add_library(greeting
SHARED
greeting.cpp
greeting.hpp
)
target_link_libraries(hello PRIVATE greeting)
cmake_minimum_required(VERSION 3.14)
project(example LANGUAGES Fortran)
add_executable(hello hello.f90)
add_library(greeting
SHARED
greeting.f90
)
target_link_libraries(hello PRIVATE greeting)
Which solution did you like better? Discuss the pros and cons.
What kind of library did you get? Static or shared? Try to get the other one.
Discussion: Granulatity of libraries
How granular should we organize our targets?
Collect all sources into one executable?
One library?
Many libraries?
Discuss pros and cons and how you do this in your projects.
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.We can define a multi-language project like this:
project(example LANGUAGES Fortran C CXX)
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.