第四章:Makefile的规则

来源:互联网 发布:多益网络offer 编辑:程序博客网 时间:2024/04/27 20:16
 

第四章:Makefile的规则


本章我们将讨论Makefile的一个重要内容,规则。熟悉规则对于书写Makefile至关重要。Makefile中,规则描述了在何种情况下使用什么命令来重建一个特定的文件,此文件被称为规则“目标”(通常规则中的目标只有一个)。规则中出目标之外的罗列的其它文件称为“目标”的依赖,而规则的命令是用来更新或者创建此规则的目标。

除了makefile的“终极目标”所在的规则以外,其它规则的顺序在makefile文件中没有意义。“终极目标”就是当没有使用make 命令行指定具体目标时,make默认的更新的哪一个目标。它是makefile文件中第一个规则的目标。如果在makefile中第一个规则有多个目标的话,那么多个目标中的第一个将会被作为make的“终极目标”。有两种情况的例外:1. 目标名以点号“.”开始的并且其后不存在斜线“/”(“./”被认为是当前目录;“../”被认为是上一级目录);2. 模式规则的目标。当这两种目标所在的规则是Makefile的第一个规则时,它们并不会被作为“终极目标”。

“终极目标”是执行make的唯一目的,其所在的规则作为第一个被执行的规则。而其它的规则是在完成重建“终极目标”的过程中被连带出来的。所以这些目标所在规则在Makefile中的顺序无关紧要。

因此,我们书写的makefile的第一个规则应该就是重建整个程序或者多个程序的依赖关系和执行命令的描述。

4.1 一个例子

我们来看一个规则的例子:

 

foo.o : foo.c defs.h       # module for twiddling the frobs

cc -c -g foo.c

 

这是一个典型的规则。看到这个例子,大家应该能够说出这个规则的各个部分之间的关系。不过我们还是要把这个例子拿出来讨论。目的是让我们更加明确地理解Makefile的规则。本例第一行中,文件“foo.o”是规则需要重建的文件,而“foo.c”和“defs.h”是重建“foo.o”所要使用的文件。我们把规则所需要重建的文件称为规则的“目标”(foo.o),而把重新目标所需要的文件称为规则的“依赖”(或者目标的依赖)。规则中的第二行“cc -c -g foo.c”是规则的“命令”。它描述了如何使用规则中的依赖文件重建目标。

而且,上面的规则告诉我们了两件事:

1.        如何确定目标文件是否过期(需要重建目标),过期是指目标文件不存在或者目标文件“foo.o”在时间戳上比依赖文件中的任何一个(“foo.c”或者“defs.h”)“老”。

2.        如何重建目标文件“foo.o”。这个规则中使用cc编译器。规则的命令中没有明确的使用到依赖文件“defs.h”。我们假设在源文件“foo.c”中已经包含了此头文件。这也是为什么它作为目标依赖出现的原因。

4.2 规则语法

通常规则的语法格式如下:

 

TARGETS : PREREQUISITES

COMMAND

...

 

或者:

 

TARGETS : PREREQUISITES ; COMMAND

COMMAND

...

 

规则中“TARGETS”可以是空格分开的多个文件名,也可以是一个标签(例如:执行清空的“clean”)。“TARGETS”的文件名可以使用通配符,格式“A(M)”表示档案文件(Linux下的静态库.a文件)的成员“M”(关于静态库的重建可参考 第十一章 使用make更新静态库文件)。通常规则只有一个目标文件(建议这么做),偶尔会在一个规则中需要多个目标。

书写规则是我们需要注意的几点:

1.        规则的命令部分有两种书写方式:a. 命令可以和目标:依赖描述放在同一行。命令在依赖文件列表后并使用分号(;)和依赖文件列表分开。b. 命令在目标:依赖的描述的下一行,作为独立的命令行。当作为独立的命令行时此行必须以[Tab]字符开始。在Makefile中,在第一个规则之后出现的所有以[Tab]字符开始的行都会被当作命令来处理。

2.        Makefile中符号“$”有特殊的含义(表示变量或者函数的引用),在规则中需要使用符号“$”的地方,需要书写两个连续的(“$$”)。

3.        前边已提到过,对于Makefile中一个较长的行,我们可以使用反斜线“\”将其书写到几个独立的物理行上。虽然make对Makefile文本行的最大长度是没有限制的,但还是建议这样做。不仅书写方便而且更有利于别人的阅读(这也是一个程序员修养的体现)。

一个规则告诉“make”两件事:1. 目标在什么情况下已经过期; 2. 如果需要重建目标时,如何去重建这个目标。目标是否过期是由那些使用空格分割的规则的依赖文件所决定的。当目标文件不存在或者目标文件的最后修改时间比依赖文件中的任何一个晚时,目标就会被创建或者重建。就是说执行规则命令行的前提条件是以下两者之一:1. 目标文件不存在; 2. 目标文件存在,但是规则的依赖文件中存在一个依赖的最后修改时间比目标的最后修改时间晚。

规则的中心思想是:目标文件的内容是由依赖文件文件决定,依赖文件的任何一处改动,将导致目前已经存在的目标文件的内容过期。规则的命令为重建目标提供了方法。这些命令运行在系统shell之上。

4.3 依赖的类型

在GNU make的规则中可以使用两种不同类型的依赖:1. 以前章节所提到的规则中使用的是常规依赖,这是书写Makefile规则时最常用的一种。2. 另外一种在我们书写Makefile时不会经常使用,它比较特殊、称之为“order-only”依赖。一个规则的常规依赖(通常是多个依赖文件)表明了两件事:首先,它决定了重建此规则目标所要执行规则(确切的说是执行命令)的顺序;表明在更新这个规则的目标(执行此规则的命令行)之前需要按照什么样的顺序、执行那些规则(命令)来重建这些依赖文件(对所有依赖文件的重建,使用明确或者隐含规则。就是说对于这样的规则:A:B C,那么在重建目标A之前,首先需要完成对它的依赖文件B和C的重建。重建B和C的过程就是执行Makefile中以文件B和C为目标的规则)。其次,它确定了一个依存关系;规则中如果依赖文件的任何一个比目标文件新,则认为规则的目标已经过期而需要重建目标文件。

通常,如果规则中依赖文件中的任何一个被更新,则规则的目标相应地也应该被更新。

有时,需要定义一个这样的规则,在更新目标(目标文件已经存在)时只需要根据依赖文件中的部分来决定目标是否需要被重建,而不是在依赖文件的任何一个被修改后都重建目标。为了实现这一目的,相应的就需要对规则的依赖进行分类,一类是在这些依赖文件被更新后,需要更新规则的目标;另一类是更新这些依赖的,可不需要更新规则的目标。我们把第二类称为:“order-only”依赖。书写规则时,“order-only”依赖使用管道符号“|”开始,作为目标的一个依赖文件。规则依赖列表中管道符号“|”左边的是常规依赖,管道符号右边的就是“order-only”依赖。这样的规则书写格式如下:

 

TARGETS : NORMAL-PREREQUISITES | ORDER-ONLY-PREREQUISITES

 

这样的规则中常规依赖文件可以是空;同样也可以对一个目标进行多次追加依赖。需要注意:规则依赖文件列表中如果一个文件同时出现在常规列表和“order-only”列表中,那么此文件被作为常规依赖处理(因为常规依赖所实现的动作是“order-only”依赖所实现的动作的一个超集)。

“order-only”依赖的使用举例:

    LIBS = libtest.a

foo : foo.c | $(LIBS)

       $(CC) $(CFLAGS) $< -o $@ $(LIBS)

make在执行这个规则时,如果目标文件“foo”已经存在。当“foo.c”被修改以后,目标“foo”将会被重建,但是当“libtest.a”被修改以后。将不执行规则的命令来重建目标“foo”。

就是说,规则中依赖文件$(LIBS)只有在目标文件不存在的情况下,才会参与规则的执行。当目标文件存在时此依赖不会参与规则的执行过程。

4.4 文件名使用通配符

Maekfile中表示文件名时可使用通配符。可使用的通配符有:“*”、“?”和“[…]”。在Makefile中通配符的用法和含义和Linux(unix)的Bourne shell完全相同。例如,“*.c”代表了当前工作目录下所有的以“.c”结尾的文件等。但是在Makefile中这些统配符并不是可以用在任何地方,Makefile中统配符可以出现在以下两种场合:

1.        可以用在规则的目标、依赖中,make在读取Makefile时会自动对其进行匹配处理(通配符展开);

2.        可出现在规则的命令中,通配符的通配处理是在shell在执行此命令时完成的。

除这两种情况之外的其它上下文中,不能直接使用通配符。而是需要通过函数“wildcard”来实现。

如果规则的一个文件名包含统配字符(“*”、“.”等字符),在使用这样的文件时需要对文件名中的统配字符使用反斜线(\)进行转义处理。例如“foo\*bar”,在Makefile中它表示了文件“foo*bar”。Makefile中对一些特殊字符的转移和B-SHELL以及C语言中的基本上相同。

另外需要注意:在Linux(unix)中,以波浪线“~”开始的文件名有特殊含义。单独使用它或者其后跟一个斜线(~/),代表了当前用户的宿主目录(在shell下可以通过命令“echo ~(~\)”来查看)。例如“~/bin”代表“/home/username/bin/”(当前用户宿主目录下的bin目录)。波浪线之后跟一个单词(~word),代表由这个“word”所指定的用户的宿主目录。例如“~john/bin”就是代表用户john的宿主目录下的bin目录。

在一些系统中(像MS-DOS和MS-Windows),用户没有各自的宿主目录,此情况下可通过设置环境变量“HOME”来模拟。

4.4.1 统配符使用举例

本节开始已经提到过,通配符可被用在规则的命令中,它是在命令被执行时由shell进行处理。例如Makefile的清空过程文件规则:

 

clean:

rm -f *.o

 

通配符也可以用在规则的依赖文件名中。看看下面这个例子。执行“make print”,执行的结果是打印当前工作目录下所有的在上一次打印以后被修改过的“.c”文件。

 

print: *.c

lpr -p $?

touch print

 

两点说明:1. 上述的规则中目标“print”时一个空目标文件。(当前目录下存在一个文件“print”,但我们不关心它的实际内容,此文件的作用只是记录最后一次执行此规则的时间。2. 自动环变量“$?”在这里表示依赖文件列表中被改变过的所有文件。

变量定义中使用的通配符不会被统配处理(因此在变量定义中不能使用通配符,否则在某些情况下会出现非预期的结果,下一小节将会详细讨论)。在Makefile有这样一个变量定义:“objects = *.o”。它表示变量“objects”的值是字符串“*.o”(并不是期望的空格分开的.o文件列表)。当需要变量“objects”代表所有.o文件列表示,需要使用函数“wildcard”(objects = $(wildcar *.o))。

4.4.2 通配符存在的缺陷

在上一小节提到过变量定义时使用通配符可能在某些情况下会导致意外的结果。本小节将对此进行详细地分析和讨论。书写Makefile时,可能存在这种不正确的使用通配符的方法。这种看似正确的方式产生的结果可能产生非期望的结果。例如在你的Makefile中,期望能够根据所有的.o文件生成可执行文件“foo”。实现如下:

 

objects = *.o

 

foo : $(objects)

cc -o foo $(CFLAGS) $(objects)

 

这里变量“objects”的值是一个字符串“*.o”。在重建“foo”的规则中对变量“objects”进行展开,目标“foo”的依赖就是“*.o”,即所有的.o文件的列表。如果在工作目录下已经存在必需的.o文件,那么这些.o文件将成为目标的依赖文件,目标“foo”将根据规则被重建。

但是如果将工作目录下所有的.o文件删除,重新执行make将会得到一个类似于“没有创建*.o文件的规则” 的错误提示。这当然不是我们所期望的结果(可能在出现这个错误时会令你感到万分迷惑!)。为了达到我们的初衷,在对变量进行定义的时需要使用一些高级的技巧,包括使用“wildcard”函数(变量定义为“objects=$(wildcard *.o)”)和实现字符串的置换。如何实现字符串的置换,后续将进行详细地讨论。

4.4.3       函数wildcard

之前提到过,在规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN...) 。在Makefile中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函数会忽略模式字符并返回空。需要注意的是:这种情况下规则中通配符的展开和上一小节匹配通配符的区别。

一般我们可以使用“$(wildcard *.c)”来获取工作目录下的所有的.c文件列表。复杂一些用法;可以使用“$(patsubst %.c,%.o,$(wildcard *.c))”,首先使用“wildcard”函数获取工作目录下的.c文件列表;之后将列表中所有文件名的后缀.c替换为.o。这样我们就可以得到在当前目录可生成的.o文件列表。因此在一个目录下可以使用如下内容的Makefile来将工作目录下的所有的.c文件进行编译并最后连接成为一个可执行文件:

 

#sample Makefile

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

 

foo : $(objects)

cc -o foo $(objects)

 

这里我们使用了make的隐含规则来编译.c的源文件。对变量的赋值也用到了一个特殊的符号(:=)。

4.5 目录搜寻

在一个较大的工程中,一般会将源代码和二进制文件(.o文件和可执行文件)安排在不同的目录来进行区分管理。这种情况下,我们可以使用make提供的目录搜索依赖文件功能(在指定的若干个目录下自动搜索依赖文件)。在Makefile中,使用依赖文件的目录搜索功能。当工程的目录结构发生变化后,就可以做到不更改Makefile的规则,只更改依赖文件的搜索目录。

本节我们将详细讨论在书写Makefile时如何使用这一特性。在自己的工程中灵活运用这一特性,将会起到事半功倍的效果。

4.5.1 一般搜索(变量VPATH

GNU make可以识别一个特殊变量“VPATH”。通过变量“VPATH”可以指定依赖文件的搜索路径,当规则的依赖文件在当前目录不存在时,make会在此变量所指定的目录下去寻找这些依赖文件。通常我们都是用此变量来指定规则的依赖文件的搜索路径。其实“VPATH”变量所指定的是Makefile中所有文件的搜索路径,包括了规则的依赖文件和目标文件。

定义变量“VPATH”时,使用空格或者冒号(:)将多个需要搜索的目录分开。make搜索目录的顺序是按照变量“VPATH”定义中的目录顺序进行的(当前目录永远是第一搜索目录)。例如对变量的定义如下:

 

VPATH = src:../headers

 

这样我们就为所有规则的依赖指定了两个搜索目录,“src”和“../headers”。对于规则“foo:foo.c”如果“foo.c”存在于“src”目录下,此规则等价于“foo:src:/foo.c”。

通过“VPATH”变量指定的路径在Makefile中对所有文件有效。当需要为不类型的文件指定不同的搜索目录时,需要使用另外一种方式。下一小节我们将会讨论这种更高级的方式。

4.5.2 选择性搜索(关键字vpath)

另一个设置文件搜索路径的方法是使用make的“vpath”关键字(全小写的)。它不是一个变量,而是一个make的关键字,它所实现的功能和上一小节提到的“VPATH”变量很类似,但是它更为灵活。它可以为不同类型的文件(由文件名区分)指定不同的搜索目录。它的使用方法有三种:

1、vpath PATTERN DIRECTORIES

为所有符合模式“PATTERN”的文件指定搜索目录“DIRECTORIES”。多个目录使用空格或者冒号(:)分开。类似上一小节的“VPATH”变量。

2、vpath PATTERN

清除之前为符合模式“PATTERN”的文件设置的搜索路径。

3、vpath

清除所有已被设置的文件搜索路径。

 

vapth使用方法中的“PATTERN”需要包含模式字符“%”。“%”意思是匹配一个或者多个字符,例如,“%.h”表示所有以“.h”结尾的文件。如果在“PATTERN”中没有包含模式字符“%”,那么它就是一个明确的文件名,这样就是给定了此文件的所在目录,我们很少使用这种方式来为单独的一个文件指定搜索路径。在“vpath”所指定的模式中我们可以使用反斜杠来对字符“%”进行引用(和其他的特使字符的引用一样)。

“PATTERN”表示了具有相同特征的一类文件,而“DIRECTORIES”则指定了搜索此类文件目录。当规则的依赖文件列表中的文件不能在当前目录下找到时,make程序将依次在“DIRECTORIES”所描述的目录下寻找此文件。例如:

 

vpath %.h ../headers

 

其含义是:Makefile中出现的.h文件;如果不能在当前目录下找到,则到目录“../headers”下寻找。注意:这里指定的路径仅限于在Makefile文件内容中出现的.h文件。 并不能指定源文件中包含的头文件所在的路径(在.c源文件中所包含的头文件路径需要使用gcc的“-I”选项来指定,可参考gcc的info文档)。

在Makefile中如果存在连续的多个vpath语句使用了相同的“PATTERN”,make就对这些vpath语句一个一个进行处理,搜索某种模式文件的目录将是所有的通过vpath指定的符合此模式的多个目录,其搜索目录的顺序由vpath语句在Makefile出现的先后次序来决定。多个具有相同“PATTERN”的vpath语句之间相互独立。下边是两种方式下,所有的.c文件的查找目录的顺序(不包含工作目录,对工作目录的搜索永远处于最优先地位)比较:

 

vpath %.c foo

vpath % blish

vpath %.c bar

 

表示对所有的.c文件,make依次查找目录:“foo”、blish”、“bar”。

而:

vpath %.c foo:bar

vpath % blish

 

对于所有的.c文件make将依次查找目录:“foo”、“bar”、“blish”

4.5.3 目录搜索的机制

规则中一个依赖文件可以通过目录搜寻找到(使用前边提到的一般搜索或者是选择性搜索任一种),可能得到的是文件的完整路径名(文件的相对路径或者绝对路径,如:/home/Stallman/foo.c),它却并不是规则中列出的文件名(规则“foo : foo.c”,在执行搜索后可能得到的依赖文件为:“../src/foo.c”。目录“../src”是使用“VPATH”或“vpath”指定的);因此使用目录搜索所到的完整的文件路径名可能需要废弃(可能废弃的是规则目标文件的全名,规则依赖文件全名不能废弃,否则无法执行规则。为了保证在规则命令行中使用正确的依赖文件,规则的命令行中必须使用自动化变量来代表依赖文件。关于这一点,在下一小节有专门讨论)。make在解析Makefile文件执行规则时对文件路径保存或废弃所依据的算法如下:

1.        首先,如果规则的目标文件在Makefile文件所在的目录(工作目录)下不存在,那么就执行目录搜寻。

2.        如果目录搜寻成功,在指定的目录下存在此规则的目标。那么搜索到的完整的路径名就被作为临时的目标文件被保存。

3.        对于规则中的所有依赖文件使用相同的方法处理。

4.        完成第三步的依赖处理后,make程序就可以决定规则的目标是否需要重建,两种情况时后续处理如下:

a)       规则的目标不需要重建:那么通过目录搜索得到的所有完整的依赖文件路径名有效,同样,规则的目标文件的完整的路径名同样有效。就是说,当规则的目标不需要被重建时,规则中的所有的文件完整的路径名有效。已经存在的目标文件所在的目录不会被改变。

b)       规则的目标需要重建:那么通过目录搜索所得到的目标文件的完整的路径名无效,规则中的目标文件将会被在工作目录下重建。就是说,当规则的目标需要重建时,规则的目标文件会在工作目录下被重建,而不是在目录搜寻时所得到的目录。这里,必须明确:此种情况只有目标文件的完整路径名失效,依赖文件的完整路径名是不会失效的。否则将无法重建目标。

该算法看起来比较法杂,但它确实使make实现了我们所需要的东西。此算法使用纯粹的语言描述可能显得晦涩。本小节后续将使用一个例子来说明。使大家能够对此算法有明确的理解。对于其他版本的make则使用了一种比较简单的算法:如果规则的目标文件的完整路径名存在(通过目录搜索可以定位到目标文件),无论该目标是否需要重建,都使用搜索到的目标文件完整路径名。

实际上,GNU make也可以实现这种功能。如果需要make在执行时,将目标文件在已存在的目录存下进行重建,我们可以使用“GPATH”变量来指定这些目标所在的目录。“GPATH”变量和“VPATH”变量具有相同的语法格式。make在执行时,如果通过目录搜寻得到一个过时的完整的目标文件路径名,而目标存在的目录又出现在“GPATH”变量的定义列表中,则该目标的完整路径将不废弃,目标将在该路径下被重建。

为了更清楚地描述此算法,我们使用一个例子来说明。存在一个目录“prom”,“prom”的子目录“src”下存在“sum.c”和“memcp.c”两个源文件。在“prom”目录下的Makefile部分内容如下:

LIBS = libtest.a

VPATH = src

 

libtest.a : sum.o memcp.o

       $(AR) $(ARFLAGS) $@ $^

 

首先,如果在两个目录(“prom”和“src”)都不存在目标“libtest.a”,执行make时将会在当前目录下创建目标文件“libtest.a”。另外;如果“src”目录下已经存在“libtest.a”,以下两种不同的执行结果:

1)       当它的两个依赖文件“sum.c”和“memcp.c”没有被更新的情况下我们执行make,首先make程序会搜索到目录“src”下的已经存在的目标“libtest.a”。由于目标“libtest.a”的依赖文件没有发生变化,所以不会重建目标。并且目标所在的目录不会发生变化。

2)       当我们修改了文件“sum.c”或者“memcp.c”以后执行make。“libtest.a”和“sum.o”或者“memcp.o”文件将会被在当前目录下创建(目标完整路径名被废弃),而不是在“src”目录下更新这些已经存在的文件。此时在两个目录下(“prom”和“src”)同时存在文件“libtest.a”。但只有“prom/libtest.a”是最新的库文件。

当在上边的Makefile文件中使用“GPATH”指定目录时,情况就不一样了。首先看看怎么使用“GPATH”,改变后的Makefile内容如下:

LIBS = libtest.a

GPATH = src

VPATH = src

LDFLAGS += -L ./. –ltest

…….

……

同样;当两个目录都不存在目标文件“libtest.a”时,目标将会在当前目录(“prom”目录)下创建。如果“src”目录下已经存在目标文件“libtest.a”。当其依赖文件任何一个被改变以后执行make,目标“libtest.a”将会被在“src”目录下被更新(目标完整路径名不会被废弃)。

4.5.4 命令行和搜索目录

make在执行时,通过目录搜索得到的目标的依赖文件可能会在其它目录(此时依赖文件为文件的完整路径名),但是已经存在的规则命令却不能发生变化。因此,书写命令时我们必须保证当依赖文件在其它目录下被发现时规则的命令能够正确执行。

解决这个问题的方式是在规则的命令行中使用“自动化变量”,诸如“$^”等。规则命令行中的自动化变量“$^”代表所有通过目录搜索得到的依赖文件的完整路径名(目录 + 一般文件名)列表。“$@”代表规则的目标。所以对于一个规则我们可以进行如下的描述:

 

foo.o : foo.c

cc -c $(CFLAGS) $^ -o $@

 

变量“CFLAGS”是编译.c文件时gcc的编译选项,可以在Makefile中给它指定明确的值、也可以使用隐含的定义值。

规则的依赖文件列表中可以包含头文件,而在命令行中不需要使用这些头文件(这些头文件的作用只有在make程序决定目标是否需要重建时才有意义)。我们可以使用另外一个变量来书代替“$^”,如下:

 

VPATH = src:../headers

foo.o : foo.c defs.h hack.h

cc -c $(CFLAGS) $< -o $@

 

自动化变量“$<”代表规则中通过目录搜索得到的依赖文件列表的第一个依赖文件。关于自动化变量我们在后续有专门的讨论。

4.5.5 隐含规则和搜索目录

通过变量“VPATH”、或者关键字“vpath”指定的搜索目录,对于隐含规则同样有效。例如:一个目标文件“foo.o”在Makefile中没有重建它的明确规则,make会使用隐含规则来由已经存在的“foo.c”来重建它。当“foo.c”在当前目录下不存在时,make将会进行目录搜索。如果能够在一个可以搜索的目录中找到此文件,同样make会使用隐含规则根据搜索到的文件完整的路径名去重建目标,编译这个.c源文件。

隐含规则中的命令行中就是使用自动化变量来解决目录搜索可能带来的问题;相应的命令中的文件名都是使用目录搜索得到的完整的路径名。(可参考上一小节)

4.5.6 库文件和搜索目录

Makefile中程序链接的静态库、共享库同样也可以通过搜索目录得到。这一特性需要我们在书规则的依赖时指定一个类似“-lNAME”的依赖文件名(一个奇怪的依赖文件!一般依赖文件名应该是一个普通文件的名字。库文件的命名也应该是“libNAME.a”而不是所写的“-lNAME”。这是为什么,熟悉GNU ld的话我想这就不难理解了,“-lNAME”的表示方式和ld的对库的引用方式完全一样,只是我们在书写Makefile的规则时使用了这种书写方式。所以你不应该感到奇怪)。下边我们来看看这种奇怪的依赖文件到底是什么。

当规则中依赖文件列表中存在一个“-lNAME”形式的文件时。make将根据“NAME”首先搜索当前系统可提供的共享库,如果当前系统不能提供这个共享库,则搜索它的静态库(当然你可以在命令行中使用连接选项来指定程序采用动态连接还是静态连接,这里我们不讨论)。来看一下详细的过程。1. make在执行规则时会在当前目录下搜索一个名字为“libNAME.so”的文件;2. 如果当前工作目录下不存在这样一个文件,则make会继续搜索使用“VPATH”或者“vpath”指定的搜索目录。3. 还是不存在,make将搜索系统库文件存在的默认目录,顺序是:“/lib”、“/usr/lib”和“PREFIX/lib”(在Linux系统中为“/usr/local/lib”,其他的系统可能不同)。

如果“libNAME.so”通过以上的途径最后还是没有找到的话,那么make将会按照以上的搜索顺序查找名字为“libNAME.a”的文件。

假设你的系统中存在“/usr/lib/libcurses.a”(不存在“/usr/lib/libcurses.so”)这个库文件。看一个例子:

 

foo : foo.c -lcurses

cc $^ -o $@

 

上例中,如果文件“foo.c”被修改或者“/usr/lib/libcurses.a”被更新,执行规则时将使用命令“cc foo.c /usr/lib/libcurses.a -o foo”来完成目标文件的重建。需要注意的是:如果“/usr/lib/libcurses.a”需要在执行make的时生成,那么就不能这样写,因为“-lNAME”只是告诉了链接器在生成目标时需要链接那个库文件。上例中的“-lcurses”并没有告诉make程序其依赖的库文件应该如何重建。当所有的搜索目录中不存在库“libcurses”时。Make将提示“没有规则可以创建目标“foo”需要的目标“-lcurses”。如果在执行make时,出现这样的提示信息,你应该明确发生了什么错误,而不要因为错误而不知所措。

在规则的依赖列表中如果出现“-lNAME”格式的依赖时,表示需要搜索的依赖文件名为“libNAME.so”和“libNAME.a”,这是由变量“.LIBPATTERNS”指定的。“.LIBPATTERNS”的值一般是多个包含模式字符(%)的字(一个不包含空格的字符串),多个字之间使用空格分开。在规则中出现“-lNAME”格式的依赖时,首先使用这里的“NAME”代替变量“.LIBPATTERNS”的第一个字的模式字符(%)而得到第一个库文件名,根据这个库文件名在搜索目录下查找,如果能够找到、就是用这个文件,否则使用“NAME”代替第二个字的模式字符,进行同样的查找。默认情况时,“.LIBPATTERNS”的值为:“lib%.so lib%.a”。这也是默认情况下在规则存在“-lNAME”格式的依赖时,链接生成目标时使用“libNAME.so”和“libNAME.a”的原因。

变量“.LIBPATTERNS”就是告诉链接器在执行链接过程中对于出现“-LNAME”的文件如何展开。当然我们也可以将此变量制空,取消链接器对“-lNAME”格式的展开。

4.6 Makefile伪目标

本节我们讨论Makefile的一个重要的特殊目标:伪目标。伪目标是这样一个目标:它不代表一个真正的文件名,在执行make时可以指定这个目标来执行其所在规则定义的命令,有时也可以将一个伪目标称为标签。使用伪目标有两点原因:1. 避免在我们的Makefile中定义的只执行命令的目标(此目标的目的为了执行执行一些列命令,而不需要创建这个目标)和工作目录下的实际文件出现名字冲突。2. 提高执行make时的效率,特别是对于一个大型的工程来说,编译的效率也许你同样关心。以下就这两个问题我们进行分析讨论:

1.  如果我们需要书写这样一个规则:规则所定义的命令不是去创建目标文件,而是通过make命令行明确指定它来执一些特定的命令。像常见的clean目标:

 

clean:

rm *.o temp

 

规则中“rm”不是创建文件“clean”的命令,而是删除当前目录下的所有.o文件和temp文件。当工作目录下不存在“clean”这个文件时,我们输入“make clean”,“rm *.o temp”总会被执行。这是我们的初衷。

但是如果在当前工作目录下存在文件“clean”,情况就不一样了,同样我们输入“make clean”,由于这个规则没有任何依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令,因此命令“rm”将不会被执行。这并不是我们的初衷。为了解决这个问题,我们需要将目标“clean”声明为伪目标。将一个目标声明为伪目标的方法是将它作为特殊目标.PHONY”的依赖。如下:

 

.PHONY : clean

 

这样目标“clean”就被声明为一个伪目标,无论在当前目录下是否存在“clean”这个文件。我们输入“make clean”之后。“rm”命令都会被执行。而且,当一个目标被声明为伪目标后,make在执行此规则时不会去试图去查找隐含规则来创建它。这样也提高了make的执行效率,同时也不用担心由于目标和文件名重名而使我们的期望失败。在书写伪目标规则时,首先需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标“clean”的完整书写格式应该如下:

.PHONY: clean

clean:

rm *.o temp

 

2. 伪目标的另外一种使用场合是在make的并行和递归执行过程中。此情况下一般会存在一个变量,定义为所有需要make的子目录。对多个目录进行make的实现方式可以是:在一个规则的命令行中使用shell循环来完成。如下:

SUBDIRS = foo bar baz

 

subdirs:

for dir in $(SUBDIRS); do \

$(MAKE) -C $$dir; \

done

 

但这种实现方法存在以下几个问题。1. 当子目录执行make出现错误时,make不会退出。就是说,在对某一个目录执行make失败以后,会继续对其他的目录进行make。在最终执行失败的情况下,我们很难根据错误提示定位出具体是在那个目录下执行make时发生错误。这样给问题定位造成了很大的困难。为了解决这个问题,可以在命令行部分加入错误监测,在命令执行错误后主动退出。不幸的是,如果在执行make时使用了“-k”选项,此方式将失效。2. 另外一个问题就是使用这种shell的循环方式时,没有用到make对目录的并行处理功能,由于规则的命令是一条完整的shell命令,不能被并行处理。

有了伪目标之后,我们可以用它来克服以上实现方式所存在的两个问题。

SUBDIRS = foo bar baz

 

.PHONY: subdirs $(SUBDIRS)

 

subdirs: $(SUBDIRS)

$(SUBDIRS):

$(MAKE) -C $@

foo: baz

 

上边的实现中有一个没有命令行的规则“foo: baz”,此规则用来限制子目录的make顺序。它的作用是限制同步目录“foo”和“baz”的make过程(在处理“foo”目录之前,需要等待“baz”目录处理完成)。提醒大家:在书写一个并行执行make的Makefile时,目录的处理顺序是需要特别注意的。

一般情况下,一个伪目标不作为另外一个目标的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则时伪目标所定义的命令都会被执行(因为它作为规则的依赖,重建规则目标时需要首先重建规则的所有依赖文件)。当一个伪目标没有作为任何目标(此目标是一个可被创建或者已存在的文件)的依赖时,我们只能通过make的命令行来明确指定它为make的终极目标,来执行它所在规则所定义的命令。例如“make clean”。

在Makefile中,一个伪目标可以有自己的依赖(可以是一个或者多个文件、一个或者多个伪目标)。在一个目录下如果需要创建多个可执行程序,我们可以将所有程序的重建规则在一个Makefile中描述。因为Makefile中第一个目标是“终极目标”,约定的做法是使用一个称为“all”的伪目标来作为终极目标,它的依赖文件就是那些需要创建的程序。下边就是一个例子:

 

#sample Makefile

all : prog1 prog2 prog3

.PHONY : all

 

prog1 : prog1.o utils.o

cc -o prog1 prog1.o utils.o

 

prog2 : prog2.o

cc -o prog2 prog2.o

 

prog3 : prog3.o sort.o utils.o

cc -o prog3 prog3.o sort.o utils.o

 

执行make时,目标“all”被作为终极目标。为了完成对它的更新,make会创建(不存在)或者重建(已存在)目标“all”的所有依赖文件(prog1、prog2和prog3)。当需要单独更新某一个程序时,我们可以通过make的命令行选项来明确指定需要重建的程序。(例如:“make prog1”)。

当一个伪目标作为另外一个伪目标依赖时,make将其作为另外一个伪目标的子例程来处理(可以这样理解:其作为另外一个伪目标的必须执行的部分,就行C语言中的函数调用一样)。下边的例子就是这种用法:

.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff

rm program

 

cleanobj :

rm *.o

 

cleandiff :

rm *.diff

 

“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思(执行目标“clearall时会触发它们所定义的命令被执行”)。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。例子首先通过特殊目标“.PHONY”声明了多个伪目标,它们之间使用空各分割,之后才是各个伪目标的规则定义。

说明:

通常在清除文件的伪目标所定义的命令中“rm”使用选项“–f”(--force)来防止在缺少删除文件时出错并退出,使“make clean”过程失败。也可以在“rm”之前加上“-”来防止“rm”错误退出,这种方式时make会提示错误信息但不会退出。为了不看到这些讨厌的信息,需要使用上述的第一种方式。

另外make存在一个内嵌隐含变量“RM”,它被定义为:“RM = rm –f”。因此在书写“clean”规则的命令行时可以使用变量“$(RM)”来代替“rm”,这样可以免出现一些不必要的麻烦!这是我们推荐的用法。

4.7 强制目标(没有命令或依赖的规则)

如果一个规则没有命令或者依赖,并且它的目标不是一个存在的文件名。在执行此规则时,目标总会被认为是最新的。就是说:这个规则一旦被执行,make就认为它的目标已经被更新过。这样的目标在作为一个规则的依赖时,因为依赖总被认为被更新过,因此作为依赖所在的规则中定义的命令总会被执行。看一个例子:

 

clean: FORCE

rm $(objects)

FORCE:

 

这个例子中,目标“FORCE”符合上边的条件。它作为目标“clean”的依赖,在执行make时,总被认为被更新过。因此“clean”所在规则在被执行时其所定义的命令总会被执行。这样的一个目标通常我们将其命名为“FORCE”。

上边的例子中使用“FORCE”目标的效果和将 “clean”声明为伪目标效果相同。两种方式相比较,使用“.PHONY”方式更加直观高效。这种方式主要用在非GNU版本的make中。

在使用GNU make,应避免使用这种方式。在GNU make中我们推荐使用伪目标方式。

4.8 空目标文件

空目标文件是伪目标的一个变种;此目标所在规则执行的目的和伪目标相同——通过make命令行指定将其作为终极目标来执行此规则所定义的命令。和伪目标不同的是:这个目标可以是一个存在的文件,但文件的具体内容我们并不关心,通常此文件是一个空文件。

空目标文件只是用来记录上一次执行此规则命令的时间。在这样的规则中,命令部分都会使用“touch”在完成所有命令之后来更新目标文件的时间戳,记录此规则命令的最后执行时间。make时通过命令行将此目标作为终极目标,当前目录下如果不存在这个文件,“touch”会在第一次执行时创建一个空的文件(命名为空目标文件名)。

通常,一个空目标文件应该存在一个或者多个依赖文件。将这个目标作为终极目标,在它所依赖的文件比它新时,此目标所在规则的命令行将被执行。就是说,如果空目标的依赖文件被改变之后,空目标所在规则中定义的命令会被执行。看一个例子:

 

print: foo.c bar.c

lpr -p $?

touch print

 

执行“make print”,当目标“print”的依赖文件任何一个被修改之后,命令“lpr –p $?”都会被执行,打印这个被修改的文件。

4.9 Makefile的特殊目标

在Makefile中,有一些名字,当它们作为规则的目标时,具有特殊含义。它们是一些特殊的目标,GNU make所支持的特殊的目标有:

.PHONY

目标“.PHONY”的所有的依赖被作为伪目标。伪目标时这样一个目标:当使用make命令行指定此目标时,这个目标所在规则定义的命令、无论目标文件是否存在都会被无条件执行。.SUFFIXES:

特殊目标“SUFFIXES”的所有依赖指出了一系列在后缀规则中需要检查的后缀名(就是当前make需要处理的后缀)。

.DEFAULT

Makefile中,目标“.DEFAULT”所在规则定义的命令,被用在重建那些没有具体规则的目标(明确规则和隐含规则)。就是说一个文件作为某个规则的依赖,但却不是另外一个规则的目标时。Make程序无法找到重建此文件的规则,此种情况时就执行“.DEFAULT”所指定的命令。

.PRECIOUS

目标“.PRECIOUS”的所有依赖文件在make过程中会被特殊处理:当命令在执行过程中被中断时,make不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。这一点目标“.PRECIOUS”和目标“.SECONDAY”实现的功能相同。

另外,目标“.PRECIOUS”的依赖文件也可以是一个模式,例如“%.o”。这样可以保留有规则创建的中间过程文件。

.INTERMEDIATE

目标“.INTERMEDIATE”的依赖文件在make时被作为中间过程文件对待。没有任何依赖文件的目标“.INTERMEDIATE”没有意义。

.SECONDARY

目标“.SECONDARY”的依赖文件被作为中间过程文件对待。但这些文件不会被自动删除。

没有任何依赖文件的目标“.SECONDARY”的含义是:将所有的文件作为中间过程文件(不会自动删除任何文件)。

.DELETE_ON_ERROR

如果在Makefile中存在特殊目标“.DELETE_ON_ERROR”,make在执行过程中,如果规则的命令执行错误,将删除已经被修改的目标文件。

.IGNORE

如果给目标“.IGNORE”指定依赖文件,则忽略创建这个文件所执行命令的错误。给此目标指定命令是没有意义的。当此目标没有依赖文件时,将忽略所有命令执行的错误。

.LOW_RESOLUTION_TIME

目标“.LOW_RESOLUTION_TIME”的依赖文件被make认为是低分辨率时间戳文件。给目标“.LOW_RESOLUTION_TIME”指定命令是没有意义的。

通常文件的时间辍都是高分辨率的,make在处理依赖关系时、对规则目标-依赖文件的高分辨率的时间戳进行比较,判断目标是否过期。但是在系统中并没有提供一个修改文件高分辨率时间辍的机制(方式),因此类似“cp -p”这样的命令在根据源文件创建目的文件时,所产生的目的文件的高分辨率时间辍的细粒度部分被丢弃(来源于源文件)。这样可能会造成目的文件的时间戳和源文件的相等甚至不及源文件新。处理此类命令创建的文件时,需要将命令创建的文件作为目标“.LOW_RESOLUTION_TIME”的依赖,声明这个文件是一个低分辨率时间辍的文件。例如:

 

.LOW_RESOLUTION_TIME: dst

dst: src

cp -p src dst

 

首先规则的命令“cp –p src dst”,所创建的文件“dst”在时间戳上稍稍比“src”晚(因为命令不能更新文件“dst”的细粒度时间)。因此make在判断文件依赖关系时会出现误判,将文件作为目标“.LOW_RESOLUTION_TIME”的依赖后,只要规则中目标和依赖文件的时间戳中的初始时间相等,就认为目标已经过期。这个特殊的目标主要作用是,弥补系统在没有提供修改文件高分辨率时间戳机制的情况下,某些命令在make中的一些缺陷。

对于静态库文件(文档文件)成员的更新也存在这个问题。make在创建或者更新静态库时,会自动将静态库的所有成员作为目标“.LOW_RESOLUTION_TIME”的依赖。

.SILENT

出现在目标“.SILENT”的依赖列表中的文件,make在创建这些文件时,不打印出重建此文件所执行的命令。同样,给目标“.SILENT”指定命令行是没有意义的。

没有任何依赖文件的目标“.SILENT”告诉make在执行过程中不打印任何执行的命令。现行版本make支持目标“.SILENT”的这种功能和用法是为了和旧版本的兼容。在当前版本中如果需要禁命令执行过程的打印,可以使用make的命令行参数“-s”或者“--silent”。

.EXPORT_ALL_VARIABLES

此目标应该作为一个简单的没有依赖的目标,它的功能含义是将之后所有的变量传递给子make进程。

.NOTPARALLEL

Makefile中,如果出现目标“.NOPARALLEL”,则所有命令按照串行方式执行,即使存在make的命令行参数“-j”。但在递归调用的字make进程中,命令可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将被忽略。

所有定义的隐含规则后缀作为目标出现时,都被视为一个特殊目标,两个后缀串联起来也是如此,例如“.c.o”。这样的目标被称为后缀规则的目标,这种定义方式是已经过时的定义隐含规则的方法(目前,这种方式还被用在很多地方)。原则上,如果将其分为两个部分、并将它们加到后缀列表中,任何目标都可采用这种方式来表示。实际中,后缀通常以“.”开始,因此,以上的这些特别目标同样是以“.”开始。

4.10 多目标

一个规则中可以有多个目标,规则所定义的命令对所有的目标有效。一个具有多目标的规则相当于多个规则。规则的命令对不同的目标的执行效果不同,因为在规则的命令中可能使用了自动环变量“$@”。多目标规则意味着所有的目标具有相同的依赖文件。多目标通常用在以下两种情况:

Ø        仅需要一个描述依赖关系的规则,不需要在规则中定义命令。例如

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

      

这个规则实现了同时给三个目标文件指定一个依赖文件。

Ø        对于多个具有类似重建命令的目标。重建这些目标的命令并不需要是完全相同,因为可以在命令行中使用自动环变量“$@”来引用具体的目标,完成对它的重建。例如规则:

 

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

 

例子中的“generate”根据命令行参数来决定输出文件的类型。使用了make的字符串处理函数“subst”来根据目标产生对应的命令行选项。

虽然在多目标的规则中,可以根据不同的目标使用不同的命令(在命令行中使用自动化变量“$@”)。但是,多目标的规则并不能做到根据目标文件自动改变依赖文件(像上边例子中使用自动化变量“$@”改变规则的命令一样)。需要实现这个目的是,要用到make的静态模式。

4.11多规则目标

Makefile中,一个文件可以作为多个规则的目标(多个规则中只能有一个规则定义命令)。这种情况时,以这个文件为目标的规则的所有依赖文件将会被合并成此目标一个依赖文件列表,当其中任何一个依赖文件比目标更新(比较目标文件和依赖文件的时间戳)时,make将会执行特定的命令来重建这个目标。

