Skip to content

Mastering Makefiles

Makefiles are powerful tools that automate the build process of software projects. They are essential in managing the compilation and linking of complex programs, especially those written in languages like C and C++. By defining rules and dependencies, Makefiles ensure that only the necessary parts of a project are rebuilt when changes occur, saving time and reducing errors.

Introduction

Makefiles are powerful tools that automate the build process of software projects. They are essential in managing the compilation and linking of complex programs, especially those written in languages like C and C++. By defining rules and dependencies, Makefiles ensure that only the necessary parts of a project are rebuilt when changes occur, saving time and reducing errors.

This tutorial will provide an in-depth understanding of Makefiles, covering everything from basic syntax to advanced features. Whether you're a beginner or looking to refine your skills, this guide will equip you with the knowledge to effectively utilize Makefiles in your projects.

Understanding Make command and Makefiles

Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles.

You are probably wondering ok but why using Makefiles?

  • Automation: Automate repetitive tasks like compilation and linking.
  • Efficiency: Rebuild only the parts of the code that have changed.
  • Organization: Manage complex build processes with multiple dependencies.
  • Portability: Provide a standard method to build software across different systems like docker but without virtualisation.

Basic Structure of a Makefile

A Makefile consists of rules. Each rule defines a target and the commands to build that target.

target some_target : dependencies to execute the following commands        something to do 
        some other command line 
          .
          .
          .

With : - target: The file to be generated. - dependencies: Files that the target depends on. - command: Shell commands to build the target.

Like ymal and others Makefiles are organised by tabulation, so you just need to put a tab character at the beginning of every command line you want to do for a specific target.

C basic example

hello: hello.o
    gcc -o hello hello.o

hello.o: hello.c
    gcc -c hello.c

Variables in Makefiles

Like every in other script adding variables make your Makefiles more flexible and maintainable 😎

You can define variable on the top of you Makefile like this :

CC = gcc
CFLAGS = -Wall -g

Using Variables

hello: hello.o
    $(CC) -o hello hello.o

hello.o: hello.c
    $(CC) $(CFLAGS) -c hello.c

Example with multiple files

CC = gcc
CFLAGS = -Wall -g

OBJECTS = main.o utils.o math.o

program: $(OBJECTS)
    $(CC) -o program $(OBJECTS)

main.o: main.c utils.h math.h
    $(CC) $(CFLAGS) -c main.c

utils.o: utils.c utils.h
    $(CC) $(CFLAGS) -c utils.c

math.o: math.c math.h
    $(CC) $(CFLAGS) -c math.c

clean:
    rm -f program $(OBJECTS)

Wildcards and pattern rules

Here we are, the programmers tricks, in every languages, shells, wildcards are everywhere 🧐

Wildcards

Simplify specifying multiple files (all .c files)

SOURCES = $(wildcard *.c)
OBJECTS = $(patsubst %.c,%.o,$(SOURCES))

Pattern Rules

Define rules for multiple targets.

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@
  • $@: The target filename.
  • $<: The first dependency filename.

Some example of pattern

CC = gcc
CFLAGS = -Wall -g

SOURCES = $(wildcard *.c)
OBJECTS = $(SOURCES:.c=.o)

program: $(OBJECTS)
    $(CC) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f program $(OBJECTS)

Functions in Makefiles

Make provides built-in functions for text manipulation, let's see how we can use the functions for our previous task like matching all .c files.

Common functions

  • $(wildcard pattern): Returns files matching the pattern.
  • $(patsubst pattern,replacement,text): Replaces pattern in text.
  • $(subst from,to,text): Replaces from with to in text.
  • $(shell command): Executes a shell command.

Example

