https://lafor.ge/feed.xml

Introduction à Autotools et m4

2023-04-26

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!

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

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

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:

  • name : nom d'appel de la macro
  • content : contenu à remplacer
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.

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:

  • AC_INIT son rôle est de générer le bash de ./configure, il prend deux paramètres obligatoires:
    • nom
    • version
  • AC_OUTPUT il génère les lignes permettant la génération du fichier config.status que l'on verra par la suite

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.

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?

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.

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
  • Le prefix est le chemin de ce qui va être produit,
  • Le IDENTIFIER définit ce qui va être produit

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
  • Le hello devient le prefix, la cible de ce que l'on va produire.
  • Ce sont des sources qui vont le produire donc SOURCES
  • C'est sources proviennent de "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 ❤️

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.