Création de « Makefile » pour GNU Make

Avertissement: Cet article date de 1999, il est désuet !

Un Makefile est un fichier qui permet d’automatiser les étapes de compilation d’un projet à l’aide de l’utilitaire GNU Make. Cet utilitaire détermine quels fichiers doivent être compilés et comment, mais pour le faire, Make doit lire le Makefile. L’objectif de ce document est de vous initier à la conception de fichiers Makefile. Que vous soyez un néophyte ou plus avancé, la connaissance de l’outil make est indispensable pour tout projet de programmation, les IDE qui génèrent sans erreurs les Makefile sont rares. De plus, le fait d’automatiser la compilation est une bonne façon d’épargner du temps, peu importe la taille du projet.

Historique de GNU Make

GNU make est une oeuvre de Richard M. Stallman et Roland McGrath, conformément aux normes POSIX.2. Les normes POSIX sont un ensemble de règles de standarts Unix. La version et l’auteur d’un utilitaire Make peut changer d’une plateforme à l’autre, mais la syntaxe du Makefile est standart et le résultat est le même. La principale raison pour ceci est que le nom du compilateur n’est pas toujours le même et certains paramètres peuvent varier. Un simple script shell n’est pas efficace pour compiler, puisque le type de shell est loin d’être standart sous Unix (ash, csh, ksh, bash, etc..). Inutile de favoriser un standard de shell, ses usages possibles sont beaucoup trop polyvalents.

Les deux étapes de la compilation

Il y a typiquement deux étapes lors de la compilation d’un programme : la transformation en code machine des instructions et la création des liens entre les fonctions (qui rajoute évidemment encore un peu de code machine).

Ces deux étapes sont bien visibles lors de la compilation avec make. Lors de la première étape, des fichiers objets (*.o) sont générés. Ils sont la conversion des fichiers sources en langage machine, mais ils ne peuvent être exécutés puisque les liens entre les fonctions de ces fichiers n’ont pas encore été créés. Par exemple, la fonction cout ou printf seront invalides. Si une erreur se produit lors de cette étape, c’est dans la majorité des cas une erreur de syntaxe dans le code source.

La deuxième étape consiste évidemment à faire le liens entre les fonctions, pour ensuite générer le fichier exécutable final. C’est à cette étape que l’on retrouve les messages d’erreurs les plus frustrants. Le nom des fonctions, les passages de paramètres et tout les autres détails qui attraient à la communication entre les fonctions sont vérifiés ici.

Notez que les messages d’erreurs proviennent du compilateur et que vous auriez les mêmes messages que si vous compiliez à partir de la ligne de commande. Par contre, lorsque vous compilez un gros programme, vous pouvez compiler et déboguer plus efficacement si vous comprenez bien comment le compilateur fonctionne.

Premier exemple de Makefile

Cet exemple est pour un programme en C++, mais la seule différence avec le C est le nom du compilateur, qui serait gcc au lieu de g++. Les lignes débutant par ’#’ signifient que c’est un commentaire.

# *******************************************************************
# Exemple 1: Makefile pour un petit programme bidon
# Il contient 2 fichiers sources et 1 fichiers d'entêtes (.h)
# *******************************************************************

# Les objets sont les fichiers qui seront compilés et formeront par
# après l'exécutable.
OBJS = fonction1.o principal.o

# Ici, nous déclarons le nom du compilateur, puisqu'il peut varier
# selon la machine et c'est plus simple d'avoir à le modifier qu'une
# seule fois plutôt qu'à chaque endroit où nous faisons appel au
# compilateur.
CC = g++

# Les CFLAGS sont les options de compilation, ici -Wall signifie que
# je veut que le compilateur s'assure que j'écris une bonne syntaxe
# de code et -g signifie que je veut rajouter le code pour déboguer,
# ceci a beaucoup d'avantages lors d'un core dump. Vous voudrez
# peut-être jeter un coup d'oeuil au man page de votre compilateur
# s'il n'est pas g++ ou gcc.
CFLAGS = -Wall -g

