0%

Linux中makefile文件

Linux基础(9):linux中makefile文件

makefile文件的规则编写,工作过程,模式匹配,函数运用等重要知识点

定义&目的

make是linux自带的项目构建工具,帮助完成大量文件编译的工作。它根据makefile中的指令来工作,自动完成项目的构建。

makefile中规定了哪些文件需要先编译,后编译,重编译等问题。当makefile文件构建后之后,只需要一个make命令便可以实现自动编译。

规则

1
2
3
4
5
6
#基本样式
target1,target2...: depend1, depend2,...
command1
command2
.......
.......

三个组成部分:目标,依赖,命令

注意事项

  • 每个命令前需要有一个Tab缩进并且每个命令独占一行
  • 依赖可以是已经存在的文件,也可以是别的规则下生成的目标
  • 因为有多个命令,所以也可以生成多个目标

具体示例

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

注意:一般情况下,在嵌套中,后面的目标通常是作为第一条规则的依赖。

工作原理

make当完成第一条规则的执行后,便结束任务。当第一条规则的依赖不存在或者不完整的时候,会去接下来的规则中寻找。如果想单独执行makefile中的某一条指令, make 该条规则的目标文件

make会比较目标文件和依赖文件的时间戳来判定是不是需要执行make操作。

  • 当目标文件不存在,直接make生成目标文件
  • 当目标文件时间比依赖文件时间早,make更新目标文件
  • 当目标文件时间比依赖文件时间晚,不执行make命令

make具有自动推导的能力,当makefile中规则不完全时,也能依赖自动推导来正确执行。

1
2
3
4
5
6
7
8
9
10
11
12
#常规情况
gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c head.h main.c makefile sub.c
gao@gao-VirtualBox:~/桌面/test/make_file$ cat makefile
#生成相应的命令
cal : add.c sub.c main.c
gcc *.c -o cal
gao@gao-VirtualBox:~/桌面/test/make_file$ make
gcc *.c -o cal
gao@gao-VirtualBox:~/桌面/test/make_file$ ./cal
a+b=20
a-b=0
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
#嵌套情况
gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c head.h main.c makefile sub.c
gao@gao-VirtualBox:~/桌面/test/make_file$ vim makefile
gao@gao-VirtualBox:~/桌面/test/make_file$ cat makefile
#生成相应的命令
cal : add.o sub.o main.o
gcc *.o -o cal

add.o: add.c
gcc -c add.c -o add.o

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

main.o: main.c
gcc -c main.c -o main.o
gao@gao-VirtualBox:~/桌面/test/make_file$ make
gcc -c add.c -o add.o
gcc -c sub.c -o sub.o
gcc -c main.c -o main.o
gcc *.o -o cal
gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c add.o cal head.h main.c main.o makefile sub.c sub.o
gao@gao-VirtualBox:~/桌面/test/make_file$ ./cal
a+b=20
a-b=0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#自动推导情况
gao@gao-VirtualBox:~/桌面/test/make_file$ vim makefile
gao@gao-VirtualBox:~/桌面/test/make_file$ cat makefile
#生成相应的命令
cal : add.o sub.o main.o
gcc *.o -o cal

gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c head.h main.c makefile sub.c
gao@gao-VirtualBox:~/桌面/test/make_file$ make
cc -c -o add.o add.c
cc -c -o sub.o sub.c
cc -c -o main.o main.c
gcc *.o -o cal
gao@gao-VirtualBox:~/桌面/test/make_file$ ./cal
a+b=20
a-b=0

变量

目的:采用变量简化makefile文件

类型:自定义变量,预定义(内置)变量,自动变量

  • 自定义变量
1
2
3
4
5
#定义方式
变量名= 变量值 #注意:一定要赋值,不然会报错

#取出变量的值
$(变量名)

操作示例

1
2
3
4
5
6
7
8
9
#常规写法
cal : add.o sub.o main.o
gcc *.o -o cal

#利用自定义变量简化
obj=add.o main.o sub.o
target=cal
$(target):$(obj)
gcc $(obj) -o $(target)
  • 预定义变量

预定义变量

操作示例

1
2
3
4
5
6
7
8
9
10
# 这是一个规则,普通写法
cal:add.o main.o sub.o
gcc add.o main.o sub.o -o cal

# 这是一个规则,里边使用了自定义变量和预定义变量
obj=add.o main.o sub.o
target=cal
CFLAGS=-O3 # 代码优化
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)
  • 自动变量

自动变量

操作示例

1
2
3
4
5
6
7
8
# 这是一个规则,普通写法
cal:add.o main.o sub.o
gcc add.o main.o sub.o -o cal

# 这是一个规则,里边使用了自定义变量
# 使用自动变量, 替换相关的内容
cal:add.o main.o sub.o
gcc $^ -o $@ # 自动变量只能在规则的命令中使用

模式匹配

基础语法

1
2
#采用固定模式来简化所有同类型的指令,相当于一个模板
%作为通配符,匹配满足条件的指令

操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#常规写法
cal : add.o sub.o main.o
gcc *.o -o cal

add.o: add.c
gcc -c add.c -o add.o

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

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

#利用模式匹配简化
cal : add.o sub.o main.o
gcc *.o -o cal

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

结合变量&模式匹配的简化makefile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#常规写法
cal : add.o sub.o main.o
gcc *.o -o cal

add.o: add.c
gcc -c add.c -o add.o

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

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

