make和makefile自动化构建
- make和makefile自动化构建
- github地址
- 前言
- 背景介绍
- 为什么需要make和makefile?
- make和makefile解析
- 什么是make和makefile
- 依赖关系和依赖方法
- 核心语法结构
- 简单演示
- 编译
- 清理
- 多阶段编译示例
- make时执行的顺序
- 场景1:clean目标在前(特殊情况)
- 场景2:总构建目标在前(一般情况)
- make如何实现不编译未被修改的文件
- 场景引入
- make实现原理
- stat命令查看文件的属性
- 三种时间:
- 1. Access Time (atime)
- 2. Modify Time (mtime)
- 3. Change Time (ctime)
- **对比总结**
- **实际应用场景**
- 文件 = 文件属性 + 文件内容
- touch命令会修改相关的时间
- **操作时间戳的命令**
- 项目清理
- 编写清理规则
- makefile中的特殊符号
- 总结make的构建原理
- 结语
make和makefile自动化构建
github地址
有梦想的电信狗
前言
在Linux环境下开发中,make和makefile是构建复杂项目的核心工具。本文将深入解析其工作原理,并通过实例演示如何编写高效的自动化构建规则。
背景介绍
为什么需要make和makefile?
回顾我们编译单个文件时,例如main.c,使用如下命令:
gcc main.c -o main
单个文件,我们如此编译尚且可以。
但是,当一个项目中有成百上千个源文件时,这样子手动编译难免有些不够优雅。
手动输入命令容易显得有些繁琐,且容易出错
make和makefile就是为了解决这样的问题的。
make和makefile的功能和优势:
- 大型项目管理:当工程包含数百个源文件时,手动编译和链接效率极低。
- 自动化编译:通过定义规则明确编译顺序和依赖关系,
make命令可一键完成全量构建。 - 跨平台协作:
makefile是Unix/Linux世界的通用构建语言,被所有主流IDE支持。
make和makefile解析
什么是make和makefile
-
make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的自动化构建方法。 -
makefile是一个文件,是一个按照特定规则编写的文件,当makefile编写完成后,make可以识别文件中的内容进行相应的操作。 -
make是一条命令,是一个可执行程序。 -
makefile是一个当前目录下的文件,和make搭配使用,完成项目自动化构建。其中makefile命名为makefile和Makefile均可,首字母M/m不区分大小写。
依赖关系和依赖方法
核心语法结构
target1: prerequisitesrecipe
target1: prerequisitesrecipe
target1: prerequisitesrecipe
# ......
targetn: prerequisitesrecipe
-
目标(Target):要构建的目标
- 最终产物(可执行文件)或中间产物(
.o文件) - 支持伪目标(
.PHONY target)用于执行操作 - 示例:
.PHONY修饰clean,install等非文件目标
- 最终产物(可执行文件)或中间产物(
-
依赖项(Prerequisites):构建目标所需的依赖文件项
- 构建目标所需的文件(如
.c、.o文件)
- 显式声明:直接列出的依赖文件,支持使用通配符
* - 隐式依赖:通过变量或通配符自动推导
- 顺序敏感:依赖列表的顺序影响构建优先级
- 构建目标所需的文件(如
-
配方(Recipe):生成目标所用到的
shell命令。- 必须以
Tab开头(4空格会导致语法错误) - 每条命令在独立
shell中执行,每条当前形成当前target所需要的方法,称为依赖方法。该方法一般是Linux下的命令。 - 使用
@前缀抑制命令回显
- 必须以
简单演示
编译
有以下源程序mycode.c


输入以下指令:我们的单个源程序就跑起来了。
make
./mycode

可以看到:
- 我们不需要输入繁琐的
gcc编译指令,只需要输入make就自动可以帮助我们编译完成。 - 输入
make的本质还是执行了gcc编译指令。
清理
- 当
makefile文件中定义了clean目标的依赖方法时,输入make clean即可完成程序文件的清理。

多阶段编译示例
有如下makefile

- 依赖链:
mycode.c → mycode.i → mycode.s → mycode.o → mycode - 编译流程:预处理→编译→汇编→链接

make时执行的顺序
$ make
- 从终极目标
mycode开始逆向解析 - 检查
mycode是否存在或需要更新 - 递归检查
mycode.s→mycode.i→mycode.c的依赖关系 - 按依赖链顺序执行编译命令
场景1:clean目标在前(特殊情况)

场景2:总构建目标在前(一般情况)

观察以上结果,可以看到,只执行make时,却执行了clean。
执行make mycode,又得到了正确的编译结果。这是为什么呢?
由上我们可以总结归纳出:
make后面要跟上构建的target名。make后面什么都不跟时,默认寻找Makefile中第一个target的依赖关系,执行其依赖方法。make targetName:make后面跟上target的名字时,该命令可以无视makefile中的出现次序,构建指定为targeName的目标文件- 建议:
- 建议将要生成的可执行程序作为第一个依赖关系,这样只输入
make即可完成构建 - 建议将
clean放在最后,防止他人的误操作。
- 建议将要生成的可执行程序作为第一个依赖关系,这样只输入
make如何实现不编译未被修改的文件
场景引入
观察一下场景:
- 我们在
make一次之后,如果没有对源文件进行修改,就无法再次make了。


这是因为没有必要 - 如果源文件没有被修改,再次编译得到的结果还是一样的。
make设计出这样的功能,是为了提高编译效率,节省编译的时间开销。
那么,以上功能是如何实现的呢?
make实现原理
可以思考下,如果让我们自己实现这样的功能,我们的思路会是怎么样的?
思路大致如下:
-
通常情况下,我们一定是由源文件编译形成可执行文件。先有源文件,才会有可执行文件。
-
因此,一般而言(
不通过命令改变文件的修改时间),源文件的最近修改时间,一定比可执行文件的最近修改时间要更老!!! -
因此,只需要比较可执行程序的最近修改时间和源文件的最近修改时间即可。
-
(此处用.exe代替可执行程序,Linux下是编译形成的有可执行权限的文件)
-
最近修改时间,
.exe新于.c,说明源文件时老的,不需要重新编译 -
最近修改时间,
.exe老于.c,说明源文件时新的,需要重新编译 -
而一般而言,
.exe和.c的最近修改时间是不会一样的,除非通过命令来统一修改。
-
-
因此,如果我们更改了源文件,历史上曾经还有可执行文件,那么源文件的最近修改时间,一定比可执行程序要新!
而文件的最近修改时间属于文件属性的一部分。
stat命令查看文件的属性

**我们今天先只关注文件属性中的时间,其他的属性暂不展开叙述。**以下是简介:
在 Linux/Unix 文件系统中,每个文件都有三个与时间相关的重要属性,可以通过 stat 命令查看。以下是它们的详细解释:
三种时间:
1. Access Time (atime)
• 定义:文件最后一次被 访问(读取)的时间
• 触发场景:
• 用 cat、less 查看文件内容
• 用 grep 搜索文件内容
• 程序读取文件(如脚本执行、软件加载配置文件)
• 例外:如果使用 mount -o noatime 挂载文件系统,访问时间不会更新(提高性能)
2. Modify Time (mtime)
• 定义:文件内容最后一次被 修改 的时间
• 触发场景:
• 用编辑器修改文件内容
• 重定向输出到文件(如 echo "text" > file)
• 文件被程序写入新数据
• 注意:修改文件内容会同时更新 change time(见下文)
示例:
echo "new content" > myfile.txt # 修改时间和change time均更新
3. Change Time (ctime)
• 定义:文件 元数据(metadata)(属性)最后一次被修改的时间
• 触发场景:
• 修改文件权限(chmod)
• 修改文件所有者(chown)
• 创建硬链接
• 重命名文件
• 修改文件内容(因为文件大小等元数据变化)
• 注意:ctime 不可通过 touch 直接修改,只能通过元数据操作间接更新
示例:
chmod 644 myfile.txt # change time更新
对比总结
| 时间类型 | 简称 | 触发操作 | 查看命令 |
|---|---|---|---|
Access | atime | 读取文件内容 | stat -c %x filename |
Modify | mtime | 修改文件内容 | stat -c %y filename |
Change | ctime | 修改文件属性 | stat -c %z filename |
实际应用场景
- 备份系统:通过
mtime判断文件内容是否变化,决定是否需要备份。 - 日志分析:通过
atime追踪可疑的文件访问记录。 - 调试问题:用
ctime确认文件权限或所有权是否被意外修改。 - 恢复文件:结合三个时间戳分析文件历史操作。
文件 = 文件属性 + 文件内容
我们之前提到过,文件 = 文件属性 + 文件内容,由上可以总结为:
- 修改文件内容后,
Modify时间会改变 - 修改文件属性后,
Change时间会改变 - 一项操作可能同时改变三种时间中的多个
三种时间,可以用acm来简化记忆。
touch命令会修改相关的时间

touch会修改文件属性,修改后make便可再次编译make通过比较源文件和可执行文件最近修改时间的新旧来判定要不要进行编译- 比较时将属性中的时间转化为
Linux时间戳进行比较

操作时间戳的命令
• 查看时间戳:
stat filename # 查看文件的属性,属性中包含有时间,filename代表系统中文件的名字
• 修改时间戳:
touch -a filename # 仅修改atime 即为Access时间
touch -m filename # 仅修改mtime 即为Modify时间
touch -t 202301010000 filename # 指定时间(格式:[[CC]YY]MMDDhhmm[.ss])
- 高级知识
• relatime:现代 Linux 默认使用 relatime(仅当 atime 比 mtime 或 ctime 旧时才更新,平衡性能与准确性)
• ext4 的纳秒精度:ext4 文件系统支持时间戳的纳秒级记录
- 注意:
touch命令可通过-a ``-m选项单独修改一个文件的Access时间和Modify,但无法单独修改文件的change时间 touch filename指令:无参数的touch命令会修改文件的三个时间。
项目清理
编写清理规则
- 实际上
makefile的清理命令make clean我们已经一直在使用了。

.PHONY: clean
clean:rm -f mycode
clean是我们的target,rm -f mycode是目标的依赖方法,clean的依赖方法也可以是其他的命令。
前文我们提到,make通过比较源文件和可执行文件最近修改时间的新旧来判定要不要进行编译,但如果我就是想让一个target在每次make时都重新进行编译,该怎么办呢?这便是.PHONY的作用。
- .PHONY:声明伪目标,避免与同名文件冲突
- 强制执行:无论是否存在
clean文件,make clean都会执行 .PHONY修饰的目标文件,只要输入make,总是会被执行

- 实际上不建议用
.PHONY修饰总的目标文件,简直用.PHONY修饰clean
makefile中的特殊符号
makefile中有很多特殊符号,本文只介绍三种特殊符号:@ ,$^ ,$@
@:@用于修饰依赖方法,使make之后,执行的命令不在终端中回显。$^和$@常用在依赖方法中:$@用来表示依赖关系冒号左边的内容。$^用来表示依赖关系冒号右边边的内容。

总结make的构建原理
- 查找Makefile:优先读取
Makefile或makefile - 确定终极目标:解析第一个目标(示例中的
hello) - 依赖树分析:递归检查所有依赖项是否存在
- 时间戳比对:若依赖文件比目标文件新,则触发重新编译
- 增量构建:仅重新编译修改过的文件及其依赖链
结语
通过合理设计makefile,开发者可以:
- 实现一键式编译,提升开发效率
- 利用增量编译节省构建时间
- 规范项目的构建和清理流程
如果本文对你有帮助,欢迎点赞收藏,技术问题欢迎在评论区交流!
一键三连,好运连连!