第十章:make的隐含规则


Makefile中重建一类目标的标准规则在很多场合需要用到。例如:根据.c源文件创建对应的.o文件,传统方式是使用GNU C编译器。

“隐含规则”为make提供了重建一类目标文件通用方法,不需要在Makefile中明确地给出重建特定目标文件所需要的细节描述。例如:典型地;makeC文件的编译过程是由.c源文件编译生成.o目标文件。当Makefile中出现一个.o文件目标时,make会使用这个通用的方式将后缀为.c的文件编译称为目标的.o文件。

另外,在make执行时根据需要也可能是用多个隐含规则。比如:make将从一个.y文件生成对应的.c文件,最后再生成最终的.o文件。就是说,只要目标文件名中除后缀以外其它部分相同,make都能够使用若干个隐含规则来最终产生这个目标文件(当然最原始的那个文件必须存在)。例如;可以在Makefile中这样来实现一个规则:“foo : foo.h”,只要在当前目录下存在“foo.c”这个文件,就可以生成“foo”可执行文件。本文前边的很多例子中已经使用到了隐含规则。

内嵌的“隐含规则”在其所定义的命令行中,会使用到一些变量(通常也是内嵌变量)。我们可以通过改变这些变量的值来控制隐含规则命令的执行。例如:内嵌变量“CFLAGS”代表了GCC编译器编译源文件的编译选项,我们就可以在Makefile中重新定义它,来实现我们编译源文件所要使用的参数。

尽管我们不能改变make内嵌的隐含规则,但是我们可以使用模式规则重新定义自己的隐含规则。也可以使用后追规则来重新定义隐含规则,后缀规则存在某些限制(目前版本make保存它的原因是为了兼容以前版本)。通常模式规则更加清晰明了。

10.1 隐含规则的使用

使用make内嵌的隐含规则,我们的Makefile中就不需要明确给出重建某一个目标的命令,甚至可以不用写出明确的规则。make会自动根据已存在(或者可以被创建)的源文件类型来启动相应的隐含规则。例如:

 

foo : foo.o bar.o

     cc -o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)

 

这里并没有给出重建文件“foo.o”的规则,make执行这条规则时,无论文件“foo.o”存在与否,都会试图根据隐含规则来重建这个文件(就是试图重新编译文件“foo.c”或者其它类型的源文件)。

make执行过程中找到的隐含规则,提供了此目标的基本依赖关系,确定了目标的依赖文件(通常是源文件,不包含对应的头文件依赖),和重建目标需要使用的命令行。隐含规则所提供的依赖文件只是一个最基本的(通常它们之间的对应关系为:“EXENAME.o”对应“EXENAME.c”、“EXENAME”对应于“EXENAME.o”)。当需要增加这个目标的依赖文件时,要在Makefile中使用没有命令行的规则来明确说明。

每一个内嵌的隐含规则中都存在一个目标模式和依赖模式关系,而且一个目标模式可以对应多个依赖模式。例如:一个.o文件的目标可以存在多个源文件模式:C编译器编译对应的.c源文件、或者Pascal编译器编译.p的源文件来实现。因此make会根据不同的源文件来使用不同的编译器。对于“foo.c”就是用GCC编译,对于“foo.p”就使用Pascal编译器编译。

上边提到,make会自动根据已存在(或者可以被创建)的源文件类型来启动相应的隐含规则。这里说说的“可被创建”的文件是指:这个文件在Makefile中被作为目标或者依赖明确的提及,或者可以根据已存在的文件使用其它的隐含规则来创建。当一个隐含规则的目标是另外一个隐含规则的依赖时,我们称它们是一个隐含规则链。

通常,make会对那些没有命令行的规则、双冒号规则寻找一个隐含规则来执行。作为一个规则的依赖文件,在没有一个规则明确描述它的依赖关系的情况下;make会将其作为一个目标并为它搜索一个隐含规则,试图重建它。

注意:给目标文件指定明确的依赖文件并不会影响隐含规则的搜索。我们来看一个例子:

 

foo.o: foo.p

 

这个规则指定了“foo”的依赖文件是“foo.p”。但是如果在工作目录下存在同名.c源文件“foo.c”。执行make的结果就不是用“pc”编译“foo.p”来生成“foo”,而是用“cc”编译“foo.c”来生成目标文件。这是因为在隐含规则列表中对.c文件的隐含规则处于.p文件隐含规则之前。

当需要给目标指定明确的重建规则时,规则描述中就不能省略命令行,这个规则必须提供明确的重建命令来说明目标需要重建所需要的动作。为了能够在存在“foo.c”的情况下编译“foo.p”。规则可以这样写:

 

foo.o: foo.p

     pc $< -o $@

 

这一点在多语言实现的工程编译中,需要特别注意!否则编译出来的可能就不是你想要得程序。

另外:当我们不想让make为一个没有命令行的规则中的目标搜索隐含规则时,我们需要使用空命令来实现。

最后让我们来看一个简单的例子,之前在 目标指定变量 一节的例子我们就可以简化为:

# sample Makefile

 

CUR_DIR = $(shell pwd)

INCS := $(CUR_DIR)/include

CFLAGS := -Wall –I$(INCS)

 

EXEF := foo bar

 

.PHONY : all clean

all : $(EXEF)

 

foo : CFLAGS+=-O2

bar : CFLAGS+=-g

 

clean :

           $(RM) *.o *.d $(EXES)

 

例子中没有出现任何关于源文件的描述。所有剩余工作全部交给了make去处理,它会自动寻找到相应规则并执行、最终完成目标文件的重建。

隐含规则为我们提供了一个编译整个工程非常高效的手段,一个大的工程中毫无例外的会用到隐含规则。实际工作中,灵活运用GNU make所提供的隐含规则功能,可以大大提供效率。

