最近在看《跟我一起写 Makefile》作者:陈皓,学到很多,谢谢大神。
本文是为了自己使用方便,在原书的基础上做了一点提取和总结,详细内容请参考《跟我一起写 Makefile》,支持原创,感谢原著作者。
目录
makefile介绍
makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile 定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile 就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。 makefile 带来的好处就是——“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。
makefile总述
Makefile 里主要包含了五个东西:
- 显式规则:要生成的文件,文件的依赖文件,生成的命令。
- 隐含规则:make 有自动推导的功能。
- 变量定义:变量一般都是字符串。
- 文件指示:其包括了三个部分
文件引用:在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的include 一样。
条件判断:根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译#if 一样。
命令包: 就是定义一个多行的命令。
- 注释:用“#”字符。
当然这其中也需要注意书写要求,函数引用等方面。
Makefile工作流程
在默认的方式下,也就是我们只输入 make 命令。那么
- make 会在当前目录下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“all”这个文件,并把这个文件作为最终的目标文件。(Makefile 中的第一个目标会被作为其默认目标)
- 如果all 文件不存在,或是 all 所依赖的后面的 .o 文件的文件修改时间要比 all这个文件新,那么他就会执行后面所定义的命令来生成all 这个文件。
- 如果 all所依赖的.o 文件也存在,那么 make 会在当前文件中找目标为.o 文件的依赖性,如果找到则再根据那一个规则生成.o 文件。(这有点像一个堆栈的过程)
- 当然你的 C 文件和 H 文件是存在的啦,于是 make 会生成 .o 文件,然后再用 .o 文件生命 make 的终极任务,也就是执行文件 all 了。
这就是整个 make 的依赖性,make 会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
makefile的显示规则
target ... : prerequisites ...
command
………
………
target :就是一个目标文件,可以是 Object File,也可以是执行文件,还可以是一个标签(Label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites: 就是要生成那个target 所需要的文件或是目标,也就是常说的依赖文件。
command: 就是 make 需要执行的命令。(任意的 Shell 命令)
这是一个文件的依赖关系,也就是说target 这一个或多个的目标文件依赖于prerequisites 中的文件,其生成规则定义在 command 中。
说白一点就是说,prerequisites中如果有一个以上的文件比 target 文件要新的话,command 所定义的命令就会被执行。这就是 Makefile 的规则,也就是 Makefile 中最核心的内容。
举例:假如有6个c文件和2和h文件,那么Makefile会这样写,见下图:
执行:直接输入make,就会生成执行文件all和各种.o文件了
清除本次执行结果:make clean
说明:clean 不是一个文件,它只不过是一个动作名字,有点像 C 语言中的 lable 一样,其冒号后什么也没有,那么make 就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个lable 的名字。(eg:make clean)
这样lable的方法非常有用,我们可以在一个 makefile 中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份等等。
Makefile的隐含规则(自动推导)
GNU 的 make 很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为我们的 make 会自动识别并自己推导命令。
只要 make 看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果 make找到一个 a.o,那么 a.c,就会是 a.o 的依赖文件,并且 cc -c a.c 也会被推导出来。举例:在上一个文件的基础上,我们做两次自动推导。如下图所示:
注:“.PHONY”表示clean是个伪目标文件,make clean 就一定会执行。
Makefile书写要求
Makefile书写规则
1、书写规则--通配符
使用通配符make 支持“*”,“?”,“[...]” ,“~”通配符。
“*.c”表示所以后缀为 c 的文件
“~/test”表示当前用户的$HOME 目录下的 test 目录。
如果想要这几个字符本身,那么可以用转义字符“\”,如“\*”来表示真实的“*”字符
2、书写规则--文件搜索路径
搜索方法一:VPATH
如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么make就会在当前目录找不到的情况下,到VPATH所指定的目录中去找寻文件了。
例如:VPATH = src:../headers
上面的的定义指定两个目录,“src”和“../headers”,make 会按照这个顺序进行搜索。目录由“冒号”分隔。
搜索方法二:使用 make 的“vpath”关键字
a、vpath <pattern> <directories>为符合模式<pattern>的文件指定搜索目录<directories>。
例1:
vpath %.h ../headers
该语句表示,要求 make 在“../headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话)
例2:
vpath %.c foo:bar
vpath % blish
而上面的语句则表示“.c”结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。
b、vpath <pattern>
清除符合模式<pattern>的文件的搜索目录。
c、vpath
清除所有已被设置好了的文件搜索目录。
3、书写规则--伪目标
target:是一个文件,通过make生成这个文件。
伪目标:不是一个文件,只是一个标签, make 无法生成它的依赖关系和决定它是否要执行。只有通过“make+伪目标”的方式才能生效。
例如:clean就是一个伪目标,执行方式:make clean
为了避免和文件重名,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,都会执行 。如下
伪目标也可以指定依赖。
4、书写规则--多目标
多个目标同时依赖于一个文件,并且其生成的命令大体类似,于是我们就能把其合并起来。
多个目标执行命令是同一个,这可能会因不用文件名的原因有点麻烦,我们可以使用一个自动化变量“$@”,这个变量表示着目前规则中所有的目标的集合,“$@”会依次取出目标,并执于命令。
例如:
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中-$(subst output,,$@)中的 subst是makefile的一个函数,具体后续再说,这里是截取字符串(big/little)的意思。
5、书写规则--静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活
语法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
....
targets :定义了一系列的目标文件,可以有通配符,是目标的一个集合。
target-parrtern :是指明了 targets 的模式,也就是的目标集模式。
prereq-parrterns :是目标的依赖模式,它对 target-parrtern 形成的模式再进行一次依赖目标的定义。
例如:
<target-parrtern>定义成“%.o”,意思是我们的<target>集合中都是以“.o”结尾的,而如果我们的<prereq-parrterns>定义成“%.c”,意思是对<target-parrtern>所形成的目标集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。
所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文件名中有“%”,那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。
知识点:$< 所有的依赖目标集(也是“a.c,b.c”),$@ 表示目标集(也就是“a.o b.o”)。
6、书写规则--自动生成依赖性
在 Makefile 中,我们的依赖关系可能会需要包含一系列的头文件,比如如果我们的 main.c中有一句“#include "defs.h"”,那么我们的依赖关系应该是:main.o : main.c defs.h。但是如果是一个比较大型的工程,你必需清楚哪些 C 文件包含了哪些头文件,并且你在加入或删除头文件时也需要小心地修改 Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
例如如果我们执行下面的命令:
cc -M main.c
其输出是:
main.o : main.c defs.h
需要提醒一句的是,如果你使用 GNU 的 C/C++编译器,你得用“-MM”参数,不然“-M”参数会把一些标准库的头文件也包含进来。
Makefile书写命令
1,每条命令的开头必须以[Tab]键开头,除非命令是紧跟在依赖规则后面的分号后的
2,“#”是注释符
3,@command,执行command内容,不会打印命令
4,上一条命令的结果应用在下一条命令时,使用分号分隔这两条命令,不要两条命令写在两行上 。
例如:
exec:
cd /home/hchen; pwd
5,忽略命令的出错,我们可以在 Makefile 的命令行前加一个减号“-”,标记为不管命令出不出错都认为是成功的
例如:
clean:
-rm -f *.o
注意:make “-i”或是“--ignore-errors”,Makefile 中所有命令都会忽略错误
make “-k”或是“--keep-going” ,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。
6,嵌套执行 make,每个目录中都书写一个该目录的 Makefile,有利于Makefile 变得更加地简洁
假如subdir下有一个Makefile,上层的Makefile可以这样写:
subsystem:
cd subdir && make
注意:传递变量到下级 Makefile 中,
语法:export <variable ...> ,export不加参数,传递所有参数到下级Makefile
例如:export variable = value
有两个变量,一个是 SHELL,一个是 MAKEFLAGS,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中。
7,定义命令包
Makefile 中多处使用一些相同命令序列,那么我们可以将这些相同的命令序列定义成一个变量。
语法:“define”开始,以“endef”结束
举例:
Makefile变量
1、变量声明
声明时需要给予初值。例如:foo = c
2、引用变量
变量名前加上“$”符号,但最好用小括号“()”或大括号“{}”把变量给包括起来,这样更安全。
变量名前加上“$”符号,但最好用小括号“()”或大括号“{}”把变量给包括起来,这样更安全。
3、变量赋值
1、“=”赋值:
说明:在“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值
例如:
foo = $(bar)
ugh = Huh?
all:
echo $(foo) ==>执行结果:Huh
2、“:=”赋值
说明:使用“:=”操作符,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。
例如:
dir := /home/test
3、“?=”赋值
说明:判断变量是否被定义
FOO ?= bar
其含义是,如果 FOO 没有被定义过,那么变量 FOO 的值就是“bar”,如果 FOO 先前被定义过,那么这条语将什么也不做。
4、两种变量的高级使用方法
1、变量替换。
我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。
例如: 先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.cc.c”。
foo := a.o b.o c.o
bar := $(foo:.o=.c)
2、变量嵌套
例如:
x = y
y = z
a := $($(x))
在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,是“x=y”,而不是“x=$(y)”)
5、追加变量值
我们可以使用“+=”操作符给变量追加值 。
如:
objects = main.o foo.o bar.o utils.o
objects += another.o
于是$(objects)值变成:“main.o foo.o bar.o utils.o another.o”(another.o被追加进去了)
注意:变量之前没有定义过,那么“+=”会自动变成“=”,如果前面有变量定义,那么“+=”会继承于前次操作的赋值符。如果前一次的是“:=”,那么“+=”会以“:=”作为其赋值符,或者前次的赋值符是“=”,那么“+=”也会以“=”来做为赋值。
6、强制赋值(override 指示符)
如果有变量是通过 make 的命令行参数设置的(make a=1),那么 Makefile 中对a这个变量的赋值会被忽略。如果你想在 Makefile 中设置这类参数的值,那么你可以使用“override”指示符。
语法:
override <variable> = <value>
override <variable> := <value>
override <variable> += <more text>
override define foo
bar
endef
例如:如果在Makefile里定义:f =11,执行make f =22,执行结果打印f就是22,如果在Makefile里定义:override f =11,执行make f =22,执行结果打印f就是11。
注意:变量在定义时使用了“override”,则后续对它值进行追加时,也需要使用带有“override”指示符的追加方式。否则对此变量值的追加不会生效。
7、多行变量
还有一种设置变量值的方法是使用 define 关键字。使用 define 关键字设置变量的值可以有换行,这有利于定义一系列的命令(前面我们讲过“命令包”的技术就是利用这个关键字)。
define two-lines
echo foo
echo $(bar)
endef
8、环境变量
make 运行时的系统环境变量可以在 make 开始运行时被载入到 Makefile 文件中,但是如果 Makefile 中已定义了这个变量,或是这个变量由 make 命令行带入,那么系统的环境变量的值将被覆盖。(如果 make 指定了“-e”参数,那么系统环境变量将覆盖 Makefile 中定义的变量)
9、目标变量
为某个目标设置的局部变量,它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效,而不会影响规则链以外的全局变量的值。
其语法是:
<target ...> : <variable-assignment>
<target ...> : overide <variable-assignment> #针对于 make 命令行带入的变量,或是系统环境变量。
<variable-assignment>可以是前面讲过的各种赋值表达式,如“=”、“:=”、“+=”或是“?=”
例如:prog : CFLAGS = -g
不管全局的$(CFLAGS)的值是什么,在 prog 目标,以及其所引发的所有规则中(prog.o foo.o bar.o 的规则),$(CFLAGS)的值都是“-g”
10、自动化变量
就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。
Makefile文件指示
1、Makefile文件名
默认的情况下最好使用“Makefile”这个文件名;make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,然后进行解释。如果要指定特定的Makefile,比如:“Make.Linux”,“Make.Solaris”等,可以使用make的“-f” (--file)参数,如:make -f Make.Linux或make --file Make.AIX。
2、引用另一个文件
include <filename>
filename 可以是当前操作系统 Shell 的文件模式(可以保含路径和通配符) 在 include前面可以有一些空字符,但是绝不能是[Tab]键开始。
例如:
include foo.make a.mk b.mk c.mk e.mk f.mk
make 命令开始时,会把找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。
有时候会看到sinclude和-include的使用,这里解释一下:
在上文书写命令第5条有提到“-”的使用,可以知道 “-include”代替“include”,是为了忽略由于包含文件不存在或者无法创建时的错误提示,让make继续执行。
为了和其它的make程序进行兼容,也可以使用“sinclude”来代替“-include”(GNU所支持的方式)。
3、条件判断
语法:
例1:
libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
例2:
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
4、命令包
我们已经在前面书写命令的部分提到,这个不再赘述。
Makefile函数
函数的调用语法:$(<function> <arguments>)或是${<function> <arguments>}
1、字符串处理函数
2、文件名操作函数
3、foreach函数
语法:$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。
举例
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”。
4、if函数
语法:$(if <condition>,<then-part>)或是 $(if <condition>,<then-part>,<else-part>)
可见,if 函数 “else”部分可选。
<condition>参数是 if 的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,<then-part>会被计算,否则<else-part>会被计算。
if 函数的返回值:<then-part>或者<else-part>的执行结果,如果不执行<then-part>并且没有<else-part>的情况,返回空。
5、call函数
makefile没有自定义函数的说法,但是有时候我们需要一些类似的使用,所以可以这样定义一个命令包,这个命令包中,可以定义许多参数,然后你可以用 call 函数来向这个命令包传递参数。call 函数是唯一一个可以用来创建新的参数化的函数。
其语法是: $(call <expression>,<parm1>,<parm2>,<parm3>…)
当 make 执行这个函数时,<expression>参数中的变量,如$(1),$(2),$(3)等,会被参数<parm1>,<parm2>,<parm3>依次赋值。而<expression>的返回值就是 call 函数的返回值。
以前是这样使用,定义一个名字为t的命令包,直接使用$(t)即可调用,例如:
现在定义一个名字为test的命令包,包含$1,$2两个参数,现在就不能使用$(test)的方式调用了,只能使用call来完成,如图,执行之后$1 变成 hello,$2 变成world
当然你也可以在命令包里使用call的方式调用其他带有参数的命令包,如图:
6、origin 函数
origin :查看变量来源
语法: $(origin <variable>)
注意,<variable>是变量的名字,不是引用不要在<variable>中使用“$”字符。
返回值:
7、shell函数
shell 函数把执行操作系统命令后的输出作为函数返回,也就是我们可以使用shell来获取操作系统命令执行后的返回值。
例如:
执行结果打印1.txt的内容
8、error和warning函数
1、error语法:$(error <text ...>)
产生一个致命的错误,<text ...>是错误信息。注意error 函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。
示例一:
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
示例二:
ERR = $(error found an error!)
.PHONY: err
err: ; $(ERR)
示例一会在变量 ERROR_001 定义了后执行时产生 error 调用,而示例二则在目录 err被执行时才发生 error 调用。
2、warning语法:$(warning <text ...>)
这个函数很像 error 函数,只是它并不会让 make 退出,只是输出一段警告信息,而 make 继续执行。
关于书中提到的make 的运行,make的参数,隐含规则,使用 make 更新函数库文件等这里不再讲述,有需要的请自行到《跟我一起写Makefile》或者其他书籍查阅,谢谢。