7.1.2 创建编译脚本
不知道读者是否注意到,几乎前面编译的所有软件在进行安装时,仅仅通过定义环境变量DESTDIR为$SYSROOT,就安装到了目录/vita/sysroot下。如果Makefile全部是由程序员手工写的,不知道是否能做到如此整齐划一?很多手写的Makefile中,目标install的规则更多的是形如下面这个样子:
很难考虑到像下面这样周全:
因此,标准化的Makefile对GNU这种由来自世界各地的程序员共同参与开发的项目非常重要。
前面,我们已经领教了内核构建系统中的Makefile,从其复杂程度可见,对于具有多级目录、多个目标的复杂项目,编写和维护一个Makefile是多么繁重的一件事情。
鉴于类UNIX系统版本的多样性,GNU软件的源代码级的可移植就变得非常重要了,因此,编译脚本时必须要小心应对不同系统环境之间的差异。以我们的环境为例,同样一个软件,在不修改配置编译脚本的前提下,在宿主系统下,应该将编译器识别为gcc,而在交叉编译环境下,应该将编译器识别为i686-none-linux-gnu-gcc。这只是非常简单的一个例子,对于复杂的项目,情况要比这个糟糕得多。
于是饱受折磨的开发者们开发了GNU构建系统(GNU Build System),或者叫GNU自动构建工具(GNU Autotools),为了行文方便,我们简称其为Autotools。Autotools核心包括Autoconf和Automake。这里要准确理解“自动构建工具”的意义,所谓Autotools,并不是自动完成整个配置编译过程,而是自动构建配置脚本configure和Makefile。
(1)Autoconf
Autoconf的准确含义是自动创建自动配置脚本(automatically create automatic configuration scripts)。怎么理解自动配置脚本呢?简单来讲,就是自动探测各种不同系统的各种特性,如是本地编译还是交叉编译,系统中使用的编译器、链接器等程序是什么,编译以及链接程序时需要的头文件、动态库以及它们所在的路径,等等,达到自动动态适配,而不是硬编码到脚本中。
可以这样概括Autoconf的工作过程:将多个shell片段最终合并为一个完整的shell脚本,即configure。Autoconf使用宏来定义这些shell片段,开发者需要根据编译需要,使用这些宏组合Autoconf的元文件configure.ac,这个元文件曾经命名为configure.in,后来更改为configure.ac,但是Autoconf也向后兼容configure.in。然后Autoconf将元文件configure.ac中的宏展开为具体的配置脚本configure。
Autoconf程序本身使用shell脚本编写,但是Autoconf并没有使用shell完成宏展开功能,而是借助了GNU的M4来完成宏的展开。简单来讲,M4就是将输入的宏名转换为宏定义,也就是说,M4的输入是宏名,而输出是shell脚本片段。Autoconf使用M4定义了一些内置的宏,并且基于M4之上又封装了一层宏,目的是为了更符合Autoconf的需求,Autoconf封装的宏一般以"AC_"开头。其他程序可以使用Autoconf封装的这些宏,或者直接使用M4定义自己的宏,但是最终,本质上都是M4宏。
因为M4宏定义很多是第三方程序提供的,可能安装在系统的多个位置,因此GNU自动构建系统编写了程序aclocal负责将这些宏定义收集到文件aclocal.m4,保存在源码的顶层目录下,供自动构建系统使用。
(2)Automake
同Autoconf类似,Automake的准确含义是"automatically generate makefile.in",开发人员只需编写一个简单的元文件,在其中描述必要的诉求:比如构建一个二进制程序,使用的源代码文件是什么,链接某某库等即可。其他的都交由Automake全权处理吧。Automake将创建一个标准的Makefile文件,包括补全开发者不愿意编写的那些琐碎的规则,如install、clean、distclean、dist等。
Automake的输出事实上是一个Makefile模板,命名为Makefile.in。然后,configure脚本使用探测到的值替换模板Makefile.in中的变量,创建最终的Makefile。显然,这种方式要比我们将所有的变量定义全部硬编码到Makefile中的做法可移植性更好。
综上,使用GNU Autotools创建Makefile的过程可以分为如下几个步骤:
1)编写元文件configure.ac。
2)执行aclocal。aclocal将扫描configure.ac中使用的M4宏,并到系统中收集这些宏的定义,然后将这些宏定义复制到源码顶层目录下的aclocal.m4中。
3)调用autoconf,将configure.ac中的宏展开为shell脚本形式的configure。
4)编写元文件Makefile.am。
5)调用automake。automake根据Makefile.am创建Makefile的模板文件Makefile.in。
6)执行脚本configure。configure探测系统环境,并使用探测到的值替换模板Makefile.in中的变量,生成具体的Makefile。
从上面的讨论可以看出,对于开发者来说,主要的工作就是创建元文件configure.ac和Makefile.am,其他的全部交给Autotools。Autotools极大地减轻了程序开发人员的负担,将烦琐的编写的Makefile任务转嫁给了Autotools的开发和维护者。
既然Autotools有如此多的优点,所以即使我们的迷你窗口管理器很小,我们还是可以借助它感同身受一下Autotools带来的好处。我们这里绝非“杀鸡用牛刀”,而是希望读者借助这个例子,可以切身体会一下Autotools,这样无论是在大型项目中使用Autotools,或者为GNU软件贡献源码,亦或基于使用Autotools的项目进行二次开发,都会大有益处。
1.创建configure
我们将这个迷你窗口管理器命名为winman,使用winman作为顶层目录的名字,在顶层目录下创建一个子目录src用来存放源代码。我们基于Xlib,使用C语言编写winman。因此,configure.ac中除了Autoconf要求的必选的宏外,最重要的就是检查C编译器和X的库了,其内容如下:
Autoconf要求configure.ac以宏AC_INIT作为开头,该宏由Autoconf定义,接收一些基本信息,如软件包的名称,版本号,开发或者维护人员的Email等。制作发布的软件包时,将用到这些信息。
宏AM_INIT_AUTOMAKE由Automake定义,用来进行与Automake相关的初始化工作,只要使用Automake,这个宏也是必选的。在默认情况下,Automake会检查项目目录中是否包含NEWS、README、ChangeLog等文件,为简单起见,我们给Automake传递了"foreign"参数,明确告诉Automake我们的项目不需要包含这些文件。
宏ACPROG_CC用来检测C编译器,根据该宏名的前缀"AC"就可判断出其他由Autoconf定义。其将在系统内搜索C编译器,并定义变量CC指向搜索到的C编译器。
接下来,我们使用软件包pkg-config提供的宏PKG_CHECK_MODULES检测X的库。该宏将定义两个变量,分别是宏的第一个参数加上后缀"_CFLAGS"和"_LIBS",这里就是X_CFLAGS和X_LIBS。如果查看congfigure脚本就可以发现,这个宏定义的核心其实就是执行命令"pkg-config—cflags x11"和"pkg-config—libs x11"。
宏AC_CONFIG_FILES告诉Automake生成哪些Makefile模板文件Makefile.in。这里,需要在顶层目录和src目录下分别创建Makefile.in文件。
在configure.ac的最后,Autoconf要求必须以宏AC_OUTPUT结束configure.ac。
准备好configure.ac后,我们使用如下命令生成脚本configure:
2.生成Makefile
窗口管理器的源码保存在顶层目录下的子目录src中,我们在顶层目录和子目录src下面分别需要编写Automake元文件Makefile.am。
顶层目录winman下的Makefile.am如下:
因为顶层目录下基本没有任何操作,所以该Makefile.am非常简单,只是通过变量SUBDIRS告诉Automake,需要递归编译子目录src。
子目录src下的Makefile.am如下:
变量bin_PROGRAMS指定了编译最后创建的二进制可执行文件的名称,该变量由两部分构成,其中"bin"表示安装在目录$prefix/bin下,"PROGRAMS"指明最后创建的文件是一个可执行文件。
winman_SOURCES表示创建winman需要的源文件;winman_CFLAGS和winman_LDADD分别表示编译链接时需要传递给编译器和链接器的参数。X_CFLAGS和X_LIBS已经在前面的configure.ac中由宏PKG_CHECK_MODULES定义了。
Automake的元文件已经准备就绪,我们使用下面的命令创建Makefile的模板Makefile.in:
其中选项"—add-missing"和"—copy"是告诉automake将其需要的一些脚本文件,比如install-sh等,直接复制到项目目录中,而不是建立这些脚本文件的链接。这么做是为了分发到其他系统时,避免因为脚本位置不同或者系统中没有安装相应脚本而导致编译链接失败。
上述命令执行后,将分别在顶层目录和子目录src下创建Makefile的模板Makefile.in。
最后,执行confugure脚本探测编译过程所需的各个变量,然后用探测到的具体的值替换Makefile.in中的变量,比如X_CFLAGS、X_LIBS,生成Makefile文件: