Makefile教程
1.Makefile简介
Makefile定义了软件开发过程中,项目工程编译链、链接的方法和规则。 由IDE自动生成或者开发者手动书写。 Unix(MAC OS、Solaris)和Linux(Red Hat、Ubuntu、SUSE)系统下由make命令调用当前目录下的Makefile文件,实现项目工程的自动化编译。
Windows环境开发人员,可能并未听说过Makefile,但是时时刻刻在使用Makefile来完成程序的编译,因为开发者并不需要手动编写Makefile,而是通过IDE自动生成。Linux环境开发人员,则有必要了解Makefile的语法规则与作用,来完成程序的自动化编译。
2.语法规则
不同厂商的Makefile在语法上可能会有细微的出入,但Makefile的主线和核心是文件依赖。语法规则如下:
target:prerequisites
command
其中,target为需要生成的目标,prerequisites为依赖项,command为make需要执行的命令(任意的Shell命令),command前必须以tab键开始。也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
3.Makefile内容
Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
1.显式规则。显式规则说明了,如何生成一个或多个的目标文件。这是由Makefile的书写者明显指出要生成的文件,文件的依赖文件,生成的命令。
2.隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。
3.变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
4.文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,请参考文末的参考资料。
5.注释。Makefile中只有行注释,和Linux的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“#”。
如果想多行注释的话,在注释行的结尾加行反斜线(\),下一行也被注释,这样就是可以实现多行注释了。很显然,Makefile的这多行注释的方法没有像C/C++的多行注释方法
/*注释*/
方便。
4.make的工作流程
执行Makefile时,在默认的方式下,我们只输入make命令,则相当于
make first_objname_in_Makefile
,意思是生成出现在Makefile中第一个目标文件。此外,我们也可以显示指明生成的目标名称,如
make objname
。
(1)make会在当前目录下找名字叫“Makefile”或“makefile”的文件; (2)如果找到,它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件; (3)如果target不存在,则根据target后的依赖项和command生成target。如果target已存在,则检测target依赖项是否是最新的,若被修改,则重新生成target; (4)如果依赖项(比如目标文件)是根据其它依赖项生成的,那么按照步骤3来检测生成依赖项。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出。在我们编程中,如果一个工程已被编译过了,当我们修改了其中一个源文件,比如file.cpp,那么根据我们的依赖性,我们的目标file.o会被重编译(也就是在这个依性关系后面所定义的命令),于是file.o的文件也是最新的啦,于是file.o的文件修改时间要比最终的可执行程序要新,所以最终的可执行程序也会被重新链接更新。
5.实例讲解
以三个源文件charset.cpp、network.cpp、buffer.cpp组成一个工程为例,为大家讲解Makefile的基本内容与编写规则。编写的较为冗余,并未使用make的自动推导能力,旨在细致剖析Makefile的编写规则。具体内容如下:
CCFILES += $(wildcard src/*.cpp)
SRCDIR := ./src/
VPATH = src:./include:./src/xmlparser:./lib
#Compilers
CC := g++
#Compilers para
FLAGS := -openmp -openmp-report -vec-report-O2
OBJECT :=charset.o network.o \
buffer.o
test.out : $(OBJECT)
$(CC)$(FLAGS) -o ALG.out $(OBJECT) ./lib/libxmlextern.a
charset.o :charset.h $(SRCDIR)charset.cpp
$(CC) $(FLAGS) -c $(SRCDIR)charset.cpp
network.o :network.h $(SRCDIR)network.cpp
$(CC) $(FLAGS) -c $(SRCDIR)network.cpp
buffer.o :buffer.h $(SRCDIR)buffer.cpp
$(CC) $(FLAGS) -c $(SRCDIR)buffer.cpp
.PHONY clean:
rm-f *.o *.out
具体说明: (1)通配符函数wildcard与Makefile注释方式。
#this is annotation
CCFILES += $(wildcardsrc/*.cpp)
利用wildcard函数获取src目录下所有.cpp文件,并赋值给自定义变量CCFILES。其中#号是Makefile的注释符号,同Shell。
(2)源文件目录
SRCDIR:= ./src/
自定义变量SRCDIR用于指明.cpp源文件所在目录。SRCDIR变量在command中出现时,以类似于宏替换的方式将其载入command中。
(3)预定义变量VPATH指明目标的依赖项所在目录
VPATH= src:./include:./src/xmlparser:./lib
指明Makefile寻找依赖项时,若当前工作目录不存在,则去VPATH指明的目录去寻找。各目录以“:”号隔开。
(4)编译器。
CC:=g++
自定义变量CC指明为编译器为g++,表示使用GNU C++ Compiler作为项目的编译器。
(5)编译选项。
FLAGS := -g -std=c++11 -Wall -O2
变量FLAGS指明编译选项,其中-g表示加入调试信息,-std=c++11表示使用C++11标准,-Wall表示允许编译器发出所有告警,-O2表示以O2等级优化代码。
(6)反斜扛“\”的作用。
OBJECT :=charset.o network.o \
buffer.o
变量OBJECT指明目标文件,其中反斜杠“\”表示一行还未结束。
(7)第一个目标文件
test.out : $(OBJECT)
$(CC) $(FLAGS) -o test.out $(OBJECT) ./lib/libxmlextern.a
此处表示Makefile需要生成的第一个目标文件,也就是不指明目标文件的make命令默认生成的目标文件。加入icpc的编译选项后,根据ALG.out依赖的目标文件和静态链接库项./lib/libxmlextern.a,链接生成可执行文件test.out。
(8)目标文件的生成。
charset.o :charset.h $(SRCDIR)charset.cpp
$(CC) $(FLAGS) -c $(SRCDIR)charset.cpp
指明charset.o的依赖项并编译成二进制文件charset.o。后面的每个目标文件皆是如此做法。
(9)伪目标的使用。
.PHONY clean
clean:
rm -f *.o *.out
使用.PHONY关键字,指明clean是伪目标,仅作标签使用。此处不依赖与任何项,使用方法是显示调用make clean,用于执行rm操作。也可以添加依赖项,如:
.PHONY all
all : prog1 prog2 prog3
则all依赖于prog1 prog2 prog3这三个文件,那么使用
make all
可以生成三个目标文件prog1、prog2和prog3。若将all放在所有目标文件的前面,则使用make即可,无需指明make all,原因是make命令将Makefile中第一个出现的目标作为最终目标,若不放在最前面,则必须指明make all。
实际上伪目标不需要使用.PHONY显示指明,直接书写即可,即
.PHONY clean
可以省略。
(10)Makefile赋值符号=、:=、+=和?=的区别。 = 是最基本的赋值,会覆盖以前的赋值,以Makefile中最后赋值为准; := 也会覆盖之前的值,但以当前赋值为准。 ?= 表示如果没有被赋值则赋予等号后面的值; += 表示追加等号后面的值。
其中=和:=的区别见如下代码: (1)“=”。 make会将整个Makefile展开后,再决定变量的值。也就是说,变量的值将会是整个Makefile中最后被指定的值。看例子:
x = foo
y = $(x) bar
x = xyz
在上例中,y的值将会是 xyz bar,而不是 foo bar 。
(2)“:=”。 “:=”表示变量的值决定于它在Makefile中的位置,而不是整个Makefile展开后的最终值。
x := foo
y := $(x) bar
x := xyz
在上例中,y的值将会是foo bar ,而不是 xyz bar 了。
6.多源文件目录的简单模板
通过上面简单示例可以大致了解Makefile的基本编写方法与内容,实际上,Makefile可以通过make自动推导特性、内置变量、自动化变量和函数等编写地更加简洁优雅,并且可以模板化。下面看一个简单的Makefile模板
假设源文件均为.cpp文件,那么简洁的、通用的Makefile模板可以书写为如下格式:
#指定多个源文件目录
DIR_SRC0 = ./src0
DIR_SRC1 = ./src1
DIR_OBJ = ./obj
DIR_BIN = ./bin
#添加第三方头文件目录,如果你用到了第三方的源码、静态或者动态链接库的话
INCDIR=-I/usr/local/json/include -I/usr/local/libcurl/inc
#添加静态链接库目录,如果你用到了第三方的静态链接库的话
LIBDIR=-L/usr/local/json -L/usr/local/libcurl
#通过扩展通配符函数wildcard在多个原文件目录寻找源文件
SRC = $(wildcard ${DIR_SRC0}/*.cpp) $(wildcard ${DIR_SRC1}/*.cpp)
#通过模式替换函数patsubst与去除目录函数notdir获取目标文件列表
OBJ = $(patsubst %.cpp,${DIR_OBJ}/%.o,$(notdir ${SRC}))
TARGET = main.out
BIN_TARGET = ${DIR_BIN}/${TARGET}
CC = g++
CFLAGS = -g -Wall ${INCDIR} -DDEBUG
${BIN_TARGET}:${OBJ}
$(CC) $(OBJ) -o $@ ${LIBDIR} -ljson -lcurl
#利用Makefile自动推导功能和自动化变量,用一条语句实现同一个目录下多个源文件的编译
#根据多个源文件目录添加多个,注意不同目录下的源文件不能重名
${DIR_OBJ}/%.o:${DIR_SRC0}/%.cpp
$(CC) $(CFLAGS) -c $< -o $@ ${INCDIR}
${DIR_OBJ}/%.o:${DIR_SRC1}/%.cpp
$(CC) $(CFLAGS) -c $< -o $@ ${INCDIR}
#下面可以添加每个目标文件的依赖的头文件,来实现头文件的更新带动目标文件的更新
#当然也可以不添加,但是这样做带来的后果就是,当修改了某个头文件,include该头文件的源文件不会被重新编译。这一点要切记
${DIR_OBJ}/main.o : defs.h
${DIR_OBJ}/kbd.o : defs.h command.h
${DIR_OBJ}/command.o : defs.h command.h
${DIR_OBJ}/display.o : defs.h buffer.h
${DIR_OBJ}/insert.o : defs.h buffer.h
.PHONY:clean
clean:
find ${DIR_OBJ} -name *.o -exec rm -rf {}
上述模板有几处需要重点了解一下。 (1)Makefile中内置变量 $@、$^、$<、$? 。 $@ 表示目标文件,$^ 表示所有的依赖文件,$< 表示第一个依赖文件,$? 表示比目标还要新的依赖文件列表。
(2)wildcard、notdir、patsubst均是Makefile内置函数,各含义如下: wildcard:扩展通配符; notdir:去除路径; patsubst:替换通配符。
(3)Makefile的规则通配符%,用于规则描述,一般用于目标文件的生成。例如:
%.o:%.cpp
$(CC) $< -o $@
(4)上面的Makefile模板一点需要注意的是,并未给每一个obj目标文件的添加头文件依赖,也就是说这样做的后果是修改了某个头文件之后,并不会重新编译使用了该头文件的源文件,请大家注意。那么如何解决这个遗憾呢?其实可以让编译器自动推导源文件使用了哪些头文件,这样我们就可以将源文件使用的头文件添加到目标obj文件的依赖项中,读者可参考网上的资料,自行给出实现。
(5)其实,上面的Makefile模板可以写的更简洁优雅一点,但可读性可能会有所下降。改进地方有两点: (5.1)将多个源文件目录写到一个变量,然后再利用Makefile的Shell函数将所有源文件目录下源文件取出。参考如下代码:
DIR_SRC=./src0 ./src1
SRC=$(shell for dir in ${CPPDIRS};do echo $${dir}/*.cpp;done)
(5.2)不必为多个目录的源文件添加多个生成目标文件的编译语句,可以使用一条语句搞定,但需要修改Makefile的环境变量VPATH让make自动寻找依赖项所在路径。
VPATH+=dir1:dir2:...
${DIR_OBJ}/%.o:%.cpp
$(CC) $(CFLAGS) -c $< -o $@ ${INCDIR}
此外,通过g++编译生成动态链接库或静态链接库,可以参考 linux: 几个常用Makefile模板 。大家也可以举一反三,给出自己的Makefile模板。
7.相关知识点
7.1Makefile中目标文件一定要把依赖的头文件包含进去吗?
不一定,可以不包含进去。Makefile是根据依赖项是否被修改决定是否重新执行command。如果不把头文件写入依赖项中,则面临的风险就是修改了头文件,目标文件不会被重新编译。我们的原则是,自己定义的头文件写入依赖项,库的头文件无需包含,除非你要修改库的头文件。
7.2VPATH的单一作用
VPATH是Makefile的特殊变量,只能用来指明Makefile寻找目标文件的依赖项所在的目录,不能帮助编译器寻找所需编译的文件。
7.3VPATH与vpath的区别
vpath是Makefile的关键字,VPATH是Makefile的特殊变量,两者的区别在于VPATH指定全局的搜索路径,而vpath可以针对特定的文件搜索路径。
vpath命令有三种形式: vpath pattern path : 符合pattern的文件在path目录搜索。 vpath pattern : 清除pattern指定的文件搜索路径 vpath : 清除所有文件搜索路径。
例如:
vpath %.h ./include //指定.h类型文件的搜索路径是include
vpath %.cpp ./src //指定.cpp类型文件的搜索路径是src
7.4Makefile中shell命令前加@字符
make执行的命令前面加了@字符,则不显示命令本身而只显示它的结果。
7.5变量的替换函数
替换变量中指定的内容有两种方式。 (1)模式匹配替换字符串函数patsubst 用法如下:
res=$(patsubst %.c,%.o,$(var) )
以上表示将变量$(var)中所有以.c结尾的字符串变成.o结尾。patsubst的英文全称是pattern substitute string,是三个单词的前三个或两个字母拼接组成的名字。
(2)使用变量的替换引用 这里用到Makefile里的替换引用规则,即用指定的变量替换另一个变量。其用法格式如下:
res=$(var:%.a=%.b)
例如:
foo:=a.c b.c
bar=$(foo:%.a=%.o)
那么bar就变成了a.o b.o。
以上表示将变量foo中以.a结尾的字符串替换成.b结尾并返回结果。 注意 ,字符串处理函数并不会改变原有的字符串,变量的替换引用规则也不会改变原来字符串。实际上变量的替换引用是模式匹配替换函数patsubst的一个简化实现。
7.6Makefile中三个内置变量:$@、$<和$^
$@,$<,$^代表的意义分别是:
$@:目标文件;
$<:第一个依赖文件;
$^:所有的依赖文件。
通过以上特殊变量,可以简化Makefile。例如:
main:main.o mytool1.o mytool2.o
gcc -o $@ $^
main.o:main.c mytool1.h mytool2.h
gcc -c $<
7.7Makefile中如何调用子目录的Makefile
$(Target):$(OBJS)
$(MAKE) -C $(SUBDIR)
$(Target):$(OBJS)
cd subdir && $(MAKE)
解释:
当生成target目标对象时,会执行
$(MAKE) -C $(SUBDIR)
这条命令,进入目录OBJDIR,该目录下有一个Makefile,并执行。其中,$(MAKE) 值make预定义的变量,一般指的就是make,无需修改,可通过make -p查看make所有的预定义的变量。当然,也可直接指明为make,即
make -C $(SUBDIR)
。
其中-C表示改变当前目录,make的命令选项可通过make -h查看。
如果想对子目录的进行make clean,该怎么做呢? 同理,进入相应的子目录之后再进行make clean,命令如下:
make clean -C $(SUBDIR) -f Makefile
Makefile中调用shell脚本: 如果稍微复杂一点,还可以使用循环进入多个子目录进行make clean。这里需要在Makefile中嵌入shell脚本,Makefile参考代码如下:
SUBDIRS=subdir1 subdir2 subdir3
RECURSIVE_CLEAN=for subdir in $(SUBDIRS);\
echo cleaning in $${subdir};\
(cd ${subdir} && $(MAKE) clean -f Makefile)||exit 1;\
.PHONY: clean
clean:
$(RECURSIVE_CLEAN)
阅读以上代码,注意如下几点: (1)shell脚本中,分号是多个语句之间的分隔符号,当一行只有一条语句的时候,末尾无需分号,当然加了也没错。
那么如何将shell的for循环写成一行呢?将shell的for循环写在一行的情况如下:
#分行写for循环
array=("lvlv0" "lvlv1") #定义数组
for dir in ${array[@]}
echo $dir
echo "end"
#for循环写成一行的形式
array=("lvlv0" "lvlv1") #定义数组
for dir in ${array[@]};do echo $dir;done;echo "end"
将for循环写成一行时,do后面需要有空格符或者tab符来分隔。如果done后面还有语句的话,需要再加上分号。
(2)当Makefile内嵌shell脚本时,Makefile中每一行的shell脚本需要一个shell进程来执行,不同行之间变量值不能传递。所以,Makefile中的shell不管多长也要写在一行。因此,多行的shell需要在Makefile使用反斜杠”\”连接为一行。此时,shell脚本中的一条语句后需要添加分号分隔。
(3)Makefile中的变量需要通过
$(variableName)
或者
${variableName}
来引用。shell脚本中变量的引用方式是
$variableName
和
${variableName}
,不能通过
$(variableName)
来引用。但是如果将shell脚本嵌入Makefile中,shell脚本中引用shell变量,则需要
$$
来引用,即
$${variableName}
或者
$$variableName
。
(4)Makefile中对一些简单变量的引用,可以不使用”()”和”{}”来标记变量名,而直接使用
$x
的格式来实现,此种用法仅限于变量名为单字符的情况。另外自动化变量也使用这种格式。对于一般多字符变量的引用必须使用括号,否则make将把变量名的首字母作为作为变量而不是整个字符串(
$PATH
在Makefile中实际上是
$(P)ATH
)。
(5)Makefile嵌入shell脚本时,要想shell脚本被执行,必须将shell脚本写在target,卸载其它地方会被忽略。考察如下Makefile 代码:
if [ "$(BUILD)" = "debug" ]; then echo "build debug"; else echo "build release"; fi
echo "done"
上面”build debug”和”build release”之类的字符串根本不会打印出来。正确写法应该是将shell脚本放在target,示例如下:
all:
if [ "$(BUILD)" = "debug" ]; then echo "build debug"; else echo "build release"; fi
echo "done"
7.8Makefile中通配符*与%的区别是什么?
此两者均为通配符,但更准确的讲,%为Makefile规则通配符,用于规则描述,*为扩展通配符,用于扩展。如
%.o:%c
$(CC) $< -o $@
表示所有的目标文件及其所有依赖文件,然后编译所有目标文件的第一个依赖文件,并生成目标文件。再如:
$(filter %.c ,SOURCES)
此处SOURCES表示包含.c .cc .cpp等多类型源文件,该过滤器函数将c文件过滤出来,而%.c即为此过滤器规则。
通配符*则不具备上述功能。尤其是在Makefile中,当变量定义或者函数调用时,通配符%的展开功能就失效了。此时需要借助wildcard函数。通配符*常用于wildcard函数中,二者应用范围不同。
7.9Makefile中PHONY关键字的作用
PHONY的用法:
.PHONY Target1 Target2
PHONY的作用: 指明Target是伪目标,并不会真正生成Target目标文件。伪Target是用来显示请求执行的命令名称。
为什么使用PHONY来指明命令名称: (1)避免和同名文件冲突。其实是可以不用.PHONY来指明命令名称,因为命令并不会被产生,也就是不存在,所以make target时命令始终会被执行。但是当存在与命令名称同名的目标文件时,一定要使用PHONY来描述命令名,因为命令名没有依赖文件,如果同名的文件始终是最新文件,那么显示make命令名时,该命令永远不会被执行。为避免这个问题,可使用”.PHONY”指明该命令名称。如:
.PHONY : clean
clean:
rm -f *.out *.o
这样执行
make clean
会无视clean文件存在与否,或者是否是最新的。直接执行clean这个伪目标依赖的命令。
(2)使用.PHONY指定伪目标可以改善性能。因为PHONY目标并非是由其它文件生成的实际文件,没有依赖项,make 会跳过依赖项的搜索和依赖项的更新检查。这就是声明phony 目标会改善性能的原因。
7.10如何使用shell脚本给Makefile变量赋值
Makefile可以内嵌shell脚本,但是在内嵌的shell脚本只能读取Makefile的变量,如何给Makefile变量赋值呢?记录下面不可行的操作。
#Makefile
CPPDIRS=mysql src
CPPS=
assign:
for dir in ${CPPDIRS};do CPPS+=$${dir};done;echo ${CPPS}
make assign之后,输出为空,说明这种方式不行。
其实可以使用Makefile的shell函数来执行shell脚本,因为shell函数把执行shell脚本后的输出作为函数返回,因此我们可以使用shell函数来为Makefile的变量赋值。参考如下代码:
CPPDIRS=mysql src
CPPS=$(shell for dir in ${CPPDIRS};do echo $${dir}/*.cpp;done)
上面的代码就可以将指定的代码源文件目录下的所有源文件连同路径赋给CPPS。
7.11Makefile中.cpp.o和.c.o
Makefile的旧式写法中,可能会出现如下的写法:
.cpp.o:
$(CC) $(INCLUDE) $(CFLAGS) -c $<
.c.o:
$(CC) $(INCLUDE) $(CFLAGS) -c $<
一眼望去,为什么目Makefile中目标文件没有依赖项。原来这种是老式的“双后缀规则”,编译器会自动将Makefile所在目录的.cpp识别为源文件后缀,而.o识别为输出文件后缀。特别需要注意的是,后缀规则不允许任何依赖文件,但也不能没有命令。
这种旧式的写法虽然简洁,但有几个缺点: (1)不能显示指定源文件所在目录; (2)不能显示指定目标生成后的目录; (3)不能指定目标依赖项。 后缀规则不允许任何的依赖文件,如果有依赖文件的话,那就不是后缀规则,那些后缀统统被认为是文件名,如:
.c.o: foo.h
$(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
这个例子,就是说,文件”.c.o”依赖于文件”foo.h”,而不是我们想要的这样:
%.o: %.c foo.h
$(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
综合来看,双后缀规则不太方便,建议还是放弃这种旧式写法。
7.12makfile中include、-include与sinclude的区别
include:包含其它Makefile至当前Makefile中,作用类似于C/C++中的#include预处理指令。 -include:作用与include相同,区别在于无法找到被包含的Makefile时,Makefile不报错。 sinclude:等同于-include,是一个兼容的写法。
8.小结
实际上,Makefile中还有很多基础知识点和复杂的特性并未在文中赘述,比如各种函数的用法、嵌套执行make、双后缀规则、定义命令包等,这些需要我们在实际使用过程中去熟悉掌握。
参考资料:
[1] Makefile经典教程(掌握这些足够)[转] [2] Makefile百度百科 [3] Makefile里PHONY的相关介绍