Cmake Projects
By the end of this lesson you'll be able to write a CMakeLists.txt from scratch, build a project with two commands, split reusable code into a library, switch between Debug and Release builds, and pull in third-party libraries with find_package — the way every professional C++ project is built.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of CMakeLists.txt as a recipe and CMake as the head chef who reads it. The recipe lists ingredients (your source files) and the steps. CMake doesn't cook — it writes a precise, station-by-station prep list (the build/ folder) that the line cooks (your compiler, g++ or MSVC) actually follow. That's why building is two steps: first the chef plans the kitchen ( cmake -B build — the configure step), then the cooks execute the plan ( cmake --build build — the build step). Write the recipe once and the same kitchen runs on Linux, macOS, or Windows.
1. Your First CMakeLists.txt & the Build Flow
A CMakeLists.txt sits at the root of your project and declares the minimum CMake version, the project name and language, the C++ standard, and the program to build. You build with two commands: cmake -B build reads the recipe and writes build files into a fresh build/ folder (the configure step), then cmake --build build runs the compiler (the build step). Keeping generated files in build/ — an "out-of-source" build — means you can delete that folder any time without touching your code.
Every recipe opens with the same three lines: cmake_minimum_required , project() , and a target created by add_executable . A target is just a thing CMake knows how to build — usually a program or a library.
Your turn. Complete the CMakeLists.txt in the comments by filling in the three ___ blanks using the // 👉 hints, then run the program to see the target it would build.
2. Libraries, Executables & Linking
add_executable() builds a runnable program; add_library() builds reusable code other targets link against (a STATIC library is bundled straight into the programs that use it). You join them with target_link_libraries(program PRIVATE the_lib) , and you expose a library's headers with target_include_directories(the_lib PUBLIC include) so anything linking it can #include them. This is what makes testing clean: your test program links the same library as your app, so it exercises real code.
Now you try. The library and program already exist — add the one line that links them so the program can call the library's code.
3. Build Types: Debug vs Release
The same source code can be compiled two ways, and you choose which at configure time with CMAKE_BUILD_TYPE . Debug keeps debug symbols and turns optimisation off so a debugger maps cleanly to your lines — use it while developing. Release turns optimisation up to -O3 and strips the symbols, producing a much faster program — use it for what you ship. RelWithDebInfo is the middle ground: optimised but still debuggable.
4. Using Other Libraries with find_package
You rarely write everything yourself. find_package(Name REQUIRED) locates a library already installed on the machine and hands you a target — like Threads::Threads or OpenSSL::SSL — that you drop straight into target_link_libraries . The REQUIRED keyword tells CMake to stop with a clear "package not found" message during configure, instead of letting you hit a confusing compile error much later.
Configure ( cmake -B build ) reads CMakeLists.txt and generates real build files (Makefiles or Ninja files) inside build/ . Build ( cmake --build build ) runs those generated files to compile your code. You only re-configure when CMakeLists.txt changes; editing a .cpp just needs a re-build.
Add build/ to your .gitignore — it is generated output, never source you commit.
No blanks this time — just a brief and an outline. Write the full CMakeLists.txt in the comments (library + executable + linking), list the three build commands, then run the program to confirm the expected output. This is exactly the shape of a real project's build file.
Practice quiz
What is the difference between CMake and a compiler like g++?
- They are the same tool with different names
- CMake compiles faster than g++
- CMake is a build-system generator that writes build files; g++ actually compiles the code
- g++ generates CMakeLists.txt for you
Answer: CMake is a build-system generator that writes build files; g++ actually compiles the code. CMake reads CMakeLists.txt and generates build files (Makefiles/Ninja) that then call the compiler; CMake is the manager, g++ the worker.
Which two commands build a CMake project, and in what order?
- cmake -B build (configure), then cmake --build build (build)
- cmake --build build, then cmake -B build
- make, then cmake
- g++ -B build, then cmake run
Answer: cmake -B build (configure), then cmake --build build (build). cmake -B build configures (generates build files), then cmake --build build runs the compiler.
When do you need to re-run the configure step (cmake -B build)?
- Every time you edit any .cpp file
- Never; it runs automatically
- After every successful run of the program
- Only when CMakeLists.txt itself changes
Answer: Only when CMakeLists.txt itself changes. Editing a .cpp just needs a re-build; you only re-configure when CMakeLists.txt changes.
Which command creates a runnable program target?
- add_library
- add_executable
- find_package
- target_link_libraries
Answer: add_executable. add_executable builds a runnable program (it has a main); add_library builds reusable code.
What does add_library(... STATIC ...) produce?
- Reusable code (a .a/.lib) that other targets link against and is bundled into them
- A runnable program
- A CMakeLists.txt file
- A shared .so loaded at runtime
Answer: Reusable code (a .a/.lib) that other targets link against and is bundled into them. A STATIC library is reusable code bundled straight into the programs that link it.
What does PRIVATE vs PUBLIC control in target_link_libraries?
- Whether the library is open source
- The compiler optimisation level
- Who inherits the dependency: PRIVATE = only this target; PUBLIC = this target and anything that links it
- Whether the target is an executable or a library
Answer: Who inherits the dependency: PRIVATE = only this target; PUBLIC = this target and anything that links it. PRIVATE keeps the dependency to this target; PUBLIC propagates it to anything linking this target. Start with PRIVATE.
How do you link a library called calc_lib into an executable called calculator?
- link_libraries(calculator calc_lib)
- target_link_libraries(calculator PRIVATE calc_lib)
- add_executable(calculator calc_lib)
- find_package(calc_lib REQUIRED)
Answer: target_link_libraries(calculator PRIVATE calc_lib). target_link_libraries(target PRIVATE lib) joins a library into a target.
Which build type should you use for the version you ship?
- Debug
- There is only one build type
- Test
- Release
Answer: Release. Release turns on optimisation (-O3) and strips debug info for a faster program; Debug is for developing.
What does find_package(Threads REQUIRED) do?
- Downloads and installs the threads library
- Locates a library already installed on the machine and fails configure with a clear error if missing
- Compiles the threads library from source
- Creates a new library target named Threads
Answer: Locates a library already installed on the machine and fails configure with a clear error if missing. find_package locates an installed library and gives you a target to link; REQUIRED stops configure with a clear message if not found.
Why are the per-target commands target_include_directories/target_link_libraries preferred over the global include_directories?
- They compile faster
- Global commands are deprecated and removed
- Global commands leak settings into every target; target_* scopes each dependency to the target that needs it
- There is no difference
Answer: Global commands leak settings into every target; target_* scopes each dependency to the target that needs it. The target_* commands scope dependencies precisely, avoiding the baffling build issues that global includes cause in big projects.