爱编程的大丙Makefile总结

主要根据这个教程学习https://subingwen.cn/linux/makefile/

1.规则

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

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

命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令
依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。
目标(target): 规则中的目标,这个目标和规则中的命令是对应的

2.工作原理

2.1规则的执行
在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第1个规则,分析并执行相关的动作。但是如果依赖不存在,这个动作就不会执行,会下向下找

# 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

比如这个例子运行第一条规则发现依赖不存在的时候,就会往下找,因此会以此执行规则2,3,4,当规则1中依赖全部被生成后对应的命令也就被执行了。
拓展:如果不想执行第一条规则,那么就不能直接make,可以比如make b.o执行规则3。

2.2文件的时间戳
make命令执行的时候会根据文件的时间戳判定是否执行makefile文件中相关规则中的命令。
(1)目标是通过依赖生成的,因此正常情况下:目标时间戳 > 所有依赖的时间戳, 如果执行 make 命令的时候检测到规则中的目标和依赖满足这个条件, 那么规则中的命令就不会被执行。
(2)当依赖文件被更新了, 文件时间戳也会随之被更新, 这时候 目标时间戳 < 某些依赖的时间戳, 在这种情况下目标文件会通过规则中的命令被重新生成。
(3)如果规则中的目标对应的文件根本就不存在, 那么规则中的命令肯定会被执行。

2.3自动推导
当我们漏写一些构建规则,但是我们会发现程序还是会被编译成功。这是因为make有自动推导的能力,不会完全依赖makefile。
比如: 使用命令 make 编译扩展名为.c 的 C 语言文件的时候,源文件的编译规则不用明确给出。这是因为 make 进行编译的时候会使用一个默认的编译规则,按照默认规则完成对.c文件的编译,生成对应的.o 文件。它使用命令cc -c来编译.c 源文件。在 Makefile 中只要给出需要构建的目标文件名(一个.o 文件),make 会自动为这个.o 文件寻找合适的依赖文件(对应的.c 文件),并且使用默认的命令来构建这个目标文件。

3.变量

3.1自定义变量
3.1.1创建变量
变量名=变量值
3.1.2使用变量
$(变量的名字)

3.2预定义变量
在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。
image

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

4.模式匹配

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

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

image

5.函数

$(函数名 参数1, 参数2, 参数3, ...)
5.1wildcard
这个函数的主要作用是获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。函数原型如下:

# 该函数的参数只有一个, 但是这个参数可以分成若干个部分, 通过空格间隔
$(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.2patsubst
这个函数的功能是按照指定的模式替换指定的文件名的后缀, 函数原型如下:

# 有三个参数, 参数之间使用 逗号间隔
$(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

6.makefile的编写

# 项目目录结构
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
# 需要编写makefile对该项目进行自动化编译

6.1版本1

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

这个版本的优点:书写简单
这版本的缺点:只要依赖中的某一个源文件被修改,所有的源文件都需要被重新编译,太耗时、效率低
改进方式:提高效率,修改哪一个源文件, 哪个源文件被重新编译, 不修改就不重新编译

6.2版本2

# 默认所有的依赖都不存在, 需要使用其他规则生成这些依赖
# 因为 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

这个版本的优点:相较于版本1效率提升了
这个版本的缺点:规则比较冗余, 需要精简
改进方式:在 makefile 中使用变量 和 模式匹配

6.3版本3

# 添加自定义变量 -> makefile中注释前 使用 # 
obj=add.o  div.o  main.o  mult.o  sub.o
target=calc

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

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

这个版本的优点:文件精简不少,变得简洁了
这个版本的缺点:变量 obj 的值需要手动的写出来, 如果需要编译的项目文件很多,都用手写出来不现实
改进方式:在makefile中使用函数

6.4版本4

# 添加自定义变量 -> makefile中注释前 使用 # 
# 使用函数搜索当前目录下的源文件 .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

这个版本的优点:解决了自动加载项目文件的问题,解放了双手
这个版本的缺点:没有文件删除的功能,不能删除项目编译过程中生成的目标文件(.o)和可执行程序
改进方式: 在makefile文件中添加新的规则用于删除生成的目标文件(
.o)和可执行程序

6.5版本5

# 添加自定义变量 -> makefile中注释前 使用 # 
# 使用函数搜索当前目录下的源文件 .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)

这个版本的优点: 添加了新的规则(16行)用于文件的删除, 直接 make clean 就可以执行规则中的删除命令了
这个版本的缺点: 在下面有具体的问题演示和分析
改进方式: 在makefile文件中声明 clean是一个伪目标,让 make 放弃对它的时间戳检测。

正常情况下这个版本的makefile是可以正常工作的,但是我们如果在这个项目目录中添加一个叫做clean的文件(和规则中的目标名称相同),再进行 make clean发现这个规则就不能正常工作了。

# 在项目目录中添加一个叫 clean的文件, 然后在 make clean 这个规则中的命令就不工作了
$ ls
add.c  calc   div.c  head.h  main.o    mult.c  sub.c
add.o  div.o  main.c  makefile  mult.o  sub.o  clean  ---> 新添加的

# 使用 makefile 中的规则删除生成的目标文件和可执行程序
$ make clean
make: 'clean' is up to date. 

# 查看目录, 发现相关文件并没有被删除, make clean 失败了
$ ls
add.c  calc   div.c  head.h  main.o    mult.c  sub.c
add.o  clean  div.o  main.c  makefile  mult.o  sub.o

这个问题的关键点在于 clean是一个伪目标, 不对应任何实体文件, 在前边讲关于文件时间戳更新问题的时候说过,如果目标不存在规则的命令肯定被执行, 如果目标文件存在了就需要比较规则中目标文件和依赖文件的时间戳,满足条件才执行规则的命令,否则不执行。
解决这个问题需要在 makefile 中声明 clean是一个伪目标,这样 make 就不会对文件的时间戳进行检测,规则中的命令也就每次都会被执行了。
在 makefile 中声明一个伪目标需要使用 .PHONY 关键字, 声明方式为: .PHONY:伪文件名称

6.6最终版

# 添加自定义变量 -> makefile中注释前 使用 # 
# 使用函数搜索当前目录下的源文件 .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, 我是测试字符串"
posted @ 2026-01-06 14:43  r5ett  阅读(2)  评论(0)    收藏  举报