Linux 版 (精华区)

发信人: xian (我想用心来点亮希望), 信区: Linux
标  题: GNU make 指南 
发信站: 紫 丁 香 (Sun May  2 15:56:31 1999), 转信

GNUmake指南
GNUmake指南

翻译:哈少

译者按:本文是一篇介绍GNUMake的文章,读完后读者应该基本掌握了make的用法。而
make是所有想在
Unix(当然也包括Linux
)系统上编程的用户必须掌握的工具。如果你写的程序中没有用到make,则说明你写的
程序只是个人的练习程
序,不具有任何实用的价值。也许这么说有点儿偏激,但
make实在是应该用在任何稍具规模的程序中的。希望本文可以为中国的Unix编程初学者
提供一点儿有用的资料。
中国的Linux用户除了学会安装红帽子以外,
实在应该尝试写一些有用的程序。个人想法,大家参考。


C-Scene题目#2
多文件项目和GNUMake工具
作者:乔治富特(GoergeFoot)
电子邮件:george.foot@merton.ox.ac.uk
Occupation:StudentatMertonCollege,OxfordUniversity,England
职业:学生,默尔顿学院,牛津城大学,英格兰
IRC匿名:gfoot


拒绝承诺:作者对于任何因此而对任何事物造成的所有损害(你所拥有或不拥有的实际
的,抽象的,或者虚拟
的)。所有的损坏都是你自己的责任,而与我无关。

所有权:“多文件项目”部分属于作者的财产,版权归乔治富特1997年五月至七月。
其它部分属CScene财
产,版权CScene1997年,保留所有版权。本
CScene文章的分发,部分或全部,应依照所有其它CScene的文章的条件来处理。

0)介绍
~~~~~~~~~~~~~~~
本文将首先介绍为什么要将你的C源代码分离成几个合理的独立档案,什么时候需要分
怎么才能分的好。然后将
会告诉你GNUMake
怎样使你的编译和连接步骤自动化。对于其它Make
工具的用户来说,虽然在用其它类似工具时要做适当的调整,本文的内容仍然是非常
有用的。如果对你自己的编程
工具有怀疑,可以实际的试一试,但请先阅读用户手册。

1)多文件项目
~~~~~~~~~~~~~~~~~~~~~~

1.1为什么使用它们?

首先,多文件项目的好处在那里呢?
它们看起来把事情弄的复杂无比。又要header文件,又要extern声明,而且如果需要
查找一个文件,你要在更
多的文件里搜索。

但其实我们有很有力的理由支持我们把一个项目分解成小块。当你改动一行代码,编译器
需要全部重新编译来生成
一个新的可执行文件。但如果你的项目是分开在几个小文件里,当你改动其中一个文件的时候,别的源文件的目标
文件(object
files)已经存在,所以没有什么原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文件,然后重新
连接所有的目标文件罢了。在大型的项目中,这意味着从很长的(几分钟到几小时)重新编译缩短为十几,二十几
秒的简单调整。

只要通过基本的规划,将一个项目分解成多个小文件可使你更加容易的找到一段代码。很简单,你根据代码的作用
把你的代码分解到不同的文件里。当你要看一段代码时,你可以准确的知道在那个文件中去寻找它。

从很多目标文件生成一个程序包(Library)比从一个单一的大目标文件生成要好的多。当然实际上这是否真是一个
优势则是由你所用的系统来决定的。但是当使用
gcc/ld(一个GNUC编译/连接器)
把一个程序包连接到一个程序时,在连接的过程中,它会尝试不去连接没有使用到的部分。但它每次只能从程序包
中把一个完整的目标文件排除在外。因此如果你参考一个程序包中某一个目标档中任何一个符号的话,那么这个目
标文件整个都会被连接进来。要是一个程序包被非常充分的分解了的话,那么经连接后,得到的可执行文件会比从
一个大目标文件组成的程序包连接得到的文件小得多。

又因为你的程序是很模块化的,文件之间的共享部分被减到最少,那就有很多好处——可以很容易的追踪到臭虫,
这些模块经常是可以用在其它的项目里的,同时别人也可以更容易的理解你的一段代码是干
什么的。当然此外还有许多别的好处……

1.2何时分解你的项目

很明显,把任何东西都分解是不合理的。象“世界,你们好”这样的简单程序根本就不能分,因为实在也没什么可
分的。把用于测试用的小程序分解也是没什么意思的。但一般来说,当分解项目有助于布局、发展和易读性的时
候,我都会采取它。在大多数的情况下,这都是适用的。(所谓“世界,你们好”,既
'helloworld',只是一个介绍一种编程语言时惯用的范例程序,它会在屏幕上显示一行'helloworld'。是最简
单的程序。)

如果你需要开发一个相当大的项目,在开始前,应该考虑一下你将如何实现它,并且生成几个文件(用适当的名
字)来放你的代码。当然,在你的项目开发的过程中,你可以建立新的文件,但如果你这么做的话,说明你可能改
变了当初的想法,你应该想想是否需要对整体结构也进行相应的调整。

对于中型的项目,你当然也可以采用上述技巧,但你也可以就那么开始输入你的代码,当你的码多到难以管理的时
候再把它们分解成不同的档案。但以我的经验来说,开始时在脑子里形成一个大概的方案,并且尽量遵从它,或在
开发过程中,随着程序的需要而修改,会使开发变得更加容易。

1.3怎样分解项目

先说明,这完全是我个人的意见,你可以(也许你真的会?)用别的方式来做。这会触动到有关编码风格的问题,
而大家从来就没有停止过在这个问题上的争论。在这里我只是给出我自己喜欢的做法(同时也给出这么做的原
因):

i)不要用一个header文件指向多个源码文件(例外:程序包的header文件)。用一个header定义一个源码文
件的方式
会更有效,也更容易查寻。否则改变一个源文件的结构(并且它的header文件)就必须重新编译好几个文件。

ii)如果可以的话,完全可以用超过一个的header文件来指向同一个源码文件。有时将不可公开调用的函数原
型,类型定义
等等,从它们的C源码文件中分离出来是非常有用的。使用一个header文件装公开符号,用另一个装私人符号意
味着如果
你改变了这个源码文件的内部结构,你可以只是重新编译它而不需要重新编译那些使用它的公开header文件的其
它的源文件。

iii)不要在多个header文件中重复定义信息。如果需要,在其中一个header文件里#include另一个,但
是不要重复输入相同的header信息两次。原因是如果你以后改变了这个信息,你只需要把它改变一次,不用搜索
并改变另外一个重复的信息。

iv)在每一个源码文件里,#include那些声明了源码文件中的符号的所有header文件。这样一来,你在源码文
件和header
文件对某些函数做出的矛盾声明可以比较容易的被编译器发现。


1.4对于常见错误的注释


a)定义符(Identifier)在源码文件中的矛盾:在C里,变量和函数的缺省状态是公用的。因此,任何C源码档
案都可以引用存在于其它源
码档中的通用(global)函数和通用变量,既使这个档案没有那个变量或函数的声明或原型。因此你必须保证在不
同的两个档案里不能
用同一个符号名称,否则会有连接错误或者在编译时会有警告。

一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有关的前缀。比如:所有在gfx.c里的函数都加
上前缀“gfx_”。如果
你很小心的分解你的程序,使用有意义的函数名称,并且不是过分使用通用变量,当然这根本就不是问题。

要防止一个符号在它被定义的源文件以外被看到,可在它的定义前加上关键字“static”。这对只在一个档案内部
使用,其它档案都
都不会用到的简单函数是很有用的。

b)多次定义的符号:header档会被逐字的替换到你源文件里#include的位置的。因此,如果header档被
#include
到一个以上的源文件里,这个header档中所有的定义就会出现在每一个有关的源码文件
里。这会使它们里的符号被定义一次以上,从而出现连接错误(见上)。

解决方法:不要在header档里定义变量。你只需要在header档里声明它们然后在适当的C源码文件(应该
#include那个
header档的那个)里定义它们(一次)。对于初学者来说,定义和声明是很容易混淆的。声明的作用是告诉编译
器其所声明的符号应该存在,
并且要有所指定的类型。但是,它并不会使编译器分配贮存空间。而定义的做用是要求编译器分配贮存空间。当做
一个声明而不是做
定义的时候,在声明前放一个关键字“extern”。

例如,我们有一个叫“counter”的变量,如果想让它成为公用的,我们在一个源码程序(只在一个里面)的开始
定义它:“int
counter;”,再在相关的header档里声明它:“externintcounter;”。

函数原型里隐含着extern的意思,所以不需顾虑这个问题。

c)重复定义,重复声明,矛盾类型:
请考虑如果在一个C源码文件中#include两个档a.h和b.h,而a.h又#include了b.h档(原因是b.h
档定义了一些a.h需要的类型),会发生什么事呢?这时该C源码文件#include了b.h两次。因此每一个在
b.h中的
#define都发生了两次,每一个声明发生了两次,等等。理论上,因为它们是完全一样的拷贝,
所以应该不会有什么问题,但在实际应用上,这是不符合C的语法的,可能在编译时出现错误,或至少是警告。

解决的方法是要确定每一个header档在任一个源码文件中只被包含了一次。我们一般是用预处理器来达到这个目
的的。当我们进入每一个
header档时,我们为这个header档#define一个巨集指令。只有在这个巨集指令没有被定义的前提下,我们才
真正使用该
header档的主体。在实际应用上,我们只要简单的把下面一段码放在每一个header档的开始部分:

#ifndefFILENAME_H
#defineFILENAME_H

然后把下面一行码放在最后:

#endif

用header档的档名(大写的)代替上面的FILENAME_H,用底线代替档名中的点。有些人喜欢在#endif加上注
释来提醒他们这个
#endif指的是什么。例如:

#endif/*#ifndefFILENAME_H*/

我个人没有这个习惯,因为这其实是很明显的。当然这只是各人的风格不同,无伤大雅。

你只需要在那些有编译错误的header档中加入这个技巧,但在所有的header档中都加入也没什么损失,到底这
是个好习惯。


1.5重新编译一个多文件项目

清楚的区别编译和连接是很重要的。编译器使用源码文件来产生某种形式的目标文件(objectfiles)。在这个过程
中,外部的符号参考并
没有被解释或替换。然后我们使用连接器来连接这些目标文件和一些标准的程序包再加你指定的程序包,最后连接
生成一个可执行程序。
在这个阶段,一个目标文件中对别的文件中的符号的参考被解释,并报告不能被解释的参考,一般是以错误信息的
形式报告出来。

基本的步骤就应该是,把你的源码文件一个一个的编译成目标文件的格式,最后把所有的目标文件加上需要的程序
包连接成一个可执行文件。
具体怎么做是由你的编译器决定的。这里我只给出gcc(GNUC编译器)的有关命令,这些有可能对你的非gcc
编译器也适用。

gcc是一个多目标的工具。它在需要的时候呼叫其它的元件(预处理程序,编译器,组合程序,连接器)。具体的
哪些元件被呼叫取决于
输入文件的类型和你传递给它的开关。

一般来说,如果你只给它C源码文件,它将预处理,编译,组合所有的文件,然后把所得的目标文件连接成一个可
执行文件(一般生成的文件被命名为a.out
)。你当然可以这么做,但这会破坏很多我们把一个项目分解成多个文件所得到的好处。

如果你给它一个-c开关,gcc只把给它的文件编译成目标文件,用源码文件的文件名命名但把其后缀由“.c”或
“.cc”变成“.o”。
如果你给它的是一列目标文件,gcc会把它们连接成可执行文件,缺省文件名是a.out。你可以改变缺省名,用
开关-o后跟你指定的文件名。

因此,当你改变了一个源码文件后,你需要重新编译它:'gcc-cfilename.c'然后重新连接你的项目:'gcc-o

exec_filename*.o'。如果你改变了一个header档,你需要重新编译所有#include过这个档的源码文件,你
可以用
'gcc-cfile1.cfile2.cfile3.c'然后象上边一样连接。

当然这么做是很繁琐的,幸亏我们有些工具使这个步骤变得简单。本文的第二部分就是介绍其中的一件工具:GNU
Make工具。

(好家伙,现在才开始见真章。您学到点儿东西没?)


2)GNUMake工具
~~~~~~~~~~~~~~~~

2.1基本makefile结构

GNUMake的主要工作是读进一个文本文件,makefile。这个文件里主要是有关哪些文件(‘target’目的文
件)是从哪些别的
文件(‘dependencies’依靠文件)中产生的,用什么命令来进行这个产生过程。有了这些信息,make会检查磁
碟上的文件,如果
目的文件的时间戳(该文件生成或被改动时的时间)比至少它的一个依靠文件旧的话,make就执行相应的命令,
以便更新目的文件。
(目的文件不一定是最后的可执行档,它可以是任何一个文件。)

