语言: 日本語 | Español | Français | Português | 中文 | English
上一章 | 下一章 | 目录 | 英文原版(gnu.org)

2 Makefile 入门

要告诉 make 你想让它做什么,需要一个称为 makefile 的文件。大多数情况下,makefile 的作用是告诉 make 如何编译并链接某个程序。

本章以一个简单的 makefile 为例进行讲解,它描述了如何编译并链接一个由 8 个 C 源文件和 3 个头文件组成的文本编辑器。makefile 中还可以写入其他命令,这些命令只有在被明确要求时才会执行(例如作为清理工作删除某些特定文件)。如果想看更复杂的 makefile 例子,请参阅复杂的 Makefile 示例

make 重新编译这个编辑器时,每个被修改过的 C 源文件都必须重新编译。如果某个头文件被修改了,为保险起见,所有 #include 了该头文件的 C 源文件都需要重新编译。每次编译都会从对应的源文件生成一个目标文件(object)。最后,只要有任何一个源文件被重新编译过,就必须把所有目标文件——无论是新生成的还是上次编译保留下来的——全部链接在一起,生成新的可执行编辑器。

2.1 规则长什么样

一个简单的 makefile 是由若干形如下面这样的“规则”组成的:

target … : prerequisitesrecipe
        …
        …

目标(target)通常是由某个程序生成的文件的名字,例如可执行文件或目标文件都是目标的例子。目标也可以是要执行的某个动作的名字,例如 clean(参阅伪目标(Phony))。

前置条件(prerequisite,旧称:依赖)是用作输入、用来生成目标的文件。一个目标常常会依赖多个文件。

命令(recipe,即规则的执行内容)是 make 所执行的动作。一条命令(recipe)可以包含多个 shell 命令,既可以并排写在同一行,也可以各自单独成行。[重要] 命令的每一行,行首都必须放置一个制表符(Tab)!这一点很容易被忽略,是个坑人的陷阱。如果你想用制表符以外的字符作为命令行的前缀,可以把 .RECIPEPREFIX 变量设置为另一个字符(参阅其他特殊变量)。

通常命令位于带前置条件的规则中,负责在任何一个前置条件发生变化时重新生成目标文件。不过,指定目标命令的规则不一定非要有前置条件。例如,包含与目标 clean 相关联的删除命令的规则,就没有前置条件。

也就是说,规则说明的是如何以及何时去重新生成作为该规则目标的某些特定文件。make 对前置条件执行命令,以创建或更新目标。规则也可以说明如何以及何时执行某个动作。参阅编写规则

makefile 中也可以包含规则以外的文本,但对于一个简单的 makefile 来说,只要有规则就足够了。实际的规则看起来可能比这个模板稍微复杂一些,但大体上都符合这个模式。

2.2 一个简单的 Makefile

下面是一个直白的 makefile,它描述了这样一种关系:可执行文件 edit 依赖于 8 个目标文件,而这些目标文件又依赖于 8 个 C 源文件和 3 个头文件。

在这个例子中,所有的 C 文件都 #includedefs.h,但只有定义编辑命令的文件才 #include command.h,而只有改动编辑器缓冲区的底层文件才 #include buffer.h

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
        cc -o edit main.o kbd.o command.o display.o \
                   insert.o search.o files.o utils.o

main.o : main.c defs.h
        cc -c main.c
kbd.o : kbd.c defs.h command.h
        cc -c kbd.c
command.o : command.c defs.h command.h
        cc -c command.c
display.o : display.c defs.h buffer.h
        cc -c display.c
insert.o : insert.c defs.h buffer.h
        cc -c insert.c
search.o : search.c defs.h buffer.h
        cc -c search.c
files.o : files.c defs.h buffer.h command.h
        cc -c files.c
utils.o : utils.c defs.h
        cc -c utils.c
clean :
        rm edit main.o kbd.o command.o display.o \
           insert.o search.o files.o utils.o

这里我们用反斜杠加换行把长行拆成了两行。这和写成一整条长行是一样的效果,但这样更易读。参阅拆分长行

要用这个 makefile 生成名为 edit 的可执行文件,请输入:

make

要用这个 makefile 从目录中删除可执行文件和所有目标文件,请输入:

make clean

在这个示例 makefile 中,目标包括可执行文件 edit,以及目标文件 main.okbd.o 等等。前置条件则是诸如 main.cdefs.h 之类的文件。实际上,每个 .o 文件既是目标,又是前置条件。命令则包括 cc -c main.ccc -c kbd.c

当目标是一个文件时,只要它的任何一个前置条件发生变化,该目标就需要被重新编译或重新链接。此外,如果某些前置条件本身是自动生成的,则应当先把它们更新好。在这个例子中,edit 依赖于 8 个目标文件中的每一个,而目标文件 main.o 依赖于源文件 main.c 和头文件 defs.h

在每一行写有目标和前置条件之后,都可以接着写命令。这些命令指示如何更新目标文件。为了把命令与 makefile 中的其他行区分开来,命令的每一行行首都必须放置一个制表符(Tab)(或者由 .RECIPEPREFIX 变量指定的字符,参阅其他特殊变量)。(请记住,make 对命令是如何工作的一无所知。提供能正确更新目标文件的命令,是你的责任。make 所做的,只是在目标文件需要更新时,执行你所指定的命令而已。)

目标 clean 不是一个文件,而仅仅是某个动作的名字。由于你平时并不想执行这条规则中的动作,所以 clean 不是任何其他规则的前置条件。因此,除非你专门指示,make 永远不会去处理它。请注意,这条规则不仅不是别人的前置条件,它自己也没有任何前置条件,所以这条规则的唯一目的就是运行指定的命令。那些不指向文件、而仅仅是动作的目标,被称为伪目标(phony)。关于这类目标的信息,请参阅伪目标(Phony);关于如何让 make 忽略 rm 或其他命令产生的错误,请参阅命令中的错误

2.3 make 如何处理 Makefile

默认情况下,make 从第一个目标开始处理(名字以 . 开头的目标除外,除非它们还含有一个或多个 /)。这个目标称为默认目标(goal)。(goal 是指 make 最终力图更新的那些目标。)你可以通过命令行(参阅指定目标的参数)或 .DEFAULT_GOAL 特殊变量(参阅其他特殊变量)来改变这一行为。

在上一节那个简单的例子中,默认目标是更新可执行程序 edit,正因如此,我们才把那条规则放在最前面。

因此,当你给出下面这条命令时:

make

make 会读取当前目录中的 makefile,并从第一条规则开始处理。在这个例子中,第一条规则用于重新链接 edit;但在 make 能够完整处理这条规则之前,它必须先处理 edit 所依赖的那些文件——在这里就是目标文件——的规则。这些文件各自按照自己的规则进行处理。这些规则指示:通过编译各 .o 文件对应的源文件来更新它们。当源文件,或作为前置条件列出的任何头文件比目标文件更新时,或者目标文件不存在时,就需要进行重新编译。

其他规则之所以会被处理,是因为它们的目标作为 goal 的前置条件出现了。如果某条规则没有被 goal(或 goal 所依赖的东西,以及它们再依赖的东西……)所依赖,那么这条规则就不会被处理,除非你用 make clean 之类的命令明确加以指示。

在重新编译某个目标文件之前,make 会考虑更新它的前置条件,也就是源文件和头文件。这个 makefile 没有为它们指定任何要做的事情——.c 文件和 .h 文件都不是任何规则的目标——所以 make 对这些文件不做任何处理。但是,对于像 Bison 或 Yacc 所生成的那种自动生成的 C 程序,make 会在这个阶段按照它们各自的规则进行更新。

在重新编译完所有需要重新编译的目标文件之后,make 会判断是否要重新链接 edit。如果文件 edit 不存在,或者任何一个目标文件比 edit 更新,就需要重新链接。如果某个目标文件刚刚被重新编译过,它就比 edit 更新了,于是 edit 会被重新链接。

因此,如果你修改了文件 insert.c 并运行 make,make 就会编译该文件以更新 insert.o,然后链接 edit。如果你修改了文件 command.h 并运行 make,make 就会重新编译目标文件 kbd.ocommand.ofiles.o,然后链接文件 edit

2.4 用变量让 Makefile 更简洁

在前面的例子中,我们不得不在 edit 的规则里把所有目标文件列举两次(此处重新列出):

edit : main.o kbd.o command.o display.o \
              insert.o search.o files.o utils.o
        cc -o edit main.o kbd.o command.o display.o \
                   insert.o search.o files.o utils.o

这样的重复很容易出错。当你向系统中添加一个新的目标文件时,可能加进了一个列表,却忘了加进另一个列表。使用变量可以消除这种风险,并让 makefile 更简洁。使用变量,可以把一段文本字符串只定义一次,之后在多处把它代入(参阅如何使用变量)。

标准做法是让每个 makefile 都拥有一个列出所有目标文件名的变量,名字为 objectsOBJECTSobjsOBJSobjOBJ 之一。我们可以在 makefile 中写下这样一行来定义这样一个变量 objects:

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

这样一来,在每个想要放置目标文件名列表的地方,我们都可以写 $(objects) 来代入这个变量的值(参阅如何使用变量)。

对目标文件使用变量后,前面那个简单的 makefile 整体就变成下面这样:

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)
main.o : main.c defs.h
        cc -c main.c
kbd.o : kbd.c defs.h command.h
        cc -c kbd.c
command.o : command.c defs.h command.h
        cc -c command.c
display.o : display.c defs.h buffer.h
        cc -c display.c
insert.o : insert.c defs.h buffer.h
        cc -c insert.c
search.o : search.c defs.h buffer.h
        cc -c search.c
files.o : files.c defs.h buffer.h command.h
        cc -c files.c
utils.o : utils.c defs.h
        cc -c utils.c
clean :
        rm edit $(objects)

2.5 让 make 推断命令

其实并不需要把编译每个 C 源文件的命令一一写出来,因为 make 能自己推断出来。make 内置了一条隐含规则,可以用 cc -c 命令从同名的 .c 文件更新出 .o 文件。例如,make 会使用命令 cc -c main.c -o main.omain.c 编译成 main.o。因此,我们可以从目标文件的规则中省去命令。参阅使用隐含规则

.c 文件以这种方式被自动使用时,它也会被自动加入前置条件列表。因此,只要我们省略了命令,就也可以从前置条件中省略 .c 文件。

把以上两处改动都应用,再使用前面建议的变量 objects,整个例子就变成下面这样:

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
        rm edit $(objects)

在实际工作中,我们就是这样编写 makefile 的。(关于 clean 所涉及的一些复杂情况,会在别处说明。参阅伪目标(Phony)命令中的错误。)

隐含规则非常方便,所以是很重要的功能。今后你会经常看到它们被使用。

2.6 另一种 Makefile 写法

当 makefile 中的目标全部仅由隐含规则生成时,还可以采用另一种写法。在这种写法中,条目不再按目标分组,而是按前置条件分组。实际写出来是下面这样:

objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

edit : $(objects)
        cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

这里 defs.h 被指定为所有目标文件的前置条件,而 command.hbuffer.h 则分别是各自所列出的那些特定目标文件的前置条件。

这种写法是否更好,见仁见智:它更紧凑,但有些人不喜欢它,因为他们觉得把关于每个目标的所有信息集中放在一处更清晰。

2.7 清理目录的规则

编译程序并不是你唯一想为之编写规则的场景。在 makefile 中,除了编译程序之外,通常还会写明几项其他工作的做法。例如,如何删除所有目标文件和可执行文件,以便把目录清理(clean)干净。

为我们这个示例编辑器编写一条清理用的 make 规则,可以这样写:

clean:
        rm edit $(objects)

在实际中,为了应对一些意料之外的情况,我们可能想把这条规则写得更讲究一些。那时我们会这样写:

.PHONY : clean
clean :
        -rm edit $(objects)

这样写可以防止 make 在真有一个名为 clean 的实际文件时被搞糊涂,并且即使 rm 报错也会继续执行。(参阅伪目标(Phony)命令中的错误。)

这样的规则不应放在 makefile 的开头,因为我们可不希望它被默认执行!所以在这个示例 makefile 中,我们希望让重新编译编辑器的 edit 规则始终保持为默认目标。

由于 clean 不是 edit 的前置条件,所以如果不带参数地执行 make 命令,这条规则根本不会运行。要让这条规则运行,必须输入 make clean。参阅如何运行 make


上一章 | 下一章 | 目录 | 英文原版(gnu.org)