makefile 中,目标文件(target)包含:执行文件 edit 和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。记住,make 并不管命令是怎么工作的,他只管执行所定义的命令。make 会比较 targets 文件和 prerequisites 文件的修改日期,如果 prerequisites 文件的日期要比 targets 文件的日期要新,或者 target 不存在的话,那么,make 就会执行后续定义的命令。
这里要说明一点的是,clean 不是一个文件,它只不过是一个动作名字,有点像 C 语言中的 lable 一样,其冒号后什么也没有,那么,make 就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个lable 的名字。这样的方法非常有用,我们可以在一个 makefile 中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。
make 是如何工作的
在默认的方式下,也就是我们只输入 make 命令。那么,
1、make 会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
3、 如果 edit 文件不存在, 或是 edit 所依赖的后面的 .o 文件的文件修改时间要比 edit这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
4、如果 edit 所依赖的.o 文件也存在,那么 make 会在当前文件中找目标为.o 文件的依赖性,如果找到则再根据那一个规则生成.o 文件。 (这有点像一个堆栈的过程)
5、 当然,你的 C 文件和 H 文件是存在的啦, 于是 make 会生成 .o 文件, 然后再用 .o 文件生命 make 的终极任务,也就是执行文件 edit 了。
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
我们可以看到[.o]文件的字符串被重复了两次, 如果我们的工程需要加入一个新的[.o]文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在 clean 中)。当然,我们的 makefile 并不复杂,所以在两个地方加也不累,但如果 makefile 变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了 makefile 的易维护,在 makefile 中我们可以使用变量。makefile 的变量也就是一个字符串,理解成 C 语言中的宏可能会更好。
比如,我们声明一个变量,叫 objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正不管什么啦,只要能够表示 obj 文件就行了。我们在 makefile 一开始就这样定义:
objects = main.o kbd.o command.o display.o
insert.o search.o files.o utils.o
于是,我们就可以很方便地在我们的 makefile 中以“$(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)
在规则中使用通配符
如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make 支持
三各通配符: “”,“?”和“[…]”。 这是和 Unix 的 B-Shell 是相同的。 波浪号(“~”)
字符在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME 目录
下的 test 目录。而“~hchen/test”则表示用户 hchen 的宿主目录下的 test 目录。(这些都
是 Unix 下的小知识了,make 也支持)而在 Windows 或是 MS-DOS 下,用户没有宿主目录,
那么波浪号所指的目录则根据环境变量“HOME”而定。
通配符代替了你一系列的文件,如“.c”表示所以后缀为 c 的文件。一个需要我们注
意的是,如果我们的文件名中有通配符,如:“”,那么可以用转义字符“\”,如“*”
来表示真实的“”字符,而不是任意长度的字符串。
好吧,还是先来看几个例子吧:
clean:
rm -f *.o
上面这个例子我不不多说了,这是操作系统 Shell 所支持的通配符。这是在命令中的通
配符。
print: .c
lpr -p
?
t
o
u
c
h
p
r
i
n
t
上
面
这
个
例
子
说
明
了
通
配
符
也
可
以
在
我
们
的
规
则
中
,
目
标
p
r
i
n
t
依
赖
于
所
有
的
[
.
c
]
文
件
。
其
中
的
“
? touch print 上面这个例子说明了通配符也可以在我们的规则中,目标 print 依赖于所有的[.c]文 件。其中的“
?touchprint上面这个例子说明了通配符也可以在我们的规则中,目标print依赖于所有的[.c]文件。其中的“?”是一个自动化变量,我会在后面给你讲述。
objects = .o
上面这个例子,表示了,通符同样可以用在变量中。 并不是说[.o]会展开, 不! objects
的值就是“.o”。Makefile 中的变量其实就是 C/C++中的宏。如果你要让通配符在变量中
展开,也就是让 objects 的值是所有[.o]的文件名的集合,那么,你可以这样:
objects := $(wildcard *.o)
这种用法由关键字“wildcard”指出,关于 Makefile 的关键字,我们将在后面讨论。
文件搜寻
在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存
放在不同的目录中。所以,当 make 需要去找寻文件的依赖关系时,你可以在文件前加上路
径,但最好的方法是把一个路径告诉 make,让 make 在自动去找。
Makefile 文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,
make 只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make
就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。
VPATH = src:…/headers
上面的的定义指定两个目录,“src”和“…/headers”,make 会按照这个顺序进行搜
索。目录由“冒号”分隔。 (当然,当前目录永远是最高优先搜索的地方)
另一个设置文件搜索路径的方法是使用 make 的“vpath”关键字(注意,它是全小写
的),这不是变量,这是一个 make 的关键字,这和上面提到的那个 VPATH 变量很类似,但是
它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使
用方法有三种:
1、vpath
为符合模式的文件指定搜索目录。
2、vpath
清除符合模式的文件的搜索目录。
3、vpath
清除所有已被设置好了的文件搜索目录。
vapth 使用方法中的需要包含“%”字符。“%”的意思是匹配零或若干字符,
例如,“%.h”表示所有以“.h”结尾的文件。指定了要搜索的文件集,而
则指定了的文件集的搜索的目录。例如:
vpath %.h …/headers
该语句表示,要求 make 在“…/headers”目录下搜索所有以“.h”结尾的文件。(如果
某文件在当前目录没有找到的话)
我们可以连续地使用 vpath 语句,以指定不同搜索策略。如果连续的 vpath 语句中出现
了相同的,或是被重复了的,那么,make 会按照 vpath 语句的先后顺
序来执行搜索。如:
vpath %.c foo
vpath % blish
vpath %.c bar
其表示“.c”结尾的文件,先在“foo”目录,然后是“blish”,最后是“bar”目录。
vpath %.c foo:bar
vpath % blish
而上面的语句则表示“.c”结尾的文件,先在“foo”目录,然后是“bar”目录,最后
才是“blish”目录。
伪目标
最早先的一个例子中,我们提到过一个“clean”的目标,这是一个“伪目标”,
clean:
rm *.o temp
正像我们前面例子中的“clean”一样,即然我们生成了许多文件编译文件,我们也应
该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make clean”来使用该目
标)
因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,
由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只
有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,
不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显
示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪
目标”。
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make
clean”这样。于是整个过程可以这样写:
PHONY: clean
clean:
rm *.o temp
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同
样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的 Makefile 需要
一口气生成若干个可执行文件,但你只想简单地敲一个 make 完事,并且,所有的目标文件
都写在一个 Makefile 中,那么你可以使用“伪目标”这个特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.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”这个目标新。所以,其它三个目标的规则总是会被决议。也就达
到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目
标”。
随便提一句,从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也
可成为依赖。看下面的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“make clean”将清除所有要被清除的文件。“ cleanobj”和“cleandiff”这两个伪
目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和
“make cleandiff”命令来达到清除不同种类文件的目的。
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同
样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的 Makefile 需要
一口气生成若干个可执行文件,但你只想简单地敲一个 make 完事,并且,所有的目标文件
都写在一个 Makefile 中,那么你可以使用“伪目标”这个特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.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”这个目标新。所以,其它三个目标的规则总是会被决议。也就达
到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目
标”。
随便提一句,从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也
可成为依赖。看下面的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“make clean”将清除所有要被清除的文件。“ cleanobj”和“cleandiff”这两个伪
目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和
“make cleandiff”命令来达到清除不同种类文件的目的。
多目标
@ − − 目 标 文 件 , @--目标文件, @−−目标文件,^–所有的依赖文件,$<–第一个依赖文件。
Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖
于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。当然,多个目标的
生成规则的执行命令是同一个,这可能会可我们带来麻烦,不过好在我们的可以使用一个自
动化变量“
@
”
(
关
于
自
动
化
变
量
,
将
在
后
面
讲
述
)
,
这
个
变
量
表
示
着
目
前
规
则
中
所
有
的
目
标
的
集
合
,
这
样
说
可
能
很
抽
象
,
还
是
看
一
个
例
子
吧
。
b
i
g
o
u
t
p
u
t
l
i
t
t
l
e
o
u
t
p
u
t
:
t
e
x
t
.
g
g
e
n
e
r
a
t
e
t
e
x
t
.
g
−
@”(关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目 标的集合,这样说可能很抽象,还是看一个例子吧。 bigoutput littleoutput : text.g generate text.g -
@”(关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合,这样说可能很抽象,还是看一个例子吧。bigoutputlittleoutput:text.ggeneratetext.g−(subst output,$@) >
@
上
述
规
则
等
价
于
:
b
i
g
o
u
t
p
u
t
:
t
e
x
t
.
g
g
e
n
e
r
a
t
e
t
e
x
t
.
g
−
b
i
g
>
b
i
g
o
u
t
p
u
t
l
i
t
t
l
e
o
u
t
p
u
t
:
t
e
x
t
.
g
g
e
n
e
r
a
t
e
t
e
x
t
.
g
−
l
i
t
t
l
e
>
l
i
t
t
l
e
o
u
t
p
u
t
其
中
,
−
@ 上述规则等价于: bigoutput : text.g generate text.g -big > bigoutput littleoutput : text.g generate text.g -little > littleoutput 其中,-
@上述规则等价于:bigoutput:text.ggeneratetext.g−big>bigoutputlittleoutput:text.ggeneratetext.g−little>littleoutput其中,−(subst output,
@
)
中
的
“
@)中的“
@)中的“”表示执行一个 Makefile 的函数,函数名为 subst,
后面的为参数。关于函数,将在后面讲述。这里的这个函数是截取字符串的意思,“
@
”
表
示
目
标
的
集
合
,
就
像
一
个
数
组
,
“
@”表 示目标的集合,就像一个数组,“
@”表示目标的集合,就像一个数组,“@”依次取出目标,并执于命令。