Autotools

  最近看公司代码, 一个Server就只有一个目录, 几百个源文件就放在一个目录里, 颇为混乱, 于是有心想整改一下, 使其模块化, 良好的结构, 有利于对整个项目更清晰的了解. 项目使用了Autotools作为整个项目的构建工具, 至于为什么, 或者为什么不用CMake就不得而知了, 或许是因为项目太过于古老了吧… 不过没关系, Autotools虽然相比CMake更复杂, 但作为行业规范, 在开源世界, 依然有着不可替代的地位. 另外在不知道Autotools时, 对于根目录下一些乱七八糟的文件(aclocal.m4, AUTHORS, autom4te.cache…)会很碍眼, 它不能被删掉, 但又不知道是用来做什么的, 虽然不影响工作, 但也限制一些操作, 工作就变成了按部就班, 在前人的模板下东拉西扯生搬硬套, 让人昏昏欲睡, 只有学点新东西, 才不至于睡着了.

下面的内容大都来自这些资源:

  1. <<Autotools - A Practitioner’s Guide to GNU Autoconf, Automake, and Libtool>>
  2. https://www.lrde.epita.fr/~adl/autotools.html
  3. https://www.gnu.org/software/autoconf/manual/autoconf-2.61/html_node/Indices.html#Indices

正如书中所说的那样, 受众群体是Unix系系统开发者, 想要学习使用Autotools构建程序的那类^_^
但实际上也可以作为学习了<<Linux程序设计>>后, 继makefile后的延伸知识(这里并不会有makefile相关的内容, 不过需要对makefile是什么, makefile中的变量,目标,命令有所了解), 因为手写makefile没必要, 程序稍微复杂一点, 就需要使用这些工具帮忙了.

Autotools简介

  简单的说Autotools提供了一系列工具,用以辅助产生configure, Makefile, 方便编译整个工程项目, 从而快速的构建出想要的产品, 快速构建易于管理只是它的一方面, 另一个重要的使用它的原因是它构建出的项目具有可移植性, 要做到这两点, 主要使用的工具是这三个, Autotools也是由这三部分组成 – Autoconf Automake和Libtool.

  • Autoconf主要通过configure.ac来生成configure文件, 用于检测平台特性, 以及检查想要成功构建项目的一些强制项, 用于确保在该平台下项目能够正确编译及运行.
  • AutoMake则用来生成Makefile文件, Makefile管理了源代码之间的依赖关系以及编译代码所使用的编译选项等等等等, 这些通过用户提供的configure.ac和Makefile.am而来.
  • Libtool则是用来构建可移植性的库, 以统一的la后缀来隐藏平台之间的特性, 用户使用库时则不需要关心平台方面的差异.

  Autotools采用Autotools构建出来的项目符合GNU软件构建标准, 回忆一下, 很多时候在linux上安装其他程序时的步骤, 大多需要通过: tar解包 -> ./configure -> make -> make install程序就能够使用了, 这正是使用Autotools的原因, 试想, 不同的程序都需要不同的安装方式, 还不能避免平台的差异, 这将大大局限软件的适用性, 也会给开发者造成更多工具无关的工作.

Autotools应用

本节通过一个实例来完整的展示Autotools的使用, 不详细展开说明, 仅使用一些命令来完成任务, 在下一节详解中会详细解释这些命令的用途, 到时在回来看下面的流程希望能有豁然开朗的效果, 所以这里对下面一些命令, 先不要问为什么, 按照这个顺序是能够构建出这个程序的, 尝尝鲜先.

这里假设需要构建项目HelloAuto, 其目录如下:

1
2
3
4
HelloAuto:
lib_provider/func.c
lib_provider/func.h
examples/main.c

lib_provider用于提供库服务, examples中的测试用例将使用lib_provider提供的库

在HelloAuto目录(一下称为根目录), 执行一下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
## autoconf
autoscan # 生成autoscan.log configure.scan文件

# ------------------ 第一次修改configure.scan ----------------------

mv configure.scan configure.ac # 修改configure.scan文件名为configure.ac

# (configure.ac第一版内容见下)

autoreconf -i # -i安装一些需要的辅助文件, 此时生成autom4te.cache config.h.in configure文件

./configure # 生成config.status config.h config.log

touch NEWS README ChangeLog AUTHORS # 手动添加这几个规定文件

## automake
automake
# 此时使用automake被告知需要在configure.ac中添加AM_INIT_AUTOMAKE
# ------------------ 第二次修改configure.ac ----------------------

# 每次修改configure.ac后都需要通过autoreconf重新生成configure文件
autoreconf -i # 生成install-sh missing文件

# 因为AM_INIT_AUTOMAKE的加入, autoreconf也会帮助执行automake系列命令, 生成了aclocal.m4
# 在执行automake时报错, 需要Makefile.am文件

# ------------------ 编写Makefile.am ----------------------
# ------------------ 第三次修改configure.scan ----------------------

autoreconf -i # 生成INSTALL COPYING depcomp Makefile.in lib_provider/Makefile.in examples/Makefile.in

./configure # 生成Makefile lib_provider/Makefile examples/Makefile

# 最后编写源码 lib_provider/func.h func.cpp examples/main.cpp

make # 编译代码
...

./examples/helloauto # 执行程序
hello Autotools! # 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 修改configure.ac

# ---------------- configure.ac第一次修改 --------------------
AC_PREREQ([2.69]) # 所需要autoconf最低版本
AC_INIT([HelloAuto], [1.0.0], [captzx@163.com]) # HelloAuto项目名, 1.0.0版本信息, 反馈信息到邮箱captzx@163.com
AC_CONFIG_SRCDIR([examples/main.cpp]) # 告知源码目录, 后面的文件倒是可以随便选一个
AC_CONFIG_HEADERS([config.h]) # 配置头文件, 用于甄别平台差异
AC_PROG_CXX # 检查编译程序
AC_OUTPUT # 输出所有符号

# ---------------- configure.ac第二次修改 --------------------
...
AC_CONFIG_HEADERS([config.h])
AM_INIT_AUTOMAKE # 新增AM_INIT_AUTOMAKE, 对automake的支持
...
AC_PROG_CXX
AC_PROG_RANLIB # 新增AC_PROG_RANLIB, 对运行时库的支持
...

# ---------------- configure.ac第三次修改 --------------------
...
AC_CONFIG_FILES([Makefile lib_provider/Makefile examples/Makefile]) # 新增AC_CONFIG_FILES, 最终需要生成的文件
AC_OUTPUT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 编写Makefile.am, 根目录下需要Makefile.am, 内容如下

# ---------------- Makefile.am --------------------
SUBDIRS = lib_provider examples # SUBDIRS为源码目录, 目录中的每个文件都需要各自的Makefile.am

# ---------------- lib_provider/Makefile.am --------------------
noinst_LIBRARIES = lib_provider.a # 生成lib_provider.a库文件
lib_provider_a_SOURCES = func.h func.cpp # 生成lib_provider.a所需要的源文件

# ---------------- examples/Makefile.am --------------------
bin_PROGRAMS = helloauto # 生成可执行文件helloauto
helloauto_SOURCES = main.cpp # 生成helloauto所需要的源文件
helloauto_CPPFLAGS = -I$(top_srcdir)/lib_provider # 添加编译选项, 头文件目录
helloauto_LDADD = ../lib_provider/lib_provider.a # 添加库依赖

# noinst_LIBRARIES bin_PROGRAMS *_SOURCES *_CPPFLAGS *_LDADD 这些名字都有含义的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ---------------- lib_provider/func.h --------------------
#pragma once
void print();

// ---------------- lib_provider/func.cpp --------------------
#include "func.h"
#include <iostream>

void print(){
std::cout << "hello Autotools!" << std::endl;
}

// ---------------- examples/main.cpp --------------------
#include "func.h"

int main(){
print();
return 0;
}

最后的目录是这样, 下面简单描述其用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# vvv 源码目录 vvv
examples # 生成可执行文件helloauto
lib_provider # 生成库lib_paovider.a
Makefile.am # Makefile.am配置文件是使用automake系列工具的起点, 需要通过它来生成Makefile脚本

# vvv 以下使用autoscan得到 vvv
autoscan.log # 如果configure.ac存在并需要改进, 则会记录需要改进的内容
(configure.scan -> )configure.ac # configure.ac配置文件是使用autoconf系列工具的起点, 需要通过它来生成configure脚本

# vvv 以下使用autoheader得到 vvv
config.h.in # 将被config.status处理成config.h

# vvv 以下使用aclocal得到 vvv
aclocal.m4 # 额外的宏处理器, 处理configure.ac中autoconf不支持的宏