10.2      make的隐含规则一览

本节所罗列出了GUN make常见的一些内嵌隐含规则,除非在Makefile中对此规则有明确定义、或者使用命令行“-r”或者“-R”参数(参考make的命令行选项 一节),否则这些隐含的规则将有效。

需要说明的是:即使我们没有使用命令行参数“-r”,在make中也并不是所有的这些隐含规则都被定义了。其实,很多的这些看似预定义的隐含规则在make执行时,实际是用后缀规则来实现的;因此,它们依赖于make中的“后缀列表”(也就是目标.SUFFIXES的一个后缀列表)。make的默认后缀列表为:“.out”、“.a”、“.ln”、“.o”、“.c”、“.cc”、“.C”、“.p”、“.f”、“.F”、“.r”、“.y”、“.l”、“.s”、“.S”、“.mod”、“.sym”、“.def”、“.h”、“.info”、“.dvi”、“.tex”、“.texinfo”、“.texi”、“txinfo”、“.w”、“.ch”、“.web”、“.sh”、“.elc”、“el”。所有我们下边将提到的隐含规则,如果其依赖文件中某一个满足列表中列出的后缀,则是后缀规则。我们可以通过“.SUFFIXES”来更改默认的后缀列表,这样做可能会是许多默认预定义的规则无效。。以下是常用的一些隐含规则(对于不常见的隐含规则这里没有描述):

1.        编译C程序

N.o”自动由“N.c 生成,执行命令为“$(CC) c $(CPPFLAGS) $(CFLAGS)”。

2.        编译C++程序

N.o”自动由“N.cc”或者“N.C 生成,执行命令为“$(CXX) c $(CPPFLAGS) $(CFLAGS)”。建议使用“.cc”作为C++源文件的后缀,而不是“.C

3.        编译Pascal程序

N.o”自动由“N.p”创建,执行命令时“$(PC) c  $(PFLAGS)”。

4.        编译Fortran/Ratfor程序

N.o”自动由“N.r”、“N.F”或者“N.f 生成,根据源文件后缀执行对应的命令:

    .f $(FC) –c  $(FFLAGS)

    .F $(FC) –c  $(FFLAGS) $(CPPFLAGS)

    .r $(FC) –c  $(FFLAGS) $(RFLAGS)

5.        预处理Fortran/Ratfor程序

N.f”自动由“N.r”或者“N.F 生成。此规则只是转换Ratfor或有预处理的Fortran程序到一个标准的Fortran程序。根据源文件后缀执行对应的命令:

    .F $(FC) –F $(CPPFLAGS) $(FFLAGS)

    .r $(FC) –F $(FFLAGS) $(RFLAGS)

6.        编译Modula-2程序

N.sym”自动由“N.def 生成,执行的命令是:“$(M2C) $(M2FLAGS) $(DEFFLAGS)”。“N.o”自动由“N.mod”生成,执行的命令是:“$(M2C) $(M2FLAGS) $(MODFLAGS)”。

7.        汇编和需要预处理的汇编程序

N.s”是不需要预处理的汇编源文件,“N.S”是需要预处理的汇编源文件。汇编器为“as”。

N.o 可自动由“N.s”生成,执行命令是:“$(AS) $(ASFLAGS)”。

N.s 可由“N.S”生成,C预编译器“cpp”,执行命令是:“$(CPP) $(CPPFLAGS)”。

8.        链接单一的object文件

N”自动由“N.o”生成,通过C编译器使用链接器(GUN ld),执行命令是:“$(CC) $(LDFLAGS) N.o $(LOADLIBES) $(LDLIBS)”。

此规则仅适用:由一个源文件可直接产生可执行文件的情况。当需要有多个源文件共同来创建一个可执行文件时,需要在Makefile中增加隐含规则的依赖文件。例如:

    x : y.o z.o

当“x.c”、“y.c”和“z.c”都存在时,规则执行如下命令:

    cc -c x.c -o x.o

   cc -c y.c -o y.o

   cc -c z.c -o z.o

   cc x.o y.o z.o -o x

   rm -f x.o

   rm -f y.o

   rm -f z.o

在复杂的场合,目标文件和源文件之间不存在向上边那样的名字对应关系时(“x”和“x.c”对应,因此隐含规则在进行链接时,自动将“x.c”作为其依赖文件)。需要明确给出描述目标依赖关系的规则。

 

通常,GCC在编译源文件时(根据源文件的后缀名来启动相应的编译器),如果没有指定“-c”选项,GCC会在编译完成之后调用“ld”连接成为可执行文件。

9.        Yacc C程序

N.c”自动由“N.y”,执行的命令:“$(YACC) $(YFALGS)”。(“Yacc”是一个语法分析工具)

10.   Lex C程序时的隐含规则。

N.c”自动由“N.l”,执行的命令是:“$(LEX) $(LFALGS)”。(关于“Lex”的细节请查看相关资料)

 

这里没有列出所有的隐含规则,仅列出我个人在实际工作中涉及到的。没有涉及的很难对英文文档进行深入地说明和理解。如果那些没有提到的各位有所使用,或者能够详细的描述可以添加到这个文档中!

 

在隐含规则中,命令行中的实际命令是使用一个变量计算得到,诸如:“COMPILE.c”、“LINK.o”(这个在前面也看到过)和“PREPROCESS.S”等。这些变量被展开之后就是对应的命令(包括了命令行选项),例如:变量“COMPILE.c”的定义为 cc -c”(如果Makefile中存在“CFLAGS”的定义,它也会存在于这个变量中)。

