Commande Unix make

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 04/08/2000 - Dernière mise à jour : 08/02/2001

Sommaire

Introduction
Fichiers de commande
Fichiers standard
Fichiers utilisateur
Exemples simples
Commande impérative : exemple 1
Commandes multiples : exemple 2
Commande conditionnelle : exemple 3
Commentaire
Ligne de cible
Fichier multi-cibles
Ligne de continuation
Ligne de commande de shell
macro-définitions
macro "utilisateur"
macros standard
Arguments de la commande make
Fichiers inclus

Bibliographie

Introduction

    La commande make est l'ancêtre des gestionnaires de projets. Par exemple, le gestionnaire de projets de Visual C++ de Microsoft génère un fichier d'attribut .mak analogue aux fichiers utilisés par la commande Unix make. Il s'agit en réalité d'un véritable langage de programmation dont le rôle essentiel est d'automatiser les compilations et éditions de liens d'applications constituées de multiples fichiers sources répartis dans de nombreux répertoires et sous-répertoires, et qui constituent un projet. Couplée au système RCS (Revision Control System) ou SCCS (Source Code Control System), elle permet de gérer de façon automatique les versions successives d'une application.

    Accessoirement, la commande make peut être utilisée en lieu et place d'une commande complexe, comme un alias.

    Le principe de la compilation conditionnelle est de ne déclencher une opération (compilation, édition de lien) que si elle est nécessaire, par exemple si une modification d'un fichier a rendu un fichier objet .o, une bibliothèque ou un exécutable obsolètes.

Sommaire

Fichiers de commande

Fichiers standard

    La commande make utilise un fichier de commande contenant les indications qui lui sont nécessaires. Par défaut, elle recherche dans le répertoire courant,  les fichiers d'identificateur standard dans l'ordre suivant :     Dans ces cas, la commande make est simplement lancée par :
 
make [options]

Sommaire

Fichiers utilisateur

    Si aucun des fichiers standard n'est trouvé, la commande make ci-dessus échoue. Il est possible de donner au fichier Makefile un identificateur particulier, par exemple MonMake, en utilisant l'option -f de la commande make :
 
make -f MonMake [options]

Sommaire

Exemples simples

Commande impérative : exemple 1

#                                            [1]
# Fichier Makefile
#
# Mise à blanc de l'écran
#
clear :                                      [2]
-->|clear                                    [3]

    Les lignes précédées d'un caractère '#' sont des commentaires : ligne [1] par exemple.

    La ligne [2] est appelée la cible : identificateur clear en colonne 1, suivie du caractère ':'.

    La ligne [3] est la commande associée à la cible clear. Attention : le symbole -->| qui précède la commande indique que la ligne doit impérativement commencer par une tabulation, mais celle-ci se traduit à la visualisation par une indentation, par exemple de 8 caractères, et le fichier apparaît ainsi :
 
#                                            [1]
# Fichier Makefile
#
# Mise à blanc de l'écran
#
clear :                                      [2]
        clear                                [3]

    L'exécution de ce fichier par :
 
make clear

provoque la mise à blanc de l'écran, comme le fait la commande Unix clear, ou l'alias classique cls :
 
alias cls "clear"

Sommaire

Commandes multiples : exemple 2

#
# Fichier MakeClean
#
# Suppressions des fichiers objets d'attribut .o
#
clean :
-->|clear                                    [1]
-->|echo "Nettoyage des fichiers .o :"       [2]
-->|rm -f *.o -v                             [3]

    Les lignes [1], [2] et [3] sont les commandes associées à la cible clean. Ce sont ces commandes que make fait exécuter en lançant successivement autant de shells qu'il y a de lignes (ici 3).

   L'exécution de ce fichier par :
 
make -f MakeClean

provoque par exemple l'affichage suivant :
 
echo "Nettoyage des fichiers .o :"            [1']
Nettoyage des fichiers .o :                   [2']
rm -f *.o -v
destruction de `exo_01.o'
destruction de `main.o'
destruction de `nsSysteme.o'
destruction de `nsUtil.o'
duo>

    La commande clear (ligne [1]) provoque le nettoyage de l'écran. La ligne [1'] est l'affichage de la commande exécutée (ligne [2]) par le shell, alors que la ligne [2'] est le résultat de l'exécution de cette commande.

Sommaire

Commande conditionnelle : exemple 3