# vvv 以上几个文件用于说明, 作为GUN软件规范, 需要存在, 不然automake会不开心的 vvv
AUTHORS
ChangeLog
NEWS
README # 设置项目的作者/修改日志/动态/项目信息等

# vvv 以下使用automake得到 vvv
Makefile.in # 由Makefile.am生成, Makefile.am为Makefile.in更高级的抽象文件
missing # 通过automake --add-missing获得
depcomp # dep(endency)+comp(ile)当系统原生的编译器不支持一些编译依赖生成选项, 通过depcomp先处理一遍依赖信息, 再编译代码
install-sh # 作为make install的安装选项用于掩盖平台之间的差异

COPYING
INSTALL # 这两个文件也是GNU软件规范的, 里面是些样板声明, 可以根据自己的项目修改它, 也可以不管

stamp-h1

# vvv 以下使用autoconf得到 vvv
autom4te.cache # 使用autoconf得到, 缓存数据, autotools内部使用, 用于辅助处理configure.ac
configure # 重要文件, 该脚本执行检查configure.ac中的设定, 并根据平台差异生成特定文件, 以此达到可移植的目的

# vvv 以下使用./configure得到 vvv
config.status # 用来处理模板(.in后缀是模板文件)
config.h # 跨平台头文件, 理应包含在所有源码文件中
Makefile # 重要文件, 用于指定make编译规则, 由config.status处理Makefile.in模板生成
config.log # 记录了执行./configure时的一些信息, 当./configure失败时可以看它来debug

./examples:
helloauto
helloauto-main.o
main.cpp
Makefile
Makefile.am
Makefile.in

./lib_provider:
func.cpp
func.h
func.o
lib_provider.a
Makefile
Makefile.am
Makefile.in

Autotools详解

如上节应用中所使用那样, 该节将按顺序详解: autoconf/automake系列命令, configure.ac/Makefile.am内容的含义, 以及libtool的详细介绍.
其中大部分在上面目录中文件后的注释里都有提及.

autoreconf
autoreconf属于autoconf包, 但其工作范围却不限于autoconf, 所以把它单独提出来, 它可以看做是一个智能的引导工具, 会根据configure.ac中的内容按顺序正确执行需要的Autotools系列工具(autoconf/automake/libtool), 所以只需要准备好它需要的东西, 然后简单的执行这一句便可.
从上面的例子中, 也说过每次修改了configure.ac都需要重新执行autoreconf也正是这个原因, 可能由于configure.ac的改变, 都可能需要添加/执行多余的工具.

autoconf

首先是autoconf, autoconf实际上是一个package, autoconf最终将生成一个完美的配置脚本(configure),它比手工编写的脚本更可移植、更准确、更可维护.
它提供了以下一些工具:

autoscan
源码准备好后, 一般autoscan是必须的, 通过它可以快速的创建configure.scan, 其内容是根据当前源码生成的必要的autoconf宏, 这些宏在后面会被展开, 我们只在做小幅修改, 就可以得到configure.ac了.
如果源码修改, autoscan会帮助检查已有的configure.ac辅助指正, 生成的信息会被放入autoscan.log, 这时需要根据提示修改configure.ac.

autoheader
autoheader则根据configure.ac生成一份可移植的头文件模板config.h.in, 之后会通过config.status处理成一份供程序使用的头文件config.h.

autoconf
确保当前做足准备来执行M4宏处理器(aclocal.m4), 从而生成configure脚本. configure脚本用于确定平台特性以及用户系统上可用的特性, 检查已安装的工具/实用程序/库和头文件, 以及这些资源中的特定功能.

其他还有一些中间工具或不重要, 或未涉及便暂且不论, 下面是一张autoconf的图解, 有助于理解文件之间的关系:

执行autoconf图解

图中我们只需要关系输入文件和输出文件即可, 内部调用不需要特别关系, 本图则示意, configure文件需要用户提供configure.ac和aclocal.m4(acsite.m4可能是其他宏处理器)再执行autoconf来获得, 而config.h.in则需要提供configure.ac和aclocal.m4通过执行autoheader来获得.

automake

automake是另外的package, automake的工作是将项目构建过程的简化规范(Makefile.am)转换为样板的makefile语法(Makefile.in).
它提供了以下一些工具:

aclocal
aclocal将生成aclocal.m4宏处理器, 它包含了所有项目中或用户定义的宏.

