GNU Autotools - Aperçu

Ce texte présente...

Ce texte ne présente pas...

Avertissement

Je tiens à mentionner au préalable que je ne suis pas du tout un expert des autotools et que je n'ai pas non plus du tout une grande expérience de ces outils. J'ai écrit ce document dans le but de donner une idée des nombreuses possibilités qu'ils offrent, et de donner envie aux personnes qui les découvrent de les utiliser. Il s'agit donc plus de faire partager l'expérience des autotools que j'ai eu dans le cadre de la diffusion de mon projet open-source, et de la compilation de références utiles à ceux qui voudront approfondir le sujet.

Table des matières

1. Introduction

Les Autotools, aussi appelés 'build system', sont des outils fournis par le projet GNU pour faciliter la fabrication de paquets à partir du code source d'un programme. Ils visent également à assurer la portabilité du paquet ainsi construit, permettant de l'installer sur une grande variété de plate-formes UNIX différentes. Lorsqu'on télécharge un paquet contenant les sources d'un logiciel depuis internet, il est très souvent indiqué de taper la succession suivante d'instructions pour construire le binaire du logiciel:

./configure
make
make install

Cette facilité d'installation est rendue possible grâce à la chaîne d'outils Autotools, dont je présente ici quelques-uns des maillons, ainsi qu'un exemple d'utilisation de ceux-ci. À la fin de ce texte pourront également être trouvées quelques références permettant d'approfondir les points non abordés ici.

2. La famille des Autotools

Les Autotools se composent principalement des outils suivants:

Remarque : D'autres logiciels GNU sont fréquemment utilisés conjointement aux Autotools, comme make, gettext, pkg-config, ou gcc, mais ils ne seront pas abordés ici.

2.1 Autoconf

Autoconf va permettre de construire le script configure. Pour ce faire, il se base sur le fichier configure.in ou configure.ac que l'éditeur du logiciel écrit lui-même, pour produire le script configure. Nous verrons comment construire un exemple simple de fichier configure.ac dans la partie "les fichiers indispensables". Le script configure est utile pour obtenir de nombreuses informations sur le système cible, et va permettre de répondre à des questions comme :

Pour résumer, il s'agit donc de déterminer avec précision l'environnement sur lequel on veut installer notre logiciel, afin d'adapter la compilation à ce nouveau système. De nombreuses variables d'environnement seront d'ailleurs fixées suite au lancement du script configure. Toutes les caractéristiques du système que le script aura pu détecter se trouvent dans le fichier config.log, dont un exemple se trouve en annexe 1 (les quelques premières lignes uniquement, le fichier complet faisant au minimum un millier de lignes...).

2.2 Automake

Automake est utilisé pour créer des Makefile aussi portables que possible. De la même manière que pour autoconf, automake a besoin d'un fichier Makefile.am dans lequel on doit spécifier les fichiers sources du projet, le binaire que l'on veut générer... Ce fichier Makefile.am sera ensuite transformé par automake en Makefile.in, qui servira enfin à générer le Makefile final. Les problématiques abordées par automake sont les suivantes :

2.3 Libtool

Libtool est quand à lui l'outil permettant de construire une bibliothèque statique ou dynamique. Mon expérience de cet outil n'est pas assez grande pour détailler son utilisation ici, et je renvoie aux liens indiqués en fin de ce document pour plus de détails sur libtool.

3. Le pour et le contre

3.1 Les inconvénients des Autotools

J'ai rencontré peu d'inconvénients dans l'utilisation des autotools, mais on peut tout de même citer:

3.2 Les avantages des Autotools

Les autotools offrent des avantages non négligeables :

4. Un exemple concret

Je présente dans cette partie l'utilisation que j'ai faite des autotools dans le cadre de mon projet open-source calcurse.

4.1 L'arborescence du projet

Lorsque l'on fait un checkout du projet depuis le dépôt cvs, on obtient les fichiers suivants à la racine du répertoire:

