GNU Make

Les Makefiles sont des fichiers permettant d’exécuter un ensemble d’actions, comme la compilation d’un projet, la mise à jour d’un rapport, où l’archivage de données. Les Makefile sont des fichiers shell particulier, ils respectent cependant les conventions de codages du shell. L’idée de ce document est de présenter l’idée générale d’un Makefile via un exemple basique, puis d’aller vers un exemple plus complexe, le Makefile pour notre robot.

Un exemple basique

Considérons un exemple de projet très basique, constitué de 3 fichiers : hello.c, hello.h et main.c. De manière littérale nous avons défini des fonctions dans hello.c, ces fonctions sont référencées par leurs prototypes dans hello.h. Par ailleurs nous avons définies des fonctions dans main.c, nous sous-entendons que les fonctions de main.c font appels à celles de hello.c (d’où l’inclusion de hello.h dans main.c).

Nous voulons donc compiler main.c pour créer un exécutable que nous appellerons project. Si nous essayons directement de faire gcc -Wall -Werror -std=c11 main.c project nous allons avoir des
références indéfinies. En effet, il faut d’abord compiler hello.c en hello.o pour que la machine connaisse les fonctions définies dans le hello.c (c’est le rôle du hello.o). Ainsi pour créer project il faut faire deux lignes de
commande gcc -Wall -Werror -std=c11 hello.c -c puis gcc -Wall -Werror -std=c99 hello.o main.c -o project.

Deux nouvelles options se présentent -c qui permet de dire au compilateur de créer un fichier “.o” et -o qui permet d’appeler le linker (c’est lui qui dit au compilateur que certainnes fonctions appelées dans main.c sont définies dans hello.o).

Maintenant que nous avons compris comment nous pouvions compiler ce projet, nous allons regarder comment automatiser ces étapes. Pour cela regardons la syntaxe type d’un Makefile.

  cible : dependances
          commandes

Analysons ces deux lignes de commandes, le cible permet de donner un nom ainsi dans le shell, nous pourrons taper make cible pour effectuer les actions correspondantes. Dans notre cas la cible serait project. Ensuite les dépendances sont les fichiers nécessaires à la création de la cible, dans notre cas, cela serait hello.o. Un exemple de Makefile complet pour ce projet serait le suivant

CFLAGS = -Wall -Werror -std=c11
CC = gcc

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

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

Un exemple plus complexe

Nous allons maintenant passer à un exemple plus complexe, l’exemple du Makefile utilisé pour le robot. Plusieurs complications et exigences vont se révéler avec ce projet

  • Beaucoup de fichiers pour le projet
  • Des dépendances entre les fichiers
  • Arreter de compiler les dépendances “à la main”
  • Avoir deux main (un main et un test)
  • Nettoyer son dossier
  • Avoir des fichiers dans différents dossier

Commençons par présenter les différents fichiers du projet.

code/
| Makefile
| rasp
|  | src
|  |  | actionneur.hpp
|  |  | affichage.cpp
|  |  | affichage.hpp
|  |  | asserv.cpp
|  |  | asserv.hpp
|  |  | detection.cpp
|  |  | detection.hpp
|  |  | main.cpp
|  |  | main.hpp
|  |  | navigation.cpp
|  |  | navigation.hpp
|  |  | protocole.cpp
|  |  | protocole.hpp
|  |  | world.cpp
|  |  | world.hpp
|  | tst
|  |   | test.cpp
|  |   | test.hpp
|

Chaque figure de ce graphique correspond à un .cpp et un .hpp (même principe qu’un .c et un .h en c++). Cela soulève donc un premier point : beaucoup de fichier à gérer il va donc falloir créer tous les fichiers binaires (les .o) pour tous ces fichiers. Nous devons donc créer asserv.o, world.o, navigation.o, detection.o, protocole.o, affichage.o. Nous allons vouloir automatiser la création de ces binaires pour cela nous allons utiliser des commandes spéciales aux Makefile. Pour automatiser la transformation des .cpp en .o nous avons les lignes suivantes

%.o: rasp/src/%.cpp
   $(CC) $(CFLAGS) -c $<

Cela constitue la première règle de notre Makefile, elle permet de transformer n’importe quel .cpp dans code/rasp/src/ en un .o (qui sera mis dans code/). Nous voyons apparaitre deux variables inconnues $(CC) et $(CFLAGS), ces variables seront définies en début de Makefile et permettent de régler le problème 6. En effet CC va contenir le compilateur et CFLAGS les options de compilation. Nous ajustons donc notre Makefile comme suit.

CFLAGS = -Wall -Wextra -std=c++11 -g -O3
CC=g++
%.o: rasp/src/%.cpp
    $(CC) $(CFLAGS) -c $<

Maintenant que nous avons obtenu un moyen de transformer tous les fichiers .cpp en fichier binaire .o nous allons voir comment créer nos deux exécutables que nous appellerons project (qui contiendra le projet en entier) et le test (qui contiendra le test à effectuer) nous créons alors deux règles :

CFLAGS = -Wall -Wextra -std=c++11 -g -O3
FILE=navigation.o detection.o protocole.o asserv.o world.o affichage.o
CC=g++

all: project

%.o: rasp/src/%.cpp
    $(CC) $(CFLAGS) -c $<

project: $(FILE)
    $(CC) $(CFLAGS) $(FILE) rasp/src/main.cpp -o project

test: $(FILE)
    $(CC) $(CFLAGS) $(FILE) rasp/tst/test.cpp -o test

Nous voyons alors une nouvelle variable apparaitre, la variable $FILE, cette dernière va lister les dépendances (c’est-à-dire tous les fichier .o nécessaire pour la réalisation des règles). A ce stade il ne reste plus beaucoup de chose à faire sur le Makefile, si vous avez tout compris jusque-là vous pourrez comprendre et créer un Makefile sans trop de soucis. Il faut encore rajouter deux règles qui sont très souvent présentes dans les Makefile all et clean (qui permet de nettoyer le dossier). Nous fournissons alors le Makefile final qui permet de réaliser le projet. Ce Makefile est totalement générique et est réutilisable pour tous vos projets.

CFLAGS = -Wall -Wextra -std=c++11 -g -O3
FILE=navigation.o detection.o protocole.o asserv.o world.o affichage.o
CC=g++

all: project

%.o: rasp/src/%.cpp
    $(CC) $(CFLAGS) -c $<

project: $(FILE) rasp/src/main.cpp
    $(CC) $(CFLAGS) $(FILE) rasp/src/main.cpp -o project

test: $(FILE) rasp/tst/test.cpp
    $(CC) $(CFLAGS) $(FILE) rasp/tst/test.cpp -o test

clean:
    rm -f *.o project test test_protocole

Exercice

Permanent link to this article: https://www.eirlab.net/2021/11/08/gnu-make/