原文:实践:GNU构建系统html
在上一篇概念:GNU构建系统和Autotool,我对GNU构建系统从用户视角和开发者视角分别进行了阐述。本篇从个人实践总结的角度,并阐述如何从头开始规划一个基于GNU构建系统的项目。事实上,随着开发者对跨平台认知的深刻和完善,才能逐渐掌握GNU构建。注意:本文的例子不依赖于任何IDE和编辑器。这样读者能够从根本上认识到每一个文件的做用。linux
须要安装的工具包括autoconf、automake、libtool。c++
首先,咱们须要规划项目的目录结构。假设,咱们的项目叫gnu-build
。设想以下目录结构:git
gnu-build |---build(用于编译) |---src |---common |---Makefile.am |---pool.c |---alloc.c |---list.c |... |---core |---Makefile.am |---main.c |... |---test |---Makefile.am |---test.c |... |---Makefile.am |---configure.ac |---Makefile.am |---.gitignore
从上面的目录结构能够看出:程序员
根目录有一个configure.ac
,这是构建系统的核心文件之一,描述整个构建的依赖和输出,是configure
脚本的原型。shell
每一个目录(包括根目录)都有一个Makefile.am
,这些文件是生成Makefile
的主要来源。使用Makefile.am的优势是能够结合configure.ac
、比手动编写Makefile
方便不少。安全
在src
目录下放置源代码,源代码被分红common
、core
、test
。common
用来实现一些可重用的代码,好比通用数据结构,内存管理,异常的封装;core
用来放置直接编译成可执行程序的代码,好比main.c等;test
用于编写单元测试程序。bash
build
目录用于存放编译过程当中的临时文件和编译获得了目标文件。通常咱们老是cd
在build
目录中,并执行../configure
来configure
,并在build目录下make。这样的话,由configure
产生的文件不会污染源码空间。咱们须要作的只是在.gitignore
中添加build/
。数据结构
在使用autoreconf的过程当中,还将在各个目录下生成其余的文件(尤为是根目录)。如今咱们只须要建立上述必要文件。框架
configure.ac
能够经过在根目录下执行autoscan
程序生成。若是你已经有一些代码了,使用autoscan生成configure.ac是个不错的开始。
每一个configure.ac
都须要以下两行。分别说明须要的autoconf的最低版本,以及程序的包名、版本、bug反馈邮件地址。
AC_PREREQ(2.59) AC_INIT([gnu-build], [1.0], [support@gnubuild.org])
configure.ac
通篇几乎都是采用这种相似函数调用的语法编写,这些称为宏
的语句,会被autoconf工具识别,并展开成相应的shell脚本,最终成为configure
脚本。除此以外,也能够混合地直接编写shell脚本。autoconf预置了不少实用的宏,能够减小工做量,后面你将看到宏
的价值。
能够直接编写shell脚本,可是推荐尽可能使用宏。由于shell程序有不少种(sh,bash,ksh,csh...),想要写出可移植的shell并非件容易的事情。
接着,一般使用AC_CONFIG_SRCDIR
来定位一个源代码文件,如此一来,autoconf程序会检查该文件是否存在,以确保autoconf的工做目录的正确性。这里,咱们指向src/core/main.c
。
AC_CONFIG_SRCDIR([src/core/main.c])
通常来讲,都会编写一个header
输出定义。这是咱们用到的第一个输出指令。输出指令告诉configure
,须要生成哪些文件。AC_CONFIG_HEADERS
的含义是在指定的目录生成.h
,通常叫作config.h
,你也能够指定其余名字。
AC_CONFIG_HEADERS([src/common/config.h])
那么这个config.h
究竟有什么用呢?回忆一下,configure
程序的主要目的是检测目标平台的软硬件环境,从而在实际调用make
命令编译程序前,对编译工做进行一个预先的配置,这里的配置落实到底,主要就是生成Makefile
和config.h
:
Makefile.am --> Makefile.in --> Makefile | configure* | config.h.in --> config.h
那么咱们的程序必须要经过某种方式,得知环境的不一样,从而经过预编译作出响应。这里的响应主要分两块:
对于源代码而言,经过config.h
中的宏定义,来改变编译行为。
对于Makefile.am而言,经过configure.ac
导出的变量,来动态改变Makefile。
在后面的叙述中,能够经过代码体会这两点。因此这里,为了让咱们的源码有能力根据环境来改变编译行为,生成config.h一般是必要的。
另外一个输出宏是AC_CONFIG_FILES
,针对这个例子,告诉autoconf,咱们须要输出Makefile文件:
AC_CONFIG_FILES([Makefile src/Makefile src/core/Makefile src/common/Makefile src/test/Makefile ]) AC_OUTPUT
注意到每一个目录都须要由对应的Makefile文件,这是automake多目录组织Makefile的通用作法。后面会讲到如何编写各个目录下的Makefile.am
。
AC_CONFIG_FILES
通常跟AC_OUTPUT
一块儿写在configure.ac
的最后部分。
为了配合automake,须要用AM_INIT_AUTOMAKE
初始化automake:
AM_INIT_AUTOMAKE([foreign])
这里foreign
是个可选项,设置foreign
跟调用automake --foreign
是等价的,前一篇有讲到。
配合使用libtool,须要加入LT_INIT
,这样autoreconf
会自动调用libtoolize
LT_INIT
configure能够帮助咱们检查编译和安装过程当中须要的系统工具是否存在。通常在进行其余检查前,先作此类检查。例以下面是一些经常使用的检查:
# 声明语言为C AC_LANG(C) # 检查cc AC_PROG_CC # 检查预编译器 AC_PROG_CXX # 检查ranlib AC_PROG_RANLIB # 检查lex程序,gnu下一般叫flex AC_PROG_LEX # 检查yacc,gnu下一般叫bison AC_PROG_YACC # 检查sed AC_PROG_SED # 检查install程序 AC_PROG_INSTALL # 检查ln -s AC_PROG_LN_S
针对这个例子咱们只须要检查cc
,cxx
就能够了。
Makefile.am
文件是一种更高层次的Makefile,抽象程度更高,比Makefile更容易编写,除了兼容Makefile语法外,一般只需包含一些变量定义便可。automake程序负责解析,并生成Makefile.in
,而Makefile.in从表现上与Makefile已经十分接近,只差变量替换了。configure脚本执行后,Makefile.in将最终转变成Makefile。
在本例中每一个目录下都有Makefile.am。根目录的Makefile.am生成的Makefile将是make程序的默认入口,可是根目录实际上并不包含任何须要构建的文件。对于须要引用子目录的Makefile来构建的时候,使用SUBDIRS
罗列包含其余Makefile.am的子目录。所以,对于根目录的Makefile.am只须要写一行:
SUBDIRS = src
同理,src目录下的Makefile.am只须要
SUBDIRS = common src test
对于包含有源代码文件的目录。首先,咱们须要定义编译的目标,目标多是库文件或可执行文件,目标又分为须要安装和不须要安装两种。例如对于common目录
下的源代码,咱们但愿生成一个不须要安装的库文件(使用libtool),由于这个库文件只在本项目内使用,那么common/Makefile.am
应当这样写:
noinst_LTLIBRARIES = libcommon.la libcommon_la_SOURCES = pool.c alloc.c list.c
定义了一个目标libcommon.la
。因为使用libtool,因此库文件必须以lib
开头,后缀为.la
。
目标的基本格式为where_PRIMARY = targets ...
where
表示安装位置,可选择bin、lib、noinst、check(make check时构建),还能够自定义。咱们着重讨论前三种:
bin
:表示安装到bindir目录下,这种状况下会编译出动态库
lib
:表示安装到libdir目录下,这种状况下会编译出动态库
noinst
:表示不安装,这种状况下会编译出静态库,在其余目标引用该目标时将进行静态连接
PRIMARY
能够是PROGRAMS
LIBRARIES
LTLIBRARIES
HEADERS
SCRIPTS
DATA
。着重讨论前三种:
PROGRAMS
:表示目标是可执行文件
LIBRARIES
:表示目标是库文件,经过后缀来区别静态库或动态库
LTLIBRARIES
:表示是libtool库文件,统一后缀为.la
与Makefile的思想同样,目标的生成须要定义来源,一般目标是有一些源程序文件获得的。Makefile.am中只需定义xxx_SOURCES
,后面跟随构建xxx这个目标须要的源代码文件列表便可。注意到xxx是目标的名字,而且.
字符须要使用_
代替。
core
目录下须要生成可执行目标,可是在连接时,须要用到libcommon.la
,此时core/Makefile.am
能够写成
bin_PROGRAMS = gnu-build GNU_BUILD_SOURCES = main.c GNU_BUILD_LIBADD = $(top_builddir)/src/common/libcommon.la
这里多了一行GNU_BUILD_LIBADD
,target_LIBADD的形式表示为target添加库文件的引用,这种引用是静态的仍是动态的取决于引用的库文件是否支持动态库,若是支持动态库,libtool优先采用动态连接。而因为libcommon.la
指定为noinst
,因此不可能以动态连接的形式存在,这里必然是静态连接。
$(top_builddir)
引用的是make发生时的工做目录,上文提到,咱们将在build目录下进行构建,那么库文件会生成在build目录下,而不是源码根目录下,因此$(top_builddir)
实际就是gnu-build/build
目录,而这样能够很好的支持在另外一个目录中编译程序。与之相对应的是$(top_srcdir)
对应的是源码的根目录,即gnu-build
目录。
还有多个能够配置用于改变编译和连接选项的配置项:
xxx_LDADD:为连接器增长参数,通常用于第三方库的引用。好比-L
-l
xxx_LIBADD:声明库文件引用,通常对于本项目中的库文件引用采用这种形式。
xxx_LDFLAG:连接器选项
xxx_CFLAGS:c编译选项,如-D
-I
xxx_CPPFLAGS:预编译选项
xxx_CXXFLAGS: c++编译选项
若是xxx是AM
,则表示全局target都采用这个选项。
刚刚提到的bindir
和libdir
是configure目录体系下的,相似的路径还有:
prefix /usr/local exec-prefix {prefix} bindir {exec-prefix}/bin libdir {exec-prefix}/lib includedir {prefix}/include datarootdir {prefix}/share datadir {datarootdir} mandir {datarootdir}/man infodir {datarootdir}/info ...
能够看到prefix
在这里的地位是一个顶层的路径,其余的路径直接或间接与之有关。而prefix的默认值为/usr/local
。因此可执行程序默认老是安装在/usr/local/bin
。用户老是能够在调用configure
脚本时经过--prefix
指定prefix。更详细的路径列表能够经过./configure --help
了解。
填充一些源代码后,就可使用autoreconf了,只须要在根目录下执行autoreconf --install
便可。
[root@xxx gnu-build]# autoreconf --install
前一篇中,对autoreconf的整个过程和产生的文件作了详尽的分析和阐述,读者也应该十分清楚这里将获得若干Makefile.in
和common/config.h.in
文件。
若是这个过程顺利的话,就能够在build目录下构建了:
# cd build # ../configure # make
这里configure后,会在build目录下生成对应位置的Makefile和common/config.h文件,而不是生成在源码目录中从而污染源码
至此,你已经完成了一个项目的基本构建框架,后面的事情,就是逐步完善构建对环境的依赖。
autoconf
为程序员提供的最为重要的功能就是提供了一种便捷、稳定、可移植的方式,让程序能在特定目标平台和目标环境上安全的编译运行程序。不过,autoconf
只是提供了一些宏,用来简化环境检查。而究竟要检查些什么,如何合理的利用这些宏完成目的,依旧是须要大量的积累的。笔者在这里对一些经常使用的宏进行一些介绍。
有些第三方库在安装到系统后,会附带安装若干可执行程序,并可在环境变量的支持下直接运行。有时,咱们经过检查此类可执行程序是否存在,来初步判断该第三方库是否已经安装在目标平台。其中一种经常使用的宏是AC_CHECK_PROGS
# 声明一个变量PERL,检查perl程序是否存在并可执行 # 若是不存在$PERL变量将是NOTFOUND,若是存在$PERL变量将是perl AC_CHECK_PROGS([PERL], [perl], [NOTFOUND]) # 声明一个变量TAR,检查tar和gtar程序是否存在并可执行 # 若是不存在$TAR变量将是:,若是存在,第一个可用的程序名将赋值给$TAR AC_CHECK_PROGS([TAR], [tar gtar], [:])
GNU软件有一种利用pkg-config,来进行自描述的机制。便可以经过注册软件自身(一般提供库文件的软件),让pkg-config可以返回库文件的安装路径等信息,以便以一种统一的方式提供给调用程序。有些库软件附带有独立的config程序,好比
pcre-config
和apr-1-config
。若是对这类库提供软件须要检查依赖和编译连接,一般能够经过AC_CHECK_PROGS
来检查config程序,从而获得编译连接选项。
打印消息能够做为调试手段,同时也能够在用户在configure过程当中,给予提示信息。
# error将终止configure AC_MSG_ERROR([zlib is required]) # warn不会终止configure AC_MSG_WARN([zlib is not found, xxx will not be support.])
注意到AC_MSG_ERROR
将中断configure的执行,通常用于必需的编译环境没法知足时。
检查某库是否存在是最重要的功能,由于咱们程序每每须要这些库,甚至是库中的某个函数的支持才能正确的运行。
使用AC_CHECK_LIB
检查库以及其中的函数是否存在,该宏的原型为:
AC_CHECK_LIB (library, function, [action-if-found],[action-if-not-found], [other-libraries])
library:须要检查的库名,无需lib
前缀,好比为了检查libssl
是否存在,这里须要传入ssl
function:这个库中的某个函数名
action-if-found:若是找到执行某个动做,这个动做能够是另外一个宏,能够是shell脚本。若是不指定这个参数,默认在LIBS
环境变量中增长-l
选项,从而将在连接过程当中将这个库连接进来。好比-lssl
。而且在config.h中定义一个宏HAVE_LIBlibrary
,例如HAVE_LIBSSL
。咱们的代码能够根据这个宏得知当前编译环境是否提供libssl
。
action-if-not-found:若是找不到则执行某个动做
经过下面几个宏能够检查系统是否包含某些头文件,以及是否支持某些函数:
AC_CHECK_FUNCS
:检查是否支持某些函数。做为检查的反作用,在config.h中会定义一个宏HAVE_funcs
(全大写)
AC_CHECK_HEADERS
:检查是否支持某些头文件。做为检查的反作用,在config.h中会定义一个宏HAVE_header_H
(全大写)
来举个例子,你们知道libiconv
是一个能够在不一样字符集间进行转化的库,若是咱们的程序但愿可以在不一样字符集间转化的字符串的话,可使用该库。然而,在不一样平台上,该库的移植方式有些区别。
gnu的标准c库(glibc)在很早的时候就把libiconv集成到了glibc中,所以在linux上能够无需额外的库支持便可使用iconv
。然而,在非linux上,极可能须要额外的libiconv
库。那么若是在非linux的平台上编写可移植的程序,能够参考以下的宏组合:
AC_CHECK_FUNCS(iconv_open, HAVE_ICONV=yes, []) if test "x$HAVE_ICONV" = "xyes"; then AC_CHECK_HEADERS(langinfo.h, [], AC_MSG_WARN([langinfo.h not found])) AC_CHECK_FUNCS([nl_langinfo], [], [AC_MSG_WARN([nl_langinfo not found])]) else AC_CHECK_LIB([iconv], [libiconv_open], [HAVE_ICONV=yes], [AC_MSG_WARN([no iconv found, will not build xm_charconv])]) if test "x$HAVE_ICONV" = "xyes"; then LIBICONV="-liconv" SAVED_LIBS=$LIBS LIBS="$LIBS $LIBICONV" AC_CHECK_HEADERS(langinfo.h, AC_CHECK_FUNCS([nl_langinfo], [], [AC_MSG_ERROR([nl_langinfo not found in your libiconv])]), AC_CHECK_FUNCS([locale_charset], [], [AC_MSG_ERROR([no langinfo.h nor locale_charset found in libiconv])])) LIBS=$SAVED_LIBS fi fi
在这个例子中,咱们能够看到许多技巧。咱们来逐一解读一下:
首先经过AC_CHECK_FUNCS
检查iconv_open
函数,若是在Linux平台上,一般该函数能够在没有任何额外库的状况下提供,因此HAVE_ICONV
这个临时变量将设置为yes
。
接着经过shell的if
测试判断临时变量HAVE_ICONV
是否为yes
。
若是已经检测到iconv,那么进一步检查langinfo.h
头文件和nl_langinfo
函数,不管是否能检查经过,因为使用了AC_MSG_WARN
,因此configure并不会失败退出,最多只是提示用户警告。更重要的是,咱们能够经过config.h中的宏,在代码中得知是否支持头文件和函数,从而调整编译分支。具体的在这个例子中这两个宏分别为HAVE_LANGINFO_H
和HAVE_NL_LANGINFO
。
在非linux下可能须要额外的libiconv库,因此在else
分支中,马上采用AC_CHECK_LIB
检测iconv
库,以及其中的libiconv_open
函数。一样的,若是存在,HAVE_ICONV
这个临时变量将设置为yes
。
在接下来的if测试中,使用到了$LIBS
变量,这是一个由编译器支持的变量,表示在连接阶段的额外库参数。当咱们检测到libiconv后,就给这个变量临时地添加-liconv
。这样接下来的AC_CHECK_FUNCS
时,能够利用$LIBS
在额外的库中查找函数。
检查langinfo.h
头文件,若是存在则再检查nl_langinfo
函数;若是不存在,则检查locale_charset
函数。从逻辑上看,要么langinfo.h
和nl_langinfo
同时存在,要么有locale_charset
函数,不然就终止configure。
最后重置$LIBS
变量。
configure脚本的检测结果应当有两个主要出口,一是config.h,它帮助咱们在源码中建立编译分支;二是Makefile.am
,咱们能够在Makefile.am
中基于这些导出的变量,改变构建方式。
有些宏能够自动帮咱们导出到config.h
,关于这一点上文已经有所阐述了。而但愿导出到Makefile.am则须要咱们本身手动调用相关宏。这里主要有两个宏:
AC_SUBST
:将一个临时变量,导出到Makefile.am。实际是在Makefile.in中声明一个变量,而且在生成Makefile时,由configure脚本对变量的值进行替换。
AM_CONDITIONAL
:由automake引入,可进行一个条件测试,从而决定是否导出变量。
例如,针对上面iconv的例子,咱们有个临时变量HAVE_ICONV
,若是iconv在当前平台可用,此时HAVE_ICONV
将会是yes
。因此可使用AM_CONDITIONAL
导出变量:
AM_CONDITIONAL([HAVE_ICONV], [test x$HAVE_ICONV != x])
或者不管如何都导出HAVE_ICONV
AC_SUBST(HAVE_ICONV)
在Makefile.am中,咱们能够对变量进行引用,这样xm_charconv.la就将在HAVE_ICONV导出的状况下构建:
if HAVE_ICONV xm_charconv_LTLIBRARIES = xm_charconv.la ... endif
不少软件都支持用户在configure阶段,可经过--with-xxx
--enable-xxx
等命令行选项对软件进行模块配置或编译配置。以--with-xxx
为例,咱们须要AC_ARG_WITH
宏:
AC_ARG_WITH(configfile, [ --with-configfile=FILE default config file to use], [ ZZ_CONFIGFILE="$withval"], [ ZZ_CONFIGFILE="${sysconfdir}/zz.conf"] ) AC_SUBST(ZZ_CONFIGFILE)
FILE
定义该参数的值应当是一个文件路径(DIR
要求一个目录路径),该宏须要提供一个默认值,这个例子中是${sysconfdir}/zz.conf
,${sysconfdir}
引用了${prefix}/etc
,而$withval
从命令行中引用--with-configfile
的值。
最后咱们经过AC_SUBST
导出一个临时变量。
上一节提到,导出的临时变量能够在Makefile.am中引用,因此咱们能够在Makefile.am中经过-D
传递给代码,从而在代码中经过宏来引用:
CFLAGS += -DCONFIGFILE=\"$(ZZ_CONFIGFILE)\"
本文以一个例子,一步步使用GNU构建系统来建立一个项目,并介绍了一些经常使用的检测宏。事实上,autotool还有不少宏,甚至能够自定义宏。可否合理利用autotool取决于程序员对可移植性这个问题的经验和理解。