Makefiles et GNU Make

Motivation

L'écriture de fichiers Makefile, pour compiler un projet Unix/Linux, est un exercice négligé pour de plus ou moins bonnes raisons:

  • c'est plus ingrat que de développer une belle application graphique;
  • les environnement de développement intégrés (Visual Studio, Eclipse, Code::blocks, ....) camouflent souvent cette tâche sous-jacente;
  • on hérite souvent d'un projet avec un Makefile existant, alors pourquoi y toucher ?
  • le langage des Makefile est assez obscur, et le manuel un peu effrayant;
  • déboguer un Makefile est une corvée;

Mais parfois on vous demande d'en écrire un tout neuf, et c'est l'occasion de s'y replonger.

Exemples bons et moins bons

Un Makefile de débutant

Rappelons d'abord quelques problèmes posés par les Makefile simplistes de débutants (i.e. ceux de votre serviteur en école d'ingé):

  • les dépendances (notamment les en-têtes) sont indiquées explicitement: il est facile d'en oublier;
  • du coup on recourre à une variable $(HEADERS) citant toutes les en-tête, mais qui peut causer des recompilations superflues
  • les règles de compilations des fichiers .o ne diffèrent que par le nom de fichier: les dupliquer est ennuyeux et source d'erreurs;

Pièce à conviction (souvenir d'école!):

#Makefile élémentaire: "make" pour compiler, "make clean" pour nettoyer

#===== Définition des variables =====

# Le nom des variables importe peu.
# On y met les bibliothèques, les fichiers objets, les headers,
# les options de compilation et celles d'édition de liens

LIBS        = -lgraphic -lXm -lXmu -lXt -lX11 -lm -lvolmgt -lsocket
OBJETS          = labyrinthe.o tracer.o graphiques.o main.o
HEADERS         = utils.h globals.h
OFLAGS          = -g -c -Wall
CFLAGS          = -g
# nom du fichier exécutable final
EXECUTABLE      = essai

#==== Règles de production des fichiers ====

# Astuce : en l'absence de fichier 'all' ou 'clean',
# chaque exécution de Make, exécute les instructions
#   des lignes 'all' et 'clean' par défaut.

all: $(EXECUTABLE) clean

main.o: main.c $(HEADERS)               # main.o dépend de main.c et de HEADERS
    gcc $(OFLAGS) main.c

graphiques.o: graphiques.c $(HEADERS)   # fichiers objet intermédiaires
    gcc $(OFLAGS) graphiques.c

...

$(EXECUTABLE): $(OBJETS)               # fabrique l'exécutable
    gcc $(CFLAGS) $(OBJETS) -o $(EXECUTABLE) $(LIBS)

clean:                  # efface les fichiers intermédiaires
    rm -f *.o *~

Un Makefile pour gros projet

Après quelques années de vie professionnelle, de gros projets info, et enfin de lecture de manuel (cherchez l'erreur de priorité) mes Makefile ont gagné en intelligence:

  • Les règles génériques %.o ne sont plus dupliquées pour chaque fichier .o; Les actions associées s'écrivent alors avec les variables @< (première dépendance) @^ (toutes les dépendances) $@ (la cible);
  • Le compilateur liste les en-têtes inclus par un fichier .cpp, et indique cette dépendance dans un fichier .d associé: adieu les oublis!
  • Les fichiers de départ .cpp ne sont donnés qu'une fois dans SRC: des fonctions de GNU make comme patsub et filter en déduisent OBJ et DEPS;
  • Les variables simples, assignées par :=, sont plus simples à suivre que les variables récursives, assignées par = et pouvant être réévaluées à chaque utilisation (parfois utiles, mais pas ici!);
  • Cette structure est largement inspirée de l'article «Recursive Make Considered Harmful» écrit par Peter Miller ... dès 1997.
#Makefile d'exemple

# ----- D'abord, les flags avec lesquels compiler -----

CXX:=clang++
CXX_DBG:=-g
CXX_OPTIM:=-O3
CXXFLAGS:=-c -Wall $(CXX_DBG) $(CXX_OPTIM)

LD:=clang++
LDFLAGS:=-g

# ----- Ensuite, les produits de compilation souhaités -----

#Les exécutables à produire, et les objets dont ils dépendent
bit_map_perf.exe: bit_map_perf.o util.o
vecteur.exe: vecteur.o util.o

#Indiquer que 'clean' est juste une action, pas un fichier
.PHONY: clean

# ----- Variables pour les dépendances entre fichiers -----

#Les noms des fichiers sources .cpp donnent ceux des fichiers objet .o et
# des fichiers de prérequis .d à produire au besoin

SRC  := bit_map_perf.cpp util.cpp vecteur.cpp
OBJ  := $(patsubst %.cpp, %.o, $(filter %.cpp, $(SRC)))
DEPS := $(OBJ:.o=.d)

# ----- Règles de construction des fichiers -----

#Fichier .o: dépendances listées dans le .d à produire avant à partir du .cpp
%.o: %.cpp %.d
    $(CXX) $(CXXFLAGS) $< -o $@

#Fichier .d: dépend des mêmes fichiers que le .o correspondant:
#invoquer le compilateur avec -MM -MG donne les dépendances du .o,
#puis avec 'sed' on change la règle "f.o: ..." en "f.d f.o: ..."
%.d: %.cpp
    g++ -MM -MG $(CXXFLAGS) $*.cpp | sed -e "s@^\(.*\)\.o:@\./\1.d \./\1.o:@" > $@

#Edition de liens: les .o requis sont à indiquer explicitement auparavant.
%.exe:
    $(LD) $(LDFLAGS) $^ -o $@

#Nettoyage: non seulement les fichiers .o, mais aussi les .d:
clean:
    rm -f $(OBJ) $(DEPS)

# ----- Utiliser les fichiers de règles produits -----

#Les prérequis de chaque fichier .o figurent dans le fichier .d associé:
include $(DEPS)

# ----- Fin du Makefile ------

Pour en savoir plus

Débogage et astuces courantes

Quelques commandes élémentaires courantes lors de la mise au point d'un Makefile (tirées de discussions comme StackOverflow 1745939):

  • make -d [<cible>] : debug avec trop d'infos
  • make --debug=FLAGS : où FLAGS vaut a (all), b (basic), v (a bit verbose), i (implicit rules), j (invocation information), m (makefile remakes)
  • make -n : dry run (montre les commandes sans les exécuter)
  • make -k : produit le plus de cibles possibles, sans s'arrêter à la première erreur (plus pour déboguer le programme que le Makefile...)
  • make -r : ignore toutes les règles implicites par défaut; pour ignorer une seule règle implicite, on peut mettre, dans le Makefile, une dépendance sans règle, avec la même cible et mêmes prérequis, comme par exemple %.o: %.cpp;
  • make -j [<n>] : si votre Makefile fonctionne bien, vous pouvez accélérer la compilation en produisant jusqu’à <n> cibles parallèlement. Avec des dépendances bien spécifiées tout devrait bien aller!

Si vraiment vous n'avez pas de chance et devez chercher plus profondément, quelques pistes figurent dans l'article DrDobbs: debugging Makefiles:

  • simple: $(warning X is $(X)) : affiche la valeur finale de la macro X
  • simple: Définir une cible-bidon juste pour afficher les variables Makefile qui nous intéressent, puis exécuter make cible-bidon;
  • futé: associer l’idée précédente aux règles génériques : print-%: ; @$(error $\* is $($\*) ($(value $\*)) (from $(origin $*))) : affiche, pour toute cible X, sa définition (value) et son origine, moyennant un simple make print-X;
  • tordue: modifier la commande de shell lancée par Make, pour lui faire afficher la règle concernée: OLD_SHELL := $(SHELL)   SHELL = $(warning [$@ ($^) ($?)]) $(OLD_SHELL)

Aspects plus ésotériques des Makefile

Cet points ne sont mentionnés que pour la culture générale: les détails sont dans le manuel.

Espaces: comptés seulement à partir de la première référence, mais jusqu'à la fin de ligne:

  • "V =     #commentaire" est vide;
  • "V =   a b" vaut "a b";
  • "V = a b   #commentaire" vaut "a b  ";

Quelques moyens supplémentaires de définir des recettes:

  • «order-only targets»: règle cible: deps | OOT: make s'assure de l'existence de OOT, mais ne reconstruit pas cible s'il change, alors qu'il le ferait pour deps;
  • Une recette peut commencer après ;, dès la ligne de dépendances;
  • La variable spéciale .RECIPEPREFIX permet d'utiliser autre chose que TAB avant les recettes;

Et des façons moins courantes de manipuler les variables:

  • define v =\n......\nendef : valeur .... avec sauts de ligne à l'intérieur:

    define v =
    premiere ligne
    seconde ligne
    endef
    
  • undefine v : plus radical que "v :=";

Retour à la page Systèmes