Ronan Blog

罗华东的博客 | 向前每多走一步,热爱和勇气就会多一分。

Makefile

2024-09-02 3 min read Docs Ronan

1.规则

Makefile的框架是由规则构成的。make命令执行时先在Makefile文件中查找各种规则,对各种规则进行解析后运行规则。规则的基本格式为:

每条规则的语法格式:
target1,target2...: depend1, depend2, ...
	command
	......
	......

每条规则由三个部分组成分别是目标(target), 依赖(depend)命令(command)

  • 命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令 例如:通过某个命令编译文件、生成库文件、进入目录等。 动作可以是多个,每个命令前必须有一个Tab缩进并且独占占一行。
  • 依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用 如果规则的命令中不需要任何依赖,那么规则的依赖可以为空 当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套 依赖可以根据要执行的命令的实际需求, 指定很多个
  • 目标(target): 规则中的目标,这个目标和规则中的命令是对应的 通过执行规则中的命令,可以生成一个和目标同名的文件 规则中可以有多个命令, 因此可以通过这多条命令来生成多个目标, 所有目标也可以有很多个 通过执行规则中的命令,可以只执行一个动作,不生成任何文件,这样的目标被称为伪目标

这里看着脑疼,举个例子

################# 例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

2.规则的执行

在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第 1 个规则,分析并执行相关的动作。但是需要注意的是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,这个动作也就不会被执行。

对应的解决方案是先将需要的依赖生成出来,我们就可以在makefile中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。

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

# 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

3.变量

3.1自定义变量

  • 定义变量
# 错误, 只创建了变量名, 没有赋值
变量名 
# 正确, 创建一个变量名并且给其赋值
变量名=变量值
  • 取出变量里的值
# 如果将变量的值取出?
$(变量的名字)

# 举例 add.o  div.o  main.o  mult.o  sub.o
# 定义变量并赋值
obj=add.o  div.o  main.o  mult.o  sub.o
# 取变量的值
$(obj)

3.2预定义变量

在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:

变 量 名含 义默 认 值
AR生成静态库库文件的程序名称ar
AS汇编编译器的名称as
CCC 语言编译器的名称cc
CPPC 语言预编译器的名称$(CC) -E
CXXC++语言编译器的名称g++
FCFORTRAN 语言编译器的名称f77
RM删除文件程序的名称rm -f
ARFLAGS生成静态库库文件程序的选项无默认值
ASFLAGS汇编语言编译器的编译选项无默认值
CFLAGSC 语言编译器的编译选项无默认值
CPPFLAGSC 语言预编译的编译选项无默认值
CXXFLAGSC++语言编译器的编译选项无默认值
FFLAGSFORTRAN 语言编译器的编译选项无默认值

举例

# 这是一个规则,普通写法
calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
        
# 这是一个规则,里边使用了自定义变量和预定义变量
obj=add.o  div.o  main.o  mult.o  sub.o
target=calc
CFLAGS=-O3 # 代码优化
$(target):$(obj)
        $(CC)  $(obj) -o $(target) $(CFLAGS)

3.3自动变量

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
        
# 这是一个规则,里边使用了自定义变量
# 使用自动变量, 替换相关的内容
calc:add.o  div.o  main.o  mult.o  sub.o
	gcc $^ -o $@ 			# 自动变量只能在规则的命令中使用

4.模式匹配

calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
# 语法格式重复的规则, 将 .c -> .o, 使用的命令都是一样的 gcc *.c -c
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

在阅读过程中能够发现从第二个规则开始到第六个规则做的是相同的事情, 但是由于文件名不同不得不在文件中写出多个规则,这就让 makefile 文件看起来非常的冗余,我们可以将这一系列的相同操作整理成一个模板,所有类似的操作都通过模板去匹配 makefile 会因此而精简不少,只是可读性会有所下降。

这个规则模板可以写成下边的样子,这种操作就称之为模式匹配.

# 模式匹配 -> 通过一个公式, 代表若干个满足条件的规则
# 依赖有一个, 后缀为.c, 生成的目标是一个 .o 的文件, % 是一个通配符, 匹配的是文件名
%.o:%.c
	gcc $< -c

5.函数

makefile中有很多函数并且所有的函数都是有返回值的。makefile中函数的格式和C/C++中函数也不同,其写法是这样的: $(函数名 参数1, 参数2, 参数3, ...),主要目的是让我们能够快速方便的得到函数的返回值。

主要说两个使用频率很高的函数wildcardpatsubst

5.1 wildcard

# 该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔
$(wildcard PATTERN...)
	参数:	指定某个目录, 搜索这个路径下指定类型的文件,比如: *.c
  • 参数功能: PATTERN 指的是某个或多个目录下的对应的某种类型的文件, 比如当前目录下的.c文件可以写成 *.c 可以指定多个目录,每个路径之间使用空格间隔
  • 返回值: 得到的若干个文件的文件列表, 文件名之间使用空格间隔 示例:$(wildcard *.c ./sub/*.c) 返回值格式: a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c

举个例子

# 使用举例: 分别搜索三个不同目录下的 .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

5.2 patsubet

# 有三个参数, 参数之间使用 逗号间隔
$(patsubst <pattern>,<replacement>,<text>)
  • pattern: 这是一个模式字符串, 需要指定出要被替换的文件名中的后缀是什么 文件名和路径不需要关心, 因此使用 % 表示即可 [通配符是 %] 在通配符后边指定出要被替换的后缀, 比如: %.c, 意味着 .c的后缀要被替换掉
  • replacement: 这是一个模式字符串, 指定参数pattern中的后缀最终要被替换为什么 还是使用 % 来表示参数pattern 中文件的路径和名字 在通配符 % 后边指定出新的后缀名, 比如: %.o 这表示原来的后缀被替换为 .o
  • 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