分享

GNU make中文手册-第四章:Makefile的规则

 todaytomo 2006-12-30
GNU make中文手册-第四章:Makefile的规则 作者: hew  发布日期: 2006-3-21    查看数: 149   出自: http://www.
第四章: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会自动将其展开;

2. 可出现在规则的命令中,其展开是在shell在执行此命令时完成。

除这两种情况之外的其它上下文中,不能直接使用通配符。二是需要通过函数“wildcard”(可参考 7.3 文件名处理函数 一节)来实现。

如果规则中的某一个文件的文件名包含作为统配符的字符(“*”、“.”字符),在使用文件时需要对文件名中的统配字符进行转义处理,使用反斜线(\)来进 行通配符的转义。例如“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”时一个空目标文件。(不存在一个这样的文件,此目标不代表一个文件,它只是记录了一个所要执行的动作或者命令。参考 4.8 空目标文件 一节)。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文件删除,在执行规则时将会得到一个类似于“没有创建*.o文件的规则” 的错误提示。这当然不是我们所期望的结果(可能在出现这个错误时会令你感到万分迷惑!)。为了实现们的初衷,在对变量进行定义的时需要使用一些高级的技 巧,包括使用“wildcard”函数和实现字符串的置换。关于如何实现字符串的置换,我们将在后续进行详细地讨论。

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的源文件。对变量的赋值也用到了一个特殊的符号(:=)。关于变量定义可参考 5.2 两种变量定义 一节。函数“patsubst”可参考 7.2 文本处理函数 一节



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

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

4.5.1 一般搜索(变量VPATH)
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的命令行来说明)。

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

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



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中程序链接的静态库、共享库同样也可以有目录搜索得到。这一特性需要我们在书规则的依赖是指定一个类似“-lNNAM”的依赖文件名(一 个奇怪的依赖文件名!一般依赖文件名应该是一个普通文件的名字。库文件的命名也应该是“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:

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。在最终执行失败的情 况下,我们很难根据错误的提示定位出具体是是那个目录下的Makefile出现错误。这给问题定位造成了很大的困难。为了避免这样的问题,我们可以在命令 行部分加入错误的监测,在命令执行错误后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的Makefile时,目录的处理顺序是需要特别注意的。

一般情况下,一个伪目标不作为一个另外一个目标文件的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则时伪目标所定义的命令都会被 执行(因为它是规则的依赖,重建规则目标文件时需要首先重建它的依赖)。当伪目标没有作为任何目标(此目标是一个可被创建或者已存在的文件)的依赖时,我 们只能通过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中我们推荐使用伪目标方式。关于伪目标可参考3.6 Makefile伪目标 一节

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

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

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



print: foo.c bar.c

lpr -p $?

touch print



执行“make print”,当目标“print”的依赖文件任何一个被修改之后,命令“lpr –p $?”都会被执行,打印这个被修改的文件。关于自动化变量“$?”可参考 9.5.3 自动化变量 一小节。

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

.PHONY:

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

.SUFFIXES:

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

.DEFAULT

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

.PRECIOUS

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

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

.INTERMEDIATE

目标“.INTERMEDIATE”的依赖文件在make时被作为中间过程文件对待。没有任何依赖文件的目标“.INTERMEDIATE”没有意义。参考 9.4 make隐含规则链 一节

.SECONDARY

目标“.SECONDARY”的依赖文件被作为中间过程文件对待。但这些文件不会被自动删除(可参考 9.4 make隐含规则链 一节)

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

.DELETE_ON_ERROR

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

.IGNORE

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

.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”。参考 8.7 make的命令行选项 一节



.EXPORT_ALL_VARIABLES

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

.NOTPARALLEL

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

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



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

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

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



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

² 对于多个具有类似重建命令的目标。重建这些目标的命令并不需要是绝对相同,因为我们可以在命令行中使用make的自动环变量“$@”来引用具体一个目标,并完成对它的重建(可参考 9.5.3 自动化变量 一小节)。例如规则:



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自动产生。可参考 3.14 自动产生依赖 一节

另外,我们也可以通过一个变量来增加目标的依赖文件,使用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”列出了此规则的一系列目标文件。像普通规则的目标一样可以包含通配符。关于通配符的使用可参考 3.4 文件名使用通配符 一节

“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”。命令行中“$<”和“$@”是自动化变量,“$<” 表示规则中的第一个依赖文件,“$@”表示规则中的目标文件(可参考 3.14 自动产生依赖 一节)。以上的规则就是描述了以下两个具体的规则:



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”函数(可参考 第七章 make的内嵌函数)来对这个文件列表进行分类,在分类之后对确定的某一类使用模式规则。例如:



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”的选项来实现此功能。“GCC”将自动找寻源文件中包含的头文件,并生成一个 依赖关系。例如,如果“main.c”只包含了头文件“defs.h”,那么在Linxu下执行下面的命令:



gcc -M main.c



其输出是:



main.o : main.c defs.h



既然编译器已经提供了自动产生依赖关系的功能,那么我们就不需要去动手写这些规则的依赖关系了。但是需要明确的是:在“main.c”中包含了其他的标准 库的头文件,其输出的依赖关系中也包含了标准库的头文件。当不需要依赖关系中不考虑标准库头文件时,需要使用“-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)”的功能是根据需要.c文件自动产生对应的.d文件,并在当前Makefile文件中包含这些.d文件。.d文件和其它的makefile文 件一样,make在执行时读取并试图重建他们。其实这些.d文件也是一些可被make解析的makefile文件。

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多