第5章 使用 make
make
是一个解释
makefile
文件中的指令的命令工具。一般来说,大多数的
IDE
都有这个命令,比如
Delphi
的
make
,
Visual C++
的
nmake
,
Linux
下
GNU
的
make
。
什么是
makefile
文件?
make
命令执行时,需要一个
makefile
文件,以告诉
make
命令需要怎样去编译和连接程序。或许很多
Windows
程序员都不知道这个工具,因为那些
Windows
的
IDE
都没有提供该功能。作
为
一个专业的程序员尤其是作为
Linux
下的程序员,要进行
Linux
下的软件编程,理解
makefile
文件是必需的,因为会不会写
makefile
文件,直接关系到是否具备完成大型工程的能力,
makefile
文件关系到了整个工程的编译规则。这就好像尽管现在有很多
HTML
编辑器,但如果想成为一个专业网页设计师,还需要了解
HTML
标识的含义一样。
一个工程中的源文件数量很多,其按类型、功能、模块分别放在若干个目录中,
makefile
文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行更复杂的操作。
makefile
文件就像一个
Shell
脚本,其中也可以执行操作系统的命令。
makefile
文件带来的好处是
——
“
自动化编译
”
,一旦写好,只需要一个
make
命令,就可自动编译整个工程,极大地提高了软件开发的效率。
5.1 makefile实例文件分析
本部分将用一个示例来说明如何建立一个
makefile
文件,以便给大家一个感性认识。这个示例来源于
GNU
的
make
使用手册,工程中有
8
个
C
文件和
3
个头文件,要写一个
makefile
文件来告诉
make
命令如何编译和连接这几个文件。
makefile
文件的操作规则是:
●
如果这个工程没有编译过,所有C文件都要编译并被连接。
●
如果这个工程的某几个C文件被修改,只需编译被修改的C文件,并连接目标程序。
●
如果这个工程的头文件被改变了,需要编译引用了这几个头文件的C文件,并连接目标程序。
只要
makefile
文件写得足够好,所有的这一切,只用一个
make
命令就可以完成,
make
命令会自动智能地根据当前文件的修改情况来确定哪些文件需要重新编译,从而自动编译所需要的文件并连接目标程序。
在讲述这个
makefile
文件之前,还是先来粗略地看一看下面的代码:
target ... : prerequisites ...
command
...
...
上面的代码中
,
target
是一个目标文件
,
可以是
Object
文件
,
也可以是执行文件
,
还可以是一个标签
(Label)
。对于标签的特性,在
5.3.5
节中讲解。
prerequisites
是要生成的
target
所需要的文件或是目标。
command
是
make
需要执行的命令
(
任意的
Shell
命令
)
。
这是一个文件的依赖关系,
target
这一个或多个的目标文件依赖于
prerequisites
中的文件,其生成规则定义在
command
中。
prerequisites
中如果有一个以上的文件比
target
文件更新的话,
command
所定义的命令就会执行。这就是
makefile
文件的规则,也就是
makefile
文件中最核心的内容。下面结合实例作详细说明。
5.1.1 make与makefile文件的关系
下面通过一个实例来讲述
make
与
makefile
文件的关系。
实例
5-1
是一个完整的
makefile
文件,在一个工程中有
3
个头文件和
8
个
C
文件,其中应用到了前面讲述的3个规则。
实例
5-1
edit : main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
gcc -o edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
main.o : main.c defs.h
gcc -c main.c
kbd.o : kbd.c defs.h command.h
gcc -c kbd.c
command.o : command.c defs.h command.h
gcc -c command.c
display.o : display.c defs.h buffer.h
gcc -c display.c
insert.o : insert.c defs.h buffer.h
gcc -c insert.c
search.o : search.c defs.h buffer.h
gcc -c search.c
files.o : files.c defs.h buffer.h command.h
gcc -c files.c
utils.o : utils.c defs.h
gcc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
提示:
反斜杠
“
/
”
是换行符的意思。这样使
makefile
文件更易读。可以把这个内容保存在
“
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
语言中的
lable
一样,冒号后什么也没有,这样
make
就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在
make
命令后明显地指出这个
lable
的名字。这样的方法非常有用,可以在一个
makefile
文件中定义不用的编译或是和编译无关的命令,比如程序的打包或备份等。
在默认方式下,只输入
make
命令。其会做如下工作:
make会在当前目录下找名字为“makefile文件”或“makefile文件夹”的文件。如果找到,它会找文件中的第一个目标文件(target)。在上面的例子中,它会找到edit这个文件,并把这个文件作为最终的目标文件;如果edit文件不存在,或是edit所依赖的后面的 .o 文件的修改时间要比edit这个文件新,它就会执行后面所定义的命令来生成edit文件。
如果edit所依赖的.o文件也存在,make会在当前文件中找目标为.o文件的依赖性,如果找到,则会根据规则生成.o文件(这有点像一个堆栈的过程)。
当然,C文件和H文件如果存在,make会生成 .o 文件,然后再用 .o 文件生成make的最终结果,也就是执行文件edit。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,make就会直接退出,并报错。而对于所定义的命令的错误,或是编译不成功,make就不会处理。如果在make找到了依赖关系之后,冒号后面的文件不存在,make仍不工作。
通过上述分析,可以看出像
clean
这样没有被第一个目标文件直接或间接关联时,它后面所定义的命令将不会被自动执行,不过,可以显式使
make
执行。即使用命令
make clean
,以此来清除所有的目标文件,并重新编译。
在编程中,如果这个工程已被编译过了,当修改了其中一个源文件时,比如
file.c
,根据依赖性,目标
file.o
会被重新编译
(
也就是在这个依赖性关系后面所定义的命令
)
,则
file.o
文件也是最新的,即
file.o
文件的修改时间要比
edit
要新,所以
edit
也会被重新连接了。而如果改变了
command.h
,
kdb.o
、
command.o
和
files.o
都会被重新编译,并且
edit
会被重新连接。
5.1.2 在makefile文件中使用变量
在上面的例子中,先通过实例
5-2
来看看
edit
的规则。
实例
5-2
edit : main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
gcc -o edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
可以看到,
[.o]
文件的字符串被重复了两次。如果这个工程需要加入一个新的
[.o]
文件,需要在两个位置插入
(
实际是3个位置,还有一个位置在
clean
中
)
。当然,这个
makefile
文件并不复杂,所以在两个位置加就可以了。但如果
makefile
文件变得复杂,就要在第
3
个位置插入,该位置容易被忘掉,从而会导致编译失败。所以,为了
makefile
文件的易维护,在
makefile
文件中可以使用变量。
makefile
文件的变量也就是一个字符串,可以理解成
C
语言中的宏。比如,声明一个变量
objects
,在
makefile
文件一开始可以这样定义,见实例
5-3
:
实例
5-3
objects = main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
于是
,
就可以很方便地在
makefile
文件中以
$(objects)
的方式来使用这个变量了。改良版的
makefile
文件就变成实例
5-4
的样子
:
实例
5-4
objects = main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
edit : $(objects)
gcc -o edit $(objects)
main.o : main.c defs.h
gcc -c main.c
kbd.o : kbd.c defs.h command.h
gcc -c kbd.c
command.o : command.c defs.h command.h
gcc -c command.c
display.o : display.c defs.h buffer.h
gcc -c display.c
insert.o : insert.c defs.h buffer.h
gcc -c insert.c
search.o : search.c defs.h buffer.h
gcc -c search.c
files.o : files.c defs.h buffer.h command.h
gcc -c files.c
utils.o : utils.c defs.h
gcc -c utils.c
clean :
rm edit $(objects)
如果有新的
.o
文件加入,只需简单地修改一下
objects
变量就可以了。
5.1.3 让make自动推导依赖关系
GNU
的
make
功能很强大,它可以自动推导文件以及文件依赖关系后面的命令,此时就没有必要在每一个
[.o]
文件后都写上类似的命令,因为
make
会自动识别,并自己推导命令。
只要
make
看到一个
.o
文件,它就会自动把
[.c]
文件加在依赖关系中;如果
make
找到
whatever.o
,则
whatever.c
就会成为
whatever.o
的依赖文件。并且
gcc -c whatever.c
也会被推导出来,于是,
makefile
文件再也不用写得太复杂,可以简化为实例
5-5
。
实例
5-5
objects = main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
edit : $(objects)
gcc -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
是个伪目标文件。
5.1.4 另类风格的makefile文件
即然
make
可以自动推导命令,则可以将过多的
[.o]
和
[.h]
进行简化,删除重复的
[.h]
,结果如实例
5-6
。
实例
5-6
objects = main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
edit : $(objects)
gcc -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
文件变得很简单,但文件依赖关系就显得有点凌乱了。鱼和熊掌不可得兼,所以并不推荐这种风格,一是文件的依赖关系看不清楚,二是文件一多,要加入几个新的
.o
文件,那就更不清楚了。
5.1.5 清空目标文件的规则
每个
makefile
文件中都应该写一个清空目标文件
(.o
和执行文件
)
的规则,这不仅便于重新编译,也很利于保持文件的清洁。一般的风格如下:
实例
5-7
clean:
rm edit $(objects)
更为稳健的做法是:
实例
5-8
.PHONY : clean
clean :
-rm edit $(objects)
前面说过,
.PHONY
表示
clean
是一个
“
伪目标
”
,而在
rm
命令前面加了一个小减号的目的是,如果某些文件出现问题将被忽略,继续进行后面的操作。当然,
clean
的规则不要放在文件的开头,否则会变成
make
的默认目标。不成文的规矩是
“
clean
从来都放在文件的最后
”
。
上面讲述的实例是一个
makefile
文件的概貌,也是编写一般
makefile
文件的基础。
5.2 makefile文件概述
makefile
文件主要包含了
5
部分内容:显式规则、隐式规则、变量定义、文件指示和注释。
●
显式规则。显式规则说明了如何生成一个或多个目标文件。这要由makefile文件的创作者指出,包括要生成的文件、文件的依赖文件、生成的命令。
●
隐式规则。由于make有自动推导的功能,所以隐式的规则可以比较粗糙地简略书写makefile文件,这是由make所支持的。
●
变量定义。在makefile文件中要定义一系列的变量,变量一般都是字符串,这有点儿像C语言中的宏。当makefile文件执行时,其中的变量都会扩展到相应的引用位置上。
●
文件指示。其包括3个部分,一个是在一个makefile文件中引用另一个makefile文件,就像C语言中的include一样;另一个是指根据某些情况指定makefile文件中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。
●
注释。makefile文件中只有行注释,和UNIX的Shell脚本一样,其注释用“#”字符,这个就像C/C++中的“/* */”和“//”一样。如果要在makefile文件中使用“#”字符,可以用反斜框进行转义,如:“/#”。
技巧:
makefile
文件中的命令必须要以
[Tab]
键开始。
默认情况下,
make
命令会在当前目录下按顺序找寻文件名为
“
GNUmakefile
文件
”
、
“
Makefile
文件
”
、
“
makefile
文件
”
的文件,找到后解释这个文件。在这
3
个文件名中,最好使用
“
makefile
文件
”
这个文件名。最好不要用
“
GNUmakefile
文件
”
,这个文件是
GNU
的
make
识别的。注意,一些
make
对
“
Makefile
文件
”
文件名不敏感,但是大多数的
make
都支持
“
Makefile
文件
”
和
“
makefile
文件
”
这两种默认文件名。当然,可以使用别的文件名来书写
makefile
文件,比如:
“
Make.Linux
”
、
“
Make.Solaris
”
、
“
Make.AIX
”
等。如果要指定特定的
makefile
文件,可以使用
make
的
-f
和
--file
参数,如:
make -f Make.Linux
。命令如下:
[david@DAVID david]$ make -f makelinux
在
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
文件
,
并把其内容安置在当前的位置
,
就好像
C/C++
的
#include
指令一样。如果文件都没有指定绝对路径或相对路径,
make
会在当前目录下首先寻找,如果当前目录下没有找到,
make
还会在下面的几个目录下寻找:
●
如果make执行时,有-I或--include-dir参数,make就会在这个参数所指定的目录下去寻找。
●
如果目录<prefix>/include(一般是:/usr/local/bin或/usr/include)存在,make也会去找。
如果有文件没有找到,
make
会生成一条警告信息,但不会马上出现致命错误。它会继续载入其他文件,一旦完成
makefile
文件的读取,
make
会再重试这些没有找到或是不能读取的文件,如果还是不行,
make
才会出现一条致命信息。如果想让
make
不理那些无法读取的文件,而继续执行,可以在
include
前加一个减号
“
-
”
。如:
-include <filename>
这表示,无论
include
过程中出现什么错误,都不会报错而是继续执行。和其他版本
make
兼容的相关命令是
sinclude
,其作用和
inculde
相同。
如果当前环境中定义了环境变量
MAKEFILES
,
make
会把这个变量中的值作一个类似于
include
的动作。这个变量中的值是其他的
makefile
文件,用空格分隔。只是它和
include
不同的是,从这个环境变量中引入的
makefile
文件的
“
目标
”
不会起作用,如果环境变量中定义的文件发现错误,
make
也会忽略。
在这里建议不要使用这个环境变量,因为只要这个变量一旦被定义,当使用
make
时,所有的
makefile
文件都会受到它的影响,这绝不是希望看到的结果。此处是为了提醒大家,也许有时候
makefile
文件出现了问题,此时可以看看当前环境中有没有定义这个变量。
GNU
的
make
工作时的执行步骤如下:
(1)
读入所有的
makefile
文件。
(2)
读入被
include
包括的其他
makefile
文件。
(3)
初始化文件中的变量。
(4)
推导隐式规则,并分析所有规则。
(5)
为所有的目标文件创建依赖关系链。
(6)
根据依赖关系,决定哪些目标要重新生成。
(7)
执行生成命令。
(1)
~
(5)
步为第一个阶段,
(6)
~
(7)
为第二个阶段。第一个阶段中,如果定义的变量被使用了,
make
会把其在使用的位置展开。但
make
并不会马上完全展开,
make
使用的是拖延战术,如果变量出现在依赖关系的规则中,仅当这条依赖被决定要使用了,变量才会在其内部展开。
5.3 make书写规则
make
书写
规则包含两个部分,一个是依赖关系,一个是生成目标的方法。在
makefile
文件中,规则的顺序是很重要的。因为
makefile
文件中只应该有一个最终目标,其他的目标都是被这个目标所连带出来的,所以一定要让
make
知道最终目标是什么。一般来说,定义在
makefile
文件中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。
make
所完成的也就是这个目标。
5.3.1 规则举例
实例
5-9
foo.o : foo.c defs.h # foo
模块
gcc -c -g foo.c
看到这个例子,应该不是很陌生了,前面也已说过,
foo.o
是目标,
foo.c
和
defs.h
是目标所依赖的源文件,此处可使用命令
gcc -c -g foo.c (
以
Tab
键开头
)
。这个规则说明两件事:
●
文件的依赖关系。foo.o依赖于foo.c和defs.h文件,如果foo.c和defs.h文件的日期比foo.o文件的日期新,或者foo.o不存在,则发生依赖关系。
●
如果生成(或更新)foo.o文件,要用到gcc命令。
5.3.2 在规则中使用通配符
如果想定义一系列比较类似的文件,很自然地就想起使用通配符。
make
支持
3
种通配符:
“
*
”
、
“
?
”
和
“
[...]
”
。波浪号
(
“
~
”
)
字符在文件名中也有比较特殊的用途。如果是
“
~/test
”
,就表示当前用户的
$HOME
目录下的
test
目录。而
“
~hchen/test
”
则表示用户
hchen
的宿主目录下的
test
目录
(
这些都是
Linux
下的常识,
make
也支持
)
。而在
Windows
或是
MS-DOS
下,用户没有宿主目录,波浪号所指的目录则根据环境变量
HOME
而定。
通配符代替了一系列的文件,如
“
*.c
”
表示后缀为
c
的文件。一个需要注意的是,如果文件名中有通配符,如
“
*
”
,可以用转义字符
“
/
”
,如
“
/*
”
来表示真实的
“
*
”
字符,而不是任意长度的字符串。
下面还是先来看几个例子:
实例
5-10
clean:
rm -f *.o
实例
5-10
是操作系统
Shell
所支持的通配符。这是在命令中的通配符。
实例
5-11
print: *.c
lpr -p $?
touch print
实例
5-11
说明了通配符也可以在规则中,目标
print
依赖于所有的
[.c]
文件。其中的
“
$?
”
是一个自动化变量,将会在后面作详细介绍。
实例
5-12
objects = *.o
实例
5-12
表示了,通配符同样可以用在变量中。
makefile
文件中的变量其实就是
C/C++
中的宏。如果要让通配符在变量中展开,也就是让
objects
的值成为所有
[.o]
的文件名的集合,可以像实例
5-13
这样:
实例
5-13
objects := $(wildcard *.o)
这种用法由关键字
wildcard
指出。
5.3.3 文件搜寻
在一些大的工程中,有大量的源文件,通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当
make
需要去找寻文件的依赖关系时,可以在文件前加上路径,但最好的方法是把一个路径告诉
make
,让
make
自动去找。
makefile
文件中的特殊变量
VPATH
就是完成这个功能的。如果没有指明这个变量,
make
只会在当前的目录中去找寻依赖文件和目标文件;如果定义了这个变量,
make
就会在当前目录找不到的情况下,到所指定的目录中去找寻文件。
实例
5-14
VPATH = src:../headers
实例
5-14
的定义指定两个目录:
src
和
../headers
。
make
会按照这个顺序进行搜索。目录由
“
冒号
”
分隔
(
当然,当前目录永远是最高优先搜索的位置
)
。另一个设置文件搜索路径的方法是使用
make
的
vpath
关键字
(
注意,它是全小写的
)
,这不是变量,这是一个
make
的关键字,而这和上面提到的那个
VPATH
变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能,它的使用方法有三种:
●
vpath <pattern> <directories>
为符合模式
<pattern>
的文件指定搜索目录
<directories>
。
●
vpath <pattern>
清除符合模式
<pattern>
的文件搜索目录。
●
vpath
清除所有已被设置好了的文件搜索目录。
vapth
使用方法中的
<pattern>
需要包含
“
%
”
字符。
“
%
”
的意思是匹配一个以上的字符,例如,
“
%.h
”
表示所有以
“
.h
”
结尾的文件。
<pattern>
指定了要搜索的文件集,而
<directories>
则指定了
<pattern>
的文件集的搜索目录。例如实例
5-15
:
实例
5-15
vpath %.h ../headers
该语句表示,要求
make
在
../headers
目录下搜索所有以
.h
结尾的文件
(
如果某文件在当前目录没有找到的话
)
。可以连续地使用
vpath
语句,以指定不同搜索策略。如果连续的
vpath
语句中出现了相同的
<pattern>
,或是被重复了的
<pattern>
,
make
会按照
vpath
语句的先后顺序来执行搜索。如实例
5-16
:
实例
5-16
vpath %.
c foo
vpath % blish
vpath %.c bar
其表示
.c
结尾的文件
,
先在
foo
目录
,
然后在
blish
目录
,
最后在
bar
目录中进行搜索。
实例
5-17
vpath %.c foo:bar
vpath % blish
实例
5-17
的语句则表示
.c
结尾的文件
,
先在
foo
目录
,
然后在
bar
目录
,
最后才在
blish
目录中进行搜索。
5.3.4 伪目标
在前面的例
5-1
中,提到过一个
clean
的目标,这是一个
“
伪目标
”
。
实例
5-18
clean:
rm *.o temp
正像前面例子中的
clean
一样,既然生成了许多编译文件,也应该提供一个清除它们的
“
目标
”
以备完整地重新编译时用
(
以
make clean
来使用该目标
)
。因为并不生成
clean
这个文件,
“
伪目标
”
并不是一个文件,只是一个标签。由于
“
伪目标
”
不是文件,所以
make
无法生成它的依赖关系和决定它是否要执行,只有通过显式地指明这个
“
目标
”
才能让其生效。当然,
“
伪目标
”
的取名不能和文件名重名,不然其就失去了
“
伪目标
”
的意义了。
为了避免和文件重名这种情况,可以使用一个特殊的标记
.PHONY
来显式地指明一个目标是
“
伪目标
”
,向
make
说明不管是否有这个文件,这个目标就是
“
伪目标
”
。
PHONY : clean
只要有这个声明,不管是否有
clean
文件,要运行
clean
这个目标,整个过程可以这样写:
.PHONY: clean
clean:
rm *.o temp
伪目标一般没有依赖的文件,但是,也可以为伪目标指定所依赖的文件。伪目标同样可以作为
“
默认目标
”
,只要将其放在最前面即可。比如,如果
makefile
文件需要连续生成若干个可执行文件,而只想简单地输入一个
make
就让其执行,并且所有的目标文件都写在一个
makefile
文件中,可以使用
“
伪目标
”
这个特性,如实例
5-19
。
实例
5-19
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
gcc -o prog1 prog1.o utils.o
prog2 : prog2.o
gcc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
gcc -o prog3 prog3.o sort.o utils.o
makefile
文件中的第
1
个目标会被作为其默认目标,声明了一个
all
的伪目标,其依赖于其他3
个目标。由于伪目标的特性是总会被执行,所以其依赖的那
3
个目标就总不如
all
这个目标新。所以,其他3
个目标的规则总是会被采纳,也就达到了一下子生成多个目标的目的。
.PHONY : all
声明了
all
这个目标为
“
伪目标
”
。
从实例
5-19
可以看出,目标也可以成为依赖关系。所以,伪目标同样也可成为依赖关系,如实例
5-20
。
实例
5-20
.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
命令来达到清除不同种类文件的目的。
5.3.5 多目标
makefile
文件规则中的目标可以不止一个,其支持多目标。有可能多个目标同时依赖于一个文件,并且其生成的命令大体类似,于是就能把其合并起来。当然,多个目标的生成规则的执行命令是同一个,这可能会带来麻烦,不过可以使用一个自动化变量
“
$@
”
。这个变量表示目前规则中所有目标的集合,这样说可能很抽象,如实例5-21
。
实例
5-21
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于实例
5-22
:
实例
5-22
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中
,
-$(subst output
”
$@)
中的
“
$
”
表示执行一个
makefile
文件的函数
,
函数名为
subst
,
后面的为参数。
“
$@
”
表示目标的集合,就像一个数组,
“
$@
”
依次取出目标,并执行命令。
5.3.6 静态模式
静态模式可以更加容易地定义多目标的规则,可以让规则变得更加灵活和有弹性。语法如下:
实例
5-23
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...
●
targets定义了一系列的目标文件,可以有通配符,表示目标的一个集合。
●
target-pattern指明了targets的模式,也就是目标集模式。
●
prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。
如果
<target-pattern>
定义成
“
%.o
”
,表示
<target>
集合中都是以
“
.o
”
结尾的;而如果
<prereq-patterns>
定义成
“
%.c
”
,表示对
<target-pattern>
所形成的目标集进行二次定义。其计算方法是,取
<target-pattern>
模式中的
“
%
”
(
也就是去掉了扩展符
[.o])
,并为其加上扩展符
[.c]
,从而形成新的集合。
所以,
“
目标模式
”
或是
“
依赖模式
”
中都应该有
“
%
”
这个字符,如果文件名中有
“
%
”
,可以使用反斜杠
“
/
”
进行转义,以标明真实的
“
%
”
字符。
看一个例子:
实例
5-24
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(gcc) -c $(CFLAGS) $< -o $@
实例
5-24
中
,
指明了目标从
$object
中获取
,
“
%.o
”
代表所有以
“
.o
”
结尾的目标
,
也就是
foo.o bar.o
,
即变量
$object
集合的模式
,
而依赖模式
“
%.c
”
则取模式
“
%.o
”
的
“
%
”
,
也就是
foo bar
,
并为其加上
“
.c
”
的后缀
,
于是
,
依赖目标就是
foo.c bar.c
。而命令中的
“
$<
”
和
“
$@
”
是自动化变量,
“
$<
”
表示所有的依赖目标集
(
也就是
foo.c bar.c)
,
“
$@
”
表示目标集
(
也就是
foo.o bar.o)
。于是,上面的规则展开后等价于实例
5-25
的规则:
实例
5-25
foo.o : foo.c
$(gcc) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(gcc) -c $(CFLAGS) bar.c -o bar.o
试想,如果“%.o”有几百个,只要用这种很简单的“静态模式规则”就可以写完一堆规则,简化多了。“静态模式规则”的用法很灵活,如果用得好,将是一个很强大的功能。再看一个例子:
实例
5-26
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(gcc) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
$(filter %.o,$(files))
表示调用
makefile
文件的
filter
函数
,
过滤
$filter
集
,
这个例子展示了
makefile
文件更大的弹性。
5.3.7 自动生成依赖性
在
makefile
文件中,依赖关系可能会需要包含一系列的头文件,比如,如果
main.c
中有一句
#include "defs.h"
,依赖关系应该是:
main.o : main.c defs.h
但是,如果是一个比较大型的工程,必须清楚哪些
C
文件包含了哪些头文件,并且,在加入或删除头文件时,也需要小心地修改
makefile
文件,这是一项繁琐的工作。为了避免这种繁琐而又容易出错的工作,可以使用
GCC
的一个
-MM
的选项,即自动寻找源文件中包含的头文件,并生成一个依赖关系。例如,如果执行下面的命令:
gcc -M main.c
其输出是:
main.o : main.c defs.h
于是由编译器自动生成依赖关系,而不必再手动书写若干文件的依赖关系,并由编译器自动生成。
编译器的这个功能如何与
makefile
文件联系在一起呢?因为
makefile
文件也要根据这些源文件重新生成,让
makefile
文件自已依赖于源文件。这样并不现实,不过可以用其他手段来迂回地实现这一功能。
GNU
组织建议把编译器为每一个源文件自动生成的依赖关系放到一个文件中,为每一个
name.c
的文件都生成一个
name.d
的
makefile
文件,
“
.d
”
文件中就存放了对应
“
.c
”
文件的依赖关系。
于是,可以写出
“
.c
”
文件和
“
.d
”
文件的依赖关系,让
make
自动更新或生成
“
.d
”
文件,并把其包含在主
makefile
文件中,就可以自动地生成每个文件的依赖关系了。这里,给出了一个模式规则来产生
“
.d
”
文件:
%.d: %.c
@set -e; rm -f $@; /
$(gcc) -M $(CPPFLAGS) $< > $@.$$$$; /
sed 's,/($*/)/.o[ :]*,/1.o $@ : ,g' < $@.$$$$ > $@; /
rm -f $@.$$$$
这个规则的意思是
,
所有的
“
.d
”
文件依赖于
“
.c
”
文件。
rm -f $@
的意思是删除所有的目标
,
也就是
“
.d
”
文件。第二行的意思是,为每个依赖文件
“
$<
”
,也就是
“
.c
”
文件生成依赖文件,
“
$@
”
表示模式
“
%.d
”
文件,如果有一个
C
文件是
name.c
,
“
%
”
就是
name
。
“
$$$$
”
意为一个随机编号,第
2
行生成的文件有可能是
name.d.12345
,第
3
行使用
sed
命令作了一个替换,第
4
行就是删除临时文件。
总之,这个模式要做的事就是在编译器生成的依赖关系中加入
“
.d
”
文件的依赖,即把依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
于是,
“
.d
”
文件也会自动更新,并会自动生成。当然,在这个
“
.d
”
文件中加入的不只是依赖关系,生成的命令也可一并加入,让每个
[.d]
文件都包含一个完赖的规则。一旦完成这个工作,接下来就要把这些自动生成的规则放进主
makefile
文件中。可以使用
makefile
文件的
include
命令来引入别的
makefile
文件
(
前面讲过
)
,例如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述语句
$(sources:.c=.d)
中的
“
.c=.d
”
的意思是作一个替换
,
把变量
$(sources)
中所有
[.c]
的字串都替换成
[.d]
,
关于这个替换的内容
,
在后面会有更为详细的讲述。当然,使用时应注意次序,因为
include
是按次序来载入文件,最先载入的
[.d]
文件中的目标会成为默认目标。
5.4 使用命令
每条规则中的命令和操作系统
Shell
的命令行是一致的。
make
会按顺序一条一条地执行命令,每条命令必须以
[Tab]
键开头,除非命令紧跟在依赖规则后面的分号后。在命令行之间的空格或是空行会被忽略,但是如果该空格或空行是以
Tab
键开头的,
make
会认为其是一个空命令,除非特别指定一个其他的
Shell
。
makefile
文件中,
“
#
”
是注释符,很像
C/C++
中的
“
//
”
,其后的本行字符都视为注释。
5.4.1 显示命令
通常,
make
会把其要执行的命令行在命令执行前输出到屏幕上。当在命令行前用
“
@
”
字符时,这个命令将不被
make
显示出来。最具代表性的例子是,用这个功能向屏幕显示一些信息。如:
@echo 正在编译XXX模块......
当执行
make
时,会输出
“
正在编译
XXX
模块
……
”
字串,但不会输出命令。如果没有
“
@
”
,
make
将输出:
echo 正在编译XXX模块......
正在编译XXX模块......
如果
make
执行时,带入
make
参数
-n
或
--just-print
,其只是显示命令,但不会执行命令。这个功能有利于调试
makefile
文件,可预览书写的命令的运行顺序及结果。
make
参数
-s
或
--slient
表示全面禁止命令的显示。
5.4.2 执行命令
当依赖目标新于目标时,也就是当规则的目标需要更新时,
make
会一条一条地执行其后的命令。需要注意的是,如果要让上一条命令的结果应用在下一条命令上,应该使用分号分隔这两条命令。比如第一条命令是
cd
命令,希望第二条命令在
cd
之后的基础上运行,就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如
:
示例一:
exec:
cd /home/hchen
pwd
示例二
:
exec:
cd /home/hchen; pwd
当执行
make exec
时
,
第一个例子中的
cd
没起作用
,
pwd
会打印出当前的
makefile
文件目录
;
而第二个例子中
,
cd
就起作用了
,
pwd
会打印出
“
/home/hchen
”
。
5.4.3 命令出错
每当命令运行完后,
make
会检测每个命令的返回码。如果命令返回成功,
make
会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了
(
命令退出码非零
)
,
make
就会终止执行当前规则,这将有可能终止所有规则的执行。
有些时候,命令的出错并不表示就是错误的。例如,
mkdir
命令用于建立一个目录,如果目录不存在,
mkdir
就成功执行,万事大吉;如果目录存在,就会出错。在使用
mkdir
时,不希望因
mkdir
出错而终止规则的运行。此时就要忽略命令的出错信息,此时可以在
makefile
文件中的命令行前加一个减号
“
-
”
(
在
Tab
键之后
)
,则此时不管命令是否出错,都认为是成功的,如实例
5-27
:
实例
5-27
clean:
-rm -f *.o
还有一个办法是
,
给
make
加上
-i
或是
--ignore-errors
参数
,
这样
makefile
文件中的所有命令都会忽略错误。而如果一个规则是以
.IGNORE
作为目标的,这个规则中的所有命令都将会忽略错误。这些是不同级别的防止命令出错的方法,可以根据自己的需要设置。
还有需要提一下的
make
参数是
-k
或是
--keep-going
,这个参数的意思是,如果某规则中的命令出错了,就终止该规则的执行,但继续执行其他规则。
5.4.4 嵌套执行make
在一些大的工程中,会将不同模块及不同功能的源文件放在不同的目录中,可以在每个目录中都书写一个该目录的
makefile
文件,这有利于让
makefile
文件变得更加简洁,而不至于把所有的东西全部写在一个
makefile
文件中,这样维护
makefile
文件时会变得困难。这个技术对于模块编译和分段编译有非常大的好处。
例如,有一个子目录叫
subdir
,这个目录下有个
makefile
文件,来指明这个目录下文件的编译规则。总控的
makefile
文件可以这样书写:
subsystem:
cd subdir && $(MAKE)
其等价于
:
subsystem:
$(MAKE) -C subdir
定义
$(MAKE)
宏变量是因为,也许
make
需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入
subdir
目录,然后执行
make
命令。
把这个
makefile
文件叫做
“
总控
makefile
文件
”
,总控
makefile
文件的变量可以传递到下级的
makefile
文件中
(
如果显式地声明
)
,但是不会覆盖下层的
makefile
文件中所定义的变量,除非指定了
-e
参数。
如果要传递变量到下级
makefile
文件中,可以使用这样的声明:
export <variable ...>
如果不想让某些变量传递到下级
makefile
文件中,可以这样声明:
unexport <variable ...>
示例一:
export variable = value
其等价于
:
variable = value
export variable
等价于:
export variable := value
等价于:
variable := value
export variable
示例二
:
export variable += value
其等价于
:
variable += value
export variable
如果要传递所有的变量,只要一个
export
就行了。后面什么也不用跟,表示传递所有的变量。
需要注意的是,有两个变量,一个是
SHELL
,一个是
MAKEFLAGS
,这两个变量不管是否进行输出,其总是要传递到下层
makefile
文件中。特别是
MAKEFILES
变量,其中包含了
make
的参数信息,如果执行总控
makefile
文件时有
make
参数或是在上层
makefile
文件中定义了这个变量,
MAKEFILES
变量将会是这些参数,并会传递到下层
makefile
文件中,这是一个系统级的环境变量。
但是
make
命令中有几个参数并不往下传递,它们是
-C
、
-f
、
-h
、
-o
和
-W
。如果不想往下层传递参数,可以这样写:
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=
如果定义了环境变量
MAKEFLAGS
,确信其中的选项是大家都会用到的。如果其中有
-t
、
-n
和
-q
参数,将会有意想不到的结果。
还有一个在
“
嵌套执行
”
中比较有用的参数,
-w
或是
--print-directory
会在
make
执行的过程中输出一些信息,并看到目前的工作目录。比如,如果下级
make
目录是
/home/hchen/gnu/make
,如果使用
make -w
来执行,当进入该目录时,会看到:
make: Entering directory '/home/hchen/gnu/make'.
而在完成下层
make
后离开目录时,会看到:
make: Leaving directory '/home/hchen/gnu/make'
当使用
-C
参数来指定
make
下层
makefile
文件时,
-w
会自动打开。如果参数中有
-s (--slient)
或是
--no-print-directory
,
-w
总是失效的。
5.4.5 定义命令包
如果
makefile
文件中出现一些相同命令序列,可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以
define
开始,以
endef
结束,如:
实例
5-28
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
这里
,
run-yacc
是这个命令包的名字
,
其不要和
makefile
文件中的变量重名。在
define
和
endef
中的两行就是命令序列。这个命令包中的第一个命令是运行
Yacc
程序,因为
Yacc
程序总是生成
y.tab.c
的文件,所以第二行的命令就是把这个文件改个名字。还是把这个命令包放到一个实例
5-29
中来看一下效果。
实例
5-29
foo.c : foo.y
$(run-yacc)
可以看见,要使用这个命令包,就好像使用变量一样。在这个命令包的使用中,命令包
run-yacc
中的
$^
就是
foo.y
,
$@
就是
foo.c
。
make
在执行命令包时,命令包中的每个命令会依次独立执行。
5.5 使用变量
在
makefile
文件中定义的变量,就像是
C/C++
语言中的宏一样,它代表了一个文本字串,在
makefile
文件中执行的时候,其会自动原样展开在所使用的位置。其与
C/C++
所不同的是,可以在
makefile
文件中改变其值。在
makefile
文件中,变量可以使用在目标、依赖目标、命令或是
makefile
文件的其他部分中。变量的命名字可以包含字符、数字、下划线
(
可以是数字开头
)
,但不应该含有
“
:
”
、
“
#
”
、
“
=
”
或是空字符
(
空格、回车等
)
。
变量是大小写敏感的,
foo
、
Foo
和
FOO
是
3
个不同的变量名。传统的
makefile
文件的变量名是全大写的命名方式,但推荐使用大小写搭配的变量名,如
MakeFlags
。这样可以避免因与系统的变量冲突而导致意外的事情。
有一些变量是很奇怪的字串,如
“
$<
”
、
“
$@
”
等,这些是自动化变量。
5.5.1 变量的基础
变量在声明时需要给予初值,而在使用时,需要在变量名前加上
“
$
”
符号,但最好用小括号
“
()
”
或是大括号
“
{}
”
把变量包括起来。如果要使用真实的
“
$
”
字符,需要用
“
$$
”
来表示。变量可以使用在许多位置,如规则中的目标、依赖、命令以及新的变量中。先看实例
5-30
:
实例
5-30
objects = program.o foo.o utils.o
program : $(objects)
gcc -o program $(objects)
$(objects) : defs.h
变量会在使用它的位置精确地展开
,
就像
C/C++
中的宏一样。例如
:
实例
5-31
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
展开后得到:
prog.o : prog.c
gcc -c prog.c
当然,千万不要在
makefile
文件中这样使用,这里只是举个例子来表明
makefile
文件中的变量在使用处展开的真实样子。可见其就是一个替代作用。另外,给变量加上括号完全是为了更加安全地使用这个变量,在上面的例子中,如果不想给变量加上括号也可以,但还是强烈建议给变量加上括号,因为这样可使代码更清晰。
5.5.2 赋值变量
在定义变量的值时,可以使用其他变量来构造变量的值,在
makefile
文件中有两种方式可以用变量定义变量的值。
先看第一种方式,也就是简单地使用
“
=
”
号,在
“
=
”
左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。如实例
5-32
:
实例
5-32
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
执行
make all
将会打出变量
$(foo)
的值是
“
Huh?
”
($(foo)
的值是
$(bar)
,
$(bar)
的值是
$(ugh)
,
$(ugh)
的值是
“
Huh?
”
)
。可见,变量是可以使用后面的变量来定义的。
这个功能有利有弊,好处是可以把变量的真实值推到后面来定义,如:
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
当
CFLAGS
在命令中被展开时,会是
-Ifoo -Ibar -O
。但这种形式也有弊端,那就是递归定义,如:
CFLAGS = $(CFLAGS) -O
或:
A = $(B)
B = $(A)
这会让
make
陷入无限的变量展开过程中。当然,
make
有能力检测这样的定义,并会报错。另外,如果在变量中使用函数,这种方式会让
make
运行时非常慢,更糟糕的是,它在使用两个
make
函数
wildcard
和
shell
时会出现不可预知的错误,因为不会知道这两个函数会被调用多少次。
为了避免上面的情形,可以使用
make
中的另一种用变量来定义变量的方法。这种方法使用的是
“
:=
”
操作符,如:
x := foo
y := $(x) bar
x := later
其等价于:
y := foo bar
x := later
值得一提的是,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。如果是这样:
y := $(x) bar
x := foo
则
y
的值是
bar
,而不是
foo bar
。
上面都是一些比较简单的变量应用。下面来看一个复杂的例子,其中包括了
make
函数、条件表达式和一个系统变量
MAKELEVEL
的使用:
实例
5-33
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif
系统变量
MAKELEVEL
表示:如果
make
有一个嵌套执行动作,这个变量会记录当前
makefile
文件的调用层数。
请先看一个例子,如果要定义一个变量,其值是一个空格,可以这样处理:
nullstring :=
space := $(nullstring) # end of the line
nullstring
是一个
Empty
变量,不含任何内容,而
space
的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个
Empty
变量来标明变量定义开始,后面再采用
“
#
”
注释符来表示变量定义终止,这样,可以定义出其值是一个空格的变量。请注意这里关于
“
#
”
的使用,注释符
“
#
”
的这种特性值得注意。如果定义一个变量:
dir := /foo/bar # directory to put the frobs in
dir
变量的值是
/foo/bar
,
后面还跟了
4
个空格
,
如果使用该变量来指定别的目录
——
$(dir)/file
就会出现不可预期的效果。
还有一个比较有用的操作符是
“
?=
”
,先看示例:
实例
5-34
FOO ?= bar
其含义是,如果
FOO
没有被定义过,变量
FOO
的值就是
bar
;如果
FOO
先前被定义过,这条语句将什么也不做,其等价于:
实例
5-35
If eq ($(origin FOO), undefined)
FOO = bar
endif
5.5.3 变量的高级用法
这里介绍两种变量的高级使用方法,第一种是变量值的替换。
可以替换变量中的共有部分,其格式是
$(var:a=b)
或是
${var:a=b}
。其意思是,把变量
var
中所有以
a
字串结尾的
a
替换成
b
字串。这里的结尾意思是空格或是结束符。
再看一个示例:
实例
5-36
foo := a.o b.o c.o
bar := $(foo:.o=.c)
这个示例中
,
先定义了一个
$(foo)
变量
,
而第二行的意思是把
$(foo)
中所有
.o
扩展符全部替换成
“
.c
”
,
所以
$(bar)
的值就是
a.c b.c c.c
。
另外一种变量替换的技术是以静态模式定义的,如实例
5-37
:
实例
5-37
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
这依赖于被替换字串中是否有相同的模式,模式中必须包含一个
“
%
”
字符,这个例子同样让
$(bar)
变量的值变为
a.c b.c c.c
。
第
2
种高级用法是
“
把变量的值再当成变量
”
。如实例
5-38
:
实例
5-38
x = y
y = z
a := $($(x))
在这个例子中
,
$(x)
的值是
y
,
所以
$($(x))
就是
$(y)
,
于是
$(a)
的值就是
z (
注意
,
是
x=y
,
而不是
x=$(y))
。
还可以使用更多的层次:
实例
5-39
x = y
y = z
z = u
a := $($($(x)))
这里的
$(a)
的值是
u
,相关的推导留给读者自己去做。
再复杂一点,使用上
“
在变量定义中使用变量
”
的第
1
个方式,如实例
5-40
:
实例
5-40
x = $(y)
y = z
z = Hello
a := $($(x))
这里的
$($(x))
被替换成了
$($(y))
,
因为
$(y)
值是
z
,
所以
,
最终结果是
:
a:=$(z)
,
也就是
Hello
。
再复杂一点,再加上一些函数,如实例
5-41
:
实例
5-41
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
这个例子中,
$($($(z)))
扩展为
$($(y))
,而其再次被扩展为
$($(subst 1,2,$(x)))
。
$(x)
的值是
variable1
,
subst
函数把
variable1
中的所有
1
字串替换成
2
字串,于是,
variable1
变成
variable2
,再取其值。所以,最终
$(a)
的值就是
$(variable2)
的值
——
Hello
。
在这种方式中,可以使用多个变量来组成一个变量的名字,然后再取其值:
first_second = Hello
a = first
b = second
all = $($a_$b)
这里的
$a_$b
组成了
first_second
,
于是
,
$(all)
的值就是
Hello
。
再来看看结合第
1
种技术的例子
:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
这个例子中
,
如果
$(a1)
的值是
a
,
$(sources)
的值就是
a.c b.c c.c
;
如果
$(a1)
的值是
1
,
$(sources)
的值是
1.c 2.c 3.c
。
再来看一个这种技术和函数与条件语句一同使用的例子:
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
这个示例中,如果定义了
do_sort
, :
foo
:
= $(sort a d b g q c)
,于是
$(foo)
的值就是
a b c d g q
。而如果没有定义
do_sort, :foo
:
= $(sort a d b g q c)
,调用的就是
strip
函数。
当然,把变量的值再当成变量这种技术,同样可以用在操作符的左边:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef
这个例子中定义了
3
个变量
:
dir
、
foo_sources
和
foo_print
。
5.5.4 追加变量值
可以使用
“
+=
”
操作符给变量追加值,如:
objects = main.o foo.o bar.o utils.o
objects += another.o
于是
,
$(objects)
值变成
:
main.o foo.o bar.o utils.o another.o (another.o
被追加进去了
)
。
使用
“
+=
”
操作符,可以模拟为下面的这种例子:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
所不同的是,用
“
+=
”
更为简洁。
如果变量之前没有定义过,
“
+=
”
会自动变成
“
=
”
;如果前面有变量定义,
“
+=
”
会继承于前一次操作的赋值符;如果前一次的是
“
:=
”
,
“
+=
”
会以
“
:=
”
作为其赋值符,如:
variable := value
variable += more
等价于
:
variable := value
variable := $(variable) more
但如果是这种情况:
variable = value
variable += more
由于前次的赋值符是
“
=
”
,所以
“
+=
”
也会以
“
=
”
来作为赋值,这样就会发生变量的递归定义,这是我们不希望看到的。不过
make
会自动解决这个问题,因此不必担心。
5.5.5 override 指示符
如果有变量是
make
的命令行参数设置的,
makefile
文件中对这个变量的赋值会被忽略。如果想在
makefile
文件中设置这类参数的值,可以使用
override
指示符。其语法是:
override <variable> = <value>
override <variable> := <value>
还可以追加:
override <variable> += <more text>
对于多行的变量定义,用
define
指示符,在
define
指示符前,也同样可以使用
ovveride
指示符,如:
override define foo
bar
endef
5.5.6 多行变量
还有一种设置变量值的方法是使用
define
关键字。使用
define
关键字设置变量的值可以包括换行符,这有利于定义一系列的命令。
define
指示符后面跟的是变量的名字,而重起一行定义变量的值,定义以
endef
关键字结束。其工作方式和
“
=
”
操作符一样。变量的值可以包含函数、命令、文字,或是其他变量。因为命令需要以
[Tab]
键开头,所以如果用
define
定义的命令变量中没有以
[Tab]
键开头,
make
就不会将其作为命令。
实例
5-42
展示了
define
的用法:
实例
5-42
define two-lines
echo foo
echo $(bar)
endef
5.5.7 环境变量
make
运行时的系统环境变量可以在
make
开始运行时被载入到
makefile
文件中,但是如果
makefile
文件中已定义了这个变量,或者这个变量由
make
命令行带入,系统的环境变量的值将被覆盖
(
如果
make
指定了
-e
参数,系统环境变量将覆盖
makefile
文件中定义的变量
)
。
因此,如果在环境变量中设置了
“
CFLAGS
”
环境变量,就可以在所有的
makefile
文件中使用这个变量了。这对于使用统一的编译参数有比较大的好处。如果
makefile
文件中定义了
CFLAGS
,则会使用
makefile
文件中的这个变量;如果没有定义,则使用系统环境变量的值,一个共性和个性的统一,很像
“
全局变量
”
和
“
局部变量
”
的特性。
当
make
嵌套调用时,上层
makefile
文件中定义的变量会以系统环境变量的方式传递到下层的
makefile
文件中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层
makefile
文件传递,则需要使用
exprot
关键字来声明。
当然,并不推荐把许多变量都定义在系统环境中,这样,在执行不用的
makefile
文件时,拥有的是同一套系统变量,这可能会带来更多的麻烦。
5.5.8 目标变量
前面所讲的在
makefile
文件中定义的变量都是全局变量,在整个文件中都可以访问这些变量。当然,自动化变量除外,如
“
$<
”
等这种自动化变量属于规则型变量,这种变量的值依赖于规则的目标和依赖目标的定义。当然,同样可以为某个目标设置局部变量,这种变量称为
Target-specific Variable
,它可以和全局变量同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。其语法是:
<target ...> : <variable-assignment>
<target ...> : overide <variable-assignment>
<variable-assignment>
可以是前面讲过的各种赋值表达式,如
“
=
”
、
“
:=
”
、
“
+=
”
或是
“
?
=
”
。第二个语法是针对
make
命令行带入的变量,或是系统环境变量。
这个特性非常有用,当设置了这样一个变量,这个变量会作用到由这个目标所引发的所有规则中去。如实例
5-43
:
实例
5-43
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
$(gcc) $(CFLAGS) prog.o foo.o bar.o
prog.o : prog.c
$(gcc) $(CFLAGS) prog.c
foo.o : foo.c
$(gcc) $(CFLAGS) foo.c
bar.o : bar.c
$(gcc) $(CFLAGS) bar.c
在
这个示例中
,
不管全局的
$(CFLAGS)
的值是什么
,
在
prog
目标以及其所引发的所有
规则中
(
prog.o foo.o bar.o
的规则
)
,
$(
CFLAG
S)
的值都是
-g
。
5.5.9 模式变量
在GNU的make中,还支持模式变量(Pattern-specific Variable),通过上面的目标变量,变量可以定义在某个目标上。模式变量的好处就是,可以给定一种模式,可以把变量定义在符合这种模式的所有目标上。
make的模式一般是至少含有一个“%”的,所以,可以以如下方式给所有以[.o]结尾的目标定义目标变量:
%.o : CFLAGS = -O
同样,模式变量的语法和目标变量一样:
<pattern ...> : <variable-assignment>
<pattern ...> : override <variable-assignment>
override同样是针对系统环境传入的变量,或是make命令行指定的变量。
5.6 使用条件判断
使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。
5.6.1 示例
下面的例子,判断
$(CC)
变量是否是
gcc
,如果是的话,则使用
GNU
函数编译目标。
libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
可见,在上面示例的这个规则中,目标foo可以根据变量$(CC)的值选取不同的函数库来编译程序。
可以从上面的示例中看到3个关键字:ifeq、else和endif。ifeq表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。else表示条件表达式为假的情况。endif表示一个条件语句的结束,任何一个条件表达式都应该以endif结束。
当变量$(CC)的值是gcc时,目标foo的规则是:
foo: $(objects)
$(CC) -o foo $(objects) $(libs_for_gcc)
而当变量
$(CC)
值不是
gcc
时
(
比如
cc)
,目标
foo
的规则是:
foo: $(objects)
$(CC) -o foo $(objects) $(normal_libs)
当然,还可以把上面的那个例子写得更简洁一些,如实例5-44:
实例5-44
ibs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
$(CC) -o foo $(objects) $(libs)
5.6.2 语法
条件表达式的语法为:
<conditional-directive>
<text-if-true>
endif
以及
:
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
其中<conditional-directive>表示条件关键字,如ifeq。这个关键字有4个。第一个是前面所见过的ifeq:
ifeq (<arg1>, <arg2>)
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"
比较参数arg1和arg2的值是否相同。当然,参数中还可以使用make的函数。如:
ifeq ($(strip $(foo)),)
<text-if-empty>
endif
这个示例中使用了strip函数,如果这个函数的返回值是空(Empty),<text-if-empty>就生效。
第2个条件关键字是ifneq。语法是:
ifneq (<arg1>, <arg2>)
ifneq '<arg1>' '<arg2>'
ifneq "<arg1>" "<arg2>"
ifneq "<arg1>" '<arg2>'
ifneq '<arg1>' "<arg2>"
其比较参数arg1和arg2的值是否相同,如果不同,则为真。
第3个条件关键字是ifdef。语法是:
ifdef <variable-name>
如果变量<variable-name>的值非空,那么表达式为真;否则,表达式为假。当然,<variable-name>同样可以是一个函数的返回值。注意,ifdef只是测试一个变量是否有值,其并不会把变量扩展到当前位置。还是来看两个例子:
实例5-45
示例一:
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
示例二:
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
实例5-46
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
实例
5-45
中
,
$(frobozz)
的
值是
yes
,
实例
5-46
则是
no
。
第
4
个条件关键字是
ifndef
。其语法是:
ifndef <variable-name>
这个和ifdef是相反的意思。在<conditional-directive>这一行上,多余的空格是允许的,但是不能以[Tab]键作为开始(不然就被认为是命令)。而注释符“#”同样也是安全的。else和endif也一样,只要不是以[Tab]键开始就行了。
注意:
make在读取makefile文件时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,最好不要把自动化变量(如$@等)放入条件表达式中,因为自动化变量是在运行时才有的。而且,为了避免混乱,make不允许把整个条件语句分成两部分放在不同的文件中。
5.7 使用函数
在makefile文件中可以使用函数来处理变量,从而让命令或是规则更为灵活和智能化。make所支持的函数不是很多,不过已经足够使用了。函数调用后,函数的返回值可以当作变量来使用。
5.7.1 函数的调用语法
函数调用很像变量的使用,也是以“$”来标识的,其语法如下:
$(<function> <arguments>)
或是:
i ${<function> <arguments>}
这里,<function>就是函数名,make支持的函数不多。<arguments>是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。函数调用以“$”开头,以圆括号或花括号把函数名和参数括起。
感觉很像一个变量,是不是?函数中的参数可以使用变量。为了风格的统一,函数和变量的括号最好一样,如使用$(subst a,b,$(x))这样的形式,而不是$(subst a,b,${x})的形式。因为统一会更清楚,也会减少一些不必要的麻烦。还是来看一个例子:
实例5-47
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
在这个示例中,$(comma)的值是一个逗号。$(space)使用了$(empty)定义了一个空格,$(foo)的值是“a b c”。$(bar)的定义,调用了函数subst,这是一个替换函数,这个函数有3个参数:第1个参数是被替换字串,第2个参数是替换字串,第3个参数是替换操作用的字串。这个函数也就是把$(foo)中的空格替换成逗号,所以$(bar)的值是“a,b,c”。
5.7.2 字符串处理函数
●
$(subst <from>,<to>,<text>)
名称:字符串替换函数
——subst。
功能:把字串
<text>
中的
<from>
字符串替换成
<to>
。
返回:函数返回被替换过后的字符串。
示例:
$(subst ee,EE,feet on the street)
把feet on the street中的ee替换成EE,返回结果是fEEt on the strEEt。
●
$(patsubst <pattern>,<replacement>,<text>)
名称:模式字符串替换函数
——patsubst。
功能:查找<text>中的单词(单词以空格、Tab键或回车、换行符分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。这里,<pattern>可以包括通配符“%”,表示任意长度的字串。如果<replacement>中也包含“%”,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串 (可以用“/”来转义,以“/%”来表示真实含义的“%”字符)。
返回:函数返回被替换过后的字符串。
示例:
$(patsubst %.c,%.o,x.c.c bar.c)
把字串x.c.c bar.c符合模式[%.c]的单词替换成[%.o],返回结果是x.c.o bar.o。
这和前面
“
变量章节
”
说过的相关知识有点相似。如:
$(var:<pattern>=<replacement>)
相当于
$(patsubst <pattern>,<replacement>,$(var))
而
$(var: <suffix>=<replacement>)
则相当于
$(patsubst %<suffix>,%<replacement>,$(var))
例如有:
objects = foo.o bar.o baz.o,
“$(objects:.o=.c)”和“$(patsubst %.o,%.c,$(objects))”是一样的。
$(strip <string>)
名称:去空格函数
——
strip
。
功能:去掉
<string>
字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
示例:
$(strip a b c )
把字串
“
a b c
”
去掉开头和结尾的空格
,
结果是
“
a b c
”
。
$(findstring <find>,<in>)
名称:查找字符串函数
——findstring。
功能:在字串<in>中查找<find>字串。
返回:如果找到,返回<find>,否则返回空字符串。
示例:
$(findstring a,a b c)
$(findstring a,b c)
第一个函数返回
“
a
”
字符串,第二个返回
“”
字符串
(
空字符串
)
●
$(filter <pattern...>,<text>)
名称:过滤函数
——
filter
。
功能:以
<pattern>
模式过滤
<text>
字符串中的单词,保留符合模式
<pattern>
的单词。可以有多个模式。
返回:返回符合模式
<pattern>
的字串。
示例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
$(filter %.c %.s,$(sources)) 返回的值是“foo.c bar.c baz.s”。
$(filter-out <pattern...>,<text>)
名称:反过滤函数
——
filter-out
。
功能:以<pattern>模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词。可以有多个模式。
返回:返回不符合模式
<pattern>
的字串。
示例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects)) 返回值是“foo.o bar.o”。
$(sort <list>)
名称:排序函数
——sort。
功能:给字符串
<list>
中的单词排序
(
升序
)
。
返回:返回排序后的字符串。
示例:
$(sort foo bar lose) 返回“bar foo lose” 。
备注:
sort函数会去掉<list>中相同的单词。
$(word <n>,<text>)
名称:取单词函数
——
word
。
功能:取字符串
<text>
中的第
<n>
个单词
(
从一开始
)
。
返回:返回字符串
<text>
中的第
<n>
个单词。如果
<n>
比
<text>
中的单词数要大,返回空字符串。
示例:
$(word 2, foo bar baz) 返回值是“bar”。
$(wordlist <s>,<e>,<text>)
名称:取单词串函数
——
wordlist
。
功能:从字符串
<text>
中取从
<s>
开始到
<e>
的单词串。
<s>
和
<e>
是一个数字。
返回:返回字符串
<text>
中从
<s>
到
<e>
的单词字串。如果
<s>
比
<text>
中的单词数要大,返回空字符串;如果
<e>
大于
<text>
的单词数,返回从
<s>
开始,到
<text>
结束的单词串。
示例:
$(wordlist 2, 3, foo bar baz) 返回值是“bar baz”。
$(words <text>)
名称:单词个数统计函数
——words。
功能:统计
<text>
中字符串中的单词个数。
返回:返回
<text>
中的单词数。
示例:
$(words, foo bar baz) 返回值是“3”。
备注:
如果要取<text>中最后的一个单词,可以这样:$(word $(words <text>),<text>)。
$(firstword <text>)。
名称:首单词函数
——firstword。
功能:取字符串
<text>
中的第一个单词。
返回:返回字符串
<text>
的第一个单词。
示例:
$(firstword foo bar) 返回值是“foo”。
备注:
这个函数可以用word函数来实现:$(word 1,<text>)。
以上是所有的字符串操作函数,如果搭配使用,可以完成比较复杂的功能。这里,举一个现实中应用的例子。make使用VPATH变量来指定依赖文件的搜索路径。于是,可以利用这个搜索路径来指定编译器对头文件的搜索路径参数CFLAGS,如:
override CFLAGS += $(patsubst %,-I%,$(subst:, ,$(VPATH)))
如果$(VPATH)的值是src:../headers,$(patsubst %,-I%,$(subst :, ,$(VPATH)))将返回-Isrc -I../headers,这正是cc或gcc搜索头文件路径的参数。
5.7.3 文件名操作函数
下面要介绍的函数主要是处理文件名的,每个函数的参数字符串都会被当做一个或是一系列的文件名来对待。
$(dir <names...>)
名称:取目录函数
——dir。
功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠“/”之前的部分。如果没有反斜杠,返回“./”。
返回:返回文件名序列<names>的目录部分。
示例:
$(dir src/foo.c hacks)返回值是“src/ ./”。
$(notdir <names...>)
名称:取文件函数
——notdir。
功能:从文件名序列<names>中取出非目录部分。非目录部分是指最后一个反斜杠“/”之后的部分。
返回:返回文件名序列<names>的非目录部分。
示例:
$(notdir src/foo.c hacks)返回值是“foo.c hacks”。
$(suffix <names...>)
名称:取后缀函数
——suffix。
功能:从文件名序列<names>中取出各个文件名的后缀。
返回:返回文件名序列<names>的后缀序列,如果文件没有后缀,则返回空字串。
示例:
$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。
●
$(basename <names...>)
名称:取前缀函数
——basename。
功能:从文件名序列<names>中取出各个文件名的前缀部分。
返回:返回文件名序列<names>的前缀序列,如果文件没有前缀,则返回空字串。
示例:
$(basename src/foo.c src-1.0/bar.c hacks) 返回值是“src/foo src-1.0/bar
hacks”。
$(addsuffix <suffix>,<names...>)
名称:加后缀函数
——addsuffix。
功能:把后缀<suffix>加到<names>中的每个单词后面。
返回:返回加过后缀的文件名序列。
示例:
$(addsuffix .c,foo bar) 返回值是“foo.c bar.c”。
$(addprefix <prefix>,<names...>)
名称:加前缀函数
——addprefix。
功能:把前缀<prefix>加到<names>中的每个单词后面。
返回:返回加过前缀的文件名序列。
示例:
$(addprefix src/,foo bar) 返回值是“src/foo src/bar”。
$(join <list1>,<list2>)
名称:连接函数
——join。
功能:把<list2>中的单词对应地加到<list1>的单词后面。如果<list1>的单词个数比<list2>多,<list1>中多出来的单词将保持原样。如果<list2>的单词个数比<list1>多,<list2>中多出来的单词将被复制到<list2>中。
返回:返回连接过后的字符串。
示例:
$(join aaa bbb , 111 222 333) 返回值是“aaa111 bbb222 333”。
5.7.4 foreach 函数
foreach函数和别的函数不同。因为这个函数是用来做循环用的,makefile文件中的foreach函数几乎是仿照Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的。它的语法是:
$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。所以,<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会使用<var>这个参数来依次枚举<list>中的单词。如实例5-48:
实例5-48
names := a b c d
files := $(foreach n,$(names),$(n).o)
上面的例子中,$(name)中的单词会被挨个取出,并存到变量n中,$(n).o每次根据$(n)计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)的值是“a.o b.o c.o d.o”。
注意:
foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不再起作用,其作用域只在foreach函数当中。
5.7.5 if 函数
if函数很像GNU的make所支持的条件语句
——ifeq(参见前面章节所述),if函数的语法是:
$(if <condition>,<then-part>)
或是
$(if <condition>,<then-part>,<else-part>)
可见,if函数可以包含else部分,或是不含。即if函数的参数可以是两个,也可以是3个。<condition>参数是if的表达式,如果其返回的为非空字符串,这个表达式就相当于返回真,于是,<then-part>会被执行,否则<else-part>会被执行。
而if函数的返回值是,如果<condition>为真(非空字符串),那个<then-part>会是整个函数的返回值;如果<condition>为假(空字符串),<else-part>会是整个函数的返回值,此时如果<else-part>没有被定义,整个函数返回空字符串。所以,<then-part>和<else-part>只会有一个被执行。
5.7.6 call函数
call函数是惟一一个可以用来创建新的参数化的函数。可以写一个非常复杂的表达式,这个表达式中,可以定义许多参数,然后可以用call函数来向这个表达式传递参数。其语法是:
$(call <expression>,<parm1>,<parm2>,<parm3>...)
当make执行这个函数时,<expression>参数中的变量,如$(1),$(2),$(3)等,会被参数<parm1>,<parm2>,<parm3>依次取代。而<expression>的返回值就是call函数的返回值。例如:
实例5-49
reverse = $(1) $(2)
foo = $(call reverse,a,b)
foo的值就是“a b”。当然,参数的次序是可以自定义的,不一定是顺序的。如实例5-50:
实例5-50
reverse = $(2) $(1)
foo = $(call reverse,a,b)
此时,foo的值就是“b a”。
5.7.7 origin函数
origin函数不像其他的函数,它并不操作变量的值,它只是告诉这个变量是哪里来的。其语法是:
$(origin <variable>)
注意:
<variable>是变量的名字,不应该是引用。所以最好不要在<variable>中使用“$”字符。
Origin
函数会以其返回值来告诉这个变量的出生情况。下面是
origin
函数的返回值:
●
undefined
如果<variable>从来没有定义过,origin函数返回值为undefined。
●
default
如果<variable>是一个默认的定义,比如CC这个变量,这种变量将在后面讲述。
●
environment
如果
<variable>
是一个环境变量,并且当
makefile
文件被执行时,
-e
参数没有被打开。
●
file
如果<variable>这个变量被定义在makefile文件中。
●
command line
如果
<variable>
这个变量是被命令行定义的。
●
override
如果
<variable>
是被
override
指示符重新定义的。
●
automatic
如果<variable>是一个命令运行中的自动化变量(关于自动化变量将在后面讲述)。
这些信息对于编写makefile文件是非常有用的。例如,假设有一个makefile文件,其包了一个定义文件Make.def,在Make.def中定义了一个变量bletch,而环境中也有一个环境变量bletch,此时,可判断一下,如果变量来源于环境,就把它重定义了,如果来源于Make.def或是命令行等非环境的,就没有重新定义它。于是,在makefile文件中,可以这样写:
实例5-51
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
当然,使用override关键字不就可以重新定义环境中的变量了吗?为什么需要使用这样的步骤?是的,用override是可以达到这样的效果,可是override同时会把从命令行定义的变量也覆盖了。这里只想重新定义环境传来的,而不想重新定义命令行传来的。
5.7.8 shell函数
shell函数也不像其他函数。顾名思义,它的参数应该是操作系统Shell的命令。也就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,可以用操作系统命令以及字符串处理命令awk,sed等命令来生成一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意:
这个函数会新生成一个Shell程序来执行命令,所以要注意其运行性能。如果makefile文件中有一些比较复杂的规则,并大量使用了这个函数,对于系统性能是有害的。特别是makefile文件的隐式规则可能会让shell函数执行的次数比想象的多得多。
5.7.9 控制make的函数
make提供了一些函数来控制make的运行。通常,需要检测一些运行makefile文件时的运行时信息,并且根据这些信息来决定是让make继续执行,还是停止。
$(error <text ...>)
产生一个致命的错误,<text ...>是错误信息。注意,error函数不会在一被使用时就产生错误信息,所以如果把其定义在某个变量中,并在后续的脚本中使用这个变量,也是可以的。例如:
实例5-52
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
实例5-53
ERR = $(error found an error!)
.PHONY: err
err: ; $(ERR)
例5-52会在变量ERROR_001定义后执行时产生error调用,而示例5-53则在目录err被执行时才发生error调用。
$(warning <text ...>)
这个函数很像error函数,它并不会让make退出,只是输出一段警告信息,而make继续执行。
5.8 make 的运行
一般来说,最简单的就是直接在命令行下输入make命令,make命令会查找当前目录的makefile文件来执行,一切都是自动的。但有时也许只想让make重编译某些文件,而不是整个工程。而有时候有几套编译规则,以便在不同的时候使用不同的编译规则。本节就讲述如何使用make命令。
5.8.1 make的退出码
make命令执行后有3个退出码:
●
0 表示成功执行。
●
1 如果make运行时出现错误,其返回值为1。
●
2 如果使用了make的-q选项,并且make使得一些目标不需要更新,返回2。
5.8.2 指定makefile文件
前面说过,GNU make找寻默认的makefile文件的规则是在当前目录下依次找3个文件
——GNUmakefile文件、Makefile文件和makefile文件。其按顺序找这3个文件,一旦找到,就开始读取这个文件并执行。
当然,也可以给make命令指定一个特殊名字的makefile文件。要实现这个功能,需要使用make的-f或是--file参数(--makefile文件参数也行)。例如,有一个makefile文件的名字是hchen.mk,可以这样让make来执行这个文件:
make –f hchen.mk
如果在make的命令行中不只一次地使用了-f参数,所有指定的makefile文件将会被连在一起传递给make执行。
5.8.3 指定目标
一般来说,make的最终目标是makefile文件中的第一个目标,而其他目标一般是由这个目标连带出来的,这是make的默认行为。当然,一般来说,makefile文件中的第一个目标由许多目标组成,可以指示make,让其完成所指定的目标。要实现这一目的很简单,只要在make命令后直接跟目标的名字就可以完成(如前面提到的make clean形式)。
任何在makefile文件中的目标都可以被指定成终极目标,但是除了以“-”打头,或是包含了“=”的目标。因为有这些字符的目标,会被解析成命令行参数或是变量。甚至没有被明确写出来的目标也可以成为make的终极目标。也就是说,只要make可以找到其隐含推导规则,这个隐含目标同样可以被指定成终极目标。
有一个make的环境变量叫MAKECMDGOALS,这个变量中会存放所指定的终极目标的列表,如果在命令行上没有指定目标,这个变量是空值。这个变量可以使用在一些比较特殊的情形下。如实例5-54:
实例5-54
sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif
基于上面这个例子,只要输入的命令不是make clean,makefile文件会自动包含foo.d和bar.d这两个makefile文件。
使用指定终极目标的方法可以很方便地编译程序,例如实例
5-55
:
实例5-55
PHONY: all
all: prog1 prog2 prog3 prog4
从这个例子中可以看到,这个makefile文件中有4个需要编译的程序
——prog1、 prog2、prog3和prog4,可以使用make all命令来编译所有的目标(如果把all置成第一个目标,只需执行make),也可以使用make prog2来单独编译目标prog2。
既然make可以指定所有makefile文件中的目标,也包括“伪目标”,于是可以根据这种性质来让makefile文件根据指定的不同目标来完成不同的事。在Linux中,软件发布时,特别是GNU这种开放源代码的软件发布时,其makefile文件都包含了编译、安装、打包等功能。可以参照这种规则来书写makefile文件中的目标。
下面说明一些常用的伪目标的功能。
●
all 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
●
clean 这个伪目标功能是删除所有被make创建的文件。
●
install 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件复 制到指定的目标中去。
●
print 这个伪目标的功能是列出改变过的源文件。
●
tar 这个伪目标的功能是把源程序打包备份。也就是一个tar文件。
●
dist 这个伪目标的功能是创建一个压缩文件,一般是把tar文件压成Z文件, 或是gz文件。
●
TAGS 这个伪目标用于更新所有的目标,以备完整地重编译使用。
●
check和test 这两个伪目标一般用来测试makefile文件的流程。
当然,一个项目的
makefile
文件中也不一定要书写这样的目标,这些都是
GNU
的内容。如果
makefile
文件中有这些功能,一是很实用,二是可以使
makefile
文件显得很专业。
5.8.4 检查规则
有时候,不想让makefile文件中的规则执行,只想检查一下命令,或是执行的序列。于是可以使用make命令的下述参数:
-n
--just-print
--dry-run
--recon
不执行makefile中的参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于调试makefile文件很有用处。
-t
--touch
这个参数是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正编译目标,只是把目标变成已编译过的状态。
-q
--question
这个参数的行为是找目标,如果目标存在,其什么也不会输出,当然也不会执行编译;如果目标不存在,其会打印出一条出错信息。
-W <file>
--what-if=<file>
--assume-new=<file>
--new-file=<file>
这个参数需要指定一个文件,一般是源文件(或依赖文件),make会根据规则推导来运行依赖于这个文件的命令。一般来说,可以和-n参数一同使用,来查看这个依赖文件所发生的规则命令。
另外一个很有意思的用法是结合-p和-v来输出makefile文件被执行时的信息(该内容将在后面讲述)。
5.8.5 make的参数
下面列举了所有GNU make 3.80版的参数定义。其他版本和其他厂商的make大同小异,不过其他厂商的make的具体参数还是请参考各自的产品文档。
-b
-m
这两个参数的作用是忽略和其他版本make的兼容性。
-B
--always-make
认为所有的目标都需要更新(重编译)。
-C <dir>
--directory=<dir>
指定读取makefile文件的目录。如果有多个-C参数,make的解释是后面的路径以前面的路径作为相对路径,并以最后的目录作为被指定目录。如:
make –C ~hchen/test –C prog
等价于
make –C ~hchen/test/prog。
--debug[=<options>]
输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是<options>的取值:
●
a 也就是all,输出所有的调试信息。
●
b 也就是basic,只输出简单的调试信息。即输出不需要重编译的目标。
●
v 也就是verbose,在b选项的级别之上。输出的信息包括哪个makefile文件被解析,不需要被重编译的依赖文件(或是依赖目标)等。
●
i 也就是implicit,输出所有的隐含规则。
●
j 也就是jobs,输出执行规则中命令的详细信息,如命令的PID、返回码等。
●
m 也就是makefile文件,输出make,读取makefile文件,更新makefile文件,执行makefile文件的信息。
-d
相当于“--debug=a”。
-e
--environment-overrides
指明环境变量的值,覆盖makefile文件中定义的变量的值。
-f=<file>
--file=<file>
--makefile文件=<file>
指定需要执行的makefile文件。
-h
--help
显示帮助信息。
-i
--ignore-errors
在执行时忽略所有的错误。
-I <dir>
--include-dir=<dir>
指定一个包含makefile文件的搜索目标。可以使用多个-I参数来指定多个目录。
-j [<jobsnum>]
--jobs[=<jobsnum>]
指定同时运行命令的个数。如果没有这个参数,make运行命令时能运行多少就运行多少。如果有一个以上的-j参数,仅最后一个-j才是有效的 (注意这个参数在MS-DOS中是无用的)。
-k
--keep-going
出错也不停止运行。如果生成一个目标失败了,依赖于其上的目标就不会被执行。
-l <load>
--load-average[=<load]
--max-load[=<load>]
指定make运行命令的负载。
-n
--just-print
--dry-run
--recon
仅输出执行过程中的命令序列,但并不执行。
-o <file>
--old-file=<file>
--assume-old=<file>
不重新生成指定的<file>,即使这个目标的依赖文件比它新。
-p
--print-data-base
输出makefile文件中的所有数据,包括所有的规则和变量。这个参数会让一个简单的makefile文件都输出一堆信息。如果只是想输出信息而不想执行makefile文件,可以使用make -qp命令。如果想查看执行makefile文件前的预设变量和规则,可以使用make –p –f /dev/null。这个参数输出的信息会包含makefile文件的文件名和行号,所以,用这个参数来调试的makefile文件很有用,特别是环境变量很复杂的时候。
-q
--question
不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是0则说明要更新,如果是2则说明有错误发生。
-r
--no-builtin-rules
禁止make使用任何隐式规则。
-R
--no-builtin-variabes
禁止make使用任何作用于变量上的隐式规则。
-s
--silent
--quiet
在命令运行时不显示命令的输出。
-S
--no-keep-going
--stop
取消-k选项的作用。因为有些时候,make的选项是从环境变量MAKEFLAGS中继承下来的。所以可以在命令行中使用这个参数来让环境变量中的-k选项失效。
-t
--touch
相当于Linux的touch命令,只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行。
-v
--version
输出make程序的版本、版权等关于make的信息。
-w
--print-directory
输出运行makefile文件之前和之后的信息。这个参数对于跟踪嵌套式调用make时很有用。
--no-print-directory
禁止-w选项。
-W <file>
--what-if=<file>
--new-file=<file>
--assume-file=<file>
假定目标
<file>
需要更新,如果和
-n
选项使用,这个参数会输出该目标更新时的运行动
作。如果没有
-n
,就像运行
UNIX
的
touch
命令一样,使得
<file>
的修改时间为当前时间。
--warn-undefined-variables
只要make发现有未定义的变量,就输出警告信息。
5.9 隐含规则
在使用makefile文件时,有一些会经常使用而且使用频率非常高的东西,比如,编译C/C++的源程序为中间目标文件(Linux下是.o文件,Windows下是.obj文件)。本章讲述的就是一些在makefile文件中隐含的,早先约定了的,不需要再写出来的规则。
隐含规则也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,哪怕makefile文件中没有书写这样的规则。例如,把.c文件编译成.o文件这一规则,根本就不用写出来,make会自动推导出这种规则,并生成需要的.o文件。
隐含规则会使用一些系统变量,可以改变这些系统变量的值来定制隐含规则运行时的参数。如系统变量CFLAGS可以控制编译时的编译器参数。还可以通过模式规则的方式写下自己的隐含规则。用后缀规则来定义隐含规则会有许多限制。使用模式规则会更加智能和清楚,但后缀规则可以用来保证makefile文件的兼容性。
了解了隐含规则,可以让其更好地服务,不至于在运行makefile文件时出现一些莫名其妙的东西。当然,任何事物都是矛盾的,“水能载舟,亦可覆舟”,所以,有时候“隐含规则”也会给造成不小的麻烦。只有了解了它,才能更好地使用它。
5.9.1 使用隐含规则
如果需要使用隐含规则生成需要的目标,所需要做的就是不要写出这个目标的规则,make会试图去自动推导产生这个目标的规则和命令。如果make可以自动推导生成这个目标的规则和命令,这个行为就是隐含规则的自动推导。当然,隐含规则是make事先约定好的一些东西。例如,有下面的一个makefile文件:
实例5-56
foo : foo.o bar.o
gcc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)
可以注意到,这个makefile文件中并没有写下如何生成foo.o和bar.o这两目标的规则和命令。因为make的隐含规则功能会自动去推导这两个目标的依赖目标和生成命令。
make会在自己的隐含规则库中寻找可以用的规则,如果找到,就会使用。如果找不到,就会报错。在上面的例子中,make调用的隐含规则是,把.o的目标依赖文件置成.c,并使用C的编译命令cc –c $(CFLAGS) [.c]来生成.o的目标。也就是说,完全没有必要写下下面的两条规则:
foo.o : foo.c
gcc –c foo.c $(CFLAGS)
bar.o : bar.c
gcc –c bar.c $(CFLAGS)
因为,这是“约定”好了的事,make已约定好了用C编译器gcc生成.o文件的规则,这就是隐含规则。当然,如果为.o文件书写了自己的规则,make就不会自动推导并调用隐含规则,它会按照写好的规则忠实地执行。
还有,在make的隐含规则库中,每一条隐含规则都在库中有其顺序,越靠前的则是越经常使用的,所以,这会导致有些时候即使显式地指定了目标依赖,make也不会管。如下面这条规则(没有命令):
foo.o : foo.p
依赖文件foo.p (Pascal程序的源文件)有可能变得没有意义。如果目录下存在foo.c文件,隐含规则一样会生效,并会通过foo.c调用C的编译器生成foo.o文件。因为,在隐含规则中,Pascal的规则出现在C的规则之后,所以,make找到可以生成foo.o的C的规则后就不再寻找下一条规则了。如果确实不希望任何隐含规则推导,就不要只写出依赖规则而不写命令。
5.9.2 隐含规则一览
这里将讲述所有预先设置(也就是make内建)的隐含规则。如果不明确地写下规则,make就会在这些规则中寻找所需要的规则和命令。当然,也可以使用make的参数-r或--no-builtin-rules选项来取消所有预设置的隐含规则。
当然,即使指定了-r参数,某些隐含规则还是会生效,因为有许多隐含规则都使用了后缀规则来定义。所以,只要隐含规则中有后缀列表 (也就是系统定义在目标.SUFFIXES的依赖目标),隐含规则就会生效。默认的后缀列表是:.out、.a、.ln、 .o、 .c、 .cc、 .C、 .p、.f、 .F、.r、 .y、 .l、.s、.S、 .mod、 .sym、 .def、.h、.info、 .dvi、 .tex、 .texinfo、 .texi、 .txinfo、.w、.ch、 .web、.sh、.elc、.el。具体的细节会在后面讲述。
还是先来看一看常用的隐含规则。
1. 编译C程序的隐含规则
<n>.o的目标的依赖目标会自动推导为<n>.c,并且其生成命令是$(CC) –c $(CPPFLAGS) $(CFLAGS)。
2. 编译C++程序的隐含规则
<n>.o的目标的依赖目标会自动推导为<n>.cc或是<n>.C,并且其生成命令是$(CXX) –c $(CPPFLAGS) $(CFLAGS) (建议使用.cc作为C++源文件的后缀,而不是.C)。
3. 编译Pascal程序的隐含规则
<n>.o的目标的依赖目标会自动推导为<n>.p,并且其生成命令是$(PC) –c $(PFLAGS)。
4. 编译Fortran/Ratfor程序的隐含规则
<n>.o的目标的依赖目标会自动推导为<n>.r、<n>.F或<n>.f,并且其生成命令是:
.f $(FC) –c $(FFLAGS)
.F $(FC) –c $(FFLAGS) $(CPPFLAGS)
.f $(FC) –c $(FFLAGS) $(RFLAGS)
5. 预处理Fortran/Ratfor程序的隐含规则
<n>.f的目标的依赖目标会自动推导为<n>.r或<n>.F。这个规则只是转换Ratfor或有预处理的Fortran程序到一个标准的Fortran程序。其使用的命令是:
.F $(FC) –F $(CPPFLAGS) $(FFLAGS)
.r $(FC) –F $(FFLAGS) $(RFLAGS)
6. 编译Modula-2程序的隐含规则
<n>.sym的目标的依赖目标会自动推导为<n>.def,并且其生成命令是$(M2C) $(M2FLAGS) $(DEFFLAGS)。
<n.o> 的目标的依赖目标会自动推导为<n>.mod,并且其生成命令是$(M2C) $(M2FLAGS) $(MODFLAGS)。
7. 汇编和汇编预处理的隐含规则
<n>.o 的目标的依赖目标会自动推导为<n>.s,默认使用编译品as,并且其生成命令是$(AS) $(ASFLAGS)。
<n>.s 的目标的依赖目标会自动推导为<n>.S,默认使用C预编译器cpp,并且其生成命令是$(AS) $(ASFLAGS)。
8. 链接Object文件的隐含规则
<n>目标依赖于<n>.o,通过运行C的编译器来运行链接程序生成(一般是ld),其生成命令是$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)。
这个规则对于只有一个源文件的工程有效,同时也对多个
Object
文件
(
由不同的源文件生成
)
有效。例如如下规则:
x : y.o z.o
并且x.c、y.c和z.c都存在时,隐含规则将执行如下命令:
cc -c x.c -o x.o
cc -c y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
rm -f x.o
rm -f y.o
rm -f z.o
如果没有一个源文件(如上例中的x.c)和目标名字(如上例中的x)相关联,最好写出自己的生成规则,不然,隐含规则会报错。
9. Yacc C程序时的隐含规则
<n>.c的依赖文件自动推导为n.y (Yacc生成的文件),其生成命令是$(YACC) $(YFALGS) (Yacc是一个语法分析器,关于其细节请查看相关资料)。
10. Lex C程序时的隐含规则
<n>.c的依赖文件自动推导为n.l (Lex生成的文件),其生成命令是$(LEX) $(LFALGS) (关于Lex的细节请查看相关资料)。
11. Lex Ratfor程序时的隐含规则
<n>.r
的依赖文件自动推导为
n.l (Lex
生成的文件
)
,其生成命令是
$(LEX) $(LFALGS)
。
12. 从C程序、Yacc文件或Lex文件创建Lint库的隐含规则
<n>.ln (lint生成的文件)的依赖文件自动推导为n.c,其生成命令是:$(LINT) $(LINTFALGS) $(CPPFLAGS) -i。
对于<n>.y和<n>.l也是同样的规则。
5.9.3 隐含规则使用的变量
在隐含规则的命令中,基本上都使用了一些预先设置的变量。可以在makefile文件中改变这些变量的值,或是在make的命令行中传入这些值,或是在环境变量中设置这些值。无论怎么样,只要设置了这些特定的变量,其就会对隐含规则起作用。当然,也可以利用make的-R或--no–builtin-variables参数来取消所定义的变量对隐含规则的作用。
例如,第一条隐含规则
——编译C程序的隐含规则的命令是$(CC)–c $(CFLAGS) $(CPPFLAGS)。Make默认的编译命令是cc,如果把变量$(CC)重定义成gcc,把变量$(CFLAGS)重定义成-g,隐含规则中的命令全部会以gcc –c -g $(CPPFLAGS)的样子来执行了。
可以把隐含规则中使用的变量分成两种:一种是命令相关的,如CC;一种是参数相的关,如CFLAGS。下面是所有隐含规则中会用到的变量。
1. 关于命令的变量
●
AR 函数库打包程序。默认命令是ar。
●
AS 汇编语言编译程序。默认命令是as。
●
CC C语言编译程序。默认命令是cc。
●
CXX C++语言编译程序。默认命令是g++。
●
CO 从 RCS文件中扩展文件程序。默认命令是co。
●
CPP C程序的预处理器(输出是标准输出设备)。默认命令是$(CC) –E。
●
FC Fortran 和 Ratfor 的编译器和预处理程序。默认命令是f77。
●
GET 从SCCS文件中扩展文件的程序。默认命令是get。
●
LEX Lex方法分析器程序(针对于C或Ratfor)。默认命令是lex。
●
PC Pascal语言编译程序。默认命令是pc。
●
YACC Yacc文法分析器(针对于C程序)。默认命令是yacc。
●
YACCR Yacc文法分析器(针对于Ratfor程序)。默认命令是yacc –r。
●
MAKEINFO 转换Texinfo源文件(.texi)到Info文件程序。默认命令是makeinfo。
●
TEX 从TeX源文件创建TeX DVI文件的程序。默认命令是tex。
●
TEXI2DVI 从Texinfo源文件创建TeX DVI 文件的程序。默认命令是texi2dvi。
●
WEAVE 转换Web到TeX的程序。默认命令是weave。
●
CWEAVE 转换C Web 到 TeX的程序。默认命令是cweave。
●
TANGLE 转换Web到Pascal语言的程序。默认命令是tangle。
●
CTANGLE 转换C Web 到 C。默认命令是ctangle。
●
RM 删除文件命令。默认命令是rm –f。
2. 关于命令参数的变量
下面的这些变量都是上面的命令相关的参数。如果没有指明其默认值,其默认值都是空。
●
ARFLAGS 函数库打包程序AR命令的参数。默认值是rv。
●
ASFLAGS 汇编语言编译器参数(当明显地调用.s或.S文件时)。
●
CFLAGS C语言编译器参数。
●
CXXFLAGS C++语言编译器参数。
●
COFLAGS RCS命令参数。
●
CPPFLAGS C预处理器参数( C 和 Fortran 编译器也会用到)。
●
FFLAGS Fortran语言编译器参数。
●
GFLAGS SCCS get程序参数。
●
LDFLAGS 链接器参数(如ld)。
●
LFLAGS Lex文法分析器参数。
●
PFLAGS Pascal语言编译器参数。
●
RFLAGS Ratfor 程序的Fortran 编译器参数。
●
YFLAGS Yacc文法分析器参数。
5.9.4 隐含规则链
有些时候,一个目标可能被一系列的隐含规则所作用。例如,一个.o的文件生成,可能会是先被Yacc的.y文件先成.c,然后再被C的编译器生成。这一系列的隐含规则叫做“隐含规则链”。
在上面的例子中,如果文件.c存在,就直接调用C的编译器的隐含规则;如果没有.c文件,但有一个.y文件,Yacc的隐含规则会被调用,生成.c文件,然后,再调用C编译的隐含规则,最终由.c生成.o文件,达到目标。
这种.c的文件(或是目标)叫做中间目标。不管怎么样,make会努力自动推导生成目标的一切方法。不管中间目标有多少,其都会执着地把所有的隐含规则和书写的规则全部合起来分析,努力达到目标。所以,有些时候,可能会觉得奇怪,怎么目标会这样生成?怎么makefile文件发疯了?
在默认情况下,中间目标和一般的目标有两个位置不同:第一个不同是除非中间的目标不存在,才会引发中间规则;第二个不同是,只要目标成功产生,产生最终目标的过程中,所产生的中间目标文件会被rm -f删除。
通常,一个被makefile文件指定成目标或是依赖目标的文件不能当作中介。然而,可以显式地说明一个文件或是目标是中介目标,可以使用伪目标.INTERMEDIATE来强制声明(如.INTERMEDIATE:mid )。
也可以阻止make自动删除中间目标。要做到这一点,可以使用伪目标.SECONDARY来强制声明(如.SECONDARY : sec)。还可以把目标以模式的方式来指定(如%.o)成伪目标.PRECIOUS的依赖目标,以保存被隐含规则所生成的中间文件。
在隐含规则链中,禁止同一个目标出现两次或两次以上,这样一来,就可防止在make自动推导时出现无限递归的情况。
Make会优化一些特殊的隐含规则,而不生成中间文件。如,从文件foo.c生成目标程序foo。按道理,make会编译生成中间文件foo.o,然后链接成foo,但在实际情况下,这一动作可以被一条cc命令完成(cc –o foo foo.c),于是优化过的规则就不会生成中间文件。
5.9.5 定义模式规则
可以使用模式规则来定义一个隐含规则。一个模式规则跟一般的规则类似,只是在规则中,目标的定义需要有“%”字符。“%”的意思是表示一个或多个任意字符。在依赖目标中同样可以使用“%”,只是依赖目标中的“%”的取值取决于其目标。
有一点需要注意的是,“%”的展开发生在变量和函数的展开之后,变量和函数的展开发生在make载入makefile文件时,而模式规则中的“%”则发生在运行时。
1. 模式规则介绍
模式规则中,至少在规则的目标定义中要包含“%”,否则,就是一般的规则。目标中的“%”定义表示对文件名的匹配,“%”表示长度任意的非空字符串。例如: %.c表示以.c结尾的文件名(文件名的长度至少为3),而s.%.c则表示以s.开头,.c结尾的文件名(文件名的长度至少为5)。
如果“%”定义在目标中,目标中的“%”的值决定了依赖目标中的“%”的值,也就是说,目标中的模式的“%”决定了依赖目标中“%”的样子。例如有一个模式规则如下:
%.o : %.c ; <command ... >
其含义是,指出了从所有的.c文件生成相应的.o文件的规则。如果要生成的目标是a.o b.o,%c就是“a.c b.c”。
一旦依赖目标中的“%”模式被确定,make会被要求去匹配当前目录下所有的文件名。在模式规则中,目标可能会是多个,如果有模式匹配出多个目标,make就会产生所有的模式目标,此时,make关心的是依赖的文件名和生成目标的命令这两件事。
2. 模式规则示例
下面这个例子表示了把所有的.c文件都编译成.o文件。
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
其中,“$@”表示所有的目标的逐个值,“$<”表示了所有依赖目标的逐个值。这些奇怪的变量叫“自动化变量”。
下面的这个例子中有两个目标是模式的:
%.tab.c %.tab.h: %.y
bison -d $<
这条规则告诉make把所有的.y文件都以bison -d <n>.y执行,然后生成<n>.tab.c和<n>.tab.h文件(其中,<n>表示一个任意字符串)。如果执行程序foo依赖于文件parse.tab.o和scan.o,并且文件scan.o依赖于文件parse.tab.h,如果parse.y文件被更新了,根据上述的规则,bison -d parse.y就会被执行一次,于是,parse.tab.o和scan.o的依赖文件就齐了 (假设parse.tab.o由parse.tab.c生成,scan.o由scan.c生成,而foo由parse.tab.o和scan.o”链接生成,而且foo和其.o文件的依赖关系已写好,所有的目标都会得到满足)。
3. 自动化变量
在上述的模式规则中,目标和依赖文件都是一系例的文件,如何书写一个命令来完成从不同的依赖文件生成相应的目标?因为在每一次的对模式规则的解析时,都会是不同的目标和依赖文件。
自动化变量就是完成这个功能的。在前面,已经对自动化变量有所提及,相信看到这里已对它有一个感性认识了。所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地逐个取出,直至所有符合模式的文件都取完。这种自动化变量只应出现在规则的命令中。
下面是所有的自动化变量及其说明:
$@
表示规则中的目标文件集。在模式规则中,如果有多个目标,“$@”就是匹配于目标中模式定义的集合。
$%
仅当目标是函数库文件时,表示规则中的目标成员名。例如,如果一个目标是foo.a(bar.o),“$%”就是bar.o,“$@”就是foo.a。如果目标不是函数库文件(如Unix下是.a,Windows下是.lib),其值为空。
$<
依赖目标中的第一个目标名字。如果依赖目标是以模式(即“%”)定义的,“$<”将是符合模式的一系列的文件集。注意,其是一个一个地取出来的。
$?
所有比目标新的依赖目标的集合,以空格分隔。
$^
所有的依赖目标的集合,以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。
$+
这个变量很像“$^”,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
$*
这个变量表示目标模式中“%”及其之前的部分。如果目标是dir/a.foo.b,并且目标的模式是a.%.b,“$*”的值就是dir/a.foo。这个变量对于构造有关联的文件名比较有效。如果目标中没有模式的定义,“$*”也就不能被推导出,但是,如果目标文件的后缀是make所识别的,“$*”就是除了后缀的那一部分。例如:如果目标是foo.c,因为.c是make所能识别的后缀名,所以,“$*”的值就是foo。这个特性是GNU make的,很有可能不兼容于其他版本的make,所以,应该尽量避免使用“$*”,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,“$*”就是空值。
当希望只对更新过的依赖文件进行操作时,“$?”在显式规则中很有用。例如,假设有一个函数库文件叫lib,其由其他几个object文件更新,把object文件打包的比较有效率的makefile文件规则是:
f lib : foo.o bar.o lose.o win.o
ar r lib $?
在上述所列出来的自动变量中,4个变量($@、$<、$%、$*)在扩展时只会有1个文件,而另3个的值是一个文件列表。这7个自动化变量还可以取得文件的目录名或是在当前目录下的符合模式的文件名,只需要搭配上“D”或“F”字样。这是GNU make中老版本的特性,在新版本中,使用函数dir或notdir就可以做到了。D的含义是Directory,就是目录;F的含义是File,就是文件。
下面是对上面的7个变量分别加上“D”或是“F”的含义:
$(@D)
表示“$@”的目录部分(不以斜杠作为结尾),如果“$@”值是dir/foo.o,“$(@D)”就是dir,而如果“$@”中没有包含斜杠,其值就是“.”(当前目录)。
$(@F)
表示“$@”的文件部分,如果“$@”值是dir/foo.o,“$(@F)”就是foo.o,“$(@F)”相当于函数$(notdir $@)。
$(*D)
$(*F)
和上面所述的同理,也是取文件的目录部分和文件部分。对于上面的那个例子,“$(*D)”返回dir,而“$(*F)”返回foo。
$(%D)
$(%F)
分别表示了函数包文件成员的目录部分和文件部分。这对于形同archive(member)形式的目标中的member中包含了不同的目录很有用。
$(<D)
$(<F)
分别表示依赖文件的目录部分和文件部分。
$(^D)
$(^F)
分别表示所有依赖文件的目录部分和文件部分(无相同的)。
$(+D)
$(+F)
分别表示所有依赖文件的目录部分和文件部分
(
可以有相同的
)
。
$(?D)
$(?F)
分别表示被更新的依赖文件的目录部分和文件部分。
最后提醒一下的是,对于“$<”,为了避免产生不必要的麻烦,最好给“$”后面的那个特定字符都加上圆括号,比如,“$(<)”就要比“$<”好一些。
还要注意的是,这些变量只使用在规则的命令中,而且一般都是“显式规则”和“静态模式规则”,其在隐含规则中并没有意义。
4. 模式的匹配
一般来说,一个目标的模式有一个带有前缀或是后缀的“%”,或是没有前后缀,直接就是一个“%”。因为“%”代表一个或多个字符,所以在定义好了的模式中,把“%”所匹配的内容叫做“茎”,例如“%.c”所匹配的文件test.c中test就是“茎”。因为在目标和依赖目标中同时有“%”时,依赖目标的“茎”会传给目标,当做目标中的“茎”。
当一个模式匹配包含有斜杠(实际也不经常包含)的文件时,在进行模式匹配时,目录部分会首先被移开,然后进行匹配,成功后,再把目录加回去。在进行“茎”的传递时,需要知道这个步骤。例如有一个模式e%t,文件src/eat匹配于该模式,于是src/a就是其“茎”。如果这个模式定义在依赖目标中,而依赖于这个模式的目标中又有个模式c%r,目标就是src/car (“茎”被传递)。
5. 重载内建隐含规则
可以重载内建的隐含规则(或是定义一个全新的),例如可以重新构造和内建隐含规则不同的命令,如:
%.o : %.c
$(gcc) -c $(CPPFLAGS) $(CFLAGS) -D$(date)
可以取消内建的隐含规则,只要不在后面写命令就行。如:
%.o : %.s
同样,也可以重新定义一个全新的隐含规则,其在隐含规则中的位置取决于在哪里写下这个规则。朝前的位置就靠前。
5.9.6 隐含规则搜索算法
比如有一个目标叫T,下面是搜索目标T的规则的算法。请注意,在下面没有提到后缀规则,原因是所有的后缀规则在makefile文件被载入内存时,会转换成模式规则。如果目标是archive(member)的函数库文件模式,这个算法会运行两次,第一次是找目标T,如果没有找到,进入第二次,第二次会把member当作T来搜索。
(1) 把T的目录部分分离出来,叫D,而剩余部分叫N(例如,如果T是src/foo.o,D就是src/,N就是foo.o)。
(2) 创建所有匹配于T或是N的模式规则列表。
(3) 如果在模式规则列表中有匹配所有文件的模式,如“%”,从列表中移除其他的模式。
(4) 移除列表中没有命令的规则。
(5) 对于第一个在列表中的模式规则:
●
推导其“茎”S,S应该是T或是N匹配于模式中“%”非空的部分。
●
计算依赖文件。把依赖文件中的“%”都替换成“茎”S。如果目标模式中没有包含斜框字符,就把D加在第一个依赖文件的开头。
●
测试是否所有的依赖文件都存在或是理当存在(如果有一个文件被定义成另外一个规则的目标文件,或者是一个显式规则的依赖文件,这个文件就叫“理当存在”)。
●
如果所有的依赖文件存在或是理当存在,或是就没有依赖文件,这条规则将被采用,退出该算法。
(6) 如果经过第5步,没有找到模式规则,就作更进一步的搜索。对于存在于列表中的第一个模式规则:
●
如果规则是终止规则,那就忽略它,继续下一条模式规则。
●
计算依赖文件(同第5步)。
●
测试所有的依赖文件是否存在或是理当存在。
●
对于不存在的依赖文件,递归调用这个算法,查找它是否可以被隐含规则找到。
●
如果所有的依赖文件存在或是理当存在,或是就根本没有依赖文件。这条规则被采用,退出该算法。
(7) 如果没有隐含规则可以使用,查看.DEFAULT规则,如果有,就采用,把.DEFAULT的命令给T使用。
一旦规则被找到,就会执行其相当的命令,而此时,自动化变量的值才会生成。
5.10 使用make更新函数库文件
函数库文件也就是对Object文件(程序编译的中间文件)的打包文件。在Unix下,一般是由命令ar来完成打包工作。
5.10.1 函数库文件的成员
一个函数库文件由多个文件组成。可以以如下格式指定函数库文件及其组成:
archive(member)
这不是一个命令,而是一个目标和依赖的定义。一般来说,这种用法基本上就是为了ar命令来服务的。如:
foolib(hack.o) : hack.o
ar cr foolib hack.o
如果要指定多个member,那就以空格分开,如:
foolib(hack.o kludge.o)
其等价于:
foolib(hack.o) foolib(kludge.o)
还可以使用Shell的文件通配符来定义,如:
foolib(*.o)
5.10.2 函数库成员的隐含规则
当make搜索一个目标的隐含规则时,一个特性是,如果这个目标是“a(m)”形式的,其会把目标变成“(m)”。于是,如果成员是“%.o”的模式定义,并且如果使用make foo.a(bar.o)的形式调用makefile文件,隐含规则会去找bar.o的规则;如果没有定义bar.o的规则,内建隐含规则生效,make会去找bar.c文件来生成bar.o。如果找得到,make执行的命令大致如下:
gcc -c bar.c -o bar.o
ar r foo.a bar.o
rm -f bar.o
还有一个变量要注意的是“$%”,这是专属函数库文件的自动化变量。
5.10.3 函数库文件的后缀规则
可以使用后缀规则和隐含规则来生成函数库打包文件,如:
c.a:
$(gcc) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o
其等效于:
(%.o) : %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o
5.10.4 注意事项
在生成函数库打包文件时,请小心使用make的并行机制(-j参数)。如果多个ar命令在同一时间运行在同一个函数库打包文件上,就很有可能损坏这个函数库文件。所以,在make未来的版本中,应该提供一种机制来避免并行操作发生在函数打包文件上。但就目前而言,还是尽量不要使用-j参数。
以上基本上就是GNU make的makefile文件的所有细节了。无论什么样的make,都是以文件的依赖性为基础的,其基本都是遵循一个标准的。对于前述所有的make的细节,不但可以利用make这个工具来编译程序,还可以利用make来完成其他的工作。因为规则中的命令可以是任何Shell之下的命令,所以,在Linux下,不一定只使用程序语言的编译器,还可以在makefile文件中书写其他的命令,如tar、awk、mail、sed、cvs、compress、ls、rm、yacc、rpm、ftp等,来完成诸如程序打包、程序备份、制作程序安装包、提交代码、使用程序模板、合并文件等诸多功能,如文件操作、文件管理、编程开发设计,或是其他一些异想天开的东西。