• Vers des Makefile génériques : partie 1

    Make est un système de construction logicielle né en 1977, peu avant la disparition des dinosaures et la Création.

     On pourrait penser qu'il est complètement dépassé par des outils très sérieux et très postérieurs en Java mâchant du XML. C'est bien possible, mais moi aussi. Il faudra aussi parler du joli concurrent CMake.

     GNU/Make présente l'avantage de la simplicité et de la lisibilité. Il est disponible à peu près partout. Comme Python, sa courbe d'apprentissage est progressive, et on peut utiliser Make de façon rudimentaire ou complexe.

    Je vous propose d'apprendre ou réviser l'utilisation de GNU/Make en deux ou trois petits articles : reformulation de script, utilisation des dépendances, factorisation.

    Voyons aujourd'hui comment passer d'un script shell à un véritable Makefile, puis, après une introduction assez brutale à la gestion des dépendances, comment obtenir non pas un, mais bien deux Makefile modulaires !


    De la reformulation d'un script à un Makefile simple

     Beaucoup de gens utilisent des scripts de build en bash.

    Considérons aujourd'hui que nous travaillons avec un projet jouet dont voici l'arborescence utile :

    •  projet/
      •  README
      •  src/
        •  source1.c
        •  source2.c

    On veut fournir un fichier ZIP release.zip contenant le fichier README et deux exécutables résultant respectivement de la compilation de source1.c et source2.c.


    Approche naiveOffice religieux dans une église

    Première transformation

    Après avoir tapé deux cent cinquante six fois les commandes permettant de compiler, disons que vous ayez produit le script suivant :

     build.sh

    1 #!/bin/bash 
    2 #remove trailing spaces 
    3 find . -name "*.c" |xargs sed -i 's/[ \t]*$$//' 
    4 
    5 cd src
    6 
    7 gcc source1.c -o executable1
    8 gcc source2.c -o executable2
    9 zip ../release.zip executable1 executable2 ../README
    

    Il est très simple de le transformer naivement en un Makefile :

     Makefile
     1 #Une seule «recette» (recipe)/cible (target) pour tout faire 
     2 release.zip: 
     3 #remove trailing spaces 
     4 	find . -name "*.c" |xargs sed -i 's/[ \t]*$$//' 
     5 
     6 	cd src
     7  
     8 	gcc source1.c -o executable1
     9 	gcc source2.c -o executable2
    10  
    11 	zip ../release.zip executable1 executable2 ../README
    

    C'est un peu plus long, mais ça sera aussi plus joli à la fin.

     L'indentation dans le Makefile doit être composée de caractères de tabulation horizontale, strictement (ASCII 9).

     Une « cible » désigne quelque chose que l'on veut construire. On lui associe des pré-requis (voir plus loin), et une recette, qui suit la déclaration de la cible et est indentée.

    Invocation par défaut pour construire notre cible

    Pour construire votre zip, lancez simplement dans votre shell 'make' ou 'make release.zip'.
    Les shells dotés d'une fonction de complètement automatique vous proposeront d'ailleurs les cibles (target) disponibles.

    S'il est invoqué sans l'option "-f", make recherche automatiquement dans le répertoire courant un fichier nommé Makefile, ou makefile pour y trouver des recettes, et sauf indication contraire, exécute la première recette.

    Différence avec le script build.sh plus haut : votre recette s'exécutera comme si vous aviez entré

    % set -x (echo des commandes exécutées)

    et

    % set -e (arrêt en cas d'erreur)

    Vers des Makefile génériques : partie 1Une seconde cible : le nettoyage

    Pour faire réaliste, prenons en compte un système rustique de nettoyage

     clean.sh 
    1 #!/bin/bash 
    2 find . -name "*.o" -delete 
    

    Ajoutons une cible clean à notre Makefile :

    13 clean: 
    14 	find . -name "*.o" -delete
    

    Invoquer une cible ou l'autre

    La première cible définie plus haut sera toujours construite par

    % make

    Pour nettoyer :

    % make clean

    Cibles spéciales et dépendances

    En trente-quatre ans, beaucoup de gens ont rencontré des Makefile, et il serait dommage de ne pas se conformer à des conventions si aisées à suivre. Il existe un certain nombre de cibles dont tout le monde comprend le sens ; clean est l'une d'elles, all une autre.

    Je vous propose maintenant d'introduire la cible all en haut de votre Makefile, comme suit :

    1 all: release.zip
    

    On a créé une cible nommée all, qui dépend de la cible release.zip décrite précédemment.

     Implicitement, toutes les cibles désignent des fichiers à construire.

     Par économie, make ne reconstruit pas un fichier qui existe déjà (sauf si les dépendances ont changé, nous allons voir cela).

    Mauvaise nouvelle : make est frileux

    Un effet de bord de ces fonctionnalités est que si par hasard vous créez maintenant un fichier nommé cleanmake ne voudra plus nettoyer ; si vous créez un fichier nommé all, make ne voudra plus compiler, et c'est ennuyeux.

    Il existe un moyen de passer outre et de construire une cible inconditionnellement :

    Il faut pour cela ajouter dans le Makefile la déclaration :

    3 .PHONY: all clean
    

    Par contre, nous ne déclarerons pas la cible release.zip comme .PHONY puisqu'elle correspond à un vrai fichier qu'on peut stater.

    Bonne nouvelle : make est paresseux

    Il est possible d'ajouter des dépendances sous formes de fichiers intermédiaires à construire.


    Vers des Makefile génériques : partie 1Par exemple, comme on sait que release.zip doit être construit à l'aide de projet/README et de deux exécutables compilés dans projet/src/, nous indiquons cette information à make :

    17 release.zip: src/executable1 src/executable2 README
    18 	zip $@ $^
    19 #Voyez les explications de cette commande magique ci-dessous
    

    Oups, j'ai utilisé des variables automatiques sans prévenir ! Pas de panique, c'est très simple :

    • $@ désigne la cible courante (release.zip) 
    • $^ désigne la liste de pré-requis (à droite du caractère «:» de la ligne numérotée 17).

     La commande zip src/executable1src/executable2 README ne sera exécutée que si l'un au moins des trois fichiers requis par la cible nommée release.zip est plus récent que  le fichier release.zip.

    Au début on a foncé, maintenant on accélère : abordons la récursion.

     


    Un peu de récursion et de structure

     


    Vers des Makefile génériques : partie 1Voyons maintenant comment structurer les recettes et sous-recettes de construction.

    De nombreuses variables implicites sont accessibles dans Make (voyez la doc de GNU/Make à l'occasion). Nous allons les utiliser pour gagner en dynamisme.

      Un nom de variable de plus d'un caractère doit indiqué entre parenthèses préfixées par «$».

    Ingrédients

    Variables simples  $(MAKE) désigne l'exécutable 'make' et permet les invocations récursives 
    $(SRCDIRS), $(GENERIC)  variables définies au sein du Makefile 
    Variables automatiques  $@ désigne la cible
    $^  contient tous les pré-requis de cette recette 
    $< contient le premier pré-requis causant l'appel de cette recette  
    Cible fichier release.zip  
    Cibles virtuelles (PHONY) all  
    clean

    Ici on continuera malgré les éventuelles erreurs (IGNORE)

    Maintenant voici simplement les fichiers résultant, abondamment commentés.
     projet/Makefile
     1 #Le ou les répertoires dans lesquels des cibles doivent être construites
     2 SRCDIRS = src
     3 #recettes fonctionnant dans les répertoires ci-dessus
     4 GENERIC = all clean
     5 
     6 # La règle qui suit sera exécutée pour les cibles 'all' et 'clean' dans chaque répertoire de SRCDIRS
     7 # l'option -C passée à make permet de conserver l'état global du process de construction
     8 #Dans la recette «all», $@ vaut «all» et $< vaut «src»
     9 $(GENERIC): $(SRCDIRS)
    10 	$(MAKE) -C $< $@
    11 
    12 #Ajout d'une dépendance spécifique à all
    13 all: release.zip
    14 
    15 #La construction de release.zip est désormais effectuée relativement au répertoire racine
    16 #-c'était moche ces .., non ?
    17 release.zip: src/executable1 src/executable2 README
    18 	zip $@ $^
    19 # zip release.zip src/executable1 src/executable2 README
    20 
    21 .PHONY: all clean
    
    
     projet/src/Makefile

    Remarquons la symétrie des opérations :

     1 all:
     2 	gcc source1.c -o executable1
     3 	gcc source2.c -o executable2
     4 
     5 clean:
     6 	rm executable1
     7 	rm executable2
     8 
     9 .PHONY: all clean
    10 
    11 #IGNORE permet d'ignorer les erreurs lors de l'exécution d'une règle,
    12 #car sinon, contrairement aux shells, Make s'arrête à la moindre erreur.
    13 .IGNORE: clean
    

    Grue tour du port de KristiansandConclusion

    Nous avons aujourd'hui abordé les bases de l'utilisation de GNU/Make. Grâce à cette connaissance, vous pouvez proposer l'interface la plus standard standard à la construction de votre projet : taper "make". Les Makefile décrits ici constituent une base extensible que nous rendrons flexible dans un prochain billet.

    Je vous recommande de jouer avec Make en essayant de générer des erreurs (supprimez des fichiers, ou créez un fichiers nommé clean, etc) afin de découvrir les messages correspondants ou les informations de progression fournies par ce très bon outil.


    Post scriptum
    • merci à mike_perdide pour sa relecture

    • merci à vim pour ses conversions de code vers le html (et à la tribune linuxfr qui m'a dit comment faire) : par exemple, vim  +"highlight makeSpecTarget ctermbg=black ctermfg=yellow" +"highlight LineNr ctermfg=grey ctermbg=black" +"highlight makeComment ctermfg=darkblue" +"set background=light" +f src/Makefile  +"syntax on" +"so /usr/share/vim/vim73/syntax/2html.vim" +"wq" +"q" (oui, je ferai un Makefile pour ça aussi)

       


    Source des images :

     

    « Thinkpad T41 pour vousFraudeurs par défaut »

    Tags Tags : ,
  • Commentaires

    Aucun commentaire pour le moment

    Suivre le flux RSS des commentaires


    Ajouter un commentaire

    Nom / Pseudo :

    E-mail (facultatif) :

    Site Web (facultatif) :

    Commentaire :