#
# Fichier Makefile
#
# Compilation et édition de lien du fichier 
# source exo_01.cxx en un exécutable exo_01
#
exo_01 : exo_01.cxx                          [1]
-->|g++ -o exo_01 exo_01.cxx                 [2]

    Ce fichier provoque la compilation du fichier exo_01.cxx par la commande g++, puis l'édition de liens, le fichier exécutable étant nommé exo_01. L'identificateur exo_01.cxx de la ligne [1] est appelé une dépendance. En effet, la cible est l'exécutable exo_01, qui dépend du fichier exo_01.cxx : si exo_01.cxx est plus récent que exo_01, la compilation doit être effectuée. Dans le cas contraire, on obtient l'affichage suivant :
 
make: `exo_01' is up to date.
duo>

    Ce fichier provoque la compilation du fichier exo_01.cxx par la commande g++, puis l'édition de liens, le fichier exécutable étant nommé exo_01.

Sommaire

Commentaire

    Une ligne commençant par le caractère # est une ligne de commentaire. Eviter toute ligne vierge : à la lecture, il est impossible de vérifier si elle ne contient pas des caractères parasites non affichables, mais que pourrait interpréter la commande make, comme par exemple une tabulation

Sommaire

Ligne de cible

    Une ligne de cible est constituée de deux parties :     Nous dirons q'une cible est à jour (up to date)si elle ne nécessite pas l'exécution des commandes associées. Nous dirons qu'une cible a été mise à jour, ou réactualisée, lorsque les commandes qui lui sont associées ont été déclenchées.

    En l'absence de dépendances, la cible est impérative et les commandes associées sont toujours effectuées. C'est le cas de l'exemple 1.

    En présence de dépendances, le déclenchement des commandes associées dépend de plusieurs facteurs :

    L'exemple ci-dessous illustre la dépendance de la cible de plusieurs fichiers :
#/**
#*
#* @File : Makefile
#*
#* @Authors : D. Mathieu
#*            M. Laporte
#*
#* @Date : 02/08/2000
#*
#* @Version : V1.0
#*
#* @Synopsis : Makefile de l'exo
#*
#**/
#
# Edition de liens
#
exo : exo.o nsUtil.o
-->|g++ -s -o exo exo.o nsUtil.o
#
exo.o : exo.cxx nsUtil.h
-->|g++ -c exo.cxx
#
nsUtil.o : nsUtil.cxx nsUtil.h
-->|g++ -c nsUtil.cxx

Sommaire

Fichier multi-cibles

Cibles dépendantes

    Nous avons vu qu'un fichier Makefile peut contenir plusieurs cibles dépendantes les unes des autres. Sauf indication particulière, la commande make ne traite que la première cible qu'elle rencontre dans le fichier. Dans cet exemple, la première cible, exo, est la cible principale à partir de laquelle make essaie d'atteindre récursivement toutes les cibles intermédiaires, jusqu'à ce que toutes les cibles terminales soient des cibles impératives ou des fichiers. Si les cibles sont permutées comme ci-dessous,
 
#
# Makefile 

exo.o : exo.cxx nsUtil.h 
-->|g++ -c exo.cxx 

nsUtil.o : nsUtil.cxx nsUtil.h 
-->|g++ -c nsUtil.cxx

exo : exo.o nsUtil.o 
-->|g++ -s -o exo exo.o nsUtil.o 

la ligne de commande :
 
duo>make -f Makefile

ne provoquera que la compilation de exo.cxx en exo.o (première cible).

    Il est possible de forcer la commande make à atteindre une cible particulière en l'indiquant dans la ligne de commande :
 
duo>make -f Makefile exo

    Cette fois, c'est la recompilation complète et l'édition de liens qui seront effectuées si nécessaire. Il est donc important que la cible principale soit toujours indiquée avant les cibles intermédiaires. Noter de plus que, même dans ce cas, le fichier Makefile pourra être utilisé aussi pour des recompilations partielles en indiquant à make la cible désirée.

Sommaire

Cibles indépendantes

    Si les cibles sont indépendantes, soit l'utilisateur indique, dans la ligne de commande, la ou les cibles qu'il désire, soit il n'indique rien et c'est la première cible rencontrée dans le fichier qui est choisie par défaut par make. Considérons le fichier suivant :
 
#
# fichier Makefile

exo : exo.o
-->|g++ -o exo exo.o

exo.o : exo.cxx
-->|g++ -c exo.cxx 

clear :
-->|clear

    et les commandes :
 
duo>make                       [1]
duo>make exo                   [2]
duo>make exo.o                 [3]
duo>make clear                 [4]
duo>make exo clear             [5]

provoquent :


Sommaire

Ligne de continuation

    La commande make interprète chaque ligne séparément : la ligne de la cible, une ligne de commande, une macro, etc... Si une ligne est trop longue, elle peut être coupée en autant de lignes que nécessaire, chaque ligne résultante doit être terminée par le caractère anti-slash \. La ligne qui suit un \peut commencer par un nombre quelconque d'espaces. Il est donc fortement conseillé d'indenter proprement, par exemple :
 
CEXCFCTSYST_HXX = CExcFctSyst.hxx $(CEXC_H)
CEXCFCTSYST_H   = CExcFctSyst.h   $(CEXC_H) $(CEXCFCTSYST_HXX)
#
NSSYSTEME_H     = nsSysteme.h     $(NSSYSTEME_HXX)     \
                                  $(CEXC_H)        [1] \
                                  $(CEXCFCTSYST_H) [2]
# ...
#
$(nom) : $(nom).o nsUtil.o nsSysteme.o main.o
        g++ -s -o $(nom) main.o $(nom).o nsUtil.o  [3] \
                         nsSysteme.o               [4]

    La ligne [3] commence par une tabulation, les lignes [1], [2] et [4] ne contiennent pas de caractère de tabulation.

Pièges :

Sommaire

Ligne de commande de shell

    Une ligne de commande doit impérativement commencer par un caractère de tabulation, représenté jusqu'à présent dans les exemples par -->|. A partir de ce paragraphe, il sera représenté par une indentation de 8 caractères, conformément à la visualisation du fichier.

     A chaque ligne de commande, make lance un shell auquel il passe la ligne comme argument. Une ligne de commande de make peut donc correspondre à plusieurs commmandes de shell, séparées par l'un des caractères ; | ou &. Par exemple :
 
# fichier Makefile
#
cible :
        clear; grep toto *.h | less        [1]
        g++ exo.cxx                        [2]

    Un premier shell est lancé (ligne [1]), qui nettoie l'écran, recherche la chaîne toto dans tous les fichiers .h du répertoire courant et les redirige vers la commande less qui les affiche page par page. A la fin de ces opérations, la session de shell est terminée et make relance une nouvelle session shell qui effectue toutes les phases de compilation de exo.cxx (ligne [2]).

    Il importe donc que toutes les commandes qui doivent être exécutées dans la même session shell soient sur la même ligne, sous peine de surprises, comme dans l'exemple ci-dessous dans lequel l'utilisateur voulait se placer sous le répertoire essai pour effectuer la compilation du fichier exo.cxx :
 
# fichier Makefile
#
cible :
        cd /users/etud2/taralf/essai
        g++ exo.cxx

    La première ligne de commande est exécutée par un shell qui prend comme répertoire de travail le répertoire courant, change bien de répertoire, mais se termine aussitôt. La seconde ligne est exécutée par un second shell qui prend aussi comme répertoire de travail le répertoire courant, puis effectue la compilation. Il aurait fallu écrire :
 
# fichier Makefile
#
cible :
        cd /users/etud2/taralf/essai; g++ exo.cxx

ou
 
# fichier Makefile
#
cible :
        cd /users/etud2/taralf/essai; \
        g++ exo.cxx

mais sans tabulation sur la dernière ligne !!!

Sommaire

macro-définitions

macro "utilisateur"

    Une macro-définition (ou plus simplement une macro) peut être considérée comme une constante locale au fichier Makefile, équivalente à une suite de caractères. Avant tout traitement d'une ligne du fichier Makefile, la commande make remplace les macros par la suite de caractères corrrespondante.

    Considérons l'exemple ci-dessous :
# fichier MakeNom
#
nom   = exo_01                      [1]
#
$(nom) : $(nom).o
        g++ -o $(nom) $(nom).o
#
$(nom).o : $(nom).cxx
        g++ -c $(nom).cxx

    La ligne [1] est définit la macro d'identificateur nom par la suite de caractères exo_01. Le nombre d'espaces de part et d'autre du signe = est quelconque.

    Le contenu de la macro nom est noté $(nom). Le fichier est donc interprété par make rigoureusement comme ci-dessus.

    Une autre utilisation permet de prendre en compte les dépendances induites par les fichiers inclus indirectement. Considérons par exemple les cinq fichiers suivants :
 
// fichier exo.cxx
//
#include "A.h"
#include "D.h"
...
// fichier A.h
//
#include "B.h"
...
// fichier B.h
//
#include "C.h"
...
// fichier C.h
//
#include "D.h"
...
// fichier A.h
//
...

    La compilation de exo.cxx doit être effectuée non seulement chaque fois que le fichier source est lui-même modifié, ce que détecte make, mais aussi lorsque A.h ou D.h sont eux-même modifiés, dépendances que make ne peut déterminer seul. Il faut donc les indiquer explicitement dans le fichier Makefile. Plus difficile, le fichier A.h peut lui-même être modifié indirectement par la modification de B.h, de C.h ou de D.h. Il appartient donc à l'utilisateur d'indiquer aussi ces dépendances indirectes :
 
# fichier Makefile
#
exo.o : exo.cxx A.h B.h C.h D.h
        g++ -c exo.cxx

    La maintenance d'un tel fichier est très rapidement impossible, et source d'erreurs, en particulier si plusieurs fichiers sources dépendent des mêmes fichiers inclus. Il faut donc dupliquer les dépendances, ce qui n'est jamais une bonne chose en informatique.

    Il est beaucoup plus sain de construire une fois pour toutes des macros de dépendances directes pour les fichiers inclus, et de les utiliser ensuite, comme le montre l'exemple ci-dessous :
 
# fichier Makefile
#
D_H = D.h                      [1]
C_H = C.h $(D_H)               [2]
B_H = B.h $(C_H)
A_H = A.h $(B_H)
#
exo.o : exo.cxx $(A_H) $(D_H)    [3]
        g++ -c exo.cxx

    L'utilisateur n'indique dans les dépendances d'une cible que les macros des fichiers inclus directement visibles (lignes [1], [2] ou [3] par exemple). Nous avons, par convention, donné à la macro le même identificateur que le fichier .h, en majuscule (conformément aux traditions du langage C et d'Unix), et remplacé le . (point) interdit par un _ (souligné). Cette méthode peut paraître lourde mais les macros doivent être construites au fur et à mesure que les fichiers inclus sont ajoutés.

Sommaire

macros standard

    Les macros standard $@ et $* représentent respectivement la cible complète et la cible sans suffixe. Elles permettent de réduire le fichier Makefile ci-dessus en :
 
# fichier Makefile
#
nom   = exo_01
#
$(nom) : $*.o
        g++ -o $@ $*.o
#
$(nom).o : $*.cxx
        g++ -c $*.cxx

Sommaire

Arguments de la commande make

    Il est possible de surcharger les macro-définitions d'un fichier Makefile lors du lancement de la commande : la valeur de la macro passée en argument a priorité sur celle contenue dans le fichier. En utilisant le fichier MakeNom défini plus haut, il est donc par exemple possible de compiler le fichier exo_02.cxx en exécutant la commande :
 
duo>make -f MakeNom  nom=exo_02

    Cette possibilité est largement utilisée pour positionner des macros lors de la compilation d'un programme. Considérons par exemple le fichier essai.cxx suivant :
 
// fichier essai.cxx

#include "Config.h"
// ...

#ifdef __DEBUG__
cerr << "Passé par ici << endl;
#endif

// ...

#ifdef VERSION_3
    // ...
#endif
#ifdef VERSION_4
    // ...
#endif

// ...

#ifdef MSV5
    #define EXCEPTION ; //
#else
    #define EXCEPTION

class CPoint
{
    // ...
    int GetX (void) const EXCEPTION throw ();
    // ...

}; // CPoint

    Le code compilé dépend de trois macros destinées au préprocesseur :

    Il est évidemment possible de modifier "en dur" et de façon statique les macros, en les définissant dans le fichier source Config.h :
 
// fichier "Config.h"

#define __DEBUG__
#define MSV5
#define VERSION_4

    Il est beaucoup plus intéressant de supprimer ce fichier et de les positionner directement dans la ligne de commande de compilation (voir Commande Unix g++ ) au moyen de l'option -D :
 
g++ essai.cxx -DMSV5 -D__DEBUG__ -DVERSION_4 -c

ou, en passant par le fichier Makefile :
 
# fichier Makefile
#
nom     = essai
version = GCC
option  = __NODEBUG__
compilo = VERSION_3
# ...
$(nom).o : $(nom).cxx
        g++ $(nom).cxx -D$(version) -D$(compilo) -D -$(option) -c

au moyen de la commande :
 
duo>make nom=tp_08

en utilisant les options par défaut, ou
 
duo>make version=MSV5 option=__DEBUG__ compilo=VERSION_4

Sommaire

Fichiers inclus

    Il est possible d'inclure à l'intérieur d'un Makefile des fichiers, par exemple dans lesquels on aurait regroupé les macros de dépendance des fichiers .h, par exemple :
 
# fichier Makefile
#
include Depend.h
#
exo.o : exo.cxx $(A_H) $(D_H)
        g++ -c exo.cxx
# fichier Depend.h
#
D_H = D.h
C_H = C.h $(D_H)
B_H = B.h $(C_H)
A_H = A.h $(B_H)

Sommaire

Bibliographie

    La commande make offre de très nombreuses autres possibilités, que vous pourrez découvrir en étudiant la documentation en ligne (man 1 make) ou en consultant le livre :

Managing Projects with make, Ed. O'Reilly & Associates

Sommaire

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique