在之前的学习中,我们曾使用到变量,现在,让我们回顾曾在范例中出现的一些变量。其中最简单的变量语法如下:
$(variable-name)
这表明我们想扩展名字为variable-name的变量。变量可以包含任何文本,变量名可以包含多数字符包括点字符(.)。例如,包含C编译命令的COMPILE.c变量。通常,变量名必须放在$()括号里,才能被make识别。一个例外是:单个字符变量名则不需要括号。
通常,makefile定义了许多变量,不过其中有许多变量是make自动定义的。一些变量可以被用户设置,以方便用户控制make的行为,其余的变量则是供make与makefile通信使用的。
自动变量
当规则匹配时,make会定义自动变量。它们提供了对目标和前提条件列表的访问,这样你就不必显示知名任何文件名。这是避免代码重复的有效方式,但更重要的是定义更为一般的模式规则(稍后讨论)。
有六种核心的自动变量($^和$+视为一个):
变量 | 作用 |
$@ | 目标文件名称 |
$% | 归档成员规范的文件名元素 |
$< | 第一个前提条件的文件名称 |
$? | 由空格隔开的,时间比目标新的前提条件的所有文件名称 |
$^ | 由空格隔开的,所有的前提条件的文件名称,这个列表删除了重复的文件名,因为,对于多数使用来说,例如编译、复制等,并不想要重复的文件 |
$+ | 类似于$^,但包含重复的文件名称,传给连接器时有用 |
$* | 目标的主文件名。文件的主文件名不包含后缀,不要在模式规则以外使用 |
除此之外,为了与其他make兼容,上面的每个变量都包含两个变体。一个变体是返回值的目录部分。它通过在变量后添加字符D实现,$(@D),$(<D)等。另一个变体返回值的文件部分。它通过在变量后添加字符F实现,$(@F),$(<F)等。注意到,这些变体名字长度超过两个字符,因此必须用放在括号里。GUNmake提供了dir和notdir函数以提高可读性。我们将在后面介绍。
在规则匹配后,make使用目标和前提条件设置自动变量,因此,这些变量只能在该规则的命令脚本中使用。
现在,我们的makefile使用自动变量替代文件名:
count_words: count_words.o counter.o lexer.o -lfl
gcc $^ -o $@
count_words.o: count_words.c
gcc -c $<
counter.o: counter.c
gcc -c $<
lexer.o: lexer.c
gcc -c $<
lexer.c lexer.l
flex -t $< > $@
通过VPATH和vpath查找文件
目前,我们的范例非常简单,makefile和源文件位于同一单一目录。现实中程序是复杂的。现在让我们重构先前的范例,进行较为实际的文件布局。我们可以通过将main重构成一个名为counter的函数来修改我们先前的单词计数程序。
<pre name="code" class="plain">#include <lexer.h>
#include <counter.h>
void counter(int counts[4])
{
while (yylex())
/* nothing to do */;
counts[0] = fee_count;
counts[1] = fie_count;
counts[2] = foe_count;
counts[3] = fum_count;
}
一个可重用的库函数应该有一个头文件声明,因此,创建counter.h包含我们的声明:
#ifndef COUNTER_H_
#define COUNTER_H_
extern void counter(int counts[4]);
#endif
我们同样将lexer.l符号的声明放在lexer.h中:
#ifndef LEXER_H_
#define LEXER_H_
extern int fee_count, fie_count, foe_count, fum_count;
extern int yylex(void);
#endif
在传统的源代码树中,头文件放在include目录中,源文件放在src目录中。我们这样做,并将makefile放在父目录。现在,范例程序的布局如下图1:
图1源代码树
因为我们的源文件现在包含头文件,新产生的依存关系需要记录在makefile中,这样,头文件修改后,相应的目标文件会被更新。
count_words: count_words.o couter.o lexer.o -lfl
gcc $^ -o $@
count_words.o: count_words.c include/counter.h
gcc -c $<
counter.o: counter.c include/counter.h include/lexer.h
gcc -c $<
lexer.o: lexer.c include/lexer.h
gcc -c $<
lexer.c: lexer.l
flex -t $< > $@
现在,当执行我们的makefile文件时,得到:
$ make
make:*** No rule to make `counter_words.c', needed by `count_words.o'.Stop.
发生了什么?makefile尝试更新count_words.c,但是这是源文件。让我们来扮演make。我们的第一个前提条件是count_words.o。我们看到不存在这个文件,但有创建它的规则。创建count_words.o引用count_words.c。但是,为什么make没有找到源文件呢?因为源文件在src目录而不在当前目录。除非高诉make,否则make只会在当前目录查找目标和前提条件文件。我们怎样让make查找src目录呢?或者,更为一般的,我们怎样告知make我们的源代码在哪儿?
你可以通过使用VPATH和vpath特性告诉make在不同的目录查找源文件。为了修复我们当前的问题,我们可以在makefile中加入VPATH赋值:
VPATH= src
这表明,如果需要的文件不在当前目录,make应该在src目录查找。现在,我们运行makefile,我们得到:
$ make
gcc-c src/count_words.c -o count_words.o
src/count_words.c:2:21:counter.h: No such file or directory
make:*** [count_words.o] Error 1
注意,现在我们可以编译第一个文件,因为填入了源代码的相对路径。使用自动变量的另一个原因是:如果你使用具体的文件名,make不能使用源代码适当的路径。不幸的是,编译失败了因为gcc不能找到头文件。我们可以解决这个问题通过自定义隐含编译规则的-I选项:
CPPFLAGS= -I include
此时的makefile文件如下:
VPATH = src include
CPPFLAGS = -I include
count_words: count_words.o couter.o lexer.o -lfl
gcc $^ -o $@
count_words.o: count_words.c include/counter.h
gcc $(CPPFLAGS) -c $<
counter.o: counter.c include/counter.h include/lexer.h
gcc $(CPPFLAGS) -c $<
lexer.o: lexer.c include/lexer.h
gcc $(CPPFLAGS) -c $<
lexer.c: lexer.l
flex -t $< > $@
现在可以成功编译了:
$ make
gcc-I include -c src/count_words.c -o count_words.o
gcc-I include -c src/counter.c -o counter.o
flex-t src/lexer.l > lexer.c
gcc-I include -c lexer.c -o lexer.o
gcccount_words.o counter.o lexer.o /lib/libfl.a -o count_words
VPATH变量包含make查找文件的目录列表。这个列表目录同时用于目标和前提条件文件的搜索,但是不包含在命令脚本的文件中。在Unix系统上,列表目录由空格和冒号分割;在Windows系统上,列表目录由空格和分号分割。我更倾向使用空格,因为它在所有的系统上都适用,这样可以避免在冒号和分号之间纠结。此外,使用空格更容易阅读。
VPATH很好的解决了上面文件搜索的问题。但是,它也有很大的不足。make会对它需要的文件在每个目录中进行查找。如果一个相同的文件在不同的地方多次出现且都在VPATH列表中,make会丢弃前面的。有时这会导致问题产生。
vpath指令以更精确的方式实现我们的目标。vpath指令的语法如下:
vpath pattern directory-list
因此,我们之前的VPATH可以重写为如下形式:
vpath %.c src
vpath %.h include
现在,我们告诉make应该在src目录下查找.c文件,include目录下查找.h文件(因此,我们可以移除前提条件中的include/)。在更复杂的应用中,这种控制可以解决一些头痛的问题和调试时间。
范例中,我们使用vpath指令解决分布在多个目录的源文件问题。怎样构建应用程序与此类似但又不同,目标文件写入二进制树中,然而源文件保存在源文件树中。正确使用vpath可以解决这个新问题,但是这个任务变得非常复杂,并且单独vpath是不足够。在后面我们会详细的讨论这个问题。