跟我一起写Makefile读书笔记(一)

第一部分 概述
第二部分 关于程序的编译和链接
第三部分 Makefile介绍
第四部分 Makefile总述
第五部分 书写规则

第一部分 概述

Makefile文件定义了一些列的规则来指定,一个工程中哪些源文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至与进行更复杂的功能操作。
Makefile像Shell脚本一样,”自动化编译”整个工程,其中也可以执行操作系统的命令。
Make是一个解释makefile中指令的命令工具。

第二部分 关于程序的编译和链接

一般来说,无论是C、C++,首先要把源文件编译成中间代码文件,在Windos下是.obj文件,UNIX下是.o文件,即ObjectFile,这个动作叫编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫做链接(link)
编译时,编译器只检查程序语法,和函数、变量是否被声明。如果未被声明,编译器则会给出一个warning,但可以生成Object File。链接程序时,主要是链接函数和全局变量,链接器只会在Object File中找寻函数的实现,而并不管函数所在的源文件,如果找不到就会报链接错误码(Linker Error),意思是链接器未能找到函数的实现,你需要指定函数的Object File。
大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以要给中间目标文件打包,在Windows下这种包叫”库文件”(Library File),也就是.lib文件,在UNIX下,是Archive File,也就是.a文件。

第三部分 Makefile介绍

make命令执行时,需要一个Makefile文件,以告诉make命令需要怎么样的去编译和链接程序。

Makefile的规则

1
2
3
4
target ... : prerequistes ...
command
...
...

target就是一个目标文件,可以是ObjectFile,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后序的”伪指标”章节中会有叙述。
prerequistes就是要生成那个target所需要的文件或是目标。
command就是make需要执行的命令(任意的Shell命令)。
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。

一个示例

假设一个工程有3个头文件,和8个C文件,并假定有3个规则:

  1. 如果这个工程没有编译过,那么所有C文件都要编译并被链接。
  2. 如果这个工程的某几个C文件被修改,那么只编译被修改的C文件,并链接目标程序。
  3. 如果这个工程的头文件被修改了,那么需要编译引用这些头文件的C文件,并链接目标程序。

基于上述条件和规则编写Makefile文件来高速make命令如何编译和链接这些文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 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”或”Makefile”的文件中,然后再该目录下直接输入命令make就可以生成执行文件edit。执行make clean可以删除执行文件和所有的中间目标文件。
在这个makefile中,目标文件(target)包含:执行文件edit和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些.c文件和.h文件。每一个.o文件都有一组以来文件,而这些.o文件又是执行文件edit的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统指令,一定要以Tab键作为开头。make不管命令是怎么工作的,它只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么make就会执行后续定义的命令。
特别说明clean并不是一个文件,它只不过是一个动作名字,类似C语言中的label,冒号后面什么也没有,那么,make就不会自动去找文件的依赖性,也就是不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显的指出这个label的名字,这里就是make clean。这样的方法可以让我们在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份等等。

make是如何工作的

默认方式下make的工作流程(即只输入make命令):

  1. make会在当前目录下寻找名字为”Makefile”或”makefile”的文件。
  2. 若找到,它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件。上例中是”edit”这个文件。
  3. 若edit文件不存在,或是edit所依赖的后面的.o文件的文件修改时间比edit文件新,那么它就会执行后面所定义的命令来生成edit文件。
  4. 若edit文件所依赖的.o文件也不存在,那么make会在当前文件中寻找目标为.o文件的文件(即.o文件所依赖的文件),如果找到则再根据那一个规则生成.o文件(有点像堆栈的过程)。
  5. 由于源文件C文件和H文件是存在的,于是make会生成.o文件,然后再利用.o文件生成执行文件edit即最终的目标文件。

这就是整个make的依赖性,make会一层又一层地找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理,make只管文件的依赖性,即make只负责找到依赖关系,冒号后面的文件若不存在则直接停止工作。因此像clean这种没有被第一个目标文件或间接关联的,它后面所定义的命令将不会被自动执行,但若希望make执行则使用命令make clean(指令形式:make label)。
在上例中若整个工程已经被编译过了,当其中一个源文件被修改了,例如file.c,那么根据依赖性可知file.o文件会被重新编译(也就是在这个依赖关系后面所定义的命令),又因为此时file.o修改时间比edit新,所以edit文件也会被重新链接(详见edit目标文件后定义的命令)。

makefile中使用变量

为了保证makefile的易维护,在makefile中可以使用变量。比如工程中需要加入新的.o文件,若不使用变量则需要在多处进行修改并有可能出现错误。
makefile中的变量是一个字符串,可以理解成C语言中的宏。
声明一个变量叫objects

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

之后在makefile中以$(objects)的方式使用这个变量,改良后的makefile文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 files.c
utils.o : utils.c defs.h
cc -c utils.c
clean:
rm edit $(objects)

如果有新的.o文件加入,只需简单地修改objects变量即可。

让make自动推导

GNU的make可以自动推导文件以及文件依赖关系后面的命令,因此每个.o文件后不需要再写上类似的命令。
只要make看到一个.o文件,它就会自动的把.c文件加在依赖关系中。于是再修改makefile文件如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

这种方法,就是make的”隐晦规则”。.PHONY用来在文件内容中表示clean是个伪目标文件。

另类风格的makefile

makefile文件可以通过将重复的.h和.o文件收拢起来做到更进一步的简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

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

对比之前的makefile文件很容易发现这种风格的makefile文件的书写规则。此书的作者并不喜欢这种风格,他认为之后可能会有两个问题:一是文件的依赖关系看不清楚;二是如果文件一多,要加入几个新的.o文件很难理清楚。

清空目标文件的规则

每个makefile中都需要写一个清空目标文件(.o和执行文件)的规则。这不仅便于重编译,还有利于保持文件的清洁。
一般的风格:

1
2
clean :
rm edit $(objects)

更稳稳健的做法是:

1
2
3
.PHONY : clean
clean :
-rm edit $(objects)

.PHONY表示clean是一个”伪目标”。在rm命令前面加上一个-(小减号)的意思是,也许某些文件出现问题,但不要管,继续做后面的事情。
clean的规则不要放在文件的开头,不然会变成make的默认目标。不成文的规矩是”clean从来都是放在文件的最后”。

第四部分 Makefile 总述

Makefile里有什么?

Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

显式规则

显式规则说明,如何生成一个或多个的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。(Makefile文件的主体)

隐晦规则

因为make具有自动推导的规则,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。

变量的定义

在Makefile中定义的变量,一般都是字符串,类似C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。

文件指示

包括了三个部分,一个是在一个Makefile中引用另一个Makefile,类似C语言中的include;另一个是根据某些情况指定Makefile中的有效部分,类似C语言中的预编译#if;还有一个就是定义一个多行的命令。文件指示部分后面后更详细的介绍。

注释

Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用”#”字符,与Python语言的注释符号相同。如果在Makefile中使用”#”字符,可以用反斜杠进行转移”#“。

特别注意,Makefile中的命令必须要以[Tab]键开始

Makefile的文件名

默认情况下,make指令会在当前目录下按顺序寻找文件名为”GNUmakefile”、”makefile”、”Makefile”的文件,解释这个名字。作者建议最好使用”Makefile”这个文件名。最好不要使用”GNUmakerfile”,这个文件是GNU的make识别的。有些make只对全小写的”makefile”文件名敏感,但是基本上来说,大多数的make都支持”makefile”和”Makefile”这两种默认文件名。
此外还可以使用别的文件名来书写Makefile,比如:”Make.Linux”,”Make.Solaris”,”Make.AIX”等。如果要指定特定的Makefile,可以使用make的-f--file参数,如:make -f Make.Linuxmake --file Make.AIX

引用其它的Makefile

在Makefile使用include关键字可以把别的Makefile包含金来,类似C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。
include的语法是:include <filename>
filename可以是当前操作系统Shell的文件模式(可以包含路径和通配符)在include前面可以有一些空字符,但是绝不能是[Tab]键开始。include<filename>可以用一个或多个空格隔开。
举例有一些Makefile:a.mk、b.mk、c.mk和foo.make,以及一个变量$(bar),其中包含了e.mk和f.mk,那么include foo.make *.mk $(bar)等价于include foo.make a.mk b.mk c.mk e.mk f.mk

make指令开始时会首先寻找include所指出的其它Makefile,并把其内容安置在对应位置,如果文件中没有指定其这些Makefile文件的绝对路径或是相对路径,make会在当前目录下首先寻找,如果当前目录下没有找到,那么make还会在下面几个目录下找:

  1. 如果make执行时有-I--include-dir参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 如果目录\/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。如果有文件没有找到,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成Makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。可以通过在include前加一个减号”-“让make不理会那些无法读取的文件而继续执行。
    如:-include <filename>
    其表示无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和-include是一样的。

环境变量 MAKEFILES