#双简化方式
target= cal
obj= add.o sub.o main.o
$(target): $(obj)
gcc $(obj) -o $(target)

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

为什么要采用逐条.o的方案
因为采用.c的方案只要依赖中的某一个源文件被修改,所有的源文件都需要被重新编译,太耗时、效率低,而采用.o分条之后,修改哪一个源文件,哪个源文件被重新编译,不修改就不重新编译

函数

wildcard函数:主要作用是为了获取指定目录下的指定类型的文件。

使用格式

1
2
3
4
5
6
$(wildcard 参数)
参数的类型:某个目录下的某种类型的文件
例如:当前文件夹下的.c文件 ./*.c ,也可以简化写为*.c
或者,具体某个文件夹下的.c文件,./sub/*.c

返回值:得到相应文件的绝对路径列表

使用案例

1
2
3
4
# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c) # *.c == ./*.c
# 返回值: 得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

patsubst函数:主要作用是按照指定的模式替换指定文件的后缀

使用格式

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

参数功能:
pattern: 这是一个模式字符串,需要指定出要被替换的文件名中的后缀是什么
文件名和路径不需要关心,因此使用 % 表示即可 [通配符是 %]
在通配符后边指定出要被替换的后缀,比如: %.c, 意味着 .c 的后缀要被替换掉
replacement: 这是一个模式字符串,指定参数 pattern 中的后缀最终要被替换为什么
还是使用 % 来表示参数 pattern 中文件的路径和名字
在通配符 % 后边指定出新的后缀名,比如: %.o 这表示原来的后缀被替换为 .o
text: 该参数中存储这要被替换的原始数据
返回值:
函数返回被替换过后的字符串。

使用案例

1
2
3
4
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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#优化写法
target= cal

#获取所有.c文件
src= $(wildcard *.c)

#替换后缀名
obj= $(patsubst %.c, %.o, $(src))

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

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

进一步优化

问题:不能去删除过程中产生的.o文件和可执行程序,不利于多次编译

办法:添加新的规则,执行删除命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#优化写法
target= cal

#获取所有.c文件
src= $(wildcard *.c)

#替换后缀名
obj= $(patsubst %.c, %.o, $(src))

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

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

#添加删除命令,方便删除&重新编译
clean:
rm $(obj) $(target)

由于这是一条伪命令,直接执行make并不会执行该条命令,实际使用中要make clean指令来执行这条规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c head.h main.c makefile sub.c

#单纯执行make指令,并没有执行clean规则
gao@gao-VirtualBox:~/桌面/test/make_file$ make
gcc main.c -c
gcc add.c -c
gcc sub.c -c
gcc main.o add.o sub.o -o cal

gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c add.o cal head.h main.c main.o makefile sub.c sub.o

#执行make clean,执行clean规则
gao@gao-VirtualBox:~/桌面/test/make_file$ make clean
rm add.o main.o sub.o cal

gao@gao-VirtualBox:~/桌面/test/make_file$ ls
add.c head.h main.c makefile sub.c

问题因为make执行时需要判断目标与依赖的时间戳,clean指令并不想执行这个功能,可以采用如下方式(借助.PHONY关键字)来声明clean是个伪目标,使得clean指令不需要对比时间戳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#优化写法
target= cal

#获取所有.c文件
src= $(wildcard *.c)

#替换后缀名
obj= $(patsubst %.c, %.o, $(src))

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

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

#添加删除规则
#添加伪目标声明
.PHONY: clean
clean:
rm $(obj) $(target)

一个小tips:在shell命令前增加-,可以保证指令强制运行,如果执行失败也不会终止

测试如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#优化写法
target= cal

#获取所有.c文件
src= $(wildcard *.c)

#替换后缀名
obj= $(patsubst %.c, %.o, $(src))

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

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

#添加删除规则
#添加伪目标声明
.PHONY: clean
clean:
-rm $(obj) $(target) #第一条语句强制执行
echo "hello!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#执行结果
gao@gao-VirtualBox:~/桌面/test/make_file$ make
gcc main.c -c
gcc add.c -c
gcc sub.c -c
gcc main.o add.o sub.o -o cal
gao@gao-VirtualBox:~/桌面/test/make_file$ ./cal
a+b=20
a-b=0
gao@gao-VirtualBox:~/桌面/test/make_file$ make clean
rm add.o main.o sub.o cal
echo "hello!"
hello!

#此时第一条指令报错,但依然强制执行了第二条
gao@gao-VirtualBox:~/桌面/test/make_file$ make clean
rm main.o add.o sub.o cal
rm: 无法删除 'main.o': 没有那个文件或目录
rm: 无法删除 'add.o': 没有那个文件或目录
rm: 无法删除 'sub.o': 没有那个文件或目录
rm: 无法删除 'cal': 没有那个文件或目录
make: [makefile:20:clean] 错误 1 (已忽略)
echo "hello!"
hello!

此处的makefile文件就是最终比较完美版的啦~

makefile文件优秀示例

附上一份丙哥的makefile相对复杂的范本

1
2
3
4
5
6
7
8
9
10
# 目录结构
.
├── include
│   └── head.h ==> 头文件, 声明了加减乘除四个函数
├── main.c ==> 测试程序, 调用了head.h中的函数
└── src
├── add.c ==> 加法运算
├── div.c ==> 除法运算
├── mult.c ==> 乘法运算
└── sub.c ==> 减法运算
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
# 最终的目标名 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://subingwen.cn/linux/makefile/