automake
automake通过Makefile.am生成Makefile.in.

执行automake图解

本图则示意, 执行automake需要用户准备configure.ac和Makefile.am文件将生成COPYING INSTALL Makefile.in以及右下角四个文件, 若Libtool需要被支持, 则继续生成config.guess config.sub和ltmain.sh, 这三个文件将供libtool构建库时使用.

./configure

确定平台特性以及用户系统上可用的特性, 检查已安装的工具/实用程序/库和头文件, 以及这些资源中的特定功能, 生成并执行config.status.
config.log包含了很多有用的信息, 如果./configure失败了, 或许可以通过config.log来debug.

执行configure图解

从图中可以看出, 执行./configure时, 将会生成config.log用来记录相关信息, 将会执行config.status脚本, 将模板文件config.h.in和Makefile.in转换成config.h和Makefile正式文件, 若Libtool被支持, 将通过ltmain.sh生成为项目定制的libtool脚本.

make的工作

执行make图解

从图中可以看出, make将使用上述程序或脚本齐力生成的Makefile文件, config.h头文件以及源码文件并使用相关脚本构建出一个完成的项目程序.

在make的时候如果你也关心那些编译选项的话, 它们都在这里:
编译选项: https://gcc.gnu.org/onlinedocs/gcc/Invoking-GCC.html#Invoking-GCC

好了, 到这里我们大致就能够推测出autoreconf帮做的工作:

aclocal -> aclocal.m4
autoheader -> config.h.in
autoconf -> configure

如果automake需要被支持则继续:

automake -> Makefile.in

大致应该就是这样, 嗯… 应该, 但听说autoreconf很智能或许还有做其他工作, 嗯… 或许.

configure.ac

下面来关注更多configure.ac的细节, 正如上面所介绍的那样, 先使用autoscan生成一个configure.scan看看(这里我已经修改为configure.ac了, 和上面基本一致):

configure.ac中语法:

这些大写开头的叫宏, 可以通过https://www.gnu.org/software/autoconf/manual/autoconf-2.61/html_node/Indices.html#Indices查阅.
从上面文档中可以看到, 有很多种类的宏, AC开头是autoconf能够识别的标准宏, 还有其他宏(大多时候只会使用少数), 虽然能写入configure.ac, 但却不能够被正确识别, 这就是为什么需要其他宏处理器(如aclocal.m4)的原因.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ([2.69]) # autoconf最小版本检查, 放在第一位但非必需, AC_INIT一般紧随其后.
AC_INIT([helloauto], [1.0.0], [captzx@163.com]) # 必需的宏, 三个参数依次为 [包名][版本][反馈邮箱], 发行包的压缩形式将被命名为 helloauto-1.0.0.tar.gz
AC_CONFIG_SRCDIR([examples/main.cpp]) # 源码路径
AC_CONFIG_HEADERS([config.h]) # 根据configure.ac所写内容, 平台特性将通过#ifdefine的形式写入头文件
AM_INIT_AUTOMAKE # 使用automake

# Checks for programs. 检查必要的程序
AC_PROG_CXX # C++编译器
# AC_PROG_CC # C编译器, 可以删掉
AC_PROG_RANLIB # 运行时库支持

# Checks for libraries. 检查必要的库

# Checks for header files. 检查必要的头文件

# Checks for typedefs, structures, and compiler characteristics. 检查系统类型 结构 编译器特性

# Checks for library functions. 检查库函数

AC_CONFIG_FILES([Makefile
examples/Makefile
lib_provider/Makefile]) # 通过*.in输出*文件
AC_OUTPUT # 在configure.ac除了AC_INIT外, 另一个必需包含的宏, 总是出现在configure.ac文件的尾部, 生成config.status并加载它.

以上宏基本为自动生成, 自动生成意味着它们可能是必须项, 比较简单的就不单独拎出来解释了, 直接写在注释里.

手动添加的只有AM_INIT_AUTOMAKE(因为需要使用automake)和AC_PROG_RANLIB(因为需要使用库).

下面介绍一些其他宏:

AC_CONFIG_SRCDIR (unique-file-in-source-dir)
unique-file-in-source-dir指定源码路径(可包含一个其中的文件), 有Makefile.am的话, 似乎没什么用, 但建议还是保留吧…

