GNU从头构建系统实践

系统 Linux
本篇从我的实践总结的角度,并阐述如何从头开始规划一个基于GNU构建系统的项目。事实上,随着开发者对跨平台认知的深入和完善,才能逐渐掌握GNU构建。注意:本文的例子不依赖于任何IDE和编辑器。这样读者可以从根本上认识到每个文件的作用。

[[172534]]

在上一篇概念:GNU构建系统和Autotool,我对GNU构建系统从用户视角和开发者视角分别进行了阐述。本篇从我的实践总结的角度,并阐述如何从头开始规划一个基于GNU构建系统的项目。事实上,随着开发者对跨平台认知的深入和完善,才能逐渐掌握GNU构建。注意:本文的例子不依赖于任何IDE和编辑器。这样读者可以从根本上认识到每个文件的作用。

安装autotools

需要安装的工具包括autoconf、automake、libtool。

目录结构规划

首先,我们需要规划项目的目录结构。假设,我们的项目叫gnu-build。设想如下目录结构:

  1. gnu-build 
  2.  
  3. |---build(用于编译) 
  4.  
  5. |---src 
  6.  
  7. |---common 
  8.  
  9. |---Makefile.am 
  10.  
  11. |---pool.c 
  12.  
  13. |---alloc.c 
  14.  
  15. |---list.c 
  16.  
  17. |... 
  18.  
  19. |---core 
  20.  
  21. |---Makefile.am 
  22.  
  23. |---main.c 
  24.  
  25. |... 
  26.  
  27. |---test 
  28.  
  29. |---Makefile.am 
  30.  
  31. |---test.c 
  32.  
  33. |... 
  34.  
  35. |---Makefile.am 
  36.  
  37. |---configure.ac 
  38.  
  39. |---Makefile.am 
  40.  
  41. |---.gitignore  

从上面的目录结构可以看出:

  1. 根目录有一个configure.ac,这是构建系统的核心文件之一,描述整个构建的依赖和输出,是configure脚本的原型。
  2. 每个目录(包括根目录)都有一个Makefile.am,这些文件是生成Makefile的主要来源。使用Makefile.am的优点是可以结合configure.ac、比手动编写Makefile方便很多。
  3. 在src目录下放置源代码,源代码被分成common、core、test。common用来实现一些可重用的代码,比如通用数据结构,内存管理,异常的封装;core用来放置直接编译成可执行程序的代码,比如main.c等;test用于编写单元测试程序。
  4. build目录用于存放编译过程中的临时文件和编译得到了目标文件。一般我们总是cd在build目录中,并执行../configure来configure,并在build目录下make。这样的话,由configure产生的文件不会污染源码空间。我们需要做的只是在.gitignore中添加build/。

在使用autoreconf的过程中,还将在各个目录下生成其他的文件(尤其是根目录)。现在我们只需要创建上述必要文件。

configure.ac可以通过在根目录下执行autoscan程序生成。如果你已经有一些代码了,使用autoscan生成configure.ac是个不错的开始。

configure.ac的基本编写

通用宏

每个configure.ac都需要如下两行。分别说明需要的autoconf的最低版本,以及程序的包名、版本、bug反馈邮件地址。

  1. AC_PREREQ(2.59) 
  2.  
  3. 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。

  1. AC_CONFIG_SRCDIR([src/core/main.c]) 

定义输出的宏

一般来说,都会编写一个header输出定义。这是我们用到的第一个输出指令。输出指令告诉configure,需要生成哪些文件。AC_CONFIG_HEADERS的含义是在指定的目录生成.h,一般叫做config.h,你也可以指定其他名字。

  1. AC_CONFIG_HEADERS([src/common/config.h]) 

那么这个config.h究竟有什么用呢?回忆一下,configure程序的主要目的是检测目标平台的软硬件环境,从而在实际调用make命令编译程序前,对编译工作进行一个预先的配置,这里的配置落实到底,主要就是生成Makefile和config.h:

  1. Makefile.am --> Makefile.in --> Makefile 
  2.                              | 
  3.                            configure* 
  4.                              | 
  5.                 config.h.in --> config.h  

那么我们的程序必需要通过某种方式,得知环境的不同,从而通过预编译做出响应。这里的响应主要分两块:

  1. 对于源代码而言,通过config.h中的宏定义,来改变编译行为。
  2. 对于Makefile.am而言,通过configure.ac导出的变量,来动态改变Makefile。

