Алексей Махоткин

домашняя страница

Autoconf

Copyright (c) Алексей Махоткин 2001
Впервые эта статья была напечатана в журнале "Программист", N10, 2001 год

Запрещается перепечатка без письменного разрешения автора.

GNU Autotools – инфраструктура для сборки программ: Autoconf

Введение

Пакет Autoconf служит двум основным целям: поддерживает написание переносимых программ, а также позволяет включать или выключать дополнительные возможности пакета (например, пользователь может захотеть собрать программу с поддержкой компрессии файлов или без).

Переносимые программы

Перед каждым программистом, пишущим программу под какой-то из вариантов UNIX’а, в той или иной степени встает проблема сборки этой программы под другими вариантами UNIX’а. По историческим причинам UNIX-совместимые операционные системы довольно сильно отличаются друг от друга, и приходится прикладывать некоторые, порою ощутимые усилия, для обеспечения переносимости. Autoconf обеспечивает инфраструктуру, облегчающую эту задачу.

Вообще, на тему переноса программ на различные UNIX’ы можно написать целую книгу, а размер этой статьи не столь велик, поэтому я только вкратце затрону этот вопрос.

Теория

Общее правило – не следует чрезмерно и без нужды увлекаться вопросами переносимости. То бишь, с самого начала достаточно не писать кода, который, очевидно, будет работать только на машине, на которой он был скомпилирован: например, код, считающий, что младший байт в слове находится перед старшим, или же код с ассемблерными вставками, или же использование жестко привязанных к системе заголовочных файлов, например #include <linux/icmp.h> (их, конечно же, можно использовать, обернув в соответствующие #ifdef, но об этом ниже). Если не допускать подобных глупостей, то работа по переносу значительно облегчится (а если допускать, да еще делать это без предупреждения в каком-нибудь глубоко спрятанном месте программы, то перенос может превратиться в пытку).

Теперь нужно проверить (и исправить, если нужно) сборку и работоспособность программы на “основных” вариантах UNIX: в данный момент, по моему мнению, это Linux и FreeBSD для свободно-распространяемых, и Solaris – для коммерческих. Работу на остальных вариантах UNIX нужно исправлять только если а) у вас есть к ним непосредственный доступ и немного свободного времени; б) ваши пользователи работают именно с этим UNIX’ом; в) кто-то, кто пользуется этим UNIX’ом, прислал вам исправления. В процессе жизни вашей программы она рано или поздно будет перенесена на все UNIX’ы, которые действительно имеют значение, а до этого беспокоиться о переносе совершенно не следует.

В документации на Autoconf есть целая глава “Существующие тесты” (Existing tests), в которой описано множество вариантов специфических заголовочных файлов, библиотек, функций, типов данных и прочего, которые можно проверить с помощью стандартных макросов Autoconf. Помимо этого, с помощью макроса AC_CHECK_HEADER() можно проверить существование заголовочного файла, с помощью AC_CHECK_LIB() можно проверить существование библиотеки, с помощью AC_CHECK_FUNC() – существование функции. Кроме того, там же можно прочитать о различных возможностях компилятора C, которые существуют не на всякой платформе, и о многом другом.

Практика

Каждый ваш исходный файл должен содержать в начале (_до_ всех остальных директив #include) директиву препроцессора

#include "config.h"

Для создания этого файла нужно, во-первых, вписать в configure.in, ближе к началу, строчку

AM_CONFIG_HEADER(config.h)

а во-вторых, запустить программу autoheader, которая обработает configure.in и создаст файл config.h.in. Рекомендуется всегда использовать именно такое имя этого файла, и никакое другое, потому что так привычнее и понятнее. Если вы пользуетесь системой контроля версий, то добавьте в нее только файл config.h.in, и ни в коем случае не добавляйте файл config.h.

После того, как вы написали в своем скрипте configure.in все макросы, которые проверяют все возможности, используемые в программе, нужно обработать исходные файлы и “обернуть” все строки, в которых подключаются нестандартные заголовочные файлы, в директивы #ifdef/#endif, чтобы они подключались только на тех платформах, на которых их наличие было проверено скриптом configure и описано в файле config.h.

Не забывайте после внесения исправлений на предмет работы под очередным вариантом UNIX проверять, не сломалась ли компиляция под уже поддерживаемым вариантом.

Содержимое configure.in

Скрипт configure, который выполняется пользователем, представляет собой обычный скрипт на языке Bourne Shell. Этот скрипт генерируется из шаблона, configure.in, с помощью макропроцессора m4 (он похож на препроцессор языка C, но гораздо более мощный, и не завязан ни на один язык программирования). Вы, как разработчик пакета, редактируете, естественно, только шаблон, никогда не трогая сам файл configure. Скрипт configure генерируется из скрипта configure.in с помощью запуска программы autoconf.

Макросы

Внутри файла configure.in можно использовать специальные макросы, определенные пакетом Autoconf, вперемешку с обычным кодом на языке Bourne Shell. В предыдущей статье мы уже приводили пример минимального configure.in:

=== configure.in ===
AC_INIT([src/hello.c])
AM_INIT_AUTOMAKE([hello], [0.1])
AC_PROG_CC

AC_OUTPUT([Makefile src/Makefile])
=== configure.in ===

Названия макросов принято писать заглавными буквами. В данном примере используется четыре макроса: AC_INIT, AM_INIT_AUTOMAKE, AC_PROG_CC и AC_OUTPUT. Параметры макросов указаны в круглых скобках через запятую. Квадратные скобки фактически играют роль кавычек (m4 позволяет назначать кавычками любую последовательность символов).

Внутри макросов (мы рассматриваем только макросы, включенные в дистрибутив: написание своих макросов описано в документации к Autoconf) делается масса различных вещей: вызовы компилятора, поиски файлов, вызовы вспомогательных утилит.

Например, макрос AC_PROG_CC призван выяснить, какой компилятор C следует использовать. Если при вызове ./configure переменная окружения CC установлена в какое-то значение, то в качестве компилятора используется именно оно (например, можно задать компилятор с полным путем); в противном случае, проверяется, существует ли исполняемый файл ’gcc’, и может ли он скомпилировать тривиальную программу; если не может, то проверяет существование компилятора с именем ’cc’. Если все попытки закончились безуспешно, то пользователю сообщается об ошибке, и скрипт configure прекращает свою работу. Окончательно выясненное имя компилятора находится в переменной окружения CC.

Передача информации в Makefile

В качестве последнего шага скрипт configure вызывает специальный макрос AC_SUBST, передавая ему в качестве параметра имя этой переменной. Переменные, помеченные таким образом, называются ”выходными переменными” (output variables). В конце работы скрипт configure станет создавать файлы Makefile из соответствующих файлов Makefile.in. Предположим, что скрипт configure выяснил, что в качестве компилятора следует использовать программу ’gcc’ (то бишь, переменная окружения CC содержит значение ”gcc”), а внутри файла Makefile.in написано

    CC = @CC@

В результате при создании файла Makefile выходная переменная CC (записанная в виде ”@CC@”) будет заменена на соответствующее значение переменной окружения:

    CC = gcc

В файлах Makefile.in, которые Automake создает из Makefile.am, используется довольно много выходных переменных, в которых задаются имена множества программ, используемых для сборки вашего пакета. Вообще, выходные переменные – один из двух основных способов передачи информации о конфигурации в ваши файлы Makefile.

Обычно вам довольно редко потребуется использовать макрос AC_SUBST в ваших скриптах configure.in: чаще всего он будет вызываться неявно, из других стандартных макросов. Например, макрос AC_CHECK_PROG превращает свой первый аргумент в выходную переменную.

Второй, более распространенный способ влияния на сборку программы – задание переменных препроцессора с помощью макроса AC_DEFINE. Например, если вы хотите выяснить, установлена ли в системе библиотека zlib, чтобы использовать её в своей программе, то можете сделать это с помощью макроса AC_CHECK_LIB, а в параметре action-if-found вызвать макрос AC_DEFINE с параметром HAVE_ZLIB. Потом в вашей программе можно будет пользоваться директивой препроцессора #ifdef HAVE_ZLIB, чтобы включить или выключить те или иные куски кода.

Все переменные препроцессора, объявленные с помощью макроса AC_DEFINE, помещаются в файл config.h (который генерируется из файла config.h.in на этапе конфигурации пакета).

Практика

В оставшихся двух разделах мы рассмотрим практическое применение Autoconf на двух простейших, но хорошо иллюстрирующих технологию примерах.

Подключение сторонних библиотек

Предположим, вы хотите, чтобы если у пользователя в системе была установлена Zlib, то она использовалась бы для компрессии данных. Так как в нашей программе hello сложно придумать, что можно было бы компрессировать, то мы ограничимся тем, что будем выводить на экран дополнительное сообщение с версией этой библиотеки, пользуясь одной из её функций.

Zlib использует заголовочный файл zlib.h, библиотечный файл libz.a, а функция, возвращающая версию библиотеки, называется zlibVersion().

Чтобы на этапе конфигурации пакета определить наличие в системе заголовочного файла zlib.h, добавим в configure.in вызов макроса AC_CHECK_HEADERS(), а чтобы убедиться, что сами библиотечные файлы установлены, добавим макрос AC_CHECK_LIB().

Теперь наш файл configure.in выглядит так:

=== configure.in ===
AC_INIT([src/hello.c])
AM_INIT_AUTOMAKE([hello], [0.2])
AC_PROG_CC

AM_CONFIG_HEADER(config.h)

AC_CHECK_HEADERS(zlib.h)
AC_CHECK_LIB(z, zlibVersion)

AC_OUTPUT([Makefile src/Makefile])
=== configure.in ===

Запустим autoheader, чтобы обновить файл config.h.in, потом запустим autoconf, чтобы пересоздать скрипт configure. В файле config.h, который будет создан этим скриптом, объявлены два препроцессорных символа: HAVE_ZLIB_H, который определен, если присутствует файл zlib.h, и HAVE_LIBZ, который определен, если существует библиотека libz.

Воспользуемся функцией zlibVersion() в файле main.c, обернув её в директивы препроцессора:

=== main.c ===
#include "config.h"

#include "hello.h"

#ifdef HAVE_ZLIB_H
#include "zlib.h"
#endif

int main(void) {
        print_hello();

#ifdef HAVE_LIBZ
        printf("Zlib version %s detected\n", zlibVersion());
#else
        puts("Zlib not detected");
#endif

        exit(0);
}
=== main.c ===

Теперь можно запускать ./configure (скорее всего, на вашей машине стоят заголовочные файлы от Zlib и она сама. Если нет, то попробуйте без них, потом установите соответствующие пакеты и попробуйте еще раз):

    $ ./configure
    creating cache ./config.cache
    checking for a BSD compatible install... /usr/bin/install -c
    [ ... skipped ... ]
    checking for zlib.h... yes
    checking for zlibVersion in -lz... yes
    [ ... skipped ... ]
    creating config.h

И пересобираем нашу программу:

    $ make
    make  all-recursive
    make[1]: Entering directory `/home/alexm/autotools-ru/hello'
    Making all in src
    [ ... skipped ... ]
    gcc  -g -O2   -o hello  main.o hello.o  -lz 
    make[2]: Leaving directory `/home/alexm/autotools-ru/hello/src'

Как видите, библиотека libz автоматически оказалась добавлена в строку компиляции исполняемого файла, и его можно попробовать запустить:

    $ ./src/hello
    Hello, world!
    Zlib version 1.1.3 detected

Ура?

Замечание: скрипт configure во время работы создает кэш результатов конфигурации, который сильно ускоряет результаты его работы. Этот кэш находится в файле config.cache. К сожалению, в этом кэше могут остаться устаревшие данные. Например, если вы сначала выполните наш скрипт configure без библиотеки Zlib, а потом установите её на машину и выполните его еще раз, то он не обнаружит этой библиотеки, потому что в кэше записано, что такой библиотеки нет. Просто удалите этот кэш-файл, и перезапустите configure.

Включение дополнительных возможностей

Предположим, вы хотите, чтобы помимо традиционного сообщения ”Hello, world!” (а также информации о версии Zlib), ваша программа выводила бы какую-нибудь смешную фразу. При этом часть ваших пользователей угрюмы и не хотят веселиться, поэтому вы хотите, чтобы они сами решали, нужно ли им такое, и включали выдачу смешных фраз на этапе компиляции. Давайте добавим дополнительный ключ к скрипту configure (кстати, давно ли вы запускали ./configure –help?):


=== configure.in ===
AC_MSG_CHECKING([if funny messages are needed])
AC_ARG_WITH([funny-messages],
[  --with-funny-messages         Enable additional funny messages],
[ if test "$with_funny_messages" = "yes"; then 
     AC_DEFINE(ENABLE_FUNNY_MESSAGES)
     AC_MSG_RESULT([yes])
  else
     AC_MSG_RESULT([no])
  fi
], [AC_MSG_RESULT([no])])
=== configure.in ===

В этом кусочке кода нужно внимательно и аккуратно разобраться с кавычками. Здесь вызывается два макроса: AC_MSG_CHECKING(), который выдает первую часть сообщения ”Checking if funny messages are needed…”), и AC_ARG_WITH, который создает еще один ключ скрипта configure, а также задает реакцию на этот ключ.

У макроса AC_ARG_WITH четыре аргумента, заключенных в квадратные скобки. Первый аргумент, ”funny-messages”, задает краткое название возможности, которая будет включена или отключена с помощью этой опции. Второй аргумент задает краткую строку помощи, которая будет выдана, если запустить скрипт configure с ключом –help. Третий аргумент самый длинный, он даже занимает несколько строк:

  if test "$with_funny_messages" = "yes"; then 
     AC_DEFINE(ENABLE_FUNNY_MESSAGES)
     AC_MSG_RESULT([yes])
  else
     AC_MSG_RESULT([no])
  fi

Как видите, это – практически обычный код на языке Shell, который выполняется, если пользователь указал ключ в командной строке. С помощью переменной окружения $with_funny_messages можно выяснить, какой именно из разрешенных вариантов написания этого ключа был использован:

    $ ./configure --with-funny-messages
    $ ./configure --without-funny-messages
    $ ./configure --with-funny-messages=yes
    $ ./configure --with-funny-messages=no

В примере указан самый надежный способ определения, нужно ли включить или выключить соответствующую возможность. Как видите, в случае если смешные фразы следует включить, то мы определяем символ препроцессора ENABLE_FUNNY_MESSAGES и заканчиваем сообщение, которое выдал макрос AC_MSG_CHECKING(), словом ”yes”. Если смешные фразы включать не следует, то мы только добавляем ”no”, и больше ничего не делаем.

Наконец, последний, четвертый аргумент макроса AC_ARG_WITH позволяет задать действия, которые выполняются, если ключ не указан совсем. По умолчанию смешные сообщения не включены, поэтому мы просто говорим ”no” с помощью макроса AC_MSG_RESULT().

Для того, чтобы символ препроцессора ENABLE_FUNNY_MESSAGES попал в файл config.h.in, а затем в config.h, его нужно описать в файле acconfig.h в верхнем каталоге проекта:


=== acconfig.h ===
/* Define if you want to see funny messages.  */

#undef ENABLE_FUNNY_MESSAGES
=== acconfig.h ===

и выполнить autoheader. Готово.

Запускаем configure, разрешая компиляцию со смешными фразами:

    $ ./configure --with-funny-messages
    creating cache ./config.cache
    checking for a BSD compatible install... /usr/bin/install -c
    [ ... skipped ... ]
    checking if funny messages are needed... yes
    [ ... skipped ... ]
    creating config.h

Проверяем, что внутри созданного файла config.h нужный символ препроцессора теперь определен:

=== config.h ===
/* Define if you want to see funny messages.  */
#define ENABLE_FUNNY_MESSAGES 1
=== config.h ===

Осталось только добавить в main.c код для выдачи смешных сообщений, заключив его в соответствующие директивы препроцессора:

=== main.c ===
#ifdef ENABLE_FUNNY_MESSAGES

puts("Here goes a funny message");

#endif /* ENABLE_FUNNY_MESSAGES */
=== main.c ===

Скомпилируем и запустим программу:

    $ ./src/hello
    Hello, world!
    Zlib version 1.1.3 detected
    Here goes a funny message

Можно также убедиться, что будучи скомпилированной без ключа –with-funny-messages, наша программа говорит на одну строчку меньше:

    $ ./src/hello
    Hello, world!
    Zlib version 1.1.3 detected

Ура.

Замечание: во время работы скрипта configure отладочная информация о выполняемых действиях записывается в файл config.log. Анализируя этот файл, можно выяснять причины появления неожиданных результатов конфигурации, если они вдруг будут неожиданными.

Заключение

Активное использование Autoconf позволяет добиться большой гибкости при конфигурировании вашего программного пакета. После того, как вы стали использовать Automake вместо доморощенных файлов Makefile, следующий шаг – дополнительные ключи конфигурации и поддержка переносимости программы между различными системами UNIX.

В комплекте программы Autoconf есть исчерпывающая документация которая полностью покрывает все вопросы, не затронутые в этой короткой статье. Эта документация была переведена на русский язык Алексом Оттом, и свободно доступна в Интернете по адресу http://alexm.here.ru/autotools-ru/

Следующая статья из этой серии будет посвящена созданию разделяемых библиотек с помощью Libtool, небольшому, но полезному пакету Shtool, а также некоторым дополнительным вопросам использования пакетов из серии Autotools.

Ссылки

Comments