编译利器:大型项目如何使用Automake和Autoconf完成编译配置(ver+0.6)
这篇帖子很棒!我从百度文库中下载了 此教程,因为未做深度的考证,所以也不知原作者是谁,但在此谢谢作者。以下是作者原文,略有删改,本人有修改部分都已做出备注。
使用过开源 C/C++ 项目的同学们都知道,标准的编译过程已经变成了简单的三部曲:configure/make/make install, 使用起来很方便,不像平时自己写代码,要手写一堆复杂的 Makefile,而且换个编译环境,Makefile 还需要修改(Eclipse 也是这样)。
这么好的东东当然要拿来用了,但 GNU 的 Autotool 系列博大精深,工具数量又多,涉及的语言也多,要是自己从头看到尾,黄花菜都凉了,项目估计早就结束了;上网搜样例倒是有一大堆,但都是“hello world”的样例,离真正完成大型项目的目标还差得远。
没有办法,对照网上的样例,再找几个开源的源码,然后参考各种 Autotools 的手册,花了 2 天时间,终于完成了一个基本可用的 Autotools。为了避免其他 XDJM 也浪费时间,因此将过程总结下来,就算是新年礼物,送给大家!!
提纲挈领:使用 Autotools 其实很简单
大家不要看到那么多工具,其实使用起来很简单,总结起来就是两部分:
- 按照顺序调用各个工具;
- 修改或者添加 3 个文件;
整个操作顺序如下图:
niel:作者这幅图强调的是操作顺序,在表现文件依赖关系上力不从心,所以我又多配了一幅图。
听到我这么讲,大家是否觉得有信心了?好的,下面我们来看具体如何操作:
- 源码根目录调用 autoscan 脚本,生成 configure.scan 文件,然后将此文件重命名为 configure.ac
- 修改 configure.ac,利用 autoconf 提供的各种 M4 宏,配置项目需要的各种自动化探测项目
- 编写自定义宏,建议每个宏一个单独的 *.m4 文件;
- 调用 aclocal 收集 configure.ac 中用到的各种非 Autoconf 的宏,包括自定义宏;
- 调用 autoheader,扫描 configure.ac、acconfig.h(如果存在),生成 config.h.in 宏定义文件,里面主要是根据 configure.ac 中某些特定宏(如
AC_DEFINE
)生成的#define
和#undefine
宏,configure 在将根据实际的探测结果决定这些宏是否定义(具体见后面例子)。 - 按照 automake 规定的规则和项目的目录结构,编写一个或多个 Makefile.am(Makefile.am 数目和存放位置和源码目录结构相关),Makefile.am 主要写的就是编译的目标及其源码组成。
- 调用 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 调用。 - 调用 autoconf,利用 M4 解析 configure.ac,生成 shell 脚本 configure。以上几步完成后,开发者的工作就算完成了,后面的定制就由开源软件的用户根据需要给 configure 输入不同的参数来完成。
- 用户调用 configure,生成 Makefile,然后
$ make && make install
。
整个过程步骤有 9 步,但其中有 6 步你只需要简单的敲一个命令即可,只有剩下的三步需要你动手写一些东西,对应上面步骤中的蓝色黑体字部分,而本文的重点就是如何在大型项目中完成这三歩。
步步为营:三步完成编译配置
niel:关键的三步即:第2步-修改 configure.ac 文件、第3步-编写自定义的 Autoconf 宏、第6步-编写 Makefile.am 文件
修改configure.ac文件
从上面的步骤可以看到,使用 autoscan 工具扫描后就会生成一个简单的 configure.acconfigure.scan 文件,这已经是一个完整的 configure.ac 文件框架了,但还不足以达到我们的要求,因此我们要在框架里面添加一些东西:
添加
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 thepackage
andversion
can be obtained from Autoconf’sAC_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 inAUTOMAKE_OPTIONS
.如果需要,添加
AC_CONFIG_HEADERS([config.h])
宏添加这个宏很简单,但关键是“如果需要”,什么情况下需要这个宏呢?
这个宏的目的是输出 config.h,这是一个 C 的头文件,里面主要是包含很多宏定义
#define
,说到这里其实就很明确了,输出这个文件的目的就是提供各种相关的宏,而宏在代码中的作用就是#ifdef
,也就是说:如果你的代码需要用到宏开关进行控制,那么就要输出这个文件。具体的使用方法如下:- 首先确定代码中需要使用什么宏来进行开关定制,确定宏的名称,编写和宏相关的代码,且要包含 config.h 的头文件;
- 在 configure.ac 中的各种处理(例如
AC_CHECK_***
,AC_ARG_***
)中使用AC_DEFINE
宏定义 C/C++ 的宏,名称和上面的相同;如果是使用AC_CHECK_HEADERS
,会自动添加宏定义; - 执行完第 7 歩(存疑?)后 ,Autoconf 就会自动生成 config .h 文件
添加编译链接需要的程序
编译、链接需要用到的程序需要添加在
# Checks for programs.
注释后面。对于 C/C++ 来说,最常见的就是 gcc、g++、静态库编译、动态库编译,对应的选项如下:1
2
3AC_PROG_CXX
AC_PROG_CC
AC_PROG_RANLIB如果使用 libtool 编译,则选项如下
LT_INIT
,注意使用了 libtool 则需要将AC_PROG_RANLIB
去掉在 configure.ac 代码中各个部分添加自己的检测处理
这一步是我们的主要工作,需要根据自己的项目具体情况来编写,常见操作对应的宏和样例请参考本文后面的“【常见操作对应的宏】”:。至于具体添加在哪个地方,configure.ac 中的注释已经清楚的告诉你了,例如:
1
2# Checks for libraries.
# Checks for library functions.在
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 中调用,看起来很清爽,修改也很简单。下面我们来看如何自定义宏:
新建一个单独的目录,用于存放自定义宏,一般定义为 m4
新建自定义宏文件
建议每个宏一个文件,文件必须以 .m4 结尾,文件名就是宏名(当然如果你非要不这么做也可以,文件名随便取)
编写 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 里面的宏,不要混淆了;
运行 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 中尽量使用相对路径,系统预定义了两个基本路径:
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 | # 定义一个自定义的目录名称 config,注意 dir 后缀是固定的 |
编译类型
常见编译类型如下,没有自定义编译类型
类型 | 说明 | 使用方式 |
---|---|---|
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, andLDADD
for executables. 引用来源
ps 相比国内博客,我更倾向于 StackOverflow 上的答案。
举例如下:
1 | #不同的编译类型只是第一句不一样,后面的编译配置都是一样的 |
niel 补充 - 可以参考 Makefile 自动生成 - 渣滓,其中提到的全局变量及个别编译配置此文未涉及。
如何编译可执行程序
对于大型项目来说,代码基本上都是分目录存放的,如果是直接写 makefile 文件,一般都是将所有源文件首先编译成 *.o
的文件,再链接成最终的二进制文件。但在 Automake 里面这样是行不通的,因为你只要仔细看编译类型表格就会发现,并没有一种编译类型能够编译 *.o
文件,无法像常规 makefile 那样来编写,所以就需要采取一些技巧。
其实这个技巧也很简单:将非 main 函数所在目录的文件编译成静态链接库,然后采用链接静态库的方式编译可执行程序。样例如下:
=================根目录Makefile.am======================
1 | #对应Makefile.am原则2 |
=================tool目录Makefile.am======================
1 | #只是为了编译而生成的.a库文件,没有必要安装, 所以是noinst |
===============common目录Makefile.am======================
1 | #只是为了编译而生成的.a库文件,没有必要安装, 所以是noinst |
==============worker目录Makefile.am============================
1 | bin_PROGRAMS=worker |
如何编译静态库
Automake 天然支持编译静态库,只需要将编译类型指定为 _LIBRARIES 即可。
如何编译动态库
需要注意的是:_LIBRARIES 只支持静态库(即 *.a
文件),而不支持编译动态库(*.so
)文件,要编译动态链接库,需要使用_PROGRAMS。除此之外,还需要采用自定义目录的方式避开 Automake 的两个隐含的限制:
- 如果使用 bin_PROGRAMS, 则库文件会安装到 bin 目录下,这个不符合我们对动态库的要求;
- automake 不允许用
lib_ PROGRAMS
下面假设将 utils 编译成 so,采用自定义目录的方式,修改 Makefile.am 如下:
1 | mylibdir=$libdir #$libdir其实就是lib目录,请参考【安装目录】表格 |
如何编译libtool库
对于跨平台可移植的库来说,推荐使用 libtool 编译,而且 Automake 内置了 libtool 的支持,只需要将编译类型修改为 _LTLIBRARIES
即可。
需要注意的是:如果要使用 libtool 编译,需要在 configure.ac 中添加 LT_INIT
宏,同时注释掉 AC_PROG_RANLIB
,因为使用了 LT_INIT
后,AC_PROG_RANLIB
就没有作用了。
打包
Automake 缺省情况下会自动打包,自动打包包含如下内容:
- 所有源文件
- 所有 Makefile.am/Makefile.in 文件
- configure 读取的文件
- Makefile.am’s (using include) 和 configure.ac’ (using m4_include)包含的文件
- 缺省的文件,例如 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_CXXFLAGS
,AM_CFLAGS
等编译链接。
完整样例
参考资料
- 入门材料:http://sources.redhat.com/autobook/autobook/autobook_toc.html 。
- autoconf手册:http://www.gnu.org/software/autoconf/manual/autoconf.html 。
- automake手册:http://sources.redhat.com/automake/automake.html 。
- libtool手册:http://www.gnu.org/software/libtool/manual/libtool.html
- tutorial:http://www.lrde.epita.fr/~adl/dl/autotools.pdf 。