文章目录
一、Make 简介
1.1、简介
- 编译(compile): 代码变成可执行文件,例如
gcc(C)、g++(C++)
- 构建(build):
- 编译众多相互关联的源码文件(且可以只对在上次编译后修改过的部分进行编译),以实现工程化的管理
Make
是最常用的构建工具,通常构建规则写在Makeflie
中(要构建哪个文件、它依赖哪些源文件,当那些文件有变动时,如何重新构建它)- 语法:
make -f target -j16
,其中 target 为编译目标(makefile 文件),-j16
为使用 16 线程并发进行编译- 若 makefile 文件名为标准文件名(Makefile、makefile 或 GNUmakefile 等),则直接执行
make -j16
即可 -n
参数:只显示所要执行的命令,但不会真正的执行,可用于调试-s
参数:禁止所有的执行命令的显示
- make 重新编译的原理:
- 编译文件时 make 检查其目标文件并比较文件的更新时间戳
- 如果源文件比目标文件具有更新的时间戳,则假定源文件已更改,那么 make 会生成新的目标文件,否则不会重新编译
readelf -d libxxx.so
: 查看此动态库所依赖的库文件
1.2、动态库编译示例
.PHONY: clean
# 1、指定需要生成的动态库名称,获取当前工程目录
PRJ_TARGET := libXXX.so
PRJ_ROOT := $(shell pwd)
$(info "------------PRJ_ROOT is:" $(PRJ_ROOT))
# 2、指定交叉编译器
COMPILER_PATH := /opt/cross_compile/Ambarella/cv2/linaro-aarch64-2020.09-gcc10.2-linux5.4/bin/aarch64-linux-gnu-
CC := $(COMPILER_PATH)gcc
CXX := $(COMPILER_PATH)g++
$(info "------------CXX is:" $(CXX))
# 3、指定编译选项
CFLAGS := -DFOR_AMBA_S88 -DFOR_AMBA # 预处理选项,选择相应的芯片
CFLAGS += -O3 -Wall -fPIC # 优化、警告及代码生成选项
CFLAGS += -mcpu=cortex-a53 -fno-aggressive-loop-optimizations -ffast-math -fpermissive # 交叉编译选项
CXXFLAGS := $(CFLAGS) -std=c++11
$(info "------------CXXFLAGS is:" $(CXXFLAGS))
# 4、指定预处理时需要的头文件选项:依赖的头文件路径
INC_DIRS := -I $(PRJ_ROOT)/SDK_DIR/include # SDK 头文件路径
INC_DIRS += -I $(PRJ_ROOT)/SRC_DIR/include # 源文件对应的头文件路径
INC_DIRS += -I $(PRJ_ROOT)/COMMON_DIR/include # 公共头文件路径
INC_DIRS += -I $(PRJ_ROOT)/THIRD_PARTY_DIR/include # 第三方库对应的头文件路径
$(info "------------INC_DIRS is:" $(INC_DIRS))
# 5、指定链接时需要的库操作选项:依赖的库文件及其路径
LDFLAGS := -ldl -lpthread # 系统库:额外需要的动态加载库及多线程库
LDFLAGS += -L $(PRJ_ROOT)/SDK_DIR/lib -lnnctrl -lvproc # SDK 库文件及路径
LDFLAGS += -L $(PRJ_ROOT)/THIRD_PARTY_DIR/lib -lzlog -losp -lyuv -ljsoncpp # 第三方库文件及路径(最好用静态库或者与业务版本保持一致)
$(info "------------LDFLAGS is:" $(LDFLAGS))
# 6、指定需要编译的源文件:SRC_C/SRC_CPP/SRC_CU
SRC_C := $(wildcard $(PRJ_ROOT)/SDK_DIR/source/*.c) # SDK 中需要编译的 C 源文件
SRC_C += $(wildcard $(PRJ_ROOT)/source/*.c) # 项目中需要编译的 C 源文件
SRC_CPP := $(wildcard $(PRJ_ROOT)/SDK_DIR/source/*.cpp) # SDK 中需要编译的 CPP 源文件
SRC_CPP += $(wildcard $(PRJ_ROOT)/source/*.cpp) # 项目中需要编译的 CPP 源文件
# 7、指定需要链接的目标文件
PRJ_OBJS := $(patsubst %.c, %.o, $(SRC_C))
PRJ_OBJS += $(patsubst %.cpp, %.o, $(SRC_CPP))
$(info "------------OBJS is:" $(PRJ_OBJS))
# 8、链接生成动态库, $^ 表示所有依赖项(命令中的 $(PRJ_OBJS) 可以通替换为 $^),$@ 表示当前目标
$(PRJ_TARGET): $(PRJ_OBJS)
$(CXX) -shared $(PRJ_OBJS) -o $@ $(LDFLAGS)
# 9、预处理/编译/汇编生成目标文件,$< 指代第一个前置条件
%.o: %.c
$(CC) -c $< -o $@ $(CFLAGS) $(INC_DIRS)
%.o: %.cpp
$(CXX) -c $< -o $@ $(CXXFLAGS) $(INC_DIRS)
# 10、删除可执行文件及目标文件
clean:
rm -f $(PRJ_OBJS) $(PRJ_TARGET)
1.3、demo 编译示例
- 汇编 demo: -c 源文件,需指定源文件、编译选项及其依赖的头文件,得到目标文件
- 链接 demo: 链接目标文件(.o),需指定目标文件、链接选项,此时不再需要编译选项和头文件
.PHONY: clean
# 1、指定需要生成的动态库名称,获取当前工程目录
PRJ_TARGET := test
MAIN_CPP := test.cpp
PRJ_ROOT := $(shell pwd)
$(info "------------PRJ_ROOT is:" $(PRJ_ROOT))
# 2、指定交叉编译器
COMPILER_PATH := /opt/cross_compile/Ambarella/cv2/linaro-aarch64-2020.09-gcc10.2-linux5.4/bin/aarch64-linux-gnu-
CC = $(COMPILER_PATH)gcc
CXX = $(COMPILER_PATH)g++
$(info "------------CXX is:" $(CXX))
# 3、指定编译选项
CFLAGS := -DFOR_AMBA_S88 -DFOR_AMBA # 预处理选项,选择相应的芯片
CFLAGS += -O3 -Wall -fPIC # 优化、警告及代码生成选项
CFLAGS += -mcpu=cortex-a53 -fno-aggressive-loop-optimizations -ffast-math -fpermissive # 交叉编译选项
CXXFLAGS := $(CFLAGS) -std=c++11
$(info "------------CXXFLAGS is:" $(CXXFLAGS))
# 4、指定预处理时需要的头文件选项:依赖的头文件路径
INC_DIRS := -I $(PRJ_ROOT)/SRC_DIR/include # 对外头文件路径
INC_DIRS += -I $(PRJ_ROOT)/THIRD_PARTY_DIR/include # 第三方库对应的头文件路径,如 opencv
$(info "------------INC_DIRS is:" $(INC_DIRS))
# 5、指定链接时需要的库操作选项:依赖的库文件及其路径
LDFLAGS := -ldl -lpthread # 系统库:额外需要的动态加载库及多线程库
LDFLAGS += -L $(PRJ_ROOT)/SDK_DIR/lib -lnnctrl -lvproc # SDK 库文件及路径
LDFLAGS += -L $(PRJ_ROOT)/THIRD_PARTY_DIR/lib -lyuv -lopencv_world # 第三方库文件及路径(最好用静态库或者与业务版本保持一致)
LDFLAGS += -L $(PRJ_ROOT)/SRC_DIR/lib -lxxx # 对外动态库及路径
$(info "------------LDFLAGS is:" $(LDFLAGS))
# 6、链接生成可执行文件,$< 指代第一个前置条件,$@ 表示当前目标
$(PRJ_TARGET): $(PRJ_TARGET).o # 等价于 $(TARGETS): *.o (当前目录下寻找)和 $(TARGETS): %: %.o(静态模式)
$(CXX) $< -o $@ $(LDFLAGS)
# 7、预处理/编译/汇编生成目标文件 (根据 demo 后缀 c 或者 cpp 处理选择一个就行了),$< 指代第一个前置条件
%.o: %.c
$(CC) -c $< -o $@ $(CFLAGS) $(INC_DIRS)
%.o: %.cpp
$(CXX) -c $< -o $@ $(CXXFLAGS) $(INC_DIRS)
# 8、删除可执行文件及目标文件
clean:
rm -f $(PRJ_TARGET).o $(PRJ_TARGET)
二、Makefile 文件的格式
2.1、概述
- Makefile 文件由一系列规则构成,每条规则的形式如下:
- 第一行:冒号前面的部分,叫做目标,冒号后面的部分叫做前置条件
- 第二行:必须由一个
tab
键开始(可以用 Notepad++ 空格转 Tab),后面跟着需要执行的命令 - 目标是必需的,不可省略,前置条件和命令是可选的,但是两者之中必须至少存在一个
# 每条规则的格式,井号(#)在 Makefile 中表示注释
<target>: <prerequisites>
[tab] <commands>
# 依赖文件 prerequisite 的搜索路径,可以通过 VPATH 指定,它是 make 的一个特殊变量
# VPATH = src:../headers, 不同路径目录用冒号:隔开
# 如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件
# 如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件
# eg:
c.txt: a.txt b.txt
cat a.txt b.txt > c.txt
# 静态模式:是一种自动编译模式,在这种模式下,我们可以容易的定义“多目标”规则,让我们的规则变得更加有弹性和灵活
<targets ...> : <target-pattern> : <prereq-patterns ...>
[tab] <commands>
# targets:定义了一些列的目标文件,也就是多目标,可以有通配符,是目标的一个集合。
# target-pattern:targets 的模式,也就是目标集模式
# prereq-patterns:目标的依赖模式,它根据目标集模式获取目标文件名(不带后缀),然后结合依赖模式得到最终的依赖目标文件名
# 注意:我们的 “目标模式” 或是 “依赖模式” 中都应该有 % 这个字符
# 静态模式举例: 遍历所有的 .c 文件,对所有的 .c 文件进行编译,然后编译生成对应的 .o 文件
$(OBJS): %.o: %.c # % 代表取去掉 .o 后的文件名
gcc -c $< -o $@
# OBJS:这个 OBJS 就是各种 .o 文件的集合
# %.o:目标集模式,表示我们的 target 集合中都是以 .o 结尾的
# %.c:对 target-parrtern 所形成的目标集进行二次定义,取<target-parrtern>模式中的 % (也就是去掉了 .o 这个结尾)
# 并为其加上 .c 这个结尾,形成的新集合,而我们在实际编写程序时,targets 是不需要的,可以简写如下:
%.o: %.c
gcc -c $< -o $@
$(OBJ): %: %.o
# 第一个 % 代表目标集模式:OBJ 中的所有文件都不带后缀
# 第二个 %.o 根据目标集模式获取目标文件名(不带后缀),然后结合依赖模式得到最终的依赖目标文件名
2.2、目标(target)
- 目标: 一个目标就构成一条规则,目标通常是需要构建对象的文件名(可以是多个)
- 伪目标: 可以不是文件名,而是某个操作的名字(它并不会创建目标文件,只是想去执行这个目标下面的命令)
# 伪目标:删除对象文件,常见的有 all、install、clean
.PHONY: clean # 告诉 make 这个不是真正意义上的 target,make 不需要自动生成依赖关系和推导规则
clean:
rm *.o
# 终端执行命令
make -f makefile clean
# 常见的伪目标命令
# make all: It compiles everything so that you can do local testing before installing applications.
# make install:It installs applications at right places.
# make clean:It cleans applications, gets rid of the executables, any temporary files, object files, etc
2.3、前置条件(prerequisites)
- 前置条件通常是一组文件名,之间用
空格分隔
;若目标后面没有前置条件,意味着它跟其他文件都无关 - 前置条件指定了
目标
是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新,目标就需要重新构建
2.4、命令(commands)
- 构建目标的具体指令,它的运行结果通常就是
生成目标文件
- 指令:由
一行或多行的 Shell 命令
组成,每行命令在一个单独的 shell 中执行(不同的进程中执行
) - Note:每行命令之前必须有一个
tab
键;命令的前面加上@
,可以关闭屏幕打印当前命令或注释
# 取不到 foo 的值,因为两行命令在两个不同的进程执行
var-lost:
export foo=bar
echo "foo=[$$foo]"
# 解决办法 1:两行命令写在一行,中间用分号分隔
var-kept:
export foo=bar; echo "foo=[$$foo]"
# 解决办法 2:加换行符
var-kept:
export foo=bar; \
echo "foo=[$$foo]"
# 解决办法 3:加 .ONESHELL: 命令
.ONESHELL:
var-kept:
export foo=bar;
echo "foo=[$$foo]"
# 关闭屏幕命令输出
all:
@# 这是测试
@echo TODO
三、Makefile 文件的语法
3.1、通配符和模式匹配
- Makefile 可以使用 shell 命令,所有 shell 支持的通配符在 Makefile 中也是同样适用的
- Makefile 的通配符与 Bash Shell 一致,主要有星号
*
(匹配0个或者是任意个字符)、问号?
(匹配任意一个字符)和中括号[]
(需要匹配的字符放在中括号里),eg:*.o
表示所有后缀名为 o 的文件 - Make 命令允许对文件名进行类似正则运算的匹配,主要用到的匹配符是
%
(取出文件的文件名,但不包含后缀)
# 假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件
%.o: %.c
# 等同于下面的写法,使用匹配符 %,可以将大量同类型的文件,只用一条规则就完成构建
f1.o: f1.c
f2.o: f2.c
3.2、变量赋值运算符
(1)递归赋值/延迟赋值(=)
# 在执行时扩展,允许递归扩展,可能影响多个变量,所有目标变量相关的其他变量都受影响
VARIABLE = value # (可以是多个值,以空格分隔,value1 value2 value3 ...)
# eg:
x = foo # 在执行时扩展
y = $(x)b # 在执行时扩展
x = new # 在执行时扩展
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
# 执行 make test 后,输出如下:
y=>newb
x=>new
(2)立即赋值(:=)
# 在定义时扩展,只对当前语句的变量有效
VARIABLE := value
# eg:
x := foo # 在定义时扩展
y := $(x)b # 在定义时扩展
x := new # 在定义时扩展
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
# 执行 make test 后,输出如下:
y=>foob
x=>new
(3)追加赋值(+=)
# 将值追加到变量的尾端(用空格隔开)
VARIABLE += value
x := foo
y := $(x)b
x += $(y)
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
# 执行 make test 后,输出如下:
y=>foob
x=>foo foob
(4)条件赋值(?=)
# 只有在该变量为空时才设置值
VARIABLE ?= value
x := foo
y := $(x)b
x ?= new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
# 执行 make test 后,输出如下:
y=>foob
x=>foo
3.3、宏定义(变量替换)
宏在 Makefile 中使用
名称 = 值
的形式来定义
a、特殊的宏
$@
:make 命令当前构建的那个目标,即每条规则中 target 的名字$?
:the names of the changed dependents,代表前置条件中所有已修改的源文件$<
:the name of the related file that caused the action,通常指代第一个前置条件,比如,规则为 t: p1 p2,那么$<
就指代p1$*
:the prefix shared by target and dependent files,通常指代匹配符%
匹配的部分, 比如%
匹配 f1.txt 中的 f1 ,$*
就表示 f1$^
:指代所有前置条件,比如,规则为t: p1 p2
,那么$^
就指代p1 p2
# $@ represents hello and $? or $@.cpp picks up all the changed source files
hello: main.cpp hello.cpp factorial.cpp
$(CC) $(CFLAGS) $? $(LDFLAGS) -o $@
# 等同于下面的写法
hello: main.cpp hello.cpp factorial.cpp
$(CC) $(CFLAGS) $@.cpp $(LDFLAGS) -o $@
# $< 的用法
a.txt: b.txt c.txt
cp $< $@
# 等同于下面的写法
a.txt: b.txt c.txt
cp b.txt a.txt
b、常规宏
CC
:编译 C 语言文件的程序,默认是cc
CXX
:编译 C++ 文件的程序,默认是g++
CPP
:运行 C 预处理器并输出到当前输出流的程序,默认是$(CC) -E
PWD
:获取当前路径LD_LIBRARY_PATH
:是 Linux 环境变量名,该环境变量主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径- 临时修改:
export LD_LIBRARY_PATH=/usr/local/cuda-9.0/lib64:$LD_LIBRARY_PATH
- 永久生效:可用
vim /etc/profile
,修改其中的LD_LIBRARY_PATH
变量 LIBRARY_PATH
is used by gcc before compilation to search for directories containing libraries that need to be linked to your programLD_LIBRARY_PATH
is used by your program to search for directories containing the libraries after it has been successfully compiled and linked
- 临时修改:
- 常见宏变量的附加参数:
CFLAGS
:传递给 C 编译器的额外 flagCXXFLAGS
:传递给 C++ 编译器的额外 flagLDFLAGS
:Extra flags to give to compilers when they are supposed to invoke the linker,ld
CPPFLAGS
:Extra flags to give to the C preprocessor and programs, which use it
3.4、Makefile 条件判断
# 1、条件判断关键字
- ifeq:判断参数是否不相等,相等为 true,不相等为 false。
- ifneq:判断参数是否不相等,不相等为 true,相等为 false。
- ifdef:判断是否有值,有值为 true,没有值为 false。
- ifndef:判断是否有值,没有值为 true,有值为 false。
- endif:结束判断
# 2、条件判断使用方式
ifeq (ARG1, ARG2) or ifeq 'ARG1' 'ARG2' or ifeq "ARG1" "ARG2"
ifdef VARIABLE_NAME
# 3、条件判断使用示例
ifeq ($(TARGET_ARCH_ABI), arm64-v8a)
ifeq ($(APP_STL), c++_shared)
SNPE_LIB_DIR := $(SNPE_ROOT)/lib/aarch64-android-clang6.0
else
$(error Unsupported APP_STL: '$(APP_STL)')
endif
else ifeq ($(TARGET_ARCH_ABI), armeabi-v7a)
ifeq ($(APP_STL), c++_shared)
SNPE_LIB_DIR := $(SNPE_ROOT)/lib/arm-android-clang6.0
else
$(error Unsupported APP_STL: '$(APP_STL)')
endif
else
$(error Unsupported TARGET_ARCH_ABI: '$(TARGET_ARCH_ABI)')
endif
3.5、Makefile 添加打印
# 直接添加方式
$(warning "this is test========================")
$(info $(KBUILD_CFLAGS))
$(error $(KBUILD_CFLAGS))
# 使用 echo 命令打印,执行 make test 即可打印 echo 内容
test:
@echo $(KBUILD_CFLAGS)
四、Makefile 常用函数
wildcard
:扩展通配符函数,src=$(wildcard *.c)
,匹配所有.c
文件,列表赋值给src
,保留了路径patsubst
:替换通配符函数,obj=$(patsubst %.c, %.o, $(wildcard *.c))
,将最后一个参数扩展后的.c
文件替换成.o
notdir
:取文件名函数(去掉目标的路径),filename=$(notdir $(wildcard ./sub/*.c))
,去除掉./sub/
路径,只保留源代码名称dir
:取文件目录函数(去掉文件名),dir=$(dir src/foo.c hacks)
,去除掉源代码名称,只保留目录,得到src/ ./
- 其它函数:去空格函数
strip
、过滤函数filter
、反过滤函数filter-out
、排序函数sort
、取后缀函数suffix
、取前缀函数basename
src=$(wildcard *.c ./sub/*.c) # 使用 wildcard 函数后,引用变量时,会自动帮我们展开
filename=$(notdir $(src))
obj=$(patsubst %.c, %.o, $(filename))
all:
@echo $(src) # 注意 TAB 键,@echo 和 echo 的区别,前者会隐藏命令,后者会输出命令
tdir:
@echo $(filename)
tobj:
@echo $(obj)
五、参考资料
1、Make 命令教程
2、Makefile 基础教程
3、跟我一起写Makefile
4、Makefile中的 patsubst 和 wildcard 函数