Table of Contents
Why Build Automation Matters for Linux Developers
On Linux, you’ll often compile software from source. Doing that by manually typing long gcc commands quickly becomes painful and error‑prone. Build automation tools like make and CMake solve this by:
- Describing what to build (targets) and how to build it (commands).
- Automatically rebuilding only what changed.
- Managing dependencies between source files.
- Providing a reproducible, one‑command build (
make,cmake --build ., etc.).
This chapter focuses on using Makefiles and CMake as build systems for C/C++ projects on Linux, but the principles apply to many languages.
Make and Makefiles
The role of `make`
make is a build tool that:
- Reads instructions from a file called
Makefile(ormakefile). - Decides which files need to be rebuilt based on timestamps.
- Runs the necessary commands.
Basic Makefile structure
The essential concept is a rule:
target: prerequisites
<TAB>command
<TAB>commandKey points:
targetis usually a file to be generated (e.g.program,main.o).prerequisitesare files the target depends on.- Commands must be indented with a tab (not spaces).
Minimal example
A simple C program with main.c and hello.c:
# File: Makefile
CC = gcc
CFLAGS = -Wall -Wextra -O2
program: main.o hello.o
$(CC) $(CFLAGS) -o program main.o hello.o
main.o: main.c hello.h
$(CC) $(CFLAGS) -c main.c
hello.o: hello.c hello.h
$(CC) $(CFLAGS) -c hello.c
clean:
rm -f program *.oUsage:
- Build:
make(default target is the first one:program). - Clean:
make clean.
make only recompiles files where the source (e.g. main.c) or header hello.h is newer than the object file main.o or hello.o.
Variables
Variables make your Makefile configurable and less repetitive:
CC = gcc
CFLAGS = -Wall -g
LDFLAGS = -lm
SRC = main.c hello.c
OBJ = $(SRC:.c=.o)- Use
$(NAME)to refer to a variable. - Simple substitution:
$(SRC:.c=.o)turnsmain.c hello.cintomain.o hello.o.
Example with variables:
CC = gcc
CFLAGS = -Wall -Wextra -O2
LDFLAGS =
SRC = main.c hello.c
OBJ = $(SRC:.c=.o)
TARGET = program
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS)
clean:
rm -f $(TARGET) $(OBJ)Automatic variables
Within a rule, make offers useful automatic variables:
$@– the name of the target.$<– the first prerequisite.$^– all prerequisites.
Pattern rule using these:
CC = gcc
CFLAGS = -Wall -Wextra -O2
SRC = main.c hello.c
OBJ = $(SRC:.c=.o)
TARGET = program
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(TARGET) $(OBJ)
Here, the %.o: %.c rule tells make how to build any .o file from the corresponding .c file.
Phony targets
Some targets do not produce a file (e.g. clean, install, test). Declare them phony so make doesn’t confuse them with real files:
.PHONY: all clean install
all: program
clean:
rm -f program *.o
install: program
install -m 755 program /usr/local/bin
If a file named clean existed, .PHONY ensures make clean still runs the commands instead of thinking the target is already up to date.
Dependency tracking
In larger projects, you want .o files to rebuild when any included header changes. A common pattern is to let the compiler generate dependency files:
CC = gcc
CFLAGS = -Wall -Wextra -O2 -MMD -MP
SRC = main.c hello.c
OBJ = $(SRC:.c=.o)
DEP = $(SRC:.c=.d)
TARGET = program
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
-include $(DEP)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@-MMD -MPtellsgccto generate.dfiles with header dependencies.-include $(DEP)includes those rules if the.dfiles exist.
Organizing multiple build configurations
You can pass variables from the command line:
make CFLAGS="-Wall -O0 -g"Or define simple configuration targets:
.PHONY: debug release
debug: CFLAGS = -Wall -Wextra -g -O0
debug: program
release: CFLAGS = -Wall -Wextra -O2
release: programNow:
make debugbuilds with debugging flags.make releasebuilds with optimization.
CMake
make works with Makefiles you write by hand. CMake is a higher‑level build system generator:
- You describe your project in
CMakeLists.txt(declarative style). - CMake generates native build files:
- On Linux often Makefiles or Ninja build files.
- You then run the native build tool (
make,ninja, etc.).
This is especially useful for portable projects (Linux, Windows, macOS).
Installing and running CMake
On Debian/Ubuntu:
sudo apt install cmakeOn Fedora:
sudo dnf install cmakeBasic usage (out‑of‑source build is recommended):
mkdir build
cd build
cmake ..
cmake --build .cmake ..configures the project and generates build files.cmake --build .builds using the generated system (e.g.make).
Minimal CMakeLists.txt
For a very simple C program:
# File: CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(HelloC C)
add_executable(hello main.c)Explanation:
cmake_minimum_required– required CMake version.project– project name and languages.add_executable– defines an executable target namedhellofrommain.c.
Handling multiple source files
If your executable has multiple .c or .cpp files:
cmake_minimum_required(VERSION 3.10)
project(MyApp C)
set(SOURCES
main.c
hello.c
)
add_executable(myapp ${SOURCES})Or more concisely:
add_executable(myapp main.c hello.c)Include directories and compile options
To use header files in additional directories:
cmake_minimum_required(VERSION 3.10)
project(MyApp C)
add_executable(myapp main.c hello.c)
target_include_directories(myapp
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
To set compile options (like -Wall -Wextra -O2):
target_compile_options(myapp
PRIVATE
-Wall
-Wextra
-O2
)Key scopes:
PRIVATE– used only when compiling this target.PUBLIC– used for this target and consumers.INTERFACE– used only for consumers (header‑only libraries, etc.).
Creating and linking libraries
CMake can create libraries and then link them to executables.
Example: static library:
cmake_minimum_required(VERSION 3.10)
project(MyLibApp C)
add_library(hello STATIC
hello.c
)
target_include_directories(hello
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE hello)add_library(hello STATIC ...)– createslibhello.a.target_include_directories(hello PUBLIC ...)– executables linking tohellowill automatically get that include directory.target_link_libraries(myapp PRIVATE hello)linksmyapptohello.
Change STATIC to SHARED for a shared library (libhello.so).
Build types and configuration
CMake uses build types like Debug, Release, RelWithDebInfo, MinSizeRel.
You typically set this when configuring:
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .
In your CMakeLists.txt, you can customize based on build type:
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Configuring Debug build")
add_compile_definitions(DEBUG_BUILD)
endif()
Note: Multi‑config generators (like some IDEs or Ninja Multi‑Config) handle build types differently, but on Linux with Makefiles, CMAKE_BUILD_TYPE is standard.
Adding install rules
To install your built binaries and libraries (useful for packaging):
cmake_minimum_required(VERSION 3.10)
project(InstallExample C)
add_executable(mytool main.c)
install(TARGETS mytool
RUNTIME DESTINATION bin
)Then:
mkdir build
cd build
cmake ..
cmake --build .
sudo cmake --install .
This installs mytool into ${CMAKE_INSTALL_PREFIX}/bin (by default /usr/local/bin on Linux; change with -DCMAKE_INSTALL_PREFIX=/some/path).
Custom commands and targets
You may need generated files or helper steps (e.g. code generation, running tests).
Custom command: generating a file
add_custom_command(
OUTPUT generated.c
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/gen.py > generated.c
DEPENDS gen.py
COMMENT "Generating source file"
)
add_executable(myapp main.c generated.c)
CMake will run the command to produce generated.c when necessary.
Custom target: utility actions
add_custom_target(run_tests
COMMAND myapp --run-tests
DEPENDS myapp
COMMENT "Running application tests"
)Run with:
cmake --build . --target run_testsThis defines a build target that doesn’t correspond to a file but a task, similar to a phony target in Make.
Generator choices (Make vs Ninja)
On Linux, CMake commonly generates GNU Makefiles, but you can choose others.
To generate Ninja files:
mkdir build-ninja
cd build-ninja
cmake -G Ninja ..
ninja # instead of 'make'Or still use CMake’s wrapper:
cmake --build .The generator is chosen once per build directory; you typically keep one build directory per configuration.
When to Use Make vs CMake
Both are standard tools; many projects use one or both.
Makefile is often a good fit when:
- The project is small/simple.
- You only need to support Linux (or a few similar environments).
- You want full manual control of build commands.
CMake is often a good fit when:
- The project is cross‑platform (Linux, Windows, macOS).
- You need advanced dependency management and integration with IDEs.
- You want to generate different kinds of build files (Make, Ninja, Visual Studio).
- You plan installation, packaging, or exporting your project as a reusable library.
As a Linux developer, you should be comfortable:
- Reading and modifying Makefiles for simple builds.
- Understanding and editing CMakeLists.txt for modern, portable projects.