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
- Add a min function to the library and a test that checks it, then run ctest again.
- Build the same project with -O0 and -O2 in two build directories and compare the output of the opt program from the options section.
- 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.
-
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.
-
Which command creates an executable target in
CMakeLists.txt?Answer
add_executableadd_executabletakes the target name and its source files and creates a program target. -
What does
PRIVATEmean intarget_compile_options(app PRIVATE -Wall)?Answer
The flag applies only to that target
PRIVATEkeeps the option on one target;PUBLICwould pass it on to targets that link this one. -
Which pair builds a static library and lets an executable use it?
Answer
add_libraryandtarget_link_librariesadd_library(util STATIC util.c)creates the archive, andtarget_link_libraries(app PRIVATE util)links it. -
This program is built as the
apptarget. What does./build/appprint?#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.
-
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
-O2defines__OPTIMIZE__, so the preprocessor keeps the first message. -
What does
enable_testing()withadd_testadd to a project?Answer
A test registered with CTest
add_testregisters a command and its expected status, andctestruns the registered tests. -
Which commands configure and then build a project in
build?Answer
cmake -S . -B buildthencmake --build buildConfiguration writes the build files first; the build command then invokes the generated tool.
-
A
CMakeLists.txtcontains the lineadd_execuable(bad main.c), and the commandcmake -S . -B buildis 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.
-
A
CMakeLists.txtcontainsadd_executable(bad main.c missing.c), and the commandcmake -S . -B buildis 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