软件开发工具——Make
掌握Makefile的使用方法和工作流程;
掌握make工具变量的相关知识,包括其引用、定义及分类等;
掌握Makefile常见的函数含义;
掌握Makefile与shell命令行的通信方法;
掌握Makefile的常见语法规则,包括显式规则、隐式规则及静态模式规则;
了解autotools的用法,了解autotools中常用的工具链以及如何使用工具链自动创建Makefile文件。
1、Make工具概述
Makefile带来的好处就是“自动化编译”,一旦写好,只需要在shell命令行中输入一个make命令,整个工程完全自动编译,可以极大提高软件开发的效率。
make是一个命令工具,它解释Makefile中的语法规则。Makefile有自己的书写格式、关键字和函数,这个文件告诉make以何种方式编译源代码和链接程序。典型地,可执行文件可由一些.o文件按照一定的顺序编译,如果在工程中已经存在Makefile,当对工程中的若干源文件修改以后,可自动根据修改情况完成源文件对应.o文件的更新、库文件的更新、最终的可执行程序的更新。
那么如果来判断一个大型工程中哪些文件发生了改变,哪些没发生变化呢?
Linun系统判断工程中文件是否发生改变的方法,是判断文件的建立和修改时间,即“文件时间戳”,Makefile文件就是基于这种“时间戳”来运行的。如果系统判断“文件时间戳”晚于上次编译的“时间戳”,就表明此文件在上次编译之后重新被修改过,需要重新编译;如果时间戳同上次一样没有变化,就表明没有修改,不需要重新编译。
2、Makefile起步
我们的规则是:
(1)如果工程没有编译过,那么所有C文件都要编译并被链接;
(2)如果工程的某几个C文件被修改,那么只编译被修改的C文件,并链接目标工程;
(3)如果工程的头文件被改变了,那么需要编译引用了这几个头文件的C文件,并链接目标程序。
Makefile的大致结构:
TRAGET: Dependency file
<TAB> COMMAND
TRAGET: Dependency file
<TAB> COMMAND
......
TRAGET: Dependency file
<TAB> COMMAND
在Makefile结构中,TARGET、Dependency file、COMMAND是相互关联、密切联系的3部分。
TARGET:表示make工具创建的目标体,通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。可以是.o文件,也可以是最后的可执行程序的文件名等。
Dependency file:表示要创建目标体需要的依赖文件,通常一个目标依赖于一个或者多个文件。如果其中一个文件比目标文件的“时间戳”新,这个目标就认为是“过时的”,需要重新编译生成。
COMMAND:表示创建目标体时需要运行的命令,它限定了make执行这条规则时需要的动作,通常可由一行或者多行命令组成。如果COMMAND不与“TARGET: Dependency ifle”在一行,那么它必须以[TAB]字符作为本行的开头,[TAB]字符告诉make此行是一个命令行;如果与"TARGET: Dependency file“在一行,那么可以使用分号作为分隔符,如果COMMAND命令太长,可使用反斜杠(\)作为换行符。
注:Makefile中“#”字符后的内容被当作是注释内容。如果此行的第一个非空字符为“#”,表示此行为注释行。当在Makefile中使用真实的字符“#”时,可以使用反斜杠加“#”(\#)来实现,它表示将“#”作为一个普通字符而不是注释的开始标志。
Makefile的一般工作过程描述:
(1)读取Makefile。根据make的执行选项,查找当前的目录或者其他目录要执行的Makefile。
(2)初始化Makefile。将制定的Makefile中的变量进行替换,如果该Makefile中包含其他的文件,则将其加载。
(3)解释规则。将Makefile中的执行规则进行解析,同时推导文件中的隐藏规则,其次,查找文件中目标、依赖、命令之间的关系,为创建目标建立关系链。
(4)分析变更。根据依赖关系和“时间戳”,判断是否有依赖文件发生变化,如果有变化,则进行重新编译;如果没有变化,当前的目标不需要重新编译。
(5)执行。执行Makefile中的命令。
Makefile编写完毕,就可以执行make命令进行编译操作。make的执行同其他命令一样,也有丰富的选项供用户选择,可以完成不同的功能。make工具的常用选项:
-f file 将指定当前目录下的file作为Makefile
-I dir 将dir作为被包含的Makefile所在目录
-C dir 将指定目录下的file作为Makefile
-i 忽略所有命令执行错误
-j 输出规则中命令的详细信息
-n 只打印要执行的命令,但不执行这些命令
-s 在执行命令时不显示命令
-d 除打印正常的操作信息外,还打印调试信息
一个目标可以没有依赖文件,只有命令,比如Makefile中的伪命令“clean“表示删除make过程中的中间文件,它就没有依赖,只有命令。伪命令是为其他命令服务的,不是强制性的。伪命令一般包括clean(删除中间文件)、install(安装编译好的程序)、uninstall(卸载已安装的程序)以及print(输出发生改变的源文件)等。
3、Makefile变量
在Makefile中,变量是一个名字,它不仅可以代表一个文本字符串,而且可以用来代表文件名、编译选项、程序运行的选项参数、搜索源文件的目录,以及编译输出的目录。在Makefile的目标、依赖、命令中任意引用变量的地方,在执行make命令后,都会被变量定义的值所取代。
在Makefile中变量有以下几个特征:
(1)Makefile中变量和函数的展开(除规则命令行中的变量和函数以外),是在make读取Makefile文件时进行的,这里的变量不仅包括使用“=”定义的变量,而且包含使用指示符“define”定义的。
(2)变量的命名可以包含字符、数字、下划线,但绝对不可以使用含有“:”、“#”、“=”或是空字符(空格、回车等)的字符,同时变量中字母、数字以及下划线以外的字符,用户应尽量避免使用,因为它们可能赋予其他特别的含义。
(3)变量中的大小写也是非常敏感的。推荐的方法是对于内部定义的一般 变量使用小写方式,而对于一些参数列表(例如:编译选项CFLAGS)采用大写方式。
(4)有一些变量名只包含一个或者很少的几个特殊字符,称它们为自动化变量。像“$”、“$@”、“$?”、“$*”等,这些变量用户在定义中也不可以使用。
当定义了一个变量后,就可以在Makefile的很多地方使用这个变量。变量的引用方式是:
$(VARIABLE_ANME) 或者 ${VARIABLE_NAME}
美元符号“$”在Makefile中有特殊的含义,所有在命令或者文件中使用“$”时需要用两个美元符号“$$”来表示。
在Makefile中对一些简单变量的引用,也可以不使用“()”和“{}”来标记变量,而直接使用“$x”的格式来实现,此种用法仅限于变量名为单字符的情况。
Makefile文件在进行变量定义时通常可以采用两种方式,第一种是递归展开定义法,另一种是直接展开定义法。两者虽然都可以对需要的变量进行定义,但是也存在一些差异,主要的不同在于定义的方式和展开的时机不同,递归展开定义法可以使用之前没有定义过的变量,而直接展开定义法不允许引用变量之后定义过的变量。
(1)递归展开定义
这种变量定义法是通过“=”或者指示符“define”来定义变量。其格式如下:
Var=variable
其中,Var是变量名,variable是赋予变量Var的值。
对使用递归展开定义的变量,其引用的地方是严格的文本替换过程。变量将会原样地被字符串替代。如果此变量定义中存在对其他变量的引用,那么被引用的变量会在此变量被展开的同时被展开。
(2)直接展开定义
为避免“递归展开法”定义变量出现的死循环和效率低的问题,Makefile中可使用另外一种变量定义的方式,称为直接展开定义。这种方式使用“:=”定义变量。其格式如下:
Var := variable
同递归定义法不同,直接展开定义法在调用变量时,变量值对另外变量的引用在定义时被展开。所以在变量被定义后就是一个实际所需要定义的文本串,不再包含任何其他变量的引用。其次,需要注意的是,使用直接定义法定义变量时,不能对其后定义的变量进行引用。
一般而言,在复杂的Makefile中,推荐使用直接展开式变量,因为这种变量的使用方式和大多数编程语言中的变量使用方式基本相同。它可以使一个比较复杂的Makefile在一定程度上具有可预测性,而且这种变量允许用户利用之前定义的值来重新定义。因此,在Makefile变量定义时,应尽量避免和减少递归方式的使用。
(3)变量嵌套定义
在Makefile中还有一种变量高级使用方法,称为“变量嵌套定义”。它表示在一个变量中可以包含对其他变量的引用。
例如:
variable1 = variable2
varialbe2 = variable3
w := $($(variable1))
(4)替换引用定义
对于一个已经定义的变量,可以使用“替换引用”将其值中的后缀字符(串)使用指定的字符(串)替换。格式是:
$(VAR:A=B) 或者 ${VAR:A=B}
意思是,替换变量VAR中所有A字符结尾的字为B结尾的字。“结尾”的含义是空格之前(变量值多个字之间使用空格分开)。而对于变量其他部分的“A"字符不进行替换。例如:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
变量分类:
除用户自己定义的变量(称为自定义变量)外,Makefile文件中还存在3中重要的变量,它们分别是:预定义变量、自动变量和环境变量。这3中变量是系统级变量,都有其默认值,用户一般不需要在Makefile中重新定义,可在Makefile中直接引用(也可根据需要替换其默认值)。
(1)预定义变量
预定义变量是进行程序预编译时经常使用的变量。有时候使用GCC进行程序预编译时,通常对某些编译的选项多次使用,预编译变量使复杂的编译选项能够 更条理、更直观。
AR 库文件维护程序的名称,默认值为ar
AS 汇编程序的名称,默认值为as
CC c编译器的名称
CPP c预编译器的名称,默认值为$(CC) -E
CXX c++编译器的名称,默认值为g++
PC Pascal编译器的名称
FC Fortran编译器的名称,默认值为f77
RM 文件删除程序的名称,默认值为rm -f
ARFLAGS 库文件维护程序的选项,无默认值
ASFLAGS 汇编程序的选项,无默认值
CFLAGS c编译器的选项,无默认值
CPPFLAGS c预编译的选项,无默认值
CXXFLAGS c++编译器的选项,无默认值
FFLAGS Fortran编译器的选项,无默认值
其中,最重要也是最经常使用的变量是CC和CFLAGS。由于CC没有默认值,因此经常要把“CC=gcc”、“CC=arm-linux-gcc”等放到变量定义中。
(2)自动变量
为了简化Makefile的编写,Makefile引入自动变量,自动变量可以代表编译语句中出现的目标文件和依赖文件等。使用自动变量可以为Makefile的编写提供方便。
$@ 表示当前规则中的完整目标文件名
$? 新修改过的依赖文件列表,即所有时间戳比目标文件晚的依赖文件,并以空格分开
$* 不包含扩展名的目标文件名
$< 当前规则中的第一个依赖文件名
$% 当目标文件为库文件时,该变量为库文件名
$^ 所有依赖文件,以空格分开,不包含重复的依赖文件
$+ 所有依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
(3)环境变量
make命令在运行时,系统中所有的环境变量对它都是可见的。在Makefile中,可以引用任何已定义的系统环境变量。(这里我们区分系统环境变量和make环境变量,系统环境变量是这个系统所有用户所拥有的,而make的环境变量只是对于make的一次执行过程有效。)
Makefile中最常见的环境变量是VPATH。通常在一些大的工程中,有大量的源文件,并放在不同的目录中。所以,当make需要寻找文件依赖关系时,就要在文件前加上路径。Makefile文件中的环境变量“VPATH”就是完成添加路径的功能,如果没有指明这个变量,make只会在当前目录下寻找依赖关系和目标文件。如果定义了“VPATH”变量make就会在在当前目录下找不到所需文件的情况下,到指定的目录中寻找文件。这有点类似gcc的目录选项中的“-I”和“-L“选项。如果要添加多个目录,一般使用“:”作为分隔。
4、Makefile常用函数
$(subst A, B, text) 将文本text中的每个A字符用B字符替换
$(patsubst A, B, text) 将文本text中符合格式为A的字符,用格式B替换。
$(strip text) 将text中多余的空格进行压缩(包括前导或者结尾的空格字符),并将多个空格变为单个空格
$(findstring A, text) 在text中查找字符串A,如果找到返回值为A,否则为空
$(sort text) 将text中的字按字母顺序排序,并去掉其中重复的单词。其输出为单个空格隔开的单词列表。如果第一个字母相同则比较第二个,依此类推
$(word N, text) 将text中第N个单词取出,并返回这个单词。如果不存在第N个单词,则返回值为空
$(wordlist N1, N2, text) 取出text中第N1到第N2个单词,其中N1,N2表示单词在text的位置数字
$(words text) 此函数表示统计text中的单词数目
$(filter A..., text) 在text中寻找由空格隔开并且匹配格式为A的字,去除不符合格式A的字符
$(filter-out A..., text) 返回由空格隔开的 并且不匹配格式为A的字符,除去格式为A的字符
$(dir text) 取出text中每个文件的路径部分
$(notdir text) 取出text中的每个文件名
$(addsuffix A, text) 将text中每个文件名后添加后缀“A”
$(addprefix A, text) 将text中每个文件名后添加前缀“A”
$(if A, B, C) 判断A,对变量A展开后,如果A的结果非空,则条件为真,同时将B作为函数的表达式;如果条件为假,则将C作为函数的表达式
5、Makefile与shell
Makefile文件通过shell函数与外部进行通信。它实现的功能同shell中的引用(``)类似,其返回结果是该命令在shell中执行的结果。Make命令仅对它的返回值当作字符串对待,如果函数返回结果中存在换行符,那么将其替换为空格,并去掉末尾的回车符号。在大多数情况下,make命令时在读取解析Makefile时完成对函数shell的展开。
shell函数的格式如下:
$(shell shell-command)
此函数的作用是在Makefile中执行shell-command命令,并将它的执行结果返回Makefile。
6、Makefile规则语法
Makefile中常见的规则包括显示规则、隐式规则以及静态模式规则3类。
(1)显示规则
显示规则描述了如何将“依赖文件”转变为“目标文件”,书写这种规则的Makefile需要用户明确地给出目标文件、目标依赖文件的列表,以及更新目标文件所需要的命令。
(2)隐式规则
隐式规则就像其名,它会把一部分规则“隐藏”,不要求用户将所有的规则列出,也不需要详细指定编译的具体细节,甚至有时候不需要任何规则,系统会根据要产生的可执行文件及其依赖文件(典型的是根据文件名的后缀),自动推导出其依赖文件时如何使用默认命令编写规则的。例如:典型地,make命令对c文件的编译过程是由.c源文件编译成.o目标文件。
另外,在make命令执行时,根据需要也可能用多个隐含规则。比如:make命令将从一个.y文件生成对应的.c文件,再生成最终的.o文件。也就是说,只要目标文件名中除后缀以外的其他部分相同,make命令就能够使用若干个隐含规则来最终产生这个目标文件(当然,最原始的那个文件必须存在)。
在Makefile中常见的隐式规则:
c编译:将file.c变为file.o $(CC)$(CPPFLAGS)$(CFLAGS) -c file.c -o file.o
c++编译:将file.cc变为file.o $(CXX)$(CPPFLAGS) -c file.cc -o file.o
Pascal编译:将file.p变为file.o $(CP)$(PFLAGS) -c file.p -o file.o
Fortran编译:将file.r变为file.o $(CP)$(FFLAGS) -c file.r -o file.o
注:以上file均表示任意文件名
(3)静态模式规则
模式规则是用来定义具有相同处理规则的多个文件。
模式规则类似于普通规则,只是在模式规则中,目标名需要包含模式字符“%”,该模式字符“%”被用来匹配一个文件名,可以匹配任何非空字符串。在依赖文件中同样可以使用“%”,依赖文件中的模式字符“%”的取值由目标中的“%”来决定。例如:对于模式规则“%.o:%.c”,它表示的含义是:所有的.o文件依赖于对应的.c文件,“%.c"可以匹配到所有以.c结尾的文件,“s%.c"可以匹配到所有第一个字母为“s”,而且必须以.c结尾的文件。
要注意的是:模式字符“%”的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在make读取Makefile时,而模式规则中“%”的匹配和替换则发生在make执行时。
c语言的模式规则一般可描述为:
%.o : %.c
COMMAND
这个模式规则指定了所有的文件“.c"都用来创建文件“.o",文件“.c"应该是已存在的或者可被创建的。
模式规则中的依赖文件也可以不包含模式字符“%”。当依赖文件名中不包含模式字符“%”时,其含义是所有符合目标模式的目标文件都依赖于一个指定的文件(例如:“%.o : debug.h ”表示所有的".o"文件都依赖于头文件"debug.h")。这样的模式规则在很多场合是非常有用的。
注:
(1)在使用模式规则时,指定的目标必须和目标模式相匹配,否则执行make时将会得到一个错误提示。
(2)相比隐式规则,模式规则更能体现其优点。对于无法确定工作目录内容或者不能确定是否存在无关文件,使用隐含规则可能会导致make命令失败。其次,当存在多个适合此文件的隐含规则时,系统将不能正确判断使用何种规则也可能导致make失败。在这两种情况下使用模式规则,就可以避免这些不确定因素,因为静态模式中,指定的目标文件有明确的规则描述其依赖关系。
除了以上3中规则外,Makefile中还存在一些基础规则,包括:
(1)在Makefile文件中,除“终极目标”所在的规则外,其余规则的顺序在Makefile中没有意义(终极目标是指Makefile文件第一个规则的目标,一般是执行make后生成的可执行文件)。如果在Makefile中第一个规则有多个目标的话,那么多个目标中的第一个将会被作为make的“终极目标”
(2)规则的命令部分有两种书写方式,一种是把命令和依赖文件放在一行,中间使用分号(:)隔开,另一种是命令在依赖文件的下一行。当作独立的命令行时,此行必须以Tab开始。
(3)对于Makefile中一个较长的行,我们可以使用反斜杠“\”将其书写到几个独立的行上。
7、Makefile自动编写工具
autotool工具是当今Linux世界中比较常用的Makefile自动生成工具。autotool是一系列工具,主要包括autoscan、autoconf、aclocal、autoheader、automake等。
下面用一个简单的程序exautomake.c来讲述autotool编写Makefile文件的方法。该文件的代码如下:
#include <stdio.h>
int main()
{
printf( "this is the first automake example!" );
}
第一步:autoscan
autoscan工具用来扫描当前目录下是否有生成Makefile的源文件(configure.scan、autoscan.log),如果没有这两个文件,系统会自动生成configure.scan、autoscan.log这两个文件。
因此,在命令行下输入:
#autoscan
autom4te: configure.ac: no such file or directory
autoscan: /usr/bin/autom4te failed with exit status: 1
# ls
autoscan.log configure.scan exautomake.c exautomake.c~
其中,configure.scan是configure.in的原型,(configure.in是autoconf的脚步配置文件),所以下一步工作就是要对configure.scan进行修改,将其转化为configure.in。
第二步:aclocal、autoconf及autoheader
使用编辑器打开configure.scan文件,其内容如下:
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.59) #命令行1
AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS) #命令行2
AC_CONFIG_SRCDIR([exautomake.c]) #命令行3
AC_CONFIG_HEADER([config.h]) #命令行4
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_OUTPUT #命令行5
命令行1中,AC_PREREQ用来声明本文件要求的autoconf版本
命令行2,用来定义软件的名称和版本等信息
命令行3,AC_CONFIG_SRCDIR用来检查所指定的源码文件是否存在,来确定源码目录的有效性。这个参数一般不需要修改
命令行4,AC_CONFIG_HEADER用于生成config.h文件,以便autoheader使用
将configure.scan文件修改为configure.in,修改的地方共有4处:
第一,将命令行2注释掉,并在命令行2后添加一新行,并键入"AC_INIT(exautomake,1.0)",表示编写该程序的名字及版本信息。
第二,在命令行3前添加新行,并键入"AM_INIT_AUTOMAKE(exautomake,1.0)",这是automake必备的一行,同前面一样,"exautomake"是所要产生的软件的名字,"1.0"表示版本号,一般而言,第一次编写版本号都可定为"1.0".
第三,将命令行4修改为"AM_CONFIG_HEADER([config.h])".(实践证明:改此处或者不改都可以)
修改完毕后,将configure.scan在当前目录下另存为configure.in。接下来要运行aclocal,系统会自动生成一个aclocal.m4文件,该文件的主要作用是处理当前的定义行;随后运行autoconf命令,该命令同样会在当前目录下生成一个名为configure的文件;最后执行autoheader,它负责生成config.h.in文件。
第四,将命令行5修改为"AC_OUTPUT([Makefile])".
#aclocal
#autoconf
#autoheader
第三步:automake
automake的执行至关重要。automake需要使用脚本配置文件Makefile.am,对于这个文件用户需要自己创建。
使用编辑器编写程序Makefile.am如下:
AUTOMAKE_OPTIONS=foreign #命令行1
bin_PROGRAMS=exautomake #命令行2
exautomake_SOURCES=exautomake.c #命令行3
其中,命令行1中,AUTOMAKE_OPTIONS表示为automake进行设置的选项。automake提供了3种软件等级:foreign、gnu、gnits,让用户选择使用,默认等级是gnu,在本例中使用foreign等级,只检查必须的文件。
命令行2中,bin_PROGRAMS表示要产生的可执行文件名。如果要产生多个可执行文件,每个文件名用空格隔开。
命令行3中的exautomake_SOURCES是用来定义exautomake这个执行程序的依赖文件。如果文件需要多个源文件,就要在后面添加生成它的源文件。例如:若生成的exautomake除需要exautomake.c外,还需要wth.c、wang.c,就要使用多个源文件定义方法"exautomake_SOURCES=exautomake.c wth.c wang.c".
接下来就是使用automake生成configure.in文件,一般而言,在执行automake时,要在后面添加选项"--add-missing",表示让automake自动添加一些必要的脚步文件。
#automake --add-missing
第四步:运行configure
通过运行configure,就可以把Makefile变成最终的Makefile
#./configure
到此为止,makefile就可以自动生成了。
第五步:执行make
执行make命令将默认执行make all,即编译所有目标体,执行完毕,会在当前目录下生成exautomake的可执行文件。
使用autotools工具除生成exautomake目标之外,还默认生成install(安装该程序到系统中),clean(清楚之前编译的所有可执行文件及目标文件)等目标文件。