6 points par GN⁺ 2025-06-21 | 1 commentaires | Partager sur WhatsApp
  • Makefile est un outil qui simplifie l’automatisation des builds C/C++ et la gestion des dépendances
  • Il détecte les fichiers modifiés à l’aide des horodatages et n’exécute la compilation que lorsque c’est nécessaire
  • Il explique avec des exemples la structure essentielle : règles (rules), commandes (commands) et dépendances (prerequisites)
  • Il aborde aussi de manière pratique des fonctions avancées comme les variables automatiques, les pattern rules et l’expansion de variables
  • Il présente l’importance de l’extensibilité et de la maintenabilité à travers un modèle de Makefile prêt pour des projets de taille intermédiaire

Présentation du guide tutoriel Makefile

  • Makefile est un outil central pour l’automatisation des builds et la gestion des dépendances d’un projet
  • À cause de nombreuses règles implicites et symboles peu visibles, il peut sembler complexe au premier abord, mais ce guide organise les points essentiels de manière concise avec des exemples directement exécutables
  • Chaque section permet une compréhension fondée sur la pratique grâce à des exemples d’exécution

Bien démarrer

Pourquoi Makefile existe

  • Un Makefile sert à recompiler uniquement les parties modifiées dans les grands programmes
  • Au-delà du C/C++, il existe des outils de build dédiés à de nombreux langages, mais Make reste utile dans des scénarios de build généraux
  • La logique clé consiste à détecter les fichiers modifiés pour n’exécuter que les tâches nécessaires

Systèmes de build alternatifs à Make

  • Écosystème C/C++ : SCons, CMake, Bazel, Ninja et d’autres options
  • Écosystème Java : Ant, Maven, Gradle
  • Go, Rust, TypeScript, etc. fournissent aussi leurs propres outils de build
  • Les langages interprétés comme Python, Ruby ou JavaScript n’ont pas besoin de compilation, donc la nécessité d’une gestion séparée via un Makefile est plus faible

Versions et variantes de Make

  • Il existe plusieurs implémentations de Make, mais ce guide est optimisé pour GNU Make (principalement utilisé sur Linux et MacOS)
  • Les exemples sont compatibles avec GNU Make 3 et 4

Comment exécuter les exemples

  • Après avoir installé make dans le terminal, enregistrez chaque exemple dans un fichier Makefile, puis exécutez la commande make
  • Dans un Makefile, les lignes de commande doivent impérativement être indentées avec un caractère de tabulation

Syntaxe de base d’un Makefile

Structure d’une règle (Rule)

  • cible: dépendance(s)

    • commande
    • commande
  • Cible : nom du fichier produit par le build (généralement un seul)

  • Commande : script shell réellement exécuté (commençant par une tabulation)

  • Dépendance : liste des fichiers qui doivent impérativement être prêts avant que la cible soit construite


L’essence de Make

Exemple Hello World

hello:  
	echo "Hello, World"  
	echo "This line will print if the file hello does not exist."  
  • La cible hello n’a pas de dépendance et exécute 2 commandes
  • Lors de l’exécution de make hello, si le fichier hello n’existe pas, les commandes sont exécutées. Si le fichier existe déjà, elles ne le sont pas
  • En général, on écrit les Makefile de sorte que la cible corresponde au nom du fichier

Exemple de base de compilation d’un fichier C

  1. Créez un fichier blah.c (avec le contenu int main() { return 0; })
  2. Écrivez le Makefile suivant
blah:  
	cc blah.c -o blah  
  • Lors de l’exécution de make, si la cible blah n’existe pas, la compilation s’exécute et crée le fichier blah
  • Même si blah.c est modifié ensuite, il n’y a pas de recompilation automatique → il faut ajouter une dépendance

Ajouter une dépendance

blah: blah.c  
	cc blah.c -o blah  
  • Désormais, si blah.c a été modifié, la cible blah est reconstruite
  • La détection des changements repose sur l’horodatage des fichiers
  • Si les horodatages sont modifiés manuellement, le comportement peut ne plus correspondre à l’intention

Exemples supplémentaires

Exemple de cibles et dépendances chaînées

blah: blah.o  
	cc blah.o -o blah   
  
