文章目录
1. 前言
Makefile 是用于管理项目构建过程的工具,广泛用于 C/C++ 等语言的编译。它通过定义规则和指令,自动化编译、链接等步骤,大大简化了开发者的工作。并且即使是一些其他的编译构建工具,最终也是以生成makefile为目的,比如cmake,所以了解Makefile的一些语法,对于项目构建非常重要。通过makefile定义的规则,最后通过make指令来执行。
make 是工作原理是:读取Makefile中所定义的规则,通过规则来递归遍历所有的依赖以及执行指令,最终完成整个工程的构建。
如果我们想使用并行编译,则可以在make后面加上-j
的参数,来指定启动的线程数,比如make -j8
表示启动8个线程进行编译,编译效率取决与你起的线程数以及cpu的内核数量。
2. Makefile 的基础语法
2.1 基础语法
Makefile的基础语法规则如下:
target: dependencies
command
- target(目标): 可以是一个目标文件或者一个动作名称
- dependencies: 生成目标所依赖的文件或者其他目标
- command: 构建目标的命令
示例:
main.o: main.cpp
g++ -c $^ -o $@
上面用例的含义是 g++ -c main.cpp -o main.o 来生成 main.o
2.2. 伪目标
伪目标(phony targets)并不是文件,而是一种命令名称,可以用于执行一些常见的操作,如清理构建文件。它们通常定义为:
.PHONY: clean
clean:
rm -f *.o
2.3 默认目标
当make不带目标时,表示编译生成默认目标
all: $(TARGET)
3. 变量
3.1 内置变量
上面用例其实我们使用了一些内置变量,比如$@, $^, 它是什么含义呢?
- $@:表示目标文件
- $^:表示所有的依赖文件
- $<:表示第一个依赖文件
- AR : 归档维护程序的名称,默认值为 ar,构建静态库时会使用
- CC : C编译器的名称,默认值为 cc
- CXX : C++ 编译器的名称,默认值为 g++
3.2 自定义变量
Makefile支持自定义自己想要的变量,可以根据自己的任务来定义变量明,比如:
SRC = main.cpp
上面的意思表示SRC变量就是main.cpp这个字符串,我们可以使用${SRC}
或者$(SRC)
来获取变量的值
变量还支持一些加法+
, +=
操作, 比如:
SRC += test.cpp
表示SRC新增一个test.cpp字符串,此时SRC = main.cpp test.cpp
,也可以写成下面形式
SRC = ${SRC} test.cpp
4. 模式匹配
上面可以看到,我们一个target,只能处理这一个目标的问题,而在现实工程中,往往是一个比较大的工程,那么就涉及到很多的文件与目录,Makefile支持通配符,使用通配符来进行模式匹配就可以大大简化我们的操作。常见的通配符
%.o: %.c
$(CC) -c $< -o $@
%.o: %.cpp
$(CXX) -c $< -o $@
我们可以看匹配规则有:
- %.o: %.c:
%
表示任意文件前缀,那么这条规则就是匹配任务后缀是.c
的文件,将.c
文件编译成同名的.o
文件. 比如 匹配到main.c,则是表示main.o: main.cpp
的编译规则 - %.o: %.cpp: 跟上一条规则类似,是将
.cpp
文件编译成同名的.o
文件
有时我们希望将特殊的文件编译的.o收集起来,以便于更好地管理,此时会用到以下的表达式:
LIB_SRC = library.cpp version.cpp
LIB_OBJS = $(LIB_SRC:.cpp=.o)
上面的表达式表示将LIB_SRC
的.cpp
文件后缀名全部替换成.o
, 并声明一个变量${LIB_OBJS}
来表示所有替换后的.o
文件
5. 函数
Makefile函数有很多,本节重点将一些常用函数
5.1 文件处理函数
1. wildcard ---- 目录遍历函数
$(wildcard PATTERN...)
- 功能:获取指定目录下指定类型的文件列表
- 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
- 返回: 得到的若干个文件的文件列表,文件名之间使用空格间隔
示例:
SRC=${wildcard src/*.cpp}
表示变量SRC
获得src目录下面的所有.cpp
文件
2. dir ---- 获取目录函数
$(dir <names...>)
- 功能:获取文件所在目录
- <names…>):输出的文件名
示例:
$(dir src/foo.c sum.txt)
输出src/ ./
3. notdir ---- 获取文件名函数
跟dir
函数正好相反,notdir
获取的是非目录的文件名
$(notdir <names...>)
- 功能:非目录的文件名
- <names…>):输出的文件名
示例:
$(notdir src/foo.c sum.txt)
输出foo.c sum.txt
4. suffix ---- 获取文件后缀函数
$(suffix <names…>)
- 功能:获取文件名后缀
- <names…>:文件名,多个文件使用空格隔开
5. basename ---- 去掉文件后缀函数
$(basename <names…>)
- 功能:去掉文件后缀
- <names…>:文件名,多个文件使用空格隔开
5.2 字符串替换函数
1. subst ---- 字符串替换函数
$(subst <src>,<dst>,<text>)
- 功能:将
中的字符 替换为字符
- 功能:将
2. patsubst ---- 按格式替换函数
$(patsubst <pattern>,<replacement>,<text>)
- 功能: 查找
字符串(可以是多个字符串,当多个时,以空格隔开)中所有匹配的字串,并将字串替换成 - :匹配规则,可以包含通配符
%
,表示任意长度的字串 - :替换的字符串,也支持通配符
%
,一般会跟联合使用,表示中的通配符所代表的字符串 - 返回:返回替换后的字符串
- 功能: 查找
3. strip ---- 去掉开头和结尾的空白字符
$(strip <string>)
- 功能: 去掉开头和结尾的空白字符
4. findstring ---- 字串查找函数
$(findstring <dst>,<src>)
- 功能:在字符串 中查找目标字符或者字符串
- 返回:找到了就返回对应字符,如果没有找到返回空字符
5. filter ---- 过滤函数
$(filter <pattern...>,<text>)
- 功能:过滤掉符合<pattern…>的字符串
- 返回:返回过滤后的字符串
5. filter-out ---- 反过滤函数
$filter-out <pattern...>,<text>)
- 功能:跟
filter
函数正好相反
- 功能:跟
5.3 其他函数
1. call ---- 调用函数
$(call <expression>,<parm1>,<parm2>,<parm3>,...)
- 功能:调用自定义函数
2. shell ---- 执行命令行命令
shell <commands>
- 功能:执行shell命令
- 返回:返回执行后的输出
6. include 关键字
当 make 读取到 include 关键字的时候,会暂停读取当前的 Makefile,而是去读 include 包含的文件,读取结束后再继读取当前的 Makefile 文件。我们常用此关键字来引入一些公共的Makefile文件
include 使用的具体方式如下:
include <filenames>
使用时,通常用 -include 来代替 include 来忽略文件不存在或者是无法创建的错误提示,使用格式如下:
-include <filenames>
这两种方式之间的区别:
- include :make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出;
- -include :当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出
7. 编译常用规则
-I
(大写的i): 用来引入编译需要的头文件目录-l
: 编译依赖的库-L
:依赖库所在目录--std=c++11
: 配置c++支持版本,可以配置c++11
、c++14
、c++17
- -W: 表示一些warning的处理,比如
-Wall
表示启用所有的编译预警,-Werror
表示将warning当error处理 - -O3: 选择编译优化器,总共有4个优化等级,
-O0
表示不开启编译优化,一般在debug时使用 - -g:设置编译debug模式
8. 代码示例
我们构建一个最简单的C++项目工程
project/
├── Makefile
├── main.cpp
├── test.cpp
├── test.h
Makefile 内容如下:
# 配置编译器,也可以不配置,默认使用g++
CXX = g++
# 配置编译选项
CXXFLAGS = -Wall -std=c++17 -O3
# 遍历当前目录下所有的.cpp文件
SRCS=$(wildcard *.cpp)
# 将cpp文件后缀换成.o后缀
OBJS = $(SRCS:.cpp=.o)
# 设置编译目标名
TARGET = app
# 默认编译TARGET
all: ${TARGET}
# 编译成可执行的规则
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)
# 编译.o文件的规则
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# 文件清理
clean:
rm -f $(OBJS) $(TARGET)
# 设置伪目标,避免命名冲突
.PHONY: all clean
我们可以通过在make
指令加-n
来查看所有的编译指令
make -n
# 输出
>> g++ -Wall -std=c++17 -O3 -c main.cpp -o main.o
>> g++ -Wall -std=c++17 -O3 -c test.cpp -o test.o
>> g++ -Wall -std=c++17 -O3 -o app main.o test.o
我们可以看到最终会生成可执行app