AUTHORS
NEWS
README
TODO
ChangeLog
Makefile.am
configure.ac
autogen.sh

ainsi que les trois répertoires suivants:

src/
po/
doc/

Les fichiers NEWS, README, AUTHORS et ChangeLog sont obligatoires dans les paquets se conformant aux normes GNU. Les fichiers configure.ac et Makefile.am sont ceux relatifs aux autotools, et je les décris plus en détails dans la partie suivante. Pour autogen.sh, c'est un script que j'ai écrit et qui lance automatiquement le mécanisme des autotools pour construire le script configure. Cela évite que ce gros script soit stocké inutilement dans le dépôt cvs. Je présente plus en détails autogen.sh dans la partie utilisation avec cvs.

Pour ce qui est des répertoires, src/ contient les .c et .h, mais aussi la manpage calcurse.1 et le script Makefile.am, que nous verrons ci-dessous. Le répertoire po/ contient les fichiers .po, utilisés pour la traduction du logiciel, par l'intermédiaire de l'API gettext. Le répertoire doc/ contient la documentation au format html.

4.2 Les fichiers indispensables

Pour faire marcher les autotools, nous n'avons besoin que des trois fichiers suivants:

Voici le code du fichier configure.ac qui se trouve à la racine:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
#
#	$calcurse: configure.ac,v 1.8 2007/01/16 10:55:43 culot Exp $

AC_PREREQ(2.59)
AC_INIT(calcurse, 1.7, frederic@monsite.org)
AM_INIT_AUTOMAKE
AM_GNU_GETTEXT([external])
AC_CONFIG_SRCDIR([src/calcurse.c])
AC_CONFIG_HEADER([config.h])

# Checks for programs.
AC_PROG_CC

# Checks for ncurses.
AC_CHECK_HEADERS([ncurses.h], [
    AC_CHECK_LIB(ncurses, initscr, [
	LIBS="$LIBS -lncurses"
	AC_DEFINE(HAVE_LIBNCURSES, 1, [Define to 1 if you have the 'ncurses' library (-lncurses).])
    ], AC_MSG_ERROR(The ncurses library is required in order to build the program!))
], AC_MSG_ERROR(The ncurses header is required in order to build the program!))

# Checks for pthread.
AC_CHECK_HEADERS([pthread.h], [
    AC_CHECK_LIB(pthread, pthread_create, [
	LIBS="$LIBS -lpthread"
	AC_DEFINE(HAVE_LIBNCURSES, 1, [Define to 1 if you have the 'pthread' library (-lpthread).])
    ], AC_MSG_ERROR(The pthread library is required in order to build the program!))
], AC_MSG_ERROR(The pthread header is required in order to build the program!))

# Checks for header files.
AC_HEADER_STDC
AC_CHECK_HEADERS([stdlib.h string.h sys/time.h time.h stdio.h unistd.h getopt.h \
sys/types.h])

# Checks for typedefs, structures, and compiler characteristics.
AC_C_CONST
AC_STRUCT_TM
AC_HEADER_STDBOOL

# Checks for library functions.
AC_FUNC_MALLOC
AC_FUNC_MKTIME
AC_FUNC_STRFTIME
AC_CHECK_FUNCS([floor mkdir strchr])

AC_OUTPUT(Makefile src/Makefile po/Makefile.in po/Makefile)

Le configure.ac contient les différents tests qu'il faudra effectuer pour vérifier la présence des librairies nécessaires à la compilation. Ce script assez long peut être généré facilement en utilisant la commande autoscan. Autoscan va examiner les sources du programme à la recherche des bibliothèques qui sont utilisées, et créer un fichier configure.scan. Il faut ensuite vérifier ce configure.scan pour être sûr que toutes les bibliothèques présentes sont nécessaires, puis renommer simplement le fichier en configure.ac.

Le deuxième fichier obligatoire qui se trouve à la racine est Makefile.am. En voici le source:

#	$calcurse: Makefile.am,v 1.2 2006/09/09 20:21:16 culot Exp $

AUTOMAKE_OPTIONS= gnu 
SUBDIRS = po src
EXTRA_DIST = ABOUT-NLS \
		doc/manual_en.html \
		doc/manual_fr.html \
		doc/manual_de.html \
		doc/manual_es.html

ACLOCAL_AMFLAGS = -I m4

Ce fichier est plus simple que le précédent, et contient des informations sur les répertoires importants du projet dans lequel on voudra mettre un Makefile. Il contient également les fichiers annexes que l'on veut rajouter dans le paquet final (ici la documentation html), et le style du paquet que l'on veut créer. Ici il s'agit d'un paquet type gnu, mais il existe d'autres types comme gnits ou foreign. Cela permet de gérer la conformité par rapport aux standards préconisés par GNU.

Le dernier fichier important est le Makefile.am contenu dans le répertoire src/, qui se présente comme suit:

#	$calcurse: Makefile.am,v 1.3 2006/09/22 13:12:06 culot Exp $

AUTOMAKE_OPTIONS= gnu 
bin_PROGRAMS= calcurse
calcurse_SOURCES= calcurse.c apoint.c event.c todo.c utils.c\
			calendar.c vars.c io.c help.c custom.c args.c\
			day.c recur.c notify.c\
			apoint.h event.h todo.h utils.h calendar.h\
			vars.h io.h help.h custom.h args.h i18n.h\
			day.h recur.h notify.h
LIBS= -lncurses -lpthread -lm
LDADD= @LTLIBINTL@
datadir= @datadir@
localedir= $(datadir)/locale
DEFS= -DLOCALEDIR=\"$(localedir)\" @DEFS@
man_MANS= calcurse.1
EXTRA_DIST= calcurse.1

Dans ce fichier on retrouve le nom de l'exécutable à produire, la liste des fichiers sources, les librairies nécessaires, et la liste des pages de manuel à inclure dans le paquet.

Une fois que l'on a ces trois fichiers indispensables, il ne reste plus qu'à lancer la succession des scripts des autotools pour construire un bôôô paquet pour son projet ;)

4.3 L'enchainement des commandes

Pour construire notre script configure, voici l'enchainement des commandes à effectuer:

aclocal -I m4
autoheader
automake --gnu --copy --add-missing
autoconf

aclocal permet de créer le fichier aclocal.m4, qui contient des macros m4 (langage de macros utilisé par les autotools) pour effectuer les tests de présence des librairies et autres. aclocal se base sur ce que l'on a écrit dans configure.ac pour générer ses macros de test.

autoheader génère un fichier config.h.in contenant des symboles pour le pré-processeur. Ce sont les #define présentant toutes les options possibles pour le logiciel (par exemple si on peut avoir l'internationalisation, on aura un #undef ENABLE_NLS). Les options sont inactives par défaut, et le script configure va générer un fichier config.h contenant la même liste de symboles, activés ou non suivant le choix de l'installateur du logiciel. Pour reprendre l'exemple précédent, si configure est lancé avec l'option --enable-nls, on retrouvera dans le fichier config.h généré le symbole: #define ENABLE_NLS 1. J'ai reporté en annexe un bout de ce fichier config.h, pour avoir une idée des différentes variables possibles (c'est dedans qu'est défini par exemple le numéro de version, que l'on peut réutiliser ensuite dans tout le programme, et qui avait été fixé dans configure.ac par l'instruction: AC_INIT(calcurse, 1.7, frederic@monsite.org)).

automake permet de générer le fichier Makefile.in à partir de Makefile.am. Ce fichier Makefile.in sera transformé en Makefile par la suite, lors de l'exécution de configure. Les options passées en argument permettent de se conformer aux normes GNU (--gnu), c'est-à-dire qu'il vérifie la présence des fichiers README, AUTHORS, NEWS et ChangeLog. Cela permet aussi de rajouter d'autres fichiers annexes qui pourraient manquer (--add-missing), et de les copier en dur (--copy) au lieu de simplement créer des liens symboliques.

