1. 初识Makefile
Makefile文件是由一些列的规则组合而成的,格式如下:
target(目标文件) …: prerequisites(依赖的文件) …
command(命令)
…
…
比如3.3.2中写的Makefile的规则:
main.o:main.c
gcc -c main.c
这条规则的main.o是目标文件(将要生成的文件),main.c是依赖的文件(生成main.o需要的文件),“gcc -c main.c”是生成main.o需要运行的命令。Makefile中每行的脚本如果有缩进的情况,必须使用“Tab”键缩进,切记不能使用空格缩进(这是Makefile的语法要求),大家一定要切记!
下面我们来分析一下图3.3.2章节中写的Makefile文件,脚本如下:
1 main:main.o calc.o
2 gcc -o main main.o calc.o
3 main.o:main.c
4 gcc -c main.c
5 calc.o:calc.c
6 gcc -c calc.c
7
8 clean:
9 rm -rf *.o
10 rm -rf main
该脚本一共有4条规则,1、2行是第一条规则,3、4行是第二条规则,5、6是第三条规则8、9、10是第四条规则。我们在运行make命令的时候,会解析当前目录下的这个Makefile文件里面的规则,首先解析第一条规则,第一条规则中的目标文件是main,只要完成了该目标文件的更新,整个Makefile的功能就完成了。在第一次编译的时候,由于目标文件main不存在,则会解析第一条规则,第一条规则依赖文件main.o、calc.o,make命令会检查当前目录下是否有这两个.o文件,经过检查发现没有,然后make会在Makefile中查找分别以main.o、calc.o为目标的规则(第二条,第三条规则)。执行第二条规则依赖的文件是main.c,make命令检查发现当前目录下有这个文件,然后执行第二条规则的命令“gcc -c main.c”生成main.o文件。然后执行第三条规则,第三条规则的目标文件是calc.o,依赖的文件是calc.c,make命令检查发现当前目录下存在该文件,然后执行第三条规则的命令“gcc -c calc.c”生成calc.o文件,至此第一条规则依赖的main.o、calc.o;两个文件已经生成了,然后运行第一条规则的命令“gcc -o main main.o calc.o”生成main文件。因为make命令运行的时候会从Makefile的第一条规则开始解析,然后根据第一条规则的依赖文件去遍历文件中的“对应规则”,然后在根据“对应规则”的依赖文件去遍历“对应的规则”,采用这样递归的方式会遍历出完成第一条规则所需要的所有规则。下面我们来看看第四条规则的目标文件是clean,我们通过查看发现该规则与第一条规则没有关联,所以我们在运行make命令的时候,不会遍历到该规则。我们可以在终端输入“make clean”命令来运行第四条规则,第四条规则没有依赖的文件,所以执行运行命令“rm -rf *.o”和“rm -rf main”,这两条命令的功能是删除以.o为结尾的所有文件,删除文件main,运行如图 1.1所示:
通过上图可以看到main.o、mcalc.o和main三个文件已经删除了。通过该规则我们可以清除编译产生的文件,实现工程的清理。
我们再来总结一下make命令的执行过程:
1.make命令会在当前目录下查找以Makefile命名的文件
2.找到Makefile文件,就会按照Makefile里面的规则去编译,并生成最终文件
3.当发现目标文件不存在或者所依赖的文件比目标文件新(修改时间),就会执行规则对应的命令来更新。我们可以看到make是一个工具,他会通过Makefile文件里面的内容来执行具体的编译过程。
2. Makefile的变量
在3.3.2章节中的Makefile第一条规则:
main:main.o calc.o
gcc -o main main.o calc.o
在该规则中main.o、calc.o这两个文件我们输入了两次,由于我们的Makefile文件内容比较少,如果Makefile复杂的情况下,这种重复的输入就会非常占用时间,而且修改起来也会很麻烦,为了解决这个问题,Makefile可以使用变量。Makefile的变量是一个字符串。比如上面的规则我们声明一个变量,叫objects,objs或者是OBJ,反正不管是什么,只要能够表示main.o、calc.o就行了,我们修改上面的规则
1 objects = main.o calc.o
2 main:$( objects)
3 gcc -o main $( objects)
我们来分析下修改后的规则,首先第一行是我们定义了一个变量objects,并给赋值“main.o calc.o”,第二行、第三行用到了变量objects。Makefile中的变量引用方式是“$(变量名)”,变量objects的赋值使用“=”,Makefile中变量的赋值还可以使用“:=”、“?=”、“+=”,这四种赋值的区别如下:
1.“=”赋值符
我们先在用户根目录的work目录下创建一个Makefile脚本,输入下面的内容:
1 ceshi1 = test
2 ceshi2 = $(ceshi1)
3 ceshi1 = temp
4
5 out:
6 @echo ceshi2:$(ceshi2)
第一行我们定义了变量并赋值“test”,第二行定义了变量ceshi2并赋值变量ceshi1,第三行修改变量ceshi1的值为“temp”,第五行、第六行是输出变量ceshi2的值。我们在终端输入“make out”命令,如图 2.1所示:
在上图可以看到变量ceshi2的值是temp,也就是变量ceshi1最后一次的赋值。
2.“:=”赋值符
我们修改“=”赋值符中的代码,第二行的“=”改成“:=”,代码如下:
1 ceshi1 = test
2 ceshi2 := $(ceshi1)
3 ceshi1 = temp
4
5 out:
6 @echo ceshi2:$(ceshi2)
我们在终端输入“make out”命令,如图 2.2所示:
我们可以看到上图的运行结果输出变量ceshi2的值是test,虽然在第三行我们修改了变量ceshi1的值,通过本实验我们可以看到“:=”赋值符的功能了。
3.“?=”赋值符
ceshi ?= test
“?=”赋值符的作用是如果前面没有给变量ceshi赋值,那么变量就赋值“test”,如果前面已经赋值了,就使用前面的赋值。
4.“+=”赋值符
objs = main.o
objs += calc.o
上面的脚本最后变量objs的值是“main.o calc.o”,“+=”赋值符的功能是实现变量的追加。
3. 条件判断
使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。其语法有下面两种:
1.
<条件比较>
[条件为真时执行的脚本]
endif
2.
<条件比较>
[条件为真时执行的脚本]
else
[条件为假时执行的脚本]
endif
条件比较用到的比较关键字有:ifeq、ifneq、ifdef、ifndef。
ifeq表示如果比较相等,语法如下:
ifeq(<参数1>, <参数2>)
ifneq表示如果不相等,语法如下:
ifneq(<参数1>, <参数2>)
ifdef表示如果定义了变量,语法如下:
ifdef <变量名>
ifndef表示如果没有定义变量,语法如下:
ifndef <变量名>
4. 使用函数
在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。
函数的调用很像变量的使用,也是以“$”来标识的,语法如下:
$(<函数名> <参数集合>)
或者:
${<函数名> <参数集合>}
函数名和参数集合之间以空格分隔,参数集合的参数通过逗号分隔。函数调用以“$
”开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量。函数中的参数可以使用变量。为了风格的统一,函数和变量的括号最好一样,如使用“$(subst a,b,$(x))
”这样的形式,而不是“$(subst a,b,${x})
”的形式。因为统一会更清楚,也会减少一些不必要的麻烦。
接下来我们介绍几个常用的函数,其它的函数可以参考文档《跟我一起写 Makefile》。
1.subst函数
$(subst <from>,<to>,<text>)
此函数的功能是把字符串<text>
中的<from>
字符串替换成<to>
,函数返回被替换过后的字符串。如下示例:
$(subst ee,EE,feet on the street)
以上脚本实现把字符串“feet on the street”中的“ee”字符串替换成“EE”字符串,替换后的字符串为“fEEt on the strEEt”。
2. patsubst函数
$(patsubst <pattern>,<replacement>,<text>)
此函数的功能是查找<text>
中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>
,如果匹配的话,则以<replacement>
替换。这里<pattern>
可以包括通配符“%”,表示任意长度的字符串。如果<replacement>
中也包含“%”,那么<replacement>
中的这个“%”将是<pattern>
中的那个“%”所代表的字符串。(可以用“\”来转义,以“%” 来表示真实含义的“%”字符)。函数返回被替换过后的字符串。如下示例:
$(patsubst %.c,%.o,x.c bar.c)
以上脚本实现把字符串“x.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.o bar.o”
3.strip函数
$(strip <string>)
此函数的功能是去掉字符串中开头和结尾的空字符,函数返回被去掉空格的字符串值。如下示例:
$(strip a b c )
以上脚本实现把字符串“a b c ”去掉开头和结尾的空格,结果是“a b c”。
4. findstring函数
$(findstring <find>,<in>)
此函数的功能是在字符串中查找字符串,如果找到,那么返回,否则返回空字符串,如下示例:
$(findstring a,a b c)
$(findstring a,b c)
以上脚本,第一个返回“a”字符串,第二个返回空字符串。
5.dir函数
$(dir <names...>)
此函数的功能是从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。返回文件名序列的目录部分,如下示例:
$(dir src/foo.c hacks)
以上脚本运行结果返回“src/”。
6. notdir函数
$(notdir <names...>)
此函数的功能是从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后的部分,返回文件名序列的非目录部分,如下示例:
$(notdir src/foo.c)
以上脚本返回字符串“foo.c”
7. foreach函数
$(foreach <var>,<list>,<text>)
此函数的功能是把参数<list>
中的单词逐一取出放到参数<var>
所指定的变量中,然后再执行<text>
所包含的表达式。每执行一次<text>
会返回一个字符串,循环过程中,<text>
的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>
所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。所以,<var>
最好是一个变量名,<list>
可以是一个表达式,而<text>
中一般会使用<var>
这个参数来依次枚举<list>
中的单词。如下示例:
names := a b c d
files := $(foreach n,$(names),$(n).o)
以上脚本实现$(name)
中的单词会被挨个取出,并存到变量“n”中,“$(n).o
”每次根据“$(n)
”计算出一个值,这些值以空格分隔,最后作为 foreach 函数的返回,所以$(files)
的值是“a.o b.o c.o d.o”。(注意,foreach 中的<var>
参数是一个临时的局部变量,foreach 函数执行完后,参数<var>
的变量将不再作用,其作用域只在 foreach 函数当中)。
5. 在规则中使用通配符
如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make命令支持三种通配符:“*
”,“?
”和“[...]
”,这是和 Unix 的 B-Shell 是相同的。“~
”字符在文件名中也有比较特殊的用途。如果是“~/test
”,这就表示当前用户根目录下的test文件。而“~admin/test
”则表示用户admin根目录下的 test文件。
通配符代替了一系列的文件,如“*.c
”表示所有后缀为 .c 的文件。一个需要我们注意的是,如果我们的文件名中有通配符,如:“*
”,那么可以用转义字符“\”,如“*” 来表示真实的“*”字符,而不是任意长度的字符串。
下面我们来看几个具体的示例:
clean:
rm -rf *.o
上面这个示例说明通配符可以在规则的命令中使用。
print: *.c
上面这个示例说明通配符可以在规则的依赖中使用
objects = *.o
上面这个示例表示了,通配符同样可以用在变量中。并不是说[*.o]
会展开,objects的值就是“*.o
”。Makefile 中的变量其实就是 C/C++中的宏。如果你要让通配符在变量中展开,也就是让 objects 的值是所有[.o]的文件名的集合,那么,你可以这样:
objects := $(wildcard *.o)
这种用法由关键字“wildcard”指出,关于Makefile的关键字可以参考文档《跟我一起写Makefile》。
关于Makefile的相关内容我们就介绍到这里,本节只是对Makefile做了基本的讲解,Makefile还有大量的知识点,有兴趣的朋友可以参考文档《跟我一起写Makefile》来深入的了解Makefile。