AC_CONFIG_HEADERS(header …, [cmds], [init-cmds])
由AC_OUTPUT生成header文件, 其中是些configure.ac中检测宏的C预编译实例.

AC_CONFIG_FILES (file…, [cmds], [init-cmds])
由AC_OUTPUT生成一些file, 这些文件通过file.in执行config.status而来.

检查必要程序 AC_PROG_*
所有的AC_PROG_*宏, 如果检查成功, 将会将其值赋给名为 * 的变量.

AC_PROG_CXX ([compiler-search-list])
检查C++编译器: compiler-search-list可以指定使用编译器优先级, 找到编译器后将其赋值给CXX变量.

AC_CHECK_PROG(variable, prog-to-check-for, value-if-found, [value-if-not-found], [path], [reject])
检查其他程序: 通过例子说明:

AC_CHECK_PROG([bash_var], [bash], [yes], [no],, [/usr/sbin/bash])

检查跳过/usr/sbin/bash查找bash程序, 如果找到了bash_var=yes, 否则bash_var=no

检查库和文件, 库和头文件相辅相成
AC_CHECK_LIB (library, function, [action-if-found], [action-if-not-found], [other-libraries])
AC_SEARCH_LIBS (function, search-libs, [action-if-found], [action-if-not-found], [other-libraries])
AC_SEARCH_LIBS是AC_CHECK_LIB的加强版, 能够指定多个库

library指定库, function随便指定库中的一个函数名, search-libs同时指定多个库, 按顺序率先使用第一个找到该函数的库, action为所要执行的命令, 如果该库依赖于其他库, 将其他库添加到other-libraries.

找到库后其值将被追加到LIBS变量.

AC_CHECK_HEADERS(header-file…, [action-if-found], [action-if-not-found], [includes = ‘default-includes’])
如果头文件找到, 将会被宏定义到config.h文件中.
库和头文件的检测通过下面这个例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
have_pthreads=no
AC_SEARCH_LIBS([pthread_create], [pthread], [have_pthreads=yes])

AC_CHECK_HEADERS([stdlib.h])
if test "x${have_pthreads}" = xyes; then
AC_CHECK_HEADERS([pthread.h], [], [have_pthreads=no]) # HAVE_PTHREAD_H将被定义到config.h
fi
if test "x${have_pthreads}" = xno; then
AC_MSG_WARN([
------------------------------------------
Unable to find pthreads on this system.
Building a single-threaded version.
------------------------------------------])
fi

找到头文件其宏将被追加到”DEFS”变量.

$(CC) $(CFLAGS) $(CPPFLAGS) -I. -I$(srcdir) -I.. -o $@ main.c $(LIBS)

创建/替换输出变量
AC_SUBST (variable, [value])
创建/替换输出变量, 变量被重新复制后输出到Makefile文件中. 这些变量在Makefile.in中以@varibale@形式存在.

打印消息
AC_MSG_WARN(problem-description)
将可能出现的问题通知用户, 此宏将消息打印到标准错误输出.
configure在之后继续运行, 因此调用AC_MSG_WARN的宏应该为它们警告的情况提供默认(备份)行为, 问题描述应该类似于 ‘ln -s seems to make hard links’.

AC_MSG_NOTICE(message)
将消息传递给用户, 列印一组特征检查的整体目的的一般描述.

AC_MSG_ERROR(error-description, [exit-status])
将阻止完成配置的错误通知用户, 该宏向标准错误输出输出一条错误消息.
错误描述应该类似于 ‘invalid value $HOME for $HOME’.
错误描述应该以小写字母开头, 并且 cannot 比 can’t 更合适.

AC_MSG_FAILURE(error-description, [exit-status])
通知用户一个阻止配置完成的错误, 并在config.log中提供更多详细信息. 这通常在编译过程中发现异常结果时使用.

(ERROR和FAILURE会中断./configure, exit-status为退出码, 默认为1)

AC_MSG_CHECKING(feature-description)
打印configure正在检查某个特定功能, 该宏打印的消息以checking开头, 以…结尾. 没有换行, 在它之后必须调用AC_MSG_RESULT来打印检查结果和换行.

AC_MSG_RESULT(result-description)
将检查结果通知用户, result-description几乎总是用于检查的缓存变量的值, 通常是yes no或文件名.

这些消息不仅会打印到stdout, 也会保存到config.log中