make会根据默认的约定,使用“COMPILE.x”来编译一个“.x”的文件。类似地使用“LINK.x”来连接“.x”文件;使用“PREPROCESS.x”对“.x”文件进行预处理。

每一个隐含规则在创建一个文件时都是用了变量“OUTPUT_OPTION”。make执行命令时根据命令行参数来决定它的值,当命令行中没有包含“-o”选项时,它的值为:“-o $@”,否则为空。建议在规则的命令行中明确使用“-o”选项执行输出文件路径。这是因为在编译一个多目录的工程时,如果我们的Makefile中使用了VPATH”指定搜索目录 时,编译后的.o文件或者其它文件会出现在和源文件不同的目录中。在有些系统的编译器不接受命令行的“-o”参数,而Makefile中包含“VPAT”的情况时,输出文件可能会出现在错误的目录下。解决这个问题的方式就是将“OUTPUT_OPTION”的值赋为“;mv $*.o $@”,其功能是将编译完成的.o文件改变为规则中的目标文件。 

10.3      隐含变量

内嵌隐含规则的命令中,所使用的变量都是预定义的变量。我们将这些变量称为“隐含变量”。这些变量我们可以对它进行修改:在Makefile中、通过命令行参数或者设置系统环境变量的的方式来对它进行重定义。无论是用那种方式,只要make在运行时它的定义有效,make的隐含规则都会使用这些变量。当然,也可以使用“-R”或“--nobuiltin-variables”选项来取消所有的隐含变量(同时将取消了所有的隐含规则)。

例如,编译.c源文件的隐含规则为:“$(CC) c $(CFLAGS) $(CPPFLAGS)”。默认的编译命令是“cc”,执行的命令是:“cc –c”。我们可以同上述的任何一种方式将变量“CC”定义为“ncc”,那么编译.c源文件所执行的命令将是“ncc -c”。同样我们可以对变量“CFLAGS”进行重定义。对这些变量重定义后如果需要整个工程的各个子目录有效,同样需要使用关键字“export”将他们导出;否则目录间编译命令可能出现不一致。关于对.c源文件的编译时,隐含规则使用“$(CC)”来引用编译器;“$(CFLAGS)”引用编译选项。

隐含规则中所使用的变量(隐含变量)分为两类:1. 代表一个程序的名字(例如:“CC”代表了编译器这个可执行程序)。2. 代表执行这个程序适用的参数(例如:变量“CFLAGS”),多个参数使用空格分开。当然也允许在程序的名字中包含参数。但是这种方式建议不要使用。对于参数的描述我们应该将它们集中在一个变量中,即就是整个工程存在一个必需的参数,我们也可以把它放置一个特殊命名的变量定义中,通用的代表参数的那个变量的定义中对它引用;通过这种方式来实现,而不是将它和所要执行的程序名定义在一个变量中。我们的Makefile要尽量做到清晰、明了、干练的管理一个工程。而不要各个变量之间存在错综复杂的关系!模块化的编程思想也同样适用于我们编写Makefile

以下是一些作为程序名的隐含变量定义:

10.3.1       代表命令的变量

AR

函数库打包程序,可创建静态库.a文档。默认是“ar”。

AS

汇编程序。默认是“as”。

 

CC

C编译程序。默认是“cc”。

CXX

C++编译程序。默认是“g++”。

CO

RCS中提取文件的程序。默认是“co”。

CPP

C程序的预处理器(输出是标准输出设备)。默认是“$(CC) E”。

FC

编译器和预处理Fortran Ratfor 源文件的编译器。默认是“f77”。

GET

SCCS中提取文件程序。默认是“get”。

LEX

Lex 语言转变为 C Ratfo 的程序。默认是“lex”。

PC

Pascal语言编译器。默认是“pc”。

YACC

Yacc文法分析器(针对于C程序)。默认命令是“yacc”。

YACCR

Yacc文法分析器(针对于Ratfor程序)。默认是“yacc r”。

MAKEINFO

转换Texinfo源文件(.texi)到Info文件程序。默认是“makeinfo”。

TEX

TeX源文件创建TeX DVI文件的程序。默认是“tex”。

TEXI2DVI

Texinfo源文件创建TeX DVI 文件的程序。默认是“texi2dvi”。

WEAVE

转换WebTeX的程序。默认是“weave”。

CWEAVE

转换C Web TeX的程序。默认是“cweave”。

 

TANGLE

转换WebPascal语言的程序。默认是“tangle”。

CTANGLE

转换C Web C。默认是“ctangle”。

RM

删除命令。默认是“rm f”。

 

10.3.2       命令参数的变量

下边的是代表命令执行参数的变量。如果给出默认值则默认值为空。

ARFLAGS

执行“AR”命令的命令行参数。默认值是“rv”。

ASFLAGS

执行汇编语器“AS”的命令行参数(明确指定“.s”或“.S”文件时)。

CFLAGS

执行“CC”编译器的命令行参数(编译.c源文件的选项)。

CXXFLAGS

执行“g++”编译器的命令行参数(编译.cc源文件的选项)。

COFLAGS

执行“co”的命令行参数(在RCS中提取文件的选项)。

CPPFLAGS

执行C预处理器“cc -E”的命令行参数(C Fortran 编译器会用到)。

FFLAGS

Fortran语言编译器“f77”执行的命令行参数(编译Fortran源文件的选项)。

GFLAGS

SCCS get”程序参数。

LDFLAGS

链接器参数。(如:“ld”)

LFLAGS

Lex文法分析器参数。

 

PFLAGS

Pascal语言编译器参数。

RFLAGS

Ratfor 程序的Fortran 编译器参数。

YFLAGS

Yacc文法分析器参数。

 

10.4      make隐含规则链

