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.
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é.
#
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
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
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
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 :
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 :
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'”.
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).
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, …)
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 :
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.
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.
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.
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.
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.
On va utiliser trois types de Makefile:
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, 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.
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 :
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