makefile用法用例与注意事项 .

时间:2021-12-20 15:06:28

一、makefile简介

定义: makefile定义了软件开发过程中,项目工程编译链、接接的方法和规则。

产生: 由IDE自动生成或者开发者手动书写。

作用: Unix(MAC OS、Solaris)和Linux(Red Hat、Ubuntu、SUSE)系统下由make命令调用当前目录下的makefile文件执行,可实现项目工程的自动化编译。


二、语法规则

target:prerequisites
command

其中,target为需要生成的目标,prerequisites为依赖项,command为make需要执行的命令(任意的Shell命令)。
注意:其中command前必须以tab键开始。


三、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的这多行注释的方法没有像CC++的多行注释方法/*注释*/方便。


四、make的工作流程

在默认的方式下,也就是我们只输入make命令,则相当于make first_objname_in_makefile。意思是生成出现在makefile中第一个目标文件。或者指明生成的目标名称,如make objname

按默认方式,输入make命令,其工作方式是:
1.make会在当前目录下找名字叫“Makefile”或“makefile”的文件。

2.如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,并把这个文件作为最终的目标文件。

3.如果target不存在,则根据target后的依赖项和command生成target。如果target已存在,则make检测target依赖项是否是最新的,若被修改,则重新生成target。


五、实例讲解

鄙人将以实际工作项目的makefile为例为大家讲解makefile的创建过程。Makefile内容如下:

CCFILES += $(wildcard src/*.cpp)

SRCDIR := ./src/

VPATH = src:./include:./src/xmlparser:./lib

#Compilers
#CC := /opt/intel/composer_xe_2013.0.079/bin/intel64/icpc
CC := icpc

#Compilers para
FLAGS := -openmp -openmp-report -vec-report-O2

OBJECT :=file_interface.o tinyxml2.omic_decomposer.o Charset.o network.o buffer.o \ task_queue.o common.o main.omic_function.o hashtree.o nodeconfig.o md5_mic.o ntlm_mic.o

ALG.out : $(OBJECT)
$(CC)$(FLAGS) -o ALG.out $(OBJECT) ./lib/libxmlextern.a

file_interface.o :global.h tinyxml2.hfile_interface.h $(SRCDIR)file_interface.cpp
$(CC)$(FLAGS) -c $(SRCDIR)file_interface.cpp
tinyxml2.o :tinyxml2.h$(SRCDIR)tinyxml2.cpp
$(CC)$(FLAGS) -c $(SRCDIR)tinyxml2.cpp
mic_decomposer.o :$(SRCDIR)mic_decomposer.cppmic_decomposer.h mic_define.h cnre.h \ common.h hashtree.h
$(CC)$(FLAGS) -c $(SRCDIR)mic_decomposer.cpp
Charset.o :Charset.h cnre.h$(SRCDIR)Charset.cpp
$(CC)$(FLAGS) -c $(SRCDIR)Charset.cpp
network.o :network.h task_queue.hnetwork_packet.h nodeconfig.h $(SRCDIR)network.cpp
$(CC)$(FLAGS) -c $(SRCDIR)network.cpp
buffer.o :network.h task_queue.hnetwork_packet.h nodeconfig.h buffer.h $(SRCDIR)buffer.cpp
$(CC)$(FLAGS) -c $(SRCDIR)buffer.cpp
task_queue.o :task_queue.h$(SRCDIR)task_queue.cpp
$(CC)$(FLAGS) -c $(SRCDIR)task_queue.cpp
common.o :common.h mic_define.hmic_function.h $(SRCDIR)common.cpp
$(CC)$(FLAGS) -c $(SRCDIR)common.cpp
main.o :task_queue.h global.h network.hnetwork_packet.h common.h nodeconfig.h \ mic_define.h mic_function.hfile_interface.h $(SRCDIR)main.cpp
$(CC)$(FLAGS) -c $(SRCDIR)main.cpp
mic_function.o :mic_function.h$(SRCDIR)mic_function.cpp
$(CC)$(FLAGS) -c $(SRCDIR)mic_function.cpp
hashtree.o :hashtree.h$(SRCDIR)hashtree.cpp
$(CC)$(FLAGS) -c $(SRCDIR)hashtree.cpp
nodeconfig.o :nodeconfig.h./src/xmlparser/tinyxml.h $(SRCDIR)nodeconfig.cpp
$(CC)$(FLAGS) -c $(SRCDIR)nodeconfig.cpp
md5_mic.o :simd.h mic_define.h common.hshare_macro.h md5_macro.h md5_mic.h \ file_interface.h $(SRCDIR)md5_mic.cpp
$(CC)$(FLAGS) -c $(SRCDIR)md5_mic.cpp
ntlm_mic.o :simd.h mic_define.h common.hshare_macro.h ntlm_macro.h ntlm_mic.h
$(CC)$(FLAGS) -c $(SRCDIR)sha256_mic.cpp

.PHONY clean:
rm-f *.o *.out

具体讲解:

1.通配符函数wildcard获取所有源文件

#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 := icpc

自定义变量CC指明为编译器为icpc,表示使用Intelc++ Compiler作为项目的编译器。

5.编译选项

FLAGS := -openmp -openmp-report-vec-report -O2

指明编译选项。

6.反斜扛 \ 的作用

OBJECT :=file_interface.otinyxml2.o mic_decomposer.o Charset.o network.o buffer.o \
task_queue.o common.o main.o mic_function.o hashtree.onodeconfig.o md5_mic.o ntlm_mic.o

指明目标文件;其中反斜杠\表示一行还未结束。

7.第一个目标文件

ALG.out : $(OBJECT)
$(CC) $(FLAGS) -o ALG.out $(OBJECT) ./lib/libxmlextern.a

此处表示makefile需要生成的第一个目标文件,也就是不指明目标文件的make命令默认生成的目标文件。加入icpc的编译选项后,根据ALG.out依赖的目标文件和静态链接库项./lib/libxmlextern.a,进行连接生成可执行文件ALG.out。

8.目标文件的生成

file_interface.o :global.htinyxml2.h file_interface.h $(SRCDIR)file_interface.cpp
$(CC) $(FLAGS) -c$(SRCDIR)file_interface.cpp

指明file_interface.o的依赖项并编译成二进制文件file_interface.o。后面的每个目标文件皆是如此做法。

9.伪目标的使用

.PHONY clean
clean:
rm -f *.o *.out

使用.PHONY关键字,指明clean是伪目标,仅作标签使用。此处不依赖与任何项,使用方法是显示调用make clean,用于执行rm操作。但也可以添加依赖项,如:

all : prog1 prog2 prog3
.PHONY : all

则all依赖于prog1 prog2 prog3这三个文件,那么使用 makeall则可以生成三个目标文件prog1、prog2和prog3。若将all放在所有目标文件的前面,则使用make即可,无需指明make all,原因是make命令将makefile中第一个出现的目标作为最终目标,若不放在最前面,则必须指明make all。


六、注意事项与知识点

1.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 了。

2.makefile中目标文件一定要把依赖的头文件包含进去吗?

不一定,可以不包含进去。makefile是根据依赖项是否被修改决定是否重新执行command。如果不把头文件写入依赖项中,则面临的风险就是修改了头文件,目标文件不会被重新编译。我们的原则是,自己定义的头文件写入依赖项,库的头文件无需包含,除非你要修改库的头文件。

3.VPATH的单一作用

VPATH是makefile的特殊变量,只能用来指明makefile寻找目标文件的依赖项所在的目录,不能帮助编译器寻找所需编译的文件。

4.VPATH与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

5.makefile中shell命令前加@字符

make执行的命令前面加了@字符,则不显示命令本身而只显示它的结果。

6.变量的替换函数

替换变量中指定的内容有两种方式。

(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.Makefile中三个自动化变量:$@,$^和$<

$@,$^,$<代表的意义分别是:
$@:目标文件;
$^:所有的依赖文件;
$<:第一个依赖文件。

通过以上特殊变量,可以简化Makefile。例如:

main:main.o mytool1.o mytool2.o
gcc -o $@ $^

main.o:main.c mytool1.h mytool2.h
gcc -c $<

8.makefile中如何调用子目录的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);\
do\
echo cleaning in $${subdir};\
(cd ${subdir} && $(MAKE) clean -f makefile)||exit 1;\
done
.PHONY: clean
clean:
$(RECURSIVE_CLEAN)

阅读以上代码,注意如下几点:
(1)shell脚本中,分号是多个语句之间的分隔符号,当一行只有一条语句的时候,末尾无需分号,当然加了也没错。

那么如何将shell的for循环写成一行呢?将shell的for循环写在一行的情况如下:

#分行写for循环
array=("lvlv0" "lvlv1") #定义数组
for dir in ${array[@]}
do
echo $dir
done
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变量,则需要$$来引用,即$${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
all:
echo "done"

上面的”build debug”和”build release”之类的字符串根本不会打印出来。上面的正确写法应该是将shell脚本放在target,示例如下:

all:
if [ "$(BUILD)" = "debug" ]; then echo "build debug"; else echo "build release"; fi
echo "done"

9.Makefile中通配符*与%的区别是什么?

此两者均为通配符,但更准确的讲,%为Makefile规则通配符,一般用于规则描述,*为扩展通配符,用于扩展。如

%.o:%c
$(CC) $< -o $@

表示所有的目标文件及其所有依赖文件,然后编译所有目标文件的第一个依赖文件,并生成目标文件。再如:

$(filter %.c ,SOURCES)

此处SOURCES表示包含.c .cc .cpp等多类型源文件,该过滤器函数将c文件过滤出来,而%.c即为此过滤器规则。

通配符*则不具备上述功能。尤其是在Makefile中,当变量定义或者函数调用时,通配符%的展开功能就失效了。此时需要借助wildcard函数。通配符*常用于wildcard函数中,二者应用范围不同。

10.makefile中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 目标会改善性能的原因。

11.多源文件目录的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)

OBJ = $(patsubst %.cpp,${DIR_OBJ}/%.o,$(notdir ${SRC}))

TARGET = main.out
BIN_TARGET = ${DIR_BIN}/${TARGET}

CC = g++
CFLAGS = -g -Wall -I${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模板一点需要注意的是,并未给每一个obj目标文件的添加头文件依赖,只也就是说这样做的后果是修改了某个头文件之后,并不会重新编译使用了该头文件的源文件,请大家注意。

那么如果解决这个遗憾呢?其实可以让编译器自动推导源文件使用了哪些头文件,这样我们就可以将源文件使用的头文件添加到目标obj文件的依赖项中,读者可参考网上的资料,自行给出实现。

(4)其实,上面的makefile模板可以写的更优雅一点,改进地方有两点。
(4.1)将多个源文件目录写到一个变量,然后再利用makefile的shell函数将所有源文件目录下源文件取出。参考如下代码:

DIR_SRC=./src0 ./src1
SRC=$(shell for dir in ${CPPDIRS};do echo $${dir}/*.cpp;done)

这个也是下面一个知识点讲解的内容。

(4.2)不必为多个源文件目录添加多个生成目标文件的编译语句,可以使用一条语句搞定,但需要修改makefile的环境变量VPATH让make自动寻找依赖项所在路径。这里需要满足一点要求是不同目录下的源文件不能重名。

VPATH+=dir1:dir2:...
${DIR_OBJ}/%.o:%.cpp
$(CC) $(CFLAGS) -c $< -o $@ ${INCDIR}

此外,要想通过g++编译生成动态链接库或静态链接库,可以参考linux: 几个常用makefile模板。大家也可以举一反三,给出自己的makefile模板。

12.如何使用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。

13. makefile中.cpp.o和.c.o

makefile的旧式写法中,可能会出现如下的写法:

.cpp.o:
$(CC) $(INCLUDE) $(CFLAGS) -c $<

.c.o:
$(CC) $(INCLUDE) $(CFLAGS) -c $<

一眼望去,这不合常理啊。为什么目makefile中目标文件没有依赖项。原来这种是老式的“后缀规则”,编译器会自动将makefile所在目录的.cpp识别为源文件后缀,而.o识别为输出文件后缀。特别需要注意的是,后缀规则不允许任何依赖文件,但也不能没有命令。

这种旧式的写法虽然简洁,但是有个两个缺点,一时不能指定源文件所在的目录,二是不能指定目标文件生成后的目录。不太方便,建议还是忍痛割爱放弃这种旧式写法吧。


参考资料:

[1] Makefile经典教程(掌握这些足够)[转].http://blog.csdn.net/ruglcc/article/details/7814546/
[2] makefile百度百科
[3]http://www.cnblogs.com/hnrainll/archive/2011/04/12/2013377.html
[4]shell语句中需要分号分隔吗.http://zhidao.baidu.com/link?url=RzR1wT_CbFT67YEPS-DYAgunOw9NCZl9_cZWVWp3k9sT4KGkRT7ihAuHT43lstEr422WEVFB7hDtLI4zW_Vxt_
[5]makefile中的shell语法.http://blog.csdn.net/qingfengtsing/article/details/21252461
[6]多个文件目录下Makefile的写法.http://www.cnblogs.com/Anker/p/3242207.html
[7]makefile里PHONY的相关介绍
[8]makefile中关于all和.PHONY .cpp.o