Introduction to CMake

CMake is cross-platform build and configuration system for C and C++ code, which also happens to be my favourite build system. In this post, I’ll present a quick tutorial on getting started with CMake.

But before we do, let’s start with an example of why other build systems such as Makefiles don’t necessarily cut it. Actually, Makefiles work fairly well, but writing Makefiles by hand is generally a pain in the neck. In a previous post, I described how to write Makefiles that take include file dependencies into account. If that post did not convince you that writing Makefiles is hard, let me show you another example of a terrible Makefile and hope that it scares you into never looking at a Makefile again. (This Makefile was actually part of a project that was handed over to me, I had to go over tonnes of other people’s code with comments written in non-ascii characters in a language that wasn’t English, so I clearly don’t have fond memories of this particular project.)

## Compiler + Linker
CC = g++
LD = g++

## external .h files to bring in
EXTRA_INCLUDES = 
CCFLAGS = -g -Wno-deprecated -I/opt/local/include/ -I/opt/local/include/opencv/ -I./GMM/ -I./MaxFlow/ -I./MeanShift/ 
LDFLAGS = -L/opt/local/lib
#LIBS = -lcv -lcxcore -lhighgui -lcvaux #-lml 
# for macs
LIBS = -lopencv_core -lopencv_highgui #-lcvaux -lml 

## sources
CCFILES_GMM = #### Some files
CCFILES_MAXFLOW = #### Some more files
CCFILES_MEANSHIFT = #### Holy shit, even more files
CCFILES = $(CCFILES_GMM) $(CCFILES_MAXFLOW) $(CCFILES_MEANSHIFT) #### And even more files
PROGFILE = main.cpp #### Seriously, how many files?

## object compilation
PROGOBJS = $(CCFILES:.cpp=.o) $(PROGFILE:.cpp=.o)

## binary compilation
PROG = BOO

all:
	@echo "-> Building $(PROG) executable..."
#	@make .cpp.o
	@make $(PROG) 

.cpp.o:
	$(CC) $(CCFLAGS) $(EXTRA_INCLUDES) -c -o $@ $^

#$(PROGOBJS):
#	$(CC) $(CCFLAGS) $(EXTRA_INCLUDES) -c -o $@ $^

$(PROG): $(PROGOBJS)
	$(LD) $(LDFLAGS) $(LIBS) $(PROGOBJS) -o $(PROG) #$(PROGOBJS) $(LDFLAGS) $(LIBS)

#$(PROG): $(CCFILES)
#	$(CC) $(CCFLAGS) $(EXTRA_INCLUDES) $(LDFLAGS) $(LIBS) -o $@ $^

clean:
	@echo "-> Cleaning..."
	rm -f $(PROG) $(PROGOBJS) 

There’s just so much to hate about this Makefile. First, it is almost unreadable. Second, it does not account for any header file dependencies. In fact, if any of the header files change, there’s no way for the system to detect this at all, forcing the user to type in make clean, then make to make everything all over again. Third, given the sheer number of files, make will take minutes to run, which kills productivity. Fourth, if you look closely at the path, you will observe that the person writing this Makefile used a Mac with MacPorts and therefore is the kind of person who is just wrong. Fifth, this Makefile obviously won’t work for someone on Linux or Windows, even worse, it won’t work for another Mac user who used HomeBrew or compiled OpenCV from source. Sixth, some rules are just commented out, which indicates sloppy behaviour. Writing Makefiles like this just sucks. It sucks productivity and makes me think of slow and painful death.

The first thing I did when I was handed this project was to fix the build system, and CMake came to the rescue here. I’ll now show how I replaced this terrible Makefile with a clean, modular CMake build system that replicated the effects of the Makefile (without the bugs, of course 😄). Hopefully, the ease of setting up this CMake system will convince you to never write a Makefile again.

As a dive into CMake, let’s start with a simple ‘Hello World’ program. The helloworld.cc file contains the following code:

#include <iostream>

int main(void)
{
	std::cout << "Hello World" << std::endl;
	return 0;
}

To compile this code, create a file called CMakeLists.txt:

add_executable(hello helloworld.cc)

Now, assuming that you are running on Linux or Mac OS terminal (something with bash), you can execute the following commands:

mkdir build;
cd build;
cmake ..;
make;

This code creates a directory called build inside the project directory. (An important point to note is that CMake always builds ‘out of source’, i.e. the build directory must be a distinct directory from the source directory.) It then switches to the build directory, executes CMake, which creates Makefiles (turns out that there is no escaping Makefiles after all… 😁) that have all the necessary information. As these files are generated by a tool and not written by a person, you never have to read them, so Makefile readability is not a concern. Second, the Makefiles encode all the dependencies correctly, so there’s no reason to go through the hoops creating complex dependency files.

