Kahibaro
Discord Login Register

Build automation (Makefile, CMake)

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:

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:

Basic Makefile structure

The essential concept is a rule:

text
target: prerequisites
<TAB>command
<TAB>command

Key points:

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 *.o

Usage:

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:

make
CC = gcc
CFLAGS = -Wall -g
LDFLAGS = -lm
SRC = main.c hello.c
OBJ = $(SRC:.c=.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:

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 $@

Organizing multiple build configurations

You can pass variables from the command line:

bash
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: program

Now:

CMake

make works with Makefiles you write by hand. CMake is a higher‑level build system generator:

This is especially useful for portable projects (Linux, Windows, macOS).

Installing and running CMake

On Debian/Ubuntu:

bash
sudo apt install cmake

On Fedora:

bash
sudo dnf install cmake

Basic usage (out‑of‑source build is recommended):

bash
mkdir build
cd build
cmake ..
cmake --build .

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:

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:

cmake
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:

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)

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:

bash
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:

bash
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:

bash
cmake --build . --target run_tests

This 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:

bash
mkdir build-ninja
cd build-ninja
cmake -G Ninja ..
ninja          # instead of 'make'

Or still use CMake’s wrapper:

bash
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:

CMake is often a good fit when:

As a Linux developer, you should be comfortable:

Views: 25

Comments

Please login to add a comment.

Don't have an account? Register now!