Linux学习三(Makefile)

Linux学习三 : Makefile

make是一个命令工具,是一个解释makefile中指令的命令工具

make工具在构造项目的时候需要加载一个叫做makefile的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

makefile的好处就是可以自动化部编译,一旦写好,只需要执行make指令,整个工程就可以自动编译。

makefile文件有两种命名方式 makefile 和 Makefile,构建项目的时候在哪个目录下执行构建命令 make这个目录下的 makefile 文件就会别加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中


Makefile的语法规则

1
2
3
4
target1,target2...: depend1, depend2, ...
command
......
......

每条规则由三个部分组成分别是目标(target), 依赖(depend)和命令(command)。
命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令。
* 例如:通过某个命令编译文件、生成库文件、进入目录等。
* 动作可以是多个,每个命令前必须有一个Tab缩进并且独占占一行。

依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。

  • 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用。
  • 如果规则的命令中不需要任何依赖,那么规则的依赖可以为空。
  • 当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套。
  • 依赖可以根据要执行的命令的实际需求, 指定很多个。

目标(target): 规则中的目标,这个目标和规则中的命令是对应的。
* 通过执行规则中的命令,可以生成一个和目标同名的文件。
* 规则中可以有多个命令, 因此可以通过这多条命令来生成多个目标, 所有目标也可以有很多个。
* 通过执行规则中的命令,可以只执行一个动作,不生成任何文件,这样的目标被称为
伪目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 举例: 有源文件 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首先生成。
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

规则的执行

在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第 1 个规则,分析并执行相关的动作。但是如果依赖不存在的话,就不会执行。需要先将依赖生成出来,就可以在makefile中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。

这样,makefile中的某一条规则在需要的时候,就会被其他的规则调用,直到makefile中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。

规则的嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
gcc -c a.c
# 规则3
b.o:b.c
gcc -c b.c
# 规则4
c.o:c.c
gcc -c c.c

当依赖不存在的时候,make就是查找其他的规则,看哪一条规则是用来生成需要的这个依赖的,找到之后就会执行这条规则中的命令。因此规则2, 规则3, 规则4里的命令会相继被执行,当规则1中依赖全部被生成之后对应的命令也就被执行了,因此规则1的目标被生成,make工作结束。

如果想要执行makefile中非第一条的命令,就要在make后面加上具体的命令,比如 make c.o 单独执行规则4。

make判断是否进行编译

  • make进行编译的时候,如果目标的时间戳>依赖的时间戳,就不再进行编译,因为依赖没有更新,也就不会生成新的目标。
  • 当目标的时间戳<依赖的时间戳,证明需要更新,依赖变化了。
  • 目标不存在,那么肯定更新。

根据上文的描述, 先执行 make 命令,基于这个 makefile 编译这几个源文件生成对应的目标文件。然后再修改例子中的 a.c, 再次通过make编译这几个源文件,那么这个时候先执行规则2更新目标文件a.o, 然后再执行规则1更新目标文件app,其余的规则是不会被执行的。

make有时候并不会完全依赖makefile,会进行自动推导,比如: 使用命令 make 编译扩展名为.c 的 C 语言文件的时候,源文件的编译规则不用明确给出。这是因为 make 进行编译的时候会使用一个默认的编译规则,按照默认规则完成对.c文件的编译,生成对应的.o 文件。它使用命令cc -c来编译.c 源文件。

变量

makefile中的变量分为三种:自定义变量,预定义变量和自动变量。

自定义变量:用 Makefile 进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。定义变量后用$符号将变量取出。

1
2
3
4
5
CC = g++ -std=c++14
CFLAGS = -g -Wall

main:
$(CC) $(CFLAGS) -o main main.cpp
1
2
3
4
5
# 这是一个规则,里边使用了自定义变量
obj=add.o div.o main.o mult.o sub.o
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)

预定义变量:在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的。
在这里插入图片描述

1
2
3
4
5
6
# 这是一个规则,里边使用了自定义变量和预定义变量
obj=add.o div.o main.o mult.o sub.o
target=calc
CFLAGS=-O3 # 代码优化
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)

自动变量:Makefile 中的规则语句中经常会出现目标文件和依赖文件,自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。

1
2
3
4
5
6
$*	表示目标文件的名称,不包含目标文件的扩展名
$+ 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件
$< 表示依赖项中第一个依赖文件的名称
$? 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@ 表示目标文件的名称,包含文件扩展名
$^ 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开
1
2
3
4
5
# 这是一个规则,里边使用了自定义变量
# 使用自动变量, 替换相关的内容
calc:add.o div.o main.o mult.o sub.o
gcc $^ -o $@ # 自动变量只能在规则的命令中使用

模式匹配

模式匹配将一系列的相同操作整理成一个模板,所有类似的操作都通过模板去匹配 makefile 会因此而精简不少,只是可读性会有所下降。

1
2
3
4
# 模式匹配 -> 通过一个公式, 代表若干个满足条件的规则
# 依赖有一个, 后缀为.c, 生成的目标是一个 .o 的文件, % 是一个通配符, 匹配的是文件名
%.o:%.c
gcc $< -c # 单独只有一个的时候就会用$<自动变量

函数

makefile的函数主要是为了方便的获取返回值,写法是这样的: $(函数名 参数1, 参数2, 参数3, …)。
wildcard函数:获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔。

1
2
3
4
5
6
7
# 该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔
$(wildcard PATTERN...)
参数: 指定某个目录, 搜索这个路径下指定类型的文件,比如: *.c
得到的若干个文件的文件列表, 文件名之间使用空格间隔
示例:$(wildcard *.c ./sub/*.c)
返回值格式: a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c
中间的参数可以更多,以空格分开就可以了。

patsubst函数:按照指定的模式替换指定的文件名的后缀。有三个参数, 参数之间使用逗号间隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 有三个参数, 参数之间使用 逗号间隔
$(patsubst <pattern>,<replacement>,<text>)

pattern: 这是一个模式字符串, 需要指定出要被替换的文件名中的后缀是什么,一般用%加后缀
replacement: 这是一个模式字符串, 指定参数pattern中的后缀最终要被替换为什么,通常也是%加后缀
text: 该参数中存储这要被替换的原始数据
返回值为替换后的字符串。

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

makefile编写:一步一步编写makefile文件。

1
2
3
4
5
6
7
8
9
# 项目目录结构
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
# 需要编写makefile对该项目进行自动化编译

版本1:最简单版本:书写简单,但只要依赖中的某一个源文件被修改,所有的源文件都需要被重新编译,太耗时、效率低。

1
2
calc:add.c  div.c  main.c  mult.c  sub.c
gcc add.c div.c main.c mult.c sub.c -o calc

版本2:修改哪一个源文件, 哪个源文件被重新编译, 不修改就不重新编译。但是代码冗余。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 默认所有的依赖都不存在, 需要使用其他规则生成这些依赖
# 因为 add.o 被更新, 需要使用最新的依赖, 生成最新的目标
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 如果修改了add.c, add.o 被重新生成
add.o:add.c
gcc add.c -c

div.o:div.c
gcc div.c -c

main.o:main.c
gcc main.c -c

sub.o:sub.c
gcc sub.c -c

mult.o:mult.c
gcc mult.c -c

版本3:降低冗余,使用变量和模式匹配。代码相对简洁,但是变量obj的值需要手动写出来,如果需要编译的项目文件很多,都用手写出来不现实。

1
2
3
4
5
6
7
8
9
# 添加自定义变量 
obj=add.o div.o main.o mult.o sub.o
target=calc

$(target):$(obj)
gcc $(obj) -o $(target)

%.o:%.c
gcc $< -c

版本4:在makefile中使用函数。解决了自动加载项目文件的问题,解放了双手,但没有文件删除的功能,不能删除项目编译过程中生成的目标文件(*.o)和可执行程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加自定义变量 
# 使用函数搜索当前目录下的源文件 .c
src=$(wildcard *.c)
# 将源文件的后缀替换为 .o
# % 匹配的内容是不能被替换的, 需要替换的是第一个参数中的后缀, 替换为第二个参数中指定的后缀
# obj=$(patsubst %.cpp, %.o, $(src)) 将src中的关键字 .cpp 替换为 .o
obj=$(patsubst %.c, %.o, $(src))
target=calc

$(target):$(obj)
gcc $(obj) -o $(target)

%.o:%.c
gcc $< -c

版本5:在makefile文件中添加新的规则用于删除生成的目标文件(*.o)和可执行程序。可以用make clean删除不需要的文件和程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 添加自定义变量 
# 使用函数搜索当前目录下的源文件 .c
src=$(wildcard *.c)
# 将源文件的后缀替换为 .o
obj=$(patsubst %.c, %.o, $(src))
target=calc
# obj 的值 xxx.o xxx.o xxx.o xx.o
$(target):$(obj)
gcc $(obj) -o $(target)

%.o:%.c
gcc $< -c

# 添加规则, 删除生成文件 *.o 可执行程序
# 这个规则比较特殊, clean根本不会生成, 这是一个伪目标
clean:
rm $(obj) $(target)

版本6:在makefile中将clean声明为一个伪目标,在 makefile 中声明一个伪目标需要使用 .PHONY 关键字, 声明方式为: .PHONY:伪文件名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 添加自定义变量 
# 使用函数搜索当前目录下的源文件 .c
src=$(wildcard *.c)
# 将源文件的后缀替换为 .o
obj=$(patsubst %.c, %.o, $(src))
target=calc

$(target):$(obj)
gcc $(obj) -o $(target)

%.o:%.c
gcc $< -c

# 添加规则, 删除生成文件 *.o 可执行程序
# 声明clean为伪文件
.PHONY:clean
clean:
# shell命令前的 - 表示强制这个指令执行, 如果执行失败也不会终止
-rm $(obj) $(target)
echo "hello, 我是测试字符串"

较复杂结构的makefile编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 目录结构
.
├── include
│ └── head.h ==> 头文件, 声明了加减乘除四个函数
├── main.c ==> 测试程序, 调用了head.h中的函数
└── src
├── add.c ==> 加法运算
├── div.c ==> 除法运算
├── mult.c ==> 乘法运算
└── sub.c ==> 减法运算


# 最终的目标名 app
target = app
# 搜索当前项目目录下的源文件
src=$(wildcard *.c ./src/*.c)
# 将文件的后缀替换掉 .c -> .o
obj=$(patsubst %.c, %.o, $(src))
# 头文件目录
include=./include

# 第一条规则
# 依赖中都是 xx.o yy.o zz.o
# gcc命令执行的是链接操作
$(target):$(obj)
gcc $^ -o $@

# 模式匹配规则
# 执行汇编操作, 前两步: 预处理, 编译是自动完成
%.o:%.c
gcc $< -c -I $(include) -o $@

# 添加一个清除文件的规则
.PHONY:clean

clean:
-rm $(obj) $(target) -f


参考列表:

https://blog.csdn.net/haoel/article/details/2886
https://subingwen.cn/


Linux学习三(Makefile)
https://cauccliu.github.io/2024/03/26/Linux学习三(Makefile)/
Author
Liuchang
Posted on
March 26, 2024
Licensed under