在后面的叙述中,可以通过代码体会这两点。所以这里,为了让我们的源码有能力根据环境来改变编译行为,生成config.h通常是必要的。

另一个输出宏是AC_CONFIG_FILES,针对这个例子,告诉autoconf,我们需要输出Makefile文件:

  1. AC_CONFIG_FILES([Makefile 
  2.                  src/Makefile 
  3.                  src/core/Makefile 
  4.                  src/common/Makefile 
  5.                  src/test/Makefile 
  6.                  ]) 
  7. AC_OUTPUT  

注意到每个目录都需要由对应的Makefile文件,这是automake多目录组织Makefile的通用做法。后面会讲到如何编写各个目录下的Makefile.am。

AC_CONFIG_FILES一般跟AC_OUTPUT一起写在configure.ac的最后部分。

automake声明

为了配合automake,需要用AM_INIT_AUTOMAKE初始化automake:

  1. AM_INIT_AUTOMAKE([foreign]) 

这里foreign是个可选项,设置foreign跟调用automake --foreign是等价的,前一篇有讲到。

libtool声明

配合使用libtool,需要加入LT_INIT,这样autoreconf会自动调用libtoolize

  1. LT_INIT 

编译器检查

configure可以帮助我们检查编译和安装过程中需要的系统工具是否存在。一般在进行其他检查前,先做此类检查。例如下面是一些常用的检查:

  1. # 声明语言为C 
  2. AC_LANG(C) 
  3.  
  4. # 检查cc 
  5. AC_PROG_CC 
  6.  
  7. # 检查预编译器 
  8. AC_PROG_CXX 
  9.  
  10. # 检查ranlib 
  11. AC_PROG_RANLIB 
  12.  
  13. # 检查lex程序,gnu下通常叫flex 
  14. AC_PROG_LEX 
  15.  
  16. # 检查yacc,gnu下通常叫bison 
  17. AC_PROG_YACC 
  18.  
  19. # 检查sed 
  20. AC_PROG_SED 
  21.  
  22. # 检查install程序 
  23. AC_PROG_INSTALL 
  24.  
  25. # 检查ln -s 
  26. AC_PROG_LN_S  

针对这个例子我们只需要检查cc,cxx就可以了。

Makefile.am的基本编写

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只需要写一行:

  1. SUBDIRS = src 

同理,src目录下的Makefile.am只需要

  1. SUBDIRS = common src test 

定义目标

对于包含有源代码文件的目录。首先,我们需要定义编译的目标,目标可能是库文件或可执行文件,目标又分为需要安装和不需要安装两种。例如对于common目录

下的源代码,我们希望生成一个不需要安装的库文件(使用libtool),因为这个库文件只在本项目内使用,那么common/Makefile.am应当这样写:

  1. noinst_LTLIBRARIES = libcommon.la 
  2.  
  3. 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可以写成

  1. bin_PROGRAMS = gnu-build 
  2.  
  3. GNU_BUILD_SOURCES = main.c 
  4.  
  5. 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目录体系下的,类似的路径还有:

  1. prefix                /usr/local 
  2. exec-prefix            {prefix} 
  3. bindir                {exec-prefix}/bin 
  4. libdir                {exec-prefix}/lib 
  5. includedir            {prefix}/include 
  6. datarootdir            {prefix}/share 
  7. datadir             {datarootdir} 
  8. mandir                {datarootdir}/man 
  9. infodir                {datarootdir}/info 
  10. ...  

可以看到prefix在这里的地位是一个顶层的路径,其他的路径直接或间接与之有关。而prefix的默认值为/usr/local。所以可执行程序默认总是安装在/usr/local/bin。用户总是可以在调用configure脚本时通过--prefix指定prefix。更详细的路径列表可以通过./configure --help了解。

开始构建

填充一些源代码后,就可以使用autoreconf了,只需要在根目录下执行autoreconf --install即可。

  1. [root@xxx gnu-build]# autoreconf --install 

前一篇中,对autoreconf的整个过程和产生的文件做了详尽的分析和阐述,读者也应该十分清楚这里将得到若干Makefile.in和common/config.h.in文件。

如果这个过程顺利的话,就可以在build目录下构建了:

  1. # cd build 
  2.  
  3. # ../configure 
  4.  
  5. # make  

这里configure后,会在build目录下生成对应位置的Makefile和common/config.h文件,而不是生成在源码目录中从而污染源码

