Introduction à Autotools et m4
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 :
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
void
Pour le construire, nous utilisons un logiciel appelé compilateur, ici gcc.
Ceci construit un exécutable qui peut être lancé:
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
void
// hello.h
const char* ;
// hello.c
const char*
Et pour compiler:
Trois fichiers:
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
:
Une des règles par défaut est le all
.
Il permet de s'appeler par
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:
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:
)
)
Son utilisation est extrêment simple.
Pour un fichier
// file.m4
Je suis un fichier M4
Si on exécute:
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:
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
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:
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
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.
)
#! /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
)
Oh ! du nouveau !
$ tree -L 1
.
├── autom4te.cache
├── config.log
├── config.status
├── configure
└── configure.ac
Encore un nouveau fichier.
#! /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.
Il nous sort tout le roman de sa conception et une ligne qui m'intéresse
with options ""
Donc maintenant si je fais
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.
)
)
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
void
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
void
// hello.h
const char* ;
// hello.c
const char*
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 ❤️
Ce travail est sous licence CC BY-NC-SA 4.0.