Previously in this blog, we have talked about Yocto and how it is an extremely popular linux build system that allows you to generate a linux distribution of your choice. Yocto contains a tool called devtool
that allows you to generate a recipe on the source tree that is provided to it. It currently supports GNU Autotools, CMake, Qmake, and Plain makefile.
Let us take a closer look at CMake.
CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. The suite of CMake tools were created by Kitware in response to the need for a powerful, cross-platform build environment for open-source projects such as ITK and VTK.
Installing CMake
The best place to grab CMake and install it is the CMake download page. This is especially true if you are interested in installing a particular release version. Also, one of the outputs of CMake is a Makefile that we will use the make tool on. It is a good idea to install that as well if not already installed!
If you are on Linux, you can also install CMake using a package manager like apt. For example, you can do the below on an Ubuntu machine.
$ sudo apt update $ sudo apt install cmake make
Similarly on a Mac, you can install CMake using HomeBrew.
$ brew install cmake make
Creating your first CMake project
Let us now create an absolutely minimal but working CMake project called hello
.
Anywhere on your PC, create a folder called hello
and navigate into it.
$ mkdir hello $ cd hello
Now, create a simple hello.c
as below in this folder.
#include <stdio.h> int main(void) { printf("Hello, World!\r\n"); return 0; }
In the same folder, create a file named CMakeLists.txt
and add the below content inside it.
cmake_minimum_required(VERSION 2.8.12) project(hello) add_executable(hello hello.c)
The CMake looks for a file named CMakeLists.txt
to do its thing – always make sure that a file with this name is present in your source tree on which you run CMake.
This minimal CMakeLists.txt file does three things as below.
- We have simply specified that the minimum version of CMake that should be used is 2.8.12
- The name of the project is hello
- We want an executable named hello which is built using the file hello.c.
NOTE: Source trees that have multiple directories can make use of CMake by either having just a top-level CMakeLists.txt
or multiple such files inside each sub-directory. The name should remain the same.
It is considered a good practice to have the CMake output in a separate directory from the source tree. So, create a new folder called build
(or anything you want!) inside the hello/
folder and navigate into it.
$ mkdir build $ cd build
All that is remaining now is to do your first ever CMake build!
Remember – you have to provide the path to the source tree that contains the CMakeLists.txt. Since, we are now in the build/
folder, the argument shall be the previous folder i.e. ..
$ cmake ..
You should now see output messages that look like the below:
-- The C compiler identification is AppleClang 12.0.0.12000032 -- The CXX compiler identification is AppleClang 12.0.0.12000032 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /Library/Developer/CommandLineTools/usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /Library/Developer/CommandLineTools/usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done -- Generating done -- Build files have been written to: /path/to/hello/build
NOTE: The output will look different depending on the OS you are on and/or the compiler version you have installed on your OS.
Previously, build/
was empty. Now, it has content that looks like this.
$ ls CMakeCache.txt CMakeFiles Makefile cmake_install.cmake
The Makefile is the one that we will use the make
tool on to build our hello application.
$ make
You should see the below output now.
[ 50%] Building C object CMakeFiles/hello.dir/hello.c.o [100%] Linking C executable hello [100%] Built target hello
You should now see the hello
binary as the output as indicated above. Simply, execute the binary as below.
$ ./hello
You should now see a Hello, World!
printed on your terminal.
Congratulations! You have successfully created and built your first CMake package! Let us now move on to something that reflects the most common use-cases of CMake.
Working with a typical source-tree
A typical source tree consists of components that have to be built mandatorily, some are optional and may have dependencies on other components. Also, often the act of including components is governed by a global configuration file. Let us work with such a source tree and see how CMake makes life simple for such cases.
Say, we want to build a simple calculator library that provides functions as below.
- Always Build
- Hello World
- Optional Build
- Mandatory if parent build is enabled – Feature Set 0
- Add
- Subtract
- Multiply
- Divide
- Optional if parent build is enabled – Feature Set 1
- Percentage
- Factorial
- Power
- Inverse
- Mandatory if parent build is enabled – Feature Set 0
Simply put, we have the below possibilities here.
- Build #1 – Only Hello World is built
- Build #2 – Only Hello World + Feature Set 0 is built
- Build #3 – Hello World + Feature Set 0 + Feature Set 1 is built
The source code for this example is located here (https://github.com/sckulkarni246/ke-cmake-sample-apps) – do check it out!
This is how the source code is organised.
<Package root folder>/
-
CMakeLists.txt
– This is where the CMake magic starts g_config.h.in
– This is the input to the CMake configurator that will be used to generate a global configuration header filehello_world .c and .h
– This is the source code that will be built alwayscalculator/
f0 .c and .h
– Implementation of the feature set 0f1 .c and .h
– Implementation of the feature set 1CMakeLists.txt
– This file will be used by the top-level CMakeLists.txt if the optional components are needed. Note – the name is exactly the same at both levels.
- << other supporting files like .gitignore, LICENSE, README, etc. >>
-
Understanding <Package Root>/CMakeLists.txt
This is how the top-level CMakeLists.txt
file looks like – see the descriptions below to understand what is happening here.
Let’s see what is happening here.
- This is a the basic configuration info like minimum CMake version supported, project name and version.
- We add the hello_world.c to the variable called
SRCS
- We create an option called
BUILD_CALC
and set it toOFF
– this is how typically optional build components are configured. If you need this component, it has to be explicitly set toON
in the CMakeLists.txt file or passed asON
during CMake invocation in the terminal (see below) - If the
BUILD_CALC
option isON
, we tell CMake to look for a subdirectory calledcalculator/
which should have a CMakeLists.txt file that tells CMake what to do - This is the interesting part – auto-generating a global configuration file. We use the
g_config.h.in
as the input and tell CMake to generate ag_config.h
as the output header file. - The
g_config.h
is located in the build directory. However, it is needed for the source code to build successfully. Hence, we tell CMake to add the build directory to the path where headers are looked for. - Our goal was to create a static library. We do that here using the
add_library(...)
command with theSTATIC
argument. We also provide the name of the library we want along with the path to the source code that is to be built (these filenames are inside theSRCS
variable).
Note how in step 4, we pointed CMake to a subdirectory called calculator/
– let us now look at the calculator/CMakeLists.txt
file.
Understanding <Package Root>/calculator/CMakeLists.txt
Let’s see what is happening in this lower-level file.
- We tell CMake to include the feature support for dependent options – we shall use this next
- We create dependent options called
BUILD_CALC_F0
andBUILD_CALC_F1
.
Notice that both of them depend onBUILD_CALC
.
IfBUILD_CALC
isON
,
–BUILD_CALC_F0
is set toON
ifBUILD_CALC
isON
making it a mandatory build
–BUILD_CALC_F1
is set toOFF
ifBUILD_CALC
isON
making it an optional build - We create an empty variable called
CALC_SRCS
- If
BUILD_CALC_F0(F1)
isON
, we add thef0.c(f1.c)
to theCALC_SRCS
variable - Finally, we append the
CALC_SRCS
variable’s contents to theSRCS
variable from the top-level CMake file – note thePARENT_SCOPE
at the end that specifies we have to useSRCS
as it exists in the parent scope
Build the package!
Let us now build the package.
Remember, we had three possibilities – let us realise all three of those now. But first, create a build/
directory inside the package root and navigate to it as below.
$ mkdir build $ cd build
Build #1 – Only mandatory components
The only mandatory component is hello_world.c
. We have already done the needful configuration in our top-level CMakeLists.txt
for this. Simply execute the below while in the build
/ directory.
$ rm -rf * $ cmake ..
You should see output similar to the one you got for the hello project above.
Now, run make
.
$ make
Notice that the only file built is the hello_world.c
in the package root. This is because we have not enabled any optional component.
[ 50%] Building C object CMakeFiles/kecmakesampleapps.dir/hello_world.c.o [100%] Linking C static library libkecmakesampleapps.a [100%] Built target kecmakesampleapps
Build #2 – Mandatory components + feature set 0
We saw that the BUILD_CALC
option needs to be set to ON
for the feature set 0 to be built. We can do the edits in the CMakeLists.txt
files but let us do it on the command-line instead as below.
We don’t need to specify the BUILD_CALC_F0
as it is set to ON
automatically if BUILD_CALC
is set to ON
.
$ rm -rf * $ cmake .. -DBUILD_CALC=ON
The cmake output is similar to the previous one. Now, run make
.
$ make
Notice that the output now shows that along with hello_world.c
, even the f0.c
gets built.
[ 33%] Building C object CMakeFiles/kecmakesampleapps.dir/hello_world.c.o [ 66%] Building C object CMakeFiles/kecmakesampleapps.dir/calculator/f0.c.o [100%] Linking C static library libkecmakesampleapps.a [100%] Built target kecmakesampleapps
Build #3 – Build all components
In order to build all components i.e. hello_world.c, f0.c and f1.c
, we need to set to ON
all the options in our configuration. We do that as below.
$ rm -rf * $ cmake .. -DBUILD_CALC=ON -DBUILD_CALC_F1=ON
Notice how we don’t need to specify anything for BUILD_CALC_F0
as it is enabled if BUILD_CALC
is enabled.
The cmake output is similar to our previous outputs. Now, run make
.
$ make
Notice that now all components are built – hello_world.c, f0.c and f1.c.
[ 25%] Building C object CMakeFiles/kecmakesampleapps.dir/hello_world.c.o [ 50%] Building C object CMakeFiles/kecmakesampleapps.dir/calculator/f0.c.o [ 75%] Building C object CMakeFiles/kecmakesampleapps.dir/calculator/f1.c.o [100%] Linking C static library libkecmakesampleapps.a [100%] Built target kecmakesampleapps
Did you observe the contents of the g_config.h
file during the three builds? What is your observation? Let us know in the comments below!
See you in the next post!
One thought on “CMake: An Essential Tutorial”