Makefile基本语法知识
一、概述
相信很多在Linux下编程的同学对Makefile都不陌生,相比较于Windows下编程的大量的IDE而言,Makefile能让我们更清晰的了解到程序编译的过程以及方法。而Windows下大量的IDE却大大简化了这些东西,让我们慢慢远离程序的本质。但是要想成为一个真正的好的程序员,了解Makfile则是非常有必要的。
Makefile 关系到了整个工程的编译规则,我们需要在Makefile中写下我们对这个那个工程文件编译的规划和方法。但是makefile 的语法却没我们想象中的那么难,Makefile支持像shell脚本一样可以执行操作系统的命令。而当我们写好Makefile文件后,只需要执行一个make指令,所有的文件就自动化编译。使用起来也是相当的方便,即一次编写终身使用,后续如右新的文件添加,只需要稍微改动一下这个Makefile文件即可。
Makefile 的规则
假如我们的工程下面有很多的c和h文件,那么当我们执行make命令后,它一般会按照下面的规则来执行。
- 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
- 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。
总结一下,make只需要去编译改动过了文件,当然我们第一次make的时候它是全编的。在此之后makefile只会编译我们改动过了文件,这就大大的提高了程序编译的速度。
目标和依赖
讲Makefile一定离不开这两个概念 ,因为这两个概念是Makefile的核心。讲他们之前我们先来看一下Makefile的基本语法。
目标: 依赖
[tab键]命令
...
或者是:
目标:依赖;命令
[tab键]命令
...
-
目标
可以是一个目标文件、也可以是一个可执行文件、或者标签(伪目标)
-
依赖
生成目标所要依赖的文件,可能不止一个。依赖文件中只要有一个文件比目标新,那么当我们执行make命令的的时候生成目标的命令就会被执行。
-
命令
生成目标所要执行的命令,注意
所有的命令必须要以tab 键开头
。
了解了上面这个模式,你基本上已经可以看懂60%的Makefile文件了。下面来看一个例子:
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
反斜杠(\)表示换行,我们最后要生成的目标edit依赖于main.o kbd.o command.o display.o insert.o search.o files.o utils.o这些文件,此时make会找到这些文件,发现main.o又依赖于main.c 和defs.h,所以就去执行gcc -c main.c 命令,得到main.o。就这样一个一个得到edit所需要的依赖文件后,再执行命令:gcc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o 最终得到我们所需要目标文件。
当我们再次执行make命令的时候,Makefile会检查所有的依赖文件是否有更新,如果有跟新,那么其下面的命令就会被执行。比如说,我们修改main.c的内容,这个时候,生成mian.o的命令会被重新执行,main.o就更新了,接着生成edit目标的命令也会因为main.o的更新而重新执行,其他的目标并不会被重新编译。
那么问题来了,Makefile怎么知道我们的文件被更新了呢?答案是日期,Makefile会对比目标和依赖的修改日期,如果依赖比目标新,那么就会重新编译。
make如何工作
我们知道,当我们在自己的工程目录下敲击make命令后,就会生成我们所需要的最终的可执行文件。那么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会去解析我们当前目录下的Makefile文件,若我们没有指定目标,那么他会默认生成第一个目标文件。第一个目标生成后, make就会退出来,所以我们可以看到在上述文件中,当我们执行完make命令后,相关的中间文件并没有被删除。即我们的clean 目标下的命令并没有被执行。假设我们要执行clean,就必须要指定,即使用make clean命令,其下的命令才会被执行。所以这就是为什么我们一般会将最终的目标文件放到第一个的原因,若我们将clean放到edit前面的话,那么edit并不会被生成。
伪目标
我们会发现我们的Makefile中,clean和edit有很大的不同,即当前目录下并没有这个文件生成。准确的来说这并不是一个目标而更像一个标签,它的存在只是为了让我们的make去执行某些指令。在Makefile中这类标签有个统一叫法,称为伪目标。在makefile中一般用.PHONY
来标记,所以前面的makefile其实并不标准,下面我们来将它改一下:
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
.PHONY : clean
clean :
@-rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
#注意这里的“-”号,clean一般默认放在Makfile的末尾
其语法就是:
.PHONY:目标
将我们的目标申明成为一个伪目标,这样make在解析的时候就会将其当做一个伪目标来处理。并不会去找它的依赖,而是去执行其后的命令,避免了没有依赖指令不执行的情况。
可以看到这次我在rm 前加了个**“-”**号,它的含义是:也许某些文件会出现问题,但是不要管继续做后面的事。
Makfile使用“#
”来注释,但一个“#
”只能注释一行,类似于C语言里的“//”。
这里的“@”符号作用是在执行该条命令时不讲它显示在终端上,当然了有时候我们需要让它显示程序编译的顺序还会专门让它显示一些东西到终端上面。比如:
@echo 正在编译XXX模块
当我们当make执行时,会输出“正在编译XXX模块……”字串,但不会输出命令,如果没有“@”,那么,make将输出:
echo 正在编译XXX模块......
正在编译XXX模块......
多目标
有时候我们的Makfile中的目标不止一个,可能有多个。而make默认去执行第一个目标文件,当我们想生成其他的目标时可以使用make + 目标的方式。指定make去生成我们所需要的目标。
引用其他的Makefile文件
类似于C语言中引用其他文件,Makefile中也可以使用include
关键将别的Makefile包含进来。被包含的文件会原模原样放在当前文件包含位置。比如:
test.mk
.PHONY : clean all
clean all:
-rm -rf a.o b.o c.o
test1.mk
a.o : a.c
gcc -c a.c
b.o : b.c
gcc -c b.c
c.o : c.c
gcc -c c.c
test2.mk
all: a.o b.o c.o
gcc -o all a.o b.o c.o
include test1.mk test2.mk
相当于:
all: a.o b.o c.o
gcc -o all a.o b.o c.o
include test1.mk test2.mk
a.o : a.c
gcc -c a.c
b.o : b.c
gcc -c b.c
c.o : c.c
gcc -c c.c
.PHONY : clean all
clean all:
-rm -rf a.o b.o c.o
在 include
前面可以有一些空字符,但是绝不能是 Tab
键开始。 include
和 <filename>
可以用一个或多个空格隔开。make命令开始时,会找寻 include
所指出的其它Makefile,并把其内容安置在当前的位置.
如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
- 如果make执行时,有
-I
或--include-dir
参数,那么make就会在这个参数所指定的目录下去寻找。 - 如果目录
<prefix>/include
(一般是:/usr/local/bin
或/usr/include
)存在的话,make也会去找。
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。
all: a.o b.o c.o
gcc -o all a.o b.o c.o
-include test1.mk test2.mk
make 工作流程
GNU的make工作时的执行步骤一般如下:
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。
Makfile内容
那么Makfile中主要包含哪些东西呢?结合上面的所给的Makfile例子,我们可以推导出来:显示规则、隐晦规则、变量定义、文件指示、注释这五个东西。
- 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
- 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
- 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。
- 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用
#
字符,这个就像C/C++中的//
一样。如果你要在你的Makefile中使用#
字符,可以用反斜杠进行转义,如:\#
。
Makfile命名
一般我们会给Makefile文件命名为Makefile,但是有的程序员也会将其命名为“GNUmakefile”、“makfile”,这些当然都是可以的,执行make时make会默认在当前目录下找名字为这几个名字的Makfile文件。但是更建议你将其命名为“Makefile”,这也是现在比较通用的命名,Linux内核中也是采用这一命名规则。
当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,Make.Solaris”,Make.AIX”等,如果要指定特定的Makefile,你可以使用make的 -f
和 --file
参数,如: make -f Make.Linux
或 make --file Make.AIX
。
自动推导
GNU的make是很强大的,它可以自动推导文件以及文件依赖关系后的main的命令,这样我们就可以大大简化我们的Makefile:
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 : 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 main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
这种方法也是make的隐晦规则
,它会自动去推导生成我们目标的方法,比如说main.o,我们并没有写相关的命令,但是make会自动推导出相关的命令并且执行。可以看到main.o后面的依赖中我们并没有写main.c,这就利用了make自动推导文件的功能,它会自动推导出我们需要main.c,而将其补全。
但是有时候,这种写法并不讨好,特别是当我们在看其他人写的Makfile时,或者在团队中别人需要看我们写的Makefile时。他/她并不知道文件的依赖关系,这样就会给别人的阅读带来困难。所以我们还是要写全比较好,既方便自己日后查看,也方便别人阅读我们写的Makfile。这提的目的是希望大家在遇到类似的Makfile语法后能够看懂。
make运行
一般来说,最简单的就是直接在命令行下输入make命令,make命令会找当前目录的makefile来执行,一切都是自动的。但是有时候我们可能定义了好几套规则,我们想在不同的时候使用不同的编译规则。该怎么办呢?
make的退出码
make命令执行后有三个退出码:
-
0
表示成功执行。
-
1
如果make运行时出现任何错误,其返回1。
-
2
如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。
指定Makfile
前面我们说过,GNU make找寻默认的Makefile的规则是在当前目录下依次找三个文件——“GNUmakefile”、“makefile”和“Makefile”。其按顺序找这三个文件,一旦找到,就开始读取这个文件并执行。
当前,我们也可以给make命令指定一个特殊名字的Makefile。要达到这个功能,我们要使用make的 -f
或是 --file
参数( --makefile
参数也行)。例如,我们有个makefile的名字是“hchen.mk”,那么,我们可以这样来让make来执行这个文件:
make –f hchen.mk
如果在make的命令行是,你不只一次地使用了 -f
参数,那么,所有指定的makefile将会被连在一起传递给make执行。
指定目标
一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。当然,一般来说,你的makefile中的第一个目标是由许多个目标组成,你可以指示make,让其完成你所指定的目标。要达到这一目的很简单,需在make命令后直接跟目标的名字就可以完成(如前面提到的“make clean”形式)
任何在makefile中的目标都可以被指定成终极目标,但是除了以 -
打头,或是包含了 =
的目标,因为有这些字符的目标,会被解析成命令行参数或是变量。甚至没有被我们明确写出来的目标也可以成为make的终极目标,也就是说,只要make可以找到其隐含规则推导规则,那么这个隐含目标同样可以被指定成终极目标。
有一个make的环境变量叫 MAKECMDGOALS
,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。这个变量可以让你使用在一些比较特殊的情形下。比如下面的例子:
sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif
基于上面的这个例子,只要我们输入的命令不是“make clean”,那么makefile会自动包含“foo.d”和“bar.d”这两个makefile。
使用指定终极目标的方法可以很方便地让我们编译我们的程序,例如下面这个例子:
.PHONY: all
all: prog1 prog2 prog3 prog4
从这个例子中,我们可以看到,这个makefile中有四个需要编译的程序——“prog1”, “prog2”,“prog3”和 “prog4”,我们可以使用“make all”命令来编译所有的目标(如果把all置成第一个目标,那么只需执行“make”),我们也可以使用 “make prog2”来单独编译目标“prog2”。
即然make可以指定所有makefile中的目标,那么也包括“伪目标”,于是我们可以根据这种性质来让我们的makefile根据指定的不同的目标来完成不同的事。在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。
- all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
- clean:这个伪目标功能是删除所有被make创建的文件。
- install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
- print:这个伪目标的功能是例出改变过的源文件。
- tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。
- dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
- TAGS:这个伪目标功能是更新所有的目标,以备完整地重编译使用。
- check和test:这两个伪目标一般用来测试makefile的流程。
检查规则
有时候,我们不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。于是我们可以使用make命令的下述参数:
-
-n
,--just-print
,--dry-run
,--recon
不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。
-
-t
,--touch
这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
-
-q
,--question
这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
-
-W <file>
,--what-if=<file>
,--assume-new=<file>
,--new-file=<file>
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。
另外一个很有意思的用法是结合 -p
和 -v
来输出makefile被执行时的信息。
二、Makefile高级语法
Makfile语法中也有一些比较高级的用法,比如说变量、函数、条件语句等,使用这些语法可以大大提高我们Makefile文件的灵活性和简化我们工作。
变量
Makefile中也是可以使用变量的,这将大大简化我们的工作。可以看到我们前面所写的Makefile文件非常繁琐,不够简洁。假如我们在工程中添加了一个问价则需要在所有用到它的地方都需要手动加上这个文件,文件少的时候还好,文件比较多的时候,就非常容易麻烦而且容易因为漏写导致错误。而现在我们学习变量后这将大大简化我们的工作。
总的来讲,Makefile中比较常用的变量可以分为四类:延时变量、即时变量、自动变量以及环境变量。下面会一一介绍。
Makefile中的变量其实就是类似于C原因中的宏定义,相应的变量只在我们使用到它的时候做一个替换。所以你只需要将Makfile中的变量当做C语言中的宏理解就好了。
延时变量
我们定义一个变量OBJS来放目标文件:
OBJS = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(OBJS)
gcc -o edit $(OBJS)
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
.PHONY : clean
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
后面如果有新的.o文件加进来我们只需要修改OBJS变量即可$
符号用来取变量的值,这是shell中的用法。我们的Makefile使用的shell的指令。$(OBJS)就表示取出变量的值,也就是该变量后面的内容。
变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $
符号,但最好用小括号 ()
或是大括号 {}
把变量给包括起来。如果你要使用真实的 $
字符,那么你需要用 $$
来表示。给变量加上括号完全是为了更加安全地使用这个变量。
延时变量
,直到我们使用它的时候,它才被赋值。
为了理解延时变量我们来看一个小例子:
VAR = $(var)
var = "hello,world"
all:
@echo $(VAR)
执行make后输出:
hello world
可以看到我们在定义变量VAR的时候是用的var变量来为它进行初始化,但是此时var是没有值的,变量var是在后边被赋值的。但是当我们打印变量VAR时却可以打印出来正确的值。这就可以看前面讲到的,延时变量知道我们使用它的时候它才会被会被赋值。即我们在定义VAR的时候它没有被立即赋值,而像是一个申明,当我们打印变量VAR的时候,make解释器会去找该变量在哪里被定义然后将该定义值赋给VAR。
即时变量
即时变量的定义形式为::=
,其含义是:
变量的值即刻被确定,即定义时即被确定。
显然这种定义形式就不能引用后面定义的变量了,下面来看一个例子:
var1 := a.o
var2 := $(var) b.o
var := c.o
最后的结果为:
var的值是c.o,而var1的值是a.o b.o
变量的另外两种定义形式:+=
、?=
?=
的含义是:如果是第一次定义变量,那么定义生效,如果不是第一次定义变量,则忽略本次定义。
同样的还是来看一个例子:
var1 = "abc"
var1 ?= "hello world!"
all:
@echo $(var)
执行make后输出:
abc
+=
的含义是:为变量追加值,但是如果变量之前没有没定义过,那么+=
会自动转化为=
,如果前面变量的定义形式为:=
那么+=
会以:=
为其赋值。
来看个例子:
var = "hello"
var += " world"
all:
@echo $(var)
执行make指令后输出:
hello world
自动变量
除了上面提到的几种形式的变量外,make还提供了一系列的自动变量,这些自动变量是根据make自动根据规则生成的,不需要显示指出相应的文件或目标名。事实上正是这些自动变量让我们在第一次看别人写的Makfile文件时看的一脸懵逼。下面列出一些比较常见的自动变量的形式:
形式 | 含义 |
---|---|
$@ | 目标文件 |
$< | 第一个依赖文件 |
$^ | 所有的依赖文件,会自动去重复文件 |
$% | 仅当目标文件为归档成员文件(.lib或.a)时,显示文件名否则为空 |
$? | 当前依赖列表中比目标新的文件,用空格隔开 |
$+ | 与$^相似,但是不会去除重复文件 |
$* | 显示目标文件的主干文件名,不包含后缀部分 |
其实比较常用的是前面三个,下面来写个例子:
CC = gcc
object = a.o b.o c.o
program := test
$(program):$(object)
$(CC) $(objeect) -o $@ #$@表示目标文件,即$(program)
*.o : *.c
$(CC) -c $^ #$^这里表示第一个依赖文件
.PHONY : clean
clean:
-rm *.o
这里还用到了通配符:*
,如*.o表示所有以.o结尾的文件,适配所有符合这个形式的文件而不管前面字符数量。
值得注意的是:自动化变量在运行时才有的。
通配符
前面讲过Makefile中支持bash-shell相关指令,其实Makefile还支持Bash-shell中先关的通配符。下面列出比较常见的几个:
形式 | 含义 |
---|---|
* | 匹配多个字母,常用于区匹配符合某种形式的多个文件 |
? | 匹配单一字符 |
~ | 表示用户家目录,如~/bin,表示$HOME目录下的bin目录 |
这个是比较简单的,这里不再做过多的描述。
环境变量
make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)
因此,如果我们在环境变量中设置了 CFLAGS
环境变量,那么我们就可以在所有的Makefile中使用这个变量了。这对于我们使用统一的编译参数有比较大的好处。如果Makefile中定义了CFLAGS,那么则会使用Makefile中的这个变量,如果没有定义则使用系统环境变量的值,一个共性和个性的统一,很像“全局变量”和“局部变量”的特性。
当make嵌套调用时(参见前面的“嵌套调用”章节),上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层Makefile传递,则需要使用exprot关键字来声明。
定义太多的环境变量并不能给我们带来好处,相反很多时候还会带来麻烦,比如做移植工作时。
变量的高级玩法
目标变量
前面提到的即时变量也好,延时变量也好,基本都属于全局变量。即定义的变量在整个文件下都是可用的。那么如果我们想要为某个特定的目标定义变量又该怎么办呢?这就可以使用到目标变量:
<target ...> : <varible-assignment>;
<target ...> : overide <varible-assignment>;
目标变量也可以使用我们前面讲到的定义变量所使用的哪几种形式:=
、+=
、?=
。当然也可以定义环境变量。
test: CFLAGS = -g
test: overide CC := gcc
test: test.o foo.o bar.o
$(CC) $(CFLAGS) test.o foo.o bar.o
test.o: test.c
$(CC) $(CFLAGS) test.c
foo.o: foo.c
$(CC) $(CFLAGS) foo.c
bar.o: bar.c
$(CC) $(CFLAGS) bar.c
这里面我们定义了一个目标变量CFLAGS,同C语言中的局部变量的特性一样。不管CFLAGS全局的定义是什么,在目标变量范围内只有目标范围值生效,其对全局变量是屏蔽的
多行变量
还有一种设置变量值的方法是使用define关键字。使用define关键字设置变量的值可以有换行,以endef关键字结尾。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其它变量。因为命令需要以[Tab]键开头,所以如果你用define定义的命令变量中没有以 Tab
键开头,那么make 就不会把其认为是命令。
define two-lines
echo foo
echo $(bar)
endef
使用define关键字还可以对命令进行打包,即将多行命令进行打包。
overide 指示符
如果有变量是通常make的命令行参数设置的,那么Makefile中对这个变量的赋值会被忽略。如果你想在Makefile中设置这类参数的值,那么,你可以使用“override”指示符。常见的三种形式为:
override <variable>; = <value>;
override <variable>; := <value>;
override <variable>; += <more text>;
对于多行的变量定义,我们用define指示符,在define指示符前,也同样可以使用override指示符,如:
override define foo
bar
endef
模式变量
在GNU的make中,还支持模式变量(Pattern-specific Variable),通过上面的目标变量中,我们知道,变量可以定义在某个目标上。模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。
我们知道,make的“模式”一般是至少含有一个 %
的,所以,我们可以以如下方式给所有以 .o
结尾的目标定义目标变量:
%.o : CFLAGS = -O
同样,模式变量的语法和“目标变量”一样:
<pattern ...>; : <variable-assignment>;
<pattern ...>; : override <variable-assignment>;
override同样是针对于系统环境传入的变量,或是make命令行指定的变量。
三、条件判断
Makefile语法中还可以使用条件判断,比如说让make根据不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或者是变量和常量的值。
ifeq 关键字
下面来看一个例子:
objects = a.o b.o c.o
CC = gcc
test:$(objects)
ifeq($(CC),gcc) #如果变量CC的值等于gcc那么执行下面这句命令
$(CC) -o test $(objects)
else
@echo "error"
endif
上面的例子中,我们拿变量CC的值和gcc作比较,来执行不同的命令。ifeq
的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。 else
表示条件表达式为假的情况。 endif
表示一个条件语句的结束,任何一个条件表达式都应该以 endif
结束。
ifneq关键字
比较两个参数的值是不是不相等,其含义和ifeq刚好相反。
ifeq ($(KERNELRELEASE),)
ifeq($(ARCH),arm)
xxxx...
else
xxxx....
endif
xxxx...
else
xxxx...
endif
这个形式是我们写内核驱动时比较常见的一个形式,含有两层if,这个可以按照C语言中的形式去理解。其中ifeq ($(KERNELRELEASE),),‘,
’后边没有写内容表示空,即如果变量KERNELRELEASE的值为空,那么执行其下面的命令。
ifdef关键字
一般形式:
ifdef <variable-name>
xxxx...
如果变量 <variable-name>
的值非空,那到表达式为真。否则,表达式为假。当然,
<variable-name>
同样可以是一个函数的返回值。注意, ifdef
只是测试一个变量是否有值,其并不会把变量扩展到当前位置。
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
执行make后结果为:
yes
下面再看一个例子:
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
执行make后结果为:
no
ifndef关键字
其一般形式为:
ifndef <variable-name>
xxxx...
显然它和ifdef
含义相反,表示如果没有定义变量则执行相关命令。其后面也要连接else
和endif
关键字使用。
四、函数
在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。
函数调用的语法
Makfile中函数调用的一般形式为:
$(<function> <arguments>)
function
为函数名,arguments
为函数参数。参数间以逗号 ,
分隔,而函数名和参数之间以“空格”分隔。函数调用以 $
开头,以圆括号或花括号把函数名和参数括起。函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用 $(subst a,b,$(x))
这样的形式,而不是$(subst a,b, ${x})
的形式。前面有提到过函数的返回值是可以当做变量来使用,现在是不是可以将二者来联系起来了?下面来看一个例子:
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
在这个示例中, $(comma)
的值是一个逗号。 $(space)
使用了 $(empty)
定义了一个空格, $(foo)
的值是 a b c
, $(bar)
的定义用,调用了函数 subst
,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把 $(foo)
中的空格替换成逗号,所以 $(bar)
的值是 a,b,c
。
字符串处理函数
subst
$(subst <from>,<to>,<text>)
-
名称:字符串替换函数
-
功能:把字串
<text>
中的<from>
字符串替换成<to>
。 -
返回:函数返回被替换过后的字符串。
-
示例:
$(subst ee,EE,feet on the street)
把 feet on the street
中的 ee
替换成 EE
,返回结果是 fEEt on the strEEt
。
patsubst
$(patsubst <pattern>,<replacement>,<text>)
-
名称:模式字符串替换函数。
-
功能:查找
<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
$(strip <string>)
-
名称:去空格函数。
-
功能:去掉
<string>
字串中开头和结尾的空字符。 -
返回:返回被去掉空格的字符串值。
-
示例:
$(strip a b c )
把字串
a b c `` 去到开头和结尾的空格,结果是 ``a b c
。
findstring
$(findstring <find>,<in>)
-
名称:查找字符串函数
-
功能:在字串
<in>
中查找<find>
字串。 -
返回:如果找到,那么返回
<find>
,否则返回空字符串。 -
示例:
$(findstring a,a b c) $(findstring a,b c)
第一个函数返回 a
字符串,第二个返回空字符串
filter
$(filter <pattern...>,<text>)
-
名称:过滤函数
-
功能:以
<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
$(filter-out <pattern...>,<text>)
-
名称:反过滤函数
-
功能:以
<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
$(sort <list>)
- 名称:排序函数
- 功能:给字符串
<list>
中的单词排序(升序)。 - 返回:返回排序后的字符串。
- 示例:
$(sort foo bar lose)
返回bar foo lose
。 - 备注:
sort
函数会去掉<list>
中相同的单词。
word
$(word <n>,<text>)
- 名称:取单词函数
- 功能:取字符串
<text>
中第<n>
个单词。(从一开始) - 返回:返回字符串
<text>
中第<n>
个单词。如果<n>
比<text>
中的单词数要大,那么返回空字符串。 - 示例:
$(word 2, foo bar baz)
返回值是bar
。
wordlist
$(wordlist <ss>,<e>,<text>)
- 名称:取单词串函数
- 功能:从字符串
<text>
中取从<ss>
开始到<e>
的单词串。<ss>
和<e>
是一个数字。 - 返回:返回字符串
<text>
中从<ss>
到<e>
的单词字串。如果<ss>
比<text>
中的单词数要大,那么返回空字符串。如果<e>
大于<text>
的单词数,那么返回从<ss>
开始,到<text>
结束的单词串。 - 示例:
$(wordlist 2, 3, foo bar baz)
返回值是bar baz
。
words
$(words <text>)
- 名称:单词个数统计函数
- 功能:统计
<text>
中字符串中的单词个数。 - 返回:返回
<text>
中的单词数。 - 示例:
$(words, foo bar baz)
返回值是3
。 - 备注:如果我们要取
<text>
中最后的一个单词,我们可以这样:$(word $(words <text>),<text>)
firstword
$(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搜索头文件路径的参数。
文件名操作函数
dir
$(dir <names...>)
- 名称:取目录函数——dir。
- 功能:从文件名序列
<names>
中取出目录部分。目录部分是指最后一个反斜杠(/
)之前的部分。如果没有反斜杠,那么返回./
。 - 返回:返回文件名序列
<names>
的目录部分。 - 示例:
$(dir src/foo.c hacks)
返回值是src/ ./
。
notdir
$(notdir <names...>)
- 名称:取文件函数——notdir。
- 功能:从文件名序列
<names>
中取出非目录部分。非目录部分是指最後一个反斜杠(/
)之后的部分。 - 返回:返回文件名序列
<names>
的非目录部分。 - 示例:
$(notdir src/foo.c hacks)
返回值是foo.c hacks
。
suffix
$(suffix <names...>)
- 名称:取後缀函数——suffix。
- 功能:从文件名序列
<names>
中取出各个文件名的后缀。 - 返回:返回文件名序列
<names>
的后缀序列,如果文件没有后缀,则返回空字串。 - 示例:
$(suffix src/foo.c src-1.0/bar.c hacks)
返回值是.c .c
。
basename
$(basename <names...>)
- 名称:取前缀函数——basename。
- 功能:从文件名序列
<names>
中取出各个文件名的前缀部分。 - 返回:返回文件名序列
<names>
的前缀序列,如果文件没有前缀,则返回空字串。 - 示例:
$(basename src/foo.c src-1.0/bar.c hacks)
返回值是src/foo src-1.0/bar hacks
。
addsuffix
$(addsuffix <suffix>,<names...>)
- 名称:加后缀函数——addsuffix。
- 功能:把后缀
<suffix>
加到<names>
中的每个单词后面。 - 返回:返回加过后缀的文件名序列。
- 示例:
$(addsuffix .c,foo bar)
返回值是foo.c bar.c
。
join
$(join <list1>,<list2>)
- 名称:连接函数——join。
- 功能:把
<list2>
中的单词对应地加到<list1>
的单词后面。如果<list1>
的单词个数要比<list2>
的多,那么,<list1>
中的多出来的单词将保持原样。如果<list2>
的单词个数要比<list1>
多,那么,<list2>
多出来的单词将被复制到<list1>
中。 - 返回:返回连接过后的字符串。
- 示例:
$(join aaa bbb , 111 222 333)
返回值是aaa111 bbb222 333
。
循环和判断函数
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>
中的单词。举个例子:
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函数当中。
if
if函数很像GNU的make所支持的条件语句——ifeq(参见前面所述的章节),if函数的语法是:
$(if <condition>,<then-part>)
或是
$(if <condition>,<then-part>,<else-part>)
可见,if函数可以包含“else”部分,或是不含。即if函数的参数可以是两个,也可以是三个。 <condition>
参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是, <then-part>
会被计算,否则 <else-part>
会被计算。
而if函数的返回值是,如果 <condition>
为真(非空字符串),那个 <then-part>
会是整个函数的返回值,如果 <condition>
为假(空字符串),那么 <else-part>
会是整个函数的返回值,此时如果 <else-part>
没有被定义,那么,整个函数返回空字串。
所以, <then-part>
和 <else-part>
只会有一个被计算。
其他函数
call 函数
call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以call函数来向这个表达式传递参数。其语法是:
$(call <expression>,<parm1>,<parm2>,...,<parmn>)
当make执行这个函数时, <expression>
参数中的变量,如 $(1)
、 $(2)
等,会被参数 <parm1>
、 <parm2>
、 <parm3>
依次取代。而 <expression>
的返回值就是 call 函数的返回值。例如:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
那么, foo
的值就是 a b
。当然,参数的次序是可以自定义的,不一定是顺序的,如:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
此时的 foo
的值就是 b a
。
需要注意:在向 call 函数传递参数时要尤其注意空格的使用。call 函数在处理参数时,第2个及其之后的参数中的空格会被保留,因而可能造成一些奇怪的效果。因而在向call函数提供参数时,最安全的做法是去除所有多余的空格。
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中,我们可以这样写:
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
当然,你也许会说,使用 override
关键字不就可以重新定义环境中的变量了吗?为什么需要使用这样的步骤?是的,我们用 override
是可以达到这样的效果,可是 override
过于粗暴,它同时会把从命令行定义的变量也覆盖了,而我们只想重新定义环境传来的,而不想重新定义命令行传来的。
shell 函数
shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。
控制make的函数
make提供了一些函数来控制make的运行。通常,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。
$(error <text ...>)
产生一个致命的错误, <text ...>
是错误信息。注意,error函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。例如:
示例一:
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
示例二:
ERR = $(error found an error!)
.PHONY: err
err: $(ERR)
示例一会在变量ERROR_001定义了后执行时产生error调用,而示例二则在目录err被执行时才发生error调用。
$(warning <text ...>)
这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。
总结
其实Makefile的函数并不太多,但是我们也不需要去死记硬背它。只要记住常用的几个和函数使用的一般形式,遇到自己不清楚的再去查一下就可以了。
四、Makefile知识点总结思维导图
待更新…
【本文参考】:
- 跟我一起写Makfile
- 《GNU make中文手册》