本章我们将讨论Makefile的一个重要内容,规则。熟悉规则对于书写Makefile至关重要。Makefile中,规则描述了在何种情况下使用什么命令来重建一个特定的文件,此文件被称为规则“目标”(通常规则中的目标只有一个)。规则中出目标之外的罗列的其它文件称为“目标”的依赖,而规则的命令是用来更新或者创建此规则的目标。
除了makefile的“终极目标”所在的规则以外,其它规则的顺序在makefile文件中没有意义。“终极目标”就是当没有使用make 命令行指定具体目标时,make默认的更新的哪一个目标。它是makefile文件中第一个规则的目标。如果在makefile中第一个规则有多个目标的话,那么多个目标中的第一个将会被作为make的“终极目标”。有两种情况的例外:1. 目标名以点号“.”开始的并且其后不存在斜线“/”(“./”被认为是当前目录;“../”被认为是上一级目录);2. 模式规则的目标。当这两种目标所在的规则是Makefile的第一个规则时,它们并不会被作为“终极目标”。
“终极目标”是执行make的唯一目的,其所在的规则作为第一个被执行的规则。而其它的规则是在完成重建“终极目标”的过程中被连带出来的。所以这些目标所在规则在Makefile中的顺序无关紧要。
因此,我们书写的makefile的第一个规则应该就是重建整个程序或者多个程序的依赖关系和执行命令的描述。我们来看一个规则的例子:
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”中已经包含了此头文件。这也是为什么它作为目标依赖出现的原因。
通常规则的语法格式如下:
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之上。
在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)只有在目标文件不存在的情况下,才会参与规则的执行。当目标文件存在时此依赖不会参与规则的执行过程。
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”来模拟。本节开始已经提到过,通配符可被用在规则的命令中,它是在命令被执行时由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))。
在上一小节提到过变量定义时使用通配符可能在某些情况下会导致意外的结果。本小节将对此进行详细地分析和讨论。书写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)”)和实现字符串的置换。如何实现字符串的置换,后续将进行详细地讨论。之前提到过,在规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“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的源文件。对变量的赋值也用到了一个特殊的符号(:=)。
在一个较大的工程中,一般会将源代码和二进制文件(.o文件和可执行文件)安排在不同的目录来进行区分管理。这种情况下,我们可以使用make提供的目录搜索依赖文件功能(在指定的若干个目录下自动搜索依赖文件)。在Makefile中,使用依赖文件的目录搜索功能。当工程的目录结构发生变化后,就可以做到不更改Makefile的规则,只更改依赖文件的搜索目录。
本节我们将详细讨论在书写Makefile时如何使用这一特性。在自己的工程中灵活运用这一特性,将会起到事半功倍的效果。
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中对所有文件有效。当需要为不类型的文件指定不同的搜索目录时,需要使用另外一种方式。下一小节我们将会讨论这种更高级的方式。
另一个设置文件搜索路径的方法是使用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”
规则中一个依赖文件可以通过目录搜寻找到(使用前边提到的一般搜索或者是选择性搜索任一种),可能得到的是文件的完整路径名(文件的相对路径或者绝对路径,如:/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”目录下被更新(目标完整路径名不会被废弃)。
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