提供选项
AC_ARG_WITH(package, help-string, [action-if-given], [action-if-not-given])
对于可能使用的每个外部软件包,configure.ac调用AC_ARG_WITH来检测configure用户是否要求使用它.

AC_ARG_ENABLE(feature, help-string, [action-if-given], [action-if-not-given])
AC_ARG_WITH控制您的项目使用可选的外部软件包, AC_ARG_ENABLE控制可选软件的包含或排除特性.

通常,当用户需要在不同包提供的功能实现或项目内部提供的功能实现之间进行选择时, 应该使用AC_ARG_WITH.

例子:
https://www.gnu.org/software/autoconf/manual/autoconf-2.61/html_node/External-Software.html#index-AC_005fARG_005fWITH-1359

AS_HELP_STRING(left-hand-side, right-hand-side, [indent-column = ‘26’], [wrap-column = ‘79’])
展开成一个帮助字符串, 当用户执行’configure –help’时, 它看起来很美观. 通常和AC_ARG_WITH AC_ARG_ENABLE一起使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
AC_ARG_WITH([readline], [AS_HELP_STRING([--with-readline], [support fancy command line editing @<:@default=check@:>@])], [], [with_readline=check])

LIBREADLINE=
AS_IF([test "x$with_readline" != xno],
[AC_CHECK_LIB([readline], [main], [AC_SUBST([LIBREADLINE], ["-lreadline -lncurses"]) AC_DEFINE([HAVE_LIBREADLINE], [1], [Define if you have libreadline])],
[
if
test "x$with_readline" != xcheck;
then
AC_MSG_FAILURE([--with-readline was given, but test for readline failed])
fi
],
-lncurses)])

执行configure时会检查设置的检查, 未通过检查./configure可能会失败, 例如库的确实将导致程序无法正常运行!

Makefile.am

automake会处理率先处理项目目录中(顶层目录)的Makefile.am, 而该Makefile.am的工作往往是告知项目中的那些目录将会被处理, 这些待处理目录中有其各自的Makefile.am, 通常顶层目录只需要包含下面一条语句:

1
SUBDIRS = lib_provider examples # SUBDIRS顾名思义, 子目录的意思, 即项目源码由这两部分组成, 里面的内容需要被编译, 至于如何编译, 则由其内部Makefile.am管理.

这里直接贴出内部Makefile.am, 以便讲解:

1
2
3
4
5
6
7
8
9
# ---------------- examples/Makefile.am --------------------
bin_PROGRAMS = helloauto # PLV: product list variable, 指定产品列表
helloauto_SOURCES = main.cpp # PSV: product sources variable, 指定产品源码
helloauto_CPPFLAGS = -I$(top_srcdir)/lib_provider # POV: product options variable, 指定选项, 包含的头文件目录
helloauto_LDADD = ../lib_provider/lib_provider.a # 指定需要包含的库

# ---------------- lib_provider/Makefile.am --------------------
noinst_LIBRARIES = lib_provider.a # PLV, 不需要安装的静态库
lib_provider_a_SOURCES = func.h func.cpp # PSV, 这里product中的'.'被替换成'_'

在应用部分, 介绍Makefile.am时也说过, Makefile.am中这些变量名是有其含义的, 可以看做是Makefile.am文件的语法吧. 主要由三部分组成:

  1. PLV: product list variable, 产品列表变量, 简单的说就是通过变量指定将要生成的目标文件
  2. PSV: product sources variable, 产品源码变量, 即指定生成目标文件的源码文件
  3. POV: product options variables, 产品功能变量, 指定生成目标文件所需要的选项, 如所需要包含的头文件目录, 库文件依赖等

这些变量都是形如prefix_PRIMARY的变量, 下划线隔开, 前小写后大写, 前面的的prefix, 后面的是PRIMARY, prefix前缀用来标识行为, PRIMARY主体执行产品类别.

PLV: 产品列表原型:

1
[modifier-list]prefix_PRIMARY = product1 product2 ... productN # 值用空格隔开

prefix
PLV的prefix大致可以分为两类, 用于定位安装目录的和其他:

用于定位安装目录的:
以dir结尾的prefix都是用来定位文件安装目录的, 使用的时候可以省略dir.
可以以*dir自定义安装目录, 然后以*作为prefix, 然后其product将被安装到*dir中.
带有pkg的特殊的dir, 会在prefix目录下, 新建一个项目目录, 用来存放该各product, pkg相当于分包存放.

Variable Default value
prefix /usr/local
exec_prefix $(prefix)
bindir $(exec_prefix)/bin
datarootdir $(prefix)/share
datadir $(datarootdir)
includedir $(prefix)/include
libdir $(exec_prefix)/lib
pkglibdir $(libdir)/$(package)
srcdir source-tree directory

其他:
noinst: no installation, 一般用来构建出来静态库.
check: 以check为前缀的产品只会在make check时被构建, 它们是用来测试的, 所以也不需要被安装.
EXTRA: 用来做条件编译, 通过configure.ac设置条件, 通过用户决定编译选项, 在Makefile.am使用EXTRA来选择性的构建目标.

PRIMARY: PROGRAMS: automake会对这类产品列表生成make rules用于构建出程序.
LIBRARIES/LTLIBRARIES: LIBRARIES表示通过系统编译器和链接器构建出静态库, LTLIBRARIES表示使用libtool脚本构建Libtool动态库, 它们只能被放到$(libdir)和$(pkglibdir)目录. 也就是说它们的prefix会被制约.
SCRIPTS: 生成脚本, 它们应该被放到bin相关的目录.
DATA: 数据文件 $(datadir), $(sysconfdir), $(sharedstatedir),$(localstatedir), and $(pkgdatadir).
HEADERS: 这个用来列出头文件, 没什么用…

构建单元测试脚本: check_SCRIPTS: SCRIPTS用来指定测试脚本, check表示在make check时测试脚本将被构建.

PSV:产品源码原型:

1
[modifier-list]product_SOURCES = file1 file2 file3 ... fileN

PSV与PLV不同在于, PSV是由PLV中的product名和SOURCES组成, 用于说明product的构建需要哪些文件.

make变量只接受小写字母 数字 @, 其他字符会被转换成下划线, 这里体现在PLV中的product如果含有其他符号, 在PSV中这些符号将被以下划线替代.

[可选项modifier-list]修饰符列表: 修饰符改变它们所前置的变量的正常行为, 其中比较重要的有dist nodist nobase notrans.
dist修饰符表示执行make dist时这些硬件应该随同发布.
nobase修饰符用于禁止删除Makefile文件从子目录中获得的已安装头文件中的路径信息.

1
2
3
4
5
dist_myprog_SOURCES = file1.c file2.c # file1.c file2.c将被构建入发行包
nodist_myprog_SOURCES = file3.c file4.c # file3.c file4.c不会

pkginclude_HEADERS = sys/gods.h # gods.h将会安装到$(pkginclude)目录下
nobase_pkginclude_HEADERS = mylib.h sys/constants.h # 因为nobase, constatns会被放入$(pkginclude)/sys目录下

POVs:产品选项原型:

这些产品选项变量(POVs)为从源代码构建产品的product指定特定于产品的选项, 其结构和PSV类似, 下面是一些重要的POV:
product_CPPFLAGS: 添加C预处理到编译命令行
product_CFLAGS: 添加选项到C编译命令行
product_LDFLAGS: 添加选项到链接命令行
program_LDADD: 添加库到链接命令行
library_LIBADD: 添加链接目标或静态库到命令行
ltlibrary_LIBADD: 添加Libtool目标文件或Libtool库到命令行

如果要使用的库是一些系统自带的库, 那么可以通过AC_CHECK_LIBS来指定, 而不用library_LIBADD, 这些一般用来加载用户生成的库.

AM_CFLAGS用来指定预定义选项, 即所有的目标都应该使用这些选项.

LibTool

libtool帮助构建可移植的库文件, 正如程序需要可移植一样, 在不同的平台上, 库文件的存在形式也不尽相同, libtool将帮助隐藏库文件再各个系统上的存在的差异, 统一管理库文件达到可移植的目的. 更多关于libtool的内容可以参阅官方文档: https://www.gnu.org/software/libtool/manual/html_node/index.html

libtool提供了一下工具:

libtool
libtool包附带的libtool shell脚本是libtoolize为项目生成的定制脚本的通用版本.

libtoolize
ibtoolize shell脚本为项目使用Libtool做准备. 它生成了通用libtool脚本的自定义版本, 并将其添加到项目目录中.
这个自定义脚本与自动生成的makefile一起提供, 它们在适当的时候在用户的系统上执行脚本.

ltdl
libtool包还提供了ltdl库和相关的头文件, 它们提供了跨平台的一致运行时共享对象管理器.
ltdl库可以静态地或动态地链接到您的程序中, 从而在平台之间为它们提供一致的运行时共享库访问接口.

ltdl.h
提供了一组接口, 用于在运行时加载libtool生成的库.

使用LibTool
应用的例子中是构建出了lib_provider.a静态库, 想要改成构建出libtool架构的库是很简单的:

1
2
3
4
5
6
7
8
# ---------------- configure.ac添加Libtool支持 --------------------
...
AM_INIT_AUTOMAKE # 新增AM_INIT_AUTOMAKE, 对automake的支持
LT_PREREQ(2.4.2) # 检查版本
LT_INIT # 这里可以添加参数, 覆盖libtool选项的默认值, libtool默认创建动态库, disable-shared后将默认创建静态库
...
# AC_PROG_RANLIB # 新增AC_PROG_RANLIB, 对运行时库的支持, 使用libtool后, 这句就可以不要了
...
1
2
3
4
5
6
7
# ---------------- lib_provider/Makefile.am修改 --------------------
noinst_LTLIBRARIES = lib_provider.la # LIBRARIES改为LTLIBRARIES *.a改为*.la, la为libtool构建的库统一后缀, 静态库和动态库都是它
lib_provider_la_SOURCES = func.h func.cpp # 注意这里要改成la

# ---------------- examples/Makefile.am修改 --------------------
...
helloauto_LDADD = ../lib_provider/lib_provider.la # 注意这里也要改成la

然后

1
2
3
4
5
autoreconf -i # 使用-i, 因为又有新的工具需要被生成了, 分别是ltmain.sh config.guess config.sub, 内部使用的不需要管它

./configure

make # 按理说就能直接大功告成了

关于生成的脚本:
ltmain.sh将配合config.status工作, 为用户项目量身制定libtool脚本. 然后在Makefile中使用libtool构建在Makefile.am中PLV使用了LTLIBRARIES的库.
libtool脚本将构建系统的作者与在不同平台上构建共享库的细微差别隔离开来. 此脚本接受一组定义良好的选项, 并将它们转换为目标平台和工具集上合适的平台和特定于连接器的选项.
这些选项在configure.ac的LT_INIT时设置, 常用的有:
shared/disable-shared, shared是默认的选项, 默认构建动态库, disable-shared后默认构建静态库.
static/disable-static, disable-static后会默认构建动态库, 不可与disable-shared同时存在.

config.guess和config.sub是两个辅助脚本用来查询系统相关的信息, 暂且不管吧.

总结

  关于Autotools, 就写这些吧, 看了书和资料后, 发现写的内容还是比较初级, 但看完之后大致还是了解了Autotools的相关用法, 剩下的就需要在实践中探索了. 但实际上目的已经达到了, 现在再来看目录里面的那些内容, 不会觉得陌生, 知道那些是重点那些不需要关心, 如果要做修改那么又何妨一试呢? 其实多试几遍会发现, 当所有的点都了解之后, 生成的文件虽多, 但实际操作相当快捷, 只需要几个命令, 几个配置文件便能够轻易完成, 另外对于./configure和make时, 那不停跳动的信息, 也有了更多的了解, 知其然知其所以然, 本身就是相当惬意的事情, 遇到问题也能胸有成之快速定位, 而不是凭感觉和闭着眼睛尝试.

  但在实际项目中, 或许Autotools仍非首选, 接触的越多越觉得, 或许Autotools更适合那些第三方中间开发者, 他们提供的工具作为用户项目的一部分, 而用户来自五湖四海, 所使用的平台工具系统等各不相同, 为了做这些兼容性才需要用到更多的Autotools知识, 采用GNU的软件构建标准来适应大众. 而实际项目中, 更多的作为商业用途, 一来不会开源, 二来普适性并不强, 压根就不会操心那么多问题, 更多的反而是确定好采用哪个版本的系统, 编译器甚至编译选项都严格一致, 以免版本的不同导致的问题, 从而省去了兼容方面的头疼问题, 这样实际上也并没有什么不好, 当然随着时间的推移, 新同学就会发现, 自己使用的工具采用的版本, 都是相当古老的东西了, 代码也就变成了祖传代码, 这些代码经手的人多, 往往让后来者不会很愉悦:). 当然如果作为个人项目, 倒是随意了.