Skip to main content

    Lesson • Intermediate

    CMake & Build Systems

    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.

    What You'll Learn

    • Write a CMakeLists.txt with cmake_minimum_required and project()
    • Build any project with cmake -B build && cmake --build build
    • Create targets with add_executable and add_library
    • Wire targets together with target_link_libraries and target_include_directories
    • Choose Debug vs Release builds with CMAKE_BUILD_TYPE
    • Pull in third-party libraries with find_package

    💡 Real-World Analogy

    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.

    Worked example: a minimal CMakeLists.txt + build flow

    Read the commented recipe, then run the C++ that the 'hello' target compiles.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // CMake config is NOT C++, so it cannot run in this editor.
    // Read the commented CMakeLists.txt below, then run the program
    // to see the exact terminal commands that build it.
    
    // ============================================================
    //  project/CMakeLists.txt   (the build recipe for your project)
    // ============================================================
    //
    //  cmake_minimum_required(VERSION 3.20)   # oldest CMake allowed
    //  project(Hell
    ...

    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.

    Worked example: the three lines every CMakeLists.txt starts with

    cmake_minimum_required, project(), and add_executable explained.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // Every CMakeLists.txt starts with the same three lines.
    // They are shown here as comments because CMake is not C++.
    
    // ------------------------------------------------------------
    //  cmake_minimum_required(VERSION 3.20)
    //      Refuse to run on a CMake older than 3.20. This guarantees
    //      the features you use below actually exist.
    //
    //  project(MyApp VERSION 1.0 LANGUAGES CXX)
    //      Name = MyApp, version = 1.0, language = C++ (CXX).
    //      S
    ...

    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.

    🎯 Your turn: complete the CMakeLists.txt header

    Fill in the project name, C++ standard, and source file.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // 🎯 YOUR TURN — complete the CMakeLists.txt written in the comments.
    // (Edit the comment text, then run the C++ to see the goal output.)
    //
    //  cmake_minimum_required(VERSION 3.20)
    //
    //  # 1) Name the project "Greeter" with language C++:
    //  project(___ LANGUAGES CXX)        // 👉 replace ___ with  Greeter
    //
    //  # 2) Build C++17:
    //  set(CMAKE_CXX_STANDARD ___)       // 👉 replace ___ with  17
    //
    //  # 3) Build an executable target "greeter" from sr
    ...

    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.

    Worked example: a library, an app, and a test target

    add_library, target_include_directories, and target_link_libraries together.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // Real projects split reusable code into a LIBRARY that programs LINK against.
    
    // ============================================================
    //  CMakeLists.txt  (a library + two executables that use it)
    // ============================================================
    //
    //  project(Calculator LANGUAGES CXX)
    //
    //  # A static library built from the calculator sources:
    //  add_library(calc_lib STATIC src/calculator.cpp)
    //
    //  # Anyone linking calc_lib 
    ...

    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.

    Worked example: Debug vs Release builds

    How CMAKE_BUILD_TYPE changes the compiler flags.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // The SAME source can be built two ways. You pick the build TYPE
    // at configure time; CMake passes the right compiler flags for you.
    
    // ============================================================
    //  Debug — for developing:
    //    $ cmake -B build -DCMAKE_BUILD_TYPE=Debug
    //    $ cmake --build build
    //    -> keeps debug symbols (-g), turns optimisation OFF (-O0)
    //    -> step through code in a debugger; build is fast, runs slower
    //
    //  Release — for 
    ...

    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.

    Worked example: find_package and linking a found target

    Discover an installed library and link it into your program.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // find_package() locates a library already installed on the machine
    // and gives you a target you can link, e.g. Threads::Threads.
    
    // ============================================================
    //  CMakeLists.txt  (using an installed library)
    // ============================================================
    //
    //  find_package(Threads REQUIRED)     # find the system threads lib
    //                                     # REQUIRED = fail now if missing
    //  
    ...

    🔎 Deep Dive: configure vs build, and the build/ folder

    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.

    # One-time per machine / after CMakeLists.txt edits:
    cmake -B build -DCMAKE_BUILD_TYPE=Release   # configure
    
    # Every time you change a .cpp:
    cmake --build build                         # build
    ./build/myapp                               # run
    
    # Start fresh (safe — build/ is generated, not source):
    rm -rf build && cmake -B build

    Add build/ to your .gitignore — it is generated output, never source you commit.

    Pro Tips

    • 💡 Prefer the target_* commands: target_link_libraries and target_include_directories scope dependencies; the old global versions leak everywhere.
    • 💡 Start dependencies as PRIVATE: only switch to PUBLIC when a library's own headers expose another library to its users.
    • 💡 One library, many executables: put logic in a library and link it into both the app and the tests so tests use real code.
    • 💡 Build into a folder you can delete: out-of-source builds (cmake -B build) keep your source tree clean.

    Common Errors (and the fix)

    • "CMake Error: ... does not appear to contain CMakeLists.txt": you ran CMake from the wrong folder. Run cmake -B build from the directory that holds your CMakeLists.txt.
    • "undefined reference to ...": the function exists in a library you forgot to link. Add target_link_libraries(your_target PRIVATE the_lib).
    • "fatal error: foo.h: No such file or directory": the compiler can't find the header. Expose it with target_include_directories(the_lib PUBLIC include).
    • "Could NOT find Boost (missing: ...)": find_package couldn't locate an installed library. Install it (e.g. with vcpkg/apt) or point CMake at it; REQUIRED is why it stops here with a clear message.
    • Stale build after editing CMakeLists.txt: sometimes the cache is confused. Delete and reconfigure: rm -rf build && cmake -B build.

    📋 Quick Reference

    CommandWhat it does
    cmake_minimum_required(VERSION 3.20)Require CMake 3.20+
    project(MyApp LANGUAGES CXX)Name the project; set language
    add_executable(app main.cpp)Build a program target
    add_library(lib STATIC a.cpp)Build a reusable library
    target_link_libraries(app PRIVATE lib)Link a library into a target
    target_include_directories(lib PUBLIC include)Expose header folders
    find_package(Threads REQUIRED)Find an installed library
    cmake -B buildConfigure (generate build files)
    cmake --build buildCompile the project
    cmake -B build -DCMAKE_BUILD_TYPE=ReleaseOptimised release build

    Frequently Asked Questions

    Q: What is the difference between CMake and a compiler like g++?

    g++ compiles one or more source files into a program. CMake does not compile anything itself — it is a build-system generator that reads your CMakeLists.txt and writes the actual build files (Makefiles or Ninja files) that then call the compiler for you. CMake is the manager; g++ is the worker.

    Q: Why do I run two cmake commands (configure then build)?

    The first command, cmake -B build, is the configure step: it reads CMakeLists.txt and generates build files inside a folder called build. The second, cmake --build build, is the build step: it runs the compiler using those generated files. You only re-run the configure step when CMakeLists.txt itself changes.

    Q: What does PRIVATE vs PUBLIC mean in target_link_libraries?

    It controls who inherits the dependency. PRIVATE means only this target uses the library. PUBLIC means this target and anything that links to it both get it (used for libraries whose headers expose the dependency). When unsure, start with PRIVATE — it keeps dependencies from leaking.

    Q: Should I use add_executable or add_library?

    Use add_executable when the output is a runnable program (it has a main). Use add_library when the output is reusable code other targets link against — a static .a/.lib or shared .so/.dll. Real projects often have one library plus several executables (the app and its tests) that link to it.

    Q: What build type should I use, Debug or Release?

    Use Debug while developing — it keeps debug symbols and turns optimisation off so a debugger maps cleanly to your code. Use Release for the version you ship — it turns on optimisation (-O3) and strips debug info, making the program much faster. Set it with cmake -B build -DCMAKE_BUILD_TYPE=Release.

    Mini-Challenge: A CMakeLists.txt for a Game

    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.

    🎯 Mini-Challenge: write the build recipe

    Write a CMakeLists.txt with a library, an executable, and linking.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // 🎯 MINI-CHALLENGE: write a CMakeLists.txt for a small game
    //
    // In the COMMENTS below, write a CMakeLists.txt that:
    //   1. Requires CMake 3.20 and names the project "Snake" (LANGUAGES CXX).
    //   2. Sets CMAKE_CXX_STANDARD to 20 and marks it REQUIRED.
    //   3. Builds a static library "snake_lib" from src/game.cpp,
    //      with its headers exposed via target_include_directories(... PUBLIC include).
    //   4. Builds an executable "snake" from src/main.cpp
    ...

    🎉 Lesson Complete

    • ✅ Every CMakeLists.txt starts with cmake_minimum_required + project()
    • ✅ Build any project with cmake -B build (configure) then cmake --build build (build)
    • add_executable makes programs; add_library makes reusable libraries
    • target_link_libraries joins targets; target_include_directories exposes headers
    • CMAKE_BUILD_TYPE=Debug for developing, Release for shipping
    • find_package(Name REQUIRED) pulls in installed third-party libraries
    • Next lesson: Header Best Practices — organise declarations and avoid duplicate-definition bugs

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service