编程通常遵循一个相当简单的程序:编辑源文件,编译源代码成可执行的格式,调试结果。尽管将源代码翻译成可执行程序是常规的过程,如果做的不正确,程序员可能会浪费大量的时间去追踪问题。大多数的开发者都经历过这样的挫折:修改一个函数并运行新代码却发现他们的修改并没有修正bug。后来他们发现他们再也不能执行这个修改过的函数,由于一些程序的错误,如未能重新编译源代码、未能重新链接成可执行文件、未能重建成一个jar包。由于不同版本程序的开发,或由于其他平台和支撑库的其他版本,随着程序复杂性的增加,这些寻常的工作越来越容易出错。
Make的目的是自动将源代码编译成可执行文件。Make的优点是通过脚本你能规范程序中文件间的关系去make,并且通过它们间的关系和时间戳,每次就准确地知道采取什么步骤来生成所需的程序。使用这些信息,Make就可以优化建造过程从而可以避免一些不必要的步骤。
GNU make是这样做的,它定义了一个语言来描述源代码、中间文件、可执行文件间的依赖关系,还提供了管理配置功能来实现规范库的重用以及用户自定义宏的参数化过程。简而言之,通过提供一个应用程序组装的一个路线图以及它们是怎样组装的,make已经被考虑作为开发过程的中心了。
make所使用的规范通常被保存为一个文件,名字为makefile。下面是一个用来生成“Hello,World”程序的makefile:
hello: hello.c
gcc hello.c -o hello
为了生成可执行程序,在你喜欢的shell命令行需要键入:
$ make
这将引起make程序去读makefile文件并且生成它所发现的第一个目标:
$ make gcc hello.c -o hello
如果给出一个目标被作为命令行参数,那么这个目标将被更新。如果没有给出命令行参数,makefile文件中的第一个目标即默认目标将被作为参数使用。大多数的makefile都是使用默认目标来生成一个程序。这经常涉及许多步骤。通常程序中的源码是不完整的并且源码必须使用工具如flex或bison产生。接下来,源码被编译成二进制目标文件(C/C++为.o文件,Java为.class文件等)。最后,对于C/C++,目标文件被链接器(通常涉及编译器,gcc)链接来产生一个可执行程序。修改源文件中任何一个并调用make将引起这些命令中的一些而不是所有的命令去重复执行,所以改变后的源码被恰当地编译成可执行程序。规范文件或makefile描述了源代码、中间文件、可执行程序间的关系,所以make可以执行最少量的必需工作去更新可执行程序。
所以,make的首要价值是:按复杂顺序执行命令去生成一个应用程序等能力以及优化这些操作去减少编辑、编译、调试循环所占用的时间。而且make是足够的灵活,而被使用在一种文件依赖于另一种文件的任何地方,如从传统的程序C/C++到Java、TEX、数据库管理等。
目标和先决条件(或依赖条件)
makefile本质上包含一组用来生成一个应用程序的规则。第一个规则是作为默认规则。一个规则由三部分组成:目标target,目标的先决条件prereq,执行的命令commands:
target:prereq1 prereq2
commands
目标target是makefile文件必须要做的。先决条件prereq或依赖条件是在目标能被成功创建前必须存在的哪些文件。命令commands是那些从先决条件prereq来创建目标target的shell命令。
下面是一个将c文件foo.c编译成目标文件foo.o的规则:
foo.o: foo.c foo.h
gcc -c foo.c
目标文件foo.o位于冒号前,先决条件foo.c和foo.h位于冒号后,命令脚本位于下一行并且前面有一个tab制表符。
当make计算一个规则时,它就开始寻找文件中先决条件prereq和目标target的标示了。如果任何的先决条件有一个相关的规则,make就尝试首先更新它们,接下来就是更新目标文件。如果先决条件比目标文件更新,目标文件就会通过执行命令被重新制作。每一个命令行都传递给shell,并且在它的子shell里执行。如果命令中的任何一个产生一个错误,目标的生成就会被终止并且make也会退出。如果一个文件被修改距离现在更近,那么这个文件就比另一个文件更新。
下面是一个程序,它在它的输入中计算单词“fee”、“fie”、“foe”、“fum”的出现次数。它使用被一个简单的main函数驱动的一个flex扫描器:
#include <stdio.h>
extern int fee_count, fie_count, foe_count, fum_count;
extern int yylex( void );
int main( int argc, char ** argv )
{
yylex();
printf( "%d %d %d %d\n", fee_count, fie_count, foe_count, fum_count );
exit( );
}
flex扫描器文件lexer.l是非常简单:
int fee_count = ;
int fie_count = ;
int foe_count = ;
int fum_count = ;
%%
fee fee_count++;
fie fie_count++;
foe foe_count++;
fum fum_count++;
这个程序的makefile也是相当简单:
count_words: count_words.o lexer.o -lfl
gcc count_words.o lexer.o -lfl -ocount_words
count_words.o: count_words.c
gcc -c count_words.c
lexer.o: lexer.c
gcc -c lexer.c
lexer.c: lexer.l
flex -t lexer.l > lexer.c
当这个makefile被首次执行时,显示如下:
$ make
gcc -c count_words.c
flex -t lexer.l > lexer.c
gcc -c lexer.c
gcc count_words.o lexer.o -lfl -ocount_words
我们现在已经生成了一个可执行程序。当然,实际的程序通常由比这个更多的模块组成。而且,后面将会发现:这个makefile没有使用make的大多数特性,所以它显得更加冗长。然而,这是一个有效并有用的makefile。例如,在写这个例子期间,当以这个程序做实验时,我执行了几十次的makefile。当你查看makefile和样例的执行时,你可能注意到:被make执行的命令的顺序与makefile中这些命令出现的顺序是相反的。在makefile中,这种自顶向下的设计方式是寻常的。通常大多数目标的一般形式在makefile中首先被指定,而细节留待以后指定。在许多方面,make支持这种风格。其中最主要的是make的两阶段执行模式和递归变量。我们将在后面讨论详细的细节。
依赖检查
make是如何做决定去做什么的?让我们重温以前执行make时的更多细节来找到答案。
首先,make注意到:命令行没有包含目标参数targets,所以它决定去make默认目标,即count_words。它检查先决条件并发现了三个:count_words.o、lexer.o、-lfl。make现在考虑如何生成count_words.o并发现了与此相关的一个规则。它检查此规则的先决条件,注意到count_words.c没有规则但那个文件已存在,所以make执行命令将count_words.c编译成count_words.o,通过执行如下命令:
gcc -c count_words.c
从目标到先决条件、再到目标、再到先决条件的链接,这是典型的make如何分析一个makefile文件来决定命令的执行。
make考虑的下一个先决条件是lexer.o。规则的链接指向lexer.c但这次此文件不存在。make寻找到从lexer.l产生lexer.c的规则,所以它运行flex程序。现在lexer.c已生成,make就可以运行gcc程序。
最后,make检查到-lfl。gcc的-l选项指明一个系统库,它必须链接到应用程序。实际的库名通过“fl”指明,即libfl.a。GNU make包含这种句法的专门支持。当形如-l<NAME>的先决条件出现时,make就搜索一个形如libNAME.so的文件;如果未找到匹配项,它就搜索libNAME.a。make此处找到的是/usr/lib/libfl.a然后继续做最终的动作,即链接。
最小化重建
当我们运行我们的程序时,我们发现除了打印fees、fiesta、foes和fums外,它还打印输入文件的文本。打印输入文件的文本不是我们想要的。这个问题是我们已经忘记了我们的lexical分析器的一些规则,并且flex传递了这些未识别的文本到它的输出。为了解决这个问题,我们简单地加上一个任意字符的规则并且一个新的行规则为:
int fee_count = ;
int fie_count = ;
int foe_count = ;
int fum_count = ;
%%
fee fee_count++;
fie fie_count++;
foe foe_count++;
fum fum_count++;
.
\n
在编辑了这个文件后,我们需要重新生成应用程序去测试我们的改正:
$ make
flex -t lexer.l > lexer.c
gcc -c lexer.c
gcc count_words.o lexer.o -lfl -ocount_words
注意到这次文件count_words.c没有重新编译。当make分析那个规则时,它发现count_words.o已存在并且此文件比它的先决条件count_words.c更新,所以没有必要去重新编译来更新此文件。当分析lexer.c时,当然,make发现先决条件lexer.l比它的目标lexer.c更新,所以make必须更新lexer.c。这样依次引起lexer.o更新,然后count_words更新。现在我们的单词计数程序形成了:
$ count_words < lexer.l
调用make
之前的例子假定:
- 所有的工程源代码和make描述文件都保存在一个目录里。
- make描述文件称为makefile、Makefile、或GNUMakefile。
- 当执行make命令时,makefile位于用户的当前目录。
当在这些条件下调用make时,它就自动创建第一个目标。为了更新一个不同的目标需要包含将目标名字包含在命令行里:
$ make lexer.c
当make被执行时,它将读取描述文件并识别那些需要更新的目标。如果目标或任何它的先决条件文件是过时的(或丢失),那么对应的规则命令脚本里的shell命令将被一个一个执行。在命令被运行后,make假定目标是最新的并移动到下一个目标或退出。
如果你指定的目标也是最新的,make将显示如下所示并立即退出,而不会做任何事。
$ make lexer.c
make: `lexer.c' is up to date.
如果你指定的目标不在makefile里并且也没有隐含的规则在makefile里,make将有如下的响应:
$ make non-existent-target
make: *** No rule to make target `non-existent-target'. Stop.
make有许多命令行选项。最有用选项中的一个是--just-print (或-n),它告诉make去显示它将为一个特别的目标而执行的命令,实际上并没有执行这些命令。当写makefile时,这是特别重要的。在命令行里设置几乎任何makfile变量来覆盖默认变量值或那些在makefile中设置的变量值。
基本的makefile句法
现在你对make有了一个基本的理解了,你也几乎能写你自己的makefile了。 此处我们将覆盖足够的makefile句法和结构知识,为了让你开始使用make。
Makefile经常是自顶向下构建的,如此以至于最一般的目标,经常称为all,被默认更新。由于程序维护,紧跟其后的是越来越多的细节目标,如删除所有不想要的临时文件的clean目标。你能从这些目标名称上猜出:目标并不需要有实际的文件,任何名称都可以。
在上面的例子中,我们看见了一个规则的简化形式。一个规则的更完整形式为:
target1 target2 target3 : prerequisite1 prerequisite2
command1
command2
command3
一个或更多的目标位于冒号的左边,零个或更多的先决条件位于冒号的右边。如果冒号右边没有先决条件,那么仅仅当目标不存在时,此目标才会被更新。为了更新一个目标而执行的命令集有时被称为命令脚本,但经常它们仅仅是命令。
每一个命令必须以一个tab制表符开始。这个句法告诉make:紧跟tab后的字符串传给一个子shell去执行。如果你突然插入一个tab制表符作为一个非命令行的第一个字符,在大多数环境下make将接下来的文本解释为一个命令。如果你是幸运的,你的错误的tab制表符被识别为一个句法错误的话,你将收到信息为:
$ make
Makefile:: *** commands commence before first target. Stop.
我们将在下一节讨论tab字符的复杂性。
make的注释字符是#。从这个#字符到这行的结束的所有文本都将被忽略。注释可能是缩进的并且前面的空格将被忽略。注释字符#没有引进作为命令文本的make注释。整行包括#和后续的字符将被传给shell执行。如何处理这些依赖于你的shell。
长行能被延续,使用标准的Unix换码符后划线(\)。以这种方式,命令行被延续是寻常的。以后划线延续先决条件列表也是常见的。后面我们将介绍其他方式处理长的先决条件列表。
你现在已经有足够的背景知识去写简单的makefile文件。第2章将详细介绍规则,第3章是make变量,第5章是命令。现在你应该避免使用变量、宏以及多行的命令序列。
(待续2013.11.17)