有时候,一个文件可以由一系列隐含规则进行创建。例如:文件“N.o”的创建过程可以是这样的:首先执行“yacc”由“N.y”生成文件“N.c”,之后执行“cc”将“N.c”编译成为“N.o”。我们把这样的一个系列称为一个“链”。

我们来看一下上边例子的执行过程。有两种情况:

1.        如果文件“N.c”存在或者它在Makefile中被提及,那就不需要进行其它搜索,make处理的过程是:首先,make可以确定出“N.o”可由“N.c”创建;之后,make试图使用隐含规则来重建“N.c”。它会寻找“N.y”这个文件,如果“N.y”存在,则执行隐含规则来重建“N.c”这个文件。之后再由“N.c”重建“N.o”;当不存在“N.y”文件时,直接编译“N.c”生成“N.o”。

2.        文件“N.c”不存在也没有在Makefile中提及的情况,只要存在“N.y”这个文件,那么make也会经过这两个步骤来完成重建“N.o”(N.y N.c N.o)的动作。这种情况下,文件“N.c”作为一个中间的过程文件。make过程中如果需要一个中间文件才能完成目标的重建,make将会自动将这个中间文件加入到依赖关系链中(和Makefile中明确提及的目标作相同处理),并根据隐含规则来重建它。

 

make的中间过程文件和那些明确指定的文件在规则的使用上完全相同。但make在对待中间过程文件和普通文件存在下列两点不同:

第一:中间文件不存在时,make处理两者的方式不同。对于一个普通文件来说,因为Makefile中有明确的提及,此文件可能是作为一个目标的依赖,make在执行它所在的规则前会试图重建它。但是对于中间文件,因为没有明确提及,make不会去试图重建它。除非这个中间文件的依赖文件(上例第二种情况中的文件“N.y”;N.c是中间过程文件)被更新。

第二:如果make执行时需要用到一个中间过程文件,默认的动作是:这个过程文件在make执行结束之后会被删除(make会在删除中间过程文件时打印出执行的命令以显示那些文件被删除了)。因此一个中间过程文件在执行完make之后就不再存在了。

Makefile中明确提及的所有文件都不会被作为中间过程文件来处理,这是缺省动作。不过可以在Makefile中使用特殊目标“.INTERMEDIATE”来声明那些文件需要被作为中间过程文件来处理(这些文件作为目标“.INTERMEDIATE”的依赖文件罗列),即使它们在Makefile中有明确的提及。这些作为特殊目标“.INTERMEDIATE”依赖的文件在make执行结束之后会被自动删除。

而另一方面,如果我们希望保留某些中间过程文件(它没有在Makefile中被提及),不希望make结束时自动删除。可以在Makefile中使用特使目标“.SECONDARY”来声明这些文件(这些文件将被作为“secondary”文件;需要保留的文件作为特殊目标“.SECONDARY”的依赖文件罗列)。“secondary”文件也同时被作为中间过程文件来对待。

需要保留中间过程文件还存在另外一种实现方式。例如需要保留所有.o的中间过程文件,我们可以将.o文件的模式(%.o)作为特殊目标“.PRECIOUS”的依赖,这样就可以实现保留所有的.o中间过程文件。

一个“链”可以包含两个以上的隐含规则的调用。一个隐含规则在一个“链”只能出现一次。否则会出现像“foo”依赖“foo.o.o”甚至“foo.o.o.o.o…”的不合逻辑的情况发生。因为如果允许同一个“链”多次调用同一隐含规则(N : N.o; $(LINK.o) $(LDFLAGS) N.o $(LOADLIBES) $(LDLIBS) ),会导致make进入到无限的循环中。

隐含规则链中的某些隐含规则,在某些情况会被优化处理。例如:从文件“foo.c”创建可执行文件“foo”,这个过程可以是:经隐含规则将“foo.c”编译生成“foo.o”文件,之后再使用另一个隐含规则来完成对“foo.o”的链接,最后生成执行文件“foo”。这个过程中编译和链接使用隐含规则链中的两个独立的规则。但是实际情况是,完成编译和链接是在一个规则中完成的,它使用“cc foo.c foo”命令直接来完成。make的隐含规则表中,所有可用的优化规则处于首选地位。

10.5      模式规则

模式规则类似于普通规则,只是在模式规则中,目标的定义中需要包含“%”字符(确切地说是一个),包含“%”的目标被用来匹配一个文件名,“%”可以匹配任何非空字符串。规则的依赖文件中同样可以使用“%”,依赖中的“%”的取值情况由目标中的“%”来决定。例如:模式规则“%.o : %.c”表示了所有的.o文件是由.c文件来生成的。我们可以使用模式规则来定义一个隐含规则。

需要注意的是:“%”的替换是在规则的其他变量和函数引用扩展完成之后进行的,变量和函数的展开一般发生在make读取Makefile时 (变量和函数的展开可参考 5 使用变量   7 make的函数 ),而模式规则中的“%”则发生在make的执行过程。

10.5.1       模式规则介绍

关于模式规则的概念本节的开始部分已经有了描述。隐含规则中目标文件,“%.c”匹配所有的以“.c”结尾的所有文件名(匹配的文件名长度最少为3个字母),“s%.c”匹配了所有第一个字母为“s”并且以“.c”结尾的文件名,它的文件名长度最小为5个字母。文件名中“%”匹配的部分称为“茎”。

模式规则中,如果依赖文件同样存在模式字符“%”,那么它也代表规则中目标文件匹配之后所得到的“茎”。使用模式规则时,目标文件匹配的同时,根据目标的匹配所得的“茎”产生对应的依赖文件,此文件必须是存在的或者可被创建的。

因此,一个模式规则的格式为:

 

%.o : %.c ; COMMAND...

 

这个模式规则指定了如何由文件“N.c”来创建文件“N.o”,文件“N.c”应该是已存在的或者可被创建的。

模式规则中依赖文件也可以不包含模式字符“%”。当模式规则的依赖文件名中不包含模式字符“%”时,它的含义是所有符合目标模式的目标文件都依赖于一个指定的文件(例如:%.o : debug.h,表示所有的.o目标文件都依赖于头文件“debug.h”)。这样的模式规则在很多场合是非常有用的。

同样一个模式规则可以由多个目标文件。但是多个目标的模式规则和普通的多目标的规则有些不同,普通多目标的规则的处理是将每一个目标作为一个独立的规则来处理,所以多个目标就就对应多个独立的规则(这些规则各自有自己的命令行,各个规则的命令行可能相同)。但对于多目标的模式规则,所有规则的目标共同拥有依赖文件和规则的命令行,当文件符合多个目标模式中的任何一个时,规则定义的重建命令可能将会执行;因为多个目标共同用于这个规则的命令行,因此一次命令执行之后,规则不会再去检查是否需要重建符合其它模式的目标。据个例子会描述得更清楚一些:

#sample Makefile

 

Objects = foo.o bar.o

CFLAGS := -Wall

 

%x : CFLAGS += -g

%.o : CFLAGS += -O2

   

%.o %.x : %.c

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

 

当我们在命令行中执行“make foo.o foo.x”时,会看到只有一个文件“foo.o”被创建了,同时make会提示“foo.x”文件是最新的(其实“foo.x”并没有被创建)。它表明了多目标的模式规则在make处理时是作为一个整体来处理的。这是多目标模式规则和多目标的普通规则的不同之处。大家可以把这个例子改写为普通的多目标规则试试看。

最后需要说明的是:

1.        模式规则在Makefile中的顺序需要注意,当一个目标文件符合多个模式规则的目标时,make将会按照第一个找到的作为重建它的规则。

2.        Makefile中指定的模式规则会覆盖隐含的模式规则。就是说在Makefile中明确指定的会替代隐含的模式规则。

3.        另外,依赖文件存在或者被提及的规则,优先于那些需要使用隐含规则来创建其依赖文件的规则。

10.5.2       模式规则示例

本小节来看一些关于模式规则的例子,这些模式规则是make实际预定义的一些模式规则。首先我们来看编译.c文件到.o文件的隐含模式规则:

 

%.o : %.c

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

 

这个规则描述了一个.o目标文件如何由对应的.c文件创建。规则的命令行中使用了自动化变量“$<”和“$@”,其中自动化变量“$<”代表规则的依赖,“$@”代表规则的目标文件。此规则在执行时,命令行中的自动化变量将根据实际的目标文件区对应值。

make中第二个内嵌模式规则是:

 

% :: RCS/%,v

       $(CO) $(COFLAGS) $<

 

这个规则的含义是:任何一个文件“X”都可以由目录“RCS”下的相应文件“x.v”生成。规则的目标为“%”,因此只要存在相对应的依赖文件(N.v),此目标可以创建文件(N)。双冒号表示该规则是最终规则,意味着规则的依赖文件不是中间过程文件。

另外,一个具有多目标的隐含规则是:

 

%.tab.c %.tab.h: %.y

       bison -d $<

 

这个规则是一个多目标的模式规则,关于多目标的特征可参考  模式规则介绍 一小节最后一个例子。

10.5.3       自动化变量

在模式规则中,规则的目标和依赖文件名代表了一类文件名。命令是对所有这一类文件重建过程的描述,显然,在命令中不能指定特定的文件名,否则模式规则将没有了意义。那么在模式规则的命令行中该如何表示文件,将成我们这一小节的讨论重点。make中使用了“自动环变量”来实现这个目的,自动化变量的取值是根据具体的规则决定的,就是说对不同的规则其所代表的文件名不同。

前边我们也看到了很多例子中使用到了自动化变量。下面对所有的自动化变量进行说明:

$@

代表规则中的目标文件名。如果目标是一个文档(Linux中,一般称.a文件为文档),那么它代表这个文档的文件名。在多目标的模式规则中,它代表的是哪个触发规则被执行的目标文件名。

$%

规则的目标文件是一个静态库文件时,代表静态库的一个成员名。例如,规则的目标是“foo.a(bar.o)”,那么,“$%”的值就为“bar.o”,“$@”的值为“foo.a”。如果目标不是函数库文件,其值为空。

$<

规则的第一个依赖文件名。如果是隐含规则,则它代表通过目标指定的第一个依赖文件。

$?

所有比目标文件更新的依赖文件列表,空格分割。如果目标是静态库文件名,代表的是库成员(.o文件)的更新情况。

$^

规则的所有依赖文件列表,使用空格分隔。如果目标是静态库文件名,它所代表的只能是所有库成员(.o文件)名。一个文件可重复的出现在目标的依赖中,变量“$^”只记录它的一次引用情况。就是说变量“$^”会去掉重复的依赖文件。

$+

类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时,库的交叉引用场合。

$*

在模式规则和态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时,“茎”也包含目录(斜杠之前)部分)。例如:文件“dir/a.foo.b”,当目标的模式为“a.%.b”时,“$*”的值为“dir/a.foo”。“茎”对于构造相关文件名非常有用。

自动化变量“$*”需要两点说明:

Ø        对于一个明确指定的规则来说不存在“茎”,这种情况下“$*”所代表的值发生变化。此时,如果目标文件名带有一个可识别的后缀,那么“$*”表示文件中除后缀以外的部分。例如:“foo.c”则“$*”的值为:“foo”,因为.c是一个可识别的文件后缀名。GUN make对明确规则的这种奇怪的处理行为是为了和其它版本的make兼容。通常,在除静态规则和模式规则以外,明确指定目标文件的规则中避免使用这个变量。

