9.2 make命令和makefile文件

你将看到,虽然make命令内置了很多智能机制,但光凭其自身是无法了解应该如何建立应用程序的。你必须为其提供一个文件,告诉它应用程序应该如何构造,这个文件称为makefile。

makefile文件一般都会和项目的其他源文件放在同一目录下。你的机器上可以同时存在许多不同的makefile文件。事实上,如果管理的是一个大项目,你可以用多个不同的makefile文件来分别管理项目的不同部分。

make命令和makefile文件的结合提供了一个在项目管理领域十分强大的工具。它不仅常被用于控制源代码的编译,而且还用于手册页的编写以及将应用程序安装到目标目录。

9.2.1 makefile的语法

makefile文件由一组依赖关系和规则构成。每个依赖关系由一个目标(即将要创建的文件)和一组该目标所依赖的源文件组成。而规则描述了如何通过这些依赖文件创建目标。一般来说,目标是一个单独的可执行文件。

make命令会读取makefile文件的内容,它先确定目标文件或要创建的文件,然后比较该目标所依赖的源文件的日期和时间以决定该采用哪条规则来构造目标。通常在创建最终的目标文件之前,它需要先创建一些中间目标。make命令会根据makefile文件来确定目标文件的创建顺序以及正确的规则调用顺序。

9.2.2 make命令的选项和参数

make程序本身有许多选项,其中最常用的3个选项如下所示。

❑ -k:它的作用是让make命令在发现错误时仍然继续执行,而不是在检测到第一个错误时就停下来。你可以利用这个选项在一次操作中发现所有未编译成功的源文件。

❑ -n:它的作用是让make命令输出将要执行的操作步骤,而不真正执行这些操作。

❑ -f <filename>:它的作用是告诉make命令将哪个文件作为makefile文件。如果未使用这个选项,标准版本的make命令将首先在当前目录下查找名为makefile的文件,如果该文件不存在,它就会查找名为Makefile的文件。但如果你是在Linux系统中,你使用的可能是GNU Make,这个版本的make命令将在搜索makefile文件和Makefile文件之前,首先查找名为GNUmakefile的文件。按惯例,许多Linux程序员使用文件名Makefile,因为如果一个目录下都是以小写字母为名称的文件,则Makefile文件将在目录的文件列表中第一个出现。我们建议不要使用文件名GNUmakefile,因为它是特定于make命令的GNU实现的。

为了指示make命令创建一个特定的目标(通常是一个可执行文件),你可以把该目标的名字作为make命令的一个参数。如果不这么做,make命令将试图创建列在makefile文件中的第一个目标。许多程序员都会在自己的makefile文件中将第一个目标定义为all,然后再列出其他从属目标。这个约定可以明确地告诉make命令,在未指定特定目标时,默认情况下应该创建哪个目标。我们建议读者都坚持使用这一约定。

1.依赖关系

依赖关系定义了最终应用程序里的每个文件与源文件之间的关系。在本章前面的程序示例中,你可以把依赖关系定义为最终应用程序依赖于文件main.o、2.o和3.o。同样,main.o依赖于main.c和a.h,2.o依赖于2.c、a.h和b.h,3.o依赖于3.c、b.h和c.h。因此,main.o受文件main.c和a.h修改的影响,如果这两个文件之一有所改变,你就需要重新编译main.c来重建main.o。

在makefile文件中,这些规则的写法是:先写目标的名称,然后紧跟着一个冒号,接着是空格或制表符tab,最后是用空格或制表符tab隔开的文件列表(这些文件用于创建目标文件)。与前面例子相对应的依赖关系列表如下所示:

9.2 make命令和makefile文件 - 图1

它表示目标myapp依赖于main.o、2.o和3.o,而main.o依赖于main.c和a.h,等等。

这组依赖关系形成一个层次结构,它显示了源文件之间的关系。你可以很容易地看出,如果文件b.h发生改变,你就需重新编译2.o和3.o,而由于2.o和3.o发生了改变,你还需要重新创建目标myapp。

如果想一次创建多个文件,你可以利用伪目标all。假设应用程序由二进制文件myapp和使用手册myapp.1组成。你可以用下面这行语句进行定义:

9.2 make命令和makefile文件 - 图2

