LinuxPedia

Wiki libre et indépendant dédié à GNU-Linux et BSD.

Outils pour utilisateurs

Outils du site


dev:makefile

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
  • Structure générale d'un Makefile
  • Utilisation d'un Makefile
  • Ordre de compilation
  • Makefile & C/C++
  • Un seul fichier source
  • Plusieurs fichiers sources
  • Compilateurs & Flags
  • Une règle pour tout
  • Création automatique des dépendances
  • Quelques règles de bonne conduite
  • Makefile & LaTeX
  • Un petit mot sur LaTeX
  • Les variables
  • Un seul fichier source
  • Plusieurs fichiers sources
  • Makefile & Java
  • Un petit mot sur Java
  • Un seul fichier source
  • Plusieurs fichiers sources & pas de package
  • Plusieurs fichiers sources & package
  • La notion de package
  • L'architecture globale des Makefile
  • Le Makefile propre à chaque package
  • Le Makefile racine
  • Le Makefile.include
  • Conclusion & Ressources

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 :

  • La définition des variables. Il s'agit de définir les valeurs pour les différentes variables à utiliser. Exemple :
  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 '$'.

  • Les règles. Ces lignes définissent sous quelles conditions un fichier donné a besoin d'être recompilé et comment il doit être compilé. Exemple :
  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é.

  • Les commentaires. Comme tout script ou programme, un Makefile peut contenir des commentaires. Une ligne sera commentée si elle commence par le caractère:
#

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 :

  • Toutes les règles ne seront pas invoquées lors de l'utilisation de make. Par exemple, la règle clean ne sera jamais utilisée lors de la compilation. Elle a simplement pour but de nettoyer le répertoire en enlevant les fichiers créés et ce, afin de gagner de l'espacer disque et de la visibilité dans le dossier. Pour invoquer la règle clean, il suffit de faire :
make clean
  • Une règle ne doit pas avoir nécessairement de dépendances. Si il n'y a pas de dépendances, les actions seront simplement exécutées, comme dans le cas de clean
  • Une règle ne doit pas nécessairement avoir de commandes. Par exemple, la règle all n'a pas de commandes propres. Elle sert seulement à appeler les autres règles. En fait, ça permet de s'assurer que si qqn invoque make sans nom de cible (comme c'est svt le cas), cette règle sera exécutée.
  • Utiliser le chemin complet pour 'rm' permet d'éviter les problèmes d'alias (si jamais l'utilisateur a défini des alias pour rm).

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 :

  • Il y a une seule règle par fichier. C'est qq peu redondant. On verra par la suite comment on peut faire pour agréger tout cela.
  • On ajoute des dépendances pour les headers (fichiers *.h). Si un de ces fichiers changent, le fichier qui les inclut doit aussi être recompilé. Ce n'est certes pas trjs vrai mais il est préférable de compiler un peu trop afin d'être sûr qu'il n'y a pas de problèmes de synchro entre le pgm et les sources.

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 :

  • Typiquement le nom des variables est tjrs en majuscule. Pour accéder à la valeur d'une variable, il suffit de la mettre entre parenthèses précédées d'un $
  • On définit énormément de variables. Ca rend les modifications plus facile car il y a un seul endroit à changer.
  • On a encore un problème. On définit une règle pour chaque fichier source. C'est redondant. Ca ne pose pas trop de problèmes si il y a 3-4 fichiers sources. C'est un gros problème qd il y en a une bonne dizaine (ce qui est le cas dans les projets de moyenne envergure).

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 :

  • Le caractère '%' est un wildcard (sorry, je ne trouve pas de mot pertinent en français) permettant de matcher toute partie d'un nom de fichier. Le passage :
%.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'”.

  • le “$<” fait référence à la liste de dépendance qui a été matchée par la règle (dans notre cas, le nom de fichier complet).

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 :

  • L'exécution de
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)

  • On inclut ensuite les dépendances dans le Makefile via
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

  • Il est tjrs bien d'avoir une règle de nettoyage afin d'enlever les fichiers temporaires.
  • Cette règle de nettoyage peut avoir plusieurs niveaux (juste supprimer les fichier *~, supprimer les fichiers temporaires, …)
  • Il est intéressant que les fichiers sources, objets et binaires ne se trouvent pas dans le(s) même(s) répertoire(s). Penser à une règle de type “install” (le 'make install' classique) qui va copier/déplacer les différents fichiers dans les répertoires ad hoc.

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 :

  • J'ai appelé ces variables 'Commands' car elles concernent uniquement les commandes qui seront exécutées par le Makefile. Elles sont donc valables qqs le projet LaTeX (un seul fichier source ou plusieurs).
  • DVIPS_OPTION reprend les options de la commande divps. Expliquer l'utilité de ces options est hors de la portée de ce tutorial. Je vous renvoie à la page man de dvips.
  • DVIPDF_OPTION reprend les options de la commande dvipdf. Idem que pour DVIPS_OPTION.

Les autres variables à définir sont les suivantes :

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

  # LaTeX files                                                                 #

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

  TARGET =

  

  BIBSRC =

  

  TEXSRC =

  

  PICTURES = \

         Pictures/ \

  

  PDFTARGET = $(TARGET).dvi

Petite explication :

  • PICTURES est la variable qui contient le chemin vers les figures (.eps) à insérer dans le document LaTeX.
  • TARGET va contenir le nom du fichier 'output'. C'est un peu l'équivalent de l'exécutable dans un projet C/C++. Ici, le TARGET aura une extension .dvi (extension temporaire fournie par LaTeX), une extension .pdf et une extension .ps
  • TEXSRC contiendra le(s) fichier(s) source du projet LaTeX. Ce sera à définir en fonction du type de projet (un seul fichier ou plusieurs)
  • BIBSRC contiendra le(s) fichier(s) de bibliographie. A noter que gérer plusieurs fichiers de bibliographie est assez lourd en LaTeX. Il existe des packages permettant cela mais ça devient moins intuitif, surtout au niveau de la compilation. Comme l'objectif du tuto n'est pas de parler de LaTeX, je vais me contenter, dans la suite de cette section, de considérer un seul fichier de bibliographie.
  • PDFTARGET va être notre variable principale…

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 :

  • LOG va contenir les infos concernant la compilation LaTeX proprement dite.
  • PDFLOG va se contenter de résumer la transformation du dvi en pdf.
  • LOGFILE est une sorte de variable globale, qui contient tous les types de fichiers de log. Ca sera utile pour le nettoyage du répertoire.

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 :

  • les premières commandes (@echo) permettent d'afficher sur la sortie standard un message…
  • La commande $(LATEX) compile notre fichier LaTeX. On remarque qu'on ne place pas le résultat dans un fichier de LOG. La raison est simple: en cas de problème lors de la compilation, ça va se bloquer (LaTeX attendant une réponse de l'utilisateur).
  • On fait ensuite une boucle, pour recompiler le fichier LaTeX tant qu'il y a des références qui sont indéfinies. Les habitués de LateX savent que, lq il y a des renvois dans le txt (\ref{}), il faut compiler plusieurs fois pq l'output en tiennent compte. Cette boucle permet de gérer cela. Le résultat est cette fois-ci placé dans un fichier de LOG. La condition de la boucle porte sur le parsing du fichier de LOG. En gros, tant que le LOG indique qu'il y a encore des références croisées, il faut recompiler.
  • La dernière commande fait un renvoi à la règle permettant de transformer le fichier .tex en .pdf. L'option -k permet d'indiquer à make de ne pas s'arrêter, quoiqu'il arrive.

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 :

  • Supposons que notre document soit constitué d'un abstract (i.e. résumé), de 7 chapitres et une bibliographie.
  • Pour des raisons de lisibilité, on ne met pas les différents fichiers source LaTeX sur la même ligne. un '\' permet d'indiquer que le contenu de la variable ne s'arrête pas là mais bien que la prochaine ligne contient encore de l'information.
  • A noter qu'on peut facilement réduire cette variable via l'utilisation de wildcard. Cela se fait comme suit:
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 :

  • La différence se situe au niveau de la 4ème ligne… On commence par appeler la règle qui permet de gérer le fichier de bibliographie
  • Le reste est identique.

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 :

  • Cette règle sera exécutée si
  • soit les fichiers .tex sont modifiés
  • soit le fichier de bibliographie a été modifié. A noter qu'on mettre les mêmes conditions d'exécution pour la règle $(TARGET).dvi
  • Une exécution de LaTeX, une de BibTeX et deux autres exécutions de LaTeX (classique pour ceux qui connaissent un peu LaTeX) avant de retourner à la règle qui a appelé cette règle.

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:

  • La notion d'Objet
  • L'encapsulation
  • L'héritage
  • Le polymorphisme

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 :

  • La première ligne définit le “compilateur” (hum hum) à utiliser. C'est un peu l'équivalent du GCC pour le C/C++ (cfr. le Chapitre 1 de ce tuto). A noter que sous SuSe, java est fourni et se trouve dans le PATH. Si jamais ce n'est pas le cas dans votre distribution (ou que vous avez installé une version du jdk différente de celle fournie par défaut par votre système), il faudra spécifier le chemin complet (si le jdk à utiliser n'est pas défini dans le PATH)
  • La deuxième ligne (mise en commentaire) fait suite à la première si le jdk n'est pas défini dans le path. Il s'agit simplement d'indiquer à javac où trouver les packages fournies avec le jdk.
  • Enfin, la dernière ligne permet de prendre en compte le fichier source. J'ai directement utilisé un 'wildcard' mais on peut bien entendu définir FILES de la façon suivante :
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 :

  • On reprend le Makefile de la section précédente en modifiant uniquement la variable FILES de la façon suivante:
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é.

  • On se base sur la compilation en domino. On obtient alors ceci:
  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 :

  • fr.lip6.tools
  • fr.lip6.stopset
  • fr.lip6.stopset.bloomfilter
  • fr.lip6.stopset.bloomfilter.hashfunction
  • fr.lip6.stopset.couplelist

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 racine, qui aura pour mission d'appeler et déléguer le travail aux Makefile des sous-répertoires contenant les sources.
  • Le Makefile de chaque sous-répertoire. Il devra appeler un Makefile permettant d'effectuer l'action désirée (nous nous limiterons à la 'compilation' et au nettoyage).
  • Le Makefile.include, qui sera inclut par les différents Makefile. C'est lui qui va se taper tout le boulot et va contenir toute l'intelligence.

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 :

  • On définit trois variables (TOP_DIR, PACKAGE_DIR, PACKAGE_NAME) et on inclut le Makefile.include (i.e. on fait un appel)
  • TOP_DIR est la variable la plus importante. Elle indique le répertoire de base, celui à partir duquel la JVM pourra chercher après les différents packages que nous avons créé.
  • PACKAGE_DIR indique le répertoire du package dans lequel nous sommes. Ca sera utilisé pour la compilation. Rappel: en Java, dès qu'il y a des packages, la compilation doit se faire à partir de la racine des packages. Un ordre de compilation devrait donc ressembler à :
  cd /home/molodoi/Code

  javac fr/lip6/stopset/bloomfilter/*.java
  • PACKAGE_NAME ne sera pas utilisé dans ce tuto. On définit simplement le nom du package. C'est utile pour la génération de documentation via l'outil JavaDoc.
  • L'include… On fait appel au fichier Makefile.include. C'est celui qui contient toutes les instructions importantes.

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 :

  • PACKAGE définit le répertoire racine. Comme le fichier Makefile se trouve dans /home/molodoi/Code et qu'il s'agit du répertoire racine, le raccourci '.' est suffisant pour définir le répertoire.
  • SUBDIRS Il faut voir cette variable comme une sorte tableau, où chaque cellule contient le répertoire correspondant à un package contenant des sources. Comme vous l'avez compris, c'est sur base de ce tableau qu'on va aller appeler les Makefiles se trouvant dans les répertoires/packages.

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 :

  • On va boucler sur le contenu du 'tableau' $(SUBDIRS). On définit une variable 'p' qui sera l'indice permettant d'accéder à chaque cellule du tableau. On accède à une cellule via
$$p

* Que signifie l'instruction :

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

??? Décomposons là…

  • l'option -C directory permet de changer de répertoire (aller dans directory) avant l'exécution de make. Dans notre cas, on va filer dans un répertoire “package” et exécuter le Makefile de ce répertoire…
  • l'option –no-print-directory permet d'éviter que le Makefile écrive à l'écran des messages du type:
Entre dans le répertoire directory
Quitte le répertoire directory
  • Le all indique qu'on va exécuter la règle par défaut du Makefile appelé. Note: le Makefile appelé ne contient que la déclaration de variables. Cpdt, rappelez-vous qu'il fait appel à un Makefile.include. C'est donc dans ce Makefile.include que se trouvera la règle 'all'.
  • Note: en principe, dans un Makefile, une instruction ou une déclaration de variable doit tenir sur une seule ligne. L'inconvénient, c'est que ce n'est pas tjrs lisible (comme dans le cas d'une boucle ou d'un 'tableau'). On peut alors mettre l'instruction/variable sur plusieurs lignes, mais, impérativement, chq ligne doit se terminer par
/

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

  • Je n'explique pas la règle 'clean'. Elle est identique à la règle par défaut.

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;

  • des variables d'environnement
        # set here the target dir for all classes

        CLASS_DIR       = $(TOP_DIR)

        #JAVA_CLASS     =

        LOCAL_CLASS_DIR = $(CLASS_DIR)/$(PACKAGE_DIR)

Explication :

  • CLASS_DIR définit la base des packages.
  • JAVA_CLASS (ici commenté et sans valeur) définit l'endroit où la JVM trouvera les librairies fournies avec le JDK. A définir si l'accès à Java n'est pas défini dans votre PATH.
  • LOCAL_CLASS_DIR définit le répertoire 'courant', càd celui où est, en fonction de l'appel par le Makefile du package.
  • des variables 'compilation'
        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 :

  • .SUFFIXES ⇒ permet de faire en sorte que les suffixes déclarés (dans notre cas, .java et .class) puissent être utilisés dans une règle de suffixe, çàd que le nom de la règle sera (dans notre cas):
.java.class
  • vpath est une instruction magique qui permet de trouver les sources dépendantes de la source courante. Dans certains cas, on peut l'utiliser pour trouver des cibles dépendantes, mais il semblerait que ce ne soit pas recommandé.

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 :

  • Il y a des règles en cascade… 'all' renvoie à 'classes' qui, lui-même, va spécifier que les fichiers sources ($(FILES)) peuvent avoir l'extension .class. Tout ça pour aboutir à la règle de suffixe.
  • La règle de suffixe. Rien de particulier (il faut connaitre un min Java pour comprendre). A noter que la variable '$<' représente le fichier de départ (le premier, s'il y en a plusieurs).
  • La règle 'clean'. Je la laisse à titre d'exercice.

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

dev/makefile.txt · Dernière modification : 2018/11/17 12:53 de 127.0.0.1