Ø        当明确指定文件名的规则中目标文件名包含不可识别的后缀时,此变量为空。

自动化变量“$?”在显式规则中也是非常有用的,规则中可以使用它来指定只对更新的依赖文件进行操作。例如,函数库文件“libN.a”,它由一些.o文件组成。如下的规则实现了根据变化的.o文件更新库文件:

 

lib: foo.o bar.o lose.o win.o

    ar r lib $?

 

上述列出的自动量变量中。其中有四个在规则中代表一个文件名($@$<$%$*)。而其它三个的在规则中代表一个文件名的列表。GUN make中,还可以通过这七个自动化变量来获取一个完整文件名中的目录部分或者具体文件名,需要在这些变量中加入“D”或者“F”字符。这样就形成了一系列变种的自动环变量。这些变量在以前版本的make中使用,在当前版本的make中,可以使用“dir”或者“notdir”函数来实现同样的功能。

$(@D)

代表目标文件的目录部分(去掉目录部分的最后一个斜杠)。如果“$@”是“dir/foo.o”,那么“$(@D)”的值为“dir”。如果“$@”不存在斜杠,其值就是“.”(当前目录)。注意它和函数“dir的区别!

$(@F)

目标文件的完整文件名中除目录以外的部分(实际文件名)。如果“$@”为“dir/foo.o”,那么“$(@F)”只就是“foo.o”。“$(@F)”等价于函数“$(notdir $@)”。

$(*D)

$(*F)

分别代表目标“茎”中的目录部分和文件名部分。

$(%D)

$(%F)

当以如“archive(member)”形式静态库为目标时,分别表示库文件成员“member”名中的目录部分和文件名部分。它仅对这种形式的规则目标有效。

$(<D)

$(<F)

分别表示规则中第一个依赖文件的目录部分和文件名部分。

$(^D)

$(^F)

分别表示所有依赖文件的目录部分和文件部分(不存在同一文件)。

$(+D)

$(+F)

分别表示所有依赖文件的目录部分和文件部分(可存在重复文件)。

$(?D)

$(?F)

分别表示被更新的依赖文件的目录部分和文件部分。

 

需要说明的是:在讨论自动化变量时,为了和普通变量(如:“CFLAGS”)的区别,我们直接使用了“$<”的形式。这只是仅仅为了和普通变量区别,没有别的目的。其实对于自动环变量和普通变量一样,代表规则第一个依赖文件名的变量名实际上是“<”,我们完全可以使用“$(<)”来替代“$<”。我们在引用自动化变量时通常的做法是形如“$<”,因为自动化变量是一个字符。

GUN make提供了支持“Sysv”的特性,允许在规则的依赖列表中使用特殊的变量引用(一般的自动化变量只能在规则的命令行中被引用)“$$@”、“$$(@D)”和“$$(@F)”(注意:要使用“$$”),它们分别代表了“目标完整的文件名”、“目标文件名中的目录部分”和“目标的实际文件名部分”。

这三个特殊的变量只能用在明确指定目标文件名的规则中或者是静态模式规则中,不能使用在于隐含规则中。另外Sysv makeGNU make对规则依赖的处理也不尽相同。Sysv make会对规则的依赖进行两次替换展开,而GUN make对依赖列表的处理只有一次,对其中的引用直接进行展开。

自动化变量存在这个有点古怪的特性完全是为了兼容Sysv 版本的makefile文件。我们在使用GNU make时可以不考虑这个特性,也可以在Makefile中使用伪目标“.POSIX”来禁止这一特性。

10.5.4       模式的匹配

通常,模式规则中目标模式由前缀、后缀、模式字符“%”组成,这三个部分中可以同时两部分为空。实际的文件名应该是以模式指定的前缀开始、后缀结束的任何文件名。文件名中除前缀和后缀以外的所有部分我们称之为“茎”(模式字符“%”可以代表若干字符。因此:模式“%.o”所匹配的文件“test.c”中“test”就是“茎”)。模式规则中依赖文件名的确定过程是:在处理模式规则时。首先根据规则定义的目标模式匹配实际的目标文件,确定“茎”,之后使用 “茎”替代规则依赖文件名中的模式字符“%”,生成依赖文件名。这样就形成了一个明确指定了目标和依赖文件的规则。例如模式规则:“%.o : %.c”,当“test.o”需要重建时将形成规则“test.o : test.c”。

当目标模式中包含斜杠(目录部分)。在进行目标文件匹配时,文件名中包含的目录字符串在匹配之前被移除,只进行基本文件名的匹配;匹配成功后,再目录加入到匹配之后的字符串之前形成“茎”。文字的描述看起来比较复杂,来看一个例子:例如目标模式为“e%t”,文件“src/eat”匹配于这个目标模式,那么“茎”就是“src/a”;模式规则中依赖文件的产生是:首先使用“茎”非目录部分(“a”)替代依赖文件中的模式字符“%”,之后再将目录部分(“src/”)加入到形成的依赖文件名之前构成依赖文件的全路径名。这里如果模式规则的依赖模式为“c%r”,则那么目标“src/eat”对应的依赖文件就为“src/car”。

10.5.5       万用规则

当一个模式规则的目标为“%”(它可以匹配任何文件名)时,我们称这个规则为万用规则。它非常有用,但是使用它可能会影响make的效率,因为make执行时将会为所有的规则的目标和依赖文件考虑万用规则。

假如在一个存在万用规则的Makefile中提及了文件“foo.c”。为了创建这个目标文件,make可能需要查找以下规则来创建这个目标:1. 通过连接一个.o文件“foo.c.o”来实现;2. 通过使用C编译和连接程序由文件“foo.c.c”创建来创建;3. 编译连接Pascal程序“foo.c.p”来创建……,make会试图使用所有可能的隐含规则去创建这个目标。

当然,我们都清楚这样的一个过程是没有必要的,我们很清楚“foo.c”是一个.c原文件,而不是一个可执行程序。make试图根据这些隐含规则来创建这个文件,由于目标的依赖文件(“foo.c.o”、“foo.c.c”等)不存在,最终这些隐含的规则无法被执行。但是存在万用规则,make所要考虑的情况就会很多,导致了make的执行效率会很低。

为了避免由于万用规则所带来的效率问题,可以对万用规则的使用加以限制。通常有两种方式来实现,在定义万用规则时可以使用两者之一来对其进行限制。

1.        将万用规则设置为最终规则,定义时使用双冒号规则。作为最终规则,此规则只有在它的依赖文件存在时才能被应用,即使它的依赖可以由隐含规则创建也不行。就是说,最终规则中没有进一步的“链”。

例如,从RCSSCCS文件中提取源文件的内建的隐含规则是最终规则。因此如果文件“foo.c,v”不存在,make就不会试图从一个中间文件“foo.c,v.o”或“RCS/SCCS/s.foo.c,v”去创建它。 RCS SCCS 文件一般都是最终源文件,它不能由其它任何文件重建;make可以记录它的时间戳,但不寻找重建它们的方式。

如果万用规则每一定义为最终规则,它就是非最终规则。非最终的万用规则不会被用在那些有明确模式指定的目标和依赖文件中。就是说Makefile中如果存在和此文件匹配的模式规则(非万用规则),那么对于这个目标来说其重建的规则就不是万用规则而是那个模式匹配的非万用规则。

例如,对于文件“foo.c”,如果在Makefile中同时存在一个万用规则和模式规则 %.c : %.y”(该规则运行Yacc)。无论该规则是否实际使用(如果碰巧存在文件“foo.y”,该规则将运行)。那么make试图重建“foo.c”的规则都是“%.c : %.y”所定义的规则,而不是万用规则。这样避免了make试图使用非最终的万用规则来重建文件“foo.c”的情况发生。

2.        特殊的内嵌伪模式规则用来指定一些特定的文件,避免在处理这些这种文件名的文件时使用非最终万用规则。这些伪模式规则没有依赖和命令行,用在其它的场合时被忽略。例如,内嵌的隐含规则:“%.p :”保证Pascal源程序如“foo.p”匹配特定的目标格式,从而避免make寻找“foo.p.o”或“foo.p.c”。 在后缀规则中,为后缀列表中的每一个有效后缀创建了一个“%.p”形式的伪模式规则。

10.5.6       重建内嵌隐含规则

对隐含规则,我们可以对它进行重建。重建一个隐含规则时,需要使用相同的目标和依赖文件,但是命令可以不同。这样就替代了有相同目标和依赖的那个make内嵌的规则,替代之后的隐含规则的顺序由它在Makefile中的位置决定。例如通常我们Makefile中可能会包含这样一个规则:

 

%.o : %.c

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

 

这个模式规则替代了编译.c文件的内嵌隐含规则。

也可以取消一个内嵌的隐含规则。同样需要定义一个和隐含规则具有相同的目标和依赖,没有命令行的模式规则。例如:如下的规则取消了make编译.s文件的内嵌规则。

%.o : %.s

 

10.6      缺省规则

有时需要定义一个缺省的规则,当在Makefile中没有重建的规则时(没有重建它的明确规则和可用的隐含规则)。就是用这个规则来重建它。就是说,当所需要的重建的目标文件没有可用的命令时、就执行这个缺省规则的命令来创建它。

定义这样一个规则,我们可以使用最终万用规则来实现。例如:在调试Makefile时(可能一些源文件还没有完成),我们所关心的是Makefile中规则的正确执行,而不关心源文件的内容。这样我们就可以使用没有内容的源文件来代替。可以在Makefile中定义这样一个规则:

 

%::

    touch $@

 

对没有找到的所有的.c文件使用“touch”命令创建一个空的文件。

实现一个缺省规则的方式也可以不使用万用规则来实现,可以使用伪目标“.DEFAULT”(可参考 Makefile的特殊目标 一节)实现。上边的例子也可以这样来书写:

 

.DEFAULT :

       touch $@

 

对于没有命令行的伪目标“.DEFAULT”,其含义是取消前边所有使用“.DEFAULT”指定的缺省执行命令。

同样,也可以让这个缺省的规则不执行任何命令(给它定义个一个空命令)。

另外缺省规则也可用来实现在一个Makefile中重载另一个makefile文件。

10.7      后缀规则

后缀规则是一种老风格定义隐含规则的方式。新版本的make中使用模式规则取代了这种实现方式,模式规则和它相比更加清晰明了。现在版本保留它的原因只是为了能够兼容旧的makefile文件。后缀规则有两种类型:“双后缀”和“单后缀”。

双后缀规则定义一对后缀:目标文件的后缀和依赖目标的后缀。它匹配所有后缀为指定目标后缀的文件名。对于一个匹配的目标文件,它的依赖文件是这样形成:将目标文件中的后缀替换为依赖文件的后缀之后得到。如:一个描述目标和依赖后缀的“.o”和“.c”的规则就等价于模式规则“%o : %c”。

单后缀规则只定义一个后缀:此后缀是源文件的后缀。它可以匹配任何文件,其依赖文件名是这样形成:将源文件后缀追加到目标文件名之后得到。例如:单后缀“.c”就等价于模式规则“% : %.c”。

后缀规则的识别过程:比较make本身所定义的后缀和规则中出现的后缀,如果规则的目标中只有一个可识别的后缀,则这个规则是一个“单后缀”规则;当规则的目标中有两个可识别的后缀时,这个规则就是“双后缀”规则。

例如:“.c”和“.o”都是make可识别的后缀。因此当定义了一个目标是“.c.o”的规则时。make会将它作为一个双后缀规则来处理,它的含义是目标为“.o”文件、依赖为“.c”文件。下边是使用后追规则定义的、编译.c源文件的规则:

 

.c.o:

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

 

一个后缀规则依赖关系描述中不能包含依赖文件。否则,此规则将被作为一个普通规则对待。因此规则:

 

.c.o: foo.h

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

 

它不是一个后缀规则。它是目标文件为“.c.o”、依赖文件是“foo.h”的普通规则。它不等价于规则:

 

%.o: %.c foo.h

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

 

需要注意的是:没有命令行的后缀规则是没有任何意义的。它和没有命令行的模式规则不同,它并不能取消之前使用后追规则定义的规则。它所实现的仅仅是将这个后缀规则作为目标加入到make的数据库中。

可识别的后缀指的是特殊目标“.SUFFIXES”的所有依赖的名字。通过给特殊目标“SUFFIXES”添加依赖来增加一个可被识别的后缀:

 

.SUFFIXES: .hack .win

 

它所实现的功能是把后缀“.hack”和“.win”加入到可识别后缀列表的末尾。

如果需要重设默认所有的可识别后缀,可以这样来实现:

 

.SUFFIXES:          #删除所有已定义的可识别后缀

.SUFFIXES: .c .o .h   #重新定义

 

其过程是首先使用没有依赖的特殊目标“.SUFFIXES”来删除所有已定义的可识别后缀;之后再重新定义。

注意:make的“-r”或“-no-builtin-rules”可以清空所有已定义的可识别后缀。

make读取所有的makefile文件之前,变量“SUFFIXE”被定义为默认的可识别后缀列表。虽然存在这样一个变量,但是请不要通过修改这个变量值的方式来改变可识别的后缀列表,应该使用特殊目标“.SUFFIXES”来实现。

10.8      隐含规则搜索算法

对于目标“T”,make为它搜索隐含规则的算法如下。此算法适合于:1. 任何没有命令行的双冒号规则;2. 任何没有命令行的普通规则;3. 那些不是任何规则的目标、但它是另外某些规则的依赖文件;4. 在递归搜索过程中,隐含规则链中前一个规则的依赖文件。

在搜索过程中没有提到后缀规则,因为所有的后缀规则在make读取Makefile时,都被转换为对应的模式规则。

对于形式为“ARCHIVE(MEMBER)”的目标,下边的算法会执行两次,第一次的目标是整个目标名“T”(“ARCHIVE(MEMBER)”),如果搜索失败,进行第二次搜索,第二次以“member”作为目标来搜索。

搜索过程如下:

1.        将目标“T”的目录部分分离,分离后目录部分称为“D”,其它部分称“N”。例如:“T”为“src/foo.o”时,D就是“src/”,“N”就为“foo.o”。

2.        列出所有和“T”或者“N”匹配的模式规则。如果模式规则的目标中包含斜杠,则认为和“T”相匹配,否则认为此模式规则和“N”相匹配。

3.        只要这个模式规则列表中包含一个非万用规则的规则,那么将列表中所有的非最终万用规则删除。

4.        删除这个模式规则列表中的所有没有命令行的规则。

5.        对于这个模式规则列表中的所有规则:

a)       计算出模式规则的“茎”SS应该是“T”或“N”中匹配“%”的非空的部分。

b)       计算依赖文件。把依赖中的“%”用“S”替换。如果目标模式中不包含斜杠,则把“D”加在替换之后的每一个依赖文件开头,构成完整的依赖文件名。

c)        测试规则的所有依赖文件是否存在或是应该存在(一个文件,如果在Makefile中它作为一个明确规则的目标,或者依赖文件被提及,我们就说这个文件是一个“应该存在”的文件)。如果所有的依赖文件都存在、应该存在或是这个规则没有依赖。退出查找,使用该规则。

6.        截止到第5步,合适的规则还是没有被找到,进一步搜索。对于这个模式规则列表中的每一规则:

a)       如果规则是一个终止规则,则忽略它,继续下一条模式规则。

b)       计算依赖文件(同第5步)。

c)        测试此规则的依赖文件是否存在或是应该存在。

d)       对于此规则中不存在的依赖文件,递归的调用这个算法查找它是否可由隐含规则来创建。

e)       如果所有的依赖文件存在、应该存在、或是它可以由一个隐含规则来创建。退出查找,使用该规则。

7.        如果没有隐含规则可以创建这个规则的依赖文件,则执行特殊目标“.DEFAULT”所指定的命令(可以创建这个文件,或者给出一个错误提示)。如果在Makefile中没有定义特殊目标“DEFAULT”,就没有可用的命令来完成“T”的创建。make退出。

一旦为一类目标查找到合适的模式规则。除匹配“T”或者“N”的模式以外,对其它模式规则中的目标模式进行配置,使用“茎”S替换其中的模式字符“%”,将得到的文件名保存直到执行命令更新这个目标文件(“T”)。在命令执行以后,把每一个储存的文件名放入数据库,并且标志为已更新,其时间戳和文件“T”相同。

在执行更新文件“T”的命令时,使用自动化变量表示规则中的依赖文件和目标文件。


下一章 上一章 目录