这里再次强调,如果未指定一个all目标,则make命令将只创建它在文件makefile中找到的第一个目标。

2.规则

makefile文件的第二部分内容是规则,它们定义了目标的创建方式。在上节的例子中,当make命令确定需要重建2.o时,它具体应该使用哪条命令呢?看上去只需使用命令gcc -c 2.c就够了(在后面你将看到,make命令内置了很多默认规则),但如果需要指定头文件目录,或者为了今后的调试需要设置符号信息选项又该怎么做呢?这就需要在makefile文件中明确定义一些规则。

此时,我们必须提及makefile文件中一个非常奇怪而又令人遗憾的语法现象:空格和制表符tab是有区别的。规则所在的行必须以制表符tab开头,用空格是不行的。由于连续几个空格和一个制表符tab看上去很相似,而且几乎在Linux编程的所有领域中,空格和制表符tab之间几乎没有差别,所以这样的语法规定会带来问题。此外,如果makefile文件中的某行以空格结尾,它也可能会导致make命令执行失败。但这些都是历史遗留问题,而且因为已有太多的makefile文件存在,企图将其全部改正是不现实的,所以请小心编写makefile文件。幸运的是,如果缺少了制表符tab,make命令就不会正常工作,所以发现这个错误很容易。

实 验 一个简单的makefile文件

大多数规则都包含一个简单的命令,该命令也可以在命令行上执行。就前面的例子来说,你把创建的第一个makefile文件命名为Makefile1:

9.2 make命令和makefile文件 - 图3

9.2 make命令和makefile文件 - 图4

你在调用make命令时加上-f选项,这是因为makefile文件并未使用常见的默认文件名makefile或Makefile。如果在一个没有任何源文件的目录下执行这个命令,你就会得到如下的输出结果:

9.2 make命令和makefile文件 - 图5

make命令假设在makefile文件中的第一个目标myapp是想创建的目标文件。然后它会检查其他的依赖关系,并确定需要有一个名为main.c的文件。由于并未创建该文件,makefile文件里也未说明如何创建该文件,所以make命令报告一个错误。下面就来创建这些源文件并重新进行尝试。由于对程序执行的结果没有兴趣,所以这些文件的内容都非常简单。头文件实际上都是空文件,你可以用touch命令来创建它们:

9.2 make命令和makefile文件 - 图6

源文件main.c中包含main函数,该函数调用了function_two和function_three函数,而这两个函数分别在另外两个文件中定义。源文件通过#include语句包含合适的头文件,使它们看上去依赖于这些头文件的内容。它其实算不上是一个应用程序,下面是其程序清单:

9.2 make命令和makefile文件 - 图7

9.2 make命令和makefile文件 - 图8

再次执行make命令:

9.2 make命令和makefile文件 - 图9

这次成功执行了make命令。

实验解析

make命令处理makefile文件中定义的依赖关系,确定需要创建的文件以及创建顺序。虽然把如何创建目标myapp列在最前面,但make命令能够自行判断出创建文件的正确顺序。它调用你在规则部分给出的命令来创建相应的文件,同时会在执行时在屏幕上将命令显示出来。现在,你可以测试在文件b.h改变时,makefile文件能否正确处理这一情况:

9.2 make命令和makefile文件 - 图10

make命令读取makefile文件,确定重建myapp所需的最少命令,并以正确的顺序执行它们。下面我们来看,如果删除一个目标文件会发生什么情况:

9.2 make命令和makefile文件 - 图11

make命令再次正确地确定出需要采取的动作。

9.2.3 makefile文件中的注释

makefile文件中的注释以#号开头,一直延续到这一行的结束。和C语言源文件中的注释一样,makefile文件中的注释可以帮助程序的编写者及其他人理解最初编写这个文件的目的。

9.2.4 makefile文件中的宏

即使上述内容就是make命令和makefile文件的全部,对于管理包含多个源文件的项目来说,它们仍然是强有力的工具。但是,对于管理包含非常多源文件的大型项目来说,它们就显得过于庞大并缺乏弹性。因此,makefile文件允许你使用宏以一种更通用的格式来书写它们。

你通过语句MACRONAME=value在makefile文件中定义宏,引用宏的方法是使用$(MACRONAME)或${MACRONAME}。make的某些版本还接受$MACRONAME的用法。如果想把一个宏的值设置为空,你可以令等号(=)后面留空。

