Table des matières

makefile, comprendre le makefile

On ne va pas chercher à faire des Makefile qui “arrachent” mais bien de comprendre comment ça fonctionne afin de, par la suite, pouvoir en faire soi-même.

Plan

Généralités

Intérêt

Le Makefile est un utilitaire écrit en version GNU par Richard Stallman et Roland McGrath. Typiquement, il est associé à la plupart des développements (principalement C/C++).

Par comparaison de date de création/mise à jour, il évite de recompiler des sources inutilement. Mais son usage va bien au-delà et vous pouvez vous en servir pour minimiser les commandes dans la plupart des projets.

Structure générale d'un Makefile

Un Makefile reste avant tout un fichier texte appelé “Makefile”.

Typiquement, un Makefile contient trois types de lignes :

  CFLAGS = -g -Wall

  SRCS = main.c file1.c file2.c

  CC = gcc

Pour expliquer, CC sera simplement une variable qui va contenir le compilateur (gcc, dans notre cas), SRCS est une variable qui va contenir tous les fichiers sources et CFLAGS permet de gérer les flags nécessaires à la compilation.

Notons que, par convention, les noms de variables seront tjrs en majuscule.

Par la suite, pour accéder à la valeur d'une variable, il suffit de faire:

$(NOM_DE_LA_VARIABLE)

Simplement, il faut mettre le nom de la variable entre parenthèses, les parenthèses étant précédées du sigle '$'.

  main.o: main.c

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

Important : la deuxième ligne ($(CC)…) doit nécessairement commencé par une tabulation. Makefile est très pointilleux sur les aspects syntaxiques.

La première ligne donne la règle. La deuxième, l'action à effectuer.

Cela signifie simplement que le fichier 'main.o' doit être recompilé (en suivant les indications de la 2e ligne) si le fichier 'main.c' a été modifié.

#

Utilisation d'un Makefile

On exécute un Makefile en tapant simplement la commande (dans une Konsole/Terminal, of course):

make

On peut aussi faire en sorte d'exécuter une règle particulière. Il suffit de taper :

make nom_de_la_règle

Ordre de compilation

Quand un Makefile est exécuté, on appelle la commande make pour exécuter une cible ('target') particulière. La cible, c'est tout simplement un nom qui apparaît au début d'une règle. Dans l'exemple de règle plus haut, c'est 'main.o'.

Une cible peut être le nom du fichier à créer ou tout simplement un nom qcq utilisé comme point de départ.

Quand make est invoqué, il évalue d'abord toutes les variables, du haut vers le bas. Ensuite, qd il rencontre une règle “A” dont la cible correspond au target donné, il essaie d'évaluer cette règle.

Je passe sur les détails de gestion de dépendance. C'est donné au lecteur à titre d'exercice :-O

Makefile & C/C++

Un seul fichier source

Considérons un exemple simple de Makefile qui sera utilisé dans le cadre d'un programme ne nécessitant qu'un seul fichier source

  # Règle de haut niveau pour créer le programme.

  all: main

  

  # Compilation du fichier source.

  main.o: main.c

         gcc -g -Wall -c main.c

  

  # "Linkage" du programme.

  main: main.o

         gcc -g main.o -o main

  

  # Nettoyage.

  clean:

         /bin/rm -f main main.o

Quelques indications :

make clean

Plusieurs fichiers sources

La plupart du tps, tout projet de programmation comportera plus d'un fichier source. C'est vraiment dans ce genre de cas que l'utilisation du Makefile devient pertinente. Changer un fichier implique de recompiler ledit fichier et toutes ces dépendances. Le Makefile bien pensé fait ça tout seul.

  # Règle de haut niveau pour créer le programme.

  all: prog

  

  # Le programme est fait de plusieurs fichiers sources.

  prog: main.o file1.o file2.o

         gcc main.o file1.o file2.o -o prog

  

  # Règle pour main.o

  main.o: main.c file1.h file2.h

         gcc -g -Wall -c main.c

  

  # Règle pour file1.o

  file1.o: file1.c file1.h

         gcc -g -Wall -c file1.c

  

  # Règle pour file2.o

  file2.o: file2.c file2.h

         gcc -g -Wall -c file2.c

  

  # Nettoyage.

  clean:

         /bin/rm -f prog main.o file1.o file2.o