Remarque : Cette étape nécessite l'utilisation de perl.

On termine enfin avec autoconf, qui va nous permettre d'obtenir notre script configure. C'est ce script que lancera l'utilisateur, et qui produira le Makefile sur base du Makefile.in.

Un petit dessin issu de Wikipedia (crédits: Dominik Menke) pour résumer le processus, à l'attention de ceux près du radiateur qui ont roupillé pendant l'explication:

autotools

4.4 Utilisation avec cvs

Le script configure généré grâce aux autotools est énorme (300k, plus gros que le binaire du projet lui-même...). Surtout que l'exécution des scripts autotools génère également d'autres fichiers annexes (Makefile.in, config.*,...). Il est donc dommage de stocker tout ça avec le reste dans le dépôt cvs, surtout que l'on peut regénérer l'ensemble simplement en utilisant la succession de commandes présentées au paragraphe précédent. Par contre, je me suis rendu compte à l'usage que cela devenait vite fastidieux, et je n'ai toujours pas réussi à me souvenir de l'ordre dans lequel on doit lancer les différents scripts. Je me suis donc fait un petit code pour regénérer automatiquement configure. C'est ce code que je stocke dans le dépôt cvs, et il prend beaucoup moins de place. J'ai reporté en annexe ce script de regénération automatique, que j'ai appelé autogen.sh (on retrouve cette démarche dans pas mal d'autres projets, où ce fichier peut être également appelé bootsrap.sh ou build.sh suivant l'inspiration de son auteur).

5. Ressources

[1] http://www.gnu.org/software/autoconf/manual/
Enorme manuel d'Autoconf, dans plusieurs format différents.

[2] http://www.gnu.org/software/automake/manual/
Manuel d'Automake

[3] http://www.gnu.org/software/libtool/
Page du projet Libtool

[4] Linux Magazine - Hors série Les Dossiers numéro 1
Janvier/Février/Mars 2004 - pages 50 à 55
Article expliquant la création d'une bibliothèque avec Libtool

6. Annexes

Extrait du fichier config.log

This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.

It was created by calcurse configure 1.7, which was
generated by GNU Autoconf 2.59.  Invocation command line was

  $ ./configure CFLAGS=-Wall -g --prefix=/home/user --with-libiconv-prefix=/usr/local 
			--with-libintl-prefix=/usr/local --no-create --no-recursion

## --------- ##
## Platform. ##
## --------- ##

hostname = puffy.my.domain
uname -m = i386
uname -r = 4.0
uname -s = OpenBSD
uname -v = GENERIC#1107

/usr/bin/uname -p = Intel(R) Celeron(R) M processor 1.30GHz ("GenuineIntel" 686-class)
/bin/uname -X     = unknown

/bin/arch              = unknown
/usr/bin/arch -k       = OpenBSD.i386
/usr/convex/getsysinfo = unknown
hostinfo               = unknown
/bin/machine           = unknown
/usr/bin/oslevel       = unknown
/bin/universe          = unknown

PATH: /sbin
PATH: /usr/sbin
PATH: /usr/local/sbin
PATH: /bin
PATH: /usr/bin
PATH: /usr/local/bin
PATH: /usr/X11R6/bin
PATH: /usr/games
PATH: /home/user/bin
PATH: .


## ----------- ##
## Core tests. ##
## ----------- ##

configure:1367: checking for a BSD-compatible install
configure:1422: result: /usr/bin/install -c
configure:1433: checking whether build environment is sane
configure:1476: result: yes
configure:1541: checking for gawk
configure:1570: result: no
configure:1541: checking for mawk
configure:1570: result: no
configure:1541: checking for nawk
configure:1557: found /usr/bin/nawk
configure:1567: result: nawk
configure:1577: checking whether make sets $(MAKE)
configure:1597: result: yes

