文章目录
😶🌫️😶🌫️😶🌫️背景——一个优秀工程师必备技能😶🌫️😶🌫️😶🌫️
关于 Makefile 的重要性
- Makefile 的掌握与工程能力 是否会编写 Makefile,从某种程度上反映了一个人是否具备完成大型工程项目的能力。它不仅是一个编译文件,更是一种有效的项目管理工具。
- 工程中的文件管理与编译规则 在工程项目中,源文件通常按类型、功能或模块组织在不同的目录中。Makefile 定义了一系列规则,指定文件的编译顺序与依赖关系,包括:- 哪些文件需要优先编译- 哪些文件需要重新编译- 更复杂的功能操作
- 自动化编译的优势 Makefile 的最大优势在于实现“自动化编译”。一旦编写完成,仅需一个
make
命令,即可完成整个项目的自动编译,大大提高了开发效率。 - 工具支持 Make 是一种解释 Makefile 指令的命令工具,广泛应用于各种 IDE,例如:- Delphi 的 make- Visual C++ 的 nmake- Linux 下的 GNU make因此,Makefile 已成为工程项目中的主流编译方法。
- Make 与 Makefile 的配合 Make 是一条命令,而 Makefile 是一个配置文件。两者相互配合,可实现项目的自动化构建与高效管理。
一、🤩🤩快速了解make与Makefile核心思想🤩🤩
1. 快速见一面
问:make与Makefile是什么呢?
答:make是一个命令,Makefile是一个指令。
2. 具体语法——Makefile
Makefile 语法基础
- 依赖关系与依赖方法 Makefile 的核心语法就是“依赖关系 + 依赖方法”。这两者决定了文件编译的顺序和方式。- 依赖关系:某个文件(称为“目标”)需要其他文件(称为“依赖”)准备好才能编译。例如,
main.o: main.c utils.h
表示main.o
依赖于main.c
和utils.h
。换句话说,在生成main.o
之前,main.c
和utils.h
必须是最新的。- 依赖方法:告诉 Makefile 如何生成目标文件的具体指令。例如,gcc -c main.c -o main.o
指明如何生成main.o
文件。Makefile 会执行这些方法来自动编译项目。 .PHONY
的作用.PHONY
是 Makefile 中的一个特殊指令,用来定义“伪目标”,即它不会检查文件的时间戳,只会直接执行定义的命令。- 用法:比如.PHONY: clean
表示clean
是个伪目标。这样在执行make clean
时,无论有没有名为clean
的文件,Makefile 都会直接运行clean
目标的命令,而不去判断文件是否最新。- 作用:让依赖方法始终被执行,而不受文件的更新情况影响。这在清理文件、初始化环境等操作中尤其有用。
以上,大家其实都不用看,J桑带大家把这些换成人话😉😉😉
我们生活中有这样的场景:
对于mytest来说:
对于这里clean来说:
这里
.PHONY
的作用是,它所修饰的
clean
是一个伪目标,所以clean被依赖的方法总是被执行。
3. 具体语法——make
make
默认执行Makefile中第一个,也可以
make
后家对应的目标名(targetname),那么就执行对应的目标。
带
.PHONY
的目标可以重复执行,不带
.PHONY
的目标只能执行一次:
二、🥳🥳.PHONY原理解释🥳🥳
1. ACM时间
我们使用
stat
命令可以看到一个文件的ACM时间:
在
stat
命令的输出中,我们可以看到文件的三个主要时间属性:
- Access Time(atime) - 访问时改变时间 表示文件最后一次被读取的时间。任何对文件内容的读取(如打开文件查看)都会更新这个时间,但文件的属性和内容不会因此改变。
A
时间不是每一次访问都被修改,这样有效降低了IO
,一般可能访问10次会做一次修改。 - Modify Time(mtime) - 修改内容改变时间 表示文件内容最后一次被修改的时间。如果文件的内容发生变化(例如文件被编辑或写入新数据),
mtime
就会更新。一般来说M
时间变化了,代表文件被修改,而一般文件被修改,它的大小等等可能会变,因此C
时间会跟着变化。 - Change Time(ctime) - 修改属性改变时间 表示文件元数据最后一次被更改的时间。例如,修改文件的权限、所有者或文件名都会更新
ctime
。注意,ctime
反映的是文件状态的变化,而不是文件内容的直接修改。
2. make对比时间
当你运行
make
时,Make 会检查源文件和生成的可执行文件的时间戳。工作流程如下:
- 初次执行 在源文件和可执行文件都存在的情况下,Make 会对比它们的时间戳。如果源文件比可执行文件更新,说明源代码被修改过,Make 将重新编译生成可执行文件。
- 重复执行 此时源文件如果没有被修改过,bin的时间比较新,他就不会去执行make(即没有更新),再次执行
make
时不会重新编译,节省了重复编译的时间。如果源文件被修改过了,源文件的时间就会更新,比bin的时间更新,因此就可以执行。 - 执行
make clean
使用make clean
时,定义的清理规则会删除生成的文件(如可执行文件、目标文件等),清理掉这些文件的时间戳记录。 - 重新生成可执行文件 由于
make clean
删除了之前生成的可执行文件,再次执行make
时,系统找不到这些文件,于是会重新编译并生成可执行文件。
通过这种时间戳的比对机制,
make
实现了智能编译,即只在必要时重新编译文件,从而加快开发流程。
3. 回到.PHONY
make比较的是原文件与可执行程序的
M
时间,
因此
,PHONY
修饰的目标原理是忽略了时间的比较,它的方法总是被执行。
以下是优化后的内容,进一步阐明了
make
的时间戳比对机制和
.PHONY
的作用及好处:
4. Make 的智能编译机制的好处
- 避免不必要的编译,提升效率
make
通过源文件和可执行文件的时间戳对比,判断哪些文件需要重新编译。这种机制确保只有在源文件发生更改时才会触发编译,从而节省时间、降低 CPU 和 I/O 资源消耗。 - 减少错误和不一致 这种机制也降低了手动编译可能产生的版本不一致或遗漏文件更新的风险。编译流程变得更可靠,确保了最新的源代码得到正确的编译。
三、😴😴makefile会自行推导😴😴
1. 现象分析——自行推导
Makefile 能够自行推导多个依赖目标的生成步骤,使得我们可以从一份源文件自动构建出最终的可执行文件。例如,以下代码展示了从
test.c
文件逐步生成可执行文件
mytest
的过程:
我们有这样的依赖关系:
依赖关系会自动推导如下:
最终可以执行:
2. 原理解释
这种逐步生成的编译流程可以理解为依赖栈的原理。
make
会依次推导每个目标文件的依赖关系,将最底层的依赖按顺序编译生成最终的目标文件
mytest
。过程如下:
- 查找依赖:当执行
make mytest
时,make
会检查mytest
的依赖项(即test.o
)是否存在且最新。 - 向下递归:如果
test.o
不存在或不是最新,make
会检查test.o
的依赖test.s
是否存在且最新。 - 逐层构建:依次向下直到最底层的
test.c
,按顺序执行编译命令: -test.c
生成test.i
(预处理)-test.i
生成test.s
(汇编代码生成)-test.s
生成test.o
(汇编)-test.o
最终生成mytest
(链接) 通过make
的自动推导机制,我们可以通过依赖栈的形式按需生成目标文件,而无需手动编写每一步的命令。
以下是对内容的优化:
3. Make 的默认工作流程
当只输入
make
命令时,
make
将按照默认方式执行以下步骤:
- 查找 Makefile 文件
make
会在当前目录中寻找名为Makefile
或makefile
的文件。 - 定位第一个目标文件 找到 Makefile 文件后,
make
会将其中定义的第一个目标(target)文件作为最终的构建目标。例如,上例中,make
会将myproc
作为最终生成的文件。 - 检查目标文件及依赖项时间 如果
myproc
文件不存在,或者它依赖的文件(例如myproc.o
)有较新的修改时间,make
将执行 Makefile 中定义的命令来生成myproc
。 - 递归生成依赖项 如果
myproc
的依赖文件myproc.o
不存在,make
会继续查找myproc.o
的生成规则。这会递归地查找依赖,直到找到一个存在的文件或底层源文件,例如.c
文件。 - 编译生成目标 当
make
找到底层源文件(如.c
文件)时,便会执行对应的命令来生成依赖文件,如myproc.o
,再继续编译生成最终目标myproc
。 - 逐层查找依赖关系
make
逐层检查和生成每一层的依赖,直到最终生成第一个目标文件。这种递归查找和编译依赖的过程类似于一个栈结构。 - 错误处理 在查找或生成的过程中,如果某个关键文件缺失,
make
会退出并报错。如果是执行命令时出现错误,例如编译失败,make
将跳过该命令继续查找其他依赖关系。 - 依赖关系管理
make
只管理文件的依赖关系。如果在解析依赖关系时无法找到某个依赖文件,make
将终止工作,不会执行进一步的命令。
这种依赖机制让
make
仅在必要时重新编译文件,从而简化了构建流程,避免了重复编译,提高了效率。
四、❤️❤️必看!!!❤️❤️逐层深入构建万能通用Makefile模板❤️❤️
1. 初代改进——.c到.o
我们说使用上述
iso
逐步推导的方式太麻烦了,直接从
test.c
到
mytest
也不方便,我们期望的是将所有的文件转换为
.o
,文件,在进行连接的时候,就是这些
.o
文件与库中的直接连接在一起,比较方便,因此,我们希望先将所有的
.c
文件变为
.o
文件。
2. 二代进阶版本——类似宏定义
# 二代代码
BIN = mytest
SRC = test.c
OBJ = test.o
CC = gcc
RM = rm -f
$(BIN): $(OBJ)
$(CC) $(OBJ)-o $(BIN)
$(OBJ): $(SRC)
$(CC)-c $(SRC)-o $(OBJ).PHONY: clean
clean:
$(RM) $(BIN) $(OBJ)
- 变量定义
BIN = mytestSRC = test.cOBJ = test.oCC = gccRM = rm -f
-BIN
: 目标可执行文件的名称。-SRC
: 源文件名称。-OBJ
: 目标文件(编译后的中间文件)的名称。-CC
: 编译器(gcc
)。-RM
: 用于删除文件的命令(rm -f
)。 - 生成目标
$(BIN)
$(BIN): $(OBJ) $(CC) $(OBJ) -o $(BIN)
- 表示$(BIN)
依赖于$(OBJ)
。- 如果$(BIN)
不存在或者$(OBJ)
的修改时间比$(BIN)
更新,则执行编译命令gcc test.o -o mytest
。 - 生成目标
$(OBJ)
$(OBJ): $(SRC) $(CC) -c $(SRC) -o $(OBJ)
- 表示$(OBJ)
依赖于$(SRC)
。- 如果$(OBJ)
不存在或test.c
有更新,则执行gcc -c test.c -o test.o
来生成目标文件test.o
。 - 清理目标
clean
.PHONY: cleanclean: $(RM) $(BIN) $(OBJ)
-.PHONY: clean
指定clean
为伪目标,不与文件同名。-clean
目标用于删除生成的二进制文件和目标文件。
3. ❤️❤️❤️! ! ! 最终版本——多文件通配 ! ! !❤️❤️❤️
首先我们需要创建100个.c文件来模拟:
使用
{}
来一次性创建 100 个
.c
文件,可通过
touch
和
{}
来批量创建文件:
touch file{1..100}.c
# 最终代码
BIN = mytest # 定义目标可执行文件的名称为 mytest
# SRC = $(shell ls *.c) # 使用 shell 命令列出当前目录下的所有 .c 文件(已注释掉,改用 wildcard)
SRC = $(wildcard *.c) # 使用 wildcard 函数获取当前目录下所有的 .c 文件,赋值给 SRC 变量
OBJ = $(SRC:.c=.o) # 使用模式替换,将 SRC 中的每个 .c 文件名替换为对应的 .o 文件名,赋值给 OBJ 变量
CC = gcc # 定义使用的编译器为 gcc
RM = rm -f # 定义删除文件的命令为 rm -f(强制删除)
# 目标文件 $(BIN) 依赖于所有的目标文件 $(OBJ)
$(BIN): $(OBJ)
@$(CC) $^ -o $@ # 编译命令,$^ 表示所有依赖文件(即 OBJ),$@ 表示目标文件(即 BIN)
@echo "链接 $^ 成 $@" # 输出编译链接的命令信息,$^ 表示依赖文件,$@ 表示目标文件
# %.o: %.c 是一个模式规则,表示对于每个 .c 文件生成对应的 .o 文件
%.o: %.c
@$(CC) -c $< # 编译命令,$< 表示第一个依赖文件(即 .c 文件),$@ 表示目标文件(即 .o 文件)
@echo "编译... $< 成 $@" # 输出编译过程的命令信息,$< 表示 .c 文件,$@ 表示 .o 文件
.PHONY: clean # 声明 clean 为伪目标,避免与文件名冲突
clean: # clean 目标用于清理编译生成的文件
$(RM) $(BIN) $(OBJ) # 使用 rm -f 命令删除可执行文件和目标文件
$(BIN)
: 表示目标文件(在这里是mytest
)。$@
在命令中代表当前目标文件名。$(OBJ)
: 表示目标文件的依赖(即.o
文件)。$^
在命令中表示所有的依赖文件。$^
: 在命令中表示所有的依赖文件(即所有.o
文件)。$@
: 在命令中表示当前目标文件(例如mytest
)。$<
: 在命令中表示第一个依赖文件(例如.c
文件)。.PHONY: clean
: 声明clean
为伪目标,意味着clean
不是文件名,而是一个命令目标,防止与实际的clean
文件冲突。@
: 在命令前加@
,表示不输出执行命令本身,仅输出命令的结果。$(wildcard *.c)
: 使用wildcard
函数获取当前目录下所有.c
文件名,并将其赋值给SRC
变量。$(SRC:.c=.o)
: 使用模式替换,将SRC
中所有.c
文件名替换为对应的.o
文件名,赋值给OBJ
变量。$(shell ls *.c)
: 使用shell
函数执行ls *.c
命令来获取.c
文件(虽然该行被注释掉,已用wildcard
替代)。
4. 适度扩展的语法
BIN=proc.exe # 定义变量
CC=gcc
#SRC=$(shell ls *.c) # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名
SRC=$(wildcard *.c) # 或者使⽤ wildcard 函数,获取当前所有.c⽂件名
OBJ=$(SRC:.c=.o) # 将SRC的所有同名.c 替换 成为.o 形成⽬标⽂件列表
LFLAGS=-o # 链接选项
FLAGS=-c # 编译选项
RM=rm -f # 引⼊命令
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^ # $@:代表⽬标⽂件名。 $^: 代表依赖⽂件列表
@echo "linking ... $^ to $@"%.o:%.c # %.c 展开当前⽬录下所有的.c。 %.o: 同时展开同
名.o
@$(CC) $(FLAGS) $< # %<: 对展开的依赖.c⽂件,⼀个⼀个的交给gcc。
@echo "compling ... $< to $@" # @:不回显命令
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN) # $(RM): 替换,⽤变量内容替换它
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
👍👍👍总结: Makefile 的好处👍👍👍
Makefile 的好处包括:
- 自动化构建:通过定义规则,Makefile 可以自动化编译、链接等过程,节省手动操作时间。
- 增量编译:Makefile 会根据文件的修改时间自动判断哪些文件需要重新编译,避免重复编译未修改的文件,提高效率。
- 简化管理复杂项目:对于有多个源文件的项目,Makefile 可以统一管理文件的依赖关系,避免手动处理每个文件,减少错误。
- 跨平台支持:Makefile 是跨平台的构建工具,支持在多种操作系统和环境中使用,如 Linux、macOS 和 Windows。
- 灵活性和可扩展性:可以根据项目的需要灵活地定义不同的构建规则,处理各种需求,如清理临时文件、执行测试等。
- 易于维护:集中管理构建命令和依赖关系,使得构建过程清晰明了,便于团队协作和后期维护。
- 支持并行构建:在支持的环境下,Makefile 可以并行执行多个任务,加速大规模项目的构建过程。
- 简化团队协作:通过共享 Makefile,团队成员可以确保一致的构建流程和规范,减少人为差错。
总的来说,Makefile 提供了一种高效、灵活、易于维护的自动化构建方式,尤其适用于大型和复杂的项目。
那么到这里,make与Make的内容就结束啦,欢迎大家交流讨论~~~😘😘😘
创作不易,J桑求各位大大一个一键三连,谢谢各位大佬 ! ! !
版权归原作者 小柯J桑_ 所有, 如有侵权,请联系我们删除。