‘But what if I’m on Windows, or if I just am the kind of person who is always wrong and uses CodeBlocks, Xcode, or another IDE?’ you ask. Well, hold on till the end, I’ll show you how CMake has you covered.

Anyway, back to our hypothetical, but totally real project. The project has multiple source files in different directories.

GMM
contains files that implement a Gaussian mixture models code.
MAXFLOW
contains files for max-flow graph cuts.
MEANSHIFT
contains files for mean shift (cluster analysis).

All these codes are independent, so could be made into libraries. Of course, actually packaging this code as independent libraries is a terrible idea because the code isn’t well written in the first place. However, I don’t see too much harm in creating a static library that will probably never be distributed for code in each of these directories.

We can create a CMakeLists.txt file for each of these folders. The CMakeLists will contain code that looks like this:

add_library(# Library name
STATIC # Need to create a static library, not a shared one.
### List of files
)

Then the top-level CMakeLists.txt can include these directories through the simple command:

# Tell CMake to look into subdirectories for CMakeLists.txt files.
subdirs(GMM MAXFLOW MEANSHIFT)
# Add these directories to the compiler search path for include files.
# ${PROJECT_SOURCE_DIR} is the path to the source code in the project.
include_directories(
	${PROJECT_SOURCE_DIR}/GMM
	${PROJECT_SOURCE_DIR}/MAXFLOW
	${PROJECT_SOURCE_DIR}/MEANSHIFT
	)

Okay, but we also notice that the top-level program PROG needs OpenCV in order to compile. CMake provides a very easy method to search for dependencies. In the top-level CMakeLists.txt, we add

find_package(OpenCV REQUIRED)

When we compile the program PROG, we need to link it against the OpenCV libraries, as well as the static libraries in the GMM, MAXFLOW, MEANSHIFT folders.

include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(PROG PROG.cc)
target_link_libraries(PROG ${OpenCV_LIBS} GMM MAXFLOW MEANSHIFT)

Putting it all together, the top-level CMakeLists.txt contains the following code:

# Set a minimum version of CMake to be used, depending on the syntax of
# the CMakeLists files.
cmake_minimum_required(VERSION 2)
# Give this project a cutsie name.
project(PROG)
# CMake subdirectories to include for these libraries.
subdirs(GMM MAXFLOW MEANSHIFT)
# Search for OpenCV. If OpenCV is found, the command sets the
# ${OpenCV_INCLUDE_DIR} and ${OpenCV_LIBS} variables to appropriate
# values. If OpenCV is not found, CMake will fail.
find_package(OpenCV REQUIRED)
# Tell the compiler to search these directories for include files.
include_directories(
	${PROJECT_SOURCE_DIR}/GMM
	${PROJECT_SOURCE_DIR}/MAXFLOW
	${PROJECT_SOURCE_DIR}/MEANSHIFT
	${OpenCV_INCLUDE_DIR}
	)
# Finally, create the executable that we want.
add_executable(PROG PROG.cc)
# And link it against the required libraries.
target_link_libraries(prog ${OpenCV_LIBS} GMM MAXFLOW MEANSHIFT)

And we’re done! Isn’t this CMakeLists.txt file so much more readable than the Makefile shown earlier? Not just that, this system will work with any OS, any compiler, any platform. Isn’t this great?

Okay, okay, you’re the kind that likes to do things in an IDE. CMake has a concept of ‘generators’, which is just a fancy name for a backend. Still assuming that we are on MacOS or Linux, we simply execute the following commands.

mkdir build;
cd build;
cmake -G "Xcode" .. # Or CodeBlocks, or whatever.

On Windows, execute the following commands on the command prompt (assuming that CMake is on your path) to create a Visual Studio 2015 project.

md build
cd build
cmake -G "Visual Studio 14" ..

Oh, did I mention that CMake also has a nice GUI that makes this entire process super easy? I won’t describe it here, I’ll just let you try it out for yourself.

The utility of CMake, however, doesn’t just end here. CMake has a very neat testing system called CTest, which is something that I have mentioned in an earlier post. This testing system interacts perfectly with a dashboard system called CDash, that can quickly allow people to view test results. CTest/CDash also has support for testing memory leaks and code coverage, so there. Finally, the last piece of the puzzle is CPack, which is a way of packaging software into installers.

The next post in the series will probably deal with advanced CMake configuration, including testing compiler versions, creating release and debug configurations. Until next time…

Related

comments powered by Disqus