首页 > 学院 > 开发设计 > 正文

关于makefile

2019-11-17 05:15:06
字体:
来源:转载
供稿:网友

  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 主要含有一系列的规则,如下:
例如,考虑以下的 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. )。


文章摘要:
  无论是在linux还是在Unix环境中,make都是一个非常重要的编译命令。不管是自己进行项
目开发还是安装应用软件,我们都经常要用到make或make install,有效的利用make和makefile
工具可以大大提高项目开发的效率。但令人遗憾的是,在许多讲述linux应用的书籍上都没有具体
介绍这个功能强大但又非常复杂的编译工具。在这里我就向大家具体介绍一下make及其描述文件makefile。
<http://member.netease.com/%7Ehalon/unixfaq/unix_31.htm>

正文:  
linux/Unix环境下的make和makefile详解  
Pathetique

  无论是在linux还是在Unix环境中,make都是一个非常重要的编译命令。不管是自己进行项
目开发还是安装应用软件, 我们都经常要用到make或make install。利用make工具,我们可以
将大型的开发项目分解成为多个更易于治理的模块, 对于一个包括几百个源文件的应用程序,
使用make和makefile工具就可以简洁明快地理顺各个源文件之间纷繁复杂的相互关系。 而且如
此多的源文件,假如每次都要键入gcc命令进行编译的话,那对程序员来说简直就是一场灾难。
而make工具则可自动完成编译工作,并且可以只对程序员在上次编译后修改过的部分进行编译。

因此,有效的利用make和makefile工具可以大大提高项目开发的效率。同时把握make和makefile
之后,您也不会再面对着linux下的应用软件手足无措了。
  但令人遗憾的是,在许多讲述linux应用的书籍上都没有具体介绍这个功能强大但又非常复
杂的编译工具。在这里我就向大家具体介绍一下make及其描述文件makefile。

Makefile文件

  Make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互关系并自
动维护编译工作。而makefile文件需要按照某种语法进行编写,文件中需要说明如何编译各个源
文件并连接生成可执行文件,并要求定义源文件之间的依靠关系。makefile文件是许多编译器--
包括 Windows NT 下的编译器--维护编译信息的常用方法,只是在集成开发环境中,用户通过友
好的界面修改 makefile 文件而已。
  在UNIX系统中,习惯使用Makefile作为makfile文件。假如要使用其他文件作为 makefile,
则可利用类似下面的 make 命令选项指定 makefile 文件:
  $ make -f Makefile.debug
  例如,一个名为prog的程序由三个C源文件filea.c、fileb.c和filec.c以及库文件LS编译生
成,这三个文件还分别包含自己的头文件a.h 、b.h和c.h。通常情况下, C编译器将会输出三个
目标文件filea.o、fileb.o和filec.o。 假设filea.c和fileb.c都要声明用到一个名为defs的文
件,但filec.c不用。即在filea.c和fileb.c里都有这样的声明:
  #include "defs"
  那么下面的文档就描述了这些文件之间的相互联系:
  ---------------------------------------------------------
   #It is a example for describing makefile
   prog : filea.o fileb.o filec.o
   cc filea.o fileb.o filec.o -LS -o prog
   filea.o : filea.c a.h defs
   cc -c filea.c
   fileb.o : fileb.c b.h defs
   cc -c fileb.c
   filec.o : filec.c c.h
   cc -c filec.c
  ----------------------------------------------------------
  这个描述文档就是一个简单的makefile文件。
  从上面的例子注重到,第一个字符为 # 的行为注释行。第一个非注释行指定prog由三个目
标文件filea.o、fileb.o和filec.o链接生成。第三行描述了如何从prog所依靠的文件建立可执
行文件。接下来的4、6、8行分别指定三个目标文件,以及它们所依靠的.c和.h文件以及defs文
件。而5、7、9行则指定了如何从目标所依靠的文件建立目标。
  当filea.c或a.h文件在编译之后又被修改,则 make 工具可自动重新编译filea.o,假如在
前后两次编译之间,filea.C 和a.h 均没有被修改,而且 test.o 还存在的话, 就没有必要重
新编译。这种依靠关系在多源文件的程序编译中尤其重要。通过这种依靠关系的定义,make 工
具可避免许多不必要的编译工作。当然,利用 Shell 脚本也可以达到自动编译的效果,但是,
Shell 脚本将全部编译任何源文件,包括哪些不必要重新编译的源文件,而 make 工具则可根据
目标上一次编译的时间和目标所依靠的源文件的更新时间而自动判定应当编译哪个源文件。
Makefile文件作为一种描述文档一般需要包含以下内容:
  ◆ 宏定义
  ◆ 源文件之间的相互依靠关系
  ◆ 可执行的命令
  Makefile中答应使用简单的宏指代源文件及其相关编译信息,在linux中也称宏为变量。在
