Skip to content

Build Systems

Writing C++ code is only the first step. To execute the code, it must be compiled and linked. While using the compiler directly works for single-file projects, professional C++ development relies on automated build systems to manage multiple dependencies, trigger incremental compilations, and compile binaries across different platforms.


1. Why Do We Need Build Systems?

A typical C++ application consists of multiple header files (.hpp) and source files (.cpp). If you modify only a single .cpp file, compiling the entire project from scratch is highly inefficient.

A Build System performs the following: 1. Dependency Tracking: Knows which source files depend on which headers. 2. Incremental Compilations: Compiles only the modified files, linking them with the cached unchanged object files. 3. Linker Orchestration: Manages linking order and integrates external third-party libraries.


2. Make and Makefiles

make is a classic build tool that reads instructions from a file named Makefile. It uses rules to compile code based on file timestamps.

1
2
3
4
5
6
7
       Target (File to create)
     my_program : main.o utils.o  ◄── Dependencies (Prerequisites)
    ┌───────────────────────────┐
    │  $(CXX) -o my_program ... │  ◄── Command (Must be preceded by a TAB character)
    └───────────────────────────┘

Anatomy of a Simple Makefile

# Define variables
CXX = g++
CXXFLAGS = -std=c++20 -Wall -Wextra -O2

# The default target
all: my_program

# Rule to link the final executable
my_program: main.o utils.o
    $(CXX) $(CXXFLAGS) -o my_program main.o utils.o

# Rule to compile main.cpp to object file
main.o: main.cpp utils.hpp
    $(CXX) $(CXXFLAGS) -c main.cpp

# Rule to compile utils.o
utils.o: utils.cpp utils.hpp
    $(CXX) $(CXXFLAGS) -c utils.cpp

# Cleanup rule
clean:
    rm -f *.o my_program

[!IMPORTANT] Strict Tab Constraint: Makefile commands must be indented using a literal Tab character. Using spaces instead of a tab will cause make to fail with a syntax error.


3. Modern Target-Based CMake

make is platform-dependent and becomes verbose in large projects. CMake is a meta-build system. It does not compile code directly; instead, it generates build files (like Makefiles for Linux, or Visual Studio Project files for Windows).

Legacy vs. Modern Target-Based CMake

  • Legacy CMake (Avoid): Set global settings using include_directories() and link_libraries(). This pollutes all targets in a project.
  • Modern CMake (Preferred): Uses a Target-Based approach where properties are bound directly to specific executable or library targets using target_* commands.

Modern CMakeLists.txt Structure

cmake_minimum_required(VERSION 3.15)
project(SensorApp VERSION 1.0 LANGUAGES CXX)

# Require C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 1. Create a Library Target (contains utility logic)
add_library(sensor_lib STATIC src/sensor.cpp)

# Specify include directories public to anyone linking this library
target_include_directories(sensor_lib PUBLIC include)

# 2. Create the Executable Target
add_executable(app src/main.cpp)

# 3. Link targets (app inherits include paths of sensor_lib automatically)
target_link_libraries(app PRIVATE sensor_lib)

Out-of-Source Builds

To keep your source code clean, always compile C++ projects in a separate build folder:

1
2
3
mkdir build && cd build
cmake ..       # 1. Generates Makefile based on CMakeLists.txt
cmake --build . # 2. Runs the compiler (e.g. calls make)

4. Third-Party Dependency Management: FetchContent

Modern CMake (3.11+) provides a built-in module called FetchContent that automatically downloads, builds, and links external libraries at configure-time, solving the legacy problem of manually installing libraries to /usr/local/lib.

include(FetchContent)

# Declare external repository (e.g. nlohmann/json library)
FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.10.5
)

# Download and load the project targets
FetchContent_MakeAvailable(json)

# Link it to your executable
target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)

5. Static vs. Dynamic Libraries

When building libraries, you must choose between two formats:

  Static Library (.a / .lib)
  ┌───────────┐     ┌───────────┐     ┌───────────────────────┐
  │ app.o     │  +  │ lib.a     │ ──> │ Compiled app (Large)  │
  └───────────┘     └───────────┘     └───────────────────────┘
                                      (Independent at runtime)

  Dynamic Library (.so / .dll)
  ┌───────────┐     ┌───────────┐     ┌───────────────────────┐
  │ app.o     │  +  │ lib.so    │ ──> │ Compiled app (Small)  │
  └───────────┘     └───────────┘     └───────────────────────┘
                          │           (Requires lib.so file)
                    Loaded at Runtime

1. Static Libraries (.a on Linux, .lib on Windows)

The linker copies all object code from the library directly into your final executable. - Pros: Independent. The executable can be distributed alone without any runtime library dependencies. - Cons: Large binary size. Modifying the library code requires compiling the entire application again.

2. Dynamic/Shared Libraries (.so on Linux, .dll on Windows)

The linker inserts a reference to the library, but the actual code is loaded dynamically in RAM when the application runs. - Pros: Small binary size. Multiple applications can share a single library file in RAM, reducing system memory usage. - Cons: Dependency issues ("DLL hell"). The application will crash on startup if the .so file is missing or updated with breaking changes.