Introduction à Autotools et m4

Lecture 13 min.

    Bonjour à toutes et tous 😃

    Depuis que je fais de l’informatique et que je compile des projets, une chose m’a toujours intrigué mais à chaque fois je me disais on verra ça plus tard …

    Ce quelque chose, c’est l’extraordinaire homogénéité entre tous les projets sur la méthode dont on les installe.

    Souvent on a :

    ./configure --prefix /usr/bin
    make
    make test
    make install
    

    Dans toutes ces lignes ce qui va nous occuper le plus c’est la première et la deuxième

    ./configure
    

    Dans la suite des exemples, je considère que vous vous trouvez dans un environnement “”“linux”“”.

    Pourquoi ?

    C’est peut-être la question fondamentale à se poser lorsque l’on se lance dans l’exploration d’un sujet ^^

    Pour cela, nous allons faire un peu de C.

    Voici un main.c

    #include "stdio.h"
    
    void main() {
        printf("Hello World!\n");
    }
    

    Pour le construire, nous utilisons un logiciel appelé compilateur, ici gcc.

    $ gcc main.c -o hello
    

    Ceci construit un exécutable qui peut être lancé:

    $ ./hello
    Hello World!
    

    Note

    Si vous n’avez pas gcc, ce site est votre meilleur ami!

    Ok, on compile

    Maintenant si on a deux fichiers, nos sources deviennent:

    // main.c
    #include "stdio.h"
    
    void main() {
        printf("Hello World!\n");
    }
    
    // hello.h
    const char* hello();
    
    // hello.c
    const char* hello() {
        return "Hello World!"
    }
    

    Et pour compiler:

    $ gcc main.c hello.c -o hello
    

    Trois fichiers:

    $ gcc main.c hello.c data.c -o hello
    

    Etc…

    Bref, pour une personne qui n’a pas une connaissance parfaite des fichiers d’un projet, cela peu être quasiment de savoir comment construire un projet.

    C’est pour cette raison que les Makefiles sont apparu

    Un Makefile est une série de règles qui construit des choses à partir de composants

    all:
        gcc main.c hello.c data.c -o hello
    

    Une des règles par défaut est le all.

    Il permet de s’appeler par

    $ make
    

    Note

    Si vous n’avez pas make, ce site est votre meilleur ami!

    Que la commande gcc est une ou dix fichiers en entré, la commande sera toujours:

    $ make
    

    Bon, cool on a réglé le problème du point de vue “utilisateur” de notre projet qui doit construire les sources.

    Mais nous ça nous arrange pas tellement.

    A chaque fois que l’on voudra ajouter des sources, nous devront mettre à jour le Makefile.

    Un autre point dérangeant, c’est que l’on ne peut pas piloter la sortie de compilation.

    Or, un bon développeur, c’est développeur … ?

    Fainéant !! Yes ça suit derrière !

    Et que fait un bon fainéant ?

    Il travaille à automatiser pour ne plus à avoir à bosser ensuite.

    Et cette petite musique remonte à loin ^^

    Les outils que je vais vous montrer datent des années 1980 !

    Le langage m4

    Le premier outils que je vais vous montrer et qui sera littéralement la base de tout par la suite est le langage m4.

    Pourquoi s’appelle-t-il ainsi ?

    Parce que son prédécesseur s’appelait m3.

    Oui, les Fondateurs de l’Informatique n’avaient pas notre temps 🤣

    Le m veut simplement dire macros et le 4 parce que c’est la 4ème itération du langage et de son interpréteur.

    En parlant d’interpréteur.

    Voici l’outils:

    $ m4 --version
    m4 (GNU M4) 1.4.18
    Copyright (C) 2016 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.
    
    Written by Rene' Seindal.
    

    Son utilisation est extrêment simple.

    Pour un fichier

    // file.m4
    Je suis un fichier M4
    

    Si on exécute:

    $ m4 file.m4 
    // file.m4
    Je suis un fichier M4
    

    Note

    Si vous n’avez pas m4, ce site est votre meilleur ami!

    Fascinant n’est-ce pas ? 😛

    Define

    Introduisons la brique fondamentale de m4.

    J’ai nommé define. Celui-ci prend deux arguments:

    define(name, content)
    

    Un fichier m4 peut alors contenir ce contenu

    // file2.m4
    Je suis un fichier M4
    define(ma_macro, je suis le contenu de la macro)
    ma_macro
    

    Si on exécute:

    $ m4 file2.m4 
    // file.m4
    Je suis un fichier M4
    
    je suis le contenu de la macro
    

    On a le remplacement du symbole ma_macro par le contenu voulu.

    dnl

    Ah mais par contre ça saute une ligne. Est-ce bien normal tout cela ?

    Oui, ça l’est, mais si on ne désire pas ce comportement, on peut écrire un dnl à la fin de la ligne.

    Note

    dnl pour delete new line, oui toujours aussi efficace ^^

    // file3.m4
    Je suis un fichier M4
    define(ma_macro, je suis le contenu de la macro)dnl
    ma_macro
    
    $ m4 file2.m4 
    // file.m4
    Je suis un fichier M4
    je suis le contenu de la macro
    

    On colle les deux lignes.

    Paramètres

    Une macro peut aussi être une fonction et donc prendre des paramètres.

    // file.m4
    define(sum, $1 + $2)dnl
    sum(7, 4)
    
    ///résultat
    // file.m4
    7 + 4
    

    Il est possible d’avoir jusqu’à 9 paramètres de $1 à $9.

    Ensuite, il faut jouer avec des tableaux, mais je vais pas expliquer ça ici, c’est hors scope.

    Une macro peut en cacher une autre

    Lors d’un appel de macro, il est possible d’en utiliser une seconde en paramètre.

    // file.m4
    define(sum, $1 + $2)dnl
    sum(7, sum(15, 2))
    
    ///résultat
    // file.m4
    7 + 15 + 2
    

    Si on décompose ce qu’il se passe

    sum(7, sum(15, 2))
    7 + sum(15, 2)
    7 + 15 + 2
    

    On a bien le remplacement successif des symboles.

    On peut faire de même dans la définission de la macro elle-même

    // file.m4
    define(sum, $1 + $2)dnl
    define(sum2, $1 + sum($2, $3))dnl
    sum2(7, 15, 2)
    
    /// résultat
    // file.m4
    7 + 15 + 2
    

    Ici, on fait l’inverse, au lieu de remplacer une macro par sa valeur, on définit une macro et on lui définit des parmètres avant de remplacer également la macro par sa valeur

    sum2(7, 15, 2)
    7 + sum(15, 2)
    7 + 15 + 2
    

    Le résultat est identique mais la manière de le faire ne l’est pas.

    C’est cela une macro. Quelque chose capable d’automatiser de la génération de textes.

    Notre but, va donc d’être dans la capacité de générer le contenu d’un Makefile qui contiendra une commande gcc avec en paramètres toutes les sources voulues et qui sera capable de définir le chemin de sortie désiré.

    Autotools

    C’est à ce moment qu’une galaxie d’outils datant eux aussi des années 80 entre en scène.

    Ce sont globalement des générérateurs de fichiers m4.

    Petit rappel, nous voulons arriver à ce résultat-ci:

    ./configure --prefix /usr/bin
    make
    make test
    make install
    

    autoconf

    La première étape de notre périple va être de créer l’exécutable ./configure.

    Ce configure ne vient pas de nulle part, il provient d’un programme qui se nomme autoconf.

    Son rôle est de créer le fichier ./configure qui on le verra n’est que du bash.

    Mais pour cela, il lui faut un template de construction permettant de spécifier ce que l’on veut construire.

    C’est le rôle du fichier configure.ac.

    Un configure.ac minimal ressemble à ceci:

    // configure.ac
    AC_INIT([hello], [1.0])
    AC_OUTPUT
    

    Maintenant que vous êtes des pros en m4 (c’est faux) vous devriez voir émerger deux macros:

    Si votre compilation nécessite un programme en particulier, il est possible que définir une vérification qui fera le travail à votre place

    Faisons quelques expériences

    Tout d’abord sans le AC_INIT et AC_OUTPUT

    // configure.ac
    
    
    $ autoconf
    $ tree
    .
    ├── autom4te.cache/
    ├── configure
    └── configure.ac
    $ wc -l configure
    0
    

    Comme prévu pas de macro, pas de contenu, mais nous avons tout de même généré un ./configure, il ne fait rien, mais au moins à le mérite d’exister ^^“

    Essayons quelque chose de plus utile.

    // configure.ac
    AC_INIT([hello], [1.0])
    
    $ autoconf
    
    $ wc -l configure
    1673
    $ head configure 
    #! /bin/sh
    # Guess values for system-dependent variables and create Makefiles.
    # Generated by GNU Autoconf 2.69 for hello 1.0.
    #
    #
    # Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc.
    #
    #
    # This configure script is free software; the Free Software Foundation
    # gives unlimited permission to copy, distribute and modify it.
    

    Note

    Si vous n’avez pas autoconf, ce site est votre meilleur ami!

    On a bien du contenu, et c’est bien du shell script.

    Qui peut alors être exécuté

    $ ./configure --version
    hello configure 1.0
    generated by GNU Autoconf 2.69
    

    On reconnait les arguments passés à la macro AC_INIT

    Et si on fait

    $ ./configure
    $ tree
    .
    ├── autom4te.cache
    ├── config.log
    ├── configure
    └── configure.ac
    

    Rien de plus… A part un fichier de log config.log

    Dedans on y trouve pricipalement les différents chemin qu’il connait dans son PATH et le nom du projet.

    Rajoutons le AC_OUTPUT

    // configure.ac
    AC_INIT([hello], [1.0])
    AC_OUTPUT
    
    $ autoconf
    
    $ ./configure
    configure: creating ./config.status
    

    Oh ! du nouveau !

    $ tree -L 1
    .
    ├── autom4te.cache
    ├── config.log
    ├── config.status
    ├── configure
    └── configure.ac
    

    Encore un nouveau fichier.

    $ head config.status 
    #! /bin/bash
    # Generated by configure.
    # Run this file to recreate the current configuration.
    # Compiler output produced by configure, useful for debugging
    # configure, is in config.log if it exists
    

    Et un nouveau bash ! Cela veut dire que cela s’exécute.

    $ ./config.status --version
    hello config.status 1.0
    configured by ./configure, generated by GNU Autoconf 2.69,
      with options ""
    

    Il nous sort tout le roman de sa conception et une ligne qui m’intéresse

    with options ""
    

    Donc maintenant si je fais

    $ ./configure --prefix /home/data
    $ ./config.status --version
    hello config.status 1.0
    configured by ./configure, generated by GNU Autoconf 2.69,
      with options "'--prefix' '/home/data'"
    

    Hé hé 😃 Bingo !

    On a un début de quelque chose qui se rapproche de notre objectif.

    autoconf réserve bien des surprises, par exemple, nous pouvons lui dire de vérifier l’existence de ce qu’il faut pour compiler du C

    // configure.ac
    AC_INIT([hello], [1.0])
    // Vérifie que gcc est présent
    AC_PROG_CC
    AC_OUTPUT
    
    $ autoconf
    $ ./configure
    checking for gcc... gcc
    checking whether the C compiler works... yes
    checking for C compiler default output file name... a.out
    checking for suffix of executables... 
    checking whether we are cross compiling... no
    checking for suffix of object files... o
    checking whether we are using the GNU C compiler... yes
    checking whether gcc accepts -g... yes
    checking for gcc option to accept ISO C89... none needed
    configure: creating ./config.status
    

    Oh, mais c’est de la magie tout ça 🤩

    4 lignes nous génère un système relativement complexe. m4 c’est trop cool !

    Cool oui, mais toujours aucune trace de notre Makefile

    automake

    Pour le coup impossible de deviner le comportement.

      graph TD
    A[configure.ac] -->|utilisé par| B(autoconf)
    B -->|génère| C[./configure]
    A -->|utilisé par| E
    D[Makefile.am] -->|utilisé par| E(automake)
    E -->|génère| F[Makefile.in]
    F -->|utilisé par| C
    C -->|génère| G([Makefile])
    

    Le Makefile est généré par le ./configure.

    Le ./configure est généré à partir du configure.ac au travers de la commande autoconf.

    Le ./configure utilise le fichier Makefile.in pour générer le Makefile

    Le Makefile.in est généré par la commande automake.

    Et pour la suite on voit ça tout de suite 😃

    Comme d’habitude, surement une mauvaise habitude ^^“ J’aime bien lancer les commandes à blanc pour voir ce que ça donne.

    $ automake
    configure.ac: error: no proper invocation of AM_INIT_AUTOMAKE was found.
    configure.ac: You should verify that configure.ac invokes AM_INIT_AUTOMAKE,
    configure.ac: that aclocal.m4 is present in the top-level directory,
    configure.ac: and that aclocal.m4 was recently regenerated (using aclocal)
    automake: error: no 'Makefile.am' found for any configure output
    automake: Did you forget AC_CONFIG_FILES([Makefile]) in configure.ac?
    

    Note

    Si vous n’avez pas automake, ce site est votre meilleur ami!

    Et pour le coup, je ne suis pas déçu. L’erreur nous explique tout ^^

    Allons-y!

    Rajoutons ce qu’il demande.

    // configure.ac
    AC_INIT([hello], [1.0])
    AM_INIT_AUTOMAKE
    AC_PROG_CC
    AC_CONFIG_FILES([Makefile])
    AC_OUTPUT
    
    $ automake
    configure.ac: error: no proper invocation of AM_INIT_AUTOMAKE was found.
    configure.ac: You should verify that configure.ac invokes AM_INIT_AUTOMAKE,
    configure.ac: that aclocal.m4 is present in the top-level directory,
    configure.ac: and that aclocal.m4 was recently regenerated (using aclocal)
    Makefile.am: error: required file './INSTALL' not found
    Makefile.am:   'automake --add-missing' can install 'INSTALL'
    Makefile.am: error: required file './NEWS' not found
    Makefile.am: error: required file './README' not found
    Makefile.am: error: required file './AUTHORS' not found
    Makefile.am: error: required file './ChangeLog' not found
    Makefile.am: error: required file './COPYING' not found
    Makefile.am:   'automake --add-missing' can install 'COPYING'
    Makefile.am: error: required file './depcomp' not found
    Makefile.am:   'automake --add-missing' can install 'depcomp'
    /usr/share/automake-1.16/am/depend2.am: error: am__fastdepCC does not appear in AM_CONDITIONAL
    /usr/share/automake-1.16/am/depend2.am:   The usual way to define 'am__fastdepCC' is to add 'AC_PROG_CC'
    /usr/share/automake-1.16/am/depend2.am:   to 'configure.ac' and run 'aclocal' and 'autoconf' again
    /usr/share/automake-1.16/am/depend2.am: error: AMDEP does not appear in AM_CONDITIONAL
    /usr/share/automake-1.16/am/depend2.am:   The usual way to define 'AMDEP' is to add one of the compiler tests
    /usr/share/automake-1.16/am/depend2.am:     AC_PROG_CC, AC_PROG_CXX, AC_PROG_OBJC, AC_PROG_OBJCXX,
    /usr/share/automake-1.16/am/depend2.am:     AM_PROG_AS, AM_PROG_GCJ, AM_PROG_UPC
    /usr/share/automake-1.16/am/depend2.am:   to 'configure.ac' and run 'aclocal' and 'autoconf' again
    

    Cela discuste pas mal, c’est le moins qu’on puisse dire ^^“

    On nous parle d’une commande aclocal à lancer.

    Tentons l’expérience:

    $ aclocal
    $ tree
    .
    ├── aclocal.m4
    ├── autom4te.cache
    └── configure.ac
    

    aclocal.m4 ça on connait ^^ C’est rempli de macros à usage interne de automake.

    Note

    Les macros que nous utilisons dans le configure.ac viennent bien de quelque part, ce quelques par c’est ce fichier 😀

    Bien relançons:

    $ automake
    configure.ac:3: error: required file './compile' not found
    configure.ac:3:   'automake --add-missing' can install 'compile'
    configure.ac:2: error: required file './install-sh' not found
    configure.ac:2:   'automake --add-missing' can install 'install-sh'
    configure.ac:2: error: required file './missing' not found
    configure.ac:2:   'automake --add-missing' can install 'missing'
    automake: error: no 'Makefile.am' found for any configure output
    

    Moins d’erreur !

    Comme il nous donne gentillement la réponse, nous n’allons pas nous casser la tête ^^“

    $ automake --add-missing
    configure.ac:3: installing './compile'
    configure.ac:2: installing './install-sh'
    configure.ac:2: installing './missing'
    automake: error: no 'Makefile.am' found for any configure output
    

    Presque !

    Nous devons créer le Makefile.am qui est attendu.

    Cette fois-ci, ce n’est pas du m4. C’est plus un clef/valeur comme un fichier de configuration.

    Sa syntaxe est pour le moins particulière.

    prefix_IDENTIFIER = value
    

    Nous nous voulons produire un programme dans le dossier <prefix>/bin. Et nous voulons l’appeler “hello”.

    Le <prefix> étant la valeur passé à ./configure --prefix /path.

    Donc notre règle sera:

    bin_PROGRAMS = hello
    

    Mais du coup, il nous faut également quelque chose pour produire le hello.

    Donc ici même principe:

    hello_SOURCES = main.c
    

    Ce qui donne au final

    // Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = main.c
    
    $ automake --add-missing
    Makefile.am: installing './depcomp'
    $ tree -L 1
    .
    ├── Makefile.am
    ├── Makefile.in
    ├── aclocal.m4
    └── configure.ac
    
    $ wc -l Makefile.in
    738
    

    Et bien voilà !

    738 lignes quand même, on comprends que l’on a pas trop envie d’écrire ça à la main ^^“

    On peut finalement générer le Makefile !

    $ autoconf
    
    $ ./configure --prefix /home/data
    checking for a BSD-compatible install... /usr/bin/install -c
    checking whether build environment is sane... yes
    checking for a thread-safe mkdir -p... /usr/bin/mkdir -p
    checking for gawk... gawk
    checking whether make sets $(MAKE)... yes
    checking whether make supports nested variables... yes
    checking for gcc... gcc
    checking whether the C compiler works... yes
    checking for C compiler default output file name... a.out
    checking for suffix of executables... 
    checking whether we are cross compiling... no
    checking for suffix of object files... o
    checking whether we are using the GNU C compiler... yes
    checking whether gcc accepts -g... yes
    checking for gcc option to accept ISO C89... none needed
    checking whether gcc understands -c and -o together... yes
    checking whether make supports the include directive... yes (GNU style)
    checking dependency style of gcc... gcc3
    checking that generated files are newer than configure... done
    configure: creating ./config.status
    config.status: creating Makefile
    config.status: executing depfiles commands
    

    Dans ce roman, deux lignes nous intéresse vraiment:

    configure: creating ./config.status
    config.status: creating Makefile
    

    Nous allons pouvoir rectifier le schéma.

      graph TD
    A[configure.ac] -->|est utilisé par| B(autoconf)
    I(aclocal) -->|génère| J[aclocal.m4]
    B -->|génère| C[./configure]
    A -->|est utilisé par| E
    D[Makefile.am] -->|est utilisé par| E(automake)
    J -->|est utilisé par| E
    E -->|génère| F[Makefile.in]
    F -->|est utilisé par| H
    C -->|génère| H[config.status]
    H -->|génère| G([Makefile])
    

    Et cette fois-ci, nous avons un Makefile.

    make

    Et si nous grepons dedans, nous pouvons voir notre configuration.

    $ grep "^prefix =" Makefile
    prefix = /home/data
    
    $ grep "main.c" Makefile
    hello_SOURCES = main.c
    

    Ok, on se rapproche ♥️

    Plus qu’un dernier effort !

    $ make
    make: *** No rule to make target 'main.c', needed by 'main.o'.  Stop.
    

    Ah, oui les sources 😒

    // main.c
    #include "stdio.h"
    
    void main() {
        printf("Hello World!\n");
    }
    
    $ make
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
    mv -f .deps/main.Tpo .deps/main.Po
    gcc  -g -O2   -o hello main.o  
    
    $ tree -L 1
    .
    ├── Makefile
    ├── Makefile.am
    ├── Makefile.in
    ├── aclocal.m4
    ├── config.log
    ├── config.status
    ├── configure
    ├── configure.ac
    ├── hello
    ├── main.c
    ├── main.o
    └── main.o
    

    Cela en fait du monde !

    Mais chose intéressante, nous pouvons décomposer le schéma en 2:

      graph TD
    A[configure.ac] -->|est utilisé par| B(autoconf)
    I(aclocal) -->|génère| J[aclocal.m4]
    B -->|génère| C[./configure]
    A -->|est utilisé par| E
    J -->|est utilisé par| E
    D[Makefile.am] -->|est utilisé par| E(automake)
    E -->|génère| F[Makefile.in]
    

    D’une part, nous générons les Makefile.in et ./configure.

    D’autre part nous les utilisons

      graph TD
    C[./configure]
    F[Makefile.in]
    F -->|est utilisé par| H
    C -->|génère| H[config.status]
    H -->|génère| G([Makefile])
    

    Nous partons d’un dossier avec

    $ tree
    .
    ├── Makefile.in
    ├── configure
    └── main.c
    

    On fait notre touille:

    $ ./configure --prefix /home/data
    configure: error: cannot find install-sh, install.sh, or shtool in "." "./.." "./../.."
    

    ¡Caramamba! Encore raté! 😶‍🌫️

    Auxilliaires

    Lors de l’exécution du

    automake --add-missing
    

    J’ai complétement passé sous silence le --add-missing.

    Celui-ci a pour rôle de rajouter ce qu’il manque:

    $ automake --add-missing
    configure.ac:3: installing './compile'
    configure.ac:2: installing './install-sh'
    configure.ac:2: installing './missing'
    $ tree -L 1
    .
    ├── Makefile.am
    ├── Makefile.in
    ├── aclocal.m4
    ├── autom4te.cache
    ├── compile -> /usr/share/automake-1.16/compile
    ├── configure.ac
    ├── depcomp -> /usr/share/automake-1.16/depcomp
    ├── install-sh -> /usr/share/automake-1.16/install-sh
    └── missing -> /usr/share/automake-1.16/missing
    

    Bon il est là le install-sh, mais de un ce n’est pas un vrai fichier mais un symlink et de deux il est en vrac dans à la racine.

    Heureusement ces deux problèmes se résolvent.

    On va tout d’abord fixer le problème de chemin.

    Pour cela, nous rajoutons un appel à la macro AC_CONFIG_AUX_DIR qui prend le dossier de destination.

    Attention

    Celui-ci doit exister avant de lancer le automake.

    // configure.ac
    AC_INIT([hello], [1.0])
    AC_CONFIG_AUX_DIR([build]) 
    AM_INIT_AUTOMAKE([foreign])
    AC_PROG_CC
    AC_CONFIG_FILES([Makefile])
    AC_OUTPUT
    
    $ mkdir build
    $ automake --add-missing
    $ tree build
    build/
    ├── compile -> /usr/share/automake-1.16/compile
    ├── depcomp -> /usr/share/automake-1.16/depcomp
    ├── install-sh -> /usr/share/automake-1.16/install-sh
    └── missing -> /usr/share/automake-1.16/missing
    

    Bon, un problème de résolu.

    Maintenant cette histoire de symlink.

    $ rm -fr build/*
    $ automake --add-missing --copy
    $ tree build
    build/
    ├── compile
    ├── depcomp
    ├── install-sh
    └── missing
    

    Et GOAL ! 😁

    Build final

    Reprenons où nous nous sommes arrêté

    $ tree
    .
    ├── Makefile.in
    ├── build
    │   ├── compile
    │   ├── depcomp
    │   ├── install-sh
    │   └── missing
    ├── configure
    └── main.c
    

    Voici notre hiérarchie de fichiers.

    Prêt pour les commandes finales ?

    Let’s go !

    $ ./configure --prefix /home/data
    $ make
    make: *** No rule to make target 'Makefile.am', needed by 'Makefile.in'.  Stop.
    

    Et ben pas final alors ^^‘’’

    Qu’est ce qu’il se passe encore ???

    On entre dans la politique du Libre, tout doit reconstructible par tout le monde tout le temps.

    Donc Makefile se reconstruit lui-même.

    Alors, c’est bien sympa, mais moi je n’ai pas envie de shipper la Terre entière.

    Je veux que seul les fichiers qui sont dans la commande tree soit présent.

    Et pour se faire, je ne vais pas vous jouer de la flûte, ça été un enfer.

    Ce n’est pas normalement comme ça que les outils doivent marcher.

    Mais je suis têtu, et j’ai fini par trouver cette page, elle explique comment débrailler le comportement ^^

    Pour cela on retourne dans le configure.ac et on rajoute la macro AM_MAINTAINER_MODE

    // configure.ac
    AC_INIT([hello], [1.0])
    AC_CONFIG_AUX_DIR([build]) 
    AM_INIT_AUTOMAKE([foreign])
    /// Plus de rebuild !
    AM_MAINTAINER_MODE([disable])
    AC_PROG_CC
    AC_CONFIG_FILES([Makefile])
    AC_OUTPUT
    

    Et comme maintenant on est fort ! On peut prendre des raccourci

    $ tree
    .
    ├── Makefile.am
    └── configure.ac
    $ autoreconf -i
    configure.ac:6: installing 'build/compile'
    configure.ac:3: installing 'build/install-sh'
    configure.ac:3: installing 'build/missing'
    Makefile.am: installing 'build/depcomp'
    $ tree
    .
    ├── Makefile.am
    ├── Makefile.in
    ├── aclocal.m4
    ├── build
    │   ├── compile
    │   ├── depcomp
    │   ├── install-sh
    │   └── missing
    ├── configure
    └── configure.ac
    

    C’est quand même cool les chemins de traverses non ? ^^

    Bon assez rigolé !

    $ ./configure --prefix /home/data
    // ... pleins de checks
    configure: creating ./config.status
    config.status: creating Makefile
    config.status: executing depfiles commands
    
    $ make
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
    mv -f .deps/main.Tpo .deps/main.Po
    gcc  -g -O2   -o hello main.o
    
    $ make install
    make[1]: Entering directory '/workspaces/nix-hello/user'
     /usr/bin/mkdir -p '/home/data/bin'
      /usr/bin/install -c hello '/home/data/bin'
    make[1]: Nothing to be done for 'install-data-am'.
    make[1]: Leaving directory '/workspaces/nix-hello/user'
    
    $ /home/data/bin/hello 
    Hello World!
    

    Nous avons bien notre exécutable dans le dossier du prefix. Mais dans le sous-dossier bin.

    Car

    // Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = main.c
    

    Donne comme chemin pour notre exécutable le dossier bin.

    Eh beh ! Pas simple !

    Et encore, là on a juste quelque chose d’horriblement complexe pour ce qu’on faisait preseque déjà avec la commande gcc.

    On profite de l’accalmie pour mettre à jour notre schéma

      graph TD
    C[./configure]
    F[Makefile.in]
    F -->|est utilisé par| H
    C -->|génère| H[config.status]
    H -->|génère| G([Makefile])
    M[ main.c ] -->|utilisé par| G
    K[[ build/ ]] -->|utilisé par| G
    G -->|génère| L{hello.exe}
    

    Mais maintenant nous allons attaquer le multi-sources

    Un vrai build

    Notre but à la base c’était d’avoir plusieurs fichiers de sources donc on reprend.

    // main.c
    #include "stdio.h"
    
    void main() {
        printf("Hello World!\n");
    }
    
    // hello.h
    const char* hello();
    
    // hello.c
    const char* hello() {
        return "Hello World!"
    }
    

    Comme notre compilation à plusieurs sources, on modifie le Makefile.am

    // Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = main.c hello.c
    
    $ ./configure --prefix /home/data
    // ... pleins de checks
    configure: creating ./config.status
    config.status: creating Makefile
    config.status: executing depfiles commands
    
    $ make
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
    mv -f .deps/main.Tpo .deps/main.Po
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.     -g -O2 -MT hello.o -MD -MP -MF .deps/hello.Tpo -c -o hello.o hello.c
    mv -f .deps/hello.Tpo .deps/hello.Po
    gcc  -g -O2   -o hello main.o hello.o 
    
    $ make install
    make[1]: Entering directory '/workspaces/nix-hello/user'
     /usr/bin/mkdir -p '/home/data/bin'
      /usr/bin/install -c hello '/home/data/bin'
    make[1]: Nothing to be done for 'install-data-am'.
    make[1]: Leaving directory '/workspaces/nix-hello/user'
    
    $ /home/data/bin/hello 
    Hello World!
    

    Cela compile ce qu’il faut et tout le monde est heureux 😀

    Alors, oui mais non …

    A part les projets hyper vieux en C de l’époque, maintenant on essaie de faire des dossier un peu carré pour les sources.

    $ tree
    .
    ├── Makefile.in
    ├── build
    │   ├── compile
    │   ├── depcomp
    │   ├── install-sh
    │   └── missing
    ├── configure
    ├── includes
    │   └── hello.h
    └── src
        ├── hello.c
        └── main.c
    

    Tout le but va donc d’être capable d’une part de rajouter les fichiers headers.

    Et d’autre part les sources.

    Si on lance avec le Makefile.am inchangé

    // Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = main.c hello.c
    

    On obtient

    $ make
    make: *** No rule to make target 'main.c', needed by 'main.o'.  Stop.
    

    Logique, on modifie en conséquence

    // Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = src/main.c src/hello.c
    
    $ make
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o `test -f 'src/main.c' || echo './'`src/main.c
    src/main.c:2:10: fatal error: hello.h: No such file or directory
        2 | #include "hello.h"
          |          ^~~~~~~~~
    compilation terminated.
    make: *** [Makefile:390: main.o] Error 1
    

    Tout aussi logique, le hello.h n’est pas trouvable dans le dossier src.

    Nous devons à nouveau modifier le Makefile.am

    // Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = src/main.c src/hello.c
    hello_CPPFLAGS= -I include
    

    Le CPPFLAGS indique un argument passé au préprocesseur de compilation, en très gros le système qui fait de la copie de lignes avant compilation, même si c’est carrément plus complexe que ça dans la réalité :p

    On vient lui dire de rajouter le dossier include/ à son “PATH” de compilation.

    $ ./configure
    Makefile.am:2: warning: source file 'src/main.c' is in a subdirectory,
    Makefile.am:2: but option 'subdir-objects' is disabled
    automake: warning: possible forward-incompatibility.
    automake: At least a source file is in a subdirectory, but the 'subdir-objects'
    automake: automake option hasn't been enabled.  For now, the corresponding output
    automake: object file(s) will be placed in the top-level directory.  However,
    automake: this behaviour will change in future Automake versions: they will
    automake: unconditionally cause object files to be placed in the same subdirectory
    automake: of the corresponding sources.
    automake: You are advised to start using 'subdir-objects' option throughout your
    automake: project, to avoid future incompatibilities.
    Makefile.am:2: warning: source file 'src/hello.c' is in a subdirectory,
    Makefile.am:2: but option 'subdir-objects' is disabled
    
    $ make
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.  -I includes   -g -O2 -MT hello-main.o -MD -MP -MF .deps/hello-main.Tpo -c -o hello-main.o `test -f 'src/main.c' || echo './'`src/main.c
    mv -f .deps/hello-main.Tpo .deps/hello-main.Po
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.  -I includes   -g -O2 -MT hello-hello.o -MD -MP -MF .deps/hello-hello.Tpo -c -o hello-hello.o `test -f 'src/hello.c' || echo './'`src/hello.c
    mv -f .deps/hello-hello.Tpo .deps/hello-hello.Po
    gcc  -g -O2   -o hello hello-main.o hello-hello.o 
    

    Cela compile mais il n’est pas d’accord.

    Il n’aime pas les sous dossiers.

    Nous allons l’aider.

    Première transformation

    // Makefile.am
    SUBDIRS =  src
    

    Ensuite on créé un deuxième Makefile.am dans le dossier src.

    // src/Makefile.am
    bin_PROGRAMS = hello
    hello_SOURCES = main.c hello.c
    hello_CPPFLAGS= -I $(top_srcdir)/include
    

    Le $(top_srcdir) est très important pour se référer à la racine du projet

    Puis on modifie le configure.ac pour lui rajouter le nouveau Makefile

    // configure.ac
    AC_INIT([hello], [1.0])
    AC_CONFIG_AUX_DIR([build]) 
    AM_INIT_AUTOMAKE([foreign])
    /// Plus de rebuild !
    AM_MAINTAINER_MODE([disable])
    AC_PROG_CC
    AC_CONFIG_FILES([Makefile src/Makefile])
    AC_OUTPUT
    

    Après moulinette et nettoyage des fichiers inutiles

    $ autoreconf -fi
    $ ./configure --prefix /home/data
    

    cela donne

    $ tree
    .
    ├── Makefile
    ├── Makefile.in
    ├── build
    │   ├── compile
    │   ├── depcomp
    │   ├── install-sh
    │   └── missing
    ├── config.log
    ├── config.status
    ├── configure
    ├── include
    │   └── hello.h
    └── src
        ├── Makefile
        ├── Makefile.in
        ├── hello.c
        └── main.c
    

    On peut make et make install

    $ make
    Making all in src
    make[1]: Entering directory '/workspaces/nix-hello/user/run/src'
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.  -I ../include   -g -O2 -MT hello-main.o -MD -MP -MF .deps/hello-main.Tpo -c -o hello-main.o `test -f 'main.c' || echo './'`main.c
    mv -f .deps/hello-main.Tpo .deps/hello-main.Po
    gcc -DPACKAGE_NAME=\"hello\" -DPACKAGE_TARNAME=\"hello\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"hello\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I.  -I ../include   -g -O2 -MT hello-hello.o -MD -MP -MF .deps/hello-hello.Tpo -c -o hello-hello.o `test -f 'hello.c' || echo './'`hello.c
    mv -f .deps/hello-hello.Tpo .deps/hello-hello.Po
    gcc  -g -O2   -o hello hello-main.o hello-hello.o  
    make[1]: Leaving directory '/workspaces/nix-hello/user/run/src'
    make[1]: Entering directory '/workspaces/nix-hello/user/run'
    make[1]: Nothing to be done for 'all-am'.
    make[1]: Leaving directory '/workspaces/nix-hello/user/run'
    
    $ make install
    Making install in src
    make[1]: Entering directory '/workspaces/nix-hello/user/run/src'
    make[2]: Entering directory '/workspaces/nix-hello/user/run/src'
     /usr/bin/mkdir -p '/home/data/bin'
      /usr/bin/install -c hello '/home/data/bin'
    make[2]: Nothing to be done for 'install-data-am'.
    make[2]: Leaving directory '/workspaces/nix-hello/user/run/src'
    make[1]: Leaving directory '/workspaces/nix-hello/user/run/src'
    make[1]: Entering directory '/workspaces/nix-hello/user/run'
    make[2]: Entering directory '/workspaces/nix-hello/user/run'
    make[2]: Nothing to be done for 'install-exec-am'.
    make[2]: Nothing to be done for 'install-data-am'.
    make[2]: Leaving directory '/workspaces/nix-hello/user/run'
    make[1]: Leaving directory '/workspaces/nix-hello/user/run'
    
    $ /home/data/bin/hello 
    Hello world!
    

    Fini !!!

    Un dernier graph récapitulatif:

      graph TD
    
        subgraph packaging
        A[configure.ac] -->|utilisé par| B(autoconf)
        A -->|utilisé par| E
        D[Makefile.am] -->|utilisé par| E(automake)
        I(aclocal) -->|génère| J[aclocal.m4]
        J -->|utilisé par| E
    
        end
    
        subgraph Y[_______configuration]
        E -->|génère| F[Makefile.in]
        B -->|génère| C
        F -->|utilisé par| C
        F[Makefile.in]
        F -->|est utilisé par| H
        C -->|génère| H[config.status]
        E -->|génère| K
        
        
        C[./configure]
        end
    
        subgraph build
        G([Makefile]) -->|utilisé par| O(make) 
        M[ src/*.c ] -->|utilisé par| O
        N[ include/*.h ] -->|utilisé par| O
        K[[ build/ ]] -->|utilisé par| O
        H -->|génère| G
        O -->|génère| L{/bin/hello}
        end
    

    Conclusion

    J’espère que cette petite visité archéologique vous a plu ^^

    Je ne dirai pas que j’utiliserai autotools pour mes projets, mais c’était amusant de démêler le vrai du faux et de comprendre la philosophie derrière tous ces outils 😀

    Merci de votre lecture et à la prochaine ❤️