引用宏时只需在变量前加$符号,但值得注重的是,假如变量名的长度超过一个字符,在引用时
就必须加圆括号()。
  下面都是有效的宏引用:
  $(CFLAGS)
  $2
  $Z
  $(Z)
  其中最后两个引用是完全一致的。
  需要注重的是一些宏的预定义变量,在Unix系统中,$*、$@、$?和$<四个非凡宏的值在执行
命令的过程中会发生相应的变化,而在GNU make中则定义了更多的预定义变量。关于预定义变量
的具体内容,
  宏定义的使用可以使我们脱离那些冗长乏味的编译选项,为编写makefile文件带来很大的方便。
  ---------------------------------------------------------
   # Define a macro for the object files
   OBJECTS= filea.o fileb.o filec.o

   # Define a macro for the library file
   LIBES= -LS

   # use macros rewrite makefile
   prog: $(OBJECTS)
   cc $(OBJECTS) $(LIBES) -o prog
   ……
  ---------------------------------------------------------
  此时假如执行不带参数的make命令,将连接三个目标文件和库文件LS;但是假如在make命令后带有新的宏定义:
  make "LIBES= -LL -LS"
则命令行后面的宏定义将覆盖makefile文件中的宏定义。若LL也是库文件,此时make命令将

连接三个目标文件以及两个库文件LS和LL。
  在Unix系统中没有对常量NULL作出明确的定义,因此我们要定义NULL字符串时要使用下述宏定义:
  STRINGNAME=

Make命令

  在make命令后不仅可以出现宏定义,还可以跟其他命令行参数,这些参数指定了需要编译
