写Rule(三)

来源:互联网 发布:淘宝二手镜头可以用吗 编辑:程序博客网 时间:2024/05/20 00:38

4 写Rule

用来描述如何以及何时创建特定文件的规则,叫做目标规则(rule’s target)。它(rule)列出target的prerequisite(依赖项),以及创建或更新target的recipe(规则)。

在一个makefile中这种rule可以有很多,他们的顺序并不重要。在执行make命令时如果没有指定,则第一个makefile文件中出现的第一个rule所确定的target将成为default goal。如果这个rule有若干target,则只有第一个被当作default goal。但是有两个例外:target以.开头时不被当作default goal,除非它包含若干/;定义了模板规则的target不会被当作default goal。

因此,我们通常写一个makefile时,第一个rule为编译的程序的入口,或者所有的程序被这个makefile描述。

4.1 Rule 语法

通常,一个rule看起来像这样:

targets : prerequisites    recipe    ...

或者:

targets : prerequisites ; recipe    recipe    ...

targets是文件名,用空格隔开,可以使用通配符。通常target只有一个,但有时也会有多个。

recipe以tab字符开始,并位于prerequisite的下一行,也可以放在prerequisite后面,用分号隔开。

$符号在make中表示变量引用的开始,如果在target或prerequisite中想要输入一个$字符,可以用两个$$来表示。如果你使用了二次展开(secondary expansion),那么在target或prerequisite中输入一个$字符时,你必须输入四个$$$$

你可以用反斜杠\分割一个长行,但这并不是必须的,因为make并不限制一行的长度。

rule告诉了make两件事:什么时候targets过期,以及必要时如何更新它们。

一个target过期的意思是,它不存在,或者它的prerequisite(依赖项)中,有一个或多个文件最后的修改时间比target更新。也就是说target的内容是否被计算,取决于它的prerequisite(依赖项)的信息,因此任何prerequisite发生了改变时,target的内容也就不再有效了。

如何更新target取决于recipe,而这些recipe被shell执行,并依赖于外部特性。

4.2 Prerequisites的种类

GNU make可以识别两种不同的prerequisites:上面所描述的普通prerequisite,以及order-only prerequisites。普通的prerequisite声明了两件事:首先,它指定了一个顺序——prerequisite的recipe会在target的recipe调用之前完成执行;其次,它确定了一个依赖关系——任何比target新的prerequisite存在时,target将过期,且必须被重建。

通常,当prerequisite被更新时,对应的target也应该被更新。

有时候,你会遇到这么一种情形:你想让rules按特定的顺序被调用,当规则中的一条被执行时,你不想让target更新。这种情况下,你可以使用order-only prerequisite。order-only prerequisite 是用管道标志符|来指定的,在管道标志符左边的是普通prerequisite,在管道标志符右边的是order-only prerequisite:

targets : normal-prerequisites | order-only-prerequisites

其中,normal-prerequisites可以为空。你也可以在多行中指定prerequisite,当同时指定normal-prerequisite和order-only prerequisite时,normal-prerequisite拥有更高的优先级。

想象如下情况,你的targets被放置在不同的目录下,而这些目录在make运行之前可能还不存在。此时,你希望这些目录在target写入之前先创建,但是由于目录的时间戳在文件被添加、删除或重命名时会发生改变,而我们又不想在目录时间戳发生变化时重建targets。处理这种情况的一种方式就是将目录作为这些targets的order-only prerequisite:

OBJDIR := objdirOBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o)$(OBJDIR)/%.o : %.c    $(COMPILE.c) $(OUTPUT_OPTION) $<all : $(OBJS)$(OBJS) : | $(OBJDIR)$(OBJDIR) :    mkdir $(OBJDIR)

现在,在任何.o文件被建立之前,创建objdir目录的规则会运行。而当objdir目录的时间戳改变时,.o文件不会被重建。

4.3 文件名中通配符的使用

使用通配符可以用一个文件名指代很多文件,与Bourn shell一样,make中使用的通配符有*, ?[...]。例如*.c表示所有的后缀为.c的文件。

~在文件名开始有特殊的意义。如果单独使用或者后面跟/,则表示你的家目录(home directory),例如,~/bin会展开成/home/you/bin。如果~后跟一个单词,这个字符串代表这个单词所指的用户名的家目录,例如,~john/bin会展开为/home/john/bin。在Windows系统中没有家目录的概念,你可以使用环境变量HOME模拟这个功能。

make会自动将targets和prerequisites中的通配符展开,但是recipe中的通配符却是由运行make的shell展开的。其它情况下,通配符只有在你显式调用通配符函数(wildcard function)时展开。

通配符的特殊含义可以通过转义符\关闭,这样,foo\*bar就表示一个文件名,由foo, *bar组成。

4.3.1 通配符示例

通配符可以用在rule的recipe中,其由shell展开。例如,下面是一个删除所有中间文件(objcet file)的rule:

clean:     rm -f *.o

通配符也可以用于rule的prerequisite中,下面的rule中,make print命令会打印所有自上次打印以来所有改变过的的.c文件:

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

这条rule中,使用print作为一个空的target文件,自动变量$?用来打印那些改变了的文件。

当你定义一个变量的时候,通配符不会被展开,因此当你定义

objects = *.o

时,objects确实就是字符串*.o本身。然而,当你在target或prerequisite中使用这个变量时,通配符将会在变量使用的地方展开。如果你在recipe中使用objects,那么shell会在这个recipe运行的时候将通配符展开。如果要在定义时就展开,可以这样定义变量:

objects := $(wildcard *.o)

4.3.2 使用通配符的误区

下面有一个简单的使用通配符的示例,它不会按照你想的方式工作。假设你想要可执行文件foo的创建依赖于目录中的所有object文件,你这样写道:

objects = *.ofoo : $(objects)    cc -o foo $(CFLAGS) $(objects)

objects的实际值是字符串*.o。通配符在foo的rule中展开,每一个已经存在的.o文件都成为foo的prerequisite,并且在必要时被重新编译。

但是倘若你删除了所有的.o文件呢?此时通配符匹配不到任何文件,那字符串*.o本身就被当作一个文件名,又因为这样的文件不存在,所以make将会给出不知道如何创建文件*.o的错误信息,而这并不是你所希望的。

事实上,是可以实现上面例子中所想要达到的通配符展开的目的的,但这需要更复杂的技术,包括wildcard函数以及字符串替换,而这些会在后面讨论。

Windows系统中使用反斜杆\分割路径名,如:

c:\foo\bar\baz.c

这等效于Unix风格c:/foo/bar/baz.cc:是驱动器号)。make运行在Windows系统中时,它支持反斜杆\,也支持Unix风格的正斜杆/。但是这种支持不包括通配符的展开,此时反斜杆\是一个引用字符。所以,此时你必须使用Unix风格的斜杆/

4.3.3 通配符函数

通配符展开在rules中自动发生,但是当设置一个变量,或者在一个函数变量的内部时,通配符并不会正常展开。此时如果想让通配符展开,需要用到通配符函数:

$(wildcard pattern ...)

这个字符串可以在makefile文件中的任何地方使用,它会展开成一个用空格分开的文件列表,这些文件匹配给定的模板pattern。当没有文件匹配时,通配符函数输出中的pattern会被省略,这不同于rule中的通配符(如果没有匹配项就使用字面值,而不是忽略)。

使用通配符函数获取一个目录下的所有.c源文件:

$(wildcard *.c)

我们可以通过替换.c.o,从而获得一个object文件列表:

$(patsubst %.c,%.o,$(wildcard *.c))

这儿我们使用的patsubst函数会在之后讨论。

因此,一个makefile文件编译所有的.c文件,并在之后链接他们可以写成这样:

objects := $(patsubst %.c,%.o,$(wildcard *.c))foo : $(objects)    cc -o foo $(objects)

4.4 在目录中搜寻prerequisites

对于大型系统,通常把源文件和二进制文件放在不同的目录中。make的目录搜索特性(directory search)有助于在若干目录中找到一个prerequisite。当把文件分放于不同目录时,你不需要去改动单个rule,只需变动搜索路径。

4.4.1 VPATH:所有prerequisites搜索路径

make中的变量VPATH指定了一个搜索目录列表。通常这些目录会包含不在当前目录的prerequisites文件,但其实make使用VPATH作为targets和prerequisites的搜索路径。

当一个作为target或prerequisite的文件不在当前目录时,make会在VPATH所列的目录中搜寻。如果在其中找到该文件,那么该文件就被作为prerequisite。

VPATH变量中,目录名以冒号或空白分割,它们的顺序就是make之后搜寻的顺序。(在Windows系统中使用分号;分割,因为冒号用在了驱动器号中,如c:\

例如:

VPATH = src:../headers

其中包含两个目录src../headers,make按这个顺序搜寻。

根据VPATH的值,下面的rule:

foo.o : foo.c

被解释成如下写法:

foo.o : src/foo.c

(假设foo.c没有在当前目录下找到,而是在src中被发现。)

4.4.2 vpath命令

vpath命令类似于变量VPATH,但是具有更强的选择性。vpath允许匹配特定模板的文件搜寻特定的目录,这样你就可以对不同类型的文件搜寻不同的目录了。

vpath有三种命令形式:

vpath pattern directories

对符合模板pattern的文件,指定搜寻目录directories列表,目录之间用冒号:(Windows系统使用分号)或空白隔开,与VPATH类似。

vpath pattern

清除模板pattern的搜索路径。

vpath

清除所有模板的搜索路径。

vpath模板是一个包含%的字符串,这个字符串必须匹配被搜寻的prerequisite文件名,%表示任意多个任意字符。例如,%.h可以匹配所有的.h文件(不使用%的模板只能匹配特定的同名文件,通常不怎么使用)。

vpathpattern中可以使用反斜杆\引用%,而反斜杆本身也可以用反斜杆引用。当pattern与文件名比较时,引用符\会被移除,保留%\本身。

当prerequisite文件不在当前目录时,其匹配的模板pattern所指定的目录directories将会被搜索,与VPATH变量一样。例如:

vpath %.h ../headers

告诉make任何在当前目录找不到的.h头文件,都可以去../headers中查找。

如果有多个vpath模板匹配到prerequisite文件名,make会依次处理每一个匹配到的vpath命令,查找每个命令中提到的directories。make按vpath命令出现的顺序处理相同的匹配模板,因此:

vpath %.c foovpath %   blishvpath %.c bar

将会先在目录foo中查找.c文件,然后是目录blish,然后是目录bar。而:

vpath %.c foo:barvpath %   blish

会先在目录foo查找.c文件,然后是目录bar,然后是目录blish

4.4.3 如何执行目录查找

当通过目录查找找到一个prerequisite文件(无论是VPATH,还是vpath),这个文件所在的目录可能并不会被make作为prerequisite文件列表的目录,有时候这个目录会被抛弃。

下面的规则说明make何时保留或抛弃一个通过目录查找确定的目录:

  1. 如果target文件不在makefile指定的路径中,目录查找就会被执行。

  2. 如果目录查找成功,这个路径会被保留,并且找到的文件会作为临时的target。

  3. 这个target的所有prerequisites都会通过这个方法进行审核。

  4. prerequisites被处理完之后,target可能会被重建,也可能不会:

    • 如果target不需要重建,前面找到的目录会被用于包含这个target的所有rerequisite列表。简单来说,如果make不需要重建target,那这个找到的目录就会被使用。

    • 如果target需要被重建(target过期),这个找到的目录就会被抛弃,并且target会在makefile指定的目录中重建。简单来说,如果make必须重建,那么target会在本地重建,而不是目录查找中找到的目录。

这个规则可能看起来很复杂,但在实际使用中其执行过程正是你想要的。

其他版本的make使用一个简单的规则:如果文件不存在,但在目录查找中找到,那么这个找到的目录总是会被使用,不管target是否需要重建。这样,如果需要时,target会在找到的目录中被重建。

如果这正是你需要的,你可以使用变量GPATHGPATH语法与VPATH相同(使用空格或冒号分割路径名)。如果一个过期的target在目录查找中被发现,并且这个找到的目录也在GPATH中,那么这个目录就不会被抛弃,同时target会在其中重建。

4.4.4 使用目录查找编写recipe

当一个prerequisite文件在目录查找中被找到,这并不会改变rule中的recipe,它们会按照编写的格式执行。因此,你必须仔细的编写recipe语句,以使其会查看make在目录查找中找到的目录。

自动变量$^可以达到上面的目的。$^的值是rule的所有的prerequisite文件,包括找到他们的目录名。变量$@的值是target。例如:

foo.o : foo.c    cc -c $(CFLAGS) $^ -o $@

(变量CFLAGS用于指定C编译标志)

通常prerequisites也包含头文件,这样可以避免将头文件放在recipe语句中。自动变量$<表示第一个prerequisite文件:

VPATH = src:../headersfoo.o : foo.c defs.h hack.h    cc -c $(CFLAGS) $< -o $@

这儿$<表示foo.c

4.4.5 目录查找和隐含规则

通过VPATHvpath进行的查找也发生在隐含规则中。例如,当一个文件foo.o没有显式规则时,make会使用内建的隐含规则对foo.c(如果存在)进行编译。如果在当前目录没有找到foo.c,就会在适当的目录中查找。如果找到foo.c.c文件编译的隐含规则就会被调用。隐含规则的recipe通常使用自动变量,因此它们使用目录查找中找到的文件名。

4.4.6 链接库文件的目录查找

连接器(linker)对库文件进行目录查找时,会使用特殊的方式。你需要通过-lname的形式写一个prerequisite文件(你可能感到奇怪,一个库文件的名字一般看起来像libname.a,而不是-lname。)

当一个prerequisite文件有-lname形式的名字时,make将会寻找libname.so的库文件,如果在当前目录没有找到,就在VPATHvpath指定的目录中查找,如果还没有找到,就在目录/lib/usr/libprefib/lib(通常是/usr/local/lib,但在Windows系统中,prefix被定义为DJGPP的安装目录)中寻找。如果还没找到,就重复上面的过程,寻找文件名为libname.a的库文件。

例如,系统中存在/usr/lib/libcurses.a库文件(并且不存在/sur/lib/libcurses.so文件),那么

foo : foo.c -lcurses    cc $^ -o $@

foofoo.c/sur/lib/libcurses.a更旧时,将会执行命令cc foo.c /usr/lib/libcurses.a -o foo

默认情况下搜寻libname.solibname.a文件,但是用户可以通过变量.LIBPATTERNS自定义搜寻的文件类型。这个变量的每一个单词就是一个模板字符串,当在prerequisite中出现-lname文件时,make就会用name替换每一个模板中的百分号%,并使用得到的库文件名进行上面的目录搜索。

变量.LIBPATTERNS的默认值是lib%.so lib%.a,这也就是上面描述的默认搜索模式。

你可以给变量.LIBPATTERNS设置一个空值,这样就会禁止展开并链接库文件。

4.5 伪目标(Phony Targets)

伪目标不是一个真实的文件名,而是当你显式指定时,recipe命令执行的名字。使用伪目标有两个原因:避免跟同名文件冲突,改善make执行方式。

假设你写了一个rule,它的recipe不创建目标文件,那么这个rule的target会在你指定时被执行,如下示例:

clean :    rm *.o temp

因为rm并不创建一个名为clean的文件(可能并没有这么一个文件存在),当你执行make clean时,rm命令将会被执行。

在这个例子中,如果同目录下存在一个名为clean的文件,那么clean作为target将不会正常工作。因为它没有prerequisites,clean始终被认为是最新的,即它的recipe将永远不会被执行。为了避免这种情况发生,你可以使用.PHONY显式指定一个target为伪目标:

.PHONY : cleanclean :     rm *.o temp

这时,不管目录下是否有clean文件存在,make clean命令都会正常执行。

伪目标在连接make的递归调用时也很有用。在这种情况下,makefile通常包含一个列出需要被建立的一系列子目录的变量。

一个简单的处理方式是定义一个rule,它的recipe循环遍历子目录,就像这样:

SUBDIRS = foo bar bazsubdirs :        for dir in $(SUBDIRS); do \            $(MAKE) -C $$dir; \        done

然而这种方法存在一些问题。首先,在这个rule中,子make(sub-make)的任何错误都会被忽略,所以当一个失败时,它会继续创建剩下的目录。可以通过命令行参数显示错误信息并退出make执行过程,但不幸的是,当make使用-k选项时还是会发生上面的问题。其次,你不能充分利用make并行创建targets的能力,因为只有一条rule。第二点可能更重要。

通过将子目录声明为伪目标可以去除这些问题(你必须这么做,因为子目录通常都存在,否则就不会被建立):

SUBDIRS = foo bar baz.PHONY : subdirs $(SUBDIRS)subdirs : $(SUBDIRS)$(SUBDIRS) :        $(MAKE) -C $@foo : baz

这儿我们还声明了子目录foo必须在baz之后建立,这种声明关系在并行建立目录时相当重要。

隐式规则会跳过.PHONY进行搜索,这就是为什么将一个target声明为.PHONY时,即使同名文件存在也可以正常运行的原因。

伪目标不能成为一个真实目标文件的prerequisite,否则,每次make更新真实目标文件时,它(伪目标)的recipe都会被执行。当伪目标不是一个真实文件的prerequisite时,只有将伪目标指定为make的目标时,它(伪目标)的recipe才会被执行。

伪目标可以有prerequisites,当一个目录包含多个程序,使用一个./Makefile文件描述所有这些程序会很方便。默认情况下,被建立的目标是makefile中的第一个,如果将所有的独立程序作为伪目标的prerequisites:

all : prog1 prog2 prog3.PHONY : allprog1 : prog1.o utils.o        cc -o prog1 prog1.o utils.oprog2 : prog2.o        cc -o prog2 prog2.oprog3 : prog3.o sort.o utils.o        cc -o prog3 porg3.o sort.o utils.o

现在你可以只输入一个make就能重建所有三个程序,或者将某一个程序作为make的参数进行指定重建(例如,make prog1 prog3)。伪目标不能继承,一个伪目标的prerequisite不是伪目标(除非它们被声明为伪目标)。

如果一个伪目标是另一个伪目标的prerequisite,那么它会作为另一个伪目标的子程序。下面的例子中,执行make cleanall将会删除.o文件、.diff文件和文件program

.PHONY : cleanall cleanobj cleandiffcleanall : cleanobj cleandiff        rm programcleanobj :         rm *.ocleandiff :        rm *.diff

4.6 没有recipe和prerequisites的rule

如果一个rule没有prerequisite和recipe,并且target是一个并不存在的文件,那么当这个rule运行时,这个target总是会被更新。这意味着所有基于这个target的targets,他们的recipe总是会被执行。

一个示例可以说明这些:

clean : FORCE        rm $(objects)FORCE :

这儿目标FORCE满足上面的情况,因此基于它的目标clean将会强制执行其recipe。名字FORCE并没有什么特殊,但通常在这种情况下被使用。

你应该可以看出来,这样使用FORCE的方式,与使用语法.PHONY : clean作用相同。

使用.PHONY更加清晰高效,但其他版本的make不支持.PHONY,因此FORCE出现在许多makefile中。

4.7 记录事件的空目标(Empty Target)文件

空目标是伪目标的一个变体,通常用于显式指定的recipe行为。不同于伪目标,空目标的target文件可以实际存在,但是文件内容无关紧要,通常是空文件。

空目标文件通常用于如下情况:当recipe被执行时,空目标文件记录最后的修改时间。之所以会这样,是因为recipe中的命令之一是一个用于更新target文件的修改命令(touch command)。

空目标文件应该有一些prerequisites(否则就没意义了),如果任何prerequisites较target新(换句话说就是在上次重建target以来有prerequisite文件被修改了),当你要求重建target时,recipe就会被执行。下面是一个例子:

print : foo.c bar.c        lpr -p $?        touch print

自上次执行make print以来,如果任何源文件被修改,那么make print将会执行lpr命令。自动变量$?表示被修改的文件。

4.8 特殊的内建目标名

以下名字出现在目标中有特殊含义:

.PHONY

.PHONY的prerequisites将会被当作伪目标,当被make当作一个伪目标时,不管同名文件是否存在,也不管这个文件的最后修改时间,伪目标的recipe总会被执行。


.SUFFIXES

.SUFFIXES的prerequisites是一个用于检查前缀规则(suffix rules)的前缀列表。


.DEFAULT

.DEFAULT的recipe被用于那些没有规则的target。如果.DEFAULT 的recipe被指定,每个在rule中被看作prerequisite而不是target的文件都会对它们自己执行这个recipe。


.PRECIOUS

基于.PRECIOUS的targets拥有如下特性:当在执行他们的recipe时,如果make被中断或退出,它们的targets将不会被删除。当然,如果这个target是一个中间文件,那它并不会在不需要时被删除(通常中间文件需要被删除)。这和.SECONDARY作用类似。你也可以将一个隐含规则的目标模板(例如,%.o)作为特殊目标.PRECIOUS的prerequisite,用于保留那些target匹配这个prerequisite的rule创建的中间文件。(类似于作为.PHONY的所有prerequisites都是伪目标,作为.PRECIOUS的所有prerequisites都会被保留)


.INTERMEDIATE

基于.INTERMEDIATE的targets都是中间文件,没有prerequisites的.INTERMEDIATE没有意义。


.SECONDARY

基于.SECONDARY的targets被当作中间文件,除非它们不会被自动删除。没有prerequisites的.SECONDARY会导致所有的targets都被当作中间文件(也就是说,没有target被删除,因为它被当作中间文件)。


.SECONDEXPANSION

如果.SECONDEXPANSION在makefile文件的任何位置被当作target,所有定义了after的prerequisites列表将会在所有makefile文件被读入以后再次展开。


.DELETE_ON_ERROR

如果.DELETE_ON_ERROR在makefile的任何位置被当作target,那么当某些rule的recipe以非零状态退出,并且其target已经被改变时,这些target会被删除。


.IGNORE

如果为.IGNORE指定prerequisites,那么make会忽略这些文件的recipe产生的错误。.IGNORE的recipe(如果有的话)将会被忽略。.IGNORE的使用只是为了向后兼容,因为其影响makefile中的所有recipe,所以并不是很有用。我们建议你使用更有选择性的方式来忽略错误信息,这将在后面讨论。


.LOW_RESOLUTION_TIME

如果你为.LOW_RESOLUTION_TIME指定prerequisites,那么make会认为这些文件是被产生低分辨率时间戳的命令所创建。.LOW_RESOLUTION_TIME的target的recipe会被忽略。现代系统的高分辨率时间戳减少了make误判文件被更新的机会。不幸的是,一些主机并没有提供设置高分辨率时间戳的方法,因此,诸如cp -p显式设置文件时间戳的命令必须去掉时间戳中次秒级(sub-second,小于秒的部分)的部分。如果一个文件被这种命令所创建,你应该将它列入.LOW_RESOLUTION_TIME的prerequisite中,这样make就不会误以为这个文件过时了。例如:

.LOW_RESOLUTION_TIME : dst dst : src cp -p src dst

由于cp -p去掉了src文件的时间戳的次秒级部分,当dst被更新时,它也只是比src稍微旧一点。当dst与src有相同的秒级时间戳,.LOW_RESOLUTION_TIME行会让make认为dst需要被更新。

由于存档格式的限制,文件总是具有低分辨率的时间戳。你不需要将存档成员列入prerequisites,因为make会自动这么做。


.SILENT

当你为.SILENT指定prerequisites,在make执行它们之前,将不会打印用于重建这些文件的recipe。.SILENT的recipe被忽略。
如果上面提到的target文件没有prerequisites,.SILENT表示在执行它们之前不要打印任何recipe。.SILENT只是为了向后兼容,我们建议你使用更具选择性的方式来不打印特定的recipe。如果你在执行make时不想打印所有的recipe,可以使用-s--silent选项。


.EXPORT_ALL_VARIABLES

作为一个target使用,告诉make将本makefile的所有变量在其子程序中可见。


.NOTPARALLEL

如果.NOTPARALLEL作为target,make执行这个makefile将使用串行方式,即使有-j选项。任何这个makefile递归调用的make命令将并行执行recipe(除非该makefile中包含这个target)。这个target的prerequisites将被忽略。


.ONESHELL

如果.ONESHELL作为target,那么当一个target被创建时,它的所有recipe行会当作一个命令被shell执行,而不是一个一个的分开执行。


.POSIX

如果.POSIX作为target,makefile会在POSIX模式下被分析并运行。这并不是说只有POSIX一致的makefile才会被接受:GNU make的所有高级特性仍让可用。而是这个target导致make的行为遵循POSIX。

特别是,当这个target被指定时,之后的recipe调用将会等同于shell被设置了-e标志:recipe中第一个失败的命令会立即终止recipe。

任何定义了隐式规则的后缀也被看作是特殊的target(如果它被作为target),即使是相连的两个后缀(如,.c.o)。这些target叫做后缀规则(suffix rules),是一种过时的定义隐式规则的方式(但仍然被广泛使用)。原则上讲,任何target的名字都可以被当作这种特殊特target(当你将其分为两半并加入到后缀列表中)。实际应用中,后缀通常以.开始,因此这些特殊的target也用.开始。

4.9 多目标规则

一个多目标规则等同于多个规则,每个规则有一个目标,其余部分完全一样。相同的recipe作用于所有的target上,但是效果并不一定相同,因为可以使用$@代替recipe中的实际target名。相同的prerequisites对于target也是一样。

在两种情况下这比较有用:

如果你只想要prerequisites,而没有recipe,例如:

kbd.o command.o files.o : command.h

给每个object文件增减了一个command.h依赖文件。

相似的recipes作用于所有的targets。recipes不需要完全相同,因为自动变量$@可以替换命令中需要重建的特定targets,例如:

bigoutput littleoutput : text.g    generate text.g -$(subst output,,$@) > $@

等效于

bigoutput : text.g        generate text.g -big > bigoutputlittleoutput : text.g        generate text.g -little > littleouput

(这儿虚构的程序generate创建了两个不同类型的文件,-big-little

变量$@允许你根据target调整prerequisites和recipe。在普通规则中你不能使用这种多目标规则,但是在静态模板规则中可以。

4.10 同一target拥有多个规则

一个文件可以成为多个规则的target,所有规则中的prerequisites会合并成target的一个prerequisites列表。如果这个target比其中任何一个prerequisite旧,则recipe将会被执行。

对于一个文件只有一个recipe可以被执行,如果对于同一个文件存在多个recipe,那么make会使用最后那个,并打印一份错误信息(有一个例外,如果文件名以.开始,将不会有错误信息。这个奇怪的行为只是为了兼容其它版本的make实现,应尽量避免使用)。有时候在不同的makefile文件中,为相同名字的target调用不同的recipe是很有用的,这在双冒号规则中讨论。

一个只包含prerequisites的规则,可以用于给一些文件一次性增加若干prerequisites。例如,makefile通常有一个变量objects,其中包含所有的编译输出文件。如果文件config.h发生改变时,为了让所有objects全部重建,一个简单的方法是:

objects = foo.o bar.ofoo.o : defs.hbar.o : defs.h test.h$(objects) : config.h

这样,在不改变objects的实际创建规则前提下,可以很方便的把依赖文件config.h加进去。

另一个技巧是,可以通过变量来指定要增加的依赖项,你只需要在执行make时提供对应的参数即可,例如:

extradeps = $(objects) : $(extradeps)

这样,执行make extradeps=foo.h命令将会把foo.h加入到每一个objects的依赖项列表中,如果只是执行make则不会。

如果所有这些target的显式规则中都没有recipe的话,make会查找可使用的隐式规则。

4.11 静态模板规则

静态模板规则是针对多目标的规则,它可以根据每个的target名字构建prerequisites。对于多目标规则来说,静态模板规则比普通规则更加通用,因为一般情况下targets不会有完全相同的prerequisites列表。它们的prerequisites列表必须相似,但不必完全相同。

4.11.1 静态模板规则的语法

静态模板规则的语法如下:

targets ...: target-pattern: prereq-patterns ...    recipe    ...

targets列表指定这条规则的应用对象。其中使用包含通配符,与普通规则的targets一样。

target-patternprereq-patterns描述如何构建target的prerequisite列表。每个target匹配于target-pattern的部分叫做词干(stem)。词干(stem)会替换到prereq-patterns模板中,用于构建prerequisites列表。

每个模板通常只含有一个%字符。当target-pattern匹配一个target时,%可以匹配target名字的任意部分,这部分叫做词干。模板的剩余部分必须完全匹配,例如,foo.o匹配%.o,其中foo是词干。foo.cfoo.out不会匹配这个模板。

每个target的prerequisite名字是通过将prereq-patterns中的%替换为对应词干得到的。例如,如果一个prereq-patterns%.c,那么prerequisite就是foo.cprereq-patterns中不包含%是合法的,此时对所有targets来说,prerequisites将完全相同。

在模板规则中,%字符可以被反斜杆\引用(\%),反斜杆可以引用自己(\\)。在模板进行比较或者替换时,引用符\会被去除。使用反斜杆引用%将会造成语义上的复杂化。例如,模板the\%weird\\%pattern\\被真正的通配符%分成了两部分:the%weird\pattern\\。最后的两个反斜杆被保留,因为它们不会影响任何的%

下面是一个例子,foo.cbar.o根据其对应的.c文件编译获得:

objects = foo.o bar.oall: $(objects)$(objects): %.o: %.c        $(CC) -c $(CFLAGS) $< -o $@

这里自动变量$<代表prerequisite名,$@代表target名。

每个target必须匹配target-pattern,对于不匹配的target,将会给出警告。如果文件列表中只有一部分文件匹配模板,可以使用filter函数过滤掉不匹配的文件名:

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 %<

其中$(filter %.o,$(files))的结果是bar.o lose.o,第一个静态模板规则中通过编译对应的C源文件更新object文件。$(filter %.elc,$(files))的结果是foo.elc,因此由foo.el创建。

另一个例子展示如何在静态模板规则中使用$*

bigoutput littleoutput : %output : text.g    generate text.g -$* > $@

generate命令运行时,$*会展开成词干big或者little

4.11.2 静态模板规则与隐式规则

静态模板规则与定义了模板的隐式规则非常类似。两者都有针对target的模板和构建prerequisites名的模板。不同之处在于make如何决定这些规则何时被使用。

隐式规则可以应用于匹配其模板的所有targets,但是只有没有其他recipe被指定,或者prerequisites可以被找到时,隐式规则才会被使用。如果有多余一个的隐式规则可以被应用,则只有一个会被使用,其选择取决于规则的顺序。

相反,静态模板规则可以精确应用于你在规则中指定的特定的targets。它不会应用于其它target,并且始终应用于每个特定的target。如果有两条规则发生冲突,并且都有recipe,就会报错。

基于如下两个原因,静态模板规则优于隐式规则:

  • 有些文件的文件名无法在语法上进行分类时,你可以使用显式列表覆盖掉普通的隐式规则。

  • 如果你无法确认正在使用的目录下有哪些文件,并且无法确认那些无关文件会使make调用错误的隐式规则,这些隐式规则的选取依赖于搜寻顺序。使用静态模板规则可以避免这些不确定性:每条规则都精确对应于特定的targets。

4.12 双冒号规则

双冒号规则是指在target名字后面用::代替:。当一个target出现在多个规则中时,双冒号规则和普通规则有不同的处理方式。双冒号模板规则有用完全不同的意义。

当target出现在不同的规则中时,这些规则必须拥有相同的类型:或者普通规则,或者双冒号规则。如果是双冒号,那这些target都是相互独立的,每个target会在其对应的prerequisites更新时更新。如果没有对应的prerequisites,则该规则总是被执行(即使对应的target已经存在)。双冒号规则对应的所有target中,可能有任意多个会被执行。

拥有相同target的双冒号规则实际上是完全互相独立的,每个规则都独自处理,就像对于不同的target一样。

一个target的双冒号规则执行顺序取决于规则在makefile中出现的顺序。但是实际的执行顺序又无所谓。

双冒号规则隐晦而又不常使用,它提供了一种机制:一个target基于不同的prerequisites进行更新,但是这种情况比较罕见。

每个双冒号规则应该指定一个recipe,如果没有指定,隐式规则将会被使用。

4.13 自动生成prerequisites

在一个程序的makefile中,你需要写很多规则来描述哪些对象依赖于哪些头文件。例如,如果main.c通过#include包含defs.h,你可以这样写:

main.o: defs.h

你需要通过这条规则让make知道,当defs.h发生改变时,要重建main.o。在一个大型程序中,你要写几十个这样的规则,而且在源文件中增加或减少#include语句时,你还要小心的增加或删除makefile中对应的规则。

为了避免这种繁琐的工作,大多数现代C编译器会通过查看源文件的#include,为你写出这些规则。通常编译器使用-M选项来实现这个功能。例如:

cc -M main.c

产生如下输出:

main.o : main.c defs.h

这样,你不在需要手动输入这些规则了,编译器会帮你做。

注意,这条规则在makefile文件中构建main.o,因此隐式规则不会把它当作中间文件。也就是说make不会在使用过它之后将其删除。

在老式make程序中,要实现这个功能需要使用命令make depend。这条命令会创建一个depend文件,其中包含自动生成的prerequisites,之后你可以在makefile中使用include语句将其读进来。

在GNU make中,重建makefile文件的特性使这个功能不再需要,你不需要显式的告诉make去重新生成prerequisites,因为当makefile过期时会自动重新生成。

推荐的自动生成prerequisite的方法是让每一个源文件对应一个makefile。对每一个源文件name.c,对应一个名为name.d的makefile,其中列出了name.o的所有依赖文件。这样,只有源文件发生改变时,才会被重新检查,生成新的prerequisites。

下面是一个模板规则,用于从C源文件name.c生成名为name.d的依赖项文件:

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

-e标志指定当$(CC)命令失败时(或者其它任何命令失败时),shell会立即退出。

对于GNU C 编译器,可以使用-MM代替-M,这可以省略对系统头文件的依赖规则。

sed命令的目的是将:

main.o : main.c defs.h

翻译成:

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

这会使每个.d文件与对应的.o文件依赖于相同的源文件和头文件。当任何源文件或头文件改变时,make就会重新生成依赖项。

一旦你定义了重建.d文件的规则,就可以使用include命令将它们读进来。例如:

sources = foo.c bar.cinclude $(source:.c=.d)

其中使用了变量引用替换,将根据源文件列表foo.c bar.c生成依赖makefile文件列表foo.d bar.d.d文件类似于其它makefile文件,make会在必要时重建它们。

注意,.d文件中包含target定义,要保证include命令放在第一个(默认)目标之后,否则有可能使用一个随机的object文件作为默认目标。

原创粉丝点击