The Embedded New Testament

The "Holy Bible" for embedded engineers


Project maintained by theEmbeddedGeorge Hosted on GitHub Pages — Theme by mattgraham

Build Systems

Understanding build systems through concepts, not just syntax. Learn why build systems matter and how to think about software construction.

📋 Table of Contents


Concept → Why it matters → Minimal example → Try it → Takeaways

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.


📋 Quick Reference: Key Facts

Build System Fundamentals

Build System Types

Key Benefits

Common Build Tools


🧠 Core Concepts

What is a Build System?

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                      │
└─────────────────────────────────────────────────────────────┘

Why Not Just Compile Manually?

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                                 │
└─────────────────────────────────────────────────────────────┘

Dependency Management

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)               │
└─────────────────────────────────────────────────────────────┘

🏗️ Build System Types

Make-Based Systems

Make is the traditional build system that uses rules and dependencies:

Strengths:

Weaknesses:

CMake-Based Systems

CMake is a modern build system generator that creates platform-specific build files:

Strengths:

Weaknesses:

IDE-Integrated Systems

Many IDEs have their own build systems:

Examples:


🔧 Make Build System

Basic Makefile Structure

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:

How Make Works

  1. Reads the Makefile to understand the project structure
  2. Builds a dependency graph to see what depends on what
  3. Determines what needs to be built based on what changed
  4. Executes the rules in the correct order
  5. Only rebuilds what’s necessary (incremental builds)

🚀 CMake Build System

Basic CMakeLists.txt

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

┌─────────────────────────────────────────────────────────────┐
│                    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                               │
└─────────────────────────────────────────────────────────────┘

Advanced Features

Parallel Builds

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)                      │
└─────────────────────────────────────────────────────────────┘

Incremental Builds

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                     │
└─────────────────────────────────────────────────────────────┘

🧪 Guided Labs

Lab 1: Simple Makefile

Objective: Understand basic Makefile concepts.

Setup: Create a project with two source files that depend on each other.

Steps:

  1. Create main.c and helper.c files
  2. Write a simple Makefile with basic rules
  3. Build the project and observe the output
  4. Modify one file and rebuild - notice what gets recompiled

Expected Outcome: Understanding of how Make tracks dependencies and only rebuilds what’s necessary.

Lab 2: CMake Basics

Objective: Learn modern CMake approach.

Setup: Convert the Make project to use CMake.

Steps:

  1. Create a CMakeLists.txt file
  2. Configure the project with CMake
  3. Build using the generated build system
  4. Compare with the Make approach

Expected Outcome: Understanding of CMake’s declarative approach and cross-platform benefits.

Lab 3: Build Optimization

Objective: Learn about build performance.

Setup: Create a larger project with many source files.

Steps:

  1. Add more source files to the project
  2. Measure build time with single-threaded builds
  3. Enable parallel builds and measure improvement
  4. Experiment with different optimization flags

Expected Outcome: Understanding of how build systems can optimize the build process.


Check Yourself

Understanding Check

Application Check

Analysis Check


Further Reading

Industry Standards