的目标文件。其标准形式为:
  target1 [target2 …]:[:][dependent1 …][;commands][#…]
  [(tab) commands][#…]
  方括号中间的部分表示可选项。Targets和dependents当中可以包含字符、数字、句点和"/"
符号。除了引用,commands中不能含有"#",也不答应换行。
  在通常的情况下命令行参数中只含有一个":",此时command序列通常和makefile文件中某些
定义文件间依靠关系的描述行有关。假如与目标相关连的那些描述行指定了相关的command序列,
那么就执行这些相关的command命令,即使在分号和(tab)后面的aommand字段甚至有可能是NULL。
假如那些与目标相关连的行没有指定command,那么将调用系统默认的目标文件生成规则。
  假如命令行参数中含有两个冒号"::",则此时的command序列也许会和makefile中所有描述
文件依靠关系的行有关。此时将执行那些与目标相关连的描述行所指向的相关命令。同时还将执
行build-in规则。
  假如在执行command命令时返回了一个非"0"的出错信号,例如makefile文件中出现了错误的
目标文件名或者出现了以连字符打头的命令字符串,make操作一般会就此终止,但假如make后带
有"-i"参数,则make将忽略此类出错信号。
  Make命本身可带有四种参数:标志、宏定义、描述文件名和目标文件名。其标准形式为:
  Make [flags] [macro definitions] [targets]
  Unix系统下标志位flags选项及其含义为:
  -f file  指定file文件为描述文件,假如file参数为"-"符,那么描述文件指向标准输入。
假如没有"-f"参数,则系统将默认当前目录下名为makefile或者名为Makefile的文件为描述文件。
在linux中, GNU make 工具在当前工作目录中按照GNUmakefile、makefile、Makefile的顺序搜索
makefile文件。
  -i   忽略命令执行返回的出错信息。
  -s   沉默模式,在执行之前不输出相应的命令行信息。
  -r   禁止使用build-in规则。
  -n   非执行模式,输出所有执行命令,但并不执行。
  -t   更新目标文件。
  -q   make操作将根据目标文件是否已经更新返回"0"或非"0"的状态信息。
  -p   输出所有宏定义和目标文件描述。
  -d   Debug模式,输出有关文件和检测时间的具体信息。
  linux下make标志位的常用选项与Unix系统中稍有不同,下面我们只列出了不同部分:
  -c dir   在读取 makefile 之前改变到指定的目录dir。
  -I dir   当包含其他 makefile文件时,利用该选项指定搜索目录。
  -h   help文挡,显示所有的make选项。
  -w   在处理 makefile 之前和之后,都显示工作目录。
  通过命令行参数中的target,可指定make要编译的目标,并且答应同时定义编译多个目标,
操作时按照从左向右的顺序依次编译target选项中指定的目标文件。假如命令行中没有指定目标,
则系统默认target指向描述文件中第一个目标文件。
  通常,makefile 中还定义有 clean 目标,可用来清除编译过程中的中间文件,例如:
  clean:
  rm -f *.o
  运行 make clean 时,将执行 rm -f *.o 命令,最终删除所有编译过程中产生的所有中间文件。

隐含规则
  在make 工具中包含有一些内置的或隐含的规则, 这些规则定义了如何从不同的依靠文件建
立特定类型的目标。Unix系统通常支持一种基于文件扩展名即文件名后缀的隐含规则。这种后缀
规则定义了如何将一个具有特定文件名后缀的文件(例如.c文件),转换成为具有另一种文件名
后缀的文件(例如.o文件):
  .c:.o
  $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
  系统中默认的常用文件扩展名及其含义为:
  .o  目标文件
  .c  C源文件
  .f  FORTRAN源文件
  .s  汇编源文件
  .y  Yacc-C源语法
  .l  Lex源语法
  在早期的Unix系统系统中还支持Yacc-C源语法和Lex源语法。在编译过程中, 系统会首先在
makefile文件中寻找与目标文件相关的.C文件,假如还有与之相依靠的.y和.l文件,则首先将其
转换为.c文件后再编译生成相应的.o文件;假如没有与目标相关的.c文件而只有相关的.y文件,
则系统将直接编译.y文件。
  而GNU make 除了支持后缀规则外还支持另一种类型的隐含规则--模式规则。 这种规则更加
通用,因为可以利用模式规则定义更加复杂的依靠性规则。模式规则看起来非常类似于正则规则,
但在目标名称的前面多了一个 %号,同时可用来定义目标和依靠文件之间的关系,例如下面的模
式规则定义了如何将任意一个 file.c 文件转换为 file.o 文件:
  %.c:%.o
  $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

#EXAMPLE#

  下面将给出一个较为全面的示例来对makefile文件和make命令的执行进行进一步的说明,其

中make命令不仅涉及到了C源文件还包括了Yacc语法。本例选自"Unix Programmer's Manual 7th
Edition, Volume 2A" Page 283-284
  下面是描述文件的具体内容:
  ---------------------------------------------------------
   #Description file for the Make command
   #Send to print
   P=und -3 opr -r2
   #The source files that are needed by object files
   FILES= Makefile version.c defs main.c donamc.c misc.c file.c
   dosys.c gram.y lex.c gcos.c
   #The definitions of object files
   OBJECTS= vesion.o main.o donamc.o misc.o file.o dosys.o gram.o
   LIBES= -LS
   LINT= lnit -p
   CFLAGS= -O
   make: $(OBJECTS)
   cc $(CFLAGS) $(OBJECTS) $(LIBES) -o make
   size make
   $(OBJECTS): defs
   gram.o: lex.c
   cleanup:
   -rm *.o gram.c
   install:
   @size make /usr/bin/make
   cp make /usr/bin/make ; rm make
   #print recently changed files
   print: $(FILES)
   pr $? $P
   toUCh print
   test:
   make -dp grep -v TIME>1zap
   /usr/bin/make -dp grep -v TIME>2zap
   diff 1zap 2zap
   rm 1zap 2zap
   lint: dosys.c donamc.c file.c main.c misc.c version.c gram.c
   $(LINT) dosys.c donamc.c file.c main.c misc.c version.c
   gram.c
   rm gram.c
   arch:
   ar uv /sys/source/s2/make.a $(FILES)
  ----------------------------------------------------------
  通常在描述文件中应象上面一样定义要求输出将要执行的命令。在执行了make命令之后,输
出结果为:
  $ make
  cc -c version.c
  cc -c main.c
  cc -c donamc.c
  cc -c misc.c
  cc -c file.c
  cc -c dosys.c
  yacc gram.y
  mv y.tab.c gram.c
  cc -c gram.c
  cc version.o main.o donamc.o misc.o file.o dosys.o gram.o
  -LS -o make
  13188+3348+3044=19580b=046174b

  最后的数字信息是执行"@size make"命令的输出结果。之所以只有输出结果而没有相应的命
令行,是因为"@size make"命令以"@"起始,这个符号禁止打印输出它所在的命令行。
  描述文件中的最后几条命令行在维护编译信息方面非常有用。 其中"print"命令行的作用是
打印输出在执行过上次"make print"命令后所有改动过的文件名称。 系统使用一个名为print的
0字节文件来确定执行print命令的具体时间, 而宏$?则指向那些在print文件改动过之后进行修
改的文件的文件名。假如想要指定执行print命令后,将输出结果送入某个指定的文件, 那么就
可修改P的宏定义:
  make print "P= cat>zap"
  在linux中大多数软件提供的是源代码,而不是现成的可执行文件, 这就要求用户根据自己
系统的实际情况和自身的需要来配置、编译源程序后,软件才能使用。只有把握了make工具,才
能让我们真正享受到到linux这个自由软件世界的带给我们无穷乐趣。

上一篇:GCC

下一篇:编程资源收集

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表