Aller au contenu
Développement medium

Makefile : automatiser vos builds et tâches DevOps

27 min de lecture

Vous répétez les mêmes commandes à chaque build ? Vous oubliez l’ordre des étapes de déploiement ? Le Makefile résout ce problème depuis 1976 — et reste l’outil d’automatisation le plus universel en 2026.

GNU Make exécute des tâches (compilation, tests, déploiement) en respectant les dépendances entre fichiers. Il ne recompile que ce qui a changé, parallélise les tâches, et documente votre workflow dans un fichier versionnable.

Make a 50 ans, mais reste l’outil d’automatisation le plus utilisé dans le monde Unix. Pourquoi ? Parce qu’il résout un problème universel : éviter de refaire ce qui a déjà été fait.

Imaginons que vous modifiez un seul fichier dans un projet de 100 fichiers. Sans Make, vous devriez tout recompiler (lent). Avec Make, seul le fichier modifié et ses dépendants sont recompilés (rapide). C’est le principe de la compilation incrémentale.

Voici ce qui rend Make incontournable :

AvantageCe que ça signifie concrètement
UniverselPréinstallé sur Linux/macOS — pas besoin de convaincre l’équipe d’installer un outil
IncrémentalModifiez 1 fichier sur 100 → seul ce fichier est recompilé (gain de temps énorme)
ParallélisableSur une machine 8 cœurs, -j8 divise le temps de build par ~8
DocumentableLe Makefile est la documentation : lisez-le pour comprendre comment builder
Sans dépendancesPas de Node.js, Python ou Java requis — juste make
Intégré CI/CDmake test en local = make test dans GitHub Actions = même résultat
Fenêtre de terminal
sudo apt update && sudo apt install build-essential

Le paquet build-essential inclut make, gcc et les outils de compilation.

Fenêtre de terminal
make --version

Sortie attendue :

GNU Make 4.3
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Un Makefile contient des règles qui définissent comment construire des cibles à partir de dépendances.

Chaque règle suit la même structure :

cible: dépendances
commande

Prenons un exemple concret pour décortiquer chaque élément :

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

La cible (hello.o) : c’est le fichier que vous voulez créer. Quand vous tapez make hello.o, Make cherche cette règle.

Les dépendances (hello.c hello.h) : ce sont les fichiers nécessaires pour construire la cible. Si l’un d’eux est plus récent que la cible, Make exécute la commande. Sinon, il considère que la cible est à jour et ne fait rien.

La commande (gcc -c hello.c -o hello.o) : l’instruction shell à exécuter pour construire la cible à partir des dépendances. Elle doit être précédée d’une tabulation.

Créons un exemple minimal :

# Mon premier Makefile
hello:
echo "Hello, Make!"

Exécution :

Fenêtre de terminal
make hello

Sortie :

echo "Hello, Make!"
Hello, Make!

Make affiche la commande avant de l’exécuter. Pour masquer la commande, préfixez avec @ :

hello:
@echo "Hello, Make!"

Les variables simplifient la maintenance du Makefile.

# Définition
CC = gcc
CFLAGS = -Wall -Wextra -pedantic
TARGET = hello
# Usage avec $(...)
$(TARGET): main.o
$(CC) $(CFLAGS) -o $(TARGET) main.o

Make propose plusieurs façons d’assigner une valeur à une variable. La différence est subtile mais importante quand vos variables référencent d’autres variables.

Le piège classique : avec =, la valeur est évaluée à chaque utilisation. Si une variable référencée change entre-temps, le résultat change aussi. Avec :=, la valeur est figée au moment de la définition.

SyntaxeQuand l’utiliserExemple concret
VAR = valeurQuand la valeur dépend d’autres variables qui peuvent changerCFLAGS = $(BASE_FLAGS) $(EXTRA)
VAR := valeurQuand vous voulez figer la valeur immédiatement (plus prévisible)NOW := $(shell date)
VAR ?= valeurPour définir une valeur par défaut surchargeableDEBUG ?= 0make DEBUG=1
VAR += valeurPour ajouter à une variable existanteCFLAGS += -O2