blah.o: blah.c  
	cc -c blah.c -o blah.o   
  
blah.c:  
	echo "int main() { return 0; }" > blah.c   
  • Le processus de génération à chaque étape est automatisé en suivant les dépendances sous forme d’arborescence

Exemple de cible toujours exécutée

some_file: other_file  
	echo "This will always run, and runs second"  
	touch some_file  
  
other_file:  
	echo "This will always run, and runs first"  
  • Comme other_file n’est pas réellement créé en tant que fichier, la commande de some_file s’exécute à chaque fois

Make clean

  • La cible clean est souvent utilisée pour supprimer les artefacts de build
  • Ce n’est pas un mot réservé spécial dans Make : il faut la définir explicitement comme commande
  • Si un fichier s’appelle clean, cela peut créer une ambiguïté ; il est donc recommandé d’utiliser .PHONY

Exemple :

some_file:   
	touch some_file  
  
clean:  
	rm -f some_file  

Gestion des variables

  • Une variable est toujours une chaîne de caractères.
  • On recommande généralement :=, mais il existe plusieurs formes d’affectation comme =, ?= et +=
  • Exemple d’utilisation :
files := file1 file2  
some_file: $(files)  
	echo "Look at this variable: " $(files)  
	touch some_file  
  
file1:  
	touch file1  
file2:  
	touch file2  
  
clean:  
	rm -f file1 file2 some_file  
  • Référencer une variable : $(variable) ou ${variable}
  • Dans un Makefile, les guillemets n’ont pas de signification pour Make lui-même (mais ils peuvent être nécessaires dans les commandes shell)

Gestion des cibles

La cible all

  • Pour exécuter plusieurs cibles d’un coup, on donne ce rôle à la première cible (celle par défaut)
all: one two three  
  
one:  
	touch one  
two:  
	touch two  
three:  
	touch three  
  
clean:  
	rm -f one two three  

Cibles multiples et variables automatiques

  • Il est possible d’exécuter des commandes distinctes pour plusieurs cibles. $@ contient le nom de la cible courante
all: f1.o f2.o  
  
f1.o f2.o:  
	echo $@  

Variables automatiques et wildcards

Wildcard *

  • * parcourt directement les noms présents dans le système de fichiers
  • Il est recommandé de l’utiliser encapsulé dans la fonction wildcard
print: $(wildcard *.c)  
	ls -la  $?  
  • N’utilisez pas * directement dans une définition de variable
thing_wrong := *.o  
thing_right := $(wildcard *.o)  

Wildcard %

  • Il est surtout utilisé dans les pattern rules, où il permet d’extraire et d’étendre un motif donné

Fancy Rules

Règles implicites (Implicit)

  • Make intègre plusieurs règles par défaut cachées liées au build C/C++
  • Variables représentatives : CC, CXX, CFLAGS, CPPFLAGS, LDFLAGS, etc.
  • Exemple C :
CC = gcc   
CFLAGS = -g   
  
blah: blah.o  
  
blah.c:  
	echo "int main() { return 0; }" > blah.c  
  
clean:  
	rm -f blah*  

Static Pattern Rules

  • Elles permettent d’écrire de façon concise plusieurs règles suivant le même motif
objects = foo.o bar.o all.o  
all: $(objects)  
	$(CC) $^ -o all  
  
$(objects): %.o: %.c  
	$(CC) -c $^ -o $@  
  
all.c:  
	echo "int main() { return 0; }" > all.c  
  
%.c:  
	touch $@  
  
clean:  
	rm -f *.c *.o all  

Static Pattern Rules + fonction filter

  • En utilisant filter, on peut sélectionner uniquement les éléments correspondant à un motif d’extension donné
obj_files = foo.result bar.o lose.o  
src_files = foo.raw bar.c lose.c  
  
all: $(obj_files)  
.PHONY: all  
  
$(filter %.o,$(obj_files)): %.o: %.c  
	echo "target: $@ prereq: $

