GNU MAKE指南

GNU make 指南

翻译: 哈少

译者按: 本文是一篇介绍 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)<command>
(tab)<command>
.
.
.

例如,考虑如下的 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 
 
相关文章
相关标签/搜索