45.2 CMake: Modern Build System Configuration
Alright, let’s talk about CMake. If you’re compiling from source, you’ve probably run into it. It’s the meta-build system that generates build files for other, less-insightful build systems like Make, Ninja, or Visual Studio projects. Think of it as a polyglot translator for build tools: you tell CMake what you want built in a high-level language, and it writes the low-level, platform-specific instructions for the builder of your choice. This is genius because it means project maintainers only have to write one CMakeLists.txt file instead of a Makefile, an MSBuild file, a Ninja file… you get the idea.
The alternative, Autotools, feels like trying to build a house by first forging your own hammer. CMake isn’t perfect, but it’s a significant upgrade.
The Absolute Minimum CMakeLists.txt
Every CMake project starts with a CMakeLists.txt file. It’s the to-do list, recipe, and manifest all in one. Here’s the simplest, most useless version that will actually run:
cmake_minimum_required(VERSION 3.10)
project(MyAwesomeProject)
add_executable(hello_world hello_world.cpp)
This tells CMake you need at least version 3.10 (a good baseline), names your project (which sets up variables like MyAwesomeProject_SOURCE_DIR), and instructs it to build an executable named hello_world from the source file hello_world.cpp. You’d run this with:
mkdir build
cd build
cmake ..
make
Why the build directory? You should always do an out-of-source build. This keeps the generated garbage (object files, cache, etc.) contained in a single, deletable folder. If you run cmake . in your source directory, you’ll pollute it with a million files, and I will personally show up at your house to sigh disappointedly. It’s a mess, and it makes version control a nightmare.
Variables, Cache, and the Art of Finding Things
CMake has variables, but they’re a bit schizophrenic. Some are normal variables (set(MY_VAR "value")), but the important ones for configuration are cache variables. These are the ones you see when you run ccmake or cmake-gui. They persist between runs and are how you tell the system about dependencies.
Speaking of dependencies, never use absolute paths. That’s a recipe for breaking on every other machine. Instead, use find_package. This is CMake’s way of searching the system for a library.
find_package(Boost REQUIRED COMPONENTS filesystem system)
The REQUIRED keyword is your friend. It stops configuration immediately if Boost isn’t found, rather than letting you get all the way to compilation just to fail with a cryptic “symbol not found” error. It’s fail-fast, and we love that.
Now, the result of find_package is usually a set of variables like Boost_INCLUDE_DIRS and Boost_LIBRARIES. The modern, less-error-prone way to use these is with targets.
Targets: The Right Way to Do Everything
Older CMake code would just shovel include paths and libraries into global compiler flags: include_directories(${Boost_INCLUDE_DIRS}). This is like using a bullhorn in a library—it works, but everyone else using the room now has your settings whether they want them or not.
Targets are CMake’s way of packaging all the properties needed to use a library or executable—its include directories, compile definitions, and linked libraries—into a single, neat object. You create a target for your executable or library, and then you link that target to its dependencies. This propagates all those necessary flags correctly and privately.
add_executable(hello_world hello_world.cpp)
# Create a library target
add_library(my_utils utils.cpp)
# Link Boost to your library. This automatically handles include dirs.
target_link_libraries(my_utils PUBLIC Boost::filesystem)
# And link your library to the executable
target_link_libraries(hello_world PRIVATE my_utils)
The PUBLIC and PRIVATE keywords are crucial. PRIVATE means “I need this to build myself.” PUBLIC means “Anyone using me needs this too.” INTERFACE is for header-only libraries—dependencies you don’t need to build but need to use. Getting this right is what separates the pros from the amateurs.
The Generators: Picking Your Fighter
Remember when I said CMake generates files for other systems? You choose which one with the -G flag. The default is often Makefiles, but for the love of all that is holy, use Ninja if you can.
cmake -G Ninja ..
ninja
Ninja is faster, smarter, and quieter than Make. It’s a purpose-built build tool, not a 45-year-old Swiss army knife. The difference in rebuild times is often dramatic. If Ninja isn’t installed, your package manager can fix that in seconds. It’s the single easiest speed boost you can give your build process.