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
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 :
Using Variables
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)
Pattern Rules
Define rules for multiple targets.
- $@: 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
Conditional Statements
Control the Makefile flow based on conditions like we programmer love doing, you know 🤗
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.
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.
Example
sources.mk
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 🥷🏼
- Best tutorial (for me) to master Makefile writing Makefiles here
- Good old official GNU manual for my RTMF people out here
- If you are not convinced yet take a look at Stackoverflow Why makefiles are so usefull
- Not for the noobs Introduction to Modern CMake
- Take a look at the official Linux Makefile form Linus Torvalds if you feel brave here