../..

Extrait du fichier config.h

/* config.h.  Generated by configure.  */
/* config.h.in.  Generated from configure.ac by autoheader.  */

/* Define to 1 if translation of program messages to the user's native
   language is requested. */
#define ENABLE_NLS 1

/* Define to 1 if you have the MacOS X function CFLocaleCopyCurrent in the
   CoreFoundation framework. */
/* #undef HAVE_CFLOCALECOPYCURRENT */

/* Define to 1 if you have the `floor' function. */
/* #undef HAVE_FLOOR */

/* Define to 1 if you have the <getopt.h> header file. */
#define HAVE_GETOPT_H 1

/* Define if the GNU gettext() function is already present or preinstalled. */
#define HAVE_GETTEXT 1

/* Define to 1 if your system has a GNU libc compatible `malloc' function, and
   to 0 otherwise. */
#define HAVE_MALLOC 1

/* Define to 1 if you have the <ncurses.h> header file. */
#define HAVE_NCURSES_H 1

/* Define to 1 if you have the <pthread.h> header file. */
#define HAVE_PTHREAD_H 1

/* Define to 1 if stdbool.h conforms to C99. */
#define HAVE_STDBOOL_H 1

/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1

/* Define to 1 if you have the <stdio.h> header file. */
#define HAVE_STDIO_H 1

/* Name of package */
#define PACKAGE "calcurse"

/* Define to the address where bug reports for this package should be sent. */
#define PACKAGE_BUGREPORT "frederic@monsite.org"

/* Define to the full name of this package. */
#define PACKAGE_NAME "calcurse"

/* Define to the full name and version of this package. */
#define PACKAGE_STRING "calcurse 1.7"

/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "calcurse"

/* Define to the version of this package. */
#define PACKAGE_VERSION "1.7"

/* Version number of package */
#define VERSION "1.7"

Script autogen.sh

#!/bin/sh
#
# Copyright (c) 2004-2006 Frederic Culot
#                                                                      
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#                                                                      
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#                                                                      
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software       
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#                                                                       
# Send your feedback or comments to : calcurse@culot.org
# Calcurse home page : http://culot.org/calcurse
#
#        $calcurse: autogen.sh,v 1.1.1.1 2006/07/31 21:00:02 culot Exp $
#
#
# autogen.sh - Generates all the necessary files to build calcurse from
# cvs tree.
#

PKG_NAME=calcurse
AC_VERSION="2 59"
AUTOCONF_VERSION=2.59
AC_FLAGS=
AM_VERSION="1 9"
AUTOMAKE_VERSION=1.9
AM_FLAGS="--gnu --copy --add-missing"
GETTEXT_VERSION="0 14"
GETTEXT_FLAGS="--copy --no-changelog"
ACLOCAL_FLAGS="-I m4"
SRCDIR=`dirname $0`
test -z "$SRCDIR" && SRCDIR=.
export AUTOMAKE_VERSION AUTOCONF_VERSION

