← All Posts

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.

Simple mental model: CMake is the portable project recipe. The compiler and build tool are the kitchen. CMake tells the kitchen what to cook, which files are ingredients, which libraries are needed, and which final executable or library should be produced.

Why Not Just Use g++ Directly?

For one file, this is fine:

g++ main.cpp -o app

But real C++ projects quickly need more:

  • multiple .cpp files, 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

TermMeaning
CMakeLists.txtThe project recipe that CMake reads.
Configure stepCMake reads your project, checks compilers and packages, and generates build files.
Build stepThe generated build tool compiles and links your targets.
GeneratorThe backend CMake writes files for, such as Ninja, Unix Makefiles, or Visual Studio.
TargetA build output such as an executable or library.
find_packageA command that locates an installed dependency and imports its CMake configuration.
add_custom_commandA build-time rule that usually produces a file.
add_custom_targetA 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:

my-project/ CMakeLists.txt src/ main.cpp

For a project with reusable code, split headers and source files:

my-project/ CMakeLists.txt include/ my_project/ math_utils.h src/ main.cpp math_utils.cpp

Step 2: Write the file from top to bottom

Most beginner CMake files follow this order:

  1. Declare the minimum CMake version.
  2. Name the project and choose the language.
  3. Create one or more targets with add_executable or add_library.
  4. Attach C++ standard requirements with target_compile_features or CMAKE_CXX_STANDARD.
  5. Add include directories only to the targets that need them.
  6. Find external packages with find_package.
  7. 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.

PreferAvoid for new projectsWhy
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 everywhereCMake can track link dependencies and propagate usage requirements.

Step 4: Use this starter template

cmake_minimum_required(VERSION 3.18) project(my_project LANGUAGES CXX) add_executable(my_app src/main.cpp ) target_compile_features(my_app PRIVATE cxx_std_17)

If you later add a second source file, add it to the target:

add_executable(my_app src/main.cpp src/math_utils.cpp )

If your headers live in include/, attach that include path to the target:

target_include_directories(my_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include )

A Minimal CMake Project

Project layout:

hello-cmake/ CMakeLists.txt main.cpp

main.cpp:

#include <iostream> int main() { std::cout << "Hello from CMake\\n"; return 0; }

CMakeLists.txt:

cmake_minimum_required(VERSION 3.18) project(hello_cmake LANGUAGES CXX) add_executable(hello main.cpp) target_compile_features(hello PRIVATE cxx_std_17)

Line-by-line explanation

CommandWhat it meansWhy 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:

cmake -S . -B build cmake --build build ./build/hello

Terminal commands explained

CommandMeaning
cmake -S . -B buildConfigure the project. -S . means the source directory is the current folder. -B build means generated build files go into build/.
cmake --build buildBuild 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/helloRun 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:

libtorch-cmake-demo/ CMakeLists.txt main.cpp

main.cpp:

#include <torch/torch.h> #include <iostream> int main() { torch::manual_seed(42); torch::nn::Linear layer(torch::nn::LinearOptions(4, 2)); torch::Tensor x = torch::randn({3, 4}); torch::Tensor y = layer->forward(x); std::cout << "Input shape: " << x.sizes() << "\\n"; std::cout << "Output shape: " << y.sizes() << "\\n"; std::cout << y << "\\n"; return 0; }

CMakeLists.txt:

cmake_minimum_required(VERSION 3.18) project(libtorch_cmake_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Torch REQUIRED) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") add_executable(libtorch_demo main.cpp) target_link_libraries(libtorch_demo PRIVATE "${TORCH_LIBRARIES}")

Every CMake command explained

CommandExplanation
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:

cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/libtorch cmake --build build ./build/libtorch_demo

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:

cmake -S . -B build -DCMAKE_PREFIX_PATH=/home/me/libtorch

A quick sanity check is to verify that this file exists:

/home/me/libtorch/share/cmake/Torch/TorchConfig.cmake

If that file is missing, CMake will not be able to satisfy find_package(Torch REQUIRED).

LibTorch build commands explained

CommandMeaning
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/libtorchConfigure the project and tell CMake where LibTorch is installed. The -D syntax sets a CMake cache variable.
cmake --build buildCompile and link the libtorch_demo target using the generated build files.
./build/libtorch_demoRun the executable. If shared libraries cannot be found at runtime, fix the runtime library path or use the platform-specific LibTorch setup instructions.
Connection to the Transformer series: the LibTorch posts in this section rely on the same build idea. Once CMake can compile a tiny tensor example, scaling up to attention heads, Transformer blocks, and GPT is mostly a matter of adding source files and targets.

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.

Important distinction: 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.

add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.txt COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/generated.txt COMMENT "Generating generated.txt" VERBATIM ) add_custom_target(generate_file DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.txt )
LineMeaning
OUTPUT .../generated.txtDeclares 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.
VERBATIMTells 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:

add_executable(app main.cpp) add_dependencies(app generate_file)

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.

add_executable(app main.cpp) add_custom_target(run COMMAND $<TARGET_FILE:app> DEPENDS app WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Running app" VERBATIM )

Build and run that target:

cmake --build build --target run
LineMeaning
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 appMakes sure app is built before the run command executes.
WORKING_DIRECTORY ...Sets the folder where the command runs.

Common custom target examples

add_custom_target(format COMMAND clang-format -i ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp COMMENT "Formatting source files" VERBATIM ) add_custom_target(copy_assets COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/assets ${CMAKE_CURRENT_BINARY_DIR}/assets COMMENT "Copying assets" VERBATIM )

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

NeedUseReason
Generate a header, source file, model artifact, or text fileadd_custom_commandThe command has a real output that CMake can track.
Create a named action like run, format, or copy_assetsadd_custom_targetYou want a target that can be invoked by name.
Run a command while CMake configures the projectexecute_processThis happens during cmake -S . -B build, not during the build step.

Common Mistakes

  • Pointing CMAKE_PREFIX_PATH at the wrong folder: point it at the LibTorch root, not the include/ 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. Edit CMakeLists.txt, then reconfigure.
  • Using global include paths everywhere: prefer target-based commands such as target_include_directories, target_link_libraries, and target_compile_features.
  • Forgetting runtime libraries: if an executable builds but cannot start, the dynamic libraries may not be discoverable at runtime.
  • Using add_custom_target for generated files: if the command produces a real file, prefer add_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.