For the sake of maintainability it’s important that non-trivial software projects have some sort of build system, whether makefiles, autotools, cmake, or some language-specific tooling. Unfortunately, how to write a build system seems to be something that’s rarely explained to computer science students. While not somewhat controversial and not as popular as older solutions like autotools, I find that CMake is easier to learn and use in many cases.
Getting CMake
Most Linux distributions, MacPorts, brew and the like will have a CMake package. This is probably what you want to use. Note that some distributions (Debian) will separate out the command-line, non-interactive CMake from the various other UIs. You probably want at least the non-interactive CMake and the curses GUI.
If you do want to build from source, the process is (ironically?) more or less in line with the autotools workflow (though not actually autotools):
#feel free to substitute a newer version
wget https://github.com/Kitware/CMake/releases/download/v3.13.4/cmake-3.13.4.tar.gz
tar xvvzf cmake*
cd cmake*
./configure --prefix=/usr/local/cmake/3.13.4
make
make install
export PATH=/usr/local/cmake/3.13.4/bin:$PATH
First CMake project
Let’s start with a trivial C project (call it “hello.c”):
#include <stdio.h>
int main(int argc, char** argv)
{
printf("hello world\n");
return(0);
}
To compile this code with CMake, we’ll need to create a file called CMakeLists.txt
which contains a little boilerplate and an instruction telling CMake what the binary should be called and which source to include in it. The CMakeLists might look like this:
#Tell CMake which version of CMake this code is designed for
cmake_minimum_required(VERSION 2.8)
#produce an executable based on "hello.c"
add_executable(hello "hello.c")
We can then run CMake to produce a makefile. In order to keep things tidy, let’s do an out of source build:
mkdir build
cd build
cmake ../
make
If all went well, you should see something like this:
mbp:cmake_demo matthb2$ mkdir build
mbp:cmake_demo matthb2$ cd build
mbp:build matthb2$ cmake ../
-- The C compiler identification is AppleClang 10.0.0.10001145
-- The CXX compiler identification is AppleClang 10.0.0.10001145
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/matthb2/cmake_demo/build
mbp:build matthb2$ make
Scanning dependencies of target hello
[ 50%] Building C object CMakeFiles/hello.dir/hello.c.o
[100%] Linking C executable hello
[100%] Built target hello
mbp:build matthb2$ ./hello
hello world
mbp:build matthb2$
##Linking with external libraries
What if our program has an external dependency? In the absolute simplest case, if we know for sure that the library will be in the compiler search path, this is as simple as adding one more call:
#Tell CMake which version of CMake this code is designed for
cmake_minimum_required(VERSION 2.8)
#produce an executable based on "hello.c"
add_executable(hello "hello.c")
#link with libm
target_link_libraries(hello m)
Of course, it would be much better for portability if we ask CMake to find libm.
#Tell CMake which version of CMake this code is designed for
cmake_minimum_required(VERSION 2.8)
#produce an executable based on "hello.c"
add_executable(hello "hello.c")
#find libm
find_library(LIBM_LIBRARY m PATHS /usr/lib)
#link with libm using the variable set above
target_link_libraries(hello ${LIBM_LIBRARY})
There’s a similar call, find_path
which you can use to find headers or other types of files.
In many cases, CMake will already come with a piece of code to find all the information you need to link with a particular library, in which case you can use find_package()
to find the path to the libraries/headers/executables automatically. find_package operates on a piece of CMake code called FindFoo.cmake (where “Foo” is the PackageName argument to CMake). These files usually live in your CMake install directory under share/cmake-$version/Modules
and are somewhat self explanatory to read
Options
What if we want to conditionally compile something? Let’s say we add an optional feature to our C program based on a preprocessor constant:
#include <stdio.h>
int main(int argc, char** argv)
{
#ifdef UNIVERSE
printf("hello universe\n");
#else
printf("hello world\n");
#endif
return(0);
}
We can add code to our CMakeLists.txt
to allow us to choose which way to compile our program
#Tell CMake which version of CMake this code is designed for
cmake_minimum_required(VERSION 2.8)
option(BUILD_UNIVERSE_MODE "build with the UNIVERSE flag set")
if(BUILD_UNIVERSE_MODE)
add_definitions(-DBUILD_UNIVERSE_MODE)
endif(BUILD_UNIVERSE_MODE) #endif() is ok too, but this will make sure your endif() matches your if()
#produce an executable based on "hello.c"
add_executable(hello "hello.c")
This time, we can tell CMake we’d like to set our new option ON:
mkdir build
cd build
cmake -DBUILD_UNIVERSE_MODE=ON ../
make
We could also have used one of the GUI versions of CMake to see a list of options and chosen the mode we want interactively, for example: ccmake ../
Choosing your compiler and flags
CMake obeys the usual variables that you would expect for choosing compilers – CC
, CXX
, FC
, etc. To set compiler flags at build time, you can set one of the built-in options, for example:
cmake -DCMAKE_C_FLAGS="-O3" -DCMAKE_CXX_FLAGS="-O3" -DCMAKE_Fortran_FLAGS="-O3 -fno-stack-arrays"
CMake also has some presets that you can use (commonly, Release
, RelWithDebInfo
or Debug
) to configure your build:
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo
If you would have written your CMakeLists to install some files, you also might want to set CMAKE_PREFIX_PATH
(just like --prefix
for an autotools based build)
Debugging hints
I usually use a print based debugging technique for CMake code. You can have CMake print messages during compilation:
message(WARNING "something interesting happened ${SOME_VARIABLE}")
Once you have successfully configured, it’s a good idea to ensure that you’re building in serial for easy debugging:
make -j 1
Normally CMake will hide the actual compiler calls from you, to see them, you might want to do this:
make -j 1 VERBOSE=1
Conclusion
CMake is a rich programming language in its own right, but this should be enough to get you started for simple projects. Perhaps this post will turn into a series, but for now, check out the Official CMake documentation or the book