The "Holy Bible" for embedded engineers
Understanding build systems through concepts, not just syntax. Learn why build systems matter and how to think about software construction.
Concept: A build system is like a smart factory that takes your source code and transforms it into a working program, automatically handling all the complex steps in between.
Why it matters: Without a build system, you’d have to manually remember and type every compilation command, manage dependencies, and ensure everything is built in the right order. This becomes impossible as projects grow, leading to build errors, forgotten steps, and wasted time.
Minimal example: A simple project with three source files that depend on each other. The build system automatically compiles them in the correct order and links them together.
Try it: Start with a single source file, then add more files and watch how the build system automatically handles the growing complexity.
Takeaways: Build systems automate the complex process of turning source code into executable programs, making development faster, more reliable, and less error-prone.
A build system is a tool that automates the process of converting source code into executable programs. Think of it as a recipe that knows exactly what ingredients (source files) are needed and in what order to combine them.
┌─────────────────────────────────────────────────────────────┐
│ Build System Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Source │───▶│ Build │───▶│ Executable │ │
│ │ Files │ │ System │ │ Program │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Dependencies│ │ Compilation │ │ Linking │ │
│ │ Graph │ │ Rules │ │ Process │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ The build system knows: │
│ • Which files depend on which │
│ • What order to compile things │
│ • How to link everything together │
│ • What to rebuild when files change │
└─────────────────────────────────────────────────────────────┘
Manual Compilation Approach:
┌─────────────────────────────────────────────────────────────┐
│ Manual Compilation │
├─────────────────────────────────────────────────────────────┤
│ │
│ $ gcc -c main.c -o main.o │
│ $ gcc -c helper.c -o helper.o │
│ $ gcc -c utils.c -o utils.o │
│ $ gcc main.o helper.o utils.o -o myprogram │
│ │
│ ❌ Problems: │
│ • Have to remember all commands │
│ • Easy to forget a file │
│ • No dependency checking │
│ • Rebuild everything every time │
│ • Different commands for different platforms │
│ • No parallel compilation │
└─────────────────────────────────────────────────────────────┘
Build System Approach:
┌─────────────────────────────────────────────────────────────┐
│ Build System Approach │
├─────────────────────────────────────────────────────────────┤
│ │
│ $ make │
│ │
│ ✅ Benefits: │
│ • Single command builds everything │
│ • Only rebuilds what changed │
│ • Automatic dependency checking │
│ • Parallel compilation possible │
│ • Works across different platforms │
│ • Easy to add new files │
└─────────────────────────────────────────────────────────────┘
The key insight is that build systems understand dependencies - which files need other files to be built first:
┌─────────────────────────────────────────────────────────────┐
│ Dependency Graph │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ main.c │ │ helper.c │ │ utils.c │ │
│ └─────┬───────┘ └─────┬───────┘ └─────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ main.o │ │ helper.o │ │ utils.o │ │
│ └─────┬───────┘ └─────┬───────┘ └─────┬───────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ myprogram │ │
│ └─────────────┘ │
│ │
│ Build order: utils.o → helper.o → main.o → myprogram │
│ │
│ If helper.c changes, only helper.o and myprogram │
│ need to be rebuilt (not utils.o or main.o) │
└─────────────────────────────────────────────────────────────┘
Make is the traditional build system that uses rules and dependencies:
Strengths:
Weaknesses:
CMake is a modern build system generator that creates platform-specific build files:
Strengths:
Weaknesses:
Many IDEs have their own build systems:
Examples:
A Makefile is a set of rules that tell the build system what to do:
# Simple Makefile for embedded project
PROJECT_NAME = my_project
TARGET = $(PROJECT_NAME).elf
# Source files
SRCS = main.c helper.c utils.c
# Object files (replace .c with .o)
OBJS = $(SRCS:.c=.o)
# Compiler and flags
CC = arm-none-eabi-gcc
CFLAGS = -mcpu=cortex-m4 -Wall -O2
# Default target
all: $(TARGET)
# Link the program
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@
# Compile source files
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Clean build files
clean:
rm -f $(OBJS) $(TARGET)
Key Concepts:
all
, clean
)CC
, CFLAGS
)CMake uses a more declarative approach:
# CMakeLists.txt for embedded project
cmake_minimum_required(VERSION 3.16)
project(MyProject)
# Set C standard
set(CMAKE_C_STANDARD 99)
# Set compiler flags
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=cortex-m4 -Wall -O2")
# Add source files
set(SOURCES
main.c
helper.c
utils.c
)
# Create executable
add_executable(${PROJECT_NAME} ${SOURCES})
Key Concepts:
┌─────────────────────────────────────────────────────────────┐
│ CMake Build Process │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CMakeLists. │───▶│ CMake │───▶│ Makefile │ │
│ │ txt │ │ Generator │ │ (or other)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Source │───▶│ Build │───▶│ Executable │ │
│ │ Files │ │ System │ │ Program │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ CMake generates the build system, then the build │
│ system builds your program │
└─────────────────────────────────────────────────────────────┘
Modern build systems can compile multiple files simultaneously:
┌─────────────────────────────────────────────────────────────┐
│ Parallel vs Sequential │
├─────────────────────────────────────────────────────────────┤
│ │
│ Sequential Build: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │file1│─▶│file2│─▶│file3│─▶│file4│─▶│link │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ Total time: 5 units │
│ │
│ Parallel Build: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │file1│ │file2│ │file3│ │file4│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │ │
│ └────────┼────────┼────────┘ │
│ ▼ ▼ │
│ ┌─────┐ ┌─────┐ │
│ │link │ │link │ │
│ └─────┘ └─────┘ │
│ Total time: 2 units (with 4 cores) │
└─────────────────────────────────────────────────────────────┘
Build systems only rebuild what changed:
┌─────────────────────────────────────────────────────────────┐
│ Incremental Build │
├─────────────────────────────────────────────────────────────┤
│ │
│ First Build: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │file1│ │file2│ │file3│ │file4│ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ After changing file2: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │file1│ │file2│ │file3│ │file4│ │
│ │ │ │ ❌ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ Only rebuild: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ │file2│ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ ✅ Saves time and ensures consistency │
└─────────────────────────────────────────────────────────────┘
Objective: Understand basic Makefile concepts.
Setup: Create a project with two source files that depend on each other.
Steps:
main.c
and helper.c
filesExpected Outcome: Understanding of how Make tracks dependencies and only rebuilds what’s necessary.
Objective: Learn modern CMake approach.
Setup: Convert the Make project to use CMake.
Steps:
CMakeLists.txt
fileExpected Outcome: Understanding of CMake’s declarative approach and cross-platform benefits.
Objective: Learn about build performance.
Setup: Create a larger project with many source files.
Steps:
Expected Outcome: Understanding of how build systems can optimize the build process.