1 commentaires

 
GN⁺ 2025-06-21
Commentaires Hacker News
  • Quelqu’un raconte avoir vu en 1985, au laboratoire Graphics de Boston University, une personne créer un moteur de rendu 3D pour l’animation avec un Makefile. Cette personne était programmeuse Lisp, travaillait sur de la génération procédurale précoce et un système d’acteurs 3D, et avait écrit un Makefile d’une élégance remarquable d’environ dix lignes. La structure permettait de générer automatiquement des centaines d’animations à partir de simples dépendances sur les dates des fichiers. Les formes 3D de chaque image étaient produites en Lisp, puis Make générait les images. À l’époque, en 1985, contrairement à aujourd’hui où la 3D et l’animation semblent aller de soi, tout le monde trouvait cela stupéfiant, et l’auteur se souvient qu’il s’agissait de Brian Gardner, qui a ensuite travaillé sur le moteur de rendu 3D de Iron Giant et Coraline

    • Quelqu’un se demande s’il s’agit de la personne présentée ici : 3d-consultant.com/bio.html

    • Quelqu’un demande confirmation qu’il s’agit bien du film Coraline

  • Présentation de quelques indicateurs utiles et peu connus de Make

    • --output-sync=recurse -j10 : cela permet de regrouper stdout/stderr jusqu’à la fin du travail de chaque cible avant affichage ; sinon, les journaux se mélangent et deviennent difficiles à analyser
    • Sur un système chargé ou dans un environnement multi-utilisateur, on peut utiliser --load-average plutôt que -j pour réguler la charge système lors du parallélisme (make -j10 --load-average=10)
    • L’option --shuffle, qui mélange aléatoirement l’ordonnancement des cibles de build, est utile pour détecter les problèmes de dépendances dans un Makefile en environnement CI
    • Quelqu’un évoque l’idée de regrouper officiellement les différentes options de make et de les inclure dans un programme sous forme de texte ou de documentation pour en améliorer l’accessibilité

    • Une personne explique que l’option qu’elle utilise souvent est le drapeau -B, pour forcer une reconstruction complète

    • Quelqu’un dit avoir souvent vu make -j provoquer des problèmes sur des machines DOS, au point de considérer cela comme un bug

    • Quelqu’un demande si, sur un système chargé ou dans un environnement multi-utilisateur, les problèmes de parallélisation ne devraient pas être gérés par l’ordonnanceur de l’OS

    • Ces indicateurs sont utiles, mais ils ne sont pas portables ; il est donc recommandé de ne pas les utiliser en dehors de projets privés destinés à soi-même

  • L’idée selon laquelle un tutoriel pourrait faire l’impasse sur .PHONY est jugée peu convaincante sous prétexte de ne pas l’utiliser. Il vaudrait mieux apprendre à utiliser correctement l’outil

    • Dans une équipe, des débats ont eu lieu parce que Make était utilisé comme task runner et qu’il fallait ajouter et maintenir .PHONY pour toutes les recettes
    • Recommandation du guide de style Makefile de Clark Grubb (clarkgrubb.com/makefile-style-guide)
    • Partage de différentes expériences de style entre déclarer .PHONY recette par recette ou tout regrouper une fois en haut du fichier, avec le souhait qu’un linter puisse l’imposer
    • Après lecture, quelqu’un trouve le document bon, mais n’est pas d’accord sur plusieurs points
      • Appliquer aveuglément -o pipefail pose problème ; cela peut casser des usages comme grep dans un pipeline, donc mieux vaut l’appliquer selon le contexte
      • Marquer les cibles non liées à des fichiers avec .PHONY est rigoureux, mais presque toujours inutile et alourdit le Makefile ; il vaudrait mieux ne le faire qu’en cas de besoin
      • Pour les recettes qui produisent plusieurs fichiers de sortie, on utilisait autrefois des fichiers factices, mais GNU Make 4.3 prend désormais officiellement en charge les cibles groupées (voir ici)
  • Selon certains, Make est un outil spécialisé dans la compilation de grandes bases de code C

    • Quelqu’un dit l’utiliser volontiers comme exécuteur de tâches par projet, mais estime que Make n’est pas adapté à ce rôle et rend difficiles des choses comme les conditions
    • Il a aussi vu des tentatives ratées d’envelopper des outils comme Terraform
    • Un autre estime que Make n’est pas tant un task runner qu’un outil shell générique permettant de transformer des scripts shell linéaires en dépendances déclaratives

    • Un autre considère que voir Make uniquement comme un outil de build pour bases de code C n’est plus juste. Il rappelle que, ces vingt dernières années, des systèmes de build plus robustes et plus explicites ont été développés, et qu’il serait temps d’actualiser cette vision

    • Question sur ce qui ferait un bon task runner. (Avec ensuite des excuses pour avoir confondu ce que signifiait task runner)

  • just est recommandé comme outil moderne pour remplacer les parties des Makefiles qui deviennent complexes

    • just est bien pour remplacer une liste de scripts shell, mais ne remplace pas la fonction essentielle de Make : n’exécuter que les règles qui doivent l’être à nouveau

    • Autres alternatives mentionnées

    • Ces outils alternatifs se présentent comme des remplaçants de Make, mais quelqu’un estime qu’ils sont en réalité très différents et difficiles à comparer. Le cœur de Make, c’est la génération d’artefacts et le fait de ne pas reconstruire ce qui l’a déjà été. just, lui, sert simplement d’exécuteur de commandes

    • L’avantage de Make lorsqu’on l’utilise comme exécuteur de commandes, c’est la stabilité d’un outil standard installé presque partout. Même si les alternatives sont mieux conçues, elles demandent une installation supplémentaire, ce qui réduit leur intérêt

    • Quelqu’un explique qu’il utilise bien Task pour de petits projets personnels en C, mais ne sait pas encore s’il convient aussi à de grands projets (site officiel de Task)

  • Le fait que CMake ait récemment choisi ninja par défaut en considérant que les Makefiles ne convenaient pas au support des modules C++20 est jugé intéressant (guide CMake)

    • En pratique, il est presque impossible de définir statiquement les dépendances des cibles, d’où l’adoption d’une analyse dynamique via des outils comme clang-scan-deps (slides techniques)
    • Quelqu’un pense que cette limitation relève surtout d’une décision côté CMake ou du manque de contributeurs pour le générateur Makefile. ninja non plus ne prend pas directement en charge les modules C++ (issue liée), et il est même plus limité que Make car il impose de déclarer statiquement toutes les dépendances

    • Quelqu’un trouve que l’introduction des modules est en elle-même complexe et déroutante

  • Quelqu’un demande s’il y a des retours d’expérience avec tup. (documentation officielle)

    • tup est un système de build qui déduit automatiquement les dépendances à partir des accès au système de fichiers, et peut donc s’appliquer à n’importe quel compilateur ou outil
  • Une personne se présente comme le créateur et mainteneur principal de l’outil alternatif à Make nommé Task. Le projet est développé depuis plus de huit ans et continue d’évoluer

    • Si vous avez envie d’une nouvelle expérience, elle recommande de l’essayer, et invite à poser toutes les questions souhaitées
    • Liens vers le site officiel de Task et le dépôt GitHub
    • just est lui aussi recommandé comme autre alternative à Make (GitHub de just)

    • Coïncidence amusante : quelqu’un dit utiliser souvent Task et avoir même ouvert une issue ce matin

  • Ce tutoriel comporterait plusieurs problèmes subtils et potentiellement dangereux

    • Lors de l’analyse des options depuis MAKEFLAGS, pour gérer correctement les options longues ou les options courtes vides, il faudrait écrire ceci
      ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
    • Si une compatibilité avec l’ancienne version de make fournie par défaut sur OS X est nécessaire, beaucoup de fonctionnalités manquent ou se comportent légèrement différemment
    • Les autres problèmes sont pour l’essentiel des fautes de frappe ou des écarts mineurs de style, donc ils sont passés sous silence
    • À noter aussi que load est plus portable que guile, et qu’en environnement de compilation croisée il faut spécifier correctement le compilateur
    • Recommandation de lire absolument Paul’s Rules of Makefiles (ici), le manuel GNU make (ici) ainsi que les documents associés
    • La personne maintient également un petit projet de démonstration Makefile (démo GitHub)
  • Quelqu’un explique avoir l’habitude d’inclure un Makefile dans chaque dépôt GitHub

    • Comme il est facile d’oublier les commandes, les conserver dans un Makefile permet aussi d’ajouter facilement des étapes complexes, et il suffit ensuite d’exécuter make pour lancer immédiatement le comportement attendu du projet, sans avoir à tout mémoriser