makefile一般被叫做“makefile”或“Makefile”。当然你可以在make的命令行指定别的文件名。如果你不特
别指定,它会寻
找“makefile”或“Makefile”,因此使用这两个名字是最简单的。

一个makefile主要含有一系列的规则,如下:

:...
(tab)<command>
(tab)<command>
.
.
.

例如,考虑以下的makefile:

===makefile开始===
myprog:foo.obar.o
gccfoo.obar.o-omyprog

foo.o:foo.cfoo.hbar.h
gcc-cfoo.c-ofoo.o

bar.o:bar.cbar.h
gcc-cbar.c-obar.o
===makefile结束===

这是一个非常基本的makefile——make从最上面开始,把上面第一个目的,‘myprog’,做为它的主要目标
(一个它需要保
证其总是最新的最终目标)。给出的规则说明只要文件‘myprog’比文件‘foo.o’或‘bar.o’中的任何一个旧,
下一行的命令将会被执行。

但是,在检查文件foo.o和bar.o的时间戳之前,它会往下查找那些把foo.o或bar.o做为目标文件的规则。
它找到的关于
foo.o的规则,该文件的依靠文件是foo.c,foo.h和bar.h。它从下面再找不到生成这些依靠文件的规则,它
就开始检查磁碟
上这些依靠文件的时间戳。如果这些文件中任何一个的时间戳比foo.o的新,命令'gcc-ofoo.ofoo.c'将会执
行,从而更新文件
foo.o。

接下来对文件bar.o做类似的检查,依靠文件在这里是文件bar.c和bar.h。

现在,make回到‘myprog’的规则。如果刚才两个规则中的任何一个被执行,myprog就需要重建(因为其中一
个.o档就会比
‘myprog’新),因此连接命令将被执行。

希望到此,你可以看出使用make工具来建立程序的好处——前一章中所有繁琐的检查步骤都由make替你做了:
检查时间戳。
你的源码文件里一个简单改变都会造成那个文件被重新编译(因为.o文件依靠.c文件),进而可执行文件被重
新连接(因为.o
文件被改变了)。其实真正的得益是在当你改变一个header档的时候——你不再需要记住那个源码文件依靠它,
因为所有的资料都在makefile里。
make会很轻松的替你重新编译所有那些因依靠这个header文件而改变了的源码文件,如有需要,再进行重新连
接。

当然,你要确定你在makefile中所写的规则是正确无误的,只列出那些在源码文件中被#include的header
档……

2.2编写make规则(Rules)

最明显的(也是最简单的)编写规则的方法是一个一个的查看源码文件,把它们的目标文件做为目的,而C源码文
件和被它#include的header
档做为依靠文件。但是你也要把其它被这些header档#include的header档也列为依靠文件,还有那些被
包括的文件所包括的文件……然后你会发现要对越来越多的文件进行管理,然后你的头发开始脱落,你的脾气开始
变坏,你的脸
色变成菜色,你走在路上开始跟电线杆子碰撞,终于你捣毁你的电脑显示器,停止编程。到低有没有些容易点儿的
方法呢?

当然有!向编译器要!在编译每一个源码文件的时候,它实在应该知道应该包括什么样的header档。使用gcc
的时候,用-M
开关,它会为每一个你给它的C文件输出一个规则,把目标文件做为目的,而这个C文件和所有应该被#include
的header文
件将做为依靠文件。注意这个规则会加入所有header文件,包括被角括号(`<',`>')和双引号(`"')所包围的文
件。其实我们可以
相当肯定系统header档(比如stdio.h,stdlib.h等等)不会被我们更改,如果你用-MM来代替-M传递给
gcc,那些用角括号包围的header档将不会被包括。(这会节省一些编译时间)

由gcc输出的规则不会含有命令部分;你可以自己写入你的命令或者什么也不写,而让make使用它的隐含的规
则(参考下面的2.4节)。

2.3Makefile变量

上面提到makefiles里主要包含一些规则。它们包含的其它的东西是变量定义。

makefile里的变量就像一个环境变量(environmentvariable)。事实上,环境变量在make过程中被解释成make

的变量。这些变量是大小写敏感的,一般使用大写字母。它们可以从几乎任何地方被引用,也可以被用来做很多
事情,比如:

i)贮存一个文件名列表。在上面的例子里,生成可执行文件的规则包含一些目标文件名做为依靠。在这个规则的
命令行里同样的那些文件被输送给gcc
做为命令参数。如果在这里使用一个变数来贮存所有的目标文件名,加入新的目标文件会变的简单而且较不易出
错。

ii)贮存可执行文件名。如果你的项目被用在一个非gcc的系统里,或者如果你想使用一个不同的编译器,你必
须将所
有使用编译器的地方改成用新的编译器名。但是如果使用一个变量来代替编译器名,那么你只需要改变一个地方,
其它所有地方的命令名就都改变了。

iii)贮存编译器旗标。假设你想给你所有的编译命令传递一组相同的选项(例如-Wall-O-g);如果你把这组
选项存
入一个变量,那么你可以把这个变量放在所有呼叫编译器的地方。而当你要改变选项的时候,你只需在一个地方改
变这个变量的内容。

要设定一个变量,你只要在一行的开始写下这个变量的名字,后面跟一个=号,后面跟你要设定的这个变量的
值。以后你要引用这个变量,写一个$
符号,后面是围在括号里的变量名。比如在下面,我们把前面的makefile利用变量重写一遍:

===makefile开始===
OBJS=foo.obar.o
CC=gcc
CFLAGS=-Wall-O-g

myprog:$(OBJS)
$(CC)$(OBJS)-omyprog

foo.o:foo.cfoo.hbar.h
$(CC)$(CFLAGS)-cfoo.c-ofoo.o

bar.o:bar.cbar.h
$(CC)$(CFLAGS)-cbar.c-obar.o
===makefile结束===

还有一些设定好的内部变量,它们根据每一个规则内容定义。三个比较有用的变量是$@,$<和$^(这些变量不
需要括号括住)。$@
扩展成当前规则的目的文件名,$<扩展成依靠列表中的第一个依靠文件,而$^扩展成整个依靠的列表(除掉了
里面所有重
复的文件名)。利用这些变量,我们可以把上面的makefile写成:

===makefile开始===
OBJS=foo.obar.o
CC=gcc
CFLAGS=-Wall-O-g

myprog:$(OBJS)
$(CC)$^-o$@

foo.o:foo.cfoo.hbar.h
$(CC)$(CFLAGS)-c$<-o$@

bar.o:bar.cbar.h
$(CC)$(CFLAGS)-c$<-o$@
===makefile结束===

你可以用变量做许多其它的事情,特别是当你把它们和函数混合使用的时候。如果需要更进一步的了解,请参考
GNUMake手册。('manmake',
'manmakefile')

2.4隐含规则(ImplicitRules)

请注意,在上面的例子里,几个产生.o文件的命令都是一样的。都是从.c文件和相关文件里产生.o文件,这
是一个标准的步骤。其实make
已经知道怎么做——它有一些叫做隐含规则的内置的规则,这些规则告诉它当你没有给出某些命令的时候,应该
怎么办。

如果你把生成foo.o和bar.o的命令从它们的规则中删除,make将会查找它的隐含规则,然后会找到一个适当
的命令。它的命令会
使用一些变量,因此你可以按照你的想法来设定它:它使用变量CC做为编译器(象我们在前面的例子),并且传
递变量CFLAGS(给C编译器,C++
编译器用CXXFLAGS),CPPFLAGS(C预处理器旗标),TARGET_ARCH(现在不用考虑这个),然后它加入旗
标'-c'
,后面跟变量$<(第一个依靠名),然后是旗标'-o'跟变量$@(目的文件名)。一个C编译的具体命令将
会是:

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

当然你可以按照你自己的需要来定义这些变量。这就是为什么用gcc的-M或-MM开关输出的码可以直接用在一
个makefile里。

2.5假象目的(PhonyTargets)

假设你的一个项目最后需要产生两个可执行文件。你的主要目标是产生两个可执行文件,但这两个文件是相互独立
的——如果一
个文件需要重建,并不影响另一个。你可以使用“假象目的”来达到这种效果。一个假象目的跟一个正常的目的几
乎是一样的,只是这个目的文件是不存在的。因此,
make总是会假设它需要被生成,当把它的依赖文件更新后,就会执行它的规则里的命令行。

如果在我们的makefile开始处输入:

all:exec1exec2

