Makefile 详解
Makefile 工作方式
在默认的方式下,也就是我们只输入make命令。那么,
1、make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。Makefile中的第一个目标会被作为其默认目标
3、如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
4、如果edit所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
5、当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生命make的终极任务,也就是执行文件edit了。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理
make工作时的执行步骤入下:
1、读入所有的Makefile。
2、读入被include的其它Makefile。
3、初始化文件中的变量。
4、推导隐晦规则,并分析所有规则。
5、为所有的目标文件创建依赖关系链。
6、根据依赖关系,决定哪些目标要重新生成。
7、执行生成命令。
Makefile 文件名
一般时Makefile,也可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的“-f”和“--file”参数,如:make -f Make.Linux或make --file Make.AIX。
引用其他Makefile
在Makefile使用include关键字可以把别的Makefile包含进来,make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位。就好像C/C++的#include指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
1、如果make执行时,有“-I”或“--include-dir”参数,那么make就会在这个参数
所指定的目录下去寻找。
2、如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。
如: -include
其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼 容的相关命令是sinclude,其作用和这一个是一样的。
Makefile书写规则
规则包含两个部分,一个是依赖关系,一个是生成目标的方法
targets : prerequisites
command
...
或是这样:
targets : prerequisites ; command
command
...
targets是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。
command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以[Tab键]开头,如果和prerequisites在一行,那么可以用分号做为分隔。(见上) prerequisites也就是目标所依赖的文件(或依赖目标)。
文件寻找
在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。
Makefile文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。
VPATH = src:../headers
伪目标
“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这样。
静态模式
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
....
targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern是指明了targets的模式,也就是的目标集模式。
prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。
objects = foo.o bar.o all: $(objects) $(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@
显示命令
通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:
@echo 正在编译XXX模块......
嵌套执行Make
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。
如果你要传递变量到下级Makefile中,那么你可以使用这样的声明:
export
如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:
unexport
export variable = value
如果你要传递所有的变量,那么,只要一个export就行了。后面什么也不用跟,表示传递所有的变量。
定义命令包
如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始,以“endef”结束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
后面可以直接通过$(run-yacc),进行引用
变量赋值
foo = $(bar)
x := later x丢掉原先的值,重新赋值为later,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量
?= 如果还没有赋过值,则赋值
两种变量的高级使用方法,第一种是变量值的替换。我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。
foo := a.o b.o c.o bar := $(foo:.o=.c)
变量替换的技术是以“静态模式”实现
foo := a.o b.o c.o bar := $(foo:%.o=%.c)
条件判断
条件表达式的语法为:
<conditional-directive>
<text-if-true>
endif
以及:
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
其中表示条件关键字,如“ifeq” “ifdef” "ifndef"
Makefile中的函数
$(subst <from>,<to>,<text>)
名称:字符串替换函数——subst。
功能:把字串中的字符串替换成。
返回:函数返回被替换过后的字符串。
$(patsubst <pattern>,<replacement>,<text>)
名称:模式字符串替换函数——patsubst。
功能:查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。这里,可以包括通配符“%”,表示任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以用“\”来转义,以“%”来表示真实含义的“%”字符)
返回:函数返回被替换过后的字符串。
$(strip <string>)
名称:去空格函数——strip。
功能:去掉字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
$(findstring <find>,<in>)
名称:查找字符串函数——findstring。
功能:在字串中查找字串。
返回:如果找到,那么返回,否则返回空字符串。
$(filter <pattern...>,<text>)
名称:过滤函数——filter。
功能:以模式过滤字符串中的单词,保留符合模式的单词。可以有多个模式。
$(filter-out <pattern...>,<text>)
名称:反过滤函数——filter-out。
功能:以模式过滤字符串中的单词,去除符合模式的单词。可以有多个模式。
返回:返回不符合模式的字串。
$(sort <list>)
名称:排序函数——sort。
功能:给字符串中的单词排序(升序)。
返回:返回排序后的字符串。
$(word <n>,<text>)
名称:取单词函数——word。
功能:取字符串中第个单词。(从一开始)
返回:返回字符串中第个单词。如果比中的单词数要大,那么返回空字符串。
$(wordlist <s>,<e>,<text>)
名称:取单词串函数——wordlist。
功能:从字符串中取从开始到的单词串。和是一个数字。
返回:返回字符串中从到的单词字串。如果比中的单词数要大,那么返回空字符串。如果大于的单词数,那么返回从开始,到结束的单词串。
$(words <text>)
名称:单词个数统计函数——words。
功能:统计中字符串中的单词个数。
返回:返回中的单词数。
示例:$(words, foo bar baz)返回值是“3”。
备注:如果我们要取中最后的一个单词,我们可以这样:$(word $(words ),)。
$(firstword <text>)
名称:首单词函数——firstword。
功能:取字符串中的第一个单词。
返回:返回字符串的第一个单词。
示例:$(firstword foo bar)返回值是“foo”。
备注:这个函数可以用word函数来实现:$(word 1,)。
$(dir <names...>)
名称:取目录函数——dir。
功能:从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
返回:返回文件名序列的目录部分。
$(notdir <names...>)
名称:取文件函数——notdir。
功能:从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“ /”)之后的部分。
返回:返回文件名序列的非目录部分。
$(suffix <names...>)
名称:取后缀函数——suffix。
功能:从文件名序列中取出各个文件名的后缀。
返回:返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。
$(basename <names...>)
名称:取前缀函数——basename。
功能:从文件名序列中取出各个文件名的前缀部分。
返回:返回文件名序列的前缀序列,如果文件没有前缀,则返回空字串。
$(addsuffix <suffix>,<names...>)
名称:加后缀函数——addsuffix。
功能:把后缀加到中的每个单词后面。
返回:返回加过后缀的文件名序列。
$(addprefix <prefix>,<names...>)
名称:加前缀函数——addprefix。
功能:把前缀加到中的每个单词后面。
返回:返回加过前缀的文件名序列。
$(join <list1>,<list2>)
名称:连接函数——join。
功能:把中的单词对应地加到的单词后面。如果的单词个数要比的多,那么,中的多出来的单词将保持原样。如果的单词个数要比多,那么,多出来的单词将被复制到中。
返回:返回连接过后的字符串。
foreach函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的。它的语法是:
$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
所以,最好是一个变量名,可以是一个表达式,而中一般会使用这个参数来依次枚举中的单词。注意,foreach中的参数是一个临时的局部变量,foreach函数执行完后,参数的变量将不在作用,其作用域只在foreach函数当中
if函数很像GNU的make所支持的条件语句——ifeq(参见前面所述的章节),if函数的语法是:
$(if <condition>,<then-part>)
或是
$(if <condition>,<then-part>,<else-part>)
可见,if函数可以包含“else”部分,或是不含。即if函数的参数可以是两个,也可以是三个。参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,会被计算,否则会被计算。
而if函数的返回值是,如果为真(非空字符串),那个会是整个函数的返回值,如果为假(空字符串),那么会是整个函数的返回值,此时如果没有被定义,那么,整个函数返回空字串。
所以,和只会有一个被计算。
call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:
$(call <expression>,<parm1>,<parm2>,<parm3>...)
当make执行这个函数时,参数中的变量,如$(1),$(2),$(3)等,会被参数,,依次取代。而的返回值就是call函数的返回值
shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量。注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。
$(error <text ...>)
产生一个致命的错误,是错误信息。注意,error函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的
$(warning <text ...>)
这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。