对于一个多规则的目标,重建此目标的命令只能出现在一个规则中(可以是多条命令)。如果多个规则同时给出重建此目标的命令,make将使用最后一个规则中所定义的命令,同时提示错误信息(一个特殊的例外是:使用“.”开头的多规则目标文件,可以在多个规则中给出多个重建命令。这种方式只是为了和其他版本make进行兼容,一般在GNU make中应该避免使用这个功能)。某些情况,需要对相同的目标使用不同的规则中所定义的命令,我们需要使用另外一种方式——“双冒号”规则来实现。

一个仅仅描述依赖关系的述规则可用来给出一个或做多个目标文件的依赖文件。例如,Makefile中通常存在一个变量,就像以前我们提到的“objects”,它定义为所有的需要编译生成的.o文件的列表。当这些.o文件在其源文件所包含的头文件“config.h”发生变化之后能够自动的被重建,我们可以使用多目标的方式来书写Makefile:

 

objects = foo.o bar.o

foo.o : defs.h

bar.o : defs.h test.h

$(objects) : config.h

 

这样做的好处是:我们可以在源文件增加或者删除了包含的头文件以后不用修改已经存在的Makefile的规则,只需要增加或者删除某一个.o文件依赖的头文件。这种方式很简单也很方便。对于一个大的工程来说,这样做的好处是显而易见的。在一个大的工程中,对于一个单独目录下的.o文件的依赖规则建议使用此方式。规则中头文件的依赖描述规则也可以使用gcc自动产生。

另外,我们也可以通过一个变量来增加目标的依赖文件,使用make的命令行来指定某一个目标的依赖头文件,例如:

 

extradeps=

$(objects) : $(extradeps)

 

它的意思是:如果我们执行“make extradeps=foo.h”那么“foo.h”将作为所有的.o文件的依赖文件。当然我们只执行“make”的话,就没有指定任何文件作为.o文件的依赖文件。

在多规则的目标中,如果目标的任何一个规则没有定义重建此目标的命令,make将会寻找一个合适的隐含规则来重建此目标。关于隐含规则可参考 第十章 make的隐含规则

4.12 静态模式

静态模式规则是这样一个规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。静态模式规则比多目标规则更通用,它不需要多个目标具有相同的依赖。但是静态模式规则中的依赖文件必须是相类似的而不是完全相同的。

4.12.1 静态模式规则的语法

首先,我们来看一下静态模式规则的基本语法:

TARGETS ...: TARGET-PATTERN: PREREQ-PATTERNS ...

COMMANDS

...

“TAGETS”列出了此规则的一系列目标文件。像普通规则的目标一样可以包含通配符。

“TAGET-PATTERN”和“PREREQ-PATTERNS”说明了如何为每一个目标文件生成依赖文件。从目标模式(TAGET-PATTERN)的目标名字中抽取一部分字符串(称为“茎”)。使用“茎”替代依赖模式(PREREQ-PATTERNS)中的相应部分来产生对应目标的依赖文件。下边详细介绍这一替代的过程。

首先在目标模式和依赖模式中,一般需要包含模式字符“%”。在目标模式(TAGET-PATTERN)中“%”可以匹配目标文件的任何部分,模式字符“%”匹配的部分就是“茎”。目标文件和目标模式的其余部分必须精确的匹配。看一个例子:目标“foo.o”符合模式“%.o”,其“茎”为“foo”。而目标“foo.c”和“foo.out”就不符合此目标模式。

每一个目标的依赖文件是使用此目标的“茎”代替依赖模式(PREREQ-PATTERNS)中的模式字符“%”而得到。例如:上边的例子中依赖模式(PREREQ-PATTERNS)为“%.c”,那么使用“茎”“foo”替代依赖模式中的“%”得到的依赖文件就是“foo.c”。需要明确的一点是:在模式规则的依赖列表中使用不包含模式字符“%”也是合法的。代表这个文件是所有目标的依赖文件。

在模式规则中字符‘%’可以用前面加反斜杠“\”方法引用。引用“%”的反斜杠也可以由更多的反斜杠引用。引用“%”、“\”的反斜杠在和文件名比较或由“茎”代替它之前会从模式中被删除。反斜杠不会因为引用“%”而混乱。如,模式“the\%weird\\%pattern\\”是“the%weird\”+“%”+“pattern\\”构成。最后的两个反斜杠由于没有任何转义引用“%”所以保持不变。

我们来看一个例子,它根据相应的.c文件来编译生成“foo.o”和“bar.o”文件:

 

objects = foo.o bar.o

 

all: $(objects)

 

$(objects): %.o: %.c

$(CC) -c $(CFLAGS) $< -o $@

 

例子中,规则描述了所有的.o文件的依赖文件为对应的.c文件,对于目标“foo.o”,取其茎“foo”替代对应的依赖模式“%.c”中的模式字符“%”之后可得到目标的依赖文件“foo.c”。这就是目标“foo.o”的依赖关系“foo.o: foo.c”,规则的命令行描述了如何完成由“foo.c”编译生成目标“foo.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

 

在使用静态模式规则时,指定的目标必须和目标模式相匹配,否则执行make时将会得到一个错误提示。如果存在一个文件列表,其中一部分符合某一种模式而另外一部分符合另外一种模式,这种情况下我们可以使用“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”。“filter”函数过滤不符合“%.o”模式的文件名而返回所有符合此模式的文件列表。第一条静态模式规则描述了这些目标文件是通过编译对应的.c源文件来重建的。同样第二条规则也是使用这种方式。

我们通过另外一个例子来看一下自动环变量“$*”在静态模式规则中的使用方法:

 

bigoutput littleoutput : %output : text.g

generate text.g -$* > $@

 

当执行此规则的命令时,自动环变量“$*”被展开为“茎”。在这里就是“big”和“little”。

静态模式规则对一个较大工程的管理非常有用。它可以对整个工程的同一类文件的重建规则进行一次定义,而实现对整个工程中此类文件指定相同的重建规则。比如,可以用来描述整个工程中所有的.o文件的依赖规则和编译命令。通常的做法是将生成同一类目标的模式定义在一个make.rules的文件中。在工程各个模块的Makefile中包含此文件。

4.12.2 静态模式和隐含规则

Makefile中,静态模式规则和被定义为隐含规则的模式规则都是我们经常使用的两种方式。两者相同的地方都是用目标模式和依赖模式来构建目标的规则中的文件依赖关系,两者不同的地方是make在执行时使用它们的时机。

隐含规则可被用在任何和它相匹配的目标上,在Makefile中没有为这个目标指定具体的规则、存在规则但规则没有命令行或者这个目标的依赖文件可被搜寻到。当存在多个隐含规则和目标模式相匹配时,只执行其中的一个规则。具体执行哪一个规则取决于定义规则的顺序。

相反的,静态模式规则只能用在规则中明确指出的那些文件的重建过程中。不能用在除此之外的任何文件的重建过程中,并且它对指定的每一个目标来说是唯一的。如果一个目标存在于两个规则,并且这两个规则都定以了命令,make执行时就会提示错误。

静态模式规则相比隐含模式规则由以下两个优点:

²       不能根据文件名通过词法分析进行分类的文件,我们可以明确列出这些文件,并使用静态模式规则来重建其隐含规则。

²       对于无法确定工作目录内容,并且不能确定是否此目录下的无关文件会使用错误的隐含规则而导致make失败的情况。当存在多个适合此文件的隐含规则时,使用哪一个隐含规则取决于其规则的定义顺序。这种情况下我们使用静态模式规则就可以避免这些不确定因素,因为静态模式中,指定的目标文件有明确的规则来描述其依赖关系和重建命令。

4.13 双冒号规则

双冒号规则就是使用“::”代替普通规则的“:”得到的规则。当同一个文件作为多个规则的目标时,双冒号规则的处理和普通规则的处理过程完全不同(双冒号规则允许在多个规则中为同一个目标指定不同的重建目标的命令)。

首先需要明确的是:Makefile中,一个目标可以出现在多个规则中。但是这些规则必须是同一类型的规则,要么都是普通规则,要么都是双冒号规则。而不允许一个目标同时出现在两种不同类型的规则中。双冒号规则和普通规则的处理的不同点表现在以下几个方面:

1.        双冒号规则中,当依赖文件比目标更新时。规则将会被执行。对于一个没有依赖而只有命令行的双冒号规则,当引用此目标时,规则的命令将会被无条件执行。而普通规则,当规则的目标文件存在时,此规则的命令永远不会被执行(目标文件永远是最新的)。

2.        当同一个文件作为多个双冒号规则的目标时。这些不同的规则会被独立的处理,而不是像普通规则那样合并所有的依赖到一个目标文件。这就意味着对这些规则的处理就像多个不同的普通规则一样。就是说多个双冒号规则中的每一个的依赖文件被改变之后,make只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执行。

我们来看一个例子,在我们的Makefile中包含以下两个规则:

 

Newprog :: foo.c

       $(CC) $(CFLAGS) $< -o $@

Newprog :: bar.c

       $(CC) $(CFLAGS) $< -o $@

 

如果“foo.c”文件被修改,执行make以后将根据“foo.c”文件重建目标“Newprog”。而如果“bar.c”被修改那么“Newprog”将根据“bar.c”被重建。回想一下,如果以上两个规则为普通规时出现的情况是什么?(make将会出错并提示错误信息)

当同一个目标出现在多个双冒号规则中时,规则的执行顺序和普通规则的执行顺序一样,按照其在Makefile中的书写顺序执行。

GNU make的双冒号规则给我们提供一种根据依赖的更新情况而执行不同的命令来重建同一目标的机制。一般这种需要的情况很少,所以双冒号规则的使用比较罕见。一般双冒号规则都需要定义命令,如果一个双冒号规则没有定义命令,在执行规则时将为其目标自动查找隐含规则。

4.14 自动产生依赖

Makefile中,有时需要书写一些规则来描述一个.o文件和头文件的依赖关系。例如,如果在main.c中使用“#include defs.h”,那么我们可能就需要一个像下边那样的规则来描述当头文件“defs.h”被修改以后再次执行make,目标“main.o”应该被重建。

main.o: defs.h

这样,对于一个大型工程。就需要在Makefile中书写很多条类似于这样的规则。并且,当在源文件中加入或删除头文件后,也需要小心地去修改Makefile。这是一件非常费力、费时并且危险(容易出错误)的工作。为了避免这个讨厌的问题,现代的c编译器提供了通过查找源文件中的“#include”来自动产生这种依赖关系的功能。Gcc通过“-M”选项来实现此功能,使用“-M”选项gcc将自动找寻源文件中包含的头文件,并生成文件的依赖关系。例如,如果“main.c”只包含了头文件“defs.h”,那么在Linxu下执行下面的命令:

gcc -M main.c

 

其输出是:

main.o : main.c defs.h

 

既然编译器已经提供了自动产生依赖关系的功能,那么我们就不需要去动手写这些规则的依赖关系了。但是需要明确的是:如果在“main.c”中包含了标准库的头文件,使用gcc的“-M”选项时,其输出结果中也包含对标准库的头文件的依赖关系描述。当不需要在依赖关系中考虑标准库头文件时,对于gcc需要使用“-MM”参数。

在使用gcc自动产生依赖关系时,所产生的规则中明确的指明了目标是“main.o”。一次在通过.c文件直接产生可执行文件时,作为中间过程文件的“main.o”在使用完之后将不会被删除。

在旧版本的make中,使用编译器此项功能通常的做法是:在Makefile中书写一个伪目标“depend”的规则来定义自动产生依赖关系文件的命令。输入“make depend”将生成一个称为“depend”的文件,其中包含了所有源文件的依赖规则描述。Makefile中使用“include”指示符包含这个文件。

在新版本的make中,推荐的方式是为每一个源文件产生一个描述其依赖关系的makefile文件。对于一个源文件“NAME.c”,对应的这个makefile文件为“NAME.d”。“NAME.d”中描述了文件“NAME.o”所要依赖的所有头文件。采用这种方式,只有源文件在修改之后才会重新使用命令生成新的依赖关系描述文件“NAME.o”。

我们可以使用如下的模式规则来自动生成每一个.c文件对应的.d文件:

 

%.d: %.c

$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \

sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \

rm -f $@.$$$$

 

此规则的含义是:所有的.d文件依赖于同名的.c文件。

第一行;使用c编译器自自动生成依赖文件($<)的头文件的依赖关系,并输出成为一个临时文件,“$$$$”表示当前进程号。如果$(CC)为GNU的c编译工具,产生的依赖关系的规则中,依赖头文件包括了所有的使用的系统头文件和用户定义的头文件。如果需要生成的依赖描述文件不包含系统头文件,可使用“-MM”代替“-M”。

第二行;使用sed处理第二行已产生的那个临时文件并生成此规则的目标文件。这里sed完成了如下的转换过程。例如对已一个.c源文件。将编译器产生的依赖关系:

main.o : main.c defs.h

转成:

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

这样就将.d加入到了规则的目标中,其和对应的.o文件文件一样依赖于对应的.c源文件和源文件所包含的头文件。当.c源文件或者头文件被改变之后规则将会被执行,相应的.d文件同样会被更新。

第三行;删除临时文件。

使用上例的规则就可以建立一个描述目标文件依赖关系的.d文件。我们可以在Makefile中使用include指示符将描述将这个文件包含进来。在执行make时,Makefile所包含的所有.d文件就会被自动创建或者更新。Makefile中对当前目录下.d文件处理可以参考如下:

 

sources = foo.c bar.c

sinclude $(sources:.c=.d)

 

例子中,变量“sources”定义了当前目录下的需要编译的源文件。变量引用置换“$(sources : .c=.d)”的功能是根据变量“source”指定的.c文件自动产生对应的.d文件,并在当前Makefile文件中包含这些.d文件。.d文件和其它的makefile文件一样,make在执行时读取并试图重建它们。其实这些.d文件也是一些可被make解析的makefile文件。

需要注意的是include指示符的书写顺序,因为在这些.d文件中已经存在规则。当一个Makefile使用指示符include这些.d文件时,应该注意它应该出现在终极目标之后,以免.d文件中的规则被是Makefile的终极规则。关于这个前面我们已经有了比较详细的讨论。

原创粉丝点击