# À partir d'ici, tout ce qu'il manque sont les différents blocs de
# commandes. Un bloc correspond à un paramètre possible pour la
# commande make. Par exemple, d'apres les conventions, "make all"
# signifie "compile tout" et c'est le premier bloc dans la liste.
# (Puisque c'est le premier bloc, c'est celui exécuté par la
# commande "make" sans parametres.) De plus, un bloc appelle souvent
# un autre bloc pour rendre la lecture plus facile.

# Le bloc "all" appelle implicitement d'autres blocs. Ici il execute
# le link de l'exécutable, mais puisque pour faire cet exécutable il
# faut les objets contenus dans la variale $(OBJS), ceux-ci seront
# compilés s'ils n'existent pas. Puisque l'action de compiler ces
# objets nécessitent des regles de compilation, leurs blocs seront
# appelés

all : $(OBJS)

     $(CC) $(OBJS) -o mon_programme_exécutable

# Le prochain bloc indique comment créer le fichier fonction1.o, puis
# main.o À la limite, nous pourrions aussi faire "make fonction1.o"
# pour le compiler. Bien entendu, c'est l'option "-c" qui indique au
# compilateur de ne pas créer les liens, donc le fichier ne sera pas
# pour le moment exécutable.

fonction1.o : fonction1.cxx fonction1.h

     $(CC) $(FLAGS) -c fonction1.cxx -o fonction1.o

main.o : main.cxx

     $(CC) $(CFLAGS) -c main.cxx -o main.o

# Maintenant que nous avons terminé d'indiquer à make comment
# compiler le code source, pourquoi pas ajouter quelques fonctions
# pour rendre l'entretien plus facile.

# "clean" existe par convention, ça signifie de nettoyer le
# répertoire et d'y laisser que le code source. Évidemment, plus le
# code source est gros, plus l'effet de cette fonction peut varier.

clean :

     rm -f mon_programme_exécutable core *.o

# La prochaine c'est moi qui s'amuse, puisque j'ai toujours des
# fichiers d'auto-sauvegarde avec xemacs.

extremiste :

     rm -f *~

Quelques trucs

À ce stade, peut-être pensez-vous que c’est facile, trop compliqué ou encore peut-être que je ne vais pas assez dans les détails de toutes les options possibles d’un Makefile. Peu importe, voici encore quelques fonctions qui peuvent accélérer la création d’un Makefile, ou encore, avoir un minimum de modifications à faire si vous recyclez un Makefile d’un projet à un autre.

- La fonction "wildcard" fait l’équivalent de chercher tout les fichiers avec un certain pattern. Elle peut être utile lorsqu’il faut affecter la variable SOURCES :

# Ceci listerais tout les fichiers .cxx du répertoire courant.
SOURCE := $(wildcard *.cxx)

- Une fois que vous aurez tout vos fichiers sources, vous voudrez sans doute les convertir dans le nom de fichier objet (.o). La fonction "patsubst" remplace toute expression "x" par "y" d’une variable.

# Cette ligne nous donne la liste d'objets.
OBJS := $(patsubst %.cxx, %.o, $(SOURCES))

- Dans un bloc de compilation, la variable $@ représente le nom du bloc. Utile plus souvent lorsque l’on compile un objet.

     fonction1.o : fonction1.cxx fonction1.h
           $(CC) $(CFLAGS) -c fonction1.cxx -o $@

Inutile de décrire toutes les variables d’environnement et toutes les fonctions possibles, mon but en écrivant ce texte était de vous initier aux Makefile. Je crois que pour un programme plus ou moins gros, vous en savez maintenant amplement.

Cet article datant de 1999 a été retrouvé grâce au « Wayback machine » et republié le 16 février 2005 par pur divertissement. Je reconnais que c’est plein de bêtises, quoique l’article reste relativement valable pour des petits projets qui dépendent sur peu de librairies (vaut mieux utiliser autogen/automake).

Français