Quelques explications :

Compilateurs & Flags

Comme on peut le voir dans les segments de code supra, il y a pas mal de pattern redondant dans les règles de notre Makefile.

Ca risque de poser problème qd on voudra faire des changements, car il faudra les répercuter partout (ou presque). Et quand le Makefile fait qq centaines de ligne, ça fout la merde.

Une solution relativement simple à ce problème est d'utiliser des variables pour stocker les valeurs des différents paramètres (ou flags) et même les noms des différentes commandes.

Ca nous donnerait un Makefile de la forme suivante :

  # Utilisation de gcc pour compiler les sources.

  CC = gcc

  # Le linker est aussi gcc.  Mais il pourrait être différent du compilateur, dans le cas d'un autre lgge.

  LD = gcc

  # Les flags du compilateurs.

  CFLAGS = -g -Wall

  # Les flags du linker.  Pour le moment, nous n'en avons pas mais tout dépend des différents besoins.

  LDFLAGS =

  # rm.  C'est bien d'en faire une variable, car son emplacement peut varier d'une machine (ou distribution) à l'autre.

  RM = /bin/rm -f

  # La liste des fichiers objet à générer.

  OBJS = main.o file1.o file2.o

  # Le nom de l'exécutable.

  PROG = prog

  

  # Règle de haut niveau pour créer le programme

  all: $(PROG)

  

  # Règle pour linker le programme

  $(PROG): $(OBJS)

         $(LD) $(LDFLAGS) $(OBJS) -o $(PROG)

  

  # Règle pour main.o

  main.o: main.c file1.h file2.h

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

  

  # Règle pour file1.o .

  file1.o: file1.c file1.h

         $(CC) $(CFLAGS) -c file1.c

  

  # Règle pour file2.o.

  file2.o: file2.c file2.h

         $(CC) $(CFLAGS) -c file2.c

  

  # Nettoyage.

  clean:

         $(RM) $(PROG) $(OBJS)

Quelques explications :

Une règle pour tout

La phase suivante est d'éliminer les règles redondante. Dans la mesure où toutes ces règles correspondent au même type d'action, on peut essayer de les regrouper dans une seule règle.

  # Je passe toutes les définitions de variables.  Il suffit de les reprendre dans le segment de code précédent

  

  # La règle de linkage, identique à ce qui a été proposé supra.

  $(PROG): $(OBJS)

         $(LD) $(LDFLAGS) $(OBJS) -o $(PROG)

  

  # Une 'meta-rgèle' permettant de compiler tout fichier source "C".

  %.o: %.c

         $(CC) $(CFLAGS) -c $<

Quelques explications :

%.o: %.c

Signifie que “un fichier avec un suffixe '.o' est dépendant d'un fichier avec le même nom mais ayant un suffixe '.c'”.

Création automatique des dépendances

Un des problèmes avec l'utilisation de règles implicites, c'est qu'on risque de perdre la liste des dépendances. Liste qui est unique pour chaque fichier.

On peut s'attaquer à cela en ajoutant des règles supplémentaires contenant les dépendances, mais aucune commande. Ça peut se faire soit manuellement, soit automatiquement.

Je propose de jeter un coup d'oeil à makedepend et son utilisation dans un Makefile.

  # La liste des fichiers source.

  SRCS = main.c file1.c file2.c

  .

  .

  # Le reste du Makefile est identique

  # A la fin, on ajoute "simplement" les lignes suivantes:

  

  # Règle pour construire la liste des dépendances et l'écrire ensuite dans un fichier appelé ".depend".

  depend:

         $(RM) .depend

         makedepend -f- -- $(CFLAGS) -- $(SRCS) > .depend

  

  # Il suffit ensuite d'inclure cette liste de dépendance.

  include .depend

Quelques explications :

make depend

Va permettre d'éxécuter le programme makedepend. Celui-ci va scanner les fichiers sources donnés et créer la liste de dépendance pour chacun d'eux. LE résultat est redirigé vers un fichier (.depend)

include .depend

Ces dépendances seront vérifiées automatiquement à chaque compilation. Il faut donc faire le “make depend” une et une seule fois (sauf si on ajoute/supprime des fichiers sources).

Quelques règles de bonne conduite

Makefile & LaTeX

Un petit mot sur LaTeX

LaTeX est un système typographique de haute qualité, développé dans les 70's (si je ne m'abuse) par Donald E. Knuth. L'objectif était de faciliter la rédaction de document (article, thèse, rapport, …) scientique. La complexité de ces documents se trouve dans la mise en page de formules, équations, bibliographie, graphiques, … Celui qui a déjà essayé de pondre un tel document avec un traitement de texte classique (MS Word, StarOffice, OpenOffice, …) sait que c'est une véritable saloperie…

L'idée de base de LaTeX est la suivante: laisser au designer s'occuper du design d'un document et laisser l'auteur s'occuper de l'écriture.

LaTeX n'est pas, a priori, WYSIWYG (what you see is what you get). Il fonctionne par déclaration d'environnement, appel de package et utilisation de fonction.

Ceux qui sont intéressés par LaTeX peuvent consulter (gratuitement) le document intitulé Une courte (?) introduction à LaTeX disponible ici

Pour la suite de cette section, je suppose que vous savez comment fonctionne LaTeX (compilation, utilisation de BibTeX, transformation en pdf, …)

Les variables

A ce stade du tuto, vous devez avoir compris qu'il est intéressant (obligatoire?) de définir des variables dans un Makefile.

Cette section a pour but de proposer qq variables qui me semblent pertinentes dans le cadre d'un Makefile ayant pour but la compilation d'un projet LaTeX.

Voici ce que je propose :

  # --------------------------------------------------------------------------- #

  # Commands                                                                    #

  # --------------------------------------------------------------------------- #

  LATEX           = latex

  BIBTEX          = bibtex

  DVIPS           = dvips

  DVIPS_OPTION    = -dPDFsettings=/prepress

  PDFLATEX        = pdflatex

  DVIPDF          = dvipdf

  DVIPDF_OPTION   = -dPDFsettings=/prepress

Une petite explication :

Les autres variables à définir sont les suivantes :

  # --------------------------------------------------------------------------- #

  # LaTeX files                                                                 #

  # --------------------------------------------------------------------------- #

  TARGET =

  

  BIBSRC =

  

  TEXSRC =

  

  PICTURES = \

         Pictures/ \

  

  PDFTARGET = $(TARGET).dvi

Petite explication :

Il est aussi intéressant de définir des fichiers de log, qui contiendront la trace de la compilation.


  # -------------------------------------------------------------------------- #

  # Log files                                                                  #

  # -------------------------------------------------------------------------- #

  

  LOG    = compile.log

  PDFLOG = compilepdf.log

  

  LOGFILE = $(LOG) $(PDFLOG)

Petite explication :

Un seul fichier source

Certains petits projets LaTeX nécessitent un seul fichier source. Pour simplifier, je vais aussi considérer qu'il n'y a pas de fichier de bibliographie. Ce cas sera abordé dans la section suivante.

La première chose à faire, c'est de compléter les variables liées au(x) fichier(s) LaTeX :

  # --------------------------------------------------------------------------- #

  # LaTeX files                                                                 #

  # --------------------------------------------------------------------------- #

  TARGET = target

  

  TEXSRC = monFichier.tex

  

  PICTURES = \

        Pictures/ \

  

  PDFTARGET = $(TARGET).dvi

Rien de bien surprenant, jusqu'ici, si on a bien compris la section précédente.

Passons maintenant en revue les règles…

default: $(PDFTARGET)

Il s'agit ici de la règle par défaut, celle qui sera appliquée 'par défaut' si on ne spécifie pas la règle à exécuter via la commande make ma_règle. C'est un peut l'équivalent de all, utilisé dans le chapitre précédant.

A noter que cette règle ne contient aucune commande à exécuter. Elle fait simplement un renvoi à la règle qui gère $(TARGET).dvi.

Cette règle est la suivante :

  # makes the dvi output file

  $(TARGET).dvi: $(TEXSRC)

         @echo

         @echo \*

         @echo \* Compiling $(TARGET) - compilation log in $(LOG)...

         @echo \*

         $(LATEX) $(TARGET).tex

         @while ( grep "Rerun to get cross-references" $(TARGET).log > /dev/null ); do \

                 echo '** Re-running LaTeX **'; \

                 $(LATEX) $(TARGET) > $(LOG); \

         done

         $(MAKE) -k $(TARGET).pdf

