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.1 Makefile 中目标文件一定要把依赖的头文件包含进去吗

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

7.2 VPATH 的单一作用

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

7.3 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

7.4 Makefile 中 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);\
	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脚本中引用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"

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.11 Makefile 中 .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.12 引用其它的 Makefile

在 Makefile 中可以使用 include关键字把别的 Makefile 包含进来,这很像 C 语言的 #include,被包含的文件会原模原样的放在当前文件的包含位置。include 的语法是:

include [filename]

而在使用 include 时,可以有如下几种形式:
(1)include:包含其它 Makefile 至当前 Makefile 中,作用类似于 C/C++ 中的 #include 预处理指令。
(2)-include:作用与 include 相同,区别在于无法找到被包含的 Makefile 时,Makefile 不报错。
(3)sinclude:等同于 -include,是一个兼容的写法。

7.13 访问 Shell 环境变量

比如下面的 makefile,使用 include 关键字包含其它 makefile,但是使用了一个变量 BASE_LIB_DIR 来指明公共makefile 的地址。

include $(BASE_LIB_DIR)/comm.mk

INC += -I./include
INC += -I../../pdu/include

假如有很多个 makefile 的都需要引入 comm.mk,当 comm.mk 的路径发生变化时,我们可以使用 export 命令将变量BASE_LIB_DIR 设置为环境变量,这样就不用逐一修改引用 comm.mk 的所有 makefile 了。

7.14 makefile 的 export

makefile 中的 export 是导出变量到子 makfile,同级的 makefile 是无法访问该导出变量。示例用法如下:

ifeq "$(PATH_PRJ_ROOT)" ""
        export PATH_PRJ_ROOT=$(shell cd $(realpath .);while true; do if [ -f PRJ_ROOT ]; then pwd;break; else cd ..;fi;done;)
endif

上面的脚本作用是给变量 PATH_PRJ_ROOT 赋值,并将其导出用于子 makefile 中。

注意:makefile 中的 export 是导出变量到子 makfile,而目标对应执行的动作中的 export,是属于 Shell 中的 export,其作用是导出变量到当前 Shell,此两个 export 的作用是不同的。

7.15 override 指示符

执行 make 时,如果通过命令行定义了一个变量,那么它将覆盖 Makefile 中的同名变量。为了防止覆盖,可以使用 override 修饰变量。语法格式如下:

override <variable> = <value>
override <variable> := <value>
override <variable> += <more text>
override <variable> ?= <value>

override define <variable>
<text>
endef

例如无论命令行指定哪些编译参数,编译时必须打开 -g 选项,那么在 Makefile 中编译选项 CFLAGS 应该这样定义:

override CFLAGS += -g

假如执行 make 时在命令行指定了变量 CFLAGS 的值:

make CFLAGS=-O2

使用 override 关键字修饰变量 CFLAGS,表示对命令行变量 CFLAGS 进行重写。这样,在执行 make 时无论在命令行中指定了哪些编译选项,即指定变量 CFLAGS 的值,编译时 -g 参数始终存在。

可以看出,通过 override 实现了在 Makefile 中修改命令行参数的一种机制。

7.16 条件判断

使用条件判断,可以让 make 根据运行时的不同情况选择不同的执行分支。条件表达式是比较变量的值。

下面的例子,判断 $(CC) 变量是否为 gcc,如果是的话,则使用 GNU 函数编译目标。

libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc)
	$(CC) -o foo $(objects) $(libs_for_gcc)
else
	$(CC) -o foo $(objects) $(normal_libs)
endif

可见,在上面示例的这个规则中,目标 foo 可以根据变量 $(CC) 值来选取不同的函数库来编译程序。

我们可以从上面的示例中看到三个关键字:ifeq、else 和 endif。ifeq 的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起,注意 ifeq 与圆括号之间需要有空格(Tab 也可以)。else 表示条件表达式为假的情况。endif 表示一个条件语句的结束,任何一个条件表达式都必须以 endif 结束。

与关键字 ifeq 作用相反的是 ifneq,表示条件不成立时执行对应的语句。

如果条件表达式中嵌套条件表达式,那么可以对子级条件表达式使用空格或 Tab 进行缩进。

env = dev 
force_formal = true

ifeq ($(env), formal)
        isformal = true
else
        ifeq ($(force_formal), true)
                isformal = force_true
        endif
endif

first_target:
        echo $(isformal)

执行 make 输出:

echo force_true
force_true

8.小结

实际上,Makefile 中还有很多基础知识点和复杂的特性并未在文中赘述,比如各种函数的用法、嵌套执行 make、双后缀规则、定义命令包等,这些需要我们在实际使用过程中去熟悉掌握。


参考资料:

[1] ruglcc.CSDN.Makefile经典教程(掌握这些足够)
[2] 百度百科.Makefile
[3] Makefile里PHONY的相关介绍
[4] shell语句中需要分号分隔吗
[5] Makefile中的shell语法
[6] 多个文件目录下Makefile的写法
[7] Makefile里PHONY的相关介绍
[8] Makefile中关于all和.PHONY .cpp.o
[9] Makefile中include、-include、sinclude
[10] make之eval函数

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页