Build Systems
Introduction to Build Systems
When you click the "Compile" button in Arduino IDE, something very specific happens behind the scenes. Understanding what that is — and why professional embedded projects use Make, CMake, and Ninja instead — is the foundation of this section.
What Happens When You Click "Compile" in Arduino?
Arduino IDE hides the build process from beginners. When you click the ✓ button, it actually runs something like this:
# Arduino calls avr-gcc (for Uno) or xtensa-esp32-elf-gcc (for ESP32)
# with a huge automatically-generated command:
xtensa-esp32-elf-gcc \
-DARDUINO=10819 \
-DARDUINO_ESP32_DEV \
-DESP_PLATFORM \
-DF_CPU=240000000L \
-DCORE_DEBUG_LEVEL=0 \
-I/home/user/.arduino15/packages/esp32/hardware/esp32/2.0.14/cores/esp32 \
-I/home/user/.arduino15/packages/esp32/hardware/esp32/2.0.14/variants/esp32 \
-I/home/user/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/include \
-mlongcalls -mtext-section-literals -falign-functions=4 \
-Os -g3 -Wall -Wextra \
-c /tmp/sketch123/sketch.ino.cpp \
-o /tmp/sketch123/sketch.ino.cpp.o
That is one compiler command — and it only compiles your sketch. Arduino then runs dozens more to compile every library you included, then links them all together. This is exactly what a Build System automates.
The Two Core Problems Build Systems Solve
Problem 1 — Remembering Everything
A production ESP32 project may have:
- Your source files (20+
.cand.hfiles) - FreeRTOS (50+ files)
- WiFi stack (100+ files)
- Driver libraries (30+ files)
- Compiler flags specific to the ESP32 Xtensa core
- Linker scripts defining memory layout
Nobody types all of this by hand. A build system reads a short config file you write and generates all the right commands automatically.
Problem 2 — Only Rebuilding What Changed
If your project has 200 source files and takes 3 minutes to compile from scratch, you cannot recompile all 200 files every time you fix a typo in one file.
Build systems track which source files changed since the last build and only recompile those files. This is called an incremental build. For ESP-IDF projects, this reduces rebuild time from 3 minutes to under 5 seconds.
The Build Tool Family Tree
Your Code
│
▼
┌─────────────────────────────────────────────────────┐
│ CMake │
│ (You write CMakeLists.txt — cross-platform config) │
└───────────────────────┬─────────────────────────────┘
│ Generates
┌─────────┴──────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Makefile │ │build.ninja│
│ (Make) │ │ (Ninja) │
└────┬─────┘ └────┬─────┘
│ Runs │ Runs
▼ ▼
┌────────────────────────────────┐
│ GCC / Clang / MSVC │
│ (The actual C compiler) │
└────────────────────────────────┘
│
▼
┌──────────────────┐
│ firmware.bin │
│ (flashable to │
│ ESP32/STM32) │
└──────────────────┘
How This Compares Across Platforms
| Platform | Build Trigger | Generator | Build Tool | Compiler |
|---|---|---|---|---|
| Arduino IDE | Click ✓ | Arduino CLI (internal) | Arduino CLI | avr-gcc / xtensa-esp32-elf-gcc |
| ESP32 ESP-IDF | idf.py build | CMake | Ninja | xtensa-esp32-elf-gcc |
| STM32CubeIDE | Click ▶ | Eclipse CDT (internal) | Make | arm-none-eabi-gcc |
| STM32 (CLI) | cmake && ninja | CMake | Ninja | arm-none-eabi-gcc |
| Raspberry Pi | make or cmake | CMake | Make or Ninja | gcc (native ARM) |
| Desktop C | gcc main.c / make | Manual or CMake | Make or Ninja | gcc / clang |
What idf.py build Actually Does
When you run idf.py build for an ESP32 project, Python does this sequence automatically:
# Step 1: CMake configure — read your CMakeLists.txt, find all components,
# and generate build/build.ninja
cmake -G Ninja \
-DIDF_TARGET=esp32 \
-DCMAKE_TOOLCHAIN_FILE=$IDF_PATH/tools/cmake/toolchain-esp32.cmake \
..
# Step 2: Ninja build — read build.ninja, compile only changed files,
# link everything into firmware.elf
ninja -C build
# Step 3: Binary conversion — convert .elf to flashable .bin
esptool.py --chip esp32 elf2image build/project.elf
idf.py is a Python wrapper that runs these three steps for you. Once you understand this, you can run them manually when debugging build issues.
What STM32CubeIDE Does Under the Hood
STM32CubeIDE is an Eclipse-based IDE. When you click ▶ (Build), it runs arm-none-eabi-gcc with a Makefile it generates from your project settings. The Makefile looks like this:
# STM32 auto-generated Makefile (simplified)
TARGET = my_stm32_project
CPU = -mcpu=cortex-m4
FPU = -mfpu=fpv4-sp-d16 -mfloat-abi=hard
CC = arm-none-eabi-gcc
SOURCES = Core/Src/main.c \
Core/Src/stm32f4xx_it.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c
INCLUDES = -ICore/Inc \
-IDrivers/CMSIS/Include \
-IDrivers/STM32F4xx_HAL_Driver/Inc
$(TARGET).elf: $(SOURCES)
$(CC) $(CPU) $(FPU) $(INCLUDES) -O2 -Wall $(SOURCES) \
-T STM32F411RETx_FLASH.ld \
-o $(TARGET).elf
You can export this Makefile and run make from the terminal — no IDE required. This is how CI/CD pipelines build STM32 firmware headlessly.
The Build Output Files
| File | What It Is |
|---|---|
.o | Object file — one compiled source file, not yet linked |
.a | Static library — archive of multiple .o files |
.elf | Executable and Linkable Format — the complete program with debug symbols |
.bin | Raw binary — the .elf stripped to just bytes for flashing |
.hex | Intel HEX format — ASCII representation of the binary, used by some programmers |
.map | Linker map — lists every function and its RAM/Flash address |
.lst | Assembly listing — C code side-by-side with generated assembly |
Summary: Build System at a Glance
| Tool | Role | You Write |
|---|---|---|
| Make | Executes build recipes | Makefile |
| CMake | Generates Make/Ninja files | CMakeLists.txt |
| Ninja | Faster replacement for Make | Never — CMake generates it |
| idf.py | CMake + Ninja wrapper for ESP-IDF | CMakeLists.txt per component |
| GCC / Clang | The actual C compiler | .c and .h source files |
In the next sections we go deep into each layer: Makefiles, CMake, and Ninja — with examples for both desktop C and microcontroller firmware.