Conseil : utilisez := par défaut. C’est plus prévisible et évite les évaluations en boucle accidentelles.

# Exemple pratique
CFLAGS := -Wall # := fige la valeur maintenant
CFLAGS += -O2 # Ajoute -O2 → CFLAGS = "-Wall -O2"
DEBUG ?= 0 # Valeur par défaut, surchargeable : make DEBUG=1

Dans une règle, Make crée automatiquement des variables qui contiennent les noms de fichiers impliqués. Ces “raccourcis” évitent de répéter les noms et rendent les règles génériques.

Analogie : pensez à $@ comme “moi” (la cible) et $< comme “mon ingrédient principal” (la première dépendance).

VariableCe qu’elle contientQuand l’utiliser
$@Le nom de la cible qu’on construitToujours, pour le fichier de sortie
$<La première dépendancePour compiler un seul fichier source
$^Toutes les dépendances (séparées par espaces)Pour l’édition des liens (rassembler plusieurs .o)
$*Le “radical” du pattern (la partie qui matche %)Dans les règles avec %

Exemple concret : dans la règle main.o: main.c hello.h, les variables valent :

  • $@ = main.o (ce qu’on fabrique)
  • $< = main.c (premier ingrédient)
  • $^ = main.c hello.h (tous les ingrédients)
# Compilation : un .c → un .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# $< = le fichier .c (ex: main.c)
# $@ = le fichier .o (ex: main.o)
# Édition des liens : plusieurs .o → un exécutable
hello: main.o utils.o
$(CC) -o $@ $^
# $@ = hello
# $^ = main.o utils.o

Voici un Makefile réaliste pour un projet C, testé et fonctionnel.

projet/
├── Makefile
├── main.c
├── hello.c
└── hello.h
# Variables
CC = gcc
CFLAGS = -Wall -Wextra -pedantic
TARGET = hello
# Fichiers objets
OBJS = main.o hello.o
# Cible par défaut
all: $(TARGET)
# Édition des liens
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# Compilation des fichiers .c en .o
%.o: %.c hello.h
$(CC) $(CFLAGS) -c $< -o $@
# Nettoyage
.PHONY: clean mrproper help
clean:
rm -f *.o
mrproper: clean
rm -f $(TARGET)
help:
@echo "Cibles disponibles :"
@echo " all - Compile le programme (défaut)"
@echo " clean - Supprime les fichiers objets"
@echo " mrproper - Supprime tout (objets + exécutable)"
@echo " help - Affiche cette aide"
Fenêtre de terminal
# Build complet
make

Sortie :

gcc -Wall -Wextra -pedantic -c main.c -o main.o
gcc -Wall -Wextra -pedantic -c hello.c -o hello.o
gcc -Wall -Wextra -pedantic -o hello main.o hello.o
Fenêtre de terminal
# Rien à faire si déjà compilé
make
make: Nothing to be done for 'all'.
Fenêtre de terminal
# Modifier un fichier → recompilation partielle
touch hello.c && make
gcc -Wall -Wextra -pedantic -c hello.c -o hello.o
gcc -Wall -Wextra -pedantic -o hello main.o hello.o

Les patterns avec % évitent de répéter les règles pour chaque fichier :

# Compile tous les .c en .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

Le % est un joker : main.o matche avec main.c, utils.o avec utils.c, etc.

Make inclut des fonctions pour manipuler les listes de fichiers. Elles évitent de coder en dur les noms de fichiers — le Makefile s’adapte automatiquement quand vous ajoutez ou supprimez des fichiers sources.

Le problème qu’elles résolvent : sans fonctions, vous devez lister manuellement chaque fichier .c. Si vous en ajoutez un, vous oubliez de mettre à jour le Makefile → bug. Avec $(wildcard *.c), Make détecte automatiquement tous les fichiers .c.