Petite explication :

Cette règle pour le pdf a la forme suivante :

  # makes the pdf output file

  $(TARGET).pdf: $(TEXSRC)

         @echo

         @echo \*

         @echo \* Running pdfLaTeX $(TARGET)

         @echo \*      

         $(DVIPDF) $(DVIPDF_OPTION) $(TARGET).dvi $(TARGET).pdf > $(PDFLOG)

Rien de bien particulier. On suit simplement la commande dvipdf (cfr. man page pour ceux qui ne connaissent pas).

Comme d'habitude, il est intéressant de disposer d'une règle de nettoyage qui pourra être appelée via la commande :

make clean

Cette règle aura la forme suivante:

  # clean the current directory

  clean:

         rm -f *~

         rm -f $(TEXSRC:.tex=.tex~)

         rm -f $(TEXSRC:.tex=.tex.flc)

         rm -f $(TEXSRC:.tex=.loa)

         rm -f $(TARGET).log $(TEXSRC:.tex=.aux)

         rm -f $(TARGET).lof $(TARGET).lot $(TARGET).toc

         rm -f $(TARGET).bbl $(TARGET).blg $(TARGET).out

         rm -f $(LOGFILE)

         clear

<:code>

**Petite explication :**

  * On supprime les fichiers temporaires créés par Emacs (pour ceux qui l'utilisent) ~

  * les actions de la forme



  rm -f $(TEXSRC:.tex=xxx)



est relativement simple à comprendre. Simplement, on remplace l'extension .tex de tous les fichiers contenus dans la variable TEXSRC par l'extension xxx.

  * On remarque que la règle clean supprime tous les fichiers temporaires propres à LaTeX.



==== Plusieurs fichiers sources ====

Dans d'autres cas, un projet LaTeX peut exiger d'avoir plusieurs fichiers sources. C'est le cas, notamment, lq on écrit des livres, thèses et autres rapports techniques. On peut envisager d'avoir un fichier LaTeX par chapitre/annexe et un fichier LaTeX principal, qui se contente d'inclure chaque chapitre/annexe. Je vous renvoie à la documentation de LaTeX pour savoir comment faire cela.



Again, il nous faut définir des variables propres à notre projet LaTeX

<code>

  # --------------------------------------------------------------------------- #

  # LaTeX files                                                                 #

  # --------------------------------------------------------------------------- #

  TARGET = StateOfTheArt

  

  BIBSRC = Bibliography.bib

  

  TEXSRC = \

         Main.tex \

         Abstract.tex \

         ApplicationLayer.tex \

         Conclusion.tex \

         Goals.tex \

         Introduction.tex \

         LinkLayer.tex \

         NetworkLayer.tex \

         TopologyGenerator.tex \

  

  PICTURES = \

         Pictures/ \

  

   PDFTARGET = $(TARGET).dvi

Petite explication :

TEXSRC = $(wildcard *.tex)

Si on gagne en espace, je trouve qu'on perd qq peu en lisibilité, surtout si on veut éviter la compilation d'un fichier tex particulier pour des raisons x ou y.

Passons maintenant aux règles… La règle par défaut sera:

default: $(PDFTARGET)

soit totalement identique à celle de la section précédente…

Pour créer le .dvi, la règle sera plus complexe que dans la précédente section…

  # makes the dvi output file

  $(TARGET).dvi: $(TEXSRC)

         @echo

         @echo \*

         @echo \* Compiling $(TARGET) - compilation log in $(LOG) ...

         @echo \*

         $(MAKE) -k $(TARGET).bbl

         $(LATEX) $(TARGET).tex > $(LOG)

         @while ( grep "Rerun to get cross-references" $(TARGET).log >/dev/null ); do \

                 echo '** Re-running LaTeX **'; \

                 $(LATEX) $(TARGET) > $(LOG); \

         done

         $(MAKE) -k $(TARGET).pdf

Petite explication :

Cette règle à la forme suivante:

  # runs bibtex

  $(TARGET).bbl: $(TEXSRC) $(BIBSRC)

         $(LATEX) $(TARGET).tex

         $(BIBTEX) $(TARGET)

         clear

         $(LATEX) $(TARGET).tex > $(LOG)

         $(LATEX) $(TARGET).tex > $(LOG)

Petite explication :

La règle pour la transformation en pdf reste la même. Idem pour le nettoyage.

Makefile & Java

Un petit mot sur Java

Java est un langage purement Orienté Objet (OO). Pour rappel, l'OO se caractérise par:

Java n'est pas un langage compilé, mais interprété. Il est sensé être multiplateforme (au sens OS et hardware). Java fonctionne avec une machine virtuelle, la fameuse JVM…

Java se caractérise aussi par les nombreuses librairies disponibles.

On va essayer, dans ce chapitre, de mettre sur pied des Makefile un peu plus compliqués rolleyes.gif

Pour plus d'infos sur Java et ses libraires, je vous renvoie sur le site de Sun.

Un seul fichier source

Tout projet de programmation peut se contenter d'un seul fichier source… A noter que, par convention, en Java, on écrit une seul classe par fichier. Ce n'est pas obligatoire, mais fortement recommandé, pour des raisons d'ingénierie du logiciel (une classe = un type = un fichier, grosso modo du moins).

Comme d'hab', on commence par définir les variables…

  JCC             = javac

  #JAVA_CLASS     = path/to/jdk

  FILES           = $(wildcard *.java

Petite explication :

FILES           = MaClasse.java

La règle est fort simple :

  all: $(FILES)

       $(JCC) $(FILES)

Rien de bien compliqué à ce stade du tuto.

Comme d'habitude, il est bon d'avoir une règle de nettoyage. Je laisse le lecteur la créer, à titre d'exercice.

Plusieurs fichiers sources et pas de package

Ceux qui connaissent qq peu Java savent que la “compilation” en Java obéit à un effet 'domino'. Cela signifie que javac va automatiquement aller “compiler” les classes dépendantes (au sens général du terme) de celle qui est actuellement compilée.

Sachant cela, on a deux possibilités :

FILES        = $(wildcard *.java)

c'est-à-dire qu'on utilise un wildcard pour désigner tous les fichiers sources et on laisse le Makefile gérer ce qu'il y a compilé.

  FILES        = $(wildcard *.java)

  TARGET       = MainClass.java

Le fichier 'MainClass.java' étant celui qui contient la méthode 'main'. La règle générale devient donc:

  all: $(FILES)

       $(JCC) $(TARGET)

Dans ce cas, on laisse Java gérer la compilation. Mais il est fort probable que Java va recompiler tous les fichiers.

Plusieurs fichiers sources et package

La notion de package

On va, maintenant, mettre au point des (il y en aura plusieurs) Makefile plus compliqués.

Java permet de regrouper des classes ayant un lien entre elles sous le même chapeau. Ce chapeau se nomme package. Un package peut être de 'haut niveau' et contenir des sous packages. La syntaxe est la suivante:

package nom_package(.nom_sous_package)*;

Pour ceux qui ne connaissent pas ce genre de notation (on appelle cela une grammaire BNF), cela signifie que le mot clé package est suivi du nom du package. Si il y a des sous packages, ils seront séparés par un point. '*' signifie qu'il peut y avoir 0, 1 ou plusieurs sous-packages (ça donne le caractère optionnel).

La notion de package est aussi liée à une notion d'ingénierie… Ainsi, un package correspond nécessairement à un répertoire du même nom. Dans ce répertoire, on trouvera toutes les classes se trouvant dans ce package. Si il y a des sous-packages, alors le répertoire contiendra des sous répertoires correspondant aux sous-packages.

Dès lors, la plupart des projets de développement en Java ayant une certaine importance comporteront plusieurs packages.

Supposons qu'on travaille sur un projet ayant les packages suivants :

Si on considère qu'on travaille dans le répertoire : /home/molodoi/Code

On aura donc les répertoires suivants :

  /home/benoit/Code/fr/lip6/tools

  /home/benoit/Code/fr/lip6/stopset

  /home/benoit/Code/fr/lip6/stopset/bloomfilter

  /home/benoit/Code/fr/lip6/stopset/bloomfilter/hashfunction

  /home/benoit/Code/fr/lip6/stopset/couplelist

Note : je passe sur la notion de classpath qui est inhérente aux packages.

L'architecture globale des Makefile

On va utiliser trois types de Makefile:

Le Makefile propre à chaque package

C'est un Makefile assez con…

Il doit se trouver dans chaque sous-répertoire correspondant à un package. Le sous-répertoire doit, of course, contenir du code. Il devra donc être placé dans les 5 sous-répertoires définit supra.

Ces Makefile auront la forme suivante (suppons qu'il s'agisse de celui dans le répertoire /home/benoit/Code/fr/lip6/stopset/bloomfilter):

  TOP_DIR         = /home/molodoi/Code

  PACKAGE_DIR     = fr/lip6/stopset/bloomfilter

  PACKAGE_NAME    = fr.lip6.stopset.bloomfilter

  

  include $(TOP_DIR)/Makefile.include

Petite explication :

  cd /home/molodoi/Code

  javac fr/lip6/stopset/bloomfilter/*.java

Le Makefile racine

Le Makefile racine, c'est celui qui va appeler lq on lancera la commande :

make

Il a pour mission d'avoir une vue d'ensemble du projet et de déléguer. Il devrait se trouver à la racine de notre arborescence de packages, c'est à dire : /home/molodoi/Code

Il contient 2 variables:

  #root makefile.  Delegate to source subdirs

  

  PACKAGE = .

  

  SUBDIRS = \

         fr/lip6/tools \

         fr/lip6/stopset \

         fr/lip6/stopset/couplelist \

         fr/lip6/stopset/bloomfilter \

         fr/lip6/stopset/bloomfilter/hashfunction \

Petite explication :

Comme prévu, on se contente de deux règles :


  all:

         @@for p in $(SUBDIRS); do \

                 echo '----building ' $(PACKAGE)/$$p; \

                 make -C $(PACKAGE)/$$p --no-print-directory all; \

         done

  

  clean:

         @@for p in $(SUBDIRS); do \

                 echo 'cleaning ' $(PACKAGE)/$$p; \

                 make -C $(PACKAGE)/$$p --no-print-directory clean; \

         done

Petite explication :

$$p

* Que signifie l'instruction :

make -C $(PACKAGE)/$$p --no-print-directory all;

??? Décomposons là…

Entre dans le répertoire directory
Quitte le répertoire directory
/

Afin d'indiquer au Makefile que la ligne suivante fait partie de la même instruction/variable.

Le Makefile.include

C'est ici que ça devient compliqué. Je vais essayer d'être clair rolleyes.gif

Ce fichier doit se trouver à la racine de nos packages (cfr la condition de compilation de classe packagisée en Java).

Ce fichier s'appuie sur les 3 variables (si on considère la JavaDoc) définies dans les Makefile de chaque package.

On va définir dans Makefile.include deux types de variables;

        # set here the target dir for all classes

        CLASS_DIR       = $(TOP_DIR)

        #JAVA_CLASS     =

        LOCAL_CLASS_DIR = $(CLASS_DIR)/$(PACKAGE_DIR)

Explication :

        JCC   = javac

        FILES = $(wildcard *.java)

Rien de spécial à dire.

On passe à l'aspect magique de notre Makefile :

  #new rule for java

  .SUFFIXES:

  .SUFFIXES: .java .class

  

  #magical command that tells make to find class files in another dir

  vpath %.class $(LOCAL_CLASS_DIR)

Petite explication :

.java.class

Les règles seront les suivantes :

  all: classes

  

  classes: $(FILES:.java=.class)

  

  .java.class:

         CLASSPATH=$(JAVA_CLASS):$(CLASS_DIR) $(JCC) -nowarn -d $(CLASS_DIR) $<

  

  clean:

         @@ echo 'rm -f *~ *.class core *.bak *# $(LOCAL_CLASS_DIR)/*class'

         @@rm -f *~ *.class core *.bak *

Petite explication :

Conclusion & ressources

Bon, voilà. C'est fini.

Je suis loin d'avoir abordé l'entièreté du Makefile. Disons que ces qq posts doivent vous avoir donné un petit aperçu de la puissance du Makefile.

Maintenant, le meilleur moyen d'apprendre et d'approfondir ses connaissances, c'est de foncer et créer ses propres Makefile.

Comme ressource sur le Web, je donnerai un seul lien: http://www.gnu.org/software/make/

molodoi 2006/01/22 11:36