Makefile用法分析
在linux开发中,应用程序的编译基本都采用GNU的make工具,而make搭配Makefile来实现工程代码的编译,在越是大型复杂的项目中,make的强悍之处越是明显。在使用了一段时间make后,对其用法进行分析。本文是在学习了陈皓的“跟我一起学Makefile”文章后,对自己学习的小结。
一.简单的例子
现在,我们有一个C++的项目需要进行编译,项目包含3个头文件,3个.cpp文件,分别是main.cpp,main.h,read.cpp,read.h,write.cpp,write.h,下面就是我们的Makefile了:
我们需要生成的目标是可执行文件test,test由中间目标文件.o链接生成,.o文件根据依赖关系由相应的头文件和.cpp源文件编译而成,伪目标clean用于清除目标文件和.o文件。
在我的Makefile中,我们发现编译工具cc即使不指明为g++也可以正常编译,但是如果是在目标文件链接时不指明的话,会有提示找不到相关的库,那关于这个cc到底是根据什么来选择编译工具的呢?通过查阅资料和测试发现,编译程序cc一般默认的都是gcc,当我们对源文件编译时,对于.c文件,gcc和g++分别识别为c和c++程序,对于.cpp文件,两者都会识别为c++程序,所以在编译时我们完全可以用默认cc来编译.c和.cpp文件(当然对于其他平台或嵌入式中还是要指定对应的编译工具,防止出错)。但是链接时,需要对.o中间文件链接库生成可执行程序,g++会链接c++的库,gcc链接c的库,所以如果链接时需要指明编译器。在平常使用中,考虑到的易维护和防出错,都是统一将编译和链接的编译器都指明为g++。
好,通过这个Makefile实现了我们的功能需求,但是我们发现,这种Makefile虽然逻辑简单,但是一旦当项目体积非常大时,写Makefile会是一件非常痛苦的事,需要写几百个中间目标文件的依赖关系,OK,别担心,GNU的make很强大,它可以通过隐晦规则进行自动推导。
二.隐晦规则和自动推导
GNU的make工具存在隐晦规则,只要make看到一个.o文件,他会默认把对应的.c或.cpp(后面讨论时会都用.c文件来讨论)文件加到依赖关系中,比如如果有一个main.o,那么他会默认把main.c加到其依赖关系中,并且make会对其进行自动推导,包括文件的依赖关系和后面的命令。那么,我们又可以得到一个新的Makefile了。
这样我们得到了一个看起来更简洁的Makefile,我们不用书写每个中间目标文件的编译指令了,因为make会自动推导,但是这样貌似只是部分降低了我们的劳动力,对于几百个源文件的程序,我们还是需要写出来每个文件隐晦规则之外的依赖关系,虽然不用写编译指令,但还是非常多,而且这样的Makefile通用性并不高,如果我们写了个其他的测试程序,这个Makefile我们还是需要更改许多,OK,make这个工具的静态模式和自动变量可以帮我们解决这个问题。
三.静态模式和自动变量
Make中的静态模式的语法如下:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
....
targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern是指明了targets的模式,也就是的目标集模式。
prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。
举个例子,对于前面的objects目标集合,<target-parrtern>定义成“%.o”,意思是我们的<target>集合中都是以“.o”结尾的,而如果我们的<prereq-parrterns>定义成“%.c”,意思是对<target-parrtern>所形成的目标集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。如main.o通过模式取到的依赖目标就是main.c文件。静态模式帮助我们完成将几百个源文件用一种通用的模式来代表,我们可以不用写出所有中间文件的依赖关系,只要用一个静态模式就OK了。
接下来有个问题,在上述的模式规则中,目标和依赖文件都是一系例的文件,那么我们如何书写一个命令来完成从不同的依赖文件生成相应的目标?因为在每一次的对模式规则的解析时,都会是不同的目标和依赖文件。这就需要make 的自动化变量。
Make的自动化变量。 所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。
下面是所有的自动化变量及其说明:
[email protected]
表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,"[email protected]"就是匹配于
目标中模式定义的集合。
$%
仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是"foo.a (bar.o)",那么,"$%"就是"bar.o","[email protected]"就是"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所不能识别的,那么"$*"就是空值。
更新后的Makefile如下:
OK,写到这里,我们的Makefile已经非常非常简洁了,但是这里面存在两个问题,第一个问题是,我们所有的目标文件的依赖关系都只有相应的源文件,并没有其他所需头文件的依赖关系,比如main.o的依赖关系包含了main.cpp,main.h,read.h,write.h,在首次编译时,依赖关系会找到相应的相应的头文件,但是之后如果修改了某个头文件,则再次编译头文件并不会更新,所以这种方式中缺少头文件的依赖关系,一种解决方法是每次重新编译时,先清空之前的编译目标文件就OK了,还有一种方法是建立.c文件的依赖关系(下次再写)。
第二个问题是,我们发现,我们还是要写出所有的目标文件集,同样的,对于几百个中间文件的项目来说,这也是很头疼的事,我们可以通过通配符和一些函数来解决这个问题。
我们的需求是让make自己找到当前目录下所有的源文件,并且对这些源文件根据依赖关系进行编译处理,实现完全的自动化。首先我们需要用通配符*来找到所有的源文件,并对应生成所有源文件对应的中间目标文件,这主要通过patsubst函数来实现,该函数是模式字符串替换函数(对于所有的函数可以查询后文的函数表),帮我们将依赖目标集的所有源文件对应生成.o文件。
Makefile写到这里,应该是简洁性和通用性都比较高的了,但是,现在的makefile在实际中可用性不高,因为实际项目中源文件往往放置在多个文件夹中,而且还会设置许多编译参数,所以下面将会写一个体现源文件结构复杂度和编译器参数的makefile。
四.编译器参数
现在,我们假设,我们的read.cpp,read.h在当前目录的read目录下,write.cpp和write.h在当前目录下的write目录下,那么我们新的makefile又出炉了。
原有的wildcard是匹配当前目录下所有的源文件,但是有多个文件夹,就无法匹配到了,于是在这里我们就可以使用VPATH这个变量(这个make默认识别的变量用于在当前目录下查找文件查找不到时,默认到VPATH路径下查找),同时还需要使用一个函数forreach。由于这里只匹配了源文件,相应的头文件的位置没有包含,所以还需要包含一下头文件。
OK,到这里,貌似我们已经写出了一个比较好的Makefile,但是,实际的编译参数我们都还没涉及,下面就具体分析一下,GNU下的编译工具的编译参数。
五.链接库
有时我们需要链接第三方库,那么如何链接库,如何使用库中的函数?
首先,在源代码中,我们需要包含所使用的库的接口函数的头文件;
其次,在Makefile中,我们需要在头文件和库的搜索路径中分别加入所需头文件和库的路径。假设库和库的头文件在路径/mnt/hgfs/share/miniupnp/miniupnpc-1.9.20160209路径下,那么包含方式
头文件:-I/mnt/hgfs/share/miniupnp/miniupnpc-1.9.20160209 //加入头文件路路径
库:-L/mnt/hgfs/share/miniupnp/miniupnpc-1.9.20160209 //加入库文件路径
如果库的名字是libminiupnpc.a,那么在链接时的参数就是 -lminiupnpc
类似于下图:
结束。