Makefile的使⽤⽅法
什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个⼯作,但我觉得要作⼀个好的和
professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为⼀个专业⼈⼠,你还是要了解HTML
的标识的含义。特别在Unix下的软件编译,你就不能不⾃⼰写makefile了,会不会写makefile,从⼀个侧⾯说明了⼀个⼈是否具备完成⼤型
⼯程的能⼒。
因为,makefile关系到了整个⼯程的编译规则。⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,makefile定义
了⼀系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄于进⾏更复杂的功能操作,因为
makefile就像⼀个Shell脚本⼀样,其中也可以执⾏操作系统的命令。
makefile带来的好处就是——“⾃动化编译”,⼀旦写好,只需要⼀个make命令,整个⼯程完全⾃动编译,极⼤的提⾼了软件开发的效率。
make是⼀个命令⼯具,是⼀个解释makefile中指令的命令⼯具,⼀般来说,⼤多数的IDE都有这个命令,⽐如:Delphi的make,Visual
C++的nmake,Linux下GNU的make。可见,makefile都成为了⼀种在⼯程⽅⾯的编译⽅法。
现在讲述如何写makefile的⽂章⽐较少,这是我想写这篇⽂章的原因。当然,不同产商的make各不相同,也有不同的语法,但其本质都是
在“⽂件依赖性”上做⽂章,这⾥,我仅对GNU的make进⾏讲述,我的环境是RedHatLinux8.0,make的版本是3.80。必竟,这个make是应
⽤最为⼴泛的,也是⽤得最多的。⽽且其还是最遵循于IEEE1003.2-1992标准的(POSIX.2)。
在这篇⽂档中,将以C/C++的源码作为我们基础,所以必然涉及⼀些关于C/C++的编译的知识,相关于这⽅⾯的内容,还请各位查看相关的
编译器的⽂档。这⾥所默认的编译器是UNIX下的GCC和CC。
关于程序的编译和链接
——————————
在此,我想多说关于程序编译的⼀些规范和⽅法,⼀般来说,⽆论是C、C++、还是pas,⾸先要把源⽂件编译成中间代码⽂件,在
Windows下也就是.obj⽂件,UNIX下是.o⽂件,即ObjectFile,这个动作叫做编译(compile)。然后再把⼤量的ObjectFile合成执⾏⽂
件,这个动作叫作链接(link)。
编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头⽂件的所在位置(头⽂件中应该只
是声明,⽽定义应该放在C/C++⽂件中),只要所有的语法正确,编译器就可以编译出中间⽬标⽂件。⼀般来说,每个源⽂件都应该对应于
⼀个中间⽬标⽂件(O⽂件或是OBJ⽂件)。
链接时,主要是链接函数和全局变量,所以,我们可以使⽤这些中间⽬标⽂件(O⽂件或是OBJ⽂件)来链接我们的应⽤程序。链接器并不
管函数所在的源⽂件,只管函数的中间⽬标⽂件(ObjectFile),在⼤多数时候,由于源⽂件太多,编译⽣成的中间⽬标⽂件太多,⽽在链
接时需要明显地指出中间⽬标⽂件名,这对于编译很不⽅便,所以,我们要给中间⽬标⽂件打个包,在Windows下这种包叫“库⽂
件”(LibraryFile),也就是.lib⽂件,在UNIX下,是ArchiveFile,也就是.a⽂件。
总结⼀下,源⽂件⾸先会⽣成中间⽬标⽂件,再由中间⽬标⽂件⽣成执⾏⽂件。在编译时,编译器只检测程序语法,和函数、变量是否被声
明。如果函数未被声明,编译器会给出⼀个警告,但可以⽣成ObjectFile。⽽在链接程序时,链接器会在所有的ObjectFile中找寻函数的实
现,如果找不到,那到就会报链接错误码(LinkerError),在VC下,这种错误⼀般是:Link2001错误,意思说是说,链接器未能找到函数
的实现。你需要指定函数的ObjectFile.
Makefile介绍
———————
make命令执⾏时,需要⼀个Makefile⽂件,以告诉make命令需要怎么样的去编译和链接程序。
⾸先,我们⽤⼀个⽰例来说明Makefile的书写规则。以便给⼤家⼀个感兴认识。这个⽰例来源于GNU的make使⽤⼿册,在这个⽰例中,我
们的⼯程有8个C⽂件,和3个头⽂件,我们要写⼀个Makefile来告诉make命令如何编译和链接这⼏个⽂件。我们的规则是:
1)如果这个⼯程没有编译过,那么我们的所有C⽂件都要编译并被链接。
2)如果这个⼯程的某⼏个C⽂件被修改,那么我们只编译被修改的C⽂件,并链接⽬标程序。
3)如果这个⼯程的头⽂件被改变了,那么我们需要编译引⽤了这⼏个头⽂件的C⽂件,并链接⽬标程序。
只要我们的Makefile写得够好,所有的这⼀切,我们只⽤⼀个make命令就可以完成,make命令会⾃动智能地根据当前的⽂件修改的情况来
确定哪些⽂件需要重编译,从⽽⾃⼰编译所需要的⽂件和链接⽬标程序。
⼀、Makefile的规则
在讲述这个Makefile之前,还是让我们先来粗略地看⼀看Makefile的规则。
target...:prerequisites...
command
...
...
target也就是⼀个⽬标⽂件,可以是ObjectFile,也可以是执⾏⽂件。还可以是⼀个标签(Label),对于标签这种特性,在后续的“伪⽬
标”章节中会有叙述。
prerequisites就是,要⽣成那个target所需要的⽂件或是⽬标。
command也就是make需要执⾏的命令。(任意的Shell命令)
这是⼀个⽂件的依赖关系,也就是说,target这⼀个或多个的⽬标⽂件依赖于prerequisites中的⽂件,其⽣成规则定义在command中。说⽩
⼀点就是说,prerequisites中如果有⼀个以上的⽂件⽐target⽂件要新的话,command所定义的命令就会被执⾏。这就是Makefile的规则。
也就是Makefile中最核⼼的内容。
⼆、⼀个⽰例
正如前⾯所说的,如果⼀个⼯程有3个头⽂件,和8个C⽂件,我们为了完成前⾯所述的那三个规则,我们的Makefile应该是下⾯的这个样⼦
的。
edit:ay.o
.o
ay.o
.o
main.o:.h
cc-cmain.c
kbd.o:nd.h
cc-ckbd.c
command.o:nd.h
cc-ccommand.c
display.o:r.h
cc-cdisplay.c
inrt.o:r.h
cc-cinrt.c
arch.o:r.h
cc-carch.c
files.o:nd.h
cc-cfiles.c
utils.o:.h
cc-cutils.c
clean:
ay.o
.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命令。那么,
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了。
这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个⽬标⽂件。在找寻的过程中,如果出现错
误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。
make只管⽂件的依赖性,即,如果在我找了依赖关系之后,冒号后⾯的⽂件还是不存在,那么对不起,我就不⼯作啦。
通过上述分析,我们知道,像clean这种,没有被第⼀个⽬标⽂件直接或间接关联,那么它后⾯所定义的命令将不会被⾃动执⾏,不过,我
们可以显⽰要make执⾏。即命令——“makeclean”,以此来清除所有的⽬标⽂件,以便重编译。
于是在我们编程中,如果这个⼯程已被编译过了,当我们修改了其中⼀个源⽂件,⽐如file.c,那么根据我们的依赖性,我们的⽬标file.o会被
重编译(也就是在这个依性关系后⾯所定义的命令),于是file.o的⽂件也是最新的啦,于是file.o的⽂件修改时间要⽐edit要新,所以edit也
会被重新链接了(详见edit⽬标⽂件后定义的命令)。
⽽如果我们改变了“command.h”,那么,kdb.o、command.o和files.o都会被重编译,并且,edit会被重链接。
四、makefile中使⽤变量
在上⾯的例⼦中,先让我们看看edit的规则:
edit:ay.o
.o
ay.o
.o
我们可以看到[.o]⽂件的字符串被重复了两次,如果我们的⼯程需要加⼊⼀个新的[.o]⽂件,那么我们需要在两个地⽅加(应该是三个地⽅,
还有⼀个地⽅在clean中)。当然,我们的makefile并不复杂,所以在两个地⽅加也不累,但如果makefile变得复杂,那么我们就有可能会忘
掉⼀个需要加⼊的地⽅,⽽导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使⽤变量。makefile的变量也就是⼀个字符
串,理解成C语⾔中的宏可能会更好。
⽐如,我们声明⼀个变量,叫objects,OBJECTS,objs,OBJS,obj,或是OBJ,反正不管什么啦,只要能够表⽰obj⽂件就⾏了。我们在
makefile⼀开始就这样定义:
objects=ay.o
.o
于是,我们就可以很⽅便地在我们的makefile中以“$(objects)”的⽅式来使⽤这个变量了,于是我们的改良版makefile就变成下⾯这个样⼦:
objects=ay.o
.o
edit:$(objects)
cc-oedit$(objects)
main.o:.h
cc-cmain.c
kbd.o:nd.h
cc-ckbd.c
command.o:nd.h
cc-ccommand.c
display.o:r.h
cc-cdisplay.c
inrt.o:r.h
cc-cinrt.c
arch.o:r.h
cc-carch.c
files.o:nd.h
cc-cfiles.c
utils.o:.h
cc-cutils.c
clean:
rmedit$(objects)
于是如果有新的.o⽂件加⼊,我们只需简单地修改⼀下objects变量就可以了。
关于变量更多的话题,我会在后续给你⼀⼀道来。
五、让make⾃动推导
GNU的make很强⼤,它可以⾃动推导⽂件以及⽂件依赖关系后⾯的命令,于是我们就没必要去在每⼀个[.o]⽂件后都写上类似的命令,因
为,我们的make会⾃动识别,并⾃⼰推导命令。
只要make看到⼀个[.o]⽂件,它就会⾃动的把[.c]⽂件加在依赖关系中,如果make找到⼀个whatever.o,那么whatever.c,就会是whatever.o
的依赖⽂件。并且cc-cwhatever.c也会被推导出来,于是,我们的makefile再也不⽤写得这么复杂。我们的是新的makefile⼜出炉了。
objects=ay.o
.o
edit:$(objects)
cc-oedit$(objects)
main.o:defs.h
kbd.o:nd.h
command.o:nd.h
display.o:r.h
inrt.o:r.h
arch.o:r.h
files.o:nd.h
utils.o:defs.h
.PHONY:clean
clean:
rmedit$(objects)
这种⽅法,也就是make的“隐晦规则”。上⾯⽂件内容中,“.PHONY”表⽰,clean是个伪⽬标⽂件。
关于更为详细的“隐晦规则”和“伪⽬标⽂件”,我会在后续给你⼀⼀道来。
七、清空⽬标⽂件的规则
每个Makefile中都应该写⼀个清空⽬标⽂件(.o和执⾏⽂件)的规则,这不仅便于重编译,也很利于保持⽂件的清洁。这是⼀个“修养”(呵
呵,还记得我的《编程修养》吗)。⼀般的风格都是:
clean:
rmedit$(objects)
更为稳健的做法是:
.PHONY:clean
clean:
-rmedit$(objects)
前⾯说过,.PHONY意思表⽰clean是⼀个“伪⽬标”,。⽽在rm命令前⾯加了⼀个⼩减号的意思就是,也许某些⽂件出现问题,但不要管,继
续做后⾯的事。当然,clean的规则不要放在⽂件的开头,不然,这就会变成make的默认⽬标,相信谁也不愿意这样。不成⽂的规矩是
——“clean从来都是放在⽂件的最后”。
上⾯就是⼀个makefile的概貌,也是makefile的基础,下⾯还有很多makefile的相关细节,准备好了吗?准备好了就来。
Makefile总述
———————
⼀、Makefile⾥有什么?
Makefile⾥主要包含了五个东西:显式规则、隐晦规则、变量定义、⽂件指⽰和注释。
1、显式规则。显式规则说明了,如何⽣成⼀个或多的的⽬标⽂件。这是由Makefile的书写者明显指出,要⽣成的⽂件,⽂件的依赖⽂件,
⽣成的命令。
2、隐晦规则。由于我们的make有⾃动推导的功能,所以隐晦的规则可以让我们⽐较粗糙地简略地书写Makefile,这是由make所⽀持的。
3、变量的定义。在Makefile中我们要定义⼀系列的变量,变量⼀般都是字符串,这个有点你C语⾔中的宏,当Makefile被执⾏时,其中的变
量都会被扩展到相应的引⽤位置上。
4、⽂件指⽰。其包括了三个部分,⼀个是在⼀个Makefile中引⽤另⼀个Makefile,就像C语⾔中的include⼀样;另⼀个是指根据某些情况指
定Makefile中的有效部分,就像C语⾔中的预编译#if⼀样;还有就是定义⼀个多⾏的命令。有关这⼀部分的内容,我会在后续的部分中讲
述。
5、注释。Makefile中只有⾏注释,和UNIX的Shell脚本⼀样,其注释是⽤“#”字符,这个就像C/C++中的“//”⼀样。如果你要在你的Makefile中
使⽤“#”字符,可以⽤反斜框进⾏转义,如:“#”。
最后,还值得⼀提的是,在Makefile中的命令,必须要以[Tab]键开始。
⼆、Makefile的⽂件名
默认的情况下,make命令会在当前⽬录下按顺序找寻⽂件名为“GNUmakefile”、“makefile”、“Makefile”的⽂件,找到了解释这个⽂件。在这
三个⽂件名中,最好使⽤“Makefile”这个⽂件名,因为,这个⽂件名第⼀个字符为⼤写,这样有⼀种显⽬的感觉。最好不要
⽤“GNUmakefile”,这个⽂件是GNU的make识别的。有另外⼀些make只对全⼩写的“makefile”⽂件名敏感,但是基本上来说,⼤多数的
make都⽀持“makefile”和“Makefile”这两种默认⽂件名。
当然,你可以使⽤别的⽂件名来书写Makefile,⽐如:“”,“s”,“”等,如果要指定特定的Makefile,你可以
使⽤make的“-f”和“--file”参数,如:或。
三、引⽤其它的Makefile
在Makefile使⽤include关键字可以把别的Makefile包含进来,这很像C语⾔的#include,被包含的⽂件会原模原样的放在当前⽂件的包含位
置。include的语法是:
include
filename可以是当前操作系统Shell的⽂件模式(可以保含路径和通配符)
在include前⾯可以有⼀些空字符,但是绝不能是[Tab]键开始。include和可以⽤⼀个或多个空格隔开。举个例⼦,你有这样⼏个Makefile:
、、,还有⼀个⽂件叫,以及⼀个变量$(bar),其包含了和,那么,下⾯的语句:
*.mk$(bar)
等价于:
make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的#include指令⼀样。如果⽂件
都没有指定绝对路径或是相对路径的话,make会在当前⽬录下⾸先寻找,如果当前⽬录下没有找到,那么,make还会在下⾯的⼏个⽬录下
找:
1、如果make执⾏时,有“-I”或“--include-dir”参数,那么make就会在这个参数所指定的⽬录下去寻找。
2、如果⽬录/include(⼀般是:/usr/local/bin或/usr/include)存在的话,make也会去找。
如果有⽂件没有找到的话,make会⽣成⼀条警告信息,但不会马上出现致命错误。它会继续载⼊其它的⽂件,⼀旦完成makefile的读
取,make会再重试这些没有找到,或是不能读取的⽂件,如果还是不⾏,make才会出现⼀条致命信息。如果你想让make不理那些⽆法读
取的⽂件,⽽继续执⾏,你可以在include前加⼀个减号“-”。如:
-include
其表⽰,⽆论include过程中出现什么错误,都不要报错继续执⾏。和其它版本make兼容的相关命令是sinclude,其作⽤和这⼀个是⼀样
的。
四、环境变量MAKEFILES
如果你的当前环境中定义了环境变量MAKEFILES,那么,make会把这个变量中的值做⼀个类似于include的动作。这个变量中的值是其它
的Makefile,⽤空格分隔。只是,它和include不同的是,从这个环境变中引⼊的Makefile的“⽬标”不会起作⽤,如果环境变量中定义的⽂件
发现错误,make也会不理。
但是在这⾥我还是建议不要使⽤这个环境变量,因为只要这个变量⼀被定义,那么当你使⽤make时,所有的Makefile都会受到它的影响,这
绝不是你想看到的。在这⾥提这个事,只是为了告诉⼤家,也许有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这
个变量。
五、make的⼯作⽅式
GNU的make⼯作时的执⾏步骤⼊下:(想来其它的make也是类似)
1、读⼊所有的Makefile。
2、读⼊被include的其它Makefile。
3、初始化⽂件中的变量。
4、推导隐晦规则,并分析所有规则。
5、为所有的⽬标⽂件创建依赖关系链。
6、根据依赖关系,决定哪些⽬标要重新⽣成。
7、执⾏⽣成命令。
1-5步为第⼀个阶段,6-7为第⼆个阶段。第⼀个阶段中,如果定义的变量被使⽤了,那么,make会把其展开在使⽤的位置。但make并不会
完全马上展开,make使⽤的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使⽤了,变量才会在其内部展
开。
当然,这个⼯作⽅式你不⼀定要清楚,但是知道这个⽅式你也会对make更为熟悉。有了这个基础,后续部分也就容易看懂了。
书写规则
————
规则包含两个部分,⼀个是依赖关系,⼀个是⽣成⽬标的⽅法。
在Makefile中,规则的顺序是很重要的,因为,Makefile中只应该有⼀个最终⽬标,其它的⽬标都是被这个⽬标所连带出来的,所以⼀定要
让make知道你的最终⽬标是什么。⼀般来说,定义在Makefile中的⽬标可能会有很多,但是第⼀条规则中的⽬标将被确⽴为最终的⽬标。如
果第⼀条规则中的⽬标有很多个,那么,第⼀个⽬标会成为最终的⽬标。make所完成的也就是这个⽬标。
好了,还是让我们来看⼀看如何书写规则。
⼀、规则举例
foo.o:.h#foo模块
cc-c-gfoo.c
看到这个例⼦,各位应该不是很陌⽣了,前⾯也已说过,foo.o是我们的⽬标,foo.c和defs.h是⽬标所依赖的源⽂件,⽽只有⼀个命令“cc-c-
gfoo.c”(以Tab键开头)。这个规则告诉我们两件事:
1、⽂件的依赖关系,foo.o依赖于foo.c和defs.h的⽂件,如果foo.c和defs.h的⽂件⽇期要⽐foo.o⽂件⽇期要新,或是foo.o不存在,那么依赖
关系发⽣。
2、如果⽣成(或更新)foo.o⽂件。也就是那个cc命令,其说明了,如何⽣成foo.o这个⽂件。(当然foo.c⽂件include了defs.h⽂件)
⼆、规则的语法
targets:prerequisites
command
...
或是这样:
targets:prerequisites;command
command
...
targets是⽂件名,以空格分开,可以使⽤通配符。⼀般来说,我们的⽬标基本上是⼀个⽂件,但也有可能是多个⽂件。
command是命令⾏,如果其不与“target:prerequisites”在⼀⾏,那么,必须以[Tab键]开头,如果和prerequisites在⼀⾏,那么可以⽤分号做
为分隔。(见上)
prerequisites也就是⽬标所依赖的⽂件(或依赖⽬标)。如果其中的某个⽂件要⽐⽬标⽂件要新,那么,⽬标就被认为是“过时的”,被认为
是需要重⽣成的。这个在前⾯已经讲过了。
如果命令太长,你可以使⽤反斜框(‘’)作为换⾏符。make对⼀⾏上有多少个字符没有限制。规则告诉make两件事,⽂件的依赖关系和如
何成成⽬标⽂件。
⼀般来说,make会以UNIX的标准Shell,也就是/bin/sh来执⾏命令。
三、在规则中使⽤通配符
如果我们想定义⼀系列⽐较类似的⽂件,我们很⾃然地就想起使⽤通配符。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$?
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%.cfoo
vpath%blish
vpath%.cbar
其表⽰“.c”结尾的⽂件,先在“foo”⽬录,然后是“blish”,最后是“bar”⽬录。
vpath%.cfoo:bar
vpath%blish
⽽上⾯的语句则表⽰“.c”结尾的⽂件,先在“foo”⽬录,然后是“bar”⽬录,最后才是“blish”⽬录。
五、伪⽬标
最早先的⼀个例⼦中,我们提到过⼀个“clean”的⽬标,这是⼀个“伪⽬标”,
clean:
rm*.otemp
正像我们前⾯例⼦中的“clean”⼀样,即然我们⽣成了许多⽂件编译⽂件,我们也应该提供⼀个清除它们的“⽬标”以备完整地重编译⽽⽤。
(以“makeclean”来使⽤该⽬标)
因为,我们并不⽣成“clean”这个⽂件。“伪⽬标”并不是⼀个⽂件,只是⼀个标签,由于“伪⽬标”不是⽂件,所以make⽆法⽣成它的依赖关系
和决定它是否要执⾏。我们只有通过显⽰地指明这个“⽬标”才能让其⽣效。当然,“伪⽬标”的取名不能和⽂件名重名,不然其就失去了“伪⽬
标”的意义了。
当然,为了避免和⽂件重名的这种情况,我们可以使⽤⼀个特殊的标记“.PHONY”来显⽰地指明⼀个⽬标是“伪⽬标”,向make说明,不管是
否有这个⽂件,这个⽬标就是“伪⽬标”。
.PHONY:clean
只要有这个声明,不管是否有“clean”⽂件,要运⾏“clean”这个⽬标,只有“makeclean”这样。于是整个过程可以这样写:
.PHONY:clean
clean:
rm*.otemp
伪⽬标⼀般没有依赖的⽂件。但是,我们也可以为伪⽬标指定所依赖的⽂件。伪⽬标同样可以作为“默认⽬标”,只要将其放在第⼀个。⼀个
⽰例就是,如果你的Makefile需要⼀⼝⽓⽣成若⼲个可执⾏⽂件,但你只想简单地敲⼀个make完事,并且,所有的⽬标⽂件都写在⼀个
Makefile中,那么你可以使⽤“伪⽬标”这个特性:
all:prog1prog2prog3
.PHONY:all
prog1:.o
.o
prog2:prog2.o
cc-oprog2prog2.o
prog3:.o
.o
我们知道,Makefile中的第⼀个⽬标会被作为其默认⽬标。我们声明了⼀个“all”的伪⽬标,其依赖于其它三个⽬标。由于伪⽬标的特性是,
总是被执⾏的,所以其依赖的那三个⽬标就总是不如“all”这个⽬标新。所以,其它三个⽬标的规则总是会被决议。也就达到了我们⼀⼝⽓⽣
成多个⽬标的⽬的。“.PHONY:all”声明了“all”这个⽬标为“伪⽬标”。
随便提⼀句,从上⾯的例⼦我们可以看出,⽬标也可以成为依赖。所以,伪⽬标同样也可成为依赖。看下⾯的例⼦:
.PHONY:cleanallcleanobjcleandiff
cleanall:cleanobjcleandiff
rmprogram
cleanobj:
rm*.o
cleandiff:
rm*.diff
“makeclean”将清除所有要被清除的⽂件。“cleanobj”和“cleandiff”这两个伪⽬标有点像“⼦程序”的意思。我们可以输⼊“make
cleanall”和“makecleanobj”和“makecleandiff”命令来达到清除不同种类⽂件的⽬的。
六、多⽬标
Makefile的规则中的⽬标可以不⽌⼀个,其⽀持多⽬标,有可能我们的多个⽬标同时依赖于⼀个⽂件,并且其⽣成的命令⼤体类似。于是我
们就能把其合并起来。当然,多个⽬标的⽣成规则的执⾏命令是同⼀个,这可能会可我们带来⿇烦,不过好在我们的可以使⽤⼀个⾃动化变
量“$@”(关于⾃动化变量,将在后⾯讲述),这个变量表⽰着⽬前规则中所有的⽬标的集合,这样说可能很抽象,还是看⼀个例⼦吧。
bigoutputlittleoutput:text.g
generatetext.g-$(substoutput,,$@)>$@
上述规则等价于:
bigoutput:text.g
generatetext.g-big>bigoutput
littleoutput:text.g
generatetext.g-little>littleoutput
其中,-$(substoutput,,$@)中的“$”表⽰执⾏⼀个Makefile的函数,函数名为subst,后⾯的为参数。关于函数,将在后⾯讲述。这⾥的这个函
数是截取字符串的意思,“$@”表⽰⽬标的集合,就像⼀个数组,“$@”依次取出⽬标,并执于命令。
七、静态模式
静态模式可以更加容易地定义多⽬标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看⼀下语法:
::
...
targets定义了⼀系列的⽬标⽂件,可以有通配符。是⽬标的⼀个集合。
target-parrtern是指明了targets的模式,也就是的⽬标集模式。
prereq-parrterns是⽬标的依赖模式,它对target-parrtern形成的模式再进⾏⼀次依赖⽬标的定义。
这样描述这三个东西,可能还是没有说清楚,还是举个例⼦来说明⼀下吧。如果我们的定义成“%.o”,意思是我们的集合中都是以“.o”结尾
的,⽽如果我们的定义成“%.c”,意思是对所形成的⽬标集进⾏⼆次定义,其计算⽅法是,取模式中的“%”(也就是去掉了[.o]这个结尾),并
为其加上[.c]这个结尾,形成的新集合。
所以,我们的“⽬标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的⽂件名中有“%”那么你可以使⽤反斜杠“”进⾏转义,来标明真实
的“%”字符。
看⼀个例⼦:
objects=.o
all:$(objects)
$(objects):%.o:%.c
$(CC)-c$(CFLAGS)$<-o$@
上⾯的例⼦中,指明了我们的⽬标从$object中获取,“%.o”表明要所有以“.o”结尾的⽬标,也就是“.o”,也就是变量$object集合的模
式,⽽依赖模式“%.c”则取模式“%.o”的“%”,也就是“foobar”,并为其加下“.c”的后缀,于是,我们的依赖⽬标就是“.c”。⽽命令中
的“$<”和“$@”则是⾃动化变量,“$<”表⽰所有的依赖⽬标集(也就是“.c”),“$@”表⽰⽬标集(也就是“.o”)。于是,上⾯
的规则展开后等价于下⾯的规则:
foo.o:foo.c
$(CC)-c$(CFLAGS)foo.c-ofoo.o
bar.o:bar.c
$(CC)-c$(CFLAGS)bar.c-obar.o
试想,如果我们的“%.o”有⼏百个,那种我们只要⽤这种很简单的“静态模式规则”就可以写完⼀堆规则,实在是太有效率了。“静态模式规
则”的⽤法很灵活,如果⽤得好,那会⼀个很强⼤的功能。再看⼀个例⼦:
files=.o
$(filter%.o,$(files)):%.o:%.c
$(CC)-c$(CFLAGS)$<-o$@
$(filter%.elc,$(files)):%.elc:%.el
emacs-fbatch-byte-compile$<
$(filter%.o,$(files))表⽰调⽤Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。其的它内容,我就不⽤多说了吧。这个例字
展⽰了Makefile中更⼤的弹性。
⼋、⾃动⽣成依赖性
在Makefile中,我们的依赖关系可能会需要包含⼀系列的头⽂件,⽐如,如果我们的main.c中有⼀句“#include"defs.h"”,那么我们的依赖
关系应该是:
main.o:.h
但是,如果是⼀个⽐较⼤型的⼯程,你必需清楚哪些C⽂件包含了哪些头⽂件,并且,你在加⼊或删除头⽂件时,也需要⼩⼼地修改
Makefile,这是⼀个很没有维护性的⼯作。为了避免这种繁重⽽⼜容易出错的事情,我们可以使⽤C/C++编译的⼀个功能。⼤多数的
C/C++编译器都⽀持⼀个“-M”的选项,即⾃动找寻源⽂件中包含的头⽂件,并⽣成⼀个依赖关系。例如,如果我们执⾏下⾯的命令:
cc-Mmain.c
其输出是:
main.o:.h
于是由编译器⾃动⽣成的依赖关系,这样⼀来,你就不必再⼿动书写若⼲⽂件的依赖关系,⽽由编译器⾃动⽣成了。需要提醒⼀句的是,如
果你使⽤GNU的C/C++编译器,你得⽤“-MM”参数,不然,“-M”参数会把⼀些标准库的头⽂件也包含进来。
gcc-Mmain.c的输出是:
main.o:.h/usr/include/stdio.h/usr/include/features.h
/usr/include/sys/cdefs.h/usr/include/gnu/stubs.h
/usr/lib/gcc-lib/i486-su-linux/2.95.3/include/stddef.h
/usr/include/bits/types.h/usr/include/bits/pthreadtypes.h
/usr/include/bits/sched.h/usr/include/libio.h
/usr/include/_G_config.h/usr/include/wchar.h
/usr/include/bits/wchar.h/usr/include/gconv.h
/usr/lib/gcc-lib/i486-su-linux/2.95.3/include/stdarg.h
/usr/include/bits/stdio_lim.h
gcc-MMmain.c的输出则是:
main.o:.h
那么,编译器的这个功能如何与我们的Makefile联系在⼀起呢。因为这样⼀来,我们的Makefile也要根据这些源⽂件重新⽣成,让Makefile⾃
已依赖于源⽂件?这个功能并不现实,不过我们可以有其它⼿段来迂回地实现这⼀功能。GNU组织建议把编译器为每⼀个源⽂件的⾃动⽣成
的依赖关系放到⼀个⽂件中,为每⼀个“name.c”的⽂件都⽣成⼀个“name.d”的Makefile⽂件,[.d]⽂件中就存放对应[.c]⽂件的依赖关系。
于是,我们可以写出[.c]⽂件和[.d]⽂件的依赖关系,并让make⾃动更新或⾃成[.d]⽂件,并把其包含在我们的主Makefile中,这样,我们就可
以⾃动化地⽣成每个⽂件的依赖关系了。
这⾥,我们给出了⼀个模式规则来产⽣[.d]⽂件:
%.d:%.c
@t-e;rm-f$@;
$(CC)-M$(CPPFLAGS)$<>$@.$$$$;
d's,($*).o[:]*,1.o$@:,g'<$@.$$$$>$@;
rm-f$@.$$$$
这个规则的意思是,所有的[.d]⽂件依赖于[.c]⽂件,“rm-f$@”的意思是删除所有的⽬标,也就是[.d]⽂件,第⼆⾏的意思是,为每个依赖⽂
件“$<”,也就是[.c]⽂件⽣成依赖⽂件,“$@”表⽰模式“%.d”⽂件,如果有⼀个C⽂件是name.c,那么“%”就是“name”,“$$$$”意为⼀个随机编
号,第⼆⾏⽣成的⽂件有可能是“name.d.12345”,第三⾏使⽤d命令做了⼀个替换,关于d命令的⽤法请参看相关的使⽤⽂档。第四⾏就
是删除临时⽂件。
总⽽⾔之,这个模式要做的事就是在编译器⽣成的依赖关系中加⼊[.d]⽂件的依赖,即把依赖关系:
main.o:.h
转成:
.d:.h
于是,我们的[.d]⽂件也会⾃动更新了,并会⾃动⽣成了,当然,你还可以在这个[.d]⽂件中加⼊的不只是依赖关系,包括⽣成的命令也可⼀
并加⼊,让每个[.d]⽂件都包含⼀个完赖的规则。⼀旦我们完成这个⼯作,接下来,我们就要把这些⾃动⽣成的规则放进我们的主Makefile
中。我们可以使⽤Makefile的“include”命令,来引⼊别的Makefile⽂件(前⾯讲过),例如:
sources=.c
include$(sources:.c=.d)
上述语句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做⼀个替换,把变量$(sources)所有[.c]的字串都替换成[.d],关于这个“替换”的内容,在
后⾯我会有更为详细的讲述。当然,你得注意次序,因为include是按次来载⼊⽂件,最先载⼊的[.d]⽂件中的⽬标会成为默认⽬标。
每条规则中的命令和操作系统Shell的命令⾏是⼀致的。make会⼀按顺序⼀条⼀条的执⾏命令,每条命令的开头必须以[Tab]键开头,除⾮,
命令是紧跟在依赖规则后⾯的分号后的。在命令⾏之间中的空格或是空⾏会被忽略,但是如果该空格或空⾏是以Tab键开头的,那么make会
认为其是⼀个空命令。
我们在UNIX下可能会使⽤不同的Shell,但是make的命令默认是被“/bin/sh”——UNIX的标准Shell解释执⾏的。除⾮你特别指定⼀个其它的
Shell。Makefile中,“#”是注释符,很像C/C++中的“//”,其后的本⾏字符都被注释。
⼀、显⽰命令
通常,make会把其要执⾏的命令⾏在命令执⾏前输出到屏幕上。当我们⽤“@”字符在命令⾏前,那么,这个命令将不被make显⽰出来,最
具代表性的例⼦是,我们⽤这个功能来像屏幕显⽰⼀些信息。如:
@echo正在编译XXX模块......
当make执⾏时,会输出“正在编译XXX模块......”字串,但不会输出命令,如果没有“@”,那么,make将输出:
echo正在编译XXX模块......
正在编译XXX模块......
如果make执⾏时,带⼊make参数“-n”或“--just-print”,那么其只是显⽰命令,但不会执⾏命令,这个功能很有利于我们调试我们的Makefile,
看看我们书写的命令是执⾏起来是什么样⼦的或是什么顺序的。
⽽make参数“-s”或“--slient”则是全⾯禁⽌命令的显⽰。
⼆、命令执⾏
当依赖⽬标新于⽬标时,也就是当规则的⽬标需要被更新时,make会⼀条⼀条的执⾏其后的命令。需要注意的是,如果你要让上⼀条命令
的结果应⽤在下⼀条命令时,你应该使⽤分号分隔这两条命令。⽐如你的第⼀条命令是cd命令,你希望第⼆条命令得在cd之后的基础上运
⾏,那么你就不能把这两条命令写在两⾏上,⽽应该把这两条命令写在⼀⾏上,⽤分号分隔。如:
⽰例⼀:
exec:
cd/home/hchen
pwd
⽰例⼆:
exec:
cd/home/hchen;pwd
当我们执⾏“makeexec”时,第⼀个例⼦中的cd没有作⽤,pwd会打印出当前的Makefile⽬录,⽽第⼆个例⼦中,cd就起作⽤了,pwd会打印
出“/home/hchen”。
make⼀般是使⽤环境变量SHELL中所定义的系统Shell来执⾏命令,默认情况下使⽤UNIX的标准Shell——/bin/sh来执⾏命令。但在MS-
DOS下有点特殊,因为MS-DOS下没有SHELL环境变量,当然你也可以指定。如果你指定了UNIX风格的⽬录形式,⾸先,make会在
SHELL所指定的路径中找寻命令解释器,如果找不到,其会在当前盘符中的当前⽬录中寻找,如果再找不到,其会在PATH环境变量中所定
义的所有路径中寻找。MS-DOS中,如果你定义的命令解释器没有找到,其会给你的命令解释器加上诸如“.exe”、“.com”、“.bat”、“.sh”等后
缀。
三、命令出错
每当命令运⾏完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执⾏下⼀条命令,当规则中所有的命令成功返回后,
这个规则就算是成功完成了。如果⼀个规则中的某个命令出错了(命令退出码⾮零),那么make就会终⽌执⾏当前规则,这将有可能终⽌
所有规则的执⾏。
有些时候,命令的出错并不表⽰就是错误的。例如mkdir命令,我们⼀定需要建⽴⼀个⽬录,如果⽬录不存在,那么mkdir就成功执⾏,万事
⼤吉,如果⽬录存在,那么就出错了。我们之所以使⽤mkdir的意思就是⼀定要有这样的⼀个⽬录,于是我们就不希望mkdir出错⽽终⽌规则
的运⾏。
为了做到这⼀点,忽略命令的出错,我们可以在Makefile的命令⾏前加⼀个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成
功的。如:
clean:
-rm-f*.o
还有⼀个全局的办法是,给make加上“-i”或是“--ignore-errors”参数,那么,Makefile中所有命令都会忽略错误。⽽如果⼀个规则是
以“.IGNORE”作为⽬标的,那么这个规则中的所有命令将会忽略错误。这些是不同级别的防⽌命令出错的⽅法,你可以根据你的不同喜欢设
置。
还有⼀个要提⼀下的make的参数的是“-k”或是“--keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终⽬该规则的执⾏,
但继续执⾏其它规则。
四、嵌套执⾏make
在⼀些⼤的⼯程中,我们会把我们不同模块或是不同功能的源⽂件放在不同的⽬录中,我们可以在每个⽬录中都书写⼀个该⽬录的
Makefile,这有利于让我们的Makefile变得更加地简洁,⽽不⾄于把所有的东西全部写在⼀个Makefile中,这样会很难维护我们的Makefile,
这个技术对于我们模块编译和分段编译有着⾮常⼤的好处。
例如,我们有⼀个⼦⽬录叫subdir,这个⽬录下有个Makefile⽂件,来指明了这个⽬录下⽂件的编译规则。那么我们总控的Makefile可以这样
书写:
subsystem:
cdsubdir&&$(MAKE)
其等价于:
subsystem:
$(MAKE)-Csubdir
定义$(MAKE)宏变量的意思是,也许我们的make需要⼀些参数,所以定义成⼀个变量⽐较利于维护。这两个例⼦的意思都是先进
⼊“subdir”⽬录,然后执⾏make命令。
我们把这个Makefile叫做“总控Makefile”,总控Makefile的变量可以传递到下级的Makefile中(如果你显⽰的声明),但是不会覆盖下层的
Makefile中所定义的变量,除⾮指定了“-e”参数。
如果你要传递变量到下级Makefile中,那么你可以使⽤这样的声明:
export
如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:
unexport
如:
⽰例⼀:
exportvariable=value
其等价于:
variable=value
exportvariable
其等价于:
exportvariable:=value
其等价于:
variable:=value
exportvariable
⽰例⼆:
exportvariable+=value
其等价于:
variable+=value
exportvariable
如果你要传递所有的变量,那么,只要⼀个export就⾏了。后⾯什么也不⽤跟,表⽰传递所有的变量。
需要注意的是,有两个变量,⼀个是SHELL,⼀个是MAKEFLAGS,这两个变量不管你是否export,其总是要传递到下层Makefile中,特别
是MAKEFILES变量,其中包含了make的参数信息,如果我们执⾏“总控Makefile”时有make参数或是在上层Makefile中定义了这个变量,那
么MAKEFILES变量将会是这些参数,并会传递到下层Makefile中,这是⼀个系统级的环境变量。
但是make命令中的有⼏个参数并不往下传递,它们是“-C”,“-f”,“-h”“-o”和“-W”(有关Makefile参数的细节将在后⾯说明),如果你不想往下层
传递参数,那么,你可以这样来:
subsystem:
cdsubdir&&$(MAKE)MAKEFLAGS=
如果你定义了环境变量MAKEFLAGS,那么你得确信其中的选项是⼤家都会⽤到的,如果其中有“-t”,“-n”,和“-q”参数,那么将会有让你意想不
到的结果,或许会让你异常地恐慌。
还有⼀个在“嵌套执⾏”中⽐较有⽤的参数,“-w”或是“--print-directory”会在make的过程中输出⼀些信息,让你看到⽬前的⼯作⽬录。⽐如,如
果我们的下级make⽬录是“/home/hchen/gnu/make”,如果我们使⽤“make-w”来执⾏,那么当进⼊该⽬录时,我们会看到:
make:Enteringdirectory`/home/hchen/gnu/make'.
⽽在完成下层make后离开⽬录时,我们会看到:
make:Leavingdirectory`/home/hchen/gnu/make'
当你使⽤“-C”参数来指定make下层Makefile时,“-w”会被⾃动打开的。如果参数中有“-s”(“--slient”)或是“--no-print-directory”,那么,“-w”总
是失效的。
五、定义命令包
如果Makefile中出现⼀些相同命令序列,那么我们可以为这些相同的命令序列定义⼀个变量。定义这种命令序列的语法以“define”开始,
以“endef”结束,如:
definerun-yacc
yacc$(firstword$^)
.c$@
endef
这⾥,“run-yacc”是这个命令包的名字,其不要和Makefile中的变量重名。在“define”和“endef”中的两⾏就是命令序列。这个命令包中的第⼀
个命令是运⾏Yacc程序,因为Yacc程序总是⽣成“.c”的⽂件,所以第⼆⾏的命令就是把这个⽂件改改名字。还是把这个命令包放到⼀个
⽰例中来看看吧。
foo.c:foo.y
$(run-yacc)
我们可以看见,要使⽤这个命令包,我们就好像使⽤变量⼀样。在这个命令包的使⽤中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就
是“foo.c”(有关这种以“$”开头的特殊变量,我们会在后⾯介绍),make在执⾏命令包时,命令包中的每个命令会被依次独⽴执⾏。
使⽤变量
————
在Makefile中的定义的变量,就像是C/C++语⾔中的宏⼀样,他代表了⼀个⽂本字串,在Makefile中执⾏的时候其会⾃动原模原样地展开在
所使⽤的地⽅。其与C/C++所不同的是,你可以在Makefile中改变其值。在Makefile中,变量可以使⽤在“⽬标”,“依赖⽬标”,“命令”或是
Makefile的其它部分中。
变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符(空格、回车等)。变量是⼤⼩写敏
感的,“foo”、“Foo”和“FOO”是三个不同的变量名。传统的Makefile的变量名是全⼤写的命名⽅式,但我推荐使⽤⼤⼩写搭配的变量名,如:
MakeFlags。这样可以避免和系统的变量冲突,⽽发⽣意外的事情。
有⼀些变量是很奇怪字串,如“$<”、“$@”等,这些是⾃动化变量,我会在后⾯介绍。
⼀、变量的基础
变量在声明时需要给予初值,⽽在使⽤时,需要给在变量名前加上“$”符号,但最好⽤⼩括号“()”或是⼤括号“{}”把变量给包括起来。如果你
要使⽤真实的“$”字符,那么你需要⽤“$$”来表⽰。
变量可以使⽤在许多地⽅,如规则中的“⽬标”、“依赖”、“命令”以及新的变量中。先看⼀个例⼦:
objects=.o
program:$(objects)
cc-oprogram$(objects)
$(objects):defs.h
变量会在使⽤它的地⽅精确地展开,就像C/C++中的宏⼀样,例如:
foo=c
prog.o:prog.$(foo)
$(foo)$(foo)-$(foo)prog.$(foo)
展开后得到:
prog.o:prog.c
cc-cprog.c
当然,千万不要在你的Makefile中这样⼲,这⾥只是举个例⼦来表明Makefile中的变量在使⽤处展开的真实样⼦。可见其就是⼀个“替代”的原
理。
另外,给变量加上括号完全是为了更加安全地使⽤这个变量,在上⾯的例⼦中,如果你不想给变量加上括号,那也可以,但我还是强烈建议
你给变量加上括号。
⼆、变量中的变量
在定义变量的值时,我们可以使⽤其它变量来构造变量的值,在Makefile中有两种⽅式来在⽤变量定义变量的值。
先看第⼀种⽅式,也就是简单的使⽤“=”号,在“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在⽂件的任何⼀处,也就是说,右
侧中的变量不⼀定⾮要是已定义好的值,其也可以使⽤后⾯定义的值。如:
foo=$(bar)
bar=$(ugh)
ugh=Huh?
all:
echo$(foo)
我们执⾏“makeall”将会打出变量$(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:=foobar
x:=later
值得⼀提的是,这种⽅法,前⾯的变量不能使⽤后⾯的变量,只能使⽤前⾯已定义好了的变量。如果是这样:
y:=$(x)bar
x:=foo
那么,y的值是“bar”,⽽不是“foobar”。
上⾯都是⼀些⽐较简单的变量使⽤了,让我们来看⼀个复杂的例⼦,其中包括了make的函数、条件表达式和⼀个系统变量“MAKELEVEL”的
使⽤:
ifeq(0,${MAKELEVEL})
cur-dir:=$(shellpwd)
whoami:=$(shellwhoami)
host-type:=$(shellarch)
MAKE:=${MAKE}host-type=${host-type}whoami=${whoami}
endif
关于条件表达式和函数,我们在后⾯再说,对于系统变量“MAKELEVEL”,其意思是,如果我们的make有⼀个嵌套执⾏的动作(参见前⾯
的“嵌套使⽤make”),那么,这个变量会记录了我们的当前Makefile的调⽤层数。
下⾯再介绍两个定义变量时我们需要知道的,请先看⼀个例⼦,如果我们要定义⼀个变量,其值是⼀个空格,那么我们可以这样来:
nullstring:=
space:=$(nullstring)#endoftheline
nullstring是⼀个Empty变量,其中什么也没有,⽽我们的space的值是⼀个空格。因为在操作符的右边是很难描述⼀个空格的,这⾥采⽤的
技术很管⽤,先⽤⼀个Empty变量来标明变量的值开始了,⽽后⾯采⽤“#”注释符来表⽰变量定义的终⽌,这样,我们可以定义出其值是⼀个
空格的变量。请注意这⾥关于“#”的使⽤,注释符“#”的这种特性值得我们注意,如果我们这样定义⼀个变量:
dir:=/foo/bar#directorytoputthefrobsin
dir这个变量的值是“/foo/bar”,后⾯还跟了4个空格,如果我们这样使⽤这样变量来指定别的⽬录——“$(dir)/file”那么就完蛋了。
还有⼀个⽐较有⽤的操作符是“?=”,先看⽰例:
FOO?=bar
其含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做,其等价于:
ifeq($(originFOO),undefined)
FOO=bar
endif
三、变量⾼级⽤法
这⾥介绍两种变量的⾼级使⽤⽅法,第⼀种是变量值的替换。
我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字
串。这⾥的“结尾”意思是“空格”或是“结束符”。
还是看⼀个⽰例吧:
foo:=.o
bar:=$(foo:.o=.c)
这个⽰例中,我们先定义了⼀个“$(foo)”变量,⽽第⼆⾏的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就
是“.c”。
另外⼀种变量替换的技术是以“静态模式”(参见前⾯章节)定义的,如:
foo:=.o
bar:=$(foo:%.o=%.c)
这依赖于被替换字串中的有相同的模式,模式中必须包含⼀个“%”字符,这个例⼦同样让$(bar)变量的值为“.c”。
第⼆种⾼级⽤法是——“把变量的值再当成变量”。先看⼀个例⼦:
x=y
y=z
a:=$($(x))
在这个例⼦中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,是“x=y”,⽽不是“x=$(y)”)
我们还可以使⽤更多的层次:
x=y
y=z
z=u
a:=$($($(x)))
这⾥的$(a)的值是“u”,相关的推导留给读者⾃⼰去做吧。
让我们再复杂⼀点,使⽤上“在变量定义中使⽤变量”的第⼀个⽅式,来看⼀个例⼦:
x=$(y)
y=z
z=Hello
a:=$($(x))
这⾥的$($(x))被替换成了$($(y)),因为$(y)值是“z”,所以,最终结果是:a:=$(z),也就是“Hello”。
再复杂⼀点,我们再加上函数:
x=variable1
variable2:=Hello
y=$(subst1,2,$(x))
z=y
a:=$($($(z)))
这个例⼦中,“$($($(z)))”扩展为“$($(y))”,⽽其再次被扩展为“$($(subst1,2,$(x)))”。$(x)的值是“variable1”,subst函数把“variable1”中的所
有“1”字串替换成“2”字串,于是,“variable1”变成“variable2”,再取其值,所以,最终,$(a)的值就是$(variable2)的值——“Hello”。(喔,好
不容易)
在这种⽅式中,或要可以使⽤多个变量来组成⼀个变量的名字,然后再取其值:
first_cond=Hello
a=first
b=cond
all=$($a_$b)
这⾥的“$a_$b”组成了“first_cond”,于是,$(all)的值就是“Hello”。
再来看看结合第⼀种技术的例⼦:
a_objects:=.o
1_objects:=1.o2.o3.o
sources:=$($(a1)_objects:.o=.c)
这个例⼦中,如果$(a1)的值是“a”的话,那么,$(sources)的值就是“.c”;如果$(a1)的值是“1”,那么$(sources)的值是“1.c2.c3.c”。
再来看⼀个这种技术和“函数”与“条件语句”⼀同使⽤的例⼦:
ifdefdo_sort
func:=sort
el
func:=strip
endif
bar:=adbgqc
foo:=$($(func)$(bar))
这个⽰例中,如果定义了“do_sort”,那么:foo:=$(sortadbgqc),于是$(foo)的值就是“abcdgq”,⽽如果没有定义“do_sort”,那么:
foo:=$(sortadbgqc),调⽤的就是strip函数。
当然,“把变量的值再当成变量”这种技术,同样可以⽤在操作符的左边:
dir=foo
$(dir)_sources:=$(wildcard$(dir)/*.c)
define$(dir)_print
lpr$($(dir)_sources)
endef
这个例⼦中定义了三个变量:“dir”,“foo_sources”和“foo_print”。
四、追加变量值
我们可以使⽤“+=”操作符给变量追加值,如:
objects=.o
objects+=another.o
于是,我们的$(objects)值变成:“er.o”(another.o被追加进去了)
使⽤“+=”操作符,可以模拟为下⾯的这种例⼦:
objects=.o
objects:=$(objects)another.o
所不同的是,⽤“+=”更为简洁。
如果变量之前没有定义过,那么,“+=”会⾃动变成“=”,如果前⾯有变量定义,那么“+=”会继承于前次操作的赋值符。如果前⼀次的是“:=”,那
么“+=”会以“:=”作为其赋值符,如:
variable:=value
variable+=more
等价于:
variable:=value
variable:=$(variable)more
但如果是这种情况:
variable=value
variable+=more
由于前次的赋值符是“=”,所以“+=”也会以“=”来做为赋值,那么岂不会发⽣变量的递补归定义,这是很不好的,所以make会⾃动为我们解决
这个问题,我们不必担⼼这个问题。
五、override指⽰符
如果有变量是通常make的命令⾏参数设置的,那么Makefile中对这个变量的赋值会被忽略。如果你想在Makefile中设置这类参数的值,那
么,你可以使⽤“override”指⽰符。其语法是:
override=
override:=
当然,你还可以追加:
override+=
对于多⾏的变量定义,我们⽤define指⽰符,在define指⽰符前,也同样可以使⽤ovveride指⽰符,如:
overridedefinefoo
bar
endef
六、多⾏变量
还有⼀种设置变量值的⽅法是使⽤define关键字。使⽤define关键字设置变量的值可以有换⾏,这有利于定义⼀系列的命令(前⾯我们讲
过“命令包”的技术就是利⽤这个关键字)。
define指⽰符后⾯跟的是变量的名字,⽽重起⼀⾏定义变量的值,定义是以endef关键字结束。其⼯作⽅式和“=”操作符⼀样。变量的值可以
包含函数、命令、⽂字,或是其它变量。因为命令需要以[Tab]键开头,所以如果你⽤define定义的命令变量中没有以[Tab]键开头,那么
make就不会把其认为是命令。
下⾯的这个⽰例展⽰了define的⽤法:
definetwo-lines
echofoo
echo$(bar)
endef
七、环境变量
make运⾏时的系统环境变量可以在make开始运⾏时被载⼊到Makefile⽂件中,但是如果Makefile中已定义了这个变量,或是这个变量由
make命令⾏带⼊,那么系统的环境变量的值将被覆盖。(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)
因此,如果我们在环境变量中设置了“CFLAGS”环境变量,那么我们就可以在所有的Makefile中使⽤这个变量了。这对于我们使⽤统⼀的编
译参数有⽐较⼤的好处。如果Makefile中定义了CFLAGS,那么则会使⽤Makefile中的这个变量,如果没有定义则使⽤系统环境变量的值,
⼀个共性和个性的统⼀,很像“全局变量”和“局部变量”的特性。
当make嵌套调⽤时(参见前⾯的“嵌套调⽤”章节),上层Makefile中定义的变量会以系统环境变量的⽅式传递到下层的Makefile中。当然,
默认情况下,只有通过命令⾏设置的变量会被传递。⽽定义在⽂件中的变量,如果要向下层Makefile传递,则需要使⽤exprot关键字来声
明。(参见前⾯章节)
当然,我并不推荐把许多的变量都定义在系统环境中,这样,在我们执⾏不⽤的Makefile时,拥有的是同⼀套系统变量,这可能会带来更多
的⿇烦。
⼋、⽬标变量
前⾯我们所讲的在Makefile中定义的变量都是“全局变量”,在整个⽂件,我们都可以访问这些变量。当然,“⾃动化变量”除外,如“$<”等这种
类量的⾃动化变量就属于“规则型变量”,这种变量的值依赖于规则的⽬标和依赖⽬标的定义。
当然,我样同样可以为某个⽬标设置局部变量,这种变量被称为“Target-specificVariable”,它可以和“全局变量”同名,因为它的作⽤范围只
在这条规则以及连带规则中,所以其值也只在作⽤范围内有效。⽽不会影响规则链以外的全局变量的值。
其语法是:
:
:overide
可以是前⾯讲过的各种赋值表达式,如“=”、“:=”、“+=”或是“?=”。第⼆个语法是针对于make命令⾏带⼊的变量,或是系统环境变量。
这个特性⾮常的有⽤,当我们设置了这样⼀个变量,这个变量会作⽤到由这个⽬标所引发的所有的规则中去。如:
prog:CFLAGS=-g
prog:.o
$(CC)$(CFLAGS).o
prog.o:prog.c
$(CC)$(CFLAGS)prog.c
foo.o:foo.c
$(CC)$(CFLAGS)foo.c
bar.o:bar.c
$(CC)$(CFLAGS)bar.c
在这个⽰例中,不管全局的$(CFLAGS)的值是什么,在prog⽬标,以及其所引发的所有规则中(.o的规则),$(CFLAGS)的
值都是“-g”
九、模式变量
在GNU的make中,还⽀持模式变量(Pattern-specificVariable),通过上⾯的⽬标变量中,我们知道,变量可以定义在某个⽬标上。模式
变量的好处就是,我们可以给定⼀种“模式”,可以把变量定义在符合这种模式的所有⽬标上。
我们知道,make的“模式”⼀般是⾄少含有⼀个“%”的,所以,我们可以以如下⽅式给所有以[.o]结尾的⽬标定义⽬标变量:
%.o:CFLAGS=-O
同样,模式变量的语法和“⽬标变量”⼀样:
:
:override
override同样是针对于系统环境传⼊的变量,或是make命令⾏指定的变量。
使⽤条件判断
——————
使⽤条件判断,可以让make根据运⾏时的不同情况选择不同的执⾏分⽀。条件表达式可以是⽐较变量的值,或是⽐较变量和常量的值。
⼀、⽰例
下⾯的例⼦,判断$(CC)变量是否“gcc”,如果是的话,则使⽤GNU函数编译⽬标。
libs_for_gcc=-lgnu
normal_libs=
foo:$(objects)
ifeq($(CC),gcc)
$(CC)-ofoo$(objects)$(libs_for_gcc)
el
$(CC)-ofoo$(objects)$(normal_libs)
endif
可见,在上⾯⽰例的这个规则中,⽬标“foo”可以根据变量“$(CC)”值来选取不同的函数库来编译程序。
我们可以从上⾯的⽰例中看到三个关键字:ifeq、el和endif。ifeq的意思表⽰条件语句的开始,并指定⼀个条件表达式,表达式包含两个
参数,以逗号分隔,表达式以圆括号括起。el表⽰条件表达式为假的情况。endif表⽰⼀个条件语句的结束,任何⼀个条件表达式都应该以
endif结束。
当我们的变量$(CC)值是“gcc”时,⽬标foo的规则是:
foo:$(objects)
$(CC)-ofoo$(objects)$(libs_for_gcc)
⽽当我们的变量$(CC)值不是“gcc”时(⽐如“cc”),⽬标foo的规则是:
foo:$(objects)
$(CC)-ofoo$(objects)$(normal_libs)
当然,我们还可以把上⾯的那个例⼦写得更简洁⼀些:
libs_for_gcc=-lgnu
normal_libs=
ifeq($(CC),gcc)
libs=$(libs_for_gcc)
el
libs=$(normal_libs)
endif
foo:$(objects)
$(CC)-ofoo$(objects)$(libs)
⼆、语法
条件表达式的语法为:
endif
以及:
el
endif
其中表⽰条件关键字,如“ifeq”。这个关键字有四个。
第⼀个是我们前⾯所见过的“ifeq”
ifeq(,)
ifeq''''
ifeq""""
ifeq""''
ifeq''""
⽐较参数“arg1”和“arg2”的值是否相同。当然,参数中我们还可以使⽤make的函数。如:
ifeq($(strip$(foo)),)
endif
这个⽰例中使⽤了“strip”函数,如果这个函数的返回值是空(Empty),那么就⽣效。
第⼆个条件关键字是“ifneq”。语法是:
ifneq(,)
ifneq''''
ifneq""""
ifneq""''
ifneq''""
其⽐较参数“arg1”和“arg2”的值是否相同,如果不同,则为真。和“ifeq”类似。
第三个条件关键字是“ifdef”。语法是:
ifdef
如果变量的值⾮空,那到表达式为真。否则,表达式为假。当然,同样可以是⼀个函数的返回值。注意,ifdef只是测试⼀个变量是否有值,
其并不会把变量扩展到当前位置。还是来看两个例⼦:
⽰例⼀:
bar=
foo=$(bar)
ifdeffoo
frobozz=yes
el
frobozz=no
endif
⽰例⼆:
foo=
ifdeffoo
frobozz=yes
el
frobozz=no
endif
第⼀个例⼦中,“$(frobozz)”值是“yes”,第⼆个则是“no”。
第四个条件关键字是“ifndef”。其语法是:
ifndef
这个我就不多说了,和“ifdef”是相反的意思。
在这⼀⾏上,多余的空格是被允许的,但是不能以[Tab]键做为开始(不然就被认为是命令)。⽽注释符“#”同样也是安全的。
“el”和“endif”也⼀样,只要不是以[Tab]键开始就⾏了。
特别注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把⾃动化变量
(如“$@”等)放⼊条件表达式中,因为⾃动化变量是在运⾏时才有的。
⽽且,为了避免混乱,make不允许把整个条件语句分成两部分放在不同的⽂件中。
使⽤函数
————
在Makefile中可以使⽤函数来处理变量,从⽽让我们的命令或是规则更为的灵活和具有智能。make所⽀持的函数也不算很多,不过已经⾜够
我们的操作了。函数调⽤后,函数的返回值可以当做变量来使⽤。
⼀、函数的调⽤语法
函数调⽤,很像变量的使⽤,也是以“$”来标识的,其语法如下:
$()
或是
${}
这⾥,就是函数名,make⽀持的函数不多。是函数的参数,参数间以逗号“,”分隔,⽽函数名和参数之间以“空格”分隔。函数调⽤以“$”开头,
以圆括号或花括号把函数名和参数括起。感觉很像⼀个变量,是不是?函数中的参数可以使⽤变量,为了风格的统⼀,函数和变量的括号最
好⼀样,如使⽤“$(substa,b,$(x))”这样的形式,⽽不是“$(substa,b,${x})”的形式。因为统⼀会更清楚,也会减少⼀些不必要的⿇烦。
还是来看⼀个⽰例:
comma:=,
empty:=
space:=$(empty)$(empty)
foo:=abc
bar:=$(subst$(space),$(comma),$(foo))
在这个⽰例中,$(comma)的值是⼀个逗号。$(space)使⽤了$(empty)定义了⼀个空格,$(foo)的值是“abc”,$(bar)的定义⽤,调⽤了函
数“subst”,这是⼀个替换函数,这个函数有三个参数,第⼀个参数是被替换字串,第⼆个参数是替换字串,第三个参数是替换操作作⽤的字
串。这个函数也就是把$(foo)中的空格替换成逗号,所以$(bar)的值是“a,b,c”。
⼆、字符串处理函数
$(subst,,)
名称:字符串替换函数——subst。
功能:把字串中的字符串替换成。
返回:函数返回被替换过后的字符串。
⽰例:
$(substee,EE,feetonthestreet),
把“feetonthestreet”中的“ee”替换成“EE”,返回结果是“fEEtonthestrEEt”。
$(patsubst,,)
名称:模式字符串替换函数——patsubst。
功能:查找中的单词(单词以“空格”、“Tab”或“回车”“换⾏”分隔)是否符合模式,如果匹配的话,则以替换。这⾥,可以包括通配符“%”,表
⽰任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以⽤“”来转义,以“%”来表⽰真实含义
的“%”字符)
返回:函数返回被替换过后的字符串。
⽰例:
$(patsubst%.c,%.o,.c)
把字串“.c”符合模式[%.c]的单词替换成[%.o],返回结果是“.o”
备注:
这和我们前⾯“变量章节”说过的相关知识有点相似。如:
“$(var:=)”
相当于
“$(patsubst,,$(var))”,
⽽“$(var:=)”
则相当于
“$(patsubst%,%,$(var))”。
例如有:objects=.o,
那么,“$(objects:.o=.c)”和“$(patsubst%.o,%.c,$(objects))”是⼀样的。
$(strip)
名称:去空格函数——strip。
功能:去掉字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
⽰例:
$(stripabc)
把字串“abc”去到开头和结尾的空格,结果是“abc”。
$(findstring,)
名称:查找字符串函数——findstring。
功能:在字串中查找字串。
返回:如果找到,那么返回,否则返回空字符串。
⽰例:
$(findstringa,abc)
$(findstringa,bc)
第⼀个函数返回“a”字符串,第⼆个返回“”字符串(空字符串)
$(filter,)
名称:过滤函数——filter。
功能:以模式过滤字符串中的单词,保留符合模式的单词。可以有多个模式。
返回:返回符合模式的字串。
⽰例:
sources:=.h
foo:$(sources)
cc$(filter%.c%.s,$(sources))-ofoo
$(filter%.c%.s,$(sources))返回的值是“.s”。
$(filter-out,)
名称:反过滤函数——filter-out。
功能:以模式过滤字符串中的单词,去除符合模式的单词。可以有多个模式。
返回:返回不符合模式的字串。
⽰例:
objects=.o
mains=2.o
$(filter-out$(mains),$(objects))返回值是“.o”。
$(sort)
名称:排序函数——sort。
功能:给字符串中的单词排序(升序)。
返回:返回排序后的字符串。
⽰例:$(sortfoobarlo)返回“barfoolo”。
备注:sort函数会去掉中相同的单词。
$(word,)
名称:取单词函数——word。
功能:取字符串中第个单词。(从⼀开始)
返回:返回字符串中第个单词。如果⽐中的单词数要⼤,那么返回空字符串。
⽰例:$(word2,foobarbaz)返回值是“bar”。
$(wordlist,,)
名称:取单词串函数——wordlist。
功能:从字符串中取从开始到的单词串。和是⼀个数字。
返回:返回字符串中从到的单词字串。如果⽐中的单词数要⼤,那么返回空字符串。如果⼤于的单词数,那么返回从开始,到结束的单词
串。
⽰例:$(wordlist2,3,foobarbaz)返回值是“barbaz”。
$(words)
名称:单词个数统计函数——words。
功能:统计中字符串中的单词个数。
返回:返回中的单词数。
⽰例:$(words,foobarbaz)返回值是“3”。
备注:如果我们要取中最后的⼀个单词,我们可以这样:$(word$(words),)。
$(firstword)
名称:⾸单词函数——firstword。
功能:取字符串中的第⼀个单词。
返回:返回字符串的第⼀个单词。
⽰例:$(firstwordfoobar)返回值是“foo”。
备注:这个函数可以⽤word函数来实现:$(word1,)。
以上,是所有的字符串操作函数,如果搭配混合使⽤,可以完成⽐较复杂的功能。这⾥,举⼀个现实中应⽤的例⼦。我们知道,make使
⽤“VPATH”变量来指定“依赖⽂件”的搜索路径。于是,我们可以利⽤这个搜索路径来指定编译器对头⽂件的搜索路径参数CFLAGS,如:
overrideCFLAGS+=$(patsubst%,-I%,$(subst:,,$(VPATH)))
如果我们的“$(VPATH)”值是“src:../headers”,那么“$(patsubst%,-I%,$(subst:,,$(VPATH)))”将返回“-Isrc-I../headers”,这正是cc或gcc搜索头
⽂件路径的参数。
三、⽂件名操作函数
下⾯我们要介绍的函数主要是处理⽂件名的。每个函数的参数字符串都会被当做⼀个或是⼀系列的⽂件名来对待。
$(dir)
名称:取⽬录函数——dir。
功能:从⽂件名序列中取出⽬录部分。⽬录部分是指最后⼀个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
返回:返回⽂件名序列的⽬录部分。
⽰例:$(dirsrc/)返回值是“src/./”。
$(notdir)
名称:取⽂件函数——notdir。
功能:从⽂件名序列中取出⾮⽬录部分。⾮⽬录部分是指最后⼀个反斜杠(“/”)之后的部分。
返回:返回⽂件名序列的⾮⽬录部分。
⽰例:$(notdirsrc/)返回值是“”。
$(suffix)
名称:取后缀函数——suffix。
功能:从⽂件名序列中取出各个⽂件名的后缀。
返回:返回⽂件名序列的后缀序列,如果⽂件没有后缀,则返回空字串。
⽰例:$(suffixsrc/-1.0/)返回值是“.c.c”。
$(baname)
名称:取前缀函数——baname。
功能:从⽂件名序列中取出各个⽂件名的前缀部分。
返回:返回⽂件名序列的前缀序列,如果⽂件没有前缀,则返回空字串。
⽰例:$(banamesrc/-1.0/)返回值是“src/foosrc-1.0/barhacks”。
$(addsuffix,)
名称:加后缀函数——addsuffix。
功能:把后缀加到中的每个单词后⾯。
返回:返回加过后缀的⽂件名序列。
⽰例:$(addsuffix.c,foobar)返回值是“.c”。
$(addprefix,)
名称:加前缀函数——addprefix。
功能:把前缀加到中的每个单词后⾯。
返回:返回加过前缀的⽂件名序列。
⽰例:$(addprefixsrc/,foobar)返回值是“src/foosrc/bar”。
$(join,)
名称:连接函数——join。
功能:把中的单词对应地加到的单词后⾯。如果的单词个数要⽐的多,那么,中的多出来的单词将保持原样。如果的单词个数要⽐多,那
么,多出来的单词将被复制到中。
返回:返回连接过后的字符串。
⽰例:$(joinaaabbb,111222333)返回值是“aaa111bbb222333”。
四、foreach函数
foreach函数和别的函数⾮常的不⼀样。因为这个函数是⽤来做循环⽤的,Makefile中的foreach函数⼏乎是仿照于Unix标准Shell(/bin/sh)
中的for语句,或是C-Shell(/bin/csh)中的foreach语句⽽构建的。它的语法是:
$(foreach,,)
这个函数的意思是,把参数中的单词逐⼀取出放到参数所指定的变量中,然后再执⾏所包含的表达式。每⼀次会返回⼀个字符串,循环过程
中,的所返回的每个字符串会以空格分隔,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是
foreach函数的返回值。
所以,最好是⼀个变量名,可以是⼀个表达式,⽽中⼀般会使⽤这个参数来依次枚举中的单词。举个例⼦:
names:=abcd
files:=$(foreachn,$(names),$(n).o)
上⾯的例⼦中,$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出⼀个值,这些值以空格分隔,最后作为
foreach函数的返回,所以,$(files)的值是“.o”。
注意,foreach中的参数是⼀个临时的局部变量,foreach函数执⾏完后,参数的变量将不在作⽤,其作⽤域只在foreach函数当中。
本文发布于:2022-11-22 17:41:50,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/584.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |