编译利器:大型项目如何使用Automake和Autoconf完成编译配置(ver+0.6)

这篇帖子很棒!我从百度文库中下载了 此教程,因为未做深度的考证,所以也不知原作者是谁,但在此谢谢作者。以下是作者原文,略有删改,本人有修改部分都已做出备注。


使用过开源 C/C++ 项目的同学们都知道,标准的编译过程已经变成了简单的三部曲:configure/make/make install, 使用起来很方便,不像平时自己写代码,要手写一堆复杂的 Makefile,而且换个编译环境,Makefile 还需要修改(Eclipse 也是这样)。

这么好的东东当然要拿来用了,但 GNU 的 Autotool 系列博大精深,工具数量又多,涉及的语言也多,要是自己从头看到尾,黄花菜都凉了,项目估计早就结束了;上网搜样例倒是有一大堆,但都是“hello world”的样例,离真正完成大型项目的目标还差得远。

没有办法,对照网上的样例,再找几个开源的源码,然后参考各种 Autotools 的手册,花了 2 天时间,终于完成了一个基本可用的 Autotools。为了避免其他 XDJM 也浪费时间,因此将过程总结下来,就算是新年礼物,送给大家!!

提纲挈领:使用 Autotools 其实很简单

大家不要看到那么多工具,其实使用起来很简单,总结起来就是两部分:

  1. 按照顺序调用各个工具;
  2. 修改或者添加 3 个文件;

整个操作顺序如下图:

流程图

niel:作者这幅图强调的是操作顺序,在表现文件依赖关系上力不从心,所以我又多配了一幅图。

依赖关系

听到我这么讲,大家是否觉得有信心了?好的,下面我们来看具体如何操作:

  1. 源码根目录调用 autoscan 脚本,生成 configure.scan 文件,然后将此文件重命名为 configure.ac
  2. 修改 configure.ac,利用 autoconf 提供的各种 M4 宏,配置项目需要的各种自动化探测项目
  3. 编写自定义宏,建议每个宏一个单独的 *.m4 文件;
  4. 调用 aclocal 收集 configure.ac 中用到的各种非 Autoconf 的宏,包括自定义宏;
  5. 调用 autoheader,扫描 configure.ac、acconfig.h(如果存在),生成 config.h.in 宏定义文件,里面主要是根据 configure.ac 中某些特定宏(如 AC_DEFINE)生成的 #define#undefine 宏,configure 在将根据实际的探测结果决定这些宏是否定义(具体见后面例子)。
  6. 按照 automake 规定的规则和项目的目录结构,编写一个或多个 Makefile.am(Makefile.am 数目和存放位置和源码目录结构相关),Makefile.am 主要写的就是编译的目标及其源码组成。
  7. 调用 automake,将每个 Makefile.am 转化成 Makefile.in,同时生成满足 GNU 编码规范的一系列文件(带 -a 选项自动添加缺少的文件,但有几个仍需要自己添加,在执行 automake 前需执行$ touch NEWS README AUTHORS ChangeLog)。如果 configure.ac 配置了使用 libtool(定义了 AC_PROG_LIBTOOL 宏(老版本)或 LT_INIT 宏),需要在此步骤前先在项目根目录执行 $ libtoolize --automake --copy --force,以生成 ltmain.sh,供 automake 和 config.status 调用。
  8. 调用 autoconf,利用 M4 解析 configure.ac,生成 shell 脚本 configure。以上几步完成后,开发者的工作就算完成了,后面的定制就由开源软件的用户根据需要给 configure 输入不同的参数来完成。
  9. 用户调用 configure,生成 Makefile,然后 $ make && make install

整个过程步骤有 9 步,但其中有 6 步你只需要简单的敲一个命令即可,只有剩下的三步需要你动手写一些东西,对应上面步骤中的蓝色黑体字部分,而本文的重点就是如何在大型项目中完成这三歩。

步步为营:三步完成编译配置

niel:关键的三步即:第2步-修改 configure.ac 文件、第3步-编写自定义的 Autoconf 宏、第6步-编写 Makefile.am 文件

修改configure.ac文件

从上面的步骤可以看到,使用 autoscan 工具扫描后就会生成一个简单的 configure.acconfigure.scan 文件,这已经是一个完整的 configure.ac 文件框架了,但还不足以达到我们的要求,因此我们要在框架里面添加一些东西:

  1. 添加 AM_INIT_AUTOMAKE

    AC_INIT 宏下一行添加 AM_INIT_AUTOMAKE([foreign -Wall -Werror]),中括号里面的选项可以根据需要来修改,具体请看 automake 手册 6.4.1 节 关于这个宏的说明。

    This macro can also be called in another, *deprecated *form: AM_INIT_AUTOMAKE(PACKAGE, VERSION, [NO-DEFINE]).This usage is mostly obsolete because the package and version can be obtained from Autoconf’s AC_INIT macro.

    Today, AM_INIT_AUTOMAKE is called with a single argument: a space-separated list of Automake options that should be applied to every Makefile.am in the tree. The effect is as if each option were listed in AUTOMAKE_OPTIONS.

  2. 如果需要,添加 AC_CONFIG_HEADERS([config.h])

    添加这个宏很简单,但关键是“如果需要”,什么情况下需要这个宏呢?

    这个宏的目的是输出 config.h,这是一个 C 的头文件,里面主要是包含很多宏定义 #define,说到这里其实就很明确了,输出这个文件的目的就是提供各种相关的宏,而宏在代码中的作用就是 #ifdef,也就是说:如果你的代码需要用到宏开关进行控制,那么就要输出这个文件。具体的使用方法如下:

    1. 首先确定代码中需要使用什么宏来进行开关定制,确定宏的名称,编写和宏相关的代码,且要包含 config.h 的头文件;
    2. 在 configure.ac 中的各种处理(例如 AC_CHECK_***AC_ARG_***)中使用 AC_DEFINE 宏定义 C/C++ 的宏,名称和上面的相同;如果是使用 AC_CHECK_HEADERS,会自动添加宏定义;
    3. 执行完第 7 歩(存疑?)后 ,Autoconf 就会自动生成 config .h 文件
  3. 添加编译链接需要的程序

    编译、链接需要用到的程序需要添加在 # Checks for programs. 注释后面。对于 C/C++ 来说,最常见的就是 gcc、g++、静态库编译、动态库编译,对应的选项如下:

    1
    2
    3
    AC_PROG_CXX
    AC_PROG_CC
    AC_PROG_RANLIB

    如果使用 libtool 编译,则选项如下 LT_INIT,注意使用了 libtool 则需要将 AC_PROG_RANLIB 去掉

  4. 在 configure.ac 代码中各个部分添加自己的检测处理

    这一步是我们的主要工作,需要根据自己的项目具体情况来编写,常见操作对应的宏和样例请参考本文后面的“【常见操作对应的宏】”:。至于具体添加在哪个地方,configure.ac 中的注释已经清楚的告诉你了,例如:

    1
    2
    # Checks for libraries.
    # Checks for library functions.
  5. AC_OUTPUT 上一行添加 AC_CONFIG_FILES

    添加这个宏的目的是制定 Autoconf 输出哪些文件,常见的文件就是 Makefile 文件,config.h 在 AC_CONFIG_HEADERS 宏里面指定了,这里不需要再次指定。例如:

    AC_CONFIG_FILES([Makefile tools/Makefile common/Makefile worker/Makefile])

niel 补充 - 关于 AC_CONFIG_FILES 宏我有话说,见 Makefile 自动生成 - 渣滓

编写自定义的 Autoconf 宏

Autoconf 虽然提供了很多内置的宏,但在实际项目中,这些宏不可能满足所有的要求,有的处理还是要自己完成。虽然在 configure.ac 文件中可以直接编写各种处理代码,但这样做有几个缺点:

  • 很不美观:打开 configure.ac 文件,密密麻麻的一大段花花绿绿的 Shell 代码,看着眼花缭乱;
  • 修改起来很麻烦:要找半天才能找到要修改的位置,一不小心就改错了;
  • niel 补充:编写跨平台的 shell 脚本太考验能力

就像写 C/C++ 代码要进行封装一样,Autoconf 的处理也需要进行封装,这个封装就是自定宏,定义完成后在 configure.ac 中调用,看起来很清爽,修改也很简单。下面我们来看如何自定义宏:

  1. 新建一个单独的目录,用于存放自定义宏,一般定义为 m4

  2. 新建自定义宏文件

    建议每个宏一个文件,文件必须以 .m4 结尾,文件名就是宏名(当然如果你非要不这么做也可以,文件名随便取)

  3. 编写 Autoconf 宏

    具体的编写方式请参考 Autoconf 的手册第 10 章节,最好边看手册边对照一个开源软件的样例,这样效果最好了。这里说明几个需要注意的地方:

    • m4宏不是 shell,请不要直接在文件中写 shell 代码,而要在宏的各个部分里面写代码;

      最常见的就是 if-else 判断,如果要在代码中编写 if-else 判断,需要使用 AS_IF 宏,或者在其它宏里面写,例如 AC_ARG_WITH, AC_CACHE_CHECK

    • AC_DEFUN 是定义 autoconf 的宏,AC_DEFINE 是定义 C/C++ 的 config .h 里面的宏,不要混淆了;

  4. 运行 aclocal 工具,生成 aclocal.m4

    由于自定义宏是放在我们新建的目录中的,configure.ac 并没有像 C/C++ 那样的 include 语句可用,因此也就找不到这些宏,这时就需要 aclocal 工具了:aclocal 会将自定义宏编译成configure.ac 可用的宏,保存在和 configure.ac 同级目录下的 aclocal.m4 文件中,这样在 configure.ac 就能够直接使用了。具体的编译方法如下(m4 就是你的目录): aclocal -I m4

    同时需要在根目录下的 Makefile.am 中添加 ACLOCAL_AMFLAGS = -I m4

还有一种方法是将所有的自定义宏都放入到一个 acinclude.m4 文件中,不过不推荐这种方法,原因是因为这种方法的缺点和直接将所有自定义宏放入 configure.ac 中没有多大差别。

编写Makefile.am文件

对于大型项目来说,代码一般都是分目录存放的,而不会像 Hello world 样例那样简单的就几个文件,因此写 Makefile.am 就麻烦一些,但其实主要是工作量增加了,原则都是一样的:

  • 原则1:每个目录一个 Makefile.am 文件;同时在 configure.ac 的 AC_CONFIG_FILES 宏中指定输出所有的 Makefile 文件,例如:

    AC_CONFIG_FILES([Makefile tools/Makefile common/Makefile worker/Makefile])

  • 原则2:父目录需要包含子目录

    在父目录下的 Makefile.am 中添加: SUBDIRS = 所有子目录,例如 SUBDIRS=test tools

  • 原则3:Makefile.am 中指明当前目录如何编译

    前两个原则很简单,这里就不多说了,重点说一下如何编写 Makefile.am。

niel 补充 - 在 Makefile.am 中尽量使用相对路径,系统预定义了两个基本路径:

Makefile.am 中可用的路径变量

niel 补充 - 这两个路径的区别:$(top_builddir)引用的是 make 发生时的工作目录,上文提到,我们将在 build 目录下进行构建,那么库文件会生成在 build 目录下,而不是源码根目录下,所以$(top_builddir) 实际就是 gnu-build/build 目录,而这样可以很好的支持在另一个目录中编译程序。与之相对应的是 $(top_srcdir) 对应的是源码的根目录,即 gnu-build 目录。 引用来源

niel 补充 - 这篇帖子缺失了“外部编译”的知识点,上述引用出处刚好补上。

编写 Makefile.am 主要是完成 3 件事情:编译(make)、安装(make install)、打包(make dist),下面我们一一来进行讲解。

编译安装

编译和安装的规则是绑定在一起的,通过同一条语句同时指定了编译和安装的处理方式,具体的格式为:安装目录_编译类型=编译目标

安装目录

例如:bin_PROGRAMS = hello subdir/goodbye,其中安装目录是 bin,编译类型是 PROGRAMS,编译目标是两个程序 hello, goodbye。

常用缺省的安装目录如下

目录 Makefile.am 中的变量 使用方式 备注
prefix /usr/local 安装目录,通过–prefix指定
exec_prefix ${prefix} 同prefix
bindir ${exec_prefix}/bin bin_编译类型
libdir ${exec_prefix}/lib lib_编译类型
includedir ${prefix}/include include_编译类型
noinstdir noinst_编译类型,特殊的目录,表示编译目标不安装。
datadir $(prefix)/share data_编译类型 niel 新增,引用来源
sysconfdir $(prefix)/etc niel 新增,引用来源同上

除了常用的缺省目录外,有时候我们还需要自定义目录,例如我们希望安装完成后安装目录下有一个配置文件目录 config,同时将指定的 test.ini 拷贝到 config 目录,则 config 目录需要通过自定义目录方式定义,然后按照缺省目录的使用方式使用。例如:

在根目录下的 Makefile.am 中添加如下内容:

1
2
3
4
5
6
# 定义一个自定义的目录名称 config,注意 dir 后缀是固定的
configdir=${prefix}/config
# 使用自定义的目录 config,必须要有这句,否则目录不会创建,
config_DATA=
# =等号后面如果有对应的文件,安装时会将对应的文件拷贝到config目录下。
config_DATA=config/test.ini

编译类型

常见编译类型如下,没有自定义编译类型

类型 说明 使用方式
PROGRAMS 可执行程序 bin_PROGRAMS
LIBRARIES 库文件 lib_LIBRARIES
LTLIBRARIES (Libtool libraries) libtool库文件 lib_LTLIBRARIES
HEADERS 头文件 include_HEADERS
SCRIPTS 脚本文件,有可执行权限 test_SCRIPTS(需要自定义test目录)
DATA 数据文件,无可执行权限 conf_DATA(需要自定义conf目录)

编译目标

译目标其实就是编译类型对应的具体文件,其中需要 make 生成的文件主要有如下几个:可执行程序_PROGRAMS,普通库文件_LIBRARIES,libtool 库文件_LTLIBRARIES,其它类型对应的编译目标不需要编译,源文件就是目标文件。

标准的编译配置

如果你熟悉 gcc 的编译命令写法,那么 Automake 的 Makefile.am 编译过程就很好写了。因为 Automake 只是将写在一行 gcc 命令里的各个不同部分的信息分开定义而已。我们来看具体是如何定义的:

  • target_SOURCES:对应 gcc 命令中的源代码文件
  • target_LIBADD:编译链接库时需要链接的其它库,对应 gcc 命令中的 *.a 等文件
  • target_LDADD:编译链接程序时需要链接的其他库,对应 gcc 命令中的 *.a 等文件
  • target_LDFLAGS:链接选项,对应 gcc 命令中的 -L, -l, -shared, -fpic 等选项
  • target_LIBTOOLFLAGS:libtool 编译时的选项
  • target_**FLAGS(例如 _CFLAGS/_CXXFLAGS):编译选项,对应 gcc 命令中的 -O2, -g, -I 等选项

niel 补充 【TODO】- xxx_LDADD:为链接器增加参数,一般用于第三方库的引用。比如-L -l;xxx_LIBADD:声明库文件引用,一般对于本项目中的库文件引用采用这种形式。引用来源

Use the LIBADD primary for libraries, and LDADD for executables. 引用来源
ps 相比国内博客,我更倾向于 StackOverflow 上的答案。

举例如下:

1
2
3
4
5
6
#不同的编译类型只是第一句不一样,后面的编译配置都是一样的
bin_PROGRAMS= myproject
myproject_SOURCES = main.c
myproject_LDADD = ./utils/libutils.a ./module1/libmodule1.a ./core1/libcore.a
myproject_LDFLAGS = -L/home/test/local -lmemcached
myproject_CFLAGS = -I./core1/ -I./module1/ -I./utils/ -O2 -g

niel 补充 - 可以参考 Makefile 自动生成 - 渣滓,其中提到的全局变量及个别编译配置此文未涉及。

如何编译可执行程序

对于大型项目来说,代码基本上都是分目录存放的,如果是直接写 makefile 文件,一般都是将所有源文件首先编译成 *.o 的文件,再链接成最终的二进制文件。但在 Automake 里面这样是行不通的,因为你只要仔细看编译类型表格就会发现,并没有一种编译类型能够编译 *.o 文件,无法像常规 makefile 那样来编写,所以就需要采取一些技巧。

其实这个技巧也很简单:将非 main 函数所在目录的文件编译成静态链接库,然后采用链接静态库的方式编译可执行程序。样例如下:

=================根目录Makefile.am======================

1
2
#对应Makefile.am原则2
SUBDIRS = tools common worker

=================tool目录Makefile.am======================

1
2
3
4
5
6
#只是为了编译而生成的.a库文件,没有必要安装, 所以是noinst
noinst_LIBRARIES=libtools.a
libtools_a_SOURCES=./urlcode.h \
./stringtools.cpp \
./stringtools.h \
./urlcode.c

===============common目录Makefile.am======================

1
2
3
4
5
#只是为了编译而生成的.a库文件,没有必要安装, 所以是noinst
noinst_LIBRARIES=libcommon.a
libcommon_a_SOURCES=./iniparser.c \
(省略很多文件, 实际使用时要一一填写)
./exception.h \

==============worker目录Makefile.am============================

1
2
3
4
5
6
7
bin_PROGRAMS=worker
worker_SOURCES=./workeralgorithm.cpp \
./worker.cpp \
(省略很多文件, 实际使用时要一一填写)
./worker.h
#通过_LDADD告诉Automake需要链接哪些库
worker_LDADD=../tools/libtools.a ../common/libcommon.a

如何编译静态库

Automake 天然支持编译静态库,只需要将编译类型指定为 _LIBRARIES 即可。

如何编译动态库

需要注意的是:_LIBRARIES 只支持静态库(即 *.a 文件),而不支持编译动态库(*.so)文件,要编译动态链接库,需要使用_PROGRAMS。除此之外,还需要采用自定义目录的方式避开 Automake 的两个隐含的限制:

  • 如果使用 bin_PROGRAMS, 则库文件会安装到 bin 目录下,这个不符合我们对动态库的要求;
  • automake 不允许用 lib_ PROGRAMS

下面假设将 utils 编译成 so,采用自定义目录的方式,修改 Makefile.am 如下:

1
2
3
4
mylibdir=$libdir         #$libdir其实就是lib目录,请参考【安装目录】表格
mylib_PROGRAMS= libutils.so
libutils_so_SOURCES = utils.c utils.h
libutils_so_LDFLAGS = -shared –fpic #这个就是gcc编译动态库的选项

如何编译libtool库

对于跨平台可移植的库来说,推荐使用 libtool 编译,而且 Automake 内置了 libtool 的支持,只需要将编译类型修改为 _LTLIBRARIES 即可。

需要注意的是:如果要使用 libtool 编译,需要在 configure.ac 中添加 LT_INIT 宏,同时注释掉 AC_PROG_RANLIB,因为使用了 LT_INIT 后,AC_PROG_RANLIB 就没有作用了。

打包

Automake 缺省情况下会自动打包,自动打包包含如下内容:

  1. 所有源文件
  2. 所有 Makefile.am/Makefile.in 文件
  3. configure 读取的文件
  4. Makefile.am’s (using include) 和 configure.ac’ (using m4_include)包含的文件
  5. 缺省的文件,例如 README, ChangeLog, NEWS, AUTHORS

如果除了这些缺省的文件外,你还想将其它文件打包(一般包括静态库、头文件、配置文件、帮助文件),有如下两种方法:

  • 粗粒度方式:通过 EXTRA_DIST 来指定,指定文件就打包文件,指定目录就打包目录,例如:

    EXTRA_DIST=conf/config.ini test tools/initialize.sh
    如果 test 是目录,那么会将 test 目录下所有的文件和目录都打包。

  • 细粒度方式:在“安装目录_编译类型=编译目标”前添加 dist(表示需要打包), 或者 nodist(不需要打包),例如:

    1
    2
    3
    4
    5
    6
    #将data_DATA= distribute-this打包
    dist_data_DATA = distribute-this

    #foo_ SOURCES不打包
    bin_PROGRAMS = foo
    nodist_foo_SOURCES = do-not-distribute.c

后记

GNU Autotool 工具博大精深,我也是结合项目的实际应用来使用的,并没有完整的研究所有的工具,因此难免存在瑕疵和纰漏,如果大家发现有疑问或者问题的地方,欢迎大家指正。当然,GNU 自己的手册是最权威的,如果你有疑问的话,参考手册,以手册为准。

如果想了解 autotools 的工作原理和流程以及更高级的技巧,请参考胡华强写的《autoconf and automake介绍与典型应用.doc》。

niel 补充 - 这篇 doc 在网上找不到,困惑。如何使用产生的 Makefile 文件,以及 ./configure 的一些参数可以参考 Makefile 自动生成 - configure & make

niel 补充 - 我读了好多篇帖子之后才反应过来 AC_ 开头的宏是 autoconf 的宏,AM_ 开头的宏是 automake 的宏。这个理解没错吧?

常见操作对应的宏

  • ./configure 添加 --with-package 参数,例如:./configure --with-libmemcached

    AC_ARG_WITH,具体如何写请参考 autoconf 手册 15.2 章节,里面给了一个完整的样例。

  • ./configure 添加 –enable-feature 参数,例如:./configure –enable-multithread

    AC_ARG_ENABLE,顾名思义,这个宏的意思就是打开开关,这个开关可以是编译开关,也可以是代码功能开关。a) 如果是编译开关,则要配合 AM_CONDITIONAL 宏来使用(样例请看 automake 手册 20.1 章节AM_CONDITIONAL 宏说明);b) 如果是代码功能开关,则要配合 AC_DEFINE 宏来使用(请参考 autoconf 手册 15.2 章节AC_ARG_WITH 宏的样例)

  • ./configure 的时候检查头文件

    AC_CHECK_HEADER: 检查一个头文件;AC_CHECK_HEADERS:检查一批头文件

  • ./configure时检查库文件

    AC_CHECK_LIB:样例请参考 autoconf 手册 15.2 章节的 AC_ARG_WITH 宏的样例

  • 修改 make 行为

    如果你想修改默认的make行为,可以先使用AC_ARG_WITH或者AC_ARG_ENABLE添加./configure参数,再结合如下两个宏来完成:

    AM_CONDITIONAL:在 ./configure.ac 中增加一个 automake 宏,在 Makefile.am 中使用 if-else-endif 来使用宏;
    AC_SUBST:在 ./configure.a c中直接修改 automake 的变量,例如 AM_CXXFLAGSAM_CFLAGS 等编译链接。

完整样例

GNU Atuotools 样例.rar

参考资料

  1. 入门材料:http://sources.redhat.com/autobook/autobook/autobook_toc.html
  2. autoconf手册:http://www.gnu.org/software/autoconf/manual/autoconf.html
  3. automake手册:http://sources.redhat.com/automake/automake.html
  4. libtool手册:http://www.gnu.org/software/libtool/manual/libtool.html
  5. tutorial:http://www.lrde.epita.fr/~adl/dl/autotools.pdf