0. 官方文档
- GNU Make 官方网站: https://www.gnu.org/software/make
- GNU Make 官方文档下载地址: https://www.gnu.org/software/make/manual/
- Makefile Tutorial:https://makefiletutorial.com/
1.基本要求
1.1 基本格式
targets : prerequisties
[tab键]
command
- target : 目标文件,可以.o后缀的目标文件,也可以是执行文件,还可以是一个标签(Label),对于标签这种特性,在后续的“伪标签”章节会有叙述
- prerequisties:要生成的那个target所需要的文件或目标
- command: 是make需要执行的命令
通常我们会将一堆.c/.cpp
文件放在prerequisties
, targets 放对应需要生成的.o
文件。执行的command
对应就是g++ - c $(prerequisties) -o $(argets)
。同样链接可执行文件的时候也是一样的,targets
放可执行文件,prerequisties则放目标.o文件
注意
: command的前面需要用tab
,而不是空格键。makefile对格式的控制是非常严格的
案例
通常在makefile中,会希望有个语句debug
, 用来打印一些需要的信息,比如变量的值或者一些函数的处理结果。
Makefile
中的内容如下
debug:
echo hello
cd 到Makefile
所在目录,然后执行make debug
就可以输出结果了。
可以看到输出了hello
结果,同时也输出了command
命令。随着工程越来越大,我们的命令会变得复杂。都打印出所有command
终端看起来会显得冗余
。 可以在命令前加上@
符号,就可以不在终端输出command
,只输出结果。
debug:
@echo hello
1.2 Makefile规则
make
会在当前目录下找到一个名字叫Makefile
或者makefile
的文件- 在执行make命令时,如果没有指定target(
make xx (target)
),它会找到文件中第一个目标文件(target
), 并把这个文件作为最终的目标文件 - 如果
target
文件不存在
,或者target文件依赖的.o
文件(prerequities)的文件修改时间要比target这个文件新,就会执行后面所定义的命令command
来生成target文件 - 如果target依赖的
.o
文件(prerequities) 文件也不存在,make会在当前文件中找到target为.o
文件的依赖项,并根据对应的command
生成.o
文件。
makefile会根据依赖,一层层去执行,直到最终生成我们需要的target。makefile中一旦源文件做了更改(.cpp/.c), 它会自动重新编译一遍,这其实是非常方便的。
1.3 伪目标
伪目标
不是一个文件,它只是一个标签
,我们需要显示地指明这个目标
才能让其生效。- 伪目标的取名不能和文件名重名,否则不会执行命令。(比如与makefile同目录存在一个与伪目标
同名
的clean
文件,此时我们想 执行make clean
删除 文件夹objs
,则会无法删除
为了避免和文件重名
的情况,我们可以使用一个特殊的标记.PHONY
来显示地指明一个目标为伪目标
,向make说明,不管是否存在该文件,都不影响该伪目标的执行。通常在一个makefile里面,对于clean,debug,run
等命令, 我们一般都会写在.PHONY
后面, 然后就可以和make 一起当作命令去执行了。
.PHONY:clean debug run
注意
:如果出现报错
,可能是使用了空格,重新tab
下
2.变量的定义与使用
变量在声明时候需要给予初值, 在使用时,需要给变量名前加上$
符号,并以()
把变量给包括起来。
2.1 变量的定义
cpp := src/main.cpp
obj := objs/main.o
2.2 变量的引用
- 可以使用() 或者{}
cpp := src/main.cpp
obj := objs/main.o
$(obj) : $(cpp)
@g++ -c $(cpp) -o $(obj)
# make compile可执行编译
compile : $(obj)
# 打印调试信息
debug :
@echo $(cpp)
@echo $(obj)
在终端运行make compile
命令,可执行编译过程。编译完后,在objs下会生成main.o目标文件
2.3 预定义的变量
除了自定义的变量,makefile还提供了一些预定义的变量,可以很方便减少command
中的语句。
$@
: 目标(target)的完整名称$<
: 第一个依赖的文件(prerequisties)的名称$^
: 所有的依赖文件(prerequisties), 以空格分开,不包括重复的依赖文件
cpp := src/main.cpp
obj := objs/main.o
$(obj) : $(cpp)
@g++ -c $< -o $@
@echo $^
compile: $(obj)
clean:
@rm -rf objs/main.o
debug :
@echo $(cpp)
@echo $(obj)
# 为了防止和文件冲突,添加伪目标标识
.PHONY : compile clean
执行
# 清除之前编译生成的.o文件
make clean
# 重新编译
make compile
3. 常用的符号
(1) =
- 简单的赋值运算符
- 用于将右边的值分配给左边变量
- 如果在后面的语句中重新定义了了该变量,则将使用新的遍历
示例
HOST_ARCH = aarch64
TARGET_ARCH = ${HOST_ARCH}
#....
#....
HOST_ARCH = amd64
debug:
@echo ${TARGET_ARCH}
终端执行:
make debug
>> amd64
可以看到TARGET_ARCH
的值随着HOST_ARCH的变化更新了
(2) :=
- 立即赋值运算符
- 用于在
定义变量
时立即求值 - 该值在定义后不再更改 (与
=
的区别) - 即使在后面的语句中重新定义了该变量
示例
HOST_ARCH := aarch64
TARGET_ARCH := ${HOST_ARCH}
#....
#....
HOST_ARCH := amd64
debug:
@echo ${TARGET_ARCH}
终端执行:
make debug
>> aarch64
可以看到打印出来的是最初赋的值aarch64
(3) ?=
- 默认赋值运算符
- 如果该变量已经定义,则不进行任何操作
- 如果该变量尚未定义,则求值并分配
示例
HOST_ARCH = aarch64
HOST_ARCH ?= amd64
debug:
@echo ${HOST_ARCH}
- 输出的是
aarch64
,已经赋值了,就不会进行赋值操作。
#HOST_ARCH = aarch64
HOST_ARCH ?= amd64
debug:
@echo ${HOST_ARCH}
- 此时输出的是
amd64
,因为没有赋值的话,就会进行赋值
需要注意下,如下也是赋值了的,只是赋值了一个空的字符串
HOST_ARCH =
(4) 累加+=
+=
是非常常用的,比如在编译的时候,添加头文件路径,库的路径,名字以及c++编译的选项等
,就会需要进行累加的操作,添加完成就相当于将所有字符串,保存称为一个字符串数组或列表中。
示例
include_path := src
CXXFLAGS := -m64 -fPIC -g -o0 -std=c++11 -w -fopenmp
CXXFLAGS += $(include_path)
debug :
@echo ${CXXFLAGS}
.PHONY: debug
终端执行:
>> -m64 -fPIC -g -o0 -std=c++11 -w -fopenmp src
可以看到将src
加到了最后。
(4) 续行符\
后面会用的库越来越多,包含头文件的路径,库文件的路径也会越来越多,所以都会用续行符\
去写,注意\
前面可以用空格隔开,后面不需要空格
,
LDLIBS := cudart opencv_core gomp \
nvinfer protobuf cudnn pthread \
cublas nvcaffe_parser nvinfer_plugin
4. 常用函数
函数的调用,很像变量的使用,也是用$
来标识的,其语法如下:
$(fn,arguments) or ${fn,arguments}
- fn函数名
- argument: 函数参数,
参数间以逗号分隔
,而函数名和参数之间以空格分隔
(1) shell 函数
shell
函数是一个非常常用的函数,可以让我们在makefile里面执行终端bash
的命令
${shell <command> <arguments>}
- 名称: shell 命令函数 —shell
- 功能: 调用shell命令 command
- 返回: 函数返回shell 命令command的执行结果
示例
查找src
目录下所有.cpp
文件
cpp_srcs := ${shell find src -name *.cpp} #src 目录 -name 以名字查找 *.cpp 所有以.cpp结尾
debug :
@echo ${cpp_srcs}
.PHONY : debug
记得如果debug是个命令,而不是生成的文件的话,记得加上.PHONY
的标识符.
终端执行:
make debug
就可以拿到src
目录下全部的.cpp
文件路径
(2)subst 函数
subst
是一个字符串替换的函数。
语法
:
$[subst <from>,<to>,<text>}
- 名称: 字符串替换函数——subst
- 功能: 把字符串
中的字符串替换成 - 返回: 被替换后的字符串
示例
:
- 将
src/*.cpp
替换为objs/*.coo
cpp_srcs := ${shell find src -name *.cpp}
cpp_objs := ${subst src/,objs/,${cpp_srcs}}
debug :
@echo ${cpp_objs}
.PHONY : debug
-
可以看到输出所有
.cpp
文件路径中的src/
字符串全部替换objs/
-
同时将得到的
objs/*.cpp
,替换为objs/*.o
, 实现如下:
cpp_srcs := ${shell find src -name *.cpp}
cpp_objs := ${subst src/,objs/,${cpp_srcs}}
cpp_objs := ${subst .cpp,.o,${cpp_objs}}
debug :
@echo ${cpp_srcs}
@echo ${cpp_objs}
.PHONY : debug
- 可以看到成功将
objs/
下的.cpp
后缀替换为.o
后缀
通过subst
可以帮我们批量操作字符串,非常方便
(3) patsubst 函数
patsubst
可以按照一定的模式
对字符串进行替换
语法
${patsubst <pattern>,<replacement>,<text>}
- 名称:模式字符串替换函数——patsubst
- 功能: 通配符%,表示任意的字符串,从text中取出
pattern
,替换成replacement
- 返回:返回被替换后的字符串
示例
cpp_srcs := ${shell find src -name *.cpp}
# cpp_objs := ${subst src/,objs/,${cpp_srcs}}
# cpp_objs := ${subst .cpp,.o,${cpp_objs}}
cpp_objs := ${patsubst src/%.cpp,objs/%.o,${cpp_srcs}}
debug :
@echo ${cpp_srcs}
@echo ${cpp_objs}
.PHONY : debug
这样的话,一行就完成了之前两行才能完成替换的任务,src/%.cpp
中的%
表示src/
与.cpp
中的任意字符串
。通过patsubst
可以极大的简化我们的工作。
(4) foreach函数
foreach
函数非常有用,尤其当我们编译大型项目时。
语法
:
${foreach <var>,<list>,<text>}
- 名称: 循环函数——foreach
- 功能: 把
<list>
中的元素逐一取出来,执行<text>
包含的表达式
示例
:
- 如果项目需要依赖其他库的话,就需要使用
Include
添加头文件的搜索路径。 - 并在编译时,将每个搜索路径加上
-I
- 对于路径只有2,3个,手动添加是ok的,如果Include 包含路径很多的话,为了方便建议使用
foreach
来遍历进行添加-I
选项
include_paths := /usr/include \
/usr/include/opencv2/core
include_paths := ${foreach item,${include_paths},-I${item}}
debug :
@echo ${include_paths}
.PHONY : debug
-
可以看到
Include
的每个搜索路径都加上了-I
选项, 这样就非常遍历。 同理libs
也可以利用foreach
加上-l
选项。 -
这里介绍一种更为简便的方法,同样实现对每个
include
搜索路径加上-I
选项, 完整代码如下:
include_paths := /usr/include \
/usr/include/opencv2/core
#include_paths := ${foreach item,${include_paths},-I${item}}
I_flag := ${include_paths:%=-I%}
debug :
@echo ${I_flag}
.PHONY : debug
可以看到和foreach遍历添加,实现的效果是一样的
(5) dir函数
dir
: 取目录函数
- 功能:从文件名中取出
目录部分
,
语法
:
${dir <names ...>}
示例
:
实现将src下面的所有.cpp文件,编译为对应的.o
文件,并存放在objs/
目录下:
cpp_srcs := ${shell find src -name *.cpp}
cpp_objs :=${patsubst src/%.cpp,objs/%.o,${cpp_srcs}}
objs/%.o : src/%.cpp
@g++ -c $^ -o $@
compile : ${cpp_objs}
debug:
@echo ${cpp_srcs}
@echo ${cpp_objs}
.PHONY : debug compile
假设此时没有创建objs
目录,由于编译的时候会编译objs
目录下面的文件,当前没有objs
目录的话,就会报错,如下:
此时可以利用mkdir
来创建目录,注意加上-p
选项,当目录存在时,也不会报错。利用dir
函数,取文件对应的目录。其中$@
代表目标文件:objs/%.o
,它的目录就是objs/
。
@mkdir -p $(dir $@)
这样先创建好了文件夹,再去编译就不会报错了,完整代码如下:
cpp_srcs := ${shell find src -name *.cpp}
cpp_objs :=${patsubst src/%.cpp,objs/%.o,${cpp_srcs}}
objs/%.o : src/%.cpp
@mkdir -p ${dir $@}
@g++ -c $^ -o $@
compile : ${cpp_objs}
debug:
@echo ${cpp_srcs}
@echo ${cpp_objs}
.PHONY : debug compile
这样就在objs下为每个src中的.cpp文件,创建了对应的.o文件。
(6) notdir 函数
去掉文件
路径中的目录
示例
:
- 找打
/usr/lib
下面的所有库。
libs := ${shell find /usr/lib -name lib*}
debug :
@echo ${libs}
.PHONY : debug
ku
- 利用
notdir
去掉路径
libs := ${notdir $(shell find /usr/lib -name lib*)}
debug :
@echo ${libs}
.PHONY : debug
(7) filter 函数
按要求过滤掉文件
- 找到所有的
静态库
(.a结尾的) - 找到所有的
动态库
(.so结尾的)
libs := ${notdir $(shell find /usr/lib -name lib*)}
a_libs := ${filter %.a,${libs}}
so_libs := ${filter %.so,${libs}}
debug :
@echo ${a_libs}
@echo ${so_libs}
.PHONY : debug
(8) basename函数
- 去掉路径中的后缀, 将
.so
,.a
后缀去掉
libs := ${notdir $(shell find /usr/lib -name lib*)}
a_libs := ${basename ${filter %.a,${libs}}}
so_libs := ${basename ${filter %.so,${libs}}}
debug :
@echo ${a_libs}
@echo ${so_libs}
.PHONY : debug
- 去掉
lib
开头,只保留库名, 将lib替换为空:${subst lib,,${a_libs}}
libs := ${notdir $(shell find /usr/lib -name lib*)}
a_libs := ${subst lib,,${basename ${filter %.a,${libs}}}}
so_libs := ${subst lib,,{basename ${filter %.so,${libs}}}}
debug :
@echo ${a_libs}
@echo ${so_libs}
.PHONY : debug
这样就拿到了干干净净的库名,没有路径,没有后缀,也没有lib前缀