至此,你已经完成了一个项目的基本构建框架,后面的事情,就是逐步完善构建对环境的依赖。

在configure.ac中配置环境检查

autoconf为程序员提供的最为重要的功能就是提供了一种便捷、稳定、可移植的方式,让程序能在特定目标平台和目标环境上安全的编译运行程序。不过,autoconf只是提供了一些宏,用来简化环境检查。而究竟要检查些什么,如何合理的利用这些宏完成目的,依旧是需要大量的积累的。笔者在这里对一些常用的宏进行一些介绍。

可执行文件检查

有些第三方库在安装到系统后,会附带安装若干可执行程序,并可在环境变量的支持下直接运行。有时,我们通过检查此类可执行程序是否存在,来初步判断该第三方库是否已经安装在目标平台。其中一种常用的宏是AC_CHECK_PROGS

  1. # 声明一个变量PERL,检查perl程序是否存在并可执行 
  2. # 如果不存在$PERL变量将是NOTFOUND,如果存在$PERL变量将是perl 
  3. AC_CHECK_PROGS([PERL], [perl], [NOTFOUND]) 
  4.  
  5. # 声明一个变量TAR,检查tar和gtar程序是否存在并可执行 
  6. # 如果不存在$TAR变量将是:,如果存在,第一个可用的程序名将赋值给$TAR 
  7. AC_CHECK_PROGS([TAR], [tar gtar], [:])  

GNU软件有一种利用pkg-config,来进行自描述的机制。即可以通过注册软件自身(通常提供库文件的软件),让pkg-config能够返回库文件的安装路径等信息,以便以一种统一的方式提供给调用程序。有些库软件附带有独立的config程序,比如pcre-config和apr-1-config。如果对这类库提供软件需要检查依赖和编译链接,通常可以通过AC_CHECK_PROGS来检查config程序,从而得到编译链接选项。

打印消息宏

打印消息可以作为调试手段,同时也可以在用户在configure过程中,给予提示信息。

  1. # error将终止configure 
  2. AC_MSG_ERROR([zlib is required]) 
  3.  
  4. # warn不会终止configure 
  5. AC_MSG_WARN([zlib is not found, xxx will not be support.])  

注意到AC_MSG_ERROR将中断configure的执行,一般用于必需的编译环境无法满足时。

库检查宏

检查某库是否存在是最重要的功能,因为我们程序往往需要这些库,甚至是库中的某个函数的支持才能正确的运行。

使用AC_CHECK_LIB检查库以及其中的函数是否存在,该宏的原型为: 

  1. 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的平台上编写可移植的程序,可以参考如下的宏组合: 

  1. AC_CHECK_FUNCS(iconv_open, HAVE_ICONV=yes, []) 
  2. if test "x$HAVE_ICONV" = "xyes"then 
  3.      AC_CHECK_HEADERS(langinfo.h, [], AC_MSG_WARN([langinfo.h not found])) 
  4.      AC_CHECK_FUNCS([nl_langinfo], [], [AC_MSG_WARN([nl_langinfo not found])]) 
  5. else 
  6.     AC_CHECK_LIB([iconv], [libiconv_open], [HAVE_ICONV=yes], [AC_MSG_WARN([no iconv found, will not build xm_charconv])]) 
  7.     if test "x$HAVE_ICONV" = "xyes"then 
  8.         LIBICONV="-liconv" 
  9.         SAVED_LIBS=$LIBS 
  10.         LIBS="$LIBS $LIBICONV" 
  11.         AC_CHECK_HEADERS(langinfo.h,  
  12.                      AC_CHECK_FUNCS([nl_langinfo], [], [AC_MSG_ERROR([nl_langinfo not found in your libiconv])]),  
  13.                      AC_CHECK_FUNCS([locale_charset], [], [AC_MSG_ERROR([no langinfo.h nor locale_charset found in libiconv])])) 
  14.         LIBS=$SAVED_LIBS 
  15.     fi 
  16. fi  