其中exec1和exec2是我们做为目的的两个可执行文件。make把这个'all'做为它的主要目的,每次执行时都
会尝试把'all'
更新。但既然这行规则里没有哪个命令来作用在一个叫'all'的实际文件(事实上all并不会在磁碟上实际产
生),所以这个规则并不真的改变
'all'的状态。可既然这个文件并不存在,所以make会尝试更新all规则,因此就检查它的依靠exec1,exec2
是否需要更新,如果需要,就把它们更新,从而达到我们的目的。

假象目的也可以用来描述一组非预设的动作。例如,你想把所有由make产生的文件删除,你可以在makefile里
设立这样一个规则:

veryclean:
rm*.o
rmmyprog

前提是没有其它的规则依靠这个'veryclean'目的,它将永远不会被执行。但是,如果你明确的使用命令'make
veryclean',
make会把这个目的做为它的主要目标,执行那些rm命令。

如果你的磁碟上存在一个叫veryclean文件,会发生什么事?这时因为在这个规则里没有任何依靠文件,所以这
个目的文件一定是
最新的了(所有的依靠文件都已经是最新的了),所以既使用户明确命令make重新产生它,也不会有任何事情发
生。解决方法是标明所有的假象目的(用
.PHONY),这就告诉make不用检查它们是否存在于磁碟上,也不用查找任何隐含规则,直接假设指定的目的需
要被更新。在makefile
里加入下面这行包含上面规则的规则:

.PHONY:veryclean

就可以了。注意,这是一个特殊的make规则,make知道.PHONY是一个特殊目的,当然你可以在它的依靠里加入
你想用的任何假象目的,而
make知道它们都是假象目的。

2.6函数(Functions)

makefile里的函数跟它的变量很相似——使用的时候,你用一个$符号跟开括号,函数名,空格后跟一列由逗号
分隔的参数,最后用关括号结束。例如,在
GNUMake里有一个叫'wildcard'的函数,它有一个参数,功能是展开成一列所有符合由其参数描述的文
件名,文件间以空格间隔。你可以像下面所示使用这个命令:

SOURCES=$(wildcard*.c)

这行会产生一个所有以'.c'结尾的文件的列表,然后存入变量SOURCES里。当然你不需要一定要把结果存入一个
变量。

另一个有用的函数是patsubst(pattensubstitude,匹配替换的缩写)函数。它需要3个参数——第一个是一
个需要匹配的
式样,第二个表示用什么来替换它,第三个是一个需要被处理的由空格分隔的字列。例如,处理那个经过上面定义
后的变量,

OBJS=$(patsubst%.c,%.o,$(SOURCES))

这行将处理所有在SOURCES字列中的字(一列文件名),如果它的结尾是'.c',就用'.o'把'.c'取代。注
意这里的%符号将匹
配一个或多个字符,而它每次所匹配的字串叫做一个‘柄’(stem)。在第二个参数里,%被解读成用第一参数所
匹配的那个柄。

2.7一个比较有效的makefile

利用我们现在所学的,我们可以建立一个相当有效的makefile。这个makefile可以完成大部分我们需要的依靠
检查,不用做太大
的改变就可直接用在大多数的项目里。

首先我们需要一个基本的makefile来建我们的程序。我们可以让它搜索当前目录,找到源码文件,并且假设它们
都是属于我们的项目的,放进一个叫
SOURCES的变量。这里如果也包含所有的*.cc文件,也许会更保险,因为源码文件可能是C++码的。

SOURCES=$(wildcard*.c*.cc)

利用patsubst,我们可以由源码文件名产生目标文件名,我们需要编译出这些目标文件。如果我们的源码文件既
有.c文件,也有.cc
文件,我们需要使用相嵌的patsubst函数呼叫:

OBJS=$(patsubst%.c,%.o,$(patsubst%.cc,%.o,$(SOURCES)))

最里面一层patsubst的呼叫会对.cc文件进行后缀替代,产生的结果被外层的patsubst呼叫处理,进行对.c
文件后缀的替代。

现在我们可以设立一个规则来建可执行文件:

myprog:$(OBJS)
gcc-omyprog$(OBJS)

进一步的规则不一定需要,gcc已经知道怎么去生成目标文件(objectfiles)。下面我们可以设定产生依靠信息
的规则:

depends:$(SOURCES)
gcc-M$(SOURCES)>depends

在这里如果一个叫'depends'的文件不存在,或任何一个源码文件比一个已存在的depends文件新,那么一个
depends文件会被生
成。depends文件将会含有由gcc产生的关于源码文件的规则(注意-M开关)。现在我们要让make把这些规
则当做makefile档
的一部分。这里使用的技巧很像C语言中的#include系统——我们要求make把这个文件include到
makefile里,如下:

includedepends

GNUMake看到这个,检查'depends'目的是否更新了,如果没有,它用我们给它的命令重新产生depends档。
然后它会把这组(新)
规则包含进来,继续处理最终目标'myprog'。当看到有关myprog的规则,它会检查所有的目标文件是否更
新——利用depends文件
里的规则,当然这些规则现在已经是更新过的了。

这个系统其实效率很低,因为每当一个源码文件被改动,所有的源码文件都要被预处理以产生一个新的'depends'
文件。而且它也不是100%
的安全,这是因为当一个header档被改动,依靠信息并不会被更新。但就基本工作来说,它也算相当有用的了。

2.8一个更好的makefile

这是一个我为我大多数项目设计的makefile。它应该可以不需要修改的用在大部分项目里。我主要把它用在
djgpp上,那是一个DOS版的
gcc编译器。因此你可以看到执行的命令名、'alleg'程序包、和RM-F变量都反映了这一点。

===makefile开始===

######################################
#
#Genericmakefile
#
#byGeorgeFoot
#email:george.foot@merton.ox.ac.uk
#
#Copyright(c)1997GeorgeFoot
#Allrightsreserved.
#保留所有版权
#
#Nowarranty,noliability;
#youusethisatyourownrisk.
#没保险,不负责
#你要用这个,你自己担风险
#
#Youarefreetomodifyand
#distributethiswithoutgiving
#credittotheoriginalauthor.
#你可以随便更改和散发这个文件
#而不需要给原作者什么荣誉。
#(你好意思?)
#
######################################

###Customising
#用户设定
#
#Adjustthefollowingifnecessary;EXECUTABLEisthetarget
#executable'sfilename,andLIBSisalistoflibrariestolinkin
#(e.g.alleg,stdcx,iostr,etc).Youcanoverridetheseonmake's
#commandlineofcourse,ifyouprefertodoitthatway.
#
#如果需要,调整下面的东西。EXECUTABLE是目标的可执行文件名,LIBS
#是一个需要连接的程序包列表(例如alleg,stdcx,iostr等等)。当然你
#可以在make的命令行覆盖它们,你愿意就没问题。
#

EXECUTABLE:=mushroom.exe
LIBS:=alleg

#Nowalteranyimplicitrules'variablesifyoulike,e.g.:
#
#现在来改变任何你想改动的隐含规则中的变量,例如

CFLAGS:=-g-Wall-O3-m486
CXXFLAGS:=$(CFLAGS)

#Thenextbitcheckstoseewhetherrmisinyourdjgppbin
#directory;ifnotitusesdelinstead,butthiscancause(harmless)
#`Filenotfound'errormessages.IfyouarenotusingDOSatall,
#setthevariabletosomethingwhichwillunquestioninglyremove
#files.
#
#下面先检查你的djgpp命令目录下有没有rm命令,如果没有,我们使用
#del命令来代替,但有可能给我们'Filenotfound'这个错误信息,这没
#什么大碍。如果你不是用DOS,把它设定成一个删文件而不废话的命令。
#(其实这一步在UNIX类的系统上是多余的,只是方便DOS用户。UNIX
#用户可以删除这5行命令。)

ifneq($(wildcard$(DJDIR)/bin/rm.exe),)
RM-F:=rm-f
else
RM-F:=del
endif

#Youshouldn'tneedtochangeanythingbelowthispoint.
#
#从这里开始,你应该不需要改动任何东西。(我是不太相信,太NB了!)

