CMake

A makefile from lesson 20 is written for one system and one compiler. CMake describes the targets, their sources and their options, then generates build files for the platform you are on, in a separate directory.

project and executable

Every file starts with the minimum CMake version and a project() call that names the project and its languages. add_executable creates a target from the source files, and set(CMAKE_C_STANDARD 23) asks the compiler for the standard this course uses.

vi main.c

#include <stdio.h> int main(void) { printf("Hello from cmake\n"); return 0; }

vi CMakeLists.txt

cmake_minimum_required(VERSION 3.20) project(hello C) set(CMAKE_C_STANDARD 23) add_executable(hello main.c) target_compile_options(hello PRIVATE -Wall -Wextra)

The first command configures and writes the build files into build/; the second builds the target, and the third runs it:

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

-- The C compiler identification is GNU 15.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done (0.5s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/c2/L21/basic/build
[ 50%] Building C object CMakeFiles/hello.dir/main.c.o
[100%] Linking C executable hello
[100%] Built target hello
Hello from cmake

The path and the times depend on the machine; the compiler line and the last three lines repeat.

Compile options

target_compile_options attaches flags to one target, not to the whole project. Optimisation is a normal flag: -O2 defines __OPTIMIZE__, which the program can test at compile time. The same source then reports how it was built.

vi opt.c

#include <stdio.h> int main(void) { #ifdef __OPTIMIZE__ printf("built with optimisation\n"); #else printf("built without optimisation\n"); #endif return 0; }

vi CMakeLists.txt

cmake_minimum_required(VERSION 3.20) project(opt C) set(CMAKE_C_STANDARD 23) add_executable(opt opt.c) target_compile_options(opt PRIVATE -Wall -Wextra -O2)

cmake -S . -B build
cmake --build build
./build/opt

-- The C compiler identification is GNU 15.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done (0.3s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/c2/L21/opt/build
[ 50%] Building C object CMakeFiles/opt.dir/opt.c.o
[100%] Linking C executable opt
[100%] Built target opt
built with optimisation

PRIVATE means the flag applies to this target only; PUBLIC would pass it on to every target that links the library.

A library target

add_library builds an archive from the sources listed after the name, and target_link_libraries lets an executable use it. CMake passes the archive to the linker and orders the targets, so the hand-written link line from lesson 19 disappears.

vi util.h

#ifndef UTIL_H #define UTIL_H int twice(int value); #endif

vi util.c

#include "util.h" int twice(int value) { return value * 2; }

vi app.c

#include <stdio.h> #include "util.h" int main(void) { printf("%d\n", twice(21)); return 0; }

vi CMakeLists.txt

cmake_minimum_required(VERSION 3.20) project(app C) set(CMAKE_C_STANDARD 23) add_library(util STATIC util.c) target_compile_options(util PRIVATE -Wall -Wextra) add_executable(app app.c) target_link_libraries(app PRIVATE util) target_compile_options(app PRIVATE -Wall -Wextra)

cmake -S . -B build
cmake --build build
./build/app

-- The C compiler identification is GNU 15.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done (0.3s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/c2/L21/lib/build
[ 25%] Building C object CMakeFiles/util.dir/util.c.o
[ 50%] Linking C static library libutil.a
[ 50%] Built target util
[ 75%] Building C object CMakeFiles/app.dir/app.c.o
[100%] Linking C executable app
[100%] Built target app
42

The library is libutil.a in the build directory.

Tests with CTest

A test is a command with an expected exit status. The program below returns success only when twice behaves, and CTest runs it and reports the result. Tests live in the same CMakeLists.txt and are registered with add_test after enable_testing().

vi check.c

#include "util.h" int main(void) { return twice(21) == 42 ? 0 : 1; }

The CMakeLists gains the test executable, its link to the library and one add_test line, after enable_testing(). Then rebuild and run the tests from the build directory:

vi CMakeLists.txt

enable_testing() add_executable(check check.c) target_link_libraries(check PRIVATE util) add_test(NAME check_twice COMMAND check)

cmake --build build
cd build && ctest

[ 33%] Built target util
[ 66%] Built target app
[100%] Built target check
Test project /tmp/c2/L21/full/build
Start 1: check_twice
1/1 Test #1: check_twice ...................... Passed 0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) = 0.00 sec

The time rounds to zero because the test only does arithmetic. ctest --output-on-failure prints the output of a failing test.

Unknown command, missing source

CMake commands are case sensitive and spelled out in full. A typo is reported with the file and line, and configuration stops before any compiler runs. The source file is the usual one-line program:

#include <stdio.h> int main(void) { return 0; }

vi CMakeLists.txt

cmake_minimum_required(VERSION 3.20) project(bad C) add_execuable(bad main.c)

cmake -S . -B build

CMake Error at CMakeLists.txt:4 (add_execuable):
Unknown CMake command "add_execuable".


-- Configuring incomplete, errors occurred!

The second mistake is a source file that does not exist. This one is found after the compiler checks, when CMake generates the build:

vi CMakeLists.txt

cmake_minimum_required(VERSION 3.20) project(bad C) add_executable(bad main.c missing.c)

cmake -S . -B build

CMake Error at CMakeLists.txt:4 (add_executable):
Cannot find source file:

missing.c


CMake Error at CMakeLists.txt:4 (add_executable):
No SOURCES given to target: bad

The two errors name the same line because the first failure leaves the target without sources. Fix the file name and configure again; the existing build/ directory can stay.

Exercises

  1. Add a min function to the library and a test that checks it, then run ctest again.
  2. Build the same project with -O0 and -O2 in two build directories and compare the output of the opt program from the options section.
  3. Remove the project() line from a copy of the first CMakeLists and describe the error CMake prints.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does CMake do in a C project?

    Answer

    It generates build files for the platform

    CMake describes the targets and writes build files for another tool, which then invokes the compiler and linker.

  2. Which command creates an executable target in CMakeLists.txt?

    Answer

    add_executable

    add_executable takes the target name and its source files and creates a program target.

  3. What does PRIVATE mean in target_compile_options(app PRIVATE -Wall)?

    Answer

    The flag applies only to that target

    PRIVATE keeps the option on one target; PUBLIC would pass it on to targets that link this one.

  4. Which pair builds a static library and lets an executable use it?

    Answer

    add_library and target_link_libraries

    add_library(util STATIC util.c) creates the archive, and target_link_libraries(app PRIVATE util) links it.

  5. This program is built as the app target. What does ./build/app print? #include <stdio.h> int twice(int value) { return value * 2; } int main(void) { printf("%d\n", twice(21)); return 0; }

    Answer

    42

    The library function doubles 21, and the executable prints 42.

  6. This program is built with gcc -std=c17 -O2 opt.c -o opt. What does it print? #include <stdio.h> int main(void) { #ifdef __OPTIMIZE__ printf("built with optimisation\n"); #else printf("built without optimisation\n"); #endif return 0; }

    Answer

    built with optimisation

    -O2 defines __OPTIMIZE__, so the preprocessor keeps the first message.

  7. What does enable_testing() with add_test add to a project?

    Answer

    A test registered with CTest

    add_test registers a command and its expected status, and ctest runs the registered tests.

  8. Which commands configure and then build a project in build?

    Answer

    cmake -S . -B build then cmake --build build

    Configuration writes the build files first; the build command then invokes the generated tool.

  9. A CMakeLists.txt contains the line add_execuable(bad main.c), and the command cmake -S . -B build is run. What does CMake report?

    Answer

    Unknown CMake command "add_execuable".

    CMake commands are case sensitive and spelled in full, so the typo stops configuration with the file and line.

  10. A CMakeLists.txt contains add_executable(bad main.c missing.c), and the command cmake -S . -B build is run. What does CMake report?

    Answer

    Cannot find source file: missing.c

    The source file is checked during generation, and the error names the missing file and the target left without sources.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 and cmake 4.2.3 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev