Makefile
2024/10/3大约 13 分钟
Makefile
- GNU Make 官方网站:https://www.gnu.org/software/make/
- GNU Make 官方文档下载地址:https://www.gnu.org/software/make/manual/
- Makefile Tutorial:https://makefiletutorial.com/
- 写给初学者的 Makefile 指南:https://zhuanlan.zhihu.com/p/618350718
- 此md文件是对 Makefile 教程的改编扩充:https://github.com/WohimLee/GNC-Tutorial
- 编译器部分见GCC/G++编译器使用
1. 介绍
1.0 make
make只是一个根据指定的Shell命令进行构建的工具。它的规则很简单,你规定要构建哪个文件、它依赖哪些源文件,当那些文件有变动时,如何重新构建它
- 可以简单的使用make(将执行第一个目标)或者在make命令后带上目标名比如all
- 使用
-B选项让所有目标总是重新建立 - 使用
-d选项打印调试信息 - 使用
-C选项改变目录 - 使用
-f选项将其它文件看作 Makefile - 使用
-p选项打印出所有变量和规则
实现递归执行make:
${make}是官方定义的变量,用于得到执行make操作的make工具绝对路径,使用make命令# 参数将传递给Makefile文件 $(MAKE) -C 文件路径(默认为.) [参数]
1.1 Makefile 命令基本格式
targets : prerequisties
[tab键]command如果是vscode等编写需要调整默认的缩进格式,点击界面右下角选择缩进修改为制表符4缩进
- target:目标文件,可以是 OjectFile,也可以是执行文件,还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
- prerequisite:要生成那个 target 所需要的文件或是目标。
- command:是 make 需要执行的命令,可以调用
1.2 Makefile 规则
- make 会在当前目录下找到一个名字叫
Makefile或makefile的文件 - 如果找到,它会找文件中第一个目标文件(target),并把这个文件作为最终的目标文件
- 如果 target 文件不存在,或是 target 文件依赖的 .o 文件(prerequities)的文件修改时间要比 target 这个文件新,就会执行后面所定义的命令 command 来生成 target 这个文件
- 如果 target 依赖的 .o 文件(prerequisties)也存在,make 会在当前文件中找到 target 为 .o 文件的依赖性,如果找到,再根据那个规则生成 .o 文件
1.3 伪目标
- 为了避免 target 和 Makefile 同级目录下
文件/文件夹重名,文件未更新无法使用的情况,我们可以使用一个特殊的标记.PHONY来显式地指明一个目标是 "伪目标",向make说明,不管是否有这个文件/文件夹,是否未更新都直接执行,这个目标就是 "伪目标"
.PHONY : clean- 只要有这个声明,不管是否有 "clean" 文件/文件夹,都运行 "clean" 这个目标
1.4 引入mk文件
- 除了
Makefile文件还可以创建其他的.mk结尾的文件,他们同样使用Makefile语法,主要用于重用,在对应的Makefile文件中使用include引入
include sub.mk
# 希望引入失败不报错,在部分make中使用sinclude才行
-include sub.mk
# 之后可以这样检查是否引入成功
ifeq($(wildcard sub.mk),)2. 变量
- 变量在声明时需要给予初值,而在使用时,需要给在变量名前加上
$符号,并用括号()/{}把变量给包括起来
2.1 变量的定义
cpp := src/main.cpp
obj := objs/main.o2.2 变量的引用
- 可以用
()或{}
cpp := src/main.cpp
obj := objs/main.o
$(obj) : ${cpp}
@g++ -c $(cpp) -o $(obj)
compile : $(obj)2.3 预定义变量
$@: 目标(target)的完整名称$<: 第一个依赖文件(prerequisties)的名称$^: 所有的依赖文件(prerequisties),以空格分开,不包含重复的依赖文件$?: 代表所有比目标更新的依赖文件(prerequisties)名称$*: 代表目标(target)文件名中去掉后缀的部分
cpp := src/main.cpp
obj := objs/main.o
$(obj) : ${cpp}
@g++ -c $< -o $@
@echo $^
compile : $(obj)
.PHONY : compile2.4 命令变量
- 在
Makefile中,你可以定义和使用类似函数的东西,称为 "命令序列" 或 "命令块" - 虽然没有传统编程语言中的函数和参数传递机制,但你可以通过定义命令块并使用
call函数执行和传递参数 - 命令前可以使用制表符或空格
- 可以使用
@$(foreach item, $(ARRAY), $(call compile_command,${item}))批量执行 - 对命令块的错误查找不完善,命令块中的错误会导致目标行或使用命令块的附近行报错,可以尝试先删除某些行定位
- 允许配合
$(eval makefile命令)和命令行输出进行赋值,但其他输出可能会影响赋值,$(eval)相当于直接调用对应的Makefile命令内容,这样就可以动态执行命令块
# 定义多行命令序列
define compile_command
$(CC) $(CFLAGS) -c $< -o $@
endef
# 定义命令块函数,使用call执行命令
define compile_command
@echo "Compiling $(1)..."
$(CC) $(CFLAGS) -c $(1) -o $(2)
endef
$(call compile_command,$<,$@)1.5 传递变量
直接将变量作为参数传递
你可以在命令行调用
make时或调用下层的make时直接将变量作为参数传递。比如:将value传递给son目录调用的变量将不会进行下层计算,比如:
=、:=、+=、?=# 定义变量 value = 1 # 在下层的`Makefile`中,可以直接使用`$(value)`来访问这个变量 all: @echo "Calling make with value" $(MAKE) -C ta value="$(value)"
使用
export关键字也可以使用
export关键字来导出变量,使得下层的make能够直接访问它们。例如:在下层的Makefile中,你可以直接使用$(value)来访问这个变量# 定义变量并导出,在下层的`Makefile`中,可以直接使用`$(value)`来访问这个变量 export value = 1 all: @echo "Calling make with value" $(MAKE) -C ta
通过环境变量传递
如果你希望将变量作为环境变量传递,可以在调用
make时直接定义它们。例如:# 将`value`作为环境变量传递给下层的`make`,下层的可以通过`$(value)`访问 all: @echo "Calling make with value" value="$(value)" $(MAKE) -C ta
2.6 通过命令赋值
- 允许通过
$(eval ARRAY += $(item))的方式在执行命令中途改变变量值,如果是希望修改数值可以使用$(shell echo $(a) + 1 | bc)
3. Makefile 常用符号
3.1 =
- 简单的赋值运算符
- 用于将右边的值分配给左边的变量
- 变量的值是整个makefile中最后被指定的值
示例
HOST_ARCH = aarch64
TARGET_ARCH = $(HOST_ARCH)
# 更改了变量 a
HOST_ARCH = amd64
debug:
@echo $(TARGET_ARCH)3.2 :=
- 立即赋值运算符
- 用于在定义变量时立即求值
- 赋予当前位置时对应的值
示例
HOST_ARCH := aarch64
TARGET_ARCH := $(HOST_ARCH)
# 更改了变量 a
HOST_ARCH := amd64
debug:
@echo $(TARGET_ARCH)3.3 ?=
- 默认赋值运算符
- 如果该变量已经定义,则不进行任何操作
- 如果该变量尚未定义,则求值并分配
HOST_ARCH = aarch64
HOST_ARCH ?= amd64
debug:
@echo $(HOST_ARCH)3.4 累加 +=
CXXFLAGS := -m64 -fPIC -g -O0 -std=c++11 -w -fopenmp
CXXFLAGS += $(include_paths)3.5 \
- 续行符
示例
LDLIBS := cudart opencv_core \
gomp nvinfer protobuf cudnn pthread \
cublas nvcaffe_parser nvinfer_plugin3.6 * 与 %
*: 通配符表示匹配任意字符串,可以用在目录名或文件名中%: 通配符表示匹配任意字符串,并将匹配到的字符串作为变量使用
# %.o 的作用是匹配所有以 .o 结尾的目标
# 而后面的 %.c 中 % 的作用,则是将 %.o 中 % 的内容拿过来用
%.o : %.c
gcc -c $(INCS) $< -o $@4. Makefile 的常用函数
函数调用,很像变量的使用,也是以 “$” 来标识的,其语法如下:
$(fn, arguments) or ${fn, arguments}- fn: 函数名
- arguments: 函数参数,参数间以逗号
,分隔,而函数名和参数之间以“空格”分隔
4.1 shell
$(shell <command> <arguments>)- 名称:shell 命令函数 —— shell
- 功能:调用 shell 命令 command
- 返回:函数返回 shell 命令 command 的执行结果
示例:
# shell 指令,src 文件夹下找到 .cpp 文件
cpp_srcs := $(shell find src -name "*.cpp")
# shell 指令, 获取计算机架构
HOST_ARCH := $(shell uname -m)4.2 subst
$(subst <from>,<to>,<text>)- 名称:字符串替换函数——subst
- 功能:把字串 <text> 中的 <from> 字符串替换成 <to>
- 返回:函数返回被替换过后的字符串
示例:
cpp_srcs := $(shell find src -name "*.cpp")
cpp_objs := $(subst src/,objs/,$(cpp_srcs))4.3 patsubst
$(patsubst <pattern>,<replacement>,<text>)- 名称:模式字符串替换函数 —— patsubst
- 功能:通配符
%,表示任意长度的字串,从 text 中取出 patttern, 替换成 replacement - 返回:函数返回被替换过后的字符串
示例:
cpp_srcs := $(shell find src -name "*.cpp") #shell指令,src文件夹下找到.cpp文件
cpp_objs := $(patsubst %.cpp,%.o,$(cpp_srcs)) #cpp_srcs变量下cpp文件替换成 .o文件4.4 foreach
$(foreach <var>,<list>,<text>)- 名称:循环函数——foreach。
- 功能:把字串<list>中的元素逐一取出来,执行<text>包含的表达式
- 返回:<text>所返回的每个字符串所组成的整个字符串(以空格分隔)
示例:
library_paths := /datav/shared/100_du/03.08/lean/protobuf-3.11.4/lib \
/usr/local/cuda-10.1/lib64
library_paths := $(foreach item,$(library_paths),-L$(item))同等效果
# 相当于foreach和subst/patsubst的结合
# subst的例子, 用 .o 替换 .c, $(SRCS:.c=.o)
I_flag := $(include_paths:%=-I%)4.5 dir
$(dir <names...>)- 名称:取目录函数——dir。
- 功能:从文件名序列names中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前 的部分。如果没有反斜杠,那么返回“./”。
- 返回:返回文件名序列names的目录部分。
示例:
$(dir src/foo.c hacks) # 返回值是“src/ ./”。4.6 notdir
$(notdir <names...>)- 用于从给定的路径中提取文件名部分
示例:
libs := $(notdir $(shell find /usr/lib -name lib*))4.7 filter
$(filter <names...>)- 用于从一个字符串列表中筛选出符合指定条件的结果
示例:
libs := $(notdir $(shell find /usr/lib -name lib*))
a_libs := $(filter %.a,$(libs))
so_libs := $(filter %.so,$(libs))4.8 basename
$(basename <names...>)- 名称:取前缀函数——basename。
- 语法:$(basename <names...>)
- 功能:从文件名序列 <names> 中取出各个文件名的前缀部分。
- 返回:返回文件名序列 <names> 的前缀序列,如果文件没有前缀,则返回空字串。
示例:
libs := $(notdir $(shell find /usr/lib -name lib*))
a_libs := $(subst lib,,$(basename $(filter %.a,$(libs))))
so_libs := $(subst lib,,$(basename $(filter %.so,$(libs))))4.9 filter-out
- 剔除不想要的字符串
示例:
objs := objs/add.o objs/minus.o objs/main.o
cpp_objs := $(filter-out objs/main.o, $(objs))4.10 wildcard
- 让通配符自动展开成字符串列表
示例:
cpp_srcs := $(wildcard src/*.cc src/*.cpp src/*.c)
# 如果路径中没有匹配文件,返回空字符串4.11 if
- 判断某个变量是否有定义
- 可以结合变量实现对循环开头元素的额外操作
$(if $(val), echo $(val), echo undefined)5. 条件规则
注意:
- 不在target中的ondition 语句里面全部不能用 Tab 缩进, 你看到的 Makefile 如果好像有 "Tab", 那全部是空格
- 使用 Tab 会报错:*** commands commence before first target
ifeq ($(DEBUG), 1)
# 使用空格缩进
common+=-DDEBUG
endif
all:
mkdir -p exec
ifeq ($(NEEDOBJ),1)
# 作为all命令的一部分,tab缩进
mkdir -p obj
endif5.1 ifeq / else / endif
# build flags
ifeq ($(TARGET_OS),darwin)
LDFLAGS += -rpath $(CUDA_PATH)/lib
CCFLAGS += -arch $(HOST_ARCH)
else ifeq ($(HOST_ARCH)-$(TARGET_ARCH)-$(TARGET_OS),x86_64-armv7l-linux)
LDFLAGS += --dynamic-linker=/lib/ld-linux-armhf.so.3
CCFLAGS += -mfloat-abi=hard
else ifeq ($(TARGET_OS),android)
LDFLAGS += -pie
CCFLAGS += -fpie -fpic -fexceptions
endif5.2 ifneq / else / endif
HOST_ARCH := $(shell uname -m)
TARGET_ARCH ?= $(HOST_ARCH)
temp := $(filter $(TARGET_ARCH),x86_64 aarch64 sbsa ppc64le armv7l)
ifneq (,$(filter $(TARGET_ARCH),x86_64 aarch64 sbsa ppc64le armv7l))
ifneq ($(TARGET_ARCH),$(HOST_ARCH))
ifneq (,$(filter $(TARGET_ARCH),x86_64 aarch64 sbsa ppc64le))
TARGET_SIZE := 64
else ifneq (,$(filter $(TARGET_ARCH),armv7l))
TARGET_SIZE := 32
endif
else
TARGET_SIZE := $(shell getconf LONG_BIT)
endif
else
$(error ERROR - unsupported value $(TARGET_ARCH) for TARGET_ARCH!)
endif5.3 ifdef / else / endif
ifdef TARGET_OVERRIDE # cuda toolkit targets override
NVCCFLAGS += -target-dir $(TARGET_OVERRIDE)
endif5.4 关于循环
注:循环在Makefile中基本上用不上,在这里仅做基本的用法整理
# 可以利用换行符优化排版
# 其中命令无法使用@取消消息显示,本身消息可以通过@for取消
# 不能与定义的命令变量一起使用,无法识别$(call)方法
# 遍历变量
for item in $(value_list); do echo $$item; echo endl; done
@$(foreach x, $(list),echo '===> ' $x;)
# 遍历指定次数
# 测试失败,值不变
couter:=0
@while [ $(counter) -lt 5 ]; do \
echo "Counter: $(counter)"; \
counter=$$(expr $(counter) + 1); \
done- 另外:使用循环时,比如
@$(foreach item, $(items), $(echo $(item));),但是千万记得添加;,不然make会将多条命令拼接在一起再执行,命令结果肯定是不对的 - 而且要注意如果是目标中的指令,出现在这里的最终必须是命令行的命令,而不是变量
6. 编译
6.1 编译过程
6.1.1 预处理
示例
cpp_srcs := $(shell find src -name "*.cpp")
pp_files := $(patsubst src/%.cpp,src/%.i,$(cpp_srcs))
src/%.i : src/%.cpp
@g++ -E $^ -o $@
preprocess : $(pp_files)
clean :
@rm -f src/*.i
debug :
@echo $(pp_files)
.PHONY : debug preprocess clean6.1.2 编译成汇编语言
示例
cpp_srcs := $(shell find src -name "*.cpp")
as_files := $(patsubst src/%.cpp,src/%.s,$(cpp_srcs))
src/%.s : src/%.cpp
@g++ -S $^ -o $@
assemble : $(as_files)
clean :
@rm -f src/*.s
debug :
@echo $(as_files)
.PHONY : debug assemble clean6.1.3 编译成目标文件
示例
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 $@
objects : $(cpp_objs)
clean :
@rm -rf objs src/*.o
debug :
@echo $(as_files)
.PHONY : debug objects clean6.1.4 链接可执行文件
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 $@
workspace/exec : $(cpp_objs)
@mkdir workspace/exec
@g++ $^ -o $@
run : workspace
@./$<
clean :
@rm -rf objs workspace/exec
debug :
@echo $(as_files)
.PHONY : debug run clean6.2 编译选项
6.3 命名隐含规则
- CC:用于编译C程序的程序;默认cc
- CXX:编译c++程序的程序;默认g++
- CFLAGS:给C编译器的额外标志
- CXXFLAGS:给c++编译器的额外标志
- CPPFLAGS:给C预处理器的额外标志
- LDFLAGS:当编译器应该调用链接器时,给编译器的额外标志
6.4 编译带头文件的程序
add.hpp
#ifndef ADD_HPP
#define ADD_HPP
int add(int a, int b);
#endif // ADD_HPPadd.cpp
int add(int a, int b)
{
return a+b;
}minus.hpp
#ifndef MINUS_HPP
#define MINUS_HPP
int minus(int a, int b);
#endif // MINUS_HPPminus.cpp
int minus(int a, int b)
{
return a-b;
}main.cpp
#include <stdio.h>
#include "add.hpp"
#include "minus.hpp"
int main()
{
int a=10; int b=5;
int res = add(a, b);
printf("a + b = %d\n", res);
res = minus(a, b);
printf("a - b = %d\n", res);
return 0;
}Makefile
cpp_srcs := $(shell find src -name "*.cpp")
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))
# 你的头文件所在文件夹路径(如果是安装的包中文件,建议绝对路径)
# gcc或g++默认查找的标准库文件不用定义
# 默认会查找的路径通过 echo | gcc -E -x c - -v 查看
include_paths := include/
I_flag := $(include_paths:%=-I%)
# 一般情况下使用 -MMD 生成.d依赖文件,并引入
# 这样修改了.h文件,就可以识别到并更新相关文件
# .d 文件中的内容类似 src/add.cpp: src/add.h src/minus.h
CFLAGS = -Wall -O2 -MMD -MP
# 直接引用 .d 文件的内容
-include $(cpp_objs:.o=.d)
objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@ $(I_flag) $(CFLAGS)
workspace/exec : $(cpp_objs)
@mkdir -p $(dir $@)
@g++ $^ -o $@
run : workspace/exec
@./$<
debug :
@echo $(I_flag) $(CFLAGS)
clean :
@rm -rf objs
.PHONY : debug run