Makefile 备忘
当工程中的文件逐渐增多,使用 GCC 命令编译每个源文件就会变得力不从心。此时我们需要借助项目构造工具 make 帮助我们完成这个任务。
本文创建于
2024-11-25
;修改于2024-11-25
;
1. make
make工具在构造项目的时候需要加载一个叫做makefile的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile 文件须命名为: Makefile
or makefile
在哪个目录下执行构建命令,哪个目录下的 makefile 文件就会被加载。
2. 语法格式
# 每条规则的语法格式:
target1,target2...: depend1, depend2, ...
command1
command2
...
每条规则由 3 部分组成,分别是 目标(target)、依赖(depend)、命令(command)。
- 命令(command):编译目标的 shell 命令
- 比如,通过 shell 命令 编译文件,库文件,进入文件夹等
- 注意,每行命令前都要有缩进
- 依赖(depend):规则所需的依赖文件
- 比如
.c
.o
等项目文件 - 如果命令不需要任何依赖,则可以为空
- 当前规则的依赖文件可能是其他规则的目标文件,从而形成了依赖的嵌套
- 多个依赖之间,可以使用逗号进行分割,也可以使用空格进行分割
- 比如
- 目标(target):规则中的目标,与规则中的命令相对应
- 通过执行规则中的命令,可以生成一个与目标同名的文件
- 规则中有多行命令,对应了多个目标
- 伪目标,即执行规则中的命令会执行相应的动作,不生成任何文件
- 多个目标之间,可以使用逗号进行分割,也可以使用空格进行分割
- 举例说明
# 举例: 有源文件 a.c b.c c.c head.h, 需要生成可执行程序 app
################# 例1 #################
app:a.c b.c c.c
gcc a.c b.c c.c -o app
################# 例2 #################
# 有多个目标, 多个依赖, 多个命令
app,app1:a.c b.c c.c d.c
gcc a.c b.c -o app
gcc c.c d.c -o app1
################# 例3 #################
# 规则之间的嵌套
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# a.o 是第一条规则中的依赖
a.o:a.c
gcc -c a.c
# b.o 是第一条规则中的依赖
b.o:b.c
gcc -c b.c
# c.o 是第一条规则中的依赖
c.o:c.c
gcc -c c.c
3. 工作原理
3.1 时间戳
make 命令执行时,会根据文件的时间戳来判断是否执行 makefile 规则中的相关命令。
make 会先判断目标是否存在,如果不存在,则命令一定会执行;如果目标已经存在,则会判断时间戳:
- 正常情况下,
目标时间戳 > 所有依赖的时间戳
,如果满足这个条件,则意味着相关依赖并没有任何更新或变化,自然也就无需重新编译目标。 - 当依赖文件被更新了,则
目标时间戳 < 某些依赖的时间戳
,此时,make 会根据命令重新生成目标。
3.2 自动推导
$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── makefile
├── mult.c
└── sub.c
# makefile 文件:
# 这是一个完整的 makefile 文件
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
此时执行 make 进行构建会发现,项目构建成功了,这说明 make 进行了自动推导,在我们的目录下并没有 add.o div.o main.o mult.o sub.o
这些依赖,make 根据同名的 .c
文件进行了自动编译,最终生成我们需要的目标文件。
4. 变量
我们可以在 makefile 文件中使用变量来进行规则的定义,makefile 文件中的变量分为三种:自定义变量、预定义变量、自动变量。
4.1 自定义变量
# 创建一个变量并赋值,不能只创建变量名而不赋值
variable=value
# 用 $(name) 的形式取出变量值
# 定义变量 variable
variable=a.o
# 对 variable 进行取值
$(variable)
# 举例:
obj=a.c b.c c.c d.c
target=app
$(target):$(obj)
gcc $(obj) -o $(target)
4.2 预定义变量
在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:
变 量 名 | 含 义 | 默 认 值 |
---|---|---|
AR | 生成静态库库文件的程序名称 | ar |
AS | 汇编编译器的名称 | as |
CC | C 语言编译器的名称 | cc |
CPP | C 语言预编译器的名称 | $(CC) -E |
CXX | C++语言编译器的名称 | g++ |
FC | FORTRAN 语言编译器的名称 | f77 |
RM | 删除文件程序的名称 | rm -f |
ARFLAGS | 生成静态库库文件程序的选项 | 无默认值 |
ASFLAGS | 汇编语言编译器的编译选项 | 无默认值 |
CFLAGS | C 语言编译器的编译选项 | 无默认值 |
CPPFLAGS | C 语言预编译的编译选项 | 无默认值 |
CXXFLAGS | C++语言编译器的编译选项 | 无默认值 |
FFLAGS | FORTRAN 语言编译器的编译选项 | 无默认 |
示例:
obj=a.c b.c c.c d.c
target=app
# 代码优化参数,注意是字母 O 不是数字 0
CFLAGS=-O3
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)
4.3 自动变量
自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。
变 量 | 含 义 |
---|---|
$* | 表示目标文件的名称,不包含目标文件的扩展名 |
$+ | 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件 |
$< | 表示依赖项中第一个依赖文件的名称 |
$? | 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开 |
$@ | 表示目标文件的名称,包含文件扩展名 |
$^ | 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开 |
示例:
# 使用自动变量,替换命令中的相关内容
app:a.c b.c c.c d.c
gcc $^ -o $@
5. 模式匹配
对于makefile文件中可能出现的大量重复性规则,我们可以将重复的动作化为一个模版,类似的操作都通过模版去匹配,从而简化了我们的makfile文件。
示例:
# 每一个 .o 目标,如果它对应的 .c 文件存在,则执行下面的命令。% 是一个通配符,匹配任意字符串。
%.o:%.c
gcc $< -c
6. 函数
makefile 中的所有函数都是有返回值的。函数的调用形式如:$(函数名 参数1, 参数2, …)
6.1 wildcard 函数
wildcard 函数的主要作用是搜索并获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。
使用示例:
# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard ./a/*.c ./b/*.c *.c) # 单独写 *.c 等价于 ./*.c
# 返回值:得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
./a/a.c ./a/b.c ./b/c.c ./b/d.c e.c f.c
6.2 patsubst 函数
patsubst 函数的功能是按照指定的模式替换指定的文件名的后缀
使用示例:
src = a.cpp b.cpp c.cpp e.cpp
# 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src))
# obj 的值为: a.o b.o c.o e.o
7. 练习
根据以下目录结构编写 makefile 文件:
# 目录结构
.
├── include
│ └── head.h ==> 头文件, 声明了加减乘除四个函数
├── main.c ==> 测试程序, 调用了head.h中的函数
└── src
├── add.c ==> 加法运算
├── div.c ==> 除法运算
├── mult.c ==> 乘法运算
└── sub.c ==> 减法运算
makefile 文件:
# 最终的目标名 app
target=app
# 搜索当前目标目录下的源文件
src=$(wildcard *.c ./src/*.c)
# 将 $(src) 中文件拓展名替换为 .o
obj=$(patsubst *.c, *.o, $(src))
# 头文件目录
include=./include
# 执行链接操作得到最终目标
$(target):$(obj)
$(CC) $^ -o $@
# 模式匹配,将每一个 .c 文件汇编为 .o 文件
%.o:%.c
$(CC) $< -c -I $(include) -o $@
# .PHONY 说明 clean 是一个伪目标
.PHONY:clean
clean:
rm $(obj) $(target) -f
本文资料来源 爱编程的大丙,看了这位老师B站的很多课程,受益匪浅,感谢!