Build Systems

CMake Basics

CMake is the industry-standard build system generator used by ESP-IDF, STM32 HAL, Qt, OpenCV, TensorFlow, and thousands of professional projects. You write one clean CMakeLists.txt and CMake generates the correct build files for any OS or compiler.


CMake vs Make — The Key Difference

MakeCMake
What you writeMakefile with compiler flagsCMakeLists.txt with high-level descriptions
What runs the compilerMake itselfMake or Ninja (CMake generates their files)
Cross-platformNo — separate Makefiles per OSYes — one CMakeLists.txt works everywhere
Cross-compile (MCU)Manual flagsToolchain files (-DCMAKE_TOOLCHAIN_FILE=...)
Used byOld projectsESP-IDF, STM32, Qt, OpenCV, TFLite

Example 1 — Single File (Desktop)

cmake
1cmake_minimum_required(VERSION 3.16)
2project(HelloWorld C)
3
4add_executable(hello main.c)
bash
mkdir build && cd build
cmake ..
cmake --build .
./hello

Example 2 — Multi-File with Library (Desktop)

text
project/
├── CMakeLists.txt
├── main.c
├── include/
│   └── sensor.h
└── src/
    └── sensor.c
cmake
1cmake_minimum_required(VERSION 3.16)
2project(SensorApp C)
3
4# Create a static library from sensor.c
5add_library(sensor_lib STATIC src/sensor.c)
6
7# Expose the include/ directory to anything that links sensor_lib
8target_include_directories(sensor_lib PUBLIC include/)
9
10# Set compile options just for this library
11target_compile_options(sensor_lib PRIVATE -Wall -O2)
12
13# Create the main executable
14add_executable(sensor_app main.c)
15
16# Link the library to the executable
17target_link_libraries(sensor_app PRIVATE sensor_lib)
bash
mkdir build && cd build
cmake ..
cmake --build . --parallel $(nproc)

Example 3 — Multi-Directory Project

text
project/
├── CMakeLists.txt          ← Top-level
├── main.c
├── drivers/
│   ├── CMakeLists.txt      ← Sub-project
│   ├── uart.c
│   └── uart.h
└── hal/
    ├── CMakeLists.txt
    ├── gpio.c
    └── gpio.h

Top-level CMakeLists.txt:

cmake
1cmake_minimum_required(VERSION 3.16)
2project(EmbeddedApp C)
3
4# Add subdirectories — CMake will read their CMakeLists.txt
5add_subdirectory(drivers)
6add_subdirectory(hal)
7
8add_executable(firmware main.c)
9target_link_libraries(firmware PRIVATE drivers hal)

drivers/CMakeLists.txt:

cmake
1add_library(drivers STATIC uart.c)
2target_include_directories(drivers PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

hal/CMakeLists.txt:

cmake
1add_library(hal STATIC gpio.c)
2target_include_directories(hal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

Example 4 — STM32 Cross-Compilation with CMake

CMake handles cross-compilation cleanly through toolchain files. The toolchain file tells CMake to use arm-none-eabi-gcc instead of the host gcc.

arm-none-eabi-toolchain.cmake:

cmake
1set(CMAKE_SYSTEM_NAME Generic)
2set(CMAKE_SYSTEM_PROCESSOR arm)
3
4set(CMAKE_C_COMPILER arm-none-eabi-gcc)
5set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
6set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
7set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
8set(CMAKE_SIZE arm-none-eabi-size)
9
10# Prevent CMake from testing the compiler with a host link
11set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

CMakeLists.txt for STM32F411:

cmake
1cmake_minimum_required(VERSION 3.16)
2project(stm32_project C ASM)
3
4# CPU and FPU flags
5set(CPU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
6set(CMAKE_C_FLAGS "${CPU_FLAGS} -Os -Wall -fdata-sections -ffunction-sections")
7set(CMAKE_EXE_LINKER_FLAGS
8 "${CPU_FLAGS} -specs=nano.specs -T${CMAKE_SOURCE_DIR}/STM32F411RETx_FLASH.ld \
9 -Wl,--gc-sections -Wl,-Map=${PROJECT_NAME}.map")
10
11# Sources
12file(GLOB_RECURSE SOURCES
13 "Core/Src/*.c"
14 "Drivers/STM32F4xx_HAL_Driver/Src/*.c"
15 "Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/gcc/startup_stm32f411xe.s"
16)
17
18# Includes
19include_directories(
20 Core/Inc
21 Drivers/STM32F4xx_HAL_Driver/Inc
22 Drivers/CMSIS/Device/ST/STM32F4xx/Include
23 Drivers/CMSIS/Include
24)
25
26add_definitions(-DSTM32F411xE -DUSE_HAL_DRIVER)
27
28add_executable(${PROJECT_NAME}.elf ${SOURCES})
29
30# Post-build: create .bin and .hex
31add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
32 COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}.elf>
33 ${PROJECT_NAME}.bin
34 COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${PROJECT_NAME}.elf>
35 ${PROJECT_NAME}.hex
36 COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${PROJECT_NAME}.elf>
37)

Build:

bash
mkdir build && cd build
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=../arm-none-eabi-toolchain.cmake
ninja

Example 5 — ESP32 ESP-IDF Component System

ESP-IDF wraps CMake in its own Component System. Instead of add_executable(), you use idf_component_register().

Minimal ESP32 project structure:

text
my_project/
├── CMakeLists.txt          ← Top-level (2 lines)
└── main/
    ├── CMakeLists.txt      ← Component (4 lines)
    └── main.c

Top-level CMakeLists.txt:

cmake
1cmake_minimum_required(VERSION 3.16)
2include($ENV{IDF_PATH}/tools/cmake/project.cmake)
3project(my_project)

main/CMakeLists.txt:

cmake
1idf_component_register(
2 SRCS "main.c"
3 INCLUDE_DIRS "."
4)

Adding a custom component:

text
my_project/
├── CMakeLists.txt
├── main/
│   ├── CMakeLists.txt
│   └── main.c
└── components/
    └── my_sensor/
        ├── CMakeLists.txt     ← Component registration
        ├── my_sensor.c
        └── include/
            └── my_sensor.h

components/my_sensor/CMakeLists.txt:

cmake
1idf_component_register(
2 SRCS "my_sensor.c"
3 INCLUDE_DIRS "include"
4 REQUIRES driver esp_log
5)

Then in main/CMakeLists.txt, add REQUIRES my_sensor:

cmake
1idf_component_register(
2 SRCS "main.c"
3 INCLUDE_DIRS "."
4 REQUIRES my_sensor
5)

ESP-IDF automatically builds my_sensor as a static library and links it to your app.


Useful CMake Variables

VariablePurpose
CMAKE_C_COMPILERC compiler to use
CMAKE_BUILD_TYPEDebug, Release, RelWithDebInfo
CMAKE_SOURCE_DIRRoot directory of the project
CMAKE_BINARY_DIRBuild directory (where you run cmake from)
CMAKE_CURRENT_SOURCE_DIRDirectory of the current CMakeLists.txt
PROJECT_NAMEName from project() command
CMAKE_TOOLCHAIN_FILEPath to cross-compilation toolchain file

CMake Build Types

bash
# Debug — no optimization, full debug symbols
cmake .. -DCMAKE_BUILD_TYPE=Debug

# Release — maximum optimization, no debug symbols
cmake .. -DCMAKE_BUILD_TYPE=Release

# RelWithDebInfo — optimized + debug symbols (best for profiling)
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo

CMake Quick Reference

CommandPurpose
cmake_minimum_required(VERSION x.x)Minimum CMake version
project(Name LANGUAGES C CXX)Name the project
add_executable(name src1.c src2.c)Create an executable
add_library(name STATIC src.c)Create a static library
target_include_directories(name PUBLIC dir/)Add include paths
target_link_libraries(name PRIVATE lib)Link a library
target_compile_options(name PRIVATE -Wall)Add compiler flags
add_definitions(-DFOO=1)Add preprocessor defines
add_subdirectory(subdir/)Include a sub-project
file(GLOB_RECURSE SRCS src/*.c)Collect all .c files
cmake .. -G NinjaGenerate Ninja files
cmake --build . --parallelBuild all with max threads
Previous
Makefiles