# Function to check if we are at the top level of calcurse package.
check_directory_level()
{
        (test -f $SRCDIR/configure.ac) || {
                printf "\n\n**Error**: Directory "\`$SRCDIR\'" does not appear to"
                printf "\nbe the top-level $PKG_NAME directory.\n"
                exit 1
        }
}

# Clean previous files before running scripts
clean_old_files()
{
        printf "Cleaning old files ... "
        rm -rf configure config.log aclocal.m4 \
                config.status config autom4te.cache \
		po/Makefile.in.in
        printf "done\n"
}

# Clean useless backup files
clean_backup_files()
{
	printf "Cleaning backup files ... "
	rm -rf configure.ac\~ Makefile.am\~
	printf "done\n"
}

# Function to check for a program availability
check_program()
{
        PROGRAM=$1
        printf "Checking for $PROGRAM ... "
        ($PROGRAM --version) < /dev/null > /dev/null 2>&1 || {
                printf "\n\n**Error**: You must have $PROGRAM installed."
                printf "\nDownload the appropriate package for your distribution,"
                printf "\nor get the source tarball at ftp://ftp.gnu.org/pub/gnu/"
		printf "\n"
                exit 1
        }
	FOUND=`which $PROGRAM`
	printf "$FOUND\n"
}

# Function to check a program's version
# (there must be a better way, but I am not good at sed...)
check_program_version()
{
        PROGRAM=$1; MAJOR=$2; MINOR=$3
        printf "Checking that $PROGRAM version is at least $MAJOR.$MINOR ... "
        VERSION=`$PROGRAM --version |head -n 1|sed 's/([^)]*)//g;s/^[a-zA-Z\.\ \
                \-]*//;s/ .*$//'`                
        MAJOR_FOUND=`echo $VERSION | cut -d. -f1`
        MINOR_FOUND=`echo $VERSION | sed s/[-,a-z,A-Z].*// | cut -d. -f2`
        [ -z "$MINOR_FOUND" ] && MINOR_FOUND=0
        
        WRONG=
        if [ -z "$MAJOR_FOUND" -lt "$MAJOR" ]; then
                WRONG=1
        elif [ "$MAJOR_FOUND" -eq "$MAJOR" ]; then
                if [ "$MINOR_FOUND" -lt "$MINOR" ]; then
                        WRONG=1
                fi
        fi        
        if [ ! -z "$WRONG" ]; then
                printf "\n\n**Error**: found version $MAJOR_FOUND.$MINOR_FOUND,"
                printf "\nwhich is too old. You should upgrade $PROGRAM."
                printf "\nDownload the appropriate package for your distribution,"
                printf "\nor get the source tarball at ftp://ftp.gnu.org/pub/gnu/"
		printf "\n"
                exit 1
        else
                printf "OK, found $MAJOR_FOUND.$MINOR_FOUND\n"
        fi
}

# Dirty hack to run gettextize: problem is that it demands to
# press Return no matter what... This gets rid of that demand. 
run_gettext()
{
	PROGRAM=gettextize
	printf "Running $PROGRAM $GETTEXT_FLAGS ... "
	sed 's:read .*< /dev/tty::' `which $PROGRAM` > my-gettextize
	chmod +x my-gettextize
	(printf "\n" | ./my-gettextize $GETTEXT_FLAGS > /dev/null 2>&1) || {
		printf "\n\n**Error**: $PROGRAM failed.\n"
		exit 1
	}

	# now restore the files modified by gettextize 
	(test -f configure.ac~) && mv -f configure.ac~ configure.ac
	(test -f Makefile.am~)  && mv -f Makefile.am~  Makefile.am
	mv -f po/Makevars.template po/Makevars
	rm my-gettextize
	printf "OK\n"
}

# Function to run a program
run_program()
{
        PROGRAM=$1        
        shift
        PROGRAM_FLAGS=$@
        printf "Running $PROGRAM $PROGRAM_FLAGS ... "
        $PROGRAM $PROGRAM_FLAGS > /dev/null 2>&1 || {
                printf "\n\n**Error**: $PROGRAM failed.\n"
                exit 1
        }
	printf "OK\n"
}

# Main

echo " --- $PKG_NAME autogen script ---\n"
check_directory_level
clean_old_files
check_program gettext
check_program_version gettext $GETTEXT_VERSION
check_program aclocal
check_program autoheader
check_program automake
check_program_version automake $AM_VERSION
check_program autoconf
check_program_version autoconf $AC_VERSION
run_gettext 
run_program aclocal $ACLOCAL_FLAGS
run_program autoheader
run_program automake $AM_FLAGS
run_program autoconf $AC_FLAGS
clean_backup_files
printf "\nYou can now run the configure script to obtain $PKG_NAME Makefile.\n"