
翻译: 哈少
译者按: 本文是一篇介绍 GNU Make 的文章,读完后读者应该基本掌控了 make 的用法。而 make 是任何想在 Unix
(当然也包括 Linux )系统上编程的用户必须掌控的工具。假如您写的程式中没有用到 make
,则说明您写的程式只是个人的练习程式,不具备任何实用的价值。也许这么说有点 儿偏激,但 make
实在是应该用在任何稍具规模的程式中的。希望本文能够为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux
用户除了学会安装红帽子以外, 实在应该尝试写一些有用的程式。个人想法,大家参考。
C-Scene 题目 #2
多文档项目和 GNU Make 工具
作者: 乔治富特 (Goerge Foot)
电子邮件: george.foot@merton.ox.ac.uk
Occupation: Student at Merton College, Oxford University, England
职业:学生,默尔顿学院,牛津城大学,英格兰
IRC匿名: gfoot
拒绝承诺:作者对于任何因此而对任何事物造成的任何损害(您所拥有或不
拥有的实际的,抽象的,或虚拟的)。任何的损坏都是您自己的责任,而
和我无关。
任何权: “多文档项目”部分属于作者的财产,版权归乔治富特1997年
五月至七月。其他部分属 CScene 财产,版权 CScene 1997年,保留任何
版权。本 CScene 文章的分发,部分或全部,应依照任何其他 CScene 的文章
的条件来处理。
0) 介绍
~~~~~~~~~~~~~~~
本文将首先介绍为什么要将您的C源代码分离成几个合理的单独档案,什么时
候需要分,怎么才能分的好。然后将会告诉您 GNU Make 怎样使您的编译和连
接步骤自动化。对于其他 Make 工具的用户来说,虽然在用其他类似工具时要
做适当的调整,本文的内容仍然是很有用的。假如对您自己的编程工具备怀
疑,能够实际的试一试,但请先阅读用户手册。
1) 多文档项目
~~~~~~~~~~~~~~~~~~~~~~
1.1为什么使用他们?
首先,多文档项目的好处在那里呢?
他们看起来把事情弄的复杂无比。又要 header 文档,又要 extern 声明,而且假如需要查找一个文档,您要在更多的文档里搜索。
但其实我们有很有力的理由支持我们把一个项目分解成小块。当您改
动一行代码,编译器需要全部重新编译来生成一个新的可执行文档。
但假如您的项目是分开在几个小文档里,当您改变其中一个文档的时
候,别的源文档的目标文档(object files)已存在,所以没有什么
原因去重新编译他们。您所需要做的只是重现编译被改变过的那个文
件,然后重新连接任何的目标文档罢了。在大型的项目中,这意味着
从很长的(几分钟到几小时)重新编译缩短为十几,二十几秒的简单
调整。
只要通过基本的规划,将一个项目分解成多个小文档可使您更加容易
的找到一段代码。很简单,您根据代码的作用把您的代码分解到不同
的文档里。当您要看一段代码时,您能够准确的知道在那个文档中去
寻找他。
从很多目标文档生成一个程式包 (Library)比从一个单一的大目标文档
生成要好的多。当然实际上这是否真是个优势则是由您所用的系统
来决定的。但是当使用 gcc/ld (一个 GNU C 编译/连接器) 把一个程
序包连接到一个程式时,在连接的过程中,他会尝试不去连接没有使
用到的部分。但他每次只能从程式包中把一个完整的目标文档排除在
外。因此假如您参考一个程式包中某一个目标档中任何一个符号的话,
那么这个目标文档整个都会被连接进来。要是个程式包被很充分
的分解了的话,那么经连接后,得到的可执行文档会比从一个大目标
文档组成的程式包连接得到的文档小得多。
又因为您的程式是很模块化的,文档之间的共享部分被减到最少,那
就有很多好处??能够很容易的追踪到臭虫,这些模块经常是能够用
在其他的项目里的,同时别人也能够更容易的理解您的一段代码是干
什么的。当然此外更有许多别的好处……
1.2 何时分解您的项目
很明显,把任何东西都分解是不合理的。象“世界,您们好”这样的
简单程式根本就不能分,因为实在也没什么可分的。把用于测试用的
小程式分解也是没什么意思的。但一般来说,当分解项目有助于布局、
发展和易读性的时候,我都会采取他。在大多数的情况下,这都是适
用的。(所谓“世界,您们好”,既 ’hello world’ ,只是个介
绍一种编程语言时惯用的范例程式,他会在屏幕上显示一行 ’hello
world’ 。是最简单的程式。)
假如您需要研发一个相当大的项目,在开始前,应该考虑一下您将
怎样实现他,并且生成几个文档(用适当的名字)来放您的代码。
当然,在您的项目研发的过程中,您能够建立新的文档,但假如您
这么做的话,说明您可能改变了当初的想法,您应该想想是否需要
对整体结构也进行相应的调整。
对于中型的项目,您当然也能够采用上述技巧,但您也能够就那么开
始输入您的代码,当您的码多到难以管理的时候再把他们分解成不同
的档案。但以我的经验来说,开始时在脑子里形成一个大概的方案,
并且尽量遵从他,或在研发过程中,随着程式的需要而修改,会使开
发变得更加容易。
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 档里声明他:“extern int
counter;”。
函数原型里隐含着 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 档的开始部分:
#ifndef FILENAME_H
#define FILENAME_H
然后把下面一行码放在最后:
#endif
用 header 档的档名(大写的)代替上面的 FILENAME_H,用底线
代替档名中的点。有些人喜欢在 #endif 加上注释来提醒他们这个
#endif 指的是什么。例如:
#endif /* #ifndef FILENAME_H */
我个人没有这个习惯,因为这其实是很明显的。当然这只是各人的
风格不同,无伤大雅。
您只需要在那些有编译错误的 header 档中加入这个技巧,但在所
有的 header 档中都加入也没什么损失,到底这是个好习惯。
1.5 重新编译一个多文档项目
清楚的区分编译和连接是很重要的。编译器使用源码文档来产生某种
形式的目标文档(object files)。在这个过程中,外部的符号参考并
没有被解释或替换。然后我们使用连接器来连接这些目标文档和一些
标准的程式包再加您指定的程式包,最后连接生成一个可执行程式。
在这个阶段,一个目标文档中对别的文档中的符号的参考被解释,并
报告不能被解释的参考,一般是以错误信息的形式报告出来。
基本的步骤就应该是,把您的源码文档一个一个的编译成目标文档的格
式,最后把任何的目标文档加上需要的程式包连接成一个可执行文档。
具体怎么做是由您的编译器决定的。这里我只给出 gcc (GNU C 编译
器)的有关命令,这些有可能对您的非 gcc 编译器也适用。
gcc 是个多目标的工具。他在需要的时候呼叫其他的元件(预处理
程式,编译器,组合程式,连接器)。具体的哪些元件被呼叫取决于
输入文档的类型和您传递给他的开关。
一般来说,假如您只给他C源码文档,他将预处理,编译,组合任何
的文档,然后把所得的目标文档连接成一个可执行文档(一般生成的
文档被命名为 a.out )。您当然能够这么做,但这会破坏很多我们
把一个项目分解成多个文档所得到的好处。
假如您给他一个 -c 开关,gcc 只把给他的文档编译成目标文档,
用源码文档的文档名命名但把其后缀由“.c”或“.cc”变成“.o”。
假如您给他的是一列目标文档, gcc 会把他们连接成可执行文档,
缺省文档名是 a.out 。您能够改变缺省名,用开关 -o 后跟您指定
的文档名。
因此,当您改变了一个源码文档后,您需要重新编译他: ’gcc -c
filename.c’ 然后重新连接您的项目: ’gcc -o exec_filename *.o’。
假如您改变了一个 header 档,您需要重新编译任何 #include 过
这个档的源码文档,您能够用 ’gcc -c file1.c file2.c file3.c’
然后象上边相同连接。
当然这么做是很繁琐的,幸亏我们有些工具使这个步骤变得简单。
本文的第二部分就是介绍其中的一件工具:GNU Make 工具。
(好家伙,现在才开始见真章。您学到点儿东西没?)
2) GNU Make 工具
~~~~~~~~~~~~~~~~
2.1 基本 makefile 结构
GNU Make 的主要工作是读进一个文本文档, makefile 。这个文
件里主要是有关哪些文档(‘target’目的文档)是从哪些别的
文档(‘dependencies’依靠文档)中产生的,用什么命令来进行
这个产生过程。有了这些信息, make 会检查磁碟上的文档,假如
目的文档的时间戳(该文档生成或被改变时的时间)比至少他的一
个依靠文档旧的话, make 就执行相应的命令,以便更新目的文档。
(目的文档不一定是最后的可执行档,他能够是任何一个文档。)
makefile 一般被叫做“makefile”或“Makefile”。当然您能够
在 make 的命令行指定别的文档名。假如您不特别指定,他会寻
找“makefile”或“Makefile”,因此使用这两个名字是最简单
的。
一个 makefile 主要含有一系列的规则,如下:
: ...
(tab)
(tab)
.
.
.
例如,考虑以下的 makefile :
=== makefile 开始 ===
myprog : foo.o bar.o
gcc foo.o bar.o -o myprog
foo.o : foo.c foo.h bar.h
gcc -c foo.c -o foo.o
bar.o : bar.c bar.h
gcc -c bar.c -o bar.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 -o foo.o foo.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.3 Makefile 变量
上面提到 makefiles 里主要包含一些规则。他们包含的其他的东
西是变量定义。
makefile 里的变量就像一个环境变量(environment variable)。
事实上,环境变量在 make 过程中被解释成 make 的变量。这些
变量是大小写敏感的,一般使用大写字母。他们能够从几乎任何
地方被引用,也能够被用来做很多事情,比如:
i) 贮存一个文档名列表。在上面的例子里,生成可执行文档的
规则包含一些目标文档名做为依靠。在这个规则的命令行
里同样的那些文档被输送给 gcc 做为命令参数。假如在这
里使用一个变数来贮存任何的目标文档名,加入新的目标
文档会变的简单而且较不易出错。
ii) 贮存可执行文档名。假如您的项目被用在一个非 gcc 的系
统里,或假如您想使用一个不同的编译器,您必须将所
有使用编译器的地方改成用新的编译器名。但是假如使用一
个变量来代替编译器名,那么您只需要改变一个地方,其
他任何地方的命令名就都改变了。
iii) 贮存编译器旗标。假设您想给您任何的编译命令传递一组
相同的选项(例如 -Wall -O -g);假如您把这组选项存
入一个变量,那么您能够把这个变量放在任何呼叫编译器
的地方。而当您要改变选项的时候,您只需在一个地方改
变这个变量的内容。
要设定一个变量,您只要在一行的开始写下这个变量的名字,后
面跟一个 = 号,后面跟您要设定的这个变量的值。以后您要引用
这个变量,写一个 $ 符号,后面是围在括号里的变量名。比如在
下面,我们把前面的 makefile 利用变量重写一遍:
=== makefile 开始 ===
OBJS = foo.o bar.o
CC = gcc
CFLAGS = -Wall -O -g
myprog : $(OBJS)
$(CC) $(OBJS) -o myprog
foo.o : foo.c foo.h bar.h
$(CC) $(CFLAGS) -c foo.c -o foo.o
bar.o : bar.c bar.h
$(CC) $(CFLAGS) -c bar.c -o bar.o
=== makefile 结束 ===
更有一些设定好的内部变量,他们根据每一个规则内容定义。三个
比较有用的变量是 $@, $< 和 $^ (这些变量无需括号括住)。
$@ 扩展成当前规则的目的文档名, $< 扩展成依靠列表中的第
一个依靠文档,而 $^ 扩展成整个依靠的列表(除掉了里面任何重
复的文档名)。利用这些变量,我们能够把上面的 makefile 写成:
=== makefile 开始 ===
OBJS = foo.o bar.o
CC = gcc
CFLAGS = -Wall -O -g
myprog : $(OBJS)
$(CC) $^ -o $@
foo.o : foo.c foo.h bar.h
$(CC) $(CFLAGS) -c $< -o $@
bar.o : bar.c bar.h
$(CC) $(CFLAGS) -c $< -o $@
=== makefile 结束 ===
您能够用变量做许多其他的事情,特别是当您把他们和函数混合
使用的时候。假如需要更进一步的了解,请参考 GNU Make 手册。
(’man make’, ’man makefile’)
2.4 隐含规则 (Implicit Rules)
请注意,在上面的例子里,几个产生 .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 假象目的 (Phony Targets)
假设您的一个项目最后需要产生两个可执行文档。您的主要目标
是产生两个可执行文档,但这两个文档是相互单独的??假如一
个文档需要重建,并不影响另一个。您能够使用“假象目的”来
达到这种效果。一个假象目的跟一个正常的目的几乎是相同的,
只是这个目的文档是不存在的。因此, make 总是会假设他需要
被生成,当把他的依赖文档更新后,就会执行他的规则里的命令
行。
假如在我们的 makefile 开始处输入:
all : exec1 exec2
其中 exec1 和 exec2 是我们做为目的的两个可执行文档。 make
把这个 ’all’ 做为他的主要目的,每次执行时都会尝试把 ’all’
更新。但既然这行规则里没有哪个命令来作用在一个叫 ’all’ 的
实际文档(事实上 all 并不会在磁碟上实际产生),所以这个规
则并不真的改变 ’all’ 的状态。可既然这个文档并不存在,所以
make 会尝试更新 all 规则,因此就检查他的依靠 exec1, exec2
是否需要更新,假如需要,就把他们更新,从而达到我们的目的。
假象目的也能够用来描述一组非预设的动作。例如,您想把任何由
make 产生的文档删除,您能够在 makefile 里设立这样一个规则:
veryclean :
rm *.o
rm myprog
前提是没有其他的规则依靠这个 ’veryclean’ 目的,他将永远
不会被执行。但是,假如您明确的使用命令 ’make veryclean’ ,
make 会把这个目的做为他的主要目标,执行那些 rm 命令。
假如您的磁碟上存在一个叫 veryclean 文档,会发生什么事?这
时因为在这个规则里没有任何依靠文档,所以这个目的文档一定是
最新的了(任何的依靠文档都已是最新的了),所以既使用户明
确命令 make 重新产生他,也不会有任何事情发生。解决方法是标
明任何的假象目的(用 .PHONY),这就告诉 make 不用检查他们
是否存在于磁碟上,也不用查找任何隐含规则,直接假设指定的目
的需要被更新。在 makefile 里加入下面这行包含上面规则的规则:
.PHONY : veryclean
就能够了。注意,这是个特别的 make 规则,make 知道 .PHONY
是个特别目的,当然您能够在他的依靠里加入您想用的任何假象
目的,而 make 知道他们都是假象目的。
2.6 函数 (Functions)
makefile 里的函数跟他的变量很相似??使用的时候,您用一个
$ 符号跟开括号,函数名,空格后跟一列由逗号分隔的参数,最后
用关括号结束。例如,在 GNU Make 里有一个叫 ’wildcard’ 的函
数,他有一个参数,功能是展开成一列任何符合由其参数描述的文
件名,文档间以空格间隔。您能够像下面所示使用这个命令:
SOURCES = $(wildcard *.c)
这行会产生一个任何以 ’.c’ 结尾的文档的列表,然后存入变量
SOURCES 里。当然您无需一定要把结果存入一个变量。
另一个有用的函数是 patsubst ( patten substitude, 匹配替
换的缩写)函数。他需要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 -o myprog $(OBJS)
进一步的规则不一定需要, gcc 已知道怎么去生成目标文档
(object files) 。下面我们能够设定产生依靠信息的规则:
depends : $(SOURCES)
gcc -M $(SOURCES) > depends
在这里假如一个叫 ’depends’ 的文档不存在,或任何一个源码文档
比一个已存在的 depends 文档新,那么一个 depends 文档会被生
成。depends 文档将会含有由 gcc 产生的关于源码文档的规则(注
意 -M 开关)。现在我们要让 make 把这些规则当做 makefile 档
的一部分。这里使用的技巧很像 C 语言中的 #include 系统??我
们需要 make 把这个文档 include 到 makefile 里,如下:
include depends
GNU Make 看到这个,检查 ’depends’ 目的是否更新了,假如没有,
他用我们给他的命令重新产生 depends 档。然后他会把这组(新)
规则包含进来,继续处理最终目标 ’myprog’ 。当看到有关 myprog
的规则,他会检查任何的目标文档是否更新??利用 depends 文档
里的规则,当然这些规则现在已是更新过的了。
这个系统其实效率很低,因为每当一个源码文档被改变,任何的源码
文档都要被预处理以产生一个新的 ’depends’ 文档。而且他也不是
100% 的安全,这是因为当一个 header 档被改变,依靠信息并不会
被更新。但就基本工作来说,他也算相当有用的了。
2.8 一个更好的 makefile
这是个我为我大多数项目设计的 makefile 。他应该能够无需修
改的用在大部分项目里。我主要把他用在 djgpp 上,那是个 DOS
版的 gcc 编译器。因此您能够看到执行的命令名、 ’alleg’ 程式包、
和 RM -F 变量都反映了这一点。
=== makefile 开始 ===
######################################
#
# Generic makefile
#
# by George Foot
# email: george.foot@merton.ox.ac.uk
#
# Copyright (c) 1997 George Foot
# All rights reserved.
# 保留任何版权
#
# No warranty, no liability;
# you use this at your own risk.
# 没保险,不负责
# 您要用这个,您自己担风险
#
# You are free to modify and
# distribute this without giving
# credit to the original author.
# 您能够随便更改和散发这个文档
# 而无需给原作者什么荣誉。
# (您好意思?)
#
######################################
### Customising
# 用户设定
#
# Adjust the following if necessary; EXECUTABLE is the target
# executable’s filename, and LIBS is a list of libraries to link in
# (e.g. alleg, stdcx, iostr, etc). You can override these on make’s
# command line of course, if you prefer to do it that way.
#
# 假如需要,调整下面的东西。 EXECUTABLE 是目标的可执行文档名, LIBS
# 是个需要连接的程式包列表(例如 alleg, stdcx, iostr 等等)。当然您
# 能够在 make 的命令行覆盖他们,您愿意就没问题。
#
EXECUTABLE := mushroom.exe
LIBS := alleg
# Now alter any implicit rules’ variables if you like, e.g.:
#
# 现在来改变任何您想改变的隐含规则中的变量,例如
CFLAGS := -g -Wall -O3 -m486
CXXFLAGS := $(CFLAGS)
# The next bit checks to see whether rm is in your djgpp bin
# directory; if not it uses del instead, but this can cause (harmless)
# `File not found’ error messages. If you are not using DOS at all,
# set the variable to something which will unquestioningly remove
# files.
#
# 下面先检查您的 djgpp 命令目录下有没有 rm 命令,假如没有,我们使用
# del 命令来代替,但有可能给我们 ’File not found’ 这个错误信息,这没
# 什么大碍。假如您不是用 DOS ,把他设定成一个删文档而不废话的命令。
# (其实这一步在 UNIX 类的系统上是多余的,只是方便 DOS 用户。 UNIX
# 用户能够删除这5行命令。)
ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)
RM-F := rm -f
else
RM-F := del
endif
# You shouldn’t need to change anything below this point.
#
# 从这里开始,您应该无需改变任何东西。(我是不太相信,太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 : everything deps objs clean veryclean rebuild
everything : $(EXECUTABLE)
deps : $(DEPS)
objs : $(OBJS)
clean :
@$(RM-F) *.o
@$(RM-F) *.d
veryclean: clean
@$(RM-F) $(EXECUTABLE)
rebuild: veryclean everything
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 总结
~~~~~~~~~~~~~~~
我希望这篇文章足够周详的解释了多文档项目是怎么运作的,也说明了
怎样安全而合理的使用他。到此,您应该能够轻松的利用 GNU Make 工
具来管理小型的项目,假如您完全理解了后面几个部分的话,这些对于
您来说应该没什么困难。
GNU Make 是一件强大的工具,虽然他主要是用来建立程式,他更有很多
别的用处。假如想要知道更多有关这个工具的知识,他的句法,函数,
和许多别的特点,您应该参看他的参考文档 (info pages, 别的 GNU
工具也相同,看他们的 info pages. )。
C Scene 官方网站: http://cscene.differnet.org
C Scene 官方电邮: cscene@mindless.com
This page is Copyright ? 1997 By C Scene. All Rights Reserved
|