FonctionCe qu’elle faitExemple d’utilisation
$(wildcard *.c)Liste tous les fichiers qui matchent le patternDétecter automatiquement les sources
$(patsubst %.c,%.o,$(SRC))Remplace .c par .o dans une listeGénérer la liste des fichiers objets
$(shell cmd)Exécute une commande shell et récupère la sortie$(shell date), $(shell git describe)
$(filter %.c,$(FILES))Ne garde que les éléments qui matchentSéparer les .c des .h dans une liste mixte
$(info message)Affiche un message pendant le parsingDebug : voir la valeur d’une variable

Exemple typique : générer automatiquement la liste des fichiers objets à partir des sources.

# Avant (manuel, fragile)
OBJS = main.o utils.o config.o # Oubli facile si on ajoute un fichier
# Après (automatique, robuste)
SRC := $(wildcard *.c) # → main.c utils.c config.c
OBJS := $(patsubst %.c,%.o,$(SRC)) # → main.o utils.o config.o
# Détection automatique des sources
SRC_DIR := .
BUILD_DIR := build
SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES))
TARGET := $(BUILD_DIR)/hello
# Debug : afficher les variables
$(info SOURCES = $(SOURCES))
$(info OBJECTS = $(OBJECTS))
all: $(BUILD_DIR) $(TARGET)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(TARGET): $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@

Sortie :

SOURCES = ./hello.c ./main.c
OBJECTS = build/hello.o build/main.o
mkdir -p build
gcc -Wall -Wextra -c hello.c -o build/hello.o
gcc -Wall -Wextra -c main.c -o build/main.o
gcc -Wall -Wextra -o build/hello build/hello.o build/main.o

Make supporte des conditions basiques :

# Activer le debug si DEBUG=1
ifdef DEBUG
CFLAGS += -g -DDEBUG
$(info Mode DEBUG activé)
else
CFLAGS += -O2
endif

Usage :

Fenêtre de terminal
# Build optimisé (défaut)
make
# Build debug
make DEBUG=1

Sortie avec DEBUG=1 :

Mode DEBUG activé
gcc -Wall -Wextra -g -DDEBUG -c main.c -o main.o

Make évalue les conditions au moment où il lit le Makefile, pas pendant l’exécution. C’est important : vous ne pouvez pas tester le résultat d’une commande shell dans une condition Make (utilisez $(if ...) ou testez dans la commande shell elle-même).

ifdef VAR : vrai si la variable est définie, même si elle est vide. Utile pour activer un mode optionnel.

# Active le debug si DEBUG est défini (à n'importe quelle valeur)
ifdef DEBUG
CFLAGS += -g -DDEBUG
endif
# Usage : make DEBUG=1 ou même make DEBUG=anything

ifeq ($(VAR),value) : vrai si la variable égale exactement la valeur. Attention aux espaces — ifeq ($(VAR), value) (avec espace) ne matche pas value.

# Ajuste les flags selon le système d'exploitation
ifeq ($(OS),Linux)
LIBS += -lpthread
endif
ifeq ($(OS),Darwin)
LIBS += -framework CoreFoundation
endif

ifneq ($(VAR),) : vrai si la variable n’est pas vide. Plus strict que ifdef (une variable définie mais vide est considérée comme “fausse”).

# Compile en release sauf si DEBUG contient quelque chose
ifneq ($(DEBUG),)
CFLAGS += -g
else
CFLAGS += -O2
endif

Les Makefiles sont parfaits pour orchestrer les workflows DevOps.

Le pattern ## commentaire permet de générer automatiquement l’aide :

# Makefile DevOps
APP_NAME := myapp
VERSION := $(shell git describe --tags 2>/dev/null || echo "dev")
DOCKER_REPO := ghcr.io/monrepo
IMAGE := $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
.PHONY: help build test lint docker-build docker-push clean
.DEFAULT_GOAL := help
help: ## Affiche cette aide
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}'
build: ## Compile l'application
@echo "Compilation de $(APP_NAME)..."
$(CC) $(CFLAGS) -o $(APP_NAME) main.c hello.c
test: build ## Lance les tests
@echo "Exécution des tests..."
./$(APP_NAME)
@echo "Tests OK"
lint: ## Vérifie le code
@echo "Analyse statique..."
@which cppcheck > /dev/null && cppcheck --enable=all . || echo "cppcheck non installé"
docker-build: ## Construit l'image Docker
docker build -t $(IMAGE) .
docker-push: docker-build ## Pousse l'image sur le registry
docker push $(IMAGE)
clean: ## Nettoie les fichiers générés
rm -f $(APP_NAME) *.o
info: ## Affiche les informations du projet
@echo "Application : $(APP_NAME)"
@echo "Version : $(VERSION)"
@echo "Image : $(IMAGE)"

Exécution :

Fenêtre de terminal
make help
help Affiche cette aide
build Compile l'application
test Lance les tests
lint Vérifie le code
docker-build Construit l'image Docker
docker-push Pousse l'image sur le registry
clean Nettoie les fichiers générés
info Affiche les informations du projet
Fenêtre de terminal
make info
Application : myapp
Version : dev
Image : ghcr.io/monrepo/myapp:dev

Un Makefile centralise les commandes utilisées dans la CI :

.github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make lint
- run: make test
- run: make docker-build

Avantage : les mêmes commandes fonctionnent en local et en CI.

Quand Make ne fait pas ce que vous attendez, plusieurs options permettent de comprendre ce qui se passe “sous le capot”.

Voici les options les plus utiles, de la plus courante à la plus détaillée :

-n (dry-run) : la plus utile au quotidien. Make affiche les commandes qu’il exécuterait, sans rien exécuter. Parfait pour vérifier avant un make deploy.

Fenêtre de terminal
make -n clean
rm -f *.o
rm -f hello

-B (force rebuild) : ignore les timestamps et reconstruit tout. Utile quand Make dit “Nothing to be done” alors que quelque chose a changé.

Fenêtre de terminal
make -B

-p (print database) : affiche toutes les variables et règles connues de Make, y compris les règles implicites. Très verbeux, mais indispensable pour comprendre d’où vient une variable.

Fenêtre de terminal
# Voir la valeur de CC et CFLAGS
make -p | grep -E '^(CC|CFLAGS) ='
CC = gcc
CFLAGS = -Wall -Wextra -pedantic

-d (debug mode) : affiche chaque décision prise par Make. Très verbeux (ça peut faire des milliers de lignes), à utiliser en dernier recours.

OptionQuand l’utiliser
-nVérifier ce que Make ferait avant d’exécuter
-BForcer la reconstruction complète
-pTrouver la valeur d’une variable ou l’origine d’une règle
-dComprendre pourquoi Make reconstruit (ou pas) une cible

Voici les erreurs que vous rencontrerez le plus souvent, avec leur cause et la solution détaillée.

C’est l’erreur #1. Make exige une tabulation (pas des espaces) avant chaque commande. Beaucoup d’éditeurs convertissent les tabulations en espaces par défaut.

# ❌ Espaces (invisible mais Make refuse)
hello:
echo "Hello" # 4 espaces = ERREUR
# ✅ Tabulation (ce que Make veut)
hello:
echo "Hello" # 1 tabulation = OK

Solution : configurez votre éditeur pour insérer de vraies tabulations dans les Makefiles, ou utilisez cat -A Makefile pour voir les caractères invisibles (^I = tabulation, espace = rien).

Make ne trouve pas comment construire un fichier demandé comme dépendance. Causes possibles :

  1. Faute de frappe : main.o dépend de mian.c (au lieu de main.c)
  2. Fichier absent : le fichier source n’existe pas
  3. Règle manquante : pas de règle pour transformer .c en .o

Solution : vérifiez que le fichier existe (ls -la) et que son nom correspond exactement à la dépendance.

Ce n’est pas une erreur. Make considère que la cible est à jour (tous les fichiers de sortie sont plus récents que les sources).

Si c’est inattendu : un fichier source a peut-être été modifié sans que son timestamp change (copie depuis un autre dossier). Utilisez make -B pour forcer la reconstruction.

La commande appelée dans une règle n’existe pas dans le PATH.

Solution : installez le programme (apt install gcc) ou spécifiez le chemin absolu (CC := /usr/local/bin/gcc).

La cible A dépend de B, qui dépend de A. Make détecte la boucle et l’ignore, ce qui produit souvent un build incomplet.

Solution : revoyez vos dépendances pour casser le cycle.

Par défaut, Make exécute les commandes une par une. Sur un processeur multi-cœurs, c’est du gâchis : pendant qu’un fichier compile, les autres cœurs ne font rien.

L’option -j (pour “jobs”) permet d’exécuter plusieurs commandes en parallèle. Make analyse les dépendances pour déterminer ce qui peut s’exécuter simultanément.

Fenêtre de terminal
# 4 compilations en parallèle (bon pour un quad-core)
make -j4
# Autant de jobs que de cœurs (détection automatique)
make -j$(nproc)
# Attention : illimité peut saturer la mémoire
make -j # Déconseillé sur les gros projets

Gain concret : sur un projet de 100 fichiers avec 8 cœurs, make -j8 peut diviser le temps de compilation par 5 à 7 (pas 8, car l’édition des liens reste séquentielle).

Après avoir utilisé Make sur des dizaines de projets, voici les conventions qui évitent les problèmes.

Organisez votre Makefile de haut en bas : d’abord les variables (ce qui change), puis les cibles principales (ce qu’on veut faire), enfin les détails d’implémentation (comment on le fait).

# === Variables ===
# Regroupées en haut pour une modification facile
CC := gcc
CFLAGS := -Wall -Wextra
# === Cibles principales ===
# Ce que l'utilisateur appelle directement
.PHONY: all clean test help
.DEFAULT_GOAL := all # "make" sans argument = "make all"
all: build
# === Build ===
build: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^
# === Tests ===
test: build
./run_tests.sh
# === Nettoyage ===
clean:
rm -f $(OBJS) $(TARGET)
# === Aide ===
help:
@echo "Utilisation: make [cible]"

Une cible all comme point d’entrée. C’est ce que les gens attendent quand ils tapent make sans argument.

Une cible clean pour repartir de zéro. Indispensable quand le build est dans un état bizarre.

Une cible help pour documenter. Même vous, dans 6 mois, vous aurez oublié les options.

.PHONY systématiquement pour les cibles non-fichiers. Ça coûte une ligne et évite des bugs subtils.

Variables en MAJUSCULES pour les distinguer des commandes shell. $(CC) est clairement une variable Make, $cc pourrait être une variable shell.

Commentaires pour les règles complexes. Si vous avez besoin de relire la doc Make pour comprendre une ligne, ajoutez un commentaire.

Si vous ne devez retenir que l’essentiel de ce guide :

  1. Make automatise des tâches en ne ré-exécutant que ce qui est nécessaire. Il compare les dates de modification des fichiers pour décider quoi reconstruire.

  2. La tabulation est obligatoire avant chaque commande. C’est la cause d’erreur #1 — configurez votre éditeur correctement.

  3. Les variables automatiques vous font gagner du temps : $@ (cible), $< (première dépendance), $^ (toutes les dépendances).

  4. .PHONY déclare les cibles qui ne sont pas des fichiers (comme clean, test, help). Sans ça, un fichier nommé clean empêcherait make clean de fonctionner.

  5. -n (dry-run) montre ce que Make ferait sans l’exécuter. Utilisez-le systématiquement avant un make deploy ou toute action irréversible.

  6. -j4 parallélise les tâches sur 4 cœurs. Sur un projet conséquent, le gain de temps est significatif.

  7. Le pattern ## commentaire avec grep permet de générer automatiquement une aide — documentez vos Makefiles !

  8. Make n’est pas limité au C : il peut orchestrer n’importe quel workflow (Docker, Terraform, tests, déploiements).

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.