makefile文件中的宏常被用于设置编译器的选项。在软件的开发过程中,通常开发人员不会对编译结果进行优化,而是将调试信息包含进去。但对于软件的发行版,往往又需反过来做,即编译结果是一个不包含调试信息的容量较小的二进制可执行文件,使其执行速度尽可能快。

Makefile1文件的另一问题是,它假设编译器的名字是gcc,而在其他UNIX系统中,编译器的名字可能是cc或c89。如果想将makefile文件移植到另一版本的UNIX系统中,或在现有系统中使用另一个编译器,为了使其工作,你将不得不修改makefile文件中许多行的内容。宏是用来收集所有这些与系统相关内容的好方法,通过使用宏定义,你可以方便地修改这些内容。

宏通常都是在makefile文件中定义的,但你也可以在调用make命令时在命令行上给出宏定义,例如命令make CC=c89。命令行上的宏定义将覆盖在makefile文件中的宏定义。当在makefile文件之外使用宏定义时,要注意宏定义必须以单个参数的形式传递,所以应避免在宏定义中使用空格或应像下面这样给宏定义加上引号:make "CC=c89"。

实 验 带宏定义的makefile文件

下面是makefile文件的一个修订版本Makefile2,它使用了一些宏定义:

9.2 make命令和makefile文件 - 图12

删除旧的安装文件,并通过这个新的makefile文件创建新的安装文件,你将看到如下的输出:

9.2 make命令和makefile文件 - 图13

实验解析

make命令将$(CC)、$(CFLAGS)和$(INCLUDE)替换为相应的宏定义,这与C语言编译器对#define语句的处理方式很相似。现在,如果想改变编译器命令,你只需修改makefile文件中的一行即可。

事实上,make命令内置了一些特殊的宏定义,通过使用它们,你可以让makefile文件变得更加简洁。我们将几个较常用的宏列在表9-1中,其使用方法可以在后面的示例中看到。这些宏在使用前才展开,所以它们的含义会随着makefile文件的处理进展而发生变化。事实上,如果这些内置宏的用法不是这样,它们就没有什么用处了。

表 9-1

9.2 make命令和makefile文件 - 图14

在makefile文件中,你可能还会看到下面两个有用的特殊字符,它们出现在命令之前。

❑ -:告诉make命令忽略所有错误。例如,如果想创建一个目录,但又想忽略任何错误(比如目录已存在),你就可以在mkdir命令的前面加上一个减号。你将在本章后面的例子中看到符号-的应用。

❑ @:告诉make在执行某条命令前不要将该命令显示在标准输出上。如果想用echo命令给出一些说明信息,这个字符将非常有用。

9.2.5 多个目标

通常制作不止一个目标文件或者将多组命令集中到一个位置来执行是很有用的。你可以通过扩展makefile文件来达到这一目的。在下面的例子中,你在makefile文件中增加一个clean选项来删除不需要的目标文件,增加一个install选项来将编译成功的应用程序安装到另一个目录下。

实 验 多个目标

下面是makefile文件的下一个版本Makefile3文件的内容:

9.2 make命令和makefile文件 - 图15

9.2 make命令和makefile文件 - 图16

这个makefile文件中有几处需要注意。首先,特殊目标all仍然只指定了myapp这一个目标。因此,如果在执行make命令时未指定目标,它的默认行为就是创建目标myapp。

下一个值得关注之处就是两个新增加的目标:clean和install。目标clean用rm命令来删除目标文件。rm命令以减号-开头,减号的含义是让make命令忽略rm命令的执行结果,这意味着,即使由于目标文件不存在而导致rm命令返回错误,命令make clean也会成功。用于制作目标clean的规则并未给目标clean定义任何依赖关系,行clean:的后面是空的,因此该目标总被认为是过时的,所以在执行make命令时,如果指定目标clean,则该目标所对应的规则将总被执行。

目标install依赖于myapp,所以make命令知道它必须首先创建myapp,然后才能执行制作该目标所需的其他命令。用于制作install目标的规则由几个shell脚本命令组成。由于make命令在执行规则时会调用一个shell,并且会针对每个规则使用一个新shell,所以必须在上面每行代码的结尾加上一个反斜杠\,让所有shell脚本命令在逻辑上处于同一行,并作为一个整体传递给一个shell执行。这个命令以符号@开头,表示make在执行这些规则之前不会在标准输出上显示命令本身。

目标install按顺序执行多个命令将应用程序安装到其最终位置。它并没有在执行下一个命令前检查前一个命令的执行是否成功。如果这点很重要,你可以将这些命令用符号&&连接起来,如下所示:

9.2 make命令和makefile文件 - 图17

大家应该记得,我们曾经在第2章见过该符号,对shell来说,它是“与”的意思,即每个后续命令只在前面的命令都执行成功的前提下才会被执行。在此例中,你并不过分关心前面的命令是否执行成功,所以可以坚持使用简单的格式。

你可能不能以普通用户的身份将新命令安装到目录/usr/local/bin下。在执行命令make install之前,你可以修改makefile文件以选择另一个安装目录,或是改变该目录的权限,或是通过命令su切换用户身份到超级用户root。

9.2 make命令和makefile文件 - 图18

实验解析

首先,删除myapp和所有目标文件。单独执行make命令的话,它将使用默认目标all,并创建可执行程序myapp。然后再次运行make命令,但因为myapp已经是最新的,所以make命令未做任何事。接下来,删除文件myapp并执行命令make install,它重新创建二进制文件myapp并将其复制到安装目录中。最后,运行命令make clean来删除当前目录下所有的目标文件。

9.2.6 内置规则

目前为止,你在makefile文件中对每个操作步骤的执行都做了精确的说明。事实上,make命令本身带有大量的内置规则,它们可以极大地简化makefile文件的内容,尤其在拥有许多源文件时更是如此。为测试这些内置规则,下面创建文件foo.c,它是一个传统的“Hello World”程序:

9.2 make命令和makefile文件 - 图19

在不指定makefile文件时,尝试用make命令来编译它:

9.2 make命令和makefile文件 - 图20

可以看到,make命令知道如何调用编译器,虽然此例中,它选择的是cc而不是gcc(在Linux系统中,这没有问题,因为cc通常是gcc的一个连接文件)。有时,这些内置规则又被称为推导规则,由于它们都会使用宏定义,因此可以通过给宏赋予新值来改变其默认行为。

9.2 make命令和makefile文件 - 图21

你可以通过-p选项让make命令打印出其所有内置规则。由于内置规则实在太多,不能在此一一列出,所以这里只给出了GNU版本make的make -p命令的部分输出,显示了其中一部分的规则:

9.2 make命令和makefile文件 - 图22

考虑到存在这些内置规则,你可以将文件makefile中用于制作目标的规则去掉,而只需指定依赖关系,从而达到简化makefile文件的目的。因此该文件中相应部分的内容将变得很简单,如下所示:

9.2 make命令和makefile文件 - 图23

读者可以在本书所对应的网站下载代码中找到这个版本的makefile文件Makefile4。

9.2.7 后缀和模式规则

你看到的内置规则在使用时都利用了文件的后缀名(这类似Windows和MS-DOS的文件扩展名),所以当给出带有某个特定后缀名的文件时,make命令知道应该用哪个规则来创建带有另一个不同后缀名的文件。最常见的一条规则是用于从一个以.c为后缀名的文件创建出一个以.o为后缀名的文件。该规则使用编译器进行编译,但并不对源文件进行链接。

有时,你需要自己创建新规则。我过去在日常工作中经常需要用多个不同的编译器对源文件进行编译:其中两个是MS-DOS下的编译器,一个是Linux下的gcc。为了让其中一个MS-DOS编译器能够正常工作,源文件(它们用的是C++语言而不是C语言)需要以.cpp为后缀名。但糟糕的是,那个时候的Linux系统下的make版本没有用于编译后缀名为.cpp的源文件的内置规则(它倒是有一条针对.cc源文件的规则,UNIX系统中的C++文件常使用这个后缀名)。

为解决这个问题,或者为每个单独的源文件指定一条规则,或者为make制定一条新的规则,专门用于从后缀名为.cpp的源文件创建目标文件。假设这个项目中的源文件数量非常大,那么制定一条新规则将节省大量的键入时间,也使得为该项目增加新的源文件变得更加容易。

要想增加一条新的后缀规则,首先需要在makefile文件中增加一行语句,告诉make命令这个新的后缀名。然后即可用这个新的后缀名来定义规则。make使用特殊语法:

.<old_suffix>.<new_suffix>:

来定义一条通用规则,利用该规则可以从带有旧后缀名的文件创建带有新后缀名的文件,并保留原文件的前半部分。

下面是makefile文件的一个片段,它用一个新的通用规则将.cpp文件编译为.o文件:

9.2 make命令和makefile文件 - 图24

特殊依赖关系.cpp.o:告诉make,紧随其后的规则是用于将后缀名为.cpp的文件转换为后缀名为.o的文件。在定义这个依赖关系时,使用了特殊的宏名称,这是因为此时你还不知道将要被转换的文件的名字。要想理解这条规则,只需要记住宏$<将被扩展为起始文件的名字(包含旧的后缀名)。注意,只需告诉make如何从.cpp文件得到.o文件,make已经知道如何从一个目标文件得到一个二进制可执行文件了。

当调用make命令时,它将使用这条新规则从bar.cpp文件得到bar.o文件,然后再使用它的内置规则从.o文件得到二进制可执行文件。-xc++标志的作用是告诉gcc编译器这是一个C++源文件。

如今的make版本已知道如何处理后缀名为.cpp的C++源文件了,但当需要将一种类型的文件转换为另一种类型的文件时,这个技术仍然很有用。

最新的make版本还包含一个新的语法以实现同样的效果,而且功能更强大。例如,模式规则可以用%通配符语法来匹配文件名,而不是仅依赖于文件的后缀名。

可以达到与上例中.cpp规则同样效果的模式规则如下所示:

9.2 make命令和makefile文件 - 图25

9.2.8 用make管理函数库

对于大型项目,一种比较方便的做法是用函数库来管理多个编译产品。函数库实际上就是文件,它们通常以.a(a是英文archive的首字母)为后缀名,在该文件中包含了一组目标文件。make命令用一个特殊的语法来处理函数库,这使得函数库的管理工作变得非常容易。

用于管理函数库的语法是lib (file.o),它的含义是目标文件file.o是存储在函数库lib.a中的。make命令用一个内置规则来管理函数库,该规则的常见形式如下所示:

9.2 make命令和makefile文件 - 图26

宏$(AR)和$(ARFLAGS)的默认取值通常分别是命令ar和选项rv。这个相当简洁的语法告诉make,要想从.c文件得到.a库文件,它必须应用上面两条规则。

❑ 第一条规则告诉它必须编译源文件以生成目标文件。

❑ 第二条规则告诉它用ar命令将新的目标文件添加到函数库中。

因此,如果有一个名为fud的函数库,其中包含目标文件bas.o,则第一条规则中的$<将被替换为bas.c,而第二条规则中的$@和$*将被分别替换为库文件fud.a和名字bas。

实 验 管理函数库

在实际应用中,管理函数库规则的使用非常简单。下面将文件2.o和3.o放入函数库mylib.a中。你只需对makefile文件做很少的修改,最终的makefile文件Makefile5如下所示:

9.2 make命令和makefile文件 - 图27

9.2 make命令和makefile文件 - 图28

请注意,我们是如何利用默认规则来完成大部分工作的。下面测试这个新版本的makefile文件:

9.2 make命令和makefile文件 - 图29

实验解析

首先,删除所有的目标文件和库文件,然后执行make命令创建myapp。make命令首先编译并创建函数库,然后把main.o和该函数库链接起来以创建myapp。接下来测试目标3.o的依赖规则,它告诉make命令,当文件c.h发生改变时,源文件3.c必须被重新编译。make命令正确地完成了这一工作,它首先编译源文件3.c,然后更新函数库,最后重新链接函数库并创建一个新的可执行文件myapp。

9.2.9 高级主题:makefile文件和子目录

对于大型的项目,有时我们希望能把构成一个函数库的几个文件从主文件中分离出来,并将它们保存到一个子目录中。使用make命令完成这一工作的方法有两个。

第一个方法是,你可以在子目录中编写出第二个makefile文件,它的作用是编译该子目录下的源文件,并将它们保存到一个函数库中,然后将该库文件复制到上一级的主目录中。在主目录中的makefile文件包含一条用于制作函数库的规则,该规则会调用第二个makefile文件,如下所示:

9.2 make命令和makefile文件 - 图30

这就是说,你必须总是执行命令make mylib.a。当make命令调用这条规则来创建函数库时,它将切换到子目录mylibdirectory中,然后调用一个新的make命令来管理函数库。由于make会针对每个命令调用一个新的shell,而使用第二个makefile文件的make命令本身又并没有执行cd命令,但它又必须在一个不同的目录下创建函数库,为解决这一问题,我们用括号将这两个命令括起来,从而确保它们只被一个单独的shell处理。

第二个方法是,在原来的makefile文件中添加一些宏。新添加的宏通过在我们已见过的宏的尾部追加一个字母得到,字母D代表目录,字母F代表文件名。然后你就可以用下面的规则来替换内置的.c.o后缀规则:

9.2 make命令和makefile文件 - 图31

这条规则的作用是:编译子目录中的源文件并将目标文件放在该子目录中。然后,你用如下的依赖关系和规则来更新当前目录下的函数库:

9.2 make命令和makefile文件 - 图32

在项目中究竟使用哪种方法是由读者决定的。许多项目避免使用子目录,但这将导致在主目录中存在大量的源文件。可以从上面的简介中看到,你只需要为makefile文件稍微增加一点复杂性,即可在make命令中使用子目录。

9.2.10 GNU make和gcc

GNU的make命令和GNU的gcc编译器有下面两个有趣的选项。

❑ 第一个选项是make命令的-jN(字母j是英文单词jobs的首字母)选项,它允许make命令同时执行N条命令。如果项目的不同部分可以彼此独立地进行编译,make命令就可以同时调用几条规则。根据系统的配置情况,这种做法可以极大地缩短重新编译所需要花费的时间。如果有许多源文件,这个选项就值得一试。一般来说,你可以先从较小的数字(比如-j3)开始尝试。但如果需要与其他用户共享你的计算机,就要小心使用这个选项,因为其他用户可能并不喜欢你每次编译时都启动大量的进程。

❑ 另一个有用的选项是gcc的-MM选项。它的作用是产生一个适用于make命令的依赖关系清单。如果某个项目包含非常多的源文件,每个源文件又包含不同的头文件组合,则理清它们之间的依赖关系将非常困难(但又非常重要)。如果让每个源文件都依赖于所有的头文件,有时候你就会编译一些没有必要编译的文件。但从另一方面来看,如果忽略一些依赖关系,问题会变得更严重,因为你没有重新编译一些需要编译的文件。

实 验 gcc-MM

在这个实验中,你用gcc的-MM选项来生成上面示例项目中的依赖关系清单:

9.2 make命令和makefile文件 - 图33

实验解析

gcc编译器扫描源文件以查找include语句,然后以一种适合于直接插入到makefile文件中的格式输出需要的依赖关系清单。你只需先把这个输出结果保存到一个临时文件中,然后把它们插入到makefile文件中,即可得到一组完美的依赖关系规则。如果拥有gcc编译器,却还出现依赖关系的错误就不应该了!

如果你对制作makefile文件非常有信心,也可以尝试使用makedepend工具,它的功能与-MM选项很类似,但其做法是将依赖关系直接附加到指定的makefile文件的末尾。

在结束对makefile文件的介绍之前,我们有必要指出makefile文件并不仅用于编译源代码或创建函数库。只要是可以通过一系列命令从某些类型的输入文件得到输出文件的任务,你都可通过makefile文件来自动地完成。一个典型的“非编译器”用途是,通过调用awk或sed命令对一些文件进行处理,或甚至通过makefile文件来生成使用手册。你可以通过它对任何与文件操纵相关的任务进行自动化处理,只要make命令可以根据文件的日期和时间信息判断出哪个文件发生了改变即可。

另一种用于控制程序创建或完成其他自动化任务的工具是ANT。它是一个基于Java的工具,它使用基于XML的配置文件。这个工具并不常被用于自动化处理Linux系统上C语言文件的创建,所以我们不会在这里对它作进一步的介绍。你可以通过网址http://ant.apache.org找到有关ANT的详细资料。