2009年2月5日星期四

学习GNU Make (2): 规则

Makefile中指定了针对某个target的规则(rule). 注意Makefile中可以有多条规则: 即针对Makefile中的一条targets, prerequisites列表, 就有一条对应的规则.

规则指定了target: 要更新的目标, 及目标所依赖的先决条件, 并指定了更新目标所采取的行为: command.

如果prerequisites比target更新, 那么Make就利用Makefile中所指定的命令脚本来更新目标. 同样地, prerequisites也可能是另一条规则中的目标, Make会查看属于它的先决条件是否比它更新以执行它的规则中的命令脚本. 这类似于C中的函数调用, 在Make中, 这些依赖关系建立了"依赖图"(dependency graph).

关于target和prerequisite的依赖性, 我们可以理解它有两方面的含义:
(1) 要生成targets, 先要保证它依赖的prerequisites是最新的
(2) 保证prerequisites在在targets之前被更新.


规则包含两个部分, 一个是依赖关系, 一个是生成目标的方法. GNU Make支持五种规则: Explicit rules, Pattern rules, Implicit rules, Static pattern rules, Suffix rules. 后面会介绍它们. 在了解这些规则之前, 我们先弄清makefile中通配符, 伪目标, 变量, VPATH, vpath的用法.

通配符

--------------------------------------------------------------------------------
在Makefile中可以使用通配符, GNU Make能够识别Bash的通配符(参考《学习Bash》, p22):
~ : 当前用户的home目录
~zp : 用户zp的home目录
*.o : 所有的目标文件
? : 单个字符
[abc] : a, 或b, 或c
[a-zA-Z]: 所有大小写字母
[!0-9] : 所有的非数字

最好不要使用*通配符, 它可能使你无意引入一些不相关的文件.

出现在targets, prerequisites中的通配符由Make展开, 而出现在commands中的通配符由shell展开.


若想使用通配符所代表字符的原意, 在它之前加上转义字符\: 比如\?表示问号, 而非任意单个字符.

伪目标 (phony targets)

--------------------------------------------------------------------------------
正如前面所说, 目标不仅可以是文件, 也可以标签. 用作标签的目标就是一个伪目标. 伪目标一般用来定义一个代表一条命令脚本的标签(也可以为伪目标指定prerequisites).

GNU Make是无法区别目标文件和伪文件的. 这样就存在一个问题, 如果你在Makefile里用到了一个标签, 如clean, 用以删除生成的中间目标文件:

clean:
rm -f *.o

如果当前目录刚好有一个名为clean的文件, Make就会把这个你想用作伪目标的clean与clean文件关联起来, 当你运行 $ make clean 时, 由于clean不存在prerequisites, make 会给出这样的信息:
make: `hello' is up to date.

如何解决呢? 这里就引入了.PHONY. 以下面的格式来生命一个伪target:

.PHONY : clean
clean:
rm -f *.o


.PHONY告诉了Make: 它后面的prerequisite, 也就是clean是一个伪目标.

.PHONY的作用:
(1)告知Make, 该伪目标并不是真正的文件, 不遵循由源代码生成目标文件的规则.
(2)告诉Make, 该伪目标始终需要被更新.


由于伪目标始终需要被更新, 那么如果它作为某个目标文件的prerequisite, 那么在每次调用make时都会执行命令. 所以通常不将伪目标作为prerequisite. 但给伪目标指定prerequisite非常有用(实际上就是让这些prerequisites对应的命令在伪目标被指定为target的时候被执行).

我们来看看这个例子: 该makefile同时生成多个可执行文件, 而且只需输入"make"

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
gcc -o prog1 prog1.o utils.o
prog2 : prog2.o
gcc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
gcc -o prog3 prog3.o sort.o utils.o

all是第一个target, 即为默认的target. 由于指定了all是一个伪目标, 所以all总是要被更新. 于是, 它的三个prerequisites: prog1, prog2, prog3所对应的规则也需要被裁决, 看它们各自是否需要被更新. 由此, 一条简单的make语句就可以生成多个可执行target.

注意: all : prog1 prog2 prog3 和 .PHONY : all 出现的前后顺序在这里是无关, 但最好将.PHONY : all放在前面, 这貌似更符合习惯.

注意, 在上述情况下, 如果生成的prog1, prog2, prog3不需要更新, make反馈的信息为:
make: Nothing to be done for `all'. 它不同于(非伪)目标不需要更新的信息: `' is up to date.

