【Makefile】04-Makefile书写规则
Makefile的书写规则包含两部分:
- 依赖关系
- 生成目标的方法
在Makefile中,规则的顺序非常重要。Makefile中应当只有一个最终目标,其他目标都是被这个最终目标连带出来的。Makefile中可能会定义很多目标,但第一条规则中的目标将被确认为最终目标。如果第一条规则中有多个目标,则将第一个目标确认为最终目标。
1 规则举例
我们还是通过一段代码来分析:
foo.o : foo.c def.h
cc -c -g foo.c
这个规则表明两件事:
- 文件的依赖关系
- foo.o依赖于foo.c和defs.h,如果foo.c和def.h的文件日期比foo.o要新,或者foo.o不存在,则依赖关系发生。
- 生成foo.o的方法
- 如果需要生成foo.o,就执行cc -c -g foo.c
2 规则的语法
规则语法有如下两种:
targets : prerequisites
command
...
targets : prerequisites ; command
command
...
targets是文件名,以空格分开,可以使用通配符。target通常是一个文件,但也可以写成多个文件。
command是命令行。如果不与target : prerequisites在一行,必须以Tab开头。如果和prerequisites在一行,就要用分号分隔。
如果命令太长,可以用反斜杠\作为换行符。make对一行上有多少个字符并无限制,这主要是为了方便阅读。
一般而言,make会以/bin/sh来执行命令。
3 在规则中使用通配符
make支持三个通配符“*”、“?”、“[…]”。
通配符可以代替一系列的文件,譬如“*.c”表示所有后缀为c的文件。如果在文件名中包含通配符,就要用反斜杠\来转义。
示例一:删除所有.o文件。
clean :
rm -f *.o
示例二:在规则中应用通配符。目标print依赖于所有的.c文件。$?是一个自动化变量,后面再详细讲解,此处不用纠结。
print : *.c
lpr -p $?
touch print
示例三:在变量中应用通配符。注意,变量objects的值就是“*.o”,它并不会展开。Makefile中的变量其实就是C语言中的宏定义。如果要让通配符在变量中展开(让objects的值是所有[.o]文件的集合),可以写成下面的格式:
objects := $(wildcard *.o)
wildcard是Makefile的关键字,我们也会在后面详细阐述。
4 文件搜寻
在实际工程中,存在大量的源文件。通常会将这些源文件分类存放在目录中。当make需要寻找文件的依赖关系时,可以在文件前加上路径。但更好的方法是把一个路径告诉make,让它自动去找。
4.1 使用特殊变量VPATH
如果在Makefile中没有指明VPATH这个变量,make只在当前目录中找寻依赖文件和目标文件。如果定义了这个变量,make在当前目录中找不到文件时,就会到指定的目录中去找寻文件。注意,当前目录是最高优先搜索的目录。
下面的定义指定两个目录“src”和“…/headers”,用冒号分隔。make会按照这个顺序搜索。
4.2 使用make的vpath关键字
vpath(全小写)是make的关键字而非变量,这一点要注意和VPATH的区分。vpath更为灵活,它可以为不同的文件指定不同的搜索目录。
vpath的使用方法有三种:
vpath <pattern> <directories> # 为符合模式<pattern>的文件指定搜索目录<directories>
vpath <pattern> # 清除符合模式<pattern>的文件的搜索目录
vpath # 清除所有已被设置好的文件搜索目录
<pattern>需要包含“%”字符,“%”的意思是匹配零或若干个字符,例如“%.h”表示所有以“.h”结尾的文件。<pattern>指定了要搜索的文件集,而<directories>则指定了<pattern>文件集的搜索目录。譬如以下示例,如果在当前目录下没有找到,则在“…/headers”目录下搜索所有以“.h”结尾的文件:
vpath %.h ../headers
我们可以连续使用vpath语句,指定不同的搜索策略。如果连续的vpath语句中出现了相同的<pattern>或重复<pattern>,make会按照vpath语句的先后顺序来搜索。
以下示例依次在foo、blish、bar目录中查找.c文件。
vpath %.c foo
vpath % blish
vpath %.c bar
以下示例依次在foo、blish、bar目录中搜索.c文件。
vpath %.c foo : bar
vpath %. blish
5 伪目标
前面提到过,clean是一个伪目标。由于伪目标只是一个标签而非实际文件,所以make无法生成其依赖关系和判断其是否要执行。我们必须显式指明这个目标才能使其生效。
伪目标不能和实际文件重名。为了避免和文件重名的情况,我们可以使用“.PHONY”来显式地指明一个target是伪目标。这即是向make说明,无论是否有这个文件,都按伪目标处理。只要有这个声明,不管是否有clean文件,要运行clean目标,就要执行make clean。
.PHONY : clean
clean :
rm *.o test
通常而言,伪目标没有依赖的文件。但我们也可以给伪目标指定依赖文件。
伪目标也可以作为默认目标,只要将其放在Makefile的开始即可。譬如我们想一次生成多个可执行文件,但又想简单地敲个make就完事儿。就可以利用伪目标的特性来实现:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
gcc -o prog1 prog1.o utils.o
prog2 : prog2.o
gcc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
gcc -o prog3 prog3.o sort.o utils.o
我们将伪目标all作为Makefile的默认目标,并使其依赖于其他三个目标。由于伪目标总是会执行,所以它依赖的那三个目标就总是不如all这个目标新。因此,这三个目标总是会重新生成,也就达到了我们的目的。
上述示例还引出了一个知识点——目标也可以成为依赖。此外,伪目标也可以成为依赖。譬如以下示例:
.PHONY : cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
make cleanall将清除所有需要被清除的文件,而make cleanobj和make cleandiff则清除指定类型文件。
6 多目标
Makefile的target可以是多个文件。如果多个target同时依赖于一个文件,而且生成命令大体相似,就可以合并起来。
多个target的command是同一个,可能会带来麻烦。但我们可以使用自动化变量“$@”(自动化变量相关内容会在后面详述),这个变量表示,目前规则中所有target的集合。
为了便于理解,我们还是来看个示例:
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,,$@) > $@中的第一个$表示执行Makefile的一个函数,函数名为subst,后面的为参数。这个函数的功能是截取字符串,$@表示目标的集合(类似数组)。$@依次取出目标,并执行命令。
关于函数,会在后续章节详述。看完函数后,再返回来看这里就容易理解了。
7 静态模式
7.1 静态模式语法
静态模式可以更容易地定义多目标的规则,从而使规则变得更为灵活。
静态规则语法如下:
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...
targets定义了一系列目标文件,可以使用通配符。我们可以将其理解为目标的集合。
target-pattern指明了targets的模式,也就是目标集模式。
prereq-patterns 是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。
如果将<target-pattern>定义成“%.o”,即<target>集合中都是以“.o”结尾的。
如果将<prereq-patterns>定义成“%.c”,意思是对<target-pattern>形成的目标及进行二次定义。计算方法是:取<target-pattern>模式中的“%”,也就是去掉.o这个结尾,并为其加上.c这个结尾,形成新集合。
综上,目标模式或依赖模式中,都应该有%字符。如果文件名中有%,可以用\转义,标明真实的%字符。
7.2 静态模式示例
7.2.1 示例一
objects = foo.o bar.o
all : $(objects)
$(objects) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
上述示例的目标是从$(objects)中获取的,“%.o”标明要所有以“.o”结尾的目标,也就是foo.o和bar.o。依赖模式“%.c”则是取模式“%.o”中的“%”,也就是foo、bar,然后为其添加“.c”的后缀。所以依赖文件就是foo.c和bar.c。
命令中的“$<”和“$@”是自动化变量。“$<”表示所有的依赖目标集(foo.c和bar.c)。“$@”表示目标集(foo.o和bar.o)。
以上规则展开后,等价于以下规则:
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
对于包含大量“.o”文件的工程,利用静态模式可以省去大量规则,效率会大大提高。
7.2.2 示例二
files = foo.elc bar.o lose.o
$(filter %.o,$(files)) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filters %.elc,$(files)) : %.elc : %.el
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.o,$(files))表示调用Makefile的filter函数,过滤$files集合,只要其中模式为“%.o”的内容。
8 自动生成依赖性
8.1 C/C++编译器的-M选项
在Makefile的依赖关系中,可能会需要包含一系列的头文件。假设main.c中包含defs.h,我们之前的写法是:
main.o : main.c defs.h
这种写法要求我们清楚每个源文件包含了哪些头文件,而且源文件发生改动时,也要相应地修改Makefile。对于大型工程而言,这是非常繁琐的工作。为了避免重复劳动,我们可以使用C/C++编译的一个功能。大多数C/C++编译器都支持“-M”选项,功能是自动查找源文件中包含的头文件并生成依赖关系。
譬如以下命令:
cc -M main.c
其输出是:
main.o : main.c defs.h
有一点需要特别注意,GNU的C/C++编译器,-M参数会把标准库头文件也包含进来,所以要使用-MM参数。
8.2 在Makefile中利用-M选项
按上一小节的方法,难道我们还要根据-M的执行结果,每次都动态修改Makefile?让Makefile依赖于源文件的做法并不可取,我们有其他手段来实现这一功能。
GNU组织建议将编译器把为每个源文件自动生成的依赖关系放到一个文件中,为每个“xxx.c”的文件都生成一个“xxx.d”的Makefile文件。.d文件中就存放着.c文件的依赖关系。
make会自动生成或更新.d文件,我们只要在主Makefile中写出.c文件和.d文件的依赖关系即可自动化生成每个文件的依赖关系。
我们通过下main的示例来加深理解:
%.d : %.c
@set -e; rm -rf $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
以上规则的含义是:
- 所有的.d文件依赖于.c文件。
- rm -f $@的含义是删除所有目标(即.d文件)。
- 第三行的意思是,为每个依赖文件“$<”,也就是.c文件生成依赖文件。
- $@表示模式%.d文件。对于xxx.c而言,%就是name。
- “$$$$”意为一个随机编号,第三行生成的文件有可能是“name.d.12345”。
- 第四行使用sed命令做了一个替换。
- 第五行是删除临时文件。
总而言之,这个模式就是在编译器生成的依赖关系中加入.d文件的依赖,即将依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
这样一来,.d文件也会自动生成并更新。我们不仅能在这个.d文件中加入依赖关系,也可以将生成命令一并加入,让每个.d文件都包含一个完整的规则。
接下来,就要使用include将这些自动生成的规则放进主Makefile中。譬如:
source = foo.c bar.c
include $(source:.c=.d)
.c=.d的意思是将变量$(source)中的所有.c替换成.d。这里要注意次序,因为include按次序载入文件,最先载入的.d文件中的目标会成为默认目标。关于替换,后面还会详述。