CMake for C++ Projects
What Is CMake?
CMake is a build system generator. It does not compile your C++ code by itself. Instead, you describe your project once in a file named CMakeLists.txt, and CMake generates the files needed by the actual build tool on your machine.
On Linux, CMake can generate Makefiles or Ninja files. On macOS, it can generate Unix Makefiles, Ninja files, or Xcode projects. On Windows, it can generate Visual Studio solutions, Ninja files, or MinGW Makefiles. The same project description can work across all of those environments.
Why Not Just Use g++ Directly?
For one file, this is fine:
But real C++ projects quickly need more:
- multiple
.cppfiles, headers, and include paths - external libraries such as LibTorch, OpenCV, Boost, or fmt
- debug and release builds
- different compiler flags on Linux, macOS, and Windows
- tests, examples, tools, and install rules
- IDE support without maintaining separate project files manually
CMake centralizes that project knowledge. Instead of copying a long compile command into notes, shell scripts, IDE settings, and CI files, you keep the project structure in one place.
Is CMake Free?
Yes. CMake is free to use for personal, academic, open-source, and commercial projects. It is open source and distributed under a permissive BSD-style license.
You do not pay for CMake, and using CMake does not force your project to become open source. You can use it in a private company codebase, a GitHub project, a research prototype, or a personal learning project.
Advantages of CMake
1. One project description across platforms
A single CMakeLists.txt can describe your executable, libraries, include paths, compiler features, and dependencies. Developers can build it with Ninja, Make, Visual Studio, or Xcode without you maintaining separate files for each tool.
2. Target-based dependency management
Modern CMake is built around targets. A target might be an executable, a static library, a shared library, or an interface library. You attach include paths, compile features, compile definitions, and linked libraries to the target that needs them.
3. Clean build directories
CMake encourages out-of-source builds. Your source files stay clean, while generated files go into a separate build/ directory. If the build gets messy, delete build/ and configure again.
4. External library discovery
Commands like find_package(Torch REQUIRED) ask CMake to locate an installed package and import its compiler and linker settings. This is especially helpful for libraries like LibTorch, where the correct include directories, libraries, ABI flags, and runtime paths matter.
5. IDE and CI friendliness
Editors and CI systems understand CMake well. CLion, Visual Studio, VS Code, Qt Creator, GitHub Actions, and many package managers can configure and build CMake projects directly.
Core Terms
| Term | Meaning |
|---|---|
CMakeLists.txt | The project recipe that CMake reads. |
| Configure step | CMake reads your project, checks compilers and packages, and generates build files. |
| Build step | The generated build tool compiles and links your targets. |
| Generator | The backend CMake writes files for, such as Ninja, Unix Makefiles, or Visual Studio. |
| Target | A build output such as an executable or library. |
find_package | A command that locates an installed dependency and imports its CMake configuration. |
add_custom_command | A build-time rule that usually produces a file. |
add_custom_target | A named build action such as run, format, copy_assets, or generate_docs. |
How to Create Your Own CMake Files
When you start a C++ project, do not begin by writing a giant build script. Start with the smallest target that compiles, then add libraries and dependencies one by one. A good CMakeLists.txt grows with your project.
Step 1: Choose a simple folder layout
For a small executable, this is enough:
For a project with reusable code, split headers and source files:
Step 2: Write the file from top to bottom
Most beginner CMake files follow this order:
- Declare the minimum CMake version.
- Name the project and choose the language.
- Create one or more targets with
add_executableoradd_library. - Attach C++ standard requirements with
target_compile_featuresorCMAKE_CXX_STANDARD. - Add include directories only to the targets that need them.
- Find external packages with
find_package. - Link dependencies with
target_link_libraries.
Step 3: Prefer target-based commands
Modern CMake is target-based. Instead of setting global flags that affect everything, attach requirements to the exact executable or library that needs them.
| Prefer | Avoid for new projects | Why |
|---|---|---|
target_compile_features(app PRIVATE cxx_std_17) | set(CMAKE_CXX_FLAGS "...") | The requirement belongs to app, not every target globally. |
target_include_directories(app PRIVATE include) | include_directories(include) | Only targets that need the headers receive the include path. |
target_link_libraries(app PRIVATE some_lib) | Manual linker flags everywhere | CMake can track link dependencies and propagate usage requirements. |
Step 4: Use this starter template
If you later add a second source file, add it to the target:
If your headers live in include/, attach that include path to the target:
A Minimal CMake Project
Project layout:
main.cpp:
CMakeLists.txt:
Line-by-line explanation
| Command | What it means | Why it matters |
|---|---|---|
cmake_minimum_required(VERSION 3.18) |
This says the project expects CMake 3.18 or newer. | CMake behavior changes over time. Setting a minimum version makes the project predictable and gives a clear error if the user's CMake is too old. |
project(hello_cmake LANGUAGES CXX) |
This names the project hello_cmake and says it uses C++. |
CMake now knows it should detect a C++ compiler and set up C++ language support. |
add_executable(hello main.cpp) |
This creates an executable target named hello from main.cpp. |
The target name is what you build, link, configure, and refer to later. |
target_compile_features(hello PRIVATE cxx_std_17) |
This tells CMake that hello needs C++17. |
CMake will pass the correct compiler flag, such as -std=gnu++17 or the Visual Studio equivalent. |
Configure and build:
Terminal commands explained
| Command | Meaning |
|---|---|
cmake -S . -B build | Configure the project. -S . means the source directory is the current folder. -B build means generated build files go into build/. |
cmake --build build | Build the project using whatever generator CMake selected. You do not need to know whether it generated Makefiles, Ninja files, or a Visual Studio project. |
./build/hello | Run the executable that was produced inside the build directory. |
LibTorch Example
LibTorch is the C++ frontend for PyTorch. A LibTorch project is a good example of why CMake is useful: you need include paths, compiled libraries, ABI settings, and runtime library paths to line up correctly.
Project layout:
main.cpp:
CMakeLists.txt:
Every CMake command explained
| Command | Explanation |
|---|---|
cmake_minimum_required(VERSION 3.18) |
Requires a CMake version new enough for modern target behavior and reliable package discovery. |
project(libtorch_cmake_demo LANGUAGES CXX) |
Creates the project and asks CMake to configure a C++ compiler. |
set(CMAKE_CXX_STANDARD 17) |
Requests C++17 as the default standard for targets in this directory. LibTorch examples commonly use C++17. |
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
Makes the C++17 request strict. If the compiler cannot provide C++17, configuration should fail instead of silently falling back. |
find_package(Torch REQUIRED) |
Asks CMake to find LibTorch's package configuration. REQUIRED means stop with an error if Torch cannot be found. |
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") |
Adds compiler flags that LibTorch says are necessary. This may include ABI-related settings depending on the LibTorch distribution. |
add_executable(libtorch_demo main.cpp) |
Creates an executable target named libtorch_demo from main.cpp. |
target_link_libraries(libtorch_demo PRIVATE "${TORCH_LIBRARIES}") |
Links the executable against the libraries imported by LibTorch. PRIVATE means this dependency is needed to build this target, but it does not need to be exposed to another target. |
Build with an official LibTorch download:
The important part is CMAKE_PREFIX_PATH. It should point to the LibTorch directory that contains the CMake package configuration files, especially TorchConfig.cmake. Once CMake finds that package, find_package(Torch REQUIRED) fills in TORCH_LIBRARIES and TORCH_CXX_FLAGS.
How to choose CMAKE_PREFIX_PATH
If you downloaded LibTorch and extracted it to /home/me/libtorch, then use:
A quick sanity check is to verify that this file exists:
If that file is missing, CMake will not be able to satisfy find_package(Torch REQUIRED).
LibTorch build commands explained
| Command | Meaning |
|---|---|
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/libtorch | Configure the project and tell CMake where LibTorch is installed. The -D syntax sets a CMake cache variable. |
cmake --build build | Compile and link the libtorch_demo target using the generated build files. |
./build/libtorch_demo | Run the executable. If shared libraries cannot be found at runtime, fix the runtime library path or use the platform-specific LibTorch setup instructions. |
Can CMake Run Custom Commands?
Yes. CMake can run custom commands during the build. This is useful for generating files, copying assets, formatting code, running an executable, generating documentation, or creating helper targets for developer workflows.
add_custom_command and add_custom_target are build-time tools. They run when you build a target. If you need to run something while CMake is configuring the project, use execute_process instead.
add_custom_command: when a command produces a file
Use add_custom_command when the command has a concrete output file. CMake can then decide whether the command needs to run again based on whether the output exists and whether its dependencies changed.
| Line | Meaning |
|---|---|
OUTPUT .../generated.txt | Declares the file that this command creates. This is how CMake tracks whether the rule is up to date. |
COMMAND ${CMAKE_COMMAND} -E touch ... | Runs CMake's portable helper command to create or update the file. ${CMAKE_COMMAND} is the path to the CMake executable currently running. |
COMMENT "..." | Prints a readable message during the build. |
VERBATIM | Tells CMake to preserve command arguments safely across platforms. |
add_custom_target(generate_file ...) | Creates a named target you can build with cmake --build build --target generate_file. |
If another target needs that generated file first, connect the dependency explicitly:
Now building app will build generate_file first.
add_custom_target: when you want a named action
Use add_custom_target when the command does not naturally produce one tracked output file, or when you want a convenient command name.
Build and run that target:
| Line | Meaning |
|---|---|
add_custom_target(run ...) | Creates a target named run. |
COMMAND $<TARGET_FILE:app> | Runs the executable produced by the app target. The generator expression expands to the correct executable path. |
DEPENDS app | Makes sure app is built before the run command executes. |
WORKING_DIRECTORY ... | Sets the folder where the command runs. |
Common custom target examples
The pattern is the same: give the action a name, write the command, add dependencies if needed, and build it with cmake --build build --target target_name.
When to use each one
| Need | Use | Reason |
|---|---|---|
| Generate a header, source file, model artifact, or text file | add_custom_command | The command has a real output that CMake can track. |
Create a named action like run, format, or copy_assets | add_custom_target | You want a target that can be invoked by name. |
| Run a command while CMake configures the project | execute_process | This happens during cmake -S . -B build, not during the build step. |
Common Mistakes
- Pointing
CMAKE_PREFIX_PATHat the wrong folder: point it at the LibTorch root, not theinclude/directory. - Mixing Debug and Release on Windows: use the LibTorch build type that matches your Visual Studio configuration.
- Editing files inside
build/: generated files are disposable. EditCMakeLists.txt, then reconfigure. - Using global include paths everywhere: prefer target-based commands such as
target_include_directories,target_link_libraries, andtarget_compile_features. - Forgetting runtime libraries: if an executable builds but cannot start, the dynamic libraries may not be discoverable at runtime.
- Using
add_custom_targetfor generated files: if the command produces a real file, preferadd_custom_command(OUTPUT ...)so CMake can track whether it is up to date.
When Should You Use CMake?
Use CMake when a C++ project has more than one source file, depends on external libraries, needs to work on multiple machines, or will be built by someone other than you. For a one-file experiment, a direct compiler command is fine. For anything that you want to keep, share, test, or extend, CMake is usually worth it.