Lesson 27 · Quality & debugging

Debugging CMake

When configure fails or a target links wrong. message(), --trace, cmake_diagnostic(), and verbose builds. A practical troubleshooting workflow.

CMake fails. Everyone’s CMake fails. The difference between frustrated and fast is knowing the three tools in this lesson. Printing, tracing, and seeing the actual commands.

1. Printing, message()

message() is your print statement. Levels matter. They are the difference between hidden info and a build-stopping error:

message(STATUS "Building calc with backend: ${CALC_BACKEND}")   # normal info
message(WARNING "Deprecated option used")                        # warning
message(FATAL_ERROR "GLFW not found. Install it or set CMAKE_PREFIX_PATH")  # stops configure

Tip

Use message(STATUS ...) for “what is happening” and reserve FATAL_ERROR for genuine show-stoppers with a hint about the fix, like the example above. message(FATAL_ERROR "foo not found") without a hint is how beginners produce forum posts.

Note

CMake 4.4 added a dedicated cmake_diagnostic() command for diagnostics, warnings and errors with full control. It is the replacement for the deprecated CMAKE_WARN_DEPRECATED and CMAKE_ERROR_DEPRECATED variables, see lesson 30. For everyday debugging, message() is still the tool.

2. Tracing, —trace

When you need to know what ran, in what order, where:

cmake --trace -S . -B build            # every command, every line
cmake --trace-expand -S . -B build     # with variables expanded
cmake --trace-source=CMakeLists.txt -S . -B build   # just one file

Perfect for “why is this set() being overridden?”. You see every call site, in order.

3. Seeing the actual commands

The configure succeeded, but the compile is wrong. See the real command lines:

cmake --build build -v          # verbose. Print every command (Ninja and Make)
# older make-only spelling.  make VERBOSE=1

ninja -C build -t commands app  # print the exact commands for target "app"

This is how you check include dirs (-I...), definitions (-D...), and flags. The ground truth, not what you think CMake generated.

A working troubleshooting flow

  1. Reproduce cleanly. rm -rf build && cmake -S . -B build. Half of “CMake bugs” are stale-cache ghosts (lesson 16).
  2. Read the error. CMake’s errors say exactly what it looked for and where. Read the last ten lines before anything else.
  3. Add message(STATUS ...) probes around the suspicious block. Print your variables, print CMAKE_CXX_COMPILER, print the paths.
  4. Trace if needed. cmake --trace-expand shows the exact order of execution.
  5. For link and compile problems. Verbose build, then read the flags, -I, -L, -l. Ninety percent of the time the answer is “the include dir you think is there is not” (lesson 9) or “the target you linked is not the one you think” (lesson 10).
  6. For policy warnings. Lesson 6. Bump cmake_minimum_required or set the policy.

The rename you will see in new CMake, 4.4

Three debugging flags got new spellings in CMake 4.4. The old spellings still work but are deprecated, so you will see the new names in docs and the old ones in blog posts:

Old (deprecated)New (4.4)
-Wdev / -Wno-dev-Wauthor / -Wno-author
--warn-uninitialized-Wuninitialized
--no-warn-unused-cli-Wno-unused-cli

Pitfall

“It worked before I changed nothing”. You almost always did change something. A cache variable persisted (lesson 16), or the build directory mixed two generators (lesson 21). The first fix to try is always a fresh build/ directory. It is cheap, safe, and eliminates the biggest class of confusion.

Tip

Build these habits early. Never debug a stale build directory. Always read the full error. Use -v builds to verify rather than guess. Those three habits solve more CMake problems than any flag.

Read the docs