如果在当前环境中定义了环境变量MAKEFILES,那么make会把这个变量中的值当作一个类似include的动作。这个动作中的值是其它的Makefile用空格分隔。只是,它和include不同的是,从这个环境变量中引入的Makefile的”目标”不会起作用,如果环境变量中定义的文件发现错误,make也不会理会。
作者建议不使用这个环境变量,因为只要这个环境变量一旦被定义,那么使用make时所有的Makefile都会受到它的影响。如果Makefile出现了怪事,可以查看当前环境中是不是定义这个变量。

make的工作方式

GNU的make工作时的执行步骤如下:

  1. 读入所有的Makefile;
  2. 读入被include的其它Makefile;
  3. 初始化文件中的变量;
  4. 推导隐晦规则,并分析所有规则;
  5. 为所有的目标文件创建依赖关系链;
  6. 根据依赖关系,决定哪个目标要重新生成;
  7. 执行生成命令;

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,make将使用拖延战术将其展开在使用的位置(即不会立即展开),如果变量出现在已改关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

第五部分 书写规则

规则包含两个部分,一个是依赖关系,一个是生成目标的方法。
在Makefile中,规则的顺序是很重要的。因为Makefile中只应有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让make知道最终目标是什么。Makefile中第一条规则中的第一个目标被确立为最终的目标,make完成的就是这个目标。

规则举例

1
2
foo.o : foo.c defs.h # foo模块
cc -c -g foo.c

在这个例子中,foo.o是我们的目标,foo.c和defs.h是目标所依赖的源文件并且只有一个命令cc -c -g foo.c(以Tab键开头)。此例说明两个问题:

  1. 文件的依赖关系,foo.o依赖于foo.c和defs.h的文件,如果foo.c和defs.h的文件日期比foo.o文件日期新,或是foo.o不存在,那么依赖关系发生。
  2. 如果生成(更新)foo.o文件。cc命令说明了如何生成foo.o这个文件。

规则的语法

1
2
3
targets : prerequisites
command
...

或是这样:

1
2
targets : prerequisites ; command
command

targets是文件名,以空格分开,可以使用通配符,目标文件可以是一个或多个。
command是命令行,如果其不与target:prerequisites在一行,那么必须以[tab键开头],如果和prerequisites在一行,那么可以用分号作为分隔。
prerequisites是目标所依赖的文件(或依赖目标)。

在规则中使用通配符

make支持三个通配符:”*“,”?”和”[…]”。和Unix的B-Shell是相同的。波浪号”~”与Unix下表示目录的功能相同。而在Windows或是MS-DIS下,用户没有宿主目录,那么波浪号所指的目录则根据环境变量”HOME”而定。
如果文件名中有通配符,可以使用转义字符”\”。

1
objects = *.o

上面这个例子中表示通配符同样可以用在变量中。并不是说[.o]会展开,不!objects的值就是”.o”。Makefile中的变量其实就是C/C++中的宏。如果要让通配符在变量中展开,也就是说objects的值是所有.o文件名的集合,可以使用关键字wildcard

1
objects := $(wildcard *.o)

文件搜寻

设置文件搜索路径的方法有两种,一种是Makefile文件中的特殊变量VPATH;另一种是vpath关键字(全小写)。

特殊变量VPATH

Makefile文件中的特殊变量VPATH可以实现指定寻找依赖文件时的路径。如果没有这个变量,make只会在当前的目录中寻找依赖文件和目标文件,通过定义VPATH可以使得make在当前目录找不到的情况下到指定的目录下寻找文件。

1
VPATH = src:../headers

上面的定义指定了两个目录”src”和”../headers”,make会按照这个顺序进行搜索。目录有“冒号”分隔(当前目录永远是最高优先搜索的地方)。

关键字vpath

vpath不是变量,而是make的关键字,虽然它和上面提到的VPATH变量类似,但是实际它更为灵活。它可以指定不同的文件在不同的搜索目录。它的使用方法有三种:

  1. vpath <pattern> <directories> 为符合模式的<pattern>的文件指定搜索目录<directories>
  2. vpath <pattern> 清除符合模式<pattern>的文件的搜索目录;
  3. vpath 清除所有已被设置好了的文件搜索目录;

vpath使用方法中的<pattern>需要包含“%”字符。“%”的意思是匹配零活若干字符。<pattern>指定了搜索的文件集,而<directories>则指定了<pattern>的文件集的搜索的目录。例如:
vpath %.h ../headers
该语句表示,要求make在“../headers”目录下搜索所有以“.h”结尾的文件(如果某文件在当前目录没有找到的话)。
可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的<pattern>,或是被重复了的<pattern>,那么make会按照vpath语句的先后顺序来执行搜索。比如:

1
2
3
vpath %.c foo
vpath % blish
vpath %.c bar

其表示“.c”结尾的文件,先在“foo”目录,然后是“blish”,最后“bar”目录。

1
2
vpath %.c foo:bar
vpath % blish

而上路的语句则表示“.c”结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。

伪目标

之前提到clean的目标是一个“伪目标”,因为并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。只能通过显示地指明这个“目标”才能让其生效。“伪目标”的取名不能和文件名重名,不然去就失去了“伪目标”的意义了。
为了避免和文件重名的情况出现,可以使用一个特殊标记.PHONY来显示地指明一个目标是“伪目标”,向make说明不管是否有这个文件,这个目标就是“伪目标”。只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”,整个过程可以这么写

1
2
3
.PHONY : clean
clean :
rm *.o temp

伪目标一般没有依赖的文件,但却可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。如果Makefile需要生成若干个可执行文件,但只想执行一次make命令,并且所有的目标文件都写在一个Makefile中,那么可以使用“伪目标”这个特性:

1
2
3
4
5
6
7
8
9
10
11
all : prog1 prog2 prog3
.PHONY : all

prog1 : prog2.o utils.o
cc -o prog1 prog1.o utils.o

prog2 : prog2.o
cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o

因为Makefile中的第一个目标会被作为默认目标,所以声明·一个“all”作为伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如“all”这个目标新。所以,其它三个目标的规则总是会被决议,也就达到了一次生成多个目标的目的。(这部分书中介绍的不是很详尽,翻看这篇文章Makefile伪目标 - 作业部落 Cmd Markdown 编辑阅读器才真正理解,引用部分片段这书中这部分加以解释)

执行make时,目标all被作为终极目标。为了完成对它的更新,make会创建(不存在)或者重建(虽存但旧)目标all的所有依赖文件prog1prog2prog3。当需要单独更新某一个程序时,可以通过make的命令行选项来明确指定需要重建的程序,例如make prog1

由上例可以看出目标可以成为依赖。所以,伪目标同样也可成为依赖。当一个伪目标作为另外一个伪目标依赖时,make将其作为另外一个伪目标的子例程来处理(可以这样理解:其作为另外一个伪目标必须执行的部分,就像C语言中的函数调用一样)。见下例:

1
2
3
4
5
6
7
8
9
10
.PHONY : cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
rm program

cleanobj :
rm *.o

cleandiff :
rm *.diff

make clean将清除所有要被清除的文件。cleanobjcleandiff这两个伪目标有点像“子程序”的意思。可以通过输入make cleanallmake cleanobjmake cleandiff命令来达到清除不同种类文件的目的。

说明:通常在清除文件的伪目标所定义的命令中rm使用选项-f--force)来防止在缺少删除文件时出错并退出,使make clean过程失败。也可以在rm之前加上-来方式rm错误退出,使用这种方式时make会提示错误信息但不会退出。为了不看到这些错误信息,需要使用上述的第一种方式。另外make存在一个内嵌隐含变量RM(全大写),它被定义为:RM = rm -f。因此,在书写clean规则的命令行时,可以使用变量$(RM)来代替rm,这样可以避免出现一些不必要的麻烦!是推荐的用法。

多目标

书中这部分内容有些难理解,因此引用文章makefile 多目标和多规则 - iosxiaoming的博客 - CSDN博客中关于Makefile多目标的介绍并与书中内容结合整理,文章中使用的示例与书中使用的示例相同。

Makefile的一个规则中可以有多个目标,规则所定义的命令对所有目标有效。多目标意味着所有的目标具有相同的依赖文件,多目标通常用在以下两种情况:

  • 仅需要一个描述依赖关系的规则,不需要在规则中命令。例如:
    kbd.o command.o files.o : command.h
    这个规则实现了同时给三个目标文件制定一个依赖文件。
  • 对于多个具有类似重建命令的目标。重建这些目标的命令不需要完全相同。因为可以在命令行中使用自动化变量$@(关于自动化变量在后面讲述,这个变量表示目前规则中所有目标的集合)来引用具体的目标,完成重建。
    例如

    1
    2
    bigoutput littleoutput : text.g
    generate text.g -$(subst output,,$@) > $@

    上述规则等价于:

    1
    2
    3
    4
    bigoutput : text.g
    generate text.g -big > bigoutput
    littleoutput : text.g
    generate text.g -little > littleoutput

    其中,-$(subst output,,$@)中的“$”表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数也在后面讲述,subst函数的作用是替换(subst函数的具体用法:makefile中函数subst函数 - Yriuo - CSDN博客),$@表示目标的集合,就像一个数组,$@依次取出目标,并执行命令。
    例子中的generate根据命令行参数来决定输出文件的类型。使用make的字符串处理函数subst来根据目标产生对应的命令行选项。

虽然在多目标的规则中,可以根据不同目标使用不同的命令(在命令行中使用自动化变量$@)。但是,多目标规则并不能根据目标文件自动改变依赖文件(上边例子中使用自动化变量$@改变规则的命令一样)。需要实现这个目的要用到make的静态模式。

静态模式

静态模式可以更加容易地定义多目标的规则,可以让规则变得更加有弹性和灵活。
语法如下:

1
2
3
<targets...>: <target-pattern>: <prereq-patterns...>
<commands>
...

target定义了一系列的目标文件,可以有通配符,是目标的一个集合。
target-pattern是指明了targets的模式,也就是目标集模式。
prereq-patterns是目标的依赖模式,它对target-pattern形式的模式再进行一次依赖目标的定义。
举例说明,若把<target-pattern定义为“%.o”,意思是<target>集合中都是以“.o”结尾的,而若把<prereq-patterns>定义为“%.c”,意思是对<target-pattern>所形成的目标集进行二次定义,其计算方法是取<target-pattern>模式中的“%”(也就是去掉了.o这个结尾),并为其加上.c这个结尾形成新集合。
因此“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果文件名中有“%”,那么可以使用反斜杠“\”进行转义,来标明真实的“%”字符。
看一个例子:

1
2
3
4
5
6
objects = foo.o bar.o

all : $(objects)

$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@

上例中指明了我们的目标从$object中获取(对应语法中<targets…>),“%.o”表明要所有以“.o”结尾的目标(对应语法中<target-pattern…>,也就是“foo.o bar.o”,也就是变量$object集合的模式,而依赖模式(<prereq-patterns…>)“%.c”则取模式“%.o”的“%”,也就是“.foo bar”,并为其加上“.c”的后缀,因此依赖目标就是“foo.c bar.c”。而命令中的$<$@则是自动化变量,$<表示所有的依赖目标集(也就是“foo.c bar.c”),$@表示目标集(也就是“foo.o bar.o”。因此上面的规则展开后等价于下面的规则:

1
2
3
4
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o

若“%.o”有几百个,通过使用这种静态模式规则可以很有效率的写完一堆规则。再看一个例子:

1
2
3
4
5
6
files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<

$(filter %.o,$(files))表示调用Makefile的filter函数,过滤$filter集,只要其中模式为“%.o”的内容。

自动生成依赖性

在Makefile中,依赖关系可能会需要包含一系列的头文件。大多数的C/C++编译器都支持一个-M的选项,即自动寻找源文件中包含的头文件,并生成一个依赖关系。例如main.c中有#include "defs.h",那么可以执行:

1
cc -M main.c

其输出是:

1
main.o : main.c defs.h

由编译器自动生成依赖关系,这样就不必再手动书写若干文件的依赖关系。需要特别说明一下,如果使用GNU的C/C++编译器,得用-MM参数。不然,-M参数会把一些标准库的头文件也包含进来。

如何将编译器自动生成依赖关系的功能与Makefile联系在一起?
GNU组织建议把编译器为每个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,.d文件中存放对应.c文件的依赖关系。
书中给出一个模式规则来生成.d文件:

1
2
3
4
5
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$

这个规则的意思是,所有的.d文件依赖于.c文件,rm -f $@的意思是删除所有的目标,也就是.d 文件,第二行的意思是,为每个依赖文件$<,也就是.c文件生成依赖文件,$@表示模式%.d文件,如果有一个C文件是name.c,那么%就是name,$$$$意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。
总而言之,这个模式规则要做的事就是在编译器生成的依赖关系中加入.d文件的依赖,即把依赖关系:

1
main.o : main.c defs.h

转换成:

1
main.o main.d : main.c defs.h

因此.d文件会自动更新并会自动生成。除了可以在这个.d文件中加入依赖关系,还可以加入生成的命令,让每个.d文件都包含一个完整的规则。完成这个工作后需要将这些自动生成的规则放进主Makefile中。可以使用Makefile的include命令,来引入别的Makefile文件,例如:

1
2
sources = foo.c bar.c
include $(sources: .c =.d)

上述语句中的$(sources: .c =.d)中的.c = .d的意思是做一个替换,把变量$(sources)中所有.c的字串都替换成.d,关于这个“替换”的内容,书中后面会有更为详细的讲述。此外还需要注意次序,因为include是按次序来载入文件,最先载入的.d文件中的目标会成为默认目标。