SOURCE:=$(wildcard*.c)$(wildcard*.cc)
OBJS:=$(patsubst%.c,%.o,$(patsubst%.cc,%.o,$(SOURCE)))
DEPS:=$(patsubst%.o,%.d,$(OBJS))
MISSING_DEPS:=$(filter-out$(wildcard$(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES:=$(wildcard$(patsubst%.d,%.c,$(MISSING_DEPS))\
$(patsubst%.d,%.cc,$(MISSING_DEPS)))
CPPFLAGS+=-MD

.PHONY:everythingdepsobjscleanverycleanrebuild

everything:$(EXECUTABLE)

deps:$(DEPS)

objs:$(OBJS)

clean:
@$(RM-F)*.o
@$(RM-F)*.d

veryclean:clean
@$(RM-F)$(EXECUTABLE)

rebuild:verycleaneverything

ifneq($(MISSING_DEPS),)
$(MISSING_DEPS):
@$(RM-F)$(patsubst%.d,%.o,$@)
endif

-include$(DEPS)

$(EXECUTABLE):$(OBJS)
gcc-o$(EXECUTABLE)$(OBJS)$(addprefix-l,$(LIBS))

===makefile结束===

有几个地方值得解释一下的。首先,我在定义大部分变量的时候使用的是:=而不是=符号。它的作用是立即把
定义中参考到的函数和变量都展开了。如果使用
=的话,函数和变量参考会留在那儿,就是说改变一个变量的值会导致其它变量的值也被改变。例如:

A=foo
B=$(A)
#现在B是$(A),而$(A)是'foo'。
A=bar
#现在B仍然是$(A),但它的值已随着变成'bar'了。
B:=$(A)
#现在B的值是'bar'。
A=foo
#B的值仍然是'bar'。

make会忽略在#符号后面直到那一行结束的所有文字。

ifneg...else...endif系统是makefile里让某一部分码有条件的失效/有效的工具。ifeq
使用两个参数,如果它们相同,它把直到else(或者endif,如果没有else的话)的一段码加进makefile
里;如果不同,把
else到endif间的一段码加入makefile(如果有else)。ifneq的用法刚好相反。

'filter-out'函数使用两个用空格分开的列表,它把第二列表中所有的存在于第一列表中的项目删除。我用它来
处理DEPS列表,把所
有已经存在的项目都删除,而只保留缺少的那些。

我前面说过,CPPFLAGS存有用于隐含规则中传给预处理器的一些旗标。而-MD开关类似-M开关,但是从源码
文件.c或.cc中
形成的文件名是使用后缀.d的(这就解释了我形成DEPS变量的步骤)。DEPS里提到的文件后来用'-include'
加进了
makefile里,它隐藏了所有因文件不存在而产生的错误信息。

如果任何依靠文件不存在,makefile会把相应的.o文件从磁碟上删除,从而使得make重建它。因为
CPPFLAGS指定了-MD,
它的.d文件也被重新产生。

最后,'addprefix'函数把第二个参数列表的每一项前缀上第一个参数值。

这个makefile的那些目的是(这些目的可以传给make的命令行来直接选用):

everything:(预设)更新主要的可执行程序,并且为每一个源码文件生成或更新一个'.d'文件和一个'.o'文
件。

deps:只是为每一个源码程序产生或更新一个'.d'文件。

objs:为每一个源码程序生成或更新'.d'文件和目标文件。

clean:删除所有中介/依靠文件(*.d和*.o)。

veryclean:做`clean'和删除可执行文件。

rebuild:先做`veryclean'然后`everything';既完全重建。

除了预设的everything以外,这里头只有clean,veryclean,和rebuild对用户是有意义的。

我还没有发现当给出一个源码文件的目录,这个makefile会失败的情况,除非依靠文件被弄乱。如果这种弄乱的
情况发生了,只要输入`make
clean',所有的目标文件和依靠文件会被删除,问题就应该被解决了。当然,最好不要把它们弄乱。如果你发现
在某种情况下这个makefile
文件不能完成它的工作,请告诉我,我会把它整好的。


3总结
~~~~~~~~~~~~~~~

我希望这篇文章足够详细的解释了多文件项目是怎么运作的,也说明了怎样安全而合理的使用它。到此,你应该可
以轻松的利用GNUMake工
具来管理小型的项目,如果你完全理解了后面几个部分的话,这些对于你来说应该没什么困难。

GNUMake是一件强大的工具,虽然它主要是用来建立程序,它还有很多别的用处。如果想要知道更多有关这个工
具的知识,它的句法,函数,
和许多别的特点,你应该参看它的参考文件(infopages,别的GNU工具也一样,看它们的infopages.)。



CScene官方网站:http://cscene.differnet.org
CScene官方电邮:cscene@mindless.com


ThispageisCopyright?1997ByCScene.AllRightsReserved

admin@studio.openunix.org


--
※ 修改:.xian 于 May  2 16:11:31 修改本文.[FROM: 202.118.239.115]
※ 来源:.紫 丁 香 bbs.hit.edu.cn.[FROM: 202.118.239.115]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:215.325毫秒