SRCS = $(wildcard src/*.c)
OBJS = $(patsubst src/%.c, build/%.o, $(SRCS))

build/%.o: src/%.c
    $(CC) $(CFLAGS) -c $< -o $@

program: $(OBJS)
    $(CC) -o $@ $^

clean:
    rm -f build/*.o program

Automatic Variables (Advanced)

Automatic variables are set by Make for use within commands.

  • $@: The target name.
  • $<: The first prerequisite.
  • $^: All prerequisites.
  • $?: Prerequisites newer than the target.
  • $%: The target member name (for archives).

Example

program: main.o lib.o
    $(CC) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

Conditional Statements

Control the Makefile flow based on conditions like we programmer love doing, you know 🤗

ifeq (arg1, arg2)
    statements
else
    statements
endif

Example

DEBUG = true

ifeq ($(DEBUG), true)
    CFLAGS += -g
else
    CFLAGS += -O2
endif

program: main.o
    $(CC) $(CFLAGS) -o $@ $^

Phony Targets

In a Makefile, a phony target is a target that is not actually the name of a file. Instead, it's a label for a set of commands you want to run. Phony targets are used for commands like clean, all, install, and other operations that don't produce an output file with the same name as the target.

.PHONY: clean all install

If a file with the same name as your target exists, Make might get confused. Declaring a target as phony tells Make that it should ignore any file with that name.

Including Other Makefiles

Split Makefiles into multiple files for better organization.

include path/to/other.mk

Example

include sources.mk

program: $(OBJECTS)
    $(CC) -o $@ $^

clean:
    rm -f $(OBJECTS) program

sources.mk

SOURCES = main.c utils.c
OBJECTS = $(SOURCES:.c=.o)

Complete example

Let's see how we can leverage makefile for building a mkdocs docker friendly env.

Mkdocs is the library I've used for the current site you're visiting and below the Makefile I use for not taping the same commands over and over.

# Makefile for automating build, serve, and deploy mkdocs documentation

# Image name for Docker
IMAGE_NAME = <your-image-name>
CONTAINER_NAME = <your-container-name>
PORT = 5002

# Help command to list all targets
.PHONY: docker-build serve build deploy docker-clean help

# Build the Docker image with a specified version
docker-build:
    @if [ -z "$(VERSION)" ]; then echo "VERSION is not set. Use 'make docker-build VERSION=<version>' to set it."; exit 1; fi
    docker build -t $(IMAGE_NAME):$(VERSION) .

# Serve the MkDocs documentation locally
serve:
    mkdocs serve -a "0.0.0.0:5002"

# Serve the MkDocs documentation locally using Docker with some mounted volume 
docker-serve:
    docker run --rm -p $(PORT):5002 -v $(pwd):/docs --name $(CONTAINER_NAME) $(IMAGE_NAME):$(VERSION)

# Build the site and prepare it for deployment
build:
    docker run --rm --name $(CONTAINER_NAME) $(IMAGE_NAME):$(VERSION) mkdocs build

# Deploy the site to a remote server
docker-push:
    docker push $(IMAGE_NAME):$(VERSION) 

# Clean up Docker image and container
docker-clean:
    docker rm -f $(CONTAINER_NAME) || true
    docker rmi -f $(IMAGE_NAME):$(VERSION) || true


# Help command to list all targets
help:
    @echo "Available targets:"
    @echo "  docker-build   - Build the Docker image for the MkDocs site (use VERSION=<version>)"
    @echo "  serve          - Serve the MkDocs documentation locally"
    @echo "  build          - Build the static MkDocs site"
    @echo "  deploy         - Deploy the site to a remote server"
    @echo "  docker-clean   - Clean up Docker images and containers"
    @echo "  help           - Display this help message"

Wrap-up

By mastering Makefile syntax and features, you have seen how you can automate complex build tasks, improve efficiency, and maintain organized and scalable projects.

Hope this tutorial covered the essential aspects of Makefiles, providing examples and explanations to help you understand and apply these concepts. With practice, you'll be able to write effective Makefiles tailored to your project's needs.

More ressources for you to write Makefile like a 🥷🏼