Getting Started with CMake - 1 - Basic

Jeff posted on  (updated on )

Table of Contents

What can CMake do?

There's no better words to describe a product, than those from their official website: https://cmake.org/

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

A few key takeways from above description:

  • CMake is cross-platform, so ideally you can code once and ship your code to Windows, Linux, Mac, etc.
  • It uses Domain Specific Language (DSL) to describe its configuration files, so a new language to learn (its syntax not that complex, though)
  • It merely generates project files for other build system, it doesn't directly call the compiler and compile you code. So you will need another build system to actually build your code (e.g. make, ninja, msbuild, etc.). Some people call it "meta-build system" for this reason.

Install CMake

There are plenty of tutorials online that best suits your platform. I better not duplicating them here.

To verify you have it correctly installed, run cmake --version. You should see the CMake version as output.

How to use CMake?

Let's prepare a classic Hello, World! C project to demonstrate.

hello_world/
 └── main.c

Just a recap, without a build system, we would manually compile the code with a compiler like this:

# gcc
gcc -c main.c -o main.o # compile
gcc main.o -o hello_world # link
./hello_world # run

# or clang
clang -c main.c -o main.o # compile
clang main.o -o hello_world # link
./hello_world # run

Side Note: clang is (almost) a drop-in replacement of gcc. You should read both clang and gcc's manual to learn the most common options, right now. It actually make life much easier. I prefer clang over gcc because it has better error messages.

make & Makefile

Let's quickly go over the Makefile we would need without CMake. To use make, we need to write a Makefile that describes how to build our project.

# File name: Makefile
build: main.o
	$(CC) main.o -o hello_world

main.o: main.c
	$(CC) -c main.c -o main.o

clean:
	rm main.o hello_world

To build your project, run make build, an executable called hello_world will be generated within the same directory, this is called a in-source build, where the output is in the same location as source files, which is typical for projects that use make build system. The downside of this is that you will have a lot of generated files in your source directory, which makes it hard to maintain. Run make clean to remove all generated files.

You can see that Makefile is quite imperative. It tells the build system exactly what command to use, what object file to generate, etc.

In this case you can just use gcc main.c -o hello_world to compile and link your code in one command. But for any other real project you will want to separate compile and link stage. So that you only need to recompile the changed files, instead of recompiling everything.

Problem with make

For this simple project, this Makefile doesn't look particularly bad, but as project grows and more source files are involved, it will become much less readable and maintainable as you will certainly start using automatic variables and your Makefile will probably start to look like this:

$(LIBNAME): $(OBJECTS)
	echo $(LIBCOMMAND) $@ ...
	$(RM) $@
	$(LIBCOMMAND) $@ $(OBJECTS)
	$(RANLIB) $@

It's filled with macro and variables and it's hard to tell what's going on.

However, it's still worth it to learn Makefile automatic variables as it's very common: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html

CMake & CMakeLists.txt

Now let's build our Hello, World! project with CMake. To use CMake, we need to write a CMakeLists.txt that describes how to build our project.

# File name: CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(HelloWorld)

add_executable(hello_world main.c)

This 3 lines of code is the minimal code you need for any CMake project. It's much more declarative than the Makefile we wrote above. It tells CMake what to build, and CMake will figure out how to build it.

I highly recommend checking CMake documentation about what each command does: https://cmake.org/cmake/help/latest/ . Unfortunately since CMake tries to abstract away the build system, it's command might not be very intuitive. It will take more time to learn CMake than Makefile, but it's worth it.

Now, to build your project, we need to do something a little different than that of make. We need to create a build directory, and run CMake from there. This is called an out-of-source build, where the output is in a separate directory from source files. This is the typical and recommended way to build CMake project. Also, it allows you to build as many different configuration as you want, without interfering with each other. For example you can have a debug version and a release version at the same time. (We will cover the Debug/Release build later).

Let's open a terminal and run:

# This 2 steps create a directory to store generated files
mkdir build
cd build

# This step calls CMake to generate targeted build system files, by reading CMakeLists.txt file from `..` directory
# On Linux, by default it will generate for make build system
# run cmake -G to check all supported build systems
cmake ..

A few notes here:

  1. You can name the build directory anything. Usually people name it build, or build/debug, build/release, etc for different build configurations.
  2. You can just run cmake . at the root directory to do a in-source build, but it's not recommended.
  3. The .. part in cmake .. is just telling where to find the CMakeLists.txt file, as you can probably guess. If current directory is build, run cmake .., if current directory is build/debug, run cmake ../.., etc.

It's all flexible and not hard-coded magic words.

Now, back to building our project, after running cmake .., you can see some file generated under build, Makefile in this case. Don't try to edit CMake generated files as they are managed by CMake and will not be very human friendly.

To actually build the executable, run make in directory build/. You should see the executable generated in the build directory. Run ./hello_world to run it.

Or just run cmake --build . and let CMake build with whatever build system it uses.
You can also just run cmake --build . to automatically regenerate build system files if needed. No need to cmake .. first.

There is no clean command in CMake, you can just delete the build directory and run cmake .. again to regenerate the build system files.

Congratulations! You have successfully built your first CMake project.

Conclusion

  • A C/C++ project is typically built with either make or CMake on non-Windows platform (on Windows, Visual Studio is simply better).
  • make is a build system where it directly calls compiler to generated the executables. Where CMake is a "meta-build system" that generates build system files for other build systems to use.
  • make looks for file called Makefile, CMake looks for file called CMakeLists.txt
  • make is imperative, CMake is declarative (mostly)