5 minutes
CMake Quick Reference
Writing this so I can quickly find a few examples of CMake setup material that I’ll inevitably need in my next project. There are plenty of great resources out there that this drew from, but these are the simple practices that have worked for me so far. If you have recommendations on things to add, then please reach out.
CMake is a cross-platform system that provides an easier interface to build the compilation tools necessary for your projects (think Makefiles, Visual Studio Solutions, Ninja files, etc.). It is a relatively intuitive interface of various text files (CMakeLists.txt) that define your project structure, compilation options, build editions, library imports/exports, and even test cases. The toolset is robust and many projects rely on CMake to perform their builds with consistency in different environments. This quick-start guide takes inspiration from some of my basic projects and guidance from the fantastic book Professional CMake: A Practical Guide.
Using CMake, compilation of a project gets broken down into two steps: configuration and build. In the simplest case, CMake detects your compilation toolchain and generates the build tools in an area separate from your source code. You can then make a build call to CMake (or call the compilation tools directly) to actually generate your binaries. The main benefit is that you can define the desired state of the project in one place and still compile to that state on many operating systems and architectures. It also gives you the nice ability to define a non-default toolchain so you can cross-compile code for other platforms.
The most generic use case is shown below. You can do this in a number of different ways, but I think this is the most modern and obvious way to show what is actually happening. Traditionally, you would change to a build directory and call cmake on the source location (cmake ..) instead of using the -S and -B options. But that isn’t as descriptive, so I prefer this more modern approach.
cmake -S srcdir -B buildir
cmake –build builddir –target TARGETOPT
If you needed to choose a specific compiler on the command line then you’d run something like CC=clang CXX=clang++ cmake .. to set those environment variables in the actual build scripts.
If you wanted to set options then these are the most common that users configure on a project. Whether via the command line or setting within CMakeLists.txt files.
The below are common CMake options for many packages. You can conditionally set these with if statements throughout any of the CMakeLists.txt files in your source tree.
DCMAKE_BUILD_TYPE- Pick from Release, RelWithDebInfo, Debug, or sometimes more.DCMAKE_INSTALL_PREFIX- The location to install to. System install on UNIX would often be/usr/local(the default), user directories are often~/.local, or you can pick a folder.DBUILD_SHARED_LIBS- You can set this ON or OFF to control the default for shared libraries (the author can pick one vs. the other explicitly instead of using the default, though)DBUILD_TESTING- This is a common name for enabling tests
Standard CMakeLists.txt:
cmake_minimum_required(VERSION 3.19)
project(MyProject VERSION 1.0
DESCRIPTION "My Project"
LANGUAGES CXX)
add_executable(one two.cpp three.h)
add_library(one STATIC two.cpp three.h)
target_include_directories(one PUBLIC include)
add_library(another STATIC another.cpp another.h)
target_link_libraries(another PUBLIC one)
General advice for project structure is to keep a build directory separate from source. If you want to run any linters in a workflow (see below) then you might have a cmake folder to define those target commands. Here is a sample project tree:
- project
- .gitignore
- README.md
- LICENSE.md
- CMakeLists.txt
- cmake
- FindSomeLib.cmake
- something_else.cmake
- include
- project
- lib.hpp
- src
- CMakeLists.txt
- lib.cpp
- tests
- CMakeLists.txt
- testlib.cpp
- docs
- CMakeLists.txt
- extern
- testinglibrary
- scripts
- helper.py
CMakePresets.json allows you to specify multiple versions of the compilation chain that may be different or for different architectures so you don’t have to remember the actual cmake command when you go to execute. You can have configuration presets, build presets, and workflow presets. Workflows allow you to call multiple other presets in a specific sequence that you prefer. A simple example is below:
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"displayName": "Debug",
"generator": "Unix Makefiles",
"binaryDir": "build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release",
"generator": "Unix Makefiles",
"binaryDir": "build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "build-debug",
"displayName": "Debug Build",
"configurePreset": "debug",
"configuration": "Debug"
},
{
"name": "build-release",
"displayName": "Release Build",
"configurePreset": "release",
"configuration": "Release"
},
{
"name": "clang-format-debug",
"displayName": "Debug clang-format linting",
"configurePreset": "debug",
"configuration": "Debug",
"targets": ["clang-format-lint"]
}
],
"workflowPresets": [
{
"description": "Debug workflow",
"name": "debug",
"steps": [
{
"type": "configure",
"name": "debug"
},
{
"type": "build",
"name": "clang-format-debug"
},
{
"type": "build",
"name": "build-debug"
}
]
},
{
"description": "Release workflow",
"name": "release",
"steps": [
{
"type": "configure",
"name": "release"
},
{
"type": "build",
"name": "build-release"
}
]
}
]
}
And then you can build a preset by doing something like cmake –workflow –preset debug, which in the case above will configure a debug version of the generators, run clang-format through the clang-format-lint CMake target, and then actually build the debug version.
I also like to have a separate Makefile that will just take my debug or testing presets and run them in a shorter command so that I only have one command to type to keep developing.
References:
- Professional CMake: A Practical Guide
904 Words
2025-09-19 05:00