Command Palette

Search for a command to run...

MTH 4300

Using CMake

Every program you write for this course is more than a single .cpp file — sooner or later you will have multiple source files, external libraries, and test binaries all living in the same project. CMake is the tool that describes how those pieces fit together and hands off the actual compilation to whatever compiler and build system your machine uses.

This primer covers enough CMake to build, link, and iterate on the projects in this course. You do not need to understand every detail; you need to be able to read a CMakeLists.txt, modify it, and know what to run when something goes wrong.

Why Not Just Use g++ Directly?

You can always compile a single file by hand:

Compiling manually
g++ -std=c++17 -Wall -o my_program main.cpp

That works for one file. It breaks down quickly when:

  • You have five source files that each depend on the same header.
  • You want to link against a third-party library like Google Test.
  • Your teammate is on Windows and you are on macOS.
  • You want your IDE to understand which files belong to the project.

CMake solves all of these. You describe your project once in a CMakeLists.txt file; CMake reads it and generates build files for whatever toolchain is on the machine — Makefiles on Linux and macOS, Visual Studio projects on Windows, Ninja files anywhere.

CMake does not compile your code. It generates the instructions for something else — make, ninja, or an IDE — to do the compiling. That two-step indirection is what makes it cross-platform.

Installation

Before writing any CMakeLists.txt, make sure CMake 3.15 or later is installed.

Check your version
cmake --version

If it is missing:

  • macOS: brew install cmake
  • Ubuntu/Debian: sudo apt install cmake
  • Windows: download the installer from cmake.org and add it to your PATH.

You will also need a C++ compiler:

  • macOS: xcode-select --install (gives you clang++)
  • Linux: sudo apt install build-essential (gives you g++)
  • Windows: install Visual Studio with the "Desktop development with C++" workload, or install MSYS2 and then pacman -S mingw-w64-x86_64-gcc

A Minimal Project

Every CMake project needs at least one file: CMakeLists.txt in the project root.

my_project/
├── CMakeLists.txt
└── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(my_program main.cpp)

Line by line:

  • cmake_minimum_required — guards against running with an older CMake that lacks features you need.
  • project — gives the project a name and sets some internal CMake variables.
  • set(CMAKE_CXX_STANDARD 17) — tells the compiler to use C++17. This course uses C++17 throughout.
  • add_executable — declares a build target named my_program that is compiled from main.cpp.

Out-of-Source Builds

Never run CMake inside your source tree. Instead, create a separate build/ directory. This keeps generated files (Makefiles, object files, the final binary) out of your source tree so git status stays clean.

Standard build workflow
mkdir build
cd build
cmake ..        # configure: reads CMakeLists.txt, generates build files
cmake --build . # compile: builds all targets

After the second command, my_program (or my_program.exe on Windows) will be in build/.

cmake .. means "configure, using the CMakeLists.txt one directory up." cmake --build . means "build whatever was configured in the current directory." You can also pass -j4 to cmake --build . -- -j4 to build with four parallel jobs on Linux/macOS.

Re-running after changes

Once the build/ directory is configured, you only need cmake --build . to recompile after editing source files. Re-run cmake .. only when you change CMakeLists.txt itself (add a new source file, change a flag, etc.).

Multiple Source Files

Real projects split code across multiple .cpp and .h files. List all the source files in add_executable:

my_project/
├── CMakeLists.txt
├── main.cpp
├── stack.h
└── stack.cpp
CMakeLists.txt — multiple sources
cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(my_program
    main.cpp
    stack.cpp
)

Header files (.h, .hpp) do not need to be listed — the compiler finds them when a .cpp includes them. Only list .cpp files (translation units that produce object files).

If you add a new .cpp file to the project and forget to add it to CMakeLists.txt, the linker will fail with "undefined reference to …" errors. The fix is almost always: add the file to add_executable, then re-run cmake ...

Libraries and Multiple Targets

As projects grow it helps to separate a reusable library from the executable that uses it. CMake supports this with add_library:

CMakeLists.txt — library + executable
cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# A static library built from these sources
add_library(data_structures
    stack.cpp
    queue.cpp
)

# The main executable, linked against that library
add_executable(my_program main.cpp)
target_link_libraries(my_program PRIVATE data_structures)

target_link_libraries wires data_structures into my_program. The PRIVATE keyword means the dependency is an implementation detail of my_program — consumers of my_program do not automatically inherit it.

Include Directories

If your headers live in a subdirectory (e.g., include/), tell CMake where to find them:

my_project/
├── CMakeLists.txt
├── include/
│   └── stack.h
├── src/
│   ├── stack.cpp
│   └── main.cpp
CMakeLists.txt — include directories
cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(data_structures src/stack.cpp)
target_include_directories(data_structures PUBLIC include/)

add_executable(my_program src/main.cpp)
target_link_libraries(my_program PRIVATE data_structures)

target_include_directories with PUBLIC means any target that links against data_structures also gets include/ added to its include path automatically. That is why my_program does not need its own target_include_directories call — it inherits the path from the library.

Compiler Warnings

Enable warnings so the compiler catches obvious mistakes before they become hard bugs:

Adding compiler warnings
add_executable(my_program src/main.cpp)
target_compile_options(my_program PRIVATE -Wall -Wextra -Wpedantic)

On MSVC (Windows), the equivalent flags are /W4 /WX. For a project that needs to build on both platforms you can branch:

Cross-platform warnings
if(MSVC)
    target_compile_options(my_program PRIVATE /W4)
else()
    target_compile_options(my_program PRIVATE -Wall -Wextra -Wpedantic)
endif()

Debug vs. Release Builds

CMake builds in an unoptimized, no-debug-info mode by default. Specify a build type when you configure:

Configuring build type
cmake .. -DCMAKE_BUILD_TYPE=Debug    # adds -g, enables sanitizers
cmake .. -DCMAKE_BUILD_TYPE=Release  # adds -O2 or -O3, disables assertions

For coursework, always use Debug so you get useful stack traces and the address sanitizer can catch memory errors. Use Release only when benchmarking.

After switching build type, delete the build/ directory and reconfigure from scratch. CMake caches the build type on first configure; changing the flag mid-run can produce inconsistent builds.

Quick Reference

TaskCommand
Configure (first time or after editing CMakeLists.txt)cmake -S . -B build
Build all targetscmake --build build
Build in parallelcmake --build build -j$(nproc)
Clean build artifactsrm -rf build (then reconfigure)
Debug buildcmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
Release buildcmake -S . -B build -DCMAKE_BUILD_TYPE=Release

-S . means "source directory is here"; -B build means "put build files in ./build". This is the modern CMake syntax and does not require you to cd into the build directory first.

Exercises

Q1.

You have this layout:

Project layout
my_project/
├── CMakeLists.txt
├── main.cpp
└── linked_list.cpp

Write a CMakeLists.txt that compiles both files into an executable named list_program using C++17.

cmake_minimum_required(VERSION 3.15)
project(ListProject)

# your code here

Q2.

You run cmake --build build and see:

Error output
undefined reference to `LinkedList::push_front(int)'
collect2: error: ld returned 1 exit status

You have linked_list.cpp in your project directory. What is the most likely cause, and how do you fix it?

The linker complains about a missing symbol, not the compiler.
Where does CMake learn which source files to compile?

Q3.

A teammate checks out your project and runs cmake --build build without configuring first. What happens, and what should they run instead?

The build/ directory does not exist yet. CMake needs two steps.

Q4.

You want to add compiler warnings (-Wall -Wextra) to your my_program target. Show where in the CMakeLists.txt you would add this, and what command to use.

cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(my_program main.cpp stack.cpp)

# add warnings here