在这个例子中,我们可以看到许多技巧。我们来逐一解读一下:

  1. 首先通过AC_CHECK_FUNCS检查iconv_open函数,如果在Linux平台上,通常该函数可以在没有任何额外库的情况下提供,所以HAVE_ICONV这个临时变量将设置为yes。
  2. 接着通过shell的if测试判断临时变量HAVE_ICONV是否为yes。
  3. 如果已经检测到iconv,那么进一步检查langinfo.h头文件和nl_langinfo函数,无论是否能检查通过,由于使用了AC_MSG_WARN,所以configure并不会失败退出,最多只是提示用户警告。更重要的是,我们可以通过config.h中的宏,在代码中得知是否支持头文件和函数,从而调整编译分支。具体的在这个例子中这两个宏分别为HAVE_LANGINFO_H和HAVE_NL_LANGINFO。
  4. 在非linux下可能需要额外的libiconv库,所以在else分支中,立刻采用AC_CHECK_LIB检测iconv库,以及其中的libiconv_open函数。同样的,如果存在,HAVE_ICONV这个临时变量将设置为yes。
  5. 在接下来的if测试中,使用到了$LIBS变量,这是一个由编译器支持的变量,表示在链接阶段的额外库参数。当我们检测到libiconv后,就给这个变量临时地添加-liconv。这样接下来的AC_CHECK_FUNCS时,可以利用$LIBS在额外的库中查找函数。
  6. 检查langinfo.h头文件,如果存在则再检查nl_langinfo函数;如果不存在,则检查locale_charset函数。从逻辑上看,要么langinfo.h和nl_langinfo同时存在,要么有locale_charset函数,否则就终止configure。
  7. 最后重置$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导出变量:

  1. AM_CONDITIONAL([HAVE_ICONV], [test x$HAVE_ICONV != x]) 

或者无论如何都导出HAVE_ICONV

  1. AC_SUBST(HAVE_ICONV) 

在Makefile.am中,我们可以对变量进行引用,这样xm_charconv.la就将在HAVE_ICONV导出的情况下构建:

  1. if HAVE_ICONV 
  2.   xm_charconv_LTLIBRARIES = xm_charconv.la 
  3.   ... 
  4. endif  

提供额外用户参数支持

很多软件都支持用户在configure阶段,可通过--with-xxx --enable-xxx等命令行选项对软件进行模块配置或编译配置。以--with-xxx为例,我们需要AC_ARG_WITH宏:

  1. AC_ARG_WITH(configfile, 
  2.   [  --with-configfile=FILE   default config file to use], 
  3.   [ ZZ_CONFIGFILE="$withval"], 
  4.   [ ZZ_CONFIGFILE="${sysconfdir}/zz.conf"
  5.   ) 
  6.  
  7. AC_SUBST(ZZ_CONFIGFILE)  

FILE定义该参数的值应当是一个文件路径(DIR要求一个目录路径),该宏需要提供一个默认值,这个例子中是${sysconfdir}/zz.conf,${sysconfdir}引用了${prefix}/etc,而$withval从命令行中引用--with-configfile的值。

最后我们通过AC_SUBST导出一个临时变量。

上一节提到,导出的临时变量可以在Makefile.am中引用,所以我们可以在Makefile.am中通过-D传递给代码,从而在代码中通过宏来引用:

  1. CFLAGS += -DCONFIGFILE=\"$(ZZ_CONFIGFILE)\" 

总结

本文以一个例子,一步步使用GNU构建系统来创建一个项目,并介绍了一些常用的检测宏。事实上,autotool还有很多宏,甚至可以自定义宏。能否合理利用autotool取决于程序员对可移植性这个问题的经验和理解。

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2016-09-28 21:50:29

GNUAutotoolLinux

2024-03-01 13:49:00

数据训练

2023-04-14 11:04:43

2023-06-12 15:43:44

鸿蒙智能家居开发

2023-03-09 15:15:21

鸿蒙模块编译

2016-09-20 13:02:12

CLinuxAutotool

2024-02-21 14:07:00

2013-04-10 10:59:45

Linux系统监控collectl

2021-05-24 15:48:38

高德打车系统可观测性

2024-06-13 08:36:11

2022-05-07 19:51:22

微软WindowsWindows 12

2023-08-11 17:30:54

决策树机器学习算法

2019-04-04 09:19:08

日志京东流式计算

2024-08-15 14:48:57

2012-09-29 10:09:19

网站架构后台构建架构

2019-12-16 12:11:53

Docker容器Kubernetes

2022-11-14 10:49:33

Linux发行版

2024-09-26 16:51:23

2022-07-22 07:18:53

代码DeepMind

2009-12-18 09:48:26

Linux中应用
点赞
收藏

51CTO技术栈公众号