变量

--------------------------------------------------------------------------------
变量以这样的形式被定义:

$(variable-name)

它表示我们希望将名为variable-name的变量在makefile中扩展.(可以把它理解成C语言中的宏)

变量能包含所有的字符, 符号. 比如:$( compile.c)

变量的格式: 除了单个字符可以直接跟在$之后外( $?), 其他的变量都必须用()或者{}引起来.

我们可以自己定义变量, 同时, Make也会自动建立一些变量(自动变量).

自动变量(automatic variables)
自动变量一般用来表征target或prerequisites, 这样你就不用显式地指定它们的名称. 一共有7种自动变量:

$@ : target
$% : archive menmber specification
$< : 第一个prerequisite
$? : 所有比target新的prerequisites, 它们之间以空格分隔.
$^ : 所有的prerequisites(但不包括重复的prerequisites).
$+ : 与$?相同, 但包括重复的prerequisites.
$* : 目标文件名的主干(既不包括后缀), 它一般只用于pattern rules.

针对上面的自动变量, 有的版本的Make还提供了变种: 在变量名之后加上D表示变量的目录部分, 加上F表示变量的文件部分. 如: $(
由于自动变量是Make在建立target和prerequisites的对应关系后自动生成的, 所以只能在command中用到它们!


利用VPATH和vpath来查找文件

--------------------------------------------------------------------------------
对于非常简单的项目, 我们可以把所有的源代码, 头文件和Makefile一起放在同一个目录. 但对于比较复杂的情况(有比较多的头文件, 源代码文件. 这往往是现实中经常存在的情况), 我们一般把代码分类, 放在不同的目录中. 一种比较简单的做法就是将源代码文件放在src(或source)目录, 将头文件放在include目录. 然后将Makefile放在它们的父目录.

如果你不在调用make时明确地作出规定, make只在当前目录中搜索targets和prerequisites. make将makefile所在的目录默认为搜索路径优先级最高的目录, 即先在当前目录查找文件, 若找不到, 再到VPATH, vpath指定的目录查找.

下面以2种情况来说明文件查找的问题, 给出了对应的3个Makefile和2个.c文件.

VAPTH

VPATH语法: VPATH = dir1 dir2 ...

在UNIX下, 目录用" " 或 ":"分隔, 在windows下, 目录用 " "或";"分隔. 推荐使用空格!

1, 在当前目录创建一个src目录, 里面放置hello.0.c, 一个Makefile.0. 它们的内容分别如下:

/* hello.0.c */
#include
int main(void)
{
printf("hello, world!\n");
return 0;
}

#Makefile.0
hello: hello.c
gcc -Wall -o hello hello.c

在Makefile所在的目录运行make -f Makefile.0, 会得到这样的报错信息:

make: *** No rule to make target `hello.0.c', needed by `hello.0'. Stop.

这正是由于Make默认地在当前目录搜索target和prerequisites. 将Makefile改写为如Makefile.1所示, 在其中引入了VPATH变量.

#Makefile.1
VPATH = src

hello.0 : hello.0.c
gcc -Wall -o hello.0 hello.0.c

运行 $ make -f Makefile.1, 我靠, 还是报错? 没办法, 把命令改为如下:
gcc -Wall -o $@ $<

这下就成功了, 是不是要使用VPATH, 命令行就必须用make的自动变量代替文件名呢?

下面在Makefile所在的目录创建include目录, 在里面放个hello.h (内容很简单: #include ), 并将hello.c改写为:

/* hello.1.c */
#include "hello.h"
int main(void)
{
printf("hello\n");
return 0;
}

Makefile当然要改写:

#Makefile.2
VPATH = src include
CFLAGS = -I include

hello.1 : hello.1.c hello.h
gcc $(CFLAGS) -Wall -o $@ $<

有两点值得注意的地方:
(1) src, include都包括在VPATH中, 要告诉Make在哪里搜寻source, .c和.h文件的目录都要指定.
(2) VPATH只是告诉了Make在哪里寻找文件, 而gcc并不知道在哪里寻找hello.h (即VPATH只和Makefile中的target, prerequisites相关, 而不和command相关). 所以要声明一个CFLAGS变量. gcc $(CFLAGS) == gcc -I src/ .
还有一个值得注意的地方: 这里使用$<, 指代第hello.1.c. 如果用$^, 会把hello.h也包含进去, 而gcc的命令行中是不带.h文件的. 如果有多个.c文件, 使用变量表征它们的时候就要斟酌一下了.

使用VPATH有一个隐患: VPATH在每个目录中搜索所有的source文件. 如果一个相同的文件名出现在不同的目录中, VPATH将第一个搜索到的文件作为结果.

考虑下面的情况: make搜索hello.1.c和hello.h文件, 它首先在当前目录找到了hello.1.c, 但找不到hello.h, 于是它到VPATH, vpath指定的src和include去查找, 在src目录它又发现了hello.1.c文件, 但它将之前在当前目录找到的的hello.1.c作为优先的prerequist, 随后它又在include目录找到了hello.h: 将原本位于src目录中的hello.1.c拷贝到Makefile.2所在的目录, 修改其printf()语句, 让其显示"from the wrong dir!", 再次执行make -f Makefile.2, 运行新生成的hello.1, 我们会发现是错误的hello.1.c被编译了.

vpath
使用vpath较之VPATH更灵活. 它的基本语法为:

vpath

其中都是可选的, 由此有三种使用vpath的方法:
1, vpath
指定符合模式的文件的搜索路径为.
2, vpath
清除符合模式的文件的搜索路径.
3, vpath
清除所有通过vpath所设置的文件搜索路径.

中需要包含"%"字符, 它类似于扩展符中的"*", 表示匹配0或若干字符. 如: %.c 表示所有以.c结尾的C源文件.

可以利用vpath将Makefile.2改为如下:


"vpath %.c src" 和"vpath %.h include" 告诉Make在src目录搜索.c文件, 在include目录搜索.h文件.
要注意的是, 如果hello.1.c仍位于当前目录, make还是会将它作为首选, 而非vpath所指定的位于src目录中的hello.1.c!

VPATH is good for finding sources, not for finding targets.

还可以利用vpath来指定目标文件的路径, 后面再讨论.

注意使用VPATH和vpath时使用自动变量!

VPATH和vpath的比较
1, VPATH是make的特殊变量, 而path是make的关键字.
2, make首先在当前目录搜索文件, 并将位于当卡那目录的文件作为首选的prerequisites, 若在当前目录找不到该文件, 再到由VPATH或vpath指定的目录去搜索.

(一) 显式规则(Explicit Rules)

--------------------------------------------------------------------------------
在makefile中显式指定了targets, prerequisites. 这即使用了make的显式规则, 它是我们常用的规则.

(二) 模式规则(Pattern Rules)

--------------------------------------------------------------------------------
如果使用的文件比较多(比如需要使用大量的.o), 再使用显式规则, 输入那些.c, .h文件名就有点让人抓狂了. 在这种情况下可利用Make的自动推导(模式规则), 这样就不用针对每个.o文件声明command了.

利用下面的Makefile可以生成前面的hello.1:

#Makefile.4
vpath %.c src
vpath %.h include
CFLAGS = -I include

%: %.c %.h
$gcc -Wall $(CFLAGS) -o $@ $<
hello.1:

利用Pattern Rules, 可以让Make进行自动推导: Make看到一个finename目标, 就会将filename.c和filename.h加入到prerequisites.



Static Pattern Rules

--------------------------------------------------------------------------------
它和前面的Pattern Rules只有一个区别: 限定了针对那些target使用你给定的命令. 可以将Makefile.4改写为:

#Makefile.5
vpath %.c src
vpath %.h include
CFLAGS = -I include

OBJECTS = hello1

%(OBJECTS): %.c %.h
$gcc -Wall $(CFLAGS) -o $@ $<
hello.1:

没有评论:

发表评论