从零开始学习Makefile(一)

0 前言

Makefile从前到后读取所有规则,建立起一个完整的依赖关系图,然后从缺省目标或者命令行指定的目标开始,根据依赖关系图选择适当的规则执行,执行Makefile中的规则和执行C不一样,并不是从前到后按顺序执行,也不是所有规则都要执行一遍。

一、makefile规则

target ...: prerequisites ...
			command
			...
			...

target是一个目标文件,可以是object file,也可以是执行文件,还可以是一个标签;
prerequisites是要生成target所需要的文件或目标;
command是make要执行的命令。(任意的shell命令)
注意:command必须以**[Tab]**键开始。

这是一个文件的依赖关系,即,target这一个或多个目标文件依赖于prerequisites中的文件,其生成规则定义在command中。prerequisites中如果有一个以上的文件比target文件要更新的话(make会比较target文件和prerequisites文件的修改日期,如果prerequisites文件日期更新,或者target不存在的话),command所定义的命令就会被执行。这就是makefile中最核心的内容。

target冒号后面什么也没有的话(如clean),则其并不是一个文件,只不过是一个动作名字(label),make就不会自动去找文件的依赖性,也不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显指出这个label名字。

二、makefile变量

objects = main.o display.o ... 在之后的使用中可以以“$(objects)”的方式使用这个变量

三、make自动推导

GNU的make很强大,只要make看到一个[.o]文件,就会自动的把[.c]文件加在依赖关系中,并且把 cc –c [.c]推导出来。于是:

display.o : display.c defs.h command.h
			cc –c display.c

可以简单写为:

display.o : defs.h command.h

这种方法,也是make的隐晦规则。

四、clean规则(-号)

一般是:

clean:
	rm edit $(objects)

更为稳健的做法是:

.PHONY : clean
clean:
		-rm edit $(objects)

1>.PHONY表示clean是个伪目标。
2>通常make执行的命令如果出错(该命令的退出状态非0)就立刻终止,不再执行后续命令,但如果命令前面加了‘-’号,即使这条命令出错,make也会继续执行后续命令。通常rm和mkdir命令前面要加-,因为rm要删除的文件可能不存在,mkdir要创建的目录可能已存在,这两个命令都有可能出错,但这种错误是应该忽略的。例如重复执行make clean时可能会报错。
另外,clean的规则不要放在文件的开头,不然就会变成make的默认目标。

五、引用其他Makefile

include <filename>

filename可以是当前操作系统shell的文件模式(可以包含路径和通配符)
make命令开始时,会找寻include所指出的其他Makefile,并把其内容安置在当前位置。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么make还会再下面几个目录下找:
1、如果make执行时,有“-I”或“—include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。
2、如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。

如果文件没有找到,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其他文件,一旦完成makefile的读取,make会再重试这些没有找到、或不能读取的文件,如果还是不行,make才会出现一条致命信息。如果想让make不理那些无法读取的文件,而继续执行,可以在include前加一个“-”。

六、make工作方式

1、读入所有Makefile
2、读入被include的其他Makefile
3、初始化文件中的变量
4、推导隐晦规则,并分析所有规则
5、为所有的目标文件创建依赖关系链
6、根据依赖关系,决定哪些目标要重新生成
7、执行生成命令

七、通配符的使用

1、命令中的通配符:

clean:
		rm –f *.o

2、规则中的通配符

print : *.c
		lpr –p $?
		touch print

3、变量中的通配符

objects = *.o

但这里,[.o]并不会展开,objects值就是“.o”。如果想让通配符在变量中展开,让objects的值是所有[.o]的文件名的集合。需要这样用:

objects :=$(wildcard  *.o)

其中wildcard 是Makefile的关键字。

八、文件搜寻

1、利用特殊变量“VPATH”

VPATH = src : ../headers

指定两个目录,“src”和“…/headers”,make会先在当前目录下找文件的依赖关系,然后按照这个文件顺序进行搜索,目录由冒号分割。

2、利用关键字“vpath”

vpath #为符合模式的文件指定搜索目录
vpath #清除符合模式的文件的搜索目录
vpath #清楚所有已被设置好了的文件搜索目录
需要包含“%”字符。“%”是匹配零或若干字符。如“%.h”表示所有以“.h”结尾的文件。指定了要搜索的文件集,指定文件集的搜索目录。例如:

vpath  %.h  ../headers

表示,要求make在“…/headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话)
可以连续的使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现相同的,make会按照先后顺序进行搜索。

九、伪目标

前面例子中用.PHONY表示clean是个伪目标。只要有这个声明,不管是否有重名的“clean”文件,“clean”也只会是伪目标,必须以“make clean”来运行。
伪目标一般没有依赖文件。但是,也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。
从零开始学习Makefile(一)_第1张图片

上述例子声明了一个“all”的伪目标,其依赖于其他三个目标。由于伪目标的特性是:总是被执行的。所以其依赖的三个目标总是会被决议。可以达到一口气生成多个目标的目的。
(Makefile中只应该有一个最终目标。一般来说,定义在Makefile的目标可能会有很多,但是第一条规则中的目标将被确立为最终目标。如果第一条规则的目标有很多个,那么,第一个目标会成为最终目标。)

十、静态模式规则

<targets ...> : <target-pattern> : <prereq-patterns ...>
			<commands>
			...

target-pattern指明了targets的模式,即目标集模式。
prereq-patterns是目标的依赖模式,对target-pattern形成的模式再进行一次依赖目标的定义。
如:

$(objects) : %.o : %.c
			$(CC)  -c  $(CFLAGS)  $<  -o  $@

指明了从$objects中获取所有以“.o”结尾的目标。而依赖模式“%.c”意味着取模式“%.o”的“%”,并为其加上“.c”的后缀。
“$<”表示所有依赖目标集(即加上.c后缀匹配上的文件)
“$@”表示目标集(即以“.o”结尾的目标)

十一、函数

函数也是以$标识,语法为:

$(<function> <arguments>)

或:

${ }

前面是函数名,后面是参数,以空格分隔,参数间以逗号分隔
1、函数subst,表示替换字符。该函数有三个参数,①被替换字串;②替换字串;③将要替换的目标。将字串中的字串替换成

$(subst <from>,<to>,<text>)

$(subst output , , $@)中的 “$@”表示依次取出目标,即将目标中的output字串都替换成空,实现截取字串的功能。
从零开始学习Makefile(一)_第2张图片

2、函数patsubst,查找中的单词(以“空格”、“Tab”、“回车”、“换行”分隔)是否符合,如果匹配的话,则以替换。这里的可以包括通配符“%”,表示任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以用“\”来转义,来表示真实含义的“%”字符)

$(patsubst <pattern>,<replacement>,<text>)

例1:
$(patsubst %.c, %.o, x.c.c bar.c)
把字串“x.c.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.c.o bar.o”

例2:
objects = foo.o bar.o baz.o
那么,"$objects := .o = .c""$(patsubst %.o, %.c, $(objects))"是一样的

3、函数strip,去掉字串中开头和结尾的空字符

$(strip <string>)

4、函数findstring,在字串中查找字串,如果找到,返回字串,否则返回空。

$(findstring <find>, <in>)

5、函数filter,以模式过滤字串中的单词,保留符合模式的单词。可以有多个模式。

$(filter <pattern…>, <text>)

$(filter %.o, $(files))表示调用Makefile的filter函数,过滤“$files”集,只要其中模式为“%.o”的内容。

files = foo.elc bar.o lose.o
$(filter %.o, $(files)): %.o : %.c
		$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc, $(files)): %.elc : %.el
		emacs -f batch-byte-compile $<

6、函数filter-out,以模式过滤字串中的单词,去除符合模式的单词。可以有多个模式。

$(filter-out <pattern…>, <text>)

7、函数sort,给字符串中的单词排序(升序)。注:首字母排序,会去掉相同的单词
8、函数word,取字符串中第个单词,如果比单词数要大,则返回空字符串

$(word <n>, <text>)
例:
$(word 2, foo bar baz)返回值是“bar”

9、函数wordlist,取字符串中从的单词字串,如果比单词数要大,则返回空字符串; 如果比单词数要大,则返回从开始,到结束的单词串。

$(wordlist <s>, <e>, <text>)
例:
$(wordlist 2, 3, foo bar baz)返回值是“bar baz”

10、函数words,统计字符串中的单词个数

$(words <text>)
如果我们要取<text>中最后的一个单词,我们可以:$(word $(words <text), <text>).

11、函数firstword,取字符串中第一个单词

$(firstword <text>)

可以用word函数实现:$(word 1, )

应用举例:

override CFLAGS += $(patsubst %, -I%, $(subst : , , $(VPATH)))
如果我们的“$(VPATH)”值是"src:../headers",那么"$(patsubst %, -I%, $(subst :, ,$(VPATH)))"将返回"-Isrc -I../hraders,这正是cc或gcc搜索头文件路径的参数。

十二、自动生成依赖性

依赖关系会包含一系列头文件,必须清楚哪些C文件包含了哪些头文件,很没有维护性。大多数C/C++编译器都支持一个“-M”选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。如:

cc –M main.c		#输出是:	main.o : main.c defs.h

注意:如果使用CNU的C/C++编译器,得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。

这样一来,Makefile也要根据这些源文件重新生成,这个功能并不现实。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。

%.d : %.c
		@sed  -e ; rm  -f  $@ ; \
		$(CC)  -M  $(CPPFLAGS)  $<  > $@. ; \
		sed  ‘s / \ ($*\) \ .o [ :] * / \ 1.o  $@  :  / g’ < $@. > $@; \
		rm  -f  $@.

规则的意思为:所有的[.d]文件依赖于[.c]文件,“rm –f $@”是删除所有的目标,也就是[.d]文件。第二行是为每个依赖文件“$<”,即[.c]文件,生成依赖文件,第二行生成的文件有可能是“name.d.12345”。第三行使用sed命令做了一个替换。第四行删除临时文件。
总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即把依赖关系:

main.o : main.c defs.h

转成:

main.o main.d : main.c defs.h

十三、显示命令(@号)

通常,make会把其要执行的命令行在命令执行前输出在屏幕上。当指令的命令前面加了@字符,就不显示命令本身而只显示它的结果。
如果male执行时,带入make参数“-n”或“–just-print”,那么其只是显示命令,但不会执行命令,这个功能很有利于调试Makefile,看看书写的命令执行起来是什么样子或是什么顺序的。
而make参数“-s”或“–slient”是全面禁止命令的显示。

十四、命令执行(分号)

如果想让上一条结果应用在下一条命令时,应该使用分号分隔这两条命令。如:

exec : 
	cd  /home/hchen
	pwd

执行“make exec”时,cd没有用,pwd会打印当前Makefile目录

exec : 
	cd  /home/hchen ; pwd

执行“make exec”时,pwd会打印/home/hchen

十五、变量赋值(= 和 :=)

使用=赋值时,左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处。不一定非要是已定义好的值,也可以使用后面定义的值。
优点:可以把变量的真实值推到后面定义
劣势:递归定义会让make陷入无限的变量展开过程中取。虽然make有能力检测并报错。但如果在变量中使用函数,会让make运行时非常慢,更糟糕的是,会使得两个make函数“wildcard”和“shell”发生不可预知的错误。

使用 :=赋值可以避免上述错误,但前面的变量不能使用后面的变量,只能使用前面已经定义好了的变量。如:

Y := $(X) bar
X := later

Y的值是“bar”,而不是 later bar。

十六、“#”特性表示空格

操作符的右边很难描述一个空格,先用一个Empty变量来表明变量定义的值开始,后面采用“#”注释符表示变量定义的终止。这样可以定义出值是一个空格的变量

nullstring := 
space := $(nullstring) # end of the line

注意:下面dir变量的定义中,其值后面跟了4个空格。容易出错。

dir = /foo/bar    #directory to put the frobs in

十七、变量替换

“$(var:a=b)”或者“${var:a=b}”——把变量“var”中所有以“a”字串结尾的“a”字串换成“b”字串。其中结尾是“空格”或是“结束符”。

foo := a.o b.o c.o
bar := $(foo : .o = .c)

上述bar的值为“a.c b.c c.c”

“静态模式”型变量替换

bar := $(foo : %.o = %.c)	#可以实现与上述相同功能

十八、变量迭代

例1:

x = y
y = z
z = u
a := $($($(x)))

其中$(x)的值是y,$($(x))是z,$($($(x)))是$(z)是u。

例2:

x = variable1
variable2 := Hello
y = $(subst 1,2, $(x))
z = y
a := $($($(z)))

其中$($($(z)))扩展为$($(y)),再扩展为$($( subst 1,2, $(x)))。$(x)的值为“variable1”。subst函数将“variable1”中所有“1”字串替换成“2”字串。$($( subst 1,2, $(x)))值扩展为$( variable2),得到“Hello”。

例3:

first_second = Hello
a = fisrt
b = second
all = $($a_$b)

其中“$a_$b”组成了“first_second”,$(all)的值就是“Hello”

十九、追加变量值(+=)

给变量追加值

objects = main.o foo.o bar.b
objects += another.o

$(objects)的值变成“main.o foo.o bar.b another.o”

第二行换成如下可以实现相同功能,只不过“+=”更加简洁

objects := $(objects) another.o

如果变量之前没有定义过,那么“+=”会自动变成“=”;如果前面有变量定义,那么“+=”会继承前次操作的赋值符(前面是:=则继承为:=,前面是=则继承为=)。如果是“=”则会发生变量的递归定义,make会自动解决这个问题。

学习参考:跟我一起写 Makefile

你可能感兴趣的:(学习,linux,bash)